Skip to content

feat(links): Add unused refactored links structure #802

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 4 commits into from
Aug 23, 2024
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
7 changes: 5 additions & 2 deletions lua/orgmode/files/file.lua
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ end
---Load the file
---@return OrgPromise<OrgFile | false>
function OrgFile.load(filename)
local ext = vim.fn.fnamemodify(filename, ':e')
if ext ~= 'org' and ext ~= 'org_archive' then
if not utils.is_org_file(filename) then
return Promise.resolve(false)
end
local bufnr = vim.fn.bufnr(filename) or -1
Expand All @@ -69,6 +68,10 @@ function OrgFile.load(filename)
}))
end

if not vim.loop.fs_stat(filename) then
return Promise.resolve(false)
end

return utils.readfile(filename, { schedule = true }):next(function(lines)
return OrgFile:new({
filename = filename,
Expand Down
5 changes: 1 addition & 4 deletions lua/orgmode/files/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -338,10 +338,7 @@ function OrgFiles:_files(skip_resolve)
all_files = vim.tbl_flatten(all_files)

return vim.tbl_filter(function(file)
local ext = vim.fn.fnamemodify(file, ':e')
local is_org = ext == 'org' or ext == 'org_archive'

if not is_org then
if not utils.is_org_file(file) then
return false
end

Expand Down
6 changes: 5 additions & 1 deletion lua/orgmode/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ local auto_instance_keys = {
org_mappings = true,
notifications = true,
completion = true,
links = true,
}

---@class Org
Expand All @@ -22,6 +23,7 @@ local auto_instance_keys = {
---@field completion OrgCompletion
---@field org_mappings OrgMappings
---@field notifications OrgNotifications
---@field links OrgLinks
local Org = {}
setmetatable(Org, {
__index = function(tbl, key)
Expand Down Expand Up @@ -51,6 +53,7 @@ function Org:init()
paths = require('orgmode.config').org_agenda_files,
})
:load_sync(true, 20000)
self.links = require('orgmode.org.links'):new({ files = self.files })
self.agenda = require('orgmode.agenda'):new({
files = self.files,
})
Expand All @@ -61,11 +64,12 @@ function Org:init()
capture = self.capture,
agenda = self.agenda,
files = self.files,
links = self.links,
})
self.clock = require('orgmode.clock'):new({
files = self.files,
})
self.completion = require('orgmode.org.autocompletion'):new({ files = self.files })
self.completion = require('orgmode.org.autocompletion'):new({ files = self.files, links = self.links })
self.statusline_debounced = require('orgmode.utils').debounce('statusline', function()
return self.clock:get_statusline()
end, 300)
Expand Down
4 changes: 3 additions & 1 deletion lua/orgmode/org/autocompletion/init.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---@class OrgCompletion
---@field files OrgFiles
---@field links OrgLinks
---@field private sources OrgCompletionSource[]
---@field private sources_by_name table<string, OrgCompletionSource>
---@field menu string
Expand All @@ -8,10 +9,11 @@ local OrgCompletion = {
}
OrgCompletion.__index = OrgCompletion

---@param opts { files: OrgFiles }
---@param opts { files: OrgFiles, links: OrgLinks }
function OrgCompletion:new(opts)
local this = setmetatable({
files = opts.files,
links = opts.links,
sources = {},
sources_by_name = {},
}, OrgCompletion)
Expand Down
6 changes: 6 additions & 0 deletions lua/orgmode/org/links/_meta.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---@meta

---@class OrgLinkType
---@field get_name fun(self: OrgLinkType): string
---@field follow fun(self: OrgLinkType, link: string): boolean
---@field autocomplete fun(self: OrgLinkType, link: string): string[]
81 changes: 81 additions & 0 deletions lua/orgmode/org/links/hyperlink.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
local OrgLinkUrl = require('orgmode.org.links.url')
local Range = require('orgmode.files.elements.range')

---@class OrgHyperlink
---@field url OrgLinkUrl
---@field desc string | nil
---@field range? OrgRange
local OrgHyperlink = {}

local pattern = '%[%[([^%]]+.-)%]%]'

---@param str string
---@param range? OrgRange
---@return OrgHyperlink
function OrgHyperlink:new(str, range)
local this = setmetatable({}, { __index = OrgHyperlink })
local parts = vim.split(str, '][', { plain = true })
this.url = OrgLinkUrl:new(parts[1] or '')
this.desc = parts[2]
this.range = range
return this
end

---@return string
function OrgHyperlink:to_str()
if self.desc then
return string.format('[[%s][%s]]', self.url:to_string(), self.desc)
else
return string.format('[[%s]]', self.url:to_string())
end
end

---@param line string
---@param pos number
---@return OrgHyperlink | nil, { from: number, to: number } | nil
function OrgHyperlink.at_pos(line, pos)
local links = {}
local found_link = nil
local position
for link in line:gmatch(pattern) do
local start_from = #links > 0 and links[#links].to or nil
local from, to = line:find(pattern, start_from)
local current_pos = { from = from, to = to }
if pos >= from and pos <= to then
found_link = link
position = current_pos
break
end
table.insert(links, current_pos)
end
if not found_link then
return nil, nil
end
return OrgHyperlink:new(found_link), position
end

---@return OrgHyperlink | nil, { from: number, to: number } | nil
function OrgHyperlink.at_cursor()
local line = vim.fn.getline('.')
local col = vim.fn.col('.') or 0
return OrgHyperlink.at_pos(line, col)
end

---@return OrgHyperlink[]
function OrgHyperlink.all_from_line(line, line_number)
local links = {}
for link in line:gmatch(pattern) do
local start_from = #links > 0 and links[#links].to or nil
local from, to = line:find(pattern, start_from)
if from and to then
local range = Range.from_line(line_number)
range.start_col = from
range.end_col = to
table.insert(links, OrgHyperlink:new(link, range))
end
end

return links
end

return OrgHyperlink
168 changes: 168 additions & 0 deletions lua/orgmode/org/links/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
local config = require('orgmode.config')
local utils = require('orgmode.utils')
local OrgLinkUrl = require('orgmode.org.links.url')
local OrgHyperlink = require('orgmode.org.links.hyperlink')

---@class OrgLinks:OrgLinkType
---@field private files OrgFiles
---@field private types OrgLinkType[]
---@field private types_by_name table<string, OrgLinkType>
---@field private stored_links table<string, string>
---@field private headline_search OrgLinkHeadlineSearch
local OrgLinks = {
stored_links = {},
}
OrgLinks.__index = OrgLinks

---@param opts { files: OrgFiles }
function OrgLinks:new(opts)
local this = setmetatable({
files = opts.files,
types = {},
types_by_name = {},
}, OrgLinks)
this:_setup_builtin_types()
return this
end

---@param link string
---@return boolean
function OrgLinks:follow(link)
for _, source in ipairs(self.types) do
if source:follow(link) then
return true
end
end

local org_link_url = OrgLinkUrl:new(link)
if org_link_url.protocol and org_link_url.protocol ~= 'file' and org_link_url.protocol ~= 'id' then
utils.echo_warning(string.format('Unsupported link protocol: %q', org_link_url.protocol))
return false
end

return self.headline_search:follow(link)
end

---@param link string
---@return string[]
function OrgLinks:autocomplete(link)
local pattern = '^' .. vim.pesc(link:lower())

local items = vim.tbl_filter(function(stored_link)
return stored_link:lower():match(pattern)
end, vim.tbl_keys(self.stored_links))

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

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

---@param headline OrgHeadline
function OrgLinks:store_link_to_headline(headline)
self.stored_links[self:get_link_to_headline(headline)] = headline:get_title()
end

---@param headline OrgHeadline
---@return string
function OrgLinks:get_link_to_headline(headline)
local title = headline:get_title()

if config.org_id_link_to_org_use_id then
local id = headline:id_get_or_create()
if id then
return ('id:%s::*%s'):format(id, title)
end
end

return ('file:%s::*%s'):format(headline.file.filename, title)
end

---@param file OrgFile
---@return string
function OrgLinks:get_link_to_file(file)
local title = file:get_title()

if config.org_id_link_to_org_use_id then
local id = file:id_get_or_create()
if id then
return ('id:%s::*%s'):format(id, title)
end
end

return ('file:%s::*%s'):format(file.filename, title)
end

---@param link_location string
function OrgLinks:insert_link(link_location)
local selected_link = OrgHyperlink:new(link_location)
local desc = selected_link.url:get_target()
if desc and (desc:match('^%*') or desc:match('^#')) then
desc = desc:sub(2)
end

if selected_link.url:get_protocol() == 'id' then
link_location = ('id:%s'):format(selected_link.url:get_path())
end

local link_description = vim.trim(vim.fn.OrgmodeInput('Description: ', desc or ''))

link_location = '[' .. vim.trim(link_location) .. ']'

if link_description ~= '' then
link_description = '[' .. link_description .. ']'
end

local insert_from
local insert_to
local target_col = #link_location + #link_description + 2

-- check if currently on link
local link, position = OrgHyperlink.at_cursor()
if link and position then
insert_from = position.from - 1
insert_to = position.to + 1
target_col = target_col + position.from
else
local colnr = vim.fn.col('.')
insert_from = colnr
insert_to = colnr + 1
target_col = target_col + colnr
end

local linenr = vim.fn.line('.') or 0
local curr_line = vim.fn.getline(linenr)
local new_line = string.sub(curr_line, 0, insert_from)
.. '['
.. link_location
.. link_description
.. ']'
.. string.sub(curr_line, insert_to, #curr_line)

vim.fn.setline(linenr, new_line)
vim.fn.cursor(linenr, target_col)
end

---@param link_type OrgLinkType
function OrgLinks:add_type(link_type)
if self.types_by_name[link_type:get_name()] then
error('Link type ' .. link_type:get_name() .. ' already exists')
end
self.types_by_name[link_type:get_name()] = link_type
table.insert(self.types, link_type)
end

---@private
function OrgLinks:_setup_builtin_types()
self:add_type(require('orgmode.org.links.types.http'):new({ files = self.files }))
self:add_type(require('orgmode.org.links.types.id'):new({ files = self.files }))
self:add_type(require('orgmode.org.links.types.line_number'):new({ files = self.files }))
self:add_type(require('orgmode.org.links.types.custom_id'):new({ files = self.files }))
self:add_type(require('orgmode.org.links.types.headline'):new({ files = self.files }))

self.headline_search = require('orgmode.org.links.types.headline_search'):new({ files = self.files })
end

return OrgLinks
Loading
Loading