From f12a79bda685fb6027ffa8352494afdfcd22a59f Mon Sep 17 00:00:00 2001 From: Alexander Nortung Date: Thu, 28 Jul 2022 21:38:38 +0200 Subject: [PATCH] Adding nvim-cmp plugin (#25) * started adding nvim-cmp * nvim-cmp: added snippet option * nvim-cmp: added mapping option * nvim-cmp: added completion option * nvim-cmp: added confirmation config option * nvim-cmp: added formatting options * nvim-cmp: added matching option * nvim-cmp: added sorting option * nvim-cmp: added sources option * nvim-cmp: added a bunch of sources * nvim-cmp: auto enabling cmp source plugins should mostly work now * nvim-cmp: added view option * nvim_cmp: added window option * nvim-cmp: added experimental option * nvim-cmp: mappingPresets now works --- plugins/completion/nvim-cmp/cmp-helpers.nix | 54 +++ plugins/completion/nvim-cmp/default.nix | 397 ++++++++++++++++++ .../completion/nvim-cmp/sources/default.nix | 12 + plugins/default.nix | 2 + 4 files changed, 465 insertions(+) create mode 100644 plugins/completion/nvim-cmp/cmp-helpers.nix create mode 100644 plugins/completion/nvim-cmp/default.nix create mode 100644 plugins/completion/nvim-cmp/sources/default.nix diff --git a/plugins/completion/nvim-cmp/cmp-helpers.nix b/plugins/completion/nvim-cmp/cmp-helpers.nix new file mode 100644 index 000000000..ca39a57f9 --- /dev/null +++ b/plugins/completion/nvim-cmp/cmp-helpers.nix @@ -0,0 +1,54 @@ +{ lib, pkgs, ... }@attrs: +let + helpers = import ../../helpers.nix { lib = lib; }; +in with helpers; with lib; +{ + mkCmpSourcePlugin = { name, extraPlugins ? [], useDefaultPackage ? true, ... }: mkPlugin attrs { + inherit name; + extraPlugins = extraPlugins ++ (lists.optional useDefaultPackage pkgs.vimPlugins.${name}); + description = "Enable ${name}"; + }; + + pluginAndSourceNames = { + "luasnip" = "cmp_luasnip"; + "snippy" = "cmp-snippy"; + "ultisnips" = "cmp-nvim-ultisnips"; + "vsnip" = "cmp-vsnip"; + "buffer" = "cmp-buffer"; + "calc" = "cmp-calc"; + "dictionary" = "cmp-dictionary"; + "digraphs" = "cmp-digraphs"; + "omni" = "cmp-omni"; + "spell" = "cmp-spell"; + "nvim_lsp" = "cmp-nvim-lsp"; + "nvim_lsp_document_symbol" = "cmp-nvim-lsp-document-symbol"; + "nvim_lsp_signature_help" = "cmp-nvim-lsp-signature-help"; + "vim_lsp" = "cmp-vim-lsp"; + "path" = "cmp-path"; + "git" = "cmp-git"; + "conventionalcommits" = "cmp-conventionalcommits"; + "cmdline" = "cmp-cmdline"; + "cmp-cmdline-history" = "cmp-cmdline-history"; + "fuzzy_buffer" = "cmp-fuzzy-buffer"; + "fuzzy_path" = "cmp-fuzzy-path"; + "rg" = "cmp-rg"; + "fish" = "cmp-fish"; + "tmux" = "cmp-tmux"; + "zsh" = "cmp-zsh"; + "crates" = "crates-nvim"; + "npm" = "cmp-npm"; + "cmp-clippy" = "cmp-clippy"; + "cmp_tabnine" = "cmp-tabnine"; + "copilot" = "cmp-copilot"; + "dap" = "cmp-dap"; + "emoji" = "cmp-emoji"; + "greek" = "cmp-greek"; + "latex_symbols" = "cmp-latex-symbols"; + "look" = "cmp-look"; + "nvim_lua" = "cmp-nvim-lua"; + "pandoc_references" = "cmp-pandoc-references"; + "cmp_pandoc" = "cmp-pandoc-nvim"; + "treesitter" = "cmp-treesitter"; + "vimwiki-tags" = "cmp-vimwiki-tags"; + }; +} diff --git a/plugins/completion/nvim-cmp/default.nix b/plugins/completion/nvim-cmp/default.nix new file mode 100644 index 000000000..b0749dd3b --- /dev/null +++ b/plugins/completion/nvim-cmp/default.nix @@ -0,0 +1,397 @@ +{ pkgs, config, lib, ... }@args: +with lib; +let + cfg = config.programs.nixvim.plugins.nvim-cmp; + helpers = import ../../helpers.nix { lib = lib; }; + mkNullOrOption = helpers.mkNullOrOption; + cmpLib = import ./cmp-helpers.nix args; + # functionName should be a string + # parameters should be a list of strings + wrapWithFunction = functionName: parameters: let + parameterString = strings.concatStringsSep "," parameters; + in ''${functionName}(${parameterString})''; +in +{ + options.programs.nixvim.plugins.nvim-cmp = { + enable = mkEnableOption "Enable nvim-cmp"; + + performance = mkOption { + default = null; + type = types.nullOr (types.submodule ({...}: { + options = { + debounce = mkOption { + type = types.nullOr types.int; + default = null; + }; + throttle = mkOption { + type = types.nullOr types.int; + default = null; + }; + }; + })); + }; + + preselect = mkOption { + type = types.nullOr (types.enum [ "Item" "None" ]); + default = null; + example = ''"Item"''; + }; + + snippet = mkOption { + default = null; + type = types.nullOr (types.submodule ({...}: { + options = { + expand = mkOption { + type = types.nullOr types.str; + example = '' + function(args) + vim.fn["vsnip#anonymous"](args.body) -- For `vsnip` users. + -- require('luasnip').lsp_expand(args.body) -- For `luasnip` users. + -- require('snippy').expand_snippet(args.body) -- For `snippy` users. + -- vim.fn["UltiSnips#Anon"](args.body) -- For `ultisnips` users. + end + ''; + }; + }; + })); + }; + + mappingPresets = mkOption { + default = []; + type = types.listOf (types.enum [ + "insert" + "cmdline" +# Not sure if there are more or if this should just be str + ]); + description = "Mapping presets to use; cmp.mapping.preset.\${mappingPreset} will be called with the configured mappings"; + example = '' + [ "insert" "cmdline" ] + ''; + }; + mapping = mkOption { + default = null; + type = types.nullOr (types.attrsOf (types.either types.str (types.submodule ({...}: { + options = { + action = mkOption { + type = types.nonEmptyStr; + description = "The function the mapping should call"; + example = ''"cmp.mapping.scroll_docs(-4)"''; + }; + modes = mkOption { + default = null; + type = types.nullOr (types.listOf types.str); + example = ''[ "i" "s" ]''; + }; + }; + })))); + example = '' + { + "" = "cmp.mapping.confirm({ select = true })"; + "" = { + modes = [ "i" "s" ]; + action = '${""}' + function(fallback) + if cmp.visible() then + cmp.select_next_item() + elseif luasnip.expandable() then + luasnip.expand() + elseif luasnip.expand_or_jumpable() then + luasnip.expand_or_jump() + elseif check_backspace() then + fallback() + else + fallback() + end + end + '${""}'; + }; + } + ''; + }; + + completion = mkOption { + default = null; + type = types.nullOr (types.submodule ({...}: { + options = { + keyword_length = mkOption { + default = null; + type = types.nullOr types.int; + }; + + keyword_pattern = mkOption { + default = null; + type = types.nullOr types.str; + }; + + autocomplete = mkOption { + default = null; + type = types.nullOr types.str; + description = "Lua code for the event."; + example = ''"false"''; + }; + + completeopt = mkOption { + default = null; + type = types.nullOr types.str; + }; + }; + })); + }; + + confirmation = mkOption { + default = null; + type = types.nullOr (types.submodule ({...}: { + options = { + get_commit_characters = mkOption { + default = null; + type = types.nullOr types.str; + description = "Direct lua code as a string"; + }; + }; + })); + }; + + formatting = mkOption { + default = null; + type = types.nullOr (types.submodule ({...}: { + options = { + fields = mkOption { + default = null; + type = types.nullOr (types.listOf types.str); + example = ''[ "kind" "abbr" "menu" ]''; + }; + format = mkOption { + default = null; + type = types.nullOr types.str; + description = "A lua function as a string"; + }; + }; + })); + }; + + matching = mkOption { + default = null; + type = types.nullOr (types.submodule ({...}: { + options = { + disallow_fuzzy_matching = mkOption { + default = null; + type = types.nullOr types.bool; + }; + disallow_partial_matching = mkOption { + default = null; + type = types.nullOr types.bool; + }; + disallow_prefix_unmatching = mkOption { + default = null; + type = types.nullOr types.bool; + }; + }; + })); + }; + + sorting = mkOption { + default = null; + type = types.nullOr (types.submodule ({...}: { + options = { + priority_weight = mkOption { + default = null; + type = types.nullOr types.int; + }; + comparators = mkOption { + default = null; + type = types.nullOr types.str; + }; + }; + })); + }; + + auto_enable_sources = mkOption { + default = true; + description = '' + Scans the sources array and installs the plugins if they are known to nixvim. + ''; + }; + + sources = let + source_config = types.submodule ({...}: { + options = { + name = mkOption { + type = types.str; + description = "The name of the source."; + example = ''"buffer"''; + }; + + option = mkOption { + default = null; + type = with types; nullOr (attrsOf anything); + description = "If direct lua code is needed use helpers.mkRaw"; + }; + + keyword_length = mkOption { + default = null; + type = types.nullOr types.int; + }; + + keyword_pattern = mkOption { + default = null; + type = types.nullOr types.int; + }; + + trigger_characters = mkOption { + default = null; + type = with types; nullOr (listOf str); + }; + + priority = mkOption { + default = null; + type = types.nullOr types.int; + }; + + max_item_count = mkOption { + default = null; + type = types.nullOr types.int; + }; + + group_index = mkOption { + default = null; + type = types.nullOr types.int; + }; + }; + }); + in mkOption { + default = null; + type = with types; nullOr (either (listOf source_config) (listOf (listOf source_config))); + description = '' + The sources to use. + Can either be a list of sourceConfigs which will be made directly to a Lua object. + Or it can be a list of lists, which will use the cmp built-in helper function `cmp.config.sources`. + ''; + example = '' + [ + { name = "nvim_lsp"; } + { name = "luasnip"; } #For luasnip users. + { name = "path"; } + { name = "buffer"; } + ] + ''; + }; + + view = mkOption { + default = null; + type = types.nullOr (types.submodule ({...}: { + options = { + entries = mkOption { + default = null; + type = with types; nullOr (either str attrs); + }; + }; + })); + }; + + window = let + # Reusable options + border = with types; mkNullOrOption (either str (listOf str)) null; + winhighlight = mkNullOrOption types.str null; + zindex = mkNullOrOption types.int null; + in mkOption { + default = null; + type = types.nullOr (types.submodule ({...}: { + options = { + completion = mkOption { + default = null; + type = types.nullOr (types.submodule ({...}: { + options = { + inherit border winhighlight zindex; + }; + })); + }; + + documentation = mkOption { + default = null; + type = types.nullOr (types.submodule ({...}: { + options = { + inherit border winhighlight zindex; + max_width = mkNullOrOption types.int "Window's max width"; + max_height = mkNullOrOption types.int "Window's max height"; + }; + })); + }; + }; + })); + }; + + # This can be kept as types.attrs since experimental features are often removed or completely changed after a while + experimental = mkNullOrOption types.attrs "Experimental features"; + }; + + config = let + options = { + enabled = cfg.enable; + performance = cfg.performance; + preselect = if (isNull cfg.preselect) then null else helpers.mkRaw "cmp.PreselectMode.${cfg.preselect}"; + + # Not very readable sorry + # If null then null + # If an attribute is a string, just treat it as lua code for that mapping + # If an attribute is a module, create a mapping with cmp.mapping() using the action as the first input and the modes as the second. + mapping = let + mappings = if (isNull cfg.mapping) then null + else mapAttrs (bind: mapping: helpers.mkRaw (if isString mapping then mapping + else "cmp.mapping(${mapping.action}${optionalString (mapping.modes != null && length mapping.modes >= 1) ("," + (helpers.toLuaObject mapping.modes))})")) cfg.mapping; + luaMappings = (helpers.toLuaObject mappings); + wrapped = lists.fold (presetName: prevString: ''cmp.mapping.preset.${presetName}(${prevString})'') luaMappings cfg.mappingPresets; + in helpers.mkRaw wrapped; + snippet = { + expand = if (isNull cfg.snippet.expand) then null else helpers.mkRaw cfg.snippet.expand; + }; + completion = if (isNull cfg.completion) then null else { + keyword_length = cfg.completion.keyword_length; + keyword_pattern = cfg.completion.keyword_pattern; + autocomplete = if (isNull cfg.completion.autocomplete) then null else mkRaw cfg.completion.autocomplete; + completeopt = cfg.completion.completeopt; + }; + confirmation = if (isNull cfg.confirmation) then null else { + get_commit_characters = + if (isString cfg.confirmation.get_commit_characters) then helpers.mkRaw cfg.confirmation.get_commit_characters + else cfg.confirmation.get_commit_characters; + }; + formatting = if (isNull cfg.formatting) then null else { + fields = cfg.formatting.fields; + format = if (isNull cfg.formatting.format) then null else helpers.mkRaw cfg.formatting.format; + }; + matching = cfg.matching; + sorting = if (isNull cfg.sorting) then null else { + priority_weight = cfg.sorting.priority_weight; + comparators = if (isNull cfg.sorting.comparators) then null else helpers.mkRaw cfg.sorting.comparators; + }; + sources = cfg.sources; + view = cfg.view; + window = cfg.window; + experimental = cfg.experimental; + }; + in mkIf cfg.enable { + programs.nixvim = { + extraPlugins = [ pkgs.vimPlugins.nvim-cmp ]; + + extraConfigLua = '' + local cmp = require('cmp') + cmp.setup(${helpers.toLuaObject options}) + ''; + + # If auto_enable_sources is set to true, figure out which are provided by the user + # and enable the corresponding plugins. + plugins = let + flattened_sources = if (isNull cfg.sources) then [] else flatten cfg.sources; + # Take only the names from the sources provided by the user + found_sources = lists.unique (lists.map (source: source.name) flattened_sources); + # A list of known source names + known_source_names = attrNames cmpLib.pluginAndSourceNames; + + attrs_enabled = listToAttrs (map (name: { + name = cmpLib.pluginAndSourceNames.${name}; + value.enable = mkIf (elem name found_sources) true; + }) known_source_names); + in mkIf cfg.auto_enable_sources attrs_enabled; + }; + }; +} diff --git a/plugins/completion/nvim-cmp/sources/default.nix b/plugins/completion/nvim-cmp/sources/default.nix new file mode 100644 index 000000000..21b8af553 --- /dev/null +++ b/plugins/completion/nvim-cmp/sources/default.nix @@ -0,0 +1,12 @@ +{ lib, pkgs, ... }@attrs: +with lib; +let + cmpLib = import ../cmp-helpers.nix attrs; + cmpSourcesPluginNames = lib.attrValues cmpLib.pluginAndSourceNames; + pluginModules = lists.map (name: cmpLib.mkCmpSourcePlugin { inherit name; }) cmpSourcesPluginNames; +in +{ + # For extra cmp plugins + imports = [ + ] ++ pluginModules; +} diff --git a/plugins/default.nix b/plugins/default.nix index 0f818b15c..73eaae778 100644 --- a/plugins/default.nix +++ b/plugins/default.nix @@ -11,6 +11,8 @@ ./colorschemes/tokyonight.nix ./completion/coq.nix + ./completion/nvim-cmp + ./completion/nvim-cmp/sources ./git/fugitive.nix ./git/gitgutter.nix