diff --git a/.githooks/pre-commit b/.githooks/pre-commit deleted file mode 100755 index eb326ad..0000000 --- a/.githooks/pre-commit +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh - -echo "Running tests before commit..." - -# run tests -make test || { - echo "Tests failed. Commit aborted." - exit 1 -} - -echo "Tests passed. Proceeding with commit." diff --git a/.gitignore b/.gitignore index 17402e5..e43b0f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ .DS_Store -doc/tags diff --git a/LICENSE b/LICENSE index 3fcee62..3cfd7b3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ MIT License -Copyright (c) 2023-2026 squibid -Copyright (c) 2021-2023 chiya.dev +(c) 2021 chiya.dev +(c) 2024 squibid Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile deleted file mode 100644 index be555db..0000000 --- a/Makefile +++ /dev/null @@ -1,4 +0,0 @@ -test: - nvim --headless -c "PlenaryBustedDirectory tests/ {minimal_init = './tests/minit.lua'}" - -.PHONY: test diff --git a/README.md b/README.md index 48e3938..73831e0 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,20 @@ # dep +> This readme is a work in progress. -A versatile, declarative and correct [neovim][2] package manager in [Lua][3]. -Originally written for personal use by [luaneko][4]. Adapted by [squibid][5] for -general use. +A versatile, declarative and correct [neovim][1] package manager in [Lua][2]. +Originally written for personal use by [luaneko][3]. What does that mean? -1. `versatile` - packages can be declared in any Lua file in any order of your -liking. +1. `versatile` - packages can be declared in any Lua file in any order of your liking. 2. `declarative` - packages are declared using simple Lua tables. -3. `correct` - packages are always loaded in a correct and consistent order -(barring any lazy loading). - -In addition to the above dep has been built to be completely in control of you, -the user. With the help of lazy loading you can choose when your plugin loads -down to the finest detail (examples may be found below). - -See also squibid's [neovim-config][10] for an example of how dep can be used in -practice. +3. `correct` - packages are always loaded in a correct and consistent order. +See also squibid's [neovim-configs][5] for an example of how dep can be used in practice. ## Requirements - -- [Neovim][2] 0.8+ -- [Git][6] 2.13+ - +- [Neovim][1] 0.6+ +- [Git][4] ## Setup - 1. Create `lua/bootstrap.lua` in your neovim config directory. ```lua @@ -34,7 +23,7 @@ practice. local path = vim.fn.stdpath("data") .. "/site/pack/deps/opt/dep" if vim.fn.empty(vim.fn.glob(path)) > 0 then - vim.fn.system({ "git", "clone", "--depth=1", "https://git.squi.bid/squibid/dep", path }) + vim.fn.system({ "git", "clone", "--depth=1", "https://github.com/chiyadev/dep", path }) end vim.cmd("packadd dep") @@ -55,8 +44,9 @@ require "dep" { cleans removed packages and reloads packages as necessary. - `:DepClean` - cleans removed packages. - `:DepReload` - reloads all packages. +- `:DepList` - prints the package list, performance metrics and dependency graphs. - `:DepLog` - opens the log file. -- `:DepUi` - opens the ui. +- `:DepConfig` - opens the file that called dep, for convenience. ## Package specification @@ -68,37 +58,28 @@ A package must be declared in the following format. -- This is the only required field; all other fields are optional. "user/package", + -- [function] Code to run after the package is loaded into neovim. + function() + require "package".setup(...) + end, + -- [function] Code to run before the package is loaded into neovim. setup = function() vim.g.package_config = ... end, - -- [function] Code to run after the package is loaded into neovim. - load = function() - require "package".setup(...) - end, - -- [function] Code to run after the package is installed or updated. config = function() os.execute(...) end, - -- [function|true] Code used to determine when the package should be loaded. - lazy = function(load) - load:cmd("LoadPackage") - end, - -- [string] Overrides the short name of the package. -- Defaults to a substring of the full name after '/'. as = "custom_package", -- [string] Overrides the URL of the git repository to clone. -- Defaults to "https://github.com/{full_name}.git". - url = "https://git.squi.bid/user/package.git", - - -- [string] Overrides the source in which the package is gotten - -- from. This is not set by default. - path = "~/my-local-package/", + url = "https://git.chiya.dev/user/package.git", -- [string] Overrides the name of the branch to clone. -- Defaults to whatever the remote configured as their HEAD, which is usually "master". @@ -114,9 +95,9 @@ A package must be declared in the following format. -- [boolean] Prevents the package from being updated. pin = true, - -- [string|array] Specifies requirements that must be loaded before the package. + -- [string|array] Specifies dependencies that must be loaded before the package. -- If given a string, it is wrapped into an array. - reqs = {...}, + requires = {...}, -- [string|array] Specifies dependents that must be loaded after the package. -- If given a string, it is wrapped into an array. @@ -142,7 +123,7 @@ combined into one. This is useful when declaring dependencies, which is explored require "dep" { { "user/package", - reqs = "user/dependency", + requires = "user/dependency", disabled = true, config = function() print "my config hook" @@ -163,7 +144,7 @@ require "dep" { require "dep" { { "user/package", - reqs = { "user/dependency", "user/another_dependency" }, + requires = { "user/dependency", "user/another_dependency" }, deps = "user/dependent", disabled = true, config = function() @@ -184,10 +165,10 @@ they are combined into one just like normal package specifications. require "dep" { { "user/package", - reqs = { + requires = { { "user/dependency1", - reqs = "user/dependency2" + requires = "user/dependency2" } } } @@ -210,7 +191,7 @@ require "dep" { require "dep" { { "user/dependency1", - reqs = "user/dependency2", + requires = "user/dependency2", deps = "user/package" } } @@ -219,11 +200,11 @@ require "dep" { require "dep" { { "user/dependency1", - reqs = "user/dependency2" + requires = "user/dependency2" }, { "user/package", - reqs = "user/dependency1" + requires = "user/dependency1" } } @@ -250,11 +231,11 @@ instead of hanging or crashing. require "dep" { { "user/package1", - reqs = "user/package2" + requires = "user/package2" }, { "user/package2", - reqs = "user/package1" + requires = "user/package1" } } ``` @@ -270,12 +251,12 @@ require "dep" { { "user/package1", disabled = true, -- implied - reqs = "user/dependency" + requires = "user/dependency" }, { "user/package2", disabled = true, -- implied - reqs = "user/dependency" + requires = "user/dependency" } } ``` @@ -286,172 +267,20 @@ If a dependency fails to load for some reason, all of its dependents are guarant require "dep" { { "user/problematic", - load = function() + function() error("bad hook") end }, { "user/dependent", requires = "user/problematic", - load = function() + function() print "unreachable" end } } ``` -## Lazy loading - -Imagine you're using [telescope.nvim][7] and you need to pull it up with a keybind, -but you don't want to have it load before that moment. With lazy loading you may -choose to only load it when needed using the built in lazy utils which are made -available to you as soon as you start using the lazy option. - -```lua -require "dep" { - { "nvim-telescope/telescope.nvim", - lazy = function(load) - load:keymap("n", "f") - end, - load = function() - require("telescope").setup {} - vim.keymap.set("n", "f", require("telescope.builtin").find_files, {}) - end - } -} -``` - -Say you wanted to use [gitsigns.nvim][9], but only wanted to load it when -in a git directory OR when you call the Gitsigns command. With the power of lazy -loading this can be accomplished by simply defining an auto command like so: - -```lua -require "dep" { - { - "lewis6991/gitsigns.nvim", - lazy = function(load) - -- load gitsigns if we're in a git repository - load:auto({ "BufEnter", "BufNew" }, { - callback = function() - local paths = vim.fs.find({ ".git", }, { upward = true }) - if #paths > 0 then - load:cleanup() - end - end - }) - - -- load gitsigns if the user trys to run the command - load:cmd("Gitsigns") - end, - load = function() - require("gitsigns").setup {} - end - } -} - -``` - -If you're in the need of a deeper understanding of how the utils work go check -out `lua/lazy/loader/init.lua` for the source code. - -## Separating code into modules - -Suppose you split your `init.lua` into two files `packages/search.lua` and -`packages/vcs.lua`, which declare the packages [telescope.nvim][7] and [vim-fugitive][8] respectively. - -```lua --- ~/.config/nvim/lua/packages/search.lua: -return { - { - "nvim-telescope/telescope.nvim", - reqs = "nvim-lua/plenary.nvim" - } -} -``` - -```lua --- ~/.config/nvim/lua/packages/vcs.lua: -return { - "tpope/vim-fugitive" -} -``` - -Package specifications from other modules can be loaded using the `modules` option. - -```lua -require "dep" { - modules = { - prefix = "packages" - } -} - --- the above is equivalent to -require "dep" { - modules = { - prefix = "packages.", - "search", - "vcs" - } -} - --- or -require "dep" { - modules = { - "packages.search", - "packages.vcs" - } -} - --- which is equivalent to -local packages = {} - -for _, package in ipairs(require "packages.search") do - table.insert(packages, package) -end - -for _, package in ipairs(require "packages.vcs") do - table.insert(packages, package) -end - -require("dep")(packages) - --- which is ultimately equivalent to -require "dep" { - { - "nvim-telescope/telescope.nvim", - reqs = "nvim-lua/plenary.nvim" - }, - "tpope/vim-fugitive" -} - --- all of the above are guaranteed to load plenary.nvim before telescope.nvim. --- order of telescope.nvim and vim-fugitive is consistent but unspecified. -``` - -Entire modules can be marked as disabled, which disables all top-level packages declared in that module. - -```lua -return { - disable = true, - { - "user/package", - disabled = true, -- implied by module - reqs = { - { - "user/dependency", - -- disabled = true -- not implied - } - }, - deps = { - { - "user/dependent", - disabled = true -- implied by dependency - } - } - } -} -``` - ## Miscellaneous configuration dep accepts configuration parameters as named fields in the package list. @@ -464,43 +293,21 @@ require "dep" { -- "always": synchronize all packages on startup sync = "new", - -- [array] Specifies the modules to load package specifications from. - -- Defaults to an empty table. - -- Items can be either an array of package specifications, - -- or a string that indicates the name of the module from which the array of package specifications is loaded. - modules = { - -- [string] Prefix string to prepend to all module names. - prefix = "", - }, + -- [function] Callback when dep is (re)loaded + -- if a table is returned it will be read as a table of config specs + load = function() + end -- list of package specs... } ``` -## Known Issues - -- Lazy loading nvim-cmp doesn't work as the external sources don't get reconized -by nvim-cmp when it's loaded. - -## Contributing - -When contributing you may choose to run tests before commiting changes, if that -is so you may choose to run the following: -```sh -git config core.hooksPath .githooks -``` - ## License dep is licensed under the [MIT License](LICENSE). -[1]: https://chiya.dev/posts/2021-11-27-why-package-manager -[2]: https://neovim.io/ -[3]: https://www.lua.org/ -[4]: https://github.com/luaneko -[5]: https://squi.bid -[6]: https://git-scm.com/ -[7]: https://github.com/nvim-telescope/telescope.nvim -[8]: https://github.com/tpope/vim-fugitive -[9]: https://github.com/lewis6991/gitsigns.nvim -[10]: https://git.squi.bid/nvim +[1]: https://neovim.io/ +[2]: https://www.lua.org/ +[3]: https://github.com/luaneko +[4]: https://git-scm.com/ +[5]: https://git.squi.bid/nvim diff --git a/doc/dep.txt b/doc/dep.txt deleted file mode 100644 index be5151d..0000000 --- a/doc/dep.txt +++ /dev/null @@ -1,670 +0,0 @@ -*dep.txt* Declarative Package Manager 02-Jul-2025 - -============================================================================== -Table of Contents *dep-table-of-contents* - -1. Introduction |dep| -2. Setup |dep-setup| -3. Specs |dep-spec| - - Package Spec |dep-package-spec| - - Module Spec |dep-module-spec| -4. Lazy Loading |dep-lazy-loading| - - Lazy Loading API |dep-lazy-loading-api| - - Lazy Loading API Shorthands |dep-lazy-loading-api-shorthands| -5. Commands |dep-commands| -6. Examples |dep-examples| - - Declaring Dependencies |dep-examples-declaring-dependencies| - - Modules |dep-examples-modules| - - Lazy Loading |dep-examples-lazy-loading| -7. Credits & License |dep-credits| - -============================================================================== -1. Introduction *dep* - -A versatile, declarative and correct neovim package manager in Lua. Originally -written for personal use by luaneko. Adapted by squibid for general use. - -What does that mean? - -1. `versatile` - packages can be declared in any Lua file in any order of your -liking. -2. `declarative` - packages are declared using simple Lua tables. -3. `correct` - packages are always loaded in a correct and consistent order -(barring any lazy loading). - -In addition to the above dep has been built to be completely in control of you, -the user. With the help of lazy loading you can choose when your plugin loads -down to the finest detail (examples may be found below). - -============================================================================== -2. Setup *dep-setup* - -Put the following anywhere before any actual use of dep. - ->lua - local path = vim.fn.stdpath("data") .. "/site/pack/deps/opt/dep" - - if vim.fn.empty(vim.fn.glob(path)) > 0 then - vim.fn.system({ - "git", - "clone", - "--depth=1", - "https://git.squi.bid/squibid/dep", - path, - }) - end - - vim.cmd("packadd dep") -< - -============================================================================== -3. Specs *dep-spec* - -dep uses a variation of specifications to ensure everything works smoothly. -This includes a basic spec used when setting up dep: ->lua - require("dep") { - -- [string] Specifies when dep should automatically synchronize. - -- "never": disable this behavior - -- "new": only install newly declared packages (default) - -- "always": synchronize all packages on startup - sync = "new", - - -- [array] Specifies the modules to load package specifications from. - -- Defaults to an empty table. - -- Items can be either an array of package specifications, - -- or a string that indicates the name of the module from which the array - -- of package specifications is loaded. - -- - -- "."'s are added between the prefix and module names as required. In - -- addition if there is only a prefix and no modules supplied then dep - -- automatically loads all lua files in the directory. - modules = { - -- [string] Prefix string to prepend to all module names. This is - -- optional. - prefix = "", - - -- [string] module names - ... - }, - - -- [table|string] package specification(s) - ... - } -< -PACKAGE SPEC *dep-package-spec* ->lua - { - -- [string] Specifies the full name of the package. - -- This is the only required field; all other fields are optional. - "user/package", - - -- [function] Code to run before the package is loaded into neovim. - setup = function() - vim.g.package_config = ... - end, - - -- [function] Code to run after the package is loaded into neovim. - load = function() - require "package".setup(...) - end, - - -- [function] Code to run after the package is installed or updated. - config = function() - os.execute(...) - end, - - -- [function|true] Code used to determine when the package should be - -- loaded. - lazy = function(load) - load:cmd("LoadPackage") - end, - - -- [string] Overrides the short name of the package. - -- Defaults to a substring of the full name after '/'. - as = "custom_package", - - -- [string] Overrides the URL of the git repository to clone. - -- Defaults to "https://github.com/{full_name}.git". - url = "https://git.squi.bid/user/package.git", - - -- [string] Overrides the source in which the package is gotten - -- from. This is not set by default. - path = "~/my-local-package/", - - -- [string] Overrides the name of the branch to clone. - -- Defaults to whatever the remote configured as their HEAD, which is - -- usually "master". - branch = "develop", - - -- [string] Overrides the commit ref to target - -- Defaults to the latest commit on the current branch - commit = "e76cb03", - - -- [boolean] Prevents the package from being loaded. - disable = true, - - -- [boolean] Prevents the package from being updated. - pin = true, - - -- [string|array] Specifies requirements that must be loaded before the - -- package. If given a string, it is wrapped into an array. - reqs = {...}, - - -- [string|array] Specifies dependents that must be loaded after the - -- package. If given a string, it is wrapped into an array. - deps = {...} - } -< -MODULE SPEC *dep-module-spec* ->lua - { - -- [string] Specifies a name for the module - name = "a name", - - -- [string] Specifies a description of the module - desc = "a description of the module", - - -- [boolean] Prevents all packages in the module from being loaded. - disable = false, - - -- [table|string] package specification(s) - ... - } -< -More information on the package specifications may be found in -|dep-package-spec|. - -============================================================================== -4. Lazy Loading *dep-lazy-loading* - -Lazy loading is important for making sure neovim can load nice and fast unlike -a certain bloated IDE. It has a seperate section in this documentation to -ensure that you can use it to it's full extent. - -If you refer to the |dep-package-spec| you'll notice the `lazy` flag which may -be used to conditionally load a package. When it is set to a function you -choose when it runs and more information on that may be found in -|dep-lazy-loading-api|. In addition to setting it to a function you may set it -to `true` in which case dep takes care of loading it for you. - -When setting a colorscheme dep checks to make sure that the plugin is loaded, -therefore it's recommended that you make use of the `lazy` flags ability to be -set to `true` by setting any colorscheme that you have installed, but do not -use as your main one to lazy. - -LAZY LOADING API *dep-lazy-loading-api* - -Within the |dep-package-spec| the lazy flag when set to a function takes one -argument `load` which is a class containing loading functions. For the -following examples assume that `load` is set to the class which may be found -within `lua/dep/lazy/loader/init.lua`. - ------------------------------------------------------------------------------- -LOAD:CMD *dep-lazy-loading-api-cmd* - -`load:cmd` is a function which allows you to specify a command for the package -to load on. It takes the similar arguments to |nvim_create_user_command()| -with a key difference in what the command runs. The following is an example of -what arguments the function takes: ->lua - load:cmd("Command", {}) -< -Notice the missing 'command' argument which is found in -|nvim_create_user_command|, this is replaced by a callback function. The above -is equivalent to the following: ->lua - load:cmd("Command", { - callback = function() - load:cleanup() - - if (rerun) then - vim.cmd("Command") - end - end - }) -< -If you wish the second argument may be completely ommitted. Note the inclusion -of a `rerun` field, this is a parameter which may be passed into the options table -to re-run the binding after loading the package. You may choose to disable the -built-in logic by passing false. - ------------------------------------------------------------------------------- -LOAD:AUTO *dep-lazy-loading-api-auto* - -`load:auto` is a function which allows you to specify an auto command for the -package to load on. It takes the same arguments as |nvim_create_autocmd()|. -The following is an example of using it: ->lua - load:auto("InsertEnter", {}) -< -Just like with |nvim_create_autocmd()| you may choose to pass in a 'callback' -by default the above is equivalent to the following: ->lua - load:auto("InsertEnter", { - callback = function() - load:cleanup() - end - }) -< -As with `load:cmd` the second argument may be ommitted. - ------------------------------------------------------------------------------- -LOAD:FT *dep-lazy-loading-api-ft* - -`load:ft` is a function which allows you to specify a filetype for the package -to load on. It takes one argument: 'filetype' like so: ->lua - load:ft("lua") -< -Which is equivalent to the following: ->lua - load:auto("FileType", { - pattern = "lua", - callback = function() - load:cleanup() - end - }) -< -Note that this is just an expansion of `load:auto` for your convenience. - ------------------------------------------------------------------------------- -LOAD:KEYMAP *dep-lazy-loading-api-keymap* - -`load:keymap` is a function which allows you to specify a keymap for the -package to load on. It takes the similar arguments to |vim.keymap.set()| with a -key difference in what the command runs. The following is an example of what -arguments the function takes: ->lua - load:keymap("n", "p", {}) -< -Notice the missing 'rhs' argument which is found in |vim.keymap.set|, this is -replaced by a callback function. The above is equivalent to the following: ->lua - load:keymap("n", "p", { - callback = function() - -- register keymap unload - load:cleanup() - - -- call the keymap after the user has mapped it - if type(rerun) == "function" then - rerun() - elseif rerun then - local keys = vim.api.nvim_replace_termcodes(bind, true, false, true) - vim.api.nvim_input(keys) - end - end - }) -< -Note the inclusion of a `rerun` field, this is a parameter which may be passed -into the options table to re-run the binding after loading the package. You -may choose to include your own logic by passing a function to the `rerun` -field or disable the built-in logic by passing false. - ------------------------------------------------------------------------------- -LOAD:PLUGIN *dep-lazy-loading-api-plugin* - -`load:plugin` is a function which allows you to specify another plugin for the -package to load after. It takes two arguments: `plugin` which is the name of -the plugin you want to follow like: 'user/package'. The second argument is -`opts` which is a table with one option: `callback` which is a function. The -following is an example: ->lua - load:plugin("user/package", {}) -< -Which is the same as: ->lua - load:plugin("user/package", { - callback = function() - self:cleanup() - end - }) -< -When 'user/package' is already loaded the `callback` is called immediately. - -LAZY LOADING API SHORTHANDS *dep-lazy-loading-api-shorthands* - -On occasion you may wish to only define one condition for the package to load. -When that is the case you may choose to use the built-in shorthands. By -loading them: ->lua - require("dep.lazy.loader.short") -< -The shorthands are very similar to those found in |dep-lazy-loading-api| with -a key exception: instead of running the functions within the body of a -function set as the lazy field to a package specification this is the lazy -field and may be use like so: ->lua - { "user/package", - lazy = require("dep.lazy.loader.short").cmd("Command") - } -< -And you may of course put the shorthands in a variable to make this actually -shorter: ->lua - local short = require("dep.lazy.loader.short") - { "user/package", - lazy = short.cmd("Command") - } -< -============================================================================== -5. Commands *dep-commands* - ------------------------------------------------------------------------------- -SYNC ALL PLUGINS *:DepSync* -- installs new packages, updates packages to the latest versions, - cleans removed packages and reloads packages as necessary. - ------------------------------------------------------------------------------- -CLEAN REMOVED PLUGINS *:DepClean* -- cleans removed packages. - ------------------------------------------------------------------------------- -RELOAD ALL PLUGINS *:DepReload* -- reloads all packages. - ------------------------------------------------------------------------------- -OPEN THE UI *:DepUi* -- opens the ui. - ------------------------------------------------------------------------------- -OPEN THE LOG *:DepLog* -- opens the log file. - -============================================================================== -6. Examples *dep-examples* - -When a string is given where a package specification table is expected, it is -assumed to be the package's full name. - ->lua - require("dep") { - -- these two are equivalent - "user/package", - { "user/package" }, - } -< - -A package can be declared multiple times. Multiple declarations of the same -package are combined into one. This is useful when declaring dependencies, -which is explored later. - ->lua - require("dep") { - { "user/package", - reqs = "user/dependency", - disabled = true, - config = function() - print("my config hook") - end - }, - { "user/package", - requires = "user/another_dependency", - deps = "user/dependent", - disabled = false, - config = function() - os.execute("make") - end - } - } - - -- the above is equivalent to - require("dep") { - { "user/package", - reqs = { "user/dependency", "user/another_dependency" }, - deps = "user/dependent", - disabled = true, - config = function() - print("my config hook") - os.execute("make") - end - } - } -< -DECLARING DEPENDENCIES *dep-examples-declaring-dependencies* - -The dependencies and dependents declared in a package specification are -themselves package specifications. If a dependency or dependent is declared -multiple times, they are combined into one just like normal package -specifications. ->lua - require("dep") { - { "user/package", - reqs = { - { "user/dependency1", - reqs = "user/dependency2" - } - } - } - } - - -- the above is equivalent to - require("dep") { - { "user/dependency2", - deps = { - { "user/dependency1", - deps = "user/package" - } - } - } - } - - -- which is equivalent to - require("dep") { - { "user/dependency1", - reqs = "user/dependency2", - deps = "user/package" - } - } - - -- which is equivalent to - require("dep") { - { "user/dependency1", - reqs = "user/dependency2" - }, - { "user/package", - reqs = "user/dependency1" - } - } - - -- which is equivalent to - require("dep") { - { "user/dependency2", - deps = "user/dependency1" - }, - { "user/dependency1", - deps = "user/package" - } - } - - -- all of the above are guaranteed to load in the following order: - -- dependency2, dependency1, package -< -If dep detects a circular dependency cycle, it reports the problematic packages -instead of hanging or crashing. ->lua - -- this throws an error saying package1 depends on package2 which depends on - -- package1 - require("dep") { - { "user/package1", - reqs = "user/package2" - }, - { "user/package2", - reqs = "user/package1" - } - } -< -A dependency can be marked as disabled, which disables all dependents -automatically. ->lua - require("dep") { - { "user/dependency", - disabled = true - }, - { "user/package1", - disabled = true, -- implied - reqs = "user/dependency" - }, - { "user/package2", - disabled = true, -- implied - reqs = "user/dependency" - } - } -< -If a dependency fails to load for some reason, all of its dependents are -guaranteed to not load. ->lua - require("dep") { - { "user/problematic", - load = function() - error("bad hook") - end - }, - { "user/dependent", - requires = "user/problematic", - load = function() - print "unreachable" - end - } - } -< - -MODULES *dep-examples-modules* - -Suppose you split your `init.lua` into two files `packages/search.lua` and -`packages/vcs.lua`, which declare the packages telescope.nvim and vim-fugitive -respectively. - ->lua - -- ~/.config/nvim/lua/packages/search.lua: - return { - { "nvim-telescope/telescope.nvim", - reqs = "nvim-lua/plenary.nvim" - } - } -< ->lua - -- ~/.config/nvim/lua/packages/vcs.lua: - return { "tpope/vim-fugitive" } -< -Package specifications from other modules can be loaded using the `modules` -option. ->lua - require("dep") { - modules = { - prefix = "packages" - } - } - - -- the above is equivalent to - require("dep") { - modules = { - prefix = "packages.", - "search", - "vcs" - } - } - - -- or - require("dep") { - modules = { - "packages.search", - "packages.vcs" - } - } - - -- which is equivalent to - local packages = {} - - for _, package in ipairs(require "packages.search") do - table.insert(packages, package) - end - - for _, package in ipairs(require "packages.vcs") do - table.insert(packages, package) - end - - require("dep")(packages) - - -- which is ultimately equivalent to - require("dep") { - { "nvim-telescope/telescope.nvim", - reqs = "nvim-lua/plenary.nvim" - }, - "tpope/vim-fugitive" - } - - -- all of the above are guaranteed to load plenary.nvim before - -- telescope.nvim. order of telescope.nvim and vim-fugitive is consistent but - -- unspecified. -< -Entire modules can be marked as disabled, which disables all top-level packages -declared in that module. ->lua - return { - disable = true, - { "user/package", - disabled = true, -- implied by module - reqs = { - { "user/dependency", - -- disabled = true -- not implied - } - }, - deps = { - { "user/dependent", - disabled = true -- implied by dependency - } - } - } - } -< -LAZY LOADING *dep-examples-lazy-loading* - -Lazy loading is a very complicated topic, and therefore this part of the -documentation assumes you have experience with regular package managment. -Let's go over loading order, and how the lazy loader determines what needs to -be loaded. - -Let's say we have the following spec: ->lua - { "user/package", - lazy = true, - deps = "user/dependent" - } -< -This is the same as the following: ->lua - { "user/package", - lazy = true - }, - - { "user/dependent", - reqs = "user/package", - lazy = require("dep.lazy.loader.short").plugin("user/package") - } -< -What you're seeing is implicit lazy loading. By default dep will lazy load -dependents who are explicitly defined in the spec. Now if we we're to modify -'user/dependent' like so: ->lua - { "user/package", - lazy = true - }, - - { "user/dependent", - reqs = "user/package", - lazy = function(load) - load:plugin("user/package") - load:cmd("LoadDependent") - end - } -< -If we were to call the command `:LoadDependent` it would first load -'user/package', and then load 'user/dependent'. - -============================================================================== -7. Credits & License *dep-credits* - -dep is licensed under the MIT License. Check the LICENSE file for more info. - -vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/lua/dep.lua b/lua/dep.lua index 9d43fe4..dbf6516 100644 --- a/lua/dep.lua +++ b/lua/dep.lua @@ -1,157 +1,917 @@ -local logger = require("dep.log") -local packager = require("dep.package") -local modules = require("dep.modules") -local bench = require("dep.bench") -local lazy = require("dep.lazy") +-- +-- Copyright (c) 2022 chiya.dev +-- +-- Use of this source code is governed by the MIT License +-- which can be found in the LICENSE file and at: +-- +-- https://chiya.dev/licenses/mit.txt +-- --- all functions for convenience -local M = {} +local logger = require("dep.log").global +local proc = require("dep.proc") ---- sync a tree of plugins ----@param tree package[] tree of plugins ----@param cb function? callback -local function synctree(tree, cb) - local progress = 0 +local initialized, perf, config_path, base_dir +local packages, root, load - ---@param spec vim.pack.Spec - ---@param path string - local function done(spec, path) - ---@type package - local p = spec.data - _ = path - progress = progress + 1 +local function bench(name, code, ...) + local start = os.clock() + code(...) + perf[name] = os.clock() - start +end - local info = vim.pack.get({ spec.name }, { info = false })[1] - if info.active then - p.exists = true - p:unconfiguretree() - p:runhooks("on_config") - logger:log("config", "package: %s configured", p.id) - p.configured = true +local function get_name(id) + local name = id:match("^[%w-_.]+/([%w-_.]+)$") + if name then + return name + else + error(string.format('invalid name "%s"; must be in the format "user/package"', id)) + end +end + +local function link_dependency(parent, child) + if not parent.dependents[child.id] then + parent.dependents[child.id] = child + parent.dependents[#parent.dependents + 1] = child + end + + if not child.dependencies[parent.id] then + child.dependencies[parent.id] = parent + child.dependencies[#child.dependencies + 1] = parent + end +end + +local function register(spec, overrides) + overrides = overrides or {} + + if type(spec) ~= "table" then + spec = { spec } + end + + local id = spec[1] + local package = packages[id] + + if not package then + package = { + id = id, + enabled = true, + exists = false, + added = false, + configured = false, + loaded = false, + subtree_configured = false, + subtree_loaded = false, + on_setup = {}, + on_config = {}, + on_load = {}, + dependencies = {}, -- inward edges + dependents = {}, -- outward edges + perf = {}, + } + + packages[id] = package + packages[#packages + 1] = package + end + + local prev_dir = package.dir -- optimization + + package.name = spec.as or package.name or get_name(id) + package.url = spec.url or package.url or ("https://github.com/" .. id .. ".git") + package.branch = spec.branch or package.branch + package.dir = base_dir .. package.name + package.commit = spec.commit + package.pin = overrides.pin or spec.pin or package.pin + package.enabled = not overrides.disable and not spec.disable and package.enabled + + if prev_dir ~= package.dir then + package.exists = vim.fn.isdirectory(package.dir) ~= 0 + package.configured = package.exists + end + + package.on_setup[#package.on_setup + 1] = spec.setup + package.on_config[#package.on_config + 1] = spec.config + package.on_load[#package.on_load + 1] = spec[2] + + -- every package is implicitly dependent on us, the package manager + if root and package ~= root then + link_dependency(root, package) + end + + if type(spec.requires) == "table" then + for i = 1, #spec.requires do + link_dependency(register(spec.requires[i]), package) + end + elseif spec.requires then + link_dependency(register(spec.requires), package) + end + + if type(spec.deps) == "table" then + for i = 1, #spec.deps do + link_dependency(package, register(spec.deps[i])) + end + elseif spec.deps then + link_dependency(package, register(spec.deps)) + end + + return package +end + +local function register_recursive(list, overrides) + overrides = overrides or {} + overrides = { + pin = overrides.pin or list.pin, + disable = overrides.disable or list.disable, + } + + for i = 1, #list do + local ok, err = pcall(register, list[i], overrides) + if not ok then + error(string.format("%s (spec=%s)", err, vim.inspect(list[i]))) + end + end +end + +local function sort_dependencies() + -- we don't do topological sort, packages are loaded by traversing the graph recursively + -- any sorting is fine as long as the order is consistent and predictable + local function compare(a, b) + local a_deps, b_deps = #a.dependencies, #b.dependencies + if a_deps == b_deps then + return a.id < b.id + else + return a_deps < b_deps end end - -- convert our spec to vim.pack.Spec - ---@type vim.pack.Spec[] - local vimspecs = {} - for _, p in ipairs(tree) do - table.insert(vimspecs, { - name = p.name, - src = p.path or p.url, - version = p.commit or p.branch, - data = p, - }) - end + table.sort(packages, compare) - -- install/update all packages - vim.pack.add(vimspecs, { load = done, confirm = false }) - vim.pack.update(vimspecs, { force = true }) - - -- reload all packages - for _, p in pairs(vimspecs) do - p.data:reload() + for i = 1, #packages do + table.sort(packages[i].dependencies, compare) + table.sort(packages[i].dependents, compare) end - if cb then cb() end end ---- check if a package should be synced ----@param opts table options ----@param package package package table spec ----@return boolean sync -local function shouldsync(opts, package) - if opts.sync == "never" then - return false - elseif opts.sync == "new" or opts.sync == nil then - return not package.exists +local function find_cycle() + local index = 0 + local indexes = {} + local lowlink = {} + local stack = {} + + -- use tarjan algorithm to find circular dependencies (strongly connected components) + local function connect(package) + indexes[package.id], lowlink[package.id] = index, index + stack[#stack + 1], stack[package.id] = package, true + index = index + 1 + + for i = 1, #package.dependents do + local dependent = package.dependents[i] + + if not indexes[dependent.id] then + local cycle = connect(dependent) + if cycle then + return cycle + else + lowlink[package.id] = math.min(lowlink[package.id], lowlink[dependent.id]) + end + elseif stack[dependent.id] then + lowlink[package.id] = math.min(lowlink[package.id], indexes[dependent.id]) + end + end + + if lowlink[package.id] == indexes[package.id] then + local cycle = { package } + local node + + repeat + node = stack[#stack] + stack[#stack], stack[node.id] = nil, nil + cycle[#cycle + 1] = node + until node == package + + -- a node is by definition strongly connected to itself + -- ignore single-node components unless it explicitly specified itself as a dependency + if #cycle > 2 or package.dependents[package.id] then + return cycle + end + end + end + + for i = 1, #packages do + local package = packages[i] + + if not indexes[package.id] then + local cycle = connect(package) + if cycle then + return cycle + end + end + end +end + +local function ensure_acyclic() + local cycle = find_cycle() + + if cycle then + local names = {} + for i = 1, #cycle do + names[i] = cycle[i].id + end + error("circular dependency detected in package dependency graph: " .. table.concat(names, " -> ")) + end +end + +local function run_hooks(package, type) + local hooks = package[type] + if #hooks == 0 then + return true + end + + local start = os.clock() + + -- chdir into the package directory to make running external commands + -- from hooks easier. + local last_cwd = vim.fn.getcwd() + vim.fn.chdir(package.dir) + + for i = 1, #hooks do + local ok, err = pcall(hooks[i]) + if not ok then + vim.fn.chdir(last_cwd) + + package.error = true + return false, err + end + end + + vim.fn.chdir(last_cwd) + package.perf[type] = os.clock() - start + + logger:log( + "hook", + string.format("triggered %d %s %s for %s", #hooks, type, #hooks == 1 and "hook" or "hooks", package.id) + ) + + return true +end + +local function ensure_added(package) + if not package.added then + local ok, err = run_hooks(package, "on_setup") + if not ok then + package.error = true + return false, err + end + + local start = os.clock() + + ok, err = pcall(vim.cmd, "packadd " .. package.name) + if not ok then + package.error = true + return false, err + end + + package.added = true + package.perf.pack = os.clock() - start + + logger:log("vim", string.format("packadd completed for %s", package.id)) + end + + return true +end + +local function configure_recursive(package) + if not package.exists or not package.enabled or package.error then + return + end + + if package.subtree_configured then + return true + end + + for i = 1, #package.dependencies do + if not package.dependencies[i].configured then + return + end + end + + if not package.configured then + local ok, err = ensure_added(package) + if not ok then + logger:log("error", string.format("failed to configure %s; reason: %s", package.id, err)) + return + end + + ok, err = run_hooks(package, "on_config") + if not ok then + logger:log("error", string.format("failed to configure %s; reason: %s", package.id, err)) + return + end + + package.configured = true + logger:log("config", string.format("configured %s", package.id)) + end + + package.subtree_configured = true + + for i = 1, #package.dependents do + package.subtree_configured = configure_recursive(package.dependents[i]) and package.subtree_configured + end + + return package.subtree_configured +end + +local function load_recursive(package) + if not package.exists or not package.enabled or package.error then + return + end + + if package.subtree_loaded then + return true + end + + for i = 1, #package.dependencies do + if not package.dependencies[i].loaded then + return + end + end + + if not package.loaded then + local ok, err = ensure_added(package) + if not ok then + logger:log("error", string.format("failed to configure %s; reason: %s", package.id, err)) + return + end + + ok, err = run_hooks(package, "on_load") + if not ok then + logger:log("error", string.format("failed to load %s; reason: %s", package.id, err)) + return + end + + package.loaded = true + logger:log("load", string.format("loaded %s", package.id)) + end + + package.subtree_loaded = true + + for i = 1, #package.dependents do + package.subtree_loaded = load_recursive(package.dependents[i]) and package.subtree_loaded + end + + return package.subtree_loaded +end + +local function reload_meta() + local ok, err + bench("meta", function() + ok, err = pcall( + vim.cmd, + [[ + silent! helptags ALL + silent! UpdateRemotePlugins + ]] + ) + end) + + if ok then + logger:log("vim", "reloaded helptags and remote plugins") else - return opts.sync == "always" + logger:log("error", string.format("failed to reload helptags and remote plugins; reason: %s", err)) end end ---- make comparison for table.sort ----@param a package package spec a ----@param b package package spec b ----@return boolean -local function comp(a, b) - -- NOTE: this doesn't have to be in any real order, it just has to be - -- consistant, thus we can just check if the unicode value of one package - -- id is less than the other - return a.id < b.id +local function reload() + -- clear errors to retry + for i = 1, #packages do + packages[i].error = false + end + + local reloaded + reloaded = configure_recursive(root) or reloaded + reloaded = load_recursive(root) or reloaded + + if reloaded then + reload_meta() + end + + return reloaded end --- basically the main function of our program ----@param opts speclist -return function(opts) - M.config_path = debug.getinfo(2, "S").source:sub(2) - logger.pipe = logger:setup() - bench.setup() - lazy.setup() +local function reload_all() + -- recall the load function + if load and type(load) == "function" then + local ok, ret = pcall(load) + if ok and type(ret) == "table" then + register_recursive(ret) + else + logger:log("error", ret) + end + end - -- generate doc tags - vim.cmd.helptags(vim.fn.stdpath('data')..'/site/pack/core/opt/dep/doc') + for i = 1, #packages do + local package = packages[i] + package.loaded, package.subtree_loaded = false, false + end - local initialized, err = pcall(function() - packager.set_base_dir(opts.base_dir or vim.fn.stdpath("data").."/site/pack/core/opt/") - bench.mark("load", function() - -- register all packages - local root = packager:new({ - "squibid/dep", - url = "https://git.squi.bid/squibid/dep.git", - branch = "pack" - }) - if not root then - logger:log("error", "couldn't register root package") + reload() +end + +local function clean() + vim.loop.fs_scandir( + base_dir, + vim.schedule_wrap(function(err, handle) + if err then + logger:log("error", string.format("failed to clean; reason: %s", err)) + else + local queue = {} + + while handle do + local name = vim.loop.fs_scandir_next(handle) + if name then + queue[name] = base_dir .. name + else + break + end + end + + for i = 1, #packages do + queue[packages[i].name] = nil + end + + for name, dir in pairs(queue) do + -- todo: make this async + local ok = vim.fn.delete(dir, "rf") + if ok then + logger:log("clean", string.format("deleted %s", name)) + else + logger:log("error", string.format("failed to delete %s", name)) + end + end + end + end) + ) +end + +local function mark_reconfigure(package) + local function mark_dependencies(node) + node.subtree_configured, node.subtree_loaded = false, false + + for i = 1, #node.dependencies do + mark_dependencies(node.dependencies[i]) + end + end + + local function mark_dependents(node) + node.configured, node.loaded, node.added = false, false, false + node.subtree_configured, node.subtree_loaded = false, false + + for i = 1, #node.dependents do + mark_dependents(node.dependents[i]) + end + end + + mark_dependencies(package) + mark_dependents(package) +end + +local function sync(package, cb) + if not package.enabled then + cb() + return + end + + if package.exists then + if package.pin then + cb() + return + end + + local function log_err(err) + logger:log("error", string.format("failed to update %s; reason: %s", package.id, err)) + end + + proc.git_rev_parse(package.dir, "HEAD", function(err, before) + if err then + log_err(before) + cb(err) + else + if package.commit then + proc.git_checkout(package.dir, package.branch, package.commit, function(err, message) + if err then + log_err(message) + cb(err) + else + proc.git_rev_parse(package.dir, package.commit, function(err, after) + if err then + log_err(after) + cb(err) + elseif before == after then + logger:log("skip", string.format("skipped %s", package.id)) + cb(err) + else + mark_reconfigure(package) + logger:log("update", string.format("updated %s; %s -> %s", package.id, before, after)) + end + end) + end + end) + else + proc.git_fetch(package.dir, "origin", package.branch or "HEAD", function(err, message) + if err then + log_err(message) + cb(err) + else + proc.git_rev_parse(package.dir, "FETCH_HEAD", function(err, after) + if err then + log_err(after) + cb(err) + elseif before == after then + logger:log("skip", string.format("skipped %s", package.id)) + cb(err) + else + proc.git_reset(package.dir, after, function(err, message) + if err then + log_err(message) + else + mark_reconfigure(package) + logger:log("update", string.format("updated %s; %s -> %s", package.id, before, after)) + end + + cb(err) + end) + end + end) + end + end) + end + end + end) + else + proc.git_clone(package.dir, package.url, package.branch, function(err, message) + if err then + logger:log("error", string.format("failed to install %s; reason: %s", package.id, message)) + else + if package.commit then + proc.git_checkout(package.dir, package.branch, package.commit, function(err, message) + if err then + logger:log("error", string.format("failed to checkout %s; reason: %s", package.id, message)) + else + package.exists = true + mark_reconfigure(package) + logger:log("install", string.format("installed %s", package.id)) + end + end) + else + package.exists = true + mark_reconfigure(package) + logger:log("install", string.format("installed %s", package.id)) + end + end + + cb(err) + end) + end +end + +local function sync_list(list, on_complete) + local progress = 0 + local has_errors = false + + local function done(err) + progress = progress + 1 + has_errors = has_errors or err + + if progress == #list then + clean() + reload() + + if has_errors then + logger:log("error", "there were errors during sync; see :messages or :DepLog for more information") + else + logger:log("update", string.format("synchronized %s %s", #list, #list == 1 and "package" or "packages")) + end + + if on_complete then + on_complete() + end + end + end + + for i = 1, #list do + sync(list[i], done) + end +end + +local function get_commits(cb) + local results = {} + local done = 0 + for i = 1, #packages do + local package = packages[i] + + if package.exists then + proc.git_rev_parse(package.dir, "HEAD", function(err, commit) + if not err then + results[package.id] = commit + end + + done = done + 1 + if done == #packages then + cb(results) + end + end) + else + done = done + 1 + end + end +end + +local function print_list(cb) + get_commits(function(commits) + local buffer = vim.api.nvim_create_buf(true, true) + local line, indent = 0, 0 + + local function print(chunks) + local concat = {} + local column = 0 + + for _ = 1, indent do + concat[#concat + 1] = " " + column = column + 2 + end + + if not chunks then + chunks = {} + elseif type(chunks) == "string" then + chunks = { { chunks } } + end + + for i = 1, #chunks do + local chunk = chunks[i] + concat[#concat + 1] = chunk[1] + chunk.offset, column = column, column + #chunk[1] + end + + vim.api.nvim_buf_set_lines(buffer, line, -1, false, { table.concat(concat) }) + + for i = 1, #chunks do + local chunk = chunks[i] + if chunk[2] then + vim.api.nvim_buf_add_highlight(buffer, -1, chunk[2], line, chunk.offset, chunk.offset + #chunk[1]) + end + end + + line = line + 1 + end + + print(string.format("Installed packages (%s):", #packages)) + indent = 1 + + local loaded = {} + + local function dry_load(package) + if loaded[package.id] then return end - -- setup all packages and modules - if opts.modules then - modules:setup(opts, nil, M.config_path) - end - packager.register_speclist(opts) - - -- sort package dependencies - for _, package in pairs(packager.get_packages()) do - table.sort(package.requirements, comp) - table.sort(package.dependents, comp) + for i = 1, #package.dependencies do + if not loaded[package.dependencies[i].id] then + return + end end - -- make sure there arent any circular dependencies - local ok = packager.findcycle(packager.get_packages()) - if type(ok) == "table" then - logger:log("error", "found a cycle in the package spec here: %s", vim.inspect(ok)) + loaded[package.id], loaded[#loaded + 1] = true, package + + local chunk = { + { string.format("[%s] ", commits[package.id] or " "), "Comment" }, + { package.id, "Underlined" }, + } + + if not package.exists then + chunk[#chunk + 1] = { " *not installed", "Comment" } end + + if not package.loaded then + chunk[#chunk + 1] = { " *not loaded", "Comment" } + end + + if not package.enabled then + chunk[#chunk + 1] = { " *disabled", "Comment" } + end + + if package.pin then + chunk[#chunk + 1] = { " *pinned", "Comment" } + end + + print(chunk) + + for i = 1, #package.dependents do + dry_load(package.dependents[i]) + end + end + + dry_load(root) + indent = 0 + + print() + print("Load time (μs):") + indent = 1 + local profiles = {} + + for i = 1, #packages do + local package = packages[i] + local profile = { + package = package, + total = 0, + setup = package.perf.on_setup or 0, + load = package.perf.on_load or 0, + pack = package.perf.pack or 0, + + "total", + "setup", + "pack", + "load", + } + + if package == root then + for k, v in pairs(perf) do + if profile[k] then + profile[k] = profile[k] + v + end + end + end + + for j = 1, #profile do + profile.total = profile.total + profile[profile[j]] + end + + profiles[#profiles + 1] = profile + end + + table.sort(profiles, function(a, b) + return a.total > b.total end) - -- load packages - for _, package in pairs(packager.get_packages()) do - package:reload() + for i = 1, #profiles do + local profile = profiles[i] + local chunk = { + { "- ", "Comment" }, + { profile.package.id, "Underlined" }, + { string.rep(" ", 40 - #profile.package.id) }, + } + + for j = 1, #profile do + local key, value = profile[j], profile[profile[j]] + chunk[#chunk + 1] = { string.format(" %5s ", key), "Comment" } + chunk[#chunk + 1] = { string.format("%4d", value * 1000000) } + end + + print(chunk) end - -- get all package that need syncing - local targets = {} - for _, package in pairs(packager.get_packages()) do - if shouldsync(opts, package) then - table.insert(targets, package) + indent = 0 + print() + print("Dependency graph:") + + local function walk_graph(package) + local chunk = { + { "| ", "Comment" }, + { package.id, "Underlined" }, + } + + local function add_edges(p) + for i = 1, #p.dependencies do + local dependency = p.dependencies[i] + + if dependency ~= root and not chunk[dependency.id] then -- don't convolute the list + chunk[#chunk + 1] = { " " .. dependency.id, "Comment" } + chunk[dependency.id] = true + add_edges(dependency) + end + end + end + + add_edges(package) + print(chunk) + + for i = 1, #package.dependents do + indent = indent + 1 + walk_graph(package.dependents[i]) + indent = indent - 1 end end - -- install all targets - synctree(targets) - end) + walk_graph(root) - if not initialized then - logger:log("error", err) - end + print() + print("Debug information:") - -- add some user commands - vim.api.nvim_create_user_command("DepSync", function() - synctree(packager.get_packages()) - end, {}) - - vim.api.nvim_create_user_command("DepReload", function() - for _, package in pairs(packager.get_packages()) do - package:reload() + local debug = {} + for l in vim.inspect(packages):gmatch("[^\n]+") do + debug[#debug + 1] = l end - end, {}) + + vim.api.nvim_buf_set_lines(buffer, line, -1, false, debug) + vim.api.nvim_buf_set_name(buffer, "packages.dep") + vim.api.nvim_buf_set_option(buffer, "bufhidden", "wipe") + vim.api.nvim_buf_set_option(buffer, "modifiable", false) + + vim.cmd("vsp") + vim.api.nvim_win_set_buf(0, buffer) + + if cb then + cb() + end + end) end + +vim.cmd([[ + command! DepSync lua require("dep").sync() + command! DepReload lua require("dep").reload() + command! DepClean lua require("dep").clean() + command! DepList lua require("dep").list() + command! DepLog lua require("dep").open_log() + command! DepConfig lua require("dep").open_config() +]]) + +local function wrap_api(name, fn) + return function(...) + if initialized then + local ok, err = pcall(fn, ...) + if not ok then + logger:log("error", err) + end + else + logger:log("error", string.format("cannot call %s; dep is not initialized", name)) + end + end +end + +--todo: prevent multiple execution of async routines +return setmetatable({ + sync = wrap_api("dep.sync", function(on_complete) + sync_list(packages, on_complete) + end), + + reload = wrap_api("dep.reload", reload_all), + clean = wrap_api("dep.clean", clean), + list = wrap_api("dep.list", print_list), + + open_log = wrap_api("dep.open_log", function() + vim.cmd("vsp " .. logger.path) + end), + + open_config = wrap_api("dep.open_config", function() + vim.cmd("vsp " .. config_path) + end), +}, { + __call = function(_, config) + local err + perf = {} + config_path = debug.getinfo(2, "S").source:sub(2) + + initialized, err = pcall(function() + base_dir = config.base_dir or (vim.fn.stdpath("data") .. "/site/pack/deps/opt/") + packages = {} + + bench("load", function() + root = register("squibid/dep") + if config["load"] and type(config["load"]) == "function" then + local ok, ret = pcall(config["load"]) + if ok and type(ret) == "table" then + load = config["load"] + register_recursive(ret) + else + logger:log("error", ret) + end + end + register_recursive(config) + sort_dependencies() + ensure_acyclic() + end) + + reload() + + local should_sync = function(package) + if config.sync == "new" or config.sync == nil then + return not package.exists + else + return config.sync == "always" + end + end + + local targets = {} + + for i = 1, #packages do + local package = packages[i] + if should_sync(package) then + targets[#targets + 1] = package + end + end + + sync_list(targets) + end) + + if not initialized then + logger:log("error", err) + end + end, +}) diff --git a/lua/dep/bench.lua b/lua/dep/bench.lua deleted file mode 100644 index e8b2135..0000000 --- a/lua/dep/bench.lua +++ /dev/null @@ -1,32 +0,0 @@ --- TODO: actually use this (ideally make a view that shows startuptime and --- which plugins are currently loaded) --- performance logging - ----@class bench ----@field perf number[] list of all perfs -local bench = {} -local b - -function bench.setup() - local o = {} - - o.perf = {} - o.inited = true - - b = o -end - ---- benchmark a peice of code ----@param name string the name of the benchmark ----@param f function the code to benchmark ----@vararg any args for f ----@return any ret the result of f -function bench.mark(name, f, ...) - local start = os.clock() - local ret = f(...) - b.perf[name] = os.clock() - start - - return ret -end - -return bench diff --git a/lua/dep/lazy/init.lua b/lua/dep/lazy/init.lua deleted file mode 100644 index a0237d6..0000000 --- a/lua/dep/lazy/init.lua +++ /dev/null @@ -1,36 +0,0 @@ -local packager = require("dep.package") - ----@class lazy -local lazy = {} - --- since this is already a ridiculous "optimization" we should really be caching --- the results of this for when the user keeps on loading the colorscheme that --- they've lazy loaded, that way we speed up the lazy loading process -local function colorscheme() - -- if a colorscheme doesn't exist attempt load it prior to it being set - vim.api.nvim_create_autocmd("ColorschemePre", { - pattern = vim.fn.getcompletion("", "color"), - callback = function(e) - for _, p in pairs(packager.get_packages()) do - if not p.loaded then - for _, ext in ipairs({ ".lua", ".vim" }) do - local path = p.dir.."/colors/"..e.match..ext - if vim.uv.fs_stat(path) then - p:ensureadded(true) - -- break out of here, we've loaded the colorscheme - return - end - end - end - end - end - }) -end - ---- setup all lazy handlers -function lazy.setup() - -- start the colorscheme watcher - colorscheme() -end - -return lazy diff --git a/lua/dep/lazy/loader/init.lua b/lua/dep/lazy/loader/init.lua deleted file mode 100644 index 0603aa1..0000000 --- a/lua/dep/lazy/loader/init.lua +++ /dev/null @@ -1,174 +0,0 @@ -local logger = require('dep.log') -local packager = require('dep.package') - ----@class lazy_loader ----@field load function the function to load the plugin ----@field command_ids string[] the commands that have been registered ----@field auto_ids number[] the auto commands that have been registered ----@field keybind_ids table[] the keybinds that have been registered ----@field plugin_ids table[] the plugins that have been registered -local lazy_loader = {} - ---- create a new instance of lazy ----@return lazy -function lazy_loader:new() - local o = {} - - setmetatable(o, self) - o.command_ids = {} - o.auto_ids = {} - o.keybind_ids = {} - o.plugin_ids = {} - - self.__index = self - - return o -end - ---- set the loading callback ----@param load function the loader function -function lazy_loader:set_load(load) - self.load = load -end - ---- create a usercommand which will trigger the plugin to load ----@param name string the name of the command ----@param opts vim.api.keyset.user_command? options -function lazy_loader:cmd(name, opts) - opts = opts or {} - - -- move the rerun arg to a seperate variable because keymap.set doesn't like - -- options it doesn't know of - local rerun = opts["rerun"] or true - opts['rerun'] = nil - - -- load the plugin on completion - if not opts["complete"] then - opts["complete"] = function(_, line, _) - self:cleanup() - - -- return all completions for the current input, we need this to ensure - -- that the new completions are loaded from the actual plugin, not our - -- definiton of the command - return vim.fn.getcompletion(line, "cmdline") - end - opts["nargs"] = "*" - end - - vim.api.nvim_create_user_command(name, opts['callback'] or function(_) - self:cleanup() - - -- attempt to rerun the command - if not rerun then - pcall(vim.cmd, name) - end - end, opts) - - table.insert(self.command_ids, name) -end - ---- create an auto command which will trigger the plugin to load ----@param event string the event to trigger on ----@param opts vim.api.keyset.create_autocmd? options -function lazy_loader:auto(event, opts) - opts = opts or {} - - opts['callback'] = opts['callback'] or function() - self:cleanup() - end - - -- create the auto command and save it - table.insert(self.auto_ids, vim.api.nvim_create_autocmd(event, opts)) -end - ---- create an auto command which will trigger on filetype ----@param filetype string filetype to register the auto on -function lazy_loader:ft(filetype) - self:auto("FileType", { - pattern = filetype - }) -end - ----@class lazy.Opts: vim.keymap.set.Opts ----@field rerun boolean|function weather to rerun and what to do - ---- create a keybind which will trigger the plugin to load ----@param mode string the mode to trigger in ----@param bind string the binding to use ----@param opts lazy.Opts? options -function lazy_loader:keymap(mode, bind, opts) - opts = opts or {} - - -- move the rerun arg to a seperate variable because keymap.set doesn't like - -- options it doesn't know of - local rerun = opts['rerun'] or true - opts['rerun'] = nil - - vim.keymap.set(mode, bind, opts['callback'] or function() - -- register keymap unload - self:cleanup() - - -- call the keymap after the user has mapped it - if type(rerun) == "function" then - rerun() - elseif rerun then - local keys = vim.api.nvim_replace_termcodes(bind, true, false, true) - vim.api.nvim_input(keys) - end - end, opts) - - table.insert(self.keybind_ids, { ['mode'] = mode, ['bind'] = bind }) -end - ---- load a plugin when another plugin loads ----@param plugin string plugin name ----@param opts table? options -function lazy_loader:plugin(plugin, opts) - opts = opts or {} - opts["callback"] = opts["callback"] or function() - self:cleanup() - end - - if packager.get_packages()[plugin].loaded then - opts["callback"]() - else - local on_load = packager.get_packages()[plugin].on_load - local on_load_idx = #on_load + 1 - on_load[on_load_idx] = opts["callback"] - - table.insert(self.plugin_ids, { plugin, on_load_idx }) - end -end - ---- cleanup all the callbacks, and load the plugin -function lazy_loader:cleanup() - -- cleanup user commands - for _, command_id in ipairs(self.command_ids) do - local ok, err = pcall(vim.api.nvim_del_user_command, command_id) - if not ok then - logger:log("lazy", err or "failed to delete user command") - end - end - -- cleanup auto commands - for _, auto_id in ipairs(self.auto_ids) do - local ok, err = pcall(vim.api.nvim_del_autocmd, auto_id) - if not ok then - logger:log("lazy", err or "failed to delete auto command") - end - end - -- cleanup keymaps - for _, keybind_id in ipairs(self.keybind_ids) do - local ok, err = pcall(vim.keymap.del, keybind_id.mode, keybind_id.bind, {}) - if not ok then - logger:log("lazy", err or "failed to delete keymap") - end - end - -- cleanup plugins - for _, plugin_id in ipairs(self.plugin_ids) do - table.remove(packager.get_packages()[plugin_id[1]].on_load, plugin_id[2]) - end - -- load the plugin - self:load() -end - -return lazy_loader diff --git a/lua/dep/lazy/loader/short.lua b/lua/dep/lazy/loader/short.lua deleted file mode 100644 index e7c74f2..0000000 --- a/lua/dep/lazy/loader/short.lua +++ /dev/null @@ -1,72 +0,0 @@ --- This file contains shorthands which rely on the loader core functions. They --- are intended to ease lazy loading condition definitions to use them you may --- do the following: --- --- ```lua --- _G.dep_short = require("dep.lazy.loader.short") --- ``` --- --- Which will allow you to reference it anywhere in your config like so: --- --- ```lua --- require("dep") { --- { "some/plugin", --- lazy = dep_short.cmd("TheCommand") --- } --- } --- ``` --- --- Happy vimming o/ -local short = {} - ---- create a single command ----@param name string the name of the command ----@param opts vim.api.keyset.user_command? options ----@return function callback -function short.cmd(name, opts) - return function(load) - load:cmd(name, opts) - end -end - ---- create a single auto command ----@param event string the event to trigger on ----@param opts vim.api.keyset.create_autocmd? options ----@return function callback -function short.auto(event, opts) - return function(load) - load:auto(event, opts) - end -end - ---- create a single auto command which will trigger on filetype ----@param filetype string filetype to register the auto on ----@return function callback -function short.ft(filetype) - return function(load) - load:ft(filetype) - end -end - ---- create a single keybind ----@param mode string the mode to trigger in ----@param bind string the binding to use ----@param opts lazy.Opts? options ----@return function callback -function short.keymap(mode, bind, opts) - return function(load) - load:keymap(mode, bind, opts) - end -end - ---- create a single plugin load event for when another plugin loads ----@param plugin string plugin name ----@param opts table? options ----@return function callback -function short.plugin(plugin, opts) - return function(load) - load:plugin(plugin, opts) - end -end - -return short diff --git a/lua/dep/log.lua b/lua/dep/log.lua index c869caf..86994b6 100644 --- a/lua/dep/log.lua +++ b/lua/dep/log.lua @@ -1,28 +1,23 @@ -local logger = {} +-- +-- Copyright (c) 2022 chiya.dev +-- +-- Use of this source code is governed by the MIT License +-- which can be found in the LICENSE file and at: +-- +-- https://chiya.dev/licenses/mit.txt +-- +local vim, setmetatable, pcall, debug, string, os, assert = vim, setmetatable, pcall, debug, string, os, assert -logger.stage_colors = { - skip = "Comment", - clean = "Boolean", - install = "MoreMsg", - update = "WarningMsg", - delete = "Directory", - error = "ErrorMsg", -} - ---- create the default logging path ----@return string path to the logfile local function default_log_path() - -- create cache directory and chmod it if it doesn't already exist + -- ensure cache directory exists (#5) local path = vim.fn.stdpath("cache") - if not vim.uv.fs_stat(path) then - vim.uv.fs_mkdir(path, 0x1ff) -- 0777 + if not vim.loop.fs_stat(path) then + vim.loop.fs_mkdir(path, 0x1ff) -- 0777 end - return vim.fs.normalize(path).."/dep.log" + return path .. "/dep.log" end ---- attempt to format a string ----@vararg string formating args local function try_format(...) local ok, s = pcall(string.format, ...) if ok then @@ -30,71 +25,85 @@ local function try_format(...) end end ---- setup all logging stuff ----@param path string|nil optional alternative path for the log file ----@return table -function logger:setup(path) - logger.path = path or default_log_path() - local pipe +--- Writes logs to a file and prints pretty status messages. +local Logger = setmetatable({ + __metatable = "Logger", + __index = { + --- Prints a message associated with a stage. + log = function(self, stage, message, ...) + -- calling function + local source = debug.getinfo(2, "Sl").short_src - logger.handle = assert(vim.uv.fs_open(logger.path, "w", 0x1a4)) -- 0644 - pipe = vim.uv.new_pipe() --[[@as uv.uv_pipe_t]] - pipe:open(logger.handle) - - return pipe -end - ---- log a message ----@param level string error level ----@param message any string message to send ----@vararg any options to go into the message -function logger:log(level, message, ...) - -- make sure the message string is actually a string, and formatted - -- appropriately - if type(message) == "string" then - message = try_format(message, ...) or message - else - message = vim.inspect(message) - end - - -- get debug info about the current function - local source = debug.getinfo(2, "Sl") - - -- schedule a log message to be sent to vim, and the log file - vim.schedule(function() - if not logger.silent then - if level == "error" then - vim.api.nvim_echo({ { string.format("[dep] %s", message) } }, true, - { err = true }) - elseif logger.stage_colors[level] then - vim.api.nvim_echo({ - { "[dep]", "Identifier" }, - { " " }, - { message, logger.stage_colors[level] }, - }, true, {}) + -- format or stringify message + if type(message) == "string" then + message = try_format(message, ...) or message + else + message = vim.inspect(message) end - end - -- write to the pipe if it's open - if logger.pipe then - logger.pipe:write(string.format("[%s] %s:%s:(%s) %s\n", os.date("%T"), - source.short_src:gsub('.*%/', ''), source.currentline, level, message)) - end - end) -end + -- print and write must be done on the main event loop + vim.schedule(function() + if not self.silent then + if stage == "error" then + vim.api.nvim_err_writeln(string.format("[dep] %s", message)) + elseif self.stage_colors[stage] then + vim.api.nvim_echo({ + { "[dep]", "Identifier" }, + { " " }, + { message, self.stage_colors[stage] }, + }, true, {}) + end + end ---- cleanup all logging stuff ----@param pipe table? pipe ----@param handle table? handle -function logger:cleanup(pipe, handle) - if pipe then - pipe:close() - pipe = nil - end - if handle then - vim.uv.fs_close(logger.handle) - handle = nil - end -end + if self.pipe then + self.pipe:write(string.format("[%s] %s: %s\n", os.date(), source, message)) + end + end) + end, -return logger + --- Closes the log file handle. + close = function(self) + if self.pipe then + self.pipe:close() + self.pipe = nil + end + + if self.handle then + vim.loop.fs_close(self.handle) + self.handle = nil + end + end, + }, +}, { + --- Constructs a new `Logger`. + __call = function(mt, path) + path = path or default_log_path() + + -- clear and open log file + local handle = assert(vim.loop.fs_open(path, "w", 0x1a4)) -- 0644 + local pipe = vim.loop.new_pipe() + pipe:open(handle) + + return setmetatable({ + path = path, + handle = handle, + pipe = pipe, + silent = false, + + -- TODO: This looks good for me ;) but it should have proper vim color mapping for other people. + stage_colors = { + skip = "Comment", + clean = "Boolean", + install = "MoreMsg", + update = "WarningMsg", + delete = "Directory", + error = "ErrorMsg", + }, + }, mt) + end, +}) + +return { + Logger = Logger, + global = Logger(), +} diff --git a/lua/dep/modules/init.lua b/lua/dep/modules/init.lua deleted file mode 100644 index f34fba5..0000000 --- a/lua/dep/modules/init.lua +++ /dev/null @@ -1,74 +0,0 @@ -local logger = require('dep.log') -local module = require("dep.modules.module") - ----@class modules ----@field modules module[] all modules in dep -local modules = {} - ---- Initialize all the modules ----@param self table? ----@param speclist table ----@param overrides spec? overrides ----@return modules modules manager ----@nodisacard -function modules:setup(speclist, overrides, config_path) - overrides = overrides or {} - - local o = {} - self = {} - self.__index = self - setmetatable(o, self) - - -- create a list of modules - o.modules = {} - - if (speclist.modules[1] == "*" or #speclist.modules == 0) - and speclist.modules.prefix then - - local path = vim.fs.joinpath(config_path:gsub("[^/]*$", ""), - "lua", (speclist.modules.prefix:gsub("%.", "/")) - ) - - local handle = vim.uv.fs_scandir(path) - while handle do - local name = vim.uv.fs_scandir_next(handle) - if name then - -- skip non-lua files - if name:sub(#name - 3) ~= ".lua" then - goto continue - end - - -- remove the file extension from the name so that lua doesn't fail - -- when attempting to load it - name = name:sub(0, #name - 4) - - -- put the module into the list of modules - table.insert(speclist.modules, name) - - ::continue:: - elseif name == nil then - -- no more entries - break - else - -- if there's a single error bail out - logger:log("error", "failed to run clean uv.fs_scandir_next failed") - break - end - end - end - - -- loop through all modules and initialize them - for _, modpath in ipairs(speclist.modules) do - local mod = module.new(nil, modpath, speclist.modules.prefix, overrides) - if not mod then - goto continue - end - - table.insert(o.modules, mod) - ::continue:: - end - - return self -end - -return modules diff --git a/lua/dep/modules/module.lua b/lua/dep/modules/module.lua deleted file mode 100644 index a16ddf6..0000000 --- a/lua/dep/modules/module.lua +++ /dev/null @@ -1,73 +0,0 @@ -local logger = require('dep.log') -local spec_man = require("dep.spec") -local packager = require("dep.package") - ----@class module ----@field name string name of the module ----@field desc string description of the module ----@field disable boolean weather to disable all the packages inside the module ----@field path string path to the module ----@field mod table the module ----@field packages package[] all packages registed from the module -local module = {} - ---- Initialize a module ----@param self table? ----@param modpath string path to the module ----@param prefix string? the prefix to all modules ----@param overrides spec? a module override ----@return module|false module false on failure to load module ----@nodiscard -function module:new(modpath, prefix, overrides) - overrides = overrides or {} - - local ok, err - local o = {} - self = {} - self.__index = self - setmetatable(o, self) - - o.name = "" - o.desc = "" - if type(modpath) == "string" then - if prefix ~= nil then - if prefix:sub(#prefix) ~= "." and modpath:sub(1, 2) ~= "." then - modpath = "."..modpath - end - o.path = prefix..modpath - else - o.path = modpath - end - o.name = modpath - ok, o.mod = pcall(require, o.path) - if not ok then - logger:log("error", "failed to load module: %s", vim.inspect(o.mod)) - return false - end - end - o.name = o.mod.name or o.name - o.desc = o.mod.desc or o.desc - - -- ensure the overrides are properly set - overrides = vim.tbl_extend("force", overrides, { - disable = o.mod.disable or overrides.disable - }) - - -- allow a module to be a spec - if spec_man.check(o.mod, true) ~= false then - o.mod = { o.mod } - end - - ok, err = pcall(packager.register_speclist, o.mod, overrides) - if not ok then - logger:log("error", "%s <- %s", err, o.name) - return false - end - - -- ensure that the module contains the packages that it's created - self.packages = err - - return self -end - -return module diff --git a/lua/dep/package.lua b/lua/dep/package.lua index 072495a..e76238d 100644 --- a/lua/dep/package.lua +++ b/lua/dep/package.lua @@ -1,511 +1,258 @@ -local logger = require('dep.log') -local spec_man = require("dep.spec") -local bench = require("dep.bench") +-- +-- Copyright (c) 2022 chiya.dev +-- +-- Use of this source code is governed by the MIT License +-- which can be found in the LICENSE file and at: +-- +-- https://chiya.dev/licenses/mit.txt +-- +local require, type, setmetatable, error, table, assert, math, os, debug = + require, type, setmetatable, error, table, assert, math, os, debug +local logger = require("dep.log").global ----@class package ----@field id string id of the package ----@field enabled boolean whether it's going to be used ----@field exists boolean if the package exists on the filesystem ----@field lazy boolean if the package is lazy loaded in any way ----@field added boolean if the package has been added in vim ----@field configured boolean if the package has been configured ----@field loaded boolean if a package has been loaded ----@field subtree_loaded boolean is the subtree has been loaded ----@field on_config function[] table of functions to run on config ----@field on_setup function[] table of function to run on setup ----@field on_load function[] table of functions to run on load ----@field lazy_load table table of functions and booleans to run which will tell the package when to load ----@field requirements package[] this package's requirements ----@field dependents package[] packages that require this package ----@field perf table performance metrics for the package ----@field name string the name of the package ----@field url string the url of the package ----@field path string? the path of the package which overrides the url ----@field branch string the branch of the package ----@field dir string the directory of the package ----@field commit string the commit of the package ----@field pin boolean whether to pin the package or not -local package = {} - ---- the base directory for the packages ----@type string -local base_dir - ---- the root package ----@type package -local root - ---- list of all package in dep ----@type package[] -local packages = {} - ---- tell the parent it has a child and the child it has a parent ----@param parent package? parent package if nil defaults to self ----@param child package child package -function package:link_dependency(parent, child) - parent = parent or self - - if not parent.dependents[child.id] then - parent.dependents[child.id] = child - table.insert(parent.dependents, child) - end - - if not child.requirements[parent.id] then - child.requirements[parent.id] = parent - table.insert(child.requirements, parent) - end -end - ---- create a new package instance ----@param spec spec|string a package spec to use for the new package ----@param overrides spec? a package spec that is used to overried this package ----@return package|false package an instance of the package or false on failure ----@nodisacard -function package:new(spec, overrides) - overrides = overrides or {} - - -- ensure that the spec is ok - local new_spec = spec_man.check(spec_man.correct_spec(spec)) - if new_spec == false then - logger:log("spec", vim.inspect(spec)) - logger:log("error", "spec check failed, check DepLog") - return false +local function parse_name_from_id(id) + local name = id:match("^[%w-_.]+/([%w-_.]+)$") + if name then + return name else - spec = new_spec + error(string.format('invalid package name "%s"; must be in the format "user/package"', id)) end +end - -- start initializing the package - local id = spec[1] +local function is_nonempty_str(s) + return type(s) == "string" and #s ~= 0 +end - local o = packages[id] or {} - self.__index = self - setmetatable(o, self) - - -- if package hasn't been registered already, get the inital spec regisitered - if not o.id then - o.id = id - o.enabled = true - o.exists = false - o.lazy = false - o.added = false - o.configured = false - o.loaded = false - o.subtree_loaded = false - o.on_config = {} - o.on_setup = {} - o.on_load = {} - o.lazy_load = {} - - o.requirements = {} - o.dependents = {} - o.perf = {} - - packages[id] = o - end - - o.name = spec.as or o.name or spec_man.get_name(spec) - o.url = spec.url or o.url or ("https://github.com/"..id..".git") - o.path = spec.path and "file://"..vim.fs.normalize(spec.path) or spec.path - o.branch = spec.branch or o.branch - o.dir = base_dir.."/"..o.name - o.commit = spec.commit - o.pin = overrides.pin or spec.pin or o.pin - o.enabled = not overrides.disable and not spec.disable and o.enabled - o.lazy = spec.lazy ~= nil or o.lazy - - -- make sure that the package exists - o.exists = vim.fn.isdirectory(o.dir) ~= 0 - o.configured = o.exists - - -- register all the callback functions - if spec.config ~= nil then - table.insert(o.on_config, spec.config) - end - if spec.setup ~= nil then - table.insert(o.on_setup, spec.setup) - end - if spec.load ~= nil then - table.insert(o.on_load, spec.load) - end - if spec.lazy ~= nil then - table.insert(o.lazy_load, spec.lazy) - end - - -- if the current package isn't the root package then it depends on the root - -- package - if root and o ~= root then - o:link_dependency(root, o) - elseif not root then - root = o - end - - -- link the dependencies - if spec.reqs then - ---it is the correct type as asserted in check_spec() - ---@diagnostic disable-next-line: param-type-mismatch - for _, req in pairs(spec.reqs) do - local pkg = package:new(req) - if type(pkg) == "table" then - o:link_dependency(pkg, o) +--- Package information. +local Package = setmetatable({ + __metatable = "Package", + __index = { + --- Runs all registered hooks of the given type. + run_hooks = function(self, hook) + local hooks = self["on_" .. hook] + if not hooks or #hooks == 0 then + return true end - end - end - -- and link the dependents - if spec.deps then - ---it is the correct type as asserted in check_spec() - ---@diagnostic disable-next-line: param-type-mismatch - for _, dep in pairs(spec.deps) do - local pkg = package:new(dep) - if not pkg then - return false - end - o:link_dependency(o, pkg) - - -- if the child package is lazy loaded make sure the child package - -- is only loaded when the parent package has finished loading - if o.lazy then - -- tell the dep that it's gonna be lazy - pkg.lazy = true - table.insert(pkg.lazy_load, - require("dep.lazy.loader.short").plugin(id)) - end - end - end - - return o -end - ---- set the base directory for packages ----@param _base_dir string base directory -function package.set_base_dir(_base_dir) - base_dir = vim.fs.normalize(_base_dir) -end - ---- get the base directory for packages ----@return string base_dir ----@nodiscard -function package.get_base_dir() - return base_dir -end - ---- get the root directory ----@return package root ----@nodiscard -function package.get_root() - return root -end - ---- get the packages in dep ----@return package[] packages ----@nodiscard -function package.get_packages() - return packages -end - ---- run specified hooks on the current package ----@param type "on_load"|"on_config"|"on_setup" which hook to run ----@return boolean, string|nil -function package:runhooks(type) - local hooks = self[type] - if #hooks == 0 then - return true - end - - local start = os.clock() - - -- chdir into the package directory to make running external commands - -- from hooks easier. - local last_cwd = vim.fn.getcwd() - vim.fn.chdir(self.dir) - - for i = 1, #hooks do - local ok, err = pcall(hooks[i]) - if not ok then - vim.fn.chdir(last_cwd) - - return false, err - end - end - - vim.fn.chdir(last_cwd) - self.perf[type] = os.clock() - start - - logger:log("hook", "triggered %d %s %s for %s", #hooks, type, - #hooks == 1 and "hook" or "hooks", self.id) - - return true -end - ---- make sure a package has been loaded ----@param force boolean? force lazy packages to load ----@return boolean|table return true or false if loaded or package spec if lazy loaded -function package:ensureadded(force) - --- load a package - ---@param pkg package - local function loadpkg(pkg) - if not self.enabled then - return false - end - - -- make sure to load the dependencies first - for _, p in pairs(pkg.requirements) do - if not p.loaded then - p:ensureadded(true) - end - end - - -- run setup hooks - pkg:runhooks("on_setup") - - -- now start loading our plugin - local start = os.clock() - - -- trigger the packadd for the plugin - ---@diagnostic disable-next-line: param-type-mismatch - local ok, err = pcall(vim.cmd, "packadd "..pkg.name) - if not ok then - return false, err - end - - pkg.added = true - pkg.perf.pack = os.clock() - start - logger:log("vim", "packadd completed for %s", pkg.id) - - -- set the package to loaded - pkg.loaded = true - logger:log("load", "loaded %s", pkg.id) - - -- trigger the on_load hooks - ok, err = pkg:runhooks("on_load") - if not ok then - logger:log("error", "failed to load %s; reason: %s", pkg.id, err) - return - end - end - - -- make sure the package is lazy loaded if need be - if not self.added and not self.loaded and not self.lazy or force then - loadpkg(self) - elseif not self.added and self.lazy then - logger:log("lazy", "registering %d lazy hooks for %s", #self.lazy_load, - self.id) - for _, load_cond in pairs(self.lazy_load) do - -- configure the lazy loader for the user - local l = require('dep.lazy.loader'):new() - if l == true then - logger:log("lazy", "failed to get lazy utils") - return false - end - l:set_load(function() - -- ensure that we can't attempt to load a plugin twice via lazy loading - if self.loaded then - return - end - - logger:log("lazy", "triggered %d lazy hooks for %s", #self.lazy_load, - self.id) - loadpkg(self) - end) - - -- run it if it's not just a stopper to keep the plugin lazy - if load_cond ~= true then - local ok, err = pcall(load_cond, l) + local start = os.clock() + for i = 1, #hooks do + local ok, err = xpcall(hooks[i], debug.traceback) if not ok then - logger:log("error", "failed to register lazy load conditions for '%s': %s", - self.name, err) + return false, err end end - end - return self - end - return true -end + local elapsed = os.clock() - start + self.perf.hooks[hook] = elapsed ---- load all packages in package tree ----@param force boolean? force lazy packages to load ----@return boolean boolean if tree was successfully loaded ----@nodiscard -function package:loadtree(force) - -- if the package doesn't exist or isn't enabled then don't load it - if not self.exists or not self.enabled then - return false - end + logger:log( + "hook", + "triggered %d %s %s for %s in %dms", + #hooks, + hook, + #hooks == 1 and "hook" or "hooks", + self.id, + elapsed + ) - -- if the subtree is loaded then it's already loaded unless it needs forcing - if not force and self.subtree_loaded then - return true - end + return true + end, + }, +}, { + --- Constructs a new `Package` with the given identifier. + __call = function(mt, id) + local name = parse_name_from_id(id) + return setmetatable({ + id = id, + name = name, + url = "https://github.com/" .. id .. ".git", + enabled = true, + exists = false, + added = false, + configured = false, + loaded = false, + dependencies = {}, + dependents = {}, + subtree_configured = false, + subtree_loaded = false, + on_setup = {}, + on_config = {}, + on_load = {}, + perf = { hooks = {} }, + }, mt) + end, +}) - -- if the package isn't lazy check that it's requirements are loaded - if not self.lazy then - for _, requirement in pairs(self.requirements) do - if not requirement.loaded and not requirement.lazy then - return false +--- Manages a set of packages. +local PackageStore = setmetatable({ + __metatable = "PackageStore", + __index = { + --- Links the given packages such that the parent must load before the child. + link_dependency = function(self, parent, child) + if not parent.dependents[child.id] then + parent.dependents[child.id] = child + parent.dependents[#parent.dependents + 1] = child end - end - end - -- if the package isn't loaded and isn't lazy then it should probably be - -- loaded - if not self.loaded then - local ok, err = self:ensureadded(force) - if not ok then - logger:log("error", "failed to load %s; reason: %s", self.id, err) - return false - end - end + if not child.dependencies[parent.id] then + child.dependencies[parent.id] = parent + child.dependencies[#child.dependencies + 1] = parent + end + end, - self.subtree_loaded = true + --- Ensures the given package spec table is valid. + validate_spec = function(self, spec) + assert(spec[1] ~= nil, "package id missing from spec") + assert(type(spec[1]) == "string", "package id must be a string") + parse_name_from_id(spec[1]) - -- make sure the dependants are loaded - for _, dependant in pairs(self.dependents) do - self.subtree_loaded = dependant:loadtree(force) and self.subtree_loaded - end + assert(spec.as == nil or is_nonempty_str(spec.as), "package name must be a string") + assert(spec.url == nil or type(spec.url) == "string", "package url must be a string") -- TODO: validate url or path + assert(spec.branch == nil or is_nonempty_str(spec.branch), "package branch must be a string") + assert(spec.pin == nil or type(spec.pin) == "boolean", "package pin must be a boolean") + assert(spec.disable == nil or type(spec.disable) == "boolean", "package disable must be a boolean") - return self.subtree_loaded -end + assert( + spec.requires == nil or type(spec.requires) == "table" or type(spec.requires) == "string", + "package requires must be a string or table" + ) + assert( + spec.deps == nil or type(spec.deps) == "table" or type(spec.deps) == "string", + "package deps must be a string or table" + ) ---- unconfigure a packages tree -function package:unconfiguretree() - -- unconfigure requirements - for _, requirement in pairs(self.requirements) do - requirement.subtree_loaded = false - end + assert(spec.setup == nil or type(spec.setup) == "function", "package setup must be a function") + assert(spec.config == nil or type(spec.config) == "function", "package config must be a function") + assert(spec[2] == nil or type(spec[2]) == "function", "package loader must be a function") + end, - -- unconfigure dependents - for _, dependant in pairs(self.dependents) do - dependant.loaded = false - dependant.added = false - dependant.configured = false + --- Creates or updates a package from the given spec table, and returns that package. + add_spec = function(self, spec, scope) + self:validate_spec(spec) + scope = scope or {} - dependant.subtree_loaded = false - end -end + local id = spec[1] + local pkg = self[id] ---- check a list of packages for any cycles ----@param pkgs package[] list of packages ----@return package[]|false cycle the cycle that was found or false if not found ----@nodisacard -function package.findcycle(pkgs) - local index = 0 - local indexes = {} - local lowlink = {} - local stack = {} + if not pkg then + pkg = Package(id) + self[id], self[#self + 1] = pkg, pkg + end - --- use tarjan algorithm to find circular dependencies (strongly connected - --- components) - ---@param pkg package - local function connect(pkg) - indexes[pkg.id], lowlink[pkg.id] = index, index - stack[#stack + 1], stack[pkg.id] = pkg, true - index = index + 1 + -- blend package spec with existing package info + pkg.name = spec.as or pkg.name + pkg.url = spec.url or pkg.url + pkg.branch = spec.branch or pkg.branch + pkg.pin = scope.pin or spec.pin or pkg.pin + pkg.enabled = not scope.disable and not spec.disable and pkg.enabled - for i = 1, #pkg.dependents do - local dependent = pkg.dependents[i] + pkg.on_setup[#pkg.on_setup + 1] = spec.setup + pkg.on_config[#pkg.on_config + 1] = spec.config + pkg.on_load[#pkg.on_load + 1] = spec[2] - if not indexes[dependent.id] then - local cycle = connect(dependent) - if cycle then - return cycle - else - lowlink[pkg.id] = math.min(lowlink[pkg.id], lowlink[dependent.id]) + local requires = type(spec.requires) == "table" and spec.requires or { spec.requires } + local deps = type(spec.deps) == "table" and spec.deps or { spec.deps } + + -- recursively add specs for dependencies and dependents + for i = 1, #requires do + self:link_dependency(self:add_spec(requires[i], scope), pkg) + end + + for i = 1, #deps do + self:link_dependency(pkg, self:add_spec(deps[i], scope)) + end + end, + + --- Adds the given list of specs. + add_specs = function(self, specs, scope) + assert(type(specs) == "table", "package list must be a table") + assert(specs.pin == nil or type(specs.pin) == "boolean", "package list pin must be a boolean") + assert(specs.disable == nil or type(specs.disable) == "boolean", "package list disable must be a boolean") + + scope = scope or {} + scope = { + -- outer scope takes precedence over inner list's overrides + pin = scope.pin or specs.pin, + disable = scope.disable or specs.disable, + } + + -- add specs in spec list + for i = 1, #specs do + self:add_spec(specs[i], scope) + end + end, + + --- Ensures there are no circular dependencies in this package store. + ensure_acyclic = function(self) + -- tarjan's strongly connected components algorithm + local idx, indices, lowlink, stack = 0, {}, {}, {} + + local function connect(pkg) + indices[pkg.id], lowlink[pkg.id] = idx, idx + stack[#stack + 1], stack[pkg.id] = pkg, true + idx = idx + 1 + + for i = 1, #pkg.dependents do + local dependent = pkg.dependents[i] + + if not indices[dependent.id] then + local cycle = connect(dependent) + if cycle then + return cycle + else + lowlink[pkg.id] = math.min(lowlink[pkg.id], lowlink[dependent.id]) + end + elseif stack[dependent.id] then + lowlink[pkg.id] = math.min(lowlink[pkg.id], indices[dependent.id]) + end + end + + if lowlink[pkg.id] == indices[pkg.id] then + local cycle = { pkg } + local node + + repeat + node = stack[#stack] + stack[#stack], stack[node.id] = nil, nil + cycle[#cycle + 1] = node + until node == pkg + + -- a node is by definition strongly connected to itself + -- ignore single-node components unless the package explicitly specified itself as a dependency (i.e. the user is being weird) + if #cycle > 2 or pkg.dependents[pkg.id] then + return cycle + end end - elseif stack[dependent.id] then - lowlink[pkg.id] = math.min(lowlink[pkg.id], indexes[dependent.id]) end - end - if lowlink[pkg.id] == indexes[pkg.id] then - local cycle = { pkg } - local node + for i = 1, #self do + local pkg = self[i] - repeat - node = stack[#stack] - stack[#stack], stack[node.id] = nil, nil - cycle[#cycle + 1] = node - until node == pkg - - -- a node is by definition strongly connected to itself ignore single-node - -- components unless it explicitly specified itself as a dependency - if #cycle > 2 or pkg.dependents[pkg.id] then - return cycle + if not indices[pkg.id] then + local cycle = connect(pkg) + if cycle then + -- found dependency cycle + local names = {} + for j = 1, #cycle do + names[j] = cycle[j].id + end + error("circular dependency detected in package dependency graph: " .. table.concat(names, " -> ")) + end + end end - end - end + end, + }, +}, { + --- Constructs a new `PackageStore`. + __call = function(mt) + -- hash part of store maps package ids to packages + -- array part of store is a list of packages + -- all packages in a store are unique based on their id + return setmetatable({}, mt) + end, +}) - -- actually check the cycle - for _, pkg in pairs(pkgs) do - if not indexes[package.id] then - local cycle = connect(pkg) - if cycle then - return cycle - end - end - end - - return false -end - ---- recurse over all packages and register them ----@param speclist speclist table of specs ----@param overrides spec? a package spec that is used to override options ----@return package[] packages -function package.register_speclist(speclist, overrides) - overrides = overrides or {} - local packages_from_speclist = {} - - -- recurse the packages - local over = overrides - for _, spec in ipairs(speclist) do - -- make sure the overrides override and take into account the packages spec - ---@diagnostic disable-next-line: missing-fields - over = { - pin = overrides.pin or spec.pin, - disable = overrides.disable or spec.disable - } - - -- While a package can fail to load we just don't care, it will work itself - -- out. The goal is to make sure every plugin that can load does load, not - -- keep working plugins from loading because an unrelated one doesn't load. - local pkg = package:new(spec, over) - if not pkg then - goto continue - end - - -- we store all the packages in a table so that the caller may keep track of - -- their packages, this is not required and therefore the return value may - -- be discarded - table.insert(packages_from_speclist, pkg) - ::continue:: - end - - return packages_from_speclist -end - ---- reload the package ----@param self package the package to reload ----@param force boolean? force all packages to load -function package:reload(force) - local reloaded = self:loadtree(force) - - if reloaded then - local ok, err - -- TODO: make a benchmark function - bench.mark("reload", function() - ok, err = pcall(vim.cmd, - [[ - silent! helptags ALL - silent! UpdateRemotePlugins - ]]) - end) - - if not ok then - logger:log("error", - "failed to reload helptags and remote plugins; reason: %s", err) - end - end -end - -return package +return { + Package = Package, + PackageStore = PackageStore, +} diff --git a/lua/dep/proc.lua b/lua/dep/proc.lua new file mode 100644 index 0000000..cf3aa9c --- /dev/null +++ b/lua/dep/proc.lua @@ -0,0 +1,74 @@ +local logger = require("dep.log").global +local proc = {} + +function proc.exec(process, args, cwd, env, cb) + local buffer = {} + + local function cb_output(_, data, _) + table.insert(buffer, table.concat(data)) + end + local function cb_exit(job_id, exit_code, _) + local output = table.concat(buffer) + logger:log( + process, + string.format( + 'Job %s ["%s"] finished with exitcode %s\n%s', + job_id, + table.concat(args, '", "'), + exit_code, + output) + ) + cb(exit_code ~= 0, output) + end + table.insert(args, 1, process) + vim.fn.jobstart(args, { + cwd = cwd, + env = env, + stdin = nil, + on_exit = cb_exit, + on_stdout = cb_output, + on_stderr = cb_output, + }) +end + +local git_env = { GIT_TERMINAL_PROMPT = 0 } + +function proc.git_rev_parse(dir, arg, cb) + local args = { "rev-parse", "--short", arg } + + proc.exec("git", args, dir, git_env, cb) +end + +function proc.git_clone(dir, url, branch, cb) + local args = { "clone", "--depth=1", "--recurse-submodules", "--shallow-submodules", url, dir } + + if branch then + args[#args + 1] = "--branch=" .. branch + end + + proc.exec("git", args, nil, git_env, cb) +end + +function proc.git_fetch(dir, remote, refspec, cb) + local args = { "fetch", "--depth=1", "--recurse-submodules", remote, refspec } + + proc.exec("git", args, dir, git_env, cb) +end + +function proc.git_reset(dir, treeish, cb) + local args = { "reset", "--hard", "--recurse-submodules", treeish, "--" } + + proc.exec("git", args, dir, git_env, cb) +end + +function proc.git_checkout(dir, branch, commit, cb) + local args = { "fetch", "--depth=2147483647", "origin", branch } + proc.exec("git", args, dir, git_env, function(err, message) + cb(err, message) + + args = { "checkout", commit } + proc.exec("git", args, dir, git_env, cb) + end) +end + +return proc diff --git a/lua/dep/spec.lua b/lua/dep/spec.lua deleted file mode 100644 index e7b1d9d..0000000 --- a/lua/dep/spec.lua +++ /dev/null @@ -1,212 +0,0 @@ -local logger = require("dep.log") - ----@class specmodules ----@field prefix string prefix to prepend to the modules ----@field [integer] string list of all modules to load - ----@class speclist ----@field modules specmodules? a list of modules ----@field base_dir string? the base directory for all plugins ----@field sync ("new"|"always")? when to sync (defaults to new) ----@field [integer] spec a spec - ----@class spec ----@field [1] string id ----@field setup function? code to run before the package is loaded ----@field load function? code to run after the package is loaded ----@field config function? code to run after the package is installed/updated ----@field lazy function|true? code to run which determines when the package is loaded ----@field as string? overrides the short name of the package which is usually set ---- to a substring of all the chars after "/" in spec[1] ----@field url string? the url to the git repository hosting the package ----@field path string? path to local version of plugin, overrides the url ----@field branch string? the branch which the version of the package resides ----@field commit string? the commit which the version of the package resides ----@field disable boolean? if true disables the package from being loaded ----@field pin boolean? if true disables the package from being installed/updated ----@field reqs spec|spec[]|string? packages that this package requires ----@field deps spec|spec[]|string? packages that depend on this package -local spec = {} - ---- check if a string seems to be a url ----@param url string the "url" to check ----@return boolean is_url -local function is_url(url) - if url:sub(1, 8) == "https://" or - url:sub(1, 7) == "http://" then - return true - end - return false -end - ---- get the proper name of a spec ----@return string spec.name -function spec:get_name() - return self[1]:match("^[%w-_.]+/([%w-_.]+)$") -end - ---- attempt to correct a spec ----@param self table|string spec to check ----@return spec spec -function spec:correct_spec() - if type(self) == "string" then - return { self } - elseif type(self) == "table" then - repeat - if type(self[1]) ~= "string" then - self = self[1] - elseif self[1] == nil then - break - end - until type(self[1]) == "string" - end - - return self -end - --- store the logger temporarily to prevent any logs from being printed when --- being run in silent mode -local __logger - ---- check a spec to see if it's correct ----@param self table spec to check ----@param silent boolean? should the checker report errors ----@return spec|false spec if the spec is ok or false -function spec:check(silent) - if silent == true then - __logger = logger - logger = { log = function() end } - end - - -- make sure all the data is correct - do -- spec[1] - if type(self[1]) ~= "string" then - logger:log("spec", "spec[1] must be a string") - return false - end - - local name = spec.get_name(self) - if not name then - logger:log("spec", 'invalid name "%s"; must be in the format "user/package"', self[1]) - return false - end - end - - if self.setup ~= nil then -- spec.setup - if type(self.setup) ~= "function" then - logger:log("spec", "spec.setup must be a function in %s", self[1]) - return false - end - end - - if self.load ~= nil then -- spec.load - if type(self.load) ~= "function" then - logger:log("spec", "spec.load must be a function in %s", self[1]) - return false - end - end - - if self.config ~= nil then -- spec.config - if type(self.config) ~= "function" then - logger:log("spec", "spec.config must be a function in %s", self[1]) - return false - end - end - - if self.lazy ~= nil then -- spec.lazy - if type(self.lazy) ~= "function" and self.lazy ~= true then - logger:log("spec", "spec.lazy must be a function or boolean in %s", - self[1]) - return false - end - end - - if self.as ~= nil then -- spec.as - if type(self.as) ~= "string" then - logger:log("spec", "spec.as must be a string in %s", self[1]) - return false - end - end - - if self.url ~= nil then -- spec.url - if type(self.url) ~= "string" then - logger:log("spec", "spec.url must be a string in %s", self[1]) - return false - elseif not is_url(self.url) then -- more strict checking on urls - logger:log("spec", "spec.url must be a properly formatted url in %s", - self[1]) - return false - end - end - - if self.path ~= nil then -- spec.path - if type(self.path) ~= "string" then - logger:log("spec", "spec.path must be a string in %s", self[1]) - return false - elseif not vim.fn.isdirectory(self.path) then - logger:log("spec", "spec.path must be a valid directory in %s", self[1]) - return false - end - end - - if self.branch ~= nil then -- spec.branch - if type(self.branch) ~= "string" then - logger:log("spec", "spec.branch must be a string in %s", self[1]) - return false - end - end - - if self.commit ~= nil then -- spec.commit - if type(self.commit) ~= "string" then - logger:log("spec", "spec.commit must be a string in %s", self[1]) - return false - end - end - - if self.disable ~= nil then -- spec.disable - if type(self.disable) ~= "boolean" then - logger:log("spec", "spec.disable must be a boolean in %s", self[1]) - return false - end - end - - if self.pin ~= nil then -- spec.pin - if type(self.pin) ~= "boolean" then - logger:log("spec", "spec.pin must be a boolean in %s", self[1]) - return false - end - end - - if self.reqs ~= nil then -- spec.reqs - local is = type(self.reqs) - if is ~= "table" and is ~= "string" then - logger:log("spec", "spec.reqs must be a table or a string in %s", self[1]) - return false - end - - -- turn an id into a spec - if (is == "string") then - self.reqs = { self.reqs } - end - end - - if self.deps ~= nil then -- spec.deps - local is = type(self.deps) - if is ~= "table" and is ~= "string" then - logger:log("spec", "spec.deps must be a table or a string in %s", self[1]) - return false - end - - -- turn an id into a spec - if (is == "string") then - self.deps = { self.deps } - end - end - - if silent == true then - logger = __logger - end - return self -end - -return spec diff --git a/lua/dep2.lua b/lua/dep2.lua new file mode 100644 index 0000000..a4d4953 --- /dev/null +++ b/lua/dep2.lua @@ -0,0 +1,12 @@ +-- +-- Copyright (c) 2022 chiya.dev +-- +-- Use of this source code is governed by the MIT License +-- which can be found in the LICENSE file and at: +-- +-- https://chiya.dev/licenses/mit.txt +-- +local logger = require("dep.log").global +local store = require("dep.package").PackageStore() + +-- placeholder for refactoring diff --git a/stylua.toml b/stylua.toml new file mode 100755 index 0000000..0435f67 --- /dev/null +++ b/stylua.toml @@ -0,0 +1,2 @@ +indent_type = "Spaces" +indent_width = 2 diff --git a/tests/dep_spec.lua b/tests/dep_spec.lua deleted file mode 100644 index 23ee2a0..0000000 --- a/tests/dep_spec.lua +++ /dev/null @@ -1,36 +0,0 @@ ----@diagnostic disable: undefined-global, undefined-field -local dep_spec_man = require("dep.spec") - -describe("package specification", function() - it("gets the package's name", function() - assert.equal(dep_spec_man.get_name({ "user/package" }), "package") - assert.equal(dep_spec_man.get_name({ "user/package.git" }), "package.git") - end) - - it("ensurses specs are in the proper format", function() - local correct = { "user/package" } - assert.same(dep_spec_man.correct_spec("user/package"), correct) - assert.same(dep_spec_man.correct_spec({ "user/package" }), correct) - assert.same(dep_spec_man.correct_spec({ { "user/package" } }), correct) - end) - - it("checks a spec for correctness", function() - assert.same( - dep_spec_man.check({ "user/package" }, true), - { "user/package" } - ) - - assert.same( - dep_spec_man.check({ - "user/package", - deps = "user/dependency" - }, true), - { - "user/package", - deps = { - "user/dependency" - } - } - ) - end) -end) diff --git a/tests/minit.lua b/tests/minit.lua deleted file mode 100644 index 22d1049..0000000 --- a/tests/minit.lua +++ /dev/null @@ -1 +0,0 @@ -vim.opt.rtp:prepend(".")