Skip to content

Commit 156d8c5

Browse files
Setup markup rendering from treesitter
1 parent b9de38e commit 156d8c5

File tree

5 files changed

+284
-88
lines changed

5 files changed

+284
-88
lines changed

lua/orgmode/colors/markup_highlighter.lua

+226-85
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
local config = require('orgmode.config')
2-
local buf_blocks = {}
2+
local ts_utils = require('nvim-treesitter.ts_utils')
3+
local query = nil
34

45
local valid_pre_marker_chars = { ' ', '(', '-', "'", '"', '{' }
56
local valid_post_marker_chars = { ' ', ')', '-', '}', '"', "'", ':', ';', '!', '\\', '[', ',', '.', '?' }
@@ -31,119 +32,258 @@ local markers = {
3132
},
3233
}
3334

34-
local function apply_markup_to_line(namespace, bufnr, line_index, line)
35-
local hl = function(from, to, opts)
36-
local options = vim.tbl_extend('force', { ephemeral = true, end_col = to }, opts or {})
37-
vim.api.nvim_buf_set_extmark(bufnr, namespace, line_index, from, options)
35+
local function get_node_text(node, source, offset_col_start, offset_col_end)
36+
local start_row, start_col = node:start()
37+
local end_row, end_col = node:end_()
38+
start_col = start_col + (offset_col_start or 0)
39+
end_col = end_col + (offset_col_end or 0)
40+
41+
local lines
42+
local eof_row = vim.api.nvim_buf_line_count(source)
43+
if start_row >= eof_row then
44+
return nil
45+
end
46+
47+
if end_col == 0 then
48+
lines = vim.api.nvim_buf_get_lines(source, start_row, end_row, true)
49+
end_col = -1
50+
else
51+
lines = vim.api.nvim_buf_get_lines(source, start_row, end_row + 1, true)
52+
end
53+
54+
if #lines > 0 then
55+
if #lines == 1 then
56+
lines[1] = string.sub(lines[1], start_col + 1, end_col)
57+
else
58+
lines[1] = string.sub(lines[1], start_col + 1)
59+
lines[#lines] = string.sub(lines[#lines], 1, end_col)
60+
end
61+
end
62+
63+
return table.concat(lines, '\n')
64+
end
65+
66+
local get_tree = ts_utils.memoize_by_buf_tick(function(bufnr)
67+
local tree = vim.treesitter.get_parser(bufnr, 'org'):parse()
68+
if not tree or not #tree then
69+
return nil
70+
end
71+
return tree[1]:root()
72+
end)
73+
74+
local function get_predicate_nodes(match)
75+
local counter = 1
76+
local start_node = nil
77+
local end_node = nil
78+
for i, node in pairs(match) do
79+
if counter == 1 then
80+
start_node = node
81+
end
82+
if counter == 2 then
83+
end_node = node
84+
end
85+
counter = counter + 1
86+
end
87+
if not start_node or not end_node then
88+
return false
89+
end
90+
return start_node, end_node
91+
end
92+
93+
local function is_valid_markup_range(match, _, source, _)
94+
local start_node, end_node = get_predicate_nodes(match)
95+
if not start_node or not end_node then
96+
return
97+
end
98+
99+
-- Ignore conflicts with hyperlink
100+
if start_node:type() == '[' or end_node:type() == ']' then
101+
return true
102+
end
103+
104+
local start_line = start_node:range()
105+
local end_line = start_node:range()
106+
107+
if start_line ~= end_line then
108+
return false
109+
end
110+
111+
local start_text = get_node_text(start_node, source, -1)
112+
local end_text = get_node_text(end_node, source, 0, 1)
113+
114+
local is_valid_start = start_text:len() < 2 or vim.tbl_contains(valid_pre_marker_chars, start_text:sub(1, 1))
115+
local is_valid_end = end_text:len() < 2 or vim.tbl_contains(valid_post_marker_chars, end_text:sub(2, 2))
116+
return is_valid_start and is_valid_end
117+
end
118+
119+
local function is_valid_hyperlink_range(match, _, source, _)
120+
local start_node, end_node = get_predicate_nodes(match)
121+
if not start_node or not end_node then
122+
return
123+
end
124+
-- Ignore conflicts with markup
125+
if start_node:type() ~= '[' or end_node:type() ~= ']' then
126+
return true
127+
end
128+
129+
local start_line = start_node:range()
130+
local end_line = start_node:range()
131+
132+
if start_line ~= end_line then
133+
return false
134+
end
135+
136+
local start_text = get_node_text(start_node, source, 0, 1)
137+
local end_text = get_node_text(end_node, source, -1)
138+
139+
local is_valid_start = start_text == '[['
140+
local is_valid_end = end_text == ']]'
141+
return is_valid_start and is_valid_end
142+
end
143+
144+
local function load_deps()
145+
-- Already defined
146+
if query then
147+
return
148+
end
149+
query = vim.treesitter.get_query('org', 'markup')
150+
vim.treesitter.query.add_predicate('org-is-valid-markup-range?', is_valid_markup_range)
151+
vim.treesitter.query.add_predicate('org-is-valid-hyperlink-range?', is_valid_hyperlink_range)
152+
end
153+
154+
---@param bufnr? number
155+
---@param first_line? number
156+
---@param last_line? number
157+
---@return table[]
158+
local function get_matches(bufnr, first_line, last_line)
159+
bufnr = bufnr or 0
160+
local root = get_tree(bufnr)
161+
if not root then
162+
return
38163
end
39164

40-
local hide_markers = config.org_hide_emphasis_markers
41-
local l = line
42-
local stars = l:match('^%*+%s+')
43-
local offset = 0
44-
if stars then
45-
l = l:sub(stars:len() + 1)
46-
offset = stars:len()
47-
end
48-
local chars = vim.split(l, '', true)
49165
local ranges = {}
166+
local taken_locations = {}
167+
168+
for _, match, _ in query:iter_matches(root, bufnr, first_line, last_line) do
169+
for _, node in pairs(match) do
170+
local char = node:type()
171+
local range = ts_utils.node_to_lsp_range(node)
172+
local linenr = tostring(range.start.line)
173+
taken_locations[linenr] = taken_locations[linenr] or {}
174+
if not taken_locations[linenr][range.start.character] then
175+
table.insert(ranges, {
176+
type = char,
177+
range = range,
178+
})
179+
taken_locations[linenr][range.start.character] = true
180+
end
181+
end
182+
end
183+
184+
table.sort(ranges, function(a, b)
185+
if a.range.start.line == b.range.start.line then
186+
return a.range.start.character < b.range.start.character
187+
end
188+
return a.range.start.line < b.range.start.line
189+
end)
190+
50191
local seek = {}
51192
local seek_link = {}
52-
local link_ranges = {}
53-
54-
for i, char in ipairs(chars) do
55-
-- Markup parsing
56-
if markers[char] then
57-
if seek[char] then
58-
local next_char = chars[i + 1]
59-
if next_char == nil or vim.tbl_contains(valid_post_marker_chars, next_char) then
60-
local to = i + offset
61-
table.insert(ranges, { type = char, from = seek[char], to = to })
62-
-- Cleanup all unclosed markers in between
63-
for c, pos in pairs(seek) do
64-
if c ~= char and pos < to and pos > seek[char] then
65-
seek[c] = nil
66-
end
193+
local result = {}
194+
local link_result = {}
195+
196+
for _, item in ipairs(ranges) do
197+
if markers[item.type] then
198+
if seek[item.type] then
199+
local from = seek[item.type]
200+
table.insert(result, {
201+
type = item.type,
202+
from = from.range,
203+
to = item.range,
204+
})
205+
206+
seek[item.type] = nil
207+
208+
for t, pos in pairs(seek) do
209+
if
210+
pos.range.start.line == from.range.start.line
211+
and pos.range.start.character > from.range['end'].character
212+
and pos.range.start.character < item.range.start.character
213+
then
214+
seek[t] = nil
67215
end
68-
seek[char] = nil
69216
end
70217
else
71-
local prev_char = chars[i - 1]
72-
if prev_char == nil or vim.tbl_contains(valid_pre_marker_chars, prev_char) then
73-
seek[char] = i + offset
74-
end
218+
seek[item.type] = item
75219
end
76220
end
77221

78-
-- Links parsing
79-
if char == '[' and chars[i - 1] == '[' then
80-
seek_link[char] = i - 1 + offset
222+
if item.type == '[' then
223+
seek_link = item
81224
end
82225

83-
if char == ']' and chars[i + 1] == ']' and seek_link['['] then
84-
table.insert(link_ranges, { from = seek_link['['], to = i + 1 + offset })
226+
if item.type == ']' and seek_link then
227+
table.insert(link_result, {
228+
from = seek_link.range,
229+
to = item.range,
230+
})
231+
seek_link = nil
85232
end
86233
end
87234

235+
return result, link_result
236+
end
237+
238+
local function apply(namespace, bufnr, _, first_line, last_line, _)
239+
local ranges, link_ranges = get_matches(bufnr, first_line, last_line)
240+
local hide_markers = config.org_hide_emphasis_markers
241+
88242
for _, range in ipairs(ranges) do
89-
hl(range.from - 1, range.to, {
243+
vim.api.nvim_buf_set_extmark(bufnr, namespace, range.from.start.line, range.from.start.character, {
244+
ephemeral = true,
245+
end_col = range.to['end'].character,
90246
hl_group = markers[range.type].hl_name,
91-
priority = 110 + range.from,
247+
priority = 110 + range.from.start.character,
92248
})
249+
93250
if hide_markers then
94-
hl(range.from - 1, range.from, { conceal = '' })
95-
hl(range.to - 1, range.to, { conceal = '' })
251+
vim.api.nvim_buf_set_extmark(bufnr, namespace, range.from.start.line, range.from.start.character, {
252+
end_col = range.from['end'].character,
253+
ephemeral = true,
254+
conceal = '',
255+
})
256+
vim.api.nvim_buf_set_extmark(bufnr, namespace, range.to.start.line, range.to.start.character, {
257+
end_col = range.to['end'].character,
258+
ephemeral = true,
259+
conceal = '',
260+
})
96261
end
97262
end
98263

99264
for _, link_range in ipairs(link_ranges) do
100-
local link = line:sub(link_range.from, link_range.to)
265+
local line = vim.api.nvim_buf_get_lines(bufnr, link_range.from.start.line, link_range.from.start.line + 1, false)[1]
266+
local link = line:sub(link_range.from.start.character + 1, link_range.to['end'].character)
101267
local alias = link:find('%]%[') or 1
102-
hl(link_range.from - 1, link_range.to, {
268+
269+
vim.api.nvim_buf_set_extmark(bufnr, namespace, link_range.from.start.line, link_range.from.start.character, {
270+
ephemeral = true,
271+
end_col = link_range.to['end'].character,
103272
hl_group = 'org_hyperlink',
104-
priority = 200 + link_range.from,
273+
priority = 110,
105274
})
106-
hl(link_range.from - 1, link_range.from + alias, { conceal = '' })
107-
hl(link_range.to - 2, link_range.to, { conceal = '' })
108-
end
109-
end
110275

111-
local function apply(namespace, bufnr, changed_lines, first_line, _, tick_changed)
112-
if not buf_blocks[bufnr] or tick_changed then
113-
buf_blocks = {}
114-
local all_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
115-
local seek_blocks = {}
116-
for i, line in ipairs(all_lines) do
117-
local lower_line = line:lower()
118-
if lower_line:match('^%s*#%+begin_') then
119-
if not seek_blocks.from then
120-
seek_blocks.from = i
121-
end
122-
end
123-
if lower_line:match('^%s*#%+end_') then
124-
if not seek_blocks.to then
125-
seek_blocks.to = i
126-
end
127-
if seek_blocks.from and seek_blocks.to then
128-
table.insert(buf_blocks, { seek_blocks.from, seek_blocks.to })
129-
seek_blocks = {}
130-
end
131-
end
132-
end
133-
end
276+
vim.api.nvim_buf_set_extmark(bufnr, namespace, link_range.from.start.line, link_range.from.start.character, {
277+
ephemeral = true,
278+
end_col = link_range.from.start.character + 1 + alias,
279+
conceal = '',
280+
})
134281

135-
for i, line in ipairs(changed_lines) do
136-
local line_nr = first_line + i
137-
local apply_to_line = true
138-
for _, block in ipairs(buf_blocks) do
139-
if line_nr >= block[1] and line_nr <= block[2] then
140-
apply_to_line = false
141-
break
142-
end
143-
end
144-
if apply_to_line then
145-
apply_markup_to_line(namespace, bufnr, line_nr - 1, line)
146-
end
282+
vim.api.nvim_buf_set_extmark(bufnr, namespace, link_range.from.start.line, link_range.to['end'].character - 2, {
283+
ephemeral = true,
284+
end_col = link_range.to['end'].character,
285+
conceal = '',
286+
})
147287
end
148288
end
149289

@@ -152,6 +292,7 @@ local function setup()
152292
vim.cmd(marker.hl_cmd)
153293
end
154294
vim.cmd('hi def link org_hyperlink Underlined')
295+
load_deps()
155296
end
156297

157298
return {

lua/orgmode/parser/file.lua

+2-2
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,8 @@ function File.load(path, callback)
148148
end
149149

150150
---@param content string|table
151-
---@param category string
152-
---@param filename string
151+
---@param category? string
152+
---@param filename? string
153153
---@param is_archive_file? boolean
154154
---@return File|nil
155155
function File.from_content(content, category, filename, is_archive_file)

lua/orgmode/parser/files.lua

+5
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ function Files.get(file)
109109
Files._set_loaded_file(file, f:refresh())
110110
return Files.orgfiles[file]
111111
end
112+
113+
if vim.bo.filetype == 'org' and vim.fn.filereadable(file) == 0 then
114+
return File.from_content(vim.api.nvim_buf_get_lines(0, 0, -1, false))
115+
end
116+
112117
return nil
113118
end
114119

lua/orgmode/utils/init.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ end
373373

374374
---@param arg_lead string
375375
---@param list string[]
376-
---@param split_chars string[]
376+
---@param split_chars? string[]
377377
---@return string[]
378378
function utils.prompt_autocomplete(arg_lead, list, split_chars)
379379
split_chars = split_chars or { '+', '-', ':', '&', '|' }

0 commit comments

Comments
 (0)