Skip to content

Commit 356ff3f

Browse files
feat(hyperlinks): add ability to add custom hyperlink sources (#892)
1 parent d7b4b60 commit 356ff3f

File tree

4 files changed

+98
-3
lines changed

4 files changed

+98
-3
lines changed

Diff for: docs/configuration.org

+74
Original file line numberDiff line numberDiff line change
@@ -2358,6 +2358,80 @@ Hyperlink types supported:
23582358
- Headline title target within the same file (starts with =*=) (Example: =*Specific headline=)
23592359
- Headline with =CUSTOM_ID= property within the same file (starts with =#=) (Example: =#my-custom-id=)
23602360
- Fallback: If file path, opens the file, otherwise, tries to find the headline title in the current file.
2361+
- Your own custom type ([[#custom-hyperlink-types][see below]])
2362+
2363+
**** Custom hyperlink types
2364+
:PROPERTIES:
2365+
:CUSTOM_ID: custom-hyperlink-types
2366+
:END:
2367+
To add your own custom hyperlink type, provide a custom handler to =hyperlinks.sources= setting.
2368+
Each handler needs to have a =get_name()= method that returns a name for the handler.
2369+
Additionally, =follow(link)= and =autocomplete(link)= optional methods are available to open the link and provide the autocompletion.
2370+
Here's an example of adding a custom "ping" hyperlink type that opens the terminal and pings the provided URL
2371+
and provides some autocompletion with few predefined URLs:
2372+
2373+
#+begin_src lua
2374+
local LinkPingType = {}
2375+
2376+
---Unique name for the handler. MUST NOT be one of these: "http", "id", "line_number", "custom_id", "headline"
2377+
---This method is required
2378+
---@return string
2379+
function LinkPingType:get_name()
2380+
return 'ping'
2381+
end
2382+
2383+
---This method is in charge of "executing" the link. For "http" links, it would open the browser, for example.
2384+
---In this example, it will open the terminal and ping the value of the link.
2385+
---The value of the link is passed as an argument.
2386+
---For example, if you have a link [[ping:google.com][ping_google]], doing an `org_open_at_point` (<leader>oo by default)
2387+
---anywhere within the square brackets, will call this method with `ping:google.com` as an argument.
2388+
---It's on this method to figure out what to do with the value.
2389+
---If this method returns `true`, it means that the link was successfully followed.
2390+
---If it returns `false`, it means that this handler cannot handle the link, and it will continue to the next source.
2391+
---This method is optional.
2392+
---@param link string - The current value of the link, for example: "ping:google.com"
2393+
---@return boolean - When true, link was handled, when false, continue to the next source
2394+
function LinkPingType:follow(link)
2395+
if not vim.startswith(link, 'ping:') then
2396+
return false
2397+
end
2398+
-- Get the part after the `ping:` part
2399+
local url = link:sub(6)
2400+
-- Open terminal in vertical split and ping the URL
2401+
vim.cmd('vsplit | term ping ' .. url)
2402+
return true
2403+
end
2404+
2405+
---This is an optional method that will provide autocompletion for your link type.
2406+
---This method needs to pre-filter the list of possible completions based on the current value of the link.
2407+
---For example, if this source has `ping:google.com` and `ping:github.com` as possible completions,
2408+
---And the current value of the link is `ping:go`, this method should return `{'ping:google.com'}`.
2409+
---This method is optional.
2410+
---@param link string - The current value of the link, for example: "ping:go"
2411+
---@return string[]
2412+
function LinkPingType:autocomplete(link)
2413+
local items = {
2414+
'ping:google.com',
2415+
'ping:github.com'
2416+
}
2417+
return vim.tbl_filter(function(item) return vim.startswith(item, link) end, items)
2418+
end
2419+
2420+
require('orgmode').setup({
2421+
hyperlinks = {
2422+
sources = {
2423+
LinkPingType,
2424+
-- Simpler types can be inlined like this:
2425+
{
2426+
get_name = function() return 'my_custom_type' end,
2427+
follow = function(self, link) print('Following link:', link) return true end,
2428+
autocomplete = function(self, link) return {'my_custom_type:my_custom_link'} end
2429+
}
2430+
}
2431+
}
2432+
})
2433+
#+end_src
2434+
23612435
*** Notifications
23622436
:PROPERTIES:
23632437
:CUSTOM_ID: notifications

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@
4848
---@field org_agenda? string Mappings used to open agenda prompt. Default: '<prefix>a'
4949
---@field org_capture? string Mappings used to open capture prompt. Default: '<prefix>c'
5050

51+
---@class OrgHyperlinksConfig
52+
---@field sources OrgLinkType[]
53+
5154
---@class OrgMappingsAgenda
5255
---@field org_agenda_later? string Default: 'f'
5356
---@field org_agenda_earlier? string Default: 'b'
@@ -242,4 +245,5 @@
242245
---@field notifications? OrgNotificationsConfig Notification settings
243246
---@field mappings? OrgMappingsConfig Mappings configuration
244247
---@field emacs_config? OrgEmacsConfig Emacs cnfiguration
245-
---@field ui? OrgUiConfig UI configuration,
248+
---@field ui? OrgUiConfig UI configuration
249+
---@field hyperlinks OrgHyperlinksConfig Custom sources for hyperlinks

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

+3
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ local DefaultConfig = {
8282
deadline_reminder = true,
8383
scheduled_reminder = true,
8484
},
85+
hyperlinks = {
86+
sources = {},
87+
},
8588
mappings = {
8689
disable_all = false,
8790
org_return_uses_meta_return = false,

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

+16-2
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,26 @@ function OrgLinks:new(opts)
2323
types_by_name = {},
2424
}, OrgLinks)
2525
this:_setup_builtin_types()
26+
this:_add_custom_sources()
2627
return this
2728
end
2829

30+
---@private
31+
function OrgLinks:_add_custom_sources()
32+
for i, source in ipairs(config.hyperlinks.sources) do
33+
if type(source.get_name) == 'function' then
34+
self:add_type(source)
35+
else
36+
vim.notify(('Hyperlink source at index %d must have a get_name method'):format(i), vim.log.levels.ERROR)
37+
end
38+
end
39+
end
40+
2941
---@param link string
3042
---@return boolean
3143
function OrgLinks:follow(link)
3244
for _, source in ipairs(self.types) do
33-
if source:follow(link) then
45+
if source.follow and source:follow(link) then
3446
return true
3547
end
3648
end
@@ -54,7 +66,9 @@ function OrgLinks:autocomplete(link)
5466
end, vim.tbl_keys(self.stored_links))
5567

5668
for _, source in ipairs(self.types) do
57-
utils.concat(items, source:autocomplete(link))
69+
if source.autocomplete then
70+
utils.concat(items, source:autocomplete(link))
71+
end
5872
end
5973

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

0 commit comments

Comments
 (0)