diff --git a/docs/configuration.org b/docs/configuration.org index 4dae1b877..a9af671d9 100644 --- a/docs/configuration.org +++ b/docs/configuration.org @@ -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` (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 diff --git a/lua/orgmode/config/_meta.lua b/lua/orgmode/config/_meta.lua index 61276506c..375eca0f5 100644 --- a/lua/orgmode/config/_meta.lua +++ b/lua/orgmode/config/_meta.lua @@ -48,6 +48,9 @@ ---@field org_agenda? string Mappings used to open agenda prompt. Default: 'a' ---@field org_capture? string Mappings used to open capture prompt. Default: 'c' +---@class OrgHyperlinksConfig +---@field sources OrgLinkType[] + ---@class OrgMappingsAgenda ---@field org_agenda_later? string Default: 'f' ---@field org_agenda_earlier? string Default: 'b' @@ -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 diff --git a/lua/orgmode/config/defaults.lua b/lua/orgmode/config/defaults.lua index 921533e8c..6053137e3 100644 --- a/lua/orgmode/config/defaults.lua +++ b/lua/orgmode/config/defaults.lua @@ -82,6 +82,9 @@ local DefaultConfig = { deadline_reminder = true, scheduled_reminder = true, }, + hyperlinks = { + sources = {}, + }, mappings = { disable_all = false, org_return_uses_meta_return = false, diff --git a/lua/orgmode/org/links/init.lua b/lua/orgmode/org/links/init.lua index 0f1bf8255..65d6f468c 100644 --- a/lua/orgmode/org/links/init.lua +++ b/lua/orgmode/org/links/init.lua @@ -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 @@ -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))