diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index de1ab37d42..fac8da95f6 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -77,6 +77,10 @@ These changes may require adaptations in your config or plugins. API +• API results containing a Lua function reference (e.g. a "callback" reported + by |nvim_get_keymap()| or |nvim_get_autocmds()|, or a "callback" option + value) are serialized over RPC as a human-readable `""` hint string, + instead of nil. • |nvim_create_autocmd()|, |nvim_exec_autocmds()| and |nvim_clear_autocmds()| no longer treat an empty non-nil pattern as nil. • |nvim_clear_autocmds()| no longer treats an empty array event as nil. @@ -345,6 +349,11 @@ LUA OPTIONS +• Lua functions/closures can be assigned to "func" and "expr" options: + |option-value-function| (e.g. `vim.o.operatorfunc`), |expr-option-function| + (e.g. `vim.wo.foldexpr`, `vim.bo.indentexpr`). Reading such an option: + • ...from Lua, gets the function itself. + • ...from Vimscript/RPC, gets a human-readable `""` hint string. • 'ttyfast' can be disabled during startup by setting |$NVIM_NOTTYFAST|. • 'scrolloffpad' allows vertically centering cursor at the end of file. • 'shortmess' flag |shm-u| silences undo/redo messages. diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 147b42a91f..8254b60bbb 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -394,9 +394,8 @@ Note: In the future more global options can be made |global-local|. Using *option-value-function* Some options ('completefunc', 'findfunc', 'omnifunc', 'operatorfunc', -'quickfixtextfunc', 'tagfunc' and 'thesaurusfunc') are set to a function name -or a function reference or a lambda function. When using a lambda it will be -converted to the name, e.g. "123". +'quickfixtextfunc', 'tagfunc' and 'thesaurusfunc') can be set to a function +name, a |Funcref|, a |lambda|, or (from Lua) a Lua function. Examples: > set opfunc=MyOpFunc @@ -419,13 +418,28 @@ Set using a variable with lambda expression: > let L = {a, b, c -> MyTagFunc(a, b , c)} let &tagfunc = L +Set to a Lua function (from Lua): >lua + vim.o.operatorfunc = function(motion) … end +< +The value read back from the option reflects how it was set: +- A Function name or |Funcref| reads back as the function name. +- A Vimscript |lambda| or partial reads back as an evaluatable string, e.g. + `function('2')` or `function('MyFunc', [10])`. +- A |script-local| ("s:") name is expanded to `` form. +- A Lua function is returned as itself from Lua, or as a `""` hint + from Vimscript/RPC. + Calling a function in an expr option *expr-option-function* -The value of a few options, such as 'foldexpr', is an expression that is -evaluated to get a value. The evaluation can have quite a bit of overhead. -One way to minimize the overhead, and also to keep the option value very -simple, is to define a function and set the option to call it without -arguments. A |v:lua-call| can also be used. Example: >vim +The value of an "expr" option ('charconvert', 'diffexpr', 'foldexpr', +'foldtext', 'formatexpr', 'includeexpr', 'indentexpr', 'patchexpr') is a +Vimscript expression that is evaluated to get a value. Context is passed in +|v:| variables, e.g. 'foldexpr' is evaluated for each line with |v:lnum| set. + +The evaluation can have quite a bit of overhead. One way to minimize the +overhead, and also to keep the option value very simple, is to define a +function and set the option to call it without arguments. A |v:lua-call| can +also be used. Example: >vim lua << EOF function _G.MyFoldFunc() -- ... compute fold level for line v:lnum @@ -434,6 +448,14 @@ arguments. A |v:lua-call| can also be used. Example: >vim EOF set foldexpr=v:lua.MyFoldFunc() +From Lua the option can also be set directly to a function, which is called +with no arguments (read the context from |v:| variables): >lua + vim.wo.foldexpr = function() return vim.v.lnum == 1 and '>1' or '1' end +< +The value read back from the option follows the same rules as +|option-value-function|: a string reads back as itself, and a Lua function +reads back as the function (from Lua) or a `""` hint (Vimscript/RPC). + Setting the filetype diff --git a/runtime/doc/treesitter.txt b/runtime/doc/treesitter.txt index 0dc12157a0..07574cf310 100644 --- a/runtime/doc/treesitter.txt +++ b/runtime/doc/treesitter.txt @@ -1685,7 +1685,7 @@ omnifunc({findstart}, {base}) *vim.treesitter.query.omnifunc()* Omnifunc for completing node names and predicates in treesitter queries. Use via >lua - vim.bo.omnifunc = 'v:lua.vim.treesitter.query.omnifunc' + vim.bo.omnifunc = vim.treesitter.query.omnifunc < Parameters: ~ diff --git a/runtime/ftplugin/lua.lua b/runtime/ftplugin/lua.lua index 7c54b2cb1e..914c4f8d4c 100644 --- a/runtime/ftplugin/lua.lua +++ b/runtime/ftplugin/lua.lua @@ -2,7 +2,7 @@ vim.treesitter.start() vim.bo.includeexpr = [[v:lua.require'vim._ftplugin.lua'.includeexpr(v:fname)]] -vim.bo.omnifunc = 'v:lua.vim.lua_omnifunc' +vim.bo.omnifunc = vim.lua_omnifunc vim.wo[0][0].foldexpr = 'v:lua.vim.treesitter.foldexpr()' vim.b.undo_ftplugin = (vim.b.undo_ftplugin or '') diff --git a/runtime/ftplugin/query.lua b/runtime/ftplugin/query.lua index e26bfa7a7f..5f786816a1 100644 --- a/runtime/ftplugin/query.lua +++ b/runtime/ftplugin/query.lua @@ -11,7 +11,7 @@ end vim.treesitter.start() -- set omnifunc -vim.bo.omnifunc = 'v:lua.vim.treesitter.query.omnifunc' +vim.bo.omnifunc = vim.treesitter.query.omnifunc vim.api.nvim_set_option_value('iskeyword', '.', { scope = 'local', operation = 'append' }) diff --git a/runtime/lua/man.lua b/runtime/lua/man.lua index 1dd5a3c585..1ad53b1c24 100644 --- a/runtime/lua/man.lua +++ b/runtime/lua/man.lua @@ -768,7 +768,7 @@ function M.open_page(count, smods, args) end local buf = api.nvim_get_current_buf() local save_tfu = vim.bo[buf].tagfunc - vim.bo[buf].tagfunc = "v:lua.require'man'.goto_tag" + vim.bo[buf].tagfunc = M.goto_tag local target = ('%s(%s)'):format(name, sect) diff --git a/runtime/lua/vim/_comment.lua b/runtime/lua/vim/_comment.lua index dbc2f38b34..7b137758d3 100644 --- a/runtime/lua/vim/_comment.lua +++ b/runtime/lua/vim/_comment.lua @@ -221,7 +221,7 @@ local function operator(mode) -- Used without arguments as part of expression mapping. Otherwise it is -- called as 'operatorfunc'. if mode == nil then - vim.o.operatorfunc = "v:lua.require'vim._comment'.operator" + vim.o.operatorfunc = operator return 'g@' end diff --git a/runtime/lua/vim/_core/defaults.lua b/runtime/lua/vim/_core/defaults.lua index 2bb5acba82..4414608cbf 100644 --- a/runtime/lua/vim/_core/defaults.lua +++ b/runtime/lua/vim/_core/defaults.lua @@ -446,14 +446,12 @@ do -- Add empty lines vim.keymap.set('n', '[', function() - -- TODO: update once it is possible to assign a Lua function to options #25672 - vim.go.operatorfunc = "v:lua.require'vim._core.util'.space_above" + vim.go.operatorfunc = require('vim._core.util').space_above return 'g@l' end, { expr = true, desc = 'Add empty line above cursor' }) vim.keymap.set('n', ']', function() - -- TODO: update once it is possible to assign a Lua function to options #25672 - vim.go.operatorfunc = "v:lua.require'vim._core.util'.space_below" + vim.go.operatorfunc = require('vim._core.util').space_below return 'g@l' end, { expr = true, desc = 'Add empty line below cursor' }) end diff --git a/runtime/lua/vim/_core/util.lua b/runtime/lua/vim/_core/util.lua index bdfb4cc03a..437399143e 100644 --- a/runtime/lua/vim/_core/util.lua +++ b/runtime/lua/vim/_core/util.lua @@ -3,7 +3,6 @@ local M = {} --- Adds one or more blank lines above or below the cursor. --- TODO: move to _core/defaults.lua once it is possible to assign a Lua function to options #25672 --- @param above? boolean Place blank line(s) above the cursor local function add_blank(above) local offset = above and 1 or 0 @@ -12,12 +11,10 @@ local function add_blank(above) vim.api.nvim_buf_set_lines(0, linenr - offset, linenr - offset, true, repeated) end --- TODO: move to _core/defaults.lua once it is possible to assign a Lua function to options #25672 function M.space_above() add_blank(true) end --- TODO: move to _core/defaults.lua once it is possible to assign a Lua function to options #25672 function M.space_below() add_blank() end diff --git a/runtime/lua/vim/_meta/options.gen.lua b/runtime/lua/vim/_meta/options.gen.lua index a16149089c..7b9e5599ea 100644 --- a/runtime/lua/vim/_meta/options.gen.lua +++ b/runtime/lua/vim/_meta/options.gen.lua @@ -840,7 +840,7 @@ vim.bo.channel = vim.o.channel --- Otherwise the expression is evaluated in the context of the script --- where the option was set, thus script-local items are available. --- ---- @type string +--- @type string|function vim.o.charconvert = "" vim.o.ccv = vim.o.charconvert vim.go.charconvert = vim.o.charconvert @@ -1125,7 +1125,7 @@ vim.bo.cpt = vim.bo.complete --- function, a `lambda` or a `Funcref`. See `option-value-function` for --- more information. --- ---- @type string +--- @type string|function vim.o.completefunc = "" vim.o.cfu = vim.o.completefunc vim.bo.completefunc = vim.o.completefunc @@ -1791,7 +1791,7 @@ vim.go.dia = vim.go.diffanchors --- Expression which is evaluated to obtain a diff file (either ed-style --- or unified-style) from two versions of a file. See `diff-diffexpr`. --- ---- @type string +--- @type string|function vim.o.diffexpr = "" vim.o.dex = vim.o.diffexpr vim.go.diffexpr = vim.o.diffexpr @@ -2672,7 +2672,7 @@ vim.go.fcs = vim.go.fillchars --- ``` --- --- ---- @type string +--- @type string|function vim.o.findfunc = "" vim.o.ffu = vim.o.findfunc vim.bo.findfunc = vim.o.findfunc @@ -2745,7 +2745,7 @@ vim.wo.fen = vim.wo.foldenable --- It is not allowed to change text or jump to another window while --- evaluating 'foldexpr' `textlock`. --- ---- @type string +--- @type string|function vim.o.foldexpr = "0" vim.o.fde = vim.o.foldexpr vim.wo.foldexpr = vim.o.foldexpr @@ -2891,7 +2891,7 @@ vim.go.fdo = vim.go.foldopen --- When set to an empty string, foldtext is disabled, and the line --- is displayed normally with highlighting and no line wrapping. --- ---- @type string +--- @type string|function vim.o.foldtext = "foldtext()" vim.o.fdt = vim.o.foldtext vim.wo.foldtext = vim.o.foldtext @@ -2943,7 +2943,7 @@ vim.wo.fdt = vim.wo.foldtext --- since changing the buffer text is not allowed. --- This option cannot be set in a modeline when 'modelineexpr' is off. --- ---- @type string +--- @type string|function vim.o.formatexpr = "" vim.o.fex = vim.o.formatexpr vim.bo.formatexpr = vim.o.formatexpr @@ -3522,7 +3522,7 @@ vim.go.inc = vim.go.include --- It is not allowed to change text or jump to another window while --- evaluating 'includeexpr' `textlock`. --- ---- @type string +--- @type string|function vim.o.includeexpr = "" vim.o.inex = vim.o.includeexpr vim.bo.includeexpr = vim.o.includeexpr @@ -3617,7 +3617,7 @@ vim.go.is = vim.go.incsearch --- It is not allowed to change text or jump to another window while --- evaluating 'indentexpr' `textlock`. --- ---- @type string +--- @type string|function vim.o.indentexpr = "" vim.o.inde = vim.o.indentexpr vim.bo.indentexpr = vim.o.indentexpr @@ -4875,7 +4875,7 @@ vim.wo.nuw = vim.wo.numberwidth --- This option is usually set by a filetype plugin: --- `:filetype-plugin-on` --- ---- @type string +--- @type string|function vim.o.omnifunc = "" vim.o.ofu = vim.o.omnifunc vim.bo.omnifunc = vim.o.omnifunc @@ -4886,7 +4886,7 @@ vim.bo.ofu = vim.bo.omnifunc --- the name of a function, a `lambda` or a `Funcref`. See --- `option-value-function` for more information. --- ---- @type string +--- @type string|function vim.o.operatorfunc = "" vim.o.opfunc = vim.o.operatorfunc vim.go.operatorfunc = vim.o.operatorfunc @@ -4923,7 +4923,7 @@ vim.go.para = vim.go.paragraphs --- Expression which is evaluated to apply a patch to a file and generate --- the resulting new version of the file. See `diff-patchexpr`. --- ---- @type string +--- @type string|function vim.o.patchexpr = "" vim.o.pex = vim.o.patchexpr vim.go.patchexpr = vim.o.patchexpr @@ -5191,7 +5191,7 @@ vim.go.pyx = vim.go.pyxversion --- It is not allowed to change text or jump to another window while --- evaluating 'qftf' `textlock`. --- ---- @type string +--- @type string|function vim.o.quickfixtextfunc = "" vim.o.qftf = vim.o.quickfixtextfunc vim.go.quickfixtextfunc = vim.o.quickfixtextfunc @@ -7324,7 +7324,7 @@ vim.go.tc = vim.go.tagcase --- `lambda` or a `Funcref`. See `option-value-function` for more --- information. --- ---- @type string +--- @type string|function vim.o.tagfunc = "" vim.o.tfu = vim.o.tagfunc vim.bo.tagfunc = vim.o.tagfunc @@ -7492,7 +7492,7 @@ vim.go.tsr = vim.go.thesaurus --- The value can be the name of a function, a `lambda` or a `Funcref`. --- See `option-value-function` for more information. --- ---- @type string +--- @type string|function vim.o.thesaurusfunc = "" vim.o.tsrfu = vim.o.thesaurusfunc vim.bo.thesaurusfunc = vim.o.thesaurusfunc diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 525b9691f9..fa23039833 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -846,12 +846,12 @@ function lsp._set_defaults(client, bufnr) if client:supports_method('textDocument/definition') and is_empty_or_default(bufnr, 'tagfunc') then - vim.bo[bufnr].tagfunc = 'v:lua.vim.lsp.tagfunc' + vim.bo[bufnr].tagfunc = lsp.tagfunc end if client:supports_method('textDocument/completion') and is_empty_or_default(bufnr, 'omnifunc') then - vim.bo[bufnr].omnifunc = 'v:lua.vim.lsp.omnifunc' + vim.bo[bufnr].omnifunc = lsp.omnifunc end if client:supports_method('textDocument/rangeFormatting') diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index 66bf09f231..026f3ac392 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -1448,10 +1448,10 @@ end --- Reset defaults set by `set_defaults`. --- Must only be called if the last client attached to a buffer exits. local function reset_defaults(bufnr) - if vim.bo[bufnr].tagfunc == 'v:lua.vim.lsp.tagfunc' then + if vim.bo[bufnr].tagfunc == vim.lsp.tagfunc then vim.bo[bufnr].tagfunc = nil end - if vim.bo[bufnr].omnifunc == 'v:lua.vim.lsp.omnifunc' then + if vim.bo[bufnr].omnifunc == vim.lsp.omnifunc then vim.bo[bufnr].omnifunc = nil end if vim.bo[bufnr].formatexpr == 'v:lua.vim.lsp.formatexpr()' then diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index dc7a73432d..32cd13ae3d 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -639,7 +639,7 @@ function M.edit_query(lang) local query_buf = api.nvim_win_get_buf(query_win) vim.b[buf].dev_edit = query_win - vim.bo[query_buf].omnifunc = 'v:lua.vim.treesitter.query.omnifunc' + vim.bo[query_buf].omnifunc = vim.treesitter.query.omnifunc set_dev_options(query_win, query_buf) -- Note that omnifunc guesses the language based on the containing folder, diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index a615d93500..3e18b8973a 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -1131,7 +1131,7 @@ end --- Use via --- --- ```lua ---- vim.bo.omnifunc = 'v:lua.vim.treesitter.query.omnifunc' +--- vim.bo.omnifunc = vim.treesitter.query.omnifunc --- ``` --- --- @param findstart 0|1 diff --git a/scripts/linterrcodes.lua b/scripts/linterrcodes.lua index 9e4e1381fb..823269bb70 100644 --- a/scripts/linterrcodes.lua +++ b/scripts/linterrcodes.lua @@ -49,7 +49,7 @@ local dup_allowed = { E509 = 2, E5101 = 2, E5102 = 2, - E5108 = 5, + E5108 = 6, E5111 = 2, E513 = 2, E521 = 2, diff --git a/src/gen/gen_eval_files.lua b/src/gen/gen_eval_files.lua index f3e6da0d86..22f59e8425 100755 --- a/src/gen/gen_eval_files.lua +++ b/src/gen/gen_eval_files.lua @@ -665,7 +665,10 @@ local function render_option_meta(_f, opt, write) values[#values + 1] = fmt("'%s'", v) end end - if #values > 0 then + if opt.type == 'func' or opt.type == 'expr' then + -- "Callback" option ('operatorfunc', 'foldexpr', …). + write('--- @type string|function') + elseif #values > 0 then write('--- @type ' .. table.concat(values, '|')) else write('--- @type ' .. OPTION_TYPES[opt.type]) @@ -879,7 +882,8 @@ local function render_option_doc(_f, opt, write) name_str = fmt("'%s'", opt.full_name) end - local otype = opt.type == 'boolean' and 'boolean' or opt.type + -- Callback (func/expr) options are labeled "string" in the help, their ":set" form. + local otype = (opt.type == 'func' or opt.type == 'expr') and 'string' or opt.type if opt.defaults.doc or opt.defaults.if_true ~= nil or opt.defaults.meta ~= nil then local v = render_option_default(opt.defaults --[[@as vim.option_defaults]], true) local pad = string.rep('\t', math.max(1, math.ceil((24 - #name_str) / 8))) diff --git a/src/gen/gen_options.lua b/src/gen/gen_options.lua index 9463edb8d1..319c6cf535 100644 --- a/src/gen/gen_options.lua +++ b/src/gen/gen_options.lua @@ -118,18 +118,24 @@ local function get_flags(o) end end + -- Callback option: `type='func'` (funcref/lambda/Lua fn) or `type='expr'` (Vimscript expr). + if o.type == 'func' then + add_flag('kOptFlagFunc') + elseif o.type == 'expr' then + add_flag('kOptFlagExpr') + end + for _, flag_desc in ipairs({ - { 'nodefault', 'NoDefault' }, - { 'no_mkrc', 'NoMkrc' }, - { 'secure' }, - { 'gettext' }, - { 'noglob', 'NoGlob' }, - { 'normal_fname_chars', 'NFname' }, - { 'normal_dname_chars', 'NDname' }, - { 'pri_mkrc', 'PriMkrc' }, { 'deny_duplicates', 'NoDup' }, + { 'gettext' }, { 'modelineexpr', 'MLE' }, - { 'func' }, + { 'no_mkrc', 'NoMkrc' }, + { 'nodefault', 'NoDefault' }, + { 'noglob', 'NoGlob' }, + { 'normal_dname_chars', 'NDname' }, + { 'normal_fname_chars', 'NFname' }, + { 'pri_mkrc', 'PriMkrc' }, + { 'secure' }, }) do local key_name, flag_suffix = flag_desc[1], flag_desc[2] if o[key_name] then @@ -144,9 +150,13 @@ end --- @param opt_type vim.option_type --- @return string local function opt_type_enum(opt_type) - return ('kObjectType%s'):format( - ({ boolean = 'Boolean', number = 'Integer', string = 'String' })[opt_type] - ) + return ('kObjectType%s'):format(({ + boolean = 'Boolean', + number = 'Integer', + string = 'String', + func = 'String', + expr = 'String', + })[opt_type]) end --- @param scope vim.option_scope diff --git a/src/nvim/api/autocmd.c b/src/nvim/api/autocmd.c index f193ae8594..0bb2eeb796 100644 --- a/src/nvim/api/autocmd.c +++ b/src/nvim/api/autocmd.c @@ -309,6 +309,7 @@ ArrayOf(DictAs(get_autocmds__ret)) nvim_get_autocmds(Dict(get_autocmds) *opts, A case kCallbackPartial: PUT_C(autocmd_info, "callback", CSTR_AS_OBJ(callback_to_string(cb, arena))); break; + case kCallbackExpr: case kCallbackNone: abort(); } diff --git a/src/nvim/api/options.c b/src/nvim/api/options.c index c68f087f8b..791dbe1046 100644 --- a/src/nvim/api/options.c +++ b/src/nvim/api/options.c @@ -391,6 +391,10 @@ Object nvim_set_option_value(uint64_t channel_id, String name, Object value, Dic case kObjectTypeBoolean: merged_val = optval_right; break; + case kObjectTypeLuaRef: + // Callback option: no ":set"-style merge; take an independent ref for the set below. + merged_val = copy_object(optval_right, NULL); + break; default: abort(); } diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index 2607969884..29996f64a3 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -2170,11 +2170,11 @@ void free_buf_options(buf_T *buf, bool free_p_ff) } clear_string_option(&buf->b_p_def); clear_string_option(&buf->b_p_inc); - clear_string_option(&buf->b_p_inex); - clear_string_option(&buf->b_p_inde); + callback_free(&buf->b_p_inex); + callback_free(&buf->b_p_inde); clear_string_option(&buf->b_p_indk); clear_string_option(&buf->b_p_fp); - clear_string_option(&buf->b_p_fex); + callback_free(&buf->b_p_fex); clear_string_option(&buf->b_p_kp); clear_string_option(&buf->b_p_mps); clear_string_option(&buf->b_p_fo); @@ -2208,12 +2208,9 @@ void free_buf_options(buf_T *buf, bool free_p_ff) clear_string_option(&buf->b_p_cinw); clear_string_option(&buf->b_p_cot); clear_string_option(&buf->b_p_cpt); - clear_string_option(&buf->b_p_cfu); - callback_free(&buf->b_cfu_cb); - clear_string_option(&buf->b_p_ofu); - callback_free(&buf->b_ofu_cb); - clear_string_option(&buf->b_p_tsrfu); - callback_free(&buf->b_tsrfu_cb); + callback_free(&buf->b_p_cfu); + callback_free(&buf->b_p_ofu); + callback_free(&buf->b_p_tsrfu); clear_cpt_callbacks(&buf->b_p_cpt_cb, buf->b_p_cpt_count); buf->b_p_cpt_count = 0; clear_string_option(&buf->b_p_gefm); @@ -2224,10 +2221,8 @@ void free_buf_options(buf_T *buf, bool free_p_ff) clear_string_option(&buf->b_p_path); clear_string_option(&buf->b_p_tags); clear_string_option(&buf->b_p_tc); - clear_string_option(&buf->b_p_tfu); - callback_free(&buf->b_tfu_cb); - clear_string_option(&buf->b_p_ffu); - callback_free(&buf->b_ffu_cb); + callback_free(&buf->b_p_tfu); + callback_free(&buf->b_p_ffu); clear_string_option(&buf->b_p_dict); clear_string_option(&buf->b_p_dia); clear_string_option(&buf->b_p_tsr); diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 6403905074..a20e3f358a 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -122,9 +122,9 @@ typedef struct { #define w_p_fml w_onebuf_opt.wo_fml // 'foldminlines' OptInt wo_fdn; #define w_p_fdn w_onebuf_opt.wo_fdn // 'foldnestmax' - char *wo_fde; + Callback wo_fde; #define w_p_fde w_onebuf_opt.wo_fde // 'foldexpr' - char *wo_fdt; + Callback wo_fdt; #define w_p_fdt w_onebuf_opt.wo_fdt // 'foldtext' char *wo_fmr; #define w_p_fmr w_onebuf_opt.wo_fmr // 'foldmarker' @@ -560,14 +560,10 @@ struct file_buffer { uint32_t b_p_cpt_flags; ///< flags for 'complete' Callback *b_p_cpt_cb; ///< F{func} in 'complete' callback int b_p_cpt_count; ///< Count of values in 'complete' - char *b_p_cfu; ///< 'completefunc' - Callback b_cfu_cb; ///< 'completefunc' callback - char *b_p_ofu; ///< 'omnifunc' - Callback b_ofu_cb; ///< 'omnifunc' callback - char *b_p_tfu; ///< 'tagfunc' option value - Callback b_tfu_cb; ///< 'tagfunc' callback - char *b_p_ffu; ///< 'findfunc' option value - Callback b_ffu_cb; ///< 'findfunc' callback + Callback b_p_cfu; ///< 'completefunc' + Callback b_p_ofu; ///< 'omnifunc' + Callback b_p_tfu; ///< 'tagfunc' + Callback b_p_ffu; ///< 'findfunc' int b_p_eof; ///< 'endoffile' int b_p_eol; ///< 'endofline' int b_p_fixeol; ///< 'fixendofline' @@ -583,13 +579,13 @@ struct file_buffer { char *b_p_isk; ///< 'iskeyword' char *b_p_def; ///< 'define' local value char *b_p_inc; ///< 'include' - char *b_p_inex; ///< 'includeexpr' + Callback b_p_inex; ///< 'includeexpr' uint32_t b_p_inex_flags; ///< flags for 'includeexpr' - char *b_p_inde; ///< 'indentexpr' + Callback b_p_inde; ///< 'indentexpr' uint32_t b_p_inde_flags; ///< flags for 'indentexpr' char *b_p_indk; ///< 'indentkeys' char *b_p_fp; ///< 'formatprg' - char *b_p_fex; ///< 'formatexpr' + Callback b_p_fex; ///< 'formatexpr' uint32_t b_p_fex_flags; ///< flags for 'formatexpr' int b_p_fs; ///< 'fsync' char *b_p_kp; ///< 'keywordprg' @@ -641,8 +637,7 @@ struct file_buffer { char *b_p_dict; ///< 'dictionary' local value char *b_p_dia; ///< 'diffanchors' local value char *b_p_tsr; ///< 'thesaurus' local value - char *b_p_tsrfu; ///< 'thesaurusfunc' local value - Callback b_tsrfu_cb; ///< 'thesaurusfunc' callback + Callback b_p_tsrfu; ///< 'thesaurusfunc' local value OptInt b_p_ul; ///< 'undolevels' local value int b_p_udf; ///< 'undofile' char *b_p_lw; ///< 'lispwords' local value diff --git a/src/nvim/bufwrite.c b/src/nvim/bufwrite.c index 5a5af1e9a0..e911f918b8 100644 --- a/src/nvim/bufwrite.c +++ b/src/nvim/bufwrite.c @@ -1249,7 +1249,7 @@ int buf_write(buf_T *buf, char *fname, char *sfname, linenr_T start, linenr_T en // When the file needs to be converted with 'charconvert' after // writing, write to a temp file instead and let the conversion // overwrite the original file. - if (*p_ccv != NUL) { + if (p_ccv.type != kCallbackNone) { wfname = vim_tempname(); if (wfname == NULL) { // Can't write without a tempfile! err = set_err(_("E214: Can't find temp file for writing")); diff --git a/src/nvim/change.c b/src/nvim/change.c index 6d2d2e7a1d..67b0d40143 100644 --- a/src/nvim/change.c +++ b/src/nvim/change.c @@ -1226,7 +1226,7 @@ bool open_line(int dir, int flags, int second_line_indent, bool *did_do_comment) } // May do indenting after opening a new line. - bool do_cindent = !p_paste && (curbuf->b_p_cin || *curbuf->b_p_inde != NUL) + bool do_cindent = !p_paste && (curbuf->b_p_cin || curbuf->b_p_inde.type != kCallbackNone) && in_cinkeys(dir == FORWARD ? KEY_OPEN_FORW : KEY_OPEN_BACK, ' ', linewhite(curwin->w_cursor.lnum)) && !(flags & OPENLINE_FORCE_INDENT); diff --git a/src/nvim/cmdexpand.c b/src/nvim/cmdexpand.c index cbae11837d..bb78779ceb 100644 --- a/src/nvim/cmdexpand.c +++ b/src/nvim/cmdexpand.c @@ -2023,7 +2023,8 @@ static const char *set_context_by_cmdname(const char *cmd, cmdidx_T cmdidx, expa case CMD_sfind: case CMD_tabfind: if (xp->xp_context == EXPAND_FILES) { - xp->xp_context = *get_findfunc() != NUL ? EXPAND_FINDFUNC : EXPAND_FILES_IN_PATH; + xp->xp_context = get_findfunc()->type != + kCallbackNone ? EXPAND_FINDFUNC : EXPAND_FILES_IN_PATH; } break; case CMD_cd: diff --git a/src/nvim/diff.c b/src/nvim/diff.c index f85a1f0824..cb1576971d 100644 --- a/src/nvim/diff.c +++ b/src/nvim/diff.c @@ -1060,7 +1060,7 @@ theend: int diff_internal(void) FUNC_ATTR_PURE { - return (diff_flags & DIFF_INTERNAL) != 0 && *p_dex == NUL; + return (diff_flags & DIFF_INTERNAL) != 0 && p_dex.type == kCallbackNone; } /// Completely update the diffs for the buffers involved. @@ -1183,7 +1183,7 @@ static int check_external_diff(diffio_T *diffio) } // When using 'diffexpr' break here. - if (*p_dex != NUL) { + if (p_dex.type != kCallbackNone) { break; } @@ -1263,7 +1263,7 @@ static int diff_file(diffio_T *dio) char *tmp_orig = dio->dio_orig.din_fname; char *tmp_new = dio->dio_new.din_fname; char *tmp_diff = dio->dio_diff.dout_fname; - if (*p_dex != NUL) { + if (p_dex.type != kCallbackNone) { // Use 'diffexpr' to generate the diff file. eval_diff(tmp_orig, tmp_new, tmp_diff); return OK; @@ -1368,7 +1368,7 @@ void ex_diffpatch(exarg_T *eap) } #endif - if (*p_pex != NUL) { + if (p_pex.type != kCallbackNone) { // Use 'patchexpr' to generate the new file. #ifdef UNIX eval_patch(tmp_orig, (fullname != NULL ? fullname : eap->arg), tmp_new); diff --git a/src/nvim/drawline.c b/src/nvim/drawline.c index e97e9c1679..8f4ba5a9a9 100644 --- a/src/nvim/drawline.c +++ b/src/nvim/drawline.c @@ -1111,7 +1111,7 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b const bool in_curline = wp == curwin && lnum == curwin->w_cursor.lnum; const bool has_fold = foldinfo.fi_level != 0 && foldinfo.fi_lines > 0; - const bool has_foldtext = has_fold && *wp->w_p_fdt != NUL; + const bool has_foldtext = has_fold && wp->w_p_fdt.type != kCallbackNone; const bool is_wrapped = wp->w_p_wrap && !has_fold; // Never wrap folded lines diff --git a/src/nvim/drawscreen.c b/src/nvim/drawscreen.c index 0027ec0868..a88790153c 100644 --- a/src/nvim/drawscreen.c +++ b/src/nvim/drawscreen.c @@ -2247,7 +2247,8 @@ static void win_update(win_T *wp) syntax_end_parsing(wp, syntax_last_parsed + 1); } - bool display_buf_line = !concealed && (foldinfo.fi_lines == 0 || *wp->w_p_fdt == NUL); + bool display_buf_line = !concealed + && (foldinfo.fi_lines == 0 || wp->w_p_fdt.type == kCallbackNone); // Display one line spellvars_T zero_spv = { 0 }; diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 7b6d21f9f9..9610565c93 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -710,6 +710,59 @@ void *call_func_retlist(const char *func, int argc, typval_T *argv) return rettv.vval.v_list; } +/// Evaluate a number-valued "expr" option ('indentexpr'/'formatexpr') via its callback. +/// +/// Copies the callback first (the option could be changed while evaluating it) and suppresses +/// errors; evaluates in the sandbox when `use_sandbox`. `v:` context, `current_sctx` and any +/// textlock are the caller's responsibility. +/// +/// @return The number result, or -1 on failure. +int eval_expr_option_number(Callback *cb, bool use_sandbox) +{ + Callback copy; + callback_copy(©, cb); + if (use_sandbox) { + sandbox++; + } + emsg_off++; + typval_T tv; + typval_T argv[1]; + int retval = -1; + if (callback_call(©, 0, argv, &tv)) { + retval = (int)tv_get_number_chk(&tv, NULL); + tv_clear(&tv); + } + emsg_off--; + if (use_sandbox) { + sandbox--; + } + callback_free(©); + return retval; +} + +/// Evaluate a "expr" option ('foldtext'/'includeexpr') via its callback, under |textlock| and an +/// isolated funccal stack; evaluates in the |sandbox| when `use_sandbox`. The caller sets up `v:` +/// context and interprets `*ret_tv` (which it must clear on success). +/// +/// @return Whether the callback was called (result in `*ret_tv`). +bool eval_expr_option_tv(Callback *cb, bool use_sandbox, typval_T *ret_tv) +{ + funccal_entry_T funccal_entry; + save_funccal(&funccal_entry); + if (use_sandbox) { + sandbox++; + } + textlock++; + typval_T argv[1]; + bool ok = callback_call(cb, 0, argv, ret_tv); + textlock--; + if (use_sandbox) { + sandbox--; + } + restore_funccal(); + return ok; +} + /// Evaluate 'foldexpr'. Returns the foldlevel, and any character preceding /// it in "*cp". Doesn't give error messages. int eval_foldexpr(win_T *wp, int *cp) @@ -717,7 +770,7 @@ int eval_foldexpr(win_T *wp, int *cp) const sctx_T saved_sctx = current_sctx; const bool use_sandbox = was_set_insecurely(wp, kOptFoldexpr, OPT_LOCAL); - char *arg = skipwhite(wp->w_p_fde); + // 'foldexpr' (string) evaluates in the script where it was set; current line is v:lnum. current_sctx = wp->w_p_script_ctx[kWinOptFoldexpr]; emsg_off++; @@ -728,10 +781,9 @@ int eval_foldexpr(win_T *wp, int *cp) *cp = NUL; typval_T tv; + typval_T argv[1]; // Unused: func 'foldexpr' takes no args (v:lnum legacy). varnumber_T retval; - // Evaluate the expression. If the expression is "FuncName()" call the - // function directly. - if (eval0_simple_funccal(arg, &tv, NULL, &EVALARG_EVALUATE) == FAIL) { + if (!callback_call(&wp->w_p_fde, 0, argv, &tv)) { retval = 0; } else { // If the result is a number, just return the number. @@ -756,7 +808,6 @@ int eval_foldexpr(win_T *wp, int *cp) sandbox--; } textlock--; - clear_evalarg(&EVALARG_EVALUATE, NULL); current_sctx = saved_sctx; return (int)retval; @@ -766,18 +817,9 @@ int eval_foldexpr(win_T *wp, int *cp) Object eval_foldtext(win_T *wp) { const bool use_sandbox = was_set_insecurely(wp, kOptFoldtext, OPT_LOCAL); - char *arg = wp->w_p_fdt; - funccal_entry_T funccal_entry; - - save_funccal(&funccal_entry); - if (use_sandbox) { - sandbox++; - } - textlock++; - typval_T tv; Object retval; - if (eval0_simple_funccal(arg, &tv, NULL, &EVALARG_EVALUATE) == FAIL) { + if (!eval_expr_option_tv(&wp->w_p_fdt, use_sandbox, &tv)) { retval = STRING_OBJ(NULL_STRING); } else { if (tv.v_type == VAR_LIST) { @@ -787,14 +829,6 @@ Object eval_foldtext(win_T *wp) } tv_clear(&tv); } - clear_evalarg(&EVALARG_EVALUATE, NULL); - - if (use_sandbox) { - sandbox--; - } - textlock--; - restore_funccal(); - return retval; } @@ -4044,11 +4078,11 @@ bool garbage_collect(bool testing) // buffer callback functions ABORTING(set_ref_in_callback)(&buf->b_prompt_callback, copyID, NULL, NULL); ABORTING(set_ref_in_callback)(&buf->b_prompt_interrupt, copyID, NULL, NULL); - ABORTING(set_ref_in_callback)(&buf->b_cfu_cb, copyID, NULL, NULL); - ABORTING(set_ref_in_callback)(&buf->b_ofu_cb, copyID, NULL, NULL); - ABORTING(set_ref_in_callback)(&buf->b_tsrfu_cb, copyID, NULL, NULL); - ABORTING(set_ref_in_callback)(&buf->b_tfu_cb, copyID, NULL, NULL); - ABORTING(set_ref_in_callback)(&buf->b_ffu_cb, copyID, NULL, NULL); + ABORTING(set_ref_in_callback)(&buf->b_p_cfu, copyID, NULL, NULL); + ABORTING(set_ref_in_callback)(&buf->b_p_ofu, copyID, NULL, NULL); + ABORTING(set_ref_in_callback)(&buf->b_p_tsrfu, copyID, NULL, NULL); + ABORTING(set_ref_in_callback)(&buf->b_p_tfu, copyID, NULL, NULL); + ABORTING(set_ref_in_callback)(&buf->b_p_ffu, copyID, NULL, NULL); if (!abort && buf->b_p_cpt_cb != NULL) { ABORTING(set_ref_in_cpt_callbacks)(buf->b_p_cpt_cb, buf->b_p_cpt_count, copyID); } @@ -4897,8 +4931,6 @@ bool callback_call(Callback *const callback, const int argcount_in, typval_T *co partial_T *partial; char *name; - Array args = ARRAY_DICT_INIT; - Object rv; switch (callback->type) { case kCallbackFuncref: name = callback->data.funcref; @@ -4920,9 +4952,37 @@ bool callback_call(Callback *const callback, const int argcount_in, typval_T *co name = partial_name(partial); break; - case kCallbackLua: - rv = nlua_call_ref(callback->data.luaref, NULL, args, kRetNilBool, NULL, NULL); - return LUARET_TRUTHY(rv); + case kCallbackLua: { + // Call the Lua function with the given args and return its result via rettv. + Arena arena = ARENA_EMPTY; + Array luaargs = arena_array(&arena, (size_t)argcount_in); + for (int i = 0; i < argcount_in; i++) { + ADD_C(luaargs, vim_to_object(&argvars_in[i], &arena, false)); + } + Error err = ERROR_INIT; + callback_depth++; + Object result = nlua_call_ref(callback->data.luaref, NULL, luaargs, kRetObject, &arena, &err); + callback_depth--; + const bool ok = !ERROR_SET(&err); + if (!ok) { + semsg_multiline("emsg", "E5108: %s", err.msg); + api_clear_error(&err); + } else { + object_to_vim(result, rettv, NULL); + } + arena_mem_free(arena_finish(&arena)); + return ok; + } + + case kCallbackExpr: { + // Evaluate Vimscript expression ("expr" options). + // Optimization: If the expression is "FuncName()" call the function directly. + callback_depth++; + int r = eval0_simple_funccal(skipwhite(callback->data.expr), rettv, NULL, &EVALARG_EVALUATE); + callback_depth--; + clear_evalarg(&EVALARG_EVALUATE, NULL); + return r == OK; + } case kCallbackNone: return false; @@ -4946,18 +5006,19 @@ bool set_ref_in_callback(Callback *callback, int copyID, ht_stack_T **ht_stack, { typval_T tv; switch (callback->type) { + case kCallbackExpr: case kCallbackFuncref: case kCallbackNone: break; + case kCallbackLua: + // LuaRef is owned by the Lua registry, not traced by Vimscript's mark-sweep; nothing to mark. + break; + case kCallbackPartial: tv.v_type = VAR_PARTIAL; tv.vval.v_partial = callback->data.partial; return set_ref_in_item(&tv, copyID, ht_stack, list_stack); - break; - - case kCallbackLua: - abort(); } return false; } diff --git a/src/nvim/eval/typval.c b/src/nvim/eval/typval.c index fc057f231a..15b07e5ed9 100644 --- a/src/nvim/eval/typval.c +++ b/src/nvim/eval/typval.c @@ -1800,13 +1800,15 @@ bool tv_callback_equal(const Callback *cb1, const Callback *cb2) } switch (cb1->type) { case kCallbackFuncref: - return strcmp(cb1->data.funcref, cb2->data.funcref) == 0; + return strequal(cb1->data.funcref, cb2->data.funcref); case kCallbackPartial: // FIXME: this is inconsistent with tv_equal but is needed for precision // maybe change dictwatcheradd to return a watcher id instead? return cb1->data.partial == cb2->data.partial; case kCallbackLua: return cb1->data.luaref == cb2->data.luaref; + case kCallbackExpr: + return strequal(cb1->data.expr, cb2->data.expr); case kCallbackNone: return true; } @@ -1829,6 +1831,9 @@ void callback_free(Callback *callback) case kCallbackLua: NLUA_CLEAR_REF(callback->data.luaref); break; + case kCallbackExpr: + xfree(callback->data.expr); + break; case kCallbackNone: break; } @@ -1880,6 +1885,9 @@ void callback_copy(Callback *dest, Callback *src) case kCallbackLua: dest->data.luaref = api_new_luaref(src->data.luaref); break; + case kCallbackExpr: + dest->data.expr = xstrdup(src->data.expr); + break; default: dest->data.funcref = NULL; break; diff --git a/src/nvim/eval/typval_defs.h b/src/nvim/eval/typval_defs.h index 51082db422..29c95bb020 100644 --- a/src/nvim/eval/typval_defs.h +++ b/src/nvim/eval/typval_defs.h @@ -60,6 +60,7 @@ typedef enum { kCallbackFuncref, kCallbackPartial, kCallbackLua, + kCallbackExpr, ///< Vimscript expression, for "expr" options. } CallbackType; typedef struct { @@ -67,6 +68,7 @@ typedef struct { char *funcref; partial_T *partial; LuaRef luaref; + char *expr; ///< kCallbackExpr: Vimscript, for "expr" options. } data; CallbackType type; } Callback; diff --git a/src/nvim/eval/vars.c b/src/nvim/eval/vars.c index 29844714e5..a5ed0a07aa 100644 --- a/src/nvim/eval/vars.c +++ b/src/nvim/eval/vars.c @@ -440,8 +440,15 @@ int eval_charconvert(const char *const enc_from, const char *const enc_to, } bool err = false; - if (eval_to_bool(p_ccv, &err, NULL, false, true)) { + typval_T tv; + typval_T argv[1]; + if (!callback_call(&p_ccv, 0, argv, &tv)) { err = true; + } else { + if (tv_get_number_chk(&tv, NULL) != 0) { + err = true; + } + tv_clear(&tv); } set_vim_var_string(VV_CC_FROM, NULL, -1); @@ -469,8 +476,11 @@ void eval_diff(const char *const origfile, const char *const newfile, const char } // errors are ignored - typval_T *tv = eval_expr_ext(p_dex, NULL, true); - tv_free(tv); + typval_T tv; + typval_T argv[1]; + if (callback_call(&p_dex, 0, argv, &tv)) { + tv_clear(&tv); + } set_vim_var_string(VV_FNAME_IN, NULL, -1); set_vim_var_string(VV_FNAME_NEW, NULL, -1); @@ -491,8 +501,11 @@ void eval_patch(const char *const origfile, const char *const difffile, const ch } // errors are ignored - typval_T *tv = eval_expr_ext(p_pex, NULL, true); - tv_free(tv); + typval_T tv; + typval_T argv[1]; + if (callback_call(&p_pex, 0, argv, &tv)) { + tv_clear(&tv); + } set_vim_var_string(VV_FNAME_IN, NULL, -1); set_vim_var_string(VV_FNAME_DIFF, NULL, -1); @@ -3286,6 +3299,11 @@ typval_T opt_to_tv(Object value, bool numbool) rettv.v_type = VAR_STRING; rettv.vval.v_string = value.data.string.data; break; + case kObjectTypeLuaRef: + // Lua callback option (e.g. 'operatorfunc'): show a human-readable hint (same as `:map` does). + rettv.v_type = VAR_STRING; + rettv.vval.v_string = nlua_funcref_str(value.data.luaref, NULL); + break; default: abort(); // Should never happen. } diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 452b31474d..3fb7771ad6 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -5493,14 +5493,6 @@ static void ex_wrongmodifier(exarg_T *eap) eap->errmsg = _(e_invcmd); } -/// callback function for 'findfunc' -static Callback ffu_cb; - -static Callback *get_findfunc_callback(void) -{ - return *curbuf->b_p_ffu != NUL ? &curbuf->b_ffu_cb : &ffu_cb; -} - /// Call 'findfunc' to obtain a list of file names. static list_T *call_findfunc(char *pat, BoolVarValue cmdcomplete) { @@ -5523,7 +5515,7 @@ static list_T *call_findfunc(char *pat, BoolVarValue cmdcomplete) current_sctx = *ctx; } - Callback *cb = get_findfunc_callback(); + Callback *cb = get_findfunc(); typval_T rettv; int retval = callback_call(cb, 2, args, &rettv); @@ -5611,53 +5603,16 @@ static char *findfunc_find_file(char *findarg, size_t findarg_len, int count) return ret_fname; } -/// Process the 'findfunc' option value. -/// Returns NULL on success and an error message on failure. -const char *did_set_findfunc(optset_T *args) -{ - buf_T *buf = (buf_T *)args->os_buf; - int retval; - - if (args->os_flags & OPT_LOCAL) { - // buffer-local option set - retval = option_set_callback_func(buf->b_p_ffu, &buf->b_ffu_cb); - } else { - // global option set - retval = option_set_callback_func(p_ffu, &ffu_cb); - // when using :set, free the local callback - if (!(args->os_flags & OPT_GLOBAL)) { - callback_free(&buf->b_ffu_cb); - } - } - - if (retval == FAIL) { - return e_invarg; - } - - // If the option value starts with or s:, then replace that with - // the script identifier. - char **varp = (char **)args->os_varp; - char *name = get_scriptlocal_funcname(*varp); - if (name != NULL) { - free_string_option(*varp); - *varp = name; - } - - return NULL; -} - void free_findfunc_option(void) { - callback_free(&ffu_cb); + callback_free(&p_ffu); } /// Mark the global 'findfunc' callback with "copyID" so that it is not /// garbage collected. bool set_ref_in_findfunc(int copyID) { - bool abort = false; - abort = set_ref_in_callback(&ffu_cb, copyID, NULL, NULL); - return abort; + return set_ref_in_callback(&p_ffu, copyID, NULL, NULL); } static void set_browse_edit_arg(exarg_T *eap) @@ -5704,7 +5659,7 @@ void ex_splitview(exarg_T *eap) } if (eap->cmdidx == CMD_sfind || eap->cmdidx == CMD_tabfind) { - if (*get_findfunc() != NUL) { + if (get_findfunc()->type != kCallbackNone) { fname = findfunc_find_file(eap->arg, strlen(eap->arg), eap->addr_count > 0 ? eap->line2 : 1); } else { @@ -5988,7 +5943,7 @@ static void ex_find(exarg_T *eap) } char *fname = NULL; - if (*get_findfunc() != NUL) { + if (get_findfunc()->type != kCallbackNone) { fname = findfunc_find_file(eap->arg, strlen(eap->arg), eap->addr_count > 0 ? eap->line2 : 1); } else { diff --git a/src/nvim/file_search.c b/src/nvim/file_search.c index 23b3d74358..01f2fe131f 100644 --- a/src/nvim/file_search.c +++ b/src/nvim/file_search.c @@ -1743,9 +1743,13 @@ static char *eval_includeexpr(const char *const ptr, const size_t len) set_vim_var_string(VV_FNAME, ptr, (ptrdiff_t)len); current_sctx = curbuf->b_p_script_ctx[kBufOptIncludeexpr]; - char *res = eval_to_string_safe(curbuf->b_p_inex, - was_set_insecurely(curwin, kOptIncludeexpr, OPT_LOCAL), - true); + const bool use_sandbox = was_set_insecurely(curwin, kOptIncludeexpr, OPT_LOCAL); + typval_T tv; + char *res = NULL; + if (eval_expr_option_tv(&curbuf->b_p_inex, use_sandbox, &tv)) { + res = xstrdup(tv_get_string(&tv)); + tv_clear(&tv); + } set_vim_var_string(VV_FNAME, NULL, 0); current_sctx = save_sctx; @@ -1772,7 +1776,7 @@ char *find_file_name_in_path(char *ptr, size_t len, int options, long count, cha len -= off; } - if ((options & FNAME_INCL) && *curbuf->b_p_inex != NUL) { + if ((options & FNAME_INCL) && curbuf->b_p_inex.type != kCallbackNone) { tofree = eval_includeexpr(ptr, len); if (tofree != NULL) { ptr = tofree; @@ -1790,7 +1794,7 @@ char *find_file_name_in_path(char *ptr, size_t len, int options, long count, cha // If the file could not be found in a normal way, try applying // 'includeexpr' (unless done already). if (file_name == NULL - && !(options & FNAME_INCL) && *curbuf->b_p_inex != NUL) { + && !(options & FNAME_INCL) && curbuf->b_p_inex.type != kCallbackNone) { tofree = eval_includeexpr(ptr, len); if (tofree != NULL) { ptr = tofree; diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c index 993f78315f..000dec5d9d 100644 --- a/src/nvim/fileio.c +++ b/src/nvim/fileio.c @@ -844,7 +844,7 @@ retry: // Use the 'charconvert' expression when conversion is required // and we can't do it internally or with iconv(). - if (fio_flags == 0 && !read_stdin && !read_buffer && *p_ccv != NUL + if (fio_flags == 0 && !read_stdin && !read_buffer && p_ccv.type != kCallbackNone && !read_fifo && iconv_fd == (iconv_t)-1) { did_iconv = false; // Skip conversion when it's already done (retry for wrong @@ -1433,7 +1433,7 @@ retry: // Detected a UTF-8 error. rewind_retry: // Retry reading with another conversion. - if (*p_ccv != NUL && iconv_fd != (iconv_t)-1) { + if (p_ccv.type != kCallbackNone && iconv_fd != (iconv_t)-1) { // iconv() failed, try 'charconvert' did_iconv = true; } else { diff --git a/src/nvim/fold.c b/src/nvim/fold.c index 61d5e69d57..a8c6dfe044 100644 --- a/src/nvim/fold.c +++ b/src/nvim/fold.c @@ -1708,7 +1708,7 @@ char *get_foldtext(win_T *wp, linenr_T lnum, linenr_T lnume, foldinfo_T foldinfo did_emsg = false; } - if (*wp->w_p_fdt != NUL) { + if (wp->w_p_fdt.type != kCallbackNone) { char dashes[MAX_LEVEL + 2]; // Set "v:foldstart" and "v:foldend". diff --git a/src/nvim/indent.c b/src/nvim/indent.c index 7184debea7..854f3e97f1 100644 --- a/src/nvim/indent.c +++ b/src/nvim/indent.c @@ -1035,7 +1035,7 @@ bool preprocs_left(void) /// @return true if the conditions are OK for smart indenting. bool may_do_si(void) { - return curbuf->b_p_si && !curbuf->b_p_cin && *curbuf->b_p_inde == NUL && !p_paste; + return curbuf->b_p_si && !curbuf->b_p_cin && curbuf->b_p_inde.type == kCallbackNone && !p_paste; } // Try to do some very smart auto-indenting. @@ -1629,21 +1629,11 @@ int get_expr_indent(void) bool save_set_curswant = curwin->w_set_curswant; set_vim_var_nr(VV_LNUM, (varnumber_T)curwin->w_cursor.lnum); - if (use_sandbox) { - sandbox++; - } textlock++; current_sctx = curbuf->b_p_script_ctx[kBufOptIndentexpr]; - // Need to make a copy, the 'indentexpr' option could be changed while - // evaluating it. - char *inde_copy = xstrdup(curbuf->b_p_inde); - int indent = (int)eval_to_number(inde_copy, true); - xfree(inde_copy); + int indent = eval_expr_option_number(&curbuf->b_p_inde, use_sandbox); - if (use_sandbox) { - sandbox--; - } textlock--; current_sctx = save_sctx; @@ -1893,7 +1883,7 @@ void fixthisline(IndentGetter get_the_indent) bool use_indentexpr_for_lisp(void) { return curbuf->b_p_lisp - && *curbuf->b_p_inde != NUL + && curbuf->b_p_inde.type != kCallbackNone && strcmp(curbuf->b_p_lop, "expr:1") == 0; } diff --git a/src/nvim/indent_c.c b/src/nvim/indent_c.c index 2ec60989af..7e40dfe2df 100644 --- a/src/nvim/indent_c.c +++ b/src/nvim/indent_c.c @@ -298,7 +298,7 @@ bool cin_is_cinword(const char *line) bool cindent_on(void) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT { - return !p_paste && (curbuf->b_p_cin || *curbuf->b_p_inde != NUL); + return !p_paste && (curbuf->b_p_cin || curbuf->b_p_inde.type != kCallbackNone); } // Skip over white space and C comments within the line. @@ -3854,7 +3854,7 @@ bool in_cinkeys(int keytyped, int when, bool line_is_empty) return false; } - if (*curbuf->b_p_inde != NUL) { + if (curbuf->b_p_inde.type != kCallbackNone) { look = curbuf->b_p_indk; // 'indentexpr' set: use 'indentkeys' } else { look = curbuf->b_p_cink; // 'indentexpr' empty: use 'cinkeys' @@ -4051,7 +4051,7 @@ bool in_cinkeys(int keytyped, int when, bool line_is_empty) // Do C or expression indenting on the current line. void do_c_expr_indent(void) { - if (*curbuf->b_p_inde != NUL) { + if (curbuf->b_p_inde.type != kCallbackNone) { fixthisline(get_expr_indent); } else { fixthisline(get_c_indent); diff --git a/src/nvim/insert.c b/src/nvim/insert.c index 4e349bbd0a..9b27ba2b50 100644 --- a/src/nvim/insert.c +++ b/src/nvim/insert.c @@ -1981,7 +1981,7 @@ void insertchar(int c, int flags, int second_indent) colnr_T virtcol = get_nolist_virtcol() + char2cells(c != NUL ? c : gchar_cursor()); - if (*curbuf->b_p_fex != NUL && (flags & INSCHAR_NO_FEX) == 0 + if (curbuf->b_p_fex.type != kCallbackNone && (flags & INSCHAR_NO_FEX) == 0 && (force_format || virtcol > (colnr_T)textwidth)) { do_internal = (fex_format(curwin->w_cursor.lnum, 1, c) != 0); // It may be required to save for undo again, e.g. when setline() diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index bcfe66f08b..d328dccf6a 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -551,7 +551,7 @@ bool check_compl_option(bool dict_opt) if (dict_opt ? (*curbuf->b_p_dict == NUL && *p_dict == NUL && !curwin->w_p_spell) : (*curbuf->b_p_tsr == NUL && *p_tsr == NUL - && *curbuf->b_p_tsrfu == NUL && *p_tsrfu == NUL)) { + && curbuf->b_p_tsrfu.type == kCallbackNone && p_tsrfu.type == kCallbackNone)) { ctrl_x_mode = CTRL_X_NORMAL; edit_submode = NULL; emsg(dict_opt ? _("'dictionary' option is empty") : _("'thesaurus' option is empty")); @@ -3011,77 +3011,9 @@ static int get_cpt_sources_count(void) return count; } -static Callback cfu_cb; ///< 'completefunc' callback function -static Callback ofu_cb; ///< 'omnifunc' callback function -static Callback tsrfu_cb; ///< 'thesaurusfunc' callback function static Callback *cpt_cb; ///< Callback functions associated with F{func} static int cpt_cb_count; ///< Number of cpt callbacks -/// Copy a global callback function to a buffer local callback. -static void copy_global_to_buflocal_cb(Callback *globcb, Callback *bufcb) -{ - callback_free(bufcb); - if (globcb->type != kCallbackNone) { - callback_copy(bufcb, globcb); - } -} - -/// Parse the 'completefunc' option value and set the callback function. -/// Invoked when the 'completefunc' option is set. The option value can be a -/// name of a function (string), or function() or funcref() or a -/// lambda expression. -const char *did_set_completefunc(optset_T *args) -{ - buf_T *buf = (buf_T *)args->os_buf; - int retval; - - if (args->os_flags & OPT_LOCAL) { - retval = option_set_callback_func(args->os_newval.data.string.data, &buf->b_cfu_cb); - } else { - retval = option_set_callback_func(args->os_newval.data.string.data, &cfu_cb); - if (retval == OK && !(args->os_flags & OPT_GLOBAL)) { - set_buflocal_cfu_callback(buf); - } - } - - return retval == FAIL ? e_invarg : NULL; -} - -/// Copy the global 'completefunc' callback function to the buffer-local -/// 'completefunc' callback for "buf". -void set_buflocal_cfu_callback(buf_T *buf) -{ - copy_global_to_buflocal_cb(&cfu_cb, &buf->b_cfu_cb); -} - -/// Parse the 'omnifunc' option value and set the callback function. -/// Invoked when the 'omnifunc' option is set. The option value can be a -/// name of a function (string), or function() or funcref() or a -/// lambda expression. -const char *did_set_omnifunc(optset_T *args) -{ - buf_T *buf = (buf_T *)args->os_buf; - int retval; - - if (args->os_flags & OPT_LOCAL) { - retval = option_set_callback_func(args->os_newval.data.string.data, &buf->b_ofu_cb); - } else { - retval = option_set_callback_func(args->os_newval.data.string.data, &ofu_cb); - if (retval == OK && !(args->os_flags & OPT_GLOBAL)) { - set_buflocal_ofu_callback(buf); - } - } - - return retval == FAIL ? e_invarg : NULL; -} - -/// Copy the global 'omnifunc' callback function to the buffer-local 'omnifunc' -/// callback for "buf". -void set_buflocal_ofu_callback(buf_T *buf) -{ - copy_global_to_buflocal_cb(&ofu_cb, &buf->b_ofu_cb); -} - /// Free an array of 'complete' F{func} callbacks and set the pointer to NULL. void clear_cpt_callbacks(Callback **callbacks, int count) { @@ -3177,30 +3109,6 @@ int set_cpt_callbacks(optset_T *args) return OK; } -/// Parse the 'thesaurusfunc' option value and set the callback function. -/// Invoked when the 'thesaurusfunc' option is set. The option value can be a -/// name of a function (string), or function() or funcref() or a -/// lambda expression. -const char *did_set_thesaurusfunc(optset_T *args FUNC_ATTR_UNUSED) -{ - buf_T *buf = (buf_T *)args->os_buf; - int retval; - - if (args->os_flags & OPT_LOCAL) { - // buffer-local option set - retval = option_set_callback_func(buf->b_p_tsrfu, &buf->b_tsrfu_cb); - } else { - // global option set - retval = option_set_callback_func(p_tsrfu, &tsrfu_cb); - // when using :set, free the local callback - if (!(args->os_flags & OPT_GLOBAL)) { - callback_free(&buf->b_tsrfu_cb); - } - } - - return retval == FAIL ? e_invarg : NULL; -} - /// Mark "copyID" references in an array of F{func} callbacks so that they are /// not garbage collected. bool set_ref_in_cpt_callbacks(Callback *callbacks, int count, int copyID) @@ -3221,40 +3129,26 @@ bool set_ref_in_cpt_callbacks(Callback *callbacks, int count, int copyID) /// "copyID" so that they are not garbage collected. bool set_ref_in_insexpand_funcs(int copyID) { - bool abort = set_ref_in_callback(&cfu_cb, copyID, NULL, NULL); - abort = abort || set_ref_in_callback(&ofu_cb, copyID, NULL, NULL); - abort = abort || set_ref_in_callback(&tsrfu_cb, copyID, NULL, NULL); + bool abort = set_ref_in_callback(&p_cfu, copyID, NULL, NULL); + abort = abort || set_ref_in_callback(&p_ofu, copyID, NULL, NULL); + abort = abort || set_ref_in_callback(&p_tsrfu, copyID, NULL, NULL); abort = abort || set_ref_in_cpt_callbacks(cpt_cb, cpt_cb_count, copyID); return abort; } -/// Get the user-defined completion function name for completion "type" -static char *get_complete_funcname(int type) -{ - switch (type) { - case CTRL_X_FUNCTION: - return curbuf->b_p_cfu; - case CTRL_X_OMNI: - return curbuf->b_p_ofu; - case CTRL_X_THESAURUS: - return *curbuf->b_p_tsrfu == NUL ? p_tsrfu : curbuf->b_p_tsrfu; - default: - return ""; - } -} - -/// Get the callback to use for insert mode completion. +/// Get the callback to use for insert mode completion of "type": 'completefunc', 'omnifunc', or +/// 'thesaurusfunc'. Returns `kCallbackNone` if the option is empty. static Callback *get_insert_callback(int type) { if (type == CTRL_X_FUNCTION) { - return &curbuf->b_cfu_cb; + return &curbuf->b_p_cfu; } if (type == CTRL_X_OMNI) { - return &curbuf->b_ofu_cb; + return &curbuf->b_p_ofu; } // CTRL_X_THESAURUS - return (*curbuf->b_p_tsrfu != NUL) ? &curbuf->b_tsrfu_cb : &tsrfu_cb; + return curbuf->b_p_tsrfu.type != kCallbackNone ? &curbuf->b_p_tsrfu : &p_tsrfu; } /// Execute user defined complete function 'completefunc', 'omnifunc' or @@ -3274,11 +3168,10 @@ static void expand_by_function(int type, char *base, Callback *cb) const bool is_cpt_function = (cb != NULL); const bool use_sandbox = is_cpt_function && was_set_insecurely(curwin, kOptComplete, OPT_LOCAL); if (!is_cpt_function) { - char *funcname = get_complete_funcname(type); - if (*funcname == NUL) { + cb = get_insert_callback(type); + if (cb->type == kCallbackNone) { return; } - cb = get_insert_callback(type); } // Call 'completefunc' to obtain the list of matches. @@ -3814,7 +3707,7 @@ void f_complete_info(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) static bool thesaurus_func_complete(int type) { return type == CTRL_X_THESAURUS - && (*curbuf->b_p_tsrfu != NUL || *p_tsrfu != NUL); + && (curbuf->b_p_tsrfu.type != kCallbackNone || p_tsrfu.type != kCallbackNone); } /// Check if 'cpt' list index can be advanced to the next completion source. @@ -4596,7 +4489,7 @@ static void get_register_completion(void) static Callback *get_callback_if_cpt_func(char *p, int idx) { if (*p == 'o') { - return &curbuf->b_ofu_cb; + return &curbuf->b_p_ofu; } if (*p == 'F') { @@ -4605,7 +4498,7 @@ static Callback *get_callback_if_cpt_func(char *p, int idx) return curbuf->b_p_cpt_cb[idx].type != kCallbackNone ? &curbuf->b_p_cpt_cb[idx] : NULL; } else { - return &curbuf->b_cfu_cb; // 'cfu' + return &curbuf->b_p_cfu; // 'cfu' } } @@ -5917,14 +5810,11 @@ static int get_userdefined_compl_info(colnr_T curs_col, Callback *cb, int *start const bool is_cpt_function = (cb != NULL); const bool use_sandbox = is_cpt_function && was_set_insecurely(curwin, kOptComplete, OPT_LOCAL); if (!is_cpt_function) { - // Call 'completefunc' or 'omnifunc' or 'thesaurusfunc' and get pattern - // length as a string - char *funcname = get_complete_funcname(ctrl_x_mode); - if (*funcname == NUL) { + cb = get_insert_callback(ctrl_x_mode); + if (cb->type == kCallbackNone) { semsg(_(e_notset), ctrl_x_mode_function() ? "completefunc" : "omnifunc"); return FAIL; } - cb = get_insert_callback(ctrl_x_mode); } typval_T args[3]; @@ -6503,9 +6393,9 @@ void free_insexpand_stuff(void) { API_CLEAR_STRING(compl_orig_text); kv_destroy(compl_orig_extmarks); - callback_free(&cfu_cb); - callback_free(&ofu_cb); - callback_free(&tsrfu_cb); + callback_free(&p_cfu); + callback_free(&p_ofu); + callback_free(&p_tsrfu); clear_cpt_callbacks(&cpt_cb, cpt_cb_count); } #endif diff --git a/src/nvim/msgpack_rpc/packer.c b/src/nvim/msgpack_rpc/packer.c index 8ef2546487..2c3aa6b2cf 100644 --- a/src/nvim/msgpack_rpc/packer.c +++ b/src/nvim/msgpack_rpc/packer.c @@ -206,13 +206,19 @@ void mpack_object_inner(Object *current, Object *container, size_t container_idx while (true) { mpack_check_buffer(packer); switch (current->type) { - case kObjectTypeLuaRef: + case kObjectTypeLuaRef: { // TODO(bfredl): could also be an error. Though kObjectTypeLuaRef // should only appear when the caller has opted in to handle references, // see nlua_pop_Object. + + // Human-readable hint string (`:map`-style "" string). + char *repr = nlua_funcref_str(current->data.luaref, NULL); api_free_luaref(current->data.luaref); current->data.luaref = LUA_NOREF; - FALLTHROUGH; + mpack_str(cstr_as_string(repr), packer); + xfree(repr); + break; + } case kObjectTypeUnset: case kObjectTypeNil: mpack_nil(&packer->ptr); diff --git a/src/nvim/ops.c b/src/nvim/ops.c index c577abb838..b58ba21bbd 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -3121,22 +3121,10 @@ static void op_colon(oparg_T *oap) // do_cmdline() does the rest } -/// callback function for 'operatorfunc' -static Callback opfunc_cb; - -/// Process the 'operatorfunc' option value. -const char *did_set_operatorfunc(optset_T *args FUNC_ATTR_UNUSED) -{ - if (option_set_callback_func(p_opfunc, &opfunc_cb) == FAIL) { - return e_invarg; - } - return NULL; -} - #ifdef EXITFREE void free_operatorfunc_option(void) { - callback_free(&opfunc_cb); + callback_free(&p_opfunc); } #endif @@ -3144,7 +3132,7 @@ void free_operatorfunc_option(void) /// garbage collected. bool set_ref_in_opfunc(int copyID) { - return set_ref_in_callback(&opfunc_cb, copyID, NULL, NULL); + return set_ref_in_callback(&p_opfunc, copyID, NULL, NULL); } /// Handle the "g@" operator: call 'operatorfunc'. @@ -3154,7 +3142,7 @@ static void op_function(const oparg_T *oap) const pos_T orig_start = curbuf->b_op_start; const pos_T orig_end = curbuf->b_op_end; - if (*p_opfunc == NUL) { + if (p_opfunc.type == kCallbackNone) { emsg(_("E774: 'operatorfunc' is empty")); } else { // Set '[ and '] marks to text to be operated on. @@ -3185,7 +3173,7 @@ static void op_function(const oparg_T *oap) finish_op = false; typval_T rettv; - if (callback_call(&opfunc_cb, 1, argv, &rettv)) { + if (callback_call(&p_opfunc, 1, argv, &rettv)) { tv_clear(&rettv); } @@ -3744,9 +3732,7 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank) } break; } - op_reindent(oap, - *curbuf->b_p_inde != NUL ? get_expr_indent - : get_c_indent); + op_reindent(oap, curbuf->b_p_inde.type != kCallbackNone ? get_expr_indent : get_c_indent); break; } @@ -3767,7 +3753,7 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank) break; case OP_FORMAT: - if (*curbuf->b_p_fex != NUL) { + if (curbuf->b_p_fex.type != kCallbackNone) { op_formatexpr(oap); // use expression } else { if (*p_fp != NUL || *curbuf->b_p_fp != NUL) { diff --git a/src/nvim/option.c b/src/nvim/option.c index 5a78142198..fc2981cbc8 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -49,8 +49,10 @@ #include "nvim/drawscreen.h" #include "nvim/errors.h" #include "nvim/eval.h" +#include "nvim/eval/encode.h" #include "nvim/eval/typval.h" #include "nvim/eval/typval_defs.h" +#include "nvim/eval/userfunc.h" #include "nvim/eval/vars.h" #include "nvim/eval/window.h" #include "nvim/ex_cmds_defs.h" @@ -1441,6 +1443,13 @@ Object get_option_newval(OptIndex opt_idx, int opt_flags, set_prefix_T prefix, c newval = CSTR_AS_OBJ(newval_str); break; } + case kObjectTypeLuaRef: { + // "Callback" option (e.g. 'omnifunc') whose current value is a Lua function. The new value is + // a plain string (funcname/expr); ":set"-style +=/-= don't apply, so use an empty old value. + const char *newval_str = stropt_get_newval(opt_idx, argp, varp, "", &op); + newval = CSTR_AS_OBJ(newval_str); + break; + } default: abort(); } @@ -3342,13 +3351,16 @@ bool is_dict_option(OptIndex opt_idx) /// Free an allocated option value. void optval_free(Object o) { - // Only strings own memory; don't free the shared empty-string-option sentinel. + // Only strings and Lua callbacks own memory. switch (o.type) { case kObjectTypeString: if (o.data.string.data != empty_string_option) { - api_free_string(o.data.string); + api_free_string(o.data.string); // Don't free the shared empty-string-option sentinel } return; + case kObjectTypeLuaRef: + api_free_luaref(o.data.luaref); + return; case kObjectTypeUnset: case kObjectTypeNil: case kObjectTypeBoolean: @@ -3378,6 +3390,8 @@ bool option_equal(Object o1, Object o2) return o1.data.string.size == o2.data.string.size && (o1.data.string.data == o2.data.string.data || strnequal(o1.data.string.data, o2.data.string.data, o1.data.string.size)); + case kObjectTypeLuaRef: + return false; // Callbacks compare unequal: setting one always counts as a change. default: abort(); // Should not happen. } @@ -3403,11 +3417,79 @@ static ObjectType optval_type(Object o) return kObjectTypeInteger; case kObjectTypeString: return kObjectTypeString; + case kObjectTypeLuaRef: + return kObjectTypeLuaRef; default: abort(); // Should not happen. } } +/// True for a "callback option" (varp stores `Callback`): func option (kOptFlagFunc, e.g. +/// 'operatorfunc') or expr option (kOptFlagExpr, e.g. 'foldexpr'). +static bool is_callback_option(uint32_t flags) +{ + return (flags & (kOptFlagFunc | kOptFlagExpr)) != 0; +} + +/// Converts a callback option's stored Callback to an option value Object. +/// +/// - Lua callback => `LuaRef` (owned by the caller) +/// - named funcref => name +/// - partial => name +/// - expr option => expression string +/// - an unset callback => empty string +static Object opt_from_callback(Callback *cb) +{ + switch (cb->type) { + case kCallbackLua: + return LUAREF_OBJ(api_new_luaref(cb->data.luaref)); + case kCallbackExpr: + return CSTR_TO_OBJ(cb->data.expr); + case kCallbackFuncref: + return CSTR_TO_OBJ(cb->data.funcref); + case kCallbackPartial: { + // Serialize with bound args so the value round-trips losslessly, e.g. `function('F', [10])`. + typval_T tv; + callback_put(cb, &tv); + char *str = encode_tv2string(&tv, NULL); + tv_clear(&tv); + return CSTR_AS_OBJ(str); + } + case kCallbackNone: + break; + } + return STATIC_CSTR_TO_OBJ(""); +} + +/// Builds a callback option's Callback from an option value Object. +/// +/// - `LuaRef` yields a Lua callback. +/// - func option (`is_expr=false`) => string parsed as a function name or lambda +/// - expr option (non-empty) string => expression string +/// - leading ``/`s:` resolves to the current script `` +static Callback opt_to_callback(Object value, bool is_expr) +{ + Callback cb = CALLBACK_NONE; + switch (value.type) { + case kObjectTypeLuaRef: + cb.type = kCallbackLua; + cb.data.luaref = api_new_luaref(value.data.luaref); + break; + case kObjectTypeString: + if (!is_expr) { + option_set_callback_func(value.data.string.data, &cb); + } else if (value.data.string.size > 0) { + cb.type = kCallbackExpr; + char *name = get_scriptlocal_funcname(value.data.string.data); + cb.data.expr = name != NULL ? name : xstrdup(value.data.string.data); + } + break; + default: + break; + } + return cb; +} + /// Creates Object from var pointer. /// /// @param opt_idx Option index in options[] table. @@ -3423,6 +3505,11 @@ Object opt_from_varp(OptIndex opt_idx, void *varp) return BOOLEAN_OBJ(curbufIsChanged()); } + // Callback options (e.g. 'operatorfunc', 'foldexpr') store a Callback, not a scalar. + if (is_callback_option(options[opt_idx].flags)) { + return opt_from_callback((Callback *)varp); + } + switch (options[opt_idx].type) { case kObjectTypeBoolean: // Boolean options are tri-states; kNone (an unset local value) maps to Unset. @@ -3445,6 +3532,16 @@ Object opt_from_varp(OptIndex opt_idx, void *varp) static void set_option_varp(OptIndex opt_idx, void *varp, Object value, bool free_oldval) FUNC_ATTR_NONNULL_ARG(2) { + // Callback options (e.g. 'operatorfunc', 'foldexpr') store a Callback, not a scalar. + if (is_callback_option(options[opt_idx].flags)) { + Callback *cb = (Callback *)varp; + if (free_oldval) { + callback_free(cb); + } + *cb = opt_to_callback(value, options[opt_idx].flags & kOptFlagExpr); + return; + } + if (free_oldval) { optval_free(opt_from_varp(opt_idx, varp)); } @@ -3491,6 +3588,8 @@ static char *optval_to_cstr(Object o) snprintf(buf, o.data.string.size + 3, "\"%s\"", o.data.string.data); return buf; } + case kObjectTypeLuaRef: + return xstrdup("v:lua"); // A Lua callback has no name; show a stable handle. default: abort(); // Should not happen. } @@ -3501,6 +3600,10 @@ static char *optval_to_cstr(Object o) /// @return Object allocated in `arena` (scalar values are returned unchanged). Object optval_to_obj(OptIndex opt_idx, Object value, Arena *arena) { + if (value.type == kObjectTypeLuaRef) { + // Callback option: return an independent ref so it outlives the source value. + return LUAREF_OBJ(api_new_luaref(value.data.luaref)); + } if (value.type != kObjectTypeString) { return value; // boolean/number/nil/unset scalar; already an Object. } @@ -3611,6 +3714,9 @@ Object optval_from_obj(OptIndex opt_idx, Object o, set_op_T op, bool *error) case kObjectTypeDict: type_ok = is_map || is_flaglist; break; + case kObjectTypeLuaRef: + type_ok = is_callback_option(flags); // A callback option accepts a funcref. + break; default: type_ok = false; } @@ -3623,6 +3729,8 @@ Object optval_from_obj(OptIndex opt_idx, Object o, set_op_T op, bool *error) case kObjectTypeBoolean: case kObjectTypeInteger: return o; // Scalar; already type-checked above. + case kObjectTypeLuaRef: + return LUAREF_OBJ(api_new_luaref(o.data.luaref)); // Funcref; stored as a Callback. default: break; // String/Array/Dict are serialized below. } @@ -3973,9 +4081,13 @@ static const char *did_set_option(OptIndex opt_idx, void *varp, Object old_value Object local_unset_value = get_option_unset_value(opt_idx); set_option_varp(opt_idx, varp_local, copy_object(local_unset_value, NULL), true); } else { - // May set global value for local option. + // May set global value for a buffer/window-local option. Skip when varp already is the + // global (a purely-global option): re-storing there is redundant, and for a callback option + // it would free the callback and then fail to re-parse an anonymous lambda's `N`. void *varp_global = get_varp_scope(opt, OPT_GLOBAL); - set_option_varp(opt_idx, varp_global, copy_object(new_value, NULL), true); + if (varp_global != varp) { + set_option_varp(opt_idx, varp_global, copy_object(new_value, NULL), true); + } } } @@ -4047,7 +4159,7 @@ static const char *did_set_option(OptIndex opt_idx, void *varp, Object old_value return errmsg; } -/// Validate the new value for an option. +/// Validates an option value (internal scalar/:set-style form). /// /// @param opt_idx Index in options[] table. Must not be kOptInvalid. /// @param newval[in,out] New option value. Might be modified. @@ -4072,6 +4184,9 @@ static const char *validate_option_value(const OptIndex opt_idx, Object *newval, } else { *newval = copy_object(get_option_unset_value(opt_idx), NULL); } + } else if (newval->type == kObjectTypeLuaRef) { + // A callback option accepts a funcref; scalar validation doesn't apply. + assert(is_callback_option(opt->flags)); } else if (!option_has_type(opt_idx, optval_type(*newval))) { char *rep = optval_to_cstr(*newval); const char *type_str = optval_type_name(opt->type); @@ -4079,6 +4194,14 @@ static const char *validate_option_value(const OptIndex opt_idx, Object *newval, opt->fullname, type_str, optval_type_name(optval_type(*newval)), rep); xfree(rep); errmsg = errbuf; + } else if ((opt->flags & kOptFlagFunc) && newval->data.string.size > 0) { + // Callback option: the string must parse to a valid function reference or lambda. Validate + // here (before storing) so an invalid value leaves the old callback intact. + Callback cb = CALLBACK_NONE; + if (option_set_callback_func(newval->data.string.data, &cb) == FAIL) { + errmsg = e_invarg; + } + callback_free(&cb); } else if (newval->type == kObjectTypeInteger) { // Validate and bound check num option values. errmsg = validate_num_option(opt_idx, &newval->data.integer, errbuf, errbuflen); @@ -5095,13 +5218,13 @@ void *get_varp_from(vimoption_T *p, buf_T *buf, win_T *win) case kOptThesaurus: return *buf->b_p_tsr != NUL ? &(buf->b_p_tsr) : p->var; case kOptThesaurusfunc: - return *buf->b_p_tsrfu != NUL ? &(buf->b_p_tsrfu) : p->var; + return buf->b_p_tsrfu.type != kCallbackNone ? &(buf->b_p_tsrfu) : p->var; case kOptFormatprg: return *buf->b_p_fp != NUL ? &(buf->b_p_fp) : p->var; case kOptFsync: return buf->b_p_fs >= 0 ? &(buf->b_p_fs) : p->var; case kOptFindfunc: - return *buf->b_p_ffu != NUL ? &(buf->b_p_ffu) : p->var; + return buf->b_p_ffu.type != kCallbackNone ? &(buf->b_p_ffu) : p->var; case kOptErrorformat: return *buf->b_p_efm != NUL ? &(buf->b_p_efm) : p->var; case kOptGrepformat: @@ -5384,12 +5507,10 @@ char *get_equalprg(void) } /// Get the value of 'findfunc', either the buffer-local one or the global one. -char *get_findfunc(void) +/// Returns the effective 'findfunc' callback: the buffer-local one if set, else the global one. +Callback *get_findfunc(void) { - if (*curbuf->b_p_ffu == NUL) { - return p_ffu; - } - return curbuf->b_p_ffu; + return curbuf->b_p_ffu.type != kCallbackNone ? &curbuf->b_p_ffu : &p_ffu; } /// Copy options from one window to another. @@ -5463,8 +5584,8 @@ void copy_winopt(winopt_T *from, winopt_T *to) to->wo_fdm = copy_option_val(from->wo_fdm); to->wo_fdm_save = from->wo_diff_saved ? xstrdup(from->wo_fdm_save) : empty_string_option; to->wo_fdn = from->wo_fdn; - to->wo_fde = copy_option_val(from->wo_fde); - to->wo_fdt = copy_option_val(from->wo_fdt); + callback_copy(&to->wo_fde, &from->wo_fde); + callback_copy(&to->wo_fdt, &from->wo_fdt); to->wo_fmr = copy_option_val(from->wo_fmr); to->wo_scl = copy_option_val(from->wo_scl); to->wo_lhi = from->wo_lhi; @@ -5498,8 +5619,6 @@ static void check_winopt(winopt_T *wop) check_string_option(&wop->wo_fdi); check_string_option(&wop->wo_fdm); check_string_option(&wop->wo_fdm_save); - check_string_option(&wop->wo_fde); - check_string_option(&wop->wo_fdt); check_string_option(&wop->wo_fmr); check_string_option(&wop->wo_eiw); check_string_option(&wop->wo_scl); @@ -5526,8 +5645,8 @@ void clear_winopt(winopt_T *wop) clear_string_option(&wop->wo_fdi); clear_string_option(&wop->wo_fdm); clear_string_option(&wop->wo_fdm_save); - clear_string_option(&wop->wo_fde); - clear_string_option(&wop->wo_fdt); + callback_free(&wop->wo_fde); + callback_free(&wop->wo_fdt); clear_string_option(&wop->wo_fmr); clear_string_option(&wop->wo_eiw); clear_string_option(&wop->wo_scl); @@ -5685,15 +5804,12 @@ void buf_copy_options(buf_T *buf, int flags) buf->b_p_csl = xstrdup(p_csl); COPY_OPT_SCTX(buf, kBufOptCompleteslash); #endif - buf->b_p_cfu = xstrdup(p_cfu); + callback_copy(&buf->b_p_cfu, &p_cfu); COPY_OPT_SCTX(buf, kBufOptCompletefunc); - set_buflocal_cfu_callback(buf); - buf->b_p_ofu = xstrdup(p_ofu); + callback_copy(&buf->b_p_ofu, &p_ofu); COPY_OPT_SCTX(buf, kBufOptOmnifunc); - set_buflocal_ofu_callback(buf); - buf->b_p_tfu = xstrdup(p_tfu); + callback_copy(&buf->b_p_tfu, &p_tfu); COPY_OPT_SCTX(buf, kBufOptTagfunc); - set_buflocal_tfu_callback(buf); buf->b_p_sts = p_sts; COPY_OPT_SCTX(buf, kBufOptSofttabstop); buf->b_p_sts_nopaste = p_sts_nopaste; @@ -5757,13 +5873,13 @@ void buf_copy_options(buf_T *buf, int flags) buf->b_s.b_p_spo = xstrdup(p_spo); COPY_OPT_SCTX(buf, kBufOptSpelloptions); buf->b_s.b_p_spo_flags = spo_flags; - buf->b_p_inde = xstrdup(p_inde); + callback_copy(&buf->b_p_inde, &p_inde); COPY_OPT_SCTX(buf, kBufOptIndentexpr); COPY_OPT_INSECURE(buf->b_p_inde_flags, kBufOptIndentexpr); buf->b_p_indk = xstrdup(p_indk); COPY_OPT_SCTX(buf, kBufOptIndentkeys); buf->b_p_fp = empty_string_option; - buf->b_p_fex = xstrdup(p_fex); + callback_copy(&buf->b_p_fex, &p_fex); COPY_OPT_SCTX(buf, kBufOptFormatexpr); COPY_OPT_INSECURE(buf->b_p_fex_flags, kBufOptFormatexpr); buf->b_p_sua = xstrdup(p_sua); @@ -5791,7 +5907,7 @@ void buf_copy_options(buf_T *buf, int flags) buf->b_p_mp = empty_string_option; buf->b_p_efm = empty_string_option; buf->b_p_ep = empty_string_option; - buf->b_p_ffu = empty_string_option; + buf->b_p_ffu = CALLBACK_NONE; buf->b_p_kp = empty_string_option; buf->b_p_path = empty_string_option; buf->b_p_tags = empty_string_option; @@ -5799,7 +5915,7 @@ void buf_copy_options(buf_T *buf, int flags) buf->b_tc_flags = 0; buf->b_p_def = empty_string_option; buf->b_p_inc = empty_string_option; - buf->b_p_inex = xstrdup(p_inex); + callback_copy(&buf->b_p_inex, &p_inex); COPY_OPT_SCTX(buf, kBufOptIncludeexpr); COPY_OPT_INSECURE(buf->b_p_inex_flags, kBufOptIncludeexpr); buf->b_p_cot = empty_string_option; @@ -5807,7 +5923,7 @@ void buf_copy_options(buf_T *buf, int flags) buf->b_p_dict = empty_string_option; buf->b_p_dia = empty_string_option; buf->b_p_tsr = empty_string_option; - buf->b_p_tsrfu = empty_string_option; + buf->b_p_tsrfu = CALLBACK_NONE; buf->b_p_qe = xstrdup(p_qe); COPY_OPT_SCTX(buf, kBufOptQuoteescape); buf->b_p_udf = p_udf; @@ -6448,6 +6564,14 @@ static void option_value2string(vimoption_T *opt, int opt_flags) void *varp = get_varp_scope(opt, opt_flags); assert(varp != NULL); + if (is_callback_option(opt->flags)) { + // Callback option: render via its option value (funcref name, expression, or Lua handle). + Object o = opt_from_varp(get_opt_idx(opt), varp); + xstrlcpy(NameBuff, o.type == kObjectTypeString ? o.data.string.data : "v:lua", MAXPATHL); + api_free_object(o); + return; + } + if (option_has_type(get_opt_idx(opt), kObjectTypeInteger)) { OptInt wc = 0; diff --git a/src/nvim/option_defs.h b/src/nvim/option_defs.h index 5955e0ae10..0e87cd5fed 100644 --- a/src/nvim/option_defs.h +++ b/src/nvim/option_defs.h @@ -38,8 +38,9 @@ typedef enum { kOptFlagNDname = 1 << 21, ///< Only normal directory name chars allowed. kOptFlagHLOnly = 1 << 22, ///< Option only changes highlight, not text. kOptFlagMLE = 1 << 23, ///< Under control of 'modelineexpr'. - kOptFlagFunc = 1 << 24, ///< Accept a function reference or a lambda. + kOptFlagFunc = 1 << 24, ///< Callback option: Lua function, or Vimscript funcref. kOptFlagColon = 1 << 25, ///< Values use colons to create sublists. + kOptFlagExpr = 1 << 26, ///< Callback option: Vimscript expr, e.g. 'foldexpr'. } OptFlags; /// Scopes that an option can support. diff --git a/src/nvim/option_vars.h b/src/nvim/option_vars.h index 2eca00a964..4aa95d57f2 100644 --- a/src/nvim/option_vars.h +++ b/src/nvim/option_vars.h @@ -1,5 +1,6 @@ #pragma once +#include "nvim/eval/typval_defs.h" // IWYU pragma: keep #include "nvim/macros_defs.h" #include "nvim/option_defs.h" #include "nvim/os/os_defs.h" @@ -179,9 +180,9 @@ EXTERN OptInt p_channel; ///< 'channel' EXTERN char *p_cink; ///< 'cinkeys' EXTERN char *p_cinsd; ///< 'cinscopedecls' EXTERN char *p_cinw; ///< 'cinwords' -EXTERN char *p_cfu; ///< 'completefunc' -EXTERN char *p_ofu; ///< 'omnifunc' -EXTERN char *p_tsrfu; ///< 'thesaurusfunc' +EXTERN Callback p_cfu; ///< 'completefunc' +EXTERN Callback p_ofu; ///< 'omnifunc' +EXTERN Callback p_tsrfu; ///< 'thesaurusfunc' EXTERN int p_ci; ///< 'copyindent' EXTERN int p_ar; ///< 'autoread' EXTERN int p_aw; ///< 'autowrite' @@ -205,7 +206,7 @@ EXTERN char *p_cmp; ///< 'casemap' EXTERN unsigned cmp_flags; EXTERN char *p_enc; ///< 'encoding' EXTERN int p_deco; ///< 'delcombine' -EXTERN char *p_ccv; ///< 'charconvert' +EXTERN Callback p_ccv; ///< 'charconvert' EXTERN char *p_cino; ///< 'cinoptions' EXTERN char *p_cedit; ///< 'cedit' EXTERN char *p_cb; ///< 'clipboard' @@ -239,7 +240,7 @@ EXTERN char *p_def; ///< 'define' EXTERN char *p_inc; EXTERN char *p_dia; ///< 'diffanchors' EXTERN char *p_dip; ///< 'diffopt' -EXTERN char *p_dex; ///< 'diffexpr' +EXTERN Callback p_dex; ///< 'diffexpr' EXTERN char *p_dict; ///< 'dictionary' EXTERN int p_dg; ///< 'digraph' EXTERN char *p_dir; ///< 'directory' @@ -266,13 +267,13 @@ EXTERN char *p_ffs; ///< 'fileformats' EXTERN int p_fic; ///< 'fileignorecase' EXTERN char *p_ft; ///< 'filetype' EXTERN char *p_fcs; ///< 'fillchar' -EXTERN char *p_ffu; ///< 'findfunc' +EXTERN Callback p_ffu; ///< 'findfunc' EXTERN int p_fixeol; ///< 'fixendofline' EXTERN char *p_fcl; ///< 'foldclose' EXTERN OptInt p_fdls; ///< 'foldlevelstart' EXTERN char *p_fdo; ///< 'foldopen' EXTERN unsigned fdo_flags; -EXTERN char *p_fex; ///< 'formatexpr' +EXTERN Callback p_fex; ///< 'formatexpr' EXTERN char *p_flp; ///< 'formatlistpat' EXTERN char *p_fo; ///< 'formatoptions' EXTERN char *p_fp; ///< 'formatprg' @@ -295,9 +296,9 @@ EXTERN int p_ic; ///< 'ignorecase' EXTERN OptInt p_iminsert; ///< 'iminsert' EXTERN OptInt p_imsearch; ///< 'imsearch' EXTERN int p_inf; ///< 'infercase' -EXTERN char *p_inex; ///< 'includeexpr' +EXTERN Callback p_inex; ///< 'includeexpr' EXTERN int p_is; ///< 'incsearch' -EXTERN char *p_inde; ///< 'indentexpr' +EXTERN Callback p_inde; ///< 'indentexpr' EXTERN char *p_indk; ///< 'indentkeys' EXTERN char *p_icm; ///< 'inccommand' EXTERN char *p_isf; ///< 'isfname' @@ -354,10 +355,10 @@ EXTERN OptInt p_mousescroll_hor INIT( = MOUSESCROLL_HOR_DFLT); EXTERN OptInt p_mouset; ///< 'mousetime' EXTERN int p_more; ///< 'more' EXTERN char *p_nf; ///< 'nrformats' -EXTERN char *p_opfunc; ///< 'operatorfunc' +EXTERN Callback p_opfunc; ///< 'operatorfunc' EXTERN char *p_para; ///< 'paragraphs' EXTERN int p_paste; ///< 'paste' -EXTERN char *p_pex; ///< 'patchexpr' +EXTERN Callback p_pex; ///< 'patchexpr' EXTERN char *p_pm; ///< 'patchmode' EXTERN char *p_path; ///< 'path' EXTERN char *p_cdpath; ///< 'cdpath' @@ -379,7 +380,7 @@ EXTERN int p_ru; ///< 'ruler' EXTERN char *p_ruf; ///< 'rulerformat' EXTERN char *p_plf; ///< 'packlockfile' EXTERN char *p_pp; ///< 'packpath' -EXTERN char *p_qftf; ///< 'quickfixtextfunc' +EXTERN Callback p_qftf; ///< 'quickfixtextfunc' EXTERN char *p_rtp; ///< 'runtimepath' EXTERN OptInt p_scbk; ///< 'scrollback' EXTERN OptInt p_sj; ///< 'scrolljump' @@ -428,7 +429,7 @@ EXTERN OptInt p_tpm; ///< 'tabpagemax' EXTERN char *p_tal; ///< 'tabline' EXTERN char *p_tpf; ///< 'termpastefilter' EXTERN unsigned tpf_flags; ///< flags from 'termpastefilter' -EXTERN char *p_tfu; ///< 'tagfunc' +EXTERN Callback p_tfu; ///< 'tagfunc' EXTERN char *p_spc; ///< 'spellcapcheck' EXTERN char *p_spf; ///< 'spellfile' EXTERN char *p_spl; ///< 'spelllang' diff --git a/src/nvim/options.lua b/src/nvim/options.lua index d429a34779..d1dd4f3d24 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -1,40 +1,36 @@ -- vim: tw=78 --- @class vim.option_meta ---- @field full_name string ---- @field desc? string --- @field abbreviation? string --- @field alias? string|string[] ---- @field short_desc? string|fun(): string ---- @field varname? string ---- @field flags_varname? string ---- @field type vim.option_type ---- @field immutable? boolean ---- @field list? 'comma'|'onecomma'|'commacolon'|'onecommacolon'|'flags'|'flagscomma' ---- @field scope vim.option_scope[] ---- @field deny_duplicates? boolean ---- @field enable_if? string ---- @field defaults? vim.option_defaults|vim.option_value|fun(): string ---- @field schema? vim.option_schema ---- @field secure? true ---- @field noglob? true ---- @field normal_fname_chars? true ---- @field pri_mkrc? true ---- @field normal_dname_chars? true ---- @field modelineexpr? true ---- @field func? true ---- @field expand? string|true ---- @field nodefault? true ---- @field no_mkrc? true ---- @field alloced? true ---- @field redraw? vim.option_redraw[] ---- --- If not provided and `values` is present, then is set to 'did_set_str_generic' --- @field cb? string ---- +--- @field defaults? vim.option_defaults|vim.option_value|fun(): string +--- @field deny_duplicates? boolean +--- @field desc? string +--- @field enable_if? string +--- @field expand? string|true --- If not provided and `values` is present, then is set to 'expand_set_str_generic' --- @field expand_cb? string +--- @field flags_varname? string +--- @field full_name string +--- @field immutable? boolean +--- @field list? 'comma'|'onecomma'|'commacolon'|'onecommacolon'|'flags'|'flagscomma' +--- @field modelineexpr? true +--- @field no_mkrc? true +--- @field nodefault? true +--- @field noglob? true +--- @field normal_dname_chars? true +--- @field normal_fname_chars? true +--- @field pri_mkrc? true +--- @field redraw? vim.option_redraw[] +--- @field schema? vim.option_schema +--- @field scope vim.option_scope[] +--- @field secure? true +--- @field short_desc? string|fun(): string --- @field tags? string[] +--- @field type vim.option_type +--- @field varname? string --- @class vim.option_defaults --- @field condition? string @@ -45,8 +41,10 @@ --- @field doc? string Default to show in options.txt --- @field meta? string Default to use in Lua meta files ---- @alias vim.option_scope 'global'|'buf'|'win' ---- @alias vim.option_type 'boolean'|'number'|'string' +--- @alias vim.option_scope 'global'|'buf'|'win'|'tab' +--- Option value type. `func` ('omnifunc') and `expr` ('foldexpr') are "callback" options: stored +--- as a `Callback`, accepting a function name/expression string or a Lua function. +--- @alias vim.option_type 'boolean'|'number'|'string'|'func'|'expr' --- @alias vim.option_value boolean|integer|string --- Options for a `char`/`chars` schema key. @@ -1179,7 +1177,6 @@ local options = { }, { abbreviation = 'ccv', - cb = 'did_set_optexpr', defaults = '', desc = [=[ An expression that is used for character encoding conversion. It is @@ -1227,7 +1224,7 @@ local options = { scope = { 'global' }, secure = true, short_desc = N_('expression for character encoding conversion'), - type = 'string', + type = 'expr', tags = { 'E202', 'E214', 'E513' }, varname = 'p_ccv', }, @@ -1600,7 +1597,6 @@ local options = { }, { abbreviation = 'cfu', - cb = 'did_set_completefunc', defaults = '', desc = [=[ This option specifies a function to be used for Insert mode completion @@ -1611,11 +1607,10 @@ local options = { more information. ]=], full_name = 'completefunc', - func = true, scope = { 'buf' }, secure = true, short_desc = N_('function to be used for Insert mode completion'), - type = 'string', + type = 'func', varname = 'p_cfu', }, { @@ -2442,7 +2437,6 @@ local options = { }, { abbreviation = 'dex', - cb = 'did_set_optexpr', defaults = '', desc = [=[ Expression which is evaluated to obtain a diff file (either ed-style @@ -2453,7 +2447,7 @@ local options = { scope = { 'global' }, secure = true, short_desc = N_('expression used to obtain a diff file'), - type = 'string', + type = 'expr', varname = 'p_dex', }, { @@ -3424,7 +3418,6 @@ local options = { }, { abbreviation = 'ffu', - cb = 'did_set_findfunc', defaults = '', desc = [=[ Function that is called to obtain the filename(s) for the |:find| @@ -3473,12 +3466,11 @@ local options = { < ]=], full_name = 'findfunc', - func = true, scope = { 'global', 'buf' }, secure = true, short_desc = N_('function called for :find'), tags = { 'E1514' }, - type = 'string', + type = 'func', varname = 'p_ffu', }, { @@ -3604,7 +3596,7 @@ local options = { redraw = { 'current_window' }, scope = { 'win' }, short_desc = N_('expression used when \'foldmethod\' is "expr"'), - type = 'string', + type = 'expr', }, { abbreviation = 'fdi', @@ -3797,7 +3789,6 @@ local options = { }, { abbreviation = 'fdt', - cb = 'did_set_optexpr', defaults = 'foldtext()', desc = [=[ An expression which is used to specify the text displayed for a closed @@ -3820,11 +3811,10 @@ local options = { redraw = { 'current_window' }, scope = { 'win' }, short_desc = N_('expression used to display for a closed fold'), - type = 'string', + type = 'expr', }, { abbreviation = 'fex', - cb = 'did_set_optexpr', defaults = '', desc = [=[ Expression which is evaluated to format a range of lines for the |gq| @@ -3871,7 +3861,7 @@ local options = { modelineexpr = true, scope = { 'buf' }, short_desc = N_('expression used with "gq" command'), - type = 'string', + type = 'expr', varname = 'p_fex', }, { @@ -4782,7 +4772,6 @@ local options = { }, { abbreviation = 'inex', - cb = 'did_set_optexpr', defaults = '', desc = [=[ Expression to be used to transform the string found with the 'include' @@ -4819,7 +4808,7 @@ local options = { modelineexpr = true, scope = { 'buf' }, short_desc = N_('expression used to process an include line'), - type = 'string', + type = 'expr', varname = 'p_inex', }, { @@ -4867,7 +4856,6 @@ local options = { }, { abbreviation = 'inde', - cb = 'did_set_optexpr', defaults = '', desc = [=[ Expression which is evaluated to obtain the proper indent for a line. @@ -4915,7 +4903,7 @@ local options = { modelineexpr = true, scope = { 'buf' }, short_desc = N_('expression used to obtain the indent of a line'), - type = 'string', + type = 'expr', varname = 'p_inde', }, { @@ -6578,7 +6566,6 @@ local options = { }, { abbreviation = 'ofu', - cb = 'did_set_omnifunc', defaults = '', desc = [=[ This option specifies a function to be used for Insert mode omni @@ -6591,11 +6578,10 @@ local options = { |:filetype-plugin-on| ]=], full_name = 'omnifunc', - func = true, scope = { 'buf' }, secure = true, short_desc = N_('function for filetype-specific completion'), - type = 'string', + type = 'func', varname = 'p_ofu', }, { @@ -6617,7 +6603,6 @@ local options = { }, { abbreviation = 'opfunc', - cb = 'did_set_operatorfunc', defaults = '', desc = [=[ This option specifies a function to be called by the |g@| operator. @@ -6626,11 +6611,10 @@ local options = { |option-value-function| for more information. ]=], full_name = 'operatorfunc', - func = true, scope = { 'global' }, secure = true, short_desc = N_('function to be called for |g@| operator'), - type = 'string', + type = 'func', varname = 'p_opfunc', }, { @@ -6706,7 +6690,6 @@ local options = { }, { abbreviation = 'pex', - cb = 'did_set_optexpr', defaults = '', desc = [=[ Expression which is evaluated to apply a patch to a file and generate @@ -6716,7 +6699,7 @@ local options = { scope = { 'global' }, secure = true, short_desc = N_('expression used to patch a file'), - type = 'string', + type = 'expr', varname = 'p_pex', }, { @@ -7023,7 +7006,6 @@ local options = { }, { abbreviation = 'qftf', - cb = 'did_set_quickfixtextfunc', defaults = '', desc = [=[ This option specifies a function to be used to get the text to display @@ -7039,11 +7021,10 @@ local options = { evaluating 'qftf' |textlock|. ]=], full_name = 'quickfixtextfunc', - func = true, scope = { 'global' }, secure = true, short_desc = N_('customize the quickfix window'), - type = 'string', + type = 'func', varname = 'p_qftf', }, { @@ -9694,7 +9675,6 @@ local options = { }, { abbreviation = 'tfu', - cb = 'did_set_tagfunc', defaults = '', desc = [=[ This option specifies a function to be used to perform tag searches @@ -9706,11 +9686,10 @@ local options = { information. ]=], full_name = 'tagfunc', - func = true, scope = { 'buf' }, secure = true, short_desc = N_('function used to perform tag searches'), - type = 'string', + type = 'func', varname = 'p_tfu', }, { @@ -9943,7 +9922,6 @@ local options = { }, { abbreviation = 'tsrfu', - cb = 'did_set_thesaurusfunc', defaults = '', desc = [=[ This option specifies a function to be used for thesaurus completion @@ -9952,11 +9930,10 @@ local options = { See |option-value-function| for more information. ]=], full_name = 'thesaurusfunc', - func = true, scope = { 'global', 'buf' }, secure = true, short_desc = N_('function used for thesaurus completion'), - type = 'string', + type = 'func', varname = 'p_tsrfu', }, { @@ -11328,7 +11305,7 @@ local function preprocess(o) if type(o.defaults) ~= 'table' then o.defaults = { - if_true = o.defaults --[[@as string|boolean|number ]], + if_true = o.defaults --[[@as any]], } end end diff --git a/src/nvim/optionstr.c b/src/nvim/optionstr.c index b657c0d4d9..a019af88b2 100644 --- a/src/nvim/optionstr.c +++ b/src/nvim/optionstr.c @@ -137,11 +137,8 @@ void check_buf_options(buf_T *buf) check_string_option(&buf->b_p_ff); check_string_option(&buf->b_p_def); check_string_option(&buf->b_p_inc); - check_string_option(&buf->b_p_inex); - check_string_option(&buf->b_p_inde); check_string_option(&buf->b_p_indk); check_string_option(&buf->b_p_fp); - check_string_option(&buf->b_p_fex); check_string_option(&buf->b_p_kp); check_string_option(&buf->b_p_mps); check_string_option(&buf->b_p_fo); @@ -167,8 +164,6 @@ void check_buf_options(buf_T *buf) check_string_option(&buf->b_p_cinsd); check_string_option(&buf->b_p_cot); check_string_option(&buf->b_p_cpt); - check_string_option(&buf->b_p_cfu); - check_string_option(&buf->b_p_ofu); check_string_option(&buf->b_p_keymap); check_string_option(&buf->b_p_gefm); check_string_option(&buf->b_p_gp); @@ -177,13 +172,10 @@ void check_buf_options(buf_T *buf) check_string_option(&buf->b_p_ep); check_string_option(&buf->b_p_path); check_string_option(&buf->b_p_tags); - check_string_option(&buf->b_p_ffu); - check_string_option(&buf->b_p_tfu); check_string_option(&buf->b_p_tc); check_string_option(&buf->b_p_dict); check_string_option(&buf->b_p_dia); check_string_option(&buf->b_p_tsr); - check_string_option(&buf->b_p_tsrfu); check_string_option(&buf->b_p_lw); check_string_option(&buf->b_p_bkc); check_string_option(&buf->b_p_menc); @@ -1257,7 +1249,6 @@ const char *did_set_filetype_or_syntax(optset_T *args) const char *did_set_foldexpr(optset_T *args) { win_T *win = (win_T *)args->os_win; - did_set_optexpr(args); if (foldmethodIsExpr(win)) { foldUpdateAll(win); } @@ -1570,22 +1561,6 @@ const char *did_set_mousescroll(optset_T *args FUNC_ATTR_UNUSED) return has_dir ? NULL : e_invarg; } -/// One of the '*expr' options is changed:, 'diffexpr', 'foldexpr', 'foldtext', -/// 'formatexpr', 'includeexpr', 'indentexpr', 'patchexpr' and 'charconvert'. -const char *did_set_optexpr(optset_T *args) -{ - char **varp = (char **)args->os_varp; - - // If the option value starts with or s:, then replace that with - // the script identifier. - char *name = get_scriptlocal_funcname(*varp); - if (name != NULL) { - free_string_option(*varp); - *varp = name; - } - return NULL; -} - /// The 'rulerformat' option is changed. const char *did_set_rulerformat(optset_T *args) { diff --git a/src/nvim/quickfix.c b/src/nvim/quickfix.c index b0a7d73ed9..00a2cd9154 100644 --- a/src/nvim/quickfix.c +++ b/src/nvim/quickfix.c @@ -595,9 +595,6 @@ static int efm_to_regpat(const char *efm, int len, efm_T *fmt_ptr, char *regpat) static efm_T *fmt_start = NULL; // cached across qf_parse_line() calls -// callback function for 'quickfixtextfunc' -static Callback qftf_cb; - static void free_efm_list(efm_T **efm_first) { for (efm_T *efm_ptr = *efm_first; efm_ptr != NULL; efm_ptr = *efm_first) { @@ -4166,14 +4163,6 @@ static buf_T *qf_find_buf(qf_info_T *qi) } /// Process the 'quickfixtextfunc' option value. -const char *did_set_quickfixtextfunc(optset_T *args FUNC_ATTR_UNUSED) -{ - if (option_set_callback_func(p_qftf, &qftf_cb) == FAIL) { - return e_invarg; - } - return NULL; -} - /// Update the w:quickfix_title variable in the quickfix/location list window in /// all the tab pages. static void qf_update_win_titlevar(qf_info_T *qi) @@ -4338,7 +4327,7 @@ static int qf_buf_add_line(qf_list_T *qfl, buf_T *buf, linenr_T lnum, const qfli // the quickfix window for the entries 'start_idx' to 'end_idx'. static list_T *call_qftf_func(qf_list_T *qfl, int qf_winid, int start_idx, int end_idx) { - Callback *cb = &qftf_cb; + Callback *cb = &p_qftf; list_T *qftf_list = NULL; static bool recursive = false; @@ -7222,7 +7211,7 @@ bool set_ref_in_quickfix(int copyID) assert(ql_info != NULL); if (mark_quickfix_ctx(ql_info, copyID) || mark_quickfix_user_data(ql_info, copyID) - || set_ref_in_callback(&qftf_cb, copyID, NULL, NULL)) { + || set_ref_in_callback(&p_qftf, copyID, NULL, NULL)) { return true; } @@ -7710,6 +7699,7 @@ void free_quickfix(void) qf_free_all(win); } + callback_free(&p_qftf); ga_clear(&qfga); } #endif diff --git a/src/nvim/tag.c b/src/nvim/tag.c index 5d7a66973d..e00b9b874d 100644 --- a/src/nvim/tag.c +++ b/src/nvim/tag.c @@ -225,35 +225,14 @@ static char *tagmatchname = NULL; // name of last used tag static taggy_T ptag_entry = { NULL, INIT_FMARK, 0, 0, NULL }; static bool tfu_in_use = false; // disallow recursive call of tagfunc -static Callback tfu_cb; // 'tagfunc' callback function // Used instead of NUL to separate tag fields in the growarrays. #define TAG_SEP 0x02 -/// Reads the 'tagfunc' option value and convert that to a callback value. -/// Invoked when the 'tagfunc' option is set. The option value can be a name of -/// a function (string), or function() or funcref() or a lambda. -const char *did_set_tagfunc(optset_T *args) -{ - buf_T *buf = (buf_T *)args->os_buf; - int retval; - - if (args->os_flags & OPT_LOCAL) { - retval = option_set_callback_func(args->os_newval.data.string.data, &buf->b_tfu_cb); - } else { - retval = option_set_callback_func(args->os_newval.data.string.data, &tfu_cb); - if (retval == OK && !(args->os_flags & OPT_GLOBAL)) { - set_buflocal_tfu_callback(buf); - } - } - - return retval == FAIL ? e_invarg : NULL; -} - #ifdef EXITFREE void free_tagfunc_option(void) { - callback_free(&tfu_cb); + callback_free(&p_tfu); } #endif @@ -261,17 +240,7 @@ void free_tagfunc_option(void) /// collected. bool set_ref_in_tagfunc(int copyID) { - return set_ref_in_callback(&tfu_cb, copyID, NULL, NULL); -} - -/// Copy the global 'tagfunc' callback function to the buffer-local 'tagfunc' -/// callback for 'buf'. -void set_buflocal_tfu_callback(buf_T *buf) -{ - callback_free(&buf->b_tfu_cb); - if (tfu_cb.type != kCallbackNone) { - callback_copy(&buf->b_tfu_cb, &tfu_cb); - } + return set_ref_in_callback(&p_tfu, copyID, NULL, NULL); } /// Jump to tag; handling of tag commands and tag stack @@ -1114,7 +1083,7 @@ static int find_tagfunc_tags(char *pat, garray_T *ga, int *match_count, int flag } } - if (*curbuf->b_p_tfu == NUL || curbuf->b_tfu_cb.type == kCallbackNone) { + if (curbuf->b_p_tfu.type == kCallbackNone) { return FAIL; } @@ -1145,7 +1114,7 @@ static int find_tagfunc_tags(char *pat, garray_T *ga, int *match_count, int flag flags & TAG_REGEXP ? "r" : ""); pos_T save_pos = curwin->w_cursor; - int result = callback_call(&curbuf->b_tfu_cb, 3, args, &rettv); + int result = callback_call(&curbuf->b_p_tfu, 3, args, &rettv); curwin->w_cursor = save_pos; // restore the cursor position check_cursor(curwin); // make sure cursor position is valid d->dv_refcount--; @@ -1416,7 +1385,7 @@ static int findtags_apply_tfu(findtags_state_T *st, char *pat, char *buf_ffname) { const bool use_tfu = ((st->flags & TAG_NO_TAGFUNC) == 0); - if (!use_tfu || tfu_in_use || *curbuf->b_p_tfu == NUL) { + if (!use_tfu || tfu_in_use || curbuf->b_p_tfu.type == kCallbackNone) { return NOTDONE; } diff --git a/src/nvim/textformat.c b/src/nvim/textformat.c index 7bfe5367ca..e4b55a710b 100644 --- a/src/nvim/textformat.c +++ b/src/nvim/textformat.c @@ -11,6 +11,7 @@ #include "nvim/cursor.h" #include "nvim/drawscreen.h" #include "nvim/eval.h" +#include "nvim/eval/typval.h" #include "nvim/eval/typval_defs.h" #include "nvim/eval/vars.h" #include "nvim/ex_cmds_defs.h" @@ -875,21 +876,12 @@ int fex_format(linenr_T lnum, long count, int c) set_vim_var_nr(VV_COUNT, (varnumber_T)count); set_vim_var_char(c); - // Make a copy, the option could be changed while calling it. - char *fex = xstrdup(curbuf->b_p_fex); current_sctx = curbuf->b_p_script_ctx[kBufOptFormatexpr]; - // Evaluate the function. - if (use_sandbox) { - sandbox++; - } - int r = (int)eval_to_number(fex, true); - if (use_sandbox) { - sandbox--; - } + // 'formatexpr' may modify the buffer, so it runs without textlock. + int r = eval_expr_option_number(&curbuf->b_p_fex, use_sandbox); set_vim_var_string(VV_CHAR, NULL, -1); - xfree(fex); current_sctx = save_sctx; return r; @@ -1044,7 +1036,7 @@ void format_lines(linenr_T line_count, bool avoid_fex) indent = get_lisp_indent(); } else { if (cindent_on()) { - indent = *curbuf->b_p_inde != NUL ? get_expr_indent() : get_c_indent(); + indent = curbuf->b_p_inde.type != kCallbackNone ? get_expr_indent() : get_c_indent(); } else { indent = get_indent(); } diff --git a/test/functional/api/command_spec.lua b/test/functional/api/command_spec.lua index 554bd5b695..7bc3790801 100644 --- a/test/functional/api/command_spec.lua +++ b/test/functional/api/command_spec.lua @@ -187,9 +187,10 @@ describe('nvim_get_commands', function() addr = NIL, bang = false, bar = false, - callback = NIL, -- RPC serializes Lua callback as NIL. - complete = NIL, -- RPC serializes Lua callback as NIL. - preview = NIL, -- RPC serializes Lua callback as NIL. + -- RPC serializes Lua func as "" (normalized below). + callback = '', + complete = '', + preview = '', complete_arg = NIL, count = NIL, definition = '', @@ -268,6 +269,10 @@ describe('nvim_get_commands', function() ]]) -- TODO(justinmk): Order is stable but undefined. Sort before return? local commands = api.nvim_get_commands({ builtin = false }) + -- Normalize the volatile ref id in the "" funcref hints. + for _, k in ipairs({ 'callback', 'complete', 'preview' }) do + commands.PreviewLuaCmd[k] = (commands.PreviewLuaCmd[k]:gsub('', '')) + end eq({ Cmd2 = cmd2, Cmd3 = cmd3, diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 5b76d8a8c7..73a9aea87d 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -1898,6 +1898,11 @@ describe('API', function() ok(not api.nvim_get_option_value('equalalways', {})) end) + it('Lua funcref RPC value is a `:map`-style "" string', function() + exec_lua('vim.o.operatorfunc = function() end') + matches('^$', api.nvim_get_option_value('operatorfunc', {})) + end) + it('works to get global value of local options', function() eq(false, api.nvim_get_option_value('lisp', {})) eq(8, api.nvim_get_option_value('shiftwidth', {})) diff --git a/test/functional/options/options_spec.lua b/test/functional/options/options_spec.lua index 647e05f503..ee381a7806 100644 --- a/test/functional/options/options_spec.lua +++ b/test/functional/options/options_spec.lua @@ -5,7 +5,10 @@ local Screen = require('test.functional.ui.screen') local describe, it, before_each, setup = t.describe, t.it, t.before_each, t.setup local command, clear = n.command, n.clear +local poke_eventloop = n.poke_eventloop local source, expect = n.source, n.expect +local eval = n.eval +local exec_lua = n.exec_lua local matches = t.matches local pcall_err = t.pcall_err local eq = t.eq @@ -119,3 +122,198 @@ describe('options validation', function() ) end) end) + +describe('callback (func/expr) options', function() + before_each(clear) + + it('accept a Lua function, preserving identity and captured upvalues', function() + -- 'operatorfunc' is global; 'tagfunc' is buffer-local: exercise both storage paths. + eq( + { identity = true, captured = 'CAP', kind = 'function' }, + exec_lua(function() + vim.api.nvim_buf_set_lines(0, 0, -1, false, { 'abcdef' }) + local captured = 'CAP' + _G.seen = nil + local fn = function(_) + _G.seen = captured + end + vim.o.operatorfunc = fn + vim.cmd('normal! g@l') + return { + identity = vim.o.operatorfunc == fn, + captured = _G.seen, + kind = type(vim.o.operatorfunc), + } + end) + ) + eq( + true, + exec_lua(function() + local tfn = function(_, _, _) + return {} + end + vim.bo.tagfunc = tfn + return vim.bo.tagfunc == tfn + end) + ) + -- Vimscript GC traverses buffer-local callback options; a Lua-function callback must survive it + -- garbagecollect() defers to main loop, so spin it with a motion to actually run collection. + command('call garbagecollect(1)') + n.feed('ll') + poke_eventloop() + eq('function', exec_lua('return type(vim.bo.tagfunc)')) + end) + + it('accept a string funcref, read back as a string', function() + eq( + { type = 'string', value = 'SomeFunc' }, + exec_lua(function() + vim.o.operatorfunc = 'SomeFunc' + return { type = type(vim.o.operatorfunc), value = vim.o.operatorfunc } + end) + ) + end) + + it('reject a non-function, non-string value', function() + matches( + "Invalid 'operatorfunc': expected a valid type, got Integer", + pcall_err(exec_lua, 'vim.o.operatorfunc = 42') + ) + -- Only callback (func/expr) options accept a function; others reject it. + matches( + "Invalid 'statusline': expected a valid type, got Function", + pcall_err(exec_lua, 'vim.o.statusline = function() end') + ) + matches( + "Invalid 'shiftwidth': expected a valid type, got Function", + pcall_err(exec_lua, 'vim.bo.shiftwidth = function() end') + ) + end) + + it("preserve a partial's bound args when the value is copied to the global default", function() + -- 'tagfunc' is buffer-local; setting it also copies the value to the global default (which + -- new buffers inherit). A partial must keep its bound args across that copy. + command([[ + func! Foo(bound, pat, flags, info) + return [] + endfunc + set tagfunc=function('Foo',\ [10]) + ]]) + eq("function('Foo', [10])", eval('&g:tagfunc')) + end) + + it('keep the previous callback when set to an invalid function', function() + command('set operatorfunc=GoodFunc') + matches('E700:', pcall_err(command, "set operatorfunc=function('NoSuchFuncXYZ')")) + eq('GoodFunc', eval('&operatorfunc')) + end) + + it('with ":set all"', function() + command('set operatorfunc=SomeFunc') + command('set quickfixtextfunc={\\ d\\ ->\\ []\\ }') + exec_lua(function() + vim.o.omnifunc = function() end + vim.wo.foldexpr = function() end + end) + command('set all') + n.assert_alive() + end) + + it("expr option ('foldexpr') accepts string, reads back string", function() + eq( + { kind = 'string', value = 'v:lnum - 1', l1 = 0, l3 = 2 }, + exec_lua(function() + vim.api.nvim_buf_set_lines(0, 0, -1, false, { 'a', 'b', 'c', 'd' }) + vim.wo.foldmethod = 'expr' + vim.wo.foldexpr = 'v:lnum - 1' + return { + kind = type(vim.wo.foldexpr), + value = vim.wo.foldexpr, + l1 = vim.fn.foldlevel(1), + l3 = vim.fn.foldlevel(3), + } + end) + ) + end) + + it("expr option ('foldexpr') accepts Lua function, preserving identity", function() + -- Lua-function 'foldexpr' is invoked per line. + eq( + { identity = true, kind = 'function', l2 = 0, l3 = 1 }, + exec_lua(function() + vim.api.nvim_buf_set_lines(0, 0, -1, false, { 'a', 'b', 'c', 'd' }) + local fn = function() + return vim.v.lnum >= 3 and 1 or 0 + end + vim.wo.foldmethod = 'expr' + vim.wo.foldexpr = fn + return { + identity = vim.wo.foldexpr == fn, + kind = type(vim.wo.foldexpr), + l2 = vim.fn.foldlevel(2), + l3 = vim.fn.foldlevel(3), + } + end) + ) + end) + + it("buffer-local expr option ('indentexpr') accepts Lua function", function() + -- Exercises the buffer-local Callback path (copy/free) and get_expr_indent via `==`. + eq( + { identity = true, indent = 8 }, + exec_lua(function() + local myfn = function() + return 8 + end + vim.bo.indentexpr = myfn + vim.api.nvim_buf_set_lines(0, 0, -1, false, { 'line1', 'line2' }) + vim.cmd('normal! 2G==') + return { + identity = vim.bo.indentexpr == myfn, + indent = vim.fn.indent(2), + } + end) + ) + end) + + it("Lua function 'formatexpr' on another buffer (vim.bo[bufnr])", function() + -- Sets via the non-curbuf API path (context switch), the way LSP configures 'formatexpr'. + eq( + { identity = true, cur_unchanged = true, invoked = true }, + exec_lua(function() + local buf = vim.api.nvim_create_buf(true, false) + local invoked = false + local fexpr = function() + invoked = true + return 0 + end + vim.bo[buf].formatexpr = fexpr + vim.api.nvim_buf_set_lines(buf, 0, -1, false, { 'some text to format' }) + local identity = vim.bo[buf].formatexpr == fexpr + vim.api.nvim_buf_call(buf, function() + vim.cmd('normal! gqq') + end) + return { + identity = identity, + cur_unchanged = vim.bo.formatexpr == '', + invoked = invoked, + } + end) + ) + end) + + it("omits a Lua-function value from :mksession/:mkvimrc (no ':set' form)", function() + -- A Lua function has no ":set" representation, so it must be skipped, not crash the writer. + local session = exec_lua(function() + vim.opt.sessionoptions:append('options') + vim.o.operatorfunc = 'StrOpFunc' -- string funcref: written + vim.o.quickfixtextfunc = function() end -- Lua function: omitted + local f = vim.fn.tempname() + vim.cmd('mksession! ' .. f) + vim.cmd('mkvimrc! ' .. vim.fn.tempname()) -- also exercises the makeset()/put_set() path + return table.concat(vim.fn.readfile(f), '\n') + end) + matches('operatorfunc=StrOpFunc', session) + eq(nil, session:find('quickfixtextfunc')) + end) +end) diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index 9738b6f650..4fff51bfba 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -42,6 +42,14 @@ local function get_buf_option(name, bufnr) end) end +--- True if buf-local option (func option like 'tagfunc') holds `vim.lsp[name]`. +local function buf_option_is_lsp(name, bufnr) + return exec_lua(function() + bufnr = bufnr or _G.BUFFER + return vim.api.nvim_get_option_value(name, { buf = bufnr }) == vim.lsp[name] + end) +end + local function make_edit(y_0, x_0, y_1, x_1, text) return { range = { @@ -459,8 +467,8 @@ describe('LSP', function() end, on_handler = function(_, _, ctx) if ctx.method == 'test' then - eq('v:lua.vim.lsp.tagfunc', get_buf_option('tagfunc')) - eq('v:lua.vim.lsp.omnifunc', get_buf_option('omnifunc')) + eq(true, buf_option_is_lsp('tagfunc')) + eq(true, buf_option_is_lsp('omnifunc')) eq('v:lua.vim.lsp.formatexpr()', get_buf_option('formatexpr')) eq('', get_buf_option('keywordprg')) eq( @@ -529,8 +537,8 @@ describe('LSP', function() end, on_handler = function(_, _, ctx) if ctx.method == 'test' then - eq('v:lua.vim.lsp.tagfunc', get_buf_option('tagfunc', BUFFER_1)) - eq('v:lua.vim.lsp.omnifunc', get_buf_option('omnifunc', BUFFER_2)) + eq(true, buf_option_is_lsp('tagfunc', BUFFER_1)) + eq(true, buf_option_is_lsp('omnifunc', BUFFER_2)) eq('v:lua.vim.lsp.formatexpr()', get_buf_option('formatexpr', BUFFER_2)) client:stop() end diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index 11431bfcc2..114b24bdc6 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -3315,12 +3315,14 @@ func Test_thesaurusfunc_callback() set thesaurusfunc= setlocal thesaurusfunc=NoSuchFunc setglobal thesaurusfunc=s:TsrFunc3 + " A callback-option (e.g. 'findfunc') stores funcref as its unambiguous form. + let tsrfunc3 = expand('') .. 'TsrFunc3' call assert_equal('NoSuchFunc', &thesaurusfunc) call assert_equal('NoSuchFunc', &l:thesaurusfunc) - call assert_equal('s:TsrFunc3', &g:thesaurusfunc) + call assert_equal(tsrfunc3, &g:thesaurusfunc) new | only - call assert_equal('s:TsrFunc3', &thesaurusfunc) - call assert_equal('s:TsrFunc3', &g:thesaurusfunc) + call assert_equal(tsrfunc3, &thesaurusfunc) + call assert_equal(tsrfunc3, &g:thesaurusfunc) call assert_equal('', &l:thesaurusfunc) call setline(1, 'script1') let g:TsrFunc3Args = [] @@ -3332,8 +3334,8 @@ func Test_thesaurusfunc_callback() set thesaurusfunc= setlocal thesaurusfunc=NoSuchFunc set thesaurusfunc=s:TsrFunc3 - call assert_equal('s:TsrFunc3', &thesaurusfunc) - call assert_equal('s:TsrFunc3', &g:thesaurusfunc) + call assert_equal(tsrfunc3, &thesaurusfunc) + call assert_equal(tsrfunc3, &g:thesaurusfunc) call assert_equal('', &l:thesaurusfunc) call setline(1, 'script1') let g:TsrFunc3Args = [] @@ -3341,8 +3343,8 @@ func Test_thesaurusfunc_callback() call assert_equal([[1, ''], [0, 'script1']], g:TsrFunc3Args) setlocal bufhidden=wipe new | only! - call assert_equal('s:TsrFunc3', &thesaurusfunc) - call assert_equal('s:TsrFunc3', &g:thesaurusfunc) + call assert_equal(tsrfunc3, &thesaurusfunc) + call assert_equal(tsrfunc3, &g:thesaurusfunc) call assert_equal('', &l:thesaurusfunc) call setline(1, 'script1') let g:TsrFunc3Args = [] diff --git a/test/old/testdir/test_quickfix.vim b/test/old/testdir/test_quickfix.vim index bb17ce56dc..67661fe2d2 100644 --- a/test/old/testdir/test_quickfix.vim +++ b/test/old/testdir/test_quickfix.vim @@ -5748,7 +5748,9 @@ func Xtest_qftextfunc(cchar) Xwindow call assert_equal(['green', 'blue'], getline(1, '$')) Xclose - call assert_equal("{d -> map(g:Xgetlist({'id' : d.id, 'items' : 1}).items[d.start_idx-1:d.end_idx-1], 'v:val.text')}", &quickfixtextfunc) + " A callback option stores the resolved funcref, so a lambda reads back as its handle + " (consistent with the per-list 'quickfixtextfunc' below). + call assert_match("function('\\d\\+')", &quickfixtextfunc) set quickfixtextfunc& " use a lambda function that returns an empty list