From 44355182aae64ec0932f136a023c023d5127c6a5 Mon Sep 17 00:00:00 2001 From: atticus-sullivan Date: Sat, 1 Oct 2022 19:12:04 +0200 Subject: [PATCH 1/4] feat! implement condition objects which implement various operators + tests Works by - providing a factory `make_condition` which wraps functions as condition objects - providing two modules `conditions.show` and `condition.expand` which contain a collection of common conditions already wrapped as condition objects Weird might be the decision to use `%` as `==`-operator. This way chosen because we can`t use `__eq` as this automatically converts the return value to a boolean (we need to return condition objects). So we decided to use something which make ones head scratch and look it up in the docs (or simply don't use it) Note: one still can continue to use the `expand_conditions` module (just rediects to the `conditions/expand` module) for backwards compatibility. This is deprecated though. --- lua/luasnip/extras/conditions/expand.lua | 14 ++ lua/luasnip/extras/conditions/init.lua | 49 ++++ lua/luasnip/extras/conditions/show.lua | 13 ++ lua/luasnip/extras/expand_conditions.lua | 14 +- tests/unit/conditions_spec.lua | 275 +++++++++++++++++++++++ tests/unit/extend_decorator_spec.lua | 5 + 6 files changed, 357 insertions(+), 13 deletions(-) create mode 100644 lua/luasnip/extras/conditions/expand.lua create mode 100644 lua/luasnip/extras/conditions/init.lua create mode 100644 lua/luasnip/extras/conditions/show.lua create mode 100644 tests/unit/conditions_spec.lua 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..752f0d0b3 --- /dev/null +++ b/lua/luasnip/extras/conditions/show.lua @@ -0,0 +1,13 @@ +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() + 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 From 3838cb36fcd3f1f8cf87a337fe584b78780ec90c Mon Sep 17 00:00:00 2001 From: atticus-sullivan Date: Wed, 5 Oct 2022 16:25:14 +0200 Subject: [PATCH 2/4] conditions.show.line_end: simply compare strings this is fast enough due to luas behaviour of interning strings (-> simple check if pointers are equal) --- lua/luasnip/extras/conditions/show.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lua/luasnip/extras/conditions/show.lua b/lua/luasnip/extras/conditions/show.lua index 752f0d0b3..c1691b8ca 100644 --- a/lua/luasnip/extras/conditions/show.lua +++ b/lua/luasnip/extras/conditions/show.lua @@ -6,7 +6,10 @@ local M = {} ----------------------- local function line_end(line_to_cursor) local line = vim.api.nvim_get_current_line() - return #line_to_cursor == #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) From 9867a30637a5997df6b8519f48d1e6229ea05e1f Mon Sep 17 00:00:00 2001 From: atticus-sullivan Date: Wed, 5 Oct 2022 16:39:45 +0200 Subject: [PATCH 3/4] Docs/Examples on condition objects --- DOC.md | 25 +++++++++++++++++++++++++ Examples/snippets.lua | 20 ++++++++++++++++---- doc/luasnip.txt | 27 ++++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 5 deletions(-) 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..752f2b9a4 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 predicats/functions used as + `show`-condition. Currently this is just `line_end` +- `conditions.expand`: Contains typical predicats/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 predicats. 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 From 4bc3e35fefa512700b62f86fed729c89c3a861ae Mon Sep 17 00:00:00 2001 From: atticus-sullivan Date: Sat, 8 Oct 2022 22:02:41 +0000 Subject: [PATCH 4/4] Auto generate docs --- doc/luasnip.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/luasnip.txt b/doc/luasnip.txt index 752f2b9a4..1b949987f 100644 --- a/doc/luasnip.txt +++ b/doc/luasnip.txt @@ -1050,9 +1050,9 @@ is only a short outline, their usage is shown more expansively in -- `conditions.show`: Contains typical predicats/functions used as +- `conditions.show`: Contains typical predicates/functions used as `show`-condition. Currently this is just `line_end` -- `conditions.expand`: Contains typical predicats/functions used as +- `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 @@ -1068,7 +1068,7 @@ is only a short outline, their usage is shown more expansively in 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 predicats. Like in + 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`.