Skip to content

Option to let blink-cmp autocomplete keep the capitalization of the currently typed word. #845

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
1 task done
yontuh opened this issue Apr 22, 2025 · 1 comment
Open
1 task done

Comments

@yontuh
Copy link

yontuh commented Apr 22, 2025

⚠️ Please verify that this feature request has NOT been suggested before.

  • I checked and didn't find a similar feature request

🏷️ Feature Type

API Additions

🔖 Feature description

When writing, the blink-cmp buffer autocomplete looks at the words in the buffer and provides auto completion capabilities with those. However, it will replace the currently typed word with one with a different capitalization, if the word in the buffer had that capitalization. Specifically keeping the first letter might be the most practical. This is what blink-cmp's documentation suggests. The case for this being built in is that using neovim to write is quite popular, and there are very few edge cases to decide not to enable this feature. The second reason is that the alternative solution (which I haven't gotten to work) would be ugly (as blink-cmp's example shows.

To provide an example use case of this issue, if I wanted to auto complete the word however (used above), with the buffer being the text above, it would complete to However, not however. Especially with words like capitalization, typing it out is unnecessarily inefficient.

✔️ Solution

I would like an option to enable the previously described functionality. I don't know what the ideal implementation details would be.

❓ Alternatives

Blink-cmp offers a bit of a hacky solution as described above. Here is my attempt of getting this to work. Does not work!

      autocomplete = {
        "blink-cmp" = {
          enable = true;
          friendly-snippets.enable = true;
          sourcePlugins.spell.enable = true;
          setupOpts = {
            sources = {
              # List your desired sources including 'buffer'
              default = ["lsp" "snippets" "copilot" "path" "spell"];

              # --- Provider Specific Settings ---
              providers = {
                # Settings for the 'buffer' provider
                buffer = {
                  # Integrate the transform_items function here
                  transform_items = lib.generators.mkLuaInline ''
                    function (a, items)
                      local keyword = a.get_keyword()
                      local correct, case
                      -- Check if keyword starts with a lowercase letter
                      if keyword:match('^%l') then
                          correct = '^%u%l+$' -- Pattern for correcting: Uppercase followed by lowercase
                          case = string.lower -- Function to apply to the first letter
                      -- Check if keyword starts with an uppercase letter
                      elseif keyword:match('^%u') then
                          correct = '^%l+$'    -- Pattern for correcting: All lowercase
                          case = string.upper -- Function to apply to the first letter
                      -- If keyword doesn't start with a letter, do nothing
                      else
                          return items
                      end

                      -- Avoid duplicates introduced by the case correction
                      local seen = {}
                      local out = {}
                      for _, item in ipairs(items) do
                          local raw = item.insertText
                          -- If the item matches the pattern needing correction
                          if raw:match(correct) then
                              -- Apply the case change to the first letter
                              local text = case(raw:sub(1,1)) .. raw:sub(2)
                              item.insertText = text
                              item.label = text -- Update the label as well
                          end
                          -- Only add the item if we haven't seen this insertText before
                          if not seen[item.insertText] then
                              seen[item.insertText] = true
                              table.insert(out, item)
                          end
                      end
                      return out -- Return the potentially modified and de-duplicated list
                    end
                  '';
                };

                copilot = {
                  name = "copilot";
                  module = lib.mkForce "blink-cmp-copilot";
                  score_offset = 100;
                  async = true;
                };
              };
            };
          };
        };
      };

📝 Additional Context

No response

@yontuh
Copy link
Author

yontuh commented Apr 22, 2025

Update, I have a code snippet that mostly works, with an edge case. If the current line was created with a command like o, as opposed to entering from the previous line, and the first letter of the word is right at the start of the line, the suggestion will ignore the case you use. I don't know why this happens.

      # --- Autocomplete ---
      autocomplete = {
        "blink-cmp" = {
          enable = true;
          friendly-snippets.enable = true;
          sourcePlugins = {
            spell = {enable = true;};
          };
          setupOpts = {
            sources = {
              default = ["lsp" "buffer" "snippets" "copilot" "path" "spell"];
              providers = {
                buffer = {
                  transform_items = lib.generators.mkLuaInline ''
                    function (a, items)
                        local keyword = a.get_keyword()
                        local prefer_lowercase = keyword:match('^%l')
                        local prefer_uppercase = keyword:match('^%u')
                        local needs_filtering = prefer_lowercase or prefer_uppercase
                        local case_func = nil
                        if prefer_lowercase then case_func = string.lower
                        elseif prefer_uppercase then case_func = string.upper
                        end
                        local out = {}
                        local seen = {};
                        for _, item in ipairs(items) do
                            local original_text = item.insertText
                            if original_text == nil or original_text == "" then goto continue end
                            local first_char = original_text:sub(1, 1)
                            local text_to_consider = original_text
                            local transformed = false
                            if needs_filtering then
                                local starts_lower = first_char:match('^%l')
                                local starts_upper = first_char:match('^%u')
                                if prefer_lowercase then
                                    if starts_lower then
                                        text_to_consider = original_text
                                        transformed = false
                                    elseif starts_upper and #original_text > 0 and original_text:sub(2) == string.lower(original_text:sub(2)) then
                                        text_to_consider = case_func(first_char) .. original_text:sub(2)
                                        transformed = true
                                    else
                                        goto continue -- Skip item (e.g., ALLCAPS)
                                    end
                                elseif prefer_uppercase then
                                    if starts_upper then
                                        text_to_consider = original_text
                                        transformed = false
                                    elseif starts_lower and #original_text > 0 then
                                        text_to_consider = case_func(first_char) .. original_text:sub(2)
                                        transformed = true
                                    else
                                        goto continue
                                    end
                                end
                            end
                            if not seen[text_to_consider] then
                                seen[text_to_consider] = true
                                if transformed then
                                    local transformed_item = vim.deepcopy(item)
                                    transformed_item.insertText = text_to_consider
                                    transformed_item.label = text_to_consider
                                    table.insert(out, transformed_item)
                                else
                                    table.insert(out, vim.deepcopy(item))
                                end
                            end
                            ::continue::
                        end
                        return out
                    end
                  '';
                }; # End buffer provider
              }; # End providers
            }; # End sources
          }; # End setupOpts
        }; # End blink-cmp settings
      }; # End autocomplete settings

This is sloppy, mostly AI generated code. There is likely a better way to get this working for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant