Skip to content

Commit 6fb42c4

Browse files
feat: Highlight dates with tree-sitter
1 parent 59b1329 commit 6fb42c4

File tree

10 files changed

+323
-104
lines changed

10 files changed

+323
-104
lines changed

lua/orgmode/colors/highlighter/markup/_meta.lua

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
---@meta
22
---@alias OrgMarkupRange { line: number, start_col: number, end_col: number }
33

4-
---@alias OrgMarkupParserType 'emphasis' | 'link' | 'latex'
4+
---@alias OrgMarkupParserType 'emphasis' | 'link' | 'latex' | 'date'
55

66
---@class OrgMarkupNode
77
---@field type OrgMarkupParserType
88
---@field char string
9-
---@field seek_char string
9+
---@field id string
10+
---@field seek_id string
1011
---@field nestable boolean
1112
---@field node TSNode
1213
---@field range OrgMarkupRange
@@ -15,7 +16,7 @@
1516
---@class OrgMarkupHighlight
1617
---@field from OrgMarkupRange
1718
---@field to OrgMarkupRange
18-
---@field type string
19+
---@field char string
1920

2021
---@class OrgMarkupHighlighter
2122
---@field parse_node fun(self: OrgMarkupHighlighter, node: TSNode): OrgMarkupNode | false
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
---@class OrgDatesHighlighter : OrgMarkupHighlighter
2+
---@field private markup OrgMarkupHighlighter
3+
local OrgDates = {}
4+
5+
---@param opts { markup: OrgMarkupHighlighter }
6+
function OrgDates:new(opts)
7+
local data = {
8+
markup = opts.markup,
9+
}
10+
setmetatable(data, self)
11+
self.__index = self
12+
return data
13+
end
14+
15+
---@param node TSNode
16+
---@return OrgMarkupNode | false
17+
function OrgDates:parse_node(node)
18+
local type = node:type()
19+
if type == '[' or type == '<' then
20+
return self:_parse_start_node(node)
21+
end
22+
23+
if type == ']' or type == '>' then
24+
return self:_parse_end_node(node)
25+
end
26+
27+
return false
28+
end
29+
30+
---@private
31+
---@param node TSNode
32+
---@return OrgMarkupNode | false
33+
function OrgDates:_parse_start_node(node)
34+
local node_type = node:type()
35+
local prev_sibling = node:prev_sibling()
36+
-- Ignore links
37+
if prev_sibling and (node_type == '[' and prev_sibling:type() == '[') then
38+
return false
39+
end
40+
local expected_next_siblings = {
41+
{
42+
type = 'num',
43+
length = 4,
44+
},
45+
{
46+
type = '-',
47+
length = 1,
48+
},
49+
{
50+
type = 'num',
51+
length = 2,
52+
},
53+
{
54+
type = '-',
55+
length = 1,
56+
},
57+
{
58+
type = 'num',
59+
length = 2,
60+
},
61+
}
62+
local next_sibling = node:next_sibling()
63+
64+
for _, sibling in ipairs(expected_next_siblings) do
65+
if not next_sibling or next_sibling:type() ~= sibling.type then
66+
return false
67+
end
68+
local _, sc, _, ec = next_sibling:range()
69+
if (ec - sc) ~= sibling.length then
70+
return false
71+
end
72+
next_sibling = next_sibling:next_sibling()
73+
end
74+
local id = table.concat({ 'date', node_type }, '_')
75+
local seek_id = table.concat({ 'date', node_type == '[' and ']' or '>' }, '_')
76+
77+
return {
78+
type = 'date',
79+
id = id,
80+
char = node_type,
81+
seek_id = seek_id,
82+
nestable = false,
83+
range = self.markup:node_to_range(node),
84+
node = node,
85+
}
86+
end
87+
88+
---@private
89+
---@param node TSNode
90+
---@return OrgMarkupNode | false
91+
function OrgDates:_parse_end_node(node)
92+
local node_type = node:type()
93+
local prev_sibling = node:prev_sibling()
94+
local next_sibling = node:next_sibling()
95+
local is_prev_sibling_valid = not prev_sibling or prev_sibling:type() == 'str' or prev_sibling:type() == 'num'
96+
-- Ensure it's not a link
97+
local is_next_sibling_valid = not next_sibling or (node_type == ']' and next_sibling:type() ~= ']')
98+
if is_prev_sibling_valid and is_next_sibling_valid then
99+
local id = table.concat({ 'date', node_type }, '_')
100+
local seek_id = table.concat({ 'date', node_type == ']' and '[' or '<' }, '_')
101+
102+
return {
103+
type = 'date',
104+
id = id,
105+
seek_id = seek_id,
106+
char = node_type,
107+
nestable = false,
108+
range = self.markup:node_to_range(node),
109+
node = node,
110+
}
111+
end
112+
113+
return false
114+
end
115+
116+
---@param entry OrgMarkupNode
117+
---@return boolean
118+
function OrgDates:is_valid_start_node(entry)
119+
return entry.type == 'date' and (entry.id == 'date_[' or entry.id == 'date_<')
120+
end
121+
122+
---@param entry OrgMarkupNode
123+
---@return boolean
124+
function OrgDates:is_valid_end_node(entry)
125+
return entry.type == 'date' and (entry.id == 'date_]' or entry.id == 'date_>')
126+
end
127+
128+
---@param highlights OrgMarkupHighlight[]
129+
---@param bufnr number
130+
function OrgDates:highlight(highlights, bufnr)
131+
local namespace = self.markup.highlighter.namespace
132+
133+
for _, entry in ipairs(highlights) do
134+
vim.api.nvim_buf_set_extmark(bufnr, namespace, entry.from.line, entry.from.start_col, {
135+
ephemeral = true,
136+
end_col = entry.to.end_col,
137+
hl_group = entry.char == '>' and 'OrgTSTimestampActive' or 'OrgTSTimestampInactive',
138+
priority = 110,
139+
})
140+
end
141+
end
142+
143+
---@param item OrgMarkupNode
144+
---@return boolean
145+
function OrgDates:has_valid_parent(item)
146+
---At this point we know that node has 2 valid parents
147+
local parent = item.node:parent():parent()
148+
149+
if parent and parent:type() == 'value' then
150+
return parent:parent() and parent:parent():type() == 'property' or false
151+
end
152+
153+
return false
154+
end
155+
156+
return OrgDates

lua/orgmode/colors/highlighter/markup/emphasis.lua

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,26 +68,26 @@ function OrgEmphasis:highlight(highlights, bufnr)
6868
vim.api.nvim_buf_set_extmark(bufnr, namespace, entry.from.line, entry.from.start_col, {
6969
ephemeral = true,
7070
end_col = entry.from.start_col + hl_offset,
71-
hl_group = markers[entry.type].hl_name .. '_delimiter',
72-
spell = markers[entry.type].spell,
71+
hl_group = markers[entry.char].hl_name .. '_delimiter',
72+
spell = markers[entry.char].spell,
7373
priority = 110 + entry.from.start_col,
7474
})
7575

7676
-- Closing delimiter
7777
vim.api.nvim_buf_set_extmark(bufnr, namespace, entry.from.line, entry.to.end_col - hl_offset, {
7878
ephemeral = true,
7979
end_col = entry.to.end_col,
80-
hl_group = markers[entry.type].hl_name .. '_delimiter',
81-
spell = markers[entry.type].spell,
80+
hl_group = markers[entry.char].hl_name .. '_delimiter',
81+
spell = markers[entry.char].spell,
8282
priority = 110 + entry.from.start_col,
8383
})
8484

8585
-- Main body highlight
8686
vim.api.nvim_buf_set_extmark(bufnr, namespace, entry.from.line, entry.from.start_col + hl_offset, {
8787
ephemeral = true,
8888
end_col = entry.to.end_col - hl_offset,
89-
hl_group = markers[entry.type].hl_name,
90-
spell = markers[entry.type].spell,
89+
hl_group = markers[entry.char].hl_name,
90+
spell = markers[entry.char].spell,
9191
priority = 110 + entry.from.start_col,
9292
})
9393

@@ -109,16 +109,19 @@ end
109109
---@param node TSNode
110110
---@return OrgMarkupNode | false
111111
function OrgEmphasis:parse_node(node)
112-
local type = node:type()
113-
if not markers[type] then
112+
local node_type = node:type()
113+
if not markers[node_type] then
114114
return false
115115
end
116116

117+
local id = table.concat({'emphasis', node_type}, '_')
118+
117119
return {
118120
type = 'emphasis',
119-
char = type,
120-
seek_char = type,
121-
nestable = markers[type].nestable,
121+
char = node_type,
122+
id = id,
123+
seek_id = id,
124+
nestable = markers[node_type].nestable,
122125
range = self.markup:node_to_range(node),
123126
node = node,
124127
}

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

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ function OrgMarkup:_init_highlighters()
2323
self.parsers = {
2424
emphasis = require('orgmode.colors.highlighter.markup.emphasis'):new({ markup = self }),
2525
link = require('orgmode.colors.highlighter.markup.link'):new({ markup = self }),
26+
date = require('orgmode.colors.highlighter.markup.dates'):new({ markup = self }),
2627
latex = require('orgmode.colors.highlighter.markup.latex'):new({ markup = self }),
2728
}
2829
end
@@ -54,6 +55,7 @@ function OrgMarkup:_get_highlights(bufnr, line, tree)
5455
emphasis = {},
5556
link = {},
5657
latex = {},
58+
date = {},
5759
}
5860
---@type OrgMarkupNode[]
5961
local entries = {}
@@ -81,34 +83,35 @@ function OrgMarkup:_get_highlights(bufnr, line, tree)
8183
if last_seek and not last_seek.nestable then
8284
return false
8385
end
84-
if not self:has_valid_parent(item.node:parent()) then
86+
if not self:has_valid_parent(item) then
8587
return false
8688
end
8789
return self.parsers[item.type]:is_valid_start_node(item, bufnr)
8890
end
8991

9092
local is_valid_end_item = function(item)
91-
if not self:has_valid_parent(item.node:parent()) then
93+
if not self:has_valid_parent(item) then
9294
return false
9395
end
9496

9597
return self.parsers[item.type]:is_valid_end_node(item, bufnr)
9698
end
9799

98100
for _, item in ipairs(entries) do
99-
local from = seek[item.seek_char]
101+
local from = seek[item.seek_id]
100102

101103
if not from and not item.self_contained then
102104
if is_valid_start_item(item) then
103-
seek[item.char] = item
105+
seek[item.id] = item
104106
last_seek = item
105107
end
106108
goto continue
107109
end
108110

109111
if is_valid_end_item(item) then
110112
table.insert(result[item.type], {
111-
type = item.char,
113+
id = item.id,
114+
char = item.char,
112115
from = item.self_contained and item.range or from.range,
113116
to = item.range,
114117
})
@@ -122,7 +125,7 @@ function OrgMarkup:_get_highlights(bufnr, line, tree)
122125
goto continue
123126
end
124127

125-
seek[item.seek_char] = nil
128+
seek[item.seek_id] = nil
126129
for t, pos in pairs(seek) do
127130
if
128131
pos.range.line == from.range.line
@@ -180,12 +183,16 @@ function OrgMarkup:node_to_range(node)
180183
}
181184
end
182185

183-
---@param node? TSNode
184-
function OrgMarkup:has_valid_parent(node)
185-
if not node then
186+
---@param item OrgMarkupNode
187+
---@return boolean
188+
function OrgMarkup:has_valid_parent(item)
189+
-- expr
190+
local parent = item.node:parent()
191+
if not parent then
186192
return false
187193
end
188-
local parent = node:parent()
194+
195+
parent = parent:parent()
189196
if not parent then
190197
return false
191198
end
@@ -204,6 +211,10 @@ function OrgMarkup:has_valid_parent(node)
204211
return p:type() == 'drawer' or p:type() == 'cell'
205212
end
206213

214+
if self.parsers[item.type].has_valid_parent then
215+
return self.parsers[item.type]:has_valid_parent(item)
216+
end
217+
207218
return false
208219
end
209220

0 commit comments

Comments
 (0)