Skip to content

Commit 48f32e6

Browse files
troigantotroigantokristijanhusak
authored
feat(properties): add option org_use_property_inheritance (#880)
* chore(types): add some type annotations * feat(properties): add setting `org_use_property_inheritance` * feat(properties): add property inheritance for API and search * fix(tags): don't exclude own tags if tag inheritance is disabled --------- Co-authored-by: troiganto <[email protected]> Co-authored-by: Kristijan Husak <[email protected]>
1 parent 472e28d commit 48f32e6

File tree

14 files changed

+162
-19
lines changed

14 files changed

+162
-19
lines changed

Diff for: docs/configuration.org

+16
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,22 @@ Prefix added to the generated id when [[#org_id_method][org_id_method]] is set t
531531
If =true=, generate ID with the Org ID module and append it to the
532532
headline as property. More info on [[#org_store_link][org_store_link]]
533533

534+
*** org_use_property_inheritance
535+
:PROPERTIES:
536+
:CUSTOM_ID: org_use_property_inheritance
537+
:END:
538+
- Type: =boolean | string | string[]=
539+
- Default: =false=
540+
Determine whether properties of one headline are inherited by sub-headlines.
541+
542+
- =false= - properties only pertain to the file or headline that defines them
543+
- =true= - properties of a headlines also pertain to all its sub-headlines
544+
- =string[]= - only the properties named in the given list are inherited
545+
- =string= - only properties matching the given regex are inherited
546+
547+
Note that for a select few properties, the inheritance behavior is hard-coded withing their special applications.
548+
See [[https://orgmode.org/manual/Property-Inheritance.html][Property Inheritance]] for details.
549+
534550
*** org_babel_default_header_args
535551
:PROPERTIES:
536552
:CUSTOM_ID: org_babel_default_header_args

Diff for: lua/orgmode/api/headline.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ end
6464
---@private
6565
function OrgHeadline._build_from_internal_headline(section, index)
6666
local todo, _, type = section:get_todo()
67-
local properties = section:get_properties()
67+
local properties = section:get_own_properties()
6868
return OrgHeadline:_new({
6969
title = section:get_title(),
7070
line = section:get_headline_line_content(),

Diff for: lua/orgmode/clock/init.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ function Clock:get_statusline()
118118
return ''
119119
end
120120

121-
local effort = self.clocked_headline:get_property('effort')
121+
local effort = self.clocked_headline:get_property('effort', false)
122122
local total = self.clocked_headline:get_logbook():get_total_with_active():to_string()
123123
if effort then
124124
return string.format('(Org) [%s/%s] (%s)', total, effort or '', self.clocked_headline:get_title())

Diff for: lua/orgmode/config/_meta.lua

+1
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@
235235
---@field org_id_method? 'uuid' | 'ts' | 'org' What method to use to generate ids via org.id module. Default: 'uuid'
236236
---@field org_id_prefix? string | nil Prefix to apply to id when `org_id_method = 'org'`. Default: nil
237237
---@field org_id_link_to_org_use_id? boolean If true, Storing a link to the headline will automatically generate ID for that headline. Default: false
238+
---@field org_use_property_inheritance boolean | string | string[] If true, properties are inherited by sub-headlines; may also be a regex or list of property names. Default: false
238239
---@field org_babel_default_header_args? table<string, string> Default header args for org-babel blocks: Default: { [':tangle'] = 'no', [':noweb'] = 'no' }
239240
---@field win_split_mode? 'horizontal' | 'vertical' | 'auto' | 'float' | string[] How to open agenda and capture windows. Default: 'horizontal'
240241
---@field win_border? 'none' | 'single' | 'double' | 'rounded' | 'solid' | 'shadow' | string[] Border configuration for `win_split_mode = 'float'`. Default: 'single'

Diff for: lua/orgmode/config/defaults.lua

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ local DefaultConfig = {
6666
org_id_method = 'uuid',
6767
org_id_prefix = nil,
6868
org_id_link_to_org_use_id = false,
69+
org_use_property_inheritance = false,
6970
org_babel_default_header_args = {
7071
[':tangle'] = 'no',
7172
[':noweb'] = 'no',

Diff for: lua/orgmode/config/init.lua

+19
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,25 @@ function Config:parse_header_args(args)
539539
return results
540540
end
541541

542+
---@param property_name string
543+
---@return boolean uses_inheritance
544+
function Config:use_property_inheritance(property_name)
545+
property_name = string.lower(property_name)
546+
547+
local use_inheritance = self.opts.org_use_property_inheritance or false
548+
549+
if type(use_inheritance) == 'table' then
550+
return vim.tbl_contains(use_inheritance, function(value)
551+
return vim.stricmp(value, property_name) == 0
552+
end, { predicate = true })
553+
elseif type(use_inheritance) == 'string' then
554+
local regex = vim.regex(use_inheritance)
555+
return regex:match_str(property_name) and true or false
556+
else
557+
return use_inheritance and true or false
558+
end
559+
end
560+
542561
---@type OrgConfig
543562
instance = Config:new()
544563
return instance

Diff for: lua/orgmode/files/file.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ function OrgFile:apply_search(search, todo_only)
278278
local deadline = item:get_deadline_date()
279279
local scheduled = item:get_scheduled_date()
280280
local closed = item:get_closed_date()
281-
local properties = item:get_properties()
281+
local properties = item:get_own_properties()
282282
local priority = item:get_priority()
283283

284284
return search:check({

Diff for: lua/orgmode/files/headline.lua

+46-13
Original file line numberDiff line numberDiff line change
@@ -390,9 +390,9 @@ function Headline:get_title_with_priority()
390390
return title
391391
end
392392

393-
memoize('get_properties')
393+
memoize('get_own_properties')
394394
---@return table<string, string>, TSNode | nil
395-
function Headline:get_properties()
395+
function Headline:get_own_properties()
396396
local section = self:node():parent()
397397
local properties_node = section and section:field('property_drawer')[1]
398398

@@ -416,34 +416,60 @@ function Headline:get_properties()
416416
return properties, properties_node
417417
end
418418

419+
memoize('get_properties')
420+
---@return table<string, string>, TSNode | nil
421+
function Headline:get_properties()
422+
local properties, own_properties_node = self:get_own_properties()
423+
424+
if not config.org_use_property_inheritance then
425+
return properties, own_properties_node
426+
end
427+
428+
local parent_section = self:node():parent():parent()
429+
while parent_section do
430+
local headline_node = parent_section:field('headline')[1]
431+
if headline_node then
432+
local headline = Headline:new(headline_node, self.file)
433+
for name, value in pairs(headline:get_own_properties()) do
434+
if properties[name] == nil and config:use_property_inheritance(name) then
435+
properties[name] = value
436+
end
437+
end
438+
end
439+
parent_section = parent_section:parent()
440+
end
441+
442+
return properties, own_properties_node
443+
end
444+
419445
---@param name string
420446
---@param value? string
421447
---@return OrgHeadline
422448
function Headline:set_property(name, value)
423449
local bufnr = self.file:get_valid_bufnr()
424450
if not value then
425-
local existing_property, property_node = self:get_property(name)
451+
local existing_property, property_node = self:get_property(name, false)
426452
if existing_property and property_node then
427453
vim.fn.deletebufline(bufnr, property_node:start() + 1)
428454
end
429455
self:refresh()
430-
local properties, properties_node = self:get_properties()
456+
local properties, properties_node = self:get_own_properties()
431457
if vim.tbl_isempty(properties) then
432458
self:_set_node_lines(properties_node, {})
433459
end
434460
return self:refresh()
435461
end
436462

437-
local _, properties = self:get_properties()
463+
local _, properties = self:get_own_properties()
438464
if not properties then
439465
local append_line = self:get_append_line()
440466
local property_drawer = self:_apply_indent({ ':PROPERTIES:', ':END:' }) --[[ @as string[] ]]
441467
vim.api.nvim_buf_set_lines(bufnr, append_line, append_line, false, property_drawer)
442-
_, properties = self:refresh():get_properties()
468+
_, properties = self:refresh():get_own_properties()
443469
end
444470

445471
local property = (':%s: %s'):format(name, value)
446-
local existing_property, property_node = self:get_property(name)
472+
local existing_property, property_node = self:get_property(name, false)
447473
if existing_property then
448474
return self:_set_node_text(property_node, property)
449475
end
@@ -472,10 +498,13 @@ function Headline:add_note(note)
472498
end
473499

474500
---@param property_name string
475-
---@param search_parents? boolean
501+
---@param search_parents? boolean if true, search parent headlines;
502+
--- if false, only search this headline;
503+
--- if nil (default), check
504+
--- `org_use_property_inheritance`
476505
---@return string | nil, TSNode | nil
477506
function Headline:get_property(property_name, search_parents)
478-
local _, properties = self:get_properties()
507+
local _, properties = self:get_own_properties()
479508
if properties then
480509
for _, node in ipairs(ts_utils.get_named_children(properties)) do
481510
local name = node:field('name')[1]
@@ -486,6 +515,10 @@ function Headline:get_property(property_name, search_parents)
486515
end
487516
end
488517

518+
if search_parents == nil then
519+
search_parents = config:use_property_inheritance(property_name)
520+
end
521+
489522
if not search_parents then
490523
return nil, nil
491524
end
@@ -495,7 +528,7 @@ function Headline:get_property(property_name, search_parents)
495528
local headline_node = parent_section:field('headline')[1]
496529
if headline_node then
497530
local headline = Headline:new(headline_node, self.file)
498-
local property, property_node = headline:get_property(property_name)
531+
local property, property_node = headline:get_property(property_name, false)
499532
if property then
500533
return property, property_node
501534
end
@@ -543,7 +576,7 @@ memoize('get_tags')
543576
function Headline:get_tags()
544577
local tags, own_tags_node = self:get_own_tags()
545578
if not config.org_use_tag_inheritance then
546-
return config:exclude_tags(tags), own_tags_node
579+
return tags, own_tags_node
547580
end
548581

549582
local parent_tags = {}
@@ -629,7 +662,7 @@ end
629662

630663
---@return number
631664
function Headline:get_append_line()
632-
local _, properties = self:get_properties()
665+
local _, properties = self:get_own_properties()
633666
if properties then
634667
local row = properties:end_()
635668
return row
@@ -918,7 +951,7 @@ function Headline:is_same(other_headline)
918951
end
919952

920953
function Headline:id_get_or_create()
921-
local id_prop = self:get_property('ID')
954+
local id_prop = self:get_property('ID', false)
922955
if id_prop then
923956
return vim.trim(id_prop)
924957
end

Diff for: lua/orgmode/org/hyperlinks/init.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function Hyperlinks.as_custom_id_anchors(url)
5757
return function(headlines)
5858
return vim.tbl_map(function(headline)
5959
---@cast headline OrgHeadline
60-
local custom_id = headline:get_property('custom_id')
60+
local custom_id = headline:get_property('custom_id', false)
6161
return ('%s#%s'):format(prefix, custom_id)
6262
end, headlines)
6363
end

Diff for: lua/orgmode/org/links/types/custom_id.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ function OrgLinkCustomId:autocomplete(link)
6060
local prefix = opts.type == 'internal' and '' or opts.link_url:get_path_with_protocol() .. '::'
6161

6262
return vim.tbl_map(function(headline)
63-
local custom_id = headline:get_property('custom_id')
63+
local custom_id = headline:get_property('custom_id', false)
6464
return prefix .. '#' .. custom_id
6565
end, headlines)
6666
end

Diff for: lua/orgmode/org/links/types/id.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ end
5656

5757
---@private
5858
---@param link string
59-
---@return string
59+
---@return string?
6060
function OrgLinkId:_parse(link)
6161
return link:match('^id:(.+)$')
6262
end

Diff for: lua/orgmode/ui/menu.lua

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ local config = require('orgmode.config')
2323
local Menu = {}
2424

2525
---@param data OrgMenuOpts
26+
---@return OrgMenu
2627
function Menu:new(data)
2728
self:_validate_data(data)
2829

Diff for: lua/orgmode/utils/init.lua

+4
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ function utils.readfile(file, opts)
5353
end)
5454
end
5555

56+
---@param file string
57+
---@param data string|string[]
58+
---@return OrgPromise<integer> bytes
5659
function utils.writefile(file, data)
5760
return Promise.new(function(resolve, reject)
5861
uv.fs_open(file, 'w', 438, function(err1, fd)
@@ -502,6 +505,7 @@ function utils.is_list(value)
502505
if vim.islist then
503506
return vim.islist(value)
504507
end
508+
---@diagnostic disable-next-line: deprecated
505509
return vim.tbl_islist(value)
506510
end
507511

Diff for: tests/plenary/files/headline_spec.lua

+68
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
local helpers = require('tests.plenary.helpers')
2+
local config = require('orgmode.config')
23

34
describe('Headline', function()
45
describe('get_category', function()
@@ -54,6 +55,73 @@ describe('Headline', function()
5455
end)
5556
end)
5657

58+
describe('use_property_inheritance', function()
59+
local file = helpers.create_file_instance({
60+
'#+CATEGORY: file_category',
61+
'* Headline 1',
62+
':PROPERTIES:',
63+
':DIR: some/dir/',
64+
':THING: 0',
65+
':COLUMNS:',
66+
':END:',
67+
'** Headline 2',
68+
' some body text',
69+
}, 'category.org')
70+
after_each(function()
71+
config:extend({ org_use_property_inheritance = false })
72+
end)
73+
it('is false by default', function()
74+
assert.is.Nil(file:get_headlines()[2]:get_property('dir'))
75+
end)
76+
it('is active if true', function()
77+
config:extend({ org_use_property_inheritance = true })
78+
assert.are.same('some/dir/', file:get_headlines()[2]:get_property('dir'))
79+
assert.are.same('0', file:get_headlines()[2]:get_property('thing'))
80+
end)
81+
it('is selective if a list', function()
82+
config:extend({ org_use_property_inheritance = { 'dir' } })
83+
assert.are.same('some/dir/', file:get_headlines()[2]:get_property('dir'))
84+
assert.is.Nil(file:get_headlines()[2]:get_property('thing'))
85+
end)
86+
it('is selective if a regex', function()
87+
config:extend({ org_use_property_inheritance = '^di.$' })
88+
assert.are.same('some/dir/', file:get_headlines()[2]:get_property('dir'))
89+
assert.is.Nil(file:get_headlines()[2]:get_property('thing'))
90+
end)
91+
it('can be overridden with true', function()
92+
assert.is.Nil(file:get_headlines()[2]:get_property('dir'))
93+
assert.are.same('some/dir/', file:get_headlines()[2]:get_property('dir', true))
94+
end)
95+
it('can be overridden with false', function()
96+
config:extend({ org_use_property_inheritance = true })
97+
assert.are.same('some/dir/', file:get_headlines()[2]:get_property('dir'))
98+
assert.is.Nil(file:get_headlines()[2]:get_property('dir', false))
99+
end)
100+
it('does not affect get_own_properties', function()
101+
config:extend({ org_use_property_inheritance = true })
102+
file.metadata.mtime = file.metadata.mtime + 1 -- invalidate cache
103+
assert.are.same({}, file:get_headlines()[2]:get_own_properties())
104+
end)
105+
it('affects get_properties', function()
106+
config:extend({ org_use_property_inheritance = true })
107+
file.metadata.mtime = file.metadata.mtime + 1 -- invalidate cache
108+
local expected = { dir = 'some/dir/', thing = '0', columns = '' }
109+
assert.are.same(expected, file:get_headlines()[2]:get_properties())
110+
end)
111+
it('makes get_properties selective if a list', function()
112+
config:extend({ org_use_property_inheritance = { 'dir' } })
113+
file.metadata.mtime = file.metadata.mtime + 1 -- invalidate cache
114+
local expected = { dir = 'some/dir/' }
115+
assert.are.same(expected, file:get_headlines()[2]:get_properties())
116+
end)
117+
it('makes get_properties selective if a regex', function()
118+
config:extend({ org_use_property_inheritance = '^th...$' })
119+
file.metadata.mtime = file.metadata.mtime + 1 -- invalidate cache
120+
local expected = { thing = '0' }
121+
assert.are.same(expected, file:get_headlines()[2]:get_properties())
122+
end)
123+
end)
124+
57125
describe('get_all_dates', function()
58126
it('should properly parse dates from the headline and body', function()
59127
local file = helpers.create_file({

0 commit comments

Comments
 (0)