Skip to content

Commit 58b33f4

Browse files
feat(footnotes): Use footnotes from tree-sitter grammar (#1140)
1 parent a829442 commit 58b33f4

10 files changed

Lines changed: 72 additions & 232 deletions

File tree

docs/configuration.org

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2969,6 +2969,8 @@ The following highlight groups are used:
29692969
- =@org.agenda.today=: Highlight for today in Agenda view - linked to =@org.bold=
29702970
- =@org.agenda.weekend=: Highlight for weekend days in Agenda view - linked to =@org.bold=
29712971
- =@org.agenda.weekend.today=: Highlight for today when it is a weekend day in Agenda view - linked to =@org.bold=
2972+
- =@org.footnote=: Highlight for footnote definition - linked to =@markup.link.url=
2973+
- =@org.footnote.reference=: Highlight for footnote reference - linked to =@markup.link.url=
29722974

29732975
📝 NOTE:
29742976
Colors used for todo keywords and agenda states (deadline, schedule ok,

lua/orgmode/colors/highlighter/markup/footnotes.lua

Lines changed: 0 additions & 133 deletions
This file was deleted.

lua/orgmode/colors/highlighter/markup/init.lua

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ end
2222
function OrgMarkup:_init_highlighters()
2323
self.parsers = {
2424
emphasis = require('orgmode.colors.highlighter.markup.emphasis'):new({ markup = self }),
25-
footnote = require('orgmode.colors.highlighter.markup.footnotes'):new({ markup = self }),
2625
latex = require('orgmode.colors.highlighter.markup.latex'):new({ markup = self }),
2726
}
2827
end
@@ -77,7 +76,6 @@ function OrgMarkup:get_node_highlights(root_node, source, line)
7776
local result = {
7877
emphasis = {},
7978
latex = {},
80-
footnote = {},
8179
}
8280
---@type OrgMarkupNode[]
8381
local entries = {}

lua/orgmode/colors/highlights.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ function M.link_highlights()
6767
['@org.latex'] = '@markup.math',
6868
['@org.latex_env'] = '@markup.environment',
6969
['@org.footnote'] = '@markup.link.url',
70+
['@org.footnote.reference'] = '@markup.link.url',
7071
-- Other
7172
['@org.table.delimiter'] = '@punctuation.special',
7273
['@org.table.heading'] = '@markup.heading',

lua/orgmode/files/file.lua

Lines changed: 21 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -836,59 +836,38 @@ function OrgFile:get_links()
836836
return links
837837
end
838838

839-
memoize('get_footnote_references')
840-
---@return OrgFootnote[]
841-
function OrgFile:get_footnote_references()
839+
---@param footnote_reference OrgFootnote
840+
---@return OrgFootnote | nil
841+
function OrgFile:find_footnote_definition(footnote_reference)
842842
self:parse(true)
843-
local ts_query = ts_utils.get_query([[
844-
(paragraph (expr) @footnotes)
845-
(drawer (contents (expr) @footnotes))
846-
(headline (item (expr)) @footnotes)
847-
(fndef) @footnotes
848-
]])
843+
local ts_query = ts_utils.get_query(([[
844+
(fndef label: (expr) @_label (#eq? @_label "%s")) @footnotes
845+
]]):format(footnote_reference.label))
849846

850-
local footnotes = {}
851-
local processed_lines = {}
852847
for _, match in ts_query:iter_captures(self.root, self:get_source()) do
853-
local line_start, _, line_end = match:range()
854-
if not processed_lines[line_start] then
855-
if line_start == line_end then
856-
vim.list_extend(footnotes, Footnote.all_from_line(self.lines[line_start + 1], line_start + 1))
857-
processed_lines[line_start] = true
858-
else
859-
for line = line_start, line_end - 1 do
860-
vim.list_extend(footnotes, Footnote.all_from_line(self.lines[line + 1], line + 1))
861-
processed_lines[line] = true
862-
end
863-
end
848+
if match and match:type() == 'fndef' then
849+
return Footnote.from_node(match, self:get_source())
864850
end
865851
end
866-
return footnotes
867852
end
868853

869-
---@param footnote_reference OrgFootnote
854+
---@param footnote_definition OrgFootnote
870855
---@return OrgFootnote | nil
871-
function OrgFile:find_footnote(footnote_reference)
872-
local footnotes = self:get_footnote_references()
873-
for i = #footnotes, 1, -1 do
874-
if footnotes[i].value:lower() == footnote_reference.value:lower() and footnotes[i].range.start_col == 1 then
875-
return footnotes[i]
876-
end
877-
end
878-
end
856+
function OrgFile:find_footnote_reference(footnote_definition)
857+
self:parse(true)
858+
local ts_query = ts_utils.get_query(([[
859+
(fnref label: (expr) @_label (#eq? @_label "%s")) @footnotes
860+
]]):format(footnote_definition.label))
879861

880-
---@param footnote OrgFootnote
881-
---@return OrgFootnote | nil
882-
function OrgFile:find_footnote_reference(footnote)
883-
local footnotes = self:get_footnote_references()
884-
for i = #footnotes, 1, -1 do
885-
if
886-
footnotes[i].value:lower() == footnote.value:lower()
887-
and footnotes[i].range.start_line < footnote.range.start_line
888-
then
889-
return footnotes[i]
862+
local matches = {}
863+
for _, match in ts_query:iter_captures(self.root, self:get_source()) do
864+
if match:type() == 'fnref' then
865+
table.insert(matches, match)
890866
end
891867
end
868+
if #matches > 0 then
869+
return Footnote.from_node(matches[#matches], self:get_source())
870+
end
892871
end
893872

894873
memoize('get_directive')

lua/orgmode/objects/footnote.lua

Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,45 @@
11
local Range = require('orgmode.files.elements.range')
2+
local ts_utils = require('orgmode.utils.treesitter')
23

34
---@class OrgFootnote
4-
---@field value string
5-
---@field range? OrgRange
5+
---@field label string
6+
---@field range OrgRange
7+
---@field is_reference boolean
68
local OrgFootnote = {}
79
OrgFootnote.__index = OrgFootnote
810

9-
local pattern = '%[fn:[^%]]+%]'
10-
11-
---@param str string
12-
---@param range? OrgRange
11+
---@param label string
12+
---@param range OrgRange
13+
---@param is_reference boolean
1314
---@return OrgFootnote
14-
function OrgFootnote:new(str, range)
15+
function OrgFootnote:new(label, range, is_reference)
1516
local this = setmetatable({}, { __index = OrgFootnote })
16-
this.value = str
17+
this.label = label
1718
this.range = range
19+
this.is_reference = is_reference or false
1820
return this
1921
end
2022

2123
function OrgFootnote:get_name()
22-
local name = self.value:match('^%[fn:([^%]]+)%]$')
23-
return name
24+
return self.label
2425
end
2526

27+
---@param node TSNode | nil
28+
---@param source? number | string
2629
---@return OrgFootnote | nil
27-
function OrgFootnote.at_cursor()
28-
local line_nr = vim.fn.line('.')
29-
local col = vim.fn.col('.') or 0
30-
local on_line = OrgFootnote.all_from_line(vim.fn.getline('.'), line_nr)
30+
function OrgFootnote.from_node(node, source)
31+
local fnode = ts_utils.closest_node(ts_utils.get_node(), { 'fnref', 'fndef' })
32+
if not fnode then
33+
return nil
34+
end
3135

32-
return vim.iter(on_line):find(function(footnote)
33-
return footnote.range:is_in_range(line_nr, col)
34-
end)
36+
local text = vim.treesitter.get_node_text(fnode:field('label')[1], source or 0)
37+
return OrgFootnote:new(text, Range.from_node(node), fnode:type() == 'fnref')
3538
end
3639

37-
---@return OrgFootnote[]
38-
function OrgFootnote.all_from_line(line, line_number)
39-
local links = {}
40-
for link in line:gmatch(pattern) do
41-
local start_from = #links > 0 and links[#links].range.end_col or nil
42-
local from, to = line:find(pattern, start_from)
43-
if from and to then
44-
local range = Range.from_line(line_number)
45-
range.start_col = from
46-
range.end_col = to
47-
table.insert(links, OrgFootnote:new(link, range))
48-
end
49-
end
50-
51-
return links
40+
---@return OrgFootnote | nil
41+
function OrgFootnote.at_cursor()
42+
return OrgFootnote.from_node(ts_utils.get_node(), vim.api.nvim_get_current_buf())
5243
end
5344

5445
return OrgFootnote

lua/orgmode/org/mappings.lua

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -917,14 +917,28 @@ function OrgMappings:open_at_point()
917917

918918
local footnote = Footnote.at_cursor()
919919
if footnote then
920-
return self:_jump_to_footnote(footnote)
920+
if footnote.is_reference then
921+
return self:_jump_to_footnote_definition(footnote)
922+
end
923+
return self:_jump_to_footnote_reference(footnote)
921924
end
922925
end
923926

927+
function OrgMappings:_jump_to_footnote_reference(footnote_definition)
928+
local file = self.files:get_current_file()
929+
local reference = file:find_footnote_reference(footnote_definition)
930+
931+
if not reference then
932+
return utils.echo_info(('Cannot find reference for footnote "%s"'):format(footnote_definition:get_name()))
933+
end
934+
935+
return vim.fn.cursor({ reference.range.start_line, reference.range.start_col })
936+
end
937+
924938
---@param footnote_reference OrgFootnote
925-
function OrgMappings:_jump_to_footnote(footnote_reference)
939+
function OrgMappings:_jump_to_footnote_definition(footnote_reference)
926940
local file = self.files:get_current_file()
927-
local footnote = file:find_footnote(footnote_reference)
941+
local footnote = file:find_footnote_definition(footnote_reference)
928942

929943
if not footnote then
930944
local choice = vim.fn.confirm('No footnote found. Create one?', '&Yes\n&No')
@@ -933,31 +947,20 @@ function OrgMappings:_jump_to_footnote(footnote_reference)
933947
end
934948

935949
local footnotes_headline = file:find_headline_by_title('footnotes')
950+
local fndef = ('[fn:%s] '):format(footnote_reference.label)
936951
if footnotes_headline then
937952
local append_line = footnotes_headline:get_append_line()
938-
vim.api.nvim_buf_set_lines(0, append_line, append_line, false, { footnote_reference.value .. ' ' })
939-
vim.fn.cursor({ append_line + 1, #footnote_reference.value + 1 })
953+
vim.api.nvim_buf_set_lines(0, append_line, append_line, false, { fndef })
954+
vim.fn.cursor({ append_line + 1, #fndef })
940955
return vim.cmd('startinsert!')
941956
end
942957
local last_line = vim.api.nvim_buf_line_count(0)
943-
vim.api.nvim_buf_set_lines(0, last_line, last_line, false, { '', '* Footnotes', footnote_reference.value .. ' ' })
944-
vim.fn.cursor({ last_line + 3, #footnote_reference.value + 1 })
958+
vim.api.nvim_buf_set_lines(0, last_line, last_line, false, { '', '* Footnotes', fndef })
959+
vim.fn.cursor({ last_line + 3, #fndef })
945960
return vim.cmd('startinsert!')
946961
end
947962

948-
local is_footnote_marker = footnote.range:is_same(footnote_reference.range)
949-
950-
if not is_footnote_marker then
951-
return vim.fn.cursor({ footnote.range.start_line, footnote.range.start_col })
952-
end
953-
954-
local reference = file:find_footnote_reference(footnote)
955-
956-
if reference then
957-
return vim.fn.cursor({ reference.range.start_line, reference.range.start_col })
958-
end
959-
960-
utils.echo_info(('Cannot find reference for footnote "%s"'):format(footnote_reference:get_name()))
963+
return vim.fn.cursor({ footnote.range.start_line, footnote.range.start_col })
961964
end
962965

963966
function OrgMappings:export()

0 commit comments

Comments
 (0)