Skip to content

Commit

Permalink
feat(hyperlinks): add ability to add custom hyperlink sources (#892)
Browse files Browse the repository at this point in the history
  • Loading branch information
kristijanhusak authored Feb 5, 2025
1 parent d7b4b60 commit 356ff3f
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 3 deletions.
74 changes: 74 additions & 0 deletions docs/configuration.org
Original file line number Diff line number Diff line change
Expand Up @@ -2358,6 +2358,80 @@ Hyperlink types supported:
- Headline title target within the same file (starts with =*=) (Example: =*Specific headline=)
- Headline with =CUSTOM_ID= property within the same file (starts with =#=) (Example: =#my-custom-id=)
- Fallback: If file path, opens the file, otherwise, tries to find the headline title in the current file.
- Your own custom type ([[#custom-hyperlink-types][see below]])

**** Custom hyperlink types
:PROPERTIES:
:CUSTOM_ID: custom-hyperlink-types
:END:
To add your own custom hyperlink type, provide a custom handler to =hyperlinks.sources= setting.
Each handler needs to have a =get_name()= method that returns a name for the handler.
Additionally, =follow(link)= and =autocomplete(link)= optional methods are available to open the link and provide the autocompletion.
Here's an example of adding a custom "ping" hyperlink type that opens the terminal and pings the provided URL
and provides some autocompletion with few predefined URLs:

#+begin_src lua
local LinkPingType = {}

---Unique name for the handler. MUST NOT be one of these: "http", "id", "line_number", "custom_id", "headline"
---This method is required
---@return string
function LinkPingType:get_name()
return 'ping'
end

---This method is in charge of "executing" the link. For "http" links, it would open the browser, for example.
---In this example, it will open the terminal and ping the value of the link.
---The value of the link is passed as an argument.
---For example, if you have a link [[ping:google.com][ping_google]], doing an `org_open_at_point` (<leader>oo by default)
---anywhere within the square brackets, will call this method with `ping:google.com` as an argument.
---It's on this method to figure out what to do with the value.
---If this method returns `true`, it means that the link was successfully followed.
---If it returns `false`, it means that this handler cannot handle the link, and it will continue to the next source.
---This method is optional.
---@param link string - The current value of the link, for example: "ping:google.com"
---@return boolean - When true, link was handled, when false, continue to the next source
function LinkPingType:follow(link)
if not vim.startswith(link, 'ping:') then
return false
end
-- Get the part after the `ping:` part
local url = link:sub(6)
-- Open terminal in vertical split and ping the URL
vim.cmd('vsplit | term ping ' .. url)
return true
end

---This is an optional method that will provide autocompletion for your link type.
---This method needs to pre-filter the list of possible completions based on the current value of the link.
---For example, if this source has `ping:google.com` and `ping:github.com` as possible completions,
---And the current value of the link is `ping:go`, this method should return `{'ping:google.com'}`.
---This method is optional.
---@param link string - The current value of the link, for example: "ping:go"
---@return string[]
function LinkPingType:autocomplete(link)
local items = {
'ping:google.com',
'ping:github.com'
}
return vim.tbl_filter(function(item) return vim.startswith(item, link) end, items)
end

require('orgmode').setup({
hyperlinks = {
sources = {
LinkPingType,
-- Simpler types can be inlined like this:
{
get_name = function() return 'my_custom_type' end,
follow = function(self, link) print('Following link:', link) return true end,
autocomplete = function(self, link) return {'my_custom_type:my_custom_link'} end
}
}
}
})
#+end_src

*** Notifications
:PROPERTIES:
:CUSTOM_ID: notifications
Expand Down
6 changes: 5 additions & 1 deletion lua/orgmode/config/_meta.lua
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
---@field org_agenda? string Mappings used to open agenda prompt. Default: '<prefix>a'
---@field org_capture? string Mappings used to open capture prompt. Default: '<prefix>c'

---@class OrgHyperlinksConfig
---@field sources OrgLinkType[]

---@class OrgMappingsAgenda
---@field org_agenda_later? string Default: 'f'
---@field org_agenda_earlier? string Default: 'b'
Expand Down Expand Up @@ -242,4 +245,5 @@
---@field notifications? OrgNotificationsConfig Notification settings
---@field mappings? OrgMappingsConfig Mappings configuration
---@field emacs_config? OrgEmacsConfig Emacs cnfiguration
---@field ui? OrgUiConfig UI configuration,
---@field ui? OrgUiConfig UI configuration
---@field hyperlinks OrgHyperlinksConfig Custom sources for hyperlinks
3 changes: 3 additions & 0 deletions lua/orgmode/config/defaults.lua
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ local DefaultConfig = {
deadline_reminder = true,
scheduled_reminder = true,
},
hyperlinks = {
sources = {},
},
mappings = {
disable_all = false,
org_return_uses_meta_return = false,
Expand Down
18 changes: 16 additions & 2 deletions lua/orgmode/org/links/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,26 @@ function OrgLinks:new(opts)
types_by_name = {},
}, OrgLinks)
this:_setup_builtin_types()
this:_add_custom_sources()
return this
end

---@private
function OrgLinks:_add_custom_sources()
for i, source in ipairs(config.hyperlinks.sources) do
if type(source.get_name) == 'function' then
self:add_type(source)
else
vim.notify(('Hyperlink source at index %d must have a get_name method'):format(i), vim.log.levels.ERROR)
end
end
end

---@param link string
---@return boolean
function OrgLinks:follow(link)
for _, source in ipairs(self.types) do
if source:follow(link) then
if source.follow and source:follow(link) then
return true
end
end
Expand All @@ -54,7 +66,9 @@ function OrgLinks:autocomplete(link)
end, vim.tbl_keys(self.stored_links))

for _, source in ipairs(self.types) do
utils.concat(items, source:autocomplete(link))
if source.autocomplete then
utils.concat(items, source:autocomplete(link))
end
end

utils.concat(items, self.headline_search:autocomplete(link))
Expand Down

0 comments on commit 356ff3f

Please sign in to comment.