From 36141c75bf3ef306b890a2d91ddb62adac6d81b8 Mon Sep 17 00:00:00 2001 From: Kristijan Husak Date: Sat, 22 Feb 2025 00:30:09 +0100 Subject: [PATCH 01/10] feat(ts): add link and timestamp tree-sitter captures --- .../colors/highlighter/markup/dates.lua | 13 -- .../colors/highlighter/markup/init.lua | 10 +- lua/orgmode/config/init.lua | 27 +---- lua/orgmode/files/elements/logbook.lua | 1 - lua/orgmode/files/file.lua | 20 ++- lua/orgmode/files/headline.lua | 32 ++--- lua/orgmode/org/links/hyperlink.lua | 68 +++-------- lua/orgmode/org/links/init.lua | 10 +- lua/orgmode/utils/treesitter/init.lua | 14 ++- lua/orgmode/utils/treesitter/install.lua | 3 +- queries/org/highlights.scm | 15 ++- queries/org/images.scm | 5 +- syntax/orgagenda.vim | 33 +++++ tests/plenary/colors/highlighter_spec.lua | 108 ----------------- tests/plenary/org/links/hyperlink_spec.lua | 114 ------------------ 15 files changed, 103 insertions(+), 370 deletions(-) delete mode 100644 tests/plenary/org/links/hyperlink_spec.lua diff --git a/lua/orgmode/colors/highlighter/markup/dates.lua b/lua/orgmode/colors/highlighter/markup/dates.lua index be20f6561..51b2a8b33 100644 --- a/lua/orgmode/colors/highlighter/markup/dates.lua +++ b/lua/orgmode/colors/highlighter/markup/dates.lua @@ -171,17 +171,4 @@ function OrgDates:prepare_highlights(highlights) return extmarks end ----@param item OrgMarkupNode ----@return boolean -function OrgDates:has_valid_parent(item) - ---At this point we know that node has 2 valid parents - local parent = item.node:parent():parent() - - if parent and parent:type() == 'value' then - return parent:parent() and parent:parent():type() == 'property' or false - end - - return false -end - return OrgDates diff --git a/lua/orgmode/colors/highlighter/markup/init.lua b/lua/orgmode/colors/highlighter/markup/init.lua index 7d8273113..0e1287cb7 100644 --- a/lua/orgmode/colors/highlighter/markup/init.lua +++ b/lua/orgmode/colors/highlighter/markup/init.lua @@ -22,8 +22,6 @@ end function OrgMarkup:_init_highlighters() self.parsers = { emphasis = require('orgmode.colors.highlighter.markup.emphasis'):new({ markup = self }), - link = require('orgmode.colors.highlighter.markup.link'):new({ markup = self }), - date = require('orgmode.colors.highlighter.markup.dates'):new({ markup = self }), footnote = require('orgmode.colors.highlighter.markup.footnotes'):new({ markup = self }), latex = require('orgmode.colors.highlighter.markup.latex'):new({ markup = self }), } @@ -72,9 +70,7 @@ end function OrgMarkup:get_node_highlights(root_node, source, line) local result = { emphasis = {}, - link = {}, latex = {}, - date = {}, footnote = {}, } ---@type OrgMarkupNode[] @@ -230,7 +226,7 @@ function OrgMarkup:has_valid_parent(item) return false end - if parent:type() == 'paragraph' then + if parent:type() == 'paragraph' or parent:type() == 'link_desc' then return true end @@ -248,8 +244,8 @@ function OrgMarkup:has_valid_parent(item) return true end - if self.parsers[item.type].has_valid_parent then - return self.parsers[item.type]:has_valid_parent(item) + if parent:type() == 'value' then + return p and p:type() == 'property' or false end return false diff --git a/lua/orgmode/config/init.lua b/lua/orgmode/config/init.lua index 6db47321e..c2ab25891 100644 --- a/lua/orgmode/config/init.lua +++ b/lua/orgmode/config/init.lua @@ -405,6 +405,7 @@ function Config:setup_ts_predicates() end, { force = true, all = false }) vim.treesitter.query.add_predicate('org-is-valid-priority?', function(match, _, source, predicate) + ---@type TSNode | nil local node = match[predicate[2]] local type = predicate[3] if not node then @@ -412,31 +413,7 @@ function Config:setup_ts_predicates() end local text = vim.treesitter.get_node_text(node, source) - local is_valid = valid_priorities[text] and valid_priorities[text].type == type - if not is_valid then - return false - end - local priority_text = '[#' .. text .. ']' - local full_node_text = vim.treesitter.get_node_text(node:parent(), source) - if priority_text ~= full_node_text then - return false - end - - local prev_sibling = node:parent():prev_sibling() - -- If first child, consider it valid - if not prev_sibling then - return true - end - - -- If prev sibling has more prev siblings, it means that the prev_sibling is not a todo keyword - -- so this priority is not valid - if prev_sibling:prev_sibling() then - return false - end - - local todo_text = vim.treesitter.get_node_text(prev_sibling, source) - local is_prev_sibling_todo_keyword = todo_keywords[todo_text] and true or false - return is_prev_sibling_todo_keyword + return valid_priorities[text] and valid_priorities[text].type == type end, { force = true, all = false }) vim.treesitter.query.add_directive('org-set-block-language!', function(match, _, bufnr, pred, metadata) diff --git a/lua/orgmode/files/elements/logbook.lua b/lua/orgmode/files/elements/logbook.lua index 18e68ac45..0838a83c9 100644 --- a/lua/orgmode/files/elements/logbook.lua +++ b/lua/orgmode/files/elements/logbook.lua @@ -1,6 +1,5 @@ local Range = require('orgmode.files.elements.range') local utils = require('orgmode.utils') -local config = require('orgmode.config') local Date = require('orgmode.objects.date') local Duration = require('orgmode.objects.duration') diff --git a/lua/orgmode/files/file.lua b/lua/orgmode/files/file.lua index be47f69a8..18a0f2a6b 100644 --- a/lua/orgmode/files/file.lua +++ b/lua/orgmode/files/file.lua @@ -735,21 +735,17 @@ memoize('get_links') ---@return OrgHyperlink[] function OrgFile:get_links() self:parse(true) - local ts_query = ts_utils.get_query([[ - (paragraph (expr) @links) - (drawer (contents (expr) @links)) - (headline (item (expr)) @links) + local links = {} + local matches = self:get_ts_matches([[ + (link) @link + (link_desc) @link ]]) - local links = {} - local processed_lines = {} - for _, match in ts_query:iter_captures(self.root, self:get_source()) do - local line = match:start() - if not processed_lines[line] then - vim.list_extend(links, Hyperlink.all_from_line(self.lines[line + 1], line + 1)) - processed_lines[line] = true - end + local source = self:get_source() + for _, match in ipairs(matches) do + table.insert(links, Hyperlink.from_node(match.link.node, source)) end + return links end diff --git a/lua/orgmode/files/headline.lua b/lua/orgmode/files/headline.lua index 0787ea78c..4ff2e9980 100644 --- a/lua/orgmode/files/headline.lua +++ b/lua/orgmode/files/headline.lua @@ -67,26 +67,21 @@ end memoize('get_priority') ---@return string, TSNode | nil function Headline:get_priority() - local _, todo_node = self:get_todo() local item = self:_get_child_node('item') - local priority_node = item and item:named_child(1) + local priority_node = item and item:field('priority')[1] - if not todo_node then - priority_node = item and item:named_child(0) + if not priority_node then + return '', nil end - if priority_node then - local text = self.file:get_node_text(priority_node) - local priority = text:match('%[#(%w+)%]') - if priority then - local priorities = config:get_priorities() - if priorities[priority] then - return priority, priority_node - end - end + local value = self.file:get_node_text(priority_node:field('value')[1]) + + if not value then + return '', nil end - return '', nil + + return value, priority_node end ---@param amount number @@ -424,15 +419,6 @@ function Headline:get_title() return title, offset end -function Headline:get_title_with_priority() - local priority = self:get_priority() - local title = self:get_title() - if priority ~= '' then - return ('[#%s] %s'):format(priority, self:get_title()) - end - return title -end - memoize('get_own_properties') ---@return table, TSNode | nil function Headline:get_own_properties() diff --git a/lua/orgmode/org/links/hyperlink.lua b/lua/orgmode/org/links/hyperlink.lua index c6f321655..88ee16ced 100644 --- a/lua/orgmode/org/links/hyperlink.lua +++ b/lua/orgmode/org/links/hyperlink.lua @@ -1,5 +1,6 @@ local OrgLinkUrl = require('orgmode.org.links.url') local Range = require('orgmode.files.elements.range') +local ts_utils = require('orgmode.utils.treesitter') ---@class OrgHyperlink ---@field url OrgLinkUrl @@ -21,61 +22,26 @@ function OrgHyperlink:new(str, 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 +---@param node TSNode +---@param source number | string +---@return OrgHyperlink +function OrgHyperlink.from_node(node, source) + local url = node:field('url')[1] + local desc = node:field('desc')[1] + local this = setmetatable({}, { __index = OrgHyperlink }) + this.url = OrgLinkUrl:new(vim.treesitter.get_node_text(url, source)) + this.desc = desc and vim.treesitter.get_node_text(desc, source) + this.range = Range.from_node(node) + return this end ----@return OrgHyperlink | nil, { from: number, to: number } | nil +---@return OrgHyperlink | 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 + local link_node = ts_utils.closest_node(ts_utils.get_node(), { 'link', 'link_desc' }) + if not link_node then + return nil end - - return links + return OrgHyperlink.from_node(link_node, vim.api.nvim_get_current_buf()) end return OrgHyperlink diff --git a/lua/orgmode/org/links/init.lua b/lua/orgmode/org/links/init.lua index 65d6f468c..c8b118ed5 100644 --- a/lua/orgmode/org/links/init.lua +++ b/lua/orgmode/org/links/init.lua @@ -141,11 +141,11 @@ function OrgLinks:insert_link(link_location, desc) 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 + local link = OrgHyperlink.at_cursor() + if link then + insert_from = link.range.start_col - 1 + insert_to = link.range.end_col + 1 + target_col = target_col + link.range.start_col elseif vim.fn.mode() == 'v' then local start_pos = vim.fn.getpos('v') local end_pos = vim.fn.getpos('.') diff --git a/lua/orgmode/utils/treesitter/init.lua b/lua/orgmode/utils/treesitter/init.lua index de5dee0e4..d33fcbb7d 100644 --- a/lua/orgmode/utils/treesitter/init.lua +++ b/lua/orgmode/utils/treesitter/init.lua @@ -63,16 +63,22 @@ function M.closest_headline_node(cursor) return M.find_headline(node) end +---@param node TSNode | nil +---@param node_type string | string[] ---@return TSNode | nil -function M.closest_node(node, type) +function M.closest_node(node, node_type) if not node then return nil end - if node:type() == type then - return node + local types = type(node_type) == 'table' and node_type or { node_type } + + for _, t in ipairs(types) do + if node:type() == t then + return node + end end - return M.closest_node(node:parent(), type) + return M.closest_node(node:parent(), types) end ---@param node? TSNode diff --git a/lua/orgmode/utils/treesitter/install.lua b/lua/orgmode/utils/treesitter/install.lua index d1ec9f48c..33f3dcbef 100644 --- a/lua/orgmode/utils/treesitter/install.lua +++ b/lua/orgmode/utils/treesitter/install.lua @@ -145,7 +145,8 @@ end ---@param type? 'install' | 'update' | 'reinstall'' function M.run(type) - local url = 'https://github.com/nvim-orgmode/tree-sitter-org' + -- local url = 'https://github.com/nvim-orgmode/tree-sitter-org' + local url = '/home/kristijan/github/tree-sitter-org' local compiler = vim.tbl_filter(function(exe) return exe ~= vim.NIL and vim.fn.executable(exe) == 1 end, M.compilers)[1] diff --git a/queries/org/highlights.scm b/queries/org/highlights.scm index a2e13666e..eace05b71 100644 --- a/queries/org/highlights.scm +++ b/queries/org/highlights.scm @@ -11,11 +11,11 @@ (headline (stars) @stars (#org-is-headline-level? @stars "8")) @org.headline.level8 (item . (expr) @org.keyword.todo @nospell (#org-is-todo-keyword? @org.keyword.todo "TODO")) (item . (expr) @org.keyword.done @nospell (#org-is-todo-keyword? @org.keyword.done "DONE")) -(item (expr "[" "#" "str" @_priority "]") @org.priority.highest (#org-is-valid-priority? @_priority "highest")) -(item (expr "[" "#" "str" @_priority "]") @org.priority.high (#org-is-valid-priority? @_priority "high")) -(item (expr "[" "#" "str" @_priority "]") @org.priority.default (#org-is-valid-priority? @_priority "default")) -(item (expr "[" "#" "str" @_priority "]") @org.priority.low (#org-is-valid-priority? @_priority "low")) -(item (expr "[" "#" "str" @_priority "]") @org.priority.lowest (#org-is-valid-priority? @_priority "lowest")) +(item priority: (priority value: (expr) @_priority) @org.priority.highest (#org-is-valid-priority? @_priority "highest")) +(item priority: (priority value: (expr) @_priority) @org.priority.high (#org-is-valid-priority? @_priority "high")) +(item priority: (priority value: (expr) @_priority) @org.priority.default (#org-is-valid-priority? @_priority "default")) +(item priority: (priority value: (expr) @_priority) @org.priority.low (#org-is-valid-priority? @_priority "low")) +(item priority: (priority value: (expr) @_priority) @org.priority.lowest (#org-is-valid-priority? @_priority "lowest")) (list (listitem (paragraph) @spell)) (body (paragraph) @spell) (bullet) @org.bullet @@ -42,3 +42,8 @@ (table (row (cell (contents) @org.table.heading))) (table (hr) @org.table.delimiter) (fndef label: (expr) @org.footnote (#offset! @org.footnote 0 -4 0 1)) +(link) @org.hyperlink +(link_desc) @org.hyperlink +(link "[[" @_link_open "]]" @_link_close (#set! conceal "")) +(link_desc "[[" @_link_open "][" @_link_separator "]]" @_link_close (#set! conceal "")) +((link_desc url: (expr)+ @_link_url (#set! @_link_url conceal "")) @_link (#set! @_link url @_link_url)) diff --git a/queries/org/images.scm b/queries/org/images.scm index 2930eb7cf..1d150794d 100644 --- a/queries/org/images.scm +++ b/queries/org/images.scm @@ -1 +1,4 @@ -((expr "[") @image (#org-set-link! @image)) +(link url: (expr) @image.src + (#gsub! @image.src "^file:" "") + (#match? @image.src "(png|jpg|jpeg|gif|bmp|webp|tiff|heic|avif|mp4|mov|avi|mkv|webm|pdf)$") +) diff --git a/syntax/orgagenda.vim b/syntax/orgagenda.vim index c3cbedc83..137752689 100644 --- a/syntax/orgagenda.vim +++ b/syntax/orgagenda.vim @@ -5,6 +5,39 @@ syn match org_hyperlinkBracketsLeft contained "\[\{2}" conceal syn match org_hyperlinkURL contained "[^][]*\]\[" conceal syn match org_hyperlinkBracketsRight contained "\]\{2}" conceal +syn match org_timestamp /\(<\d\d\d\d-\d\d-\d\d \k\k\k>\)/ +"<2003-09-16 Tue 12:00> +syn match org_timestamp /\(<\d\d\d\d-\d\d-\d\d \k\k\k \d\d:\d\d>\)/ +"<2003-09-16 Tue 12:00 +1d> +syn match org_timestamp /\(<\d\d\d\d-\d\d-\d\d \k\k\k \d\d:\d\d [+-]\d\+\w>\)/ +"<2003-09-16 Tue 12:00 +1d -1y> +syn match org_timestamp /\(<\d\d\d\d-\d\d-\d\d \k\k\k \d\d:\d\d [+-]\d\+\w [+-]\d\+\w>\)/ +"<2003-09-16 Tue 12:00-12:30> +syn match org_timestamp /\(<\d\d\d\d-\d\d-\d\d \k\k\k \d\d:\d\d-\d\d:\d\d>\)/ + +"<2003-09-16 Tue>--<2003-09-16 Tue> +syn match org_timestamp /\(<\d\d\d\d-\d\d-\d\d \k\k\k>--<\d\d\d\d-\d\d-\d\d \k\k\k>\)/ +"<2003-09-16 Tue 12:00>--<2003-09-16 Tue 12:00> +syn match org_timestamp /\(<\d\d\d\d-\d\d-\d\d \k\k\k \d\d:\d\d>--<\d\d\d\d-\d\d-\d\d \k\k\k \d\d:\d\d>\)/ + +"[2003-09-16 Tue] +syn match org_timestamp_inactive /\(\[\d\d\d\d-\d\d-\d\d \k\k\k\]\)/ +"[2003-09-16 Tue 12:00] +syn match org_timestamp_inactive /\(\[\d\d\d\d-\d\d-\d\d \k\k\k \d\d:\d\d\]\)/ + +"[2003-09-16 Tue 12:00 +1d] +syn match org_timestamp_inactive /\(\[\d\d\d\d-\d\d-\d\d \k\k\k \d\d:\d\d [+-]\d\+\w\]\)/ +"[2003-09-16 Tue 12:00 +1d -1y] +syn match org_timestamp_inactive /\(\[\d\d\d\d-\d\d-\d\d \k\k\k \d\d:\d\d [+-]\d\+\w [+-]\d\+\w\]\)/ + +"[2003-09-16 Tue]--[2003-09-16 Tue] +syn match org_timestamp_inactive /\(\[\d\d\d\d-\d\d-\d\d \k\k\k\]--\[\d\d\d\d-\d\d-\d\d \k\k\k\]\)/ +"[2003-09-16 Tue 12:00]--[2003-09-16 Tue 12:00] +syn match org_timestamp_inactive /\(\[\d\d\d\d-\d\d-\d\d \k\k\k \d\d:\d\d\]--\[\d\d\d\d-\d\d-\d\d \k\k\k \d\d:\d\d\]\)/ + +hi default link org_timestamp @org.timestamp.active +hi default link org_timestamp_inactive @org.timestamp.inactive + hi default link @org.agenda.day Statement hi default link @org.agenda.today @org.bold hi default link @org.agenda.weekend @org.bold diff --git a/tests/plenary/colors/highlighter_spec.lua b/tests/plenary/colors/highlighter_spec.lua index dec82b111..25bea517d 100644 --- a/tests/plenary/colors/highlighter_spec.lua +++ b/tests/plenary/colors/highlighter_spec.lua @@ -236,72 +236,6 @@ describe('highlighter', function() end) end) - describe('links', function() - it('should highlight links without label', function() - local extmarks = get_extmarks({ - 'I have [[https://google.com]] link', - }) - assert.are.same(3, #extmarks) - assert_extmark(extmarks[1], { line = 0, start_col = 7, end_col = 9, conceal = '' }) - assert_extmark( - extmarks[2], - { line = 0, start_col = 7, end_col = 29, hl_group = '@org.hyperlink', url = 'https://google.com' } - ) - assert_extmark(extmarks[3], { line = 0, start_col = 27, end_col = 29, conceal = '' }) - end) - - it('should highlight links with label', function() - local extmarks = get_extmarks({ - 'I have [[https://google.com][google]] link', - }) - assert.are.same(4, #extmarks) - assert_extmark(extmarks[1], { line = 0, start_col = 7, end_col = 29, conceal = '' }) - assert_extmark( - extmarks[2], - { line = 0, start_col = 7, end_col = 28, hl_group = '@org.hyperlink', url = 'https://google.com' } - ) - assert_extmark( - extmarks[3], - { line = 0, start_col = 28, end_col = 37, hl_group = '@org.hyperlink', url = 'https://google.com' } - ) - assert_extmark(extmarks[4], { line = 0, start_col = 35, end_col = 37, conceal = '' }) - end) - - it('should highlight links with label and render markup only in label', function() - local extmarks = get_extmarks({ - 'I have [[https://google.com][google I am *bold* text]] link', - }) - assert.are.same(7, #extmarks) - assert_extmark(extmarks[1], { line = 0, start_col = 7, end_col = 29, conceal = '' }) - assert_extmark( - extmarks[2], - { line = 0, start_col = 7, end_col = 28, hl_group = '@org.hyperlink', url = 'https://google.com' } - ) - assert_extmark( - extmarks[3], - { line = 0, start_col = 28, end_col = 54, hl_group = '@org.hyperlink', url = 'https://google.com' } - ) - assert_extmark(extmarks[4], { line = 0, start_col = 41, end_col = 42, hl_group = '@org.bold.delimiter' }) - assert_extmark(extmarks[5], { line = 0, start_col = 42, end_col = 46, hl_group = '@org.bold' }) - assert_extmark(extmarks[6], { line = 0, start_col = 46, end_col = 47, hl_group = '@org.bold.delimiter' }) - assert_extmark(extmarks[7], { line = 0, start_col = 52, end_col = 54, conceal = '' }) - end) - - it('should not highlight invalid link with description', function() - local extmarks = get_extmarks({ - 'I am not a [https://google.com][text]] link', - }) - assert.are.same(0, #extmarks) - end) - - it('should not highlight invalid link', function() - local extmarks = get_extmarks({ - 'I am not a [[https://google.com] link', - }) - assert.are.same(0, #extmarks) - end) - end) - describe('latex', function() it('should highlight latex with backslash only', function() local extmarks = get_extmarks({ @@ -335,46 +269,4 @@ describe('highlighter', function() assert_extmark(extmarks[3], { line = 2, start_col = 8, end_col = 17, hl_group = '@org.latex' }) end) end) - - describe('dates', function() - it('should highlight active dates', function() - local extmarks = get_extmarks({ - 'the date <2024-02-16>', - 'the date <2024-02-16 Fri>', - 'the date <2024-02-16 Fri 12:30>', - 'the date <2024-02-16 Fri 12:30 +1m>', - 'the date <2024-02-16 Fri 12:30 +1m -1d>', - }) - assert.are.same(5, #extmarks) - assert_extmark(extmarks[1], { line = 0, start_col = 9, end_col = 21, hl_group = '@org.timestamp.active' }) - assert_extmark(extmarks[2], { line = 1, start_col = 9, end_col = 25, hl_group = '@org.timestamp.active' }) - assert_extmark(extmarks[3], { line = 2, start_col = 9, end_col = 31, hl_group = '@org.timestamp.active' }) - assert_extmark(extmarks[4], { line = 3, start_col = 9, end_col = 35, hl_group = '@org.timestamp.active' }) - assert_extmark(extmarks[5], { line = 4, start_col = 9, end_col = 39, hl_group = '@org.timestamp.active' }) - end) - - it('should highlight inactive dates', function() - local extmarks = get_extmarks({ - 'the date [2024-02-16]', - 'the date [2024-02-16 Fri]', - 'the date [2024-02-16 Fri 12:30]', - 'the date [2024-02-16 Fri 12:30 +1m]', - 'the date [2024-02-16 Fri 12:30 +1m -1d]', - }) - assert.are.same(5, #extmarks) - assert_extmark(extmarks[1], { line = 0, start_col = 9, end_col = 21, hl_group = '@org.timestamp.inactive' }) - assert_extmark(extmarks[2], { line = 1, start_col = 9, end_col = 25, hl_group = '@org.timestamp.inactive' }) - assert_extmark(extmarks[3], { line = 2, start_col = 9, end_col = 31, hl_group = '@org.timestamp.inactive' }) - assert_extmark(extmarks[4], { line = 3, start_col = 9, end_col = 35, hl_group = '@org.timestamp.inactive' }) - assert_extmark(extmarks[5], { line = 4, start_col = 9, end_col = 39, hl_group = '@org.timestamp.inactive' }) - end) - - it('should not highlight invalid dates', function() - local extmarks = get_extmarks({ - 'the date [2024-02-16 .]', - 'the date <2024-02-16 Fri <>', - }) - assert.are.same(0, #extmarks) - end) - end) end) diff --git a/tests/plenary/org/links/hyperlink_spec.lua b/tests/plenary/org/links/hyperlink_spec.lua deleted file mode 100644 index 06d5934cd..000000000 --- a/tests/plenary/org/links/hyperlink_spec.lua +++ /dev/null @@ -1,114 +0,0 @@ -local Hyperlink = require('orgmode.org.links.hyperlink') - -describe('Hyperlink.at_pos', function() - ---@param obj any sut - ---@param col number cursor position in line - local function assert_valid_link_at(obj, col) - assert(obj, string.format('%q at pos %d', obj, col)) - end - - ---@param property string 'url' or 'desc' - ---@param obj any sut - ---@param col number cursor position in line - ---@param exp any - local function assert_valid_link_property_at(property, obj, col, exp) - local msg = function(_exp) - return string.format('%s: Expected to be %s at %s, actually %q.', property, _exp, col, obj) - end - if exp then - assert(obj == exp, msg(exp)) - else - assert(obj ~= nil, msg('valid')) - end - end - - ---@param property string 'url' or 'desc' - ---@param line string line of an orgfile - ---@param col number cursor position in line - local function assert_empty_link_property_at(property, line, col) - assert(line == nil, string.format("%s: Expected to be 'nil' at %s, actually %q.", property, col, line)) - end - - ---@param line string line of an orgfile - ---@param lb number position of left outer bracket of the link within the line - ---@param rb number position of right outer bracket of the link within the line - local function assert_link_in_range(line, lb, rb, opt) - for pos = lb, rb do - local link = Hyperlink.at_pos(line, pos) - assert_valid_link_at(link, pos) - if not link then - return - end - assert_valid_link_property_at('url', link.url, pos) - assert_valid_link_property_at('url', link.url:to_string(), pos, opt and opt.url) - if not opt or not opt.desc then - assert_empty_link_property_at('desc', link.desc, pos) - elseif opt and opt.desc then - assert_valid_link_property_at('desc', link.desc, pos, opt.desc) - else - assert(false, string.format('invalid opt %s', opt)) - end - end - end - - local function assert_not_link_in_range(line, lb, rb) - for pos = lb, rb do - local nil_link = Hyperlink.at_pos(line, pos) - assert( - not nil_link, - string.format('Expected no link between %s and %s, got actually %q', lb, rb, nil_link and nil_link:to_str()) - ) - end - end - - it('should not be empty like [[]]', function() - local line = '[[]]' - assert_not_link_in_range(line, 1, #line) - end) - it('should not be empty like [[][]]', function() - local line = '[[][]]' - assert_not_link_in_range(line, 1, #line) - end) - it('should not have an empty url like [[][some description]]', function() - local line = '[[][some description]]' - assert_not_link_in_range(line, 1, #line) - end) - it('could have an empty description like [[someurl]]', function() - local line = '[[someurl]]' - assert_link_in_range(line, 1, #line) - local link_str = Hyperlink.at_pos(line, 1):to_str() - assert(link_str == line, string.format('Expected %q, actually %q', line, link_str)) - end) - it('should parse valid [[somefile][Some Description]]', function() - local line = '[[somefile][Some Description]]' - assert_link_in_range(line, 1, #line, { url = 'somefile', desc = 'Some Description' }) - end) - it('should find link at valid positions in "1...5[[u_1][desc_1]]21.[[u_2]]...35[[u_3][desc_2]]51......60"', function() - local line = '1...5[[u_1][desc_1]]21.[[u_2]]...35[[u_3][desc_2]]51......60' - assert_not_link_in_range(line, 1, 5) - assert_link_in_range(line, 6, 20, { url = 'u_1', desc = 'desc_1' }) - assert_not_link_in_range(line, 21, 23) - assert_link_in_range(line, 24, 30, { url = 'u_2' }) - assert_not_link_in_range(line, 33, 35) - assert_link_in_range(line, 36, 50, { url = 'u_3', desc = 'desc_2' }) - assert_not_link_in_range(line, 51, 60) - end) - it('should resolve a relative file path', function() - local examples = { - { - '- [ ] Look here: [[file:./../sibling-folder/somefile.org::*some headline][some description]]', - { 3, 4, 5 }, - { 20, 90 }, - }, - } - for _, o in ipairs(examples) do - local line, valid_cols, invalid_cols = o[1], o[2], o[3] - for _, valid_pos in ipairs(valid_cols) do - assert_valid_link_at(line, valid_pos) - end - for _, invalid_pos in ipairs(invalid_cols) do - assert_valid_link_at(line, invalid_pos) - end - end - end) -end) From 5e7d971bf2d069c8de8252f7e8ed8b03ae977096 Mon Sep 17 00:00:00 2001 From: Kristijan Husak Date: Sat, 22 Feb 2025 23:43:15 +0100 Subject: [PATCH 02/10] feat(ts): use timestamp nodes for all dates --- lua/orgmode/files/file.lua | 3 + lua/orgmode/files/headline.lua | 63 +++--- lua/orgmode/objects/date.lua | 46 ----- lua/orgmode/org/mappings.lua | 24 ++- lua/orgmode/utils/treesitter/init.lua | 17 ++ queries/org/injections.scm | 1 + tests/plenary/object/date_spec.lua | 273 -------------------------- 7 files changed, 70 insertions(+), 357 deletions(-) diff --git a/lua/orgmode/files/file.lua b/lua/orgmode/files/file.lua index 18a0f2a6b..de1044d2f 100644 --- a/lua/orgmode/files/file.lua +++ b/lua/orgmode/files/file.lua @@ -172,6 +172,9 @@ end function OrgFile:get_ts_matches(query, node) self:parse() node = node or self.root + if not node then + return {} + end local ts_query = ts_utils.get_query(query) local matches = {} diff --git a/lua/orgmode/files/headline.lua b/lua/orgmode/files/headline.lua index 4ff2e9980..f8c16bca5 100644 --- a/lua/orgmode/files/headline.lua +++ b/lua/orgmode/files/headline.lua @@ -759,39 +759,46 @@ memoize('get_non_plan_dates') function Headline:get_non_plan_dates() local headline_node = self:node() local section = headline_node:parent() - local body = section and section:field('body')[1] - local headline_text = self.file:get_node_text(headline_node) or '' - local dates = Date.parse_all_from_line(headline_text, self:node():start() + 1) - local properties_node = section and section:field('property_drawer')[1] + if not section then + return {} + end - if properties_node then - local properties_text = self.file:get_node_text_list(properties_node) or {} - local start = properties_node:start() - for i, line in ipairs(properties_text) do - vim.list_extend(dates, Date.parse_all_from_line(line, start + i)) - end + local body_node = section:field('body')[1] + local property_node = section:field('property_drawer')[1] + local matches = {} + + local headline_matches = self.file:get_ts_matches('(item (timestamp) @timestamp)', headline_node) + vim.list_extend(matches, headline_matches) + + if property_node then + local property_matches = self.file:get_ts_matches('(property (value (timestamp) @timestamp))', property_node) + vim.list_extend(matches, property_matches) end - if not body then - return dates - end - - local start_line = body:range() - local lines = self.file:get_node_text_list(body, ts_utils.range_with_zero_start_col(body)) - for i, line in ipairs(lines) do - local line_dates = Date.parse_all_from_line(line, start_line + i) - local is_clock_line = line:match('^%s*:?CLOCK:') ~= nil - for _, date in ipairs(line_dates) do - -- Assume that the date is part of logbook if line starts with clock - -- TODO: Make this more reliable - if not date.active and is_clock_line then - date.type = 'LOGBOOK' - end - end - vim.list_extend(dates, line_dates) + if body_node then + local body_matches = self.file:get_ts_matches( + [[ + (paragraph (timestamp) @timestamp) + (table (row (cell (contents (timestamp) @timestamp)))) + (drawer (contents (timestamp) @timestamp)) + (fndef (description (timestamp) @timestamp)) + ]], + body_node + ) + vim.list_extend(matches, body_matches) end - return dates + local all_dates = {} + local source = self.file:get_source() + for _, match in ipairs(matches) do + local dates = Date.from_org_date(match.timestamp.text, { + range = Range.from_node(match.timestamp.node), + type = ts_utils.is_date_in_drawer(match.timestamp.node, 'logbook', source) and 'LOGBOOK' or 'NONE', + }) + vim.list_extend(all_dates, dates) + end + + return all_dates end ---@param sorted? boolean diff --git a/lua/orgmode/objects/date.lua b/lua/orgmode/objects/date.lua index d1bbe44ea..5ea05a3c4 100644 --- a/lua/orgmode/objects/date.lua +++ b/lua/orgmode/objects/date.lua @@ -127,34 +127,6 @@ local function parse_date(date, adjustments, data) return OrgDate:new(opts) end ----@param line string ----@param lnum number ----@param open string ----@param datetime string ----@param close string ----@param last_match? OrgDate ----@param type? string ----@return OrgDate | nil -local function from_match(line, lnum, open, datetime, close, last_match, type) - local search_from = last_match ~= nil and last_match.range.end_col or 0 - local from, to = line:find(vim.pesc(open .. datetime .. close), search_from) - local is_date_range_end = last_match and last_match.is_date_range_start and line:sub(from - 2, from - 1) == '--' - local opts = { - type = type, - active = open == '<', - range = Range:new({ start_line = lnum, end_line = lnum, start_col = from, end_col = to }), - is_date_range_start = line:sub(to + 1, to + 2) == '--', - } - local parsed_date = OrgDate.from_string(vim.trim(datetime), opts) - if is_date_range_end then - parsed_date.is_date_range_end = true - parsed_date.related_date = last_match - last_match.related_date = parsed_date - end - - return parsed_date -end - ---@param opts OrgDateOpts ---@return OrgDate function OrgDate:new(opts) @@ -340,24 +312,6 @@ function OrgDate.is_date_instance(value) return getmetatable(value) == OrgDate end ----@param line string ----@param lnum number ----@return OrgDate[] -function OrgDate.parse_all_from_line(line, lnum) - local is_comment = line:match('^%s*#[^%+]') - if is_comment then - return {} - end - local dates = {} - for open, datetime, close in line:gmatch(pattern) do - local parsed_date = from_match(line, lnum, open, datetime, close, dates[#dates]) - if parsed_date then - table.insert(dates, parsed_date) - end - end - return dates -end - ---@param datestr string ---@param opts? OrgDateOpts ---@return OrgDate | nil diff --git a/lua/orgmode/org/mappings.lua b/lua/orgmode/org/mappings.lua index ffab6caeb..560951e1a 100644 --- a/lua/orgmode/org/mappings.lua +++ b/lua/orgmode/org/mappings.lua @@ -16,6 +16,7 @@ local Babel = require('orgmode.babel') local Promise = require('orgmode.utils.promise') local Input = require('orgmode.ui.input') local Footnote = require('orgmode.objects.footnote') +local Range = require('orgmode.files.elements.range') ---@class OrgMappings ---@field capture OrgCapture @@ -1123,19 +1124,22 @@ function OrgMappings:_get_date_under_cursor(col_offset) local item = self.files:get_closest_headline_or_nil() local dates = {} if item then - dates = vim.tbl_filter(function(date) - return date.range:is_in_range(line, col) - end, item:get_all_dates()) + dates = item:get_all_dates() else - dates = Date.parse_all_from_line(vim.fn.getline('.'), line) - end - - if #dates == 0 then - return nil + local date_node = ts_utils.closest_node(ts_utils.get_node(), 'timestamp') + if not date_node then + return nil + end + dates = Date.from_org_date(vim.treesitter.get_node_text(date_node, 0), { + range = Range.from_node(date_node), + type = ts_utils.is_date_in_drawer(date_node, 'logbook') and 'LOGBOOK' or 'NONE', + }) end - -- TODO: this will result in a bug, when more than one date is in the line - return dates[1] + local valid_dates = vim.tbl_filter(function(date) + return date.range:is_in_range(line, col) + end, dates) + return valid_dates[1] end ---@param amount number diff --git a/lua/orgmode/utils/treesitter/init.lua b/lua/orgmode/utils/treesitter/init.lua index d33fcbb7d..0c1eb0563 100644 --- a/lua/orgmode/utils/treesitter/init.lua +++ b/lua/orgmode/utils/treesitter/init.lua @@ -118,6 +118,23 @@ function M.parents_until(node, type) end end +---@param node TSNode +---@param drawer string +---@param source? number|string +---@return boolean +function M.is_date_in_drawer(node, drawer, source) + if + (node:parent() and node:parent():type() == 'contents') + and (node:parent():parent() and node:parent():parent():type() == 'drawer') + then + local drawer_node = node:parent():parent() --[[@as TSNode]] + local drawer_name = vim.treesitter.get_node_text(drawer_node:field('name')[1], source or 0) + return drawer_name:lower() == drawer + end + + return false +end + function M.node_to_lsp_range(node) local start_line, start_col, end_line, end_col = vim.treesitter.get_node_range(node) local rtn = {} diff --git a/queries/org/injections.scm b/queries/org/injections.scm index 2a64075ab..f03158bfd 100644 --- a/queries/org/injections.scm +++ b/queries/org/injections.scm @@ -1,2 +1,3 @@ (block parameter: (expr) @_lang (contents) @injection.content (#set! injection.include-children) (#org-set-block-language! @_lang)) + ; (inline_code_block language: (language) @_lang contents: (contents) @injection.content (#set! injection.include-children) (#org-set-block-language! @_lang)) (latex_env (contents) @injection.content (#set! injection.include-children) (#set! injection.language "tex")) diff --git a/tests/plenary/object/date_spec.lua b/tests/plenary/object/date_spec.lua index 1577f56d9..0944249b5 100644 --- a/tests/plenary/object/date_spec.lua +++ b/tests/plenary/object/date_spec.lua @@ -440,146 +440,6 @@ describe('Date object', function() assert.are.same('8 d. ago', date:humanize(now)) end) - it('should parse single date from line', function() - local line = 'This is some line and has a date <2021-05-15 Sat> that is active' - local dates = Date.parse_all_from_line(line, 1) - assert.are.same(1, #dates) - assert.are.same({ - active = true, - adjustments = {}, - date_only = true, - day = 15, - dayname = 'Sat', - isdst = true, - wday = 7, - hour = 0, - min = 0, - month = 5, - is_date_range_start = false, - is_date_range_end = false, - related_date = nil, - range = Range:new({ - start_line = 1, - end_line = 1, - start_col = 34, - end_col = 49, - }), - timestamp = get_timestamp(2021, 5, 15), - type = 'NONE', - year = 2021, - }, dates[1]) - end) - - it('should parse multiple dates from line', function() - local line = - 'This is some line and has a date <2021-05-15 Sat> that is active and has a date [2021-06-15 Tue 09:25] that is inactive' - local dates = Date.parse_all_from_line(line, 1) - assert.are.same(2, #dates) - assert.are.same({ - active = true, - adjustments = {}, - date_only = true, - day = 15, - dayname = 'Sat', - hour = 0, - min = 0, - isdst = true, - month = 5, - is_date_range_start = false, - is_date_range_end = false, - related_date = nil, - wday = 7, - range = Range:new({ - start_line = 1, - end_line = 1, - start_col = 34, - end_col = 49, - }), - timestamp = get_timestamp(2021, 5, 15), - type = 'NONE', - year = 2021, - }, dates[1]) - assert.are.same({ - active = false, - adjustments = {}, - date_only = false, - day = 15, - dayname = 'Tue', - hour = 9, - wday = 3, - min = 25, - month = 6, - isdst = true, - is_date_range_start = false, - is_date_range_end = false, - related_date = nil, - range = Range:new({ - start_line = 1, - end_line = 1, - start_col = 81, - end_col = 102, - }), - timestamp = get_timestamp(2021, 6, 15, 9, 25), - type = 'NONE', - year = 2021, - }, dates[2]) - end) - - it('should parse multiple dates from line and setup proper range with same dates', function() - local line = - 'This is some line and has a date <2021-05-15 Sat> and again has the same date <2021-05-15 Sat> for no reason' - local dates = Date.parse_all_from_line(line, 1) - assert.are.same(2, #dates) - assert.are.same({ - active = true, - adjustments = {}, - date_only = true, - day = 15, - dayname = 'Sat', - hour = 0, - min = 0, - isdst = true, - month = 5, - wday = 7, - is_date_range_start = false, - is_date_range_end = false, - related_date = nil, - range = Range:new({ - start_line = 1, - end_line = 1, - start_col = 34, - end_col = 49, - }), - timestamp = get_timestamp(2021, 5, 15), - type = 'NONE', - year = 2021, - }, dates[1]) - assert.are.same({ - active = true, - adjustments = {}, - date_only = true, - day = 15, - dayname = 'Sat', - hour = 0, - min = 0, - month = 5, - wday = 7, - isdst = true, - is_date_range_start = false, - is_date_range_end = false, - related_date = nil, - range = Range:new({ - start_line = 1, - end_line = 1, - start_col = 79, - end_col = 94, - }), - timestamp = get_timestamp(2021, 5, 15), - type = 'NONE', - year = 2021, - }, dates[2]) - end) - it('should set and get isoweekday', function() local sunday = Date.from_string('2021-05-16') assert.are.same(7, sunday:get_isoweekday()) @@ -683,139 +543,6 @@ describe('Date object', function() assert.are.same({ '+1w' }, tuesday_morning.adjustments) assert.are.same('2021-05-17 Mon 23:30-00:30 +1w', tuesday_morning:to_string()) assert.are.same('23:30-00:30', tuesday_morning:format_time()) - - local line = - 'This line has a date rang <2021-05-15 Sat 14:30-15:30 +1w> and again has some date <2021-05-17 Mon> for no reason' - local dates = Date.parse_all_from_line(line, 1) - assert.are.same(2, #dates) - assert.are.same({ - active = true, - adjustments = { '+1w' }, - date_only = false, - day = 15, - dayname = 'Sat', - hour = 14, - min = 30, - isdst = true, - month = 5, - wday = 7, - is_date_range_start = false, - is_date_range_end = false, - related_date = nil, - range = Range:new({ - start_line = 1, - end_line = 1, - start_col = 27, - end_col = 58, - }), - timestamp = get_timestamp(2021, 5, 15, 14, 30), - timestamp_end = get_timestamp(2021, 5, 15, 15, 30), - type = 'NONE', - year = 2021, - }, dates[1]) - assert.are.same({ - active = true, - adjustments = {}, - date_only = true, - day = 17, - dayname = 'Mon', - hour = 0, - min = 0, - month = 5, - wday = 2, - isdst = true, - is_date_range_start = false, - is_date_range_end = false, - related_date = nil, - range = Range:new({ - start_line = 1, - end_line = 1, - start_col = 84, - end_col = 99, - }), - timestamp = get_timestamp(2021, 5, 17), - type = 'NONE', - year = 2021, - }, dates[2]) - end) - - it('should parse date range from line', function() - local line = - 'This line has a date rang <2021-05-15 Sat>--<2021-05-16 Sun> and again has some date <2021-05-17 Mon> for no reason' - local dates = Date.parse_all_from_line(line, 1) - assert.are.same(3, #dates) - assert.are.same({ - active = true, - adjustments = {}, - date_only = true, - day = 15, - dayname = 'Sat', - hour = 0, - isdst = true, - wday = 7, - min = 0, - month = 5, - is_date_range_start = true, - is_date_range_end = false, - related_date = dates[2], - range = Range:new({ - start_line = 1, - end_line = 1, - start_col = 27, - end_col = 42, - }), - timestamp = get_timestamp(2021, 5, 15), - type = 'NONE', - year = 2021, - }, dates[1]) - assert.are.same({ - active = true, - adjustments = {}, - date_only = true, - day = 16, - dayname = 'Sun', - hour = 0, - min = 0, - wday = 1, - month = 5, - isdst = true, - is_date_range_start = false, - is_date_range_end = true, - related_date = dates[1], - range = Range:new({ - start_line = 1, - end_line = 1, - start_col = 45, - end_col = 60, - }), - timestamp = get_timestamp(2021, 5, 16), - type = 'NONE', - year = 2021, - }, dates[2]) - assert.are.same({ - active = true, - adjustments = {}, - date_only = true, - day = 17, - dayname = 'Mon', - hour = 0, - min = 0, - wday = 2, - month = 5, - is_date_range_start = false, - is_date_range_end = false, - related_date = nil, - isdst = true, - range = Range:new({ - start_line = 1, - end_line = 1, - start_col = 86, - end_col = 101, - }), - timestamp = get_timestamp(2021, 5, 17), - type = 'NONE', - year = 2021, - }, dates[3]) end) it('should allow diffing time in minutes', function() From c9876cfebcec5b634c5d30b45f01ba6a4c47eb2c Mon Sep 17 00:00:00 2001 From: Kristijan Husak Date: Sun, 23 Feb 2025 13:03:35 +0100 Subject: [PATCH 03/10] feat(ts): set up priority capture and add inline code block --- docs/configuration.org | 2 ++ lua/orgmode/config/init.lua | 28 ++++++++++++++++++++++++++++ lua/orgmode/files/headline.lua | 8 +++++--- lua/orgmode/org/links/hyperlink.lua | 2 -- queries/org/highlights.scm | 11 ++++++----- queries/org/injections.scm | 6 +++++- 6 files changed, 46 insertions(+), 11 deletions(-) diff --git a/docs/configuration.org b/docs/configuration.org index a9af671d9..2936aae93 100644 --- a/docs/configuration.org +++ b/docs/configuration.org @@ -2825,6 +2825,8 @@ The following highlight groups are used: - =@org.drawer=: Drawer start/end delimiters - linked to =@property= - =@org.tag=: A tag for a headline item, shown on the righthand side like =:foo:= - linked to =@tag.attribute= - =@org.plan=: =SCHEDULED=, =DEADLINE=, =CLOSED=, etc. keywords - linked to =Constant= +- =@org.block=: A =begin/end= block (example: =#begin_src=) - linked to =@comment= +- =@org.inline_block=: A =src_lang= block (example: src_lua{ print('foo') } ) - linked to =@comment= - =@org.comment=: A comment block - linked to =@comment= - =@org.latex_env=: LaTeX block - linked to =@markup.environment= - =@org.directive=: Blocks starting with =#+= - linked to =@comment= diff --git a/lua/orgmode/config/init.lua b/lua/orgmode/config/init.lua index c2ab25891..835b151b3 100644 --- a/lua/orgmode/config/init.lua +++ b/lua/orgmode/config/init.lua @@ -10,6 +10,7 @@ local PriorityState = require('orgmode.objects.priority_state') ---@class OrgConfig:OrgConfigOpts ---@field opts table ---@field todo_keywords OrgTodoKeywords +---@field priorities table local Config = {} ---@param opts? table @@ -17,6 +18,7 @@ function Config:new(opts) local data = { opts = vim.tbl_deep_extend('force', defaults, opts or {}), todo_keywords = nil, + priorities = nil, } setmetatable(data, self) return data @@ -41,6 +43,7 @@ end ---@return OrgConfig function Config:extend(opts) self.todo_keywords = nil + self.priorities = nil opts = opts or {} self:_deprecation_notify(opts) if not self:_are_priorities_valid(opts) then @@ -326,6 +329,10 @@ function Config:get_priority_range() end function Config:get_priorities() + if self.priorities then + return self.priorities + end + local priorities = { [self.opts.org_priority_highest] = { type = 'highest', hl_group = '@org.priority.highest' }, } @@ -351,6 +358,9 @@ function Config:get_priorities() -- we need to overwrite the lowest value set by the second loop priorities[self.opts.org_priority_lowest] = { type = 'lowest', hl_group = '@org.priority.lowest' } + -- Cache priorities to avoid unnecessary recalculations + self.priorities = priorities + return priorities end @@ -413,6 +423,8 @@ function Config:setup_ts_predicates() end local text = vim.treesitter.get_node_text(node, source) + -- Leave only priority cookie: [#A] -> A + text = text:sub(3, -2) return valid_priorities[text] and valid_priorities[text].type == type end, { force = true, all = false }) @@ -428,6 +440,22 @@ function Config:setup_ts_predicates() metadata['injection.language'] = utils.detect_filetype(text, true) end, { force = true, all = false }) + vim.treesitter.query.add_directive('org-set-inline-block-language!', function(match, _, bufnr, pred, metadata) + local lang_node = match[pred[2]] + if not lang_node then + return + end + local text = vim.treesitter.get_node_text(lang_node, bufnr) + if not text or vim.trim(text) == '' then + return + end + -- Remove `src_` part: src_lua -> lua + text = text:sub(5) + -- Remove opening brackend and parameters: lua[params]{ -> lua + text = text:gsub('[%{%[].*', '') + metadata['injection.language'] = utils.detect_filetype(text, true) + end, { force = true, all = false }) + vim.treesitter.query.add_predicate('org-is-headline-level?', function(match, _, _, predicate) ---@type TSNode local node = match[predicate[2]] diff --git a/lua/orgmode/files/headline.lua b/lua/orgmode/files/headline.lua index f8c16bca5..16673b1a1 100644 --- a/lua/orgmode/files/headline.lua +++ b/lua/orgmode/files/headline.lua @@ -75,13 +75,15 @@ function Headline:get_priority() return '', nil end - local value = self.file:get_node_text(priority_node:field('value')[1]) + local value = self.file:get_node_text(priority_node) + -- Parse only the priority cookie, [#A] -> A + local priority = value:sub(3, -2) - if not value then + if not config:get_priorities()[priority] then return '', nil end - return value, priority_node + return priority, priority_node end ---@param amount number diff --git a/lua/orgmode/org/links/hyperlink.lua b/lua/orgmode/org/links/hyperlink.lua index 88ee16ced..f118448bb 100644 --- a/lua/orgmode/org/links/hyperlink.lua +++ b/lua/orgmode/org/links/hyperlink.lua @@ -8,8 +8,6 @@ local ts_utils = require('orgmode.utils.treesitter') ---@field range? OrgRange local OrgHyperlink = {} -local pattern = '%[%[([^%]]+.-)%]%]' - ---@param str string ---@param range? OrgRange ---@return OrgHyperlink diff --git a/queries/org/highlights.scm b/queries/org/highlights.scm index eace05b71..7b334c52e 100644 --- a/queries/org/highlights.scm +++ b/queries/org/highlights.scm @@ -11,11 +11,11 @@ (headline (stars) @stars (#org-is-headline-level? @stars "8")) @org.headline.level8 (item . (expr) @org.keyword.todo @nospell (#org-is-todo-keyword? @org.keyword.todo "TODO")) (item . (expr) @org.keyword.done @nospell (#org-is-todo-keyword? @org.keyword.done "DONE")) -(item priority: (priority value: (expr) @_priority) @org.priority.highest (#org-is-valid-priority? @_priority "highest")) -(item priority: (priority value: (expr) @_priority) @org.priority.high (#org-is-valid-priority? @_priority "high")) -(item priority: (priority value: (expr) @_priority) @org.priority.default (#org-is-valid-priority? @_priority "default")) -(item priority: (priority value: (expr) @_priority) @org.priority.low (#org-is-valid-priority? @_priority "low")) -(item priority: (priority value: (expr) @_priority) @org.priority.lowest (#org-is-valid-priority? @_priority "lowest")) +(item priority: (priority) @org.priority.highest (#org-is-valid-priority? @org.priority.highest "highest")) +(item priority: (priority) @org.priority.high (#org-is-valid-priority? @org.priority.high "high")) +(item priority: (priority) @org.priority.default (#org-is-valid-priority? @org.priority.default "default")) +(item priority: (priority) @org.priority.low (#org-is-valid-priority? @org.priority.low "low")) +(item priority: (priority) @org.priority.lowest (#org-is-valid-priority? @org.priority.lowest "lowest")) (list (listitem (paragraph) @spell)) (body (paragraph) @spell) (bullet) @org.bullet @@ -23,6 +23,7 @@ (checkbox status: (expr "-") @org.checkbox.halfchecked) (checkbox status: (expr "str") @org.checkbox.checked (#any-of? @org.checkbox.checked "x" "X")) (block "#+begin_" @org.block "#+end_" @org.block) +(inline_code_block open: (open) @org.inline_block close: (close) @org.inline_block) (block name: (expr) @org.block) (block end_name: (expr) @org.block) (block parameter: (expr) @org.block) diff --git a/queries/org/injections.scm b/queries/org/injections.scm index f03158bfd..0944e03da 100644 --- a/queries/org/injections.scm +++ b/queries/org/injections.scm @@ -1,3 +1,7 @@ (block parameter: (expr) @_lang (contents) @injection.content (#set! injection.include-children) (#org-set-block-language! @_lang)) - ; (inline_code_block language: (language) @_lang contents: (contents) @injection.content (#set! injection.include-children) (#org-set-block-language! @_lang)) + (inline_code_block + open: (open) @_lang + contents: (contents) @injection.content + (#set! injection.include-children) + (#org-set-inline-block-language! @_lang)) (latex_env (contents) @injection.content (#set! injection.include-children) (#set! injection.language "tex")) From 06131e15f327c655e3bfd1ce556221b2d75e790b Mon Sep 17 00:00:00 2001 From: Kristijan Husak Date: Sun, 23 Feb 2025 13:46:09 +0100 Subject: [PATCH 04/10] feat(injection): add elisp and shell as valid filetypes --- lua/orgmode/utils/init.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/orgmode/utils/init.lua b/lua/orgmode/utils/init.lua index 443f59340..f63ac48dd 100644 --- a/lua/orgmode/utils/init.lua +++ b/lua/orgmode/utils/init.lua @@ -565,12 +565,14 @@ end function utils.detect_filetype(name, skip_ftmatch) local map = { ['emacs-lisp'] = 'lisp', + elisp = 'lisp', js = 'javascript', ts = 'typescript', md = 'markdown', ex = 'elixir', pl = 'perl', sh = 'bash', + shell = 'bash', uxn = 'uxntal', } if not skip_ftmatch then From cf97218624ee9a3b5def95588b1962255ae5bd7c Mon Sep 17 00:00:00 2001 From: Kristijan Husak Date: Fri, 28 Feb 2025 22:28:19 +0100 Subject: [PATCH 05/10] feat(ts)!: Update required tree-sitter-org version --- .github/workflows/luarocks.yml | 2 +- lua/orgmode/health.lua | 7 +++++-- lua/orgmode/utils/init.lua | 14 ++++++++++++++ lua/orgmode/utils/treesitter/install.lua | 22 ++++++++++++++-------- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/.github/workflows/luarocks.yml b/.github/workflows/luarocks.yml index e8ab51189..0c0d6239d 100644 --- a/.github/workflows/luarocks.yml +++ b/.github/workflows/luarocks.yml @@ -31,4 +31,4 @@ jobs: with: version: ${{ env.LUAROCKS_VERSION }} dependencies: | - tree-sitter-orgmode ~> 1 + tree-sitter-orgmode ~> 2 diff --git a/lua/orgmode/health.lua b/lua/orgmode/health.lua index 4d8d8adc8..029aa27fa 100644 --- a/lua/orgmode/health.lua +++ b/lua/orgmode/health.lua @@ -10,10 +10,13 @@ function M.check() end function M.check_has_treesitter() - local ok, result, err = pcall(vim.treesitter.language.add, 'org') - if not ok or (not result and err ~= nil) then + local ts = require('orgmode.utils.treesitter.install') + if ts.not_installed() then return h.error('Treesitter grammar is not installed. Run `:Org install_treesitter_grammar` to install it.') end + if ts.outdated() then + return h.error('Treesitter grammar is out of date. Run `:Org install_treesitter_grammar` to update it.') + end return h.ok('Treesitter grammar installed') end diff --git a/lua/orgmode/utils/init.lua b/lua/orgmode/utils/init.lua index f63ac48dd..5d80a15ad 100644 --- a/lua/orgmode/utils/init.lua +++ b/lua/orgmode/utils/init.lua @@ -622,4 +622,18 @@ function utils.get_visual_selection() return table.concat(vim.fn.getregion(vim.fn.getpos('v'), vim.fn.getpos('.')), '\n') end +---@param msg string|string[] +---@param opts? { level?: 'info' | 'warn' | 'error', id: string } +---@return string +function utils.notify(msg, opts) + opts = vim.tbl_extend('force', { + title = 'Orgmode', + id = 'orgmode', + level = 'info', + }, opts or {}) + + local message = type(msg) == 'table' and table.concat(msg, '\n') or msg --[[@as string]] + vim.notify(message, vim.log.levels[opts.level:upper()], opts) +end + return utils diff --git a/lua/orgmode/utils/treesitter/install.lua b/lua/orgmode/utils/treesitter/install.lua index 33f3dcbef..37b77d61a 100644 --- a/lua/orgmode/utils/treesitter/install.lua +++ b/lua/orgmode/utils/treesitter/install.lua @@ -5,7 +5,7 @@ local M = { compilers = { vim.fn.getenv('CC'), 'cc', 'gcc', 'clang', 'cl', 'zig' }, } -local required_version = '1.3.4' +local required_version = '2.0.0' function M.install() if M.not_installed() then @@ -55,7 +55,7 @@ function M.get_package_path() end function M.get_lock_file() - return M.get_package_path() .. '/.org-ts-lock.json' + return vim.fs.joinpath(M.get_package_path(), '.org-ts-lock.json') end function M.select_compiler_args(compiler) @@ -119,7 +119,7 @@ function M.get_path(url, type) local is_local_path = vim.fn.isdirectory(local_path) == 1 if is_local_path then - utils.echo_info('Using local version of tree-sitter grammar...') + utils.notify('Using local version of tree-sitter grammar...') return Promise.resolve(local_path) end @@ -132,7 +132,7 @@ function M.get_path(url, type) reinstall = 'Reinstalling', } - utils.echo_info(('%s tree-sitter grammar...'):format(msg[type])) + utils.notify(('%s tree-sitter grammar...'):format(msg[type])) return M.exe('git', { args = { 'clone', '--filter=blob:none', '--depth=1', '--branch=' .. required_version, url, path }, }):next(function(code) @@ -145,8 +145,7 @@ end ---@param type? 'install' | 'update' | 'reinstall'' function M.run(type) - -- local url = 'https://github.com/nvim-orgmode/tree-sitter-org' - local url = '/home/kristijan/github/tree-sitter-org' + local url = 'https://github.com/nvim-orgmode/tree-sitter-org' local compiler = vim.tbl_filter(function(exe) return exe ~= vim.NIL and vim.fn.executable(exe) == 1 end, M.compilers)[1] @@ -171,12 +170,19 @@ function M.run(type) if code ~= 0 then error('[orgmode] Failed to compile parser', 0) end - local renamed = vim.fn.rename(path .. '/parser.so', package_path .. '/parser/org.so') + local source = vim.fs.joinpath(path, 'parser.so') + local destination = vim.fs.joinpath(package_path, 'parser', 'org.so') + local renamed = vim.fn.rename(source, destination) if renamed ~= 0 then error('[orgmode] Failed to move generated tree-sitter parser to runtime folder', 0) end utils.writefile(M.get_lock_file(), vim.json.encode({ version = required_version })):wait() - utils.echo_info('Done!') + local msg = { 'Done!' } + if type == 'update' then + table.insert(msg, 'Please restart Neovim to apply the changes.') + end + utils.notify(msg) + vim.treesitter.language.add('org') return true end)) :wait(60000) From b6dcfe5747f740bedc847c4ccddb360cf75e4374 Mon Sep 17 00:00:00 2001 From: Kristijan Husak Date: Sat, 1 Mar 2025 21:43:37 +0100 Subject: [PATCH 06/10] chore(links): deprecate old link class --- lua/orgmode/org/hyperlinks/link.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/orgmode/org/hyperlinks/link.lua b/lua/orgmode/org/hyperlinks/link.lua index 63f320ba2..9ca27c1e2 100644 --- a/lua/orgmode/org/hyperlinks/link.lua +++ b/lua/orgmode/org/hyperlinks/link.lua @@ -9,6 +9,7 @@ local Link = {} local pattern = '%[%[([^%]]+.-)%]%]' +---@deprecated Use OrgHyperlink instead ('orgmode.org.links.hyperlink') ---@param str string ---@param range? OrgRange ---@return OrgLink @@ -30,6 +31,7 @@ function Link:to_str() end end +---@deprecated Use OrgHyperlink instead ('orgmode.org.links.hyperlink') ---@param line string ---@param pos number ---@return OrgLink | nil, table | nil From c39d562db318df7046471bd8ec1d86b5d5b0d54c Mon Sep 17 00:00:00 2001 From: Kristijan Husak Date: Sat, 1 Mar 2025 22:07:55 +0100 Subject: [PATCH 07/10] chore: remove unused markup captures --- queries/org/markup.scm | 6 ------ 1 file changed, 6 deletions(-) diff --git a/queries/org/markup.scm b/queries/org/markup.scm index ed782a89f..e57a23ff0 100644 --- a/queries/org/markup.scm +++ b/queries/org/markup.scm @@ -4,12 +4,6 @@ (expr "_" @underline) (expr "=" @verbatim) (expr "+" @strikethrough) - (expr "[" @link.start) - (expr "]" @link.end) - (expr "[" @date_inactive.start "num" "-" "num" "-" "num") - (expr "]" @date_inactive.end) - (expr "<" @date_active.start "num" "-" "num" "-" "num") - (expr ">" @date_active.end) (expr "[" @footnote.start "str" @_fn ":" (#eq? @_fn "fn")) (expr "]" @footnote.end) (expr "\\" "str" @latex.plain) From ccd731973e660f061c91ffa6d2f1ad149a58b262 Mon Sep 17 00:00:00 2001 From: Kristijan Husak Date: Sun, 2 Mar 2025 09:07:21 +0100 Subject: [PATCH 08/10] chore(checkhealth): check for version mismatch --- lua/orgmode/health.lua | 17 ++++++--- lua/orgmode/utils/init.lua | 1 - lua/orgmode/utils/treesitter/install.lua | 46 ++++++++++++++++++------ 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/lua/orgmode/health.lua b/lua/orgmode/health.lua index 029aa27fa..98c7460ad 100644 --- a/lua/orgmode/health.lua +++ b/lua/orgmode/health.lua @@ -10,14 +10,23 @@ function M.check() end function M.check_has_treesitter() - local ts = require('orgmode.utils.treesitter.install') - if ts.not_installed() then + local version_info = require('orgmode.utils.treesitter.install').get_version_info() + if not version_info.installed then return h.error('Treesitter grammar is not installed. Run `:Org install_treesitter_grammar` to install it.') end - if ts.outdated() then + if version_info.outdated then return h.error('Treesitter grammar is out of date. Run `:Org install_treesitter_grammar` to update it.') end - return h.ok('Treesitter grammar installed') + + if version_info.version_mismatch then + return h.warn( + ('Treesitter grammar version mismatch (installed %s, required %s). Run `:Org install_treesitter_grammar` to update it.'):format( + version_info.installed_version, + version_info.required_version + ) + ) + end + return h.ok(('Treesitter grammar installed (version %s)'):format(version_info.installed_version)) end function M.check_setup() diff --git a/lua/orgmode/utils/init.lua b/lua/orgmode/utils/init.lua index 5d80a15ad..ffa2c0ab3 100644 --- a/lua/orgmode/utils/init.lua +++ b/lua/orgmode/utils/init.lua @@ -628,7 +628,6 @@ end function utils.notify(msg, opts) opts = vim.tbl_extend('force', { title = 'Orgmode', - id = 'orgmode', level = 'info', }, opts or {}) diff --git a/lua/orgmode/utils/treesitter/install.lua b/lua/orgmode/utils/treesitter/install.lua index 37b77d61a..a62edd1f4 100644 --- a/lua/orgmode/utils/treesitter/install.lua +++ b/lua/orgmode/utils/treesitter/install.lua @@ -8,16 +8,22 @@ local M = { local required_version = '2.0.0' function M.install() - if M.not_installed() then + local version_info = M.get_version_info() + if not version_info.installed then M.run('install') return true end - if M.outdated() then + if version_info.outdated then M.run('update') return true end + if version_info.version_mismatch then + M.reinstall() + return true + end + return false end @@ -25,9 +31,25 @@ function M.reinstall() return M.run('reinstall') end -function M.outdated() +function M.get_version_info() + local not_installed = M.not_installed() + if not_installed then + return { + installed = false, + installed_version = nil, + outdated = false, + required_version = required_version, + version_mismatch = false, + } + end local installed_version = M.get_installed_version() - return vim.version.lt(installed_version, required_version) + return { + installed = true, + installed_version = installed_version, + outdated = vim.version.lt(installed_version, required_version), + required_version = required_version, + version_mismatch = installed_version ~= required_version, + } end function M.not_installed() @@ -119,11 +141,11 @@ function M.get_path(url, type) local is_local_path = vim.fn.isdirectory(local_path) == 1 if is_local_path then - utils.notify('Using local version of tree-sitter grammar...') + utils.notify('Using local version of tree-sitter grammar...', { id = 'orgmode-treesitter-install' }) return Promise.resolve(local_path) end - local path = ('%s/tree-sitter-org'):format(vim.fn.stdpath('cache')) + local path = vim.fs.joinpath(vim.fn.stdpath('cache'), 'tree-sitter-org') vim.fn.delete(path, 'rf') local msg = { @@ -132,7 +154,7 @@ function M.get_path(url, type) reinstall = 'Reinstalling', } - utils.notify(('%s tree-sitter grammar...'):format(msg[type])) + utils.notify(('%s tree-sitter grammar...'):format(msg[type]), { id = 'orgmode-treesitter-install' }) return M.exe('git', { args = { 'clone', '--filter=blob:none', '--depth=1', '--branch=' .. required_version, url, path }, }):next(function(code) @@ -177,12 +199,16 @@ function M.run(type) error('[orgmode] Failed to move generated tree-sitter parser to runtime folder', 0) end utils.writefile(M.get_lock_file(), vim.json.encode({ version = required_version })):wait() - local msg = { 'Done!' } + local msg = { + 'Tree-sitter grammar installed!', + ('Version: %s'):format(required_version), + } if type == 'update' then table.insert(msg, 'Please restart Neovim to apply the changes.') end - utils.notify(msg) - vim.treesitter.language.add('org') + utils.notify(msg, { + id = 'orgmode-treesitter-install', + }) return true end)) :wait(60000) From dc6ce71be914036e69823445544416ff1598a775 Mon Sep 17 00:00:00 2001 From: Kristijan Husak Date: Sun, 2 Mar 2025 14:06:11 +0100 Subject: [PATCH 09/10] chore: ensure tree-sitter compatibility with new api --- .../colors/highlighter/markup/link.lua | 3 +- lua/orgmode/config/init.lua | 24 +++--- lua/orgmode/files/file.lua | 78 +++++++++++-------- lua/orgmode/files/headline.lua | 14 ++-- lua/orgmode/objects/date.lua | 18 +++++ lua/orgmode/org/mappings.lua | 9 +-- 6 files changed, 87 insertions(+), 59 deletions(-) diff --git a/lua/orgmode/colors/highlighter/markup/link.lua b/lua/orgmode/colors/highlighter/markup/link.lua index a2a898244..64f8fdf2e 100644 --- a/lua/orgmode/colors/highlighter/markup/link.lua +++ b/lua/orgmode/colors/highlighter/markup/link.lua @@ -31,6 +31,7 @@ function OrgLink:_set_directive() ---@type TSNode local capture_id = pred[2] local node = match[capture_id] + node = node and node[#node] metadata['image.ignore'] = true if not node or not self.has_extmark_url_support then @@ -63,7 +64,7 @@ function OrgLink:_set_directive() end metadata['image.ignore'] = nil metadata['image.src'] = url - end, { force = true, all = false }) + end, { force = true, all = true }) end ---@param node TSNode diff --git a/lua/orgmode/config/init.lua b/lua/orgmode/config/init.lua index 835b151b3..713cf2d03 100644 --- a/lua/orgmode/config/init.lua +++ b/lua/orgmode/config/init.lua @@ -370,13 +370,14 @@ function Config:setup_ts_predicates() vim.treesitter.query.add_predicate('org-is-todo-keyword?', function(match, _, source, predicate) local node = match[predicate[2]] + node = node and node[#node] if node then local text = vim.treesitter.get_node_text(node, source) return todo_keywords[text] and todo_keywords[text].type == predicate[3] or false end return false - end, { force = true, all = false }) + end, { force = true, all = true }) local org_cycle_separator_lines = math.max(self.opts.org_cycle_separator_lines, 0) @@ -384,9 +385,9 @@ function Config:setup_ts_predicates() if org_cycle_separator_lines == 0 then return end - ---@type TSNode | nil local capture_id = pred[2] local section_node = match[capture_id] + section_node = section_node and section_node[#section_node] if not capture_id or not section_node or section_node:type() ~= 'section' then return end @@ -412,24 +413,26 @@ function Config:setup_ts_predicates() end range[3] = range[3] - 1 metadata[capture_id].range = range - end, { force = true, all = false }) + end, { force = true, all = true }) vim.treesitter.query.add_predicate('org-is-valid-priority?', function(match, _, source, predicate) ---@type TSNode | nil local node = match[predicate[2]] - local type = predicate[3] + node = node and node[#node] if not node then return false end + local type = predicate[3] local text = vim.treesitter.get_node_text(node, source) -- Leave only priority cookie: [#A] -> A text = text:sub(3, -2) return valid_priorities[text] and valid_priorities[text].type == type - end, { force = true, all = false }) + end, { force = true, all = true }) vim.treesitter.query.add_directive('org-set-block-language!', function(match, _, bufnr, pred, metadata) local lang_node = match[pred[2]] + lang_node = lang_node and lang_node[#lang_node] if not lang_node then return end @@ -438,10 +441,11 @@ function Config:setup_ts_predicates() return end metadata['injection.language'] = utils.detect_filetype(text, true) - end, { force = true, all = false }) + end, { force = true, all = true }) vim.treesitter.query.add_directive('org-set-inline-block-language!', function(match, _, bufnr, pred, metadata) local lang_node = match[pred[2]] + lang_node = lang_node and lang_node[#lang_node] if not lang_node then return end @@ -454,18 +458,18 @@ function Config:setup_ts_predicates() -- Remove opening brackend and parameters: lua[params]{ -> lua text = text:gsub('[%{%[].*', '') metadata['injection.language'] = utils.detect_filetype(text, true) - end, { force = true, all = false }) + end, { force = true, all = true }) vim.treesitter.query.add_predicate('org-is-headline-level?', function(match, _, _, predicate) - ---@type TSNode local node = match[predicate[2]] - local level = tonumber(predicate[3]) + node = node and node[#node] if not node then return false end + local level = tonumber(predicate[3]) local _, _, _, node_end_col = node:range() return ((node_end_col - 1) % 8) + 1 == level - end, { force = true, all = false }) + end, { force = true, all = true }) end ---@param content table diff --git a/lua/orgmode/files/file.lua b/lua/orgmode/files/file.lua index de1044d2f..30046d3a6 100644 --- a/lua/orgmode/files/file.lua +++ b/lua/orgmode/files/file.lua @@ -167,47 +167,63 @@ function OrgFile:parse(skip_if_not_modified) end ---Parse the given tree-sitter query +---Prefer get_ts_captures if detailed node information is not required ---@param query string ----@param node? TSNode -function OrgFile:get_ts_matches(query, node) +---@param parent_node? TSNode +function OrgFile:get_ts_matches(query, parent_node) self:parse() - node = node or self.root - if not node then + parent_node = parent_node or self.root + if not parent_node then return {} end local ts_query = ts_utils.get_query(query) local matches = {} - local from, _, to = node:range() - for _, match, _ in ts_query:iter_matches(node, self:get_source(), from, to + 1, { all = false }) do + for _, match, _ in ts_query:iter_matches(parent_node, self:get_source(), nil, nil, { all = true }) do local items = {} - for id, matched_nodes in pairs(match) do + for id, nodes in pairs(match) do local name = ts_query.captures[id] - local matched_node = matched_nodes - if type(matched_nodes) == 'table' then - matched_node = matched_nodes[#matched_nodes] + for _, node in ipairs(nodes) do + local node_text = self:get_node_text_list(node) + items[name] = { + node = node, + text_list = node_text, + text = node_text[1], + } end - local node_text = self:get_node_text_list(matched_node) - items[name] = { - node = matched_node, - text_list = node_text, - text = node_text[1], - } end table.insert(matches, items) end return matches end +---@param query string +---@param node? TSNode +---@return TSNode[] +function OrgFile:get_ts_captures(query, node) + self:parse() + node = node or self.root + if not node then + return {} + end + local ts_query = ts_utils.get_query(query) + local matches = {} + + for _, match in ts_query:iter_captures(node, self:get_source()) do + table.insert(matches, match) + end + return matches +end + memoize('get_headlines') ---@return OrgHeadline[] function OrgFile:get_headlines() if self:is_archive_file() then return {} end - local matches = self:get_ts_matches('(section (headline) @headline)') - return vim.tbl_map(function(match) - return Headline:new(match.headline.node, self) + local matches = self:get_ts_captures('(section (headline) @headline)') + return vim.tbl_map(function(node) + return Headline:new(node, self) end, matches) end @@ -217,18 +233,18 @@ function OrgFile:get_top_level_headlines() if self:is_archive_file() then return {} end - local matches = self:get_ts_matches('(document (section (headline) @headline))') - return vim.tbl_map(function(match) - return Headline:new(match.headline.node, self) + local matches = self:get_ts_captures('(document (section (headline) @headline))') + return vim.tbl_map(function(node) + return Headline:new(node, self) end, matches) end memoize('get_headlines_including_archived') ---@return OrgHeadline[] function OrgFile:get_headlines_including_archived() - local matches = self:get_ts_matches('(section (headline) @headline)') - return vim.tbl_map(function(match) - return Headline:new(match.headline.node, self) + local matches = self:get_ts_captures('(section (headline) @headline)') + return vim.tbl_map(function(node) + return Headline:new(node, self) end, matches) end @@ -531,9 +547,9 @@ end memoize('get_blocks') --- @return OrgBlock[] function OrgFile:get_blocks() - local matches = self:get_ts_matches('(block) @block') - return vim.tbl_map(function(match) - return Block:new(match.block.node, self) + local matches = self:get_ts_captures('(block) @block') + return vim.tbl_map(function(node) + return Block:new(node, self) end, matches) end @@ -739,14 +755,14 @@ memoize('get_links') function OrgFile:get_links() self:parse(true) local links = {} - local matches = self:get_ts_matches([[ + local matches = self:get_ts_captures([[ (link) @link (link_desc) @link ]]) local source = self:get_source() - for _, match in ipairs(matches) do - table.insert(links, Hyperlink.from_node(match.link.node, source)) + for _, node in ipairs(matches) do + table.insert(links, Hyperlink.from_node(node, source)) end return links diff --git a/lua/orgmode/files/headline.lua b/lua/orgmode/files/headline.lua index 16673b1a1..1bde5b19b 100644 --- a/lua/orgmode/files/headline.lua +++ b/lua/orgmode/files/headline.lua @@ -736,8 +736,7 @@ function Headline:get_plan_dates() if name ~= 'NONE' then has_plan_dates = true end - dates[name:upper()] = Date.from_org_date(self.file:get_node_text(timestamp), { - range = Range.from_node(timestamp), + dates[name:upper()] = Date.from_node(timestamp, self.file:get_source(), { type = name:upper(), }) dates_nodes[name:upper()] = node @@ -769,16 +768,16 @@ function Headline:get_non_plan_dates() local property_node = section:field('property_drawer')[1] local matches = {} - local headline_matches = self.file:get_ts_matches('(item (timestamp) @timestamp)', headline_node) + local headline_matches = self.file:get_ts_captures('(item (timestamp) @timestamp)', headline_node) vim.list_extend(matches, headline_matches) if property_node then - local property_matches = self.file:get_ts_matches('(property (value (timestamp) @timestamp))', property_node) + local property_matches = self.file:get_ts_captures('(property (value (timestamp) @timestamp))', property_node) vim.list_extend(matches, property_matches) end if body_node then - local body_matches = self.file:get_ts_matches( + local body_matches = self.file:get_ts_captures( [[ (paragraph (timestamp) @timestamp) (table (row (cell (contents (timestamp) @timestamp)))) @@ -793,10 +792,7 @@ function Headline:get_non_plan_dates() local all_dates = {} local source = self.file:get_source() for _, match in ipairs(matches) do - local dates = Date.from_org_date(match.timestamp.text, { - range = Range.from_node(match.timestamp.node), - type = ts_utils.is_date_in_drawer(match.timestamp.node, 'logbook', source) and 'LOGBOOK' or 'NONE', - }) + local dates = Date.from_node(match, source) vim.list_extend(all_dates, dates) end diff --git a/lua/orgmode/objects/date.lua b/lua/orgmode/objects/date.lua index 5ea05a3c4..b64ace184 100644 --- a/lua/orgmode/objects/date.lua +++ b/lua/orgmode/objects/date.lua @@ -2,6 +2,7 @@ local spans = { d = 'day', m = 'month', y = 'year', h = 'hour', w = 'week', M = 'min' } local config = require('orgmode.config') local utils = require('orgmode.utils') +local ts_utils = require('orgmode.utils.treesitter') local Range = require('orgmode.files.elements.range') local pattern = '([<%[])(%d%d%d%d%-%d?%d%-%d%d[^>%]]*)([>%]])' local date_format = '%Y-%m-%d' @@ -246,6 +247,23 @@ function OrgDate.from_timestamp(timestamp, opts) return OrgDate:new(data) end +---@param node TSNode | nil +---@param source? integer | string +---@param opts? OrgDateOpts +---@return OrgDate[] +function OrgDate.from_node(node, source, opts) + if not node then + return {} + end + opts = opts or {} + opts.range = opts.range or Range.from_node(node) + source = source or 0 + if not opts.type then + opts.type = ts_utils.is_date_in_drawer(node, 'logbook', source) and 'LOGBOOK' or 'NONE' + end + return OrgDate.from_org_date(vim.treesitter.get_node_text(node, source), opts) +end + ---Accept org format date, for example <2025-22-01 Wed> or range <2025-22-01 Wed>--<2025-24-01 Fri> ---@param datestr string ---@param opts? OrgDateOpts diff --git a/lua/orgmode/org/mappings.lua b/lua/orgmode/org/mappings.lua index 560951e1a..6dc8a9619 100644 --- a/lua/orgmode/org/mappings.lua +++ b/lua/orgmode/org/mappings.lua @@ -1126,14 +1126,7 @@ function OrgMappings:_get_date_under_cursor(col_offset) if item then dates = item:get_all_dates() else - local date_node = ts_utils.closest_node(ts_utils.get_node(), 'timestamp') - if not date_node then - return nil - end - dates = Date.from_org_date(vim.treesitter.get_node_text(date_node, 0), { - range = Range.from_node(date_node), - type = ts_utils.is_date_in_drawer(date_node, 'logbook') and 'LOGBOOK' or 'NONE', - }) + dates = Date.from_node(ts_utils.closest_node(ts_utils.get_node(), 'timestamp')) end local valid_dates = vim.tbl_filter(function(date) From fef4396989c5fb4a83503038b3e6a7da3cfe091e Mon Sep 17 00:00:00 2001 From: Kristijan Husak Date: Sun, 2 Mar 2025 15:08:45 +0100 Subject: [PATCH 10/10] chore(ts): print correct message on grammar installation --- lua/orgmode/utils/treesitter/install.lua | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lua/orgmode/utils/treesitter/install.lua b/lua/orgmode/utils/treesitter/install.lua index a62edd1f4..8af0da644 100644 --- a/lua/orgmode/utils/treesitter/install.lua +++ b/lua/orgmode/utils/treesitter/install.lua @@ -199,16 +199,18 @@ function M.run(type) error('[orgmode] Failed to move generated tree-sitter parser to runtime folder', 0) end utils.writefile(M.get_lock_file(), vim.json.encode({ version = required_version })):wait() - local msg = { - 'Tree-sitter grammar installed!', - ('Version: %s'):format(required_version), - } + local msg = { 'Tree-sitter grammar installed!' } + if type == 'update' then - table.insert(msg, 'Please restart Neovim to apply the changes.') + msg = { + 'Tree-sitter grammar updated!', + 'Please restart Neovim to apply the changes.', + } end utils.notify(msg, { id = 'orgmode-treesitter-install', }) + vim.treesitter.language.add('org') return true end)) :wait(60000)