Skip to content

Commit

Permalink
add function for editing current snippet.
Browse files Browse the repository at this point in the history
  • Loading branch information
L3MON4D3 committed Mar 19, 2023
1 parent d357a66 commit 9cef330
Show file tree
Hide file tree
Showing 2 changed files with 206 additions and 0 deletions.
61 changes: 61 additions & 0 deletions DOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -1761,6 +1761,29 @@ sl.open({display = modified_default_display})

<!-- panvimdoc-ignore-end -->

## Snippet Location

This module can consume a snippets [source](#source), more specifically, jump to
the location referred by it.
This is primarily implemented for snippet which got their source from one of the
loaders, but might also work for snippets where the source was set manually.

`require("luasnip.extras.snip_location")`:
* `snip_location.jump_to_snippet(snip, opts)`
Jump to the definition of `snip`.
* `snip`: a snippet with attached source-data.
* `opts`: `nil|table`, optional arguments, valid keys are:
* `hl_duration_ms`: `number`, duration for which the definition should be highlighted,
in milliseconds. 0 disables the highlight.
* `edit_fn`: `function(file)`, this function will be called with the file
the snippet is located in, and is responsible for jumping to it.
We assume that after it has returned, the current buffer contains `file`.
* `snip_location.jump_to_active_snippet(opts)`
Jump to definition of active snippet.
* `opts`: `nil|table`, accepts the same keys as the `opts`-parameter of
`jump_to_snippet`.


# Extend Decorator

Most of luasnip's functions have some arguments to control their behaviour.
Expand Down Expand Up @@ -2849,6 +2872,38 @@ there already exists a `luasnip.log.old`, it will be deleted.
`ls.log.ping()` can be used to verify the log is working correctly: it will
print a short message to the log.

# Source
It is possible to attach, to a snippet, information about its source. This can
be done either by the various loaders (if it is enabled in `ls.setup`
([Config-Options](#config-options), `loaders_store_source`)), or manually. The
attached data can be used by [Extras-Snippet-Location](#snippet-location) to
jump to the definition of a snippet.

It is also possible to get/set the source of a snippet via API:

`ls.snippet_source`:

* `get(snippet) -> source_data`:
Retrieve the source-data of `snippet`. `source_data` always contains the key
`file`, the file in which the snippet was defined, and may additionally
contain `line` or `line_end`, the first and last line of the definition.
* `set(snippet, source)`:
Set the source of a snippet.
* `snippet`: a snippet which was added via `ls.add_snippets`.
* `source`: a `source`-object, obtained from either `from_debuginfo` or
`from_location`.
* `from_location(file, opts) -> source`:
* `file`: `string`, The path to the file in which the snippet is defined.
* `opts`: `table|nil`, optional parameters for the source.
* `line`: `number`, the first line of the definition. 1-indexed.
* `line_end`: `number`, the final line of the definition. 1-indexed.
* `from_debuginfo(debuginfo) -> source`:
Generates source from the table returned by `debug.getinfo` (from now on
referred to as `debuginfo`). `debuginfo` has to be of a frame of a function
which is backed by a file, and has to contain this information, ie. has to be
generated by `debug.get_info(*, "Sl")` (at least `"Sl"`, it may also contain
more info).

# Config-Options

These are the settings you can provide to `luasnip.setup()`:
Expand Down Expand Up @@ -2920,6 +2975,12 @@ These are the settings you can provide to `luasnip.setup()`:
`lua-language-server` or add `---@diagnostic disable: undefined-global`
somewhere in the affected files.

- `loaders_store_source`, boolean, whether loaders should store the source of
the loaded snippets.
Enabling this means that the definition of any snippet can be jumped to via
[Extras-Snippet-Location](#snippet-location), but also entails slightly
increased memory consumption (and load-time, but it's not really noticeable).

# API

`require("luasnip")`:
Expand Down
145 changes: 145 additions & 0 deletions lua/luasnip/extras/snip_location.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
local log = require("luasnip.util.log").new("snip_edit")
local Source = require("luasnip.session.snippet_collection.source")

local M = {}

-- return: 4-tuple, {start_line, start_col, end_line, end_col}, range of
-- function-call.
local function lua_find_function_call_node_at(bufnr, line)
local has_parser, parser = pcall(vim.treesitter.get_parser, bufnr)
if not has_parser then
log.warn("Cannot determine range of function-call: treesitter-parser for lua not installed.")
return nil
end

local root = parser:parse()[1]:root()
local query = vim.treesitter.parse_query("lua", [[(function_call) @f_call]])
for _, node, _ in query:iter_captures(root, bufnr, line, line+300) do
if node:range() == line then
return {node:range()}
end
end
log.warn("Cannot determine range of function-call: Query for `(function_call)` starting at line %s did not yield a result.")
return nil
end

local function range_highlight(line_start, line_end, hl_duration_ms)
-- make sure line_end is also visible.
vim.api.nvim_win_set_cursor(0, {line_end, 0})
vim.api.nvim_win_set_cursor(0, {line_start, 0})

if hl_duration_ms > 0 then
local hl_buf = vim.api.nvim_get_current_buf()

-- highlight snippet for 1000ms
local id = vim.api.nvim_buf_set_extmark(hl_buf, ls.session.ns_id, line_start-1, 0, {
-- one line below, at col 0 => entire last line is highlighted.
end_row = line_end-1+1,
hl_group = "Visual"
})
vim.defer_fn(function()
vim.api.nvim_buf_del_extmark(hl_buf, ls.session.ns_id, id)
end, hl_duration_ms)
end
end

local function json_find_snippet_definition(bufnr, snippet_name)
local has_parser, parser = pcall(vim.treesitter.get_parser, bufnr)
if not has_parser then
log.warn("Cannot determine definition of snippet: treesitter-parser for json(c) not installed.")
return nil
end

local root = parser:parse()[1]:root()
-- for a, b in root:child(0):iter_children() do
-- Insp(a:field("key")[1]:child(1):range())
-- end
-- don't want to pass through if this file is json or jsonc, just use
-- parser-language.
local query = vim.treesitter.parse_query(parser:lang(), ([[
(pair
key: (string (string_content) @key (#eq? @key "%s"))
) @snippet
]]):format(snippet_name))
for id, node, _ in query:iter_captures(root, bufnr) do
if query.captures[id] == "snippet" and node:parent():parent() == root then
return {node:range()}
end
end

return nil
end

local function win_edit(file)
vim.api.nvim_command(":e " .. file)
end

function M.jump_to_snippet(snip, opts)
opts = opts or {}
local hl_duration_ms = opts.hl_duration_ms or 1500
local edit_fn = opts.edit_fn or win_edit

local source = Source.get(snip)
if not source then
print("Snippet does not have a source.")
return
end

edit_fn(source.file)
-- assumption: after this, file is the current buffer.

if source.line and source.line_end then
-- happy path: we know both begin and end of snippet-definition.
range_highlight(source.line, source.line_end, hl_duration_ms)
return
end

local fcall_range
if vim.api.nvim_buf_get_name(0):match("%.lua$") then
if source.line then
-- in lua-file, can get region of definition via treesitter.
-- 0: current buffer.
fcall_range = lua_find_function_call_node_at(0, source.line-1)
if not fcall_range then
vim.api.nvim_win_set_cursor(0, {source.line, 0})
return
end
else
print("Can't jump to snippet: source does not provide line.")
return
end
-- matches *.json or *.jsonc.
elseif vim.api.nvim_buf_get_name(0):match("%.jsonc?$") then
-- 0: current buffer.
fcall_range = json_find_snippet_definition(0, snip.name)
if not fcall_range then
print("Could not find range of snippet `" .. snip.name .. "`.")
return
end
else
print("Unknown filetype, don't know how to highlight snippet.")
return
end

-- 1 is line_from, 3 is line_end.
-- +1 since range is row-0-indexed.
range_highlight(fcall_range[1]+1, fcall_range[3]+1, hl_duration_ms)

local new_source = Source.from_location(
source.file,
{ line = fcall_range[1]+1, line_end = fcall_range[3]+1 })
Source.set(snip, new_source)
end

function M.jump_to_active_snippet(opts)
local active_node = require("luasnip.session").current_nodes[vim.api.nvim_get_current_buf()]
if not active_node then
print("No active snippet.")
return
end

local snip = active_node.parent.snippet
M.jump_to_snippet(snip, opts)
end

return M

0 comments on commit 9cef330

Please sign in to comment.