This commit is contained in:
Hadi
2026-05-21 13:56:31 +02:00
commit 2a0d03c258
4 changed files with 299 additions and 0 deletions
+155
View File
@@ -0,0 +1,155 @@
local M = {}
local _cache = nil
local function parse_transforms(help_output)
local transforms = {}
local in_commands = false
for line in help_output:gmatch('[^\n]+') do
if line:match('^Available Commands:') then
in_commands = true
elseif in_commands then
if not line:match('^%s') then
break
end
local cmd = line:match('^%s+(%S+)')
local desc = line:match('^%s+%S+%s+(.*%S)') or ''
if cmd and cmd ~= 'help' and cmd ~= 'completion' then
table.insert(transforms, { cmd = cmd, desc = desc })
end
end
end
return transforms
end
function M.get_transforms()
if _cache then return _cache end
local help = vim.fn.system('sttr --help 2>&1')
if not help:match('Available Commands') then
vim.notify('[sttr] sttr not found in PATH', vim.log.levels.ERROR)
return nil
end
_cache = parse_transforms(help)
return _cache
end
function M.clear_cache()
_cache = nil
end
-- Returns exclusive byte end of the char starting at byte `start_byte` (0-indexed) in `line`
local function char_exclusive_end(line, start_byte)
local ci = vim.fn.charidx(line, start_byte)
local next_byte = vim.fn.byteidx(line, ci + 1)
return next_byte >= 0 and next_byte or #line
end
local function get_visual_selection()
local mode = vim.fn.visualmode()
local start_pos = vim.fn.getpos("'<")
local end_pos = vim.fn.getpos("'>")
local sl = start_pos[2]
local el = end_pos[2]
-- getregion handles all visual modes correctly (requires Neovim 0.10+)
local selected_lines = vim.fn.getregion(start_pos, end_pos, { type = mode })
local text = table.concat(selected_lines, '\n')
if mode == 'V' then
return text, { mode = 'V', sl = sl, el = el }
elseif mode == 'v' then
local sc = start_pos[3] - 1 -- 0-indexed
local last_line = vim.api.nvim_buf_get_lines(0, el - 1, el, false)[1] or ''
local ec = char_exclusive_end(last_line, math.min(end_pos[3] - 1, #last_line))
return text, { mode = 'v', sl = sl, sc = sc, el = el, ec = ec }
else -- block visual '\22'
local sc_vcol = vim.fn.virtcol("'<")
local ec_vcol = vim.fn.virtcol("'>")
local block_data = {}
for lnum = sl, el do
local line = vim.api.nvim_buf_get_lines(0, lnum - 1, lnum, false)[1] or ''
local sc_v2c = vim.fn.virtcol2col(0, lnum, sc_vcol)
-- Line too short to reach the block start: empty selection at end of line
if sc_v2c <= 0 then
table.insert(block_data, { lnum = lnum, sc = #line, ec = #line })
else
local sc_byte = sc_v2c - 1
local ec_v2c = vim.fn.virtcol2col(0, lnum, ec_vcol)
local ec_byte = ec_v2c > 0
and char_exclusive_end(line, math.min(ec_v2c - 1, #line))
or #line
table.insert(block_data, { lnum = lnum, sc = sc_byte, ec = ec_byte })
end
end
return text, { mode = '\22', block_data = block_data, selected_lines = selected_lines }
end
end
local function apply(sel, result)
if sel.mode == 'V' then
vim.api.nvim_buf_set_lines(0, sel.sl - 1, sel.el, false,
vim.split(result, '\n', { plain = true }))
elseif sel.mode == 'v' then
vim.api.nvim_buf_set_text(0, sel.sl - 1, sel.sc, sel.el - 1, sel.ec,
vim.split(result, '\n', { plain = true }))
else -- block: result is a list of per-line strings
-- Reverse order so line numbers stay valid during replacement
for i = #sel.block_data, 1, -1 do
local bd = sel.block_data[i]
local line = vim.api.nvim_buf_get_lines(0, bd.lnum - 1, bd.lnum, false)[1] or ''
-- sc is 0-indexed inclusive start, ec is 0-indexed exclusive end
-- Lua string.sub is 1-indexed inclusive on both ends
local before = line:sub(1, bd.sc)
local after = line:sub(bd.ec + 1)
vim.api.nvim_buf_set_lines(0, bd.lnum - 1, bd.lnum, false,
{ before .. (result[i] or '') .. after })
end
end
end
function M.run()
local transforms = M.get_transforms()
if not transforms or #transforms == 0 then return end
local text, sel = get_visual_selection()
if not text then return end
require('sttr.picker').pick(transforms, function(item)
if not item then return end
if sel.mode == '\22' then
-- Block visual: transform each line independently
local result = {}
for _, bd in ipairs(sel.block_data) do
local line = vim.api.nvim_buf_get_lines(0, bd.lnum - 1, bd.lnum, false)[1] or ''
local portion = line:sub(bd.sc + 1, bd.ec)
local r = vim.fn.system({ 'sttr', item.cmd }, portion)
if vim.v.shell_error ~= 0 then
vim.notify('[sttr] Error: ' .. r, vim.log.levels.ERROR)
return
end
table.insert(result, r:gsub('\n$', ''))
end
apply(sel, result)
else
local result = vim.fn.system({ 'sttr', item.cmd }, text)
if vim.v.shell_error ~= 0 then
vim.notify('[sttr] Error: ' .. result, vim.log.levels.ERROR)
return
end
apply(sel, result:gsub('\n$', ''))
end
end)
end
return M