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 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 diff --git a/docs/spec/0.1/functional.md b/docs/spec/0.1/functional.md index 070c23b..717e0e3 100644 --- a/docs/spec/0.1/functional.md +++ b/docs/spec/0.1/functional.md @@ -4,19 +4,21 @@ * [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) * [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) @@ -98,6 +100,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 @@ -120,9 +130,19 @@ vim.g.cpptools = { # 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. diff --git a/docs/spec/0.1/technical.md b/docs/spec/0.1/technical.md index 0503f80..5cfc052 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,78 @@ 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: 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). +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. +2. 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.` + + - **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. + +- **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 + - **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 + +For now consult the `lua/cpp-tools/lib/luakittens/parser` file, the `__test` function and the `only_parse()` function. 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 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/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/config.lua b/lua/cpp-tools/lib/config.lua new file mode 100644 index 0000000..0e1be0f --- /dev/null +++ b/lua/cpp-tools/lib/config.lua @@ -0,0 +1,400 @@ +---@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 + +---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 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) + + 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/lua/cpp-tools/lib/iter/init.lua b/lua/cpp-tools/lib/iter/init.lua index f2ee9b7..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,6 +64,47 @@ function M.partition(range, pred, proj_beg) vim.iter(range):map(proj_beg):filter(fp.nah(pred)):totable() or {} end +-- 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 ... integer|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 == 'number' then + return final[mapping] + elseif mapping_type == 'function' then + return mapping(final) + else + assert( + false, + ('The type of mapping must be a function, a string or an integer, not [%s]'):format(mapping_type) + ) + end + end) + end) + :totable() +end + ---@package function M.__test() describe('`array_equals()`', function() @@ -197,6 +240,92 @@ 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) + + 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 return M diff --git a/lua/cpp-tools/lib/paths.lua b/lua/cpp-tools/lib/paths.lua new file mode 100644 index 0000000..17cf6a3 --- /dev/null +++ b/lua/cpp-tools/lib/paths.lua @@ -0,0 +1,137 @@ +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 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() + --[=[ + 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/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 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 ]; }; } 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', + }, + }, +} 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