From d05247548243ebffd5093022bfa0c02e5cf647d2 Mon Sep 17 00:00:00 2001 From: b4mbus Date: Tue, 30 Jul 2024 19:04:01 +0200 Subject: [PATCH 01/16] docs(spec.technical): add technical spec for modules --- docs/spec/0.1/technical.md | 86 +++++++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/docs/spec/0.1/technical.md b/docs/spec/0.1/technical.md index 0503f80..90c846d 100644 --- a/docs/spec/0.1/technical.md +++ b/docs/spec/0.1/technical.md @@ -1,6 +1,27 @@ # Technical spec of cpp-tools.nvim -## Automatic test runner +# Table of Contents + + + +* [Technical spec of cpp-tools.nvim](#technical-spec-of-cpp-toolsnvim) +* [Automatic test runner](#automatic-test-runner) +* [The module system](#the-module-system) + * [Conventions](#conventions) + * [Types](#types) + * [Structure](#structure) +* [luaKITTENS](#luakittens) + + + +# Technical spec of cpp-tools.nvim + +> [!WARNING] +> This is just a draft, some implementation is done already, but I may encounter +> some issues and technical limitations later which may alter whatever is written here. + + +# Automatic test runner Each module (lua module, not cpp-tools module) can define its tests by exposing a `__test` function. The function should contain ordinary busted tests. @@ -15,3 +36,66 @@ It's called by busted (see `/.busted`'s `_all.pattern` key) and receives i It then scans all lua files in `lua`, evaluates them and collects all that returned a table with the `__test` key, then calls each of the test functions with the context. + +# The module system + +TODO: A `CppToolsProject` event, which will fire if neovim is opened up in a project directory. +This is useful to already provide language intelligence features even if we're not in a c/c++ file yet. +E.g. starting the lsp, to be able to use find references. + +TODO: Possibly another implicit fields which allows disabling certain events, or just the project event. + +## Conventions + +1. The types are denoted after `:` and use the [luaKITTENS annotation system](#luaKITTENS). +3. `^` denotes a required field, dependent on some condition (e.g. the `required` - `default` relation). + +## Types + +1. The `kitty` type refers to a `string` that's a valid `kitten`, that is a valid `luaKITTENS` annotation. +1. The `EventName` type refers to a `string` that's a valid neovim event name, see `:h events` + +## Structure + +The module system exists to be able to easily define all cpp-tools modules and ensure consistency and compatibility +between all of them. + +Each module has its own namespace of form `('cpp-tools.%s'):format(name)` created. It's later used for events. + +Each valid cpp-tools module has the following structure: +- **name**: `string` - The name of the module. (This is used for documentation generation as well) +- **description**: `string` - Description of the module (This is used for documentation generation as well) +- **config**: `{ [string]: ConfigEntry }?` - Configuration definition. `ConfigEntry` is defined as follows: + - **type**: `kitten` - A luaKITTENS annotation denoting the type of this config field. + - **validate**: `(fn | []any)?` - Either a function that takes in a value and checks if it's valid for the given field + or an array of valid values. A luaKITTENS type validation happens before this anyway. + - **required**^: `bool?` - Is this config field mandatory? (This field is not required if `default` is set, it implies `required = false`) + - **default**^: `any?` - A default value for this config field. Will error if `required = true`. (This field is not required if `required` is set to `false`) + + - **example**^: `string` - An example of this field's usage. If `default` is set and this is not set, it will stringify the `default`'s value and use it instead. (This is used for documentation generation) + - **description**: `string` - The description for this config field. (This is used for documentation generation) + + Each config additionally has the following **implicit** fields + - **enable**: + - **type** - `bool` + - **required** - `false` + - **default** - `false` + - **description** - `('Enables the %s module'):format(name)` + + - **filetypes**: + - **type** - `[]string` + - **required** - `false` + - **default** - `{ 'c', 'cpp' }` + - **description** - `The filetypes this module should be loaded on.` + + If any module writes their own config fields with the same names, they will not get overridden. + This is the case for kickstart modules, which are loaded automatically. + +- **events**: `{ [EventName]: fn }` - The functions has to have the following signature: `fn(config, ctx): ModuleResult` + +- **init**: `fn` - A function called once at the beginning if the module is enabled and the user visited one of the filetypes once. + +# luaKITTENS + Proper spec + +For now consult the `lua/cpp-tools/lib/luakittens/parser` file, the `__test` function and the `only_parse()` function. From e12f845a99d2022942f1ad92b230b476fd6737db Mon Sep 17 00:00:00 2001 From: b4mbus Date: Tue, 30 Jul 2024 22:03:57 +0200 Subject: [PATCH 02/16] docs(spec.technical): describe the events module field --- docs/spec/0.1/technical.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/spec/0.1/technical.md b/docs/spec/0.1/technical.md index 90c846d..73b043f 100644 --- a/docs/spec/0.1/technical.md +++ b/docs/spec/0.1/technical.md @@ -48,12 +48,12 @@ TODO: Possibly another implicit fields which allows disabling certain events, or ## Conventions 1. The types are denoted after `:` and use the [luaKITTENS annotation system](#luaKITTENS). -3. `^` denotes a required field, dependent on some condition (e.g. the `required` - `default` relation). +2. `^` denotes a required field, dependent on some condition (e.g. the `required` - `default` relation). ## Types 1. The `kitty` type refers to a `string` that's a valid `kitten`, that is a valid `luaKITTENS` annotation. -1. The `EventName` type refers to a `string` that's a valid neovim event name, see `:h events` +2. The `EventName` type refers to a `string` that's a valid neovim event name, see `:h events` ## Structure @@ -92,7 +92,15 @@ Each valid cpp-tools module has the following structure: This is the case for kickstart modules, which are loaded automatically. - **events**: `{ [EventName]: fn }` - The functions has to have the following signature: `fn(config, ctx): ModuleResult` - + `config` is the evaluated config for this function, `ctx` is the execution context, which currently consists of: + - **id**: `number` - Autocommand id + - **event**: `string` - Name of the triggered event + - **group**: `number` - Autocommand group id + - **match**: `string` - Expanded value of + - **buf**: `number` - Expanded value of + - **file**: `string` - Expanded value of + - **data**: `any` - Arbitrary data passed from `nvim_exec_autocmds()` + - **init**: `fn` - A function called once at the beginning if the module is enabled and the user visited one of the filetypes once. # luaKITTENS From 8331967d95b5dccee238b1bc935a60551ee8efd4 Mon Sep 17 00:00:00 2001 From: b4mbus Date: Tue, 30 Jul 2024 22:04:22 +0200 Subject: [PATCH 03/16] docs(spec): describe the `CppToolsProject` event --- docs/spec/0.1/functional.md | 23 ++++++++++++++++------- docs/spec/0.1/technical.md | 12 ++++++------ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/docs/spec/0.1/functional.md b/docs/spec/0.1/functional.md index 070c23b..ee7f7bf 100644 --- a/docs/spec/0.1/functional.md +++ b/docs/spec/0.1/functional.md @@ -4,14 +4,15 @@ * [Functional spec of cpp-tools.nvim](#functional-spec-of-cpp-toolsnvim) * [What is cpp-tools.nvim](#what-is-cpp-toolsnvim) - * [Main objectives](#main-objectives) - * [Installation](#installation) - * [Using Nix](#using-nix) - * [Using rocks.nvim](#using-rocksnvim) - * [Using lazy.nvim](#using-lazynvim) - * [Usage & configuration](#usage--configuration) - * [General info](#general-info) + * [Main objectives](#main-objectives) + * [Installation](#installation) + * [Using Nix](#using-nix) + * [Using rocks.nvim](#using-rocksnvim) + * [Using lazy.nvim](#using-lazynvim) + * [Usage & configuration](#usage--configuration) + * [General info](#general-info) * [Definitions](#definitions) +* [Events](#events) * [Goals for *some* future release](#goals-for-some-future-release) * [Goals for this release](#goals-for-this-release) * [Goals for the **next** release](#goals-for-the-next-release) @@ -98,6 +99,14 @@ vim.g.cpptools = { ... +# Events + +The plugin defines and responds to the following autocommand events: +- `CppToolsProject` - Fires once some time after neovim is opened and the current working directory is detected + to be a C++ project. It is used by some modules to start some stuff that is useful outside of the listed filetypes, + for example starting a language server on startup, to provide global workspace symbols and such out of the box. + If you don't want a certain module to respond to this event, you can set the `disable_project_event` option to `true`. + # Goals for *some* future release # Goals for this release diff --git a/docs/spec/0.1/technical.md b/docs/spec/0.1/technical.md index 73b043f..f3575a3 100644 --- a/docs/spec/0.1/technical.md +++ b/docs/spec/0.1/technical.md @@ -39,12 +39,6 @@ then calls each of the test functions with the context. # The module system -TODO: A `CppToolsProject` event, which will fire if neovim is opened up in a project directory. -This is useful to already provide language intelligence features even if we're not in a c/c++ file yet. -E.g. starting the lsp, to be able to use find references. - -TODO: Possibly another implicit fields which allows disabling certain events, or just the project event. - ## Conventions 1. The types are denoted after `:` and use the [luaKITTENS annotation system](#luaKITTENS). @@ -88,6 +82,12 @@ Each valid cpp-tools module has the following structure: - **default** - `{ 'c', 'cpp' }` - **description** - `The filetypes this module should be loaded on.` + - **disable_project_event**: + - **type** - `bool` + - **required** - `false` + - **default** - `false` + - **description** - `Whether to disable the project event. It is fired once when neovim starts and a valid C/C++ project is detected in cwdc. Useful for starting up stuff that provides code intelligence outside of the given filetypes, for example global workspace symbols.` + If any module writes their own config fields with the same names, they will not get overridden. This is the case for kickstart modules, which are loaded automatically. From ed8458edaf57f6937b18279fb61215c16168fe13 Mon Sep 17 00:00:00 2001 From: b4mbus Date: Wed, 31 Jul 2024 20:43:39 +0200 Subject: [PATCH 04/16] docs(spec.technical): refine the modules spec a bit --- docs/spec/0.1/technical.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/spec/0.1/technical.md b/docs/spec/0.1/technical.md index f3575a3..5cfc052 100644 --- a/docs/spec/0.1/technical.md +++ b/docs/spec/0.1/technical.md @@ -39,6 +39,8 @@ then calls each of the test functions with the context. # The module system +TODO: Think what to do about config with possible side effects, dynamic binding and overriding config options for kickstart. + ## Conventions 1. The types are denoted after `:` and use the [luaKITTENS annotation system](#luaKITTENS). @@ -93,15 +95,17 @@ Each valid cpp-tools module has the following structure: - **events**: `{ [EventName]: fn }` - The functions has to have the following signature: `fn(config, ctx): ModuleResult` `config` is the evaluated config for this function, `ctx` is the execution context, which currently consists of: + - **id**: `number` - Autocommand id - **event**: `string` - Name of the triggered event - **group**: `number` - Autocommand group id - - **match**: `string` - Expanded value of - - **buf**: `number` - Expanded value of - - **file**: `string` - Expanded value of + - **buffer**: `number` - The buffer the event was fired in + - **filetype**: `string` - The filetype the the event fired in + - **file**: `string` - The filename in which the event filed - **data**: `any` - Arbitrary data passed from `nvim_exec_autocmds()` - **init**: `fn` - A function called once at the beginning if the module is enabled and the user visited one of the filetypes once. + The signature is the same as the one of `events` fn. # luaKITTENS Proper spec From 6a29da48b1fb0cf6fb93b744c6e4db6c01ba9a80 Mon Sep 17 00:00:00 2001 From: b4mbus Date: Thu, 1 Aug 2024 17:05:26 +0200 Subject: [PATCH 05/16] docs(todo): add more todo --- TODO.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/TODO.md b/TODO.md index 9a743e6..240a6ef 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,10 @@ # TODO - [x] Add testing and linting to CI -- [ ] Add iter spec -- [ ] Add basic technical spec -- [ ] Create the module system based on the spec (+ tests) -- [ ] Create the first module (preamble) (+ tests) +- [x] Add iter tests +- [x] Add basic technical tests +- [ ] Create the module system based on the spec +- [ ] Create the first module (lsp) +- [ ] Create the preamble module +- [ ] Continue work on luaKITTENS +- [ ] Continue work on luaKITTENS From a38bf613ffe5c704726adb9f22786711b043ede8 Mon Sep 17 00:00:00 2001 From: b4mbus Date: Sat, 3 Aug 2024 20:32:59 +0200 Subject: [PATCH 06/16] feat(ftplugin.cpp): only setup if `vim.g.cpp_tools.enable` is true --- ftplugin/cpp.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ftplugin/cpp.lua b/ftplugin/cpp.lua index 785743a..ee828e6 100644 --- a/ftplugin/cpp.lua +++ b/ftplugin/cpp.lua @@ -1 +1,4 @@ -require('cpp-tools').setup() +-- TODO: Version check here probably +if vim.tbl_get(vim.g, 'cpp_tools', 'enable') == true then + require('cpp-tools').setup() +end From 0645e3f12c6f2133bd0edf9a1324351e9954416e Mon Sep 17 00:00:00 2001 From: b4mbus Date: Wed, 7 Aug 2024 22:43:25 +0200 Subject: [PATCH 07/16] docs(spec.functional): add stash --- docs/spec/0.1/functional.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/spec/0.1/functional.md b/docs/spec/0.1/functional.md index ee7f7bf..717e0e3 100644 --- a/docs/spec/0.1/functional.md +++ b/docs/spec/0.1/functional.md @@ -18,6 +18,7 @@ * [Goals for the **next** release](#goals-for-the-next-release) * [Non-goals for **any** release](#non-goals-for-any-release) * [Not sure if I'll ever implement these](#not-sure-if-ill-ever-implement-these) + * [Stash (For things that are not categorized or well though of yet)](#stash-for-things-that-are-not-categorized-or-well-though-of-yet) * [Known issues and questions](#known-issues-and-questions) @@ -129,9 +130,19 @@ The plugin defines and responds to the following autocommand events: # Not sure if I'll ever implement these +## Stash (For things that are not categorized or well though of yet) + - [ ] Find a way to reliably have access to the language server's features outside of C++ files. + - [ ] The problem with just attaching a client to any buffers will try to do things with the buffer - parse it, show diagnostics, etc. + - [ ] Some way to still provide go to definition even for semantic errors? + - [ ] Some way to easily check the versions of dependencies and general project info + - [ ] Better insert adding. If a symbol has been used manually and has a valid insert we insert a header lmao. + - Linting: - [ ] Better integration with iwyu and such +- Intelligence: + - [ ] Better lsp symbols with filtering and whatnot + - Productivity: - [ ] Automatically define templates based on the contents - [ ] .as, .each, etc. From 405b5b218a3afb16721c232f54adea443a1bfdd7 Mon Sep 17 00:00:00 2001 From: b4mbus Date: Thu, 8 Aug 2024 21:45:57 +0200 Subject: [PATCH 08/16] feat(lib.paths): add a lib for handling paths and requirables --- .../auto_test_runner/auto_test_runner.lua | 2 +- lua/cpp-tools/lib/paths.lua | 131 ++++++++++++++++++ .../paths/try_bulk_require/bad/flat/file1.lua | 1 + .../paths/try_bulk_require/bad/flat/file2.lua | 2 + .../try_bulk_require/bad/nested/file1.lua | 1 + .../try_bulk_require/bad/nested/file2.lua | 1 + .../bad/nested/nested/file3.lua | 1 + .../bad/nested/nested/nested/file4.lua | 1 + .../try_bulk_require/good/flat/file1.lua | 1 + .../try_bulk_require/good/flat/file2.lua | 1 + .../try_bulk_require/good/nested/file1.lua | 1 + .../try_bulk_require/good/nested/file2.lua | 1 + .../good/nested/nested/file3.lua | 1 + .../good/nested/nested/nested/file4.lua | 1 + 14 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 lua/cpp-tools/lib/paths.lua create mode 100644 testfiles/lib/paths/try_bulk_require/bad/flat/file1.lua create mode 100644 testfiles/lib/paths/try_bulk_require/bad/flat/file2.lua create mode 100644 testfiles/lib/paths/try_bulk_require/bad/nested/file1.lua create mode 100644 testfiles/lib/paths/try_bulk_require/bad/nested/file2.lua create mode 100644 testfiles/lib/paths/try_bulk_require/bad/nested/nested/file3.lua create mode 100644 testfiles/lib/paths/try_bulk_require/bad/nested/nested/nested/file4.lua create mode 100644 testfiles/lib/paths/try_bulk_require/good/flat/file1.lua create mode 100644 testfiles/lib/paths/try_bulk_require/good/flat/file2.lua create mode 100644 testfiles/lib/paths/try_bulk_require/good/nested/file1.lua create mode 100644 testfiles/lib/paths/try_bulk_require/good/nested/file2.lua create mode 100644 testfiles/lib/paths/try_bulk_require/good/nested/nested/file3.lua create mode 100644 testfiles/lib/paths/try_bulk_require/good/nested/nested/nested/file4.lua diff --git a/lua/cpp-tools/auto_test_runner/auto_test_runner.lua b/lua/cpp-tools/auto_test_runner/auto_test_runner.lua index dcf72c7..4b59f7b 100644 --- a/lua/cpp-tools/auto_test_runner/auto_test_runner.lua +++ b/lua/cpp-tools/auto_test_runner/auto_test_runner.lua @@ -2,7 +2,7 @@ --- This file scans all the requirable `lua/` files from here --- and runs their __test functions with busted test context. -local current_filename = debug.getinfo(1).source:match('([%w_%.]+)$') +local current_filename = require('cpp-tools.lib.paths').current_filename() local testfiles_dir = vim.fs.root(0, 'testfiles') .. '/testfiles' local function project_lua_files(path, type) diff --git a/lua/cpp-tools/lib/paths.lua b/lua/cpp-tools/lib/paths.lua new file mode 100644 index 0000000..5738e35 --- /dev/null +++ b/lua/cpp-tools/lib/paths.lua @@ -0,0 +1,131 @@ +local M = {} + +-------------------------------------------------- +-- Types +-------------------------------------------------- + +---@alias cpp-tools.paths.Requirable string +---@alias cpp-tools.paths.Path string + +---@class cpp-tools.paths.BulkRequireResult +---@field path cpp-tools.paths.Path The path for which fun was called +---@field result any The result of fun(path) + +-------------------------------------------------- +-- Types +-------------------------------------------------- + +local function bulk_require_impl(path, fs_dir_opts, fun) + return vim + .iter(vim.fs.dir(path, fs_dir_opts)) + :filter(function(_name, type) + return type == 'file' + end) + :map(function(file) + return ('%s/%s'):format(path, file) + end) + :map(function(path) + return { + path = path, + result = { fun(path) }, + } + end) + :totable() +end + +---Returns an absolute path of the calling script +---@return string # The path +function M.current_path() + return debug.getinfo(2).source:sub(2) +end + +---Returns the filename of the calling script +---@return string # The filename +function M.current_filename() + --[=[ + NOTE: This *cannot* be refactored into + ```lua + return- M.current_path():match(...) + ``` + Because `debug.getinfo` returns debug info from the standpoint of + a given function level. When calling `getinfo(2)` from some other script, + we go 2 levels up - to the calling file, + but if we called this and then in turn call `getinfo(2)`, we would go 2 levels up - to this function + ]=] + return debug.getinfo(2).source:match('([%w_%.]+)$') +end + +---Tries to iterate over a directory and pcall(dofile) each file in there +--- +---@param path cpp-tools.paths.Path The path to all the modules +---@param fs_dir_opts table? The opts to pass to `vim.fs.dir` +---@return cpp-tools.paths.BulkRequireResult[] +function M.try_bulk_require(path, fs_dir_opts) + return bulk_require_impl(path, fs_dir_opts, function(p) + return pcall(dofile, p) + end) +end + +---Iterates over a directory and dofile's each file in there +--- +---@param path cpp-tools.paths.Path The path to all the modules +---@param fs_dir_opts table? The opts to pass to `vim.fs.dir` +---@return cpp-tools.paths.BulkRequireResult[] +function M.bulk_require(path, fs_dir_opts) + return bulk_require_impl(path, fs_dir_opts, dofile) +end + +---@package +function M.__test(testfiles) + describe('`try_bulk_require()`', function() + it('properly requires good, flat modules', function() + local test_files = testfiles .. '/lib/paths/try_bulk_require/good/flat' + + local mods = M.try_bulk_require(test_files) + + assert.are.no.equal(#mods, 0) + + for _, result in ipairs(mods) do + assert.message(('%s - %s'):format(result.path, result.result[2])).is.truthy(result.result[1]) + end + end) + + it('properly requires good, nested modules', function() + local test_files = testfiles .. '/lib/paths/try_bulk_require/good/nested' + + local mods = M.try_bulk_require(test_files, { depth = 3 }) + + assert.are.no.equal(#mods, 0) + + for _, result in ipairs(mods) do + assert.message(('%s - %s'):format(result.path, result.result[2])).is.truthy(result.result[1]) + end + end) + + it('properly requires bad, flat modules', function() + local test_files = testfiles .. '/lib/paths/try_bulk_require/bad/flat' + + local mods = M.try_bulk_require(test_files) + + assert.are.no.equal(#mods, 0) + + for _, result in ipairs(mods) do + assert.message(('%s - %s'):format(result.path, result.result[2])).is.falsy(result.result[1]) + end + end) + + it('properly requires bad, nested modules', function() + local test_files = testfiles .. '/lib/paths/try_bulk_require/bad/nested' + + local mods = M.try_bulk_require(test_files, { depth = 3 }) + + assert.are.no.equal(#mods, 0) + + for _, result in ipairs(mods) do + assert.message(('%s - %s'):format(result.path, result.result[2])).is.falsy(result.result[1]) + end + end) + end) +end + +return M diff --git a/testfiles/lib/paths/try_bulk_require/bad/flat/file1.lua b/testfiles/lib/paths/try_bulk_require/bad/flat/file1.lua new file mode 100644 index 0000000..fbf2089 --- /dev/null +++ b/testfiles/lib/paths/try_bulk_require/bad/flat/file1.lua @@ -0,0 +1 @@ +some_syntax_error = diff --git a/testfiles/lib/paths/try_bulk_require/bad/flat/file2.lua b/testfiles/lib/paths/try_bulk_require/bad/flat/file2.lua new file mode 100644 index 0000000..447914b --- /dev/null +++ b/testfiles/lib/paths/try_bulk_require/bad/flat/file2.lua @@ -0,0 +1,2 @@ +-- This won't work +return 1 + 'asd' + {} diff --git a/testfiles/lib/paths/try_bulk_require/bad/nested/file1.lua b/testfiles/lib/paths/try_bulk_require/bad/nested/file1.lua new file mode 100644 index 0000000..bf7e899 --- /dev/null +++ b/testfiles/lib/paths/try_bulk_require/bad/nested/file1.lua @@ -0,0 +1 @@ +asdasd diff --git a/testfiles/lib/paths/try_bulk_require/bad/nested/file2.lua b/testfiles/lib/paths/try_bulk_require/bad/nested/file2.lua new file mode 100644 index 0000000..5c1a9c7 --- /dev/null +++ b/testfiles/lib/paths/try_bulk_require/bad/nested/file2.lua @@ -0,0 +1 @@ +{}{} diff --git a/testfiles/lib/paths/try_bulk_require/bad/nested/nested/file3.lua b/testfiles/lib/paths/try_bulk_require/bad/nested/nested/file3.lua new file mode 100644 index 0000000..59900e9 --- /dev/null +++ b/testfiles/lib/paths/try_bulk_require/bad/nested/nested/file3.lua @@ -0,0 +1 @@ +return 3 + 'xx' . + 10 diff --git a/testfiles/lib/paths/try_bulk_require/bad/nested/nested/nested/file4.lua b/testfiles/lib/paths/try_bulk_require/bad/nested/nested/nested/file4.lua new file mode 100644 index 0000000..569cf52 --- /dev/null +++ b/testfiles/lib/paths/try_bulk_require/bad/nested/nested/nested/file4.lua @@ -0,0 +1 @@ +:3 diff --git a/testfiles/lib/paths/try_bulk_require/good/flat/file1.lua b/testfiles/lib/paths/try_bulk_require/good/flat/file1.lua new file mode 100644 index 0000000..a4325f6 --- /dev/null +++ b/testfiles/lib/paths/try_bulk_require/good/flat/file1.lua @@ -0,0 +1 @@ +return 1 diff --git a/testfiles/lib/paths/try_bulk_require/good/flat/file2.lua b/testfiles/lib/paths/try_bulk_require/good/flat/file2.lua new file mode 100644 index 0000000..3ee3fca --- /dev/null +++ b/testfiles/lib/paths/try_bulk_require/good/flat/file2.lua @@ -0,0 +1 @@ +return 2 diff --git a/testfiles/lib/paths/try_bulk_require/good/nested/file1.lua b/testfiles/lib/paths/try_bulk_require/good/nested/file1.lua new file mode 100644 index 0000000..a4325f6 --- /dev/null +++ b/testfiles/lib/paths/try_bulk_require/good/nested/file1.lua @@ -0,0 +1 @@ +return 1 diff --git a/testfiles/lib/paths/try_bulk_require/good/nested/file2.lua b/testfiles/lib/paths/try_bulk_require/good/nested/file2.lua new file mode 100644 index 0000000..3ee3fca --- /dev/null +++ b/testfiles/lib/paths/try_bulk_require/good/nested/file2.lua @@ -0,0 +1 @@ +return 2 diff --git a/testfiles/lib/paths/try_bulk_require/good/nested/nested/file3.lua b/testfiles/lib/paths/try_bulk_require/good/nested/nested/file3.lua new file mode 100644 index 0000000..32d0c30 --- /dev/null +++ b/testfiles/lib/paths/try_bulk_require/good/nested/nested/file3.lua @@ -0,0 +1 @@ +return 3 diff --git a/testfiles/lib/paths/try_bulk_require/good/nested/nested/nested/file4.lua b/testfiles/lib/paths/try_bulk_require/good/nested/nested/nested/file4.lua new file mode 100644 index 0000000..857f0f6 --- /dev/null +++ b/testfiles/lib/paths/try_bulk_require/good/nested/nested/nested/file4.lua @@ -0,0 +1 @@ +return 4 From c95640e219b846aa476cc8dc2412c0dfbd902685 Mon Sep 17 00:00:00 2001 From: b4mbus Date: Thu, 15 Aug 2024 21:32:10 +0200 Subject: [PATCH 09/16] feat(lib.test): add a lib for tests This library is supposed to be used within tests as it provides general non-generic information and utilities, like project root and such. --- lua/cpp-tools/auto_test_runner/test.lua | 1 - lua/cpp-tools/lib/test.lua | 11 +++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 lua/cpp-tools/lib/test.lua diff --git a/lua/cpp-tools/auto_test_runner/test.lua b/lua/cpp-tools/auto_test_runner/test.lua index 29ff3eb..cc9f908 100644 --- a/lua/cpp-tools/auto_test_runner/test.lua +++ b/lua/cpp-tools/auto_test_runner/test.lua @@ -8,7 +8,6 @@ function M.__test(testfiles_dir) describe('auto test runner', function() it('returns a proper testfiles dir', function() local file = testfiles_dir .. '/auto_test_runner/test.txt' - print(file) local f, msg = io.open(file) if not f then diff --git a/lua/cpp-tools/lib/test.lua b/lua/cpp-tools/lib/test.lua new file mode 100644 index 0000000..43dcc0e --- /dev/null +++ b/lua/cpp-tools/lib/test.lua @@ -0,0 +1,11 @@ +---This file contains useful functions for usage inside tests. +---They're here, because they are not necessarily generic and are more of the "debug" functions kind. +local M = {} + +---Returns the root of the project +---@return string +function M.root() + return vim.fs.root(0, 'testfiles') --[[@as string]] +end + +return M From f03b802bee3a125e0759f58478b1d830d3e5ee24 Mon Sep 17 00:00:00 2001 From: b4mbus Date: Thu, 15 Aug 2024 21:45:45 +0200 Subject: [PATCH 10/16] chore: add .styluaignore We don't want to format files inside testfiles. --- .gitignore | 1 + .styluaignore | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 .styluaignore diff --git a/.gitignore b/.gitignore index 334b6cb..d84260e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ !.luarc.json !.editorconfig !stylua.toml +!.styluaignore !.pre-commit-config.yaml !git-conventional-commits.yaml diff --git a/.styluaignore b/.styluaignore new file mode 100644 index 0000000..5e98418 --- /dev/null +++ b/.styluaignore @@ -0,0 +1,2 @@ +testfiles/ +*.rockspec From 859e4ca005fe6012e1c56b8d91833e9618b4e699 Mon Sep 17 00:00:00 2001 From: b4mbus Date: Sun, 18 Aug 2024 22:23:42 +0200 Subject: [PATCH 11/16] feat(lib.config): add library for dealing with config This library deals with evaluating and validating the configuration based on the module spec and runtime value. It also exposes a function for evaluating docs, which is not implemented yet. --- lua/cpp-tools/lib/config.lua | 399 ++++++++++++++++++ .../lacking_example_and_default.lua | 8 + .../lacking_required_and_default.lua | 9 + .../config_specs_test/lacking_type_field.lua | 10 + ...equired_is_true_and_default_is_present.lua | 10 + 5 files changed, 436 insertions(+) create mode 100644 lua/cpp-tools/lib/config.lua create mode 100644 testfiles/lib/config/config_specs_test/lacking_example_and_default.lua create mode 100644 testfiles/lib/config/config_specs_test/lacking_required_and_default.lua create mode 100644 testfiles/lib/config/config_specs_test/lacking_type_field.lua create mode 100644 testfiles/lib/config/config_specs_test/required_is_true_and_default_is_present.lua diff --git a/lua/cpp-tools/lib/config.lua b/lua/cpp-tools/lib/config.lua new file mode 100644 index 0000000..0b46862 --- /dev/null +++ b/lua/cpp-tools/lib/config.lua @@ -0,0 +1,399 @@ +---@module 'cpp-tools.lib.types' + +local M = {} + +---@class (exact) cpp-tools.config.ConfigFieldSpec +---@field type string|string[] The type of the field, currently only lua types are allowed. +---@field validator nil|any[]|fun(in: any): boolean, string Either an array of allowed values or a function that takes in the value +---and returns if the validation succeeded and what the error is. Defaults to `function() return true end`. +---@field required boolean? Is this required? Defaults to `true`. +---@field default nil|any (Required if `required` is `false`!!!) A default value for this option. If required isn't set it implies `required = false`. +---@field example string An example (used for generating documentation). +---@field description string A description (used for generating documentation). + +---@alias cpp-tools.config.ConfigSpec table +---@alias cpp-tools.config.UserConfig table + +---Returns implicit fields for a module +---@param name string Module's name +---@return cpp-tools.config.ConfigSpec +local function create_implicit_fields(name) + ---@type cpp-tools.config.ConfigSpec + return { + enable = { + type = 'boolean', + required = true, + example = 'false', + description = ([[Whether to enable the '%s' module]]):format(name), + }, + + filetypes = { + type = { 'string', '[]string' }, + required = false, + default = { 'c', 'cpp' }, + example = [[{ 'c', 'cpp' }]], + description = [[The filetypes for which this module should be setup and run]], + }, + } +end + +---Adds implicit fields to the config's spec +---@param name string Module's name +---@param config_spec cpp-tools.config.ConfigSpec The configuration spec +local function add_implicit_fields(name, config_spec) + local implicit_fields = create_implicit_fields(name) + + return vim.tbl_extend('keep', config_spec, implicit_fields) +end + +function M.get_config() + return vim.tbl_extend_deep('force', vim.g.cpp_tools_global or {}, vim.g.cpp_tools or {}) +end + +---Evaluates a configuration for a module +---@param name string The module name +---@param config_spec cpp-tools.config.ConfigSpec +---@param runtime_value cpp-tools.config.UserConfig +function M.evaluate(name, config_spec, runtime_value) + config_spec = add_implicit_fields(name, config_spec) + + local evaluated_config = {} + -- TODO: Add context with levenshtein distance and such + local potential_errors = {} + + local did_error = false + -- TODO: Refactor into a function, to also use inside healtcheck + for field_name, definition in pairs(config_spec) do + local field_value = runtime_value[field_name] + local field_spec = config_spec[field_name] + + if field_value == nil and field_spec.required then + did_error = true + + table.insert(potential_errors, ('Field `%s` is required but a value for it wasn\'t provided.'):format(field_name)) + end + + if field_value and not M.validate_type(field_value, field_spec) then + did_error = true + + table.insert( + potential_errors, + ('Field `%s` is expected to have type `%s`. Instead, it is of type "%s".'):format( + field_name, + config_spec[field_name].type, + type(field_value) + ) + ) + end + + if did_error then + goto continue + end + + if field_value == nil then + evaluated_config[field_name] = definition.default + else + evaluated_config[field_name] = field_value + end + + ::continue:: + runtime_value[field_name] = nil + end + + for field_name, _value in pairs(runtime_value) do + table.insert(potential_errors, ('Extraneous field `%s` given.'):format(field_name)) + end + + if #potential_errors ~= 0 then + return false, potential_errors + end + + return true, evaluated_config +end + +---Checks if the runtime value for a config field has the correct type +---@param field_value any The runtime value of a field +---@param field_spec cpp-tools.config.ConfigFieldSpec The specification for the field +---@return boolean +function M.validate_type(field_value, field_spec) + -- TODO: Luakittens validation + if type(field_spec.type) == 'table' then + return vim.iter(field_spec.type):any(function(expected_type) + return expected_type == type(field_value) + end) + end + + return type(field_value) == field_spec.type +end + +---Evaluates only the documentation part of a config. +---@param name string The module name +---@param config table +function M.evaluate_docs(name, config) end + +---@package +function M.__test(testfiles) + -- TODO: Validate each module's config spec + describe('`add_implicit_fields()`', function() + it('Adds fields if they don\'t exist', function() + local name = 'test' + + local spec = add_implicit_fields(name, {}) + local implicit_fields = create_implicit_fields(name) + + assert.are.equal(vim.tbl_count(spec), vim.tbl_count(implicit_fields)) + assert.are.same(spec, implicit_fields) + end) + + it('Ignores fields that already exist', function() + local name = 'test' + + local implicit_fields = create_implicit_fields(name) + + local field_name, field_val = vim.iter(vim.deepcopy(implicit_fields)):nth(1) + field_val.required = not field_val.required + + local changed_spec = add_implicit_fields(name, { [field_name] = field_val }) + + assert.are.equal(vim.tbl_count(changed_spec), vim.tbl_count(implicit_fields)) + assert.are.no.same(changed_spec[field_name].required, implicit_fields[field_name].required) + end) + end) + + describe('`evaluate()`', function() + it('Properly evaluates a good config', function() + local spec = { + text = { + type = { 'function', 'string' }, + }, + comment_style = { + type = 'string', + validate = { 'c', 'cpp' }, + }, + } + + local value = { + enable = false, -- Implicit required field + text = 'foo', + comment_style = 'c', + } + + local ok, evaluated_config = M.evaluate('test', spec, vim.deepcopy(value)) + + assert.message(evaluated_config).truthy(ok) + assert.are.equal(value.enable, evaluated_config.enable) + assert.are.equal(value.text, evaluated_config.text) + assert.are.equal(value.comment_style, evaluated_config.comment_style) + end) + + it('Properly evaluates a good config with default values', function() + local spec = { + comment_style = { + type = 'string', + default = 'cpp', + validate = { 'c', 'cpp' }, + }, + } + + local value = { + enable = true, -- Implicit required field + } + + local ok, evaluated_config = M.evaluate('test', spec, vim.deepcopy(value)) + + assert.message(evaluated_config).truthy(ok) + assert.are.equal(value.enable, evaluated_config.enable) + assert.are.equal('cpp', evaluated_config.comment_style) + end) + + it('Properly evaluates a good config and overrides implicit fields', function() + local spec = { + enable = { + default = true, + type = 'boolean', + }, + comment_style = { + type = 'string', + default = 'cpp', + validate = { 'c', 'cpp' }, + }, + } + + local value = {} + + local ok, evaluated_config = M.evaluate('test', spec, vim.deepcopy(value)) + + assert.message(evaluated_config).truthy(ok) + assert.are.equal(true, evaluated_config.enable) + assert.are.equal('cpp', evaluated_config.comment_style) + end) + + it('Errors on not providing required value', function() + local spec = {} -- Will only get the implicit fields + + local value = {} + + local ok, evaluated_config = M.evaluate('test', spec, vim.deepcopy(value)) + + assert.message(evaluated_config).falsy(ok) + assert.message(evaluated_config).are.same(1, vim.tbl_count(evaluated_config)) + end) + + it('Errors on extraneous fields', function() + local spec = {} -- Will only get the implicit fields + + local value = { + enable = true, + foo = 1, + bar = 1, + } + + local ok, evaluated_config = M.evaluate('test', spec, vim.deepcopy(value)) + + assert.message(evaluated_config).falsy(ok) + assert.message(evaluated_config).are.same(2, vim.tbl_count(evaluated_config)) + end) + + it('Errors on wrong type', function() + local spec = {} + + local value = { + enable = 'hello', + } + + local ok, evaluated_config = M.evaluate('test', spec, vim.deepcopy(value)) + + assert.message(evaluated_config).falsy(ok) + assert.message(evaluated_config).are.same(1, vim.tbl_count(evaluated_config)) + end) + + it('Returns all the errors', function() + local spec = { + bar = { + type = 'string', + }, + } + + local value = { + -- Lack of required enable field + bar = 1, -- Wrong bar type + foo = 1, -- Extraneous foo field + } + + local ok, evaluated_config = M.evaluate('test', spec, vim.deepcopy(value)) + + assert.message(evaluated_config).falsy(ok) + -- We expect two, because in this case it will both error about the type + -- being wrong and about extraneous field + assert.message(evaluated_config).are.same(3, vim.tbl_count(evaluated_config)) + end) + end) + + -- TODO: Refactor based on M.evaluate and parse_structure into a helper to lib.test + local function parse_config_spec(was_ok, spec) + if not was_ok then + return false, spec + end + + local config = spec.config + + local fields = { + type = { 'string', 'table' }, + description = 'string', + } + + local function type_valid(field, valid_type) + if type(valid_type) == 'table' then + return vim.iter(valid_type):any(function(vt) + return vt == type(field) + end) + else + return valid_type == type(field) + end + end + + for spec_field_name, spec in pairs(config) do + for field_name, required_type in pairs(fields) do + local field = vim.tbl_get(spec, field_name) + + if field == nil then + return false, ('In `%s` - `%s` required field lacking'):format(spec_field_name, field_name) + else + local fields_type = type(field) + if not type_valid(field, required_type) then + return false, + ('In `%s` - `%s`\'s type is `%s` - `%s` was expected.'):format( + spec_field_name, + field_name, + fields_type, + required_type + ) + end + end + end + + if vim.tbl_get(spec, 'example') == nil and vim.tbl_get(spec, 'default') == nil then + return false, ('In `%s` - either \'example\' or \'default\' need to be specified.'):format(spec_field_name) + end + + if vim.tbl_get(spec, 'required') == nil and vim.tbl_get(spec, 'default') == nil then + return false, ('In `%s` - `required` needs to be specified if `default` isn\t present.'):format(spec_field_name) + end + + if vim.tbl_get(spec, 'required') == true and vim.tbl_get(spec, 'default') ~= nil then + return false, ('In `%s` - `default` cannot be set if `required == true`.'):format(spec_field_name) + end + end + + return true, 'ok' + end + + describe('cpp-tools.nvim modules', function() + local paths = require('cpp-tools.lib.paths') + + it('[sanity check - incorrect modules should fail]', function() + local test_mods_dir = testfiles .. '/lib/config/config_specs_test' + + local test_mods = vim + .iter(paths.try_bulk_require(test_mods_dir)) + :map(function(bulk_require_result) + return { + path = bulk_require_result.path, + result = { parse_config_spec(unpack(bulk_require_result.result)) }, + } + end) + :totable() + + assert.are.no.equal(#test_mods, 0) + + for _, result in ipairs(test_mods) do + assert.message(('File [%s] - %s'):format(result.path, result.result[2])).is.falsy(result.result[1]) + end + end) + + --[[ it('All have valid config structure', function() + local root = require('cpp-tools.lib.test').root() + local modules_dir = root .. '/lua/cpp-tools/modules' + + local parsed_mods = + vim.iter(paths.try_bulk_require(modules_dir, { depth = 10 })) + :map(function(bulk_require_result) + return { + path = bulk_require_result.path, + result = { parse_config_spec(unpack(bulk_require_result.result)) } + } + end) + :totable() + + assert.are.no.equal(#parsed_mods, 0) + + for _, result in ipairs(parsed_mods) do + assert + .message(('File [%s] - %s'):format(result.path, result.result[2])) + .is.truthy(result.result[1]) + end + end) ]] + end) +end + +return M diff --git a/testfiles/lib/config/config_specs_test/lacking_example_and_default.lua b/testfiles/lib/config/config_specs_test/lacking_example_and_default.lua new file mode 100644 index 0000000..97ba149 --- /dev/null +++ b/testfiles/lib/config/config_specs_test/lacking_example_and_default.lua @@ -0,0 +1,8 @@ +return { + config = { + foo = { + type = 'string', + description = 'Foobar', + }, + }, +} diff --git a/testfiles/lib/config/config_specs_test/lacking_required_and_default.lua b/testfiles/lib/config/config_specs_test/lacking_required_and_default.lua new file mode 100644 index 0000000..d4eb02f --- /dev/null +++ b/testfiles/lib/config/config_specs_test/lacking_required_and_default.lua @@ -0,0 +1,9 @@ +return { + config = { + foo = { + type = 'string', + description = 'Foobar', + example = 'x', + }, + }, +} diff --git a/testfiles/lib/config/config_specs_test/lacking_type_field.lua b/testfiles/lib/config/config_specs_test/lacking_type_field.lua new file mode 100644 index 0000000..2f1ca4f --- /dev/null +++ b/testfiles/lib/config/config_specs_test/lacking_type_field.lua @@ -0,0 +1,10 @@ +return { + config = { + foo = { + -- Lacking type + description = 'Foobar', + example = 'x', + required = false, + }, + }, +} diff --git a/testfiles/lib/config/config_specs_test/required_is_true_and_default_is_present.lua b/testfiles/lib/config/config_specs_test/required_is_true_and_default_is_present.lua new file mode 100644 index 0000000..4b83ac6 --- /dev/null +++ b/testfiles/lib/config/config_specs_test/required_is_true_and_default_is_present.lua @@ -0,0 +1,10 @@ +return { + config = { + foo = { + type = 'string', + description = 'Foobar', + required = true, + default = 'asd', + }, + }, +} From 7e0bbab4ce0f5bf5b635f871302cf09de8f959ab Mon Sep 17 00:00:00 2001 From: b4mbus Date: Thu, 3 Oct 2024 23:25:33 +0200 Subject: [PATCH 12/16] feat(lib.paths): add `current_dir` --- lua/cpp-tools/lib/paths.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lua/cpp-tools/lib/paths.lua b/lua/cpp-tools/lib/paths.lua index 5738e35..17cf6a3 100644 --- a/lua/cpp-tools/lib/paths.lua +++ b/lua/cpp-tools/lib/paths.lua @@ -39,6 +39,12 @@ function M.current_path() return debug.getinfo(2).source:sub(2) end +---Returns an absolute dir of the calling script +---@return string # The path +function M.current_dir() + return vim.fs.dirname(debug.getinfo(2).source:sub(2)) +end + ---Returns the filename of the calling script ---@return string # The filename function M.current_filename() From b987ece351cec6e9ea06ae28a9679495c43be311 Mon Sep 17 00:00:00 2001 From: b4mbus Date: Thu, 3 Oct 2024 23:26:21 +0200 Subject: [PATCH 13/16] fix(lib.config): typo `tbl_extend_deep` -> `tbl_deep_extend` --- lua/cpp-tools/lib/config.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lua/cpp-tools/lib/config.lua b/lua/cpp-tools/lib/config.lua index 0b46862..0e1be0f 100644 --- a/lua/cpp-tools/lib/config.lua +++ b/lua/cpp-tools/lib/config.lua @@ -46,13 +46,14 @@ local function add_implicit_fields(name, config_spec) return vim.tbl_extend('keep', config_spec, implicit_fields) end -function M.get_config() - return vim.tbl_extend_deep('force', vim.g.cpp_tools_global or {}, vim.g.cpp_tools or {}) +---Returns the value of user-set config, `vim.g.cpp_tools_global` overridden with `vim.g.cpp_tools` +function M.get_user_config() + return vim.tbl_deep_extend('force', vim.g.cpp_tools_global or {}, vim.g.cpp_tools or {}) end ---Evaluates a configuration for a module ---@param name string The module name ----@param config_spec cpp-tools.config.ConfigSpec +---@param config_spec cpp-tools.config.ConfigSpec The config specification of the module ---@param runtime_value cpp-tools.config.UserConfig function M.evaluate(name, config_spec, runtime_value) config_spec = add_implicit_fields(name, config_spec) From 34a2110d715778015a528dde8f38f6a570b65e4b Mon Sep 17 00:00:00 2001 From: b4mbus Date: Sun, 1 Dec 2024 00:54:49 +0100 Subject: [PATCH 14/16] feat(lib.iter): add `map` --- lua/cpp-tools/lib/iter/init.lua | 88 +++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/lua/cpp-tools/lib/iter/init.lua b/lua/cpp-tools/lib/iter/init.lua index f2ee9b7..a46acc0 100644 --- a/lua/cpp-tools/lib/iter/init.lua +++ b/lua/cpp-tools/lib/iter/init.lua @@ -62,6 +62,35 @@ function M.partition(range, pred, proj_beg) vim.iter(range):map(proj_beg):filter(fp.nah(pred)):totable() or {} end +---Maps a range using a function or a name of an internal field +---@generic T, U +---@param range [`T`] The range to partition +---@param ... string|fun(T): U Mapping functions or field names +---@return [U]|[any] +function M.map(range, ...) + local mappings = { ... } + return vim + .iter(range) + :map(function(t) + return vim.iter(mappings):fold(t, function(final, mapping) + local mapping_type = type(mapping) + if mapping_type == 'string' then + assert( + type(t) == 'table', + ('The type of element must be a table to use a field name mapping (type type is [%s])'):format(type(t)) + ) + + return vim.tbl_get(final, mapping) + elseif mapping_type == 'function' then + return mapping(final) + else + assert(false, ('The type of mapping must be a function or a string, not [%s]'):format(mapping_type)) + end + end) + end) + :totable() +end + ---@package function M.__test() describe('`array_equals()`', function() @@ -197,6 +226,65 @@ function M.__test() assert.are.same(bad, { 10, 12, 14, 16 }) end) end) + + describe('`map()`', function() + it('Maps using a function', function() + local arr = { 1, 2, 3, 4 } + local mapped = M.map(arr, function(n) + return n * 2 + end) + + assert.are.same(mapped, { 2, 4, 6, 8 }) + end) + it('Maps using chained functions', function() + local arr = { 1, 2, 3, 4 } + local mapped = M.map( + arr, + function(n) + return n * 2 + end, -- 2 4 6 8 + function(n) + return n - 1 + end -- 1 3 5 7 + ) + + assert.are.same(mapped, { 1, 3, 5, 7 }) + end) + it('Maps using a single field name', function() + local arr = { + { foo = 1, bar = 2 }, + { foo = 1, bar = 2 }, + { foo = 1, bar = 2 }, + } + local foos = M.map(arr, 'foo') + local bars = M.map(arr, 'bar') + + assert.are.same(foos, { 1, 1, 1 }) + assert.are.same(bars, { 2, 2, 2 }) + end) + it('Maps nested tables', function() + local arr = { + { foo = { bar = 1, qoox = { foox = 2, boox = 5 } } }, + { foo = { bar = 2, qoox = { foox = 1, boox = 4 } } }, + } + local bars = M.map(arr, 'foo', 'bar') + local fooxes = M.map(arr, 'foo', 'qoox', 'foox') + + assert.are.same(bars, { 1, 2 }) + assert.are.same(fooxes, { 2, 1 }) + end) + it('Maps with both functions and field names tables', function() + local arr = { + { foo = { bar = 1, qoox = { foox = 2, boox = 5 } } }, + { foo = { bar = 2, qoox = { foox = 2, boox = 5 } } }, + } + local bars = M.map(arr, 'foo', 'bar', function(e) + return e * 2 + end) + + assert.are.same(bars, { 2, 4 }) + end) + end) end return M From 72f81b5b9d0622efb29b602186940e740a74b7c9 Mon Sep 17 00:00:00 2001 From: b4mbus Date: Sun, 1 Dec 2024 00:55:36 +0100 Subject: [PATCH 15/16] style: format nix code --- nix/checks.nix | 10 +++++----- nix/shell.nix | 26 +++++++++++++++----------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/nix/checks.nix b/nix/checks.nix index 49b2400..ac33a2f 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -11,13 +11,13 @@ overlays = [inputs.neorocks.overlays.default]; }; - checks.pre-commit = pkgs.writeShellApplication { - name = "pre-commit-check"; + checks.pre-commit = pkgs.writeShellApplication { + name = "pre-commit-check"; - runtimeInputs = [pkgs.pre-commit]; + runtimeInputs = [pkgs.pre-commit]; - text = "pre-commit run --all-files"; - }; + text = "pre-commit run --all-files"; + }; checks.default = pkgs.writeShellApplication { name = "typos-check"; diff --git a/nix/shell.nix b/nix/shell.nix index f5bb212..31087ae 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -1,20 +1,24 @@ -{pkgs, lib, ...}: { +{ + pkgs, + lib, + ... +}: { devShells.default = pkgs.mkShellNoCC { - shellHook = '' - ${lib.getExe pkgs.pre-commit} install - ''; + shellHook = '' + ${lib.getExe pkgs.pre-commit} install + ''; packages = [ pkgs.lua-language-server pkgs.luajitPackages.luacheck - pkgs.luarocks + pkgs.luarocks - pkgs.pre-commit - pkgs.ruby - pkgs.stylua - pkgs.typos - pkgs.yamllint - pkgs.actionlint + pkgs.pre-commit + pkgs.ruby + pkgs.stylua + pkgs.typos + pkgs.yamllint + pkgs.actionlint ]; }; } From 1018d5ef9f1305ccf6687ae0e13c0ff394fcee41 Mon Sep 17 00:00:00 2001 From: b4mbus Date: Sun, 1 Dec 2024 15:38:13 +0100 Subject: [PATCH 16/16] feat(lib.iter): add index support to map This allows for mapping subsequent arrays into their inside values. e.g. ```lua iter.map( { { { foo = 1} } }, 1, 'foo' ) == { 1 } ``` --- lua/cpp-tools/lib/iter/init.lua | 47 ++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/lua/cpp-tools/lib/iter/init.lua b/lua/cpp-tools/lib/iter/init.lua index a46acc0..d6d3a42 100644 --- a/lua/cpp-tools/lib/iter/init.lua +++ b/lua/cpp-tools/lib/iter/init.lua @@ -1,5 +1,7 @@ local M = {} +-- TODO: Use an iter mechanism instead of relying on tables everywhere + ---Counts the lines in a string ---@param str string the string to count lines for ---@return number line count # The number of lines @@ -62,10 +64,17 @@ function M.partition(range, pred, proj_beg) vim.iter(range):map(proj_beg):filter(fp.nah(pred)):totable() or {} end ----Maps a range using a function or a name of an internal field +-- TODO: Instead of using an overengineered map thingy +-- have a more robust vim.iter mechanism + +---Maps a range using a function, index or a name of an internal field +---Using a function will map the current element using the function, +---Using a field name will do `vim.tbl_get(t, name)` +---Using an integer will do `t[idx]` +---e.g. `fp.map({ { foo = 1 } }, 1, 'foo', function(x) return x * 2 end)` will return { 2 } ---@generic T, U ---@param range [`T`] The range to partition ----@param ... string|fun(T): U Mapping functions or field names +---@param ... integer|string|fun(T): U Mapping functions or field names ---@return [U]|[any] function M.map(range, ...) local mappings = { ... } @@ -81,10 +90,15 @@ function M.map(range, ...) ) return vim.tbl_get(final, mapping) + elseif mapping_type == 'number' then + return final[mapping] elseif mapping_type == 'function' then return mapping(final) else - assert(false, ('The type of mapping must be a function or a string, not [%s]'):format(mapping_type)) + assert( + false, + ('The type of mapping must be a function, a string or an integer, not [%s]'):format(mapping_type) + ) end end) end) @@ -250,6 +264,7 @@ function M.__test() assert.are.same(mapped, { 1, 3, 5, 7 }) end) + it('Maps using a single field name', function() local arr = { { foo = 1, bar = 2 }, @@ -262,6 +277,7 @@ function M.__test() assert.are.same(foos, { 1, 1, 1 }) assert.are.same(bars, { 2, 2, 2 }) end) + it('Maps nested tables', function() local arr = { { foo = { bar = 1, qoox = { foox = 2, boox = 5 } } }, @@ -273,6 +289,7 @@ function M.__test() assert.are.same(bars, { 1, 2 }) assert.are.same(fooxes, { 2, 1 }) end) + it('Maps with both functions and field names tables', function() local arr = { { foo = { bar = 1, qoox = { foox = 2, boox = 5 } } }, @@ -284,6 +301,30 @@ function M.__test() assert.are.same(bars, { 2, 4 }) end) + + it('Maps using index', function() + local arr1 = { + { { foo = 1, bar = 2 } }, + { { foo = 1, bar = 2 } }, + } + local arr2 = { + { foo = { 1, 2 } }, + { foo = { 1, 2 } }, + } + local arr3 = { + { { { { 'foo' } } } }, + } + + local mapped1 = M.map(arr1, 1, 'foo') + local mapped2 = M.map(arr2, 'foo', 2) + local mapped3 = M.map(arr3, 1, 1, 1, 1, function(s) + return ('%sbar'):format(s) + end) + + assert.are.same(mapped1, { 1, 1 }) + assert.are.same(mapped2, { 2, 2 }) + assert.are.same(mapped3, { 'foobar' }) + end) end) end