Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate source for loaded snippets. #826

Merged
merged 9 commits into from
May 1, 2023
61 changes: 61 additions & 0 deletions DOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -1763,6 +1763,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 @@ -2861,6 +2884,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 @@ -2932,6 +2987,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
72 changes: 67 additions & 5 deletions doc/luasnip.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
*luasnip.txt* For NVIM v0.8.0 Last change: 2023 April 23
*luasnip.txt* For NVIM v0.8.0 Last change: 2023 May 01

==============================================================================
Table of Contents *luasnip-table-of-contents*
Expand Down Expand Up @@ -36,6 +36,7 @@ Table of Contents *luasnip-table-of-contents*
- Filetype-Functions |luasnip-extras-filetype-functions|
- Postfix-Snippet |luasnip-extras-postfix-snippet|
- Snippet List |luasnip-extras-snippet-list|
- Snippet Location |luasnip-extras-snippet-location|
16. Extend Decorator |luasnip-extend-decorator|
17. LSP-Snippets |luasnip-lsp-snippets|
- Snipmate Parser |luasnip-lsp-snippets-snipmate-parser|
Expand All @@ -56,8 +57,9 @@ Table of Contents *luasnip-table-of-contents*
24. Events |luasnip-events|
25. Cleanup |luasnip-cleanup|
26. Logging |luasnip-logging|
27. Config-Options |luasnip-config-options|
28. API |luasnip-api|
27. Source |luasnip-source|
28. Config-Options |luasnip-config-options|
29. API |luasnip-api|
>
__ ____
/\ \ /\ _`\ __
Expand Down Expand Up @@ -1676,6 +1678,26 @@ Let’s recreate the custom display example above:
<


SNIPPET LOCATION *luasnip-extras-snippet-location*

This module can consume a snippets |luasnip-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`.


==============================================================================
16. Extend Decorator *luasnip-extend-decorator*

Expand Down Expand Up @@ -2774,7 +2796,43 @@ print a short message to the log.


==============================================================================
27. Config-Options *luasnip-config-options*
27. Source *luasnip-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`
(|luasnip-config-options|, `loaders_store_source`)), or manually. The attached
data can be used by |luasnip-extras-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).


==============================================================================
28. Config-Options *luasnip-config-options*

These are the settings you can provide to `luasnip.setup()`:

Expand Down Expand Up @@ -2841,10 +2899,14 @@ These are the settings you can provide to `luasnip.setup()`:
warnings, consider adding the undefined globals to the globals recognized by
`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 |luasnip-extras-snippet-location|, but also entails slightly
increased memory consumption (and load-time, but it’s not really noticeable).


==============================================================================
28. API *luasnip-api*
29. API *luasnip-api*

`require("luasnip")`:

Expand Down
1 change: 1 addition & 0 deletions lua/luasnip/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ local defaults = {
load_ft_func = ft_functions.from_filetype_load,
-- globals injected into luasnippet-files.
snip_env = util.lazy_table({}, lazy_snip_env),
loaders_store_source = false,
}

local function set_snip_env(target_conf_defaults, user_config)
Expand Down
186 changes: 186 additions & 0 deletions lua/luasnip/extras/snip_location.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
local Source = require("luasnip.session.snippet_collection.source")

-- stylua: ignore
local tsquery_parse =
(vim.treesitter.query and vim.treesitter.query.parse)
and vim.treesitter.query.parse
or vim.treesitter.parse_query

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, "lua")
if not has_parser then
error("Error while getting parser: " .. parser)
end

local root = parser:parse()[1]:root()
local query = tsquery_parse("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
error(
"Query for `(function_call)` starting at line %s did not yield a result."
)
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, extension, snippet_name)
local parser_ok, parser = pcall(vim.treesitter.get_parser, bufnr, extension)
if not parser_ok then
error("Error while getting parser: " .. parser)
end

local root = parser:parse()[1]:root()
-- don't want to pass through whether this file is json or jsonc, just use
-- parser-language.
local query = tsquery_parse(
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 first match.
return { node:range() }
end
end

error(
("Treesitter did not find the definition for snippet `%s`"):format(
snippet_name
)
)
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.
local ok
ok, fcall_range =
pcall(lua_find_function_call_node_at, 0, source.line - 1)
if not ok then
print(
"Could not determine range for snippet-definition: "
.. fcall_range
)
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
local extension = vim.api.nvim_buf_get_name(0):match("jsonc?$")
local ok
ok, fcall_range =
pcall(json_find_snippet_definition, 0, extension, snip.name)
if not ok then
print(
"Could not determine range of snippet-definition: "
.. fcall_range
)
return
end
else
print(
("Don't know how to highlight snippet-definitions in current buffer `%s`.%s"):format(
vim.api.nvim_buf_get_name(0),
source.line ~= nil and " Jumping to `source.line`" or ""
)
)

if source.line ~= nil then
vim.api.nvim_win_set_cursor(0, { source.line, 0 })
end
return
end
assert(fcall_range ~= nil, "fcall_range is not nil")

-- 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
1 change: 1 addition & 0 deletions lua/luasnip/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,7 @@ local ls_lazy = {
parser = function() return require("luasnip.util.parser") end,
config = function() return require("luasnip.config") end,
multi_snippet = function() return require("luasnip.nodes.multiSnippet").new_multisnippet end,
snippet_source = function() return require("luasnip.session.snippet_collection.source") end,
}

ls = util.lazy_table({
Expand Down
Loading