diff --git a/.github/ISSUE_TEMPLATE/ask_a_question.yml b/.github/ISSUE_TEMPLATE/ask_a_question.yml index e1fdd6e..5d3b52b 100644 --- a/.github/ISSUE_TEMPLATE/ask_a_question.yml +++ b/.github/ISSUE_TEMPLATE/ask_a_question.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - name: Ask a question - url: https://github.com/ColinKennedy/nvim-best-practices-plugin-template/discussions + url: https://github.com/ColinKennedy/cursor-text-objects.nvim/discussions about: Use Github discussions instead diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5367888..aaac485 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -6,20 +6,20 @@ body: - type: markdown attributes: value: | - **Before** reporting an issue, make sure to read the [documentation](https://github.com/ColinKennedy/nvim-best-practices-plugin-template) - and search [existing issues](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/issues) (even the [closed issues](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/issues?q=is%3Aissue+is%3Aclosed)) + **Before** reporting an issue, make sure to read the [documentation](https://github.com/ColinKennedy/cursor-text-objects.nvim) + and search [existing issues](https://github.com/ColinKennedy/cursor-text-objects.nvim/issues) (even the [closed issues](https://github.com/ColinKennedy/cursor-text-objects.nvim/issues?q=is%3Aissue+is%3Aclosed)) - Usage questions such as ***"How do I...?"*** belong in [Discussions](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/discussions) and will be closed. + Usage questions such as ***"How do I...?"*** belong in [Discussions](https://github.com/ColinKennedy/cursor-text-objects.nvim/discussions) and will be closed. - type: checkboxes attributes: label: Did you read the documentation and check existing issues? description: Make sure you checked and all of the below before submitting an issue options: - - label: I have read all the [`:help plugin-template`](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/blob/main/doc/my-template-docs.txt) documentation + - label: I have read all the [`:help cursor-text-objects`](https://github.com/ColinKennedy/cursor-text-objects.nvim/blob/main/doc/my-template-docs.txt) documentation required: true - label: I have updated the plugin to the latest version before submitting this issue required: true - - label: I have searched the [existing issues](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/issues) and [closed issues](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/issues?q=is%3Aissue+is%3Aclosed) issues + - label: I have searched the [existing issues](https://github.com/ColinKennedy/cursor-text-objects.nvim/issues) and [closed issues](https://github.com/ColinKennedy/cursor-text-objects.nvim/issues?q=is%3Aissue+is%3Aclosed) issues required: true - type: input attributes: @@ -58,12 +58,12 @@ body: - type: textarea attributes: label: Health - description: Attach the output of `:checkhealth plugin_template` here + description: Attach the output of `:checkhealth cursor_text_objects` here render: log - type: textarea attributes: label: Log - description: Please enable logging with `vim.g.plugin_template_configuration = {logging = {level = "debug", use_file = true}}` and attach the contents of `~/.local/share/nvim` here or call `:PluginTemplate copy-logs` + description: Please enable logging with `vim.g.cursor_text_objects_configuration = {logging = {level = "debug", use_file = true}}` and attach the contents of `~/.local/share/nvim` here or call `:CursorTextObjects copy-logs` render: log - type: textarea attributes: @@ -77,7 +77,7 @@ body: spec = { { -- Add anything you need here (configuration, other plugins, etc) - "ColinKennedy/nvim-best-practices-plugin-template", + "ColinKennedy/cursor-text-objects.nvim", }, }) render: lua diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 067611b..807c670 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -18,15 +18,10 @@ jobs: neovim: true version: stable - - name: Create API Documentation - run: | - nvim --version - make api_documentation - - name: Create User Documentation uses: kdheepak/panvimdoc@main with: - vimdoc: plugin-template + vimdoc: cursor-text-objects version: "Neovim >= 0.8.0" demojify: true treesitter: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 79ae824..9e3246e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: - name: build run: | - luarocks test plugin-template-scm-1.rockspec --prepare + luarocks test cursor-text-objects-scm-1.rockspec --prepare - name: test run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 11b7ac3..825c32f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,36 +1 @@ # Changelog - -## [1.3.2](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/compare/v1.3.1...v1.3.2) (2024-11-12) - - -### Bug Fixes - -* **luals:** Added missing diagnostics paths ([d5f93ef](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/commit/d5f93ef89c47ae5dd09c684526f7050a0f829e11)) - -## [1.3.1](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/compare/v1.3.0...v1.3.1) (2024-10-26) - - -### Bug Fixes - -* **urlchecker:** Added README.md + CHANGELOG.md as a checker ([a4b7d41](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/commit/a4b7d410f4d853d7bf98e4ca6dc198f6ea29bb8d)) - -## [1.3.0](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/compare/v1.2.0...v1.3.0) (2024-10-26) - - -### Features - -* **ci:** Added urlchecker.yml ([764f485](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/commit/764f4859522c6c810e75bd82eda6073ef4fc0c0c)) - -## [1.2.0](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/compare/v1.1.0...v1.2.0) (2024-10-26) - - -### Features - -* **ci:** Enabled llscheck.yml ([18273bf](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/commit/18273bf3526364ca05d2798318b86f59a3c124e8)) - -## [1.1.0](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/compare/v1.0.3...v1.1.0) (2024-10-26) - - -### Features - -* **ci:** Added better GitHub workflows ([da48f5a](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/commit/da48f5a27fb01e9c597d82931e551d10c31b94d0)) diff --git a/Makefile b/Makefile index 564e785..dab38aa 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,5 @@ .PHONY: api_documentation llscheck luacheck stylua test -api_documentation: - nvim -u scripts/make_api_documentation/minimal_init.lua -l scripts/make_api_documentation/main.lua - llscheck: VIMRUNTIME=`nvim -l scripts/print_vimruntime_environment_variable.lua` llscheck --configpath .luarc.json . @@ -13,5 +10,4 @@ stylua: stylua lua plugin scripts spec test: - eval $(luarocks path --lua-version 5.1 --bin) busted --helper spec/minimal_init.lua . diff --git a/README.md b/README.md index f18ed06..8bc0576 100644 --- a/README.md +++ b/README.md @@ -1,210 +1,83 @@ -# A Neovim Plugin Template +# cursor-text-objects.nvim | | | |--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Build Status | [![unittests](https://img.shields.io/github/actions/workflow/status/ColinKennedy/nvim-best-practices-plugin-template/test.yml?branch=main&style=for-the-badge&label=Unittests)](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/actions/workflows/test.yml) [![documentation](https://img.shields.io/github/actions/workflow/status/ColinKennedy/nvim-best-practices-plugin-template/documentation.yml?branch=main&style=for-the-badge&label=Documentation)](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/actions/workflows/documentation.yml) [![luacheck](https://img.shields.io/github/actions/workflow/status/ColinKennedy/nvim-best-practices-plugin-template/luacheck.yml?branch=main&style=for-the-badge&label=Luacheck)](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/actions/workflows/luacheck.yml) [![llscheck](https://img.shields.io/github/actions/workflow/status/ColinKennedy/nvim-best-practices-plugin-template/llscheck.yml?branch=main&style=for-the-badge&label=llscheck)](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/actions/workflows/llscheck.yml) [![stylua](https://img.shields.io/github/actions/workflow/status/ColinKennedy/nvim-best-practices-plugin-template/stylua.yml?branch=main&style=for-the-badge&label=Stylua)](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/actions/workflows/stylua.yml) [![urlchecker](https://img.shields.io/github/actions/workflow/status/ColinKennedy/nvim-best-practices-plugin-template/urlchecker.yml?branch=main&style=for-the-badge&label=URLChecker)](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/actions/workflows/urlchecker.yml) | -| License | [![License-MIT](https://img.shields.io/badge/License-MIT-blue?style=for-the-badge)](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/blob/main/LICENSE) | -| Social | [![RSS](https://img.shields.io/badge/rss-F88900?style=for-the-badge&logo=rss&logoColor=white)](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/commits/main/doc/news.txt.atom) | - -A template repository used to create Neovim plugins. - - -# Features -- Follows [nvim-best-practices](https://github.com/nvim-neorocks/nvim-best-practices) -- Fast start-up (~1 ms) -- Auto-release to [luarocks](https://luarocks.org) & [GitHub](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/releases) -- Automated user documentation (using [panvimdoc](https://github.com/kdheepak/panvimdoc)) -- Automated API documentation (using [mini.doc](https://github.com/echasnovski/mini.doc)) -- Vimtags generation -- Built-in Vim commands -- A high quality command mode parser -- Auto-completes your commands at any cursor position -- No external dependencies[*](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/wiki/External-Dependencies-Disclaimer) -- [LuaCATS](https://luals.github.io/wiki/annotations/) annotations and type-hints, everywhere -- [RSS feed support](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/commits/main/doc/news.txt.atom) -- Built-in logging to stdout / files -- Unittests use the full power of native [busted](https://github.com/lunarmodules/busted) -- Automated testing matrix supports 6 Neovim/OS combinations - - neovim: `[v0.10.0, stable, nightly]` - - os: `[ubuntu-latest, macos-latest]` -- 100% Lua -- Uses [Semantic Versioning](https://semver.org) -- Integrations - - [lualine.nvim](https://github.com/nvim-lualine/lualine.nvim) - - [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) - - `:checkhealth` -- Github actions for: - - [StyLua](https://github.com/JohnnyMorganz/StyLua) - Auto-formats Lua code - - [llscheck](https://github.com/jeffzi/llscheck) - Checks for Lua type mismatches - - [luacheck](https://github.com/mpeterv/luacheck) - Checks for Lua code issues - - [luarocks](https://luarocks.org) auto-release ([LUAROCKS_API_KEY secret](https://github.com/nvim-neorocks/sample-luarocks-plugin?tab=readme-ov-file#publishing-to-luarocks) configuration required) - - [GitHub](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/releases) auto-release ([PERSONAL_ACCESS_TOKEN secret](https://github.com/nvim-neorocks/sample-luarocks-plugin?tab=readme-ov-file#installing-release-please-recommended) configuration required) - - [mini.doc](https://github.com/echasnovski/mini.doc) - API documentation auto-generator - - [panvimdoc](https://github.com/kdheepak/panvimdoc) - User documentation auto-generator - - [urlchecker](https://github.com/urlstechie/urlchecker-action) - Checks for broken URL links - - PR reviews - Reminds users to update `doc/news.txt` - - -# Using This Template -1. Follow the [Wiki instructions](https://github.com/ColinKennedy/nvim-best-practices-plugin-template/wiki/Using-This-Template) -2. Once you're done, remove this section (the rest of this README.md file should be kept / customized to your needs) +| Build Status | [![unittests](https://img.shields.io/github/actions/workflow/status/ColinKennedy/cursor-text-objects.nvim/test.yml?branch=main&style=for-the-badge&label=Unittests)](https://github.com/ColinKennedy/cursor-text-objects.nvim/actions/workflows/test.yml) [![documentation](https://img.shields.io/github/actions/workflow/status/ColinKennedy/cursor-text-objects.nvim/documentation.yml?branch=main&style=for-the-badge&label=Documentation)](https://github.com/ColinKennedy/cursor-text-objects.nvim/actions/workflows/documentation.yml) [![luacheck](https://img.shields.io/github/actions/workflow/status/ColinKennedy/cursor-text-objects.nvim/luacheck.yml?branch=main&style=for-the-badge&label=Luacheck)](https://github.com/ColinKennedy/cursor-text-objects.nvim/actions/workflows/luacheck.yml) [![llscheck](https://img.shields.io/github/actions/workflow/status/ColinKennedy/cursor-text-objects.nvim/llscheck.yml?branch=main&style=for-the-badge&label=llscheck)](https://github.com/ColinKennedy/cursor-text-objects.nvim/actions/workflows/llscheck.yml) [![stylua](https://img.shields.io/github/actions/workflow/status/ColinKennedy/cursor-text-objects.nvim/stylua.yml?branch=main&style=for-the-badge&label=Stylua)](https://github.com/ColinKennedy/cursor-text-objects.nvim/actions/workflows/stylua.yml) [![urlchecker](https://img.shields.io/github/actions/workflow/status/ColinKennedy/cursor-text-objects.nvim/urlchecker.yml?branch=main&style=for-the-badge&label=URLChecker)](https://github.com/ColinKennedy/cursor-text-objects.nvim/actions/workflows/urlchecker.yml) | +| License | [![License-MIT](https://img.shields.io/badge/License-MIT-blue?style=for-the-badge)](https://github.com/ColinKennedy/cursor-text-objects.nvim/blob/main/LICENSE) | +| Social | [![RSS](https://img.shields.io/badge/rss-F88900?style=for-the-badge&logo=rss&logoColor=white)](https://github.com/ColinKennedy/cursor-text-objects.nvim/commits/main/doc/news.txt.atom) | -# Installation - -- [lazy.nvim](https://github.com/folke/lazy.nvim) -```lua -{ - "ColinKennedy/nvim-best-practices-plugin-template", - -- TODO: (you) - Make sure your first release matches v1.0.0 so it auto-releases! - version = "v1.*", -} -``` +# How To Use +## Summary +In short, mappings like `dap`, which delete a whole paragraph, now can use +`d[ap` which means "delete from the start of the paragraph to your current +cursor position" and `d]ap` means "delete from the current cursor position to +the end of the paragraph". +It works with any text operator or text object pair and integrates with existing plugins. -# Configuration -(These are default values) - - -- [lazy.nvim](https://github.com/folke/lazy.nvim) -```lua -{ - "ColinKennedy/nvim-best-practices-plugin-template", - config = function() - vim.g.plugin_template_configuration = { - cmdparse = { - auto_complete = { display = { help_flag = true } }, - }, - commands = { - goodnight_moon = { read = { phrase = "A good book" } }, - hello_world = { - say = { ["repeat"] = 1, style = "lowercase" }, - }, - }, - logging = { - level = "info", - use_console = false, - use_file = false, - }, - tools = { - lualine = { - arbitrary_thing = { - color = "Visual", - text = " Arbitrary Thing", - }, - copy_logs = { - color = "Comment", - text = "󰈔 Copy Logs", - }, - goodnight_moon = { - color = "Question", - text = " Goodnight moon", - }, - hello_world = { - color = "Title", - text = " Hello, World!", - }, - }, - telescope = { - goodnight_moon = { - { "Foo Book", "Author A" }, - { "Bar Book Title", "John Doe" }, - { "Fizz Drink", "Some Name" }, - { "Buzz Bee", "Cool Person" }, - }, - hello_world = { "Hi there!", "Hello, Sailor!", "What's up, doc?" }, - }, - }, - } - end -} -``` +## Details +For every text object and text operator that Vim has, you can now "include" +your cursor as a part of the command. +Here's a practical example. Have you ever had a paragraph of text like this. -## Lualine - - - -> Note: You can customize lualine colors here or using -> `vim.g.plugin_template_configuration`. - -[lualine.nvim](https://github.com/nvim-lualine/lualine.nvim) -```lua -require("lualine").setup { - sections = { - lualine_y = { - -- ... Your other configuration ... - { - "plugin_template", - -- NOTE: These will override default values - -- display = { - -- goodnight_moon = {color={fg="#FFFFFF"}, text="Custom message 1"}}, - -- hello_world = {color={fg="#333333"}, text="Custom message 2"}, - -- }, - }, - } - } -} ``` +Some text with many lines. And something +that wraps multiple|cursor here| lines in a single sentence. +Lorem ipsum and all that. +More paragraph text +``` -## Telescope +And you'd like delete just from your current `|cursor|` to the bottom of the +paragraph? You can't use `dap`, that deletes the whole paragraph. With +cursor-text-objects.nvim, you can use `d]ap` which means "delete from the +current cursor position around the end of the paragraph". And you can delete to +the start of the paragraph with `d[ap`. If you want to delete to the start of +the sentence, use `d[is`. - +Again, **any text operator or text object** command that you can think of now +works with your cursor. Here's more examples. -> Note: You can customize telescope colors here or using -> `vim.g.plugin_template_configuration`. +- `d[a}` - Delete around the start of a {}-pair to the cursor. +- `d]a}` - Delete around the cursor to the end of a {}-pair. +- `gc[ip` - Comment from the start of the paragraph to the cursor. +- `gc]ip` - Comment from the cursor to the end of the paragraph. +- `gw[ip` - Format from the start of the paragraph to the cursor. +- `gw]ip` - Format from the cursor to the end of the paragraph. +- `v[ip` - Select from the start of the paragraph to the cursor. +- `v]ip` - Select from the cursor to the end of the paragraph. +- `y[ib` - Yank inside start of a ()-pair to the cursor. +- `y]ib` - Yank inside the cursor to the end of a ()-pair. -[telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) -```lua -{ - "nvim-telescope/telescope.nvim", - cmd = "Telescope", - config = function() - -- ... Your other configuration ... - require("telescope").load_extension("plugin_template") - end, - dependencies = { - "ColinKennedy/nvim-best-practices-plugin-template", - "nvim-lua/plenary.nvim", - }, - version = "0.1.*", -}, -``` +It works with custom operators and objects too! +Using [nvim-treesitter-textobjects](https://github.com/nvim-treesitter/nvim-treesitter-textobjects) -### Colors -This plugin provides two default highlights - -- `PluginTemplateTelescopeEntry` -- `PluginTemplateTelescopeSecondary` - -Both come with default colors that should look nice. If you want to change them, here's how: -```lua -vim.api.nvim_set_hl(0, "PluginTemplateTelescopeEntry", {link="Statement"}) -vim.api.nvim_set_hl(0, "PluginTemplateTelescopeSecondary", {link="Question"}) -``` +- `v]ic` - Select lines from the cursor to the end of a class. +- `v[ic` - Select lines from the start of a class to the cursor. +- `v]if` - Select lines from the cursor to the end of a function. +- `v[if` - Select lines from the start of a function to the cursor. +Using [vim-textobj-indent](https://github.com/kana/vim-textobj-indent) -# Commands -Here are some example commands: +- `c[ii` - Change from start of the indented-lines to the cursor. +- `c]ii` - Change from the cursor to the end of the indented-lines. - - +etc. etc. etc. -```vim -" A typical subcommand -:PluginTemplate hello-world say phrase "Hello, World!" " How are you?" -:PluginTemplate hello-world say phrase "Hello, World!" --repeat=2 --style=lowercase +Give your right-pinky a workout and install `cursor-text-objects.nvim` today! -" An example of a flag this repeatable and 3 flags, -a, -b, -c, as one dash -:PluginTemplate arbitrary-thing -vvv -abc -f -" Separate commands with completely separate, flexible APIs -:PluginTemplate goodnight-moon count-sheep 42 -:PluginTemplate goodnight-moon read "a book" -:PluginTemplate goodnight-moon sleep -z -z -z +# Installation +- [lazy.nvim](https://github.com/folke/lazy.nvim) +```lua +{ + "ColinKennedy/cursor-text-objects.nvim", + version = "v1.*", +} ``` @@ -232,16 +105,18 @@ Run test based on tags busted --helper spec/minimal_init.lua . --tags=simple ``` + # Tracking Updates See [doc/news.txt](doc/news.txt) for updates. You can watch this plugin for changes by adding this URL to your RSS feed: ``` -https://github.com/ColinKennedy/nvim-best-practices-plugin-template/commits/main/doc/news.txt.atom +https://github.com/ColinKennedy/cursor-text-objects.nvim/commits/main/doc/news.txt.atom ``` + # Other Plugins This template is full of various features. But if your plugin is only meant to be a simple plugin and you don't want the bells and whistles that this template provides, consider instead using -[nvim-plugin-template](https://github.com/ellisonleao/nvim-plugin-template) +[nvim-cursor-text-object](https://github.com/ellisonleao/nvim-plugin-template) diff --git a/plugin-template-scm-1.rockspec b/cursor-text-objects-scm-1.rockspec similarity index 95% rename from plugin-template-scm-1.rockspec rename to cursor-text-objects-scm-1.rockspec index fee7db5..3b4140c 100644 --- a/plugin-template-scm-1.rockspec +++ b/cursor-text-objects-scm-1.rockspec @@ -1,5 +1,5 @@ rockspec_format = "3.0" -package = "plugin-template" +package = "cursor-text-objects" version = "scm-1" local user = "ColinKennedy" diff --git a/doc/plugin-template.txt b/doc/cursor-text-objects.txt similarity index 59% rename from doc/plugin-template.txt rename to doc/cursor-text-objects.txt index 20f8f4b..aff7c48 100644 --- a/doc/plugin-template.txt +++ b/doc/cursor-text-objects.txt @@ -1,24 +1,24 @@ -*plugin-template.txt* For Neovim >= 0.8.0 Last change: 2024 November 13 +*cursor-text-objects.txt* For Neovim >= 0.8.0 Last change: 2024 November 13 ============================================================================== -Table of Contents *plugin-template-table-of-contents* - -1. A Neovim Plugin Template |plugin-template-a-neovim-plugin-template| -2. Features |plugin-template-features| -3. Using This Template |plugin-template-using-this-template| -4. Installation |plugin-template-installation| -5. Configuration |plugin-template-configuration| - - Lualine |plugin-template-configuration-lualine| - - Telescope |plugin-template-configuration-telescope| -6. Commands |plugin-template-commands| -7. Tests |plugin-template-tests| - - Initialization |plugin-template-tests-initialization| - - Running |plugin-template-tests-running| -8. Tracking Updates |plugin-template-tracking-updates| -9. Other Plugins |plugin-template-other-plugins| +Table of Contents *cursor-text-objects-table-of-contents* + +1. A Neovim Plugin Template |cursor-text-objects-a-neovim-plugin-template| +2. Features |cursor-text-objects-features| +3. Using This Template |cursor-text-objects-using-this-template| +4. Installation |cursor-text-objects-installation| +5. Configuration |cursor-text-objects-configuration| + - Lualine |cursor-text-objects-configuration-lualine| + - Telescope |cursor-text-objects-configuration-telescope| +6. Commands |cursor-text-objects-commands| +7. Tests |cursor-text-objects-tests| + - Initialization |cursor-text-objects-tests-initialization| + - Running |cursor-text-objects-tests-running| +8. Tracking Updates |cursor-text-objects-tracking-updates| +9. Other Plugins |cursor-text-objects-other-plugins| ============================================================================== -1. A Neovim Plugin Template *plugin-template-a-neovim-plugin-template* +1. A Neovim Plugin Template *cursor-text-objects-a-neovim-plugin-template* -------------------------------------------------------------------------------- @@ -34,20 +34,17 @@ A template repository used to create Neovim plugins. ============================================================================== -2. Features *plugin-template-features* +2. Features *cursor-text-objects-features* - Follows nvim-best-practices - Fast start-up (~1 ms) -- Auto-release to luarocks & GitHub - Automated user documentation (using panvimdoc ) - Automated API documentation (using mini.doc ) - Vimtags generation - Built-in Vim commands - A high quality command mode parser - Auto-completes your commands at any cursor position -- No external dependencies - LuaCATS annotations and type-hints, everywhere -- RSS feed support - Built-in logging to stdout / files - Unittests use the full power of native busted - Automated testing matrix supports 6 Neovim/OS combinations @@ -64,28 +61,25 @@ A template repository used to create Neovim plugins. - llscheck - Checks for Lua type mismatches - luacheck - Checks for Lua code issues - luarocks auto-release (LUAROCKS_API_KEY secret configuration required) - - GitHub auto-release (PERSONAL_ACCESS_TOKEN secret configuration required) - mini.doc - API documentation auto-generator - panvimdoc - User documentation auto-generator - urlchecker - Checks for broken URL links - - PR reviews - Reminds users to update `doc/news.txt` ============================================================================== -3. Using This Template *plugin-template-using-this-template* +3. Using This Template *cursor-text-objects-using-this-template* -1. Follow the Wiki instructions 2. Once you’re done, remove this section (the rest of this README.md file should be kept / customized to your needs) ============================================================================== -4. Installation *plugin-template-installation* +4. Installation *cursor-text-objects-installation* - lazy.nvim >lua { - "ColinKennedy/nvim-best-practices-plugin-template", + "ColinKennedy/cursor-text-objects.nvim", -- TODO: (you) - Make sure your first release matches v1.0.0 so it auto-releases! version = "v1.*", } @@ -93,7 +87,7 @@ A template repository used to create Neovim plugins. ============================================================================== -5. Configuration *plugin-template-configuration* +5. Configuration *cursor-text-objects-configuration* (These are default values) @@ -101,9 +95,9 @@ A template repository used to create Neovim plugins. >lua { - "ColinKennedy/nvim-best-practices-plugin-template", + "ColinKennedy/cursor-text-objects.nvim", config = function() - vim.g.plugin_template_configuration = { + vim.g.cursor_text_objects_configuration = { cmdparse = { auto_complete = { display = { help_flag = true } }, }, @@ -153,11 +147,11 @@ A template repository used to create Neovim plugins. < -LUALINE *plugin-template-configuration-lualine* +LUALINE *cursor-text-objects-configuration-lualine* Note: You can customize lualine colors here or using - `vim.g.plugin_template_configuration`. + `vim.g.cursor_text_objects_configuration`. lualine.nvim >lua @@ -166,7 +160,7 @@ lualine.nvim lualine_y = { -- ... Your other configuration ... { - "plugin_template", + "cursor_text_objects", -- NOTE: These will override default values -- display = { -- goodnight_moon = {color={fg="#FFFFFF"}, text="Custom message 1"}}, @@ -179,11 +173,11 @@ lualine.nvim < -TELESCOPE *plugin-template-configuration-telescope* +TELESCOPE *cursor-text-objects-configuration-telescope* Note: You can customize telescope colors here or using - `vim.g.plugin_template_configuration`. + `vim.g.cursor_text_objects_configuration`. telescope.nvim >lua @@ -192,10 +186,10 @@ telescope.nvim cmd = "Telescope", config = function() -- ... Your other configuration ... - require("telescope").load_extension("plugin_template") + require("telescope").load_extension("cursor_text_objects") end, dependencies = { - "ColinKennedy/nvim-best-practices-plugin-template", + "ColinKennedy/cursor-text-objects.nvim", "nvim-lua/plenary.nvim", }, version = "0.1.*", @@ -207,43 +201,43 @@ COLORS ~ This plugin provides two default highlights -- `PluginTemplateTelescopeEntry` -- `PluginTemplateTelescopeSecondary` +- `CursorTextObjectsTelescopeEntry` +- `CursorTextObjectsTelescopeSecondary` Both come with default colors that should look nice. If you want to change them, here’s how: >lua - vim.api.nvim_set_hl(0, "PluginTemplateTelescopeEntry", {link="Statement"}) - vim.api.nvim_set_hl(0, "PluginTemplateTelescopeSecondary", {link="Question"}) + vim.api.nvim_set_hl(0, "CursorTextObjectsTelescopeEntry", {link="Statement"}) + vim.api.nvim_set_hl(0, "CursorTextObjectsTelescopeSecondary", {link="Question"}) < ============================================================================== -6. Commands *plugin-template-commands* +6. Commands *cursor-text-objects-commands* Here are some example commands: >vim " A typical subcommand - :PluginTemplate hello-world say phrase "Hello, World!" " How are you?" - :PluginTemplate hello-world say phrase "Hello, World!" --repeat=2 --style=lowercase + :CursorTextObjects hello-world say phrase "Hello, World!" " How are you?" + :CursorTextObjects hello-world say phrase "Hello, World!" --repeat=2 --style=lowercase " An example of a flag this repeatable and 3 flags, -a, -b, -c, as one dash - :PluginTemplate arbitrary-thing -vvv -abc -f + :CursorTextObjects arbitrary-thing -vvv -abc -f " Separate commands with completely separate, flexible APIs - :PluginTemplate goodnight-moon count-sheep 42 - :PluginTemplate goodnight-moon read "a book" - :PluginTemplate goodnight-moon sleep -z -z -z + :CursorTextObjects goodnight-moon count-sheep 42 + :CursorTextObjects goodnight-moon read "a book" + :CursorTextObjects goodnight-moon sleep -z -z -z < ============================================================================== -7. Tests *plugin-template-tests* +7. Tests *cursor-text-objects-tests* -INITIALIZATION *plugin-template-tests-initialization* +INITIALIZATION *cursor-text-objects-tests-initialization* Run this line once before calling any `busted` command @@ -252,7 +246,7 @@ Run this line once before calling any `busted` command < -RUNNING *plugin-template-tests-running* +RUNNING *cursor-text-objects-tests-running* Run all tests @@ -270,27 +264,4 @@ Run test based on tags busted --helper spec/minimal_init.lua . --tags=simple < - -============================================================================== -8. Tracking Updates *plugin-template-tracking-updates* - -See doc/news.txt for updates. - -You can watch this plugin for changes by adding this URL to your RSS feed: - -> - https://github.com/ColinKennedy/nvim-best-practices-plugin-template/commits/main/doc/news.txt.atom -< - - -============================================================================== -9. Other Plugins *plugin-template-other-plugins* - -This template is full of various features. But if your plugin is only meant to -be a simple plugin and you don’t want the bells and whistles that this -template provides, consider instead using nvim-plugin-template - - -Generated by panvimdoc - vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/doc/plugin_template_api.txt b/doc/cursor_text_objects_api.txt similarity index 80% rename from doc/plugin_template_api.txt rename to doc/cursor_text_objects_api.txt index 42b24e0..21d48ba 100644 --- a/doc/plugin_template_api.txt +++ b/doc/cursor_text_objects_api.txt @@ -6,7 +6,7 @@ If a function's signature here changes in some incompatible way, this package must get a new **major** version. ------------------------------------------------------------------------------ - *plugin_template.run_arbitrary_thing()* + *cursor_text_objects.run_arbitrary_thing()* `run_arbitrary_thing`({names}) @@ -16,7 +16,7 @@ Parameters ~ {names} `(string[]?)` Some text to print out. e.g. `{"a", "b", "c"}`. ------------------------------------------------------------------------------ - *plugin_template.run_copy_logs()* + *cursor_text_objects.run_copy_logs()* `run_copy_logs`({path}) @@ -28,7 +28,7 @@ Parameters ~ location is used instead. ------------------------------------------------------------------------------ - *plugin_template.run_hello_world_say_phrase()* + *cursor_text_objects.run_hello_world_say_phrase()* `run_hello_world_say_phrase`({phrase}, {repeat_}, {style}) @@ -43,7 +43,7 @@ Parameters ~ Control how the text should be shown. ------------------------------------------------------------------------------ - *plugin_template.run_hello_world_say_word()* + *cursor_text_objects.run_hello_world_say_word()* `run_hello_world_say_word`({word}, {repeat_}, {style}) @@ -58,7 +58,7 @@ Parameters ~ Control how the text should be shown. ------------------------------------------------------------------------------ - *plugin_template.run_goodnight_moon_count_sheep()* + *cursor_text_objects.run_goodnight_moon_count_sheep()* `run_goodnight_moon_count_sheep`({count}) @@ -68,7 +68,7 @@ Parameters ~ {count} `(number)` Prints 1 sheep per `count`. A value that is 1-or-greater. ------------------------------------------------------------------------------ - *plugin_template.run_goodnight_moon_read()* + *cursor_text_objects.run_goodnight_moon_read()* `run_goodnight_moon_read`({book}) @@ -78,7 +78,7 @@ Parameters ~ {book} `(string)` The name of the book. ------------------------------------------------------------------------------ - *plugin_template.run_goodnight_moon_sleep()* + *cursor_text_objects.run_goodnight_moon_sleep()* `run_goodnight_moon_sleep`({count}) diff --git a/doc/plugin_template_types.txt b/doc/cursor_text_objects_types.txt similarity index 63% rename from doc/plugin_template_types.txt rename to doc/cursor_text_objects_types.txt index f9211c3..e0db75c 100644 --- a/doc/plugin_template_types.txt +++ b/doc/cursor_text_objects_types.txt @@ -6,29 +6,29 @@ These types are either required by the Lua API or required for the normal operation of this Lua plugin. ------------------------------------------------------------------------------ -*plugin_template.Configuration* +*cursor_text_objects.Configuration* The user's customizations for this plugin. Fields ~ - {cmdparse} plugin_template.ConfigurationCmdparse? + {cmdparse} cursor_text_objects.ConfigurationCmdparse? All settings that control the command mode tools (parsing, auto-complete, etc). - {commands} plugin_template.ConfigurationCommands? - Customize the fallback behavior of all `:PluginTemplate` commands. - {logging} plugin_template.LoggingConfiguration? + {commands} cursor_text_objects.ConfigurationCommands? + Customize the fallback behavior of all `:CursorTextObjects` commands. + {logging} cursor_text_objects.LoggingConfiguration? Control how and which logs print to file / Neovim. - {tools} plugin_template.ConfigurationTools? + {tools} cursor_text_objects.ConfigurationTools? Optional third-party tool integrations. ------------------------------------------------------------------------------ -*plugin_template.ConfigurationCmdparse* +*cursor_text_objects.ConfigurationCmdparse* All settings that control the command mode tools (parsing, auto-complete, etc). Fields ~ - {auto_complete} plugin_template.ConfigurationCmdparseAutoComplete + {auto_complete} cursor_text_objects.ConfigurationCmdparseAutoComplete The settings that control what happens during auto-completion. ------------------------------------------------------------------------------ -*plugin_template.ConfigurationCmdparseAutoComplete* +*cursor_text_objects.ConfigurationCmdparseAutoComplete* The settings that control what happens during auto-completion. Fields ~ @@ -36,25 +36,25 @@ Fields ~ help_flag = Show / Hide the --help flag during auto-completion. ------------------------------------------------------------------------------ -*plugin_template.ConfigurationCommands* - Customize the fallback behavior of all `:PluginTemplate` commands. +*cursor_text_objects.ConfigurationCommands* + Customize the fallback behavior of all `:CursorTextObjects` commands. Fields ~ - {goodnight_moon} plugin_template.ConfigurationGoodnightMoon? - The default values when a user calls `:PluginTemplate goodnight-moon`. - {hello_world} plugin_template.ConfigurationHelloWorld? - The default values when a user calls `:PluginTemplate hello-world`. + {goodnight_moon} cursor_text_objects.ConfigurationGoodnightMoon? + The default values when a user calls `:CursorTextObjects goodnight-moon`. + {hello_world} cursor_text_objects.ConfigurationHelloWorld? + The default values when a user calls `:CursorTextObjects hello-world`. ------------------------------------------------------------------------------ -*plugin_template.ConfigurationGoodnightMoon* - The default values when a user calls `:PluginTemplate goodnight-moon`. +*cursor_text_objects.ConfigurationGoodnightMoon* + The default values when a user calls `:CursorTextObjects goodnight-moon`. Fields ~ - {read} plugin_template.ConfigurationGoodnightMoonRead? - The default values when a user calls `:PluginTemplate goodnight-moon read`. + {read} cursor_text_objects.ConfigurationGoodnightMoonRead? + The default values when a user calls `:CursorTextObjects goodnight-moon read`. ------------------------------------------------------------------------------ -*plugin_template.LoggingConfiguration* +*cursor_text_objects.LoggingConfiguration* Control whether or not logging is printed to the console or to disk. Fields ~ @@ -80,24 +80,24 @@ Fields ~ Defaults to "/home/selecaoone/.local/share/nvim/plugin_name.log". ------------------------------------------------------------------------------ -*plugin_template.ConfigurationGoodnightMoonRead* - The default values when a user calls `:PluginTemplate goodnight-moon read`. +*cursor_text_objects.ConfigurationGoodnightMoonRead* + The default values when a user calls `:CursorTextObjects goodnight-moon read`. Fields ~ {phrase} `(string)` The book to read if no book is given by the user. ------------------------------------------------------------------------------ -*plugin_template.ConfigurationHelloWorld* - The default values when a user calls `:PluginTemplate hello-world`. +*cursor_text_objects.ConfigurationHelloWorld* + The default values when a user calls `:CursorTextObjects hello-world`. Fields ~ - {say} plugin_template.ConfigurationHelloWorldSay? - The default values when a user calls `:PluginTemplate hello-world say`. + {say} cursor_text_objects.ConfigurationHelloWorldSay? + The default values when a user calls `:CursorTextObjects hello-world say`. ------------------------------------------------------------------------------ -*plugin_template.ConfigurationHelloWorldSay* - The default values when a user calls `:PluginTemplate hello-world say`. +*cursor_text_objects.ConfigurationHelloWorldSay* + The default values when a user calls `:CursorTextObjects hello-world say`. Fields ~ {repeat} `(number)` @@ -107,16 +107,16 @@ Fields ~ Control how the text is displayed. e.g. "uppercase" changes "hello" to "HELLO". ------------------------------------------------------------------------------ -*plugin_template.ConfigurationTools* +*cursor_text_objects.ConfigurationTools* Optional third-party tool integrations. Fields ~ - {lualine} plugin_template.ConfigurationToolsLualine? + {lualine} cursor_text_objects.ConfigurationToolsLualine? A Vim statusline replacement that will show the command that the user just ran. ------------------------------------------------------------------------------ -*plugin_template.ConfigurationToolsLualineData* - The display values that will be used when a specific `plugin_template` +*cursor_text_objects.ConfigurationToolsLualineData* + The display values that will be used when a specific `cursor_text_objects` command runs. Fields ~ diff --git a/doc/news.txt b/doc/news.txt index 59c53d4..fa2ea1c 100644 --- a/doc/news.txt +++ b/doc/news.txt @@ -1,17 +1,17 @@ -*plugin-template-news.txt* +*cursor-text-objects-news.txt* -Notable changes since PluginTemplate 1.0 +Notable changes since CursorTextObjects 1.0 =============================================================================== -NEW FEATURES *plugin-template-new-features* +NEW FEATURES *cursor-text-objects-new-features* - Added llscheck.yml - A GitHub workflow that detects type annotation issues! - Added urlchecker.yml - A GitHub workflow that finds broken URLs! =============================================================================== -BREAKING CHANGES *plugin-template-new-breaking* +BREAKING CHANGES *cursor-text-objects-new-breaking* n/a diff --git a/doc/tags b/doc/tags index da2dfee..281a8e8 100644 --- a/doc/tags +++ b/doc/tags @@ -1,36 +1,36 @@ -plugin-template-a-neovim-plugin-template plugin-template.txt /*plugin-template-a-neovim-plugin-template* -plugin-template-commands plugin-template.txt /*plugin-template-commands* -plugin-template-configuration plugin-template.txt /*plugin-template-configuration* -plugin-template-configuration-lualine plugin-template.txt /*plugin-template-configuration-lualine* -plugin-template-configuration-telescope plugin-template.txt /*plugin-template-configuration-telescope* -plugin-template-features plugin-template.txt /*plugin-template-features* -plugin-template-installation plugin-template.txt /*plugin-template-installation* -plugin-template-new-breaking news.txt /*plugin-template-new-breaking* -plugin-template-new-features news.txt /*plugin-template-new-features* -plugin-template-news.txt news.txt /*plugin-template-news.txt* -plugin-template-other-plugins plugin-template.txt /*plugin-template-other-plugins* -plugin-template-table-of-contents plugin-template.txt /*plugin-template-table-of-contents* -plugin-template-tests plugin-template.txt /*plugin-template-tests* -plugin-template-tests-initialization plugin-template.txt /*plugin-template-tests-initialization* -plugin-template-tests-running plugin-template.txt /*plugin-template-tests-running* -plugin-template-tracking-updates plugin-template.txt /*plugin-template-tracking-updates* -plugin-template-using-this-template plugin-template.txt /*plugin-template-using-this-template* -plugin-template.txt plugin-template.txt /*plugin-template.txt* -plugin_template.Configuration plugin_template_types.txt /*plugin_template.Configuration* -plugin_template.ConfigurationCmdparse plugin_template_types.txt /*plugin_template.ConfigurationCmdparse* -plugin_template.ConfigurationCmdparseAutoComplete plugin_template_types.txt /*plugin_template.ConfigurationCmdparseAutoComplete* -plugin_template.ConfigurationCommands plugin_template_types.txt /*plugin_template.ConfigurationCommands* -plugin_template.ConfigurationGoodnightMoon plugin_template_types.txt /*plugin_template.ConfigurationGoodnightMoon* -plugin_template.ConfigurationGoodnightMoonRead plugin_template_types.txt /*plugin_template.ConfigurationGoodnightMoonRead* -plugin_template.ConfigurationHelloWorld plugin_template_types.txt /*plugin_template.ConfigurationHelloWorld* -plugin_template.ConfigurationHelloWorldSay plugin_template_types.txt /*plugin_template.ConfigurationHelloWorldSay* -plugin_template.ConfigurationTools plugin_template_types.txt /*plugin_template.ConfigurationTools* -plugin_template.ConfigurationToolsLualineData plugin_template_types.txt /*plugin_template.ConfigurationToolsLualineData* -plugin_template.LoggingConfiguration plugin_template_types.txt /*plugin_template.LoggingConfiguration* -plugin_template.run_arbitrary_thing() plugin_template_api.txt /*plugin_template.run_arbitrary_thing()* -plugin_template.run_copy_logs() plugin_template_api.txt /*plugin_template.run_copy_logs()* -plugin_template.run_goodnight_moon_count_sheep() plugin_template_api.txt /*plugin_template.run_goodnight_moon_count_sheep()* -plugin_template.run_goodnight_moon_read() plugin_template_api.txt /*plugin_template.run_goodnight_moon_read()* -plugin_template.run_goodnight_moon_sleep() plugin_template_api.txt /*plugin_template.run_goodnight_moon_sleep()* -plugin_template.run_hello_world_say_phrase() plugin_template_api.txt /*plugin_template.run_hello_world_say_phrase()* -plugin_template.run_hello_world_say_word() plugin_template_api.txt /*plugin_template.run_hello_world_say_word()* +cursor-text-objects-a-neovim-plugin-template plugin-template.txt /*plugin-template-a-neovim-plugin-template* +cursor-text-objects-commands plugin-template.txt /*plugin-template-commands* +cursor-text-objects-configuration plugin-template.txt /*plugin-template-configuration* +cursor-text-objects-configuration-lualine plugin-template.txt /*plugin-template-configuration-lualine* +cursor-text-objects-configuration-telescope plugin-template.txt /*plugin-template-configuration-telescope* +cursor-text-objects-features plugin-template.txt /*plugin-template-features* +cursor-text-objects-installation plugin-template.txt /*plugin-template-installation* +cursor-text-objects-new-breaking news.txt /*plugin-template-new-breaking* +cursor-text-objects-new-features news.txt /*plugin-template-new-features* +cursor-text-objects-news.txt news.txt /*plugin-template-news.txt* +cursor-text-objects-other-plugins plugin-template.txt /*plugin-template-other-plugins* +cursor-text-objects-table-of-contents plugin-template.txt /*plugin-template-table-of-contents* +cursor-text-objects-tests plugin-template.txt /*plugin-template-tests* +cursor-text-objects-tests-initialization plugin-template.txt /*plugin-template-tests-initialization* +cursor-text-objects-tests-running plugin-template.txt /*plugin-template-tests-running* +cursor-text-objects-tracking-updates plugin-template.txt /*plugin-template-tracking-updates* +cursor-text-objects-using-this-template plugin-template.txt /*plugin-template-using-this-template* +cursor-text-objects.txt plugin-template.txt /*plugin-template.txt* +cursor_text_objects.Configuration plugin_template_types.txt /*plugin_template.Configuration* +cursor_text_objects.ConfigurationCmdparse plugin_template_types.txt /*plugin_template.ConfigurationCmdparse* +cursor_text_objects.ConfigurationCmdparseAutoComplete plugin_template_types.txt /*plugin_template.ConfigurationCmdparseAutoComplete* +cursor_text_objects.ConfigurationCommands plugin_template_types.txt /*plugin_template.ConfigurationCommands* +cursor_text_objects.ConfigurationGoodnightMoon plugin_template_types.txt /*plugin_template.ConfigurationGoodnightMoon* +cursor_text_objects.ConfigurationGoodnightMoonRead plugin_template_types.txt /*plugin_template.ConfigurationGoodnightMoonRead* +cursor_text_objects.ConfigurationHelloWorld plugin_template_types.txt /*plugin_template.ConfigurationHelloWorld* +cursor_text_objects.ConfigurationHelloWorldSay plugin_template_types.txt /*plugin_template.ConfigurationHelloWorldSay* +cursor_text_objects.ConfigurationTools plugin_template_types.txt /*plugin_template.ConfigurationTools* +cursor_text_objects.ConfigurationToolsLualineData plugin_template_types.txt /*plugin_template.ConfigurationToolsLualineData* +cursor_text_objects.LoggingConfiguration plugin_template_types.txt /*plugin_template.LoggingConfiguration* +cursor_text_objects.run_arbitrary_thing() plugin_template_api.txt /*plugin_template.run_arbitrary_thing()* +cursor_text_objects.run_copy_logs() plugin_template_api.txt /*plugin_template.run_copy_logs()* +cursor_text_objects.run_goodnight_moon_count_sheep() plugin_template_api.txt /*plugin_template.run_goodnight_moon_count_sheep()* +cursor_text_objects.run_goodnight_moon_read() plugin_template_api.txt /*plugin_template.run_goodnight_moon_read()* +cursor_text_objects.run_goodnight_moon_sleep() plugin_template_api.txt /*plugin_template.run_goodnight_moon_sleep()* +cursor_text_objects.run_hello_world_say_phrase() plugin_template_api.txt /*plugin_template.run_hello_world_say_phrase()* +cursor_text_objects.run_hello_world_say_word() plugin_template_api.txt /*plugin_template.run_hello_world_say_word()* diff --git a/lua/cursor_text_objects.lua b/lua/cursor_text_objects.lua new file mode 100644 index 0000000..29520fa --- /dev/null +++ b/lua/cursor_text_objects.lua @@ -0,0 +1,134 @@ +--- All function(s) that can be called externally by other Lua modules. +--- +--- If a function's signature here changes in some incompatible way, this +--- package must get a new **major** version. +--- +---@module 'cursor_text_objects' +--- + +local M = {} + +local _Direction = { down = "down", up = "up" } + +local unpack = unpack or table.unpack -- Future Lua versions use table.unpack + +local _CURSOR +local _DIRECTION +local _OPERATOR +local _OPERATOR_FUNCTION + +--- Check if the operatorfunc that is running will run on a whole-line. +--- +--- See Also: +--- :help g@ +--- +---@param mode "block" | "char" | "line" +---@return boolean # If `mode` is meant to run on the full-line (ignores column data). +--- +local function _is_line_mode(mode) + return mode == "line" +end + +--- Move the start / end marks of the text object as-needed. +--- +--- Run this function once, just before executing any text-object / text-operator. +--- +--- Important: +--- You must call `prepare()` once before this function can be called. +--- +---@param mode "block" | "char" | "line" +--- The caller context. See `:help :map-operator` for details. +---@return string +--- If the user's current cursor position is included with the text-object, +--- we return the string necessary to do that. If the cursor position is not +--- meant to be included, an empty string is returned instead. +---@return string +--- Decide whether to operate on whole-lines ("'") or by-character ("`"). +---@return "[" | "]" +--- Which way does is this text operator scanning, up ("[") or down ("]"). +--- +local function _adjust_marks(mode) + vim.fn.setpos(".", _CURSOR) + local mark + local is_line = _is_line_mode(mode) + + if is_line then + mark = "'" -- Includes only line information + else + mark = "`" -- Includes column information + end + + local direction + local inclusive_toggle = "" + + if _DIRECTION == _Direction.up then + direction = "[" + + if not is_line then + local buffer, row, column, offset = unpack(vim.fn.getpos("'" .. direction)) + + if column > #vim.fn.getline(row) then + row = row + 1 + column = 0 + end + + vim.fn.setpos("'" .. direction, { buffer, row, column, offset }) + end + else + direction = "]" + + local buffer, row, column, offset = unpack(vim.fn.getpos("'" .. direction)) + + if not is_line and column == #vim.fn.getline(row) then + -- NOTE: Move the mark past the current cursor column + inclusive_toggle = "v" + else + vim.fn.setpos("'" .. direction, { buffer, row, column + 1, offset }) + end + end + + return inclusive_toggle, mark, direction +end + +--- Execute the original operatorfunc but crop it based on the cursor position. +--- +--- Important: +--- You must call `prepare()` once before this function can be called. +--- +---@param mode "block" | "char" | "line" +--- The caller context. See `:help :map-operator` for details. +--- +function M.operatorfunc(mode) + vim.o.operatorfunc = _OPERATOR_FUNCTION + + local inclusive_toggle, mark, direction = _adjust_marks(mode) + + vim.fn.feedkeys(_OPERATOR .. inclusive_toggle .. mark .. direction) +end + +--- Make a visual selection of some text-object. +--- +--- Important: +--- You must call `prepare()` once before this function can be called. +--- +---@param mode "block" | "char" | "line" +--- The caller context. See `:help :map-operator` for details. +--- +function M.visual(mode) + local _, mark, direction = _adjust_marks(mode) + + vim.cmd(string.format("normal v%s%s", mark, direction)) +end + +--- Remember anything that we will need to recall once we execute `operatorfunc`. +--- +---@param direction "down" | "up" Which way to crop the text object. +--- +function M.prepare(direction) + _DIRECTION = direction + _OPERATOR = vim.v.operator + _CURSOR = vim.fn.getpos(".") + _OPERATOR_FUNCTION = vim.o.operatorfunc +end + +return M diff --git a/lua/lualine/components/plugin_template.lua b/lua/lualine/components/plugin_template.lua deleted file mode 100644 index c1223fa..0000000 --- a/lua/lualine/components/plugin_template.lua +++ /dev/null @@ -1,141 +0,0 @@ ---- Tell the user which command they just ran, using lualine.nvim ---- ----@source https://github.com/nvim-lualine/lualine.nvim ---- ----@module 'lualine.components.plugin_template' ---- - -local arbitrary_thing_runner = require("plugin_template._commands.arbitrary_thing.runner") -local configuration = require("plugin_template._core.configuration") -local copy_log_runner = require("plugin_template._commands.copy_logs.runner") -local count_sheep = require("plugin_template._commands.goodnight_moon.count_sheep") -local lualine_require = require("lualine_require") -local modules = lualine_require.lazy_require({ highlight = "lualine.highlight" }) -local read = require("plugin_template._commands.goodnight_moon.read") -local say_runner = require("plugin_template._commands.hello_world.say.runner") -local sleep = require("plugin_template._commands.goodnight_moon.sleep") -local tabler = require("plugin_template._core.tabler") - -local M = require("lualine.component"):extend() - ----@type string? -M.PREVIOUS_COMMAND = nil - ----@class plugin_template.LualineConfiguration ---- The Raw user settings from lualine's configuration. ---- e.g. `require("lualine").setup { sections = { { "plugin_template", ... }}}` ---- where "..." is the user's settings. ----@field display table? - ----@class plugin_template.LualineDisplayData ---- Any text, icons, etc that will be displayed for `plugin_template` commands. ----@field prefix string ---- The text to display when a command was called. e.g. " Goodnight moon". - ---- Track the given `command` any time a function (`callers`) in `module` runs. ---- ---- Warning: ---- To prevent unwanted behavior, only call this function one for every ---- unique Lua `module` + caller. ---- ----@param module table A Lua file to directly edit. ----@param callers string[] The names of each function(s) to modify. ----@param command string The command name to track when a function `callers` runs. ---- -local function _patch_runner_commands(module, callers, command) - for _, name in ipairs(callers) do - local original_caller = module[name] - - module[name] = function(...) - M.PREVIOUS_COMMAND = command - - return original_caller(...) - end - end -end - -_patch_runner_commands(arbitrary_thing_runner, { "run" }, "arbitrary_thing") -_patch_runner_commands(copy_log_runner, { "run" }, "copy_logs") -_patch_runner_commands(count_sheep, { "run" }, "goodnight_moon") -_patch_runner_commands(read, { "run" }, "goodnight_moon") -_patch_runner_commands(say_runner, { "run_say_phrase", "run_say_word" }, "hello_world") -_patch_runner_commands(sleep, { "run" }, "goodnight_moon") - ---- Setup all colors / text for lualine to display later. ---- ----@param options plugin_template.LualineConfiguration? ---- The options to pass from lualine to `plugin_templaet`. ---- -function M:init(options) - configuration.initialize_data_if_needed() - - --- @type table - local data - - if options then - data = options.display or {} - end - - local defaults = tabler.get_value(configuration.DATA, { "tools", "lualine" }) or {} - defaults = vim.tbl_deep_extend("force", defaults, data) - - M.super.init(self, options) - - self._command_text = { - arbitrary_thing = tabler.get_value(defaults, { "arbitrary_thing", "text" }) - or "", - copy_logs = tabler.get_value(defaults, { "copy_logs", "text" }) or "", - hello_world = tabler.get_value(defaults, { "hello_world", "text" }) or "", - goodnight_moon = tabler.get_value(defaults, { "goodnight_moon", "text" }) - or "", - } - - self._highlight_groups = { - arbitrary_thing = modules.highlight.create_component_highlight_group( - defaults.arbitrary_thing.color or "Visual", - "plugin_template_arbitrary_thing", - self.options - ), - copy_logs = modules.highlight.create_component_highlight_group( - defaults.copy_logs.color or "Comment", - "plugin_template_copy_logs", - self.options - ), - goodnight_moon = modules.highlight.create_component_highlight_group( - defaults.goodnight_moon.color or "Question", - "plugin_template_goodnight_moon", - self.options - ), - hello_world = modules.highlight.create_component_highlight_group( - defaults.hello_world.color or "Title", - "plugin_template_hello_world", - self.options - ), - } -end - ----@return string? # Get the text for the Lualine component. -function M:update_status() - local command = M.PREVIOUS_COMMAND - - if not command then - return nil - end - - local text = self._command_text[command] - local color = self._highlight_groups[M.PREVIOUS_COMMAND] - - if not color then - return text - end - - local prefix = modules.highlight.component_format_highlight(color) - - if not prefix then - return text - end - - return prefix .. text -end - -return M diff --git a/lua/plugin_template/_cli/argparse.lua b/lua/plugin_template/_cli/argparse.lua deleted file mode 100644 index 8db97d1..0000000 --- a/lua/plugin_template/_cli/argparse.lua +++ /dev/null @@ -1,382 +0,0 @@ ---- Parse text into positional / named arguments. ---- ----@module 'plugin_template._cli.argparse' ---- - -local vlog = require("plugin_template._vendors.vlog") - -local M = {} - -M.PREFIX_CHARACTERS = { "-", "+" } - ----@enum argparse.ArgumentType -M.ArgumentType = { - flag = "__flag", - named = "__named", - position = "__position", -} - ----@class argparse.BaseArgument ---- A base class to inherit from. ----@field argument_type argparse.ArgumentType ---- An type indicator for this argument. ----@field range argparse.ArgumentRange ---- The start and end index (both are inclusive) of the argument. - ----@class argparse.ArgumentRange ---- The start and end index (both are inclusive) of the argument. ----@field start_column number ---- The first index of the argument (inclusive). ----@field end_column number ---- The last index of the argument (inclusive). - ----@class argparse.FlagArgument : argparse.BaseArgument ---- An argument that has a name but no value. It starts with either - or -- ---- Examples: `-f` or `--foo` or `--foo-bar` ----@field name string ---- The text of the flag. e.g. The `"foo"` part of `"--foo"`. - ----@class argparse.PositionArgument : argparse.BaseArgument ---- An argument that is just text. e.g. `"foo bar"` is two positions, foo and bar. ----@field value string ---- The position's label. - ----@class argparse.NamedArgument : argparse.FlagArgument ---- A --key=value pair. Basically it's a argparse.FlagArgument that has an extra value. ----@field value string | boolean ---- The second-hand side of the argument. e.g. The `"bar"` part of ---- `"--foo=bar"`. If the argument is partially written like `"--foo="` ---- then this will be an empty string. - ----@alias argparse.Argument argparse.FlagArgument | argparse.PositionArgument | argparse.NamedArgument - ----@class argparse.Results ---- All information that was found from parsing some user's input. ----@field arguments argparse.Argument[] ---- The arguments that were able to be parsed ----@field remainder argparse.Remainder ---- Any leftover text during parsing that didn't match an argument. ----@field text string ---- The original, raw, unparsed user arguments. - ----@class argparse.Remainder ---- Any leftover text during parsing that didn't match an argument. ----@field value string ---- The raw, unparsed text. - ---- An internal tracker for the arguments. -local _State = { - argument_start = "argument_start", - in_double_flag = "in_double_flag", - in_quote = "in_quote", - in_single_flag = "in_single_flag", - in_value = "in_value", - normal = "normal", - value_is_pending = "values_is_pending", -} - ---- Check if `character` is a typical a-zA-Z0-9 character. ---- ----@param character string Basically any non-special character. ----@return boolean # If a-zA-Z0-9, return `true`. ---- -local function is_alpha_numeric(character) - return character:match("[^='\"%s]") ~= nil -end - ---- Check if `character` marks the start of a `argparse.FlagArgument` or `argparse.NamedArgument`. ---- ----@param character string A starting character. e.g. `-`, `+`, etc. ----@return boolean # If `character` is a `argparse.PositionArgument` character, return `true`. ---- -local function _is_prefix(character) - return vim.tbl_contains(M.PREFIX_CHARACTERS, character) -end - ---- Check if `character` is a space, tab, or newline. ---- ----@param character string Basically `" "`, `\n`, `\t`. ----@return boolean # If it's any whitespace, return `true`. ---- -local function _is_whitespace(character) - return character:match("%s") -end - ---- Check if `character` starts a multi-word quote. ---- ----@param character string Basically ' or ". ----@return boolean # If ' or ", return `true`. ---- -local function _is_quote(character) - return character == '"' or character == "'" -end - ---- Parse for positional arguments, named arguments, and flag arguments. ---- ---- In a command like `bar -f --buzz --some="thing else"`... ---- - `bar` is positional ---- - `-f` is a single-letter flag ---- - `--buzz` is a multi-letter flag ---- - `--some="thing else" is a named argument whose value is "thing else" ---- ----@param text string ---- Some command to parse. e.g. `bar -f --buzz --some="thing else"`. ----@return argparse.Results ---- All found for positional arguments, named arguments, and flag arguments. ---- -function M.parse_arguments(text) - local output = {} - - local state = _State.argument_start - --- @type string | boolean - local current_argument = "" - --- @type string | boolean - local current_name = "" - local is_escaping = false - local needs_name = false - local needs_value = false - --- @type argparse.Remainder - local remainder = { value = "" } - local start_index = 1 - local escaped_character_count = 0 - - local physical_index = 1 - - --- Look ahead to the next character in `text`. - --- - --- @param index number A 1-or-more value to check. - --- @return string # The found character, if any. - --- - local function peek(index) - return text:sub(index + 1, index + 1) - end - - local logical_index = physical_index - - --- Adds any accumulated argument data to the final output. - --- - --- Any buffered / remainder text is cleared. - --- - local function _add_to_output() - remainder.value = "" - local end_index = physical_index - escaped_character_count - 1 - local range = { start_column = start_index, end_column = end_index } - - if not needs_value then - table.insert(output, { - argument_type = M.ArgumentType.position, - range = range, - value = current_argument, - }) - - return - end - - if current_argument == true then - table.insert(output, { - argument_type = M.ArgumentType.flag, - name = current_name, - range = range, - }) - - return - end - - table.insert(output, { - argument_type = M.ArgumentType.named, - name = current_name, - range = range, - value = current_argument, - }) - end - - local function _reset_argument() - current_argument = "" - end - - local function _reset_all() - _reset_argument() - current_name = "" - is_escaping = false - needs_name = false - needs_value = false - state = _State.argument_start - end - - while physical_index <= #text do - local character = text:sub(physical_index, physical_index) - remainder.value = remainder.value .. character - - local function _append_to_wip_argument(alternate_character) - current_argument = current_argument .. (alternate_character or character) - end - - if character == "\\" then - is_escaping = true - end - - if state == _State.argument_start then - start_index = logical_index - - if is_alpha_numeric(character) then - -- NOTE: We know we've encounted some -f` or `--foo` or - -- `--foo=bar` but we aren't sure which it is yet. - -- - if _is_prefix(character) then - local next_character = peek(physical_index) - - if _is_prefix(next_character) and next_character == character then - -- NOTE: It's definitely a `--foo` flag or `--foo=bar` argument. - state = _State.in_double_flag - _reset_argument() - current_name = character .. next_character - remainder.value = remainder.value .. next_character - logical_index = logical_index + 1 - physical_index = physical_index + 1 - needs_name = true - needs_value = true - else - -- NOTE: It's definitely a `-f` flag. - state = _State.in_single_flag - _reset_argument() - current_name = character - needs_name = false - needs_value = true - end - elseif _is_quote(character) then - -- NOTE: Actually we're inside of some thing. e.g. `"foo -b thing"! - state = _State.in_quote - needs_value = false - else - state = _State.normal - _append_to_wip_argument() - needs_value = false - end - elseif _is_quote(character) then - state = _State.in_quote - end - elseif state == _State.in_quote then - -- NOTE: We're inside of some grouped text. e.g. `"foo -b thing"` - -- is not treated as position text + a flag + position but just as - -- "some text within quotes". - -- - if not is_escaping and _is_quote(character) then - -- NOTE: We've reached the end of the quote - physical_index = physical_index + 1 - logical_index = logical_index + 1 - _add_to_output() - _reset_all() - else - _append_to_wip_argument() - end - elseif state == _State.in_double_flag then - if character == "=" then - -- NOTE: We've discovered a `--foo=bar` argument and we're just about - -- to find the `bar` part - -- - needs_name = false - current_name = current_name .. current_argument - _reset_argument() - - if _is_quote(peek(physical_index)) then - -- NOTE: We've discovered a `--foo="bar thing"` argument - -- and we're just about to find the `"bar thing"` part - -- - state = _State.in_quote - physical_index = physical_index + 1 - logical_index = logical_index + 1 - else - state = _State.value_is_pending - end - elseif _is_whitespace(character) then - -- NOTE: Ignore whitespace in some situations. - if not is_escaping then - current_name = current_name .. current_argument - current_argument = true - _add_to_output() - _reset_all() - end - elseif needs_name then - _append_to_wip_argument() - end - elseif state == _State.in_single_flag then - -- NOTE: Since single-flags can be appended together like `"-abc"` - -- or "-zzz", we need some special logic to keep track of every flag. - -- - local next_character = peek(physical_index) - - if _is_whitespace(next_character) or next_character == "" then - -- NOTE: We've reached the end of 1+ single flag(s). - -- Add every found character as flags - -- - local current_argument_ = current_argument .. character - local current_name_ = current_name - current_argument = true - - start_index = start_index - 1 - - for index_ = 1, #current_argument_ do - local character_ = current_argument_:sub(index_, index_) - start_index = start_index + 1 - current_name = current_name_ .. character_ - physical_index = physical_index + 1 - _add_to_output() - physical_index = physical_index - 1 - end - - _reset_all() - else - _append_to_wip_argument() - end - elseif state == _State.value_is_pending then - if _is_whitespace(character) then - if current_argument == "" then - current_argument = false - end - _add_to_output() - _reset_all() - else - _append_to_wip_argument() - end - elseif state == _State.normal then - if is_escaping then - local next = peek(physical_index) - _append_to_wip_argument(next) - physical_index = physical_index + 1 - escaped_character_count = escaped_character_count + 1 - is_escaping = false -- NOTE: The escaped character was consumed - elseif _is_whitespace(character) then - _add_to_output() - _reset_all() - remainder.value = remainder.value .. character - else - _append_to_wip_argument() - end - end - - logical_index = logical_index + 1 - physical_index = physical_index + 1 - end - - if state == _State.value_is_pending then - if current_argument == "" then - current_argument = false - end - - _add_to_output() - _reset_all() - elseif state == _State.normal and current_argument ~= "" then - _add_to_output() - elseif (state == _State.in_double_flag or state == _State.in_single_flag) and current_argument ~= "" then - current_name = current_name .. current_argument - current_argument = true - needs_value = true - _add_to_output() - end - - vlog.fmt_debug('Got "%s" arguments.', { arguments = output, text = text, remainder = remainder }) - - return { arguments = output, text = text, remainder = remainder } -end - -return M diff --git a/lua/plugin_template/_cli/argparse_helper.lua b/lua/plugin_template/_cli/argparse_helper.lua deleted file mode 100644 index 4fbb25a..0000000 --- a/lua/plugin_template/_cli/argparse_helper.lua +++ /dev/null @@ -1,48 +0,0 @@ ---- Make dealing with COMMAND mode parsed arguments a bit easier. ---- ----@module 'plugin_template._cli.argparse_helper' ---- - -local tabler = require("plugin_template._core.tabler") - -local M = {} - ---- Remove the starting `index` arguments from `results`. ---- ---- This function is useful for handling "subcommand triage". ---- ----@param results argparse.Results ---- The parsed arguments + any remainder text. ----@param index number ---- A 1-or-more value. 1 has not effect. 2-or-more will start removing ---- arguments from the left-hand side of `results`. ---- -function M.lstrip_arguments(results, index) - local copy = vim.tbl_deep_extend("force", {}, results) - local arguments = tabler.get_slice(results.arguments, index) - copy.arguments = arguments - - return copy -end - ---- Remove the ending `index` arguments from `results`. ---- ---- This function is useful for handling "subcommand triage". ---- ----@param results argparse.Results ---- The parsed arguments + any remainder text. ----@param index number ---- A 1-or-more value. 1 has not effect. 2-or-more will remove arguments ---- from the right-hand side of `results`. ----@return argparse.Results ---- The stripped copy from `results`. ---- -function M.rstrip_arguments(results, index) - local copy = vim.tbl_deep_extend("force", {}, results) - local arguments = tabler.get_slice(results.arguments, 1, index) - copy.arguments = arguments - - return copy -end - -return M diff --git a/lua/plugin_template/_cli/cli_subcommand.lua b/lua/plugin_template/_cli/cli_subcommand.lua deleted file mode 100644 index 4393ceb..0000000 --- a/lua/plugin_template/_cli/cli_subcommand.lua +++ /dev/null @@ -1,404 +0,0 @@ ---- Connect Neovim's COMMAND mode to our Lua functions. ---- ----@module 'plugin_template._cli.cli_subcommand' ---- - -local M = {} - ----@class argparse.SubcommandRunnerOptions ---- User input to send to the legacy argparse API. ----@field args string ---- The full user input text, unparsed. e.g. `"some_subcommand arg1 --flag --foo=bar"`. ----@field fargs string[] ---- The parsed user input text. e.g. `{"some_subcommand", "arg1", "--flag" "--foo=bar"}`. - ----@class plugin_template.NamespaceExecuteArguments ---- The expected data that's passed to any `set_execute` call in plugin-template. ----@field input argparse.Results ---- The user's raw input, split into tokens. ----@field namespace cmdparse.Namespace ---- The collected results from comparing `input` to our cmdparse tree. - ----@class plugin_template.CompleteData ---- The data that gets passed when `plugin_template.Subcommand.complete` is called. ----@field input argparse.Results ---- All information that was found from parsing some user's input. - ----@class plugin_template.RunData ---- The data that gets passed when `plugin_template.Subcommand.run` is called. ----@field input argparse.Results ---- All information that was found from parsing some user's input. - ----@class plugin_template.Subcommand ---- A subparser's definition. At minimum you need to define `parser` or ---- `run` or code will error when you try to run commands. If you define ---- `parser`, you don't need to define `complete` or `run` (`parser` is the ---- preferred way to make parsers). ----@field complete (fun(data: plugin_template.CompleteData): string[])? ---- Command completions callback, the `data` are the lead of the subcommand's arguments ----@field parser (fun(): cmdparse.ParameterParser)? ---- The primary parser used for subcommands. It handles auto-complete, ---- expression-evaluation, and running a user's code. ----@field run (fun(data: plugin_template.SubcommandRun): nil)? ---- The function to run when the subcommand is called. - ----@class plugin_template.SubcommandRun ---- The data that gets passed to the `run` function. Most of the time, ---- a user never needs or touches this data. It's only for people who need ---- absolute control over the CLI or some unsupported behavior. ----@field input argparse.Results ---- The parsed arguments (that the user is now trying to execute some function with). - ----@alias plugin_template.ParserCreator fun(): cmdparse.ParameterParser - ----@alias plugin_template.Subcommands table - ---- Check if `full` contains `prefix` + whitespace. ---- ----@param full string Some full text like `"PluginTemplate blah"`. ----@param prefix string The expected starting text. e.g. `"PluginTemplate"`. ----@return boolean # If a subcommand syntax was found, return true. ---- -local function _is_subcommand(full, prefix) - local expression = "^" .. prefix .. "%s+.*$" - - return full:match(expression) ~= nil -end - ---- Get the auto-complete, if any, for a subcommand. ---- ----@param text string Some full text like `"PluginTemplate blah"`. ----@param prefix string The expected starting text. e.g. `"PluginTemplate"`. ----@param subcommands plugin_template.Subcommands All allowed commands. ---- -local function _get_subcommand_completion(text, prefix, subcommands) - local argparse = require("plugin_template._cli.argparse") - - local expression = "^" .. prefix .. "*%s(%S+)%s(.*)$" - local subcommand_key, arguments = text:match(expression) - - if not subcommand_key or not arguments then - return nil - end - - if not subcommands[subcommand_key] then - vim.notify( - string.format( - 'PluginTemplate: Unknown command "%s". Please check your spelling and try again.', - subcommand_key - ), - vim.log.levels.ERROR - ) - - return nil - end - - local subcommand = subcommands[subcommand_key] - - if type(subcommand) == "function" then - local parser = subcommand() - - if not parser then - vim.notify( - string.format('Subcommand "%s" does not define a parser. Please fix!', subcommand_key), - vim.log.levels.ERROR - ) - - return nil - end - - local column = vim.fn.getcmdpos() - - return parser:get_completion(arguments, column) - end - - ---@cast subcommand plugin_template.Subcommand - - if subcommand.parser then - local parser = subcommand.parser() - local column = vim.fn.getcmdpos() - - return parser:get_completion(arguments, column) - end - - if subcommand.complete then - local result = subcommand.complete({ input = argparse.parse_arguments(arguments) }) - - if result == nil or vim.islist(result) then - if arguments == "" then - arguments = "" - end - - vim.notify( - string.format( - 'plugin-template: Subcommand / Arguments "%s / %s" must be a string[]. Got "%s".', - subcommand, - arguments, - vim.inspect(result) - ) - ) - - return result - end - - return nil - end - - return nil -end - ---- Run `parser` and pass it the user's raw input `text`. ---- ----@param parser cmdparse.ParameterParser The decision tree that parses and runs `text`. ----@param text string The (unparsed) text that user provides from COMMAND mode. ---- -local function _run_subcommand(parser, text) - local argparse = require("plugin_template._cli.argparse") - - local arguments = argparse.parse_arguments(text) - local namespace = parser:parse_arguments(arguments) - ---@type fun(data: plugin_template.NamespaceExecuteArguments): nil - local execute = namespace.execute - - if execute then - execute({ input = arguments, namespace = namespace }) - - return - end - - vim.notify( - string.format( - 'PluginTemplate: Command "%s" parsed "%s" text into "%s" namespace but no `execute` ' - .. "function was defined. " - .. 'Call parser:set_execute(function() print("Your function here") end)', - parser.name or parser.help or "", - text, - vim.inspect(namespace) - ), - vim.log.levels.ERROR - ) -end - ---- Remove `prefix` from `text` if needed. ---- ----@param prefix string A character / phrase to remove from `text`. ----@param text string The text that might start with `prefix`. ----@return string # Basically `text.lstrip(prefix)`. ---- -local function _strip_prefix(prefix, text) - return (text:gsub("^" .. vim.pesc(prefix) .. "%s*", "")) -end - ---- If anything in `subcommands` is missing data, define default value(s) for it. ---- ----@param subcommands plugin_template.Subcommands ---- All registered commands for `plugin_template` to possibly modify. ---- -function M.initialize_missing_values(subcommands) - if type(subcommands) == "table" then - for _, subcommand in pairs(subcommands) do - if type(subcommand) == "table" and not subcommand.complete then - subcommand.complete = function() - return {} - end - end - end - end -end - ---- Create a deferred function that can parse and execute a user's arguments. ---- ----@param parser_creator plugin_template.ParserCreator ---- A function that creates the decision tree that parses text. ----@return fun(opts: table): nil ---- A function that will parse the user's arguments. ---- -function M.make_parser_triager(parser_creator) - local function runner(opts) - local argparse = require("plugin_template._cli.argparse") - - local text = opts.name .. " " .. opts.args - local arguments = argparse.parse_arguments(text) - local parser = parser_creator() - local success - ---@type table | string - local result - success, result = pcall(function() - return parser:parse_arguments(arguments) - end) - - if not success then - ---@cast result string The error message. - vim.notify(result, vim.log.levels.ERROR) - - return - end - - ---@type fun(data: plugin_template.NamespaceExecuteArguments): nil - local execute = result.execute - - if execute then - execute({ input = arguments, namespace = result }) - - return - end - - vim.notify(parser:get_concise_help(text), vim.log.levels.ERROR) - end - - return runner -end - ---- Make a function that can auto-complete based on the parser of `parser_creator`. ---- ----@param parser_creator plugin_template.ParserCreator ---- A function that creates the decision tree that parses text. ----@return fun(_: any, all_text: string, _: any): string[]? ---- A deferred function that creates the COMMAND mode parser, runs it, and ---- gets all auto-complete values back if any were found. ---- -function M.make_parser_completer(parser_creator) - local function runner(_, all_text, _) - local configuration = require("plugin_template._core.configuration") - configuration.initialize_data_if_needed() - - local parser = parser_creator() - local column = vim.fn.getcmdpos() - - return parser:get_completion(all_text, column) - end - - return runner -end - ---- Use `subcommands` to make a COMMAND mode auto-completer. ---- ----@param prefix string The command to exclude from auto-complete. e.g. `"PluginTemplate"`. ----@param subcommands plugin_template.Subcommands All allowed commands. ----@return fun(latest_text: string, all_text: string): string[]? # The generated auto-complete function. ---- -function M.make_subcommand_completer(prefix, subcommands) - local function runner(latest_text, all_text, _) - local configuration = require("plugin_template._core.configuration") - configuration.initialize_data_if_needed() - - local completion = _get_subcommand_completion(all_text, prefix, subcommands) - - if completion then - return completion - end - - if _is_subcommand(all_text, prefix) then - local escaped_latest_text = vim.pesc(latest_text) - local keys = vim.tbl_keys(subcommands) - local output = {} - - for _, key in ipairs(keys) do - if key:find(escaped_latest_text) ~= nil then - table.insert(output, key) - end - end - - return output - end - - return nil - end - - return runner -end - ---- Create a deferred function that creates separate parsers for each subcommand. ---- ----@param subcommands plugin_template.Subcommands ---- Each subcommand to register. ----@return fun(opts: argparse.SubcommandRunnerOptions): nil ---- A function that will parse the user's arguments. ---- -function M.make_subcommand_triager(subcommands) - --- Check for a subcommand and, if found, call its `run` caller field. - --- - --- - ---@param opts argparse.SubcommandRunnerOptions The parsed user inputs. - --- - local function _runner(opts) - local configuration = require("plugin_template._core.configuration") - local argparse = require("plugin_template._cli.argparse") - configuration.initialize_data_if_needed() - - local subcommand_key = opts.fargs[1] - local subcommand = subcommands[subcommand_key] - - if not subcommand then - vim.notify("PluginTemplate: Unknown command: " .. subcommand_key, vim.log.levels.ERROR) - - return - end - - local stripped_text = _strip_prefix(subcommand_key, opts.args) - - if type(subcommand) == "function" then - local parser = subcommand() - - if not parser then - vim.notify( - string.format('Subcommand "%s" does not define a parser. Please fix!', subcommand_key), - vim.log.levels.ERROR - ) - - return - end - - _run_subcommand(parser, stripped_text) - - return - end - - if subcommand.parser then - local parser = subcommand.parser() - _run_subcommand(parser, stripped_text) - - return - end - - if subcommand.run then - subcommand.run(vim.tbl_deep_extend("keep", { input = argparse.parse_arguments(stripped_text) }, opts)) - - return - end - - vim.notify(string.format('Subcommand "%s" must define `parser` or `run`.', vim.log.levels.ERROR)) - end - - --- Check for a subcommand and, if found, call its `run` caller field. - --- - --- - ---@param opts argparse.SubcommandRunnerOptions The parsed user options. - --- - local function runner(opts) - local configuration = require("plugin_template._core.configuration") - configuration.initialize_data_if_needed() - - local help_message = require("plugin_template._cli.cmdparse.help_message") - - local success, result = pcall(function() - _runner(opts) - end) - - if not success then - ---@cast result string - - if help_message.is_help_message(result) then - help_message.show_help(result) - - return - end - - error(result) - end - end - - return runner -end - -return M diff --git a/lua/plugin_template/_cli/cmdparse/constant.lua b/lua/plugin_template/_cli/cmdparse/constant.lua deleted file mode 100644 index 4e797c5..0000000 --- a/lua/plugin_template/_cli/cmdparse/constant.lua +++ /dev/null @@ -1,39 +0,0 @@ ---- Meaningful global variables to reuse across modules. ---- ----@module 'plugin_template._cli.cmdparse.constant' ---- - -local M = {} - ----@enum cmdparse.ActionOption -M.Action = { count = "count", store_false = "store_false", store_true = "store_true" } - ----@enum cmdparse.ChoiceContext ---- Extra information provided to `cmdparse.Parameter.choices()` when ---- resolving for allowed values. ---- ---- auto_completing = "Getting the next auto-completion suggestion(s), if any". ---- error_message = "An error occurred and we want to give the user a list of possible choices". ---- help_message = "The user wrote --help so we need to get choices to display for that". ---- parameter_names = "The (initial) auto-complete options. Show be the full list of possibilities". ---- parsing = "Evaluating the arguments". ---- position_matching = "Trying to match a positional argument". ---- value_matching = "Getting the value that follows a flag or named argument". ---- -M.ChoiceContext = { - auto_completing = "auto_completing", - error_message = "error_message", - help_message = "help_message", - parameter_names = "parameter_names", - parsing = "parsing", - position_matching = "position_matching", - value_matching = "value_matching", -} - ----@enum cmdparse.Counter -M.Counter = { - one_or_more = "+", - zero_or_more = "*", -} - -return M diff --git a/lua/plugin_template/_cli/cmdparse/evaluator.lua b/lua/plugin_template/_cli/cmdparse/evaluator.lua deleted file mode 100644 index 67d0a40..0000000 --- a/lua/plugin_template/_cli/cmdparse/evaluator.lua +++ /dev/null @@ -1,186 +0,0 @@ ---- Parse and evaluate parameters, using CLI arguments. ---- ----@module 'plugin_template._cli.cmdparse.evaluator' ---- - -local argparse = require("plugin_template._cli.argparse") -local constant = require("plugin_template._cli.cmdparse.constant") - -local M = {} -local _Private = {} - ---- Check if `name` is a possible value of `parameter`. ---- ----@param name string ---- The written user text. e.g. `"foo"`. ----@param parameter cmdparse.Parameter ---- Some position parameter to check. e.g. `{choices={"foo", "bar"}}`. ----@return boolean ---- If `parameter` has defined `parameter.choices` and `name` matches one of ---- them, return `true`. ---- -local function _has_position_parameter_match(name, parameter) - if not parameter.choices then - -- NOTE: Any value is valid if there are no explicit choices - return true - end - - if - vim.tbl_contains( - parameter.choices({ - contexts = { constant.ChoiceContext.position_matching }, - current_value = name, - }), - name - ) - then - return true - end - - return false -end - ---- Check if `arguments` is valid data for `parameter`. ---- ----@param parameter cmdparse.Parameter ---- A parser parameter that may expect 0-or-more values. ----@param arguments argparse.Argument ---- User inputs to check to check against `parameter`. ----@return boolean ---- If `parameter` is satisified by is satisified by `arguments`, return `true`. ---- -function _Private.has_satisfying_value(parameter, arguments) - if _Private.is_single_nargs_and_named_parameter(parameter, arguments) then - return true - end - - local nargs = parameter:get_nargs() - - if nargs == 0 or nargs == constant.Counter.zero_or_more then - -- NOTE: If `parameter` doesn't need any value then it is definitely satisified. - return true - end - - local count = 0 - - for _, argument in ipairs(arguments) do - if argument.argument_type ~= argparse.ArgumentType.position then - -- NOTE: Flag arguments can only accept non-flag arguments, in general. - return false - end - - count = count + 1 - - if count == nargs or nargs == constant.Counter.one_or_more then - return true - end - end - - -- NOTE: There wasn't enough `arguments` left to satisfy `parameter`. - return false -end - ---- Check if `parameter` is expected to have exactly one value. ---- ----@param parameter cmdparse.Parameter ---- A parser parameter that may expect 0-or-more values. ----@param arguments argparse.Argument ---- User inputs to check. ----@return boolean ---- If `parameter` needs exactly one value, return `true`. ---- -function _Private.is_single_nargs_and_named_parameter(parameter, arguments) - if parameter:get_nargs() ~= 1 then - return false - end - - local argument = arguments[1] - - if not argument then - return false - end - - if argument.argument_type ~= argparse.ArgumentType.named then - return false - end - - return vim.tbl_contains(parameter.names, argument.name) -end - ---- Find + increment all flag parameters of `parser` that match the other inputs. ---- ----@param parser cmdparse.ParameterParser ---- A parser whose parameters may be modified. ----@param argument_name string ---- The expected flag argument name. ----@param arguments argparse.Argument ---- All of the upcoming argumenst after `argument_name`. We use these to figure out ---- if `parser` is an exact match. ----@return boolean ---- If `true` a flag argument was matched and incremented. ---- -function _Private.compute_exact_flag_match(parser, argument_name, arguments) - for _, parameter in ipairs(parser:get_flag_parameters()) do - if - not parameter:is_exhausted() - and vim.tbl_contains(parameter.names, argument_name) - and _Private.has_satisfying_value(parameter, arguments) - then - parameter:increment_used() - - return true - end - end - - return false -end - ---- Find + increment all position parameters of `parser` that match the other inputs. ---- ----@param parser cmdparse.ParameterParser ---- A parser whose parameters may be modified. ----@param argument_name string ---- The expected position argument name. Most of the time position arguments ---- don't even have an expected name so this value is not always used. ----@return boolean ---- If `true` a position argument was matched and incremented. ---- -function _Private.compute_exact_position_match(argument_name, parser) - for _, parameter in ipairs(parser:get_position_parameters()) do - if not parameter:is_exhausted() then - if _has_position_parameter_match(argument_name, parameter) then - parameter:increment_used() - - return true - end - - return false - end - end - - return false -end - ---- Find + increment the parameter(s) of `parser` that match the other inputs. ---- ----@param parser cmdparse.ParameterParser ---- A parser whose parameters may be modified. ----@param argument_name string ---- The expected flag argument name. ----@param arguments argparse.Argument ---- All of the upcoming argumenst after `argument_name`. We use these to figure out ---- if `parser` is an exact match. ----@return boolean ---- If `true` a flag argument was matched and incremented. ---- -function M.compute_and_increment_parameter(parser, argument_name, arguments) - local found = _Private.compute_exact_flag_match(parser, argument_name, arguments) - - if found then - return found - end - - return _Private.compute_exact_position_match(argument_name, parser) -end - -return M diff --git a/lua/plugin_template/_cli/cmdparse/help_message.lua b/lua/plugin_template/_cli/cmdparse/help_message.lua deleted file mode 100644 index f4607b3..0000000 --- a/lua/plugin_template/_cli/cmdparse/help_message.lua +++ /dev/null @@ -1,301 +0,0 @@ ---- Functions that make writing / reading / changing help messages. ---- ----@module 'plugin_template._cli.cmdparse.help_message' ---- - -local constant = require("plugin_template._cli.cmdparse.constant") -local iterator_helper = require("plugin_template._cli.cmdparse.iterator_helper") -local text_parse = require("plugin_template._cli.cmdparse.text_parse") -local texter = require("plugin_template._core.texter") - -local M = {} -local _Private = {} - -M.HELP_MESSAGE_PREFIX = "Usage: " -M.HELP_NAMES = { "--help", "-h" } - ---- Add `items` to `table_` if it is not empty. ---- ----@param table_ any[] An array to add to. ----@param items string[] Values to add into `table_`, maybe. ---- -local function _insert_if_value(table_, items) - if vim.tbl_isempty(items) then - return - end - - table.insert(table_, vim.fn.join(items, "\n")) -end - ---- Convert `text` into an expected value hint help message text. ---- ---- For example `"foo-bar"` becomes `"FOO_BAR"`. This is just for display-purposes. ---- ----@param text string A parameter name to replace. ----@return string # The replaced text. ---- -function _Private.get_recommended_value_hint_name(text) - local found - - for index = 1, #text do - local character = text:sub(index, index) - - if texter.is_alphanumeric(character) or texter.is_unicode(character) then - found = index - - break - end - end - - if not found then - return "" - end - - local word = text:sub(found, #text) - - return (word:upper():gsub("-", "_")) -end - ---- Create the help message for a parameter. ---- ----@param parameter cmdparse.Parameter ---- Any position, flag, or named parameter to get a help message for. ----@return string ---- The help created message. ---- -function _Private.get_parameter_usage_help_text(parameter) - local text - - if parameter.value_hint and parameter.value_hint ~= "" then - text = parameter.value_hint - elseif parameter.choices then - local choices = parameter.choices({ contexts = { constant.ChoiceContext.help_message } }) - - text = "{" .. vim.fn.join(choices, ",") .. "}" - else - text = _Private.get_recommended_value_hint_name(parameter.names[1]) - end - - ---@cast text string - - local nargs = parameter:get_nargs() - - if type(nargs) == "number" then - local output = {} - - for _ = 1, nargs do - table.insert(output, text) - end - - return vim.fn.join(output, " ") - end - - if nargs == constant.Counter.zero_or_more then - return string.format("[%s ...]", text) - end - - if nargs == constant.Counter.one_or_more then - return string.format("%s [%s ...]", text, text) - end - - return text -end - ---- Get all subcomands (child parsers) from `parser`. ---- ----@param parser cmdparse.ParameterParser Some runnable command to get parameters from. ----@return string[] # The labels of all of the flags. ---- -function _Private.get_parser_child_parser_help_text(parser) - local output = {} - - for parser_ in iterator_helper.iter_parsers(parser) do - local names = parser_:get_names() - local text = names[1] - - if #names ~= 1 then - text = M.get_help_command_labels(names) - end - - if parser_.help then - text = text .. " " .. parser_.help - end - - table.insert(output, texter.indent(text)) - end - - output = vim.fn.sort(output) - - if not vim.tbl_isempty(output) then - table.insert(output, 1, "Commands:") - end - - return output -end - ---- Get all option flag / named parameter --help text from `parser`. ---- ----@param parser cmdparse.ParameterParser Some runnable command to get parameters from. ----@return string[] # The labels of all of the flags. ---- -function _Private.get_parser_flag_help_text(parser) - local output = {} - - for _, flag in ipairs(iterator_helper.sort_parameters(parser:get_flag_parameters())) do - local names = vim.fn.join(flag.names, " ") - local text = names - - local hint = M.get_position_usage_help_text(flag) - - if hint and hint ~= "" then - text = text .. " " .. hint - end - - if flag.help then - text = text .. " " .. flag.help - end - - table.insert(output, texter.indent(text)) - end - - if not vim.tbl_isempty(output) then - table.insert(output, 1, "Options:") - end - - return output -end - ---- Convert a position parameter ---- Create the help message for a position parameter or subparser. ---- ----@param position cmdparse.Parameter ---- Any position, flag, or named parameter to get a help message for. ----@return string ---- The help created message. ---- -function _Private.get_position_description_help_text(position) - local text = M.get_position_usage_help_text(position) - - if position.help and position.help ~= "" then - text = text .. " " .. position.help - end - - return text -end - ---- Get all position argument --help text from `parser`. ---- ----@param parser cmdparse.ParameterParser Some runnable command to get arguments from. ----@return string[] # The labels of all of the flags. ---- -function _Private.get_parser_position_help_text(parser) - local output = {} - - for _, position in ipairs(parser:get_position_parameters()) do - local text = _Private.get_position_description_help_text(position) - - table.insert(output, texter.indent(text)) - end - - output = vim.fn.sort(output) - - if not vim.tbl_isempty(output) then - table.insert(output, 1, "Positional Arguments:") - end - - return output -end - ---- Check if `arguments` includes a `--help` or `-h` flag. ---- ----@param arguments argparse.Argument[] All user inputs to check. ----@return boolean # If the flag is found, return `true`. ---- -function M.has_help(arguments) - for _, argument in ipairs(arguments) do - if vim.tbl_contains(M.HELP_NAMES, text_parse.get_argument_name(argument)) then - return true - end - end - - return false -end - ---- Check if `text` looks like a help message from `cmdparse`. ---- ----@param text string The expected message. e.g. `"Usage: "`. ----@return boolean # If `text` is just a normal text, return `true`. Otherwise return `false`. ---- -function M.is_help_message(text) - return texter.startswith(text, M.HELP_MESSAGE_PREFIX) -end - ---- Get the help message for a `flag` parameter. ---- ----@param flag cmdparse.Parameter A `--foo` or `--foo=bar` parameter to convert. ----@return string # The generated help message. ---- -function M.get_flag_help_text(flag) - local text = _Private.get_parameter_usage_help_text(flag) - - if text and text ~= "" then - return string.format("[%s %s]", flag:get_raw_name(), text) - end - - return string.format("[%s]", flag:get_raw_name()) -end - ---- Combine `labels` into a single-line summary (for help messages). ---- ----@param labels string[] All commands to run. ----@return string # The created text. ---- -function M.get_help_command_labels(labels) - return string.format("{%s}", vim.fn.join(vim.fn.sort(labels), ",")) -end - ---- Get the help message for all parameters and subparsers of `parser`. ---- ----@param parser cmdparse.ParameterParser ---- The root to get a help message for. ----@return string[] ---- The generated help message lines. ---- -function M.get_parser_help_text_body(parser) - local output = {} - - local position_text = _Private.get_parser_position_help_text(parser) - local flag_text = _Private.get_parser_flag_help_text(parser) - local child_parser_text = _Private.get_parser_child_parser_help_text(parser) - - _insert_if_value(output, position_text) - _insert_if_value(output, child_parser_text) - _insert_if_value(output, flag_text) - - return output -end - ---- Get the help message for a typical position parameter. ---- ----@param position cmdparse.Parameter A regular parameter. Not the `"--foo"` kinds. ----@return string # The created help message. ---- -function M.get_position_usage_help_text(position) - local text = _Private.get_parameter_usage_help_text(position) - - if type(position.count) == "string" then - text = text .. position.count - end - - return text -end - ---- Print `text` to the user. ---- ----@param text string a formatted help message to send. ---- -function M.show_help(text) - vim.notify(text, vim.log.levels.INFO) -end - -return M diff --git a/lua/plugin_template/_cli/cmdparse/init.lua b/lua/plugin_template/_cli/cmdparse/init.lua deleted file mode 100644 index 14350af..0000000 --- a/lua/plugin_template/_cli/cmdparse/init.lua +++ /dev/null @@ -1,2396 +0,0 @@ ---- Parse text into positional / named arguments. ---- ----@module 'plugin_template._cli.cmdparse' ---- - -local argparse = require("plugin_template._cli.argparse") -local argparse_helper = require("plugin_template._cli.argparse_helper") -local configuration = require("plugin_template._core.configuration") -local constant = require("plugin_template._cli.cmdparse.constant") -local evaluator = require("plugin_template._cli.cmdparse.evaluator") -local help_message = require("plugin_template._cli.cmdparse.help_message") -local iterator_helper = require("plugin_template._cli.cmdparse.iterator_helper") -local matcher = require("plugin_template._cli.cmdparse.matcher") -local sorter = require("plugin_template._cli.cmdparse.sorter") -local tabler = require("plugin_template._core.tabler") -local text_parse = require("plugin_template._cli.cmdparse.text_parse") -local texter = require("plugin_template._core.texter") -local types_input = require("plugin_template._cli.cmdparse.types_input") - ----@alias cmdparse.Action "append" | "count" | "store_false" | "store_true" | fun(data: cmdparse.ActionData): nil ---- This controls the behavior of how parsed arguments are added into the ---- final parsed `cmdparse.Namespace`. - ----@alias cmdparse.Namespace table All parsed values. - ----@alias cmdparse.MultiNumber number | "*" | "+" ---- The number of elements needed to satisfy a parameter. * == 0-or-more. ---- + == 1-or-more. A number means "we need exactly this number of ---- elements". - ----@class cmdparse.ActionData ---- A struct of data that gets passed to an Parameter's action. ----@field name string ---- The parameter name to set/append/etc some `value`. ----@field namespace cmdparse.Namespace ---- The container where a parsed argument + value will go into. This ---- object gets directly modified when an action is called. ----@field value any ---- A value to add into `namespace`. - ----@class cmdparse.ChoiceData ---- The information that gets passed to a typical `option.choices(...)` call. ----@field contexts cmdparse.ChoiceContext[] ---- Extra information about what caused `choices()` to be called. For ---- example we pass information like "I am currently auto-completing" or ---- other details using this value. ----@field current_value (string | string[])? ---- If the argument has an existing-written value written by the user, this ---- text is passed as `current_value`. - ----@class cmdparse.ParameterInputOptions ---- All of the settings to include in a new parameter. ----@field action cmdparse.Action? ---- This controls the behavior of how parsed arguments are added into the ---- final parsed `cmdparse.Namespace`. ----@field choices (string[] | fun(data: cmdparse.ChoiceData?): string[])? ---- If included, the parameter can only accept these choices as values. ----@field count cmdparse.MultiNumber? ---- The number of times that this parameter must be written. ----@field default any? ---- When this parameter is visited, this value is added to the returned ---- `cmdparse.Namespace` assuming no other value overwrites it. ----@field destination string? ---- When a parsed `cmdparse.Namespace` is created, this field is used to store ---- the final parsed value(s). If no `destination` is given an ---- automatically assigned name is used instead. ----@field help string ---- Explain what this parser is meant to do and the parameter(s) it needs. ---- Keep it brief (< 88 characters). ----@field name string? ---- The ways to refer to this instance. ----@field names string[]? ---- The ways to refer to this instance. ----@field nargs cmdparse.MultiNumber? ---- The number of elements that this parameter consumes at once. ----@field parent cmdparse.ParameterParser? ---- The parser that owns this instance. ----@field required boolean? ---- If `true`, this parameter must get satisfying value(s) before the ---- parser is complete. If `false` then the parameter doesn't need to be ---- defined as an argument. ----@field type ("number" | "string" | fun(value: string): any)? ---- The expected output type. If a function is given, assume that the user ---- knows what they're doing and use their function's return value. ----@field value_hint string? ---- Extra text to include in --help messages. Usually to indicate ---- the sort of value that a position / named argument needs. - ----@class cmdparse.ParameterOptions: cmdparse.ParameterInputOptions ---- All of the settings to include in a new parameter. ----@field choices (fun(data: cmdparse.ChoiceData?): string[])? ---- If included, the parameter can only accept these choices as values. ----@field required boolean ---- If `true`, this parameter must get satisfying value(s) before the ---- parser is complete. If `false` then the parameter doesn't need to be ---- defined as an argument. ----@field type (fun(value: string): any)? ---- The expected output type. If a function is given, assume that the user ---- knows what they're doing and use their function's return value. - ----@class cmdparse.ParameterParserInputOptions ---- The options that we might pass to `cmdparse.ParameterParser.new`. ----@field choices (string[] | fun(data: cmdparse.ChoiceData?): string[])? ---- If included, the parameter can only accept these choices as values. ----@field help string ---- Explain what this parser is meant to do and the parameter(s) it needs. ---- Keep it brief (< 88 characters). ----@field name string? ---- The parser name. This only needed if this parser has a parent subparser. ----@field parent cmdparse.Subparsers? ---- A subparser that own this `cmdparse.ParameterParser`, if any. - ----@class cmdparse.ParameterParserOptions: cmdparse.ParameterParserInputOptions ---- The options that we might pass to `cmdparse.ParameterParser.new`. ----@field choices (fun(data: cmdparse.ChoiceData?): string[])? ---- If included, the parameter can only accept these choices as values. - ----@class cmdparse.SubparsersOptions ---- Customization options for the new cmdparse.Subparsers. ----@field destination string? ---- An internal name to track this subparser group. ----@field help string ---- Explain what types of parsers this object is meant to hold Keep it ---- brief (< 88 characters). ----@field name string ---- The identifier for all parsers under this instance. ----@field parent cmdparse.ParameterParser? ---- The parser that owns this instance, if any. ----@field required boolean? ---- If `true` then one of the parser children must be matched or the user's ---- argument input is considered invalid. If `false` then the inner parser ---- does not have to be explicitly written. Defaults to false. - ----@class cmdparse.SubparsersInputOptions: cmdparse.SubparsersOptions ---- Customization options for the new cmdparse.Subparsers. ----@field [1] string? ---- A shorthand for the subparser name. - ----@class cmdparse._core.DisplayOptions ---- Control minor behaviors of this function. e.g. What data to show. ----@field excluded_names string[]? ---- Prevent parameters from returning from functions if they are in this ---- list. e.g. don't show any parameter in during auto-completion if it is ---- in `excluded_names`. - -local vlog = require("plugin_template._vendors.vlog") - -local M = {} -local _Private = {} - ----@class cmdparse.Parameter ---- An optional / required parameter for some parser. ----@field action cmdparse.Action? ---- This controls the behavior of how parsed parameters are added into the ---- final parsed `cmdparse.Namespace`. ----@field destination string? ---- When a parsed `cmdparse.Namespace` is created, this field is used to store ---- the final parsed value(s). If no `destination` is given an ---- automatically assigned name is used instead. ---- -M.Parameter = { - __tostring = function(parameter) - return string.format( - "cmdparse.Parameter({names=%s, help=%s, type=%s, action=%s, " - .. "nargs=%s, choices=%s, count=%s, required=%s, used=%s})", - vim.inspect(parameter.names), - vim.inspect(parameter.help), - vim.inspect(parameter.type), - vim.inspect(parameter._action), - vim.inspect(parameter._nargs), - vim.inspect(parameter.choices), - vim.inspect(parameter.count), - parameter.required, - vim.inspect(parameter._used) - ) - end, -} -M.Parameter.__index = M.Parameter - ----@class cmdparse.ParameterParser ---- A starting point for parameters (positional parameters, flag parameters, etc). ----@field choices (fun(data: cmdparse.ChoiceData?): string[])? ---- If included, this parser can be referred to using these names instead of its expected name. ----@field help string ---- Explain what this parser is meant to do and the parameter(s) it needs. ---- Keep it brief (< 88 characters). ----@field name string? ---- The parser name. This only needed if this parser has a parent subparser. ---- -M.ParameterParser = { - __tostring = function(parser) - return string.format( - 'cmdparse.ParameterParser({name="%s", help="%s", choices=%s})', - parser.name, - parser.help, - parser.choices - ) - end, -} -M.ParameterParser.__index = M.ParameterParser - ----@class cmdparse.Subparsers A group of parsers. -M.Subparsers = { - __tostring = function(subparsers) - return string.format( - 'cmdparse.Subparsers({help="%s", destination="%s"})', - subparsers.help, - subparsers.destination - ) - end, -} -M.Subparsers.__index = M.Subparsers - ---- Check if `data` wants to show "--help" flags during cmdparse commands. ---- ----@param data plugin_template.ConfigurationCmdparseAutoComplete? ---- The user settings to read from, if any. If no data is given, the user's ---- default configuration is used insteand. ----@return boolean ---- If `true` then the --help flag should be down. If `false`, don't. ---- -local function _is_help_flag_enabled(data) - local value - - if data then - value = tabler.get_value(data, { "display", "help_flag" }) - else - value = tabler.get_value(configuration.DATA, { "cmdparse", "auto_complete", "display", "help_flag" }) - end - - if value == nil then - return true - end - - return value -end - ---- Check if `object` is a `cmdparse.ParameterParser`. ---- ----@param object any Anything. ----@return boolean # If match, return `true`. ---- -local function _is_parser(object) - return object._flag_parameters ~= nil -end - ---- Check if `object` is a `cmdparse.Parameter`. ---- ----@param object any Anything. ----@return boolean # If match, return `true`. ---- -local function _is_parameter(object) - return object._parent and not _is_parser(object) -end - ---- Find all child parsers, recursively. ---- ---- Note: ---- This function is **inclusive**, meaning `parser` will be returned. ---- ----@param parser cmdparse.ParameterParser The starting point to look for parsers. ----@return cmdparse.ParameterParser[] # All found `parser` + child parsers. ---- -local function _get_all_parsers(parser) - local stack = { parser } - local output = {} - - while #stack > 0 do - local current = table.remove(stack) - - if not current then - break - end - - table.insert(output, current) - - for _, subparsers in ipairs(current:get_subparsers()) do - vim.list_extend(stack, subparsers:get_parsers()) - end - end - - return output -end - ---- Find the first subparser that matches `prefix`, if any. ---- ----@param parser cmdparse.ParameterParser The starting point to look for parsers. ----@param prefix string An expected name. e.g. `"sub-command"`. ----@return cmdparse.ParameterParser? # The found subparser, if any. ---- -local function _get_child_parser_by_name(parser, prefix) - for parser_ in iterator_helper.iter_parsers(parser) do - if vim.tbl_contains(parser_:get_names(), prefix) then - return parser_ - end - end - - return nil -end - ---- Find all child parser names under `parser`. ---- ----@param parser cmdparse.ParameterParser The starting point to look for child parsers. ----@return string[] # All parser names, if any are defined. ---- -local function _get_child_parser_names(parser) - return vim.iter(iterator_helper.iter_parsers(parser)) - :map(function(parser_) - return parser_:get_names()[1] - end) - :totable() -end - ---- Scan `input` and stop processing arguments after `column`. ---- ----@param input argparse.Results ---- The user's parsed text. ----@param column number ---- The point to stop checking for arguments. Must be a 1-or-greater value. ----@return number ---- The found index. If all arguments are < `column` then the returning ---- index will cover all of `input.arguments`. ---- -local function _get_cursor_offset(input, column) - for index, argument in ipairs(input.arguments) do - if argument.range.end_column == column then - return index - elseif argument.range.end_column > column then - return index - 1 - end - end - - return #input.arguments -end - ---- Complete values for `argument`, using choices from `parameter`. ---- ----@param parameter cmdparse.Parameter ---- The named parameter to query from. ----@param argument argparse.Argument ---- The actual user input to include in the auto-complete result. ----@param contexts cmdparse.ChoiceContext[] ---- Extra information about what caused `choices()` to be called. ----@return string[] ---- The generated output. e.g. `--foo=bar`, `--foo=fizz`, `--foo=buzz`, etc. ---- -local function _get_named_argument_completion_choices(parameter, argument, contexts) - if not parameter.choices then - return {} - end - - local prefix = argument.name - local current_value = argument.value - ---@cast current_value string - local output = {} - - for _, choice in - ipairs(parameter.choices({ - contexts = vim.list_extend({ constant.ChoiceContext.value_matching }, contexts), - current_value = current_value, - })) - do - table.insert(output, string.format("%s=%s", prefix, choice)) - end - - return output -end - ---- Decide from `data` what should be displayed to a user (e.g. during auto-complete). ---- ----@param options plugin_template.ConfigurationCmdparseAutoComplete? ---- The user settings to read from, if any. If no data is given, the user's ---- default configuration is used insteand. ----@return cmdparse._core.DisplayOptions ---- If `true` then the --help flag should be down. If `false`, don't. ---- -local function _get_display_options(options) - local output = { excluded_names = {} } - - if not _is_help_flag_enabled(options) then - vim.list_extend(output.excluded_names, help_message.HELP_NAMES) - end - - return output -end - ---- Get the next parameter that matches `argument` or fallback to `parameter`. ---- ---- If `parameter` is a position then `argument` must a new parameter ---- because `parameter` would only be found if its arguments were satisfied. ---- ---- If `parameter` is a flag then and `argument` is a position then ---- `argument` could either be the starting value for another parameter or ---- an additional value for `parameter`. We aren't sure. ---- ---- If `parameter is a flag and `argument` is not a position then `argument` ---- is definitely a starting value for another parameter. ---- ----@param parser cmdparse.ParameterParser The direct parent to look within. ----@param parameter cmdparse.Parameter A fallback parameter to use. ----@param argument argparse.Argument The next position that we think we can match. ----@return cmdparse.Parameter # The recommended parameter that should be used next. ---- -local function _get_next_parameter_if_needed(parser, parameter, argument) - if parameter:is_position() then - if argument.argument_type == argparse.ArgumentType.position then - local positions = parser:get_position_parameters() - - for _, position in ipairs(positions) do - if not position:is_exhausted() then - return position - end - end - - return positions[#positions] - end - - error("Bug encountered. Normally this situation cannot come up. Add a better error message here.") - end - - if argument.argument_type ~= argparse.ArgumentType.position then - local name = argument.name - - for _, flag in ipairs(parser:get_flag_parameters()) do - if not flag:is_exhausted() and not vim.tbl_isempty(texter.get_array_startswith(flag.names, name)) then - return flag - end - end - - return parameter - end - - -- NOTE: We aren't sure if `argument` is for `parameter` or another, next parameter. - -- Until we have concrete cases let's just return the original parameter. - -- - return parameter -end - ---- Get the label / text of `arguments`. ---- ----@param arguments argparse.PositionArgument[] # Each value to serialize. ----@return string[] # The found labels, e.g. `{"foo", "bar", ...}`. ---- -local function _get_position_argument_values(arguments) - return vim.iter(arguments) - :map(function(argument) - return argument.value - end) - :totable() -end - ---- Check `position` for matching, contiguous `arguments`. ---- ----@param position cmdparse.Parameter ---- The `foo`, `bar`, etc parameter to check. ----@param arguments argparse.Argument[] ---- The arguments to match against `positions`. Every element in `arguments` ---- is checked. ----@return number ---- The number of `arguments` that match `position`'s requirements. ---- -local function _get_used_position_arguments_count(position, arguments) - local function _error(index, nargs) - local template = 'Parameter "%s" requires "%s" values. Got "%s"' - - if index < 2 then - template = template .. " value." - else - template = template .. " values." - end - - error(string.format(template, position.names[1], nargs, index), 0) - end - - local nargs = position:get_nargs() - - if type(nargs) == "number" then - for index = 1, nargs do - local argument = arguments[index] - - if not argument then - _error(index - 1, nargs) - end - - if argument.argument_type ~= argparse.ArgumentType.position then - _error(index, nargs) - end - end - - return nargs - end - - return _Private.validate_variable_position_arguments(nargs, arguments, position.names[1]) -end - ---- Combined `namespace` with all other `...` namespaces. ---- ----@param namespace cmdparse.Namespace ---- The starting namespace that will be modified. ----@param ... cmdparse.Namespace[] ---- All other namespaces to merge into `namespace`. Later entries will ---- override previous entries. ---- -local function _merge_namespaces(namespace, ...) - for _, override in ipairs({ ... }) do - for key, value in pairs(override) do - namespace[key] = value - end - end -end - ---- Convert `values` according to `type_converter`. ---- ----@param type_converter fun(data: any): any ----@param values (boolean | string | string[])? The values to convert. ----@return any # The converted value(s). ---- -local function _resolve_value(type_converter, values) - if type(values) ~= "table" then - return type_converter(values) - end - - local output = {} - - for _, value in ipairs(values) do - table.insert(output, type_converter(value)) - end - - return output -end - ---- Remove the ending `index` options from `input`. ---- ----@param input argparse.Results ---- The parsed arguments + any remainder text. ----@param column number ---- The found index. If all arguments are < `column` then the returning ---- index will cover all of `input.arguments`. ----@return argparse.Results ---- The stripped copy from `input`. ---- -local function _rstrip_input(input, column) - local stripped = argparse_helper.rstrip_arguments(input, _get_cursor_offset(input, column)) - - local last = stripped.arguments[#stripped.arguments] - - if last then - stripped.remainder.value = input.text:sub(last.range.end_column + 1, column) - else - stripped.remainder.value = input.text:sub(1, column) - end - - stripped.text = input.text:sub(1, column) - - return stripped -end - ---- Check if `argument` is available to parse in `parser`. ---- ----@param argument argparse.Argument ---- Some position, flag, or named user argument. ----@param parser cmdparse.ParameterParser ---- The direct parent to look within. ----@param contexts cmdparse.ChoiceContext[]? ---- A description of how / when this function is called. It gets passed to ---- `cmdparse.Parameter.choices()`. ----@return boolean ---- If `argument` has a parameter that matches, return `true`. ---- -local function _validate_last_argument(argument, parser, contexts) - contexts = contexts or {} - - if argument.argument_type == argparse.ArgumentType.position then - local argument_name = text_parse.get_argument_name(argument) - - if vim.tbl_contains(parser:get_names(), argument_name) then - return true - end - - for _, position in ipairs(parser:get_position_parameters()) do - if not position:is_exhausted() then - if not position.choices then - return true - else - local value = text_parse.get_argument_value_text(argument) - local choices = position.choices({ - contexts = vim.list_extend({ constant.ChoiceContext.value_matching }, contexts), - current_value = value, - }) - - return vim.tbl_contains(choices, value) - end - end - end - - return false - elseif argument.argument_type == argparse.ArgumentType.flag then - local argument_name = text_parse.get_argument_name(argument) - - for _, flag in ipairs(parser:get_flag_parameters()) do - if not flag:is_exhausted() and vim.tbl_contains(flag.names, argument_name) then - return true - end - end - - return false - elseif argument.argument_type == argparse.ArgumentType.named then - local argument_name = text_parse.get_argument_name(argument) - - for _, named in ipairs(parser:get_flag_parameters()) do - if not named:is_exhausted() and vim.tbl_contains(named.names, argument_name) then - if not named.choices then - return true - else - local value = text_parse.get_argument_value_text(argument) - local choices = named.choices({ - contexts = vim.list_extend({ constant.ChoiceContext.value_matching }, contexts), - current_value = value, - }) - - if vim.tbl_contains(choices, value) then - return true - end - end - end - end - - return false - end - - vlog.fmt_error('Unknown argument type "%s" found and we don\'t know how to check it.', argument) - - return false -end - ---- Get the "top line" of a typical --help message. ---- ----@param parser cmdparse.ParameterParser The root parser to get a summary for. ----@return string # A one/two liner explanation of this instance's expected parameters. ---- -function _Private.get_usage_summary(parser) - local output = {} - - local names = parser:get_names() - - if #names == 1 then - if parser:get_parent_parser() then - table.insert(output, "{" .. names[1] .. "}") - else - table.insert(output, names[1]) - end - else - if not vim.tbl_isempty(names) then - table.insert(output, help_message.get_help_command_labels(names)) - end - end - - for _, position in ipairs(parser:get_position_parameters()) do - table.insert(output, help_message.get_position_usage_help_text(position)) - end - - for _, flag in ipairs(iterator_helper.sort_parameters(parser:get_flag_parameters({ hide_implicits = true }))) do - table.insert(output, help_message.get_flag_help_text(flag)) - end - - local parser_names = _get_child_parser_names(parser) - - if not vim.tbl_isempty(parser_names) then - table.insert(output, string.format("{%s}", vim.fn.join(vim.fn.sort(parser_names), ","))) - end - - for _, flag in ipairs(iterator_helper.sort_parameters(parser:get_implicit_flag_parameters())) do - table.insert(output, string.format("[%s]", flag:get_raw_name())) - end - - return help_message.HELP_MESSAGE_PREFIX .. vim.fn.join(output, " ") -end - ---- Make sure `arguments` satisfies `nargs`. ---- ---- Raises: ---- If `arguments` fails to meet the requirements of `nargs`. ---- ----@param nargs number | cmdparse.Counter ---- A fixed number or "0-or-more" or "1-or-more" etc. Basically it's the ---- condition that this function must satisfy or raise an exception. ----@param arguments argparse.Argument[] ---- All user input to consider. ----@param name string ---- The parameter where this validation originated from. Used just for the ---- error message. ----@return number ---- The number of "satisfying" `arguments`. If `nargs` is a number then this ---- return value will always be `nargs`. But if `nargs` is "0-or-more" or ---- "1-or-more" etc then the returned number could be less-than-or-equal to `nargs`. ---- -function _Private.validate_variable_position_arguments(nargs, arguments, name) - local found = 0 - - for index, argument in ipairs(arguments) do - if argument.argument_type ~= argparse.ArgumentType.position then - return found - end - - found = index - end - - if nargs == constant.Counter.one_or_more then - if found == 0 then - error(string.format('Parameter "%s" requires a value.', name), 0) - end - elseif type(nargs) == "number" then - if found < nargs then - local template = 'Parameter "%s" requires "%s" values. Got "%s"' - - if found == 1 then - template = template .. " value." - else - template = template .. " values." - end - - error(string.format(template, name, nargs, found), 0) - end - end - - return found -end - ---- Create a new group of parsers. ---- ----@param options cmdparse.SubparsersInputOptions | cmdparse.SubparsersOptions ---- Customization options for the new cmdparse.Subparsers. ----@return cmdparse.Subparsers ---- A group of parsers (which will be filled with parsers later). ---- -function M.Subparsers.new(options) - if not options.name and options[1] then - options.name = options[1] - end - ---@cast options cmdparse.SubparsersOptions - - --- @class cmdparse.Subparsers - local self = setmetatable({}, M.Subparsers) - - self.name = options.name - self.visited = false -- NOTE: Noting when a child parser is used / touched - self._parent = options.parent - self._parsers = {} - - self.help = options.help - self.required = options.required or false - - return self -end - ---- Create a new `cmdparse.ParameterParser` using `options`. ---- ----@param options cmdparse.ParameterParserInputOptions | cmdparse.ParameterParserOptions | cmdparse.ParameterParser ---- The options to pass to `cmdparse.ParameterParser.new`. ----@return cmdparse.ParameterParser ---- The created parser. ---- -function M.Subparsers:add_parser(options) - if _is_parser(options) then - ---@cast options cmdparse.ParameterParser - options:set_parent(self) - table.insert(self._parsers, options) - - return options - end - - ---@cast options cmdparse.ParameterParserInputOptions | cmdparse.ParameterParserOptions - local new_options = vim.tbl_deep_extend("force", options, { parent = self }) - local parser = M.ParameterParser.new(new_options) - - table.insert(self._parsers, parser) - - return parser -end - ----@return cmdparse.ParameterParser[] # Get all of the child parsers for this instance. -function M.Subparsers:get_parsers() - return self._parsers -end - ---- Create a new instance using `options`. ---- ----@param options cmdparse.ParameterOptions All of the settings to include in a new parse argument. ----@return cmdparse.Parameter # The created instance. ---- -function M.Parameter.new(options) - --- @class cmdparse.Parameter - local self = setmetatable({}, M.Parameter) - - self._action = nil - self._action_type = nil - self._nargs = options.nargs or 1 - self._type = options.type - self._used = 0 - self.choices = options.choices - self.count = options.count or 1 - self.default = options.default - self.names = options.names - self.help = options.help - self.destination = text_parse.get_nice_name(options.destination or options.names[1]) - self:set_action(options.action) - self.required = options.required - self.value_hint = options.value_hint - self._parent = options.parent - - return self -end - ----@return boolean # Check if this parameter expects a fixed number of uses. -function M.Parameter:has_numeric_count() - return type(self.count) == "number" -end - ----@return boolean # Check if this instance cannot be used anymore. -function M.Parameter:is_exhausted() - if self.count == constant.Counter.zero_or_more then - return false - end - - return self._used >= self.count -end - ----@return boolean # If this instance is a flag like `--foo` or `--foo=bar`, return `false`. -function M.Parameter:is_position() - return text_parse.is_position_name(self.names[1]) -end - ----@return fun(data: cmdparse.ActionData): nil # A function that directly modifies the contents of `data`. -function M.Parameter:get_action() - return self._action -end - ----@return cmdparse.Action # The original action type. e.g. `"store_true"`. -function M.Parameter:get_action_type() - return self._action_type -end - ----@return cmdparse.MultiNumber # The number of elements that this argument consumes at once. -function M.Parameter:get_nargs() - return self._nargs -end - ----@return string # The (clean) argument mame. e.g. `"--foo"` becomes `"foo"`. -function M.Parameter:get_nice_name() - return text_parse.get_nice_name(self.destination or self.names[1]) -end - ----@return string # The (raw) argument mame. e.g. `"--foo"`. -function M.Parameter:get_raw_name() - return self.names[1] -end - ---- Get a converter function that takes in a raw argument's text and outputs some converted result. ---- ----@return fun(value: (string | boolean)?): any # The converter function. ---- -function M.Parameter:get_type() - return self._type -end - ---- Use up more of the available use(s) of this instance. ---- ---- Most arguments can only be used one time but some can be used multiple ---- times. This function takes up at least one of these available uses. ---- ----@param increment number? The number of uses to consume. ---- -function M.Parameter:increment_used(increment) - increment = increment or 1 - self._used = self._used + increment -end - ---- Describe how this argument should ingest new CLI value(s). ---- ----@param action cmdparse.Action The selected functionality. ---- -function M.Parameter:set_action(action) - self._action_type = action - - if action == constant.Action.store_false then - action = function(data) - ---@cast data cmdparse.ActionData - data.namespace[data.name] = false - end - elseif action == constant.Action.store_true then - action = function(data) - ---@cast data cmdparse.ActionData - data.namespace[data.name] = true - end - elseif action == constant.Action.count then - action = function(data) - ---@cast data cmdparse.ActionData - local name = data.name - local namespace = data.namespace - - if not namespace[name] then - namespace[name] = 0 - end - - namespace[name] = namespace[name] + 1 - end - elseif action == "append" then - action = function(data) - ---@cast data cmdparse.ActionData - local name = data.name - local namespace = data.namespace - - if not namespace[name] then - namespace[name] = {} - end - - table.insert(namespace[name], data.value) - end - elseif type(action) == "function" then - action = action - else - action = function(data) - ---@cast data cmdparse.ActionData - data.namespace[data.name] = data.value - end - end - - self._action = action -end - ---- Tell how many value(s) are needed to satisfy this instance. ---- ---- e.g. nargs=2 means that every time this instance is detected there need to ---- be at least 2 values to ingest or it is not valid CLI input. ---- ---- Raises: ---- If `count` isn't a valid input. ---- ----@param count string | number ---- The number of values we need for this instance. `"*"` == 0-or-more, ---- `"+"` == 1-or-more. A number means there needs to exactly that many ---- arguments (no less no more). ---- -function M.Parameter:set_nargs(count) - if type(count) == "string" then - if count == "*" then - count = constant.Counter.zero_or_more - elseif count == "+" then - count = constant.Counter.one_or_more - end - - error(string.format('The given string count "%s" must be + or * or a number.')) - end - - self._nargs = count -end - ---- Create a new `cmdparse.ParameterParser`. ---- ---- If the parser is a child of a subparser then this instance must be given ---- a name via `{name="foo"}` or this function will error. ---- ----@param options cmdparse.ParameterParserInputOptions | cmdparse.ParameterParserOptions ---- The options that we might pass to `cmdparse.ParameterParser.new`. ----@return cmdparse.ParameterParser ---- The created instance. ---- -function M.ParameterParser.new(options) - if options[1] and not options.name then - options.name = options[1] - end - - if options.parent then - ---@cast options cmdparse.ParameterParserOptions - types_input.validate_name(options) - end - - types_input.expand_choices_options(options) - --- @cast options cmdparse.ParameterParserOptions - - --- @class cmdparse.ParameterParser - local self = setmetatable({}, M.ParameterParser) - - self.name = options.name - self.choices = options.choices - self.help = options.help - self._defaults = {} - self._position_parameters = {} - self._flag_parameters = {} - self._subparsers = {} - self._parent = options.parent - - self._implicit_flag_parameters = {} - self:_add_help_parameter() - - return self -end - ---- Parse `arguments` and get the help summary line (the top "Usage: ..." line). ---- ----@param data argparse.Results ---- User text that needs to be parsed. ----@param parser cmdparse.ParameterParser? ---- The root parser to get a summary for. If no parser is given, ---- we auto-find it using `data`. ----@return string ---- The found "Usage: ..." line. ----@return cmdparse.ParameterParser ---- The lowest parser that was found during parsing. ---- -function M.ParameterParser:_get_argument_usage_summary(data, parser) - if not parser then - parser = self:_compute_matching_parsers(data) - end - - if parser:is_satisfied() then - local last = data.arguments[#data.arguments] - - if last then - local last_name = text_parse.get_argument_name(last) - parser = _get_child_parser_by_name(parser, last_name) or parser - end - end - - local summary = _Private.get_usage_summary(parser) - - return summary, parser -end - ----@return string[] # Find all unfinished parameters in this instance. -function M.ParameterParser:_get_issues() - local output = {} - - for parameter in tabler.chain(self:get_flag_parameters(), self:get_position_parameters()) do - if parameter.required and not parameter:is_exhausted() then - if parameter:has_numeric_count() then - local used = parameter._used - local text - - if used == 0 then - text = string.format('Parameter "%s" must be defined.', parameter.names[1]) - else - text = string.format( - 'Parameter "%s" used "%s" times but must be used "%s" times.', - parameter.names[1], - parameter._used, - parameter.count - ) - end - - if parameter.choices then - text = string.format( - '%s Valid choices are "%s"', - text, - vim.fn.join( - vim.fn.sorted(parameter.choices({ contexts = { constant.ChoiceContext.error_message } })), - ", " - ) - ) - end - - table.insert(output, text) - end - end - end - - return output -end - ---- Get auto-complete options based on this instance + the user's `data` input. ---- ----@param data argparse.Results | string ---- The user input. ----@param column number? ---- A 1-or-more value that represents the user's cursor. ----@param options plugin_template.ConfigurationCmdparseAutoComplete? ---- The user settings to read from, if any. If no data is given, the user's ---- default configuration is used insteand. ----@return string[] ---- All found auto-complete options, if any. ---- -function M.ParameterParser:_get_completion(data, column, options) - if type(data) == "string" then - data = argparse.parse_arguments(data) - end - - local display_options = _get_display_options(options) - local count = #data.text - column = column or count - local stripped = _rstrip_input(data, column) - local remainder = stripped.remainder.value - local output = {} - - if vim.tbl_isempty(stripped.arguments) then - if column ~= count then - -- NOTE: The user is in an unknown column but with no arguments. - -- This probably is an error, just return nothing. - -- - return {} - end - - if not texter.is_whitespace(remainder) then - vim.list_extend( - output, - matcher.get_matching_partial_flag_text( - remainder, - self:get_flag_parameters(), - nil, - { constant.ChoiceContext.auto_completing }, - display_options - ) - ) - - -- NOTE: If there was unparsed text then it means that the user is - -- in the middle of an argument. We don't want to show completion - -- options in that situation. - -- - return output - end - - vim.list_extend(output, matcher.get_current_parser_completions(self, display_options)) - - return output - end - - local contexts = { constant.ChoiceContext.auto_completing } - local parser, recent_item, index = self:_compute_matching_parsers(stripped, contexts) - local finished = index == #stripped.arguments - - if not finished then - vlog.fmt_error('Could not fully parse "%s".', stripped) - - return {} - end - - local last = stripped.arguments[#stripped.arguments] - local last_name = text_parse.get_argument_name(last) - - local is_allowed_to_match_partial_results = remainder == "" - - if is_allowed_to_match_partial_results then - if _is_parser(recent_item) then - local last_value = text_parse.get_argument_value_text(last) - ---@cast recent_item cmdparse.ParameterParser - vim.list_extend( - output, - matcher.get_parser_exact_or_partial_matches(recent_item, last_name, last_value, contexts) - ) - elseif text_parse.is_incomplete_named_argument(last) then - ---@cast recent_item cmdparse.Parameter - local parameter = _get_next_parameter_if_needed(parser, recent_item, last) - vim.list_extend(output, _get_named_argument_completion_choices(parameter, last, contexts)) - elseif _is_parameter(recent_item) then - ---@cast recent_item cmdparse.Parameter - local parameter = _get_next_parameter_if_needed(parser, recent_item, last) - - if not parameter then - return {} - end - - vim.list_extend( - output, - matcher.get_exact_or_partial_matches(parameter, last, parser, contexts, display_options) - ) - else - error(string.format('Bug found. Item "%s" is unknown.', vim.inspect(recent_item))) - end - - if parser:is_satisfied() then - for parser_ in iterator_helper.iter_parsers(parser) do - vim.list_extend(output, texter.get_array_startswith(parser_:get_names(), last_name)) - end - end - - return output - end - - local child_parser = matcher.get_exact_subparser_child(last_name, parser) - - if child_parser then - -- NOTE: The last, parsed argument is a subparser. So we use it - parser = child_parser - end - - if not is_allowed_to_match_partial_results then - local success = _validate_last_argument(last, parser, contexts) - - if not success then - return {} - end - end - - if not child_parser then - -- NOTE: If the last argument isn't a parser then it has to be - -- a argument that matches a parameter. Find it and make sure - -- that parameter calls `increment_used()`! - -- - local next_index = index - local argument_name = text_parse.get_argument_name(stripped.arguments[next_index]) - - local found = evaluator.compute_and_increment_parameter( - parser, - argument_name, - tabler.get_slice(stripped.arguments, next_index) - ) - - if not found then - error("Bug found - This shouldn't be able to happen! Fix!", 0) - end - end - - local stripped_remainder = texter.lstrip(remainder) - - if parser:is_satisfied() then - vim.list_extend(output, vim.fn.sort(matcher.get_matching_subparser_names("", parser))) - end - - vim.list_extend( - output, - matcher.get_matching_position_parameters(stripped_remainder, parser:get_position_parameters(), contexts) - ) - - local prefix - - if texter.is_whitespace(remainder) then - prefix = "" - else - prefix = text_parse.get_argument_name(last) - end - - vim.list_extend( - output, - matcher.get_matching_partial_flag_text( - prefix, - parser:get_flag_parameters(), - text_parse.get_argument_value_text(last), - contexts, - display_options - ) - ) - - return output -end - ----@return cmdparse.Namespace # All default values from all (direct) child parameters. -function M.ParameterParser:_get_default_namespace() - local output = {} - - for parameter in tabler.chain(self:get_position_parameters(), self:get_flag_parameters()) do - if parameter.default then - output[parameter:get_nice_name()] = parameter.default - else - local action = parameter:get_action_type() - - if action == constant.Action.store_true then - output[parameter:get_nice_name()] = false - elseif action == constant.Action.store_false then - output[parameter:get_nice_name()] = true - end - end - end - - return output -end - ---- Search recursively for the lowest possible `cmdparse.ParameterParser` from `data`. ---- ----@param data argparse.Results All of the arguments to consider. ----@return cmdparse.ParameterParser # The found parser, if any. ---- -function M.ParameterParser:_get_leaf_parser(data) - local parser = self - --- @cast parser cmdparse.ParameterParser - - for index, argument in ipairs(data.arguments) do - if argument.argument_type == argparse.ArgumentType.position then - local argument_name = text_parse.get_argument_name(argument) - - local found, found_parser = - parser:_handle_subparsers(argparse_helper.lstrip_arguments(data, index + 1), argument_name, {}) - - if not found or not found_parser then - break - end - - parser = found_parser - end - end - - return parser -end - ---- Make a `--help` parameter and add it to this current instance. -function M.ParameterParser:_add_help_parameter() - local parameter = self:add_parameter({ - action = function(data) - data.namespace.execute = function(...) -- luacheck: ignore 212 unused argument - help_message.show_help(self:get_full_help("")) - end - end, - help = "Show this help message and exit.", - names = help_message.HELP_NAMES, - nargs = 0, - }) - - -- NOTE: `self:add_parameter` just added the help flag to - -- `self._flag_parameters` so we need to remove it (so we can add it - -- somewhere else). - -- - table.remove(self._flag_parameters) - table.insert(self._implicit_flag_parameters, parameter) -end - ---- Add `flags` to `namespace` if they match `argument`. ---- ---- Raises: ---- If a flag is found and a value is expected but we fail to get a value for it. ---- ----@param flags cmdparse.Parameter[] ---- All `-f`, `--foo`, `-f=ttt`, and `--foo=ttt`, parameters to check. ----@param arguments argparse.Argument[] ---- The arguments to match against `flags`. If the first element in ---- `arguments` matches one of `flags`, the **remainder** of the arguments ---- are treated as **values** for the found parameter. ----@param namespace cmdparse.Namespace ---- A container for the found match(es). ----@param contexts cmdparse.ChoiceContext[]? ---- A description of how / when this function is called. It gets passed to ---- `cmdparse.Parameter.choices()`. ----@return boolean ---- If a match was found, return `true`. ----@return number ---- The number of arguments used by the found flag, if any. ---- -function M.ParameterParser:_handle_exact_flag_parameters(flags, arguments, namespace, contexts) - contexts = contexts or {} - - local function _needs_a_value(parameter) - local nargs = parameter._nargs - - if type(nargs) == "number" then - return nargs ~= 0 - end - - return nargs == constant.Counter.one_or_more - end - - local function _get_next_position_arguments(value_arguments) - for index = 1, #value_arguments do - local argument = value_arguments[index] - - if - not argument - or argument.argument_type == argparse.ArgumentType.flag - or argument.argument_type == argparse.ArgumentType.named - then - return tabler.get_slice(value_arguments, 1, index) - end - end - - return value_arguments - end - - --- Get all values from `value_arguments` according to `flag`. - --- - --- Raises: - --- If `value_arguments` does not satisfy `flag`. - --- - ---@param flag cmdparse.Parameter - --- The option to get values for, if needed. - ---@param value_arguments argparse.Argument[] - --- All of the values that we think could be related to `flag`. - ---@return (string[] | string)? - --- The found value, if any. - --- - local function _get_flag_values(flag, value_arguments) - local nargs = flag:get_nargs() - - -- TODO: Need to handle expressions, probably - - if nargs == constant.Counter.one_or_more then - local found_arguments = _get_next_position_arguments(value_arguments) - - if vim.tbl_isempty(found_arguments) then - error(string.format('Parameter "%s" requires 1-or-more values. Got none.', flag.names[1]), 0) - end - - return _get_position_argument_values(found_arguments) - end - - if nargs == constant.Counter.zero_or_more then - local found_arguments = _get_next_position_arguments(value_arguments) - - return _get_position_argument_values(found_arguments) - end - - if type(nargs) == "number" then - if nargs == 0 then - return nil - end - - if nargs == 1 then - local argument = value_arguments[1] - - if - not argument - or argument.argument_type == argparse.ArgumentType.flag - or argument.argument_type == argparse.ArgumentType.named - then - return nil - end - - ---@cast argument argparse.PositionArgument - - return argument.value - end - - local values_count = #value_arguments - - if nargs > values_count then - local template = 'Parameter "%s" requires "%s" values. Got "%s"' - - if values_count > 1 then - error(string.format(template .. " values.", flag.names[1], nargs, values_count), 0) - else - error(string.format(template .. " value.", flag.names[1], nargs, values_count), 0) - end - end - - for index = 1, nargs do - local argument = value_arguments[index] - - if - not argument - or argument.argument_type == argparse.ArgumentType.flag - or argument.argument_type == argparse.ArgumentType.named - then - local template = 'Parameter "%s" requires "%s" values. Got "%s"' - local found_index = index - 1 - - if found_index == 1 then - template = template .. " value." - else - template = template .. " values." - end - - error(string.format(template, flag.names[1], nargs, found_index), 0) - end - - if index == nargs then - local arguments_ = tabler.get_slice(arguments, 1, nargs + 1) - - return _get_position_argument_values(arguments_) - end - end - - -- NOTE: This code shouldn't be possible because conditions above - -- should have covered all cases. - -- - vlog.error("Unexpected code path found. This is probably a bug. Fix!") - - local arguments_ = tabler.get_slice(arguments, 1, nargs + 1) - - return _get_position_argument_values(arguments_) - end - end - - local function _validate_value_choices(flag, values, choices, argument_name) - if choices == nil then - local expected = flag.choices({ - contexts = vim.list_extend({ constant.ChoiceContext.error_message }, contexts), - current_value = values, - }) - - error( - string.format( - 'Parameter "%s" got invalid "%s" value. Expected one of %s.', - flag.names[1], - values, - vim.inspect(vim.fn.sort(expected)) - ), - 0 - ) - end - - if type(values) == "table" then - local invalids = {} - - for _, value in ipairs(values) do - if not vim.tbl_contains(choices, value) then - table.insert(invalids, value) - end - end - - if vim.tbl_isempty(invalids) then - return - end - - local template = 'Parameter "%s" got invalid %s value. Expected one of %s.' - - if #invalids > 1 then - template = 'Parameter "%s" got invalid %s values. Expected one of %s.' - end - - error(string.format(template, argument_name, vim.inspect(invalids), vim.inspect(vim.fn.sort(choices))), 0) - end - - if not vim.tbl_contains(choices, values) then - error( - string.format( - 'Parameter "%s" got invalid %s value. Expected one of %s.', - argument_name, - vim.inspect(values), - vim.inspect(vim.fn.sort(choices)) - ), - 0 - ) - end - end - - local argument = arguments[1] - - if argument.argument_type == argparse.ArgumentType.named then - if not argument.value or argument.value == "" then - error(string.format('Parameter "%s" requires 1 value.', argument.name), 0) - end - end - - local value_arguments = tabler.get_slice(arguments, 2) - - for _, flag in ipairs(flags) do - if vim.tbl_contains(flag.names, argument.name) and not flag:is_exhausted() then - -- TODO: Need to handle expression statements here, I think. Somehow. - - local total = 1 -- NOTE: Always include the current argument in the total - local values - - if argument.argument_type == argparse.ArgumentType.named then - local nargs = flag:get_nargs() - - if type(nargs) == "number" and nargs ~= 1 then - error(string.format('Parameter "%s" requires "2" values. Got "1" value.', flag.names[1]), 0) - end - - values = argument.value - elseif argument.argument_type == argparse.ArgumentType.flag then - values = _get_flag_values(flag, value_arguments) - - if values then - if type(values) == "string" then - total = total + 1 - else - total = total + #values - end - end - end - - local current_value = values - ---@cast current_value (string[] | string)? - - if flag.choices then - local choices = flag.choices({ - contexts = vim.list_extend({ constant.ChoiceContext.value_matching }, contexts), - current_value = current_value, - }) - - _validate_value_choices(flag, values, choices, argument.name) - end - - local needs_a_value = _needs_a_value(flag) - - if needs_a_value then - if values == nil then - error( - string.format( - 'Parameter "%s" failed to find a value. This could be a parser bug!', - argument.name - ), - 0 - ) - end - end - - local name = flag:get_nice_name() - local value = _resolve_value(flag:get_type(), values) - - if needs_a_value then - if value == nil then - error( - string.format( - 'Parameter "%s" failed to find a value. Please check your `type` parameter and fix it!', - argument.name - ), - 0 - ) - end - end - - local action = flag:get_action() - - action({ namespace = namespace, name = name, value = value }) - - flag:increment_used() - - return true, total - end - end - - return false, 0 -end - ---- Add `positions` to `namespace` if they match `argument`. ---- ----@param positions cmdparse.Parameter[] ---- All `foo`, `bar`, etc parameters to check. ----@param arguments argparse.Argument[] ---- The arguments to match against `positions`. If a match is found, the ---- remainder of the arguments are treated as **values** for the found ---- parameter. ----@param namespace cmdparse.Namespace ---- A container for the found match(es). ----@param contexts cmdparse.ChoiceContext[]? ---- A description of how / when this function is called. It gets passed to ---- `cmdparse.Parameter.choices()`. ----@return boolean ---- If a match was found, return `true`. ----@return number ---- The number of arguments used by the found flag, if any. ----@return cmdparse.Parameter? ---- The matching parameter, if any. ---- -function M.ParameterParser:_handle_exact_position_parameters(positions, arguments, namespace, contexts) - local function _get_values(arguments_, count) - if count == 1 then - return arguments_[1].value - end - - return vim.iter(tabler.get_slice(arguments_, 1, count)) - :map(function(argument_) - return argument_.name or argument_.value - end) - :totable() - end - - contexts = contexts or {} - - for _, position in ipairs(positions) do - if not position:is_exhausted() then - local total = _get_used_position_arguments_count(position, arguments) - - local name = position:get_nice_name() - local values = _get_values(arguments, total) - - if position.choices and vim.tbl_contains(contexts, constant.ChoiceContext.parsing) then - local values_ = values - - if type(values) ~= "table" then - values_ = { values } - end - - local current_value = values_[#values_] - - local choices = position.choices({ - contexts = vim.list_extend({ constant.ChoiceContext.value_matching }, contexts), - current_value = current_value, - }) - - for _, value in ipairs(values_) do - if not vim.tbl_contains(choices, value) then - error( - string.format( - 'Parameter "%s" got invalid "%s" value. Expected one of %s.', - position.names[1], - current_value, - vim.inspect(vim.fn.sort(choices)) - ), - 0 - ) - end - end - end - - local value = _resolve_value(position:get_type(), values) - local action = position:get_action() - - action({ namespace = namespace, name = name, value = value }) - - position:increment_used() - - return true, total, position - end - end - - return false, 0, nil -end - ---- Check if `argument_name` matches a registered subparser. ---- ----@param data argparse.Results The parsed arguments + any remainder text. ----@param argument_name string A raw argument name. e.g. `foo`. ----@param namespace cmdparse.Namespace An existing namespace to set/append/etc to the subparser. ----@return boolean # If a match was found, return `true`. ----@return cmdparse.ParameterParser? # The found subparser, if any. ---- -function M.ParameterParser:_handle_subparsers(data, argument_name, namespace) - --- (Before we allow running a subparser), Make sure that there are no issues. - local function _validate_no_issues() - local issues = self:_get_issues() - - if not vim.tbl_isempty(issues) then - error(vim.fn.join(issues, "\n"), 0) - end - end - - for _, subparser in ipairs(self._subparsers) do - for _, parser in ipairs(subparser:get_parsers()) do - if vim.tbl_contains(parser:get_names(), argument_name) then - _validate_no_issues() - - parser:_parse_arguments(data, namespace) - subparser.visited = true - - return true, parser - end - end - end - - return false, nil -end - ---- Traverse the parsers, marking arguments as used / exhausted as we traverse down. ---- ----@param data argparse.Results ---- User text that needs to be parsed. ----@return cmdparse.ParameterParser ---- The parser that was found in a current or previous iteration. ----@return cmdparse.ParameterParser | cmdparse.Parameter ---- The most recently parsed thing. Basically wherever we left off in the ---- parsing of `data`. ----@return number ---- A 1-or-more index value of the argument that we stopped parsing on. ---- -function M.ParameterParser:_compute_matching_parsers(data, contexts) - ---@return cmdparse.Parameter? - ---@return number - local function _seek_next_argument_from_flag(flag_parameters, arguments) - local flag_argument = arguments[1] - local other_arguments = tabler.get_slice(arguments, 2) - - for _, parameter in ipairs(flag_parameters) do - if not parameter:is_exhausted() and vim.tbl_contains(parameter.names, flag_argument.name) then - local nargs = parameter:get_nargs() - - if type(nargs) == "number" then - if nargs == 0 then - -- NOTE: We encountered a flag. We just accept this one - -- flag and move on. - -- - return parameter, 1 - end - - for index = 1, nargs do - local argument = other_arguments[index] - - if argument.argument_type ~= argparse.ArgumentType.position then - local found_index = index - 1 - local template = 'Parameter "%s" requires "%s" values. Got "%s"' - - if found_index == 1 then - template = template .. " value." - else - template = template .. " values." - end - - error(string.format(template, parameter.names[1], nargs, found_index), 0) - end - end - - return parameter, nargs - elseif nargs == constant.Counter.zero_or_more then - local values_count = #other_arguments - - for index = 1, values_count do - local argument = other_arguments[index] - - if argument.argument_type ~= argparse.ArgumentType.position then - return parameter, index - end - end - - return parameter, values_count - elseif nargs == constant.Counter.one_or_more then - local values_count = #other_arguments - local found = false - - for index = 1, values_count do - local argument = other_arguments[index] - - if argument.argument_type ~= argparse.ArgumentType.position then - if not found then - error( - string.format( - 'Parameter "%s" requires 1-or-more values. Got "%s" values.', - text_parse.get_argument_name(argument), - index - 1 - ), - 0 - ) - end - - return parameter, index - end - - found = true - end - - return parameter, values_count - end - end - end - - return nil, 0 - end - - local current_parser = self - local count = #data.arguments - - contexts = contexts or {} - - -- NOTE: We search all but the last argument here. - -- IMPORTANT: Every argument must have a match or it means the `arguments` - -- failed to match something in the parser tree. - -- - local index = 1 - local found = false - ---@type cmdparse.ParameterParser | cmdparse.Parameter - local current_item = self - - while index < count do - local argument = data.arguments[index] - local argument_name = text_parse.get_argument_name(argument) - - if argument.argument_type == argparse.ArgumentType.position then - --- @cast argument argparse.PositionArgument - - for parser_ in iterator_helper.iter_parsers(current_parser) do - if vim.tbl_contains(parser_:get_names(), argument_name) then - found = true - current_parser = parser_ - current_item = current_parser - - break - end - end - - if found then - index = index + 1 - else - -- TODO: Need to finish this part. It's not quite right because - -- it doesn't take into account if a position is nargs=2 - -- / * / + - -- - local position_parameters = current_parser:get_position_parameters() - local arguments = tabler.get_slice(data.arguments, index) - local used_arguments - local found_parameter - found, used_arguments, found_parameter = - current_parser:_handle_exact_position_parameters(position_parameters, arguments, {}, contexts) - - if not found then - if help_message.has_help(data.arguments) then - error(current_parser:get_full_help(data, current_parser), 0) - end - - current_parser:_raise_suggested_positional_argument_fix(argument) - end - - if found_parameter then - current_item = found_parameter - end - - -- NOTE: We don't call increment_used() here because - -- `_handle_exact_position_parameters` already does it for us. - - index = index + used_arguments - end - elseif argument.argument_type == argparse.ArgumentType.named then - found = false - local flag_parameters = current_parser:get_flag_parameters() - - for _, parameter in ipairs(flag_parameters) do - if not parameter:is_exhausted() and vim.tbl_contains(parameter.names, argument.name) then - found = true - current_item = parameter - - break - end - end - - if not found then - vlog.fmt_error( - 'Argument "%s" could not be parsed. Please check your spelling and try again.', - argument_name - ) - - return current_parser, current_item, index - end - - ---@cast current_item cmdparse.Parameter - current_item:increment_used() - - index = index + 1 - elseif argument.argument_type == argparse.ArgumentType.flag then - ---@cast argument argparse.FlagArgument | argparse.NamedArgument - - local flag_parameters = current_parser:get_flag_parameters() - local arguments = tabler.get_slice(data.arguments, index) - local found_item - local used_arguments - found_item, used_arguments = _seek_next_argument_from_flag(flag_parameters, arguments) - - if not found_item then - vlog.fmt_error( - 'Argument "%s" could not be parsed. Please check your spelling and try again.', - argument_name - ) - - return current_parser, current_item, index - end - - current_item = found_item - current_item:increment_used() - index = index + used_arguments - end - end - - -- NOTE: Because we completed the while loop without returning early, we - -- assume that the loop fully completed so we return `count`. We can only - -- do this because there is no `break` that terminates the while loop early. - -- - return current_parser, current_item, count -end - ---- Parse user text `data`. ---- ---- Raises: ---- If we have to stop parsing midway because we found an unknown argument. ---- ----@param data string | argparse.Results ---- User text that needs to be parsed. e.g. `hello "World!"` ----@param namespace cmdparse.Namespace ---- All pre-existing, default parsed values. If this is the first ---- cmdparse.ParameterParser then this `namespace` will always be empty ---- but a nested parser will usually have the parsed arguments of the ---- parent subparsers that were before it. ----@return cmdparse.Namespace ---- All of the parsed data as one group. ---- -function M.ParameterParser:_parse_arguments(data, namespace) - local function _validate_current_parser() - -- NOTE: Because `_parse_arguments` is called recursively, this validation - -- runs at every subparser level. - -- - local issues = self:_get_issues() - - if not vim.tbl_isempty(issues) then - error(vim.fn.join(issues, "\n"), 0) - end - end - - local function _handle_position_argument(current_argument, data_, index, contexts) - local arguments_to_consider = tabler.get_slice(data_.arguments, index) - local position_parameters = self:get_position_parameters() - local found, used_arguments = - self:_handle_exact_position_parameters(position_parameters, arguments_to_consider, namespace, contexts) - - if not found then - if help_message.has_help(data.arguments) then - error(self:get_full_help(data), 0) - end - - self:_raise_suggested_positional_argument_fix(current_argument) - end - - return used_arguments - end - - local function _handle_not_found(data_, index) - -- NOTE: We lost our place in the parse so we can't continue. - - _validate_current_parser() - - local remaining_arguments = tabler.get_slice(data_.arguments, index) - - if #remaining_arguments == 1 then - error( - string.format('Unexpected argument "%s".', text_parse.get_arguments_raw_text(remaining_arguments)[1]), - 0 - ) - end - - error( - string.format( - 'Unexpected arguments "%s".', - vim.fn.join(text_parse.get_arguments_raw_text(remaining_arguments), ", ") - ), - 0 - ) - end - - if type(data) == "string" then - data = argparse.parse_arguments(data) - end - - namespace = namespace or {} - _merge_namespaces(namespace, self._defaults, self:_get_default_namespace()) - - local flag_parameters = self:get_flag_parameters() - local found = false - local count = #data.arguments - local index = 1 - - local contexts = { constant.ChoiceContext.parsing } - - while index <= count do - local argument = data.arguments[index] - - if argument.argument_type == argparse.ArgumentType.position then - --- @cast argument argparse.PositionArgument - local argument_name = text_parse.get_argument_name(argument) - - found = self:_handle_subparsers(argparse_helper.lstrip_arguments(data, index + 1), argument_name, namespace) - - if found then - -- NOTE: We can only do this because `self:_handle_subparsers` - -- calls `_parse_arguments` which creates a recursive loop. - -- Once we finally terminate the loop and return here the - -- `found` is the final status of all of those recursions. - -- - return namespace - end - - local used_arguments = _handle_position_argument(argument, data, index, contexts) - found = used_arguments ~= 0 - index = index + used_arguments - elseif - argument.argument_type == argparse.ArgumentType.named - or argument.argument_type == argparse.ArgumentType.flag - then - --- @cast argument argparse.FlagArgument | argparse.NamedArgument - local arguments = tabler.get_slice(data.arguments, index) - local used_arguments - found, used_arguments = self:_handle_exact_flag_parameters(flag_parameters, arguments, namespace, contexts) - - if not found then - error(vim.fn.join(self:_get_issues(), "\n"), 0) - end - - index = index + used_arguments - end - - if not found then - _handle_not_found(data, index) - end - end - - if not namespace.execute then - -- IMPORTANT: This is a bit of a hack to get --help to work when a user - -- forgets to include all arguments. It's not technically correct for - -- us to do that and could accidentally break stuff. But If this burns - -- us later, we can change it. - -- - _validate_current_parser() - end - - return namespace -end - ---- Tell the user how to solve the unparseable `argument` ---- ---- Raises: ---- All issue(s) found, assuming 1+ issue was found. ---- ----@param argument argparse.Argument ---- Some position / flag that we don't know what to do with. ---- -function M.ParameterParser:_raise_suggested_positional_argument_fix(argument) - local names = {} - - for _, parameter in ipairs(self:get_all_parameters()) do - if parameter.required and not parameter:is_exhausted() then - table.insert(names, parameter.names[1]) - end - end - - for _, subparser in ipairs(self._subparsers) do - if not subparser.visited then - for _, parser in ipairs(subparser:get_parsers()) do - for _, name in ipairs(parser:get_names()) do - if not vim.tbl_contains(names, name) then - table.insert(names, name) - end - end - end - end - end - - if vim.tbl_isempty(names) then - return - end - - if #names == 1 then - local message = string.format( - 'Got unexpected "%s" value. Did you mean this incomplete parameter? %s', - argument.name or argument.value, - vim.fn.join(names, "\n") - ) - - error(message, 0) - end - - local message = string.format( - 'Got unexpected "%s" value. Did you mean one of these incomplete parameters?\n%s', - argument.name or argument.value, - vim.fn.join(names, "\n") - ) - - error(message, 0) -end - ---- (Assuming parameter counts were modified by any function) Reset counts back to zero. -function M.ParameterParser:_reset_used() - for _, parser in ipairs(_get_all_parsers(self)) do - for parameter in tabler.chain(parser:get_position_parameters(), parser:get_flag_parameters()) do - parameter._used = 0 - end - - for _, subparser in ipairs(self._subparsers) do - subparser.visited = false - end - end -end - ----@return boolean # If all required parameters of this instance have values. -function M.ParameterParser:is_satisfied() - for parameter in tabler.chain(self:get_flag_parameters(), self:get_position_parameters()) do - if parameter.required and not parameter:is_exhausted() then - return false - end - end - - return true -end - ---- Get all registered or implicit child parameters of this instance. ---- ----@return cmdparse.Parameter # All found parameters, if any. ---- -function M.ParameterParser:get_all_parameters() - local output = {} - - for _, parameter in tabler.chain(self:get_position_parameters(), self:get_flag_parameters()) do - table.insert(output, parameter) - end - - return output -end - ---- Get auto-complete options based on this instance + the user's `data` input. ---- ----@param data argparse.Results | string ---- The user input. ----@param column number? ---- A 1-or-more value that represents the user's cursor. ----@param options plugin_template.ConfigurationCmdparseAutoComplete? ---- The user settings to read from, if any. If no data is given, the user's ---- default configuration is used insteand. ----@return string[] ---- All found auto-complete options, if any. ---- -function M.ParameterParser:get_completion(data, column, options) - local success, result = pcall(function() - local unsorted_output = self:_get_completion(data, column, options) - local categories = sorter.categorize_arguments(unsorted_output) - - local output = {} - - vim.list_extend(output, vim.fn.sort(categories.positions)) - vim.list_extend(output, sorter.sort_and_flatten_flags(categories.flags)) - - return output - end) - - self:_reset_used() - - if success then - return result - end - - error(result, 0) -end - ---- Get a 1-to-2 line summary on how to run the CLI. ---- ----@param data string | argparse.Results ---- User text that needs to be parsed. e.g. `hello "World!"` ---- If `data` includes subparsers, that subparser's help message is returned instead. ----@return string ---- A one/two liner explanation of this instance's expected arguments. ---- -function M.ParameterParser:get_concise_help(data) - if type(data) == "string" then - data = argparse.parse_arguments(data) - end - - local summary, _ = self:_get_argument_usage_summary(data) - - return summary -end - ---- Get all of information on how to run the CLI. ---- ----@param data string | argparse.Results ---- User text that needs to be parsed. e.g. `hello "World!"` ---- If `data` includes subparsers, that subparser's help message is returned instead. ----@param parser cmdparse.ParameterParser? ---- The root parser to get a summary for. If no parser is given, ---- we auto-find it using `data`. ----@return string ---- The full explanation of this instance's expected arguments (can be pretty long). ---- -function M.ParameterParser:get_full_help(data, parser) - if type(data) == "string" then - data = argparse.parse_arguments(data) - end - - local summary - summary, parser = self:_get_argument_usage_summary(data, parser) - - local output = { summary } - vim.list_extend(output, help_message.get_parser_help_text_body(parser)) - - return vim.fn.join(output, "\n\n") .. "\n" -end - ---- The flags that a user didn't add to the parser but are included anyway. ---- ----@return cmdparse.Parameter[] ---- -function M.ParameterParser:get_implicit_flag_parameters() - return self._implicit_flag_parameters -end - ---- Get the `--foo` style parameters from this instance. ---- ----@param options {hide_implicits: boolean?}? ---- If `hide_implicits` is true, only the flag parameters that a user ---- explicitly added are returned. If `false` or not defined, all flags are ---- returned. ----@return cmdparse.Parameter[] ---- Get all arguments that can be placed in any order. ---- -function M.ParameterParser:get_flag_parameters(options) - if options and options.hide_implicits then - return self._flag_parameters - end - - local output = {} - - vim.list_extend(output, self._flag_parameters) - vim.list_extend(output, self._implicit_flag_parameters) - - return output -end - ----@return string[] # Get all of the (initial) auto-complete options for this instance. -function M.ParameterParser:get_names() - if self.choices then - return self.choices({ contexts = { constant.ChoiceContext.parameter_names } }) - end - - return { self.name } -end - ----@return cmdparse.ParameterParser? # Get the parser that owns this parser, if any. -function M.ParameterParser:get_parent_parser() - if not self._parent then - return nil - end - - ---@diagnostic disable-next-line undefined-field - return self._parent._parent -end - ----@return cmdparse.Parameter[] # Get all arguments that must be put in a specific order. -function M.ParameterParser:get_position_parameters() - return self._position_parameters -end - ----@return cmdparse.Subparsers # All immediate parser containers for this instance. -function M.ParameterParser:get_subparsers() - return self._subparsers -end - ---- Create a child parameter so we can use it to parse text later. ---- ----@param options cmdparse.ParameterInputOptions ---- All of the settings to include in the new parameter. ----@return cmdparse.Parameter ---- The created `cmdparse.Parameter` instance. ---- -function M.ParameterParser:add_parameter(options) - types_input.expand_parameter_names(options) - local is_position = text_parse.is_position_name(options.names[1]) - types_input.expand_parameter_options(options, is_position) - - --- @cast options cmdparse.ParameterOptions - - types_input.validate_parameter_options(options) - - local new_options = vim.tbl_deep_extend("force", options, { parent = self }) - local parameter = M.Parameter.new(new_options) - - if is_position then - table.insert(self._position_parameters, parameter) - else - table.insert(self._flag_parameters, parameter) - end - - return parameter -end - ---- Create a group so we can add nested parsers underneath it later. ---- ----@param options cmdparse.SubparsersInputOptions | cmdparse.SubparsersOptions ---- Customization options for the new cmdparse.Subparsers. ----@return cmdparse.Subparsers ---- A new group of parsers. ---- -function M.ParameterParser:add_subparsers(options) - local new_options = vim.tbl_deep_extend("force", options, { parent = self }) - local subparsers = M.Subparsers.new(new_options) - - table.insert(self._subparsers, subparsers) - - return subparsers -end - ---- Parse user text `data`. ---- ----@param data string | argparse.Results ---- User text that needs to be parsed. e.g. `hello "World!"` ----@return cmdparse.Namespace ---- All of the parsed data as one group. ---- -function M.ParameterParser:parse_arguments(data) - local success, result = pcall(function() - return self:_parse_arguments(data, {}) - end) - - self:_reset_used() - - if success then - return result - end - - error(result, 0) -end - ---- Whenever this parser is visited add all of these values to the resulting namespace. ---- ----@param data table ---- All of the data to set onto the namespace when it's found. ---- -function M.ParameterParser:set_defaults(data) - self._defaults = data -end - ---- Whenever this parser is visited, return `{execute=caller}` so people can use it. ---- ----@param caller fun(any: any): any ---- A function that runs a specific parser command. e.g. a "Hello, World!" program. ---- -function M.ParameterParser:set_execute(caller) - self._defaults.execute = caller -end - ---- Re-parent this instance underneath `parser`. ---- ----@param parser cmdparse.Subparsers The new parent to set. ---- -function M.ParameterParser:set_parent(parser) - self._parent = parser -end - -return M diff --git a/lua/plugin_template/_cli/cmdparse/iterator_helper.lua b/lua/plugin_template/_cli/cmdparse/iterator_helper.lua deleted file mode 100644 index 05372fa..0000000 --- a/lua/plugin_template/_cli/cmdparse/iterator_helper.lua +++ /dev/null @@ -1,81 +0,0 @@ ---- Generic functions used in other files. ---- ----@module 'plugin_template._cli.cmdparse.iterator_helper' ---- - -local M = {} - -local _FULL_HELP_FLAG = "--help" -local _SHORT_HELP_FLAG = "-h" - ---- Find all direct-children parsers of `parser`. ---- ---- Note: ---- This is not recursive. It just gets the direct children. ---- ----@param parser cmdparse.ParameterParser ---- The starting point ot saerch for child parsers. ----@return fun(): cmdparse.ParameterParser? ---- An iterator that find all child parsers. ---- -function M.iter_parsers(parser) - local subparsers_index = 1 - local all_subparsers = parser:get_subparsers() - local current_subparsers = all_subparsers[subparsers_index] - - local parser_index = 1 - local parsers = {} - - if current_subparsers then - parsers = current_subparsers:get_parsers() - end - - local parser_count = #parsers - - return function() - if parser_index > parser_count then - -- NOTE: Get the next subparsers. - parser_index = 1 - subparsers_index = subparsers_index + 1 - parsers = all_subparsers[subparsers_index] - - if not parsers then - -- NOTE: We reached the end. - return nil - end - - return parsers[parser_index] - end - - local result = parsers[parser_index] - - parser_index = parser_index + 1 - - return result - end -end - ---- Re-order `parameters` alphabetically but put the `--help` flag at the end. ---- ----@param parameters cmdparse.Parameter[] All position / flag / named parameters. ----@return cmdparse.Parameter[] # The sorted entries. ---- -function M.sort_parameters(parameters) - local output = vim.deepcopy(parameters) - - table.sort(output, function(left, right) - if vim.tbl_contains(left.names, _FULL_HELP_FLAG) or vim.tbl_contains(left.names, _SHORT_HELP_FLAG) then - return false - end - - if vim.tbl_contains(right.names, _FULL_HELP_FLAG) or vim.tbl_contains(right.names, _SHORT_HELP_FLAG) then - return true - end - - return left.names[1] < right.names[1] - end) - - return output -end - -return M diff --git a/lua/plugin_template/_cli/cmdparse/matcher.lua b/lua/plugin_template/_cli/cmdparse/matcher.lua deleted file mode 100644 index 013c41a..0000000 --- a/lua/plugin_template/_cli/cmdparse/matcher.lua +++ /dev/null @@ -1,330 +0,0 @@ ---- Match exact / partial text of cmdparse parameters and argparse arguments. ---- ----@module 'plugin_template._cli.cmdparse.matcher' ---- - -local argparse = require("plugin_template._cli.argparse") -local constant = require("plugin_template._cli.cmdparse.constant") -local iterator_helper = require("plugin_template._cli.cmdparse.iterator_helper") -local text_parse = require("plugin_template._cli.cmdparse.text_parse") -local texter = require("plugin_template._core.texter") -local vlog = require("plugin_template._vendors.vlog") - -local M = {} - ---- Remove whitespace from `text` but only if `text` is 100% whitespace. ---- ----@param text string Some text to possibly strip. ----@return string # The processed `text` or, if it contains whitespace, the original `text`. ---- -local function _remove_contiguous_whitespace(text) - return (text:gsub("^%s*$", "")) -end - ---- Get all auto-completions that `parser` is currently allowed to recommend. ---- ---- All exhausted child parameters are excluded from the output. ---- ----@param parser cmdparse.ParameterParser ---- A parser to query. ----@param options cmdparse._core.DisplayOptions? ---- Control minor behaviors of this function. e.g. What data to show. ----@return string[] ---- The found auto-completion results, if any. ---- -function M.get_current_parser_completions(parser, options) - local output = {} - - -- NOTE: Get all possible initial arguments (check all parameters / subparsers) - if parser:is_satisfied() then - vim.list_extend(output, vim.fn.sort(M.get_matching_subparser_names("", parser))) - end - - vim.list_extend(output, M.get_matching_position_parameters("", parser:get_position_parameters())) - - vim.list_extend( - output, - M.get_matching_partial_flag_text( - "", - parser:get_flag_parameters(), - nil, - { constant.ChoiceContext.auto_completing }, - options - ) - ) - - return output -end - ---- Find all Argments starting with `prefix`. ---- ----@param parameter cmdparse.Parameter ---- The position, flag, or named parameter to consider nargs / choices / etc. ----@param argument argparse.Argument ---- A user's actual CLI input. It must either match `parameter` or be ---- a valid input to `parameter`. Or be the next valid parameter that would ---- normally follow `parameter`. ----@param parser cmdparse.ParameterParser ---- The starting point to search within. ----@param contexts cmdparse.ChoiceContext[] ---- A description of how / when this function is called. It gets passed to ---- `cmdparse.Parameter.choices()`. ----@param options cmdparse._core.DisplayOptions ---- Control minor behaviors of this function. e.g. What data to show. ----@return string[] # The matching names, if any. ---- -function M.get_exact_or_partial_matches(parameter, argument, parser, contexts, options) - -- local function _get_longest_match(prefix, options) - -- local longest_match_count = 0 - -- local longest_match - -- - -- for _, name in ipairs(options) do - -- if vim.startswith(name, prefix) then - -- local count = #prefix - -- - -- if count > longest_match_count then - -- longest_match = name - -- longest_match_count = count - -- end - -- end - -- end - -- - -- return longest_match - -- end - - local output = {} - - local prefix = text_parse.get_argument_name(argument) - - local matches = texter.get_array_startswith(parameter.names, prefix) - - if not vim.tbl_isempty(matches) and argument.value == false then - if parameter:is_exhausted() then - return {} - end - - local nargs = parameter:get_nargs() - - if nargs == 1 then - return { matches[1] .. "=" } - end - end - - prefix = text_parse.get_argument_value_text(argument) - - if argument.argument_type == argparse.ArgumentType.position and parameter.choices then - return parameter.choices({ current_value = prefix, contexts = contexts }) - end - - local value = argument.value or nil - ---@cast value string - - prefix = text_parse.get_argument_name(argument) - vim.list_extend(output, M.get_matching_position_parameters(prefix, parser:get_position_parameters(), contexts)) - vim.list_extend( - output, - M.get_matching_partial_flag_text(prefix, parser:get_flag_parameters(), value, contexts, options) - ) - - return output -end - ---- Create auto-complete text for `parameter`, given some `value`. ---- ----@param parameter cmdparse.Parameter ---- A parameter that (we assume) takes exactly one value that we need ---- auto-completion options for. ----@param value string ---- The user-provided (exact or partial) value for the flag / named argument ---- value, if any. e.g. the `"bar"` part of `"--foo=bar"`. ----@param contexts cmdparse.ChoiceContext[]? ---- A description of how / when this function is called. It gets passed to ---- `cmdparse.Parameter.choices()`. ----@return string[] ---- All auto-complete values, if any. ---- -local function _get_single_choices_text(parameter, value, contexts) - if not parameter.choices then - return { parameter.names[1] .. "=" } - end - - contexts = contexts or {} - - local output = {} - - for _, choice in - ipairs(parameter.choices({ - contexts = vim.list_extend({ constant.ChoiceContext.value_matching }, contexts), - current_value = value, - })) - do - table.insert(output, parameter.names[1] .. "=" .. choice) - end - - return output -end - ---- Find the child parser that matches `name`. ---- ----@param name string The name of a child parser within `parser`. ----@param parser cmdparse.ParameterParser The parent parser to search within. ----@return cmdparse.ParameterParser? # The matching child parser, if any. ---- -function M.get_exact_subparser_child(name, parser) - for child_parser in iterator_helper.iter_parsers(parser) do - if vim.tbl_contains(child_parser:get_names(), name) then - return child_parser - end - end - - return nil -end - ---- Check all `flags` that match `prefix` and `value`. ---- ----@param prefix string ---- The name of the flag that must match, exactly or partially. ----@param flags cmdparse.Parameter[] ---- All position / flag / named parameters. ----@param value string? ---- The user-provided (exact or partial) value for the flag / named argument ---- value, if any. e.g. the `"bar"` part of `"--foo=bar"`. ----@param contexts cmdparse.ChoiceContext[]? ---- A description of how / when this function is called. It gets passed to ---- `cmdparse.Parameter.choices()`. ----@param options cmdparse._core.DisplayOptions? ---- Control minor behaviors of this function. e.g. What data to show. ----@return cmdparse.Parameter[] ---- The matched parameters, if any. ---- -function M.get_matching_partial_flag_text(prefix, flags, value, contexts, options) - local output = {} - - local excluded_names = {} - - if options then - excluded_names = options.excluded_names or {} - end - - for _, parameter in ipairs(iterator_helper.sort_parameters(flags)) do - if not parameter:is_exhausted() then - for _, name in ipairs(parameter.names) do - if vim.tbl_contains(excluded_names, name) then - vlog.fmt_debug('Skipped adding "%s" because it was found in "%s".', parameter, excluded_names) - - break - elseif name == prefix then - if parameter:get_nargs() == 1 then - if not value then - table.insert(output, parameter.names[1] .. "=") - else - vim.list_extend(output, _get_single_choices_text(parameter, value, contexts)) - end - else - table.insert(output, name) - end - - break - elseif vim.startswith(name, prefix) then - if parameter:get_nargs() == 1 then - table.insert(output, name .. "=") - else - table.insert(output, name) - end - - break - end - end - end - end - - return output -end - ---- Find all `options` that match `name`. ---- ---- By default a position option takes any argument / value. Some position parameters ---- have specific, required choice(s) that this function means to match. ---- ----@param name string ---- The user's input text to try to match. ----@param parameters cmdparse.Parameter[] ---- All position parameters to check. ----@param contexts cmdparse.ChoiceContext[]? ---- A description of how / when this function is called. It gets passed to ---- `cmdparse.Parameter.choices()`. ----@return cmdparse.Parameter[] # The found matches, if any. ---- -function M.get_matching_position_parameters(name, parameters, contexts) - contexts = contexts or {} - local output = {} - - for _, parameter in ipairs(iterator_helper.sort_parameters(parameters)) do - if not parameter:is_exhausted() and parameter.choices then - vim.list_extend( - output, - texter.get_array_startswith( - parameter.choices({ - contexts = vim.list_extend({ constant.ChoiceContext.position_matching }, contexts), - current_value = name, - }), - name - ) - ) - end - end - - return output -end - ---- Find all all child parsers that start with `prefix`, starting from `parser`. ---- ---- This function is **exclusive** - `parser` cannot be returned from this function. ---- ----@param prefix string Some text to search for. ----@param parser cmdparse.ParameterParser The starting point to search within. ----@return string[] # The names of all matching child parsers. ---- -function M.get_matching_subparser_names(prefix, parser) - local output = {} - - for parser_ in iterator_helper.iter_parsers(parser) do - local names = parser_:get_names() - - vim.list_extend(output, texter.get_array_startswith(names, prefix)) - end - - return output -end - ---- Get the next auto-complete options for `parser`. ---- ----@param parser cmdparse.ParameterParser ---- The starting point to search within. ----@param prefix string ---- The name of the flag that must match, exactly or partially. ----@param value string ---- If the user provided a (exact or partial) value for the flag / named ---- position, the text is given here. ----@param contexts cmdparse.ChoiceContext[] ---- A description of how / when this function is called. It gets passed to ---- `cmdparse.Parameter.choices()`. ----@param options cmdparse._core.DisplayOptions? ---- Control minor behaviors of this function. e.g. What data to show. ----@return string[] ---- All auto-completion results found, if any. ---- -function M.get_parser_exact_or_partial_matches(parser, prefix, value, contexts, options) - prefix = _remove_contiguous_whitespace(prefix) - local output = {} - - vim.list_extend(output, M.get_matching_position_parameters(prefix, parser:get_position_parameters(), contexts)) - vim.list_extend( - output, - M.get_matching_partial_flag_text(prefix, parser:get_flag_parameters(), value, contexts, options) - ) - - return output -end - -return M diff --git a/lua/plugin_template/_cli/cmdparse/sorter.lua b/lua/plugin_template/_cli/cmdparse/sorter.lua deleted file mode 100644 index 3404f1c..0000000 --- a/lua/plugin_template/_cli/cmdparse/sorter.lua +++ /dev/null @@ -1,119 +0,0 @@ ---- Make sorting arguments easier. ---- ----@module 'plugin_template._cli.cmdparse.sorter' ---- - -local argparse = require("plugin_template._cli.argparse") -local help_message = require("plugin_template._cli.cmdparse.help_message") -local texter = require("plugin_template._core.texter") - -local M = {} - ----@class cmdparse._sorter.ArgumentCategories ---- A series of arguments that the user wrote, split into various sections. ----@field flags table ---- All arguments that starts with - / + e.g. `{"--foo", "--bar", "--fizz=buzz"}`. ----@field positions table ---- All arguments that don't start with - / + e.g. `{"foo", "bar", "fizz", "buzz"}`. - ---- Check if `text` is starts with a typical - or +. ---- ----@param text string An argument. e.g. `"--foo"`. ----@return boolean # If it starts with - or +, return `true`. ---- -local function _is_flag(text) - for _, prefix in ipairs(argparse.PREFIX_CHARACTERS) do - if texter.startswith(text, prefix) then - return true - end - end - - return false -end - ---- Get the starting text of a named argument. ---- ----@param text string Some flag text. e.g. `"--foo-bar=thing"`. ----@return string # The found name, if any. e.g. `"--foo"`. ---- -local function _get_base_name(text) - return (text:match("(.+)=")) or "" -end - ---- Split `arguments` based on what type they are. ---- ----@param arguments string[] ---- The values to categorize based on if they are a position, flag, or named argument. ---- e.g. `{"a", "z", "b", "--named=foo", "--help", "--named=bar", "-a", "-z"}` ----@return cmdparse._sorter.ArgumentCategories ---- The arguments by-category. e.g. `{flags={"--named=foo", "--help", ---- "--named=bar", "-a", "-z"}, positions={"a", "z", "b"}}`. ---- -function M.categorize_arguments(arguments) - local categories = { flags = {}, positions = {} } - - for _, argument in ipairs(arguments) do - if _is_flag(argument) then - local base_name = _get_base_name(argument) - - if base_name == "" then - if not vim.tbl_contains(vim.tbl_keys(categories.flags), argument) then - categories.flags[argument] = { argument } - end - else - if not vim.tbl_contains(vim.tbl_keys(categories.flags), base_name) then - categories.flags[base_name] = {} - end - - table.insert(categories.flags[base_name], argument) - end - else - if not vim.tbl_contains(vim.tbl_keys(categories.positions), argument) then - categories.flags = {} - end - - table.insert(categories.positions, argument) - end - end - - return categories -end - ---- Sort the flag and named arguments by our conventions. ---- ---- - Double-dash flags and names are mixed ---- - Flags are alphabetically sorted ---- - Named flags are also alphabetically sorted ---- - But their values, which come directly from the user, are not sorted! ---- - Certain, known flags that are not likely to be picked go at the end. e.g. --help. ---- ----@param arguments cmdparse._sorter.ArgumentCategories ---- The values to sort. e.g. `{"b", "a", "zzz" "--help", "-a", "--zoo", "--abc", ---- "--named=a", "--named=c", "--named=b"}`. ----@return string[] ---- The sorted output. `{"a", "b", "zzz" "--abc", "--named=a", "--named=c", ---- "--named=b", "--zoo", "-a", "--help"}`. ---- -function M.sort_and_flatten_flags(arguments) - local output = {} - - local found_help_names = {} - - for _, key in ipairs(vim.fn.sort(vim.tbl_keys(arguments))) do - for _, value in ipairs(arguments[key]) do - if vim.tbl_contains(help_message.HELP_NAMES, value) then - table.insert(found_help_names, value) - else - table.insert(output, value) - end - end - end - - for _, value in ipairs(found_help_names) do - table.insert(output, value) - end - - return output -end - -return M diff --git a/lua/plugin_template/_cli/cmdparse/text_parse.lua b/lua/plugin_template/_cli/cmdparse/text_parse.lua deleted file mode 100644 index ff0e07f..0000000 --- a/lua/plugin_template/_cli/cmdparse/text_parse.lua +++ /dev/null @@ -1,91 +0,0 @@ ---- Make dealing with raw text from cmdparse types easier to handle. ---- ----@module 'plugin_template._cli.cmdparse.text_parse' ---- - -local argparse = require("plugin_template._cli.argparse") - -local M = {} - ---- Check if `argument` is an argument with a missing value. e.g. `--foo=`. ---- ----@param argument argparse.Argument Some position, flag, or named argument. ----@return boolean # If `argument` is a named argument with no value, return `true`. ---- -function M.is_incomplete_named_argument(argument) - return argument.argument_type == argparse.ArgumentType.named and argument.value == false -end - ---- Check if `text`. ---- ----@param text string Some text. e.g. `--foo`. ----@return boolean # If `text` is a word, return `true. ---- -function M.is_position_name(text) - return not vim.tbl_contains(argparse.PREFIX_CHARACTERS, text:sub(1, 1)) -end - ---- Get the raw argument name. e.g. `"--foo"`. ---- ---- Important: ---- If `argument` is a flag, this function must return back the prefix character(s) too. ---- ----@param argument argparse.Argument Some named argument to get text from. ----@return string # The found name. ---- -function M.get_argument_name(argument) - return argument.name or argument.value -end - ---- If the `argument` is a Named Argument with a value, get it. ---- ----@param argument argparse.Argument Some user input argument to check. ----@return string # The found value, if any. ---- -function M.get_argument_value_text(argument) - local value = argument.value - - if type(value) == "boolean" then - return "" - end - - ---@cast value string - - return value -end - ---- Get the labels of all `arguments`. ---- ----@param arguments argparse.Argument[] The flag, position, or named arguments. ----@return string[] # All raw user input text. ---- -function M.get_arguments_raw_text(arguments) - local output = {} - - for _, argument in ipairs(arguments) do - if argument.argument_type == argparse.ArgumentType.named then - table.insert(output, string.format("%s=%s", argument.name, argument.value)) - else - table.insert(output, argument.value or argument.name) - end - end - - return output -end - ---- Strip argument name of any flag / prefix text. e.g. `"--foo"` becomes `"foo"`. ---- ----@param text string Some raw argument name. e.g. `"--foo"`. ----@return string # The (clean) argument mame. e.g. `"foo"`. ---- -function M.get_nice_name(text) - local bad_characters = "" - - for _, prefix in ipairs(argparse.PREFIX_CHARACTERS) do - bad_characters = bad_characters .. vim.pesc(prefix) - end - - return (text:gsub(string.format("^[%s]+", bad_characters), "")) -end - -return M diff --git a/lua/plugin_template/_cli/cmdparse/types_input.lua b/lua/plugin_template/_cli/cmdparse/types_input.lua deleted file mode 100644 index 02e16a7..0000000 --- a/lua/plugin_template/_cli/cmdparse/types_input.lua +++ /dev/null @@ -1,259 +0,0 @@ ---- Functions that fill-in missing values, validate values, etc for cmdparse types. ---- ----@module 'plugin_template._cli.cmdparse.types_input' ---- - -local constant = require("plugin_template._cli.cmdparse.constant") -local text_parse = require("plugin_template._cli.cmdparse.text_parse") -local texter = require("plugin_template._core.texter") - -local M = {} - -local _FLAG_ACTIONS = { constant.Action.count, constant.Action.store_false, constant.Action.store_true } - ---- Print `data` but don't recurse. ---- ---- If you don't call this function when you try to print one of our Parameter ---- types, it will print parent / child objects and it ends up printing the ---- whole tree. This function instead prints just the relevant details. ---- ----@param data any Anything. Usually an Parameter type from this file. ----@return string # The found data. ---- -local function _concise_inspect(data) - --- NOTE: Not sure why llscheck doesn't like this line. Maybe the - --- annotations for `vim.inspect` are incorret. - --- - ---@diagnostic disable-next-line redundant-parameter - return vim.inspect(data, { depth = 1 }) or "" -end - ---- Find a proper type converter from `options`. ---- ----@param options cmdparse.ParameterInputOptions | cmdparse.ParameterOptions The suggested type for an parameter. ---- -local function _expand_type_options(options) - if not options.type then - options.type = function(value) - return value - end - elseif options.type == "string" then - options.type = function(value) - return value - end - elseif options.type == "number" then - options.type = function(value) - return tonumber(value) - end - elseif type(options.type) == "function" then - -- NOTE: Do nothing. Assume the user knows what they're doing. - return - else - error(string.format('Type "%s" is unknown. We can\'t parse it.', _concise_inspect(options)), 0) - end -end - ---- Add / modify `options.choices` as needed. ---- ---- Basically if `options.choices` is not defined, that's fine. If it is ---- a `string` or `string[]`, handle that. If it's a function, assume the user ---- knows what they're doing and include it. ---- ----@param options cmdparse.ParameterInputOptions ---- | cmdparse.ParameterOptions ---- | cmdparse.ParameterParserOptions ---- | cmdparse.ParameterParserInputOptions ---- The user-written options. (sparse or not). ---- -function M.expand_choices_options(options) - if not options.choices then - return - end - - local input = options.choices - local choices - - if type(options.choices) == "string" then - choices = function() - return { input } - end - elseif texter.is_string_list(input) then - ---@cast input string[] - choices = function(data) - ---@cast data cmdparse.ChoiceData - - if not data or not data.current_value then - return input - end - - local value = data.current_value - ---@cast value string | string[] - - if vim.tbl_contains(data.contexts, constant.ChoiceContext.auto_completing) then - ---@cast value string - return texter.get_array_startswith(input, value) - end - - return input - end - elseif type(options.choices) == "function" then - choices = input - else - error( - string.format( -- NOTE: choices has to be a known format. - 'Got invalid "%s" choices. Expected a string[] or a function.', - _concise_inspect(options.choices) - ), - 0 - ) - end - - options.choices = choices -end - ---- Make sure an `cmdparse.Parameter` has a name and every name is the same type. ---- ---- If `names` is `{"foo", "-f"}` then this function will error. ---- ----@param options cmdparse.ParameterInputOptions | cmdparse.ParameterOptions All data to check. ---- -function M.expand_parameter_names(options) - local function _get_type(name) - if text_parse.is_position_name(name) then - return "position" - end - - return "flag" - end - - local names = options.names or options.name or options[1] - - if type(names) == "string" then - names = { names } - end - - if not names then - error(string.format('Options "%s" is missing a "name" key.', vim.inspect(options)), 0) - end - - local found_type = nil - - for _, name in ipairs(names) do - if not found_type then - found_type = _get_type(name) - elseif found_type ~= _get_type(name) then - error( - string.format( - "Parameter names have to be the same type. " - .. 'e.g. If one name starts with "-", all names ' - .. 'must start with "-" and vice versa.' - ), - 0 - ) - end - end - - if not found_type then - error(string.format('Options "%s" must provide at least one name.', vim.inspect(names)), 0) - end - - options.names = names -end - ---- If `options` is sparsely written, "expand" all of its values. so we can use it. ---- ----@param options cmdparse.ParameterInputOptions | cmdparse.ParameterOptions ---- The user-written options. (sparse or not). ----@param is_position boolean ---- If `options` is meant to be a non-flag argument. e.g. `--foo` is `false`. ---- -function M.expand_parameter_options(options, is_position) - _expand_type_options(options) - M.expand_choices_options(options) - - if options.required == nil then - if is_position then - options.required = options.count ~= constant.Counter.zero_or_more - else - options.required = false - end - end - - if vim.tbl_contains(_FLAG_ACTIONS, options.action) and not options.nargs then - options.nargs = 0 - end - - if not options.nargs then - options.nargs = 1 - end - - if options.required == nil then - if is_position then - options.required = true - else - options.required = false - end - end -end - ---- Make sure `options` has no conflicting / missing data. ---- ---- Raises: ---- If an issue is found. ---- ----@param options cmdparse.ParameterInputOptions | cmdparse.ParameterOptions ---- All data to check. ---- -function M.validate_parameter_options(options) - local is_position = text_parse.is_position_name(options.names[1]) - - if is_position then - if vim.tbl_contains(_FLAG_ACTIONS, options.action) then - error(string.format('Parameter "%s" cannot use action="%s".', options.names[1], options.action), 0) - end - - if options.nargs == 0 then - error(string.format('Parameter "%s" cannot be nargs=0.', options.names[1]), 0) - end - end - - if type(options.nargs) == "number" and options.nargs < 0 then - error(string.format('Nargs "%s" cannot be less than zero.', options.nargs), 0) - end - - if vim.tbl_contains(_FLAG_ACTIONS, options.action) then - if options.choices ~= nil then - error( - string.format( - 'Parameter "%s" cannot use action "%s" and choices at the same time.', - options.names[1], - options.action - ), - 0 - ) - end - - if options.nargs ~= 0 then - error( - string.format( - 'Parameter "%s" cannot use action "%s" and nargs at the same time.', - options.names[1], - options.action - ), - 0 - ) - end - end -end - ---- Make sure a name was provided from `options`. ---- ----@param options cmdparse.ParameterParserOptions ---- -function M.validate_name(options) - if not options.name or texter.is_whitespace(options.name) then - error(string.format('Parameter "%s" must have a name.', _concise_inspect(options)), 0) - end -end - -return M diff --git a/lua/plugin_template/_commands/arbitrary_thing/parser.lua b/lua/plugin_template/_commands/arbitrary_thing/parser.lua deleted file mode 100644 index a273f61..0000000 --- a/lua/plugin_template/_commands/arbitrary_thing/parser.lua +++ /dev/null @@ -1,36 +0,0 @@ ---- The main parser for the `:PluginTemplate arbitrary-thing` command. ---- ----@module 'plugin_template._commands.arbitrary_thing.parser' ---- - -local cmdparse = require("plugin_template._cli.cmdparse") - -local M = {} - ----@return cmdparse.ParameterParser # The main parser for the `:PluginTemplate arbitrary-thing` command. -function M.make_parser() - local parser = cmdparse.ParameterParser.new({ "arbitrary-thing", help = "Prepare to sleep or sleep." }) - - parser:add_parameter({ "-a", action = "store_true", help = "The -a flag." }) - parser:add_parameter({ "-b", action = "store_true", help = "The -b flag." }) - parser:add_parameter({ "-c", action = "store_true", help = "The -c flag." }) - parser:add_parameter({ "-v", action = "store_true", count = "*", destination = "verbose", help = "The -v flag." }) - parser:add_parameter({ "-f", action = "store_true", count = "*", help = "The -f flag." }) - - parser:set_execute(function(data) - ---@cast data plugin_template.NamespaceExecuteArguments - local runner = require("plugin_template._commands.arbitrary_thing.runner") - - local names = {} - - for _, argument in ipairs(data.input.arguments) do - table.insert(names, argument.name) - end - - runner.run(names) - end) - - return parser -end - -return M diff --git a/lua/plugin_template/_commands/arbitrary_thing/runner.lua b/lua/plugin_template/_commands/arbitrary_thing/runner.lua deleted file mode 100644 index a13e34b..0000000 --- a/lua/plugin_template/_commands/arbitrary_thing/runner.lua +++ /dev/null @@ -1,24 +0,0 @@ ---- The main file that implements `arbitrary-thing` outside of COMMAND mode. ---- ----@module 'plugin_template._commands.arbitrary_thing.runner' ---- - -local M = {} - ---- Print the `names`. ---- ----@param names string[]? Some text to print out. e.g. `{"a", "b", "c"}`. ---- -function M.run(names) - local text - - if not names or vim.tbl_isempty(names) then - text = "" - else - text = vim.fn.join(names, ", ") - end - - vim.notify(text, vim.log.levels.INFO) -end - -return M diff --git a/lua/plugin_template/_commands/copy_logs/parser.lua b/lua/plugin_template/_commands/copy_logs/parser.lua deleted file mode 100644 index c20adad..0000000 --- a/lua/plugin_template/_commands/copy_logs/parser.lua +++ /dev/null @@ -1,30 +0,0 @@ ---- The main parser for the `:PluginTemplate copy-logs` command. ---- ----@module 'plugin_template._commands.copy_logs.parser' ---- - -local cmdparse = require("plugin_template._cli.cmdparse") - -local M = {} - ----@return cmdparse.ParameterParser # The main parser for the `:PluginTemplate copy-logs` command. -function M.make_parser() - local parser = cmdparse.ParameterParser.new({ "copy-logs", help = "Get debug logs for PluginTemplate." }) - - parser:add_parameter({ - "log", - required = false, - help = "The path on-disk to look for logs. If no path is given, a fallback log path is used instead.", - }) - - parser:set_execute(function(data) - ---@cast data plugin_template.NamespaceExecuteArguments - local runner = require("plugin_template._commands.copy_logs.runner") - - runner.run(data.namespace.log) - end) - - return parser -end - -return M diff --git a/lua/plugin_template/_commands/copy_logs/runner.lua b/lua/plugin_template/_commands/copy_logs/runner.lua deleted file mode 100644 index 0dff961..0000000 --- a/lua/plugin_template/_commands/copy_logs/runner.lua +++ /dev/null @@ -1,90 +0,0 @@ ---- The main file that implements `copy-logs` outside of COMMAND mode. ---- ----@module 'plugin_template._commands.copy_logs.runner' ---- - -local vlog = require("plugin_template._vendors.vlog") - -local M = {} - ---- Modify the user's system clipboard with `result`. ---- ----@param result plugin_template.ReadFileResult The file path + its contents that we read. ---- -local function _callback(result) - vim.fn.setreg("+", result.data) - - vim.notify(string.format('Log file "%s" was copied to the clipboard.', result.path), vim.log.levels.INFO) -end - ----@class plugin_template.ReadFileResult ---- A file path + its contents. ----@field data string ---- The blob of text that was read from `path`. ----@field path string ---- An absolute path to a file on-disk. - ---- Read the contents of `path` and pass its contents to `callback`. ---- ----@param path string An absolute path to a file on-disk. ----@param callback fun(result: plugin_template.ReadFileResult): nil Call this once `path` is read. ---- -function M._read_file(path, callback) - -- NOTE: mode 428 == rw-rw-rw- - vim.uv.fs_open(path, "r", 438, function(error_open, handler) - if error_open then - error(error_open) - end - if not handler then - error(string.format('Path "%s" could not be opened.', path)) - end - - vim.uv.fs_fstat(handler, function(error_stat, stat) - if error_stat then - error(error_stat) - end - if not stat then - error(string.format('Path "%s" could not be stat-ed.', path)) - end - - vim.uv.fs_read(handler, stat.size, 0, function(error_read, data) - if error_read then - error(error_read) - end - if not data then - error(string.format('Path "%s" could no be read for data.', path)) - end - - vim.uv.fs_close(handler, function(error_close) - assert(not error_close, error_close) - - return callback({ data = data, path = path }) - end) - end) - end) - end) -end - ---- Copy the log data from the given `path` to the user's clipboard. ---- ----@param path string? ---- A path on-disk to look for logs. If none is given, the default fallback ---- location is used instead. ---- -function M.run(path) - path = path or vlog:get_log_path() - - if not path or vim.fn.filereadable(path) ~= 1 then - vim.notify(string.format('No "%s" path. Cannot copy the logs.', path), vim.log.levels.ERROR) - - return - end - - local success, _ = pcall(M._read_file, path, vim.schedule_wrap(_callback)) - - if not success then - vim.notify(string.format('Failed to read "%s" path. Cannot copy the logs.', path), vim.log.levels.ERROR) - end -end - -return M diff --git a/lua/plugin_template/_commands/goodnight_moon/count_sheep.lua b/lua/plugin_template/_commands/goodnight_moon/count_sheep.lua deleted file mode 100644 index 62ba789..0000000 --- a/lua/plugin_template/_commands/goodnight_moon/count_sheep.lua +++ /dev/null @@ -1,30 +0,0 @@ ---- The main file that implements `goodnight-moon count-sheep` outside of COMMAND mode. ---- ----@module 'plugin_template._commands.goodnight_moon.count_sheep.runner' ---- - -local configuration = require("plugin_template._core.configuration") -local vlog = require("plugin_template._vendors.vlog") - -local M = {} - ---- Count a sheep for each `count`. ---- ----@param count number Prints 1 sheep per `count`. A value that is 1-or-greater. ---- -function M.run(count) - configuration.initialize_data_if_needed() - vlog.debug("Running goodnight-moon count-sheep") - - if count < 1 then - vlog.fmt_warn('Count "%s" cannot be less than 1. Using 1 instead.', count) - - count = 1 - end - - for index = 1, count do - vim.notify(string.format("%s Sheep", index), vim.log.levels.INFO) - end -end - -return M diff --git a/lua/plugin_template/_commands/goodnight_moon/parser.lua b/lua/plugin_template/_commands/goodnight_moon/parser.lua deleted file mode 100644 index a6caf5a..0000000 --- a/lua/plugin_template/_commands/goodnight_moon/parser.lua +++ /dev/null @@ -1,54 +0,0 @@ ---- The main parser for the `:PluginTemplate goodnight-moon` command. ---- ----@module 'plugin_template._commands.goodnight_moon.parser' ---- - -local cmdparse = require("plugin_template._cli.cmdparse") - -local M = {} - ----@return cmdparse.ParameterParser # The main parser for the `:PluginTemplate goodnight-moon` command. -function M.make_parser() - local parser = cmdparse.ParameterParser.new({ "goodnight-moon", help = "Prepare to sleep or sleep." }) - local subparsers = - parser:add_subparsers({ destination = "commands", help = "All goodnight-moon commands.", required = true }) - - local count_sheep = subparsers:add_parser({ "count-sheep", help = "Count some sheep to help you sleep." }) - count_sheep:add_parameter({ "count", type = "number", help = "The number of sheept to count." }) - local read = subparsers:add_parser({ "read", help = "Read a book in bed." }) - read:add_parameter({ "book", help = "The name of the book to read." }) - - local sleep = subparsers:add_parser({ "sleep", help = "Sleep tight!" }) - sleep:add_parameter({ - "-z", - action = "count", - count = "*", - destination = "count", - help = "The number of Zzz to print.", - }) - - count_sheep:set_execute(function(data) - ---@cast data plugin_template.NamespaceExecuteArguments - local count_sheep_ = require("plugin_template._commands.goodnight_moon.count_sheep") - - count_sheep_.run(data.namespace.count) - end) - - read:set_execute(function(data) - ---@cast data plugin_template.NamespaceExecuteArguments - local read_ = require("plugin_template._commands.goodnight_moon.read") - - read_.run(data.namespace.book) - end) - - sleep:set_execute(function(data) - ---@cast data plugin_template.NamespaceExecuteArguments - local sleep_ = require("plugin_template._commands.goodnight_moon.sleep") - - sleep_.run(data.namespace.count) - end) - - return parser -end - -return M diff --git a/lua/plugin_template/_commands/goodnight_moon/read.lua b/lua/plugin_template/_commands/goodnight_moon/read.lua deleted file mode 100644 index 872ebb2..0000000 --- a/lua/plugin_template/_commands/goodnight_moon/read.lua +++ /dev/null @@ -1,20 +0,0 @@ ---- The main file that implements `goodnight-moon read` outside of COMMAND mode. ---- ----@module 'plugin_template._commands.goodnight_moon.read.runner' ---- - -local vlog = require("plugin_template._vendors.vlog") - -local M = {} - ---- Print the name of the book. ---- ----@param book string The name of the book. ---- -function M.run(book) - vlog.debug("Running goodnight-moon count-sheep") - - vim.notify(string.format("%s: it is a book", book), vim.log.levels.INFO) -end - -return M diff --git a/lua/plugin_template/_commands/goodnight_moon/sleep.lua b/lua/plugin_template/_commands/goodnight_moon/sleep.lua deleted file mode 100644 index 4392a95..0000000 --- a/lua/plugin_template/_commands/goodnight_moon/sleep.lua +++ /dev/null @@ -1,32 +0,0 @@ ---- The main file that implements `goodnight-moon sleep` outside of COMMAND mode. ---- ----@module 'plugin_template._commands.goodnight_moon.sleep.runner' ---- - -local vlog = require("plugin_template._vendors.vlog") - -local M = {} - ---- Print Zzz each `count`. ---- ----@param count number? Prints 1 Zzz per `count`. A value that is 1-or-greater. ---- -function M.run(count) - vlog.debug("Running goodnight-moon count-sheep") - - if count == nil then - count = 1 - end - - if count < 1 then - vlog.fmt_warn('count-sheep "%s" is invalid. Setting the value to to 1-or-greater, instead.', count) - - count = 1 - end - - for _ = 1, count do - vim.notify("Zzz", vim.log.levels.INFO) - end -end - -return M diff --git a/lua/plugin_template/_commands/hello_world/parser.lua b/lua/plugin_template/_commands/hello_world/parser.lua deleted file mode 100644 index 972b3ba..0000000 --- a/lua/plugin_template/_commands/hello_world/parser.lua +++ /dev/null @@ -1,109 +0,0 @@ ---- The main parser for the `:PluginTemplate hello-world` command. ---- ----@module 'plugin_template._commands.hello_world.parser' ---- - -local cmdparse = require("plugin_template._cli.cmdparse") -local constant = require("plugin_template._commands.hello_world.say.constant") - -local M = {} - ---- Add the `--repeat` parameter onto `parser`. ---- ----@param parser cmdparse.ParameterParser The parent parser to add the parameter onto. ---- -local function _add_repeat_parameter(parser) - parser:add_parameter({ - names = { "--repeat", "-r" }, - choices = function(data) - --- @cast data cmdparse.ChoiceData? - - local output = {} - - if not data or not data.current_value or data.current_value == "" then - for index = 1, 5 do - table.insert(output, tostring(index)) - end - - return output - end - - local value = tonumber(data.current_value) - - if not value then - return {} - end - - table.insert(output, tostring(value)) - - for index = 1, 4 do - table.insert(output, tostring(value + index)) - end - - return output - end, - default = 1, - help = "Print to the user X number of times (default=1).", - }) -end - ---- Add the `--style` parameter onto `parser`. ---- ----@param parser cmdparse.ParameterParser The parent parser to add the parameter onto. ---- -local function _add_style_parameter(parser) - parser:add_parameter({ - names = { "--style", "-s" }, - choices = { - constant.Keyword.style.lowercase, - constant.Keyword.style.uppercase, - }, - help = "lowercase makes WORD into word. uppercase does the reverse.", - }) -end - ----@return cmdparse.ParameterParser # The main parser for the `:PluginTemplate hello-world` command. -function M.make_parser() - local parser = cmdparse.ParameterParser.new({ "hello-world", help = "Print hello to the user." }) - local top_subparsers = - parser:add_subparsers({ destination = "commands", help = "All hello-world commands.", required = true }) - --- @cast top_subparsers cmdparse.Subparsers - - local say = top_subparsers:add_parser({ "say", help = "Print something to the user." }) - local subparsers = - say:add_subparsers({ destination = "say_commands", help = "All say-related commands.", required = true }) - - local phrase = subparsers:add_parser({ "phrase", help = "Print everything that the user types." }) - phrase:add_parameter({ "phrases", count = "*", action = "append", help = "All of the text to print." }) - _add_repeat_parameter(phrase) - _add_style_parameter(phrase) - - local word = subparsers:add_parser({ "word", help = "Print only the first word that the user types." }) - word:add_parameter({ "word", help = "The word to print." }) - _add_repeat_parameter(word) - _add_style_parameter(word) - - phrase:set_execute(function(data) - ---@cast data plugin_template.NamespaceExecuteArguments - local runner = require("plugin_template._commands.hello_world.say.runner") - - local phrases = data.namespace.phrases - - if not phrases then - phrases = {} - end - - runner.run_say_phrase(phrases, data.namespace["repeat"], data.namespace.style) - end) - - word:set_execute(function(data) - ---@cast data plugin_template.NamespaceExecuteArguments - local runner = require("plugin_template._commands.hello_world.say.runner") - - runner.run_say_word(data.namespace.word or "", data.namespace["repeat"], data.namespace.style) - end) - - return parser -end - -return M diff --git a/lua/plugin_template/_commands/hello_world/say/constant.lua b/lua/plugin_template/_commands/hello_world/say/constant.lua deleted file mode 100644 index b217670..0000000 --- a/lua/plugin_template/_commands/hello_world/say/constant.lua +++ /dev/null @@ -1,9 +0,0 @@ ---- Symbolic variables to use for all `hello-world say`-related commands. ---- ----@module 'plugin_template._commands.hello_world.say.constant' ---- - -return { - Subcommand = { phrase = "phrase", word = "word" }, - Keyword = { style = { lowercase = "lowercase", uppercase = "uppercase" } }, -} diff --git a/lua/plugin_template/_commands/hello_world/say/runner.lua b/lua/plugin_template/_commands/hello_world/say/runner.lua deleted file mode 100644 index 3123956..0000000 --- a/lua/plugin_template/_commands/hello_world/say/runner.lua +++ /dev/null @@ -1,112 +0,0 @@ ---- The main file that implements `hello-world say` outside of COMMAND mode. ---- ----@module 'plugin_template._commands.hello_world.say.runner' ---- - -local constant = require("plugin_template._commands.hello_world.say.constant") -local vlog = require("plugin_template._vendors.vlog") - -local M = {} - ---- Check if `text` is only whitespace. ---- ----@param text string Some words / phrase to check. ----@return boolean # If `text` has only whitespace, return `true`. ---- -local function _is_whitespace(text) - return text:match("^%s*$") == nil -end - ---- Remove any phrases from `text` that has no meaningful words. ---- ----@param text string[] All of the words to check. ----@return string[] # The non-empty text. ---- -local function _filter_missing_strings(text) - local output = {} - - for _, phrase in ipairs(text) do - if _is_whitespace(phrase) then - table.insert(output, phrase) - end - end - - return output -end - ---- Print `phrase` according to the other options. ---- ----@param phrase string[] ---- The text to say. ----@param repeat_? number ---- A 1-or-more value. The number of times to print `word`. ----@param style? string ---- Control how the text should be shown. ---- -local function _say(phrase, repeat_, style) - repeat_ = repeat_ or 1 - style = style or constant.Keyword.style.lowercase - local text = vim.fn.join(phrase, " ") - - if style == constant.Keyword.style.lowercase then - text = string.lower(text) - elseif style == constant.Keyword.style.uppercase then - text = string.upper(text) - end - - for _ = 1, repeat_ do - vim.notify(text, vim.log.levels.INFO) - end -end - ---- Print `phrase` according to the other options. ---- ----@param phrase string[] ---- The text to say. ----@param repeat_ number? ---- A 1-or-more value. The number of times to print `word`. ----@param style string? ---- Control how the text should be shown. ---- -function M.run_say_phrase(phrase, repeat_, style) - vlog.debug("Running hello-world say word.") - - phrase = _filter_missing_strings(phrase) - - if vim.tbl_isempty(phrase) then - vim.notify("No phrase was given", vim.log.levels.INFO) - - return - end - - vim.notify("Saying phrase", vim.log.levels.INFO) - - _say(phrase, repeat_, style) -end - ---- Print `phrase` according to the other options. ---- ----@param word string ---- The text to say. ----@param repeat_ number? ---- A 1-or-more value. The number of times to print `word`. ----@param style string? ---- Control how the text should be shown. ---- -function M.run_say_word(word, repeat_, style) - vlog.debug("Running hello-world say word.") - - if word == "" then - vim.notify("No word was given", vim.log.levels.INFO) - - return - end - - word = vim.fn.split(word, " ")[1] -- Make sure it's only one word - - vim.notify("Saying word", vim.log.levels.INFO) - - _say({ word }, repeat_, style) -end - -return M diff --git a/lua/plugin_template/_core/configuration.lua b/lua/plugin_template/_core/configuration.lua deleted file mode 100644 index d26bf2b..0000000 --- a/lua/plugin_template/_core/configuration.lua +++ /dev/null @@ -1,100 +0,0 @@ ---- All functions and data to help customize `plugin_template` for this user. ---- ----@module 'plugin_template._core.configuration' ---- - -local say_constant = require("plugin_template._commands.hello_world.say.constant") - -local vlog = require("plugin_template._vendors.vlog") - -local M = {} - --- NOTE: Don't remove this line. It makes the Lua module much easier to reload -vim.g.loaded_plugin_template = false - ----@type plugin_template.Configuration -M.DATA = {} - --- TODO: (you) If you use the vlog.lua for built-in logging, keep the `logging` --- section. Otherwise delete it. --- --- It's recommended to keep the `display` section in any case. --- ----@type plugin_template.Configuration -local _DEFAULTS = { - cmdparse = { auto_complete = { display = { help_flag = true } } }, - logging = { level = "info", use_console = false, use_file = false }, -} - --- TODO: (you) Update these sections depending on your intended plugin features. -local _EXTRA_DEFAULTS = { - commands = { - goodnight_moon = { read = { phrase = "A good book" } }, - hello_world = { - say = { ["repeat"] = 1, style = say_constant.Keyword.style.lowercase }, - }, - }, - tools = { - lualine = { - arbitrary_thing = { - -- color = { link = "#555555" }, - color = "Visual", - text = " Arbitrary Thing", - }, - copy_logs = { - -- color = { link = "#D3D3D3" }, - color = "Comment", - text = "󰈔 Copy Logs", - }, - goodnight_moon = { - -- color = { fg = "#0000FF" }, - color = "Question", - text = "⏾ Goodnight moon", - }, - hello_world = { - -- color = { fg = "#FFA07A" }, - color = "Title", - text = " Hello, World!", - }, - }, - telescope = { - goodnight_moon = { - { "Foo Book", "Author A" }, - { "Bar Book Title", "John Doe" }, - { "Fizz Drink", "Some Name" }, - { "Buzz Bee", "Cool Person" }, - }, - hello_world = { "Hi there!", "Hello, Sailor!", "What's up, doc?" }, - }, - }, -} - -_DEFAULTS = vim.tbl_deep_extend("force", _DEFAULTS, _EXTRA_DEFAULTS) - ---- Setup `plugin_template` for the first time, if needed. -function M.initialize_data_if_needed() - if vim.g.loaded_plugin_template then - return - end - - M.DATA = vim.tbl_deep_extend("force", _DEFAULTS, vim.g.plugin_template_configuration or {}) - - vim.g.loaded_plugin_template = true - - vlog.new(M.DATA.logging or {}, true) - - vlog.fmt_debug("Initialized plugin-template's configuration.") -end - ---- Merge `data` with the user's current configuration. ---- ----@param data plugin_template.Configuration? All extra customizations for this plugin. ----@return plugin_template.Configuration # The configuration with 100% filled out values. ---- -function M.resolve_data(data) - M.initialize_data_if_needed() - - return vim.tbl_deep_extend("force", M.DATA, data or {}) -end - -return M diff --git a/lua/plugin_template/_core/tabler.lua b/lua/plugin_template/_core/tabler.lua deleted file mode 100644 index 50a52cb..0000000 --- a/lua/plugin_template/_core/tabler.lua +++ /dev/null @@ -1,126 +0,0 @@ ---- Make dealing with Lua tables a bit easier. ---- ----@module 'plugin_template._core.tabler' ---- - -local M = {} - ---- Get a sub-section copy of `table_` as a new table. ---- ----@param table_ table ---- A list / array / dictionary / sequence to copy + reduce. ----@param first? number ---- The start index to use. This value is **inclusive** (the given index ---- will be returned). Uses `table_`'s first index if not provided. ----@param last? number ---- The end index to use. This value is **inclusive** (the given index will ---- be returned). Uses every index to the end of `table_`' if not provided. ----@param step? number ---- The step size between elements in the slice. Defaults to 1 if not provided. ----@return table ---- The subset of `table_`. ---- -function M.get_slice(table_, first, last, step) - local sliced = {} - - for i = first or 1, last or #table_, step or 1 do - sliced[#sliced + 1] = table_[i] - end - - return sliced -end - ---- Access the attribute(s) within `data` from `items`. ---- ----@param data any Some nested data to query. e.g. `{a={b={c=true}}}`. ----@param items string[] Some attributes to query. e.g. `{"a", "b", "c"}`. ----@return any? # The found value, if any. ---- -function M.get_value(data, items) - local current = data - local found = {} - local count = #items - - for index = 1, count do - local item = items[index] - current = current[item] - - if current == nil then - return nil - end - - table.insert(found, item) - - local type_ = type(current) - - if index < count and type_ ~= "table" then - error(string.format("%s: expected table, got %s", vim.fn.join(found, "."), type_), 0) - end - end - - return current -end - ---- Iterate over all of the given arrays. ---- ----@param ... table[] All of the tables to expand ----@return any # Every element of each table, in order. ---- -function M.chain(...) - local lists = { ... } - local index = 0 - local current = 1 - - return function() - while current <= #lists do - index = index + 1 - - if index <= #lists[current] then - return lists[current][index] - else - -- Move to the next list - index = 0 - current = current + 1 - end - end - end -end - ---- Delete the contents of `data`. ---- ----@param data table A dictionary or array to clear. ---- -function M.clear(data) - -- Clear the table - for index = #data, 1, -1 do - table.remove(data, index) - end -end - ---- Append all of `items` to `table_`. ---- ----@param table_ any[] Any values to add. ----@param items any The values to add. ---- -function M.extend(table_, items) - for _, item in ipairs(items) do - table.insert(table_, item) - end -end - ---- Create a copy of `array` with its items in reverse order. ---- ----@param array table Some (non-dictionary) items e.g. `{"a", "b", "c"}`. ----@return table # The reversed items e.g. `{"c", "b", "a"}`. ---- -function M.reverse_array(array) - local output = {} - - for index = #array, 1, -1 do - table.insert(output, array[index]) - end - - return output -end - -return M diff --git a/lua/plugin_template/_core/texter.lua b/lua/plugin_template/_core/texter.lua deleted file mode 100644 index 6b82a8b..0000000 --- a/lua/plugin_template/_core/texter.lua +++ /dev/null @@ -1,104 +0,0 @@ ---- Make manipulating Lua text easier. ---- ----@module 'plugin_template._core.texter' ---- - -local M = {} - ---- Check if `character` is a standard A-Z 0-9ish character. ---- ----@param character string Some single-value to check. ----@return boolean # If it's alpha return `true`. ---- -function M.is_alphanumeric(character) - return character:match("^[A-Za-z0-9]$") ~= nil -end - ---- Check if `character` is "regular" text but not alphanumeric. ---- ---- Examples would be Asian characters, Arabic, emojis, etc. ---- ----@param character string Some single-value to check. ----@return boolean # If found return `true`. ---- -function M.is_unicode(character) - local code_point = character:byte() - - return code_point > 127 -end - ---- Check if `items` is a flat array/list of string values. ---- ----@param items any An array to check. ----@return boolean # If found, return `true`. ---- -function M.is_string_list(items) - if type(items) ~= "table" then - return false - end - - for _, item in ipairs(items) do - if type(item) ~= "string" then - return false - end - end - - return true -end - ---- Check if `character` is a space, tab, or newline. ---- ----@param character string Basically `" "`, `\n`, `\t`. ----@return boolean # If it's any whitespace, return `true`. ---- -function M.is_whitespace(character) - return character == "" or character:match("%s+") -end - ---- Check all elements in `values` for `prefix` text. ---- ----@param values string[] All values to check. e.g. `{"foo", "bar"}`. ----@param prefix string The prefix text to search for. ----@return string[] # All found values, if any. ---- -function M.get_array_startswith(values, prefix) - local output = {} - - for _, value in ipairs(values) do - if vim.startswith(value, prefix) then - table.insert(output, value) - end - end - - return output -end - ---- Add indentation to `text. ---- ----@param text string Some phrase to indent one level. e.g. `"foo"`. ----@return string # The indented text, `" foo"`. ---- -function M.indent(text) - return string.format(" %s", text) -end - ---- Remove leading (left) whitespace `text`, if there is any. ---- ----@param text string Some text e.g. `" -- "`. ----@return string # The removed text e.g. `"-- "`. ---- -function M.lstrip(text) - return (text:gsub("^%s*", "")) -end - ---- Check if `text` starts with `start` string. ---- ----@param text string The full character / word / phrase. e.g. `"foot"`. ----@param start string The first letter(s) to check for. e.g.g `"foo"`. ----@return boolean # If found, return `true`. ---- -function M.startswith(text, start) - return text:sub(1, #start) == start -end - -return M diff --git a/lua/plugin_template/_vendors/vlog.lua b/lua/plugin_template/_vendors/vlog.lua deleted file mode 100644 index 3693c3d..0000000 --- a/lua/plugin_template/_vendors/vlog.lua +++ /dev/null @@ -1,180 +0,0 @@ --- vlog.lua --- --- Inspired by rxi/log.lua --- Modified by tjdevries and can be found at github.com/tjdevries/vlog.nvim --- --- This library is free software; you can redistribute it and/or modify it --- under the terms of the MIT license. See LICENSE for details. - --- User configuration section -local default_config = { - -- Name of the plugin. Prepended to log messages - plugin = "plugin_template", - - -- Should print the output to neovim while running - use_console = true, - - -- Should highlighting be used in console (using echohl) - highlights = true, - - -- Should write to a file - use_file = true, - - -- Any messages above this level will be logged. - level = "info", - - -- Level configuration - modes = { - { name = "trace", hl = "Comment" }, - { name = "debug", hl = "Comment" }, - { name = "info", hl = "None" }, - { name = "warn", hl = "WarningMsg" }, - { name = "error", hl = "ErrorMsg" }, - { name = "fatal", hl = "ErrorMsg" }, - }, - - -- Define this path to redirect the log file to wherever you need it to go - output_path = nil, - - -- Can limit the number of decimals displayed for floats - float_precision = 0.01, -} - --- {{{ NO NEED TO CHANGE -local log = {} - ----@diagnostic disable-next-line: deprecated -local unpack = unpack or table.unpack - -local _LEVEL_NUMBER_TO_LEVEL_NAME = { - [vim.log.levels.DEBUG] = "debug", - [vim.log.levels.ERROR] = "error", - [vim.log.levels.INFO] = "info", - [vim.log.levels.TRACE] = "trace", - [vim.log.levels.WARN] = "warn", -} - -log.new = function(config, standalone) - config = vim.tbl_deep_extend("force", default_config, config) - config.level = _LEVEL_NUMBER_TO_LEVEL_NAME[config.level] or config.level - - local obj - if standalone then - obj = log - else - obj = {} - end - - obj._is_logging_to_file_enabled = config.use_file - - obj._output_path = config.output_path - or vim.fs.joinpath(vim.api.nvim_call_function("stdpath", { "data" }), string.format("%s.log", config.plugin)) - - local levels = {} - for i, v in ipairs(config.modes) do - levels[v.name] = i - end - - local round = function(x, increment) - increment = increment or 1 - x = x / increment - return (x > 0 and math.floor(x + 0.5) or math.ceil(x - 0.5)) * increment - end - - local make_string = function(...) - local t = {} - for i = 1, select("#", ...) do - local x = select(i, ...) - - if type(x) == "number" and config.float_precision then - x = tostring(round(x, config.float_precision)) - elseif type(x) == "table" then - x = vim.inspect(x) - else - x = tostring(x) - end - - t[#t + 1] = x - end - return table.concat(t, " ") - end - - local log_at_level = function(level, level_config, message_maker, ...) - -- Return early if we're below the config.level - if level < levels[config.level] then - return - end - local nameupper = level_config.name:upper() - - local msg = message_maker(...) - local info = debug.getinfo(2, "Sl") - local lineinfo = info.short_src .. ":" .. info.currentline - - -- Output to console - if config.use_console then - local console_string = string.format("[%-6s%s] %s: %s", nameupper, os.date("%H:%M:%S"), lineinfo, msg) - - if config.highlights and level_config.hl then - vim.cmd(string.format("echohl %s", level_config.hl)) - end - - local split_console = vim.split(console_string, "\n") - for _, v in ipairs(split_console) do - vim.cmd(string.format([[echom "[%s] %s"]], config.plugin, vim.fn.escape(v, '"'))) - end - - if config.highlights and level_config.hl then - vim.cmd("echohl NONE") - end - end - - -- Output to log file - if obj._is_logging_to_file_enabled then - local fp = io.open(obj._output_path, "a") - - if not fp then - vim.notify(string.format('Unable to log to "%s" path.', obj._output_path), vim.log.levels.ERROR) - else - local str = string.format("[%-6s%s] %s: %s\n", nameupper, os.date(), lineinfo, msg) - fp:write(str) - fp:close() - end - end - end - - for i, x in ipairs(config.modes) do - obj[x.name] = function(...) - return log_at_level(i, x, make_string, ...) - end - - obj[("fmt_%s"):format(x.name)] = function(...) - local passed = { ... } - return log_at_level(i, x, function() - local fmt = table.remove(passed, 1) - local inspected = {} - for _, v in ipairs(passed) do - if type(v) == "string" then - table.insert(inspected, v) - else - table.insert(inspected, vim.inspect(v)) - end - end - return string.format(fmt, unpack(inspected)) - end) - end - end - - obj["is_logging_to_file"] = function() - return obj._is_logging_to_file_enabled - end - - obj["get_log_path"] = function() - return obj._output_path - end - - obj["toggle_file_logging"] = function() - obj._is_logging_to_file_enabled = not obj._is_logging_to_file_enabled - end -end - -return log diff --git a/lua/plugin_template/health.lua b/lua/plugin_template/health.lua deleted file mode 100644 index 812a890..0000000 --- a/lua/plugin_template/health.lua +++ /dev/null @@ -1,471 +0,0 @@ ---- Make sure `plugin_template` will work as expected. ---- ---- At minimum, we validate that the user's configuration is correct. But other ---- checks can happen here if needed. ---- ----@module 'plugin_template.health' ---- - -local configuration_ = require("plugin_template._core.configuration") -local say_constant = require("plugin_template._commands.hello_world.say.constant") -local tabler = require("plugin_template._core.tabler") -local texter = require("plugin_template._core.texter") -local vlog = require("plugin_template._vendors.vlog") - -local M = {} - --- NOTE: This file is defer-loaded so it's okay to run this in the global scope -configuration_.initialize_data_if_needed() - ----@class lualine.ColorHex ---- The table that Lualine expects when it sets colors. ----@field bg string ---- The background hex color. e.g. `"#444444"`. ----@field fg string ---- The text hex color. e.g. `"#DD0000"`. ----@field gui string ---- The background hex color. e.g. `"#444444"`. - ---- Check if `value` has keys that it should not. ---- ----@param value lualine.ColorHex ---- -local function _has_extra_color_keys(value) - local keys = { "bg", "fg", "gui" } - - for key, _ in pairs(value) do - if not vim.tbl_contains(keys, key) then - return true - end - end - - return false -end - ---- Make sure `text` is a HEX code. e.g. `"#D0FF1A"`. ---- ----@param text string An expected HEX code. ----@return boolean # If `text` matches, return `true`. ---- -local function _is_hex_color(text) - if type(text) ~= "string" then - return false - end - - return text:match("^#%x%x%x%x%x%x$") ~= nil -end - ---- Add issues to `array` if there are errors. ---- ---- Todo: ---- Once Neovim 0.10 is dropped, use the new function signature ---- for vim.validate to make this function cleaner. ---- ----@param array string[] ---- All of the cumulated errors, if any. ----@param name string ---- The key to check for. ----@param value_creator fun(): any ---- A function that generates the value. ----@param expected string | fun(value: any): boolean ---- If `value_creator()` does not match `expected`, this error message is ---- shown to the user. ----@param message (string | boolean)? ---- If it's a string, it's the error message when ---- `value_creator()` does not match `expected`. When it's ---- `true`, it means it's okay for `value_creator()` not to match `expected`. ---- -local function _append_validated(array, name, value_creator, expected, message) - local success, value = pcall(value_creator) - - if not success then - table.insert(array, value) - - return - end - - local validated - success, validated = pcall(vim.validate, { - -- TODO: I think the Neovim type annotation is wrong. Once Neovim - -- 0.10 is dropped let's just change this over to the new - -- vim.validate signature. - -- - ---@diagnostic disable-next-line: assign-type-mismatch - [name] = { value, expected, message }, - }) - - if not success then - table.insert(array, validated) - end -end - ---- Check if `data` is a boolean under `key`. ---- ----@param key string The configuration value that we are checking. ----@param data any The object to validate. ----@return string? # The found error message, if any. ---- -local function _get_boolean_issue(key, data) - local success, message = pcall(vim.validate, { - [key] = { - data, - function(value) - if value == nil then - -- NOTE: This value is optional so it's fine it if is not defined. - return true - end - - return type(value) == "boolean" - end, - -- TODO: I think the Neovim type annotation is wrong. Once Neovim - -- 0.10 is dropped let's just change this over to the new - -- vim.validate signature. - -- - ---@diagnostic disable-next-line: assign-type-mismatch - "a boolean", - }, - }) - - if success then - return nil - end - - return message -end - ---- Check all "cmdparse" values for issues. ---- ----@param data plugin_template.Configuration All of the user's fallback settings. ----@return string[] # All found issues, if any. ---- -local function _get_cmdparse_issues(data) - local output = {} - - _append_validated(output, "cmdparse.auto_complete.display.help_flag", function() - return tabler.get_value(data, { "cmdparse", "auto_complete", "display", "help_flag" }) - end, "boolean", true) - - return output -end - ---- Check all "commands" values for issues. ---- ----@param data plugin_template.Configuration All of the user's fallback settings. ----@return string[] # All found issues, if any. ---- -local function _get_command_issues(data) - local output = {} - - _append_validated(output, "commands.goodnight_moon.read.phrase", function() - return tabler.get_value(data, { "commands", "goodnight_moon", "read", "phrase" }) - end, "string") - - _append_validated(output, "commands.hello_world.say.repeat", function() - return tabler.get_value(data, { "commands", "hello_world", "say", "repeat" }) - end, function(value) - return type(value) == "number" and value > 0 - end, "a number (value must be 1-or-more)") - - _append_validated(output, "commands.hello_world.say.style", function() - return tabler.get_value(data, { "commands", "hello_world", "say", "style" }) - end, function(value) - local choices = vim.tbl_keys(say_constant.Keyword.style) - - return vim.tbl_contains(choices, value) - end, '"lowercase" or "uppercase"') - - return output -end - ---- Check the contents of the "tools.lualine" configuration for any issues. ---- ---- Issues include: ---- - Defining tools.lualine but it's not a table ---- - Or the table, which is `table>`, has an incorrect value. ---- - The inner tables must also follow a specific structure. ---- ----@param command string A supported `plugin_template` command. e.g. `"hello_world"`. ----@return string[] # All found issues, if any. ---- -local function _get_lualine_command_issues(command, data) - local output = {} - - _append_validated(output, string.format("tools.lualine.%s", command), function() - return data - end, function(value) - if type(value) ~= "table" then - return false - end - - return true - end, 'a table. e.g. { text="some text here" }') - - if not vim.tbl_isempty(output) then - return output - end - - _append_validated(output, string.format("tools.lualine.%s.text", command), function() - return tabler.get_value(data, { "text" }) - end, function(value) - if type(value) ~= "string" then - return false - end - - return true - end, 'a string. e.g. "some text here"') - - _append_validated(output, string.format("tools.lualine.%s.color", command), function() - return tabler.get_value(data, { "color" }) - end, function(value) - if value == nil then - -- NOTE: It's okay for this value to be undefined because - -- we define a fallback for the user. - -- - return true - end - - local type_ = type(value) - - if type_ == "string" then - -- NOTE: We assume that there is a linkable highlight group - -- with the name of `value` already or one that will exist. - -- - return true - end - - if type_ == "table" then - if value.bg ~= nil and not _is_hex_color(value.bg) then - return false - end - - if value.fg ~= nil and not _is_hex_color(value.fg) then - return false - end - - if value.gui ~= nil and type(value.gui) ~= "string" then - return false - end - - if _has_extra_color_keys(value) then - return false - end - - return true - end - - return false - end, 'a table. e.g. {fg="#000000", bg="#FFFFFF", gui="effect"}') - - return output -end - ---- Check all "tools.lualine" values for issues. ---- ----@param data plugin_template.Configuration All of the user's fallback settings. ----@return string[] # All found issues, if any. ---- -local function _get_lualine_issues(data) - local output = {} - - local lualine = tabler.get_value(data, { "tools", "lualine" }) - - _append_validated(output, "tools.lualine", function() - return lualine - end, function(value) - if type(value) ~= "table" then - return false - end - - return true - end, "a table. e.g. { goodnight_moon = {...}, hello_world = {...} }") - - if not vim.tbl_isempty(output) then - return output - end - - for _, command in ipairs({ "arbitrary_thing", "goodnight_moon", "hello_world" }) do - local value = tabler.get_value(lualine, { command }) - - -- NOTE: We have fallback values so it's okay if the value is nil. - if value ~= nil then - local issues = _get_lualine_command_issues(command, value) - - vim.list_extend(output, issues) - end - end - - return output -end - ---- Check if logging configuration `data` has any issues. ---- ----@param data plugin_template.LoggingConfiguration The user's logger settings. ----@return string[] # All of the found issues, if any. ---- -local function _get_logging_issues(data) - local output = {} - - _append_validated(output, "logging", function() - return data - end, function(value) - if type(value) ~= "table" then - return false - end - - return true - end, 'a table. e.g. { level = "info", ... }') - - if not vim.tbl_isempty(output) then - return output - end - - _append_validated(output, "logging.level", function() - return data.level - end, function(value) - if type(value) ~= "string" then - return false - end - - if not vim.tbl_contains({ "trace", "debug", "info", "warn", "error", "fatal" }, value) then - return false - end - - return true - end, 'an enum. e.g. "trace" | "debug" | "info" | "warn" | "error" | "fatal"') - - local message = _get_boolean_issue("logging.use_console", data.use_console) - - if message ~= nil then - table.insert(output, message) - end - - message = _get_boolean_issue("logging.use_file", data.use_file) - - if message ~= nil then - table.insert(output, message) - end - - return output -end - ---- Check all "tools.lualine" values for issues. ---- ----@param data plugin_template.Configuration All of the user's fallback settings. ----@return string[] # All found issues, if any. ---- -local function _get_telescope_issues(data) - local output = {} - - local telescope = tabler.get_value(data, { "tools", "telescope" }) - - _append_validated(output, "tools.telescope", function() - return telescope - end, function(value) - if type(value) ~= "table" then - return false - end - - return true - end, "a table. e.g. { goodnight_moon = {...}, hello_world = {...}}") - - if not vim.tbl_isempty(output) then - return output - end - - _append_validated(output, "tools.telescope.goodnight_moon", function() - return telescope.goodnight_moon - end, function(value) - if value == nil then - return true - end - - if type(value) ~= "table" then - return false - end - - for _, item in ipairs(value) do - if not texter.is_string_list(item) then - return false - end - - if #item ~= 2 then - return false - end - end - - return true - end, 'a table. e.g. { {"Book", "Author"} }') - - _append_validated(output, "tools.telescope.hello_world", function() - return telescope.hello_world - end, function(value) - if value == nil then - return true - end - - if type(value) ~= "table" then - return false - end - - return texter.is_string_list(value) - end, 'a table. e.g. { "Hello", "Hi", ...} }') - - return output -end - ---- Check `data` for problems and return each of them. ---- ----@param data plugin_template.Configuration? All extra customizations for this plugin. ----@return string[] # All found issues, if any. ---- -function M.get_issues(data) - if not data or vim.tbl_isempty(data) then - data = configuration_.resolve_data(vim.g.plugin_template_configuration) - end - - local output = {} - vim.list_extend(output, _get_cmdparse_issues(data)) - vim.list_extend(output, _get_command_issues(data)) - - local logging = data.logging - - if logging ~= nil then - vim.list_extend(output, _get_logging_issues(data.logging)) - end - - local lualine = tabler.get_value(data, { "tools", "lualine" }) - - if lualine ~= nil then - vim.list_extend(output, _get_lualine_issues(data)) - end - - local telescope = tabler.get_value(data, { "tools", "telescope" }) - - if telescope ~= nil then - vim.list_extend(output, _get_telescope_issues(data)) - end - - return output -end - ---- Make sure `data` will work for `plugin_template`. ---- ----@param data plugin_template.Configuration? All extra customizations for this plugin. ---- -function M.check(data) - vlog.debug("Running plugin-template health check.") - - vim.health.start("Configuration") - - local issues = M.get_issues(data) - - if vim.tbl_isempty(issues) then - vim.health.ok("Your vim.g.plugin_template_configuration variable is great!") - end - - for _, issue in ipairs(issues) do - vim.health.error(issue) - end -end - -return M diff --git a/lua/plugin_template/init.lua b/lua/plugin_template/init.lua deleted file mode 100644 index 4495a6b..0000000 --- a/lua/plugin_template/init.lua +++ /dev/null @@ -1,91 +0,0 @@ ---- All function(s) that can be called externally by other Lua modules. ---- ---- If a function's signature here changes in some incompatible way, this ---- package must get a new **major** version. ---- ----@module 'plugin_template' ---- - -local configuration = require("plugin_template._core.configuration") -local arbitrary_thing_runner = require("plugin_template._commands.arbitrary_thing.runner") -local copy_logs_runner = require("plugin_template._commands.copy_logs.runner") -local count_sheep = require("plugin_template._commands.goodnight_moon.count_sheep") -local read = require("plugin_template._commands.goodnight_moon.read") -local say_runner = require("plugin_template._commands.hello_world.say.runner") -local sleep = require("plugin_template._commands.goodnight_moon.sleep") - -local M = {} - -configuration.initialize_data_if_needed() - --- TODO: (you) - Change this file to whatever you need. These are just examples - ---- Print the `names`. ---- ----@param names string[]? Some text to print out. e.g. `{"a", "b", "c"}`. ---- -function M.run_arbitrary_thing(names) - arbitrary_thing_runner.run(names) -end - ---- Copy the log data from the given `path` to the user's clipboard. ---- ----@param path string? ---- A path on-disk to look for logs. If none is given, the default fallback ---- location is used instead. ---- -function M.run_copy_logs(path) - copy_logs_runner.run(path) -end - ---- Print `phrase` according to the other options. ---- ----@param phrase string[] ---- The text to say. ----@param repeat_ number? ---- A 1-or-more value. The number of times to print `word`. ----@param style string? ---- Control how the text should be shown. ---- -function M.run_hello_world_say_phrase(phrase, repeat_, style) - say_runner.run_say_phrase(phrase, repeat_, style) -end - ---- Print `phrase` according to the other options. ---- ----@param word string ---- The text to say. ----@param repeat_ number? ---- A 1-or-more value. The number of times to print `word`. ----@param style string? ---- Control how the text should be shown. ---- -function M.run_hello_world_say_word(word, repeat_, style) - say_runner.run_say_word(word, repeat_, style) -end - ---- Count a sheep for each `count`. ---- ----@param count number Prints 1 sheep per `count`. A value that is 1-or-greater. ---- -function M.run_goodnight_moon_count_sheep(count) - count_sheep.run(count) -end - ---- Print the name of the book. ---- ----@param book string The name of the book. ---- -function M.run_goodnight_moon_read(book) - read.run(book) -end - ---- Print Zzz each `count`. ---- ----@param count number? Prints 1 Zzz per `count`. A value that is 1-or-greater. ---- -function M.run_goodnight_moon_sleep(count) - sleep.run(count) -end - -return M diff --git a/lua/plugin_template/types.lua b/lua/plugin_template/types.lua deleted file mode 100644 index a344bc7..0000000 --- a/lua/plugin_template/types.lua +++ /dev/null @@ -1,104 +0,0 @@ ---- A collection of types to be included / used in other Lua files. ---- ---- These types are either required by the Lua API or required for the normal ---- operation of this Lua plugin. ---- ----@module 'plugin_template.types' ---- - ----@alias vim.log.levels.DEBUG number Messages to show to plugin maintainers. ----@alias vim.log.levels.ERROR number Unrecovered issues to show to the plugin users. ----@alias vim.log.levels.INFO number Informative messages to show to the plugin users. ----@alias vim.log.levels.TRACE number Low-level or spammy messages. ----@alias vim.log.levels.WARN number An error that was recovered but could be an issue. - ----@class plugin_template.Configuration ---- The user's customizations for this plugin. ----@field cmdparse plugin_template.ConfigurationCmdparse? ---- All settings that control the command mode tools (parsing, auto-complete, etc). ----@field commands plugin_template.ConfigurationCommands? ---- Customize the fallback behavior of all `:PluginTemplate` commands. ----@field logging plugin_template.LoggingConfiguration? ---- Control how and which logs print to file / Neovim. ----@field tools plugin_template.ConfigurationTools? ---- Optional third-party tool integrations. - ----@class plugin_template.ConfigurationCmdparse ---- All settings that control the command mode tools (parsing, auto-complete, etc). ----@field auto_complete plugin_template.ConfigurationCmdparseAutoComplete ---- The settings that control what happens during auto-completion. - ----@class plugin_template.ConfigurationCmdparseAutoComplete ---- The settings that control what happens during auto-completion. ----@field display {help_flag: boolean} ---- help_flag = Show / Hide the --help flag during auto-completion. - ----@class plugin_template.ConfigurationCommands ---- Customize the fallback behavior of all `:PluginTemplate` commands. ----@field goodnight_moon plugin_template.ConfigurationGoodnightMoon? ---- The default values when a user calls `:PluginTemplate goodnight-moon`. ----@field hello_world plugin_template.ConfigurationHelloWorld? ---- The default values when a user calls `:PluginTemplate hello-world`. - ----@class plugin_template.ConfigurationGoodnightMoon ---- The default values when a user calls `:PluginTemplate goodnight-moon`. ----@field read plugin_template.ConfigurationGoodnightMoonRead? ---- The default values when a user calls `:PluginTemplate goodnight-moon read`. - ----@class plugin_template.LoggingConfiguration ---- Control whether or not logging is printed to the console or to disk. ----@field level ( ---- | "trace" ---- | "debug" ---- | "info" ---- | "warn" | "error" ---- | "fatal" ---- | vim.log.levels.DEBUG ---- | vim.log.levels.ERROR ---- | vim.log.levels.INFO ---- | vim.log.levels.TRACE ---- | vim.log.levels.WARN)? ---- Any messages above this level will be logged. ----@field use_console boolean? ---- Should print the output to neovim while running. Warning: This is very ---- spammy. You probably don't want to enable this unless you have to. ----@field use_file boolean? ---- Should write to a file. ----@field output_path string? ---- The default path on-disk where log files will be written to. ---- Defaults to "/home/selecaoone/.local/share/nvim/plugin_name.log". - ----@class plugin_template.ConfigurationGoodnightMoonRead ---- The default values when a user calls `:PluginTemplate goodnight-moon read`. ----@field phrase string ---- The book to read if no book is given by the user. - ----@class plugin_template.ConfigurationHelloWorld ---- The default values when a user calls `:PluginTemplate hello-world`. ----@field say plugin_template.ConfigurationHelloWorldSay? ---- The default values when a user calls `:PluginTemplate hello-world say`. - ----@class plugin_template.ConfigurationHelloWorldSay ---- The default values when a user calls `:PluginTemplate hello-world say`. ----@field repeat number ---- A 1-or-more value. When 1, the phrase is said once. When 2+, the phrase ---- is repeated that many times. ----@field style "lowercase" | "uppercase" ---- Control how the text is displayed. e.g. "uppercase" changes "hello" to "HELLO". - ----@class plugin_template.ConfigurationTools ---- Optional third-party tool integrations. ----@field lualine plugin_template.ConfigurationToolsLualine? ---- A Vim statusline replacement that will show the command that the user just ran. - ----@alias plugin_template.ConfigurationToolsLualine table ---- Each runnable command and its display text. - ----@class plugin_template.ConfigurationToolsLualineData ---- The display values that will be used when a specific `plugin_template` ---- command runs. ----@diagnostic disable-next-line: undefined-doc-name ----@field color vim.api.keyset.highlight? ---- The foreground/background color to use for the Lualine status. ----@field prefix string? ---- The text to display in lualine. diff --git a/lua/telescope/_extensions/plugin_template/init.lua b/lua/telescope/_extensions/plugin_template/init.lua deleted file mode 100644 index 21ea58c..0000000 --- a/lua/telescope/_extensions/plugin_template/init.lua +++ /dev/null @@ -1,45 +0,0 @@ ---- Register `plugin_template` to telescope.nvim. ---- ----@source https://github.com/nvim-telescope/telescope.nvim ---- ----@module 'telescope._extensions.plugin_template' ---- - -local has_telescope, telescope = pcall(require, "telescope") - -if not has_telescope then - error("Telescope interface requires telescope.nvim (https://github.com/nvim-telescope/telescope.nvim)") -end - -local configuration = require("plugin_template._core.configuration") -local runner = require("telescope._extensions.plugin_template.runner") - --- NOTE: This file is defer-loaded so it's okay to run this in the global scope -configuration.initialize_data_if_needed() - ---- Run the `:Telescope plugin_template goodnight-moon` command. ---- ----@param options telescope.CommandOptions The Telescope UI / layout options. ---- -local function _run_goodnight_moon(options) - local picker = runner.get_goodnight_moon_picker(options) - - picker:find() -end - ---- Run the `:Telescope plugin_template hello-world` command. ---- ----@param options telescope.CommandOptions The Telescope UI / layout options. ---- -local function _run_hello_world(options) - local picker = runner.get_hello_world_picker(options) - - picker:find() -end - -return telescope.register_extension({ - exports = { - ["goodnight-moon"] = _run_goodnight_moon, - ["hello-world"] = _run_hello_world, - }, -}) diff --git a/lua/telescope/_extensions/plugin_template/runner.lua b/lua/telescope/_extensions/plugin_template/runner.lua deleted file mode 100644 index c8891e8..0000000 --- a/lua/telescope/_extensions/plugin_template/runner.lua +++ /dev/null @@ -1,166 +0,0 @@ ---- Register `plugin_template` to telescope.nvim. ---- ----@source https://github.com/nvim-telescope/telescope.nvim ---- ----@module 'telescope._extensions.plugin_template_' ---- - -local M = {} - -local action_state = require("telescope.actions.state") -local action_utils = require("telescope.actions.utils") -local entry_display = require("telescope.pickers.entry_display") -local finders = require("telescope.finders") -local pickers = require("telescope.pickers") -local telescope_actions = require("telescope.actions") -local telescope_config = require("telescope.config").values - -local configuration = require("plugin_template._core.configuration") -local read = require("plugin_template._commands.goodnight_moon.read") -local say_runner = require("plugin_template._commands.hello_world.say.runner") -local tabler = require("plugin_template._core.tabler") - ----@diagnostic disable-next-line: deprecated -local unpack = unpack or table.unpack - -vim.api.nvim_set_hl(0, "PluginTemplateTelescopeEntry", { link = "TelescopeResultsNormal", default = true }) -vim.api.nvim_set_hl(0, "PluginTemplateTelescopeSecondary", { link = "TelescopeResultsComment", default = true }) - ----@alias telescope.CommandOptions table - ---- Run the `:Telescope plugin_template goodnight-moon` command. ---- ----@param options telescope.CommandOptions The Telescope UI / layout options. ---- -function M.get_goodnight_moon_picker(options) - local function _select_book(buffer) - for _, book in ipairs(M.get_selection(buffer)) do - read.run(book) - end - - telescope_actions.close(buffer) - end - - local displayer = entry_display.create({ - separator = " ", - items = { - { width = 0.8 }, - { remaining = true }, - }, - }) - - local books = tabler.get_value(configuration.DATA, { "tools", "telescope", "goodnight_moon" }) or {} - books = tabler.reverse_array(books) - - local picker = pickers.new(options, { - prompt_title = "Choose A Book", - finder = finders.new_table({ - results = books, - entry_maker = function(data) - local name, author = unpack(data) - local value = string.format("%s - %s", name, author) - - return { - display = function(entry) - return displayer({ - { entry.name, "PluginTemplateTelescopeEntry" }, - { entry.author, "PluginTemplateTelescopeSecondary" }, - }) - end, - author = author, - name = name, - value = value, - ordinal = value, - } - end, - }), - previewer = false, - sorter = telescope_config.generic_sorter(options), - attach_mappings = function() - telescope_actions.select_default:replace(_select_book) - - return true - end, - }) - - return picker -end - ---- Run the `:Telescope plugin_template hello-world` command. ---- ----@param options telescope.CommandOptions The Telescope UI / layout options. ---- -function M.get_hello_world_picker(options) - local function _select_phrases(buffer) - local phrases = M.get_selection(buffer) - - say_runner.run_say_phrase(phrases) - - telescope_actions.close(buffer) - end - - local displayer = entry_display.create({ - separator = " ", - items = { { width = 0.8 }, { remaining = true } }, - }) - - local phrases = tabler.get_value(configuration.DATA, { "tools", "telescope", "hello_world" }) or {} - phrases = tabler.reverse_array(phrases) - - local picker = pickers.new(options, { - prompt_title = "Say Hello", - finder = finders.new_table({ - results = phrases, - entry_maker = function(data) - return { - display = function(entry) - return displayer({ entry.value }) - end, - name = data, - value = data, - ordinal = data, - } - end, - }), - previewer = false, - sorter = telescope_config.generic_sorter(options), - attach_mappings = function() - telescope_actions.select_default:replace(_select_phrases) - - return true - end, - }) - - return picker -end - ---- Gather the selected Telescope entries. ---- ---- If the user made selections, get each of those. If they pressed ---- without any assignments then just get the line that they ---- called on. ---- ----@param buffer number A 0-or-more value of some Vim buffer. ----@return string[] # The found selection(s) if any. ---- -function M.get_selection(buffer) - local books = {} - - action_utils.map_selections(buffer, function(selection) - table.insert(books, selection.value) - end) - - if not vim.tbl_isempty(books) then - return books - end - - local selection = action_state.get_selected_entry() - - if selection ~= nil then - return { selection.value } - end - - return {} -end - -return M diff --git a/plugin/cursor_text_objects.lua b/plugin/cursor_text_objects.lua new file mode 100644 index 0000000..3ef3cf3 --- /dev/null +++ b/plugin/cursor_text_objects.lua @@ -0,0 +1,36 @@ +--- Setup the [ and ] pending-operator mappings. + +--- Set up the basic `cursor_text_objects` mappings. + +--- Create a cursor xmap mapping. +--- +---@param keys string +--- The new pending-operator to add. +---@param mode "o" | "x" +--- The Neovim mode to consider for the mapping. +---@param direction "down" | "up" +--- Which way to crop the text object. "up" means "operate from the cursor's +--- position to the up/left-most position" and "down" means "operate from +--- the cursor's position (including the cursor's current line, if +--- applicable) to the down/right-most position". +--- +local function _map(keys, mode, direction) + local command + + if mode == "o" then + command = ":set operatorfunc=v:lua.require'cursor_text_objects'.operatorfunc%sg@" + elseif mode == "x" then + command = ":set operatorfunc=v:lua.require'cursor_text_objects'.visual%sg@" + end + + vim.keymap.set(mode, keys, function() + require("cursor_text_objects").prepare(direction) + + return string.format(command, vim.v.count1) + end, { expr = true, silent = true }) +end + +_map("[", "o", "up") +_map("]", "o", "down") +_map("[", "x", "up") +_map("]", "x", "down") diff --git a/plugin/plugin_template.lua b/plugin/plugin_template.lua deleted file mode 100644 index 10ca622..0000000 --- a/plugin/plugin_template.lua +++ /dev/null @@ -1,42 +0,0 @@ ---- All `plugin_template` command definitions. - -local cli_subcommand = require("plugin_template._cli.cli_subcommand") - -local _PREFIX = "PluginTemplate" - ----@type plugin_template.ParserCreator -local _SUBCOMMANDS = function() - local arbitrary_thing = require("plugin_template._commands.arbitrary_thing.parser") - local cmdparse = require("plugin_template._cli.cmdparse") - local copy_logs = require("plugin_template._commands.copy_logs.parser") - local goodnight_moon = require("plugin_template._commands.goodnight_moon.parser") - local hello_world = require("plugin_template._commands.hello_world.parser") - - local root_parser = cmdparse.ParameterParser.new({ help = "The root of all commands." }) - local root_subparsers = root_parser:add_subparsers({ "command", help = "All root commands." }) - - local parser = root_subparsers:add_parser({ name = _PREFIX, help = "The starting command." }) - local subparsers = parser:add_subparsers({ "commands", help = "All runnable commands." }) - - subparsers:add_parser(arbitrary_thing.make_parser()) - subparsers:add_parser(copy_logs.make_parser()) - subparsers:add_parser(goodnight_moon.make_parser()) - subparsers:add_parser(hello_world.make_parser()) - - return root_parser -end - -vim.api.nvim_create_user_command(_PREFIX, cli_subcommand.make_parser_triager(_SUBCOMMANDS), { - nargs = "*", - desc = "PluginTemplate's command API.", - complete = cli_subcommand.make_parser_completer(_SUBCOMMANDS), -}) - -vim.keymap.set("n", "(PluginTemplateSayHi)", function() - local configuration = require("plugin_template._core.configuration") - local plugin_template = require("plugin_template") - - configuration.initialize_data_if_needed() - - plugin_template.run_hello_world_say_word("Hi!") -end, { desc = "Say hi to the user." }) diff --git a/scripts/make_api_documentation/main.lua b/scripts/make_api_documentation/main.lua deleted file mode 100644 index 89b52e3..0000000 --- a/scripts/make_api_documentation/main.lua +++ /dev/null @@ -1,445 +0,0 @@ ---- The file that auto-creates documentation for `plugin_template`. - -local success, doc = pcall(require, "mini.doc") - -if not success then - error("mini.doc is required to run this script. Please clone + source https://github.com/echasnovski/mini.doc") -end - ----@diagnostic disable-next-line: undefined-field -if _G.MiniDoc == nil then - doc.setup() -end - ----@class MiniDoc.Hooks ---- Customization options during documentation generation. It can control ---- section headers, newlines, etc. ----@field sections table ---- When a section is visited by the documentation generator, this table is ---- consulted to decide what to do with that section. - ----@class MiniDoc.SectionInfo ---- A description of what this section is meant to display / represent. ----@field id string ---- The section label. e.g. `"@param"`, `"@return"`, etc. - ----@class MiniDoc.Section ---- A renderable blob of text (which will later auto-create into documentation). ---- This class is from mini.doc. We're just type-annotating it so `llscheck` is happy. ----@see https://github.com/echasnovski/mini.doc ----@field info MiniDoc.SectionInfo ---- A description of what this section is meant to display / represent. ----@field parent MiniDoc.Section? ---- The section that includes this instance as one of its children, if any. ----@field parent_index number? ---- If a `parent` is defined, this is the position of this instance in `parent`. ----@field type string ---- A description about what this object is. Is it a section or a block or ---- something else? Stuff like that. ---- -local _Section = {} -- luacheck: ignore 241 -- variable never accessed - ---- Add `child` to this instance at `index`. ---- ----@param index number The 1-or-more position to add `child` into. ----@param child string The text to add. ---- -function _Section:insert(index, child) end -- luacheck: ignore 212 -- unused argument - ---- Remove a child from this instance at `index`. ---- ----@param index number? The 1-or-more position to remove `child` from. ---- -function _Section:remove(index) end -- luacheck: ignore 212 -- unused argument - ---- Check if `text` is the start of a function's parameters. ---- ----@param text string Some text. e.g. `"Parameters ~"`. ----@return boolean # If it's a section return `true`. ---- -local function _is_field_section(text) - return text:match("%s*Fields%s*~%s*") -end - ---- Check if `text` is the start of a function's parameters. ---- ----@param text string Some text. e.g. `"Parameters ~"`. ----@return boolean # If it's a section return `true`. ---- -local function _is_parameter_section(text) - return text:match("%s*Parameters%s*~%s*") -end - ---- Check if `text` is the start of a function's parameters. ---- ----@param text string Some text. e.g. `"Return ~"`. ----@return boolean # If it's a section return `true`. ---- -local function _is_return_section(text) - return text:match("%s*Return%s*~%s*") -end - ---- Add the text that Vimdoc uses to generate doc/tags (basically surround the text with *s). ---- ----@param text string Any text, e.g. `"plugin_template.ClassName"`. ----@return string # The wrapped text, e.g. `"*plugin_template.ClassName*"`. ---- -local function _add_tag(text) - return (text:gsub("(%S+)", "%*%1%*")) -end - ---- Run `caller` on `section` and all of its children recursively. ---- ----@param caller fun(section: MiniDoc.Section): nil A callback used to modify its given `section`. ----@param section MiniDoc.Section The starting point to traverse underneath. ---- -local function _apply_recursively(caller, section) - caller(section) - - if type(section) == "table" then - for _, t in ipairs(section) do - _apply_recursively(caller, t) - end - end -end - ---- Remove any quotes around `text`. ---- ----@param text string ---- Text that might have prefix / suffix quotes. e.g. `'foo'`. ----@return string ---- The `text` but without the quotes. Inner quotes are retained. e.g. ---- `'foo"bar'` becomes `foo"bar`. ---- -local function _strip_quotes(text) - return (text:gsub("^['\"](.-)['\"]$", "%1")) -end - ---- Get the last (contiguous) key in `data` that is numbered. ---- ----`data` might be a combination of number or string keys. The first key is ----expected to be numbered. If so, we get the last key that is a number. ---- ----@param data table The data to check. ----@return number # The last found key. ---- -local function _get_last_numeric_key(data) - local found = nil - - for key, _ in pairs(data) do - if type(key) ~= "number" then - if not found then - error("No number key could be found.") - end - - return found - end - - found = key - end - - return found -end - ---- Ensure there is one blank space around `section` by modifying it. ---- ----@param section MiniDoc.Section ---- A renderable blob of text (which will later auto-create into documentation). ---- -local function _add_before_after_whitespace(section) - section:insert(1, "") - local last = _get_last_numeric_key(section) - section:insert(last + 1, "") -end - ---- Add leading whitespace to `text`, if `text` is not an empty line. ---- ----@param text string The text to modify, maybe. ----@return string # The modified `text`, as needed. ---- -local function _indent(text) - if not text or text == "" then - return text - end - - return " " .. text -end - ---- Change the function name in `section` from `module_identifier` to `module_name`. ---- ----@param section MiniDoc.Section ---- A renderable blob of text (which will later auto-create into documentation). ---- We assume this `section` represents a Lua function. ----@param module_identifier string ---- Usually a function in Lua is defined with `function M.foo`. In this ---- example, `module_identifier` would be the `M` part. ----@param module_name string ---- The real name for the module. e.g. `"plugin_template"`. ---- -local function _replace_function_name(section, module_identifier, module_name) - local prefix = string.format("^%s%%.", module_identifier) - local replacement = string.format("%s.", module_name) - - for index, line in ipairs(section) do - line = line:gsub(prefix, replacement) - section[index] = line - end -end - ---- Add newlines around `section` if needed. ---- ----@param section MiniDoc.Section ---- The object to possibly modify. ----@param count number? ---- The number of lines to put before `section` if needed. If the section ---- has more newlines than `count`, it is converted back to `count`. ---- -local function _set_trailing_newline(section, count) - local function _is_not_whitespace(text) - return text:match("%S+") - end - - count = count or 1 - local found_text = false - local lines = 0 - - for _, line in ipairs(section) do - if not found_text then - if _is_not_whitespace(line) then - found_text = true - end - elseif _is_not_whitespace(line) then - lines = 0 - else - lines = lines + 1 - end - end - - if count > lines then - for _ = 1, count - lines do - section:insert(1, "") - end - else - for _ = 1, lines - count do - section:remove(1) - end - end -end - ---- Remove the prefix identifier (usually `"M"`, from `"M.get_foo"`). ---- ----@param section MiniDoc.Section ---- A renderable blob of text (which will later auto-create into documentation). ----@param module_identifier string ---- If provided, any reference to this identifier (e.g. `M`) will be ---- replaced with the real import path. ---- -local function _strip_function_identifier(section, module_identifier) - local prefix = string.format("^%s%%.", module_identifier) - - for index, line in ipairs(section) do - line = line:gsub(prefix, "") - section[index] = line - end -end - ---- Create the callbacks that we need to create our documentation. ---- ----@param module_identifier string? ---- If provided, any reference to this identifier (e.g. `M`) will be ---- replaced with the real import path. ----@return MiniDoc.Hooks ---- All of the generated callbacks. ---- -local function _get_module_enabled_hooks(module_identifier) - local module_name = nil - - local hooks = vim.deepcopy(doc.default_hooks) - - hooks.sections["@class"] = function(section) - if #section == 0 or section.type ~= "section" then - return - end - - section[1] = _add_tag(section[1]) - end - - local original_field_hook = hooks.sections["@field"] - - hooks.sections["@field"] = function(section) - original_field_hook(section) - - for index, line in ipairs(section) do - section[index] = _indent(line) - end - end - - hooks.sections["@module"] = function(section) - module_name = _strip_quotes(section[1]) - - section:clear_lines() - end - - local original_param_hook = hooks.sections["@param"] - - hooks.sections["@param"] = function(section) - original_param_hook(section) - - for index, line in ipairs(section) do - section[index] = _indent(line) - end - end - - local original_signature_hook = hooks.sections["@signature"] - - hooks.sections["@signature"] = function(section) - if module_identifier then - _strip_function_identifier(section, module_identifier) - end - - _add_before_after_whitespace(section) - - original_signature_hook(section) - - -- NOTE: Remove the leading whitespace caused by MiniDoc - for index, text in ipairs(section) do - section[index] = (text:gsub("^%s+", "")) - end - end - - local original_tag_hook = hooks.sections["@tag"] - - hooks.sections["@tag"] = function(section) - if module_identifier and module_name then - _replace_function_name(section, module_identifier, module_name) - end - - original_tag_hook(section) - end - - local original_block_post_hook = hooks.block_post - - hooks.block_post = function(block) - original_block_post_hook(block) - - if not block:has_lines() then - return - end - - _apply_recursively(function(section) - if not (type(section) == "table" and section.type == "section") then - return - end - - if section.info.id == "@field" and _is_field_section(section[1]) then - local previous_section = section.parent[section.parent_index - 1] - - if previous_section then - _set_trailing_newline(section) - end - end - - if section.info.id == "@param" and _is_parameter_section(section[1]) then - local previous_section = section.parent[section.parent_index - 1] - - if previous_section then - _set_trailing_newline(previous_section) - end - end - - if section.info.id == "@return" and _is_return_section(section[1]) then - local previous_section = section.parent[section.parent_index - 1] - - if previous_section then - _set_trailing_newline(section) - end - end - end, block) - end - - -- TODO: Add alias support. These lines effectively clear aliases, which is a shame. - hooks.section_pre = function(...) -- luacheck: ignore 212 -- unused argument - end - - hooks.write_pre = function(lines) - table.insert(lines, #lines - 1, "WARNING: This file is auto-generated. Do not edit it!") - - return lines - end - - return hooks -end - ----@return string # Get the directory on-disk where this Lua file is running from. -local function _get_script_directory() - local path = debug.getinfo(1, "S").source:sub(2) -- Remove the '@' at the start - - return path:match("(.*/)") -end - ---- Parse `path` to find the source code that refers to the user's Lua file, if any. - ----@param path string ---- The absolute path to a Lua file on-disk that we assume may have a line ---- like `return M` at the bottom which exports 0-or-more Lua classes / functions. ----@return string? ---- The found identifier. By convention it's usually `"M"` or nothing. ---- -local function _get_module_identifier(path) -- luacheck: ignore 212 -- unused argument - -- TODO: Need to replace this later - -- Ignore weird returns - -- Only get the last return - return "M" -end - ----@class plugin_template.AutoDocumentationEntry ---- The simple source/destination of "Lua file that we want to auto-create ---- documentation from + the .txt file that we want auto-create to". ----@field source string ---- An absolute path to a Lua file on-disk. e.g. `"/path/to/init.lua"`. ----@field destination string ---- An absolute path for the auto-created documentation. ---- e.g. `"/out/plugin_template.txt"`. - ---- Make sure `paths` can be processed by this script. ---- ----@param paths plugin_template.AutoDocumentationEntry[] ---- The source/destination pairs to check. ---- -local function _validate_paths(paths) - for _, entry in ipairs(paths) do - if vim.fn.filereadable(entry.source) ~= 1 then - error(string.format('Source "%s" is not readable.', vim.inspect(entry))) - end - end -end - ---- Convert the files in this plug-in from Lua docstrings to Vimdoc documentation. -local function main() - local current_directory = _get_script_directory() - local root = vim.fs.normalize(vim.fs.joinpath(current_directory, "..", "..")) - local paths = { - { - source = vim.fs.joinpath(root, "lua", "plugin_template", "init.lua"), - destination = vim.fs.joinpath(root, "doc", "plugin_template_api.txt"), - }, - { - source = vim.fs.joinpath(root, "lua", "plugin_template", "types.lua"), - destination = vim.fs.joinpath(root, "doc", "plugin_template_types.txt"), - }, - } - - _validate_paths(paths) - - for _, entry in ipairs(paths) do - local source = entry.source - local destination = entry.destination - - local module_identifier = _get_module_identifier(source) - local hooks = _get_module_enabled_hooks(module_identifier) - - doc.generate({ source }, destination, { hooks = hooks }) - end -end - -main() diff --git a/scripts/make_api_documentation/minimal_init.lua b/scripts/make_api_documentation/minimal_init.lua deleted file mode 100644 index a45657e..0000000 --- a/scripts/make_api_documentation/minimal_init.lua +++ /dev/null @@ -1,6 +0,0 @@ -local directory = os.getenv("MINI_DOC_DIRECTORY") or "/tmp/mini.doc" -local url = "https://github.com/echasnovski/mini.doc" - -vim.fn.system({ "git", "clone", url, directory }) - -vim.opt.rtp:append(directory) diff --git a/spec/cursor_text_objects_spec.lua b/spec/cursor_text_objects_spec.lua new file mode 100644 index 0000000..6b3e515 --- /dev/null +++ b/spec/cursor_text_objects_spec.lua @@ -0,0 +1,1413 @@ +--- Make sure the basic functionality of cursor-text-objects works. +--- +---@module 'spec.cursor_text_objects_spec' +--- + +--- Get the lines of `buffer`. +--- +---@param buffer number # A 1-or-more identifier for the Vim buffer. +---@return string # The text in `buffer`. +--- +local function get_lines(buffer) + return vim.fn.join(vim.api.nvim_buf_get_lines(buffer, 0, -1, false), "\n") +end + +--- Run `keys` from NORMAL mode. +--- +---@param keys string Some command to run. e.g. `d]ap`. +--- +local function call_command(keys) + vim.cmd("normal " .. keys) +end + +--- Create a new Vim buffer with `text` contents. +--- +---@param text string All of the text to add into the buffer. +---@param file_type string? Apply a type to the newly created buffer. +---@return number # A 1-or-more identifier for the Vim buffer. +---@return number # A 1-or-more identifier for the Vim window. +--- +local function make_buffer(text, file_type) + local buffer = vim.api.nvim_create_buf(false, false) + vim.api.nvim_set_current_buf(buffer) + + if file_type then + vim.api.nvim_set_option_value("filetype", file_type, {}) + end + + vim.api.nvim_buf_set_lines(buffer, 0, -1, false, vim.fn.split(text, "\n")) + + return buffer, vim.api.nvim_get_current_win() +end + +--- Make sure `input` becomes `expected` when `keys` are called. +--- +---@param cursor {[1]: number, [2]: number} The row & column position. (row=1-or-more, column=0-or-more). +---@param keys string Some command to run. e.g. `d]ap`. +---@param input string The buffer's original text. +---@param expected string The text that we expect to get after calling `keys`. +--- +local function run_simple_test(cursor, keys, input, expected) + local buffer, window = make_buffer(input) + vim.api.nvim_win_set_cursor(window, cursor) + + call_command(keys) + + assert.same(expected, get_lines(buffer)) +end + +--- Initialize 'commentstring' so `:help gc` related tests work as expected. +--- +---@param text string The template for creating comments. e.g. `"# %s"`. +---@param buffer number A 0-or-more Vim buffer ID. +--- +local function set_commentstring(text, buffer) + vim.api.nvim_set_option_value("commentstring", text, { buf = buffer }) +end + +describe("basic", function() + it("works with inner count", function() + run_simple_test( + { 2, 0 }, + "d]2ap", + [[ + some text + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + another paragraph + with text in it + + last paragraph + ]], + [[ + some text + last paragraph + ]] + ) + end) + + it("works with outer count", function() + run_simple_test( + { 2, 0 }, + "2d]ap", + [[ + some text + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + another paragraph + with text in it + + last paragraph + ]], + [[ + some text + last paragraph + ]] + ) + end) +end) + +describe(":help c", function() + describe("down", function() + it("works cap", function() + run_simple_test( + { 2, 0 }, + "c]ap", + [[ + some text + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + another paragraph + with text in it + ]], + [[ + some text + + another paragraph + with text in it + ]] + ) + end) + + it("works ca}", function() + run_simple_test( + { 3, 0 }, + "c]a}", + [[ + { + some text + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + more lines + } + + { + another paragraph + with text in it + } + ]], + [[ + { + some text + + + { + another paragraph + with text in it + } + ]] + ) + end) + end) + + describe("up", function() + it("works cap", function() + run_simple_test( + { 7, 0 }, + "c[ap", + [[ + some text + more text + even more lines! + still part of the paragraph + + first line, second paragraph + another paragraph <-- NOTE: The cursor will be set here + with text in it + ]], + [[ + some text + more text + even more lines! + still part of the paragraph + + + with text in it + ]] + ) + end) + + it("works ca}", function() + run_simple_test( + { 3, 0 }, + "c[a}", + [[ + { + some text + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + more lines + } + + { + another paragraph + with text in it + } + ]], + [[ + + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + more lines + } + + { + another paragraph + with text in it + } + ]] + ) + end) + end) +end) + +describe(":help d", function() + describe("down", function() + it("works with da)", function() + run_simple_test( + { 3, 0 }, + "d]a)", + [[ + ( + some text + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + more lines + ) + + ( + another paragraph + with text in it + ) + ]], + [[ + ( + some text + + + ( + another paragraph + with text in it + ) + ]] + ) + end) + + it("works with da]", function() + run_simple_test( + { 3, 0 }, + "d]a]", + [[ + [ + some text + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + more lines + ] + + [ + another paragraph + with text in it + ] + ]], + [[ + [ + some text + + + [ + another paragraph + with text in it + ] + ]] + ) + end) + + it("works with da}", function() + run_simple_test( + { 3, 0 }, + "d]a}", + [[ + { + some text + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + more lines + } + + { + another paragraph + with text in it + } + ]], + [[ + { + some text + + + { + another paragraph + with text in it + } + ]] + ) + end) + + it("works with dap - 3 paragraphs", function() + run_simple_test( + { 4, 0 }, + "d]ap", + [[ + first first + + second 1 + second 2 <-- NOTE: The cursor will be set here + second 3 + + third paragraph + ]], + [[ + first first + + second 1 + third paragraph + ]] + ) + end) + + it("works with dap", function() + run_simple_test( + { 2, 0 }, + "d]ap", + [[ + some text + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + another paragraph + with text in it + ]], + [[ + some text + another paragraph + with text in it + ]] + ) + end) + + it("works with das", function() + run_simple_test( + { 1, 28 }, + "d]as", + [[ + some sentences. With text and stuff + multiple lines. + other code + ]], + [[ + some sentences. Withother code + ]] + ) + end) + + it("works with dat", function() + run_simple_test( + { 3, 0 }, + "d]at", + [[ + + some text + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + ]], + [[ + + some text + + ]] + ) + end) + + it("works with di)", function() + run_simple_test( + { 3, 0 }, + "d]i)", + [[ + ( + some lines + and more things <-- NOTE: The cursor will be set here + last in the paragraph + + last bits + ) + + ( + another one + ) + ]], + [[ + ( + some lines + ) + + ( + another one + ) + ]] + ) + end) + + it("works with di]", function() + run_simple_test( + { 3, 0 }, + "d]i]", + [[ + [ + some lines + and more things <-- NOTE: The cursor will be set here + last in the paragraph + + last bits + ] + + [ + another one + ] + ]], + [[ + [ + some lines + ] + + [ + another one + ] + ]] + ) + end) + + it("works with di}", function() + run_simple_test( + { 3, 0 }, + "d]i}", + [[ + { + some lines + and more things <-- NOTE: The cursor will be set here + last in the paragraph + + last bits + } + + { + another one + } + ]], + [[ + { + some lines + } + + { + another one + } + ]] + ) + end) + + it("works with dip", function() + run_simple_test( + { 2, 0 }, + "d]ip", + [[ + some text + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + another paragraph + with text in it + ]], + [[ + some text + + another paragraph + with text in it + ]] + ) + end) + + it("works with dis", function() + run_simple_test( + { 1, 22 }, + "d]is", + "some sentences. With text and stuff\nmultiple lines\nother code", + "some sentences. With t" + ) + end) + + it("works with dit", function() + run_simple_test( + { 3, 0 }, + "d]it", + [[ + + some text + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + ]], + [[ + + some text + + ]] + ) + end) + end) + + describe("single-line", function() + describe("left", function() + it("works with daW", function() + run_simple_test({ 1, 11 }, "d[aW", "sometext.morethings", "rethings") + end) + + it("works with das", function() + run_simple_test( + { 1, 19 }, + "d[as", + "some sentences. With text and stuff. other code", + "some sentences. h text and stuff. other code" + ) + end) + + it("works with daw", function() + run_simple_test({ 1, 2 }, "d[aw", "sometext.morethings", "metext.morethings") + end) + + it("works with da[", function() + run_simple_test({ 1, 15 }, "d[a[", "some text [ inner text ] t", "some text er text ] t") + end) + + it("works with da{", function() + run_simple_test({ 1, 15 }, "d[a{", "some text { inner text } ", "some text er text } ") + end) + + it("works with dis", function() + run_simple_test( + { 1, 23 }, + "d[is", + "some sentences. With text and stuff. other code", + "some sentences. xt and stuff. other code" + ) + end) + + it("works with di[", function() + run_simple_test({ 1, 15 }, "d[i[", "some text [ inner text ] ", "some text [er text ] ") + end) + + it("works with di{", function() + run_simple_test({ 1, 15 }, "d[i{", "some text { inner text } ", "some text {er text } ") + end) + end) + + describe("right", function() + it("works with daW", function() + run_simple_test({ 1, 2 }, "d]aW", "sometext.morethings", "so") + end) + + it("works with daw", function() + run_simple_test({ 1, 2 }, "d]aw", "sometext.morethings", "so.morethings") + end) + + it("works with da]", function() + run_simple_test({ 1, 15 }, "d]a]", "some text [ inner text ] ", "some text [ inn ") + end) + + it("works with da}", function() + run_simple_test({ 1, 15 }, "d]a}", "some text { inner text } ", "some text { inn ") + end) + + it("works with di]", function() + run_simple_test({ 1, 15 }, "d]i]", "some text [ inner text ] ", "some text [ inn] ") + end) + + it("works with di}", function() + run_simple_test({ 1, 15 }, "d[i}", "some text { inner text } ", "some text {er text } ") + end) + end) + end) + + describe("up", function() + it("works with da)", function() + run_simple_test( + { 7, 0 }, + "d[a)", + [[ + ( + some text + more text + even more lines! + still part of the paragraph + + more lines <-- NOTE: The cursor will be set here + last line + ) + + ( + another paragraph + with text in it + ) + ]], + [[ + more lines <-- NOTE: The cursor will be set here + last line + ) + + ( + another paragraph + with text in it + ) + ]] + ) + end) + + it("works with da]", function() + run_simple_test( + { 3, 22 }, + "d[a]", + [[ + [ + some text + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + more lines + ] + + [ + another paragraph + with text in it + ] + ]], + [[ + ext <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + more lines + ] + + [ + another paragraph + with text in it + ] + ]] + ) + end) + + it("works with da}", function() + run_simple_test( + { 3, 24 }, + "d[a}", + [[ + { + some text + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + more lines + } + + { + another paragraph + with text in it + } + ]], + [[ + t <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + more lines + } + + { + another paragraph + with text in it + } + ]] + ) + end) + + it("works with dat", function() + run_simple_test( + { 3, 0 }, + "d[at", + [[ + + some text + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + ]], + [[ + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + ]] + ) + end) + + it("works with dap", function() + run_simple_test( + { 7, 0 }, + "d[ap", + [[ + some text + more text + even more lines! + still part of the paragraph + + first line, second paragraph + another paragraph <-- NOTE: The cursor will be set here + with text in it + ]], + [[ + some text + more text + even more lines! + still part of the paragraph + + with text in it + ]] + ) + end) + + it("works with di)", function() + run_simple_test( + { 3, 0 }, + "d[i)", + [[ + ( + some text + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + more lines + ) + + ( + another paragraph + with text in it + ) + ]], + [[ + ( + even more lines! + still part of the paragraph + + more lines + ) + + ( + another paragraph + with text in it + ) + ]] + ) + end) + + it("works with di]", function() + run_simple_test( + { 3, 0 }, + "d[i]", + [[ + [ + some text + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + more lines + ] + + [ + another paragraph + with text in it + ] + ]], + [[ + [ + even more lines! + still part of the paragraph + + more lines + ] + + [ + another paragraph + with text in it + ] + ]] + ) + end) + + it("works with di}", function() + run_simple_test( + { 3, 0 }, + "d[i}", + [[ + { + some text + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + more lines + } + + { + another paragraph + with text in it + } + ]], + [[ + { + even more lines! + still part of the paragraph + + more lines + } + + { + another paragraph + with text in it + } + ]] + ) + end) + + it("works with dip", function() + run_simple_test( + { 2, 23 }, + "d[ip", + [[ + some text + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + another paragraph + with text in it + ]], + [[ + even more lines! + still part of the paragraph + + another paragraph + with text in it + ]] + ) + end) + + it("works with dis", function() + run_simple_test( + { 1, 19 }, + "d[is", + "some sentences. With text and stuff. other code", + "some sentences. h text and stuff. other code" + ) + end) + + it("works with dit - 001", function() + run_simple_test( + { 3, 0 }, + "d[it", + [[ + + some text + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + ]], + [[ + + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + ]] + ) + end) + + it("works with dit - 002 - Include characters", function() + run_simple_test( + { 3, 23 }, + "d[it", + [[ + some text + some text + more text <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + ]], + [[ + xt <-- NOTE: The cursor will be set here + even more lines! + still part of the paragraph + + ]] + ) + end) + end) +end) + +describe(":help gU", function() + describe("down", function() + it("works with gUip", function() + run_simple_test( + { 5, 0 }, + "gU]ip", + [[ + aaaa + bbbbb + + ccccc + ddddddddddd <-- NOTE: The cursor will be set here + eeeeeeeee + + fffff + ]], + [[ + aaaa + bbbbb + + ccccc + DDDDDDDDDDD <-- NOTE: THE CURSOR WILL BE SET HERE + EEEEEEEEE + + fffff + ]] + ) + end) + end) + + describe("up", function() + it("works with gUip", function() + run_simple_test( + { 5, 0 }, + "gU[ip", + [[ + aaaa + bbbbb + + ccccc + ddddddddddd <-- NOTE: The cursor will be set here + eeeeeeeee + + fffff + ]], + [[ + aaaa + bbbbb + + CCCCC + DDDDDDDDDDD <-- NOTE: THE CURSOR WILL BE SET HERE + eeeeeeeee + + fffff + ]] + ) + end) + end) +end) + +describe(":help gc", function() + describe("down", function() + it("works with gcip", function() + local buffer, window = make_buffer( + [[ + def foo() -> None: + """Some function.""" <-- NOTE: The cursor will be set here + print("do stuff") + + for _ in range(10): + print("stuff") + ]], + "python" + ) + vim.api.nvim_win_set_cursor(window, { 2, 0 }) + set_commentstring("# %s", buffer) + + call_command("gc]ip") + + assert.same( + [[ + def foo() -> None: + # """Some function.""" <-- NOTE: The cursor will be set here + # print("do stuff") + + for _ in range(10): + print("stuff") + ]], + get_lines(buffer) + ) + end) + end) + + describe("up", function() + it("works with gcip", function() + local buffer, window = make_buffer( + [[ + def foo() -> None: + """Some function.""" <-- NOTE: The cursor will be set here + print("do stuff") + + for _ in range(10): + print("stuff") + ]], + "python" + ) + vim.api.nvim_win_set_cursor(window, { 2, 0 }) + set_commentstring("# %s", buffer) + + call_command("gc[ip") + + assert.same( + [[ + # def foo() -> None: + # """Some function.""" <-- NOTE: The cursor will be set here + print("do stuff") + + for _ in range(10): + print("stuff") + ]], + get_lines(buffer) + ) + end) + end) +end) + +describe(":help gu", function() + describe("down", function() + it("works with guip", function() + run_simple_test( + { 5, 0 }, + "gu]ip", + [[ + aaaa + bbbbb + + ccccc + ddDddDddDdd <-- NOTE: The cursor will be set here + eeeEeEEee + + fffff + ]], + [[ + aaaa + bbbbb + + ccccc + ddddddddddd <-- note: the cursor will be set here + eeeeeeeee + + fffff + ]] + ) + end) + end) + + describe("up", function() + it("works with guip", function() + run_simple_test( + { 5, 0 }, + "gu[ip", + [[ + aaaa + bbbbb + + cCCcc + ddDddDddDdd <-- NOTE: The cursor will be set here + eeeEeEEee + + fffff + ]], + [[ + aaaa + bbbbb + + ccccc + ddddddddddd <-- note: the cursor will be set here + eeeEeEEee + + fffff + ]] + ) + end) + end) +end) + +describe(":help g~", function() + describe("down", function() + it("works with g~ip", function() + run_simple_test( + { 5, 0 }, + "g~]ip", + [[ + aaaa + bbbbb + + cCCcc + ddDddDddDdd <-- NOTE: The cursor will be set here + eeeEeEEee + + fffff + ]], + [[ + aaaa + bbbbb + + cCCcc + DDdDDdDDdDD <-- note: tHE CURSOR WILL BE SET HERE + EEEeEeeEE + + fffff + ]] + ) + end) + end) + + describe("up", function() + it("works with g~ip", function() + run_simple_test( + { 5, 0 }, + "g~[ip", + [[ + aaaa + bbbbb + + cCCcc + ddDddDddDdd <-- NOTE: The cursor will be set here + eeeEeEEee + + fffff + ]], + [[ + aaaa + bbbbb + + CccCC + DDdDDdDDdDD <-- note: tHE CURSOR WILL BE SET HERE + eeeEeEEee + + fffff + ]] + ) + end) + end) +end) + +describe(":help y", function() + describe("down", function() + it("works with yap", function() + local _, window = make_buffer([[ + aaaa + bbbb <-- NOTE: The cursor will be set here + cccc + + next + lines + blah + + ]]) + vim.api.nvim_win_set_cursor(window, { 2, 0 }) + + call_command("y]ap") + + assert.same( + [[ + bbbb <-- NOTE: The cursor will be set here + cccc + +]], + vim.fn.getreg("") + ) + end) + end) + + describe("up", function() + it("works with yap", function() + local _, window = make_buffer([[ + aaaa + bbbb <-- NOTE: The cursor will be set here + cccc + + next + lines + blah + + ]]) + vim.api.nvim_win_set_cursor(window, { 2, 0 }) + + call_command("y[ap") + + assert.same( + " aaaa\n bbbb <-- NOTE: The cursor will be set here\n", + vim.fn.getreg("") + ) + end) + end) +end) + +describe("marks", function() + describe("marks - '", function() + describe("down", function() + it("works with yank", function() + local buffer, window = make_buffer([[ + aaaa + bbbb <-- NOTE: The cursor will be set here + cccc + + next + lines + blah + + ]]) + vim.api.nvim_win_set_cursor(window, { 2, 0 }) + + vim.api.nvim_buf_set_mark(buffer, "b", 6, 18, {}) + + call_command("y]'b") + + assert.same( + [[ + bbbb <-- NOTE: The cursor will be set here + cccc + + next + lines +]], + vim.fn.getreg("") + ) + end) + end) + + describe("up", function() + it("works with yank", function() + local buffer, window = make_buffer([[ + aaaa + bbbb + cccc + + next + lines <-- NOTE: The cursor will be set here + blah + + ]]) + vim.api.nvim_win_set_cursor(window, { 6, 19 }) + + vim.api.nvim_buf_set_mark(buffer, "b", 2, 19, {}) + + call_command("y['b") + + assert.same( + [[ + bbbb + cccc + + next + lines <-- NOTE: The cursor will be set here +]], + vim.fn.getreg("") + ) + end) + end) + end) + + describe("marks - `", function() + describe("down", function() + it("works with yank", function() + local buffer, window = make_buffer([[ + aaaa + bbbb <-- NOTE: The cursor will be set here + cccc + + next + lines + blah + + ]]) + vim.api.nvim_win_set_cursor(window, { 2, 22 }) + + vim.api.nvim_buf_set_mark(buffer, "b", 6, 22, {}) + + call_command("y]`b") + + assert.same( + [[bb <-- NOTE: The cursor will be set here + cccc + + next + li]], + vim.fn.getreg("") + ) + end) + end) + + describe("up", function() + it("works with yank", function() + local buffer, window = make_buffer([[ + aaaa + bbbb + cccc + + next + lines <-- NOTE: The cursor will be set here + blah + + ]]) + vim.api.nvim_win_set_cursor(window, { 6, 23 }) + + vim.api.nvim_buf_set_mark(buffer, "b", 2, 23, {}) + + call_command("y[`b") + + assert.same( + [[b + cccc + + next + lin]], + vim.fn.getreg("") + ) + end) + end) + end) +end) + +describe("scenario", function() + it("works with a curly function - da{", function() + run_simple_test( + { 3, 0 }, + "d[a{", + vim.fn.join({ + "void main() {", + " something", + " ttttt <-- NOTE: The cursor will be set here", + " fffff", + "}", + }, "\n"), + vim.fn.join({ "void main() ", " ttttt <-- NOTE: The cursor will be set here", " fffff", "}" }, "\n") + ) + end) + + it("works with a curly function - di{", function() + run_simple_test( + { 3, 0 }, + "d[i{", + [[ + void main() { + something + ttttt <-- NOTE: The cursor will be set here + fffff + } + ]], + [[ + void main() { + fffff + } + ]] + ) + end) +end) diff --git a/spec/minimal_init.lua b/spec/minimal_init.lua index 01512cb..b49efed 100644 --- a/spec/minimal_init.lua +++ b/spec/minimal_init.lua @@ -1,35 +1,5 @@ ---- Run the is file before you run unittests to download any extra dependencies. - -local _PLUGINS = { - ["https://github.com/nvim-lualine/lualine.nvim"] = os.getenv("LUALINE_DIR") or "/tmp/lualine.nvim", - ["https://github.com/nvim-telescope/telescope.nvim"] = os.getenv("TELESCOPE_DIR") or "/tmp/telescope.nvim", - ["https://github.com/nvim-lua/plenary.nvim"] = os.getenv("PLENARY_DIR") or "/tmp/plenary.nvim", -} - -local cloned = false - -for url, directory in pairs(_PLUGINS) do - if vim.fn.isdirectory(directory) ~= 1 then - print(string.format('Cloning "%s" plug-in to "%s" path.', url, directory)) - - vim.fn.system({ "git", "clone", url, directory }) - - cloned = true - end - - vim.opt.rtp:append(directory) -end - -if cloned then - print("Finished cloning.") -end +--- Run this file before you run unittests to download any extra dependencies. vim.opt.rtp:append(".") -vim.cmd("runtime plugin/plugin_template.lua") - -vim.cmd("runtime plugin/plenary.vim") - -require("lualine").setup() - -require("plugin_template._core.configuration").initialize_data_if_needed() +vim.cmd("runtime plugin/cursor_text_objects.lua") diff --git a/spec/plugin_template/argparse_spec.lua b/spec/plugin_template/argparse_spec.lua deleted file mode 100644 index 20186cf..0000000 --- a/spec/plugin_template/argparse_spec.lua +++ /dev/null @@ -1,1128 +0,0 @@ ---- Make sure the argument parser works as expected. ---- ----@module 'plugin_template.argparse_spec' ---- - -local argparse = require("plugin_template._cli.argparse") - -describe("default", function() - it("works even if #empty #simple", function() - assert.same({ arguments = {}, text = "", remainder = { value = "" } }, argparse.parse_arguments("")) - end) -end) - -describe("positional arguments", function() - it("#simple #single argument", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 1, end_column = 3 }, - value = "foo", - }, - }, - text = "foo", - remainder = { value = "" }, - }, argparse.parse_arguments("foo")) - end) - - it("#simple #multiple arguments", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 1, end_column = 3 }, - value = "foo", - }, - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 5, end_column = 7 }, - value = "bar", - }, - }, - text = "foo bar", - remainder = { value = "" }, - }, argparse.parse_arguments("foo bar")) - end) - - it("#escaped #positional arguments 001", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 1, end_column = 4 }, - value = "foo ", - }, - }, - text = "foo\\ ", - remainder = { value = "" }, - }, argparse.parse_arguments("foo\\ ")) - end) - - it("#escaped #positional arguments 002", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 1, end_column = 7 }, - value = "foo bar", - }, - }, - text = "foo\\ bar", - remainder = { value = "" }, - }, argparse.parse_arguments("foo\\ bar")) - end) -end) - -describe("quotes", function() - it("#quoted #position arguments", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 1, end_column = 3 }, - value = "foo", - }, - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 5, end_column = 19 }, - value = "bar fizz buzz", - }, - }, - text = 'foo "bar fizz buzz"', - remainder = { value = "" }, - }, argparse.parse_arguments('foo "bar fizz buzz"')) - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 1, end_column = 15 }, - value = "bar fizz buzz", - }, - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 17, end_column = 19 }, - value = "foo", - }, - }, - text = '"bar fizz buzz" foo', - remainder = { value = "" }, - }, argparse.parse_arguments('"bar fizz buzz" foo')) - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 1, end_column = 3 }, - value = "foo", - }, - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 5, end_column = 14 }, - value = "bar fizz", - }, - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 16, end_column = 19 }, - value = "buzz", - }, - }, - text = 'foo "bar fizz" buzz', - remainder = { value = "" }, - }, argparse.parse_arguments('foo "bar fizz" buzz')) - end) - - it("#quoted flag is treated as a #position argument", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 1, end_column = 4 }, - value = "-1", - }, - }, - text = '"-1"', - remainder = { value = "" }, - }, argparse.parse_arguments('"-1"')) - end) - - it("has #flag within the quotes", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 1, end_column = 3 }, - value = "foo", - }, - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 5, end_column = 19 }, - value = "bar -f --fizz", - }, - }, - text = "foo 'bar -f --fizz'", - remainder = { value = "" }, - }, argparse.parse_arguments("foo 'bar -f --fizz'")) - end) - - it("#multiple arguments", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 1, end_column = 3 }, - value = "foo", - }, - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 5, end_column = 7 }, - value = "bar", - }, - }, - text = "foo bar", - remainder = { value = "" }, - }, argparse.parse_arguments("foo bar")) - end) - - it("#escaped spaces 001", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - value = "foo ", - range = { start_column = 1, end_column = 4 }, - }, - }, - text = "foo\\ ", - remainder = { value = "" }, - }, argparse.parse_arguments("foo\\ ")) - end) - - it("#escaped spaces 002", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - value = "foo bar", - range = { start_column = 1, end_column = 7 }, - }, - }, - text = "foo\\ bar", - remainder = { value = "" }, - }, argparse.parse_arguments("foo\\ bar")) - end) - - it("#escaped #multiple backslashes - 001", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - value = "foo\\", - range = { start_column = 1, end_column = 4 }, - }, - { - argument_type = argparse.ArgumentType.position, - value = "bar", - range = { start_column = 6, end_column = 8 }, - }, - }, - text = "foo\\\\ bar", - remainder = { value = "" }, - }, argparse.parse_arguments("foo\\\\ bar")) - end) - - it("#escaped #multiple backslashes - 002", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - value = "foo\\", - range = { start_column = 1, end_column = 4 }, - }, - { - argument_type = argparse.ArgumentType.position, - value = "b\\ar", - range = { start_column = 6, end_column = 9 }, - }, - }, - text = "foo\\\\ b\\\\ar", - remainder = { value = "" }, - }, argparse.parse_arguments("foo\\\\ b\\\\ar")) - end) - - it("#escaped #multiple backslashes - 003", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - value = "foo\\ bar", - range = { start_column = 1, end_column = 8 }, - }, - }, - text = "foo\\\\\\ bar", - remainder = { value = "" }, - }, argparse.parse_arguments("foo\\\\\\ bar")) - end) - - it("#escaped #multiple backslashes - 004", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - value = "foo\\", - range = { start_column = 1, end_column = 4 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "-z", - range = { start_column = 6, end_column = 9 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "-z", - range = { start_column = 7, end_column = 9 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "-z", - range = { start_column = 8, end_column = 9 }, - }, - }, - text = "foo\\\\ -zzz", - remainder = { value = "" }, - }, argparse.parse_arguments("foo\\\\ -zzz")) - end) -end) - -describe("double-dash flags", function() - it("mixed #flag", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.flag, - name = "--foo-bar", - range = { start_column = 1, end_column = 9 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "--fizz", - range = { start_column = 11, end_column = 16 }, - }, - }, - text = "--foo-bar --fizz", - remainder = { value = "" }, - }, argparse.parse_arguments("--foo-bar --fizz")) - end) - - it("#single #flag", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.flag, - name = "--foo-bar", - range = { start_column = 1, end_column = 9 }, - }, - }, - text = "--foo-bar", - remainder = { value = "" }, - }, argparse.parse_arguments("--foo-bar")) - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.flag, - name = "--foo", - range = { start_column = 1, end_column = 5 }, - }, - }, - text = "--foo", - remainder = { value = "" }, - }, argparse.parse_arguments("--foo")) - end) - - it("multiple #flag", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.flag, - name = "--foo", - range = { start_column = 1, end_column = 5 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "--bar", - range = { start_column = 7, end_column = 11 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "--fizz", - range = { start_column = 13, end_column = 18 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "--buzz", - range = { start_column = 20, end_column = 25 }, - }, - }, - remainder = { value = "" }, - text = "--foo --bar --fizz --buzz", - }, argparse.parse_arguments("--foo --bar --fizz --buzz")) - end) - - it("partial #named-argument - single", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.named, - name = "--foo-bar", - range = { start_column = 1, end_column = 10 }, - value = false, - }, - }, - text = "--foo-bar=", - remainder = { value = "" }, - }, argparse.parse_arguments("--foo-bar=")) - end) - - it("full #named-argument - multiple", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 1, end_column = 11 }, - value = "hello-world", - }, - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 13, end_column = 15 }, - value = "say", - }, - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 17, end_column = 20 }, - value = "word", - }, - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 22, end_column = 25 }, - value = "Hi", - }, - { - argument_type = argparse.ArgumentType.named, - name = "--repeat", - range = { start_column = 27, end_column = 36 }, - value = "2", - }, - { - argument_type = argparse.ArgumentType.named, - name = "--style", - range = { start_column = 38, end_column = 54 }, - value = "uppercase", - }, - }, - remainder = { value = "" }, - text = 'hello-world say word "Hi" --repeat=2 --style=uppercase', - }, argparse.parse_arguments('hello-world say word "Hi" --repeat=2 --style=uppercase')) - end) - it("partial --flag= - multiple", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.named, - name = "--foo-bar", - range = { start_column = 1, end_column = 10 }, - value = false, - }, - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 12, end_column = 15 }, - value = "blah", - }, - { - argument_type = argparse.ArgumentType.named, - name = "--fizz-buzz", - range = { start_column = 17, end_column = 28 }, - value = false, - }, - { - argument_type = argparse.ArgumentType.named, - name = "--one-more", - range = { start_column = 30, end_column = 40 }, - value = false, - }, - }, - remainder = { value = "" }, - text = "--foo-bar= blah --fizz-buzz= --one-more=", - }, argparse.parse_arguments("--foo-bar= blah --fizz-buzz= --one-more=")) - end) -end) - -describe("double-dash equal-flags", function() - it("mixed #flag", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.flag, - name = "--foo-bar", - range = { start_column = 1, end_column = 9 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "--fizz", - range = { start_column = 11, end_column = 16 }, - }, - }, - remainder = { value = "" }, - text = "--foo-bar --fizz", - }, argparse.parse_arguments("--foo-bar --fizz")) - end) - - it("#single #flag", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.flag, - name = "--foo-bar", - range = { start_column = 1, end_column = 9 }, - }, - }, - remainder = { value = "" }, - text = "--foo-bar", - }, argparse.parse_arguments("--foo-bar")) - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.flag, - name = "--foo", - range = { start_column = 1, end_column = 5 }, - }, - }, - remainder = { value = "" }, - text = "--foo", - }, argparse.parse_arguments("--foo")) - end) - - it("multiple", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.named, - name = "--foo", - range = { start_column = 1, end_column = 12 }, - value = "text", - }, - { - argument_type = argparse.ArgumentType.named, - name = "--bar", - range = { start_column = 14, end_column = 31 }, - value = "some thing", - }, - { - argument_type = argparse.ArgumentType.flag, - name = "--fizz", - range = { start_column = 33, end_column = 38 }, - }, - { - argument_type = argparse.ArgumentType.named, - name = "--buzz", - range = { start_column = 40, end_column = 52 }, - value = "blah", - }, - }, - remainder = { value = "" }, - text = "--foo='text' --bar=\"some thing\" --fizz --buzz='blah'", - }, argparse.parse_arguments("--foo='text' --bar=\"some thing\" --fizz --buzz='blah'")) - end) -end) - -describe("single-dash flags", function() - it("#single", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.flag, - name = "-f", - range = { start_column = 1, end_column = 2 }, - }, - }, - remainder = { value = "" }, - text = "-f", - }, argparse.parse_arguments("-f")) - end) - - it("#multiple, combined", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.flag, - name = "-f", - range = { start_column = 1, end_column = 4 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "-b", - range = { start_column = 2, end_column = 4 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "-z", - range = { start_column = 3, end_column = 4 }, - }, - }, - text = "-fbz", - remainder = { value = "" }, - }, argparse.parse_arguments("-fbz")) - end) - - it("#multiple, separate", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.flag, - name = "-f", - range = { start_column = 1, end_column = 2 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "-b", - range = { start_column = 4, end_column = 5 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "-z", - range = { start_column = 7, end_column = 8 }, - }, - }, - text = "-f -b -z", - remainder = { value = "" }, - }, argparse.parse_arguments("-f -b -z")) - end) -end) - -describe("remainder - positions", function() - it("keeps track of single position text", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 1, end_column = 3 }, - value = "foo", - }, - }, - text = "foo ", - remainder = { value = " " }, - }, argparse.parse_arguments("foo ")) - end) - - it("keeps track of multiple position text", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 1, end_column = 3 }, - value = "foo", - }, - { - argument_type = argparse.ArgumentType.position, - range = { start_column = 5, end_column = 7 }, - value = "bar", - }, - }, - text = "foo bar ", - remainder = { value = " " }, - }, argparse.parse_arguments("foo bar ")) - end) -end) - -describe("remainder - flags", function() - it("keeps track of flag text - 001", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.flag, - name = "-f", - range = { start_column = 1, end_column = 2 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "-b", - range = { start_column = 4, end_column = 5 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "-z", - range = { start_column = 7, end_column = 8 }, - }, - }, - text = "-f -b -z -", - remainder = { value = " -" }, - }, argparse.parse_arguments("-f -b -z -")) - end) - - it("keeps track of flag text - 002", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.flag, - name = "-f", - range = { start_column = 1, end_column = 2 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "-b", - range = { start_column = 4, end_column = 5 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "-z", - range = { start_column = 7, end_column = 8 }, - }, - }, - text = "-f -b -z --", - remainder = { value = " --" }, - }, argparse.parse_arguments("-f -b -z --")) - end) - - it("keeps track of flag text - 003", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.flag, - name = "-f", - range = { start_column = 1, end_column = 2 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "-b", - range = { start_column = 4, end_column = 5 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "-z", - range = { start_column = 7, end_column = 8 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "--r", - range = { start_column = 10, end_column = 12 }, - }, - }, - text = "-f -b -z --r", - remainder = { value = "" }, - }, argparse.parse_arguments("-f -b -z --r")) - end) - - it("sees spaces when no arguments are given", function() - assert.same({ arguments = {}, text = " ", remainder = { value = " " } }, argparse.parse_arguments(" ")) - end) - - it("stores the last space(s) - #multiple", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.flag, - name = "-f", - range = { start_column = 1, end_column = 2 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "-b", - range = { start_column = 4, end_column = 5 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "-z", - range = { start_column = 7, end_column = 8 }, - }, - }, - text = "-f -b -z ", - remainder = { value = " " }, - }, argparse.parse_arguments("-f -b -z ")) - end) - - it("stores the last space(s) - #single", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.flag, - name = "-f", - range = { start_column = 1, end_column = 2 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "-b", - range = { start_column = 4, end_column = 5 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "-z", - range = { start_column = 7, end_column = 8 }, - }, - }, - text = "-f -b -z ", - remainder = { value = " " }, - }, argparse.parse_arguments("-f -b -z ")) - end) - - it("stores the last space(s) - combined", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.flag, - name = "-f", - range = { start_column = 1, end_column = 2 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "-b", - range = { start_column = 4, end_column = 5 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "-x", - range = { start_column = 7, end_column = 10 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "-y", - range = { start_column = 8, end_column = 10 }, - }, - { - argument_type = argparse.ArgumentType.flag, - name = "-z", - range = { start_column = 9, end_column = 10 }, - }, - }, - text = "-f -b -xyz ", - remainder = { value = " " }, - }, argparse.parse_arguments("-f -b -xyz ")) - end) -end) - -describe("+ flags", function() - it("works with ++double flags 001 - start", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.flag, - name = "++double", - range = { - end_column = 8, - start_column = 1, - }, - }, - { - argument_type = argparse.ArgumentType.position, - range = { - end_column = 13, - start_column = 10, - }, - value = "here", - }, - }, - remainder = { - value = "", - }, - text = "++double here", - }, argparse.parse_arguments("++double here")) - end) - - it("works with ++double flags 002 - middle", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - range = { - end_column = 5, - start_column = 1, - }, - value = "thing", - }, - { - argument_type = argparse.ArgumentType.flag, - name = "++double", - range = { - end_column = 14, - start_column = 7, - }, - }, - { - argument_type = argparse.ArgumentType.position, - range = { - end_column = 19, - start_column = 16, - }, - value = "here", - }, - }, - remainder = { - value = "", - }, - text = "thing ++double here", - }, argparse.parse_arguments("thing ++double here")) - end) - - it("works with +s (single) flags 003 - end", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - range = { - end_column = 5, - start_column = 1, - }, - value = "thing", - }, - { - argument_type = argparse.ArgumentType.flag, - name = "++double", - range = { - end_column = 14, - start_column = 7, - }, - }, - }, - remainder = { - value = "", - }, - text = "thing ++double", - }, argparse.parse_arguments("thing ++double")) - end) - - it("works with +s (single) flags 004 - only", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.flag, - name = "+s", - range = { - end_column = 2, - start_column = 1, - }, - }, - }, - remainder = { - value = "", - }, - text = "+s", - }, argparse.parse_arguments("+s")) - end) - - it("works with ++named=foo arguments 001 - start", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.named, - name = "++double", - range = { - end_column = 12, - start_column = 1, - }, - value = "foo", - }, - { - argument_type = argparse.ArgumentType.position, - range = { - end_column = 17, - start_column = 14, - }, - value = "here", - }, - }, - remainder = { - value = "", - }, - text = "++double=foo here", - }, argparse.parse_arguments("++double=foo here")) - end) - - it("works with ++named=foo arguments 002 - middle", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - range = { - end_column = 5, - start_column = 1, - }, - value = "thing", - }, - { - argument_type = argparse.ArgumentType.named, - name = "++double", - range = { - end_column = 18, - start_column = 7, - }, - value = "bar", - }, - { - argument_type = argparse.ArgumentType.position, - range = { - end_column = 23, - start_column = 20, - }, - value = "here", - }, - }, - remainder = { - value = "", - }, - text = "thing ++double=bar here", - }, argparse.parse_arguments("thing ++double=bar here")) - end) - - it("works with ++named=foo arguments 003 - end", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - range = { - end_column = 5, - start_column = 1, - }, - value = "thing", - }, - { - argument_type = argparse.ArgumentType.named, - name = "++double", - range = { - end_column = 18, - start_column = 7, - }, - value = "foo", - }, - }, - remainder = { - value = "", - }, - text = "thing ++double=foo", - }, argparse.parse_arguments("thing ++double=foo")) - end) - - it("works with ++named=foo arguments 004 - only", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.named, - name = "++named", - range = { - end_column = 11, - start_column = 1, - }, - value = "foo", - }, - }, - remainder = { - value = "", - }, - text = "++named=foo", - }, argparse.parse_arguments("++named=foo")) - end) - - it("works with +s (single) flags 001 - start", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.flag, - name = "+s", - range = { - end_column = 2, - start_column = 1, - }, - }, - { - argument_type = argparse.ArgumentType.position, - range = { - end_column = 7, - start_column = 4, - }, - value = "here", - }, - }, - remainder = { - value = "", - }, - text = "+s here", - }, argparse.parse_arguments("+s here")) - end) - - it("works with +s (single) flags 002 - middle", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - range = { - end_column = 5, - start_column = 1, - }, - value = "thing", - }, - { - argument_type = argparse.ArgumentType.flag, - name = "+s", - range = { - end_column = 8, - start_column = 7, - }, - }, - { - argument_type = argparse.ArgumentType.position, - range = { - end_column = 13, - start_column = 10, - }, - value = "here", - }, - }, - remainder = { - value = "", - }, - text = "thing +s here", - }, argparse.parse_arguments("thing +s here")) - end) - - it("works with +s (single) flags 003 - end", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.position, - range = { - end_column = 5, - start_column = 1, - }, - value = "thing", - }, - { - argument_type = argparse.ArgumentType.flag, - name = "+s", - range = { - end_column = 8, - start_column = 7, - }, - }, - }, - remainder = { - value = "", - }, - text = "thing +s", - }, argparse.parse_arguments("thing +s")) - end) - - it("works with +s (single) flags 004 - only", function() - assert.same({ - arguments = { - { - argument_type = argparse.ArgumentType.flag, - name = "+s", - range = { - end_column = 2, - start_column = 1, - }, - }, - }, - remainder = { - value = "", - }, - text = "+s", - }, argparse.parse_arguments("+s")) - end) -end) diff --git a/spec/plugin_template/autocomplete_spec.lua b/spec/plugin_template/autocomplete_spec.lua deleted file mode 100644 index 71dfd12..0000000 --- a/spec/plugin_template/autocomplete_spec.lua +++ /dev/null @@ -1,812 +0,0 @@ ---- Make sure auto-complete works as expected. ---- ----@module 'plugin_template.autocomplete_spec' ---- - -local cmdparse = require("plugin_template._cli.cmdparse") - ----@return cmdparse.ParameterParser # Create a tree of commands for unittests. - ---- Add `--repeat=` to `parser`. ---- ----@param parser cmdparse.ParameterParser Some tree to add a new parameter. ----@param short string? The parameter name. e.g. `"-r"`. ----@param long string? The parameter name. e.g. `"--repeat"`. ---- -local function _add_repeat_parameter(parser, short, long) - local choices = function(data) - --- @cast data cmdparse.ChoiceData? - - local output = {} - - if not data or not data.current_value or data.current_value == "" then - for index = 1, 5 do - table.insert(output, tostring(index)) - end - - return output - end - - local value = tonumber(data.current_value) - - if not value then - return {} - end - - table.insert(output, tostring(value)) - - for index = 1, 4 do - table.insert(output, tostring(value + index)) - end - - return output - end - - short = short or "-r" - long = long or "--repeat" - - parser:add_parameter({ - names = { long, short }, - choices = choices, - help = "The number of times to display the message.", - }) -end - ---- Add `--style=` to `parser`. ---- ----@param parser cmdparse.ParameterParser Some tree to add a new parameter. ----@param short string? The parameter name. e.g. `"-s"`. ----@param long string? The parameter name. e.g. `"--style"`. ---- -local function _add_style_parameter(parser, short, long) - short = short or "-s" - long = long or "--style" - - parser:add_parameter({ - names = { long, short }, - choices = { "lowercase", "uppercase" }, - help = "The format of the message.", - }) -end - ---- Create multi-parameter for unittests. ---- ----@param pluses boolean? If ``true``, the created parameters will use + / ++. ----@return cmdparse.ParameterParser # Create a `say {phrase,word} [--repeat --style]`. ---- -local function _make_simple_parser(pluses) - local parser = cmdparse.ParameterParser.new({ name = "top_test", help = "Test." }) - local subparsers = parser:add_subparsers({ destination = "commands", help = "Test." }) - local say = subparsers:add_parser({ name = "say", help = "Print stuff to the terminal." }) - local say_subparsers = say:add_subparsers({ destination = "say_commands", help = "All commands that print." }) - local say_word = say_subparsers:add_parser({ name = "word", help = "Print a single word." }) - local say_phrase = say_subparsers:add_parser({ name = "phrase", help = "Print a whole sentence." }) - - local long_repeat - local short_repeat - local long_style - local short_style - - if pluses then - long_repeat = "++repeat" - short_repeat = "+r" - long_style = "++style" - short_style = "+s" - end - - _add_repeat_parameter(say_phrase, short_repeat, long_repeat) - _add_repeat_parameter(say_word, short_repeat, long_repeat) - _add_style_parameter(say_phrase, short_style, long_style) - _add_style_parameter(say_word, short_style, long_style) - - return parser -end - ---- Create a --style= parameter. ---- ----@param prefix string? The text used as a start for the parameter. e.g. `"--"`. ----@return cmdparse.ParameterParser # Create a tree of commands for unittests. ---- -local function _make_style_parser(prefix) - local parser = cmdparse.ParameterParser.new({ name = "test", help = "Test" }) - prefix = prefix or "--" - local choice = { "lowercase", "uppercase" } - - parser:add_parameter({ - name = prefix .. "style", - choices = choice, - destination = "style_flag", - help = "Define how to print to the terminal", - }) - parser:add_parameter({ - name = "style", - destination = "style_position", - help = "Define how to print to the terminal", - }) - - return parser -end - -describe("default", function() - it("works even if #simple", function() - local parser = _make_simple_parser() - - assert.same({ "say", "--help" }, parser:get_completion("")) - end) -end) - -describe("plugin", function() - it("works with a telescope-like plugin CLI", function() - ---@class TeleskopePluginData - --- Data that would come from other Lua plugins. - ---@field name string - --- The name of the parser to register. - ---@field help string - --- A description of what this plugin does. Keep it brief! < 80 characters. - ---@field add_parameters (fun(parser: cmdparse.ParameterParser): nil)? - --- The callback used to add extra - - ---@return TeleskopePluginData[] # All of the Teleskope-registered plugin. - local function _get_plugin_registry() - return { - { - name = "colorscheme", - help = "Preview / Select other colorschemes.", - add_parameters = function(parser) - parser:add_parameter({ "name", required = false }) - end, - }, - { - name = "jumplist", - help = "Jump up, jump up, and get down jump! Jump! Jump! Jump!", - add_parameters = function(parser) - local subparsers = parser:add_subparsers({ "jumplist_commands", help = "Test." }) - local cursor = subparsers:add_parser({ "cursor", help = "Jump to the last cursor." }) - cursor:set_execute(function() - return 8 - end) - local tab = subparsers:add_parser({ "tab", help = "Jump to the last tab." }) - tab:set_execute(function() - return 10 - end) - end, - }, - } - end - - local parser = cmdparse.ParameterParser.new({ name = "top_test", help = "Test." }) - local subparsers = parser:add_subparsers({ destination = "commands", help = "Test." }) - local teleskope = subparsers:add_parser({ name = "Teleskope", help = "Something." }) - local teleskope_subparsers = teleskope:add_subparsers({ "teleskope_commands", help = "Test." }) - - for _, data in ipairs(_get_plugin_registry()) do - local inner_parser = teleskope_subparsers:add_parser({ data.name, help = data.help }) - - if data.add_parameters then - data.add_parameters(inner_parser) - end - end - - assert.same({ name = "light" }, parser:parse_arguments("Teleskope colorscheme light")) - assert.same({}, parser:parse_arguments("Teleskope jumplist")) - local cursor_namespace = parser:parse_arguments("Teleskope jumplist cursor") - local tab_namespace = parser:parse_arguments("Teleskope jumplist tab") - assert.equal(8, cursor_namespace.execute()) - assert.equal(10, tab_namespace.execute()) - - assert.same({ "jumplist" }, parser:get_completion("Teleskope ju")) - assert.same({ "cursor", "tab", "--help" }, parser:get_completion("Teleskope jumplist ")) - end) -end) - -describe("simple", function() - it("works with multiple position arguments", function() - local parser = _make_simple_parser() - - assert.same({ "say" }, parser:get_completion("sa")) - assert.same({ "say" }, parser:get_completion("say")) - assert.same({ "phrase", "word", "--help" }, parser:get_completion("say ")) - assert.same({ "--repeat=", "--style=", "--help" }, parser:get_completion("say phrase ")) - end) - - it("works when two positions start with the same text", function() - local parser = cmdparse.ParameterParser.new({ name = "top_test", help = "Test." }) - local subparsers = parser:add_subparsers({ destination = "commands", help = "Test." }) - local bottle = subparsers:add_parser({ name = "bottle", help = "Something." }) - local bottle_subparsers = bottle:add_subparsers({ destination = "bottle", help = "Test." }) - local bottles = subparsers:add_parser({ name = "bottles", help = "Somethings." }) - bottles:add_parameter({ name = "bar", help = "Any text allowed here." }) - - bottle_subparsers:add_parser({ name = "foo", choices = { "foo" }, help = "Print stuff to the terminal." }) - - local bottlez = subparsers:add_parser({ name = "bottlez", destination = "weird_name", help = "Test." }) - local bottlez_subparsers = bottlez:add_subparsers({ destination = "bottlez", help = "Test." }) - bottlez_subparsers:add_parser({ name = "fizz", help = "Fizzy drink." }) - - assert.same({ "bottle", "bottles", "bottlez" }, parser:get_completion("bottle")) - assert.same({ "bottles" }, parser:get_completion("bottles")) - assert.same({ "bottlez" }, parser:get_completion("bottlez")) - assert.same({ "foo", "--help" }, parser:get_completion("bottle ")) - assert.same({ "--help" }, parser:get_completion("bottles ")) - assert.same({ "fizz", "--help" }, parser:get_completion("bottlez ")) - end) - - it("works when two positions start with the same text - 002", function() - local parser = cmdparse.ParameterParser.new({ name = "top_test", help = "Test." }) - local subparsers = parser:add_subparsers({ destination = "commands", help = "Test." }) - local bottle = subparsers:add_parser({ name = "bottle", help = "Something." }) - local bottle_subparsers = bottle:add_subparsers({ destination = "bottle", help = "Test." }) - local bottles = subparsers:add_parser({ name = "bottles", help = "Somethings." }) - bottles:add_parameter({ name = "bar", help = "Any text allowed here." }) - - bottle_subparsers:add_parser({ name = "foo", choices = { "foo" }, help = "Print stuff to the terminal." }) - - local bottlez = subparsers:add_parser({ name = "bottlez", destination = "weird_name", help = "Test." }) - local bottlez_subparsers = bottlez:add_subparsers({ destination = "bottlez", help = "Test." }) - bottlez_subparsers:add_parser({ name = "fizz", help = "Fizzy drink." }) - - parser:add_parameter({ name = "bottle", help = "Something." }) - - -- IMPORTANT: This is a rare case where a required parameter is in - -- `top_test` but a subparser has the same name. We prefer the current - -- parser in this case which means preferring the required parameter. - -- So instead of auto-completing like `"bottle"` is a partial name of - -- some subparsers, we treat it as a parameter. - -- - assert.same({}, parser:get_completion("bottle")) - end) - - it("works when two positions start with the same text - 003", function() - local parser = cmdparse.ParameterParser.new({ name = "top_test", help = "Test." }) - local subparsers = parser:add_subparsers({ destination = "commands", help = "Test." }) - local bottle = subparsers:add_parser({ name = "bottle", help = "Something." }) - local bottle_subparsers = bottle:add_subparsers({ destination = "bottle", help = "Test." }) - local bottles = subparsers:add_parser({ name = "bottles", help = "Somethings." }) - bottles:add_parameter({ name = "bar", help = "Any text allowed here." }) - - bottle_subparsers:add_parser({ name = "foo", choices = { "foo" }, help = "Print stuff to the terminal." }) - - local bottlez = subparsers:add_parser({ name = "bottlez", destination = "weird_name", help = "Test." }) - local bottlez_subparsers = bottlez:add_subparsers({ destination = "bottlez", help = "Test." }) - bottlez_subparsers:add_parser({ name = "fizz", help = "Fizzy drink." }) - - parser:add_parameter({ name = "bottle", choices = { "bots", "botz" }, help = "Something." }) - - -- IMPORTANT: This is a rare case where a required parameter is in - -- `top_test` but a subparser has the same name. We prefer the current - -- parser in this case which means preferring the required parameter. - -- So instead of auto-completing like `"bottle"` is a partial name of - -- some subparsers, we treat it as a parameter. - -- - assert.same({ "bots", "botz" }, parser:get_completion("bot")) - end) - - it("works with a basic multi-key example", function() - local parser = _make_simple_parser() - - assert.same({ - "--repeat=", - "--style=", - "--help", - }, parser:get_completion("say phrase ")) - end) - - it("works even if there is a named / position argument at the same time - 001", function() - local parser = _make_style_parser() - - assert.same({ "--style=", "--help" }, parser:get_completion("")) - assert.same({}, parser:get_completion("foo")) - end) - - it("works even if there is a named / position argument at the same time - 002", function() - local parser = _make_style_parser() - - assert.same({}, parser:get_completion("sty")) - assert.same({ "--style=" }, parser:get_completion("--sty")) - end) - - it("works even if there is a named / position argument at the same time - 003", function() - local parser = cmdparse.ParameterParser.new({ name = "test", help = "Test" }) - local choice = { "lowercase", "uppercase" } - parser:add_parameter({ - name = "--style", - choices = choice, - destination = "style_flag", - help = "Define how to print to the terminal", - }) - parser:add_parameter({ - name = "style", - choices = { "style" }, - destination = "style_position", - help = "Define how to print to the terminal", - }) - - assert.same({ "style" }, parser:get_completion("sty")) - assert.same({ "--style=" }, parser:get_completion("--sty")) - end) - - it("works with a basic multi-position example", function() - local parser = _make_simple_parser() - - -- NOTE: Simple examples - assert.same({ "say" }, parser:get_completion("sa")) - assert.same({ "say" }, parser:get_completion("say")) - assert.same({ "phrase", "word", "--help" }, parser:get_completion("say ")) - assert.same({ "phrase" }, parser:get_completion("say p")) - assert.same({ "phrase" }, parser:get_completion("say phrase")) - assert.same({ "--repeat=", "--style=", "--help" }, parser:get_completion("say phrase ")) - - -- NOTE: Beginning a --double-dash named argument, maybe (we don't know yet) - assert.same({ "--repeat=", "--style=", "--help" }, parser:get_completion("say phrase --")) - - -- NOTE: Completing the name to a --double-dash named argument - assert.same({ "--repeat=" }, parser:get_completion("say phrase --r")) - -- NOTE: Completing the =, so people know that this is requires an argument - assert.same({ "--repeat=" }, parser:get_completion("say phrase --repeat")) - -- NOTE: Completing the value of the named argument - assert.same({ - "--repeat=1", - "--repeat=2", - "--repeat=3", - "--repeat=4", - "--repeat=5", - }, parser:get_completion("say phrase --repeat=")) - assert.same({ - "--repeat=5", - "--repeat=6", - "--repeat=7", - "--repeat=8", - "--repeat=9", - }, parser:get_completion("say phrase --repeat=5")) - - assert.same({ "--style=", "--help" }, parser:get_completion("say phrase --repeat=5 ")) - - -- NOTE: Asking for repeat again will not show the value (because count == 0) - assert.same({}, parser:get_completion("say phrase --repeat=5 --repea")) - - assert.same({ "--style=", "--help" }, parser:get_completion("say phrase --repeat=5 -")) - assert.same({ "--style=", "--help" }, parser:get_completion("say phrase --repeat=5 --")) - - assert.same({ "--style=" }, parser:get_completion("say phrase --repeat=5 --s")) - - assert.same({ "--style=" }, parser:get_completion("say phrase --repeat=5 --style")) - - assert.same( - { "--style=lowercase", "--style=uppercase" }, - parser:get_completion("say phrase --repeat=5 --style=") - ) - - assert.same({ "--style=lowercase" }, parser:get_completion("say phrase --repeat=5 --style=l")) - - assert.same({ "--style=lowercase" }, parser:get_completion("say phrase --repeat=5 --style=lowercase")) - end) -end) - -describe("named argument", function() - describe("++foo=bar", function() - it("allow named argument as key", function() - local parser = _make_style_parser("++") - - assert.same({ "++style=" }, parser:get_completion("++s")) - assert.same({ "--help" }, parser:get_completion("++style=lowercase ")) - assert.same({}, parser:get_completion("sty")) - end) - - it("auto-completes on a #partial argument name - 001", function() - local parser = _make_style_parser("++") - - assert.same({}, parser:get_completion("--s", 1)) - assert.same({}, parser:get_completion("--s", 2)) - end) - - it("auto-completes on a #partial argument value - 001", function() - local parser = _make_style_parser("++") - - assert.same({ "++style=lowercase" }, parser:get_completion("++style=low")) - assert.same({}, parser:get_completion("++style=lowercase", 1)) - assert.same({}, parser:get_completion("++style=lowercase", 3)) - assert.same({}, parser:get_completion("++style=lowercase", 7)) - assert.same({}, parser:get_completion("++style=lowercase", 8)) - assert.same({}, parser:get_completion("++style=lowercase", 9)) - assert.same({}, parser:get_completion("++style=lowercase", 10)) - assert.same({}, parser:get_completion("++style=lowercase", 16)) - end) - - it("should only auto-complete --repeat once", function() - local parser = _make_simple_parser(true) - - assert.same({ - "++repeat=1", - "++repeat=2", - "++repeat=3", - "++repeat=4", - "++repeat=5", - }, parser:get_completion("say word ++repeat= ++repe", 18)) - assert.same({}, parser:get_completion("say word ++repeat= ++repe")) - end) - end) - - it("allow named argument as key", function() - local parser = _make_style_parser() - - assert.same({ "--style=" }, parser:get_completion("--s")) - assert.same({ "--help" }, parser:get_completion("--style=lowercase ")) - assert.same({ "--help" }, parser:get_completion("--style uppercase ")) - assert.same({}, parser:get_completion("sty")) - end) - - it("auto-completes on a #partial argument name - 001", function() - local parser = _make_style_parser() - - assert.same({}, parser:get_completion("--s", 1)) - assert.same({}, parser:get_completion("--s", 2)) - end) - - it("auto-completes on a #partial argument name - 002", function() - local parser = _make_style_parser() - - assert.same({}, parser:get_completion("--styl", 1)) - assert.same({}, parser:get_completion("--styl", 2)) - assert.same({}, parser:get_completion("--styl", 3)) - assert.same({}, parser:get_completion("--styl", 4)) - assert.same({}, parser:get_completion("--styl", 5)) - end) - - it("auto-completes on a #partial argument name - 003", function() - local parser = _make_style_parser() - - assert.same({}, parser:get_completion("--style", 1)) - assert.same({}, parser:get_completion("--style", 2)) - assert.same({}, parser:get_completion("--style", 3)) - assert.same({}, parser:get_completion("--style", 4)) - assert.same({}, parser:get_completion("--style", 5)) - assert.same({}, parser:get_completion("--style", 6)) - end) - - it("auto-completes on a #partial argument value - 001", function() - local parser = _make_style_parser() - - assert.same({ "--style=lowercase" }, parser:get_completion("--style=low")) - assert.same({}, parser:get_completion("--style=lowercase", 1)) - assert.same({}, parser:get_completion("--style=lowercase", 3)) - assert.same({}, parser:get_completion("--style=lowercase", 7)) - assert.same({}, parser:get_completion("--style=lowercase", 8)) - assert.same({}, parser:get_completion("--style=lowercase", 9)) - assert.same({}, parser:get_completion("--style=lowercase", 10)) - assert.same({}, parser:get_completion("--style=lowercase", 16)) - end) - - it("does not auto-complete if the name does not match", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - - local choices = function(data) - local output = {} - local value = data.value or 0 - - for index = 1, 5 do - table.insert(output, tostring(value + index)) - end - - return output - end - - parser:add_parameter({ "--repeat", choices = choices, help = "Number of values." }) - - assert.same({}, parser:get_completion("--style=", 1)) - assert.same({}, parser:get_completion("--style=", 2)) - assert.same({}, parser:get_completion("--style=", 3)) - assert.same({}, parser:get_completion("--style=", 4)) - assert.same({}, parser:get_completion("--style=", 5)) - assert.same({}, parser:get_completion("--style=", 6)) - assert.same({}, parser:get_completion("--style=", 7)) - assert.same({}, parser:get_completion("--style=", 8)) - end) - - it("does not auto-complete the name anymore and auto-completes the value", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - - parser:add_parameter({ - names = { "--style", "-s" }, - choices = { "lowercase", "uppercase" }, - help = "The format of the message.", - }) - - assert.same({}, parser:get_completion("--style=", 1)) - assert.same({}, parser:get_completion("--style=", 2)) - assert.same({}, parser:get_completion("--style=", 3)) - assert.same({}, parser:get_completion("--style=", 4)) - assert.same({}, parser:get_completion("--style=", 5)) - assert.same({}, parser:get_completion("--style=", 6)) - assert.same({}, parser:get_completion("--style=", 7)) - end) - - it("should only auto-complete --repeat once", function() - local parser = _make_simple_parser() - - assert.same({ - "--repeat=1", - "--repeat=2", - "--repeat=3", - "--repeat=4", - "--repeat=5", - }, parser:get_completion("say word --repeat= --repe", 18)) - assert.same({}, parser:get_completion("say word --repeat= --repe")) - end) - - it("suggests new named argument values based on the current value", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - - _add_repeat_parameter(parser) - - assert.same({ - "--repeat=3", - "--repeat=4", - "--repeat=5", - "--repeat=6", - "--repeat=7", - }, parser:get_completion("--repeat=3")) - end) -end) - -describe("flag argument", function() - it("auto-completes on the dash - 001", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "-f", help = "Force it." }) - - assert.same({ "-f=", "--help" }, parser:get_completion("-"), 1) - end) - - it("auto-completes on the dash - 002", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "-f", action = "store_true", help = "Force it." }) - - assert.same({ "-f", "--help" }, parser:get_completion("-"), 1) - end) - - it("does not auto-complete if at the end of the flag - 001", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "-f", help = "Force it." }) - - assert.same({}, parser:get_completion("-f", 1)) - assert.same({ "-f=" }, parser:get_completion("-f", 2)) - end) - - it("does not auto-complete if at the end of the flag - 002", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "-f", action = "store_true", help = "Force it." }) - - assert.same({}, parser:get_completion("-f", 1)) - assert.same({ "-f" }, parser:get_completion("-f", 2)) - end) - - describe("++flag examples", function() - it("auto-completes on the dash - 001", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "+f", help = "Force it." }) - - assert.same({ "+f=" }, parser:get_completion("+"), 1) - assert.same({ "--help" }, parser:get_completion("-"), 1) - end) - - it("auto-completes on the dash - 002", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "+f", action = "store_true", help = "Force it." }) - - assert.same({ "+f" }, parser:get_completion("+"), 1) - assert.same({ "--help" }, parser:get_completion("-"), 1) - end) - - it("does not auto-complete if at the end of the flag - 001", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "+f", help = "Force it." }) - - assert.same({}, parser:get_completion("+f", 1)) - assert.same({ "+f=" }, parser:get_completion("+f", 2)) - end) - end) -end) - -describe("nargs", function() - describe("flag", function() - it("works with nargs 1", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "--items", choices = { "barty", "bar", "foo" }, nargs = 1, help = "Test." }) - - assert.same({ "bar", "barty" }, parser:get_completion("--items b")) - end) - - it("works with nargs 2+", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "--items", choices = { "barty", "bar", "foo" }, nargs = 2, help = "Test." }) - - assert.same({ "foo" }, parser:get_completion("--items bar f")) - end) - - it("works with nargs *", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "--items", choices = { "barty", "bar", "foo" }, nargs = "*", help = "Test." }) - - assert.same({ "foo" }, parser:get_completion("--items bar f")) - end) - - it("works with nargs +", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "--items", choices = { "barty", "bar", "foo" }, nargs = "+", help = "Test." }) - - assert.same({ "foo" }, parser:get_completion("--items bar f")) - end) - end) - - describe("position", function() - it("works with nargs 1", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "items", choices = { "barty", "bar", "foo" }, nargs = 1, help = "Test." }) - - assert.same({ "bar", "barty" }, parser:get_completion("b")) - end) - - it("works with nargs 2+", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "items", choices = { "barty", "bar", "foo" }, nargs = 2, help = "Test." }) - - assert.same({ "foo" }, parser:get_completion("bar f")) - end) - - it("works with nargs *", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "items", choices = { "barty", "bar", "foo" }, nargs = "*", help = "Test." }) - - assert.same({ "foo" }, parser:get_completion("bar f")) - end) - - it("works with nargs +", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ - "items", - choices = { "barty", "bar", "foo" }, - nargs = "+", - help = "Parameter test.", - }) - - assert.same({ "foo" }, parser:get_completion("bar f")) - end) - end) - - describe("named argument", function() - it("does not complete with = when nargs is 2+", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "--items", nargs = 2, help = "Test." }) - - assert.same({ "--items" }, parser:get_completion("--ite")) - - local parser2 = cmdparse.ParameterParser.new({ help = "Test." }) - parser2:add_parameter({ "--items", nargs = 1, help = "Test." }) - - assert.same({ "--items=" }, parser2:get_completion("--ite")) - end) - end) -end) - -describe("numbered count - named argument", function() - it("works with count = 2", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ name = "--foo", choices = { "bar", "fizz", "buzz" }, count = 2, help = "Test." }) - - assert.same({ "--foo=" }, parser:get_completion("--fo")) - assert.same({ "--foo=bar", "--foo=fizz", "--foo=buzz" }, parser:get_completion("--foo=")) - assert.same({ "--foo=", "--help" }, parser:get_completion("--foo=bar ")) - assert.same({ "--foo=bar", "--foo=fizz", "--foo=buzz" }, parser:get_completion("--foo=bar --foo=")) - assert.same({ "--help" }, parser:get_completion("--foo=bar --foo=bar ")) - end) -end) - -describe("numbered count - named argument", function() - it("works with count = 2", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ name = "--foo", choices = { "bar", "fizz", "buzz" }, count = 2, help = "Test." }) - - assert.same({ "--foo=" }, parser:get_completion("--fo")) - assert.same({ "--foo=bar", "--foo=fizz", "--foo=buzz" }, parser:get_completion("--foo=")) - assert.same({ "--foo=", "--help" }, parser:get_completion("--foo=bar ")) - assert.same({ "--foo=bar", "--foo=fizz", "--foo=buzz" }, parser:get_completion("--foo=bar --foo=")) - assert.same({ "--help" }, parser:get_completion("--foo=bar --foo=bar ")) - end) -end) - -describe("* count", function() - describe("simple", function() - it("works with position arguments", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ name = "thing", choices = { "foo" }, count = "*", help = "Test." }) - - assert.same({ "foo", "--help" }, parser:get_completion("")) - assert.same({ "foo" }, parser:get_completion("fo")) - assert.same({ "foo" }, parser:get_completion("foo")) - assert.same({ "foo", "--help" }, parser:get_completion("foo ")) - assert.same({ "foo" }, parser:get_completion("foo fo")) - assert.same({ "foo" }, parser:get_completion("foo foo")) - assert.same({ "foo", "--help" }, parser:get_completion("foo foo ")) - assert.same({ "foo" }, parser:get_completion("foo foo foo")) - assert.same({ "foo", "--help" }, parser:get_completion("foo foo foo ")) - end) - end) -end) - -describe("dynamic argument", function() - it("works even if matches use spaces", function() - local parser = cmdparse.ParameterParser.new({ name = "top_test", help = "Test" }) - local subparsers = parser:add_subparsers({ destination = "commands", help = "All main commands." }) - local say_parser = subparsers:add_parser({ name = "say", help = "Say something." }) - local inner_subparsers = say_parser:add_subparsers({ destination = "thing_subparsers", help = "Test." }) - - local dynamic = inner_subparsers:add_parser({ - name = "dynamic_thing", - choices = function() - return { "item with spaces", "cc", "zzz", "lazers" } - end, - help = "Test.", - }) - - local inner_dynamic = dynamic:add_subparsers({ name = "inner_dynamic_thing", help = "Test." }) - local different = inner_dynamic:add_parser({ name = "different", help = "Test." }) - different:add_parameter({ - name = "last", - choices = function() - return { "branch", "here" } - end, - help = "Test.", - }) - - assert.same({ "item with spaces" }, parser:get_completion('say "item "')) - assert.same({ "branch" }, parser:get_completion('say "item with spaces" different b')) - end) - - it("works with positional arguments", function() - local parser = cmdparse.ParameterParser.new({ name = "top_test", help = "Test" }) - local subparsers = parser:add_subparsers({ destination = "commands", help = "All main commands." }) - local say_parser = subparsers:add_parser({ name = "say", help = "Say something." }) - say_parser:add_parameter({ - name = "thing", - choices = function() - return { "a", "bb", "asteroid", "tt" } - end, - help = "Choices that come from a function.", - }) - local inner_subparsers = say_parser:add_subparsers({ destination = "thing_subparsers", help = "Test." }) - local thing = inner_subparsers:add_parser({ name = "thing_parser", help = "Inner thing." }) - thing:add_parameter({ name = "last_thing", choices = { "another", "last" }, help = "Test." }) - - local dynamic = inner_subparsers:add_parser({ - name = "dynamic_thing", - choices = function() - return { "ab", "cc", "zzz", "lazers" } - end, - help = "Test.", - }) - local inner_dynamic = dynamic:add_subparsers({ name = "inner_dynamic_thing", help = "Test." }) - local different = inner_dynamic:add_parser({ name = "different", help = "Test." }) - different:add_parameter({ - name = "last", - choices = function() - return { "branch", "here" } - end, - help = "Test.", - }) - - -- NOTE: We don't complete the next subparsers because required - -- parameter(s) from the `say` subparser have no been satisfied yet. - -- - assert.same({ "a", "asteroid", "bb", "tt", "--help" }, parser:get_completion("say ")) - -- IMPORTANT: Notice we do not include `ab` in the completion because - -- the `thing` argument is required and must be satisfied first before - -- we can continue to the subparser. - -- - assert.same({ "a", "asteroid" }, parser:get_completion("say a")) - assert.same({ "ab", "cc", "lazers", "thing_parser", "zzz", "--help" }, parser:get_completion("say a ")) - assert.same({ "ab", "cc", "lazers", "thing_parser", "zzz", "--help" }, parser:get_completion("say tt ")) - assert.same({ "another", "last", "--help" }, parser:get_completion("say a thing_parser ")) - - assert.same({ "different", "--help" }, parser:get_completion("say ab ")) - assert.same({ "branch", "here", "--help" }, parser:get_completion("say ab different ")) - end) -end) diff --git a/spec/plugin_template/cmdparse_error_spec.lua b/spec/plugin_template/cmdparse_error_spec.lua deleted file mode 100644 index ba693f1..0000000 --- a/spec/plugin_template/cmdparse_error_spec.lua +++ /dev/null @@ -1,639 +0,0 @@ ---- Make sure that `cmdparse` errors when it should. ---- ----@module 'plugin_template.cmdparse_error_spec' ---- - -local cmdparse = require("plugin_template._cli.cmdparse") - -describe("bad auto-complete input", function() - it("errors if an incorrect flag is given", function() - local parser = cmdparse.ParameterParser.new({ "arbitrary-thing", help = "Prepare to sleep or sleep." }) - - parser:add_parameter({ "--bar", action = "store_true", help = "The -bar flag." }) - parser:add_parameter({ "--foo", action = "store_true", help = "The -foo flag." }) - - assert.same({}, parser:get_completion("--does-not-exist ")) - assert.same({}, parser:get_completion("--bar --does-not-exist ")) - assert.same({}, parser:get_completion("--does-not-exist")) - assert.same({}, parser:get_completion("--bar --does-not-exist")) - end) - - describe("named arguments", function() - it("errors if an unknown named argument is given", function() - local parser = cmdparse.ParameterParser.new({ "arbitrary-thing", help = "Prepare to sleep or sleep." }) - - parser:add_parameter({ "--bar", help = "The -bar flag." }) - parser:add_parameter({ "--foo", action = "store_true", help = "The -foo flag." }) - - assert.same({}, parser:get_completion("--unknown=thing ")) - assert.same({}, parser:get_completion("--unknown=thing")) - - assert.same({}, parser:get_completion("--bar=thing --unknown=thing ")) - assert.same({}, parser:get_completion("--bar=thing --unknown=thing")) - - assert.same({}, parser:get_completion("--unknown=thing --bar=thing")) - assert.same({}, parser:get_completion("--unknown=thing --bar=thing ")) - end) - end) -end) - -describe("bad definition input", function() - describe("parsers definition issues", function() - it("errors if you define a flag argument with choices", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - - local success, result = pcall(function() - parser:add_parameter({ "--foo", action = "store_true", choices = { "f" }, help = "Test." }) - end) - - assert.is_false(success) - assert.equal('Parameter "--foo" cannot use action "store_true" and choices at the same time.', result) - end) - - it("errors if you define a nargs=0 + position argument", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - - local success, result = pcall(function() - parser:add_parameter({ "foo", nargs = 0, help = "Test." }) - end) - - assert.is_false(success) - assert.equal('Parameter "foo" cannot be nargs=0.', result) - end) - - it("errors if you define a nargs + flag argument", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - - local success, result = pcall(function() - parser:add_parameter({ "--foo", action = "store_true", nargs = 2, help = "Test." }) - end) - - assert.is_false(success) - assert.equal('Parameter "--foo" cannot use action "store_true" and nargs at the same time.', result) - end) - - it("errors if you define a position parameter + action store_true #asdf", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - - local success, result = pcall(function() - parser:add_parameter({ "ɧelp", action = "store_true", help = "Test." }) - end) - - assert.is_false(success) - assert.equal('Parameter "ɧelp" cannot use action="store_true".', result) - end) - - it("errors if a custom type=foo doesn't return a value - 001", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ "--foo", type = tonumber, help = "Test." }) - - local success, result = pcall(function() - parser:parse_arguments("--foo=not_a_number") - end) - - assert.is_false(success) - assert.equal( - 'Parameter "--foo" failed to find a value. Please check your `type` parameter and fix it!', - result - ) - - success, result = pcall(function() - return parser:parse_arguments("--foo=123") - end) - - assert.is_true(success) - assert.same({ foo = 123 }, result) - end) - - it("errors if a custom type=foo doesn't return a value - 002", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ - "--foo", - nargs = 1, - type = function(_) - return nil - end, - help = "Test.", - }) - - local success, result = pcall(function() - parser:parse_arguments("--foo=not_a_number") - end) - - assert.is_false(success) - assert.equal( - 'Parameter "--foo" failed to find a value. Please check your `type` parameter and fix it!', - result - ) - - success, result = pcall(function() - return parser:parse_arguments("--foo=123") - end) - - assert.is_false(success) - assert.equal( - 'Parameter "--foo" failed to find a value. Please check your `type` parameter and fix it!', - result - ) - end) - - it("errors if no name is give", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - - local success, result = pcall(function() - parser:add_parameter({ - nargs = 1, - type = function(_) - return nil - end, - help = "Test.", - }) - end) - - assert.is_false(success) - assert.equal( - [[Options "{ - help = "Test.", - nargs = 1, - type = -}" is missing a "name" key.]], - result - ) - end) - - it("includes named argument choices", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ "--foo", choices = { "aaa", "bbb", "zzz" }, help = "Test." }) - - local success, result = pcall(function() - parser:parse_arguments("--foo=not_a_valid_choice") - end) - - assert.is_false(success) - assert.equal( - 'Parameter "--foo" got invalid "not_a_valid_choice" value. Expected one of { "aaa", "bbb", "zzz" }.', - result - ) - end) - - it("includes nested subparsers argument choices - 001 required", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ "thing", help = "Test." }) - - local subparsers = parser:add_subparsers({ "commands", help = "Test." }) - subparsers.required = true - - local inner_parser = subparsers:add_parser({ "inner_command", help = "Test." }) - local inner_subparsers = inner_parser:add_subparsers({ "commands", help = "Test." }) - inner_subparsers.required = true - inner_subparsers:add_parser({ "child", choices = { "foo", "bar", "thing" }, help = "Test." }) - - local success, result = pcall(function() - parser:parse_arguments("some_text inner_command does_not_exist") - end) - - assert.is_false(success) - assert.equal( - [[Got unexpected "does_not_exist" value. Did you mean one of these incomplete parameters? -foo -bar -thing]], - result - ) - end) - - it("includes position argument choices", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ "foo", choices = { "aaa", "bbb", "zzz" }, help = "Test." }) - - local success, result = pcall(function() - parser:parse_arguments("not_a_valid_choice") - end) - - assert.is_false(success) - assert.equal( - 'Parameter "foo" got invalid "not_a_valid_choice" value. Expected one of { "aaa", "bbb", "zzz" }.', - result - ) - end) - - it("includes subparsers argument choices - 001 required", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ "thing", help = "Test." }) - local subparsers = parser:add_subparsers({ "commands", help = "Test." }) - subparsers.required = true - subparsers:add_parser({ "inner_command", choices = { "foo", "bar", "thing" } }) - - local success, result = pcall(function() - parser:parse_arguments("foo") - end) - - assert.is_false(success) - assert.equal('Parameter "thing" must be defined.', result) - - success, result = pcall(function() - parser:parse_arguments("something not_valid") - end) - - assert.is_false(success) - assert.equal( - [[Got unexpected "not_valid" value. Did you mean one of these incomplete parameters? -foo -bar -thing]], - result - ) - end) - - it("includes subparsers argument choices - 002 - required", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ "thing", help = "Test." }) - local subparsers = parser:add_subparsers({ "commands", help = "Test." }) - subparsers.required = false - subparsers:add_parser({ "inner_command", choices = { "foo", "bar", "thing" } }) - - local success, result = pcall(function() - parser:parse_arguments("something not_valid_subparser") - end) - - assert.is_false(success) - assert.equal( - [[Got unexpected "not_valid_subparser" value. Did you mean one of these incomplete parameters? -foo -bar -thing]], - result - ) - end) - - describe("nargs", function() - describe("nargs number", function() - it("errors if not enough values are given", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ "--foo", nargs = 2, help = "Test." }) - parser:add_parameter({ "--bar", nargs = 2, help = "Test." }) - - local command = "--foo thing --bar something else" - local success, result = pcall(function() - parser:parse_arguments(command) - end) - - assert.is_false(success) - assert.equal('Parameter "--foo" requires "2" values. Got "1" value.', result) - - success, result = pcall(function() - parser:get_completion(command) - end) - - assert.is_false(success) - assert.equal('Parameter "--foo" requires "2" values. Got "1" value.', result) - end) - - it("works with nargs=2 + count", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - - local success, result = pcall(function() - parser:add_parameter({ "--foo", nargs = 2, action = "count", help = "Test." }) - end) - - assert.is_false(success) - assert.equal('Parameter "--foo" cannot use action "count" and nargs at the same time.', result) - end) - - it("works with nargs=2 + store_false", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - - local success, result = pcall(function() - parser:add_parameter({ "--foo", nargs = 2, action = "store_false", help = "Test." }) - end) - - assert.is_false(success) - assert.equal( - 'Parameter "--foo" cannot use action "store_false" and nargs at the same time.', - result - ) - end) - - it("works with nargs=2 + store_true", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - - local success, result = pcall(function() - parser:add_parameter({ "--foo", nargs = 2, action = "store_true", help = "Test." }) - end) - - assert.is_false(success) - assert.equal('Parameter "--foo" cannot use action "store_true" and nargs at the same time.', result) - end) - end) - - describe("nargs *", function() - it("works with nargs=* + count", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - - local success, result = pcall(function() - parser:add_parameter({ "--foo", nargs = "*", action = "count", help = "Test." }) - end) - - assert.is_false(success) - assert.equal('Parameter "--foo" cannot use action "count" and nargs at the same time.', result) - end) - - it("works with nargs=* + store_false", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - - local success, result = pcall(function() - parser:add_parameter({ "--foo", nargs = "*", action = "store_false", help = "Test." }) - end) - - assert.is_false(success) - assert.equal( - 'Parameter "--foo" cannot use action "store_false" and nargs at the same time.', - result - ) - end) - - it("works with nargs=* + store_true", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - - local success, result = pcall(function() - parser:add_parameter({ "--foo", nargs = "*", action = "store_true", help = "Test." }) - end) - - assert.is_false(success) - assert.equal('Parameter "--foo" cannot use action "store_true" and nargs at the same time.', result) - end) - end) - - describe("nargs +", function() - it("works with nargs=+ + count", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - - local success, result = pcall(function() - parser:add_parameter({ "--foo", nargs = "+", action = "count", help = "Test." }) - end) - - assert.is_false(success) - assert.equal('Parameter "--foo" cannot use action "count" and nargs at the same time.', result) - end) - - it("works with nargs=+ + store_false", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - - local success, result = pcall(function() - parser:add_parameter({ "--foo", nargs = "+", action = "store_false", help = "Test." }) - end) - - assert.is_false(success) - assert.equal( - 'Parameter "--foo" cannot use action "store_false" and nargs at the same time.', - result - ) - end) - - it("works with nargs=+ + store_true", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - - local success, result = pcall(function() - parser:add_parameter({ "--foo", nargs = "+", action = "store_true", help = "Test." }) - end) - - assert.is_false(success) - assert.equal('Parameter "--foo" cannot use action "store_true" and nargs at the same time.', result) - end) - end) - - describe("nargs + --foo=bar named argument syntax", function() - it("errors with nargs=2 and --foo=bar syntax", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - - parser:add_parameter({ "--foo", nargs = 2, help = "Test." }) - - local success, result = pcall(function() - parser:parse_arguments("--foo=thing") - end) - - assert.is_false(success) - assert.equal('Parameter "--foo" requires "2" values. Got "1" value.', result) - end) - - it("works with nargs=* and --foo=bar syntax", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - - parser:add_parameter({ "--foo", nargs = "*", help = "Test." }) - - assert.same({ foo = "thing" }, parser:parse_arguments("--foo=thing")) - end) - - it("works with nargs=+ and --foo=bar syntax", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - - parser:add_parameter({ "--foo", nargs = "+", help = "Test." }) - - assert.same({ foo = "thing" }, parser:parse_arguments("--foo=thing")) - end) - end) - end) - end) - - describe("simple", function() - it("does not error if there is no text and all arguments are optional", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ name = "foo", required = false, help = "Test." }) - - parser:parse_arguments("") - end) - - it("errors if a flag is given instead of an expected position", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - - parser:add_parameter({ "foo", help = "Test." }) - - local success, result = pcall(function() - parser:parse_arguments("--not=a_position") - end) - - assert.is_false(success) - assert.equal('Parameter "foo" must be defined.', result) - end) - - it("errors if nargs doesn't get enough expected values", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - - parser:add_parameter({ "--foo", nargs = 3, required = true, help = "Test." }) - - local success, result = pcall(function() - parser:parse_arguments("--foo thing another") - end) - - assert.is_false(success) - assert.equal('Parameter "--foo" requires "3" values. Got "2" values.', result) - end) - - it("errors if the user is #missing a required flag argument - 001", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - - parser:add_parameter({ "--foo", action = "store_true", required = true, help = "Test." }) - - local success, result = pcall(function() - parser:parse_arguments("") - end) - - assert.is_false(success) - assert.equal('Parameter "--foo" must be defined.', result) - end) - - it("errors if the user is #missing a required flag argument - 002", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "thing", help = "Test." }) - parser:add_parameter({ "--foo", action = "store_true", required = true, help = "Test." }) - - assert.same({ foo = true, thing = "blah" }, parser:parse_arguments("blah --foo")) - - local success, result = pcall(function() - parser:parse_arguments("blah ") - end) - - assert.is_false(success) - assert.equal('Parameter "--foo" must be defined.', result) - end) - - it("errors if the user is #missing a required named argument - 001", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "--foo", required = true, help = "Test." }) - - local success, result = pcall(function() - parser:parse_arguments("") - end) - - assert.is_false(success) - assert.equal('Parameter "--foo" must be defined.', result) - end) - - it("errors if the user is #missing a required named argument - 002", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "--foo", required = true, help = "Test." }) - - local success, result = pcall(function() - parser:parse_arguments("--foo= ") - end) - - assert.is_false(success) - assert.equal('Parameter "--foo" requires 1 value.', result) - end) - - it("errors if the user is #missing a required position argument", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ name = "foo", help = "Test." }) - - local success, result = pcall(function() - parser:parse_arguments("") - end) - - assert.is_false(success) - assert.equal('Parameter "foo" must be defined.', result) - end) - - it("ignores an optional position argument", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ name = "foo", required = false, help = "Test." }) - - parser:parse_arguments("") - end) - - it("errors if the user is #missing one of several arguments - 003 - position argument", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ name = "foo", nargs = 2, help = "Test." }) - - local success, result = pcall(function() - parser:parse_arguments("thing") - end) - - assert.is_false(success) - assert.equal('Parameter "foo" requires "2" values. Got "1" value.', result) - end) - - it("errors if the user is #missing one of several arguments - 004 - flag-value argument", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ name = "--foo", nargs = 2, help = "Test." }) - - local success, result = pcall(function() - parser:parse_arguments("--foo blah") - end) - - assert.is_false(success) - assert.equal('Parameter "--foo" requires "2" values. Got "1" value.', result) - end) - - it("errors if a named argument in the middle of parse that is not given a value", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ name = "--foo", required = true, help = "Test." }) - parser:add_parameter({ name = "--bar", help = "Test." }) - - local success, result = pcall(function() - parser:parse_arguments("--foo= --bar=thing") - end) - - assert.is_false(success) - assert.equal('Parameter "--foo" requires 1 value.', result) - end) - - it("errors if a position argument in the middle of parse that is not given a value", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ name = "foo", help = "Test." }) - parser:add_parameter({ name = "bar", help = "Test." }) - parser:add_parameter({ name = "--fizz", help = "Test." }) - - local success, result = pcall(function() - parser:parse_arguments("some --fizz=thing") - end) - - assert.is_false(success) - assert.equal('Parameter "bar" must be defined.', result) - end) - - it("errors if a position argument at the end of a parse that is not given a value", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ name = "foo", help = "Test." }) - parser:add_parameter({ name = "bar", help = "Test." }) - parser:add_parameter({ name = "thing", help = "Test." }) - - local success, result = pcall(function() - parser:parse_arguments("some fizz") - end) - - assert.is_false(success) - assert.equal('Parameter "thing" must be defined.', result) - end) - end) -end) - -describe("bugs", function() - describe("auto-complete", function() - it("works with arbitrary-thing's flags", function() - local parser = cmdparse.ParameterParser.new({ "arbitrary-thing", help = "Prepare to sleep or sleep." }) - - parser:add_parameter({ "-a", action = "store_true", help = "The -a flag." }) - parser:add_parameter({ "-b", action = "store_true", help = "The -b flag." }) - parser:add_parameter({ "-c", action = "store_true", help = "The -c flag." }) - parser:add_parameter({ "-f", action = "store_true", count = "*", help = "The -f flag." }) - parser:add_parameter({ - "-v", - action = "store_true", - count = "*", - destination = "verbose", - help = "The -v flag.", - }) - - assert.same({ "-c", "-f", "-v", "--help" }, parser:get_completion("-a -b ")) - end) - - it("works with arbitrary-thing's flags - 002", function() - local parser = cmdparse.ParameterParser.new({ "arbitrary-thing", help = "Prepare to sleep or sleep." }) - - parser:add_parameter({ "--bar", action = "store_true", help = "The -bar flag." }) - parser:add_parameter({ "--foo", action = "store_true", help = "The -foo flag." }) - - assert.same({}, parser:get_completion("-a -b ")) - end) - end) -end) diff --git a/spec/plugin_template/cmdparse_spec.lua b/spec/plugin_template/cmdparse_spec.lua deleted file mode 100644 index ee75625..0000000 --- a/spec/plugin_template/cmdparse_spec.lua +++ /dev/null @@ -1,1325 +0,0 @@ ---- Make sure that `cmdparse` parses and auto-completes as expected. ---- ----@module 'plugin_template.cmdparse_spec' ---- - -local cmdparse = require("plugin_template._cli.cmdparse") -local configuration = require("plugin_template._core.configuration") -local constant = require("plugin_template._cli.cmdparse.constant") - -local _DATA - ---- Save the user's current configuration (so we can modify & restore it later). -local function _keep_configuration() - _DATA = vim.deepcopy(configuration.DATA) -end - ---- Revert the configuration to its previously-saved state. -local function _restore_configuration() - configuration.DATA = _DATA -end - ----@return cmdparse.ParameterParser # Create a tree of commands for unittests. -local function _make_simple_parser() - local choices = function(data) - if vim.tbl_contains(data.contexts, constant.ChoiceContext.help_message) then - local output = {} - - for index = 1, 5 do - table.insert(output, tostring(index)) - end - - return output - end - - local value = data.text - - if value == "" then - value = 0 - else - value = tonumber(value) - - if type(value) ~= "number" then - return {} - end - end - - --- @cast value number - - local output = {} - - for index = 1, 5 do - table.insert(output, tostring(value + index)) - end - - return output - end - - local function _add_repeat_parameter(parser) - parser:add_parameter({ - names = { "--repeat", "-r" }, - choices = choices, - help = "The number of times to display the message.", - }) - end - - local function _add_style_parameter(parser) - parser:add_parameter({ - names = { "--style", "-s" }, - choices = { "lowercase", "uppercase" }, - help = "The format of the message.", - }) - end - - local parser = cmdparse.ParameterParser.new({ name = "top_test", help = "Test." }) - local subparsers = parser:add_subparsers({ destination = "commands", help = "Test." }) - local say = subparsers:add_parser({ name = "say", help = "Print stuff to the terminal." }) - local say_subparsers = say:add_subparsers({ destination = "say_commands", help = "All commands that print." }) - local say_word = say_subparsers:add_parser({ name = "word", help = "Print a single word." }) - local say_phrase = say_subparsers:add_parser({ name = "phrase", help = "Print a whole sentence." }) - - _add_repeat_parameter(say_phrase) - _add_repeat_parameter(say_word) - _add_style_parameter(say_phrase) - _add_style_parameter(say_word) - - return parser -end - -describe("action", function() - describe("action - append", function() - it("simple", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ name = "--foo", action = "append", nargs = 1, count = "*", help = "Test." }) - - assert.same({ foo = { "bar", "fizz", "buzz" } }, parser:parse_arguments("--foo=bar --foo=fizz --foo=buzz")) - end) - end) - - describe("action - custom function", function() - it("external table", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - local values = { "a", "bb", "ccc" } - parser:add_parameter({ - name = "--foo", - action = function(data) - local result = values[1] - data.namespace[result] = true - - table.remove(values, 1) - end, - nargs = 1, - count = "*", - help = "Test.", - }) - - assert.same({ a = true, bb = true, ccc = true }, parser:parse_arguments("--foo=bar --foo=fizz --foo=buzz")) - end) - end) - - describe("action - store_false", function() - it("simple", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ name = "--foo", action = "store_false", help = "Test." }) - - assert.same({ foo = false }, parser:parse_arguments("--foo")) - end) - end) - - describe("action - store_true", function() - it("simple", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ name = "--foo", action = "store_true", help = "Test." }) - - assert.same({ foo = true }, parser:parse_arguments("--foo")) - end) - end) -end) - --- -- TODO: Add this later --- describe("choices", function() --- it("ensures there are no duplicate choices during a parse", function() --- local function remove_value(array, value) --- print("removing value from array") --- print(value) --- print(vim.inspect(array)) --- --- for index = #array, 1, -1 do --- if array[index] == value then --- table.remove(array, index) --- end --- end --- end --- --- local parser = cmdparse.ParameterParser.new({ help = "Test" }) --- local values = { "1", "2", "3", "4", "5" } --- parser:add_parameter({ --- "--items", --- choices = function(data) --- print('DEBUGPRINT[5]: cmdparse_spec.lua:129: data=' .. vim.inspect(data)) --- return values --- end, --- type = function(value) --- remove_value(values, value) --- end, --- count = "*", --- help = "Test.", --- }) --- --- assert.same({ --- "--items=1", --- "--items=2", --- "--items=3", --- "--items=4", --- "--items=5", --- }, parser:get_completion("--items=")) --- assert.same({ --- "--items=2", --- "--items=3", --- "--items=4", --- "--items=5", --- }, parser:get_completion("--items=1 --items=")) --- assert.same({ --- "--items=2", --- "--items=4", --- "--items=5", --- }, parser:get_completion("--items=1 --items=3 --items=")) --- assert.same({ --- "--items=2", --- "--items=5", --- }, parser:get_completion("--items=1 --items=3 --items=4 --items=")) --- assert.same({ "--items=5" }, parser:get_completion("--items=1 --items=3 --items=4 --items=2 --items=")) --- end) --- end) - -describe("configuration", function() - before_each(_keep_configuration) - after_each(_restore_configuration) - - describe("auto-completion", function() - it("hides the --help flag if the user asks to, explicitly", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ "--items", help = "Test." }) - - assert.same({ "--items=", "--help" }, parser:get_completion("")) - assert.same({ "--items=" }, parser:get_completion("", nil, { display = { help_flag = false } })) - end) - - it("hides the --help flag if the user asks to, implicitly via configuration", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ "--items", help = "Test." }) - configuration.DATA.cmdparse.auto_complete.display.help_flag = false - - assert.same({ "--items=" }, parser:get_completion("")) - end) - - it("works even if the user deletess their configuration", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ "--items", help = "Test." }) - - configuration.DATA = {} - assert.same({ "--items=", "--help" }, parser:get_completion("")) - end) - end) -end) - -describe("count", function() - it("sets a position parameter to optional when count = *", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - local parameter = parser:add_parameter({ "foo", count = "*", help = "Test." }) - - assert.is_false(parameter.required) - end) - - it("works with position + count=* parameters - 001 - multiple parameters", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "foo", nargs = "*", help = "Test." }) - parser:add_parameter({ "bar", nargs = "*", help = "Test." }) - parser:add_parameter({ "--flag", action = "store_true", help = "Test." }) - - local namespace = parser:parse_arguments("1 2 3 4 --flag 5 6 7") - assert.same({ foo = { "1", "2", "3", "4" }, flag = true, bar = { "5", "6", "7" } }, namespace) - end) - - it("works with position + count=* parameters - 002 - split, single parameter", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "foo", action = "append", count = "*", nargs = "*", type = tonumber, help = "Test." }) - parser:add_parameter({ "--flag", action = "store_true", help = "Test." }) - - local namespace = parser:parse_arguments("1 2 3 4 --flag 5 6 7") - assert.same({ foo = { [1] = { 1, 2, 3, 4 }, [2] = { 5, 6, 7 } }, flag = true }, namespace) - end) -end) - -describe("default", function() - it("works with a #default", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - - assert.equal("Usage: [--help]", parser:get_concise_help("")) - end) - - it("works with a #empty type", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ name = "foo", help = "Test." }) - - local namespace = parser:parse_arguments("12") - assert.same({ foo = "12" }, namespace) - end) - - it("shows the full #help if the user asks for it", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - - assert.equal( - [[Usage: [--help] - -Options: - --help -h Show this help message and exit. -]], - parser:get_full_help("") - ) - end) - - it("creates a default value if store_true / store_false has no value", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "--truthy", action = "store_true", help = "Test." }) - parser:add_parameter({ "--falsey", action = "store_false", help = "Test." }) - parser:add_parameter({ "--lastly", action = "store_true", default = 10, help = "Test." }) - - local namespace = parser:parse_arguments("") - assert.equal(false, namespace.truthy) - assert.equal(true, namespace.falsey) - assert.equal(10, namespace.lastly) - - namespace = parser:parse_arguments("--truthy --falsey --lastly") - assert.equal(true, namespace.truthy) - assert.equal(false, namespace.falsey) - assert.equal(true, namespace.lastly) - end) -end) - -describe("help", function() - describe("full", function() - it("shows all of the options for a #basic parser - 001", function() - local parser = _make_simple_parser() - - assert.equal( - [[Usage: top_test {say} [--help] - -Commands: - say Print stuff to the terminal. - -Options: - --help -h Show this help message and exit. -]], - parser:get_full_help("") - ) - end) - - it("shows all of the options for a #basic parser - 002", function() - local parser = _make_simple_parser() - - assert.equal( - [[Usage: {word} [--repeat {1,2,3,4,5}] [--style {lowercase,uppercase}] [--help] - -Options: - --repeat -r {1,2,3,4,5} The number of times to display the message. - --style -s {lowercase,uppercase} The format of the message. - --help -h Show this help message and exit. -]], - parser:get_full_help("say word ") - ) - end) - - it("shows all of the options for a subparser - 001", function() - local parser = _make_simple_parser() - - assert.equal( - [[Usage: {say} {phrase,word} [--help] - -Commands: - phrase Print a whole sentence. - word Print a single word. - -Options: - --help -h Show this help message and exit. -]], - parser:get_full_help("say ") - ) - end) - - it("shows all of the options for a subparser - 002", function() - local parser = _make_simple_parser() - - assert.equal( - [[Usage: {phrase} [--repeat {1,2,3,4,5}] [--style {lowercase,uppercase}] [--help] - -Options: - --repeat -r {1,2,3,4,5} The number of times to display the message. - --style -s {lowercase,uppercase} The format of the message. - --help -h Show this help message and exit. -]], - parser:get_full_help("say phrase ") - ) - end) - - it("shows even if there parsing errors", function() - local function _assert(parser, command, expected) - local success, result = pcall(function() - parser:parse_arguments(command) - end) - - assert.is_false(success) - assert.equal(expected, result) - end - - local parser = _make_simple_parser() - - _assert( - parser, - "does_not_exist --help", - [[Usage: top_test {say} [--help] - -Commands: - say Print stuff to the terminal. - -Options: - --help -h Show this help message and exit. -]] - ) - _assert( - parser, - "say does_not_exist --help", - [[Usage: {say} {phrase,word} [--help] - -Commands: - phrase Print a whole sentence. - word Print a single word. - -Options: - --help -h Show this help message and exit. -]] - ) - _assert( - parser, - "say phrase does_not_exist --help", - [[Usage: {phrase} [--repeat {1,2,3,4,5}] [--style {lowercase,uppercase}] [--help] - -Options: - --repeat -r {1,2,3,4,5} The number of times to display the message. - --style -s {lowercase,uppercase} The format of the message. - --help -h Show this help message and exit. -]] - ) - end) - - it("works with a parser that has more than one choice for its name", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - local subparsers = parser:add_subparsers({ destination = "commands", help = "Test." }) - subparsers:add_parser({ name = "thing", choices = { "aaa", "bbb", "ccc" }, help = "Do a thing." }) - - assert.equal( - [[Usage: {aaa} [--help] - -Commands: - {aaa,bbb,ccc} Do a thing. - -Options: - --help -h Show this help message and exit. -]], - parser:get_full_help("") - ) - end) - end) - - describe("value_hint - defaults", function() - describe("flags", function() - describe("choices", function() - it("choices + default", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "--items", choices = { "a", "b" }, help = "Test." }) - - assert.equal( - [[Usage: [--items {a,b}] [--help] - -Options: - --items {a,b} Test. - --help -h Show this help message and exit. -]], - parser:get_full_help("") - ) - end) - - it("choices + nargs=number + append", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ - "--items", - choices = { "a", "b" }, - nargs = 3, - action = "append", - help = "Test.", - }) - - assert.equal( - [[Usage: [--items {a,b} {a,b} {a,b}] [--help] - -Options: - --items {a,b} {a,b} {a,b} Test. - --help -h Show this help message and exit. -]], - parser:get_full_help("") - ) - end) - - it("choices + nargs=number", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "--items", choices = { "a", "b" }, nargs = 3, help = "Test." }) - - assert.equal( - [[Usage: [--items {a,b} {a,b} {a,b}] [--help] - -Options: - --items {a,b} {a,b} {a,b} Test. - --help -h Show this help message and exit. -]], - parser:get_full_help("") - ) - end) - - it("choices + nargs=* + append", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ - "--items", - choices = { "a", "b" }, - nargs = "*", - action = "append", - help = "Test.", - }) - - assert.equal( - [=[Usage: [--items [{a,b} ...]] [--help] - -Options: - --items [{a,b} ...] Test. - --help -h Show this help message and exit. -]=], - parser:get_full_help("") - ) - end) - - it("choices + nargs=*", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "--items", choices = { "a", "b" }, nargs = "*", help = "Test." }) - - assert.equal( - [=[Usage: [--items [{a,b} ...]] [--help] - -Options: - --items [{a,b} ...] Test. - --help -h Show this help message and exit. -]=], - parser:get_full_help("") - ) - end) - - it("choices + nargs=+ + append", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ - "--items", - choices = { "a", "b" }, - nargs = "+", - action = "append", - help = "Test.", - }) - - assert.equal( - [=[Usage: [--items {a,b} [{a,b} ...]] [--help] - -Options: - --items {a,b} [{a,b} ...] Test. - --help -h Show this help message and exit. -]=], - parser:get_full_help("") - ) - end) - - it("choices + nargs=+", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "--items", choices = { "a", "b" }, nargs = "+", help = "Test." }) - - assert.equal( - [=[Usage: [--items {a,b} [{a,b} ...]] [--help] - -Options: - --items {a,b} [{a,b} ...] Test. - --help -h Show this help message and exit. -]=], - parser:get_full_help("") - ) - end) - end) - - describe("no choices", function() - it("converts - to _", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "--file-paths", help = "Test." }) - parser:add_parameter({ "--last-thing", action = "store_true", help = "Test." }) - parser:add_parameter({ "some-thing", help = "Test." }) - - assert.equal( - [=[Usage: SOME_THING [--file-paths FILE_PATHS] [--last-thing] [--help] - -Positional Arguments: - SOME_THING Test. - -Options: - --file-paths FILE_PATHS Test. - --last-thing Test. - --help -h Show this help message and exit. -]=], - parser:get_full_help("") - ) - end) - - it("nargs=number", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "--items", nargs = 3, help = "Test." }) - - assert.equal( - [=[Usage: [--items ITEMS ITEMS ITEMS] [--help] - -Options: - --items ITEMS ITEMS ITEMS Test. - --help -h Show this help message and exit. -]=], - parser:get_full_help("") - ) - end) - - it("has no value hint if nargs=0", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "--thing", nargs = 0, help = "Test." }) - - assert.equal( - [[Usage: [--thing] [--help] - -Options: - --thing Test. - --help -h Show this help message and exit. -]], - parser:get_full_help("") - ) - end) - end) - - it("has value hint if details are empty", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "--thing", help = "Test." }) - - assert.equal( - [[Usage: [--thing THING] [--help] - -Options: - --thing THING Test. - --help -h Show this help message and exit. -]], - parser:get_full_help("") - ) - end) - end) - - describe("positions", function() - describe("choices", function() - it("choices + default", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "items", choices = { "a", "b" }, help = "Test." }) - - assert.equal( - [[Usage: {a,b} [--help] - -Positional Arguments: - {a,b} Test. - -Options: - --help -h Show this help message and exit. -]], - parser:get_full_help("") - ) - end) - - it("choices + nargs=number + append", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ - "items", - choices = { "a", "b" }, - nargs = 3, - action = "append", - help = "Test.", - }) - - assert.equal( - [[ -Usage: {a,b} {a,b} {a,b} [--help] - -Positional Arguments: - {a,b} {a,b} {a,b} Test. - -Options: - --help -h Show this help message and exit. -]], - parser:get_full_help("") - ) - end) - - it("choices + nargs=number", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "items", choices = { "a", "b" }, nargs = 3, help = "Test." }) - - assert.equal( - [[ -Usage: {a,b} {a,b} {a,b} [--help] - -Positional Arguments: - {a,b} {a,b} {a,b} Test. - -Options: - --help -h Show this help message and exit. -]], - parser:get_full_help("") - ) - end) - - it("choices + nargs=* + append", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ - "items", - choices = { "a", "b" }, - nargs = "*", - action = "append", - help = "Test.", - }) - - assert.equal( - [[Usage: [{a,b} ...] [--help] - -Positional Arguments: - [{a,b} ...] Test. - -Options: - --help -h Show this help message and exit. -]], - parser:get_full_help("") - ) - end) - - it("choices + nargs=*", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "items", choices = { "a", "b" }, nargs = "*", help = "Test." }) - - assert.equal( - [[Usage: [{a,b} ...] [--help] - -Positional Arguments: - [{a,b} ...] Test. - -Options: - --help -h Show this help message and exit. -]], - parser:get_full_help("") - ) - end) - - it("choices + nargs=+ + append", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ - "items", - choices = { "a", "b" }, - nargs = "+", - action = "append", - help = "Test.", - }) - - assert.equal( - [[Usage: {a,b} [{a,b} ...] [--help] - -Positional Arguments: - {a,b} [{a,b} ...] Test. - -Options: - --help -h Show this help message and exit. -]], - parser:get_full_help("") - ) - end) - - it("choices + nargs=+", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "items", choices = { "a", "b" }, nargs = "+", help = "Test." }) - - assert.equal( - [[Usage: {a,b} [{a,b} ...] [--help] - -Positional Arguments: - {a,b} [{a,b} ...] Test. - -Options: - --help -h Show this help message and exit. -]], - parser:get_full_help("") - ) - end) - end) - - describe("no choices", function() - it("nargs=number", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "items", nargs = 3, help = "Test." }) - - assert.equal( - [[Usage: ITEMS ITEMS ITEMS [--help] - -Positional Arguments: - ITEMS ITEMS ITEMS Test. - -Options: - --help -h Show this help message and exit. -]], - parser:get_full_help("") - ) - end) - end) - - it("has value hint if details are empty", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "thing", help = "Test." }) - - assert.equal( - [[Usage: THING [--help] - -Positional Arguments: - THING Test. - -Options: - --help -h Show this help message and exit. -]], - parser:get_full_help("") - ) - end) - end) - end) - - describe("value_hint", function() - it("works with named arguments", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "--thing", help = "Test.", value_hint = "FILE_PATH" }) - - assert.equal( - [[Usage: [--thing FILE_PATH] [--help] - -Options: - --thing FILE_PATH Test. - --help -h Show this help message and exit. -]], - parser:get_full_help("") - ) - end) - - it("works with position arguments", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "thing", help = "Test.", value_hint = "FILE_PATH" }) - - assert.equal( - [[Usage: FILE_PATH [--help] - -Positional Arguments: - FILE_PATH Test. - -Options: - --help -h Show this help message and exit. -]], - parser:get_full_help("") - ) - end) - end) -end) - -describe("nargs", function() - it("flag + nargs + should parse into a string[]", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "--items", nargs = 2, help = "Test." }) - - local namespace = parser:parse_arguments("--items foo bar") - assert.same({ items = { "foo", "bar" } }, namespace) - end) - - it("flag + nargs=2 + append should parse into a string[][]", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "--items", action = "append", nargs = 2, help = "Test." }) - - local success, result = pcall(function() - parser:parse_arguments("--items foo bar fizz buzz") - end) - - assert.is_false(success) - assert.equal('Unexpected arguments "fizz, buzz".', result) - - local namespace = parser:parse_arguments("--items foo bar") - assert.same({ items = { { "foo", "bar" } } }, namespace) - end) - - it("flag + nargs=* + append should parse into a string[][]", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "--items", action = "append", nargs = "*", help = "Test." }) - - local namespace = parser:parse_arguments("--items foo bar fizz buzz") - assert.same({ items = { { "foo", "bar", "fizz", "buzz" } } }, namespace) - end) - - it("flag + nargs=+ + append should error if no argument is given", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "--items", nargs = "+", help = "Test." }) - - local success, result = pcall(function() - parser:get_completion("--items --another") - end) - - assert.is_false(success) - assert.equal('Parameter "--another" requires 1-or-more values. Got "0" values.', result) - end) - - it("flag + nargs=+ + append should parse into a string[][]", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "--items", action = "append", nargs = "+", help = "Test." }) - - local namespace = parser:parse_arguments("--items foo bar fizz buzz") - assert.same({ items = { { "foo", "bar", "fizz", "buzz" } } }, namespace) - end) - - it("flag + nargs=+ + append + count + type - #complex", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ - "--items", - action = "append", - count = "*", - nargs = "+", - type = tonumber, - help = "Test.", - }) - parser:add_parameter({ "--other", help = "Test." }) - - local namespace = parser:parse_arguments("--items 12 2 3 --other buzz --items 12 1 93") - assert.same({ items = { { 12, 2, 3 }, { 12, 1, 93 } }, other = "buzz" }, namespace) - end) - - it("position + nargs=2 + append should parse into a string[][]", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "items", action = "append", nargs = 2, help = "Test." }) - - local success, result = pcall(function() - parser:parse_arguments("foo bar fizz buzz") - end) - - assert.is_false(success) - assert.equal('Unexpected arguments "fizz, buzz".', result) - - local namespace = parser:parse_arguments("foo bar") - assert.same({ items = { { "foo", "bar" } } }, namespace) - end) - - it("position + nargs=2 + append + count should parse into a string[][]", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "items", action = "append", count = "*", nargs = 2, help = "Test." }) - - local namespace = parser:parse_arguments("foo bar fizz buzz") - assert.same({ items = { { "foo", "bar" }, { "fizz", "buzz" } } }, namespace) - end) - - it("position + nargs=2 + append + count + choices should parse into a string[][]", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ - "--items", - action = "append", - choices = { "1", "2", "3" }, - count = "*", - nargs = 2, - help = "Test.", - }) - - local namespace = parser:parse_arguments("--items 1 3") - assert.same({ items = { { "1", "3" } } }, namespace) - - local success, result = pcall(function() - parser:parse_arguments("--items 1 3 1") - end) - - assert.is_false(success) - assert.equal('Unexpected argument "1".', result) - - success, result = pcall(function() - parser:parse_arguments("--items 1 not_allowed") - end) - - assert.is_false(success) - assert.equal( - 'Parameter "--items" got invalid { "not_allowed" } value. Expected one of { "1", "2", "3" }.', - result - ) - end) - - it("position + nargs=* + append should parse into a string[][]", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "items", action = "append", nargs = "*", count = "*", help = "Test." }) - parser:add_parameter({ "--foo", help = "Test." }) - - local namespace = parser:parse_arguments("foo bar --foo thing fizz buzz") - assert.same({ foo = "thing", items = { { "foo", "bar" }, { "fizz", "buzz" } } }, namespace) - namespace = parser:parse_arguments("foo bar fizz buzz") - assert.same({ items = { { "foo", "bar", "fizz", "buzz" } } }, namespace) - end) - - it("position + nargs=+ + append should parse into a string[][]", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "items", action = "append", nargs = "+", help = "Test." }) - - local namespace = parser:parse_arguments("foo bar fizz buzz") - assert.same({ items = { { "foo", "bar", "fizz", "buzz" } } }, namespace) - end) -end) - -describe("set_defaults", function() - it("works with a basic execute function", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - local count = 0 - parser:set_defaults({ - execute = function() - count = count + 1 - end, - }) - - local namespace = parser:parse_arguments("") - - assert.equal(0, count) - - namespace.execute(namespace) - - assert.equal(1, count) - end) - - it("works with an #empty value", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:set_defaults({}) - - local namespace = parser:parse_arguments("") - - assert.same({}, namespace) - end) - - it("works with nested parsers where a parent also defines a default", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:set_defaults({ foo = "bar" }) - - local subparsers = parser:add_subparsers({ destination = "commands", help = "The available commands" }) - local creator = subparsers:add_parser({ name = "create", help = "Create stuff" }) - creator:add_parameter({ - names = { "--style", "-s" }, - default = "blah", - help = "If included, always run the action", - }) - creator:set_defaults({ foo = "fizz" }) - - assert.same({ foo = "bar" }, parser:parse_arguments("")) - assert.same({ foo = "fizz", style = "blah" }, parser:parse_arguments("create ")) - end) -end) - -describe("scenarios", function() - describe("subparsers", function() - it("errors if the last argument is a required subparser and it has required parameters", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - local subparsers = parser:add_subparsers({ - destination = "commands", - help = "All commands.", - required = true, - }) - - local say = subparsers:add_parser({ "say", help = "Test." }) - say:add_parameter({ "inner_thing", help = "Test." }) - - local success, result = pcall(function() - parser:parse_arguments("say") - end) - - assert.is_false(success) - assert.equal('Parameter "inner_thing" must be defined.', result) - end) - - it("passes if the last argument is a required subparser but its parameters are all optional", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - local subparsers = parser:add_subparsers({ - destination = "commands", - help = "All commands.", - required = true, - }) - - local say = subparsers:add_parser({ "say", help = "Test." }) - say:add_parameter({ "--thing", action = "store_false", help = "Test." }) - - assert.same({ thing = true }, parser:parse_arguments("say")) - end) - - it("passes if the last argument subparser is optional", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - local subparsers = parser:add_subparsers({ - destination = "commands", - help = "All commands.", - required = false, - }) - - local say = subparsers:add_parser({ "say", help = "Test." }) - say:add_parameter({ "inner_thing", help = "Test." }) - - local namespace = parser:parse_arguments("") - assert.same({}, namespace) - end) - end) - - it("works with a #basic flag argument", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ - names = { "--force", "-f" }, - action = "store_true", - destination = "force", - help = "If included, always run the action", - }) - - local namespace = parser:parse_arguments("-f") - assert.same({ force = true }, namespace) - - namespace = parser:parse_arguments("--force") - assert.same({ force = true }, namespace) - end) - - it("works with a #basic named argument", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ - names = { "--book" }, - help = "Write your book title here", - }) - - local namespace = parser:parse_arguments('--book="Goodnight Moon"') - assert.same({ book = "Goodnight Moon" }, namespace) - end) - - it("works with a #basic position argument", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ - names = { "book" }, - help = "Write your book title here", - }) - - local namespace = parser:parse_arguments('"Goodnight Moon"') - assert.same({ book = "Goodnight Moon" }, namespace) - end) - - it("works with repeated flags", function() - local parser = cmdparse.ParameterParser.new({ "goodnight-moon", help = "Prepare to sleep or sleep." }) - local subparsers = parser:add_subparsers({ - destination = "commands", - help = "All commands for goodnight-moon.", - required = true, - }) - - local sleep = subparsers:add_parser({ "sleep", help = "Sleep tight!" }) - sleep:add_parameter({ - "-z", - action = "count", - count = "*", - help = "The number of Zzz to print.", - nargs = 0, - }) - - sleep:set_execute(function(namespace) - assert.same(3, namespace.z) - end) - - local namespace = parser:parse_arguments("sleep -z -z -z") - - namespace.execute(namespace) - end) - - it("works with nested subcommands", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - - local subparsers = parser:add_subparsers({ destination = "commands", help = "The available commands" }) - local creator = subparsers:add_parser({ name = "create", help = "Create stuff" }) - - local creator_subparsers = - creator:add_subparsers({ destination = "creator", help = "Some options for creating" }) - local create_book = creator_subparsers:add_parser({ name = "book", help = "Create a book!" }) - - create_book:add_parameter({ name = "book", help = "The book name" }) - create_book:add_parameter({ - names = { "--author" }, - help = "The author of the book", - }) - - local namespace = parser:parse_arguments('create book "Goodnight Moon" --author="Margaret Wise Brown"') - assert.same({ author = "Margaret Wise Brown", book = "Goodnight Moon" }, namespace) - end) - - it("works with subcommands", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - local subparsers = parser:add_subparsers({ destination = "commands", help = "The available commands" }) - local creator = subparsers:add_parser({ name = "create", help = "Create stuff" }) - creator:add_parameter({ - names = { "book" }, - help = "Create a book!", - }) - creator:add_parameter({ - names = { "--author" }, - help = "The author of the book", - }) - - local namespace = parser:parse_arguments('create "Goodnight Moon" --author="Margaret Wise Brown"') - assert.same({ author = "Margaret Wise Brown", book = "Goodnight Moon" }, namespace) - end) -end) - -describe("type", function() - it("works with a known type function", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ - name = "foo", - type = function(value) - return value .. "tt" - end, - help = "Test.", - }) - - local namespace = parser:parse_arguments("12") - assert.same({ foo = "12tt" }, namespace) - end) - - it("works with a known type name", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ name = "foo", type = "number", help = "Test." }) - - local namespace = parser:parse_arguments("12") - assert.same({ foo = 12 }, namespace) - end) -end) - -describe("utf-8", function() - describe("get_completion", function() - it("works with flag parameters", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ "--ɧelp", action = "store_true", help = "Test." }) - - assert.same({ "--ɧelp" }, parser:get_completion("--ɧel")) - end) - - it("works with position parameters - 001", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ "ɧelp", help = "Test." }) - - assert.same({}, parser:get_completion("ɧel")) - end) - - it("works with position parameters - 002", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ "ɧelp", choices = { "a", "bb", "ccc" }, help = "Test." }) - - assert.same({ "ccc" }, parser:get_completion("cc")) - end) - - it("works with named parameters", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ "--ɧelp", choices = { "aa", "bb" }, help = "Test." }) - - assert.same({ "--ɧelp=" }, parser:get_completion("--ɧ")) - assert.same({ "--ɧelp=" }, parser:get_completion("--ɧel")) - assert.same({ "--ɧelp=" }, parser:get_completion("--ɧelp")) - assert.same({ "--ɧelp=aa", "--ɧelp=bb" }, parser:get_completion("--ɧelp=")) - end) - end) - - describe("parse_arguments", function() - it("works with flag parameters", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ "--ɧelp", action = "store_true", help = "Test." }) - - local namespace = parser:parse_arguments("--ɧelp") - assert.same({ ["ɧelp"] = true }, namespace) - end) - - it("works with position parameters", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ "ɧ", help = "Test." }) - - local namespace = parser:parse_arguments("thing") - assert.same({ ["ɧ"] = "thing" }, namespace) - end) - - it("works with named parameters", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ "--ɧelp", help = "Test." }) - - local namespace = parser:parse_arguments("--ɧelp=thing") - assert.same({ ["ɧelp"] = "thing" }, namespace) - end) - end) -end) - -describe("+ flags", function() - it("works with ++double flags", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ "++foo", action = "store_true", destination = "blah", help = "Test." }) - - local namespace = parser:parse_arguments("++foo") - assert.same({ blah = true }, namespace) - end) - - it("works with ++named=foo arguments", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ - "++foo", - destination = "blah", - type = function(value) - return value .. "tt" - end, - help = "Test.", - }) - - local namespace = parser:parse_arguments("++foo=12") - assert.same({ blah = "12tt" }, namespace) - end) - - it("works with +s (single) flags", function() - local parser = cmdparse.ParameterParser.new({ help = "Test" }) - parser:add_parameter({ - "+s", - destination = "blah", - type = function(value) - return value .. "tt" - end, - help = "Test.", - }) - - local namespace = parser:parse_arguments("+s 12") - assert.same({ blah = "12tt" }, namespace) - end) -end) - -describe("quotes", function() - describe("position arguments", function() - it("processes -1 as a #position argument instead of a flag argument", function() - local parser = cmdparse.ParameterParser.new({ help = "Test." }) - parser:add_parameter({ "value", type = tonumber, help = "Test." }) - - local namespace = parser:parse_arguments('"-1"') - assert.same({ value = -1 }, namespace) - - namespace = parser:parse_arguments("'-1'") - assert.same({ value = -1 }, namespace) - end) - end) -end) diff --git a/spec/plugin_template/configuration_spec.lua b/spec/plugin_template/configuration_spec.lua deleted file mode 100644 index f326ca5..0000000 --- a/spec/plugin_template/configuration_spec.lua +++ /dev/null @@ -1,235 +0,0 @@ ---- Make sure configuration health checks succeed or fail where they should. ---- ----@module 'plugin_template.configuration_spec' ---- - -local configuration_ = require("plugin_template._core.configuration") -local health = require("plugin_template.health") -local tabler = require("plugin_template._core.tabler") - -local mock_vim = require("test_utilities.mock_vim") - ---- Make sure `data`, whether undefined, defined, or partially defined, is broken. ---- ----@param data plugin_template.Configuration? The user customizations, if any. ----@param messages string[] All found, expected error messages. ---- -local function _assert_bad(data, messages) - data = configuration_.resolve_data(data) - local issues = health.get_issues(data) - - if vim.tbl_isempty(issues) then - error(string.format('Test did not fail. Configuration "%s is valid.', vim.inspect(data))) - - return - end - - assert.same(messages, issues) -end - ---- Make sure `data`, whether undefined, defined, or partially defined, works. ---- ----@param data plugin_template.Configuration? The user customizations, if any. ---- -local function _assert_good(data) - data = configuration_.resolve_data(data) - local issues = health.get_issues(data) - - if vim.tbl_isempty(issues) then - return - end - - error( - string.format( - 'Test did not succeed. Configuration "%s fails with "%s" issues.', - vim.inspect(data), - vim.inspect(issues) - ) - ) -end - -describe("default", function() - it("works with an #empty configuration", function() - _assert_good({}) - _assert_good() - end) - - it("works with a fully defined, custom configuration", function() - _assert_good({ - commands = { - goodnight_moon = { - read = { phrase = "The Origin of Consciousness in the Breakdown of the Bicameral Mind" }, - }, - hello_world = { say = { ["repeat"] = 12, style = "uppercase" } }, - }, - }) - end) - - it("works with the default configuration", function() - _assert_good({ - commands = { - goodnight_moon = { phrase = "A good book" }, - hello_world = { say = { ["repeat"] = 1, style = "lowercase" } }, - }, - }) - end) - - it("works with the partially-defined configuration", function() - _assert_good({ - commands = { - goodnight_moon = {}, - hello_world = {}, - }, - }) - end) -end) - ----@diagnostic disable: assign-type-mismatch ----@diagnostic disable: missing-fields -describe("bad configuration - cmdparse", function() - it("happens with a bad type for #cmdparse.auto_complete.display.help_flag", function() - _assert_bad({ - cmdparse = { auto_complete = { display = { help_flag = "aaa" } } }, - }, { "cmdparse.auto_complete.display.help_flag: expected boolean, got string" }) - - _assert_bad({ - cmdparse = { auto_complete = { display = 123 } }, - }, { "cmdparse.auto_complete.display: expected table, got number" }) - - _assert_bad({ cmdparse = { auto_complete = "bnb" } }, { "cmdparse.auto_complete: expected table, got string" }) - - _assert_bad({ cmdparse = 123 }, { "cmdparse: expected table, got number" }) - end) - - it("works with an #empty configuration", function() - _assert_good({ - cmdparse = { auto_complete = { display = { help_flag = true } } }, - }) - - _assert_good({ - cmdparse = { auto_complete = { display = { help_flag = false } } }, - }) - - _assert_good({ - cmdparse = { auto_complete = { display = { help_flag = nil } } }, - }) - end) -end) - -describe("bad configuration - commands", function() - it("happens with a bad type for #commands.goodnight_moon.phrase", function() - _assert_bad( - { commands = { goodnight_moon = { read = { phrase = 10 } } } }, - { "commands.goodnight_moon.read.phrase: expected string, got number" } - ) - end) - - it("happens with a bad type for #commands.hello_world.say.repeat", function() - _assert_bad( - { commands = { hello_world = { say = { ["repeat"] = "foo" } } } }, - { "commands.hello_world.say.repeat: expected a number (value must be 1-or-more), got foo" } - ) - end) - - it("happens with a bad value for #commands.hello_world.say.repeat", function() - _assert_bad( - { commands = { hello_world = { say = { ["repeat"] = -1 } } } }, - { "commands.hello_world.say.repeat: expected a number (value must be 1-or-more), got -1" } - ) - end) - - it("happens with a bad type for #commands.hello_world.say.style", function() - _assert_bad( - { commands = { hello_world = { say = { style = 123 } } } }, - { 'commands.hello_world.say.style: expected "lowercase" or "uppercase", got 123' } - ) - end) - - it("happens with a bad value for #commands.hello_world.say.style", function() - _assert_bad( - { commands = { hello_world = { say = { style = "bad_value" } } } }, - { 'commands.hello_world.say.style: expected "lowercase" or "uppercase", got bad_value' } - ) - end) -end) ----@diagnostic enable: assign-type-mismatch ----@diagnostic enable: missing-fields - ----@diagnostic disable: assign-type-mismatch -describe("bad configuration - logging", function() - it("happens with a bad value for #logging", function() - _assert_bad({ logging = false }, { 'logging: expected a table. e.g. { level = "info", ... }, got false' }) - end) - - it("happens with a bad value for #logging.level", function() - _assert_bad({ logging = { level = false } }, { - "logging.level: expected an enum. " - .. 'e.g. "trace" | "debug" | "info" | "warn" | "error" | "fatal", got false', - }) - - _assert_bad({ logging = { level = "does not exist" } }, { - "logging.level: expected an enum. " - .. 'e.g. "trace" | "debug" | "info" | "warn" | "error" | "fatal", got does not exist', - }) - end) - - it("happens with a bad value for #logging.use_console", function() - _assert_bad({ logging = { use_console = "aaa" } }, { "logging.use_console: expected a boolean, got aaa" }) - end) - - it("happens with a bad value for #logging.use_file", function() - _assert_bad({ logging = { use_file = "aaa" } }, { "logging.use_file: expected a boolean, got aaa" }) - end) -end) ----@diagnostic enable: assign-type-mismatch - ----@diagnostic disable: assign-type-mismatch -describe("health.check", function() - before_each(function() - mock_vim.mock_vim_health() - end) - after_each(mock_vim.reset_mocked_vim_health) - - it("works with an empty configuration", function() - health.check({}) - health.check() - - assert.same({}, mock_vim.get_vim_health_errors()) - end) - - it("shows all issues at once", function() - health.check({ - commands = { - goodnight_moon = { read = { phrase = 123 } }, - hello_world = { say = { ["repeat"] = "aaa", style = 789 } }, - }, - logging = { - level = false, - use_console = "aaa", - use_file = "fdas", - }, - tools = { - lualine = { - goodnight_moon = false, - hello_world = { text = 456 }, - }, - }, - }) - - local found = mock_vim.get_vim_health_errors() - local issues = tabler.get_slice(found, 1, #found - 1) - - assert.same({ - "commands.goodnight_moon.read.phrase: expected string, got number", - "commands.hello_world.say.repeat: expected a number (value must be 1-or-more), got aaa", - 'commands.hello_world.say.style: expected "lowercase" or "uppercase", got 789', - 'logging.level: expected an enum. e.g. "trace" | "debug" | "info" | "warn" | "error" | "fatal", got false', - "logging.use_console: expected a boolean, got aaa", - "logging.use_file: expected a boolean, got fdas", - 'tools.lualine.goodnight_moon: expected a table. e.g. { text="some text here" }, got false', - }, issues) - - vim.startswith(found[#found], 'tools.lualine.hello_world.text: expected a string. e.g. "some text here", got ') - end) -end) ----@diagnostic enable: assign-type-mismatch diff --git a/spec/plugin_template/configuration_tools_lualine_spec.lua b/spec/plugin_template/configuration_tools_lualine_spec.lua deleted file mode 100644 index a674dc7..0000000 --- a/spec/plugin_template/configuration_tools_lualine_spec.lua +++ /dev/null @@ -1,340 +0,0 @@ ---- Make sure configuration health checks for lua succeed or fail where they should. ---- ----@module 'plugin_template.configuration_tools_lualine_spec' ---- - -local configuration_ = require("plugin_template._core.configuration") -local health = require("plugin_template.health") - ----@diagnostic disable: assign-type-mismatch - ---- Make sure `data`, whether undefined, defined, or partially defined, is broken. ---- ----@param data plugin_template.Configuration? The user customizations, if any. ----@param messages string[] All found, expected error messages. ---- -local function _assert_bad(data, messages) - data = configuration_.resolve_data(data) - local issues = health.get_issues(data) - - if vim.tbl_isempty(issues) then - error(string.format('Test did not fail. Configuration "%s is valid.', vim.inspect(data))) - - return - end - - assert.same(messages, issues) -end - ---- Make sure `data`, whether undefined, defined, or partially defined, works. ---- ----@param data plugin_template.Configuration? The user customizations, if any. ---- -local function _assert_good(data) - data = configuration_.resolve_data(data) - local issues = health.get_issues(data) - - if vim.tbl_isempty(issues) then - return - end - - error( - string.format( - 'Test did not succeed. Configuration "%s fails with "%s" issues.', - vim.inspect(data), - vim.inspect(issues) - ) - ) -end - -describe("bad configuration - tools.lualine", function() - it("happens with a bad value for #tools.lualine.goodnight_moon", function() - _assert_bad( - { tools = { lualine = { goodnight_moon = true } } }, - { 'tools.lualine.goodnight_moon: expected a table. e.g. { text="some text here" }, got true' } - ) - end) - - it("happens with a bad value for #tools.lualine.goodnight_moon.color", function() - local data = configuration_.resolve_data({ tools = { lualine = { goodnight_moon = { color = false } } } }) - local issues = health.get_issues(data) - - assert.equal(1, #issues) - - assert.is_truthy( - vim.startswith( - issues[1], - "tools.lualine.goodnight_moon.color: expected a table. " - .. 'e.g. {fg="#000000", bg="#FFFFFF", gui="effect"}' - ) - ) - end) - - it("happens with a bad value for #tools.lualine.goodnight_moon.color - 002", function() - local data = - configuration_.resolve_data({ tools = { lualine = { goodnight_moon = { color = { bg = false } } } } }) - local issues = health.get_issues(data) - - assert.equal(1, #issues) - - assert.is_truthy( - vim.startswith( - issues[1], - "tools.lualine.goodnight_moon.color: expected a table. " - .. 'e.g. {fg="#000000", bg="#FFFFFF", gui="effect"}' - ) - ) - end) - - it("happens with a bad value for #tools.lualine.goodnight_moon.color - 003", function() - local data = - configuration_.resolve_data({ tools = { lualine = { goodnight_moon = { color = { fg = false } } } } }) - local issues = health.get_issues(data) - - assert.equal(1, #issues) - - assert.is_truthy( - vim.startswith( - issues[1], - "tools.lualine.goodnight_moon.color: expected a table. " - .. 'e.g. {fg="#000000", bg="#FFFFFF", gui="effect"}' - ) - ) - end) - - it("happens with a bad value for #tools.lualine.goodnight_moon.color - 004", function() - local data = - configuration_.resolve_data({ tools = { lualine = { goodnight_moon = { color = { gui = false } } } } }) - local issues = health.get_issues(data) - - assert.equal(1, #issues) - - assert.is_truthy( - vim.startswith( - issues[1], - "tools.lualine.goodnight_moon.color: expected a table. " - .. 'e.g. {fg="#000000", bg="#FFFFFF", gui="effect"}' - ) - ) - end) - - it("happens with a bad value for #tools.lualine.goodnight_moon.color - 005", function() - local data = - configuration_.resolve_data({ tools = { lualine = { goodnight_moon = { color = { bad_key = "ttt" } } } } }) - local issues = health.get_issues(data) - - assert.equal(1, #issues) - - assert.is_truthy( - vim.startswith( - issues[1], - "tools.lualine.goodnight_moon.color: expected a table. " - .. 'e.g. {fg="#000000", bg="#FFFFFF", gui="effect"}' - ) - ) - end) - - it("happens with a bad value for #tools.lualine.goodnight_moon.text", function() - local data = configuration_.resolve_data({ tools = { lualine = { goodnight_moon = { text = false } } } }) - local issues = health.get_issues(data) - - assert.equal(1, #issues) - - assert.is_truthy( - vim.startswith( - issues[1], - 'tools.lualine.goodnight_moon.text: expected a string. e.g. "some text here", got ' - ) - ) - end) - - it("happens with a bad value for #tools.lualine.hello_world", function() - _assert_bad( - { tools = { lualine = { hello_world = true } } }, - { 'tools.lualine.hello_world: expected a table. e.g. { text="some text here" }, got true' } - ) - end) - - it("happens with a bad value for #tools.lualine.hello_world.color - 001", function() - local data = configuration_.resolve_data({ tools = { lualine = { hello_world = { color = false } } } }) - local issues = health.get_issues(data) - - assert.equal(1, #issues) - - assert.is_truthy( - vim.startswith( - issues[1], - "tools.lualine.hello_world.color: expected a table. " - .. 'e.g. {fg="#000000", bg="#FFFFFF", gui="effect"}' - ) - ) - end) - - it("happens with a bad value for #tools.lualine.hello_world.color - 002", function() - local data = configuration_.resolve_data({ tools = { lualine = { hello_world = { color = { bg = false } } } } }) - local issues = health.get_issues(data) - - assert.equal(1, #issues) - - assert.is_truthy( - vim.startswith( - issues[1], - "tools.lualine.hello_world.color: expected a table. " - .. 'e.g. {fg="#000000", bg="#FFFFFF", gui="effect"}' - ) - ) - end) - - it("happens with a bad value for #tools.lualine.hello_world.color - 003", function() - local data = configuration_.resolve_data({ tools = { lualine = { hello_world = { color = { fg = false } } } } }) - local issues = health.get_issues(data) - - assert.equal(1, #issues) - - assert.is_truthy( - vim.startswith( - issues[1], - "tools.lualine.hello_world.color: expected a table. " - .. 'e.g. {fg="#000000", bg="#FFFFFF", gui="effect"}' - ) - ) - end) - - it("happens with a bad value for #tools.lualine.hello_world.color - 004", function() - local data = - configuration_.resolve_data({ tools = { lualine = { hello_world = { color = { gui = false } } } } }) - local issues = health.get_issues(data) - - assert.equal(1, #issues) - - assert.is_truthy( - vim.startswith( - issues[1], - "tools.lualine.hello_world.color: expected a table. " - .. 'e.g. {fg="#000000", bg="#FFFFFF", gui="effect"}' - ) - ) - end) - - it("happens with a bad value for #tools.lualine.hello_world.color - 005", function() - local data = - configuration_.resolve_data({ tools = { lualine = { hello_world = { color = { bad_key = "bbb" } } } } }) - local issues = health.get_issues(data) - - assert.equal(1, #issues) - - assert.is_truthy( - vim.startswith( - issues[1], - "tools.lualine.hello_world.color: expected a table. " - .. 'e.g. {fg="#000000", bg="#FFFFFF", gui="effect"}' - ) - ) - end) - - it("happens with a bad value for #tools.lualine.hello_world.text", function() - local data = configuration_.resolve_data({ tools = { lualine = { hello_world = { text = false } } } }) - local issues = health.get_issues(data) - - assert.equal(1, #issues) - - assert.is_truthy( - vim.startswith( - issues[1], - "tools.lualine.hello_world.text: " .. 'expected a string. e.g. "some text here", got ' - ) - ) - end) - - it("happens with a bad value for #tools.lualine", function() - _assert_bad( - { tools = { lualine = false } }, - { "tools.lualine: expected a table. e.g. { goodnight_moon = {...}, hello_world = {...} }, got false" } - ) - end) -end) - -describe("good configuration - tools.lualine", function() - it("example good values", function() - _assert_good({ - tools = { - lualine = { - goodnight_moon = { text = "ttt" }, - hello_world = { text = "yyyy" }, - }, - }, - }) - end) - - it("happens with a bad value for #tools.lualine.goodnight_moon.color", function() - local data = configuration_.resolve_data({ tools = { lualine = { goodnight_moon = { color = false } } } }) - local issues = health.get_issues(data) - - assert.equal(1, #issues) - - assert.is_truthy( - vim.startswith( - issues[1], - "tools.lualine.goodnight_moon.color: expected a table. " - .. 'e.g. {fg="#000000", bg="#FFFFFF", gui="effect"}' - ) - ) - end) - - it("happens with a bad value for #tools.lualine.goodnight_moon.text", function() - local data = configuration_.resolve_data({ tools = { lualine = { goodnight_moon = { text = false } } } }) - local issues = health.get_issues(data) - - assert.equal(1, #issues) - - assert.is_truthy( - vim.startswith( - issues[1], - 'tools.lualine.goodnight_moon.text: expected a string. e.g. "some text here", got ' - ) - ) - end) - - it("happens with a bad value for #tools.lualine.hello_world", function() - _assert_bad( - { tools = { lualine = { hello_world = true } } }, - { 'tools.lualine.hello_world: expected a table. e.g. { text="some text here" }, got true' } - ) - end) - - it("happens with a bad value for #tools.lualine.hello_world.color", function() - local data = configuration_.resolve_data({ tools = { lualine = { hello_world = { color = false } } } }) - local issues = health.get_issues(data) - - assert.equal(1, #issues) - - assert.is_truthy( - vim.startswith( - issues[1], - "tools.lualine.hello_world.color: expected a table. " - .. 'e.g. {fg="#000000", bg="#FFFFFF", gui="effect"}' - ) - ) - end) - - it("happens with a bad value for #tools.lualine.hello_world.text", function() - local data = configuration_.resolve_data({ tools = { lualine = { hello_world = { text = false } } } }) - local issues = health.get_issues(data) - - assert.equal(1, #issues) - - assert.is_truthy( - vim.startswith( - issues[1], - "tools.lualine.hello_world.text: " .. 'expected a string. e.g. "some text here", got ' - ) - ) - end) - - it("happens with a bad value for #tools.lualine", function() - _assert_bad( - { tools = { lualine = false } }, - { "tools.lualine: expected a table. e.g. { goodnight_moon = {...}, hello_world = {...} }, got false" } - ) - end) -end) diff --git a/spec/plugin_template/configuration_tools_telescope_spec.lua b/spec/plugin_template/configuration_tools_telescope_spec.lua deleted file mode 100644 index 3bb7725..0000000 --- a/spec/plugin_template/configuration_tools_telescope_spec.lua +++ /dev/null @@ -1,116 +0,0 @@ ---- Make sure configuration health checks for lua succeed or fail where they should. ---- ----@module 'plugin_template.configuration_tools_telescope_spec' ---- - -local configuration_ = require("plugin_template._core.configuration") -local health = require("plugin_template.health") - ---- Make sure `data`, whether undefined, defined, or partially defined, is broken. ---- ----@param data plugin_template.Configuration? The user customizations, if any. ----@param messages string[] All found, expected error messages. ---- -local function _assert_bad(data, messages) - data = configuration_.resolve_data(data) - local issues = health.get_issues(data) - - if vim.tbl_isempty(issues) then - error(string.format('Test did not fail. Configuration "%s is valid.', vim.inspect(data))) - - return - end - - assert.same(messages, issues) -end - ---- Make sure `data`, whether undefined, defined, or partially defined, works. ---- ----@param data plugin_template.Configuration? The user customizations, if any. ---- -local function _assert_good(data) - data = configuration_.resolve_data(data) - local issues = health.get_issues(data) - - if vim.tbl_isempty(issues) then - return - end - - error( - string.format( - 'Test did not succeed. Configuration "%s fails with "%s" issues.', - vim.inspect(data), - vim.inspect(issues) - ) - ) -end - -describe("bad configuration - #tools.telescope", function() - it("happens with a bad type for #tools.telescope", function() - _assert_bad( - { tools = { telescope = true } }, - { "tools.telescope: expected a table. e.g. { goodnight_moon = {...}, hello_world = {...}}, got true" } - ) - end) - - it("happens with a bad type for #tools.telescope.goodnight_moon", function() - _assert_bad( - { tools = { telescope = { goodnight_moon = true } } }, - { 'tools.telescope.goodnight_moon: expected a table. e.g. { {"Book", "Author"} }, got true' } - ) - end) - - it("happens with a bad value for #tools.telescope.goodnight_moon", function() - local data = - configuration_.resolve_data({ tools = { telescope = { goodnight_moon = { { true, "asdfasd" } } } } }) - local issues = health.get_issues(data) - - assert.is_truthy( - vim.startswith( - issues[1], - "tools.telescope.goodnight_moon: expected a table. " .. 'e.g. { {"Book", "Author"} }, ' - ) - ) - end) - - it("happens with a bad type for #tools.telescope.hello_world", function() - _assert_bad( - { tools = { telescope = { hello_world = true } } }, - { 'tools.telescope.hello_world: expected a table. e.g. { "Hello", "Hi", ...} }, got true' } - ) - end) - - it("happens with a bad value for #tools.telescope.hello_world", function() - local data = configuration_.resolve_data({ tools = { telescope = { hello_world = { true } } } }) - local issues = health.get_issues(data) - - assert.is_truthy( - vim.startswith( - issues[1], - "tools.telescope.hello_world: expected a table. e.g. " .. '{ "Hello", "Hi", ...} }, ' - ) - ) - end) -end) - -describe("good configuration - #tools.telescope", function() - it("works with a default #tools.telescope", function() - _assert_good({ tools = { telescope = {} } }) - end) - - it("works with an empty #tools.telescope.goodnight_moon", function() - _assert_good({ tools = { telescope = { goodnight_moon = {} } } }) - end) - - it("works with a valid #tools.telescope.goodnight_moon", function() - _assert_good({ tools = { telescope = { goodnight_moon = { { "Foo", "Bar" } } } } }) - end) - - it("works with an empty #tools.telescope.hello_world", function() - _assert_good({ tools = { telescope = { hello_world = {} } } }) - end) - - it("works with a valid #tools.telescope.hello_world", function() - _assert_good({ tools = { telescope = { hello_world = { "Foo", "Bar" } } } }) - end) -end) diff --git a/spec/plugin_template/lualine_spec.lua b/spec/plugin_template/lualine_spec.lua deleted file mode 100644 index 5b0af5a..0000000 --- a/spec/plugin_template/lualine_spec.lua +++ /dev/null @@ -1,189 +0,0 @@ ---- Make that the Lualine component works as expected. ---- ----@module 'plugin_template.lualine_spec' ---- - -local highlight = require("lualine.highlight") -local loader = require("lualine.utils.loader") -local lualine_plugin_template = require("lualine.components.plugin_template") -local mock_test = require("test_utilities.mock_test") -local plugin_template = require("plugin_template") - ----@return table # The generated Lualine component. -local function _make_component() - return lualine_plugin_template({ self = { section = "y" } }) -end - ---- Delete all existing highlight groups and recreate them (so we can keep tests clean). -local function _refresh_highlight_groups() - local theme = loader.load_theme("gruvbox") - highlight.create_highlight_groups(theme) -end - ---- Enable lualine so we can create lualine component(s) and other various tasks. -local function _setup_lualine() - lualine_plugin_template.PREVIOUS_COMMAND = nil - - mock_test.silence_all_internal_prints() - - _refresh_highlight_groups() -end - -describe("default", function() - before_each(_setup_lualine) - after_each(mock_test.reset_mocked_vim_inspect) - - it("displays nothing if no command has been run yet", function() - local component = _make_component() - - assert.is_nil(component:update_status()) - end) -end) - -describe("API calls", function() - before_each(_setup_lualine) - - it("works with #arbitrary-thing", function() - local component = _make_component() - - assert.is_nil(component:update_status()) - - plugin_template.run_arbitrary_thing() - - assert.equal("%#lualine_y_plugin_template_arbitrary_thing# Arbitrary Thing", component:update_status()) - end) - - it("works with #copy-logs", function() - local component = _make_component() - - assert.is_nil(component:update_status()) - - plugin_template.run_copy_logs() - - assert.equal("%#lualine_y_plugin_template_copy_logs#󰈔 Copy Logs", component:update_status()) - end) - - it("works with #goodnight-moon #count-sheep", function() - local component = _make_component() - - assert.is_nil(component:update_status()) - - plugin_template.run_goodnight_moon_count_sheep(10) - - assert.equal("%#lualine_y_plugin_template_goodnight_moon#⏾ Goodnight moon", component:update_status()) - end) - - it("works with #goodnight-moon #read", function() - local component = _make_component() - - assert.is_nil(component:update_status()) - - plugin_template.run_goodnight_moon_read("a book") - - assert.equal("%#lualine_y_plugin_template_goodnight_moon#⏾ Goodnight moon", component:update_status()) - end) - - it("works with #goodnight-moon #sleep", function() - local component = _make_component() - - assert.is_nil(component:update_status()) - - plugin_template.run_goodnight_moon_sleep() - - assert.equal("%#lualine_y_plugin_template_goodnight_moon#⏾ Goodnight moon", component:update_status()) - end) - - it("works with #hello-world #say phrase", function() - local component = _make_component() - - assert.is_nil(component:update_status()) - - plugin_template.run_hello_world_say_phrase({ "A phrase!" }) - - assert.equal("%#lualine_y_plugin_template_hello_world# Hello, World!", component:update_status()) - end) - - it("works with #hello-world #say word", function() - local component = _make_component() - - assert.is_nil(component:update_status()) - - plugin_template.run_hello_world_say_word("some_text_here") - - assert.equal("%#lualine_y_plugin_template_hello_world# Hello, World!", component:update_status()) - end) -end) - -describe("Command calls", function() - before_each(_setup_lualine) - - it("works with #arbitrary-thing", function() - local component = _make_component() - - assert.is_nil(component:update_status()) - - vim.cmd([[PluginTemplate arbitrary-thing]]) - - assert.equal("%#lualine_y_plugin_template_arbitrary_thing# Arbitrary Thing", component:update_status()) - end) - - it("works with #copy-logs", function() - local component = _make_component() - - assert.is_nil(component:update_status()) - - vim.cmd([[PluginTemplate copy-logs]]) - - assert.equal("%#lualine_y_plugin_template_copy_logs#󰈔 Copy Logs", component:update_status()) - end) - - it("works with #goodnight-moon #count-sheep", function() - local component = _make_component() - - assert.is_nil(component:update_status()) - - vim.cmd([[PluginTemplate goodnight-moon count-sheep 10]]) - - assert.equal("%#lualine_y_plugin_template_goodnight_moon#⏾ Goodnight moon", component:update_status()) - end) - - it("works with #goodnight-moon #read", function() - local component = _make_component() - - assert.is_nil(component:update_status()) - - vim.cmd([[PluginTemplate goodnight-moon read "a book"]]) - - assert.equal("%#lualine_y_plugin_template_goodnight_moon#⏾ Goodnight moon", component:update_status()) - end) - - it("works with #goodnight-moon #sleep", function() - local component = _make_component() - - assert.is_nil(component:update_status()) - - vim.cmd([[PluginTemplate goodnight-moon sleep -zzz]]) - - assert.equal("%#lualine_y_plugin_template_goodnight_moon#⏾ Goodnight moon", component:update_status()) - end) - - it("works with #hello-world #say phrase", function() - local component = _make_component() - - assert.is_nil(component:update_status()) - - vim.cmd([[PluginTemplate hello-world say phrase "something more text"]]) - - assert.equal("%#lualine_y_plugin_template_hello_world# Hello, World!", component:update_status()) - end) - - it("works with #hello-world #say word", function() - local component = _make_component() - - assert.is_nil(component:update_status()) - - vim.cmd([[PluginTemplate hello-world say word some_text_here]]) - - assert.equal("%#lualine_y_plugin_template_hello_world# Hello, World!", component:update_status()) - end) -end) diff --git a/spec/plugin_template/manual_cmdparse_spec.lua b/spec/plugin_template/manual_cmdparse_spec.lua deleted file mode 100644 index 6c52ffa..0000000 --- a/spec/plugin_template/manual_cmdparse_spec.lua +++ /dev/null @@ -1,55 +0,0 @@ ---- Make sure that the old, manual way of setting up cmdparse also works. ---- ----@module 'plugin_template.manual_cmdparse_spec' ---- - -local cli_subcommand = require("plugin_template._cli.cli_subcommand") - -describe("complete", function() - it("completes with a basic parser", function() - local completer = cli_subcommand.make_subcommand_completer("Foo", { - some_subcommand = { - complete = function(data) - return data.input - end, - }, - }) - - assert.same({ "some_subcommand" }, completer("some_sub", "Foo some_subcommand lines here")) - end) -end) - -describe("run", function() - it("parses and runs some fake code", function() - local arguments = nil - - local triager = cli_subcommand.make_subcommand_triager({ - some_subcommand = { - run = function(data) - arguments = data.input.arguments - end, - }, - }) - - triager({ fargs = { "some_subcommand" }, args = "some_subcommand more stuff" }) - - assert.same({ - { - argument_type = "__position", - range = { - end_column = 4, - start_column = 1, - }, - value = "more", - }, - { - argument_type = "__position", - range = { - end_column = 10, - start_column = 6, - }, - value = "stuff", - }, - }, arguments) - end) -end) diff --git a/spec/plugin_template/plugin_template_spec.lua b/spec/plugin_template/plugin_template_spec.lua deleted file mode 100644 index 950751a..0000000 --- a/spec/plugin_template/plugin_template_spec.lua +++ /dev/null @@ -1,433 +0,0 @@ ---- Basic API tests. ---- ---- This module is pretty specific to this plugin template so you'll most ---- likely want to delete or heavily modify this file. But it does give a quick ---- look how to mock a test and some things you can do with Neovim/busted. ---- ----@module 'plugin_template.plugin_template_spec' ---- - -local configuration = require("plugin_template._core.configuration") -local copy_logs_runner = require("plugin_template._commands.copy_logs.runner") -local plugin_template = require("plugin_template") -local vlog = require("plugin_template._vendors.vlog") - ----@class plugin_template.Configuration -local _CONFIGURATION_DATA - ----@type string[] -local _DATA = {} - -local _ORIGINAL_COPY_LOGS_READ_FILE = copy_logs_runner._read_file -local _ORIGINAL_NOTIFY = vim.notify - ---- Keep track of text that would have been printed. Save it to a variable instead. ---- ----@param data string Some text to print to stdout. ---- -local function _save_prints(data) - table.insert(_DATA, data) -end - ---- Mock all functions / states before a unittest runs (call this before each test). -local function _initialize_prints() - vim.notify = _save_prints -end - ---- Watch the `copy-logs` API command for function calls. -local function _initialize_copy_log() - local function _save_path(path) - _DATA = { path } - end - - _CONFIGURATION_DATA = vim.deepcopy(configuration.DATA) - copy_logs_runner._read_file = _save_path -end - ---- Write a log file so we can query its later later. -local function _make_fake_log(path) - local file = io.open(path, "w") -- Open the file in write mode - - if not file then - error(string.format('Path "%s" is not writable.', path)) - end - - file:write("aaa\nbbb\nccc\n") - file:close() -end - ---- Remove the "watcher" that we added during unittesting. -local function _reset_copy_log() - copy_logs_runner._read_file = _ORIGINAL_COPY_LOGS_READ_FILE - - configuration.DATA = _CONFIGURATION_DATA - _DATA = {} -end - ---- Reset all functions / states to their previous settings before the test had run. -local function _reset_prints() - vim.notify = _ORIGINAL_NOTIFY - _DATA = {} -end - ---- Wait for our (mocked) unittest variable to get some data back. ---- ----@param timeout number? ---- The milliseconds to wait before continuing. If the timeout is exceeded ---- then we stop waiting for all of the functions to call. ---- -local function _wait_for_result(timeout) - if timeout == nil then - timeout = 1000 - end - - vim.wait(timeout, function() - return not vim.tbl_isempty(_DATA) - end) -end - -describe("arbitrary-thing API", function() - before_each(_initialize_prints) - after_each(_reset_prints) - - it("runs #arbitrary-thing with #default arguments", function() - plugin_template.run_arbitrary_thing({}) - - assert.same({ "" }, _DATA) - end) - - it("runs #arbitrary-thing with arguments", function() - plugin_template.run_arbitrary_thing({ "v", "t" }) - - assert.same({ "v, t" }, _DATA) - end) -end) - -describe("arbitrary-thing commands", function() - before_each(_initialize_prints) - after_each(_reset_prints) - - it("runs #arbitrary-thing with #default arguments", function() - vim.cmd([[PluginTemplate arbitrary-thing]]) - assert.same({ "" }, _DATA) - end) - - it("runs #arbitrary-thing with arguments", function() - vim.cmd([[PluginTemplate arbitrary-thing -vvv -abc -f]]) - - assert.same({ "-v, -v, -v, -a, -b, -c, -f" }, _DATA) - end) -end) - -describe("copy logs API", function() - before_each(_initialize_copy_log) - after_each(_reset_copy_log) - - it("runs with an explicit file path", function() - local path = vim.fn.tempname() .. "copy_logs_test.log" - _make_fake_log(path) - - plugin_template.run_copy_logs(path) - _wait_for_result() - - assert.same({ path }, _DATA) - end) - - it("runs with default arguments", function() - local expected = vim.fn.tempname() .. "copy_logs_default_test.log" - configuration.DATA.logging.output_path = expected - vlog.new(configuration.DATA.logging or {}, true) - _make_fake_log(expected) - - plugin_template.run_copy_logs() - _wait_for_result() - - assert.same({ expected }, _DATA) - end) -end) - -describe("copy logs command", function() - before_each(_initialize_copy_log) - after_each(_reset_copy_log) - - it("runs with an explicit file path", function() - local path = vim.fn.tempname() .. "copy_logs_test.log" - _make_fake_log(path) - - vim.cmd(string.format('PluginTemplate copy-logs "%s"', path)) - _wait_for_result() - - assert.same({ path }, _DATA) - end) - - it("runs with default arguments", function() - local expected = vim.fn.tempname() .. "copy_logs_default_test.log" - configuration.DATA.logging.output_path = expected - vlog.new(configuration.DATA.logging or {}, true) - _make_fake_log(expected) - - vim.cmd([[PluginTemplate copy-logs]]) - - _wait_for_result() - - assert.same({ expected }, _DATA) - end) -end) - -describe("hello world API - say phrase/word", function() - before_each(_initialize_prints) - after_each(_reset_prints) - - it("runs #hello-world with default `say phrase` arguments - 001", function() - plugin_template.run_hello_world_say_phrase({ "" }) - - assert.same({ "No phrase was given" }, _DATA) - end) - - it("runs #hello-world with default `say phrase` arguments - 002", function() - plugin_template.run_hello_world_say_phrase({}) - - assert.same({ "No phrase was given" }, _DATA) - end) - - it("runs #hello-world with default `say word` arguments - 001", function() - plugin_template.run_hello_world_say_word("") - - assert.same({ "No word was given" }, _DATA) - end) - - it("runs #hello-world say phrase - with all of its arguments", function() - plugin_template.run_hello_world_say_phrase({ "Hello,", "World!" }, 2, "lowercase") - - assert.same({ "Saying phrase", "hello, world!", "hello, world!" }, _DATA) - end) - - it("runs #hello-world say word - with all of its arguments", function() - plugin_template.run_hello_world_say_phrase({ "Hi" }, 2, "uppercase") - - assert.same({ "Saying phrase", "HI", "HI" }, _DATA) - end) -end) - -describe("hello world commands - say phrase/word", function() - before_each(_initialize_prints) - after_each(_reset_prints) - - it("runs #hello-world with default arguments", function() - vim.cmd([[PluginTemplate hello-world say phrase]]) - - assert.same({ "No phrase was given" }, _DATA) - end) - - it("runs #hello-world say phrase - with all of its arguments", function() - vim.cmd([[PluginTemplate hello-world say phrase "Hello, World!" --repeat=2 --style=lowercase]]) - - assert.same({ "Saying phrase", "hello, world!", "hello, world!" }, _DATA) - end) - - it("runs #hello-world say word - with all of its arguments", function() - vim.cmd([[PluginTemplate hello-world say word "Hi" --repeat=2 --style=uppercase]]) - - assert.same({ "Saying word", "HI", "HI" }, _DATA) - end) -end) - -describe("goodnight-moon API", function() - before_each(_initialize_prints) - after_each(_reset_prints) - - it("runs #goodnight-moon #count-sheep with all of its arguments", function() - plugin_template.run_goodnight_moon_count_sheep(3) - - assert.same({ "1 Sheep", "2 Sheep", "3 Sheep" }, _DATA) - end) - - it("runs #goodnight-moon #read with all of its arguments", function() - plugin_template.run_goodnight_moon_read("a good book") - - assert.same({ "a good book: it is a book" }, _DATA) - end) - - it("runs #goodnight-moon #sleep with all of its arguments", function() - plugin_template.run_goodnight_moon_sleep(3) - - assert.same({ "Zzz", "Zzz", "Zzz" }, _DATA) - end) -end) - -describe("goodnight-moon commands", function() - before_each(_initialize_prints) - after_each(_reset_prints) - - it("runs #goodnight-moon #count-sheep with all of its arguments", function() - vim.cmd([[PluginTemplate goodnight-moon count-sheep 3]]) - - assert.same({ "1 Sheep", "2 Sheep", "3 Sheep" }, _DATA) - end) - - it("runs #goodnight-moon #read with all of its arguments", function() - vim.cmd([[PluginTemplate goodnight-moon read "a good book"]]) - - assert.same({ "a good book: it is a book" }, _DATA) - end) - - it("runs #goodnight-moon #sleep with all of its arguments", function() - vim.cmd([[PluginTemplate goodnight-moon sleep -z -z -z]]) - - assert.same({ "Zzz", "Zzz", "Zzz" }, _DATA) - end) -end) - -describe("help API", function() - before_each(_initialize_prints) - after_each(_reset_prints) - - describe("fallback help", function() - it("works on a nested subparser - 003", function() - vim.cmd("PluginTemplate hello-world say") - assert.same({ "Usage: {say} {phrase,word} [--help]" }, _DATA) - end) - end) - - describe("--help flag", function() - it("works on the base parser", function() - vim.cmd("PluginTemplate --help") - - assert.same({ - [[ -Usage: {PluginTemplate} {arbitrary-thing,copy-logs,goodnight-moon,hello-world} [--help] - -Commands: - arbitrary-thing Prepare to sleep or sleep. - copy-logs Get debug logs for PluginTemplate. - goodnight-moon Prepare to sleep or sleep. - hello-world Print hello to the user. - -Options: - --help -h Show this help message and exit. -]], - }, _DATA) - end) - - it("works on a nested subparser - 001", function() - vim.cmd([[PluginTemplate hello-world say --help]]) - - assert.same({ - [[ -Usage: {say} {phrase,word} [--help] - -Commands: - phrase Print everything that the user types. - word Print only the first word that the user types. - -Options: - --help -h Show this help message and exit. -]], - }, _DATA) - end) - - it("works on a nested subparser - 002", function() - vim.cmd([[PluginTemplate hello-world say phrase --help]]) - - assert.same({ - [[ -Usage: {phrase} PHRASES* [--repeat {1,2,3,4,5}] [--style {lowercase,uppercase}] [--help] - -Positional Arguments: - PHRASES* All of the text to print. - -Options: - --repeat -r {1,2,3,4,5} Print to the user X number of times (default=1). - --style -s {lowercase,uppercase} lowercase makes WORD into word. uppercase does the reverse. - --help -h Show this help message and exit. -]], - }, _DATA) - end) - - it("works on a nested subparser - 003", function() - vim.cmd([[PluginTemplate hello-world say word --help]]) - - assert.same({ - [[ -Usage: {word} WORD [--repeat {1,2,3,4,5}] [--style {lowercase,uppercase}] [--help] - -Positional Arguments: - WORD The word to print. - -Options: - --repeat -r {1,2,3,4,5} Print to the user X number of times (default=1). - --style -s {lowercase,uppercase} lowercase makes WORD into word. uppercase does the reverse. - --help -h Show this help message and exit. -]], - }, _DATA) - end) - - it("works on the subparsers - 001", function() - vim.cmd([[PluginTemplate arbitrary-thing --help]]) - - assert.same({ - [[ -Usage: {arbitrary-thing} [-a] [-b] [-c] [-f] [-v] [--help] - -Options: - -a The -a flag. - -b The -b flag. - -c The -c flag. - -f * The -f flag. - -v * The -v flag. - --help -h Show this help message and exit. -]], - }, _DATA) - end) - - it("works on the subparsers - 002", function() - vim.cmd([[PluginTemplate copy-logs --help]]) - - assert.same({ - [[ -Usage: {copy-logs} LOG [--help] - -Positional Arguments: - LOG The path on-disk to look for logs. If no path is given, a fallback log path is used instead. - -Options: - --help -h Show this help message and exit. -]], - }, _DATA) - end) - - it("works on the subparsers - 003", function() - vim.cmd([[PluginTemplate goodnight-moon --help]]) - - assert.same({ - [[ -Usage: {goodnight-moon} {count-sheep,read,sleep} [--help] - -Commands: - count-sheep Count some sheep to help you sleep. - read Read a book in bed. - sleep Sleep tight! - -Options: - --help -h Show this help message and exit. -]], - }, _DATA) - end) - - it("works on the subparsers - 004", function() - vim.cmd([[PluginTemplate hello-world --help]]) - - assert.same({ - [[ -Usage: {hello-world} {say} [--help] - -Commands: - say Print something to the user. - -Options: - --help -h Show this help message and exit. -]], - }, _DATA) - end) - end) -end) diff --git a/spec/plugin_template/telescope_spec.lua b/spec/plugin_template/telescope_spec.lua deleted file mode 100644 index 237cbf1..0000000 --- a/spec/plugin_template/telescope_spec.lua +++ /dev/null @@ -1,153 +0,0 @@ ---- Make sure the Telescope integration works as expected. ---- ----@module 'plugin_template.telescope_spec' ---- - -local mock_test = require("test_utilities.mock_test") -local plugin_template = require("telescope._extensions.plugin_template") -local runner = require("telescope._extensions.plugin_template.runner") -local telescope_actions = require("telescope.actions") -local telescope_actions_state = require("telescope.actions.state") - -local _ORIGINAL_GET_SELECTION_FUNCTION = runner.get_selection -local _RESULT = nil - ---- Tempoarily wrap `runner.get_selection` so we can use it for unittests. -local function _mock_get_selection() - local function _mock(caller) - ---@diagnostic disable-next-line: duplicate-set-field - runner.get_selection = function(...) - local selection = caller(...) - _RESULT = selection - - return selection - end - end - - _mock(runner.get_selection) -end - ---- Reset the unittests back to their original values. -local function _restore_get_selection() - runner.get_selection = _ORIGINAL_GET_SELECTION_FUNCTION - - _RESULT = nil -end - ---- Run Neovim's event loop so that all currently-scheduled function can run. ---- ---- If you or some other API call `vim.schedule` / `vim.schedule_wrap` and want ---- to make sure that function runs, call this function. ---- ----@param timeout number? ---- The milliseconds to wait before continuing. If the timeout is exceeded ---- then we stop waiting for all of the functions to call. ---- -local function _wait_for_picker_to_initialize(timeout) - if timeout == nil then - timeout = 1000 - end - - local initialized = false - - vim.schedule(function() - initialized = true - end) - - vim.wait(timeout, function() - return initialized - end) -end - ---- Wait for our (mocked) unittest variable to get some data back. ---- ----@param timeout number? ---- The milliseconds to wait before continuing. If the timeout is exceeded ---- then we stop waiting for all of the functions to call. ---- -local function _wait_for_result(timeout) - if timeout == nil then - timeout = 1000 - end - - vim.wait(timeout, function() - return _RESULT ~= nil - end) -end - ---- Create a Telescope picker for `command` and get the created "prompt" buffer back. ---- ----@param command string ---- A Telescope sub-command. e.g. If the command was `:Telescope ---- plugin_template foo` then this function would require `"foo"`. ---- -local function _make_telescope_picker(command) - plugin_template.exports[command]() - - _wait_for_picker_to_initialize() - - return vim.api.nvim_get_current_buf() -end - ---- Setup all mocks and get ready for the unittest to run. -local function _initialize_all() - _mock_get_selection() - mock_test.silence_all_internal_prints() -end - -describe("telescope goodnight-moon", function() - before_each(_initialize_all) - after_each(_restore_get_selection) - - it("selects a book", function() - local buffer = _make_telescope_picker("goodnight-moon") - telescope_actions.select_default(buffer) - _wait_for_result() - - assert.same({ "Buzz Bee - Cool Person" }, _RESULT) - end) - - it("selects 2+ books", function() - local buffer = _make_telescope_picker("goodnight-moon") - local picker = telescope_actions_state.get_current_picker(buffer) - picker:set_selection(picker.max_results - picker.manager:num_results()) - telescope_actions.move_selection_previous(buffer) - picker:toggle_selection(picker:get_selection_row()) - telescope_actions.move_selection_previous(buffer) - picker:toggle_selection(picker:get_selection_row()) - telescope_actions.select_default(buffer) - _wait_for_result() - - assert.same({ - "Buzz Bee - Cool Person", - "Fizz Drink - Some Name", - }, _RESULT) - end) -end) - -describe("telescope hello-world", function() - before_each(_initialize_all) - after_each(_restore_get_selection) - - it("selects a phrase", function() - local buffer = _make_telescope_picker("hello-world") - telescope_actions.select_default(buffer) - _wait_for_result() - - assert.same({ "What's up, doc?" }, _RESULT) - end) - - it("selects 2+ phrases", function() - local buffer = _make_telescope_picker("hello-world") - local picker = telescope_actions_state.get_current_picker(buffer) - picker:set_selection(picker.max_results - picker.manager:num_results()) - telescope_actions.move_selection_previous(buffer) - picker:toggle_selection(picker:get_selection_row()) - telescope_actions.move_selection_previous(buffer) - picker:toggle_selection(picker:get_selection_row()) - telescope_actions.select_default(buffer) - _wait_for_result() - - assert.same({ "What's up, doc?", "Hello, Sailor!" }, _RESULT) - end) -end) diff --git a/spec/test_utilities/mock_test.lua b/spec/test_utilities/mock_test.lua deleted file mode 100644 index ff27b89..0000000 --- a/spec/test_utilities/mock_test.lua +++ /dev/null @@ -1,41 +0,0 @@ ---- Temporarily track when certain built-in Vim commands are called. ---- ----@module 'test_utilities.mock_test' ---- - -local M = {} - -local _ORIGINAL_INSPECT = vim.inspect -local _DATA = nil - ---- Temporarily track vim.inspect calls. ---- ----@param data any The passed value(s). ---- -local function _set_inspection_data(data) - _DATA = data -end - ----@return any # Get all saved vim.inspect calls. -function M.get_inspection_data() - return _DATA -end - ---- Temporarily track vim.inspect calls. -function M.mock_vim_inspect() - _ORIGINAL_INSPECT = vim.inspect - vim.inspect = _set_inspection_data -end - ---- Restore the previous vim.inspect function. -function M.reset_mocked_vim_inspect() - vim.inspect = _ORIGINAL_INSPECT -end - ---- Make it so no existing API calls or commands print text. -function M.silence_all_internal_prints() - ---@diagnostic disable-next-line: duplicate-set-field - vim.notify = function(...) end -- luacheck: ignore 212 -end - -return M diff --git a/spec/test_utilities/mock_vim.lua b/spec/test_utilities/mock_vim.lua deleted file mode 100644 index beda563..0000000 --- a/spec/test_utilities/mock_vim.lua +++ /dev/null @@ -1,31 +0,0 @@ ---- Temporarily track when certain built-in Vim commands are called. ---- ----@module 'test_utilities.mock_vim' ---- - -local M = {} - -local _ERROR_MESSAGES = {} -local _ORIGINAL_HEALTH_ERROR = vim.health.error - ----@return string[] # Get all saved vim.health.error calls. -function M.get_vim_health_errors() - return _ERROR_MESSAGES -end - ---- Temporarily track vim.health calls. -function M.mock_vim_health() - local function _save_health_error_message(message) - table.insert(_ERROR_MESSAGES, message) - end - - vim.health.error = _save_health_error_message -end - ---- Restore the previous vim.health function. -function M.reset_mocked_vim_health() - vim.health.error = _ORIGINAL_HEALTH_ERROR - _ERROR_MESSAGES = {} -end - -return M