diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ea7f5b6..d366984 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,19 +15,19 @@ jobs: runs-on: ubuntu-latest name: lint steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: JohnnyMorganz/stylua-action@v2 with: token: ${{ secrets.GITHUB_TOKEN }} version: latest - args: --check . + args: --check . -g '*.lua' -g '!deps/' documentation: runs-on: ubuntu-latest name: documentation steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 2 @@ -35,7 +35,7 @@ jobs: uses: rhysd/action-setup-vim@v1 with: neovim: true - version: v0.8.3 + version: v0.9.4 - name: generate documentation run: make documentation-ci @@ -51,15 +51,15 @@ jobs: timeout-minutes: 2 strategy: matrix: - neovim_version: ['v0.7.2', 'v0.8.3', 'v0.9.1', 'nightly'] + neovim_version: ['v0.7.2', 'v0.8.3', 'v0.9.4', 'nightly'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: date +%F > todays-date - name: restore cache for today's nightly. - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: _neovim key: ${{ runner.os }}-x64-${{ hashFiles('todays-date') }} @@ -80,7 +80,7 @@ jobs: - tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: google-github-actions/release-please-action@v3 id: release diff --git a/Makefile b/Makefile index 33820ec..d234a56 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,17 @@ test: nvim --version | head -n 1 && echo '' nvim --headless --noplugin -u ./scripts/minimal_init.lua \ -c "lua require('mini.test').setup()" \ - -c "lua MiniTest.run({ execute = { reporter = MiniTest.gen_reporter.stdout({ group_depth = 1 }) } })" + -c "lua MiniTest.run({ execute = { reporter = MiniTest.gen_reporter.stdout({ group_depth = 2 }) } })" + +# runs all the test files on the nightly version, `bob` must be installed. +test-nightly: + bob use nightly + make test + +# runs all the test files on the 0.8.3 version, `bob` must be installed. +test-0.8.3: + bob use 0.8.3 + make test # installs `mini.nvim`, used for both the tests and documentation. deps: @@ -27,7 +37,7 @@ documentation-ci: deps documentation # performs a lint check and fixes issue if possible, following the config in `stylua.toml`. lint: - stylua . + stylua . -g '*.lua' -g '!deps/' # setup setup: diff --git a/README.md b/README.md index 507ec28..b439a5d 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ - Minimal run time, ideal for free plans - Docs with [mini.nvim `doc` plugin](https://github.com/echasnovski/mini.nvim/blob/main/lua/mini/doc.lua) - Tests with [mini.nvim `test` plugin](https://github.com/echasnovski/mini.nvim/blob/main/lua/mini/test.lua) + - Versioned testing with [`bob`](https://github.com/MordechaiHadad/bob) - Linting with [Stylua](https://github.com/JohnnyMorganz/StyLua) ## 📋 Installation @@ -29,6 +30,7 @@ > This section is only required if you wish to use the linter provided by the template. - [Install Stylua linter](https://github.com/JohnnyMorganz/StyLua#installation) +- [Install `bob` neovim version manager](https://github.com/MordechaiHadad/bob) ## ☄ Getting started @@ -55,7 +57,7 @@ gh repo create my-awesome-plugin --template shortcuts/neovim-plugin-boilerplate ### 2 - Replace placeholder names with your plugin name -#### Automatically +#### ✨ Automatically The [setup script](https://github.com/shortcuts/neovim-plugin-boilerplate/blob/main/scripts/setup.sh) will rename files and placeholder names for you. Once done, you can remove anything `setup` related if you want to. @@ -67,12 +69,12 @@ make setup USERNAME=my-github-username PLUGIN_NAME=my-awesome-plugin REPOSITORY_NAME=my-awesome-plugin.nvim make setup ``` -#### Manually +#### ✍️ Manually > **Note**: > The placeholder names are purposely written with different casing. Make sure to keep it. -#### File names +##### File names ```sh rm -rf doc @@ -82,7 +84,7 @@ mv README_TEMPLATE.md README.md ``` -#### Search and replace placeholder occurrences: +##### Search and replace placeholder occurrences: ```vim :vimgrep /YourPluginName/ **/* diff --git a/doc/neovim-plugin-boilerplate.txt b/doc/neovim-plugin-boilerplate.txt index b1045cd..8b687a5 100644 --- a/doc/neovim-plugin-boilerplate.txt +++ b/doc/neovim-plugin-boilerplate.txt @@ -1,3 +1,20 @@ +============================================================================== +------------------------------------------------------------------------------ + *YourPluginName.toggle()* + `YourPluginName.toggle`() +Toggle the plugin by calling the `enable`/`disable` methods respectively. + +------------------------------------------------------------------------------ + *YourPluginName.enable()* + `YourPluginName.enable`() +Initializes the plugin, sets event listeners and internal state. + +------------------------------------------------------------------------------ + *YourPluginName.disable()* + `YourPluginName.disable`() +Disables the plugin, clear highlight groups and autocmds, closes side buffers and resets the internal state. + + ============================================================================== ------------------------------------------------------------------------------ *YourPluginName.options* @@ -18,10 +35,10 @@ Default values: `YourPluginName.setup`({options}) Define your your-plugin-name setup. -Parameters~ +Parameters ~ {options} `(table)` Module config table. See |YourPluginName.options|. -Usage~ +Usage ~ `require("your-plugin-name").setup()` (add `{}` with your |YourPluginName.options| table) diff --git a/doc/tags b/doc/tags index 5a74bec..8b0bb98 100644 --- a/doc/tags +++ b/doc/tags @@ -1,2 +1,5 @@ +YourPluginName.disable() neovim-plugin-boilerplate.txt /*YourPluginName.disable()* +YourPluginName.enable() neovim-plugin-boilerplate.txt /*YourPluginName.enable()* YourPluginName.options neovim-plugin-boilerplate.txt /*YourPluginName.options* YourPluginName.setup() neovim-plugin-boilerplate.txt /*YourPluginName.setup()* +YourPluginName.toggle() neovim-plugin-boilerplate.txt /*YourPluginName.toggle()* diff --git a/lua/your-plugin-name/config.lua b/lua/your-plugin-name/config.lua index c104526..500ec0c 100644 --- a/lua/your-plugin-name/config.lua +++ b/lua/your-plugin-name/config.lua @@ -1,3 +1,5 @@ +local D = require("your-plugin-name.util.debug") + local YourPluginName = {} --- Your plugin configuration with its default values. @@ -9,15 +11,42 @@ YourPluginName.options = { debug = false, } +---@private +local defaults = vim.deepcopy(YourPluginName.options) + +--- Defaults YourPluginName options by merging user provided options with the default plugin values. +--- +---@param options table Module config table. See |YourPluginName.options|. +--- +---@private +function YourPluginName.defaults(options) + local tde = function(t1, t2) + return vim.deepcopy(vim.tbl_deep_extend("keep", t1 or {}, t2 or {})) + end + + YourPluginName.options = tde(options, defaults) + + -- let your user know that they provided a wrong value, this is reported when your plugin is executed. + assert( + type(YourPluginName.options.debug) == "boolean", + "`debug` must be a boolean (`true` or `false`)." + ) + + return YourPluginName.options +end + --- Define your your-plugin-name setup. --- ---@param options table Module config table. See |YourPluginName.options|. --- ---@usage `require("your-plugin-name").setup()` (add `{}` with your |YourPluginName.options| table) function YourPluginName.setup(options) - options = options or {} + YourPluginName.options = YourPluginName.defaults(options or {}) + + -- Useful for later checks that requires nvim 0.9 features at runtime. + YourPluginName.options.hasNvim9 = vim.fn.has("nvim-0.9") == 1 - YourPluginName.options = vim.tbl_deep_extend("keep", options, YourPluginName.options) + D.warnDeprecation(YourPluginName.options) return YourPluginName.options end diff --git a/lua/your-plugin-name/init.lua b/lua/your-plugin-name/init.lua index a1c0151..b1ec9f4 100644 --- a/lua/your-plugin-name/init.lua +++ b/lua/your-plugin-name/init.lua @@ -1,39 +1,34 @@ local M = require("your-plugin-name.main") +local C = require("your-plugin-name.config") + local YourPluginName = {} --- Toggle the plugin by calling the `enable`/`disable` methods respectively. +--- Toggle the plugin by calling the `enable`/`disable` methods respectively. function YourPluginName.toggle() - -- when the config is not set to the global object, we set it if _G.YourPluginName.config == nil then - _G.YourPluginName.config = require("your-plugin-name.config").options + _G.YourPluginName.config = C.options end - _G.YourPluginName.state = M.toggle() + M.toggle("publicAPI_toggle") end --- starts YourPluginName and set internal functions and state. +--- Initializes the plugin, sets event listeners and internal state. function YourPluginName.enable() if _G.YourPluginName.config == nil then - _G.YourPluginName.config = require("your-plugin-name.config").options - end - - local state = M.enable() - - if state ~= nil then - _G.YourPluginName.state = state + _G.YourPluginName.config = C.options end - return state + M.enable("publicAPI_enable") end --- disables YourPluginName and reset internal functions and state. +--- Disables the plugin, clear highlight groups and autocmds, closes side buffers and resets the internal state. function YourPluginName.disable() - _G.YourPluginName.state = M.disable() + M.disable("publicAPI_disable") end -- setup YourPluginName options and merge them with user provided ones. function YourPluginName.setup(opts) - _G.YourPluginName.config = require("your-plugin-name.config").setup(opts) + _G.YourPluginName.config = C.setup(opts) end _G.YourPluginName = YourPluginName diff --git a/lua/your-plugin-name/main.lua b/lua/your-plugin-name/main.lua index 6eba137..5b188c1 100644 --- a/lua/your-plugin-name/main.lua +++ b/lua/your-plugin-name/main.lua @@ -1,49 +1,55 @@ +local S = require("your-plugin-name.state") local D = require("your-plugin-name.util.debug") -- internal methods local YourPluginName = {} --- state -local S = { - -- Boolean determining if the plugin is enabled or not. - enabled = false, -} - ----Toggle the plugin by calling the `enable`/`disable` methods respectively. +-- Toggle the plugin by calling the `enable`/`disable` methods respectively. +-- +--- @param scope string: internal identifier for logging purposes. ---@private -function YourPluginName.toggle() - if S.enabled then - return YourPluginName.disable() +function YourPluginName.toggle(scope) + if S.getEnabled(S) then + return YourPluginName.disable(scope) end - return YourPluginName.enable() + return YourPluginName.enable(scope) end ----Initializes the plugin. +--- Initializes the plugin, sets event listeners and internal state. +--- +--- @param scope string: internal identifier for logging purposes. ---@private -function YourPluginName.enable() - if S.enabled then - return S +function YourPluginName.enable(scope) + if S.getEnabled(S) then + D.log(scope, "Plugin is already enabled.") + + return end - S.enabled = true + -- sets the plugin as `enabled` + S.setEnabled(S) - return S + -- saves the state globally to `_G.YourPluginName.state` + S.save(S) end ----Disables the plugin and reset the internal state. +--- Disables the plugin for the given tab, clear highlight groups and autocmds, closes side buffers and resets the internal state. +--- +--- @param scope string: internal identifier for logging purposes. ---@private -function YourPluginName.disable() - if not S.enabled then - return S +function YourPluginName.disable(scope) + if not S.getEnabled(S) then + D.log(scope, "Plugin is already disabled.") + + return end - -- reset the state - S = { - enabled = false, - } + -- resets the state to its initial value + S.init(S) - return S + -- saves the state globally to `_G.YourPluginName.state` + S.save(S) end return YourPluginName diff --git a/lua/your-plugin-name/state.lua b/lua/your-plugin-name/state.lua new file mode 100644 index 0000000..a80e90e --- /dev/null +++ b/lua/your-plugin-name/state.lua @@ -0,0 +1,36 @@ +local D = require("your-plugin-name.util.debug") + +local State = { enabled = false } + +---Sets the state to its original value. +--- +---@private +function State:init() + self.enabled = false +end + +---Saves the state in the global _G.YourPluginName.state object. +--- +---@private +function State:save() + D.log("state.save", "saving state globally to _G.YourPluginName.state") + + _G.YourPluginName.state = self +end + +---Whether the YourPluginName is enabled or not. +--- +---@private +function State:setEnabled() + self.enabled = true +end + +---Whether the YourPluginName is enabled or not. +--- +---@return boolean: the `enabled` state value. +---@private +function State:getEnabled() + return self.enabled +end + +return State diff --git a/lua/your-plugin-name/util/debug.lua b/lua/your-plugin-name/util/debug.lua index b550553..72065e7 100644 --- a/lua/your-plugin-name/util/debug.lua +++ b/lua/your-plugin-name/util/debug.lua @@ -11,50 +11,45 @@ function D.log(scope, str, ...) return end - local info = debug.getinfo(2, "Sl") - local line = "" - - if info then - line = "L" .. info.currentline - end - print( string.format( - "[your-plugin-name:%s %s in %s] > %s", - os.date("%H:%M:%S"), - line, + "[your-plugin-name@%s in '%s'] > %s", + os.date("%X"), scope, string.format(str, ...) ) ) end ----prints the table if debug is true. +---analyzes the user provided `setup` parameters and sends a message if they use a deprecated option, then gives the new option to use. --- ----@param table table: the table to print. ----@param indent number?: the default indent value, starts at 0. +---@param options table: the options provided by the user. ---@private -function D.tprint(table, indent) - if _G.YourPluginName.config ~= nil and not _G.YourPluginName.config.debug then - return - end - - if not indent then - indent = 0 +function D.warnDeprecation(options) + local usesDeprecatedOption = false + + local notice = "is now deprecated, use `%s` instead." + local rootDeprecated = { + foo = "bar", + bar = "baz", + } + + for name, warning in pairs(rootDeprecated) do + if options[name] ~= nil then + usesDeprecatedOption = true + print( + string.format( + "[your-plugin-name.nvim] `%s` %s", + name, + string.format(notice, warning) + ) + ) + end end - for k, v in pairs(table) do - local formatting = string.rep(" ", indent) .. k .. ": " - if type(v) == "table" then - print(formatting) - D.tprint(v, indent + 1) - elseif type(v) == "boolean" then - print(formatting .. tostring(v)) - elseif type(v) == "function" then - print(formatting .. "FUNCTION") - else - print(formatting .. v) - end + if usesDeprecatedOption then + print("[your-plugin-name.nvim] sorry to bother you with the breaking changes :(") + print("[your-plugin-name.nvim] use `:h YourPluginName.options` to read more.") end end diff --git a/plugin/your-plugin-name.lua b/plugin/your-plugin-name.lua index 2860542..d1f1355 100644 --- a/plugin/your-plugin-name.lua +++ b/plugin/your-plugin-name.lua @@ -5,6 +5,11 @@ end _G.YourPluginNameLoaded = true -vim.api.nvim_create_user_command("YourPluginName", function() - require("your-plugin-name").toggle() -end, {}) +-- Useful if you want your plugin to be compatible with older (<0.7) neovim versions +if vim.fn.has("nvim-0.7") == 0 then + vim.cmd("command! YourPluginName lua require('your-plugin-name').toggle()") +else + vim.api.nvim_create_user_command("YourPluginName", function() + require("your-plugin-name").toggle() + end, {}) +end diff --git a/tests/helpers.lua b/tests/helpers.lua index f30346c..57526d1 100644 --- a/tests/helpers.lua +++ b/tests/helpers.lua @@ -1,78 +1,87 @@ --- partially imported from https://github.com/echasnovski/mini.nvim +-- imported from https://github.com/echasnovski/mini.nvim local Helpers = {} -- Add extra expectations Helpers.expect = vim.deepcopy(MiniTest.expect) --- The error message returned when a test fails. +function Helpers.toggle(child) + child.cmd("YourPluginName") + Helpers.wait(child) +end + +function Helpers.wait(child) + child.loop.sleep(10) +end + +function Helpers.currentWin(child) + return child.lua_get("vim.api.nvim_get_current_win()") +end + +function Helpers.winsInTab(child, tab) + tab = tab or "_G.YourPluginName.state.activeTab" + + return child.lua_get("vim.api.nvim_tabpage_list_wins(" .. tab .. ")") +end + +function Helpers.listBuffers(child) + return child.lua_get("vim.api.nvim_list_bufs()") +end + local function errorMessage(str, pattern) return string.format("Pattern: %s\nObserved string: %s", vim.inspect(pattern), str) end --- Check equality of a global `field` against `value` in the given `child` process. --- @usage global_equality(child, "_G.YourPluginNameLoaded", true) -Helpers.expect.global_equality = MiniTest.new_expectation( +Helpers.expect.buf_width = MiniTest.new_expectation( "variable in child process matches", function(child, field, value) - return Helpers.expect.equality(child.lua_get(field), value) + return Helpers.expect.equality( + child.lua_get("vim.api.nvim_win_get_width(_G.YourPluginName.state." .. field .. ")"), + value + ) end, errorMessage ) --- Check type equality of a global `field` against `value` in the given `child` process. --- @usage global_type_equality(child, "_G.YourPluginNameLoaded", "boolean") -Helpers.expect.global_type_equality = MiniTest.new_expectation( - "variable type in child process matches", +Helpers.expect.global = MiniTest.new_expectation( + "variable in child process matches", function(child, field, value) - return Helpers.expect.global_equality(child, "type(" .. field .. ")", value) + return Helpers.expect.equality(child.lua_get(field), value) end, errorMessage ) --- Check equality of a config `field` against `value` in the given `child` process. --- @usage option_equality(child, "debug", true) -Helpers.expect.config_equality = MiniTest.new_expectation( - "config option matches", +Helpers.expect.global_type = MiniTest.new_expectation( + "variable type in child process matches", function(child, field, value) - return Helpers.expect.global_equality(child, "_G.YourPluginName.config." .. field, value) + return Helpers.expect.global(child, "type(" .. field .. ")", value) end, errorMessage ) --- Check type equality of a config `field` against `value` in the given `child` process. --- @usage config_type_equality(child, "debug", "boolean") -Helpers.expect.config_type_equality = MiniTest.new_expectation( - "config option type matches", +Helpers.expect.config = MiniTest.new_expectation( + "config option matches", function(child, field, value) - return Helpers.expect.global_equality( - child, - "type(_G.YourPluginName.config." .. field .. ")", - value - ) + return Helpers.expect.global(child, "_G.YourPluginName.config." .. field, value) end, errorMessage ) --- Check equality of a state `field` against `value` in the given `child` process. --- @usage state_equality(child, "enabled", true) -Helpers.expect.state_equality = MiniTest.new_expectation( - "state matches", +Helpers.expect.config_type = MiniTest.new_expectation( + "config option type matches", function(child, field, value) - return Helpers.expect.global_equality(child, "_G.YourPluginName.enabled." .. field, value) + return Helpers.expect.global(child, "type(_G.YourPluginName.config." .. field .. ")", value) end, errorMessage ) --- Check type equality of a state `field` against `value` in the given `child` process. --- @usage state_type_equality(child, "enabled", "boolean") -Helpers.expect.state_type_equality = MiniTest.new_expectation( +Helpers.expect.state = MiniTest.new_expectation("state matches", function(child, field, value) + return Helpers.expect.global(child, "_G.YourPluginName.state." .. field, value) +end, errorMessage) + +Helpers.expect.state_type = MiniTest.new_expectation( "state type matches", function(child, field, value) - return Helpers.expect.global_equality( - child, - "type(_G.YourPluginName.state." .. field .. ")", - value - ) + return Helpers.expect.global(child, "type(_G.YourPluginName.state." .. field .. ")", value) end, errorMessage ) @@ -90,8 +99,9 @@ Helpers.new_child_neovim = function() local child = MiniTest.new_child_neovim() local prevent_hanging = function(method) - -- stylua: ignore - if not child.is_blocked() then return end + if not child.is_blocked() then + return + end local msg = string.format("Can not use `child.%s` because child process is blocked.", method) @@ -152,6 +162,23 @@ Helpers.new_child_neovim = function() return { child.o.lines, child.o.columns } end + --- Assert visual marks + --- + --- Useful to validate visual selection + --- + ---@param first number|table Table with start position or number to check linewise. + ---@param last number|table Table with finish position or number to check linewise. + ---@private + child.expect_visual_marks = function(first, last) + child.ensure_normal_mode() + + first = type(first) == "number" and { first, 0 } or first + last = type(last) == "number" and { last, 2147483647 } or last + + MiniTest.expect.equality(child.api.nvim_buf_get_mark(0, "<"), first) + MiniTest.expect.equality(child.api.nvim_buf_get_mark(0, ">"), last) + end + child.expect_screenshot = function(opts, path, screenshot_opts) if child.fn.has("nvim-0.8") == 0 then MiniTest.skip("Screenshots are tested for Neovim>=0.8 (for simplicity).") diff --git a/tests/test_API.lua b/tests/test_API.lua index c2f6172..949ef59 100644 --- a/tests/test_API.lua +++ b/tests/test_API.lua @@ -1,14 +1,8 @@ -local helpers = dofile("tests/helpers.lua") +local Helpers = dofile("tests/helpers.lua") -- See https://github.com/echasnovski/mini.nvim/blob/main/lua/mini/test.lua for more documentation -local child = helpers.new_child_neovim() -local eq_global, eq_config, eq_state = - helpers.expect.global_equality, helpers.expect.config_equality, helpers.expect.state_equality -local eq_type_global, eq_type_config, eq_type_state = - helpers.expect.global_type_equality, - helpers.expect.config_type_equality, - helpers.expect.state_type_equality +local child = Helpers.new_child_neovim() local T = MiniTest.new_set({ hooks = { @@ -29,19 +23,19 @@ T["setup()"]["sets exposed methods and default options value"] = function() child.lua([[require('your-plugin-name').setup()]]) -- global object that holds your plugin information - eq_type_global(child, "_G.YourPluginName", "table") + Helpers.expect.global_type(child, "_G.YourPluginName", "table") -- public methods - eq_type_global(child, "_G.YourPluginName.toggle", "function") - eq_type_global(child, "_G.YourPluginName.disable", "function") - eq_type_global(child, "_G.YourPluginName.enable", "function") + Helpers.expect.global_type(child, "_G.YourPluginName.toggle", "function") + Helpers.expect.global_type(child, "_G.YourPluginName.disable", "function") + Helpers.expect.global_type(child, "_G.YourPluginName.enable", "function") -- config - eq_type_global(child, "_G.YourPluginName.config", "table") + Helpers.expect.global_type(child, "_G.YourPluginName.config", "table") -- assert the value, and the type - eq_config(child, "debug", false) - eq_type_config(child, "debug", "boolean") + Helpers.expect.config(child, "debug", false) + Helpers.expect.config_type(child, "debug", "boolean") end T["setup()"]["overrides default values"] = function() @@ -51,8 +45,8 @@ T["setup()"]["overrides default values"] = function() })]]) -- assert the value, and the type - eq_config(child, "debug", true) - eq_type_config(child, "debug", "boolean") + Helpers.expect.config(child, "debug", true) + Helpers.expect.config_type(child, "debug", "boolean") end return T