Skip to content

feat: Add org id support and org store link #654

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jan 24, 2024
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
37 changes: 36 additions & 1 deletion DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,34 @@ Determine if blank line should be prepended when:
* Adding heading via `org_meta_return` and `org_insert_*` mappings
* Adding a list item via `org_meta_return`

#### **org_id_uuid_program**
*type*: `string`<br />
*default value*: `uuidgen`<br />
External program used to generate uuid's for id module

#### **org_id_ts_format**
*type*: `string`<br />
*default value*: `%Y%m%d%H%M%S`<br />
Format of the id generated when [org_id_method](#org_id_method) is set to `ts`.

#### **org_id_method**
*type*: `'uuid' | 'ts' | 'org'`<br />
*default value*: `uuid`<br />
What method to use to generate ids via org id module.
* `uuid` - Use [org_id_uuid_program](#org_id_uuid_program) to generate the id
* `ts` - Generate id from current timestamp using format [org_id_ts_format](#org_id_ts_format)
* `org` - Generate a random 12 digit number and prepend [org_id_prefix](#org_id_prefix)

#### **org_id_prefix**
*type*: `string | nil`<br />
*default value*: `nil`<br />
Prefix added to the generated id when [org_id_method](#org_id_method) is set to `org`.

#### **org_id_link_to_org_use_id**
*type*: `boolean`<br />
*default value*: `false`<br />
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)

#### **calendar_week_start_day**
*type*: `number`<br />
*default value*: `1`<br />
Expand Down Expand Up @@ -896,8 +924,15 @@ Toggle current line checkbox state
*mapped to*: `<Leader>o*`<br />
Toggle current line to headline and vice versa. Checkboxes will turn into TODO headlines.
#### **org_insert_link**
*mapped to*: `<Leader>oil`<br />
*mapped to*: `<Leader>oli`<br />
Insert a hyperlink at cursor position. When the cursor is on a hyperlink, edit that hyperlink.<br />
If there are any links stored with [org_store_link](#org_store_link), pressing `<TAB>` to autocomplete the input
will show list of all stored links to select. Links generated with ID are properly expanded to valid links after selection.
#### **org_store_link**
*mapped to*: `<Leader>ols`<br />
Generate a link to the closest headline. If [org_id_link_to_org_use_id](#org_id_link_to_org_use_id) is `true`,
it appends the `ID` property to the headline, and generates link with that id to be inserted via [org_insert_link](#org_insert_link).
When [org_id_link_to_org_use_id](#org_id_link_to_org_use_id) is `false`, it generates the standard file::*headline link (example: `file:/path/to/my/todos.org::*My headline`)
#### **org_open_at_point**
*mapped to*: `<Leader>oo`<br />
Open hyperlink or date under cursor. When date is under the cursor, open the agenda for that day.<br />
Expand Down
32 changes: 32 additions & 0 deletions lua/orgmode/api/headline.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ local Promise = require('orgmode.utils.promise')
---@field tags string[] List of own tags
---@field deadline Date|nil
---@field scheduled Date|nil
---@field properties table<string, string> Table containing all properties. All keys are lowercased
---@field closed Date|nil
---@field dates Date[] List of all dates that are not "plan" dates
---@field position Range
Expand Down Expand Up @@ -44,6 +45,7 @@ function OrgHeadline:_new(opts)
data.all_tags = opts.all_tags
data.priority = opts.priority
data.deadline = opts.deadline
data.properties = opts.properties
data.scheduled = opts.scheduled
data.closed = opts.closed
data.dates = opts.dates
Expand Down Expand Up @@ -71,6 +73,7 @@ function OrgHeadline._build_from_internal_section(section, index)
all_tags = { unpack(section.tags) },
tags = section:get_own_tags(),
position = OrgPosition:_build_from_internal_range(section.range),
properties = section:get_properties(),
deadline = section:get_deadline_date(),
scheduled = section:get_scheduled_date(),
closed = section:get_closed_date(),
Expand Down Expand Up @@ -213,6 +216,35 @@ function OrgHeadline:set_scheduled(date)
end)
end

--- Set property on a headline
---@param key string
---@param value string
function OrgHeadline:set_property(key, value)
return self:_do_action(function()
local headline = ts_org.closest_headline()
return headline:set_property(key, value)
end)
end

--- Get headline property
---@param key string
---@return string | nil
function OrgHeadline:get_property(key)
return self.properties[key:lower()]
end

--- Get headline id or create a new one if it doesn't exist
--- @return string
function OrgHeadline:id_get_or_create()
local id = self:get_property('id')
if id then
return id
end
local org_id = require('orgmode.org.id').new()
self:set_property('ID', org_id)
return org_id
end

---@param action function
---@private
function OrgHeadline:_do_action(action)
Expand Down
9 changes: 8 additions & 1 deletion lua/orgmode/config/defaults.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---@class DefaultConfig
---@field org_id_method 'uuid' | 'ts' | 'org'
local DefaultConfig = {
org_agenda_files = '',
org_default_notes_file = '',
Expand Down Expand Up @@ -42,6 +43,11 @@ local DefaultConfig = {
},
org_src_window_setup = 'top 16new',
org_edit_src_content_indentation = 0,
org_id_uuid_program = 'uuidgen',
org_id_ts_format = '%Y%m%d%H%M%S',
org_id_method = 'uuid',
org_id_prefix = nil,
org_id_link_to_org_use_id = false,
win_split_mode = 'horizontal',
win_border = 'single',
notifications = {
Expand Down Expand Up @@ -144,7 +150,8 @@ local DefaultConfig = {
org_schedule = '<prefix>is',
org_time_stamp = '<prefix>i.',
org_time_stamp_inactive = '<prefix>i!',
org_insert_link = '<prefix>il',
org_insert_link = '<prefix>li',
org_store_link = '<prefix>ls',
org_clock_in = '<prefix>xi',
org_clock_out = '<prefix>xo',
org_clock_cancel = '<prefix>xq',
Expand Down
4 changes: 2 additions & 2 deletions lua/orgmode/config/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -393,8 +393,8 @@ function Config:ts_highlights_enabled()
end

---@param content table
---@param option string
---@param prepend_content any
---@param option? string
---@param prepend_content? any
---@return table
function Config:respect_blank_before_new_entry(content, option, prepend_content)
if self.opts.org_blank_before_new_entry[option or 'heading'] then
Expand Down
1 change: 1 addition & 0 deletions lua/orgmode/config/mappings/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ return {
{ args = { true }, opts = { desc = 'org timestamp (inactive)' } }
),
org_insert_link = m.action('org_mappings.insert_link', { opts = { desc = 'org insert link' } }),
org_store_link = m.action('org_mappings.store_link', { opts = { desc = 'org store link' } }),
org_clock_in = m.action('clock.org_clock_in', { opts = { desc = 'org clock in' } }),
org_clock_out = m.action('clock.org_clock_out', { opts = { desc = 'org clock out' } }),
org_clock_cancel = m.action('clock.org_clock_cancel', { opts = { desc = 'org clock cancel' } }),
Expand Down
20 changes: 15 additions & 5 deletions lua/orgmode/objects/link.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
local Url = require('orgmode.objects.url')
local utils = require('orgmode.utils')
local config = require('orgmode.config')
local Range = require('orgmode.parser.range')

---@class Link
---@field url Url
Expand All @@ -10,6 +13,7 @@ function Link:init(str)
local parts = vim.split(str, '][', true)
self.url = Url.new(parts[1] or '')
self.desc = parts[2]
return self
end

---@return string
Expand All @@ -22,29 +26,35 @@ function Link:to_str()
end

---@param str string
---@return Link
function Link.new(str)
local self = setmetatable({}, { __index = Link })
self:init(str)
return self
return self:init(str)
end

---@param line string
---@param pos number
---@return Link | nil
---@return Link | nil, table | nil
function Link.at_pos(line, pos)
local links = {}
local found_link = nil
local pattern = '%[%[([^%]]+.-)%]%]'
local position
for link in line:gmatch(pattern) do
local start_from = #links > 0 and links[#links].to or nil
local from, to = line:find(pattern, start_from)
local current_pos = { from = from, to = to }
if pos >= from and pos <= to then
found_link = link
position = current_pos
break
end
table.insert(links, { link = link, from = from, to = to })
table.insert(links, current_pos)
end
if not found_link then
return nil, nil
end
return (found_link and Link.new(found_link) or nil)
return Link.new(found_link), position
end

return Link
24 changes: 19 additions & 5 deletions lua/orgmode/objects/url.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
local utils = require('orgmode.utils')
local fs = require('orgmode.utils.fs')

---@class Url
Expand Down Expand Up @@ -27,16 +26,22 @@ end

---@return boolean
function Url:is_file_headline()
return self:is_file() and self:get_headline() and true
return self:is_file() and self:get_headline() and true or false
end

---@return boolean
function Url:is_custom_id()
return self:is_file_custom_id() or self:is_internal_custom_id()
return (self:is_file_custom_id() or self:is_internal_custom_id()) and true or false
end

---@return boolean
function Url:is_id()
return self.str:find('^id:') and true or false
end

---@return boolean
function Url:is_file_custom_id()
return self:is_file() and self:get_custom_id() and true
return self:is_file() and self:get_custom_id() and true or false
end

---@return boolean
Expand Down Expand Up @@ -64,7 +69,7 @@ end

---@return boolean
function Url:is_internal_headline()
return self.str:find('^*') and true
return self.str:find('^*') and true or false
end

function Url:is_internal_custom_id()
Expand Down Expand Up @@ -122,6 +127,10 @@ function Url:get_custom_id()
or self.str:match('^#(.-)$')
end

function Url:get_id()
return self.str:match('^id:(%S+)')
end

---@return number | false
function Url:get_linenumber()
-- official orgmode convention
Expand Down Expand Up @@ -181,4 +190,9 @@ function Url:get_http_url()
return self.str:match('^https?://.+$')
end

---@return string | false
function Url:extract_target()
return self:get_headline() or self:get_custom_id() or self:get_dedicated_target()
end

return Url
55 changes: 53 additions & 2 deletions lua/orgmode/org/hyperlinks.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
local Files = require('orgmode.parser.files')
local utils = require('orgmode.utils')
local fs = require('orgmode.utils.fs')
local Hyperlinks = {}
local Url = require('orgmode.objects.url')
local config = require('orgmode.config')
local Hyperlinks = {
stored_links = {},
}

---@param url Url
local function get_file_from_url(url)
Expand Down Expand Up @@ -66,7 +70,7 @@ function Hyperlinks.as_custom_id_anchors(headlines)
and '#' .. headline.properties.items.custom_id
end, headlines)
end
--

---@param headlines Section[]
---@param omit_prefix? boolean
---@return string[]
Expand Down Expand Up @@ -166,4 +170,51 @@ function Hyperlinks.find_matching_links(url)
return result, mapper
end

---@param headline Headline
---@param path? string
function Hyperlinks.get_link_to_headline(headline, path)
path = path or utils.current_file_path()
local title = headline:title()
local id
if config.org_id_link_to_org_use_id then
id = headline:id_get_or_create()
end
return Hyperlinks._generate_link_to_headline(title, id, path)
end

---@private
function Hyperlinks._generate_link_to_headline(title, id, path)
if not config.org_id_link_to_org_use_id or not id then
return ('file:%s::*%s'):format(path, title)
end
return ('id:%s %s'):format(id, title)
end

---@param headline Headline
function Hyperlinks.store_link_to_headline(headline)
local title = headline:title()
Hyperlinks.stored_links[Hyperlinks.get_link_to_headline(headline)] = title
end

---@param arg_lead string
---@return string[]
function Hyperlinks.autocomplete_links(arg_lead)
local url = Url.new(arg_lead)
local result, mapper = Hyperlinks.find_matching_links(url)

if url:is_file_plain() then
return mapper(result)
end

if url:is_custom_id() or url:is_headline() then
local file = get_file_from_url(url)
local results = mapper(result)
return vim.tbl_map(function(value)
return ('file:%s::%s'):format(file.filename, value)
end, results)
end

return vim.tbl_keys(Hyperlinks.stored_links)
end

return Hyperlinks
47 changes: 47 additions & 0 deletions lua/orgmode/org/id.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
local config = require('orgmode.config')
local utils = require('orgmode.utils')
local state = require('orgmode.state.state')

local OrgId = {
uuid_pattern = '%x%x%x%x%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%x%x%x%x%x%x%x%x',
}

---@return string
function OrgId.new()
return OrgId._generate()
end

---@return boolean
function OrgId.is_valid_uuid(value)
if not value or vim.trim(value) == '' then
return false
end

return value:match(OrgId.uuid_pattern) ~= nil
end

---@private
---@return string
function OrgId._generate()
if config.org_id_method == 'uuid' then
if vim.fn.executable(config.org_id_uuid_program) ~= 1 then
utils.echo_error('org_id_uuid_program is not executable: ' .. config.org_id_uuid_program)
return ''
end
return tostring(vim.fn.system(config.org_id_uuid_program):gsub('%s+', ''))
end

if config.org_id_method == 'ts' then
return tostring(os.date(config.org_id_ts_format))
end

if config.org_id_method == 'org' then
math.randomseed(os.clock() * 100000000000)
return ('%s%s'):format(vim.trim(config.org_id_prefix or ''), math.random(100000000000000))
end

utils.echo_error('Invalid org_id_method: ' .. config.org_id_method)
return ''
end

return OrgId
Loading