mirror of
https://github.com/folke/snacks.nvim
synced 2025-07-07 13:15:08 +00:00
feat(image): added support for a bunch of aditional languages
This commit is contained in:
parent
332f4278b0
commit
a596f8a9ea
21 changed files with 236 additions and 179 deletions
|
@ -6,160 +6,22 @@ local M = {}
|
|||
---@field win snacks.win
|
||||
---@field buf number
|
||||
|
||||
---@class snacks.image.Query
|
||||
---@field setup fun():vim.treesitter.Query
|
||||
---@field query? vim.treesitter.Query|false
|
||||
---@field transform? fun(buf:number, src:string, anchor: TSNode, image: TSNode): string
|
||||
---@alias snacks.image.transform fun(buf:number, src:string, anchor: TSNode, image: TSNode): string
|
||||
|
||||
---@type table<string, {setup:(fun():vim.treesitter.Query), query?:vim.treesitter.Query|false}>
|
||||
M._queries = {
|
||||
markdown = {
|
||||
setup = function()
|
||||
return vim.treesitter.query.parse(
|
||||
"markdown_inline",
|
||||
[[
|
||||
(image
|
||||
[
|
||||
(link_destination) @image
|
||||
(image_description (shortcut_link (link_text) @image))
|
||||
]) @anchor
|
||||
]]
|
||||
)
|
||||
end,
|
||||
transform = function(_, src)
|
||||
return src:gsub("|.*", ""):gsub("^<", ""):gsub(">$", "")
|
||||
end,
|
||||
},
|
||||
html = {
|
||||
setup = function()
|
||||
return vim.treesitter.query.parse(
|
||||
"html",
|
||||
[[
|
||||
(element
|
||||
(start_tag
|
||||
(tag_name) @tag (#eq? @tag "img")
|
||||
(attribute
|
||||
(attribute_name) @attr_name (#eq? @attr_name "src")
|
||||
(quoted_attribute_value (attribute_value) @image)
|
||||
)
|
||||
)
|
||||
) @anchor
|
||||
(self_closing_tag
|
||||
(tag_name) @tag (#eq? @tag "img")
|
||||
(attribute
|
||||
(attribute_name) @attr_name (#eq? @attr_name "src")
|
||||
(quoted_attribute_value (attribute_value) @image)
|
||||
)
|
||||
) @anchor
|
||||
]]
|
||||
)
|
||||
end,
|
||||
},
|
||||
tsx = {
|
||||
setup = function()
|
||||
return vim.treesitter.query.parse(
|
||||
"tsx",
|
||||
[[
|
||||
(jsx_element
|
||||
(jsx_opening_element
|
||||
(identifier) @tag (#eq? @tag "img")
|
||||
(jsx_attribute
|
||||
(property_identifier) @attr_name (#eq? @attr_name "src")
|
||||
(string (string_fragment) @image)
|
||||
)
|
||||
)
|
||||
) @anchor
|
||||
|
||||
(jsx_self_closing_element
|
||||
(identifier) @tag (#eq? @tag "img")
|
||||
(jsx_attribute
|
||||
(property_identifier) @attr_name (#eq? @attr_name "src")
|
||||
(string (string_fragment) @image)
|
||||
)
|
||||
) @anchor
|
||||
]]
|
||||
)
|
||||
end,
|
||||
},
|
||||
javascript = {
|
||||
setup = function()
|
||||
return vim.treesitter.query.parse(
|
||||
"javascript",
|
||||
[[
|
||||
(jsx_element
|
||||
(jsx_opening_element
|
||||
(identifier) @tag (#eq? @tag "img")
|
||||
(jsx_attribute
|
||||
(property_identifier) @attr_name (#eq? @attr_name "src")
|
||||
(string (string_fragment) @image)
|
||||
)
|
||||
)
|
||||
) @anchor
|
||||
|
||||
(jsx_self_closing_element
|
||||
(identifier) @tag (#eq? @tag "img")
|
||||
(jsx_attribute
|
||||
(property_identifier) @attr_name (#eq? @attr_name "src")
|
||||
(string (string_fragment) @image)
|
||||
)
|
||||
) @anchor
|
||||
]]
|
||||
)
|
||||
end,
|
||||
},
|
||||
css = {
|
||||
setup = function()
|
||||
return vim.treesitter.query.parse(
|
||||
"css",
|
||||
[[
|
||||
(declaration
|
||||
(call_expression
|
||||
(function_name) @fn (#eq? @fn "url")
|
||||
(arguments [(plain_value) @image (string_value (string_content) @image)]))
|
||||
) @anchor
|
||||
]]
|
||||
)
|
||||
end,
|
||||
},
|
||||
norg = {
|
||||
setup = function()
|
||||
return vim.treesitter.query.parse(
|
||||
"norg",
|
||||
[[
|
||||
(infirm_tag
|
||||
(tag_name) @tag (#eq? @tag "image")
|
||||
(tag_parameters (tag_param) @image)
|
||||
) @anchor
|
||||
]]
|
||||
)
|
||||
end,
|
||||
---@param anchor TSNode
|
||||
---@param img TSNode
|
||||
transform = function(buf, _, anchor, img)
|
||||
local row, col = img:start()
|
||||
local line = vim.api.nvim_buf_get_lines(buf, row, row + 1, false)[1]
|
||||
return line:sub(col + 1)
|
||||
end,
|
||||
},
|
||||
---@type table<string, snacks.image.transform>
|
||||
M.transforms = {
|
||||
---@param anchor TSNode
|
||||
---@param img TSNode
|
||||
norg = function(buf, _, anchor, img)
|
||||
local row, col = img:start()
|
||||
local line = vim.api.nvim_buf_get_lines(buf, row, row + 1, false)[1]
|
||||
return line:sub(col + 1)
|
||||
end,
|
||||
}
|
||||
|
||||
local hover ---@type snacks.image.Hover?
|
||||
local uv = vim.uv or vim.loop
|
||||
|
||||
function M.queries()
|
||||
local ret = {} ---@type snacks.image.Query[]
|
||||
for _, query in pairs(M._queries) do
|
||||
if query.query == nil then
|
||||
local ok, q = pcall(query.setup)
|
||||
query.query = ok and q or false
|
||||
end
|
||||
if query.query then
|
||||
table.insert(ret, query)
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
---@param str string
|
||||
function M.url_decode(str)
|
||||
return str:gsub("+", " "):gsub("%%(%x%x)", function(hex)
|
||||
|
@ -201,36 +63,39 @@ function M.find(buf, from, to)
|
|||
end
|
||||
parser:parse(from and to and { from, to } or true)
|
||||
local ret = {} ---@type {id:string, pos:snacks.image.Pos, src:string}[]
|
||||
parser:for_each_tree(function(tstree)
|
||||
parser:for_each_tree(function(tstree, tree)
|
||||
if not tstree then
|
||||
return
|
||||
end
|
||||
for _, query in ipairs(M.queries()) do
|
||||
for _, match in query.query:iter_matches(tstree:root(), buf, from and from - 1 or nil, to and to - 1 or nil) do
|
||||
local src, pos, nid ---@type string, snacks.image.Pos, string
|
||||
local anchor, image ---@type TSNode, TSNode
|
||||
for id, nodes in pairs(match) do
|
||||
nodes = type(nodes) == "userdata" and { nodes } or nodes
|
||||
local name = query.query.captures[id]
|
||||
for _, node in ipairs(nodes) do
|
||||
if name == "image" then
|
||||
image = node
|
||||
src = vim.treesitter.get_node_text(node, buf)
|
||||
elseif name == "anchor" then
|
||||
anchor = node
|
||||
local range = { node:range() }
|
||||
pos = { range[1] + 1, range[2] }
|
||||
nid = node:id()
|
||||
end
|
||||
local query = vim.treesitter.query.get(tree:lang(), "images")
|
||||
if not query then
|
||||
return
|
||||
end
|
||||
for _, match, meta in query:iter_matches(tstree:root(), buf, from and from - 1 or nil, to and to - 1 or nil) do
|
||||
local src, pos, nid ---@type string, snacks.image.Pos, string
|
||||
local anchor, image ---@type TSNode, TSNode
|
||||
for id, nodes in pairs(match) do
|
||||
nodes = type(nodes) == "userdata" and { nodes } or nodes
|
||||
local name = query.captures[id]
|
||||
for _, node in ipairs(nodes) do
|
||||
if name == "image" then
|
||||
image = node
|
||||
src = vim.treesitter.get_node_text(node, buf, { metadata = meta[id] })
|
||||
elseif name == "anchor" then
|
||||
anchor = node
|
||||
local range = { node:range() }
|
||||
pos = { range[1] + 1, range[2] }
|
||||
nid = node:id()
|
||||
end
|
||||
end
|
||||
if src and pos and nid then
|
||||
if query.transform then
|
||||
src = query.transform(buf, src, anchor, image)
|
||||
end
|
||||
src = M.resolve(buf, src)
|
||||
ret[#ret + 1] = { id = nid, pos = pos, src = src }
|
||||
end
|
||||
if src and pos and nid then
|
||||
local transform = M.transforms[tree:lang()]
|
||||
if transform then
|
||||
src = transform(buf, src, anchor, image)
|
||||
end
|
||||
src = M.resolve(buf, src)
|
||||
ret[#ret + 1] = { id = nid, pos = pos, src = src }
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
|
|
@ -51,7 +51,6 @@ local defaults = {
|
|||
-- a treesitter parser must be available for the enabled languages.
|
||||
-- supported language injections: markdown, html
|
||||
enabled = true,
|
||||
lang = { "markdown", "html", "norg", "tsx", "javascript", "css", "vue", "angular" },
|
||||
-- render the image inline in the buffer
|
||||
-- if your env doesn't support unicode placeholders, this will be disabled
|
||||
-- takes precedence over `opts.float` on supported terminals
|
||||
|
@ -127,6 +126,14 @@ function M.hover()
|
|||
M.doc.hover()
|
||||
end
|
||||
|
||||
---@return string[]
|
||||
function M.langs()
|
||||
local queries = vim.api.nvim_get_runtime_file("queries/*/images.scm", true)
|
||||
return vim.tbl_map(function(q)
|
||||
return q:match("queries/(.-)/images%.scm")
|
||||
end, queries)
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param ev? vim.api.keyset.create_autocmd.callback_args
|
||||
function M.setup(ev)
|
||||
|
@ -156,12 +163,13 @@ function M.setup(ev)
|
|||
})
|
||||
end
|
||||
if M.config.enabled and M.config.doc.enabled then
|
||||
local langs = M.langs()
|
||||
vim.api.nvim_create_autocmd("FileType", {
|
||||
group = group,
|
||||
callback = function(e)
|
||||
local ft = vim.bo[e.buf].filetype
|
||||
local lang = vim.treesitter.language.get_lang(ft)
|
||||
if vim.tbl_contains(M.config.doc.lang, lang) then
|
||||
if vim.tbl_contains(langs, lang) then
|
||||
vim.schedule(function()
|
||||
M.doc.attach(e.buf)
|
||||
end)
|
||||
|
@ -208,14 +216,15 @@ function M.health()
|
|||
)
|
||||
)
|
||||
|
||||
M.doc.queries()
|
||||
for lang, q in pairs(M.doc._queries) do
|
||||
if q.query then
|
||||
Snacks.health.ok("Images rendering for `" .. lang .. "` is available")
|
||||
for _, lang in ipairs(M.langs()) do
|
||||
local ok, parser = pcall(vim.treesitter.get_string_parser, "", lang)
|
||||
if ok and parser then
|
||||
Snacks.health.ok("Image rendering for `" .. lang .. "` is available")
|
||||
else
|
||||
Snacks.health.warn("Images rendering for `" .. lang .. "` is not available.\nMissing treesitter parser.")
|
||||
Snacks.health.error("Image rendering for `" .. lang .. "` is not available")
|
||||
end
|
||||
end
|
||||
|
||||
if env.supported then
|
||||
Snacks.health.ok("your terminal supports the kitty graphics protocol")
|
||||
elseif M.config.force then
|
||||
|
|
6
queries/css/images.scm
Normal file
6
queries/css/images.scm
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
(declaration
|
||||
(call_expression
|
||||
(function_name) @fn (#eq? @fn "url")
|
||||
(arguments [(plain_value) @image (string_value (string_content) @image)]))
|
||||
) @anchor
|
17
queries/html/images.scm
Normal file
17
queries/html/images.scm
Normal file
|
@ -0,0 +1,17 @@
|
|||
|
||||
(element
|
||||
(start_tag
|
||||
(tag_name) @tag (#eq? @tag "img")
|
||||
(attribute
|
||||
(attribute_name) @attr_name (#eq? @attr_name "src")
|
||||
(quoted_attribute_value (attribute_value) @image)
|
||||
)
|
||||
)
|
||||
) @anchor
|
||||
(self_closing_tag
|
||||
(tag_name) @tag (#eq? @tag "img")
|
||||
(attribute
|
||||
(attribute_name) @attr_name (#eq? @attr_name "src")
|
||||
(quoted_attribute_value (attribute_value) @image)
|
||||
)
|
||||
) @anchor
|
18
queries/javascript/images.scm
Normal file
18
queries/javascript/images.scm
Normal file
|
@ -0,0 +1,18 @@
|
|||
|
||||
(jsx_element
|
||||
(jsx_opening_element
|
||||
(identifier) @tag (#eq? @tag "img")
|
||||
(jsx_attribute
|
||||
(property_identifier) @attr_name (#eq? @attr_name "src")
|
||||
(string (string_fragment) @image)
|
||||
)
|
||||
)
|
||||
) @anchor
|
||||
|
||||
(jsx_self_closing_element
|
||||
(identifier) @tag (#eq? @tag "img")
|
||||
(jsx_attribute
|
||||
(property_identifier) @attr_name (#eq? @attr_name "src")
|
||||
(string (string_fragment) @image)
|
||||
)
|
||||
) @anchor
|
1
queries/markdown/images.scm
Normal file
1
queries/markdown/images.scm
Normal file
|
@ -0,0 +1 @@
|
|||
; extends
|
9
queries/markdown_inline/images.scm
Normal file
9
queries/markdown_inline/images.scm
Normal file
|
@ -0,0 +1,9 @@
|
|||
|
||||
(image
|
||||
[
|
||||
(link_destination) @image
|
||||
(image_description (shortcut_link (link_text) @image))
|
||||
(#gsub! @image "|.*" "") ; remove wikilink image options
|
||||
(#gsub! @image "^<" "") ; remove bracket link
|
||||
(#gsub! @image "^>" "")
|
||||
]) @anchor
|
5
queries/norg/images.scm
Normal file
5
queries/norg/images.scm
Normal file
|
@ -0,0 +1,5 @@
|
|||
|
||||
(infirm_tag
|
||||
(tag_name) @tag (#eq? @tag "image")
|
||||
(tag_parameters (tag_param) @image)
|
||||
) @anchor
|
11
queries/scss/images.scm
Normal file
11
queries/scss/images.scm
Normal file
|
@ -0,0 +1,11 @@
|
|||
(declaration
|
||||
(call_expression
|
||||
(function_name) @fn (#eq? @fn "url")
|
||||
(arguments [
|
||||
(plain_value) @image
|
||||
(string_value) @image
|
||||
; Remove quotes from the image URL
|
||||
(#gsub! @image "^['\"]" "")
|
||||
(#gsub! @image "['\"]$" "")
|
||||
]))
|
||||
) @anchor
|
18
queries/tsx/images.scm
Normal file
18
queries/tsx/images.scm
Normal file
|
@ -0,0 +1,18 @@
|
|||
|
||||
(jsx_element
|
||||
(jsx_opening_element
|
||||
(identifier) @tag (#eq? @tag "img")
|
||||
(jsx_attribute
|
||||
(property_identifier) @attr_name (#eq? @attr_name "src")
|
||||
(string (string_fragment) @image)
|
||||
)
|
||||
)
|
||||
) @anchor
|
||||
|
||||
(jsx_self_closing_element
|
||||
(identifier) @tag (#eq? @tag "img")
|
||||
(jsx_attribute
|
||||
(property_identifier) @attr_name (#eq? @attr_name "src")
|
||||
(string (string_fragment) @image)
|
||||
)
|
||||
) @anchor
|
2
queries/vue/images.scm
Normal file
2
queries/vue/images.scm
Normal file
|
@ -0,0 +1,2 @@
|
|||
; inherits: html
|
||||
; extends
|
4
tests/image/test.css
Normal file
4
tests/image/test.css
Normal file
|
@ -0,0 +1,4 @@
|
|||
.foo {
|
||||
background: lightblue url("./test.png") no-repeat fixed center;
|
||||
content: url(test.png);
|
||||
}
|
19
tests/image/test.html
Normal file
19
tests/image/test.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
<body>
|
||||
<a href="https://github.com/folke/lazy.nvim/releases/latest">
|
||||
<img
|
||||
alt="Latest release"
|
||||
src="https://img.shields.io/github/v/release/folke/lazy.nvim?style=for-the-badge&logo=starship&color=C9CBFF&logoColor=D9E0EE&labelColor=302D41&include_prerelease&sort=semver"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/folke/lazy.nvim/pulse">
|
||||
<img
|
||||
alt="Last commit"
|
||||
src="https://img.shields.io/github/last-commit/folke/lazy.nvim?style=for-the-badge&logo=starship&color=8bd5ca&logoColor=D9E0EE&labelColor=302D41"
|
||||
/>
|
||||
</a>
|
||||
</body>
|
||||
|
||||
<style>
|
||||
background: lightblue url("./test.png") no-repeat fixed center;
|
||||
content: url(test.png);
|
||||
</style>
|
BIN
tests/image/test.jpg
Normal file
BIN
tests/image/test.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 MiB |
10
tests/image/test.jsx
Normal file
10
tests/image/test.jsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
|
||||
export const Modal = (props) => {
|
||||
|
||||
return (
|
||||
<Show>
|
||||
<img src="test.png" />
|
||||
<img src="https://picsum.photos/200/300"></img>
|
||||
</Show>
|
||||
)
|
||||
}
|
25
tests/image/test.md
Normal file
25
tests/image/test.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
# test
|
||||
|
||||
## Wikilinks
|
||||
|
||||
!![[test.png]]
|
||||
|
||||
!![[test.png|options]]
|
||||
|
||||
## Injected HTML
|
||||
|
||||
<img src="test.png" alt="png" width="200" height="30" />
|
||||
|
||||
<a href="https://github.com/folke/lazy.nvim/releases/latest">
|
||||
<img alt="Latest release" src="https://img.shields.io/github/v/release/folke/lazy.nvim?style=for-the-badge&logo=starship&color=C9CBFF&logoColor=D9E0EE&labelColor=302D41&include_prerelease&sort=semver" />
|
||||
</a>
|
||||
|
||||
## Markdown Links
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
7
tests/image/test.norg
Normal file
7
tests/image/test.norg
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Test
|
||||
|
||||
- foo
|
||||
|
||||
.image ./test.png
|
||||
|
||||
.image test.png
|
BIN
tests/image/test.png
Normal file
BIN
tests/image/test.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.4 MiB |
5
tests/image/test.scss
Normal file
5
tests/image/test.scss
Normal file
|
@ -0,0 +1,5 @@
|
|||
.foo {
|
||||
background: lightblue url("./test.png") no-repeat fixed center;
|
||||
background: lightblue url("./test.png") no-repeat fixed center;
|
||||
content: url(test.png);
|
||||
}
|
9
tests/image/test.tsx
Normal file
9
tests/image/test.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
export const Modal = (props) => {
|
||||
|
||||
return (
|
||||
<Show>
|
||||
<img src="test.png" />
|
||||
<img src="https://picsum.photos/200/300"></img>
|
||||
</Show>
|
||||
)
|
||||
}
|
17
tests/image/test.vue
Normal file
17
tests/image/test.vue
Normal file
|
@ -0,0 +1,17 @@
|
|||
<script setup>
|
||||
import { ref } from "vue";
|
||||
const greeting = ref("Hello World!");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p class="greeting">{{ greeting }}</p>
|
||||
<img src="test.png" alt="test" />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.greeting {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
background: url("test.png");
|
||||
}
|
||||
</style>
|
Loading…
Add table
Add a link
Reference in a new issue