vim-patch:9.1.1301: completion: cannot configure completion functions with 'complete'

Problem:  completion: cannot configure completion functions with
          'complete'
Solution: add support for setting completion functions using the f and o
          flag for 'complete' (Girish Palya)

This change adds two new values to the `'complete'` (`'cpt'`) option:
- `f` – invokes the function specified by the `'completefunc'` option
- `f{func}` – invokes a specific function `{func}` (can be a string or `Funcref`)

These new flags extend keyword completion behavior (e.g., via `<C-N>` /
`<C-P>`) by allowing function-based sources to participate in standard keyword
completion.

**Key behaviors:**

- Multiple `f{func}` values can be specified, and all will be called in order.
- Functions should follow the interface defined in `:help complete-functions`.
- When using `f{func}`, escaping is required for spaces (with `\`) and commas
  (with `\\`) in `Funcref` names.
- If a function sets `'refresh'` to `'always'`, it will be re-invoked on every
  change to the input text. Otherwise, Vim will attempt to reuse and filter
  existing matches as the input changes, which matches the default behavior of
  other completion sources.
- Matches are inserted at the keyword boundary for consistency with other completion methods.
- If finding matches is time-consuming, `complete_check()` can be used to
  maintain responsiveness.
- Completion matches are gathered in the sequence defined by the `'cpt'`
  option, preserving source priority.

This feature increases flexibility of standard completion mechanism and may
reduce the need for external completion plugins for many users.

**Examples:**

Complete matches from [LSP](https://github.com/yegappan/lsp) client. Notice the use of `refresh: always` and `function()`.

```vim
set cpt+=ffunction("g:LspCompletor"\\,\ [5]). # maxitems = 5

def! g:LspCompletor(maxitems: number, findstart: number, base: string): any
    if findstart == 1
        return g:LspOmniFunc(findstart, base)
    endif
    return {words: g:LspOmniFunc(findstart, base)->slice(0, maxitems), refresh: 'always'}
enddef
autocmd VimEnter * g:LspOptionsSet({ autoComplete: false, omniComplete: true })
```

Complete matches from `:iabbrev`.

```vim
set cpt+=fAbbrevCompletor

def! g:AbbrevCompletor(findstart: number, base: string): any
    if findstart > 0
        var prefix = getline('.')->strpart(0, col('.') - 1)->matchstr('\S\+$')
        if prefix->empty()
            return -2
        endif
        return col('.') - prefix->len() - 1
    endif
    var lines = execute('ia', 'silent!')
    if lines =~? gettext('No abbreviation found')
        return v:none  # Suppresses warning message
    endif
    var items = []
    for line in lines->split("\n")
        var m = line->matchlist('\v^i\s+\zs(\S+)\s+(.*)$')
        if m->len() > 2 && m[1]->stridx(base) == 0
            items->add({ word: m[1], info: m[2], dup: 1 })
        endif
    endfor
    return items->empty() ? v:none :
        items->sort((v1, v2) => v1.word < v2.word ? -1 : v1.word ==# v2.word ? 0 : 1)
enddef
```

**Auto-completion:**

Vim's standard completion frequently checks for user input while searching for
new matches. It is responsive irrespective of file size. This makes it
well-suited for smooth auto-completion. You can try with above examples:

```vim
set cot=menuone,popup,noselect inf

autocmd TextChangedI * InsComplete()

def InsComplete()
    if getcharstr(1) == '' && getline('.')->strpart(0, col('.') - 1) =~ '\k$'
        SkipTextChangedIEvent()
        feedkeys("\<c-n>", "n")
    endif
enddef

inoremap <silent> <c-e> <c-r>=<SID>SkipTextChangedIEvent()<cr><c-e>

def SkipTextChangedIEvent(): string
    # Suppress next event caused by <c-e> (or <c-n> when no matches found)
    set eventignore+=TextChangedI
    timer_start(1, (_) => {
        set eventignore-=TextChangedI
    })
    return ''
enddef
```

closes: vim/vim#17065

cbe53191d0

Temporarily remove bufname completion with #if 0 to make merging easier.

Co-authored-by: Girish Palya <girishji@gmail.com>
Co-authored-by: Christian Brabandt <cb@256bit.org>
Co-authored-by: glepnir <glephunter@gmail.com>
This commit is contained in:
zeertzjq
2025-05-31 16:38:28 +08:00
parent 0af6d6ff5e
commit 7651c43252
9 changed files with 1087 additions and 88 deletions

View File

@@ -45,6 +45,7 @@
#include "nvim/spellfile.h"
#include "nvim/spellsuggest.h"
#include "nvim/strings.h"
#include "nvim/tag.h"
#include "nvim/terminal.h"
#include "nvim/types_defs.h"
#include "nvim/vim_defs.h"
@@ -54,10 +55,12 @@
# include "optionstr.c.generated.h"
#endif
static const char e_unclosed_expression_sequence[]
= N_("E540: Unclosed expression sequence");
static const char e_illegal_character_after_chr[]
= N_("E535: Illegal character after <%c>");
static const char e_comma_required[]
= N_("E536: Comma required");
static const char e_unclosed_expression_sequence[]
= N_("E540: Unclosed expression sequence");
static const char e_unbalanced_groups[]
= N_("E542: Unbalanced groups");
static const char e_backupext_and_patchmode_are_equal[]
@@ -836,40 +839,45 @@ const char *did_set_commentstring(optset_T *args)
return NULL;
}
/// The 'complete' option is changed.
/// Check if value for 'complete' is valid when 'complete' option is changed.
const char *did_set_complete(optset_T *args)
{
char **varp = (char **)args->os_varp;
char buffer[LSIZE];
// check if it is a valid value for 'complete' -- Acevedo
for (char *s = *varp; *s;) {
while (*s == ',' || *s == ' ') {
s++;
}
if (!*s) {
break;
}
if (vim_strchr(".wbuksid]tUf", (uint8_t)(*s)) == NULL) {
return illegal_char(args->os_errbuf, args->os_errbuflen, (uint8_t)(*s));
}
if (*++s != NUL && *s != ',' && *s != ' ') {
if (s[-1] == 'k' || s[-1] == 's') {
// skip optional filename after 'k' and 's'
while (*s && *s != ',' && *s != ' ') {
if (*s == '\\' && s[1] != NUL) {
s++;
}
s++;
}
for (char *p = *varp; *p;) {
memset(buffer, 0, LSIZE);
char *buf_ptr = buffer;
int escape = 0;
// Extract substring while handling escaped commas
while (*p && (*p != ',' || escape) && buf_ptr < (buffer + LSIZE - 1)) {
if (*p == '\\' && *(p + 1) == ',') {
escape = 1; // Mark escape mode
p++; // Skip '\'
} else {
if (args->os_errbuf != NULL) {
vim_snprintf(args->os_errbuf, args->os_errbuflen,
_("E535: Illegal character after <%c>"),
*--s);
return args->os_errbuf;
}
return "";
escape = 0;
*buf_ptr++ = *p;
}
p++;
}
*buf_ptr = NUL;
if (vim_strchr(".wbuksid]tUfo", (uint8_t)(*buffer)) == NULL) {
return illegal_char(args->os_errbuf, args->os_errbuflen, (uint8_t)(*buffer));
}
if (!vim_strchr("ksf", (uint8_t)(*buffer)) && *(buffer + 1) != NUL) {
if (args->os_errbuf != NULL) {
vim_snprintf(args->os_errbuf, args->os_errbuflen,
_(e_illegal_character_after_chr), (uint8_t)(*buffer));
return args->os_errbuf;
}
}
// Skip comma and spaces
while (*p == ',' || *p == ' ') {
p++;
}
}
return NULL;