From 1247556247cd271a747301b0f23a518efbcb93fd Mon Sep 17 00:00:00 2001 From: Vladan Popovic Date: Tue, 1 Oct 2024 20:07:57 +0200 Subject: [PATCH] nvim: refactor telescope and add luasnip config --- dotfiles/.config/nvim/init.lua | 5 +- dotfiles/.config/nvim/lua/plugins/init.lua | 2 +- .../nvim/lua/plugins/luasnip_config.lua | 12 + .../.config/nvim/lua/plugins/telescope.lua | 54 +- dotfiles/.config/nvim/lua/snippets.lua | 578 ++++++++++++++++++ 5 files changed, 631 insertions(+), 20 deletions(-) create mode 100644 dotfiles/.config/nvim/lua/plugins/luasnip_config.lua create mode 100644 dotfiles/.config/nvim/lua/snippets.lua diff --git a/dotfiles/.config/nvim/init.lua b/dotfiles/.config/nvim/init.lua index 58112d6..a924923 100644 --- a/dotfiles/.config/nvim/init.lua +++ b/dotfiles/.config/nvim/init.lua @@ -1,7 +1,8 @@ -- remap leader to vim.g.mapleader = ' ' -require('settings') +require('plugins') require('lsp') require('keymappings') -require('plugins') +require('settings') +require('snippets') diff --git a/dotfiles/.config/nvim/lua/plugins/init.lua b/dotfiles/.config/nvim/lua/plugins/init.lua index 6095fa2..719c3d8 100644 --- a/dotfiles/.config/nvim/lua/plugins/init.lua +++ b/dotfiles/.config/nvim/lua/plugins/init.lua @@ -81,12 +81,12 @@ require('packer').startup(function() use { 'hedyhli/outline.nvim' } use { "catppuccin/nvim", as = "catppuccin" } - end) require('plugins.catppuccin') require('plugins.cmp') require('plugins.gitsigns') +require('plugins.luasnip_config') require('plugins.neogit') require('plugins.oil') require('plugins.outline') diff --git a/dotfiles/.config/nvim/lua/plugins/luasnip_config.lua b/dotfiles/.config/nvim/lua/plugins/luasnip_config.lua new file mode 100644 index 0000000..af161b1 --- /dev/null +++ b/dotfiles/.config/nvim/lua/plugins/luasnip_config.lua @@ -0,0 +1,12 @@ +local luasnip = require('luasnip') +local lst = require('telescope').extensions.luasnip + +vim.keymap.set({ "i" }, "", function() luasnip.expand() end, { silent = true }) +vim.keymap.set({ "i", "s" }, "", function() luasnip.jump(1) end, { silent = true }) +vim.keymap.set({ "i", "s" }, "", function() luasnip.jump(-1) end, { silent = true }) + +vim.keymap.set({ "i", "s" }, "", function() + if luasnip.choice_active() then + luasnip.change_choice(1) + end +end, { silent = true }) diff --git a/dotfiles/.config/nvim/lua/plugins/telescope.lua b/dotfiles/.config/nvim/lua/plugins/telescope.lua index 707ba1b..3accc4a 100644 --- a/dotfiles/.config/nvim/lua/plugins/telescope.lua +++ b/dotfiles/.config/nvim/lua/plugins/telescope.lua @@ -4,25 +4,47 @@ local builtin = require('telescope.builtin') local telescope_last = 0 function telescope_resume() - if telescope_last == 0 then - telescope_last = 1 - builtin.live_grep() - else - builtin.resume() - end + if telescope_last == 0 then + telescope_last = 1 + builtin.live_grep() + else + builtin.resume() + end end +local files_config = { + cache_picker = false, + layout_strategy = "vertical", + layout_config = { + vertical = { + prompt_position = "top", + mirror = true, + width = 0.5, + }, + }, +} + telescope.setup { defaults = { sorting_strategy = "ascending", - live_grep_arguments = { 'rg', - '--hidden', '--no-ignore', '--color=never', - '--with-filename', '--line-number', - '--column', '--smart-case', '--no-heading', + vimgrep_arguments = { + 'rg', + '--hidden', + '--no-ignore', + '--color=never', + '--with-filename', + '--line-number', + '--column', + '--smart-case', + '--no-heading', + '-u', }, preview = { treesitter = false }, + file_ignore_patterns = { + '.git', + }, }, pickers = { live_grep = { @@ -31,12 +53,10 @@ telescope.setup { n = { [""] = telescope_resume }, }, }, - find_files = { - cache_picker = false, - }, - git_files = { - cache_picker = false, - }, + find_files = vim.tbl_extend("force", files_config, { + hidden = true, + }), + git_files = files_config, }, } @@ -44,7 +64,7 @@ telescope.setup { vim.keymap.set('n', 'ss', telescope.extensions.luasnip.luasnip, {}) -- Fuzzy * files, symbols, buffers, help, etc. -vim.keymap.set('n', 'fa', builtin.find_files) -- f[ind]a[ll] +vim.keymap.set('n', 'fa', builtin.find_files) -- f[ind]a[ll] vim.keymap.set('n', 'fb', builtin.buffers) vim.keymap.set('n', 'fd', builtin.diagnostics) vim.keymap.set('n', 'ff', builtin.git_files) diff --git a/dotfiles/.config/nvim/lua/snippets.lua b/dotfiles/.config/nvim/lua/snippets.lua new file mode 100644 index 0000000..e3f1ea0 --- /dev/null +++ b/dotfiles/.config/nvim/lua/snippets.lua @@ -0,0 +1,578 @@ +local ls = require("luasnip") +-- some shorthands... +local s = ls.snippet +local sn = ls.snippet_node +local t = ls.text_node +local i = ls.insert_node +local f = ls.function_node +local c = ls.choice_node +local d = ls.dynamic_node +local r = ls.restore_node +local l = require("luasnip.extras").lambda +local rep = require("luasnip.extras").rep +local p = require("luasnip.extras").partial +local m = require("luasnip.extras").match +local n = require("luasnip.extras").nonempty +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.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. + +-- Every unspecified option will be set to the default. +ls.setup({ + keep_roots = true, + link_roots = true, + link_children = true, + + -- Update more often, :h events for more info. + update_events = "TextChanged,TextChangedI", + -- Snippets aren't automatically removed if their text is deleted. + -- `delete_check_events` determines on which events (:h events) a check for + -- deleted snippets is performed. + -- This can be especially useful when `history` is enabled. + delete_check_events = "TextChanged", + ext_opts = { + [types.choiceNode] = { + active = { + virt_text = { { "choiceNode", "Comment" } }, + }, + }, + }, + -- treesitter-hl has 100, use something higher (default is 200). + ext_base_prio = 300, + -- minimal increase in priority. + ext_prio_increase = 1, + enable_autosnippets = true, + -- mapping for cutting selected text so it's usable as SELECT_DEDENT, + -- SELECT_RAW or TM_SELECTED_TEXT (mapped via xmap). + store_selection_keys = "", + -- luasnip uses this function to get the currently active filetype. This + -- is the (rather uninteresting) default, but it's possible to use + -- eg. treesitter for getting the current filetype by setting ft_func to + -- require("luasnip.extras.filetype_functions").from_cursor (requires + -- `nvim-treesitter/nvim-treesitter`). This allows correctly resolving + -- the current filetype in eg. a markdown-code block or `vim.cmd()`. + ft_func = function() + return vim.split(vim.bo.filetype, ".", true) + end, +}) + +-- args is a table, where 1 is the text in Placeholder 1, 2 the text in +-- placeholder 2,... +local function copy(args) + return args[1] +end + +-- 'recursive' dynamic snippet. Expands to some text followed by itself. +local rec_ls +rec_ls = function() + return sn( + nil, + c(1, { + -- Order is important, sn(...) first would cause infinite loop of expansion. + t(""), + sn(nil, { t({ "", "\t\\item " }), i(1), d(2, rec_ls, {}) }), + }) + ) +end + +-- complicated function for dynamicNode. +local function jdocsnip(args, _, old_state) + -- !!! old_state is used to preserve user-input here. DON'T DO IT THAT WAY! + -- Using a restoreNode instead is much easier. + -- View this only as an example on how old_state functions. + local nodes = { + t({ "/**", " * " }), + i(1, "A short Description"), + t({ "", "" }), + } + + -- These will be merged with the snippet; that way, should the snippet be updated, + -- some user input eg. text can be referred to in the new snippet. + local param_nodes = {} + + if old_state then + nodes[2] = i(1, old_state.descr:get_text()) + end + param_nodes.descr = nodes[2] + + -- At least one param. + if string.find(args[2][1], ", ") then + vim.list_extend(nodes, { t({ " * ", "" }) }) + end + + local insert = 2 + for indx, arg in ipairs(vim.split(args[2][1], ", ", true)) do + -- Get actual name parameter. + arg = vim.split(arg, " ", true)[2] + if arg then + local inode + -- if there was some text in this parameter, use it as static_text for this new snippet. + if old_state and old_state[arg] then + inode = i(insert, old_state["arg" .. arg]:get_text()) + else + inode = i(insert) + end + vim.list_extend( + nodes, + { t({ " * @param " .. arg .. " " }), inode, t({ "", "" }) } + ) + param_nodes["arg" .. arg] = inode + + insert = insert + 1 + end + end + + if args[1][1] ~= "void" then + local inode + if old_state and old_state.ret then + inode = i(insert, old_state.ret:get_text()) + else + inode = i(insert) + end + + vim.list_extend( + nodes, + { t({ " * ", " * @return " }), inode, t({ "", "" }) } + ) + param_nodes.ret = inode + insert = insert + 1 + end + + if vim.tbl_count(args[3]) ~= 1 then + local exc = string.gsub(args[3][2], " throws ", "") + local ins + if old_state and old_state.ex then + ins = i(insert, old_state.ex:get_text()) + else + ins = i(insert) + end + vim.list_extend( + nodes, + { t({ " * ", " * @throws " .. exc .. " " }), ins, t({ "", "" }) } + ) + param_nodes.ex = ins + insert = insert + 1 + end + + vim.list_extend(nodes, { t({ " */" }) }) + + local snip = sn(nil, nodes) + -- Error on attempting overwrite. + snip.old_state = param_nodes + return snip +end + +-- Make sure to not pass an invalid command, as io.popen() may write over nvim-text. +local function bash(_, _, command) + local file = io.popen(command, "r") + local res = {} + for line in file:lines() do + table.insert(res, line) + end + return res +end + +-- Returns a snippet_node wrapped around an insertNode whose initial +-- text value is set to the current date in the desired format. +local date_input = function(args, snip, old_state, fmt) + local fmt = fmt or "%Y-%m-%d" + return sn(nil, i(1, os.date(fmt))) +end + +-- snippets are added via ls.add_snippets(filetype, snippets[, opts]), where +-- opts may specify the `type` of the snippets ("snippets" or "autosnippets", +-- for snippets that should expand directly after the trigger is typed). +-- +-- opts can also specify a key. By passing an unique key to each add_snippets, it's possible to reload snippets by +-- re-`:luafile`ing the file in which they are defined (eg. this one). +ls.add_snippets("all", { + -- trigger is `fn`, second argument to snippet-constructor are the nodes to insert into the buffer on expansion. + s("fn", { + -- Simple static text. + t("//Parameters: "), + -- function, first parameter is the function, second the Placeholders + -- whose text it gets as input. + f(copy, 2), + t({ "", "function " }), + -- Placeholder/Insert. + i(1), + t("("), + -- Placeholder with initial text. + i(2, "int foo"), + -- Linebreak + t({ ") {", "\t" }), + -- Last Placeholder, exit Point of the snippet. + i(0), + t({ "", "}" }), + }), + s("class", { + -- Choice: Switch between two different Nodes, first parameter is its position, second a list of nodes. + c(1, { + t("public "), + t("private "), + }), + t("class "), + i(2), + t(" "), + c(3, { + t("{"), + -- sn: Nested Snippet. Instead of a trigger, it has a position, just like insertNodes. !!! These don't expect a 0-node!!!! + -- Inside Choices, Nodes don't need a position as the choice node is the one being jumped to. + sn(nil, { + t("extends "), + -- restoreNode: stores and restores nodes. + -- pass position, store-key and nodes. + r(1, "other_class", i(1)), + t(" {"), + }), + sn(nil, { + t("implements "), + -- no need to define the nodes for a given key a second time. + r(1, "other_class"), + t(" {"), + }), + }), + t({ "", "\t" }), + i(0), + t({ "", "}" }), + }), + -- Alternative printf-like notation for defining snippets. It uses format + -- string with placeholders similar to the ones used with Python's .format(). + s( + "fmt1", + fmt("To {title} {} {}.", { + i(2, "Name"), + i(3, "Surname"), + title = c(1, { t("Mr."), t("Ms.") }), + }) + ), + -- To escape delimiters use double them, e.g. `{}` -> `{{}}`. + -- Multi-line format strings by default have empty first/last line removed. + -- Indent common to all lines is also removed. Use the third `opts` argument + -- to control this behaviour. + s( + "fmt2", + fmt( + [[ + foo({1}, {3}) {{ + return {2} * {4} + }} + ]], + { + i(1, "x"), + rep(1), + i(2, "y"), + rep(2), + } + ) + ), + -- Empty placeholders are numbered automatically starting from 1 or the last + -- value of a numbered placeholder. Named placeholders do not affect numbering. + s( + "fmt3", + fmt("{} {a} {} {1} {}", { + t("1"), + t("2"), + a = t("A"), + }) + ), + -- The delimiters can be changed from the default `{}` to something else. + s("fmt4", fmt("foo() { return []; }", i(1, "x"), { delimiters = "[]" })), + -- `fmta` is a convenient wrapper that uses `<>` instead of `{}`. + s("fmt5", fmta("foo() { return <>; }", i(1, "x"))), + -- By default all args must be used. Use strict=false to disable the check + s( + "fmt6", + fmt("use {} only", { t("this"), t("not this") }, { strict = false }) + ), + -- Use a dynamicNode to interpolate the output of a + -- function (see date_input above) into the initial + -- value of an insertNode. + s("novel", { + t("It was a dark and stormy night on "), + d(1, date_input, {}, { user_args = { "%A, %B %d of %Y" } }), + t(" and the clocks were striking thirteen."), + }), + -- Parsing snippets: First parameter: Snippet-Trigger, Second: Snippet body. + -- Placeholders are parsed into choices with 1. the placeholder text(as a snippet) and 2. an empty string. + -- This means they are not SELECTed like in other editors/Snippet engines. + ls.parser.parse_snippet( + "lspsyn", + "Wow! This ${1:Stuff} really ${2:works. ${3:Well, a bit.}}" + ), + + -- When wordTrig is set to false, snippets may also expand inside other words. + ls.parser.parse_snippet( + { trig = "te", wordTrig = false }, + "${1:cond} ? ${2:true} : ${3:false}" + ), + + -- When regTrig is set, trig is treated like a pattern, this snippet will expand after any number. + ls.parser.parse_snippet({ trig = "%d", regTrig = true }, "A Number!!"), + -- Using the condition, it's possible to allow expansion only in specific cases. + s("cond", { + t("will only expand in c-style comments"), + }, { + condition = function(line_to_cursor, matched_trigger, captures) + -- optional whitespace followed by // + return line_to_cursor:match("%s*//") + end, + }), + -- 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_expand.line_begin, + }), + s("cond3", { + t("will only expand at the end of the line"), + }, { + 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( + { trig = "a%d", regTrig = true }, + f(function(_, snip) + return "Triggered with " .. snip.trigger .. "." + end, {}) + ), + -- It's possible to use capture-groups inside regex-triggers. + s( + { trig = "b(%d)", regTrig = true }, + f(function(_, snip) + return "Captured Text: " .. snip.captures[1] .. "." + end, {}) + ), + s({ trig = "c(%d+)", regTrig = true }, { + t("will only expand for even numbers"), + }, { + condition = function(line_to_cursor, matched_trigger, captures) + return tonumber(captures[1]) % 2 == 0 + end, + }), + -- Use a function to execute any shell command and print its text. + s("bash", f(bash, {}, { user_args = { "ls" } })), + -- Short version for applying String transformations using function nodes. + s("transform", { + i(1, "initial text"), + t({ "", "" }), + -- lambda nodes accept an l._1,2,3,4,5, which in turn accept any string transformations. + -- This list will be applied in order to the first node given in the second argument. + l(l._1:match("[^i]*$"):gsub("i", "o"):gsub(" ", "_"):upper(), 1), + }), + + s("transform2", { + i(1, "initial text"), + t("::"), + i(2, "replacement for e"), + t({ "", "" }), + -- Lambdas can also apply transforms USING the text of other nodes: + l(l._1:gsub("e", l._2), { 1, 2 }), + }), + s({ trig = "trafo(%d+)", regTrig = true }, { + -- env-variables and captures can also be used: + l(l.CAPTURE1:gsub("1", l.TM_FILENAME), {}), + }), + -- Set store_selection_keys = "" (for example) in your + -- luasnip.config.setup() call to populate + -- TM_SELECTED_TEXT/SELECT_RAW/SELECT_DEDENT. + -- In this case: select a URL, hit Tab, then expand this snippet. + s("link_url", { + t(''), + i(1), + t(""), + i(0), + }), + -- Shorthand for repeating the text in a given node. + s("repeat", { i(1, "text"), t({ "", "" }), rep(1) }), + -- Directly insert the ouput from a function evaluated at runtime. + s("part", p(os.date, "%Y")), + -- use matchNodes (`m(argnode, condition, then, else)`) to insert text + -- based on a pattern/function/lambda-evaluation. + -- It's basically a shortcut for simple functionNodes: + s("mat", { + i(1, { "sample_text" }), + t(": "), + m(1, "%d", "contains a number", "no number :("), + }), + -- The `then`-text defaults to the first capture group/the entire + -- match if there are none. + s("mat2", { + i(1, { "sample_text" }), + t(": "), + m(1, "[abc][abc][abc]"), + }), + -- It is even possible to apply gsubs' or other transformations + -- before matching. + s("mat3", { + i(1, { "sample_text" }), + t(": "), + m( + 1, + l._1:gsub("[123]", ""):match("%d"), + "contains a number that isn't 1, 2 or 3!" + ), + }), + -- `match` also accepts a function in place of the condition, which in + -- turn accepts the usual functionNode-args. + -- The condition is considered true if the function returns any + -- non-nil/false-value. + -- If that value is a string, it is used as the `if`-text if no if is explicitly given. + s("mat4", { + i(1, { "sample_text" }), + t(": "), + m(1, function(args) + -- args is a table of multiline-strings (as usual). + return (#args[1][1] % 2 == 0 and args[1]) or nil + end), + }), + -- The nonempty-node inserts text depending on whether the arg-node is + -- empty. + s("nempty", { + i(1, "sample_text"), + n(1, "i(1) is not empty!"), + }), + -- dynamic lambdas work exactly like regular lambdas, except that they + -- don't return a textNode, but a dynamicNode containing one insertNode. + -- This makes it easier to dynamically set preset-text for insertNodes. + s("dl1", { + i(1, "sample_text"), + t({ ":", "" }), + dl(2, l._1, 1), + }), + -- Obviously, it's also possible to apply transformations, just like lambdas. + s("dl2", { + i(1, "sample_text"), + i(2, "sample_text_2"), + t({ "", "" }), + dl(3, l._1:gsub("\n", " linebreak ") .. l._2, { 1, 2 }), + }), +}, { + key = "all", +}) + +ls.add_snippets("java", { + -- Very long example for a java class. + s("fn", { + d(6, jdocsnip, { 2, 4, 5 }), + t({ "", "" }), + c(1, { + t("public "), + t("private "), + }), + c(2, { + t("void"), + t("String"), + t("char"), + t("int"), + t("double"), + t("boolean"), + i(nil, ""), + }), + t(" "), + i(3, "myFunc"), + t("("), + i(4), + t(")"), + c(5, { + t(""), + sn(nil, { + t({ "", " throws " }), + i(1), + }), + }), + t({ " {", "\t" }), + i(0), + t({ "", "}" }), + }), +}, { + key = "java", +}) + +ls.add_snippets("tex", { + -- rec_ls is self-referencing. That makes this snippet 'infinite' eg. have as many + -- \item as necessary by utilizing a choiceNode. + s("ls", { + t({ "\\begin{itemize}", "\t\\item " }), + i(1), + d(2, rec_ls, {}), + t({ "", "\\end{itemize}" }), + }), +}, { + key = "tex", +}) + +-- set type to "autosnippets" for adding autotriggered snippets. +ls.add_snippets("all", { + s("autotrigger", { + t("autosnippet"), + }), +}, { + type = "autosnippets", + key = "all_auto", +}) + +-- in a lua file: search lua-, then c-, then all-snippets. +ls.filetype_extend("lua", { "c" }) +-- in a cpp file: search c-snippets, then all-snippets only (no cpp-snippets!!). +ls.filetype_set("cpp", { "c" }) + +-- Beside defining your own snippets you can also load snippets from "vscode-like" packages +-- that expose snippets in json files, for example . + +require("luasnip.loaders.from_vscode").load({ include = { "python" } }) -- Load only python snippets + +-- The directories will have to be structured like eg. (include +-- a similar `package.json`) +require("luasnip.loaders.from_vscode").load({ paths = { "./my-snippets" } }) -- Load snippets from my-snippets folder + +-- You can also use lazy loading so snippets are loaded on-demand, not all at once (may interfere with lazy-loading luasnip itself). +require("luasnip.loaders.from_vscode").lazy_load() -- You can pass { paths = "./my-snippets/"} as well + +-- You can also use snippets in snipmate format, for example . +-- The usage is similar to vscode. + +-- One peculiarity of honza/vim-snippets is that the file containing global +-- snippets is _.snippets, so we need to tell luasnip that the filetype "_" +-- contains global snippets: +ls.filetype_extend("all", { "_" }) + +require("luasnip.loaders.from_snipmate").load({ include = { "c" } }) -- Load only snippets for c. + +-- Load snippets from my-snippets folder +-- The "." refers to the directory where of your `$MYVIMRC` (you can print it +-- out with `:lua print(vim.env.MYVIMRC)`. +-- NOTE: It's not always set! It isn't set for example if you call neovim with +-- the `-u` argument like this: `nvim -u yeet.txt`. +require("luasnip.loaders.from_snipmate").load({ path = { "./my-snippets" } }) +-- If path is not specified, luasnip will look for the `snippets` directory in rtp (for custom-snippet probably +-- `~/.config/nvim/snippets`). + +require("luasnip.loaders.from_snipmate").lazy_load() -- Lazy loading + +-- see DOC.md/LUA SNIPPETS LOADER for some details. +require("luasnip.loaders.from_lua").load({ include = { "c" } }) +require("luasnip.loaders.from_lua").lazy_load({ include = { "all", "cpp" } })