Skip to content

Commit a06784e

Browse files
committed
feat: command macro support
- macro for target filetype (issue: #114)
1 parent f18f78d commit a06784e

File tree

5 files changed

+312
-1
lines changed

5 files changed

+312
-1
lines changed

lua/gp/init.lua

+41-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
--------------------------------------------------------------------------------
77
local config = require("gp.config")
88

9+
local uv = vim.uv or vim.loop
10+
table.unpack = table.unpack or unpack -- 5.1 compatibility
11+
912
local M = {
1013
_Name = "Gp", -- plugin name
1114
_state = {}, -- table of state variables
@@ -24,6 +27,7 @@ local M = {
2427
tasker = require("gp.tasker"), -- tasker module
2528
vault = require("gp.vault"), -- handles secrets
2629
whisper = require("gp.whisper"), -- whisper module
30+
macro = require("gp.macro"), -- builder for macro completion
2731
}
2832

2933
--------------------------------------------------------------------------------
@@ -189,12 +193,41 @@ M.setup = function(opts)
189193
end)
190194
end
191195

196+
M.logger.debug("hook setup done")
197+
198+
local ft_completion = M.macro.build_completion({
199+
require("gp.macros.target_filetype"),
200+
}, {})
201+
202+
M.logger.debug("ft_completion done")
203+
204+
local do_completion = M.macro.build_completion({
205+
require("gp.macros.target"),
206+
require("gp.macros.target_filetype"),
207+
require("gp.macros.target_filename"),
208+
}, {})
209+
210+
M.logger.debug("do_completion done")
211+
212+
M.command_parser = M.macro.build_parser({
213+
require("gp.macros.target"),
214+
require("gp.macros.target_filetype"),
215+
require("gp.macros.target_filename"),
216+
})
217+
218+
M.logger.debug("command_parser done")
219+
192220
local completions = {
193221
ChatNew = { "popup", "split", "vsplit", "tabnew" },
194222
ChatPaste = { "popup", "split", "vsplit", "tabnew" },
195223
ChatToggle = { "popup", "split", "vsplit", "tabnew" },
196224
Context = { "popup", "split", "vsplit", "tabnew" },
197225
Agent = agent_completion,
226+
Do = do_completion,
227+
Enew = ft_completion,
228+
New = ft_completion,
229+
Vnew = ft_completion,
230+
Tabnew = ft_completion,
198231
}
199232

200233
-- register default commands
@@ -1653,6 +1686,13 @@ M.Prompt = function(params, target, agent, template, prompt, whisper, callback)
16531686
local filetype = M.helpers.get_filetype(buf)
16541687
local filename = vim.api.nvim_buf_get_name(buf)
16551688

1689+
local state = {}
1690+
local response = M.command_parser(command, {}, state)
1691+
if response then
1692+
command = M.render.template(response.template, response.artifacts)
1693+
state = response.state
1694+
end
1695+
16561696
local sys_prompt = M.render.prompt_template(agent.system_prompt, command, selection, filetype, filename)
16571697
sys_prompt = sys_prompt or ""
16581698
table.insert(messages, { role = "system", content = sys_prompt })
@@ -1749,7 +1789,7 @@ M.Prompt = function(params, target, agent, template, prompt, whisper, callback)
17491789
end,
17501790
})
17511791

1752-
local ft = target.filetype or filetype
1792+
local ft = state.target_filetype or target.filetype or filetype
17531793
vim.api.nvim_set_option_value("filetype", ft, { buf = buf })
17541794

17551795
handler = M.dispatcher.create_handler(buf, win, 0, false, "", cursor)

lua/gp/macro.lua

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
local logger = require("gp.logger")
2+
3+
---@class gp.Macro_cmd_params
4+
---@field arg_lead string
5+
---@field cmd_line string
6+
---@field cursor_pos number
7+
---@field cropped_line string
8+
9+
---@class gp.Macro_parser_result
10+
---@field template string
11+
---@field artifacts table<string, string>
12+
---@field state table
13+
14+
--- gp.Macro Interface
15+
-- @field name string: Name of the macro.
16+
-- @field description string: Description of the macro.
17+
-- @field default string: Default value for the macro (optional).
18+
-- @field max_occurrences number: Maximum number of occurrences for the macro (optional).
19+
-- @field triggered function: Function that determines if the macro is triggered.
20+
-- @field completion function: Function that provides completion options.
21+
-- @field parser function: Function that processes the macro in the template.
22+
23+
---@class gp.Macro
24+
---@field name string
25+
---@field description string
26+
---@field default? string
27+
---@field max_occurrences? number
28+
---@field triggered fun(params: gp.Macro_cmd_params, state: table): boolean
29+
---@field completion fun(params: gp.Macro_cmd_params, state: table): string[]
30+
---@field parser fun(params: gp.Macro_parser_result): gp.Macro_parser_result
31+
32+
---@param value string # string to hash
33+
---@return string # returns hash of the string
34+
local fnv1a_hash = function(value)
35+
---@type number
36+
local hash = 2166136261
37+
for i = 1, #value do
38+
hash = vim.fn.xor(hash, string.byte(value, i))
39+
hash = vim.fn["and"]((hash * 16777619), 0xFFFFFFFF)
40+
end
41+
return string.format("%08x", hash) -- return as an 8-character hex string
42+
end
43+
44+
local M = {}
45+
46+
---@param prefix string # prefix for the placeholder
47+
---@param value string # value to hash
48+
---@return string # returns placeholder
49+
M.generate_placeholder = function(prefix, value)
50+
local hash_value = fnv1a_hash(value)
51+
local placeholder = "{{" .. prefix .. "." .. hash_value .. "}}"
52+
return placeholder
53+
end
54+
55+
---@param macros gp.Macro[]
56+
---@return fun(template: string, artifacts: table, state: table): gp.Macro_parser_result
57+
M.build_parser = function(macros)
58+
---@param template string
59+
---@param artifacts table
60+
---@param state table
61+
---@return {template: string, artifacts: table, state: table}
62+
local function parser(template, artifacts, state)
63+
template = template or ""
64+
---@type gp.Macro_parser_result
65+
local result = {
66+
template = " " .. template .. " ",
67+
artifacts = artifacts or {},
68+
state = state or {},
69+
}
70+
logger.debug("macro parser input: " .. vim.inspect(result))
71+
72+
for _, macro in pairs(macros) do
73+
logger.debug("macro parser current macro: " .. vim.inspect(macro))
74+
result = macro.parser(result)
75+
logger.debug("macro parser result: " .. vim.inspect(result))
76+
end
77+
return result
78+
end
79+
80+
return parser
81+
end
82+
83+
---@param macros gp.Macro[]
84+
---@param state table
85+
---@return fun(arg_lead: string, cmd_line: string, cursor_pos: number): string[]
86+
M.build_completion = function(macros, state)
87+
---@type table<string, gp.Macro>
88+
local map = {}
89+
for _, macro in pairs(macros) do
90+
map[macro.name] = macro
91+
state[macro.name .. "_default"] = macro.default
92+
end
93+
94+
---@param arg_lead string
95+
---@param cmd_line string
96+
---@param cursor_pos number
97+
---@return string[]
98+
local function completion(arg_lead, cmd_line, cursor_pos)
99+
local cropped_line = cmd_line:sub(1, cursor_pos)
100+
101+
---@type gp.Macro_cmd_params
102+
local params = {
103+
arg_lead = arg_lead,
104+
cmd_line = cmd_line,
105+
cursor_pos = cursor_pos,
106+
cropped_line = cropped_line,
107+
}
108+
109+
local suggestions = {}
110+
111+
logger.debug("macro completion input: " .. vim.inspect({
112+
params = params,
113+
state = state,
114+
}))
115+
116+
---@type table<string, number>
117+
local candidates = {}
118+
local cand = nil
119+
for c in cropped_line:gmatch("%s@(%S+)%s") do
120+
candidates[c] = candidates[c] and candidates[c] + 1 or 1
121+
cand = c
122+
end
123+
logger.debug("macro completion candidates: " .. vim.inspect(candidates))
124+
125+
if cand and map[cand] and map[cand].triggered(params, state) then
126+
suggestions = map[cand].completion(params, state)
127+
elseif cropped_line:match("%s$") or cropped_line:match("%s@%S*$") then
128+
for _, c in pairs(macros) do
129+
if not candidates[c.name] or candidates[c.name] < c.max_occurrences then
130+
table.insert(suggestions, "@" .. c.name)
131+
end
132+
end
133+
end
134+
135+
logger.debug("macro completion suggestions: " .. vim.inspect(suggestions))
136+
return vim.deepcopy(suggestions)
137+
end
138+
139+
return completion
140+
end
141+
142+
return M

lua/gp/macros/target.lua

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
local macro = require("gp.macro")
2+
3+
local values = {
4+
"rewrite",
5+
"append",
6+
"prepend",
7+
"popup",
8+
"enew",
9+
"new",
10+
"vnew",
11+
"tabnew",
12+
}
13+
14+
local M = {}
15+
16+
---@type gp.Macro
17+
M = {
18+
name = "target",
19+
description = "handles target for commands",
20+
default = "rewrite",
21+
max_occurrences = 1,
22+
23+
triggered = function(params, state)
24+
local cropped_line = params.cropped_line
25+
return cropped_line:match("@target%s+%S*$")
26+
end,
27+
28+
completion = function(params, state)
29+
return values
30+
end,
31+
32+
parser = function(result)
33+
local template = result.template
34+
local s, e, value = template:find("@target%s+(%S+)")
35+
if not value then
36+
return result
37+
end
38+
39+
local placeholder = macro.generate_placeholder(M.name, value)
40+
result.template = template:sub(1, s - 2) .. placeholder .. template:sub(e + 1)
41+
result.state[M.name] = value
42+
result.artifacts[placeholder] = ""
43+
return result
44+
end,
45+
}
46+
47+
return M

lua/gp/macros/target_filename.lua

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
local macro = require("gp.macro")
2+
3+
local M = {}
4+
5+
---@type gp.Macro
6+
M = {
7+
name = "target_filename`",
8+
description = "handles target buffer filename for commands",
9+
default = nil,
10+
max_occurrences = 1,
11+
12+
triggered = function(params, state)
13+
local cropped_line = params.cropped_line
14+
return cropped_line:match("@target_filename`[^`]*$")
15+
end,
16+
17+
completion = function(params, state)
18+
-- TODO state.root_dir ?
19+
local files = vim.fn.glob("**", true, true)
20+
-- local files = vim.fn.getcompletion("", "file")
21+
files = vim.tbl_map(function(file)
22+
return file .. " `"
23+
end, files)
24+
return files
25+
end,
26+
27+
parser = function(result)
28+
local template = result.template
29+
local s, e, value = template:find("@target_filename`([^`]*)`")
30+
if not value then
31+
return result
32+
end
33+
34+
value = value:match("^%s*(.-)%s*$")
35+
local placeholder = macro.generate_placeholder(M.name, value)
36+
37+
result.template = template:sub(1, s - 2) .. placeholder .. template:sub(e + 1)
38+
result.state[M.name] = value
39+
result.artifacts[placeholder] = ""
40+
return result
41+
end,
42+
}
43+
44+
return M

lua/gp/macros/target_filetype.lua

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
local macro = require("gp.macro")
2+
3+
local values = vim.fn.getcompletion("", "filetype")
4+
5+
local M = {}
6+
7+
---@type gp.Macro
8+
M = {
9+
name = "target_filetype",
10+
description = "handles target buffer filetype for commands like GpEnew",
11+
default = "markdown",
12+
max_occurrences = 1,
13+
14+
triggered = function(params, state)
15+
local cropped_line = params.cropped_line
16+
return cropped_line:match("@target_filetype%s+%S*$")
17+
end,
18+
19+
completion = function(params, state)
20+
return values
21+
end,
22+
23+
parser = function(result)
24+
local template = result.template
25+
local s, e, value = template:find("@target_filetype%s+(%S+)")
26+
if not value then
27+
return result
28+
end
29+
30+
local placeholder = macro.generate_placeholder(M.name, value)
31+
result.template = template:sub(1, s - 2) .. placeholder .. template:sub(e + 1)
32+
result.state[M.name] = value
33+
result.artifacts[placeholder] = ""
34+
return result
35+
end,
36+
}
37+
38+
return M

0 commit comments

Comments
 (0)