diff --git a/doc/MACROS.md b/doc/MACROS.md index 36cc4099..4d9ddb11 100644 --- a/doc/MACROS.md +++ b/doc/MACROS.md @@ -120,27 +120,26 @@ Create or get an augroup, or override an existing augroup. - ``: It indicates that `callback` must be callback function by itself. - `cb`: An alias of `` key. -- `callback`: (string|function) Set either callback function or vim Ex command. - Symbol, and anonymous function constructed by `fn`, `hashfn`, `lambda`, and - `partial`, is regarded as Lua function; otherwise, as Ex command. - - Note: Insert `` key in `extra-opts` to set string via symbol. - - Note: Set `vim.fn.foobar`, or set `:foobar` to `callback` with `` key - in `extra-opts`, to call Vim script function `foobar` without table arg from - `nvim_create_autocmd()`; instead, set `#(vim.fn.foobar $)` to call `foobar` - with the table arg. +- `callback`: (string|function) Set either callback function or Ex command. To + tell `callback` is Lua function, either prepend a quote `` ` `` as an + identifer (the quoted symbol, or list, is supposed to result in Lua function + at runtime), or set it in anonymous function constructed by `fn`, `hashfn`, + `lambda`, and `partial`; otherwise, Ex command. + + Note: Set `` `vim.fn.foobar `` to call Vim script function `foobar` without + table argument from `nvim_create_autocmd()`; on the other hand, set + `#(vim.fn.foobar $)` to call `foobar` with the table argument. - [`?api-opts`](#api-opts): (kv-table) `:h nvim_create_autocmd()`. ```fennel (augroup! :sample-augroup [:TextYankPost #(vim.highlight.on_yank {:timeout 450 :on_visual false})] (autocmd! [:InsertEnter :InsertLeave] - [: :desc "call foo#bar() without any args"] vim.fn.foo#bar) + [: :desc "call foo#bar() without any args"] `vim.fn.foo#bar) (autocmd! :VimEnter [:once :nested :desc "call baz#qux() with "] #(vim.fn.baz#qux $.match))) (autocmd! :LspAttach - #(au! $.group :CursorHold [:buffer $.buf] vim.lsp.buf.document_highlight)) + #(au! $.group :CursorHold [:buffer $.buf] `vim.lsp.buf.document_highlight)) ``` is equivalent to @@ -263,13 +262,56 @@ Map `lhs` to `rhs` in `modes`, non-recursively by default. - ``: It indicates that `rhs` must be callback function by itself. - `cb`: An alias of `` key. - `lhs`: (string) Left-hand-side of the mapping. -- `rhs`: (string|function) Right-hand-side of the mapping. Symbol, and anonymous - function constructed by `fn`, `hashfn`, `lambda`, and `partial`, is regarded - as Lua function; otherwise, as Normal mode command execution. - - Note: Insert `` key in `extra-opts` to set string via symbol. +- `rhs`: (string|function) Right-hand-side of the mapping. Set either callback + function or Ex command. To tell `callback` is Lua function, either prepend a + quote `` ` `` as an identifer (the quoted symbol, or list, is supposed to + result in Lua function at runtime), or set it in anonymous function + constructed by `fn`, `hashfn`, `lambda`, and `partial`; otherwise, Ex command. + + Note: To call Vim script function `foobar` without table arg from + `nvim_create_autocmd()`, just set `vim.fn.foobar`, or `` `vim.fn.foobar `` if + you prefer, there; on the other hand, set `#(vim.fn.foobar $)` to call + `foobar` with the table arg. - [`?api-opts`](#api-opts): (kv-table) `:h nvim_set_keymap()`. +```fennel +(map! :i :jk :) +(map! :n :lhs [:desc "call foo#bar()"] `vim.fn.foo#bar) +(map! [:n :x] [:remap :expr :literal] :d "&readonly ? '(readonly-d)' : '(noreadonly-d)'") +(map! [:n :x] [:remap :expr] :u #(if vim.bo.readonly + "(readonly-u)" + "(noreadonly-u)")) +``` + +is equivalent to + +```vim +inoremap jk +nnoremap lhs call foo#bar() +nmap d &readonly ? "\(readonly-d)" : "\(noreadonly-d)" +xmap u &readonly ? "\(readonly-u)" : "\(noreadonly-u)" +``` + +```lua +vim.keymap.set("i", "jk", "") +vim.keymap.set("n", "lhs", function() + vim.fn["foo#bar"]() +end) +-- or, if you don't care about lazy loading, +vim.keymap.set("n", "lhs", vim.fn["foo#bar"]) +vim.keymap.set({ "n", "x" }, "d", "&readonly ? '(readonly-d)' : '(noreadonly-d)'", { + remap = true, + expr = true, + replace_keycodes = false, +}) +vim.keymap.set({ "n", "x" }, "u", function() + return vim.bo.readonly and "(readonly-u)" or "(noreadonly-u)" +end, { + remap = true, + expr = true, +}) +``` + #### `unmap!` Delete keymap. @@ -846,6 +888,14 @@ in another anonymous function is meaningless in many cases. ## Deprecated +### v0.5.1 + +- Symbol will no longer be an identifer as callback function for the macros, + [`map!`](#map!), [`autocmd!`](#autocmd), and so on; set `` `foobar `` to set a + symbol `foobar` as callback function instead. + +### v0.5.0 + - `nmap!`: Use [`map!`](#map) with `remap` option for corresponding mode instead. - `vmap!`: Use [`map!`](#map) with `remap` option for corresponding mode diff --git a/fnl/nvim-laurel/macros.fnl b/fnl/nvim-laurel/macros.fnl index 1752d418..f823ec5e 100644 --- a/fnl/nvim-laurel/macros.fnl +++ b/fnl/nvim-laurel/macros.fnl @@ -76,6 +76,12 @@ @return undefined" (. xs 1)) +(lambda second [xs] + "Return the second value in `xs` + @param xs sequence + @return undefined" + (. xs 2)) + (lambda slice [xs ?start ?end] "Return sequence from `?start` to `?end`. @param xs sequence @@ -89,6 +95,13 @@ ;; Additional predicates ///2 +(fn quoted? [x] + "Check if `x` is a list which begins with `quote`. + @param x any + @return boolean" + (and (list? x) ; + (= `quote (first x)))) + (fn anonymous-function? [x] "(Compile time) Check if type of `x` is anonymous function. @param x any @@ -156,14 +169,21 @@ (collect [k v (pairs ?api-opts) &into ?extra-opts] (values k v)))) +(fn ->unquoted [x] + "If quoted, return unquoted `x`; otherwise, just return `x` itself. + @param x any but nil + @return any" + (if (quoted? x) + (second x) + x)) + (lambda extract-?vim-fn-name [x] "Extract \"foobar\" from multi-symbol `vim.fn.foobar`, or return `nil`. @param x any @return string|nil" - (when (multi-sym? x) - (let [(fn-name pos) (-> (->str x) (: :gsub "^vim%.fn%." ""))] - (when (< 0 pos) - fn-name)))) + (let [name (->str x) + pat-vim-fn "^vim%.fn%.(%S+)$"] + (name:match pat-vim-fn))) (lambda deprecate [deprecated alternative version compatible] "Return a wrapper function, which returns `compatible`, about to notify @@ -274,15 +294,23 @@ (set extra-opts.command callback) (or extra-opts. extra-opts.cb ; (sym? callback) ; - (anonymous-function? callback)) + (anonymous-function? callback) ; + (quoted? callback)) ;; Note: Ignore the possibility to set Vimscript function to callback ;; in string; however, convert `vim.fn.foobar` into "foobar" to set ;; to "callback" key because functions written in Vim script are ;; rarely supposed to expect the table from `nvim_create_autocmd` for ;; its first arg. - (set extra-opts.callback - (or (extract-?vim-fn-name callback) ; - callback)) + (let [cb (->unquoted callback)] + (set extra-opts.callback + ;; Note: Either vim.fn.foobar or `vim.fn.foobar should be + ;; "foobar" set to "callback" key. + (or (extract-?vim-fn-name cb) ; + (if (sym? callback) + (deprecate "callback function in symbol for `augroup!`, `autocmd!`, ..." + "quote \"`\" like `foobar" :v0.6.0 + callback) + cb)))) (set extra-opts.command callback)) (let [api-opts (merge-api-opts (autocmd/->compatible-opts! extra-opts) ?api-opts)] @@ -406,7 +434,8 @@ (if (or extra-opts. extra-opts.ex) raw-rhs (or extra-opts. extra-opts.cb ; (sym? raw-rhs) ; - (anonymous-function? raw-rhs)) ; + (anonymous-function? raw-rhs) ; + (quoted? raw-rhs)) (do ;; Hack: `->compatible-opts` must remove ;; `cb`/`` key instead, but it doesn't at @@ -414,7 +443,12 @@ ;; but no idea how to reproduce it in minimal codes. (set extra-opts.cb nil) (set extra-opts. nil) - (set extra-opts.callback raw-rhs) + (set extra-opts.callback + (if (sym? raw-rhs) + (deprecate "callback function in symbol for `map!`" + "quote \"`\" like `foobar" :v0.6.0 + raw-rhs) + (->unquoted raw-rhs))) "") ; ;; Otherwise, Normal mode commands. raw-rhs)) @@ -866,11 +900,12 @@ : :register :keepscript])) - [extra-opts name command ?api-opts] (if-not ?extra-opts - [{} a1 a2 ?a3] - (sequence? a1) - [?extra-opts a2 ?a3 ?a4] - [?extra-opts a1 ?a3 ?a4]) + [extra-opts name raw-command ?api-opts] (if-not ?extra-opts + [{} a1 a2 ?a3] + (sequence? a1) + [?extra-opts a2 ?a3 ?a4] + [?extra-opts a1 ?a3 ?a4]) + command (->unquoted raw-command) ?bufnr (if extra-opts. 0 extra-opts.buffer) api-opts (merge-api-opts (command/->compatible-opts! extra-opts) ?api-opts)] diff --git a/tests/spec/autocmd_spec.fnl b/tests/spec/autocmd_spec.fnl index 303d327a..de4c4295 100644 --- a/tests/spec/autocmd_spec.fnl +++ b/tests/spec/autocmd_spec.fnl @@ -1,9 +1,16 @@ (import-macros {: augroup! : augroup+ : au! : autocmd!} :nvim-laurel.macros) +(macro macro-callback [] + `#:macro-callback) + +(macro macro-command [] + :macro-command) + (local default-augroup :default-test-augroup) (local default-event :BufRead) (local default-callback #:default-callback) (local default-command :default-command) +(local default {:multi {:sym #:default.multi.sym}}) (lambda get-autocmds [?opts] (let [opts (collect [k v (pairs (or ?opts {})) ; @@ -16,6 +23,11 @@ (describe :autocmd (fn [] + (setup (fn [] + (vim.cmd "function g:Test() abort + endfunction"))) + (teardown (fn [] + (vim.cmd "delfunction g:Test"))) (before_each (fn [] (augroup! default-augroup) (let [aus (get-autocmds)] @@ -36,7 +48,57 @@ #(let [id (augroup! default-augroup)] (assert.is.same id (augroup+ default-augroup)))))) (describe :au!/autocmd! + (it "sets callback via macro with quote" + (fn [] + (autocmd! default-augroup default-event [:pat] `(macro-callback)) + (let [au (get-first-autocmd {:pattern :pat})] + (assert.is_not_nil au.callback)))) + (it "set command in macro with no args" + (fn [] + (autocmd! default-augroup default-event [:pat] (macro-command)) + (let [au (get-first-autocmd {:pattern :pat})] + (assert.is_same :macro-command au.command)))) + (it "set command in macro with some args" + (fn [] + (autocmd! default-augroup default-event [:pat] + (macro-command :foo :bar)) + (let [au (get-first-autocmd {:pattern :pat})] + (assert.is_same :macro-command au.command)))) (fn [] + (it "sets callback function with quoted symbol" + #(do + (autocmd! default-augroup default-event [:pat] `default-callback) + (assert.is_same default-callback + (. (get-first-autocmd {:pattern :pat}) :callback)))) + (it "sets callback function with quoted multi-symbol" + #(let [desc :multi.sym] + (autocmd! default-augroup default-event [:pat] `default.multi.sym + {: desc}) + ;; FIXME: In vusted, callback is unexpectedly set to a string + ;; ""; it must be the same as + ;; `default.multi.sym`. + (assert.is_same desc (. (get-first-autocmd {:pattern :pat}) :desc)))) + (it "sets callback function with quoted list" + #(let [desc :list] + (autocmd! default-augroup default-event [:pat] + `(default-callback :foo :bar) {: desc}) + (let [au (get-first-autocmd {:pattern :pat})] + (assert.is_same desc au.desc)))) + (it "set `vim.fn.Test in string \"Test\"" + (fn [] + (autocmd! default-augroup default-event [:pat] `vim.fn.Test) + (let [au (get-first-autocmd {:pattern :pat})] + (assert.is_same "" au.callback)))) + (it "set `(vim.fn.Test) to callback as #(vim.fn.Test)" + (fn [] + (autocmd! default-augroup default-event [:pat] `(vim.fn.Test)) + (let [au (get-first-autocmd {:pattern :pat})] + (assert.is_not_same "" au.callback)))) + (it "set #(vim.fn.Test) to callback without modification" + (fn [] + (autocmd! default-augroup default-event [:pat] #(vim.fn.Test)) + (let [au (get-first-autocmd {:pattern :pat})] + (assert.is_not_same "" au.callback)))) (describe "detects 2 args:" (fn [] (it "sequence pattern and string callback" @@ -138,10 +200,6 @@ (assert.is.same "" autocmd2.callback)))) (it "sets vim.fn.Test to callback in string" (fn [] - (vim.cmd " - function! g:Test() abort - endfunction - ") (assert.has_no.errors #(autocmd! default-augroup default-event vim.fn.Test)) (let [[autocmd] (get-autocmds)] diff --git a/tests/spec/command_spec.fnl b/tests/spec/command_spec.fnl index c06f46b1..83c2e7ae 100644 --- a/tests/spec/command_spec.fnl +++ b/tests/spec/command_spec.fnl @@ -1,60 +1,111 @@ (import-macros {: command!} :nvim-laurel.macros) +(macro macro-callback [] + `#:macro-callback) + +(macro macro-command [] + :macro-command) + +(local default-callback #:default-callback) +(local default {:multi {:sym #:default.multi.sym}}) + (lambda get-command [name] (-> (vim.api.nvim_get_commands {:builtin false}) (. name))) +(lambda get-command-definition [name] + "Return command, or value for desc if callback is Lua function. + Read `Parameters.opts.desc` of `:h nvim_create_user_command()`" + (. (get-command name) :definition)) + (lambda get-buf-command [bufnr name] (-> (vim.api.nvim_buf_get_commands bufnr {:builtin false}) (. name))) -(describe :command! ; +(describe :command! + (fn [] + (before_each (fn [] + (pcall vim.api.nvim_del_user_command :Foo) + (pcall vim.api.nvim_buf_del_user_command 0 :Foo) + (assert.is_nil (get-command :Foo)))) + (it "defines user command" + (fn [] + (assert.is_nil (get-command :Foo)) + (command! :Foo :Bar) + (assert.is_not_nil (get-command :Foo)))) + (it "defines local user command for current buffer with `` attr" + (fn [] + (assert.is_nil (get-buf-command 0 :Foo)) + (command! [:] :Foo :Bar) + (assert.is_not_nil (get-buf-command 0 :Foo)))) + (it "defines local user command with buffer number" + (fn [] + (let [bufnr (vim.api.nvim_get_current_buf)] + (assert.is_nil (get-buf-command bufnr :Foo)) + (vim.cmd.new) + (vim.cmd.only) + (command! :Foo [:buffer bufnr] :Bar) + (assert.is_not_nil (get-buf-command bufnr :Foo)) + (assert.has_no_error #(vim.api.nvim_buf_del_user_command bufnr :Foo))))) + (it "can set callback function with quoted symbol" + (fn [] + (command! :Foo `default-callback) + ;; Note: command.definition should be empty string if callback is + ;; function without `desc` key. + (assert.is_same "" (get-command-definition :Foo)))) + (it "can set callback function with quoted multi-symbol" + (fn [] + (let [desc :multi.sym] + (command! :Foo `default.multi.sym {: desc}) + (assert.is_same desc (get-command-definition :Foo))))) + (it "can set quoted list result to callback" + (fn [] + (let [desc :list] + (command! :Foo `(default-callback :foo :bar) {: desc}) + (assert.is_same (default-callback) (get-command-definition :Foo))))) + (it "which sets callback `vim.fn.Test will not be overridden by `desc` key" + ;; Note: The reason is probably vim.fn.Test is not a Lua function but + ;; a Vim one. + (fn [] + (let [desc :Test] + (command! :Foo `vim.fn.Test) + (assert.is_same "" (get-command-definition :Foo)) + (assert.is_not_same desc (get-command-definition :Foo))))) + (it "sets callback via macro with quote" + (fn [] + (command! :Foo `(macro-callback)) + ;; TODO: Check if callback is set. + (assert.is_not_nil (get-command :Foo)))) + (it "set command in macro with no args" + (fn [] + (command! :Foo (macro-command)) + (assert.is_same :macro-command (get-command-definition :Foo)))) + (it "set command in macro with some args" + (fn [] + (command! :Foo (macro-command :foo :bar)) + (assert.is_same :macro-command (get-command-definition :Foo)))) + (describe :extra-opts + (fn [] + (it "can be either first arg or second arg" + (fn [] + (assert.has_no_error #(command! [:bang] :Foo :Bar)) + (assert.has_no_error #(command! :Foo [:bang] :Bar)))))) + (describe :api-opts + (fn [] + (it "gives priority api-opts over extra-opts" (fn [] - (before_each (fn [] - (pcall vim.api.nvim_del_user_command :Foo) - (pcall vim.api.nvim_buf_del_user_command 0 :Foo))) - (it "defines user command" - (fn [] - (assert.is_nil (get-command :Foo)) - (command! :Foo :Bar) - (assert.is_not_nil (get-command :Foo)))) - (it "defines local user command for current buffer with `` attr" - (fn [] - (assert.is_nil (get-buf-command 0 :Foo)) - (command! [:] :Foo :Bar) - (assert.is_not_nil (get-buf-command 0 :Foo)))) - (it "defines local user command with buffer number" - (fn [] - (let [bufnr (vim.api.nvim_get_current_buf)] - (assert.is_nil (get-buf-command bufnr :Foo)) - (vim.cmd.new) - (vim.cmd.only) - (command! :Foo [:buffer bufnr] :Bar) - (assert.is_not_nil (get-buf-command bufnr :Foo)) - (assert.has_no_error #(vim.api.nvim_buf_del_user_command bufnr - :Foo))))) - (describe :extra-opts - (fn [] - (it "can be either first arg or second arg" - (fn [] - (assert.has_no_error #(command! [:bang] :Foo :Bar)) - (assert.has_no_error #(command! :Foo [:bang] :Bar)))))) - (describe :api-opts - (fn [] - (it "gives priority api-opts over extra-opts" - (fn [] - (command! :Foo [:bar :bang] :FooBar) - (assert.is_true (-> (get-command :Foo) (. :bang))) - (assert.is_true (-> (get-command :Foo) (. :bar))) - (command! :Bar [:bar :bang] :FooBar {:bar false}) - (assert.is_false (-> (get-command :Bar) (. :bar))) - (let [tbl-opts {:bar false} - fn-opts #{:bang false}] - (command! :Baz [:bar :bang] :FooBar tbl-opts) - (command! :Qux [:bar :bang] :FooBar (fn-opts)) - (let [cmd-baz (get-command :Baz) - cmd-qux (get-command :Qux)] - (assert.is_false cmd-baz.bar) - (assert.is_true cmd-baz.bang) - (assert.is_true cmd-qux.bar) - (assert.is_false cmd-qux.bang))))))))) + (command! :Foo [:bar :bang] :FooBar) + (assert.is_true (-> (get-command :Foo) (. :bang))) + (assert.is_true (-> (get-command :Foo) (. :bar))) + (command! :Bar [:bar :bang] :FooBar {:bar false}) + (assert.is_false (-> (get-command :Bar) (. :bar))) + (let [tbl-opts {:bar false} + fn-opts #{:bang false}] + (command! :Baz [:bar :bang] :FooBar tbl-opts) + (command! :Qux [:bar :bang] :FooBar (fn-opts)) + (let [cmd-baz (get-command :Baz) + cmd-qux (get-command :Qux)] + (assert.is_false cmd-baz.bar) + (assert.is_true cmd-baz.bang) + (assert.is_true cmd-qux.bar) + (assert.is_false cmd-qux.bang))))))))) diff --git a/tests/spec/keymap_spec.fnl b/tests/spec/keymap_spec.fnl index 27d629f2..79a69e53 100644 --- a/tests/spec/keymap_spec.fnl +++ b/tests/spec/keymap_spec.fnl @@ -8,8 +8,15 @@ : : } :nvim-laurel.macros) +(macro macro-callback [] + `#:macro-callback) + +(macro macro-command [] + :macro-command) + (local default-rhs :default-rhs) (local default-callback #:default-callback) +(local default {:multi {:sym #:default.multi.sym}}) (local new-callback #(fn [] $)) @@ -43,6 +50,11 @@ (insulate :macros.keymap (fn [] + (setup (fn [] + (vim.cmd "function g:Test() abort + endfunction"))) + (teardown (fn [] + (vim.cmd "delfunction g:Test"))) (before_each (fn [] (let [all-modes ["" "!" :l :t]] (each [_ mode (ipairs all-modes)] @@ -50,6 +62,22 @@ (assert.is_nil (get-rhs mode :lhs)))))) (describe :map! (fn [] + (it "sets callback function with quoted symbol" + #(do + (map! :n :lhs `default-callback) + (assert.is_same default-callback (get-callback :n :lhs)))) + (it "sets callback function with quoted multi-symbol" + #(let [desc :multi.sym] + (map! :n :lhs `default.multi.sym {: desc}) + (assert.is_same default.multi.sym (get-callback :n :lhs)))) + (it "sets callback function with quoted list" + #(let [desc :list] + (map! :n :lhs `(default-callback :foo :bar) {: desc}) + (assert.is_same desc (. (get-mapargs :n :lhs) :desc)))) + (it "set callback function in string for `vim.fn.Test" + (fn [] + (map! :n :lhs `vim.fn.Test) + (assert.is_same vim.fn.Test (get-callback :n :lhs)))) (it "maps non-recursively by default" #(let [mode :n modes [:n :o :t]] @@ -82,6 +110,18 @@ (each [_ m (ipairs modes)] (let [{: noremap} (get-mapargs m :lhs)] (assert.is.same 1 noremap))))) + (it "sets callback via macro with quote" + (fn [] + (map! :n :lhs `(macro-callback)) + (assert.is_not_nil (get-callback :n :lhs)))) + (it "set command in macro with no args" + (fn [] + (map! :n :lhs (macro-command)) + (assert.is_same :macro-command (get-rhs :n :lhs)))) + (it "set command in macro with some args" + (fn [] + (map! :n :lhs (macro-command :foo :bar)) + (assert.is_same :macro-command (get-rhs :n :lhs)))) (it "maps multiple mode mappings with a sequence at once" #(let [modes [:n :c :t]] (noremap! modes :lhs :rhs)