vim / vim

The official Vim repository
https://www.vim.org
Vim License
36.64k stars 5.46k forks source link

provide a "sid()" function to manually expand the "s:" function scope or the pseudo-key "<SID>" #6602

Closed lacygoill closed 4 years ago

lacygoill commented 4 years ago

Is your feature request about something that is currently impossible or hard to do? Please describe the problem.

It is not easy to manually expand the s: function scope or the pseudo-key <SID>.

That's an issue – for example – when we need to assign a script-local function name to an option; this doesn't work:

let &l:includeexpr = 's:my_includeexpr()'

When pressing gf on a path which can't be found, E120 is raised:

E120: Using <SID> not in a script context: s:my_includeexpr

Describe the solution you'd like

A sid() function which expands the s: function scope and the <SID> pseudo-key into the string <SNR>123_, where 123is the id of the current script. With it, one could write:

let &l:includeexpr = sid() .. 'my_includeexpr()'

Describe alternatives you've considered

The current general workaround is to emulate sid() with a custom function:

if !exists('s:SID')
    fu s:SID() abort
        return expand('<sfile>')->matchstr('<SNR>\zs\d\+\ze_SID$')->str2nr()
    endfu
    const s:SID = s:SID()->printf('<SNR>%d_')
    delfu s:SID
endif

let &l:includeexpr = s:SID .. 'my_includeexpr()'

It works, but it makes the code more verbose. It would be easier to read/write if we had a builtin sid() function.

As an example, that's what vim-matchup does:

function! s:snr()
  return str2nr(matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze_snr$'))
endfunction
let s:sid = printf("\<SNR>%d_", s:snr())

and vim-Verdin:

function! s:SID() abort
  return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze_SID$')
endfunction
let s:SID = printf("\<SNR>%s_", s:SID())
delfunction s:SID

and vim-sandwich:

function! s:SID() abort
  return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze_SID$')
endfunction
let s:SID = printf("\<SNR>%s_", s:SID())
delfunction s:SID

Sometimes, we can also use function():

let &l:includeexpr = function('s:my_includeexpr')->string() .. '()'

But it looks awkward, and a builtin sid() would still make the code easier to read/write.

Besides, we have to make sure that s:my_includeexpr() is defined before setting 'includeexpr'. Similarly, with s:SID(), we have to make sure that the function is defined before we invoke it.

With sid(), there would be no such requirement.

Also, note that function() does not always work. For example, it doesn't for 'operatorfunc':

nno <expr> <c-b> <sid>setup()
fu s:setup()
    let &opfunc = function('s:op')->string()
    return 'g@'
endfu
fu s:op(type)
    echom a:type
endfu
call feedkeys("\<c-b>l")
E117: Unknown function: function('<SNR>1_op')

Nor for 'completefunc':

let s:matches = ['foo', 'bar', 'baz']
fu s:complete_words(findstart, base) abort
    if a:findstart
        return searchpos('\<', 'bnW', line('.'))[1] - 1
    else
        return filter(copy(s:matches), {_, v -> stridx(v, a:base) == 0})
    endif
endfu
let &l:cfu = function('s:complete_words')->string()
call feedkeys("i\<c-x>\<c-u>")
E117: Unknown function: function('<SNR>1_complete_words')

And more generally, it probably doesn't work for any option which expects a function name, instead of an expression.

Additional context

It's not an issue just when setting an option. It's also an issue when starting a timer:

call timer_start(0, 's:func')
fu s:func(_) abort
    echom 'test'
endfu
E120: Using <SID> not in a script context: s:func

And when we need to invoke a script-local function from a command-line populated by a script:

noremap <expr><silent> <c-b> Func()
fu Func()
    let mode = mode(1)
    if mode is# "\<c-v>"
        let mode = "\<c-v>\<c-v>"
    endif
    return string(mode)->printf(":\<c-u>call s:func(%s)\<cr>")
endfu
fu s:func(mode)
    echom 'the mapping was pressed while in ' .. a:mode .. ' mode'
endfu
call feedkeys("\<c-b>")
E81: Using <SID> not in a script context

And when we pass the name of script-local function as a third optional argument to input():

fu s:CompleteWords(_a, _l, _p) abort
    return getline(1, '$')->join(' ')->split('\s\+')
        \ ->filter('v:val =~# "^\\a\\k\\+$"')
        \ ->sort()->uniq()->join("\n")
endfu
let word = input('word: ', '', 'custom,s:CompleteWords')
" press Tab while on the input line
E120: Using <SID> not in a script context: s:func

If such a function was provided, I think it should accept an optional boolean argument to additionally make Vim expand the pseudo-key <SNR> into a byte sequence (e.g. <80><fd>R). Most of the time, it's either not needed (e.g. in a timer's callback, or in an option setting), or not desirable (e.g. when invoking a script-local function from a command-line populated by a script). But sometimes, it is needed. See here and there for 2 examples.

:echo sid()
<SNR>123_

:echo sid(1)
<80><fd>R123_

In the future, we may not need sid() to set options, because all the options expecting a function name may accept lambda expressions (and maybe funcrefs/partials?):

I recently sent an email to the list about the support for using lambda functions with the following options:

balloonexpr, charconvert, completefunc, diffexpr, foldexpr, foldtext, formatexpr, imactivatefunc, imstatusfunc, includeexpr, indentexpr, omnifunc, operatorfunc, patchexpr, printexpr, quickfixtextfunc, tagfunc

Bram responded to the email stating that it will be useful to support it for these options.

Source.

Incidentally, this should also allow us to pass arbitrary data to 'opfunc', which is awkward/cumbersome right now (we need to use global or script-local variables).

But it would still not fix the issue in other contexts (timers, input(), command-line, ...).

bfrg commented 4 years ago

Whether a builtin sid() or a custom SID() function, the following still looks awkward (in my opinion):

let &l:includeexpr = sid() .. 'my_includeexpr()'

It would make a lot more sense to allow funcrefs for the above mentioned options, including the input() function. It seems sid() is just a dirty workaround since funcrefs are not accepted (yet). In my opinion users shouldn't worry about these internal script IDs.

For timer_start() I have always wrapped the script-local callback inside a lambda, something like:

call timer_start(1, {-> s:foo()})
brammool commented 4 years ago

Funcrefs are indeed a better solution.

The help already explains how is expanded to 123_. In that line of thought, I think it would be OK to make expand('') work. It's a bit more explicit than sid(), which will look strange to someone who has no clue what that means.

lacygoill commented 4 years ago

Whether a builtin sid() or a custom SID() function, the following still looks awkward (in my opinion):

Oh yes, I definitely agree. It's just that in some cases, I don't see any alternative; mainly when I have to invoke a script-local function from a command-line populated by a script. I guess that's an edge case which doesn't warrant a dedicated sid() function.

It would make a lot more sense to allow funcrefs for the above mentioned options, including the input() function. It seems sid() is just a dirty workaround since funcrefs are not accepted (yet). In my opinion users shouldn't worry about these internal script IDs.

I'll keep an eye on future PRs, regarding the ability of providing a funcref to '*func' options. When one will be submitted, I'll check input() is also supported.

lacygoill commented 4 years ago

In that line of thought, I think it would be OK to make expand('') work.

That would certainly help reduce the amount of code in most cases I mentioned in the OP.

poetaman commented 3 years ago

@lacygoill Does this work now? How to expand this? I am trying to set quickfixtextfunc=s:functioname, what's the best technique for now?

let seems to work, set does not for setting quickfixtextfunc :

let &quickfixtextfunc=expand('<SID>') . 'myQfPrintFunc'

chrisbra commented 3 years ago

yes and that is expected, because :set does not expect expressions. See also :help <SID> where an example is shown.

poetaman commented 3 years ago

@chrisbra Thanks!