diff --git a/DOCS.md b/DOCS.md index fe3a2838e..2f0c26ded 100644 --- a/DOCS.md +++ b/DOCS.md @@ -488,6 +488,8 @@ Templates have the following fields: * `template` (`string|string[]`) — body of the template that will be used when creating capture * `target` (`string?`) — name of the file to which the capture content will be added. If the target is not specified, the content will be added to the [`org_default_notes_file`](#orgdefaultnotesfile) file * `headline` (`string?`) — title of the headline after which the capture content will be added. If no headline is specified, the content will be appended to the end of the file + * `datetree (boolean | { time_prompt: boolean })` — Create a [date tree](https://orgmode.org/manual/Template-elements.html#FOOT84) with current day in the target file and put the capture content there. + When `time_prompt = true`, open up a date picker to select a date before opening up a capture buffer. * `properties` (`table?`): * `empty_lines` (`table|number?`) — if the value is a number, then empty lines are added before and after the content. If the value is a table, then the following fields are expected: * `before` (`integer?`) — add empty lines to the beginning of the content diff --git a/lua/orgmode/capture/init.lua b/lua/orgmode/capture/init.lua index 20841a769..f63ef1bd4 100644 --- a/lua/orgmode/capture/init.lua +++ b/lua/orgmode/capture/init.lua @@ -6,6 +6,7 @@ local Menu = require('orgmode.ui.menu') local Range = require('orgmode.files.elements.range') local CaptureWindow = require('orgmode.capture.window') local Date = require('orgmode.objects.date') +local Datetree = require('orgmode.capture.template.datetree') ---@class OrgProcessRefileOpts ---@field source_headline OrgHeadline @@ -18,7 +19,7 @@ local Date = require('orgmode.objects.date') ---@field template OrgCaptureTemplate ---@field source_file OrgFile ---@field source_headline? OrgHeadline ----@field destination_file? OrgFile +---@field destination_file OrgFile ---@field destination_headline? OrgHeadline ---@class OrgCapture @@ -55,6 +56,15 @@ function Capture:open_template(template) end, }) + if template:has_input_prompts() then + return template:prompt_for_inputs():next(function(proceed) + if not proceed then + return utils.echo_info('Canceled.') + end + return self._window:open() + end) + end + return self._window:open() end @@ -124,17 +134,23 @@ end function Capture:_refile_from_capture_buffer(opts) local target_level = 0 local target_line = -1 + local destination_file = opts.destination_file + local destination_headline = opts.destination_headline - if opts.destination_headline then - target_level = opts.destination_headline:get_level() - target_line = opts.destination_headline:get_range().end_line + if opts.template.datetree then + destination_headline = Datetree:new({ files = self.files }):create(opts.template) + end + + if destination_headline then + target_level = destination_headline:get_level() + target_line = destination_headline:get_range().end_line end local lines = opts.source_file.lines if opts.source_headline then lines = opts.source_headline:get_lines() - if opts.destination_headline or opts.source_headline:get_level() > 1 then + if destination_headline or opts.source_headline:get_level() > 1 then lines = self:_adapt_headline_level(opts.source_headline, target_level, false) end end @@ -142,13 +158,13 @@ function Capture:_refile_from_capture_buffer(opts) lines = opts.template:apply_properties_to_lines(lines) self.files - :update_file(opts.destination_file.filename, function(file) + :update_file(destination_file.filename, function(file) local range = self:_get_destination_range_without_empty_lines(Range.from_line(target_line)) vim.api.nvim_buf_set_lines(file:bufnr(), range.start_line, range.end_line, false, lines) end) :wait() - utils.echo_info(('Wrote %s'):format(opts.destination_file.filename)) + utils.echo_info(('Wrote %s'):format(destination_file.filename)) self:kill() return true end @@ -413,8 +429,7 @@ end ---@private ---@return OrgProcessCaptureOpts | false function Capture:_get_refile_vars() - local target = self._window.template.target - local file = vim.fn.resolve(vim.fn.fnamemodify(target, ':p')) + local file = self._window.template:get_target() if vim.fn.filereadable(file) == 0 then local choice = vim.fn.confirm(('Refile destination %s does not exist. Create now?'):format(file), '&Yes\n&No') diff --git a/lua/orgmode/capture/template/datetree.lua b/lua/orgmode/capture/template/datetree.lua new file mode 100644 index 000000000..ab8d981de --- /dev/null +++ b/lua/orgmode/capture/template/datetree.lua @@ -0,0 +1,163 @@ +local utils = require('orgmode.utils') +local Date = require('orgmode.objects.date') + +---@class OrgDatetree +---@field files OrgFiles +local Datetree = {} +Datetree.__index = Datetree + +---@param opts { files: OrgFiles } +function Datetree:new(opts) + return setmetatable({ + files = opts.files, + }, Datetree) +end + +---@param template OrgCaptureTemplate +---@return OrgHeadline +function Datetree:create(template) + local date = template:get_datetree_date() + local destination_file = self.files:get(template:get_target()) + local result = self:_get_datetree_destination(destination_file, date) + + if result.create then + self.files + :update_file(destination_file.filename, function(file) + vim.api.nvim_buf_set_lines(file:bufnr(), result.target_line, result.target_line, false, result.content) + end) + :wait() + destination_file = destination_file:reload_sync() + end + + return destination_file:get_closest_headline({ result.headline_at, 0 }) +end + +---@private +---@param destination_file OrgFile +---@param date OrgDate +---@return { create: boolean, target_line: number, content: string[], headline_at: number } +function Datetree:_get_datetree_destination(destination_file, date) + local year_date = date:format('%Y') + local month_date = date:start_of('month') + local month_date_str = date:format('%Y-%m %B') + local day_date = date:format('%Y-%m-%d %A') + local year_headline = utils.find(destination_file:get_top_level_headlines(), function(headline) + return headline:get_title() == year_date + end) + + if not year_headline then + local target_line = self:_get_insert_year_at(destination_file, year_date) + return { + create = true, + target_line = target_line, + headline_at = target_line + 3, + content = { + '* ' .. year_date, + '** ' .. month_date_str, + '*** ' .. day_date, + }, + } + end + + local month_headline = utils.find(year_headline:get_child_headlines(), function(month) + return month:get_title() == month_date_str + end) + + if not month_headline then + local target_line = self:_get_insert_month_at(year_headline, month_date) + return { + create = true, + target_line = target_line, + headline_at = target_line + 2, + content = { + '** ' .. month_date_str, + '*** ' .. day_date, + }, + } + end + + local month_headlines = month_headline:get_child_headlines() + local day_headline = utils.find(month_headlines, function(day) + return day:get_title() == day_date + end) + + if not day_headline then + local target_line = self:_get_insert_day_at(month_headline, date) + + return { + create = true, + target_line = target_line, + headline_at = target_line + 1, + content = { + '*** ' .. day_date, + }, + } + end + + return { + create = false, + headline_at = day_headline:get_range().start_line, + } +end + +---@private +---@param destination_file OrgFile +---@param year_date string -- year in format YYYY +---@return number +function Datetree:_get_insert_year_at(destination_file, year_date) + local future_year_headline = utils.find(destination_file:get_top_level_headlines(), function(headline) + local get_year = headline:get_title():match('^%d%d%d%d$') + return get_year and tonumber(get_year) > tonumber(year_date) + end) + + if future_year_headline then + return future_year_headline:get_range().start_line - 1 + end + + return #destination_file.lines +end + +---@private +---@param year_headline OrgHeadline +---@param month_date OrgDate +---@return number +function Datetree:_get_insert_month_at(year_headline, month_date) + local future_month_headline = utils.find(year_headline:get_child_headlines(), function(headline) + local year_num, month_num = headline:get_title():match('^(%d%d%d%d)%-(%d%d)%s+%w+$') + if year_num and month_num then + local timestamp = os.time({ year = year_num, month = month_num, day = 1 }) + return timestamp > month_date.timestamp + end + return false + end) + + if future_month_headline then + return future_month_headline:get_range().start_line - 1 + end + + return year_headline:get_range().end_line +end + +---@private +---@param month_headline OrgHeadline +---@param date OrgDate +---@return number +function Datetree:_get_insert_day_at(month_headline, date) + local future_day_headline = utils.find(month_headline:get_child_headlines(), function(headline) + local year_num, month_num, day_num = headline:get_title():match('^(%d%d%d%d)%-(%d%d)%-(%d%d)%s+%w+$') + return year_num + and Date.from_table({ + year = year_num, + month = month_num, + day = day_num, + }):is_after(date, 'day') + end) + + if future_day_headline then + return future_day_headline:get_range().start_line - 1 + end + + return month_headline:get_range().end_line +end + +return Datetree diff --git a/lua/orgmode/capture/template/init.lua b/lua/orgmode/capture/template/init.lua index 9d6b2bd3d..ccbda1bd3 100644 --- a/lua/orgmode/capture/template/init.lua +++ b/lua/orgmode/capture/template/init.lua @@ -1,6 +1,8 @@ local TemplateProperties = require('orgmode.capture.template.template_properties') local Date = require('orgmode.objects.date') local utils = require('orgmode.utils') +local Calendar = require('orgmode.objects.calendar') +local Promise = require('orgmode.utils.promise') local expansions = { ['%f'] = function() @@ -32,15 +34,20 @@ local expansions = { end, } +---@alias OrgCaptureTemplateDatetree boolean | { time_prompt: boolean, date?: OrgDate } + ---@class OrgCaptureTemplate ----@field description string ----@field template string|string[] ----@field target string? ----@field headline string? ----@field properties OrgCaptureTemplateProperties ----@field subtemplates table +---@field description? string +---@field template? string|string[] +---@field target? string +---@field datetree? OrgCaptureTemplateDatetree +---@field headline? string +---@field properties? OrgCaptureTemplateProperties +---@field subtemplates? table local Template = {} +---@param opts OrgCaptureTemplate +---@return OrgCaptureTemplate function Template:new(opts) opts = opts or {} @@ -51,6 +58,7 @@ function Template:new(opts) headline = { opts.headline, 'string', true }, properties = { opts.properties, 'table', true }, subtemplates = { opts.subtemplates, 'table', true }, + datetree = { opts.datetree, { 'boolean', 'table' }, true }, }) local this = {} @@ -59,6 +67,7 @@ function Template:new(opts) this.target = self:_compile(opts.target or '') this.headline = opts.headline this.properties = TemplateProperties:new(opts.properties) + this.datetree = opts.datetree this.subtemplates = {} for key, subtemplate in pairs(opts.subtemplates or {}) do @@ -89,10 +98,39 @@ function Template:compile() if type(content) == 'table' then content = table.concat(content, '\n') end - content = self:_compile(content) + content = self:_compile(content or '') return vim.split(content, '\n', { plain = true }) end +function Template:has_input_prompts() + return self.datetree and type(self.datetree) == 'table' and self.datetree.time_prompt +end + +function Template:prompt_for_inputs() + if not self:has_input_prompts() then + return Promise.resolve(true) + end + return Calendar.new({ date = Date.now() }):open():next(function(date) + if date then + self.datetree.date = date + return true + end + return false + end) +end + +function Template:get_datetree_date() + if self:has_input_prompts() then + return self.datetree.date + end + return Date.today() +end + +---@return string +function Template:get_target() + return vim.fn.resolve(vim.fn.fnamemodify(self.target, ':p')) +end + ---@param lines string[] ---@return string[] function Template:apply_properties_to_lines(lines) diff --git a/lua/orgmode/files/file.lua b/lua/orgmode/files/file.lua index 431311a35..9c6a7f4aa 100644 --- a/lua/orgmode/files/file.lua +++ b/lua/orgmode/files/file.lua @@ -170,6 +170,18 @@ function OrgFile:get_headlines() end, matches) end +memoize('get_top_level_headlines') +---@return OrgHeadline[] +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) + end, matches) +end + memoize('get_headlines_including_archived') ---@return OrgHeadline[] function OrgFile:get_headlines_including_archived() @@ -181,8 +193,9 @@ end ---@param title string ---@param exact? boolean +---@param search_from_end? boolean ---@return OrgHeadline[] -function OrgFile:find_headlines_by_title(title, exact) +function OrgFile:find_headlines_by_title(title, exact, search_from_end) return vim.tbl_filter(function(item) local pattern = '^' .. vim.pesc(title:lower()) if exact then @@ -195,7 +208,9 @@ end ---@param title string ---@return OrgHeadline | nil function OrgFile:find_headline_by_title(title) - return self:find_headlines_by_title(title, true)[1] + return utils.find(self:get_headlines(), function(item) + return item:get_title():lower() == title:lower() + end) end ---@return OrgHeadline[] diff --git a/lua/orgmode/files/headline.lua b/lua/orgmode/files/headline.lua index aeeeffa46..f832afb15 100644 --- a/lua/orgmode/files/headline.lua +++ b/lua/orgmode/files/headline.lua @@ -166,11 +166,11 @@ function Headline:get_logbook() return nil end ----@return OrgDate +---@return OrgDate | nil function Headline:get_closed_date() - return vim.tbl_filter(function(date) + return utils.find(self:get_all_dates(), function(date) return date:is_closed() - end, self:get_all_dates())[1] + end) end function Headline:get_priority_sort_value() diff --git a/lua/orgmode/files/init.lua b/lua/orgmode/files/init.lua index a6ec028e0..0b77caf9b 100644 --- a/lua/orgmode/files/init.lua +++ b/lua/orgmode/files/init.lua @@ -281,7 +281,7 @@ function OrgFiles:_files() local all_files = vim.tbl_map(function(file) return vim.tbl_map(function(path) return vim.fn.resolve(path) - end, vim.fn.glob(vim.fn.fnamemodify(file, ':p'), 0, 1)) + end, vim.fn.glob(vim.fn.fnamemodify(file, ':p'), false, true)) end, files) all_files = utils.concat(vim.tbl_flatten(all_files), all_filenames, true) diff --git a/lua/orgmode/objects/date.lua b/lua/orgmode/objects/date.lua index 3989175e1..1a4bbdfa6 100644 --- a/lua/orgmode/objects/date.lua +++ b/lua/orgmode/objects/date.lua @@ -1,5 +1,6 @@ -- TODO -- Support diary format and format without short date name +---@type table local spans = { d = 'day', m = 'month', y = 'year', h = 'hour', w = 'week', M = 'min' } local config = require('orgmode.config') local utils = require('orgmode.utils') @@ -8,6 +9,8 @@ local pattern = '([<%[])(%d%d%d%d%-%d?%d%-%d%d[^>%]]*)([>%]])' local date_format = '%Y-%m-%d' local time_format = '%H:%M' +---@alias OrgDateSpan 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year' + ---@class OrgDate ---@field type string ---@field active boolean @@ -184,7 +187,8 @@ end ---@param data? table ---@return OrgDate local function today(data) - local opts = vim.tbl_deep_extend('force', os.date('*t', os.time()), data or {}) + local date = os.date('*t', os.time()) --[[@as osdate]] + local opts = vim.tbl_deep_extend('force', date, data or {}) opts.date_only = true return Date:new(opts) end @@ -198,7 +202,8 @@ end ---@param data? table ---@return OrgDate local function now(data) - local opts = vim.tbl_deep_extend('force', os.date('*t', os.time()), data or {}) + local date = os.date('*t', os.time()) --[[@as osdate]] + local opts = vim.tbl_deep_extend('force', date, data or {}) return Date:new(opts) end @@ -403,7 +408,7 @@ function Date:without_adjustments() return self:clone({ adjustments = {} }) end ----@param span string +---@param span OrgDateSpan ---@return OrgDate function Date:start_of(span) if #span == 1 then @@ -651,7 +656,7 @@ end ---@param format string ---@return string function Date:format(format) - return os.date(format, self.timestamp) + return tostring(os.date(format, self.timestamp)) end ---@param from OrgDate @@ -956,10 +961,23 @@ local function is_date_instance(value) return getmetatable(value) == Date end +---@param opts { year: number, month?: number, day?: number, hour?: number, min?: number, sec?: number } +---@return OrgDate +local function from_table(opts) + return Date:from_time_table({ + year = opts.year, + month = opts.month or 1, + day = opts.day or 1, + hour = opts.hour or 0, + min = opts.min or 0, + sec = opts.sec or 0, + }) +end return { parse_parts = parse_parts, from_org_date = from_org_date, from_string = from_string, + from_table = from_table, now = now, today = today, tomorrow = tomorrow, diff --git a/lua/orgmode/ui/virtual_indent.lua b/lua/orgmode/ui/virtual_indent.lua index 3257978b1..f59217388 100644 --- a/lua/orgmode/ui/virtual_indent.lua +++ b/lua/orgmode/ui/virtual_indent.lua @@ -138,6 +138,9 @@ function VirtualIndent:attach() self:set_indent(start_line, end_line) end) end, + on_reload = function() + self:set_indent(0, vim.api.nvim_buf_line_count(self._bufnr) - 1, true) + end, }) self._attached = true end diff --git a/lua/orgmode/utils/init.lua b/lua/orgmode/utils/init.lua index 8d56b57d4..74b4e683d 100644 --- a/lua/orgmode/utils/init.lua +++ b/lua/orgmode/utils/init.lua @@ -598,4 +598,17 @@ function utils.has_version_10() return not vim.version.lt({ v.major, v.minor, v.patch }, { 0, 10, 0 }) end +---@generic EntryType : any +---@param entries EntryType[] +---@param check_fn fun(entry: EntryType, index: number): boolean +---@return EntryType | nil +function utils.find(entries, check_fn) + for i, entry in ipairs(entries) do + if check_fn(entry, i) then + return entry + end + end + return nil +end + return utils diff --git a/tests/plenary/capture/capture_spec.lua b/tests/plenary/capture/capture_spec.lua index c23604e84..9ab9491bc 100644 --- a/tests/plenary/capture/capture_spec.lua +++ b/tests/plenary/capture/capture_spec.lua @@ -131,9 +131,13 @@ describe('Refile', function() it('to empty file', function() local destination_file = helpers.create_file({}) - local capture_lines = { '* foo' } - local capture_file = helpers.create_file_instance(capture_lines) - local source_headline = capture_file:get_headlines()[1] + local capture_lines = { + '* bar', + '* foo', + '* baz', + } + local capture_file = helpers.create_file(capture_lines) + local source_headline = capture_file:get_headlines()[2] ---@diagnostic disable-next-line: invisible org.capture:_refile_from_org_file({ @@ -155,10 +159,14 @@ describe('Refile', function() '', }) - local capture_lines = { '** baz' } - local capture_file = helpers.create_file_instance(capture_lines) + local capture_lines = { + '* foo', + '** baz', + '* bar', + } + local capture_file = helpers.create_file(capture_lines) assert(capture_file) - local item = capture_file:get_headlines()[1] + local item = capture_file:get_headlines()[2] ---@diagnostic disable-next-line: invisible org.capture:_refile_from_org_file({ @@ -207,7 +215,7 @@ describe('Refile', function() end) end) -describe('Refile with empty lines', function() +describe('Capture with empty lines', function() it('to empty file', function() local destination_file = helpers.create_file({}) diff --git a/tests/plenary/capture/datetree_spec.lua b/tests/plenary/capture/datetree_spec.lua new file mode 100644 index 000000000..3093a2866 --- /dev/null +++ b/tests/plenary/capture/datetree_spec.lua @@ -0,0 +1,376 @@ +---@diagnostic disable: invisible +local helpers = require('tests.plenary.helpers') +local Template = require('orgmode.capture.template') +local Date = require('orgmode.objects.date') + +describe('Datetree', function() + local org = require('orgmode') + ---@param date OrgDate + ---@return OrgProcessCaptureOpts + local get_template = function(date, content) + local filename = vim.fn.tempname() .. '.org' + vim.fn.writefile(content or {}, filename) + return { + destination_file = org.files:get(filename), + template = Template:new({ + target = filename, + template = '* %?', + datetree = { + time_prompt = true, + date = date, + }, + }), + } + end + + describe('When datetree does not exist', function() + it('creates a whole datetree', function() + local date = Date.today() + local opts = get_template(date) + local capture_lines = { '* baz' } + local capture_file = helpers.create_file_instance(capture_lines) + opts.source_file = capture_file + opts.source_headline = capture_file:get_headlines()[1] + + org.capture:_refile_from_capture_buffer(opts) + opts.destination_file:reload_sync() + assert.are.same({ + '* ' .. date:format('%Y'), + '** ' .. date:format('%Y-%m %B'), + '*** ' .. date:format('%Y-%m-%d %A'), + '**** baz', + }, opts.destination_file.lines) + end) + + it('creates a whole datetree before an existing future date', function() + local in_two_years = Date.today():add({ year = 2 }) + local date = Date.today() + local opts = get_template(date, { + '* ' .. in_two_years:format('%Y'), + '** ' .. in_two_years:format('%Y-%m %B'), + '*** ' .. in_two_years:format('%Y-%m-%d %A'), + }) + local capture_lines = { '* baz' } + local capture_file = helpers.create_file_instance(capture_lines) + opts.source_file = capture_file + opts.source_headline = capture_file:get_headlines()[1] + + org.capture:_refile_from_capture_buffer(opts) + opts.destination_file:reload_sync() + assert.are.same({ + '* ' .. date:format('%Y'), + '** ' .. date:format('%Y-%m %B'), + '*** ' .. date:format('%Y-%m-%d %A'), + '**** baz', + '* ' .. in_two_years:format('%Y'), + '** ' .. in_two_years:format('%Y-%m %B'), + '*** ' .. in_two_years:format('%Y-%m-%d %A'), + }, opts.destination_file.lines) + end) + + it('creates a whole datetree before after a past date', function() + local two_years_ago = Date.today():subtract({ year = 2 }) + local date = Date.today() + local opts = get_template(date, { + '* ' .. two_years_ago:format('%Y'), + '** ' .. two_years_ago:format('%Y-%m %B'), + '*** ' .. two_years_ago:format('%Y-%m-%d %A'), + }) + local capture_lines = { '* baz' } + local capture_file = helpers.create_file_instance(capture_lines) + opts.source_file = capture_file + opts.source_headline = capture_file:get_headlines()[1] + + org.capture:_refile_from_capture_buffer(opts) + opts.destination_file:reload_sync() + assert.are.same({ + '* ' .. two_years_ago:format('%Y'), + '** ' .. two_years_ago:format('%Y-%m %B'), + '*** ' .. two_years_ago:format('%Y-%m-%d %A'), + '* ' .. date:format('%Y'), + '** ' .. date:format('%Y-%m %B'), + '*** ' .. date:format('%Y-%m-%d %A'), + '**** baz', + }, opts.destination_file.lines) + end) + + it('creates a whole datetree between a past and future date', function() + local two_years_ago = Date.today():subtract({ year = 2 }) + local in_two_years = Date.today():add({ year = 2 }) + local date = Date.today() + local opts = get_template(date, { + '* ' .. two_years_ago:format('%Y'), + '** ' .. two_years_ago:format('%Y-%m %B'), + '*** ' .. two_years_ago:format('%Y-%m-%d %A'), + '* ' .. in_two_years:format('%Y'), + '** ' .. in_two_years:format('%Y-%m %B'), + '*** ' .. in_two_years:format('%Y-%m-%d %A'), + }) + local capture_lines = { '* baz' } + local capture_file = helpers.create_file_instance(capture_lines) + opts.source_file = capture_file + opts.source_headline = capture_file:get_headlines()[1] + + org.capture:_refile_from_capture_buffer(opts) + opts.destination_file:reload_sync() + assert.are.same({ + '* ' .. two_years_ago:format('%Y'), + '** ' .. two_years_ago:format('%Y-%m %B'), + '*** ' .. two_years_ago:format('%Y-%m-%d %A'), + '* ' .. date:format('%Y'), + '** ' .. date:format('%Y-%m %B'), + '*** ' .. date:format('%Y-%m-%d %A'), + '**** baz', + '* ' .. in_two_years:format('%Y'), + '** ' .. in_two_years:format('%Y-%m %B'), + '*** ' .. in_two_years:format('%Y-%m-%d %A'), + }, opts.destination_file.lines) + end) + end) + + describe('When only datetree year exist', function() + it('creates a month datetree', function() + local date = Date.today() + local opts = get_template(date, { + '* ' .. date:format('%Y'), + }) + local capture_lines = { '* baz' } + local capture_file = helpers.create_file_instance(capture_lines) + opts.source_file = capture_file + opts.source_headline = capture_file:get_headlines()[1] + + org.capture:_refile_from_capture_buffer(opts) + opts.destination_file:reload_sync() + assert.are.same({ + '* ' .. date:format('%Y'), + '** ' .. date:format('%Y-%m %B'), + '*** ' .. date:format('%Y-%m-%d %A'), + '**** baz', + }, opts.destination_file.lines) + end) + + it('creates a month datetree before an existing future month', function() + local date = Date.today() + local opts = get_template(date, { + '* ' .. date:format('%Y'), + '** ' .. date:add({ month = 2 }):format('%Y-%m %B'), + '*** ' .. date:add({ month = 2 }):format('%Y-%m-%d %A'), + '**** future month note', + }) + local capture_lines = { '* baz' } + local capture_file = helpers.create_file_instance(capture_lines) + opts.source_file = capture_file + opts.source_headline = capture_file:get_headlines()[1] + + org.capture:_refile_from_capture_buffer(opts) + opts.destination_file:reload_sync() + assert.are.same({ + '* ' .. date:format('%Y'), + '** ' .. date:format('%Y-%m %B'), + '*** ' .. date:format('%Y-%m-%d %A'), + '**** baz', + '** ' .. date:add({ month = 2 }):format('%Y-%m %B'), + '*** ' .. date:add({ month = 2 }):format('%Y-%m-%d %A'), + '**** future month note', + }, opts.destination_file.lines) + end) + + it('creates a month datetree after a past month', function() + local date = Date.from_string('2024-05-10') + local opts = get_template(date, { + '* ' .. date:format('%Y'), + '** ' .. date:subtract({ month = 1 }):format('%Y-%m %B'), + '*** ' .. date:subtract({ month = 1 }):format('%Y-%m-%d %A'), + '**** one month ago', + '** ' .. date:subtract({ month = 2 }):format('%Y-%m %B'), + '*** ' .. date:subtract({ month = 2 }):format('%Y-%m-%d %A'), + '**** two months ago', + }) + local capture_lines = { '* baz' } + local capture_file = helpers.create_file_instance(capture_lines) + opts.source_file = capture_file + opts.source_headline = capture_file:get_headlines()[1] + + org.capture:_refile_from_capture_buffer(opts) + opts.destination_file:reload_sync() + assert.are.same({ + '* ' .. date:format('%Y'), + '** ' .. date:subtract({ month = 1 }):format('%Y-%m %B'), + '*** ' .. date:subtract({ month = 1 }):format('%Y-%m-%d %A'), + '**** one month ago', + '** ' .. date:subtract({ month = 2 }):format('%Y-%m %B'), + '*** ' .. date:subtract({ month = 2 }):format('%Y-%m-%d %A'), + '**** two months ago', + '** ' .. date:format('%Y-%m %B'), + '*** ' .. date:format('%Y-%m-%d %A'), + '**** baz', + }, opts.destination_file.lines) + end) + + it('creates a month datetree between a past and future month', function() + local date = Date.from_string('2023-05-05') + local two_months_ago = date:subtract({ month = 2 }) + local in_two_months = date:add({ month = 2 }) + local opts = get_template(date, { + '* ' .. date:format('%Y'), + '** ' .. two_months_ago:format('%Y-%m %B'), + '*** ' .. two_months_ago:format('%Y-%m-%d %A'), + '**** two months ago', + '** ' .. in_two_months:format('%Y-%m %B'), + '*** ' .. in_two_months:format('%Y-%m-%d %A'), + '**** in two months', + }) + local capture_lines = { '* baz' } + local capture_file = helpers.create_file_instance(capture_lines) + opts.source_file = capture_file + opts.source_headline = capture_file:get_headlines()[1] + + org.capture:_refile_from_capture_buffer(opts) + opts.destination_file:reload_sync() + assert.are.same({ + '* ' .. date:format('%Y'), + '** ' .. two_months_ago:format('%Y-%m %B'), + '*** ' .. two_months_ago:format('%Y-%m-%d %A'), + '**** two months ago', + '** ' .. date:format('%Y-%m %B'), + '*** ' .. date:format('%Y-%m-%d %A'), + '**** baz', + '** ' .. in_two_months:format('%Y-%m %B'), + '*** ' .. in_two_months:format('%Y-%m-%d %A'), + '**** in two months', + }, opts.destination_file.lines) + end) + end) + + describe('When only datetree year and month exist', function() + it('creates a day datetree', function() + local date = Date.today() + local opts = get_template(date, { + '* ' .. date:format('%Y'), + '** ' .. date:format('%Y-%m %B'), + }) + local capture_lines = { '* baz' } + local capture_file = helpers.create_file_instance(capture_lines) + opts.source_file = capture_file + opts.source_headline = capture_file:get_headlines()[1] + + org.capture:_refile_from_capture_buffer(opts) + opts.destination_file:reload_sync() + assert.are.same({ + '* ' .. date:format('%Y'), + '** ' .. date:format('%Y-%m %B'), + '*** ' .. date:format('%Y-%m-%d %A'), + '**** baz', + }, opts.destination_file.lines) + end) + + it('creates a day datetree before an existing future day', function() + local date = Date.today() + local opts = get_template(date, { + '* ' .. date:format('%Y'), + '** ' .. date:format('%Y-%m %B'), + '*** ' .. date:add({ day = 2 }):format('%Y-%m-%d %A'), + '**** future day note', + }) + local capture_lines = { '* baz' } + local capture_file = helpers.create_file_instance(capture_lines) + opts.source_file = capture_file + opts.source_headline = capture_file:get_headlines()[1] + + org.capture:_refile_from_capture_buffer(opts) + opts.destination_file:reload_sync() + assert.are.same({ + '* ' .. date:format('%Y'), + '** ' .. date:format('%Y-%m %B'), + '*** ' .. date:format('%Y-%m-%d %A'), + '**** baz', + '*** ' .. date:add({ day = 2 }):format('%Y-%m-%d %A'), + '**** future day note', + }, opts.destination_file.lines) + end) + + it('creates a day datetree after a past day', function() + local date = Date.from_string('2024-05-10') + local opts = get_template(date, { + '* ' .. date:format('%Y'), + '** ' .. date:format('%Y-%m %B'), + '*** ' .. date:subtract({ day = 2 }):format('%Y-%m-%d %A'), + '**** past day note', + }) + local capture_lines = { '* baz' } + local capture_file = helpers.create_file_instance(capture_lines) + opts.source_file = capture_file + opts.source_headline = capture_file:get_headlines()[1] + + org.capture:_refile_from_capture_buffer(opts) + opts.destination_file:reload_sync() + assert.are.same({ + '* ' .. date:format('%Y'), + '** ' .. date:format('%Y-%m %B'), + '*** ' .. date:subtract({ day = 2 }):format('%Y-%m-%d %A'), + '**** past day note', + '*** ' .. date:format('%Y-%m-%d %A'), + '**** baz', + }, opts.destination_file.lines) + end) + + it('creates a day datetree between a past and future day', function() + local date = Date.from_string('2023-05-05') + local two_months_ago = date:subtract({ month = 2 }) + local in_two_months = date:add({ month = 2 }) + local opts = get_template(date, { + '* ' .. date:format('%Y'), + '** ' .. date:format('%Y-%m %B'), + '*** ' .. date:subtract({ day = 2 }):format('%Y-%m-%d %A'), + '**** past day note', + '*** ' .. date:add({ day = 5 }):format('%Y-%m-%d %A'), + '**** future day note', + }) + local capture_lines = { '* baz' } + local capture_file = helpers.create_file_instance(capture_lines) + opts.source_file = capture_file + opts.source_headline = capture_file:get_headlines()[1] + + org.capture:_refile_from_capture_buffer(opts) + opts.destination_file:reload_sync() + assert.are.same({ + '* ' .. date:format('%Y'), + '** ' .. date:format('%Y-%m %B'), + '*** ' .. date:subtract({ day = 2 }):format('%Y-%m-%d %A'), + '**** past day note', + '*** ' .. date:format('%Y-%m-%d %A'), + '**** baz', + '*** ' .. date:add({ day = 5 }):format('%Y-%m-%d %A'), + '**** future day note', + }, opts.destination_file.lines) + end) + end) + + describe('When whole datetree exists', function() + it('it appends to the day tree', function() + local date = Date.today() + local opts = get_template(date, { + '* ' .. date:format('%Y'), + '** ' .. date:format('%Y-%m %B'), + '*** ' .. date:format('%Y-%m-%d %A'), + '**** existing day note', + '**** existing day second note', + }) + local capture_lines = { '* baz' } + local capture_file = helpers.create_file_instance(capture_lines) + opts.source_file = capture_file + opts.source_headline = capture_file:get_headlines()[1] + + org.capture:_refile_from_capture_buffer(opts) + opts.destination_file:reload_sync() + assert.are.same({ + '* ' .. date:format('%Y'), + '** ' .. date:format('%Y-%m %B'), + '*** ' .. date:format('%Y-%m-%d %A'), + '**** existing day note', + '**** existing day second note', + '**** baz', + }, opts.destination_file.lines) + end) + end) +end) diff --git a/tests/plenary/capture/templates_spec.lua b/tests/plenary/capture/templates_spec.lua index 73bd39943..a21666771 100644 --- a/tests/plenary/capture/templates_spec.lua +++ b/tests/plenary/capture/templates_spec.lua @@ -1,4 +1,5 @@ local Template = require('orgmode.capture.template') +local Date = require('orgmode.objects.date') describe('Capture template', function() it('should compile expression', function() @@ -22,4 +23,26 @@ describe('Capture template', function() vim.fn.setreg('+', clip_backup) end) + + it('gets current date for datetree enabled with true', function() + local template = Template:new({ + template = '* %?', + datetree = true, + }) + + assert.are.same(Date.today():to_string(), template:get_datetree_date():to_string()) + end) + + it('gets a proper date for datetree enabled as time prompt', function() + local date = Date.today():subtract({ month = 2 }) + local template = Template:new({ + template = '* %?', + datetree = { + time_prompt = true, + date = date, + }, + }) + + assert.are.same(date:to_string(), template:get_datetree_date():to_string()) + end) end) diff --git a/tests/plenary/ui/mappings/refile_spec.lua b/tests/plenary/ui/mappings/refile_spec.lua deleted file mode 100644 index d679df14e..000000000 --- a/tests/plenary/ui/mappings/refile_spec.lua +++ /dev/null @@ -1,95 +0,0 @@ -local helpers = require('tests.plenary.helpers') -local org = require('orgmode') - -describe('Refile mappings', function() - after_each(function() - vim.cmd([[silent! %bw!]]) - end) - - it('should refile to headline that matches name exactly', function() - local destination_file = helpers.create_file({ - '* foobar', - '* baz', - '** foo', - }) - - local source_file = helpers.create_file({ - '* to be refiled', - '* not to be refiled', - }) - - local item = source_file:get_closest_headline() - ---@diagnostic disable-next-line: invisible - org.capture:_refile_from_org_file({ - destination_file = destination_file, - source_headline = item, - destination_headline = destination_file:get_headlines()[3], - }) - assert.are.same('* not to be refiled', vim.fn.getline(1)) - vim.cmd('edit' .. vim.fn.fnameescape(destination_file.filename)) - assert.are.same({ - '* foobar', - '* baz', - '** foo', - '*** to be refiled', - }, vim.api.nvim_buf_get_lines(0, 0, 5, false)) - end) - - it('should refile to headline and properly demote', function() - local destination_file = helpers.create_file({ - '* foobar', - '* baz', - '** foo', - }) - - local source_file = helpers.create_file({ - '* to be refiled', - '* not to be refiled', - }) - - local item = source_file:get_closest_headline() - ---@diagnostic disable-next-line: invisible - org.capture:_refile_from_org_file({ - destination_file = destination_file, - source_headline = item, - destination_headline = destination_file:get_headlines()[1], - }) - assert.are.same('* not to be refiled', vim.fn.getline(1)) - vim.cmd('edit' .. vim.fn.fnameescape(destination_file.filename)) - assert.are.same({ - '* foobar', - '** to be refiled', - '* baz', - '** foo', - }, vim.api.nvim_buf_get_lines(0, 0, 5, false)) - end) - - it('should refile to headline and properly promote', function() - local destination_file = helpers.create_file({ - '* foobar', - '* baz', - '** foo', - }) - - local source_file = helpers.create_file({ - '**** to be refiled', - '* not to be refiled', - }) - - local item = source_file:get_closest_headline() - ---@diagnostic disable-next-line: invisible - org.capture:_refile_from_org_file({ - destination_file = destination_file, - source_headline = item, - destination_headline = destination_file:get_headlines()[1], - }) - assert.are.same('* not to be refiled', vim.fn.getline(1)) - vim.cmd('edit' .. vim.fn.fnameescape(destination_file.filename)) - assert.are.same({ - '* foobar', - '** to be refiled', - '* baz', - '** foo', - }, vim.api.nvim_buf_get_lines(0, 0, 5, false)) - end) -end)