Skip to content

Support a lambda or a partial for the 'operatorfunc' option#8775

Closed
yegappan wants to merge 1 commit intovim:masterfrom
yegappan:opfunc
Closed

Support a lambda or a partial for the 'operatorfunc' option#8775
yegappan wants to merge 1 commit intovim:masterfrom
yegappan:opfunc

Conversation

@yegappan
Copy link
Member

@yegappan yegappan commented Aug 20, 2021

The 'operatorfunc' option currently accepts a function name (as a string).
Add support for using a lambda or a partial as a value for this option.

@codecov
Copy link

codecov bot commented Aug 20, 2021

Codecov Report

Merging #8775 (ce51807) into master (6555500) will decrease coverage by 0.00%.
The diff coverage is 84.37%.

❗ Current head ce51807 differs from pull request most recent head 350256b. Consider uploading reports for the commit 350256b to get more accurate results
Impacted file tree graph

@@            Coverage Diff             @@
##           master    #8775      +/-   ##
==========================================
- Coverage   90.15%   90.14%   -0.01%     
==========================================
  Files         151      151              
  Lines      169356   169369      +13     
==========================================
+ Hits       152678   152680       +2     
- Misses      16678    16689      +11     
Flag Coverage Δ
huge-clang-none 90.67% <80.00%> (-0.02%) ⬇️
huge-gcc-none 89.71% <84.37%> (-0.02%) ⬇️
huge-gcc-testgui 88.36% <84.37%> (-0.01%) ⬇️
huge-gcc-unittests 2.45% <0.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
src/optionstr.c 95.20% <66.66%> (-0.09%) ⬇️
src/option.c 94.01% <81.81%> (-0.09%) ⬇️
src/ops.c 95.61% <100.00%> (+0.01%) ⬆️
src/quickfix.c 94.81% <100.00%> (+0.09%) ⬆️
src/message.c 88.50% <0.00%> (-0.32%) ⬇️
src/term.c 83.80% <0.00%> (-0.22%) ⬇️
src/if_xcmdsrv.c 89.61% <0.00%> (-0.18%) ⬇️
src/eval.c 96.03% <0.00%> (-0.15%) ⬇️
src/window.c 92.88% <0.00%> (+0.03%) ⬆️
src/ex_getln.c 91.52% <0.00%> (+0.04%) ⬆️
... and 1 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 6555500...350256b. Read the comment docs.

@brammool
Copy link
Contributor

This deserves a good example, or perhaps two, for how the lambda can be useful.

@lacygoill
Copy link

This could be handy for when the opfunc is a script-local function. We can't write this:

set operatorfunc=s:MyOpfunc

Because it raises E120:

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

So, we have to write this:

let &operatorfunc = expand('<SID>') .. 'MyOpfunc'

Which looks a bit weird.

But with a lambda, we could write this instead:

set operatorfunc={type\ ->\ s:MyOpfunc(type)}

One could argue that it still looks weird. I still prefer reading a lambda which can be used in many other contexts, rather than expand('<SID>'), which is less often used.


Just 2 remarks. It works with :set but not with :let. That is, we cannot write this:

let &operatorfunc = {type -> s:MyOpfunc(type)}

Because it raises E729:

E729: Using a Funcref as a String

It still doesn't work if we manually coerce the lambda into a string:

let &operatorfunc = {type -> s:MyOpfunc(type)}->string()
                                              ^--------^

Because this time, it raises E129:

E129: Function name required

It would be nice if could work so that we don't have to escape spaces:

# ugly
set operatorfunc={type\ ->\ s:MyOpfunc(type)}
                      ^   ^

# nicer
let &operatorfunc = {type -> s:MyOpfunc(type)}
                         ^  ^

Also, it doesn't work with a Vim9 lambda. That is, in a Vim9 script/function, we cannot write this:

set operatorfunc=(type)\ =>\ MyOpfunc(type)

Because it raises E117:

E117: Unknown function: (type) => MyOpfunc(type)

I suspect that's because the option is wrongly evaluated in the legacy context. Possibly relevant todo item:

Use the location where the option was set for deciding whether it's to be
evaluated in Vim9 script context.

As a workaround, we have to use the :legacy modifier:

legacy set operatorfunc={type\ ->\ MyOpfunc(type)}
^----^

Using the example given at :help g@:

vim9script
nnoremap <expr> <F4> <SID>CountSpaces()
xnoremap <expr> <F4> <SID>CountSpaces()
nnoremap <expr> <F4><F4> <SID>CountSpaces() .. '_'

def CountSpaces(type = ''): string
  if type == ''
    legacy set operatorfunc={type\ ->\ CountSpaces(type)}
    return 'g@'
  endif

  var sel_save: string = &selection
  var reg_save: dict<any> = getreginfo('"')
  var cb_save: string = &clipboard
  var visual_marks_save: list<list<number>> = [getpos("'<"), getpos("'>")]

  try
    set clipboard= selection=inclusive
    var commands: dict<string> = {line: "'[V']y", char: "`[v`]y", block: "`[\<c-v>`]y"}
    silent execute 'noautocmd keepjumps normal! ' .. get(commands, type, '"')
    echom getreg('"')->count(' ')
  finally
    setreg('"', reg_save)
    setpos("'<", visual_marks_save[0])
    setpos("'>", visual_marks_save[1])
    &clipboard = cb_save
    &selection = sel_save
  endtry
  return ''
enddef

'a b c d e'->setline(1)
feedkeys("\<F4>\<F4>")

What's weird is that we don't have to specify the s: prefix in the legacy lambda in front of the script-local function name. Usually, in the legacy context, s: is mandatory; it can only be dropped in the Vim9 context.

@vim-ml
Copy link

vim-ml commented Aug 21, 2021 via email

@brammool
Copy link
Contributor

There are several options that use a string value which is the name of a function.
Most functions, also lambdas, have a string name, some with a non-alpha prefix.
That allows using any function, so long as we can get the name as a string.
Then we could allow using:

let &operatorfunc = {type -> [some expression]}

And also:

let &operatorfunc = s:SomeFunc

Keeping the option type a string avoids having to deal with a different option value in very many places.
We just need to add a flag to the options that use a function name, and covert the function reference to a name for these.

@brammool
Copy link
Contributor

Finally had time to look into the patch. It does make sense, I think we can use his for all options that take the name of a function as an argument.
The use of function() and funcref() is not documented. We should make a separate section in the help, with some examples, and link to it from the options that support this format.
We have to make sure that the callback is cleared when exiting. The asan build doesn't report a leak, but that's probably because the tests end with making the option empty.

@lacygoill
Copy link

It seems that we can use a partial in a legacy function:

vim9script
nnoremap <expr> zd <SID>ZdSetup('zd')
function ZdSetup(cmd)
    set operatorfunc=function('s:Zd',\ [a:cmd])
    return 'g@l'
endfunction
def Zd(cmd: string, type: string)
    execute 'normal! ' .. cmd
enddef
var lines =<< trim END
    some folded lines {{{
    some folded lines
    some folded lines }}}
END
lines->setline(1)
&foldmethod = 'marker'
normal zd
the fold is correctly deleted

But not in a Vim9 function:

vim9script
nnoremap <expr> zd <SID>ZdSetup('zd')
def ZdSetup(cmd: string): string
    set operatorfunc=function(Zd,\ [cmd])
    return 'g@l'
enddef
def Zd(cmd: string, type: string)
    execute 'normal! ' .. cmd
enddef
var lines =<< trim END
    some folded lines {{{
    some folded lines
    some folded lines }}}
END
lines->setline(1)
&foldmethod = 'marker'
normal zd
E121: Undefined variable: cmd

Am I missing something? Shouldn't Vim be able to find the cmd argument in a :def function:

set operatorfunc=function(Zd,\ [cmd])
                                ^^^

just like it is able to in a legacy function:

set operatorfunc=function('s:Zd',\ [a:cmd])
                                    ^---^

Note that the issue is not caused by the omission of the s: scope in front of Zd, nor by the omission of the quotes around. None of them are required in the Vim9 context. And it doesn't suppress the error here anyway.

@lacygoill
Copy link

Sorry, I think it's the same issue as the one reported earlier about the lambdas:

Also, it doesn't work with a Vim9 lambda. That is, in a Vim9 script/function, we cannot write this:

set operatorfunc=(type)\ =>\ MyOpfunc(type)

@vim-ml
Copy link

vim-ml commented Nov 16, 2021 via email

@vim-ml
Copy link

vim-ml commented Nov 16, 2021 via email

@yegappan yegappan force-pushed the opfunc branch 2 times, most recently from 9499dab to f5308aa Compare November 18, 2021 06:16
@vim-ml
Copy link

vim-ml commented Nov 18, 2021 via email

@vim-ml
Copy link

vim-ml commented Dec 6, 2021 via email

@lacygoill
Copy link

Thank you very much for all your work on this. I don't know whether it's planned at some point, but we can still not set an opfunc with a simple assignment in Vim9:

vim9script
nnoremap <expr> <F4><F4> <SID>CountSpaces() .. '_'
def CountSpaces(type = ''): string
  if type == ''
    &operatorfunc = (t) => CountSpaces(t)
    return 'g@'
  endif
  normal! '[V']y
  echomsg getreg('"')->count(' ')
  return ''
enddef
'a b c d e'->setline(1)
feedkeys("\<F4>\<F4>")
E1012: Type mismatch; expected string but got func(any): string

The issue comes from this line:

&operatorfunc = (t) => CountSpaces(t)

Which works in legacy:

legacy let &operatorfunc = {t -> CountSpaces(t)}

Test:

vim9script
nnoremap <expr> <F4><F4> <SID>CountSpaces() .. '_'
def CountSpaces(type = ''): string
  if type == ''
    legacy let &operatorfunc = {t -> CountSpaces(t)}
    return 'g@'
  endif
  normal! '[V']y
  echomsg getreg('"')->count(' ')
  return ''
enddef
'a b c d e'->setline(1)
feedkeys("\<F4>\<F4>")
4

Also, still in Vim9, we can not use a partial; neither with :set:

set operatorfunc=function(Zd,\ [cmd])

Test:

vim9script
nnoremap <expr> zd <SID>ZdSetup('zd')
def ZdSetup(cmd: string): string
    set operatorfunc=function(Zd,\ [cmd])
    return 'g@l'
enddef
def Zd(cmd: string, type: string)
    execute 'normal! ' .. cmd
enddef
var lines =<< trim END
    some folded lines {{{
    some folded lines
    some folded lines }}}
END
lines->setline(1)
&foldmethod = 'marker'
normal zd
E121: Undefined variable: cmd

nor with an assignment:

&operatorfunc = function(Zd, [cmd])

Test:

vim9script
nnoremap <expr> zd <SID>ZdSetup('zd')
def ZdSetup(cmd: string): string
    &operatorfunc = function(Zd, [cmd])
    return 'g@l'
enddef
def Zd(cmd: string, type: string)
    execute 'normal! ' .. cmd
enddef
var lines =<< trim END
    some folded lines {{{
    some folded lines
    some folded lines }}}
END
lines->setline(1)
&foldmethod = 'marker'
normal zd
E1012: Type mismatch; expected string but got func(...): unknown

@vim-ml
Copy link

vim-ml commented Dec 8, 2021 via email

@brammool
Copy link
Contributor

brammool commented Dec 9, 2021

The option assignment to a lambda in a :def function was implemented with 8.2.3765. I hope it works for you.

The "set" command is not compiled, thus it cannot use variables from its context. You can see this with ":disas":

 set operatorfunc=function(Zd,\ [cmd])
   0 EXEC     set operatorfunc=function(Zd,\ [cmd])

The last example fails because the 'operatorfunc' option is set to the lambda, not the partial.
Perhaps we could make this work but since using a lambda is easier and already works, I don't think we need it.

@brammool
Copy link
Contributor

brammool commented Dec 9, 2021

Actually, using a lambda doesn't work, because the 'operatorfunc' calls the function without the context of where it was defined.

zeertzjq added a commit to zeertzjq/neovim that referenced this pull request Aug 19, 2022
Problem:    Cannot use a lambda for 'operatorfunc'.
Solution:   Support using a lambda or partial. (Yegappan Lakshmanan,
            closes vim/vim#8775)
vim/vim@777175b

Omit duplicate docs. It's removed in patch 8.2.3623.
zeertzjq added a commit to neovim/neovim that referenced this pull request Aug 19, 2022
Problem:    Cannot use a lambda for 'operatorfunc'.
Solution:   Support using a lambda or partial. (Yegappan Lakshmanan,
            closes vim/vim#8775)
vim/vim@777175b

Omit duplicate docs. It's removed in patch 8.2.3623.
Nvim doesn't seem to need callback_set() as it was omitted when patch 8.1.1437
was first ported.
smjonas pushed a commit to smjonas/neovim that referenced this pull request Dec 31, 2022
)

Problem:    Cannot use a lambda for 'operatorfunc'.
Solution:   Support using a lambda or partial. (Yegappan Lakshmanan,
            closes vim/vim#8775)
vim/vim@777175b

Omit duplicate docs. It's removed in patch 8.2.3623.
Nvim doesn't seem to need callback_set() as it was omitted when patch 8.1.1437
was first ported.
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

Successfully merging this pull request may close these issues.

4 participants