Skip to content

Commit ac275b0

Browse files
author
Alec Stewart
committed
feat(capture): Make sure tags are written as :tag: and not just plain text.
1 parent b3b58f7 commit ac275b0

2 files changed

Lines changed: 193 additions & 0 deletions

File tree

lua/orgmode/capture/template/init.lua

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,89 @@ local Calendar = require('orgmode.objects.calendar')
55
local Promise = require('orgmode.utils.promise')
66
local Input = require('orgmode.ui.input')
77

8+
---@description Make sure to deduplicate the tags grabbed by `get_target_tags` and `get_all_tags`
9+
---@param tags string[]
10+
---@return string[]
11+
local function uniq_tags(tags)
12+
local unique = {}
13+
for _, tag in ipairs(tags or {}) do
14+
if tag ~= '' then
15+
unique[tag] = true
16+
end
17+
end
18+
local list = vim.tbl_keys(unique)
19+
table.sort(list)
20+
return list
21+
end
22+
23+
---@description For `%^g` expansion in capture templates: gets all tags in the targeted file.
24+
---@param template? table
25+
---@return string[]
26+
local function get_target_tags(template)
27+
local org = require('orgmode')
28+
if not org.files or not template or template.target == '' then
29+
return {}
30+
end
31+
32+
local ok, file = pcall(function()
33+
return org.files:get(template:get_target())
34+
end)
35+
36+
if not ok or not file then
37+
return {}
38+
end
39+
40+
local tags = {}
41+
for _, tag in ipairs(file:get_filetags()) do
42+
table.insert(tags, tag)
43+
end
44+
for _, headline in ipairs(file:get_headlines()) do
45+
local own_tags = headline:get_own_tags()
46+
for _, tag in ipairs(own_tags) do
47+
table.insert(tags, tag)
48+
end
49+
end
50+
51+
return uniq_tags(tags)
52+
end
53+
54+
---@description For `%^G` expansion in capture templates: gets all tags in all agenda files.
55+
---@return string[]
56+
local function get_all_tags()
57+
local org = require('orgmode')
58+
if not org.files then
59+
return {}
60+
end
61+
return org.files:get_tags()
62+
end
63+
64+
---@param single boolean
65+
---@param tags_source string[]
66+
---@return OrgPromise<string>
67+
local function prompt_tags(single, tags_source)
68+
local completion = function(arg_lead)
69+
return vim.tbl_filter(function(tag)
70+
return tag:match('^' .. vim.pesc(arg_lead))
71+
end, tags_source)
72+
end
73+
local prompt = single and 'Tag: ' or 'Tags: '
74+
return Input.open(prompt, '', completion):next(function(input)
75+
if input == nil then
76+
return nil
77+
end
78+
if input == '' then
79+
return ''
80+
end
81+
82+
local tags = utils.parse_tags_string(input)
83+
if single then
84+
return tags[1] and utils.tags_to_string({ tags[1] }) or ''
85+
end
86+
87+
return utils.tags_to_string(tags)
88+
end)
89+
end
90+
891
local expansions = {
992
['%%f'] = function()
1093
return vim.fn.expand('%')
@@ -73,6 +156,12 @@ local expansions = {
73156
return date and date:to_wrapped_string(false) or nil
74157
end)
75158
end,
159+
['%%%^g'] = function(_, template)
160+
return prompt_tags(true, get_target_tags(template))
161+
end,
162+
['%%%^G'] = function()
163+
return prompt_tags(false, get_all_tags())
164+
end,
76165
['%%a'] = function()
77166
return string.format('[[file:%s::%s]]', utils.current_file_path(), vim.api.nvim_win_get_cursor(0)[1])
78167
end,

tests/plenary/capture/templates_spec.lua

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
local Template = require('orgmode.capture.template')
22
local Date = require('orgmode.objects.date')
3+
local helpers = require('tests.plenary.helpers')
4+
local Input = require('orgmode.ui.input')
5+
local Promise = require('orgmode.utils.promise')
36

47
describe('Capture template', function()
58
it('should compile expression', function()
@@ -87,4 +90,105 @@ describe('Capture template', function()
8790
end)
8891
assert.is.Nil(template:compile():wait())
8992
end)
93+
94+
it('should prompt for single tag with %^g', function()
95+
helpers.with_var(Input, 'open', function(_prompt, _default, _completion)
96+
return Promise.resolve('mytag')
97+
end, function()
98+
local template = Template:new({
99+
template = '* TODO %^g',
100+
})
101+
assert.are.same({ '* TODO :mytag:' }, template:compile():wait())
102+
end)
103+
end)
104+
105+
it('should prompt for multiple tags with %^G', function()
106+
helpers.with_var(Input, 'open', function(_prompt, _default, _completion)
107+
return Promise.resolve('tag1:tag2')
108+
end, function()
109+
local template = Template:new({
110+
template = '* TODO %^G',
111+
})
112+
assert.are.same({ '* TODO :tag1:tag2:' }, template:compile():wait())
113+
end)
114+
end)
115+
116+
it('should prompt for restricted tags with %^{tag1|tag2}G', function()
117+
helpers.with_var(Input, 'open', function(_prompt, _default, _completion)
118+
return Promise.resolve('tag1')
119+
end, function()
120+
local template = Template:new({
121+
template = '* TODO %^{tag1|tag2}G',
122+
})
123+
assert.are.same({ '* TODO :tag1:' }, template:compile():wait())
124+
end)
125+
end)
126+
127+
it('should not cancel capture when %^g input is empty', function()
128+
helpers.with_var(Input, 'open', function(_prompt, _default, _completion)
129+
return Promise.resolve('')
130+
end, function()
131+
local template = Template:new({
132+
template = '* TODO %^g',
133+
})
134+
assert.are.same({ '* TODO ' }, template:compile():wait())
135+
end)
136+
end)
137+
138+
it('should complete %^g from target file tags only', function()
139+
local files = helpers.create_agenda_files({
140+
{
141+
filename = 'target.org',
142+
content = {
143+
'#+FILETAGS: :target_file:',
144+
'* TODO target item :target_headline:',
145+
},
146+
},
147+
{
148+
filename = 'other.org',
149+
content = {
150+
'* TODO other item :other_headline:',
151+
},
152+
},
153+
})
154+
155+
helpers.with_var(Input, 'open', function(_prompt, _default, completion)
156+
assert.are.same({ 'target_file', 'target_headline' }, completion(''))
157+
return Promise.resolve('target_headline')
158+
end, function()
159+
local template = Template:new({
160+
template = '* TODO %^g',
161+
target = files['target.org'],
162+
})
163+
assert.are.same({ '* TODO :target_headline:' }, template:compile():wait())
164+
end)
165+
end)
166+
167+
it('should complete %^G from all loaded agenda file tags', function()
168+
helpers.create_agenda_files({
169+
{
170+
filename = 'target.org',
171+
content = {
172+
'#+FILETAGS: :target_file:',
173+
'* TODO target item :target_headline:',
174+
},
175+
},
176+
{
177+
filename = 'other.org',
178+
content = {
179+
'* TODO other item :other_headline:',
180+
},
181+
},
182+
})
183+
184+
helpers.with_var(Input, 'open', function(_prompt, _default, completion)
185+
assert.are.same({ 'other_headline', 'target_file', 'target_headline' }, completion(''))
186+
return Promise.resolve('target_headline:other_headline')
187+
end, function()
188+
local template = Template:new({
189+
template = '* TODO %^G',
190+
})
191+
assert.are.same({ '* TODO :target_headline:other_headline:' }, template:compile():wait())
192+
end)
193+
end)
90194
end)

0 commit comments

Comments
 (0)