diff --git a/DOC.md b/DOC.md index cdddc335a..91181e0a2 100644 --- a/DOC.md +++ b/DOC.md @@ -1064,6 +1064,31 @@ ls.add_snippets("all", { }) ``` +- `conditions.show`: Contains typical predicates/functions used as + `show`-condition. Currently this is just `line_end` +- `conditions.expand`: Contains typical predicates/functions used as + `expand`-condition. Currently this is just `line_begin` + Contains everything from `conditions.show` as well. +- `conditions`: Provides a function `make_condition(foo)` which takes a function + as argument and returns a *condition object* for which several operators are + defined: + - `c1 + c2 -> c1 or c2` + - `c1 * c2 -> c1 and c2` + - `-c1 -> not c1` + - `c1 ^ c2 -> c1 xor/!= c2` + - `c1 % c2 -> c1 xnor/== c2`: This decision may look weird but as we weren't + able to use `==`, we decided to take something that makes one scratch ones + head (and thus avoid making false assumptions). + For more details look at [this comment](https://github.com/L3MON4D3/LuaSnip/pull/612#issuecomment-1264487743). + + `conditions.show`s and `conditions.expand`s members all are also condition + objects so you can work with those too. + + Thus you can easily combine existing predicates. Like in + `conditions.expand.line_end + conditions.expand.line_begin` instead of doing + something like + `function(...) return conditions.expand.line_end(...) or conditions.expand.line_begin(...) end`. + extras1: ![extras1](https://user-images.githubusercontent.com/25300418/184359431-50f90599-3db0-4df0-a3a9-27013e663649.gif) diff --git a/Examples/snippets.lua b/Examples/snippets.lua index f9077b922..8a213b698 100644 --- a/Examples/snippets.lua +++ b/Examples/snippets.lua @@ -17,7 +17,8 @@ local dl = require("luasnip.extras").dynamic_lambda local fmt = require("luasnip.extras.fmt").fmt local fmta = require("luasnip.extras.fmt").fmta local types = require("luasnip.util.types") -local conds = require("luasnip.extras.expand_conditions") +local conds = require("luasnip.extras.conditions") +local conds_expand = require("luasnip.extras.conditions.expand") -- If you're reading this file for the first time, best skip to around line 190 -- where the actual snippet-definitions start. @@ -320,16 +321,27 @@ ls.add_snippets("all", { return line_to_cursor:match("%s*//") end, }), - -- there's some built-in conditions in "luasnip.extras.expand_conditions". + -- there's some built-in conditions in "luasnip.extras.conditions.expand" and "luasnip.extras.conditions.show". s("cond2", { t("will only expand at the beginning of the line"), }, { - condition = conds.line_begin, + condition = conds_expand.line_begin, }), s("cond3", { t("will only expand at the end of the line"), }, { - condition = conds.line_end, + condition = conds_expand.line_end, + }), + -- on conditions some logic operators are defined + s("cond4", { + t("will only expand at the end and the start of the line"), + }, { + -- last function is just an example how to make own function objects and apply operators on them + condition = conds_expand.line_end + + conds_expand.line_begin + * conds.make_condition(function() + return true + end), }), -- The last entry of args passed to the user-function is the surrounding snippet. s( diff --git a/doc/luasnip.txt b/doc/luasnip.txt index 8aee5d573..1b949987f 100644 --- a/doc/luasnip.txt +++ b/doc/luasnip.txt @@ -1,4 +1,4 @@ -*luasnip.txt* For NVIM v0.5.0 Last change: 2022 September 28 +*luasnip.txt* For NVIM v0.5.0 Last change: 2022 October 08 ============================================================================== Table of Contents *luasnip-table-of-contents* @@ -1049,6 +1049,31 @@ is only a short outline, their usage is shown more expansively in < + +- `conditions.show`: Contains typical predicates/functions used as + `show`-condition. Currently this is just `line_end` +- `conditions.expand`: Contains typical predicates/functions used as + `expand`-condition. Currently this is just `line_begin` Contains everything + from `conditions.show` as well. +- `conditions`: Provides a function `make_condition(foo)` which takes a function + as argument and returns a _condition object_ for which several operators are + defined: + - `c1 + c2 -> c1 or c2` + - `c1 * c2 -> c1 and c2` + - `-c1 -> not c1` + - `c1 ^ c2 -> c1 xor/!= c2` + - `c1 % c2 -> c1 xnor/== c2`: This decision may look weird but as we weren’t + able to use `==`, we decided to take something that makes one scratch ones + head (and thus avoid making false assumptions). + For more details look at this comment . + `conditions.show`s and `conditions.expand`s members all are also condition + objects so you can work with those too. + Thus you can easily combine existing predicates. Like in + `conditions.expand.line_end + conditions.expand.line_begin` instead of doing + something like `function(...) return conditions.expand.line_end(...) or + conditions.expand.line_begin(...) end`. + + FMT *luasnip-fmt* `require("luasnip.extras.fmt").fmt` can be used to create snippets in a more diff --git a/lua/luasnip/extras/conditions/expand.lua b/lua/luasnip/extras/conditions/expand.lua new file mode 100644 index 000000000..e97d85fa9 --- /dev/null +++ b/lua/luasnip/extras/conditions/expand.lua @@ -0,0 +1,14 @@ +local cond_obj = require("luasnip.extras.conditions") + +-- use the functions from show as basis and extend/overwrite functions specific for expand here +local M = vim.deepcopy(require("luasnip.extras.conditions.show")) +----------------------- +-- PRESET CONDITIONS -- +----------------------- +local function line_begin(line_to_cursor, matched_trigger) + -- +1 because `string.sub("abcd", 1, -2)` -> abc + return line_to_cursor:sub(1, -(#matched_trigger + 1)):match("^%s*$") +end +M.line_begin = cond_obj.make_condition(line_begin) + +return M diff --git a/lua/luasnip/extras/conditions/init.lua b/lua/luasnip/extras/conditions/init.lua new file mode 100644 index 000000000..3bc99a2a8 --- /dev/null +++ b/lua/luasnip/extras/conditions/init.lua @@ -0,0 +1,49 @@ +local M = {} + +----------------------- +-- CONDITION OBJECTS -- +----------------------- +local condition_mt = { + -- logic operators + -- not '-' + __unm = function(o1) + return M.make_condition(function(...) + return not o1(...) + end) + end, + -- or '+' + __add = function(o1, o2) + return M.make_condition(function(...) + return o1(...) or o2(...) + end) + end, + -- and '*' + __mul = function(o1, o2) + return M.make_condition(function(...) + return o1(...) and o2(...) + end) + end, + -- xor '^' + __pow = function(o1, o2) + return M.make_condition(function(...) + return o1(...) ~= o2(...) + end) + end, + -- xnor '%' + -- might be counter intuitive, but as we can't use '==' (must return bool) + -- it's best to use something weird (doesn't have to be used) + __mod = function(o1, o2) + return function(...) + return o1(...) == o2(...) + end + end, + -- use table like a function by overloading __call + __call = function(tab, line_to_cursor, matched_trigger, captures) + return tab.func(line_to_cursor, matched_trigger, captures) + end, +} +function M.make_condition(func) + return setmetatable({ func = func }, condition_mt) +end + +return M diff --git a/lua/luasnip/extras/conditions/show.lua b/lua/luasnip/extras/conditions/show.lua new file mode 100644 index 000000000..c1691b8ca --- /dev/null +++ b/lua/luasnip/extras/conditions/show.lua @@ -0,0 +1,16 @@ +local cond_obj = require("luasnip.extras.conditions") + +local M = {} +----------------------- +-- PRESET CONDITIONS -- +----------------------- +local function line_end(line_to_cursor) + local line = vim.api.nvim_get_current_line() + -- looks pretty inefficient, but as lue interns strings, this is just a + -- comparision of pointers (which probably is faster than calculate the + -- length and then checking) + return line_to_cursor == line +end +M.line_end = cond_obj.make_condition(line_end) + +return M diff --git a/lua/luasnip/extras/expand_conditions.lua b/lua/luasnip/extras/expand_conditions.lua index 52a6101dc..69c662fa8 100644 --- a/lua/luasnip/extras/expand_conditions.lua +++ b/lua/luasnip/extras/expand_conditions.lua @@ -1,13 +1 @@ -local M = {} - -function M.line_begin(line_to_cursor, matched_trigger) - -- +1 because `string.sub("abcd", 1, -2)` -> abc - return line_to_cursor:sub(1, -(#matched_trigger + 1)):match("^%s*$") -end - -function M.line_end(line_to_cursor) - local line = vim.api.nvim_get_current_line() - return #line_to_cursor == #line -end - -return M +return require("luasnip.extras.conditions.expand") diff --git a/tests/unit/conditions_spec.lua b/tests/unit/conditions_spec.lua new file mode 100644 index 000000000..0c9ba2a0d --- /dev/null +++ b/tests/unit/conditions_spec.lua @@ -0,0 +1,275 @@ +local helpers = require("test.functional.helpers")(after_each) +local ls_helpers = require("helpers") + +describe("expand_conditions", function() + before_each(function() + helpers.clear() + ls_helpers.session_setup_luasnip() + helpers.exec_lua("noop = function() end") + end) + + -- apparently clear() needs to run before anything else... + helpers.clear() + helpers.exec("set rtp+=" .. os.getenv("LUASNIP_SOURCE")) + it("simple", function() + local function foo() + return helpers.exec_lua([[ + local mkcond = require("luasnip.extras.conditions").make_condition + local c = mkcond(function() return true end) + return c() == true + ]]) + end + assert.has_no.errors(foo) + assert.is_true(foo()) + end) + describe("logic ops", function() + describe("and", function() + local function foo(b1, b2) + -- Attention: use this only when testing (otherwise (pot. + -- malicious) users might inject code) + return helpers.exec_lua( + ([[ + local mkcond = require("luasnip.extras.conditions").make_condition + local c = mkcond(function() return %s end) * mkcond(function() return %s end) + return c() == %s + ]]):format( + tostring(b1), + tostring(b2), + tostring(b1 and b2) + ) + ) + end + for _, ele in ipairs({ + { true, true }, + { true, false }, + { false, true }, + { false, false }, + }) do + it( + ("%s and %s"):format(tostring(ele[1]), tostring(ele[2])), + function() + local test = function() + return foo(ele[1], ele[2]) + end + assert.has_no.errors(test) + assert.is_true(test()) + end + ) + end + end) + describe("or", function() + local function foo(b1, b2) + -- Attention: use this only when testing (otherwise (pot. + -- malicious) users might inject code) + return helpers.exec_lua(([[ + local mkcond = require("luasnip.extras.conditions").make_condition + local c = mkcond(function() return %s end) + mkcond(function() return %s end) + return c() == %s + ]]):format(tostring(b1), tostring(b2), tostring(b1 or b2))) + end + for _, ele in ipairs({ + { true, true }, + { true, false }, + { false, true }, + { false, false }, + }) do + it( + ("%s or %s"):format(tostring(ele[1]), tostring(ele[2])), + function() + local test = function() + return foo(ele[1], ele[2]) + end + assert.has_no.errors(test) + assert.is_true(test()) + end + ) + end + end) + describe("xor", function() + local function foo(b1, b2) + -- Attention: use this only when testing (otherwise (pot. + -- malicious) users might inject code) + return helpers.exec_lua( + ([[ + local mkcond = require("luasnip.extras.conditions").make_condition + local c = mkcond(function() return %s end) ^ mkcond(function() return %s end) + return c() == %s + ]]):format( + tostring(b1), + tostring(b2), + tostring((b1 and not b2) or (not b1 and b2)) + ) + ) + end + for _, ele in ipairs({ + { true, true }, + { true, false }, + { false, true }, + { false, false }, + }) do + it( + ("%s xor %s"):format(tostring(ele[1]), tostring(ele[2])), + function() + local test = function() + return foo(ele[1], ele[2]) + end + assert.has_no.errors(test) + assert.is_true(test()) + end + ) + end + end) + describe("xnor", function() + local function foo(b1, b2) + -- Attention: use this only when testing (otherwise (pot. + -- malicious) users might inject code) + return helpers.exec_lua(([[ + local mkcond = require("luasnip.extras.conditions").make_condition + local c = mkcond(function() return %s end) %% mkcond(function() return %s end) + return c() == %s + ]]):format(tostring(b1), tostring(b2), tostring(b1 == b2))) + end + for _, ele in ipairs({ + { true, true }, + { true, false }, + { false, true }, + { false, false }, + }) do + it( + ("%s xnor %s"):format(tostring(ele[1]), tostring(ele[2])), + function() + local test = function() + return foo(ele[1], ele[2]) + end + assert.has_no.errors(test) + assert.is_true(test()) + end + ) + end + end) + describe("not", function() + local function foo(b1) + -- Attention: use this only when testing (otherwise (pot. + -- malicious) users might inject code) + return helpers.exec_lua(([[ + local mkcond = require("luasnip.extras.conditions").make_condition + local c = -mkcond(function() return %s end) + return c() == %s + ]]):format(tostring(b1), tostring(not b1))) + end + for _, ele in ipairs({ { true }, { false } }) do + it(("not %s"):format(tostring(ele[1])), function() + local test = function() + return foo(ele[1]) + end + assert.has_no.errors(test) + assert.is_true(test()) + end) + end + end) + describe("composite", function() + local function foo(b1, b2, b3) + -- Attention: use this only when testing (otherwise (pot. + -- malicious) users might inject code) + return helpers.exec_lua( + ([[ + local mkcond = require("luasnip.extras.conditions").make_condition + local c = - ( mkcond(function() return %s end) + mkcond(function() return %s end) * mkcond(function() return %s end)) ^ ( mkcond(function() return %s end) + mkcond(function() return %s end) * mkcond(function() return %s end)) + return c() == %s + ]]):format( + tostring(b1), + tostring(b2), + tostring(b3), + tostring(b3), + tostring(b1), + tostring(b2), + tostring(not (b1 or b2 and b3) ~= (b3 or b1 and b2)) + ) + ) + end + for _, ele in ipairs({ + { true, true, true }, + { true, true, false }, + { true, false, true }, + { true, false, false }, + { false, true, true }, + { false, true, false }, + { false, false, true }, + { false, false, false }, + }) do + it( + ("composite %s %s %s"):format( + tostring(ele[1]), + tostring(ele[2]), + tostring(ele[3]) + ), + function() + local test = function() + return foo(ele[1], ele[2], ele[3]) + end + assert.has_no.errors(test) + assert.is_true(test()) + end + ) + end + end) + end) + describe("line_begin", function() + it("is at begin", function() + local function foo() + return helpers.exec_lua([[ + local conds = require("luasnip.extras.expand_conditions") + local c = conds.line_begin + return not c("hello world", "hello world") ~= true -- allow nil/object + ]]) + end + assert.has_no.errors(foo) + assert.is_true(foo()) + end) + it("is NOT at begin", function() + local function foo() + return helpers.exec_lua([[ + local conds = require("luasnip.extras.expand_conditions") + local c = conds.line_begin + return not c("hello world", "ld") ~= false -- allow nil/object + ]]) + end + assert.has_no.errors(foo) + assert.is_true(foo()) + end) + end) + describe("line_end", function() + it("is at begin", function() + local function foo() + return helpers.exec_lua([[ + local vim_bak = vim + -- vim.api.nvim_get_current_line + vim = {api = {nvim_get_current_line = function() return "hello world ending" end}} + local conds = require("luasnip.extras.expand_conditions") + local c = conds.line_end + local ret = not c("hello world ending") ~= true -- allow nil/object + vim = vim_bak + return ret + ]]) + end + assert.has_no.errors(foo) + assert.is_true(foo()) + end) + it("is NOT at begin", function() + local function foo() + return helpers.exec_lua([[ + local vim_bak = vim + -- vim.api.nvim_get_current_line + vim = {api = {nvim_get_current_line = function() return "hello world ending" end}} + local conds = require("luasnip.extras.expand_conditions") + local c = conds.line_end + local ret = not c("hello world") ~= false -- allow nil/object + vim = vim_bak + return ret + ]]) + end + assert.has_no.errors(foo) + assert.is_true(foo()) + end) + end) +end) diff --git a/tests/unit/extend_decorator_spec.lua b/tests/unit/extend_decorator_spec.lua index 3840cab5c..6326fff6e 100644 --- a/tests/unit/extend_decorator_spec.lua +++ b/tests/unit/extend_decorator_spec.lua @@ -4,6 +4,11 @@ local exec = helpers.exec local ls_helpers = require("helpers") describe("luasnip.util.extend_decorator", function() + before_each(function() + helpers.clear() + ls_helpers.session_setup_luasnip() + helpers.exec_lua("noop = function() end") + end) local shared_setup1 = [[ local function passthrough(arg1, arg2) return arg1, arg2