Skip to content

Commit 2e4389a

Browse files
feat(properties): Allow removing properties
1 parent 974663f commit 2e4389a

File tree

4 files changed

+237
-16
lines changed

4 files changed

+237
-16
lines changed

lua/orgmode/api/headline.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,9 @@ function OrgHeadline:set_scheduled(date)
217217
end)
218218
end
219219

220-
--- Set property on a headline
220+
--- Set property on a headline. Setting value to nil removes the property
221221
---@param key string
222-
---@param value string
222+
---@param value? string
223223
function OrgHeadline:set_property(key, value)
224224
return self:_do_action(function()
225225
local headline = org.files:get_closest_headline()

lua/orgmode/files/file.lua

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ local ts = vim.treesitter
66
local config = require('orgmode.config')
77
local Block = require('orgmode.files.elements.block')
88
local Link = require('orgmode.org.hyperlinks.link')
9+
local Range = require('orgmode.files.elements.range')
910

1011
---@class OrgFileMetadata
1112
---@field mtime number
@@ -431,7 +432,7 @@ function OrgFile:set_node_text(node, text, front_trim)
431432
end_row = last_line
432433
end_col = vim.fn.col({ end_row, '$' }) - 2
433434
end
434-
local ok = pcall(vim.api.nvim_buf_set_text, 0, start_row, start_col, end_row, end_col, replacement)
435+
local ok = pcall(vim.api.nvim_buf_set_text, bufnr, start_row, start_col, end_row, end_col, replacement)
435436
return ok
436437
end
437438

@@ -543,36 +544,89 @@ function OrgFile:get_drawer(name)
543544
return false
544545
end)
545546

546-
if not drawer or #drawer:field('contents') == 0 then
547+
if not drawer then
547548
return nil
548549
end
549550

550551
return drawer
551552
end
552553

553554
memoize('get_properties')
554-
---@return table<string, string>
555+
---@return table<string, string>, table<string, OrgRange>, TSNode | nil
555556
function OrgFile:get_properties()
556557
local property_drawer = self:get_drawer('properties')
557558
if not property_drawer then
558-
return {}
559+
return {}, {}, nil
559560
end
560561
local properties = {}
561-
local contents = self:get_node_text_list(property_drawer:field('contents')[1])
562-
for _, line in ipairs(contents) do
562+
local properties_ranges = {}
563+
local contents_node = property_drawer:field('contents')[1]
564+
local contents = self:get_node_text_list(contents_node)
565+
local start_line = contents_node and contents_node:start() or 0
566+
for i, line in ipairs(contents) do
563567
local property_name, property_value = line:match('^%s*:([^:]-):%s*(.*)$')
564568
if property_name and property_value then
565569
properties[property_name:lower()] = property_value
570+
properties_ranges[property_name:lower()] = Range.from_line(start_line + i)
566571
end
567572
end
568-
return properties
573+
return properties, properties_ranges, property_drawer
569574
end
570575

571576
memoize('get_property')
572-
---@return string | nil
577+
---@return string | nil, OrgRange
573578
function OrgFile:get_property(name)
574-
local property_drawer = self:get_properties()
575-
return property_drawer[name:lower()]
579+
local property_drawer, properties_ranges = self:get_properties()
580+
return property_drawer[name:lower()], properties_ranges[name:lower()]
581+
end
582+
583+
---@param name string
584+
---@param value? string
585+
---@return OrgFile
586+
function OrgFile:set_property(name, value)
587+
local bufnr = self:bufnr()
588+
if bufnr < 0 then
589+
return self
590+
end
591+
592+
if not value then
593+
local existing_property, property_range = self:get_property(name)
594+
if existing_property and property_range then
595+
vim.fn.deletebufline(bufnr, property_range.start_line)
596+
end
597+
self:parse()
598+
local properties, _, properties_drawer = self:get_properties()
599+
if vim.tbl_isempty(properties) then
600+
self:set_node_lines(properties_drawer, {})
601+
self:parse()
602+
end
603+
return self
604+
end
605+
606+
local _, _, properties_drawer = self:get_properties()
607+
local property = (':%s: %s'):format(name, value)
608+
609+
if not properties_drawer then
610+
vim.api.nvim_buf_set_lines(bufnr, 0, 0, false, {
611+
':PROPERTIES:',
612+
property,
613+
':END:',
614+
})
615+
self:parse()
616+
return self
617+
end
618+
619+
local existing_property, property_range = self:get_property(name)
620+
if existing_property then
621+
vim.api.nvim_buf_set_lines(bufnr, property_range.start_line - 1, property_range.start_line, false, { property })
622+
self:parse()
623+
return self
624+
end
625+
626+
local property_end = properties_drawer and properties_drawer:end_()
627+
vim.api.nvim_buf_set_lines(bufnr, property_end - 1, property_end - 1, false, { property })
628+
self:parse()
629+
return self
576630
end
577631

578632
memoize('get_category')

lua/orgmode/files/headline.lua

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -376,8 +376,8 @@ function Headline:get_properties()
376376
local name = node:field('name')[1]
377377
local value = node:field('value')[1]
378378

379-
if name and value then
380-
properties[self.file:get_node_text(name):lower()] = self.file:get_node_text(value)
379+
if name then
380+
properties[self.file:get_node_text(name):lower()] = self.file:get_node_text(value) or ''
381381
end
382382
end
383383
end
@@ -386,8 +386,22 @@ function Headline:get_properties()
386386
end
387387

388388
---@param name string
389-
---@param value string
389+
---@param value? string
390+
---@return OrgHeadline
390391
function Headline:set_property(name, value)
392+
if not value then
393+
local existing_property, property_node = self:get_property(name)
394+
if existing_property and property_node then
395+
vim.fn.deletebufline(vim.api.nvim_get_current_buf(), property_node:start() + 1)
396+
end
397+
self:refresh()
398+
local properties_node, properties = self:get_properties()
399+
if vim.tbl_isempty(properties) then
400+
self:_set_node_lines(properties_node, {})
401+
end
402+
return self:refresh()
403+
end
404+
391405
local properties = self:get_properties()
392406
if not properties then
393407
local append_line = self:get_append_line()
@@ -418,7 +432,7 @@ function Headline:get_property(property_name, search_parents)
418432
local name = node:field('name')[1]
419433
local value = node:field('value')[1]
420434
if name and self.file:get_node_text(name):lower() == property_name:lower() then
421-
return value and self.file:get_node_text(value), node
435+
return value and self.file:get_node_text(value) or '', node
422436
end
423437
end
424438
end

tests/plenary/org/org_spec.lua

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,115 @@ local Date = require('orgmode.objects.date')
33
local org = require('orgmode')
44

55
describe('Org file', function()
6+
it('should properly add new properties to a file', function()
7+
helpers.create_file({
8+
'* TODO Test orgmode :WORK:',
9+
'DEADLINE: <2021-05-10 11:00 +1w>',
10+
'* TODO Another todo',
11+
})
12+
13+
local file = org.files:get_current_file()
14+
file:set_property('CATEGORY', 'testing')
15+
16+
assert.are.same({
17+
':PROPERTIES:',
18+
':CATEGORY: testing',
19+
':END:',
20+
'* TODO Test orgmode :WORK:',
21+
'DEADLINE: <2021-05-10 11:00 +1w>',
22+
'* TODO Another todo',
23+
}, vim.api.nvim_buf_get_lines(0, 0, 8, false))
24+
end)
25+
26+
it('should append to existing file properties', function()
27+
helpers.create_file({
28+
':PROPERTIES:',
29+
':CATEGORY: Testing',
30+
':END:',
31+
'* TODO Test orgmode :WORK:',
32+
'DEADLINE: <2021-05-10 11:00 +1w>',
33+
'* TODO Another todo',
34+
})
35+
local file = org.files:get_current_file()
36+
file:set_property('CUSTOM_ID', '1')
37+
38+
assert.are.same({
39+
':PROPERTIES:',
40+
':CATEGORY: Testing',
41+
':CUSTOM_ID: 1',
42+
':END:',
43+
'* TODO Test orgmode :WORK:',
44+
'DEADLINE: <2021-05-10 11:00 +1w>',
45+
'* TODO Another todo',
46+
}, vim.api.nvim_buf_get_lines(0, 0, 8, false))
47+
end)
48+
--
49+
it('should update existing file property', function()
50+
helpers.create_file({
51+
':PROPERTIES:',
52+
':CATEGORY: Testing',
53+
':CUSTOM_ID: 1',
54+
':END:',
55+
'* TODO Test orgmode :WORK:',
56+
'DEADLINE: <2021-05-10 11:00 +1w>',
57+
'* TODO Another todo',
58+
})
59+
local file = org.files:get_current_file()
60+
file:set_property('CATEGORY', 'Updated')
61+
62+
assert.are.same({
63+
':PROPERTIES:',
64+
':CATEGORY: Updated',
65+
':CUSTOM_ID: 1',
66+
':END:',
67+
'* TODO Test orgmode :WORK:',
68+
'DEADLINE: <2021-05-10 11:00 +1w>',
69+
'* TODO Another todo',
70+
}, vim.api.nvim_buf_get_lines(0, 0, 8, false))
71+
end)
72+
73+
it('should remove existing file property', function()
74+
helpers.create_file({
75+
':PROPERTIES:',
76+
':CATEGORY: Testing',
77+
':CUSTOM_ID: 1',
78+
':END:',
79+
'* TODO Test orgmode :WORK:',
80+
'DEADLINE: <2021-05-10 11:00 +1w>',
81+
'* TODO Another todo',
82+
})
83+
local file = org.files:get_current_file()
84+
file:set_property('CATEGORY', nil)
85+
86+
assert.are.same({
87+
':PROPERTIES:',
88+
':CUSTOM_ID: 1',
89+
':END:',
90+
'* TODO Test orgmode :WORK:',
91+
'DEADLINE: <2021-05-10 11:00 +1w>',
92+
'* TODO Another todo',
93+
}, vim.api.nvim_buf_get_lines(0, 0, 8, false))
94+
end)
95+
96+
it('should remove existing file property and whole drawer if its only property', function()
97+
helpers.create_file({
98+
':PROPERTIES:',
99+
':CATEGORY: Testing',
100+
':END:',
101+
'* TODO Test orgmode :WORK:',
102+
'DEADLINE: <2021-05-10 11:00 +1w>',
103+
'* TODO Another todo',
104+
})
105+
local file = org.files:get_current_file()
106+
file:set_property('CATEGORY', nil)
107+
108+
assert.are.same({
109+
'* TODO Test orgmode :WORK:',
110+
'DEADLINE: <2021-05-10 11:00 +1w>',
111+
'* TODO Another todo',
112+
}, vim.api.nvim_buf_get_lines(0, 0, 8, false))
113+
end)
114+
6115
it('should properly add new properties to a section', function()
7116
helpers.create_file({
8117
'* TODO Test orgmode :WORK:',
@@ -73,6 +182,50 @@ describe('Org file', function()
73182
}, vim.api.nvim_buf_get_lines(0, 0, 8, false))
74183
end)
75184

185+
it('should remove existing property', function()
186+
helpers.create_file({
187+
'* TODO Test orgmode :WORK:',
188+
'DEADLINE: <2021-05-10 11:00 +1w>',
189+
' :PROPERTIES:',
190+
' :CATEGORY: Testing',
191+
' :CUSTOM_ID: 1',
192+
' :END:',
193+
'* TODO Another todo',
194+
})
195+
local headline = org.files:get_closest_headline({ 1, 0 })
196+
assert.are.same('Test orgmode', headline:get_title())
197+
headline:set_property('CATEGORY', nil)
198+
199+
assert.are.same({
200+
'* TODO Test orgmode :WORK:',
201+
'DEADLINE: <2021-05-10 11:00 +1w>',
202+
' :PROPERTIES:',
203+
' :CUSTOM_ID: 1',
204+
' :END:',
205+
'* TODO Another todo',
206+
}, vim.api.nvim_buf_get_lines(0, 0, 8, false))
207+
end)
208+
209+
it('should remove existing property and whole drawer if its only property', function()
210+
helpers.create_file({
211+
'* TODO Test orgmode :WORK:',
212+
'DEADLINE: <2021-05-10 11:00 +1w>',
213+
' :PROPERTIES:',
214+
' :CATEGORY: Testing',
215+
' :END:',
216+
'* TODO Another todo',
217+
})
218+
local headline = org.files:get_closest_headline({ 1, 0 })
219+
assert.are.same('Test orgmode', headline:get_title())
220+
headline:set_property('CATEGORY', nil)
221+
222+
assert.are.same({
223+
'* TODO Test orgmode :WORK:',
224+
'DEADLINE: <2021-05-10 11:00 +1w>',
225+
'* TODO Another todo',
226+
}, vim.api.nvim_buf_get_lines(0, 0, 8, false))
227+
end)
228+
76229
it('should add closed date to section if it does not exist', function()
77230
local now = Date.now()
78231
helpers.create_file({

0 commit comments

Comments
 (0)