diff --git a/lua/telescope-orgmode/init.lua b/lua/telescope-orgmode/init.lua new file mode 100644 index 0000000..d52f758 --- /dev/null +++ b/lua/telescope-orgmode/init.lua @@ -0,0 +1,4 @@ +return { + refile_heading = require('telescope-orgmode.refile_heading'), + search_headings = require('telescope-orgmode.search_headings'), +} diff --git a/lua/telescope-orgmode/refile_heading.lua b/lua/telescope-orgmode/refile_heading.lua new file mode 100644 index 0000000..96c89dc --- /dev/null +++ b/lua/telescope-orgmode/refile_heading.lua @@ -0,0 +1,57 @@ +local pickers = require('telescope.pickers') +local finders = require('telescope.finders') +local conf = require('telescope.config').values +local action_set = require('telescope.actions.set') +local actions = require('telescope.actions') +local action_state = require('telescope.actions.state') + +local utils = require('telescope-orgmode.utils') + +local OrgApi = require('orgmode.api') + +local M = {} + +M.refile = function(prompt_bufnr) + local entry = action_state.get_selected_entry() + actions.close(prompt_bufnr) + + -- Refile to the file by default + local destination = entry.value.file + + -- Refile to a specific heading if is set + if entry.value.headline then + destination = entry.value.headline + end + + return OrgApi.refile({ + source = M.closest_headline, + destination = destination, + }) +end + +M.closest_headline = nil + +return function(opts) + opts = opts or {} + + M.closest_headline = OrgApi.current():get_closest_headline() + + pickers + .new(opts, { + -- TODO: alter prompt title when depth is 0: Refile under file, Refile + -- under Headline + prompt_title = 'Refile Destination', + finder = finders.new_table({ + results = utils.get_entries(opts), + entry_maker = opts.entry_maker or utils.make_entry(opts), + }), + sorter = conf.generic_sorter(opts), + previewer = conf.grep_previewer(opts), + attach_mappings = function(prompt_bufnr, map) + action_set.select:replace(M.refile) + map('i', '', utils.gen_depth_toggle(opts, prompt_bufnr)) + return true + end, + }) + :find() +end diff --git a/lua/telescope-orgmode/search_headings.lua b/lua/telescope-orgmode/search_headings.lua new file mode 100644 index 0000000..f67a5df --- /dev/null +++ b/lua/telescope-orgmode/search_headings.lua @@ -0,0 +1,21 @@ +local pickers = require('telescope.pickers') +local finders = require('telescope.finders') +local conf = require('telescope.config').values + +local utils = require('telescope-orgmode.utils') + +return function(opts) + opts = opts or {} + + pickers + .new(opts, { + prompt_title = 'Search Headings', + finder = finders.new_table({ + results = utils.get_entries(opts), + entry_maker = opts.entry_maker or utils.make_entry(opts), + }), + sorter = conf.generic_sorter(opts), + previewer = conf.grep_previewer(opts), + }) + :find() +end diff --git a/lua/telescope-orgmode/typehints.lua b/lua/telescope-orgmode/typehints.lua new file mode 100644 index 0000000..fb468d1 --- /dev/null +++ b/lua/telescope-orgmode/typehints.lua @@ -0,0 +1,61 @@ +-- Type-hints copied from nvim-orgmode to simplify development + +---@class OrgFileMetadata +---@field mtime number +---@field changedtick number + +---@class OrgFileOpts +---@field filename string +---@field lines string[] +---@field bufnr? number + +---@class OrgFile +---@field filename string +---@field lines string[] +---@field content string +---@field metadata OrgFileMetadata +---@field parser vim.treesitter.LanguageTree +---@field root TSNode + +---@class OrgApiFile +---@field category string current file category name. By default it's only filename without extension unless defined differently via #+CATEGORY directive +---@field filename string absolute path of the current file +---@field headlines OrgApiHeadline[] +---@field is_archive_file boolean +---@field private _file OrgFile +-- +---@class OrgRange +---@field start_line number +---@field start_col number +---@field end_line number +---@field end_col number + +---@class OrgApiHeadline +---@field title string headline title without todo keyword, tags and priority. Ex. `* TODO I am a headline :SOMETAG:` returns `I am a headline` +---@field line string full headline line +---@field level number headline level (number of asterisks). Example: 1 +---@field todo_value? string todo keyword of the headline (Example: TODO, DONE) +---@field todo_type? 'TODO' | 'DONE' | '' +---@field tags string[] List of own tags +---@field deadline OrgDate|nil +---@field scheduled OrgDate|nil +---@field properties table Table containing all properties. All keys are lowercased +---@field closed OrgDate|nil +---@field dates OrgDate[] List of all dates that are not "plan" dates +---@field position OrgRange +---@field all_tags string[] List of all tags (own + inherited) +---@field file OrgApiFile +---@field parent OrgApiHeadline|nil +---@field priority string|nil +---@field is_archived boolean headline marked with the `:ARCHIVE:` tag +---@field headlines OrgApiHeadline[] +-- +---@class OrgApiRefileOpts +---@field source OrgApiHeadline +---@field destination OrgApiFile | OrgApiHeadline + +---@class OrgApi +---@field load fun(name?: string|string[]): OrgApiFile|OrgApiFile[] +---@field current fun(): OrgApiFile +---@field refile fun(opts: OrgApiRefileOpts) +---@field insert_link fun(link_location: string): boolean diff --git a/lua/telescope-orgmode/utils.lua b/lua/telescope-orgmode/utils.lua index 43af337..091fcf3 100644 --- a/lua/telescope-orgmode/utils.lua +++ b/lua/telescope-orgmode/utils.lua @@ -1,79 +1,121 @@ -local entry_display = require("telescope.pickers.entry_display") +require('telescope-orgmode.typehints') -local orgmode = require("orgmode.api") +local entry_display = require('telescope.pickers.entry_display') +local finders = require('telescope.finders') +local action_state = require('telescope.actions.state') +local state = require('telescope.state') + +local orgmode = require('orgmode.api') local utils = {} +---@class OrgEntry +---@field file OrgApiFile +---@field filename string +---@field headline? OrgApiHeadline + +---Fetches entrys from OrgApi and extracts the relevant information +---@param opts any +---@return OrgEntry[] utils.get_entries = function(opts) - local file_results = vim.tbl_map(function(file) - return { file = file, filename = file.filename } - end, orgmode.load()) - - if not opts.archived then - file_results = vim.tbl_filter(function(entry) - return not entry.file.is_archive_file - end, file_results) - end - - if opts.max_depth == 0 then - return file_results - end - - local results = {} - for _, file_entry in ipairs(file_results) do - for _, headline in ipairs(file_entry.file.headlines) do - local allowed_depth = opts.max_depth == nil or headline.level <= opts.max_depth - local allowed_archive = opts.archived or not headline.is_archived - if allowed_depth and allowed_archive then - local entry = { - file = file_entry.file, - filename = file_entry.filename, - headline = headline, - } - table.insert(results, entry) - end - end - end - - return results + ---@type { file: OrgApiFile, filename: string }[] + local file_results = vim.tbl_map(function(file) + return { file = file, filename = file.filename } + end, orgmode.load()) + + if not opts.archived then + file_results = vim.tbl_filter(function(entry) + return not entry.file.is_archive_file + end, file_results) + end + + if opts.max_depth == 0 then + return file_results + end + + local results = {} + for _, file_entry in ipairs(file_results) do + for _, headline in ipairs(file_entry.file.headlines) do + local allowed_depth = opts.max_depth == nil or headline.level <= opts.max_depth + local allowed_archive = opts.archived or not headline.is_archived + if allowed_depth and allowed_archive then + local entry = { + file = file_entry.file, + filename = file_entry.filename, + headline = headline, + } + table.insert(results, entry) + end + end + end + + return results end +---Entry-Maker for Telescope +---@param opts any +---@return fun(entry: OrgEntry):MatchEntry utils.make_entry = function(opts) - local displayer = entry_display.create({ - separator = " ", - items = { - { width = vim.F.if_nil(opts.location_width, 20) }, - { remaining = true }, - }, - }) - - local function make_display(entry) - return displayer({ entry.location, entry.line }) - end - - return function(entry) - local headline = entry.headline - - local lnum = nil - local location = vim.fn.fnamemodify(entry.filename, ":t") - local line = "" - - if headline then - lnum = headline.position.start_line - location = string.format("%s:%i", location, lnum) - line = string.format("%s %s", string.rep("*", headline.level), headline.title) - end - - return { - value = entry, - ordinal = location .. " " .. line, - filename = entry.filename, - lnum = lnum, - display = make_display, - location = location, - line = line, - } - end + local displayer = entry_display.create({ + separator = ' ', + items = { + { width = vim.F.if_nil(opts.location_width, 20) }, + { remaining = true }, + }, + }) + + local function make_display(entry) + return displayer({ entry.location, entry.line }) + end + + return function(entry) + local headline = entry.headline + + local lnum = nil + local location = vim.fn.fnamemodify(entry.filename, ':t') + local line = '' + + if headline then + lnum = headline.position.start_line + location = string.format('%s:%i', location, lnum) + line = string.format('%s %s', string.rep('*', headline.level), headline.title) + end + + return { + value = entry, + ordinal = location .. ' ' .. line, + filename = entry.filename, + lnum = lnum, + display = make_display, + location = location, + line = line, + } + end +end + +utils.gen_depth_toggle = function(opts, prompt_bufnr) + local status = state.get_status(prompt_bufnr) + status._ot_current_depth = opts.max_depth + status._ot_next_depth = nil + if status._ot_current_depth ~= 0 then + status._ot_next_depth = 0 + end + + return function() + local current_picker = action_state.get_current_picker(prompt_bufnr) + + local aux = status._ot_current_depth + status._ot_current_depth = status._ot_next_depth + status._ot_next_depth = aux + + opts.max_depth = status._ot_current_depth + local new_finder = finders.new_table({ + results = utils.get_entries(opts), + entry_maker = opts.entry_maker or utils.make_entry(opts), + }) + + current_picker:refresh(new_finder, opts) + end end return utils diff --git a/lua/telescope/_extensions/orgmode/init.lua b/lua/telescope/_extensions/orgmode/init.lua index 1526e2b..70c47ef 100644 --- a/lua/telescope/_extensions/orgmode/init.lua +++ b/lua/telescope/_extensions/orgmode/init.lua @@ -2,9 +2,9 @@ -- public orgmode api -- TODO: add highlight groups -return require("telescope").register_extension({ - exports = { - search_headings = require("telescope._extensions.orgmode.search_headings"), - refile_heading = require("telescope._extensions.orgmode.refile_heading"), - }, +return require('telescope').register_extension({ + exports = { + search_headings = require('telescope-orgmode').search_headings, + refile_heading = require('telescope-orgmode').refile_heading, + }, }) diff --git a/lua/telescope/_extensions/orgmode/refile_heading.lua b/lua/telescope/_extensions/orgmode/refile_heading.lua deleted file mode 100644 index 58fb090..0000000 --- a/lua/telescope/_extensions/orgmode/refile_heading.lua +++ /dev/null @@ -1,79 +0,0 @@ -local pickers = require("telescope.pickers") -local finders = require("telescope.finders") -local conf = require("telescope.config").values -local action_set = require("telescope.actions.set") -local actions = require("telescope.actions") -local action_state = require("telescope.actions.state") -local state = require("telescope.state") - -local utils = require("telescope-orgmode.utils") - -local api = require("orgmode.api") - -return function(opts) - opts = opts or {} - - local closest_headline = api.current():get_closest_headline() - - local function refile(prompt_bufnr) - local entry = action_state.get_selected_entry() - actions.close(prompt_bufnr) - - -- Refile to the file by default - local destination = entry.value.file - - -- Refile to a specific heading if is set - if entry.value.headline then - destination = entry.value.headline - end - - return api.refile({ - source = closest_headline, - destination = destination, - }) - end - - local function gen_depth_toggle(opts, prompt_bufnr) - local status = state.get_status(prompt_bufnr) - status._ot_current_depth = opts.max_depth - status._ot_next_depth = nil - if status._ot_current_depth ~= 0 then - status._ot_next_depth = 0 - end - - return function() - local current_picker = action_state.get_current_picker(prompt_bufnr) - - local aux = status._ot_current_depth - status._ot_current_depth = status._ot_next_depth - status._ot_next_depth = aux - - opts.max_depth = status._ot_current_depth - local new_finder = finders.new_table({ - results = utils.get_entries(opts), - entry_maker = opts.entry_maker or utils.make_entry(opts), - }) - - current_picker:refresh(new_finder, opts) - end - end - - pickers - .new(opts, { - -- TODO: alter prompt title when depth is 0: Refile under file, Refile - -- under Headline - prompt_title = "Refile Destination", - finder = finders.new_table({ - results = utils.get_entries(opts), - entry_maker = opts.entry_maker or utils.make_entry(opts), - }), - sorter = conf.generic_sorter(opts), - previewer = conf.grep_previewer(opts), - attach_mappings = function(prompt_bufnr, map) - action_set.select:replace(refile) - map("i", "", gen_depth_toggle(opts, prompt_bufnr)) - return true - end, - }) - :find() -end diff --git a/lua/telescope/_extensions/orgmode/search_headings.lua b/lua/telescope/_extensions/orgmode/search_headings.lua deleted file mode 100644 index c946fe7..0000000 --- a/lua/telescope/_extensions/orgmode/search_headings.lua +++ /dev/null @@ -1,21 +0,0 @@ -local pickers = require("telescope.pickers") -local finders = require("telescope.finders") -local conf = require("telescope.config").values - -local utils = require("telescope-orgmode.utils") - -return function(opts) - opts = opts or {} - - pickers - .new(opts, { - prompt_title = "Search Headings", - finder = finders.new_table({ - results = utils.get_entries(opts), - entry_maker = opts.entry_maker or utils.make_entry(opts), - }), - sorter = conf.generic_sorter(opts), - previewer = conf.grep_previewer(opts), - }) - :find() -end