diff --git a/docs/modules.md b/docs/modules.md index f9eb945..0666fbb 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -1,5 +1,30 @@ # Doom Nvim Modules +- [Doom Nvim Modules](#doom-nvim-modules) + * [Introduction](#introduction) + * [All modules](#all-modules) + + [`core` modules](#-core--modules) + + [`features` modules](#-features--modules) + + [`langs` modules](#-langs--modules) + * [Configuring modules](#configuring-modules) + + [Quick Guide](#quick-guide) + + [Advanced guide](#advanced-guide) + - [Module lifecycle](#module-lifecycle) + - [Limitations](#limitations) + * [Direct plugin access](#direct-plugin-access) + * [Conditional keybinds and autocommands](#conditional-keybinds-and-autocommands) + - [Module spec](#module-spec) + * [Implementing your own doom module](#implementing-your-own-doom-module) + + [1. Setting up](#1-setting-up) + + [2. Adding autocommands](#2-adding-autocommands) + + [3. Enabling and testing your module](#3-enabling-and-testing-your-module) + + [4. Adding the character counter](#4-adding-the-character-counter) + + [5. Adding commands to get and reset the count](#5-adding-commands-to-get-and-reset-the-count) + + [6. Adding keybinds](#6-adding-keybinds) + + [7. Adding and lazyloading a plugin](#7-adding-and-lazyloading-a-plugin) + + [8. Exposing settings to the user](#8-exposing-settings-to-the-user) + + [9. You're done! Final output](#9-you-re-done---final-output) + ## Introduction Doom Nvim consists of around 40 plugins and growing. @@ -106,16 +131,50 @@ comment_module.binds = { > **NOTE:** If you have the `lua` language module and `lsp` feature module enabled, all of these properties should be auto completeable. -### Advanced +### Advanced guide +#### Module lifecycle +1. Doom nvim reads `modules.lua` to determine which modules to load. +2. Doom loads the config for each module, saves it to `doom.modules` global object. +3. User can override the settings for a module in `config.lua` using the `doom.modules` global object. +4. Doom executes the modules, installing plugins and setting keybinds/autocommands. + #### Limitations -#### Module lifecycle -1. Modules are defined in `modules.lua`. -2. Doom loads the config for each module, saves it to `doom.modules`. -3. User can override the settings for a module in `config.lua` -4. Doom executes the module, loads and installs the dependencies, sets the keybinds and autocommands. +##### Direct plugin access + +The main limitation is that plugins are not yet loaded when `config.lua` is executed meaning you +cant have direct access to plugins. We're working on a more concrete solution but for now you can +splice your own custom configure function with the default one. + +```lua +--- config.lua + +local lsp = doom.modules.lsp +local old_nvim_cmp_config_function = lsp.configs['nvim-cmp'] +lsp.configs['nvim-cmp'] = function() + old_nvim_cmp_config_function() -- Run the default config + local cmp = require("cmp") -- direct access to plugin +end +``` + +##### Conditional keybinds and autocommands + +Sometimes keybinds and autocommands will be conditionally added/disabled depending on if another module +is enabled or if a specific config option is set. In this case we use a function that returns a valid config +instead of a simple table. + +This is not ideal as it's harder to `vim.inspect` the defaults. As a workaround you can `vim.inspect` the returned value. + +```lua +-- config.lua + +print(type(doom.modules.lsp.binds)) -- "table" (not conditional) +print(vim.inspect(doom.modules.lsp.binds)) -- Shows config +print(type(doom.modules.telescope.binds)) -- "function" (adds extra keybinds if "lsp" module is enabled) +print(vim.inspect(doom.modules.telescope.binds())) -- You must execute the function to see the config. +``` #### Module spec @@ -123,7 +182,7 @@ These are the possible values a ```lua module.settings: table -- Table of settings that can be tweaked -module.uses: table> -- Table of packer specs +module.uses: table> -- Table of packer specs module.configs: table -- Table of plugin config functions relating to the packer specs module.binds: table|function -> table -- Table of NestConfig or function that returns Table of NestConfig module.autocmds: table|function -> table -- Table of AutoCmds (see below) or function that returns a table of AutoCmds @@ -181,3 +240,400 @@ module.autocmds = function() return autocmds end ``` + +## Implementing your own doom module + +I will use an example of implementing a module that counts the number of chars that you've typed. +This module will: + - Use autocommands to count the number of chars in a buffer when you enter insert mode vs when you leave insert mode + - Add it to an accumulated sum + - Provide keybinds + commands to restart or display total count + - Use a plugin to display the results in a popup window + - Include some settings to change the displayed output + +### 1. Setting up + +> Modules are loaded from the `lua/doom/modules/` folder. Within this folder there is a `features/`, `langs/` and `core/` directory. +> If you look at [`modules.lua`](../modules.lua) you'll see that this maps 1:1 with the returned data structure (except for `core` modules which can't be disabled). +> +> Because modules are implemented as folders with an `init.lua` inside, they must be named after valid folder names. +> Best practices are: +> - Seperate words with an underscore, this is so the plugin can be represented as a lua variable +> - Name the module after the functionality rather than the plugin it uses. +> +> If you're adding language support, add a new folder module to `lua/doom/modules/langs/`, else if +> it's a new feature add a directory to `lua/doom/module/features/`. + +For our example of adding char counting plugin I will add a folder called `lua/doom/modules/langs/char_counter/` and create a new `init.lua` +inside of it. +```lua +-- lua/doom/modules/features/char_counter/init.lua +local char_counter = {} + +return char_counter +``` + +### 2. Adding autocommands + +> Autocommands are set using the `module.autocmds` field. And follow the structure of +> ```lua +> module.autocmds = { +> { "{event}", "{aupat}", "command or function" } +> } +> ``` + +For our example we need to hook into the [InsertEnter](https://neovim.io/doc/user/autocmd.html#InsertEnter) +and [InsertLeave](https://neovim.io/doc/user/autocmd.html#InsertLeave) auto commands. + +```lua +-- lua/doom/modules/features/char_counter/init.lua +char_counter.autocmds = { + { "InsertEnter", "*", function () + print('Entered insert mode') + end}, + { "InsertLeave", "*", function () + print('Exited insert mode') + end}, +} +``` + +### 3. Enabling and testing your module + +Now you can enable the module in `modules.lua`! Once enabled, restart your doom-nvim instance and check +`:messages` to see if it's printing correctly. + +```lua +-- modules.lua +return { + features = { + "char_counter" -- Must match the name of the folder i.e. `lua/doom/modules/features/char_counter` + } +} +``` + +### 4. Adding the character counter + +Because modules are just tables, you can add any properties or functions that you need to the module table. +To implement the character counter we will add a few fields to the module table. Unless you want users to +access these fields we recommend prefixing them with an underscore. +1. A function that gets the character count of a buffer. +2. A field to store the character count when we enter insert mode. +3. A field to store the accumulated count when we exit insert mode. + +We will also check if the [`buftype`](https://neovim.io/doc/user/options.html#'buftype') is empty, this +means we wont count other interactive buffers like terminals, prompts or quick fix lists. + +```lua +-- lua/doom/modules/features/char_counter/init.lua + +local char_counter = {} + +char_counter._insert_enter_char_count = nil +char_counter._accumulated_difference = 0 +char_counter._get_current_buffer_char_count = function() + local lines = vim.api.nvim_buf_line_count(0) + local chars = 0 + for _, line in ipairs(vim.api.nvim_buf_get_lines(0, 0, lines, false)) do + chars = chars + #line + end + return chars +end + +char_counter.autocmds = { + { "InsertEnter", "*", function () + -- Only operate on normal file buffers + print(("buftype: %s"):format(vim.bo.buftype)) + if vim.bo.buftype == "" then + -- Store current char count + char_counter._insert_enter_char_count = char_counter._get_current_buffer_char_count() + end + end}, + { "InsertLeave", "*", function () + -- Only operate on normal file buffers + if vim.bo.buftype == "" and char_counter._insert_enter_char_count then + -- Find the amount of chars added or removed + local new_count = char_counter._get_current_buffer_char_count() + local diff = new_count - char_counter._insert_enter_char_count + print(new_count, diff) + -- Add the difference to the accumulated total + char_counter._accumulated_difference = char_counter._accumulated_difference + diff + print(('Accumulated difference %s'):format(char_counter._accumulated_difference)) + end + end}, +} + +return char_counter +``` + +### 5. Adding commands to get and reset the count + +Using the `module.cmds` property we can define and expose vim commands to the user. Here we will define a +`:CountPrint` and `:CountReset` command. + +```lua +-- lua/doom/modules/features/char_counter/init.lua + +char_counter.cmds = { + { "CountPrint", function () + local msg = ("char_counter: You have typed %s characters since I started counting."):format(char_counter._accumulated_difference) + vim.notify(msg, "info") + end}, + { "CountReset", function () + char_counter._accumulated_difference = 0 + vim.notify("char_counter: Reset count!", "info") + end} +} +``` +> **NOTE**: Instead of using a function you can also provide a string that will be executed using `vim.cmd` + +Now restart doom nvim and run `:CountPrint` and `:CountReset` to test it out. + +### 6. Adding keybinds + +Keybinds are provided using the `module.binds` field. We use a modified [nest.nvim]() config that integrates with whichkey and nvim-mapper. +You can read more about it [here](https://github.com/connorgmeehan/nest.nvim/tree/integrations-api#quickstart-guide) but generally you should +provide the `name` field for all entries so it displays in whichkey. + +```lua +-- lua/doom/modules/features/char_counter/init.lua + +char_counter.binds = { + { 'i', name = '+info', { -- Adds a new `whichkey` folder called `+info` + { 'c', ':CountPrint', name = 'Print new chars' }, -- Binds `:CountPrint` to `ic` + { 'r', ':CountReset', name = 'Reset char count' } -- Binds `:CountPrint` to `ic` + } } +} +``` +> **NOTE**: Instead of a cmd you can also provide a lua function that will be executed when the keybind is triggered. + +### 7. Adding and lazyloading a plugin + +Plugins are added using the `module.uses` field and are configured using the `module.configs` field. +We use the repository name as a key to connect the plugin to its config function. +The API for `module.uses` is passed to Packer nvim's use function. [DOCS](https://github.com/wbthomason/packer.nvim#specifying-plugins) + +In this example I will add [nui.nvim](https://github.com/MunifTanjim/nui.nvim) to display the results in a popup when +the user uses the `CountPrint` command. + +```lua +-- lua/doom/modules/features/char_counter/init.lua + +-- Add these two fields to `char_counter` at the top of the file. +char_counter.uses = { + ["nui.nvim"] = { + "MunifTanjim/nui.nvim", + cmd = { "CountPrint" } -- Here, nui.nvim wont be loaded until user does the `ic` or `:CountPrint` command. + } +} + +char_counter.configs = { + ["nui.nvim"] = function() + -- Log when nui loads so we can check that it's being lazy loaded correctly + vim.notify("char_counter: nui.nvim loaded", "info") + + -- If your plugin requires a `.setup({ ... config ... })` function, this is where you'd execute it. + + -- WARNING: Because of how Packer compiles plugin configs, this function does not have direct access to `char_counter` table. + -- Instead you must re-define it from the `doom` global object as `local char_counter` = doom.modules.char_counter + end +} + +-- Modify `char_counter.cmds` + +char_counter.cmds = { + { "CountPrint", function () + -- We can ensure that nui has loaded due to the `cmd = { "CountPrint" }` in the plugin's config + local Popup = require('nui.popup') + + local popup = Popup({ + position = '50%', + size = { + width = 80, + height = 40, + }, + border = { + padding = { + top = 2, + bottom = 2, + left = 3, + right = 3, + }, + }, + style = "rounded", + enter = true, + buf_options = { + modifiable = true, + readonly = true, + } + }) + popup:mount() + popup:map("n", "", function() popup:unmount() end) + + local msg = ("char_counter: You have typed %s characters since I started counting."):format(char_counter._accumulated_difference) + vim.api.nvim_buf_set_lines(popup.bufnr, 0, 1, false, { msg }) + end}, + { "CountReset", function () + char_counter._accumulated_difference = 0 + vim.notify("char_counter: Reset count!", "info") + end} +} +``` + +### 8. Exposing settings to the user + +In order to keep doom-nvim flexible, it's best practice to expose settings for the module. A common practice is just to expose the entire config +object. This will allow users to tweak the config in their `config.lua` file without replacing and rewriting all of the logic for a small change. + + +```lua +-- lua/doom/modules/features/char_counter/init.lua + +-- Copy the settings that are passed to the `Popup` function, place them in `char_counter.settings.popup` +char_counter.settings = { + popup = { + position = '50%', + size = { + width = 80, + height = 40, + }, + border = { + padding = { + top = 2, + bottom = 2, + left = 3, + right = 3, + }, + }, + style = "rounded", + enter = true, + buf_options = { + modifiable = true, + readonly = true, + } + } +} + +-- Modify the Popup function +char_counter.cmds = { + { "CountPrint", function () + local Popup = require('nui.popup') + + local popup = Popup(char_counter.settings.popup) -- Configured via the `settings.popup` field. + + popup:mount() + popup:map("n", "", function() popup:unmount() end) + + local msg = ("char_counter: You have typed %s characters since I started counting."):format(char_counter._accumulated_difference) + vim.api.nvim_buf_set_lines(popup.bufnr, 0, 1, false, { msg }) + end}, + { "CountReset", function () + char_counter._accumulated_difference = 0 + vim.notify("char_counter: Reset count!", "info") + end} +} +``` + +### 9. You're done! Final output + +If you'd just like to look at the end result, or if you're comparing why your implementation didn't work, here is the final working output. + +```lua +-- lua/doom/modules/features/char_counter/init.lua +local char_counter = {} + +char_counter.settings = { + popup = { + position = '50%', + size = { + width = 80, + height = 40, + }, + border = { + padding = { + top = 2, + bottom = 2, + left = 3, + right = 3, + }, + }, + style = "rounded", + enter = true, + buf_options = { + modifiable = true, + readonly = true, + } + } +} + +char_counter.uses = { + ["nui.nvim"] = { + "MunifTanjim/nui.nvim", + cmd = { "CountPrint" } + } +} + +char_counter.configs = { + ["nui.nvim"] = function() + vim.notify("char_counter: nui.nvim loaded", "info") + end +} + +char_counter._insert_enter_char_count = nil +char_counter._accumulated_difference = 0 +char_counter._get_current_buffer_char_count = function() + local lines = vim.api.nvim_buf_line_count(0) + local chars = 0 + for _, line in ipairs(vim.api.nvim_buf_get_lines(0, 0, lines, false)) do + chars = chars + #line + end + return chars +end + +char_counter.autocmds = { + { "InsertEnter", "*", function () + -- Only operate on normal file buffers + print(("buftype: %s"):format(vim.bo.buftype)) + if vim.bo.buftype == "" then + -- Store current char count + char_counter._insert_enter_char_count = char_counter._get_current_buffer_char_count() + end + end}, + { "InsertLeave", "*", function () + -- Only operate on normal file buffers + if vim.bo.buftype == "" and char_counter._insert_enter_char_count then + -- Find the amount of chars added or removed + local new_count = char_counter._get_current_buffer_char_count() + local diff = new_count - char_counter._insert_enter_char_count + print(new_count, diff) + -- Add the difference to the accumulated total + char_counter._accumulated_difference = char_counter._accumulated_difference + diff + print(('Accumulated difference %s'):format(char_counter._accumulated_difference)) + end + end}, +} + +char_counter.cmds = { + { "CountPrint", function () + local Popup = require('nui.popup') + local popup = Popup(char_counter.settings.popup) + popup:mount() + popup:map("n", "", function() popup:unmount() end) + + local msg = ("char_counter: You have typed %s characters since I started counting."):format(char_counter._accumulated_difference) + vim.api.nvim_buf_set_lines(popup.bufnr, 0, 1, false, { msg }) + end}, + { "CountReset", function () + char_counter._accumulated_difference = 0 + vim.notify("char_counter: Reset count!", "info") + end} +} + +char_counter.binds = { + { 'i', name = '+info', { -- Adds a new `whichkey` folder called `+info` + { 'c', ':CountPrint', name = 'Print new chars' }, -- Binds `:CountPrint` to `ic` + { 'r', ':CountReset', name = 'Reset char count' } -- Binds `:CountPrint` to `ic` + } } +} + +return char_counter +```