completion

blink-cmp vs nvim-cmp

* as the config looks too hacking and too many issues remaining unsolved in github

* and nvim-cmp last maintain is 5 months ago, blink-cmp is 5 days ago.

* and minuet-ai-nvim delay in nvim-cmp and not solved, blink-cmp is async

# TODO: split providers into separate files
{ pkgs, ... }: { programs.neovim = {
  plugins = [
  pkgs.vimPlugins.blink-cmp-dictionary
  {
    # match unicode characters => match alphabet characters instead.
    # E.g. I don't want to completion a long CJK sentence.
    # E.g. I want alphabet next to CJK can be completed: "例子example"
    #      In original blink-fuzzy-lib, "example" cannot be completed.
    # (Due to the libblink_cmp_fuzzy being not easy to be patched,
    # the nix patch code becomes a large chunk as below.)
    plugin = pkgs.vimPlugins.blink-cmp.overrideAttrs (old: let
      postPatch = ''
        ## This is where texts being collected
        sed -i 's/\\p{L}/a-zA-Z/g' lua/blink/cmp/fuzzy/rust/lib.rs
        ## This is where trigger range is decided
        ## This change will break blink-fuzzy-lib check, so doCheck=false
        sed -i 's/\\p{L}/a-zA-Z/g' lua/blink/cmp/fuzzy/rust/keyword.rs
      '';
    in {
      # Here, postPatch does not effect blink-fuzzy-lib,
      # however, for consistency between source code and libblink_cmp_fuzzy.so,
      # I patch the source code as well.
      inherit postPatch;

      preInstall = let
        ext = pkgs.stdenv.hostPlatform.extensions.sharedLibrary;
        blink-fuzzy-lib = old.passthru.blink-fuzzy-lib.overrideAttrs { inherit postPatch; doCheck=false; };
      in ''
        mkdir -p target/release
        ln -s ${blink-fuzzy-lib}/lib/libblink_cmp_fuzzy${ext} target/release/libblink_cmp_fuzzy${ext}
      '';
    });
    type = "lua";
    config = /*lua*/ ''
      require("blink.cmp").setup({
        keymap = { preset = 'none',
          ['<Tab>']   = { 'select_next', 'fallback' },
          ['<S-Tab>'] = { 'select_prev', 'fallback' },
          ['<Up>']   = { function(cmp) return cmp.select_prev({ auto_insert = false }) end, 'fallback' },
          ['<Down>'] = { function(cmp) return cmp.select_next({ auto_insert = false }) end, 'fallback' },
          ['<CR>'] = { 'accept', 'fallback' },
          ['<A-1>'] = { function(cmp) cmp.accept({ index = 1 }) end },
          ['<A-2>'] = { function(cmp) cmp.accept({ index = 2 }) end },
          ['<A-3>'] = { function(cmp) cmp.accept({ index = 3 }) end },
          ['<A-4>'] = { function(cmp) cmp.accept({ index = 4 }) end },
          ['<A-5>'] = { function(cmp) cmp.accept({ index = 5 }) end },
          ['<A-6>'] = { function(cmp) cmp.accept({ index = 6 }) end },
          ['<A-7>'] = { function(cmp) cmp.accept({ index = 7 }) end },
          ['<A-8>'] = { function(cmp) cmp.accept({ index = 8 }) end },
          ['<A-9>'] = { function(cmp) cmp.accept({ index = 9 }) end },
          ['<A-0>'] = { function(cmp) cmp.accept({ index = 10 }) end },
          ['<A-a>'] = require('minuet').make_blink_map(),
        },
        completion = {
          documentation = { auto_show = true },
          menu = {
            draw = {
              columns = {
                {'item_idx'},
                {'kind_icon'}, {'label', 'label_description', gap = 1},
                {'source_name'},
              },
              components = {
                item_idx = {
                  text = function(ctx) return ctx.idx == 10 and '0' or ctx.idx >= 10 and ' ' or tostring(ctx.idx) end,
                  highlight = 'BlinkCmpSource' -- optional, only if you want to change its color
                },
              },
            },
          },
          list = { selection = { preselect = false }, },
        },
        sources = {
          default = {
            'lsp', 'path', 'cwd_path', 'buffer', 'snippets',
            'minuet',
            "dictionary"
          },
          providers = {
            -- > By default, the buffer source will only show when the LSP source returns no items
            -- Always show buffer completion, defaults to `{ 'buffer' }`
            lsp = { fallbacks = {}, },
            cwd_path = {
              name = 'CwdPath',
              module = 'blink.cmp.sources.path',
              -- same priority as the default path source
              score_offset = 3,
              -- Disable CwdPath according to following conditions:
              enabled = function()
                local buf_dir = vim.fn.expand(('#%d:p:h'):format(vim.api.nvim_get_current_buf()))
                -- Condition 1: Same dir → Path source already covers it.
                if vim.fn.resolve(buf_dir) == vim.fn.resolve(vim.fn.getcwd()) then
                  return false
                end
                -- Condition 2: Absolute path (~/ or /) → doesn't depend on cwd.
                do
                  local col = vim.api.nvim_win_get_cursor(0)[2]
                  local line = vim.api.nvim_get_current_line()
                  local prefix = line:sub(1, col):match("[^%s'\"`{}%[%]()<>,;:]*$") or ""
                  if prefix:match("^[~/]") ~= nil then
                    return false
                  end
                end
                return true
              end,
              opts = {
                get_cwd = function(_)
                  return vim.fn.getcwd()
                end,
              },
            },
            buffer = {
              opts = {
                -- copy from the default get_bufnrs (all visible buffers).
                get_bufnrs = function()
                  return vim
                    .iter(vim.api.nvim_list_wins())
                    :map(function(win) return vim.api.nvim_win_get_buf(win) end)
                    -- The default get_bufnrs filter out nofile buffers (codecompanion, vim help, ...).
                    -- Here, we remove the filter.
                    -- :filter(function(buf) return vim.bo[buf].buftype ~= 'nofile' end)
                    :totable()
                end,
              },
            },
            minuet = {
              name = 'minuet',
              module = 'minuet.blink',
              async = true,
              -- Should match minuet.config.request_timeout * 1000,
              -- since minuet.config.request_timeout is in seconds
              timeout_ms = 3000,
              score_offset = -1,
            },
            -- blink-cmp-dictionary vs blink-cmp-dat-word
            -- former can handle capitalization proper, while latter cannot
            dictionary = {
              name = 'dict',
              module = 'blink-cmp-dictionary',
              min_keyword_length = 3,
              score_offset = -4, -- lower priority than buffer's -3
              opts = {
                -- TODO: clean this words list:
                -- - remove word < 3 characters
                dictionary_files = { "${pkgs.fetchurl {
                  url = "https://github.com/dwyl/english-words/raw/8179fe68775df3f553ef19520db065228e65d1d3/words_alpha.txt";
                  sha256 = "sha256-PtDJRhDYvPfBG7tJxWqknHI00ytmgk35H1VBaeVy2kg=";
                }}" },
                dictionary_directories = { vim.fn.expand('~/Gist/dicts/blink-cmp') },
              },
            },
          },
        },
        cmdline = {
          -- TODO: blink-cmp cmdline cannot complete `'`,
          -- for example: `:h statusline` cannot complete to `:h 'statusline'`
          -- thus disable it currently
          enabled = false,
          keymap = { preset = 'inherit',
            ['<Tab>'] = { 'show_and_insert', 'select_next' },
            ['<CR>'] = { 'accept_and_enter', 'fallback' },
          },
          completion = { list = { selection = { preselect = false }, }, },
        },
      })
      vim.lsp.config('*', {
        capabilities = require('blink.cmp').get_lsp_capabilities(),
      })
    '';
  }];
};
}