-
-
Notifications
You must be signed in to change notification settings - Fork 142
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
[WIP][RFC] Hyperlink Refactor - Registry Version #793
Conversation
831e77a
to
941688f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR!
There's progress from the previous PR, but we still have few things to handle.
Since things are fairly complicated, we can chunk these changes a bit more for now. For example, we can leave autocompleting the links for a separate PR, and for now just build the sources and all the structure around it, and maybe have a follow
on the top links
class. From there we can later add autocompletion.
lua/orgmode/init.lua
Outdated
@@ -20,6 +20,7 @@ local auto_instance_keys = { | |||
---@field capture OrgCapture | |||
---@field clock OrgClock | |||
---@field completion OrgCompletion | |||
---@field links OrgLinkHandlerRegistry |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's name this just OrgLinks
---@field links OrgLinkHandlerRegistry | |
---@field links OrgLinks |
end | ||
|
||
function CustomId:follow() | ||
local headlines = Org.files:get_current_file():find_headlines_with_property_matching('custom_id', self.custom_id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should pass down files
from the top through constructor instead of requiring orgmode
directly.
return utils.echo_warning(('Could not find custom ID "%s".'):format(self.custom_id)) | ||
end | ||
|
||
self.goto_oneof(headlines) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are using self
to call something, call it with a colon notation
self.goto_oneof(headlines) | |
self:goto_oneof(headlines) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to utils
, as it seems more applicable than as a method of Internal
local completions = {} | ||
for _, headline in pairs(headlines) do | ||
local id = headline:get_property('CUSTOM_ID') | ||
table.insert(completions, self:new(id):__tostring()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we are using the metamethod to set up a string format of an entry, we should use tostring(self:new(id))
. Other option is to have a custom to_string
method and call that here directly. I'm ok with any of the options.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Used tostring
, just forgot it existed tbh.
return self | ||
end | ||
|
||
local id = headlines[1]:get_property('id') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we getting the id property here? Ids are handled through a separate handler.
Also, we need to consider that there are multiple headlines here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this was meant to implement the behaviour determined by org-id-link-to-use-id
as described in https://orgmode.org/org.pdf#Handling%20Links.
Upon further reading however, this is resolved on storing a link, rather than on inserting it. I thought it happened at both. Removed
end | ||
|
||
local function autocompletions_filenames(lead) | ||
local Org = require('orgmode') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see this is a very WIP, but just a note that we should not require orgmode directly anywhere. We need to pass down dependencies from top.
---@param target string | nil | ||
---@param disallow_file boolean? | ||
---@return OrgLinkHandlerInternal | OrgLinkHandlerFile | nil | ||
function Internal.parse(target, disallow_file) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This whole class is doing a bit too much, and also has the checks that we are trying to avoid with the sources.
Each source should do this check and know how to parse the value from the target. Some example code:
local sources = { Headline, CustomId, LineNumber, File, Plain }
for _, source in ipairs(sources) do
local parsed = source:parse(target)
if parsed then
return parsed
end
end
This should happen in the top level class, not here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I split this up into a separate handler because these are a bit of a special case, on account of not having a <protocol>:
prefix.
It seemed redundant to me to repeat the <protocol>:
prefix handling in each of the LinkHandler
s individually, so I put this logic in OrgLinks
, but these five cases are special in the fact that they don't have prefixes.
These are also special in that these are the only allowed targets for id:
and file:
links after the ::
, so it made handling of that easier.
file:
is an even more special case because its <protocol>:
prefix is optional, so if we say each 'internal' link should be able to parse itself in its entirety, either File
needs special treatment to parse both the prefixed case and the non-prefixed case, or all LinkHandler
s need to parse their own prefix.
It's a weird set of special cases, and I'm not sure there's a perfect solution, but I'm interested in your thoughts.
end | ||
|
||
function OrgLinkHandlerRegistry:setup_builtin_handlers() | ||
self:add_handler(require('orgmode.org.links.handlers.file')) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should instantiate the handlers here and pass down the dependencies.
for k, v in pairs(Link) do | ||
handler[k] = handler[k] or v | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need this for now since we are not exposing adding custom handlers yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's just as valid for protecting against internal handler's issues as it is for protecting custom handlers
lua/orgmode/org/links/init.lua
Outdated
local function filter_by_prefix(lead) | ||
return function(needle) | ||
if lead == '' or needle:find('^' .. lead) then | ||
return true | ||
end | ||
return false | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part can be just inlined where needed since it's a simple check
local function filter_by_prefix(lead) | |
return function(needle) | |
if lead == '' or needle:find('^' .. lead) then | |
return true | |
end | |
return false | |
end | |
end | |
local function filter_by_prefix(lead) | |
return function(needle) | |
return lead == '' or needle:find('^'..lead) | |
end | |
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Applied
Thanks for the review @kristijanhusak! I've been processing a lot of the comments, but I'm left with a couple of questions:
And I'm currently of half a mind to split up each handler into a local Id = Link:new('id')
function Id:new(id, target, files)
-- Returns instance of Id
end
function Id:follow()
-- Follows this instance
end
function Id:__tostring()
-- Converts to link
end
local IdFactory = {}
function IdFactory:init(files)
self.files = files
end
function IdFactory:new(id, target)
return Id:new(id, target, self.files)
end
---@param input string
function IdFactory:parse(input)
-- Parse id and target
return self:new(id, target)
end
---@param lead string
function IdFactory:complete(lead)
-- Find and return the completions
end
return IdFactory This has the benefit of separating the link's functionality from the functionality to parse the link, (The |
I'll attempt to answer both of your comments with an example. This is what I have in mind: Top level local OrgLinks = {}
function OrgLinks:new(opts)
local this = setmetatable({
files = opts.files,
sources = {},
sources_by_name = {},
}, OrgLinks)
this:setup_builtin_sources()
end
function OrgLinks:setup_builtin_sources()
self:add_source(require('orgmode.org.links.sources.headline'):new({ files = self.files }))
self:add_source(require('orgmode.org.links.sources.custom_id'):new({ files = self.files }))
--- etc
end
function OrgLinks:add_source(source)
if self.sources_by_name[source:get_name()] then
error('Link source ' .. source:get_name() .. ' already exists')
end
self.sources_by_name[source:get_name()] = source
table.insert(self.sources, source)
end
---@param link string
function OrgLinks:follow(link)
-- For example, link = 'file:~/Documents/notes.org::*headline'
for _, source in ipairs(self.sources) do
-- I named this `can_handle`, but we can think of a better name, maybe `source:matches` or `source:handles`
if source:can_handle(link) then
return source:follow(link)
end
-- Alternative is to atempt to follow immediately and return boolean if it was successful or not
if source:follow(link) then
return true
end
end
end
return OrgLinks And then the headline link example would look like this: local OrgLinkHeadline = {}
function OrgLinkHeadline:new(opts)
local this = setmetatable({
files = opts.files,
}, OrgLinkHeadline)
end
---@param link string
function OrgLinkHeadline:can_handle(link)
-- check if link have any of these formats
-- * file:~/Documents/notes.org::*headline
-- * *headline
if 'matches_above_checks' then
return true
end
return false
end
function OrgLinkHeadline:follow(link)
local link_parts = self:parse_link_parts(link)
-- if se use the altnerative approach to follow immediately in the for loop, we can just check if parsing parts was successful or not
local headlines = self.files:find_headlines_by_title(link_parts.headline)
return utils.goto_oenof(headlines)
end
return OrgLinkHeadline And the custom id source would look like this: local OrgLinkCustomId = {}
function OrgLinkCustomId:new(opts)
local this = setmetatable({
files = opts.files,
}, OrgLinkCustomId)
end
---@param link string
function OrgLinkCustomId:can_handle(link)
-- check if link have any of these formats
-- * file:~/Documents/notes.org::#custom-id
-- * #custom-id
if 'matches_above_checks' then
return true
end
return false
end
function OrgLinkCustomId:follow(link)
local link_parts = self:parse_link_parts(link)
-- if se use the altnerative approach to follow immediately in the for loop, we can just check if parsing parts was successful or not
local headlines = self.files:find_headlines_with_property_matching('CUSTOM_ID', link_parts.custom_id)
return utils.goto_oenof(headlines)
end
return OrgCustomId We should not use factories since it complicates things additionally.
We should extract the parsing of it to a util or something and reuse it where necessary. In the top 2 examples we do need it to check if it's a Hope this helps clearing up some of the misunderstandings. |
70c3d46
to
9fed766
Compare
Thanks for the response @kristijanhusak. I'm willing to concede on parsing the protocol prefix within their own handler. I do have a concern about the example you provided however. |
It's just an example. It can be an instance of something, like what currently is |
9fed766
to
7fc4a67
Compare
7fc4a67
to
6b5ca2b
Compare
Hello @kristijanhusak. After some thinking, I've landed on the following option, and wanted to know your opinion on it before I implemented it for everything. The following example described a handler for ---@class OrgFiles
---@class OrgLinkHandler
---@field protocol string
---@field parse fun(self : OrgLinkHandler, input : string): OrgLinkHandler
---@class OrgLinkHandlerId:OrgLinkHandler
---@field private files OrgFiles
---@field private id string
local LinkHandler = {}
---@param opts table
---@return OrgLinkHandler
function LinkHandler:new(opts)
local this = {
files = opts.files,
}
setmetatable(this, self)
self.__index = self
return this
end
---@param input string
---@return OrgLinkHandlerId | nil
function LinkHandler:parse(input)
if not input:match("^id:") then return nil end
return {
id = input:sub(4)
}
end
---@param input OrgLinkHandlerId
function LinkHandler.follow(input)
-- Go to the file defined in `input`
end
---@param input OrgLinkHandlerId
function LinkHandler.__tostring(input)
return 'id:' .. input.id
end The following example describes ---Responsible for delegations to the different LinkHandlers
---@class OrgLinks
---@field private files OrgFiles
---@field private handlers OrgLinkHandler[]
---@field private handlers_by_name table<string, OrgLinkHandler>
local OrgLinks = {
files = {},
handlers = {},
handlers_by_name = {},
}
function OrgLinks:setup_builtin_handlers()
self:add_handler(require('orgmode.org.links.handlers.file'):new({ files = self.files }))
end
---@param handler OrgLinkHandler
function OrgLinks:add_handler(handler)
if self.handlers_by_name[handler.protocol] then
error('Completion handler ' .. handler.protocol .. ' already exists')
end
handler = self:wrap_parse(handler)
self.handlers_by_name[handler.protocol] = handler
table.insert(self.handlers, handler)
end
---@param handler OrgLinkHandler
---@return OrgLinkHandler
function OrgLinks:wrap_parse(handler)
local internal_parse = handler.parse
handler.parse = function(_handler, input)
local this = internal_parse(_handler, input)
setmetatable(this, _handler)
_handler.__index = _handler
return this
end
return handler
end The essential linchpin is
Does this seem sensible, or is this a bad idea? |
I'm not really liking the idea of modifying the handlers in that way.
I don't think we need this anyway.
I believe it will be only a single table. We just need to inject the
I'm not sure what we would share besides
For giving access to the
local OrgLinks = {
files = {},
handlers = {},
handlers_by_name = {},
}
function OrgLinks:setup_builtin_handlers()
-- Note that I removed `files` here.
self:add_handler(require('orgmode.org.links.handlers.file'):new())
end
---@param handler OrgLinkHandler
function OrgLinks:add_handler(handler)
if self.handlers_by_name[handler.protocol] then
error('Completion handler ' .. handler.protocol .. ' already exists')
end
--- assign files to the handler so they can use it
handler.files = self.files
self.handlers_by_name[handler.protocol] = handler
table.insert(self.handlers, handler)
end Let me know if I misunderstood what you are trying to solve here. |
The problem I'm trying to solve is a certain combination of requirements, some self-imposed, some imposed in the review. I will try to enumerate them here. For examples I will use a handler for
I don't think just adding {
id: 'some-file',
files: OrgFiles,
target: {
headline: 'some-title',
files: OrgFiles,
},
} To be able to parse it to something like that, Let me know if anything is unclear, or if you think I missed or misunderstood anything. |
I agree with first 3 points. Regarding the 4th point:
I think we have misunderstanding on what
That's true, but I think we can pass
We should not reference the "parent" class in the handlers. We can have some helpers in another utils file. Here's a more concrete example of how I envisioned it. Mostly copied from my previous comment, with more details around parsing: Top level local OrgLinks = {}
function OrgLinks:new(opts)
local this = setmetatable({
files = opts.files,
sources = {},
sources_by_name = {},
}, OrgLinks)
this:setup_builtin_sources()
end
function OrgLinks:setup_builtin_sources()
self:add_source(require('orgmode.org.links.sources.id'):new({ files = self.files }))
self:add_source(require('orgmode.org.links.sources.custom_id'):new({ files = self.files }))
--- etc
end
function OrgLinks:add_source(source)
if self.sources_by_name[source:get_name()] then
error('Link source ' .. source:get_name() .. ' already exists')
end
self.sources_by_name[source:get_name()] = source
table.insert(self.sources, source)
end
---@param link string
function OrgLinks:follow(link)
for _, source in ipairs(self.sources) do
if source:follow(link) then
return true
end
end
end
return OrgLinks
And then the id handler example would look like this: local OrgLinkId = {}
function OrgLinkId:new(opts)
local this = setmetatable({
files = opts.files,
}, OrgLinkId)
end
---@param link string
---@return boolean
function OrgLinkId:follow(link)
local link_info = self:_parse(link)
if not link_info then
return false
end
local id = link_info.id
-- Part below copied from `mappings:open_at_point`
local files = self.files:find_files_with_property('id', id)
if #files > 0 then
if #files > 1 then
utils.echo_warning(string.format('Multiple files found with id: %s, jumping to first one found', id))
end
vim.cmd(('edit %s'):format(files[1].filename))
return
end
local headlines = self.files:find_headlines_with_property('id', id)
if #headlines == 0 then
return utils.echo_warning(string.format('No headline found with id: %s', id))
end
if #headlines > 1 then
return utils.echo_warning(string.format('Multiple headlines found with id: %s', id))
end
local headline = headlines[1]
end
---Note that this is an internal method and should not be part of interface that handlers need to implement
---@private
---@param link string
---@return { id: string } | nil
function OrgLinkId:_parse(link)
local id = link:match('^id:(.+)$')
if not id then
return nil
end
return { id = id }
end
return OrgLinkId
It's ok if we need to duplicate some of the code, as long as is scoped within a single handler. Once everything is finished, we can easily extract reusable parts. Hope this clarifies things a bit. |
I am sorry, but the structure you propose is not something I would enjoy writing. I do not have the time, nor the energy, to continue pushing this PR anymore. Feel free to use what I have made here as a basis for your own implementation of this. |
I very much appreciate all the time you put into this, and I'm sorry that it became a burden because of my strict guidelines. |
This pull request achieves most of the same goals as #790, but is structured in a similar way to
orgmode.org.autocompletion
.This PR superseeds #790.
TODO: unit tests