Skip to content

Commit

Permalink
feat(interactive): live_colors() for internal development
Browse files Browse the repository at this point in the history
Add a new, experimental function `live_colors()` to the `interactive`
module. It displays colors live, inline in the current buffer and
refreshes on every write. Works with palette files and module files at
the moment. Module files display using the current theme/colorscheme.
This can be used to assist with internal development. Simply run
`=require('github-theme.interactive').live_colors(true)` from the
cmdline to try it out. Pass `false` to disable (or restart neovim).
  • Loading branch information
tmillr committed Aug 3, 2024
1 parent 4f03e4e commit bbab6a4
Show file tree
Hide file tree
Showing 5 changed files with 324 additions and 25 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ indent_style = tab
indent_style = space
indent_size = 2

[*.lua]
[*.{lua,md}]
max_line_length = 90

[*.{yml,yaml}]
Expand Down
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/CHANGELOG
/CHANGELOG.md
*.lua
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic
### What's New?

- Added static/class function `Color.is_Color()` for detecting Color instances
- Added `live_colors()` to the `interactive` module to assist internal development (debugging, color picking)

### Highlight Improvements

- **JSON:** keys
- **Lua:** keys (in table literals and type annotations)

### Changes

Expand Down
2 changes: 1 addition & 1 deletion lua/github-theme/group/modules/treesitter.lua
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ If you want to stay on nvim 0.7, disable the module, or track on 'v0.0.x' branch

--- Identifiers --------------------------------------------------------------------------

['@variable'] = { fg = syn.variable, style = stl.variables }, -- Any variable name that does not have another highlighC.
['@variable'] = { fg = syn.variable, style = stl.variables }, -- Any variable name that does not have another highlight.
['@variable.builtin'] = { fg = syn.builtin0, style = stl.variables }, -- Var names defined by the language: this, self, super
['@variable.member'] = { fg = syn.field }, -- For fields
['@variable.parameter'] = { fg = syn.param, style = stl.variables }, -- For parameters of a function
Expand Down
336 changes: 313 additions & 23 deletions lua/github-theme/interactive.lua
Original file line number Diff line number Diff line change
@@ -1,38 +1,328 @@
local util = require('github-theme.util')
local api = vim.api
local cmd = util.is_nvim and vim.cmd or vim.command
local fmt = string.format

local M = {}

local function get_filetype()
return vim['bo'] and vim.bo.filetype or vim.eval('&filetype')
-- TODO: move to util module?
---@param tbl table the target search in
---@param keypath string[]|string
---@return any? value # value in `tbl` at `keypath`, or `nil` if unsuccessful
---@return any? last # the last/deepest value reached if unsuccessful, otherwise `nil`
local function keypath_get(tbl, keypath)
if type(keypath) == 'string' then
keypath = vim.split(keypath, '.', { plain = true, trimempty = true })
end

local current = tbl
for _, k in ipairs(keypath) do
if type(current) ~= 'table' then
return nil, current
end

current = current[k]
end

return current
end

---@return integer id autocmd id
function M.attach()
vim.g.github_theme_debug = true
cmd([[
augroup GithubThemeInteractiveAugroup
autocmd!
autocmd BufWritePost <buffer> lua require("github-theme.interactive").execute()
augroup END
]])
end

function M.execute()
local source_method = get_filetype() == 'lua' and 'luafile' or 'source'
local name = vim['g'] and vim.g.colors_name or vim.eval('g:colors_name')
return api.nvim_create_autocmd('BufWritePost', {
group = api.nvim_create_augroup('github-theme.interactive', { clear = true }),
buffer = 0,
desc = 'Reloads user config when the buffer is written',
nested = true,
callback = require('github-theme.interactive').execute(),
})
end

function M.execute(info)
require('github-theme.config').reset()
require('github-theme.override').reset()
cmd(fmt(
[[
%s %%
colorscheme %s
doautoall ColorScheme
]],
source_method,
name
))
cmd(([[
%s %%
colorscheme %s
]]):format(vim.bo[info.buf].ft == 'lua' and 'luafile' or 'source', vim.g.colors_name))
end

---**EXPERIMENTAL**
---
---For internal development. Displays colors live, inline in the current buffer and
---refreshes on write. Works with palette files and module files at the moment, although
---there may be some bugs. Module files display in/with the current theme/colorscheme.
---
---TODO: support other files as well, expose via cmd?, etc.
---@param enable? boolean
function M.live_colors(enable)
local ts = vim.treesitter
local ns = api.nvim_create_namespace('github-theme.interactive')
local augroup = api.nvim_create_augroup('github-theme.interactive', { clear = true })

local function get_nodes(node, src, typ, cb)
local stack = {}

-- {{{ Queries

local q1 = ts.query.parse(
'lua',
[[
(assignment_statement
.
(variable_list) @k (#vim-match? @k "\\m^\\%(palette\\|pal\\|spec\\)\\%(\\.\\w\\+\\)*$")
.
"="
.
(expression_list
(table_constructor) @defs
)
)
]]
)
local q2 = ts.query.parse(
'lua',
[[
(field
name: [
(_ content: (_) @k)
(_ !content) @k
]
value: (_) @v
)
(table_constructor
"}" @tbl_end
.
)
]]
)

local modquery = ts.query.parse(
'lua',
[[
(function_declaration
name: (_) @fname (#match? @fname "^M[.:]get$")
body: (_
(return_statement
(expression_list
(table_constructor
(field
name: [
(_ content: (_) @k)
(_ !content) @k
]
value: (table_constructor) @v
)
)
)
)
)
)
]]
)

-- }}}

-- Map capture names to their ID
local cap1, cap2, cap3 = {}, {}, {}
for _, v in ipairs({
{ q1.captures, cap1 },
{ q2.captures, cap2 },
{ modquery.captures, cap3 },
}) do
for id, name in pairs(v[1]) do
v[2][name] = id
end
end

local function matched_node(match, cap_id)
vim.validate({
match = { match, { 'table', 'userdata' } },
cap_id = { cap_id, 'number' },
})

local nodes = match[cap_id]
if nodes then
assert(#nodes == 1)
return nodes[1]
end
end

---@param node TSNode
---@diagnostic disable-next-line: redefined-local
local function inner(node)
for _pat, match, meta in q2:iter_matches(node, src, nil, nil, { all = true }) do
if matched_node(match, cap2.tbl_end) then
table.remove(stack)
else
local knode = assert(matched_node(match, cap2.k))
local knode_text = ts.get_node_text(knode, src, { metadata = meta[cap2.k] })
local vnode = assert(matched_node(match, cap2.v))

if vnode:type() == 'table_constructor' then
table.insert(stack, knode_text)
else
cb({
knode = knode,
vnode = vnode,
keypath = table.concat(stack, '.') .. '.' .. knode_text,
})
end
end
end
end

if typ == 'mod' then
for _pat, match, meta in modquery:iter_matches(node, src, nil, nil, { all = true }) do
local knode = assert(matched_node(match, cap3.k))
local vnode = assert(matched_node(match, cap3.v))
cb({
knode = knode,
vnode = vnode,
keypath = ts.get_node_text(knode, src, { metadata = meta[cap3.k] }),
})
end
else
for _pat, match, meta in q1:iter_matches(node, src, nil, nil, { all = true }) do
---@diagnostic disable-next-line: redefined-local
local node = assert(matched_node(match, cap1.defs))
local k = matched_node(match, cap1.k)
and ts.get_node_text(
matched_node(match, cap1.k),
src,
{ metadata = meta[cap1.k] }
)
or ''

for part in k:gmatch('[^.]+') do
table.insert(stack, part)
end

inner(node)

for _ in k:gmatch('[^.]+') do
table.remove(stack)
end
end
end
end

local function disp_colors(buf, nodes, colors, typ, theme)
for _, v in ipairs(nodes) do
local lnum, col = v.vnode:end_()
local color = typ == 'mod' and '' or keypath_get(colors, v.keypath)

if type(color) == 'string' then
local def, grp

if typ == 'mod' then
grp, def = 'github.' .. v.keypath, vim.deepcopy(colors[v.keypath], true)
if (def.style or 'NONE') ~= 'NONE' then
for s in def.style:gmatch('[^,]+') do
def[s] = true
end
end
def.style = nil
else
if v.keypath:find('bg') or v.keypath:find('sel%d+$') then
def, grp = { bg = color }, theme .. v.keypath .. '-bg'
else
def, grp = { fg = color }, theme .. v.keypath .. '-fg'
end
end

def.force = true
api.nvim_set_hl(0, grp, def)
api.nvim_buf_set_extmark(buf, ns, lnum, 0, {
end_row = lnum,
end_col = 0,
strict = false,
undo_restore = true,
invalidate = true,
spell = false,
right_gravity = true,
end_right_gravity = true,
hl_mode = 'combine',
virt_text_pos = 'inline',
virt_text = { { ' ' }, { ' Example ', grp } },
})
end
end
end

local function refresh(opts)
opts = opts or {}
local buf = opts.buf or 0
local fname = api.nvim_buf_get_name(buf)
local reqname = fname:gsub('%.lua$', ''):gsub('[/\\]', '.'):gsub('^.-%.lua%.', '')
local typ = reqname:find([[^github[-_]*theme%.palette%.github[^.]+$]]) and 'pal'
or (reqname:find([[^github[-_]*theme%.group%.modules%.[^.]+$]]) and 'mod')

if not typ then
return
end

api.nvim_buf_clear_namespace(buf, ns, 0, -1)
local env

do
local reloaded = {}
env = setmetatable({
require = function(...)
if not reloaded[...] then
reloaded[...] = ...
package.loaded[...] = nil
end
return _G.require(...)
end,
}, { __index = _G })

setfenv(env.require, env)
end

-- Be quiet about syntax errors and just return
local ok, mod = pcall(env.require, reqname)

if ok then
local defs, theme
if typ == 'pal' then
theme = mod.meta.name
defs = { spec = mod.generate_spec(mod.palette), palette = mod.palette }
else
theme = vim.g.colors_name
local pal = env.require('github-theme.palette.' .. theme)
local spec = pal.generate_spec(pal.palette)
pal.palette.meta, spec.palette = pal.meta, pal.palette
defs = mod.get(spec, require('github-theme.config').options, { enable = true })
end

local nodes, parser = {}, ts.get_parser(buf)
get_nodes(parser:parse(false)[1]:root(), buf, typ, function(node_info)
table.insert(nodes, node_info)
end)

disp_colors(buf, nodes, defs, typ, theme)
end
end

if enable == false then
for _, buf in ipairs(api.nvim_list_bufs()) do
api.nvim_buf_clear_namespace(buf, ns, 0, -1)
end

return
end

api.nvim_create_autocmd({ 'ColorScheme', 'BufNew', 'BufWritePost' }, {
group = augroup,
pattern = '*.lua',
desc = 'Refresh live-displayed colors',
nested = true,
callback = vim.schedule_wrap(refresh),
})

refresh()
end

return M

0 comments on commit bbab6a4

Please sign in to comment.