Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat(properties): add option org_use_property_inheritance #880

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/configuration.org
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,22 @@ Prefix added to the generated id when [[#org_id_method][org_id_method]] is set t
If =true=, generate ID with the Org ID module and append it to the
headline as property. More info on [[#org_store_link][org_store_link]]

*** org_use_property_inheritance
:PROPERTIES:
:CUSTOM_ID: org_use_property_inheritance
:END:
- Type: =boolean | string | string[]=
- Default: =false=
Determine whether properties of one headline are inherited by sub-headlines.

- =false= - properties only pertain to the file or headline that defines them
- =true= - properties of a headlines also pertain to all its sub-headlines
- =string[]= - only the properties named in the given list are inherited
- =string= - only properties matching the given regex are inherited

Note that for a select few properties, the inheritance behavior is hard-coded withing their special applications.
See [[https://orgmode.org/manual/Property-Inheritance.html][Property Inheritance]] for details.

*** org_babel_default_header_args
:PROPERTIES:
:CUSTOM_ID: org_babel_default_header_args
Expand Down
2 changes: 1 addition & 1 deletion lua/orgmode/api/headline.lua
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ end
---@private
function OrgHeadline._build_from_internal_headline(section, index)
local todo, _, type = section:get_todo()
local properties = section:get_properties()
local properties = section:get_own_properties()
return OrgHeadline:_new({
title = section:get_title(),
line = section:get_headline_line_content(),
Expand Down
2 changes: 1 addition & 1 deletion lua/orgmode/clock/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ function Clock:get_statusline()
return ''
end

local effort = self.clocked_headline:get_property('effort')
local effort = self.clocked_headline:get_property('effort', false)
local total = self.clocked_headline:get_logbook():get_total_with_active():to_string()
if effort then
return string.format('(Org) [%s/%s] (%s)', total, effort or '', self.clocked_headline:get_title())
Expand Down
1 change: 1 addition & 0 deletions lua/orgmode/config/_meta.lua
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@
---@field org_id_method? 'uuid' | 'ts' | 'org' What method to use to generate ids via org.id module. Default: 'uuid'
---@field org_id_prefix? string | nil Prefix to apply to id when `org_id_method = 'org'`. Default: nil
---@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
---@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
---@field org_babel_default_header_args? table<string, string> Default header args for org-babel blocks: Default: { [':tangle'] = 'no', [':noweb'] = 'no' }
---@field win_split_mode? 'horizontal' | 'vertical' | 'auto' | 'float' | string[] How to open agenda and capture windows. Default: 'horizontal'
---@field win_border? 'none' | 'single' | 'double' | 'rounded' | 'solid' | 'shadow' | string[] Border configuration for `win_split_mode = 'float'`. Default: 'single'
Expand Down
1 change: 1 addition & 0 deletions lua/orgmode/config/defaults.lua
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ local DefaultConfig = {
org_id_method = 'uuid',
org_id_prefix = nil,
org_id_link_to_org_use_id = false,
org_use_property_inheritance = false,
org_babel_default_header_args = {
[':tangle'] = 'no',
[':noweb'] = 'no',
Expand Down
19 changes: 19 additions & 0 deletions lua/orgmode/config/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,25 @@ function Config:parse_header_args(args)
return results
end

---@param property_name string
---@return boolean uses_inheritance
function Config:use_property_inheritance(property_name)
property_name = string.lower(property_name)

local use_inheritance = self.opts.org_use_property_inheritance or false

if type(use_inheritance) == 'table' then
return vim.tbl_contains(use_inheritance, function(value)
return vim.stricmp(value, property_name) == 0
end, { predicate = true })
elseif type(use_inheritance) == 'string' then
local regex = vim.regex(use_inheritance)
return regex:match_str(property_name) and true or false
else
return use_inheritance and true or false
end
end

---@type OrgConfig
instance = Config:new()
return instance
2 changes: 1 addition & 1 deletion lua/orgmode/files/file.lua
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ function OrgFile:apply_search(search, todo_only)
local deadline = item:get_deadline_date()
local scheduled = item:get_scheduled_date()
local closed = item:get_closed_date()
local properties = item:get_properties()
local properties = item:get_own_properties()
local priority = item:get_priority()

return search:check({
Expand Down
59 changes: 46 additions & 13 deletions lua/orgmode/files/headline.lua
Original file line number Diff line number Diff line change
Expand Up @@ -390,9 +390,9 @@ function Headline:get_title_with_priority()
return title
end

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

Expand All @@ -416,34 +416,60 @@ function Headline:get_properties()
return properties, properties_node
end

memoize('get_properties')
---@return table<string, string>, TSNode | nil
function Headline:get_properties()
local properties, own_properties_node = self:get_own_properties()

if not config.org_use_property_inheritance then
return properties, own_properties_node
end

local parent_section = self:node():parent():parent()
while parent_section do
local headline_node = parent_section:field('headline')[1]
if headline_node then
local headline = Headline:new(headline_node, self.file)
for name, value in pairs(headline:get_own_properties()) do
if properties[name] == nil and config:use_property_inheritance(name) then
properties[name] = value
end
end
end
parent_section = parent_section:parent()
end

return properties, own_properties_node
end

---@param name string
---@param value? string
---@return OrgHeadline
function Headline:set_property(name, value)
local bufnr = self.file:get_valid_bufnr()
if not value then
local existing_property, property_node = self:get_property(name)
local existing_property, property_node = self:get_property(name, false)
if existing_property and property_node then
vim.fn.deletebufline(bufnr, property_node:start() + 1)
end
self:refresh()
local properties, properties_node = self:get_properties()
local properties, properties_node = self:get_own_properties()
if vim.tbl_isempty(properties) then
self:_set_node_lines(properties_node, {})
end
return self:refresh()
end

local _, properties = self:get_properties()
local _, properties = self:get_own_properties()
if not properties then
local append_line = self:get_append_line()
local property_drawer = self:_apply_indent({ ':PROPERTIES:', ':END:' }) --[[ @as string[] ]]
vim.api.nvim_buf_set_lines(bufnr, append_line, append_line, false, property_drawer)
_, properties = self:refresh():get_properties()
_, properties = self:refresh():get_own_properties()
end

local property = (':%s: %s'):format(name, value)
local existing_property, property_node = self:get_property(name)
local existing_property, property_node = self:get_property(name, false)
if existing_property then
return self:_set_node_text(property_node, property)
end
Expand Down Expand Up @@ -472,10 +498,13 @@ function Headline:add_note(note)
end

---@param property_name string
---@param search_parents? boolean
---@param search_parents? boolean if true, search parent headlines;
--- if false, only search this headline;
--- if nil (default), check
--- `org_use_property_inheritance`
---@return string | nil, TSNode | nil
function Headline:get_property(property_name, search_parents)
local _, properties = self:get_properties()
local _, properties = self:get_own_properties()
if properties then
for _, node in ipairs(ts_utils.get_named_children(properties)) do
local name = node:field('name')[1]
Expand All @@ -486,6 +515,10 @@ function Headline:get_property(property_name, search_parents)
end
end

if search_parents == nil then
search_parents = config:use_property_inheritance(property_name)
end

if not search_parents then
return nil, nil
end
Expand All @@ -495,7 +528,7 @@ function Headline:get_property(property_name, search_parents)
local headline_node = parent_section:field('headline')[1]
if headline_node then
local headline = Headline:new(headline_node, self.file)
local property, property_node = headline:get_property(property_name)
local property, property_node = headline:get_property(property_name, false)
if property then
return property, property_node
end
Expand Down Expand Up @@ -543,7 +576,7 @@ memoize('get_tags')
function Headline:get_tags()
local tags, own_tags_node = self:get_own_tags()
if not config.org_use_tag_inheritance then
return config:exclude_tags(tags), own_tags_node
return tags, own_tags_node
end

local parent_tags = {}
Expand Down Expand Up @@ -629,7 +662,7 @@ end

---@return number
function Headline:get_append_line()
local _, properties = self:get_properties()
local _, properties = self:get_own_properties()
if properties then
local row = properties:end_()
return row
Expand Down Expand Up @@ -918,7 +951,7 @@ function Headline:is_same(other_headline)
end

function Headline:id_get_or_create()
local id_prop = self:get_property('ID')
local id_prop = self:get_property('ID', false)
if id_prop then
return vim.trim(id_prop)
end
Expand Down
2 changes: 1 addition & 1 deletion lua/orgmode/org/hyperlinks/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ function Hyperlinks.as_custom_id_anchors(url)
return function(headlines)
return vim.tbl_map(function(headline)
---@cast headline OrgHeadline
local custom_id = headline:get_property('custom_id')
local custom_id = headline:get_property('custom_id', false)
return ('%s#%s'):format(prefix, custom_id)
end, headlines)
end
Expand Down
2 changes: 1 addition & 1 deletion lua/orgmode/org/links/types/custom_id.lua
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ function OrgLinkCustomId:autocomplete(link)
local prefix = opts.type == 'internal' and '' or opts.link_url:get_path_with_protocol() .. '::'

return vim.tbl_map(function(headline)
local custom_id = headline:get_property('custom_id')
local custom_id = headline:get_property('custom_id', false)
return prefix .. '#' .. custom_id
end, headlines)
end
Expand Down
2 changes: 1 addition & 1 deletion lua/orgmode/org/links/types/id.lua
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ end

---@private
---@param link string
---@return string
---@return string?
function OrgLinkId:_parse(link)
return link:match('^id:(.+)$')
end
Expand Down
1 change: 1 addition & 0 deletions lua/orgmode/ui/menu.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ local config = require('orgmode.config')
local Menu = {}

---@param data OrgMenuOpts
---@return OrgMenu
function Menu:new(data)
self:_validate_data(data)

Expand Down
4 changes: 4 additions & 0 deletions lua/orgmode/utils/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ function utils.readfile(file, opts)
end)
end

---@param file string
---@param data string|string[]
---@return OrgPromise<integer> bytes
function utils.writefile(file, data)
return Promise.new(function(resolve, reject)
uv.fs_open(file, 'w', 438, function(err1, fd)
Expand Down Expand Up @@ -502,6 +505,7 @@ function utils.is_list(value)
if vim.islist then
return vim.islist(value)
end
---@diagnostic disable-next-line: deprecated
return vim.tbl_islist(value)
end

Expand Down
68 changes: 68 additions & 0 deletions tests/plenary/files/headline_spec.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local helpers = require('tests.plenary.helpers')
local config = require('orgmode.config')

describe('Headline', function()
describe('get_category', function()
Expand Down Expand Up @@ -54,6 +55,73 @@ describe('Headline', function()
end)
end)

describe('use_property_inheritance', function()
local file = helpers.create_file_instance({
'#+CATEGORY: file_category',
'* Headline 1',
':PROPERTIES:',
':DIR: some/dir/',
':THING: 0',
':COLUMNS:',
':END:',
'** Headline 2',
' some body text',
}, 'category.org')
after_each(function()
config:extend({ org_use_property_inheritance = false })
end)
it('is false by default', function()
assert.is.Nil(file:get_headlines()[2]:get_property('dir'))
end)
it('is active if true', function()
config:extend({ org_use_property_inheritance = true })
assert.are.same('some/dir/', file:get_headlines()[2]:get_property('dir'))
assert.are.same('0', file:get_headlines()[2]:get_property('thing'))
end)
it('is selective if a list', function()
config:extend({ org_use_property_inheritance = { 'dir' } })
assert.are.same('some/dir/', file:get_headlines()[2]:get_property('dir'))
assert.is.Nil(file:get_headlines()[2]:get_property('thing'))
end)
it('is selective if a regex', function()
config:extend({ org_use_property_inheritance = '^di.$' })
assert.are.same('some/dir/', file:get_headlines()[2]:get_property('dir'))
assert.is.Nil(file:get_headlines()[2]:get_property('thing'))
end)
it('can be overridden with true', function()
assert.is.Nil(file:get_headlines()[2]:get_property('dir'))
assert.are.same('some/dir/', file:get_headlines()[2]:get_property('dir', true))
end)
it('can be overridden with false', function()
config:extend({ org_use_property_inheritance = true })
assert.are.same('some/dir/', file:get_headlines()[2]:get_property('dir'))
assert.is.Nil(file:get_headlines()[2]:get_property('dir', false))
end)
it('does not affect get_own_properties', function()
config:extend({ org_use_property_inheritance = true })
file.metadata.mtime = file.metadata.mtime + 1 -- invalidate cache
assert.are.same({}, file:get_headlines()[2]:get_own_properties())
end)
it('affects get_properties', function()
config:extend({ org_use_property_inheritance = true })
file.metadata.mtime = file.metadata.mtime + 1 -- invalidate cache
local expected = { dir = 'some/dir/', thing = '0', columns = '' }
assert.are.same(expected, file:get_headlines()[2]:get_properties())
end)
it('makes get_properties selective if a list', function()
config:extend({ org_use_property_inheritance = { 'dir' } })
file.metadata.mtime = file.metadata.mtime + 1 -- invalidate cache
local expected = { dir = 'some/dir/' }
assert.are.same(expected, file:get_headlines()[2]:get_properties())
end)
it('makes get_properties selective if a regex', function()
config:extend({ org_use_property_inheritance = '^th...$' })
file.metadata.mtime = file.metadata.mtime + 1 -- invalidate cache
local expected = { thing = '0' }
assert.are.same(expected, file:get_headlines()[2]:get_properties())
end)
end)

describe('get_all_dates', function()
it('should properly parse dates from the headline and body', function()
local file = helpers.create_file({
Expand Down
Loading