Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Enhance file: link support #112

Merged
merged 1 commit into from
Nov 1, 2021
Merged
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
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
test:
nvim --headless --noplugin -u tests/minimal_init.vim -c "PlenaryBustedDirectory tests/plenary/ {minimal_init = 'tests/minimal_init.vim'}"
testfile:
nvim --headless --noplugin -u tests/minimal_init.vim -c "PlenaryBustedFile $(FILE)"
ci:
nvim --noplugin -u tests/minimal_init.vim -c "TSUpdateSync org" -c "qa!" && make test
docs:
42 changes: 33 additions & 9 deletions lua/orgmode/org/autocompletion/omni.lua
Original file line number Diff line number Diff line change
@@ -9,23 +9,38 @@ local data = {
metadata = { 'DEADLINE:', 'SCHEDULED:', 'CLOSED:' },
}

local directives = { rgx = vim.regex([[^\#+\?\w*$]]), line_rgx = vim.regex([[^\#\?+\?\w*$]]), list = data.directives }
local directives = {
line_rgx = vim.regex([[^\#\?+\?\w*$]]),
rgx = vim.regex([[^\#+\?\w*$]]),
list = data.directives,
}

local begin_blocks = {
rgx = vim.regex([[\(^\s*\)\@<=\#+\?\w*$]]),
line_rgx = vim.regex([[^\s*\#\?+\?\w*$]]),
rgx = vim.regex([[\(^\s*\)\@<=\#+\?\w*$]]),
list = data.begin_blocks,
}

local properties = {
line_rgx = vim.regex([[\(^\s\+\|^\s*:\?$\)]]),
rgx = vim.regex([[\(^\|^\s\+\)\@<=:\w*$]]),
extra_cond = function(line, _)
return not string.find(line, 'file:.*$')
end,
list = data.properties,
}

local links = {
line_rgx = vim.regex([[\(\(^\|\s\+\)\[\[\)\@<=\(\*\|\#\)\?\(\w\+\)\?]]),
rgx = vim.regex([[\(\*\|\#\)\?\(\w\+\)\?$]]),
line_rgx = vim.regex([[\(\(^\|\s\+\)\[\[\)\@<=\(\*\|\#\|file:\)\?\(\(\w\|\/\|\.\|\\\|-\|_\|\d\)\+\)\?]]),
rgx = vim.regex([[\(\*\|\#\|file:\)\?\(\(\w\|\/\|\.\|\\\|-\|_\|\d\)\+\)\?$]]),
fetcher = Hyperlinks.find_matching_links,
}
local metadata = { rgx = vim.regex([[\(\s*\)\@<=\w\+$]]), list = data.metadata }

local metadata = {
rgx = vim.regex([[\(\s*\)\@<=\w\+$]]),
list = data.metadata,
}

local tags = {
rgx = vim.regex([[:\([0-9A-Za-z_%@\#]*\)$]]),
fetcher = function()
@@ -38,6 +53,9 @@ local tags = {
local filetags = {
line_rgx = vim.regex([[\c^\#+FILETAGS:\s\+]]),
rgx = vim.regex([[:\([0-9A-Za-z_%@\#]*\)$]]),
extra_cond = function(line, _)
return not string.find(line, 'file:.*$')
end,
fetcher = function()
return vim.tbl_map(function(tag)
return ':' .. tag .. ':'
@@ -46,8 +64,8 @@ local filetags = {
}

local todo_keywords = {
rgx = vim.regex([[\(^\(\*\+\s\+\)\?\)\@<=\w*$]]),
line_rgx = vim.regex([[^\*\+\s\+\w*$]]),
rgx = vim.regex([[\(^\(\*\+\s\+\)\?\)\@<=\w*$]]),
fetcher = function()
return config:get_todo_keywords().ALL
end,
@@ -75,19 +93,25 @@ local function omni(findstart, base)
if findstart == 1 then
for _, context in ipairs(ctx) do
local word = context.rgx:match_str(line)
if word then
if word and (not context.extra_cond or context.extra_cond(line, base)) then
return word
end
end
return -1
end

local fetcher_ctx = { base = base, line = line }
local results = {}

for _, context in ipairs(ctx) do
if (not context.line_rgx or context.line_rgx:match_str(line)) and context.rgx:match_str(base) then
if
(not context.line_rgx or context.line_rgx:match_str(line))
and context.rgx:match_str(base)
and (not context.extra_cond or context.extra_cond(line, base))
then
local items = {}
if context.fetcher then
items = context.fetcher(base)
items = context.fetcher(fetcher_ctx)
else
items = { unpack(context.list) }
end
105 changes: 83 additions & 22 deletions lua/orgmode/org/hyperlinks.lua
Original file line number Diff line number Diff line change
@@ -2,33 +2,87 @@ local Files = require('orgmode.parser.files')
local utils = require('orgmode.utils')
local Hyperlinks = {}

function Hyperlinks.find_by_custom_id_property(base, skip_mapping)
local headlines = Files.get_current_file():find_headlines_with_property_matching('CUSTOM_ID', base:sub(2))
if skip_mapping then
local function get_file_from_context(ctx)
return (
ctx.hyperlinks and ctx.hyperlinks.filepath and Files.get(ctx.hyperlinks.filepath, true)
or Files.get_current_file()
)
end

local function update_hyperlink_ctx(ctx)
if not ctx.line then
return
end

-- TODO: Support text search, see here [https://orgmode.org/manual/External-Links.html]
local hyperlinks_ctx = {
filepath = false,
headline = false,
custom_id = false,
}

local file_match = ctx.line:match('file:(.-)::')
file_match = file_match and vim.fn.fnamemodify(file_match, ':p') or file_match

if file_match and Files.get(file_match) then
hyperlinks_ctx.filepath = Files.get(file_match).filename
hyperlinks_ctx.headline = ctx.line:match('file:.-::(%*.-)$')

if not hyperlinks_ctx.headline then
hyperlinks_ctx.custom_id = ctx.line:match('file:.-::(#.-)$')
end

ctx.base = hyperlinks_ctx.headline or hyperlinks_ctx.custom_id or ctx.base
end

ctx.hyperlinks = hyperlinks_ctx
end

function Hyperlinks.find_by_filepath(ctx)
local filenames = Files.filenames()
local file_base = ctx.base:gsub('^file:', '')
if vim.trim(file_base) ~= '' then
filenames = vim.tbl_filter(function(f)
return f:find('^' .. file_base)
end, filenames)
end

-- Outer checks already filter cases where `ctx.skip_add_prefix` is truthy,
-- so no need to check it here
return vim.tbl_map(function(path)
return 'file:' .. path
end, filenames)
end

function Hyperlinks.find_by_custom_id_property(ctx)
local file = get_file_from_context(ctx)
local headlines = file:find_headlines_with_property_matching('CUSTOM_ID', ctx.base:sub(2))
if ctx.skip_add_prefix then
return headlines
end
return vim.tbl_map(function(headline)
return '#' .. headline.properties.items.CUSTOM_ID
end, headlines)
end

function Hyperlinks.find_by_title_pointer(base, skip_mapping)
local headlines = Files.get_current_file():find_headlines_by_title(base:sub(2))
if skip_mapping then
function Hyperlinks.find_by_title_pointer(ctx)
local file = get_file_from_context(ctx)
local headlines = file:find_headlines_by_title(ctx.base:sub(2))
if ctx.skip_add_prefix then
return headlines
end
return vim.tbl_map(function(headline)
return '*' .. headline.title
end, headlines)
end

function Hyperlinks.find_by_dedicated_target(base, skip_mapping)
if not base or base == '' then
function Hyperlinks.find_by_dedicated_target(ctx)
if not ctx.base or ctx.base == '' then
return {}
end
local term = string.format('<<<?(%s[^>]*)>>>?', base):lower()
local term = string.format('<<<?(%s[^>]*)>>>?', ctx.base):lower()
local headlines = Files.get_current_file():find_headlines_matching_search_term(term, true)
if skip_mapping then
if ctx.skip_add_prefix then
return headlines
end
local targets = {}
@@ -45,32 +99,39 @@ function Hyperlinks.find_by_dedicated_target(base, skip_mapping)
return targets
end

function Hyperlinks.find_by_title(base, skip_mapping)
if not base or base == '' then
function Hyperlinks.find_by_title(ctx)
if not ctx.base or ctx.base == '' then
return {}
end
local headlines = Files.get_current_file():find_headlines_by_title(base)
if skip_mapping then
local headlines = Files.get_current_file():find_headlines_by_title(ctx.base)
if ctx.skip_add_prefix then
return headlines
end
return vim.tbl_map(function(headline)
return headline.title
end, headlines)
end

function Hyperlinks.find_matching_links(base, skip_mapping)
base = vim.trim(base)
local prefix = base:sub(1, 1)
if prefix == '#' then
return Hyperlinks.find_by_custom_id_property(base, skip_mapping)
function Hyperlinks.find_matching_links(ctx)
ctx = ctx or {}
ctx.base = ctx.base and vim.trim(ctx.base) or nil

update_hyperlink_ctx(ctx)

if ctx.base:find('^file:') and not ctx.skip_add_prefix then
return Hyperlinks.find_by_filepath(ctx)
end

local prefix = ctx.base:sub(1, 1)
if prefix == '#' then
return Hyperlinks.find_by_custom_id_property(ctx)
end
if prefix == '*' then
return Hyperlinks.find_by_title_pointer(base, skip_mapping)
return Hyperlinks.find_by_title_pointer(ctx)
end

local results = Hyperlinks.find_by_dedicated_target(base, skip_mapping)
local all = utils.concat(results, Hyperlinks.find_by_title(base, skip_mapping))
local results = Hyperlinks.find_by_dedicated_target(ctx)
local all = utils.concat(results, Hyperlinks.find_by_title(ctx))
return all
end

14 changes: 10 additions & 4 deletions lua/orgmode/org/mappings.lua
Original file line number Diff line number Diff line change
@@ -488,14 +488,20 @@ function OrgMappings:open_at_point()
end
local parts = vim.split(link, '][', true)
local url = parts[1]
local link_ctx = { base = url, skip_add_prefix = true }
if url:find('^file:') then
if url:find(' +') then
if url:find(' +', 1, true) then
parts = vim.split(url, ' +', true)
url = parts[1]
local line_number = parts[2]
return vim.cmd(string.format('edit +%s %s', line_number, url:sub(6)))
end
return vim.cmd(string.format('edit %s', url:sub(6)))

if url:find('^file:(.-)::') then
link_ctx.line = url
else
return vim.cmd(string.format('edit %s', url:sub(6)))
end
end
if url:find('^https?://') then
if not vim.g.loaded_netrwPlugin then
@@ -507,12 +513,12 @@ function OrgMappings:open_at_point()
if stat and stat.type == 'file' then
return vim.cmd(string.format('edit %s', url))
end

local current_headline = Files.get_closest_headline()
local headlines = vim.tbl_filter(function(headline)
return headline.line ~= current_headline.line and headline.id ~= current_headline.id
end, Hyperlinks.find_matching_links(
url,
true
link_ctx
))
if #headlines == 0 then
return
29 changes: 28 additions & 1 deletion tests/plenary/org/autocompletion_spec.lua
Original file line number Diff line number Diff line change
@@ -85,6 +85,10 @@ describe('Autocompletion', function()
result = OrgmodeOmniCompletion(1, '')
assert.are.same(4, result)

mock_line(api, ' [[file:')
result = OrgmodeOmniCompletion(1, '')
assert.are.same(4, result)

mock.revert(api)
end)

@@ -196,7 +200,30 @@ describe('Autocompletion', function()
{ menu = '[Org]', word = ':PRIVATE:' },
}, result)

-- TODO: Add hyperlinks test
-- TODO: Add more hyperlink tests
local MockFiles = mock(Files, true)
local filename = 'work.org'
local headlines = {
{ title = 'Item for work 1' },
{ title = 'Item for work 2' },
}

MockFiles.filenames.returns({ filename })
MockFiles.get.returns({
filename = filename,
find_headlines_by_title = function()
return headlines
end,
})

mock_line(api, string.format(' [[file:%s::*', filename))
result = OrgmodeOmniCompletion(0, '*')
assert.are.same({
{ menu = '[Org]', word = '*' .. headlines[1].title },
{ menu = '[Org]', word = '*' .. headlines[2].title },
}, result)

mock.revert(MockFiles)

mock.revert(api)
end)