Skip to content

feat(hyperlinks): add ability to add custom hyperlink sources #892

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 1 commit into from
Feb 5, 2025
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
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
Loading