From 938f9af3877088861dd26779c555f0d9b47ffed7 Mon Sep 17 00:00:00 2001 From: Daniil Shvalov <57654917+danilshvalov@users.noreply.github.com> Date: Thu, 26 Oct 2023 12:45:56 +0300 Subject: [PATCH] refactor: captures (#613) * refactor: captures * refactor: make headline detached from template * feat: subtemplates * refactor: move `empty_lines` to `properties` * feat: remove buffer empty lines * fix: properly place capture without headline * tests: add tests for org captures * fix: template validation * chore: remove old tests --- DOCS.md | 68 ++++-- lua/orgmode/capture/init.lua | 216 +++++++++++------ lua/orgmode/capture/template.lua | 85 +++++++ lua/orgmode/capture/templates.lua | 22 +- lua/orgmode/org/mappings.lua | 10 +- tests/plenary/capture/capture_spec.lua | 267 ++++++++++++++++++++-- tests/plenary/ui/mappings/refile_spec.lua | 21 +- 7 files changed, 581 insertions(+), 108 deletions(-) create mode 100644 lua/orgmode/capture/template.lua diff --git a/DOCS.md b/DOCS.md index 22e5c19a3..5d477bbe0 100644 --- a/DOCS.md +++ b/DOCS.md @@ -412,6 +412,16 @@ Variables: * `%^{PROMPT|DEFAULT|COMPLETION...}`: Prompt for input, if completion is provided an :h inputlist will be used * `%(EXP)`: Runs the given lua code and inserts the result. NOTE: this will internally pass the content to the lua `load()` function. So the body inside `%()` should be the body of a function that returns a string. +Templates have the following fields: + * `description` (`string`) — description of the template that is displayed in the template selection menu + * `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 + * `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 + * `after` (`integer?`) — add empty lines to the end of the content + Example:
```lua { T = { @@ -423,20 +433,24 @@ Example:
Journal example:
```lua - { j = { - description = 'Journal', - template = '\n*** %<%Y-%m-%d> %<%A>\n**** %U\n\n%?', - target = '~/sync/org/journal.org' - } } + { + j = { + description = 'Journal', + template = '\n*** %<%Y-%m-%d> %<%A>\n**** %U\n\n%?', + target = '~/sync/org/journal.org' + }, + } ``` Journal example with dynamic target, i.e. a separate file per month:
```lua - { J = { - description = 'Journal', - template = '\n*** %<%Y-%m-%d> %<%A>\n**** %U\n\n%?', - target = '~/sync/org/journal/%<%Y-%m>.org' - } } + { + J = { + description = 'Journal', + template = '\n*** %<%Y-%m-%d> %<%A>\n**** %U\n\n%?', + target = '~/sync/org/journal/%<%Y-%m>.org' + }, + } ``` Nested key example:
@@ -456,16 +470,38 @@ Nested key example:
headline = 'one-time' } } + -- or + { + e = { + description = 'Event', + subtemplates = { + r = { + description = 'recurring', + template = '** %?\n %T', + target = '~/org/calendar.org', + headline = 'recurring' + }, + o = { + description = 'one-time', + template = '** %?\n %T', + target = '~/org/calendar.org', + headline = 'one-time' + }, + }, + }, + } ``` Lua expression example:
```lua - { j = { - description = 'Journal', - template = '* %(return vim.fn.getreg "w")', - -- get the content of register "w" - target = '~/sync/org/journal.org' - } } + { + j = { + description = 'Journal', + template = '* %(return vim.fn.getreg "w")', + -- get the content of register "w" + target = '~/sync/org/journal.org' + }, + } ``` #### **org_agenda_min_height** diff --git a/lua/orgmode/capture/init.lua b/lua/orgmode/capture/init.lua index dfaf0bbc8..02b74dc90 100644 --- a/lua/orgmode/capture/init.lua +++ b/lua/orgmode/capture/init.lua @@ -3,8 +3,18 @@ local config = require('orgmode.config') local Files = require('orgmode.parser.files') local File = require('orgmode.parser.file') local Templates = require('orgmode.capture.templates') +local Template = require('orgmode.capture.template') local ClosingNote = require('orgmode.capture.closing_note') local Menu = require('orgmode.ui.menu') +local Range = require('orgmode.parser.range') + +---@class CaptureOpts +---@field file string +---@field range Range +---@field lines string[] +---@field template Template? +---@field headline string? +---@field item Section? ---@class Capture ---@field templates Templates @@ -20,6 +30,8 @@ function Capture:new() return data end +---@param base_key string +---@param templates table function Capture:_get_subtemplates(base_key, templates) local subtemplates = {} for key, template in pairs(templates) do @@ -30,6 +42,7 @@ function Capture:_get_subtemplates(base_key, templates) return subtemplates end +---@param templates table function Capture:_create_menu_items(templates) local menu_items = {} for key, template in pairs(templates) do @@ -42,6 +55,11 @@ function Capture:_create_menu_items(templates) item.action = function() self:_create_prompt(self:_get_subtemplates(key, templates)) end + elseif vim.tbl_count(template.subtemplates) > 0 then + item.label = template.description .. '...' + item.action = function() + self:_create_prompt(template.subtemplates) + end else item.label = template.description item.action = function() @@ -54,6 +72,7 @@ function Capture:_create_menu_items(templates) return menu_items end +---@param templates table function Capture:_create_prompt(templates) local menu = Menu:new({ title = 'Select a capture template', @@ -79,8 +98,9 @@ function Capture:open_template(template) self._close_tmp = utils.open_tmp_org_window(16, config.win_split_mode, config.win_border, on_close) vim.api.nvim_buf_set_lines(0, 0, -1, true, content) self.templates:setup() - vim.api.nvim_buf_set_var(0, 'org_template', template) - vim.api.nvim_buf_set_var(0, 'org_capture', true) + + vim.b.org_template = template + vim.b.org_capture = true config:setup_mappings('capture') end @@ -97,23 +117,22 @@ end ---@param confirm? boolean function Capture:refile(confirm) local is_modified = vim.bo.modified - local file, lines, item, template = self:_get_refile_vars() - if not file then + local opts = self:_get_refile_vars() + if not opts.file then return end - local headline_title = template.headline if confirm and is_modified then - local choice = vim.fn.confirm(string.format('Do you want to refile this to %s?', file), '&Yes\n&No') + local choice = vim.fn.confirm(string.format('Do you want to refile this to %s?', opts.file), '&Yes\n&No') vim.cmd([[redraw!]]) if choice ~= 1 then return utils.echo_info('Canceled.') end end vim.defer_fn(function() - if headline_title then - self:refile_to_headline(file, lines, item, headline_title) + if opts.headline then + self:refile_to_headline(opts) else - self:_refile_to_end(file, lines, item) + self:_refile_to_end(opts) end if not confirm then @@ -124,17 +143,18 @@ end ---Triggered when refiling to destination from capture buffer function Capture:refile_to_destination() - local file, lines, item = self:_get_refile_vars() - if not file then + local opts = self:_get_refile_vars() + if not opts.file then return end - self:_refile_content_with_fallback(lines, file, item) + self:_refile_content_with_fallback(opts) self:kill() end ---@private +---@return CaptureOpts function Capture:_get_refile_vars() - local template = vim.api.nvim_buf_get_var(0, 'org_template') or {} + local template = vim.b.org_template or {} local target = self.templates:compile_target(template.target or config.org_default_notes_file) local file = vim.fn.resolve(vim.fn.fnamemodify(target, ':p')) @@ -142,7 +162,7 @@ function Capture:_get_refile_vars() local choice = vim.fn.confirm(('Refile destination %s does not exist. Create now?'):format(file), '&Yes\n&No') if choice ~= 1 then utils.echo_error('Cannot proceed without a valid refile destination') - return + return {} end vim.fn.mkdir(vim.fn.fnamemodify(file, ':h'), 'p') vim.fn.writefile({}, file) @@ -154,48 +174,55 @@ function Capture:_get_refile_vars() item = org_file:get_headlines()[1] end - return file, lines, item, template + return { + file = file, + lines = lines, + item = item, + template = template, + headline = template.headline, + } end ---Triggered from org file when we want to refile headline function Capture:refile_headline_to_destination() local destination_file = Files.get_current_file() local item = destination_file:get_closest_headline() + if not item then + return + end local lines = destination_file:get_headline_lines(item) - return self:_refile_content_with_fallback(lines, nil, item) + return self:_refile_content_with_fallback({ + lines = lines, + item = item, + template = Template:new(), + }) end ----@param file File ----@param item string ----@param archive_file string ----@return string -function Capture:refile_file_headline_to_archive(file, item, archive_file) - local lines = file:get_headline_lines(item) - return self:_refile_to_end(archive_file, lines, item, string.format('Archived to %s', archive_file)) +---@param opts CaptureOpts +---@return boolean +function Capture:refile_file_headline_to_archive(opts) + opts.message = string.format('Archived to %s', opts.file) + return self:_refile_to_end(opts) end ---@private ----@param file string ----@param lines string[] ----@param item? Section ----@param message? string +---@param opts CaptureOpts ---@return boolean -function Capture:_refile_to_end(file, lines, item, message) - local refiled = self:_refile_to(file, lines, item, '$') +function Capture:_refile_to_end(opts) + opts.range = Range.from_line(-1) + local refiled = self:_refile_to(opts) if not refiled then return false end - utils.echo_info(message or string.format('Wrote %s', file)) + utils.echo_info(opts.message or string.format('Wrote %s', opts.file)) return true end ---@private ----@param lines string[] ----@param fallback_file string ----@param item? Section ----@return string -function Capture:_refile_content_with_fallback(lines, fallback_file, item) - local default_file = fallback_file and fallback_file ~= '' and vim.fn.fnamemodify(fallback_file, ':p') or nil +---@param opts CaptureOpts +---@return boolean +function Capture:_refile_content_with_fallback(opts) + local default_file = opts.file and opts.file ~= '' and vim.fn.fnamemodify(opts.file, ':p') or nil local valid_destinations = {} for _, file in ipairs(Files.filenames()) do @@ -210,79 +237,125 @@ function Capture:_refile_content_with_fallback(lines, fallback_file, item) utils.echo_error( "'" .. destination[1] .. "' is not a file specified in the 'org_agenda_files' setting. Refiling cancelled." ) - return + return false end - return self:_refile_to_end(default_file, lines, item) + opts.file = default_file + return self:_refile_to_end(opts) end - local destination_file = valid_destinations[destination[1]] - local destination_headline = table.concat({ unpack(destination, 2) }, '/') - if not destination_headline or destination_headline == '' then - return self:_refile_to_end(destination_file, lines, item) + opts.file = valid_destinations[destination[1]] + opts.headline = table.concat({ unpack(destination, 2) }, '/') + if not opts.headline or opts.headline == '' then + return self:_refile_to_end(opts) end - return self:refile_to_headline(destination_file, lines, item, destination_headline) + return self:refile_to_headline(opts) end ----@param destination_filename string ----@param lines string[] ----@param item? Section ----@param headline_title? string -function Capture:refile_to_headline(destination_filename, lines, item, headline_title) - local destination_file = Files.get(destination_filename) +---@param opts CaptureOpts +function Capture:refile_to_headline(opts) + local destination_file = Files.get(opts.file) local headline - if headline_title then - headline = destination_file:find_headline_by_title(headline_title, true) + if opts.headline then + headline = destination_file:find_headline_by_title(opts.headline, true) if not headline then - utils.echo_error( - "headline '" .. headline_title .. "' does not exist in '" .. destination_filename .. "'. Aborted refiling." - ) + utils.echo_error("headline '" .. opts.headline .. "' does not exist in '" .. opts.file .. "'. Aborted refiling.") return false end end + local item = opts.item if item then -- Refiling in same file just moves the lines from one position -- to another,so we need to apply demote instantly local is_same_file = destination_file.filename == item.root.filename if item.level <= headline.level then - lines = item:demote(headline.level - item.level + 1, true, not is_same_file) + opts.lines = item:demote(headline.level - item.level + 1, true, not is_same_file) else - lines = item:promote(item.level - headline.level - 1, true, not is_same_file) + opts.lines = item:promote(item.level - headline.level - 1, true, not is_same_file) end end - local refiled = self:_refile_to(destination_filename, lines, item, headline.range.end_line) + opts.range = Range.from_line(headline.range.end_line) + local refiled = self:_refile_to(opts) if not refiled then return false end - utils.echo_info(string.format('Wrote %s', destination_filename)) + utils.echo_info(string.format('Wrote %s', opts.file)) return true end +---@param opts CaptureOpts +local function add_empty_lines(opts) + local empty_lines = opts.template.properties.empty_lines + + for _ = 1, empty_lines.before do + table.insert(opts.lines, 1, '') + end + + for _ = 1, empty_lines.after do + table.insert(opts.lines, '') + end +end + +---@param opts CaptureOpts +local function apply_properties(opts) + if opts.template then + add_empty_lines(opts) + end +end + +local function remove_buffer_empty_lines(opts) + local line_count = vim.api.nvim_buf_line_count(0) + local range = opts.range + + local end_line = range.end_line + if end_line < 0 then + end_line = end_line + line_count + 1 + end + + local start_line = end_line - 1 + + local is_line_empty = function(row) + local line = vim.api.nvim_buf_get_lines(0, row, row + 1, true)[1] + line = vim.trim(line) + return #line == 0 + end + + while start_line >= 0 and is_line_empty(start_line) do + start_line = start_line - 1 + end + start_line = start_line + 1 + + while end_line < line_count and is_line_empty(end_line) do + end_line = end_line + 1 + end + + range.start_line = start_line + range.end_line = end_line +end + ---@private ----@param file string ----@param lines string[] ----@param item? Section ----@param destination_line string|number +---@param opts CaptureOpts ---@return boolean -function Capture:_refile_to(file, lines, item, destination_line) - if not file then +function Capture:_refile_to(opts) + if not opts.file then return false end - local is_same_file = file == utils.current_file_path() + apply_properties(opts) + + local is_same_file = opts.file == utils.current_file_path() local cur_win = vim.api.nvim_get_current_win() + local item = opts.item if is_same_file and item then - vim.cmd( - string.format('silent! %d,%d move %s', item.range.start_line, item.range.end_line, tostring(destination_line)) - ) + vim.cmd(string.format('silent! %d,%d move %s', item.range.start_line, item.range.end_line, tostring(opts.file))) return true end if not is_same_file then - local bufnr = vim.fn.bufadd(file) + local bufnr = vim.fn.bufadd(opts.file) vim.api.nvim_open_win(bufnr, true, { relative = 'editor', width = 1, @@ -295,7 +368,10 @@ function Capture:_refile_to(file, lines, item, destination_line) }) end - vim.fn.append(destination_line, lines) + remove_buffer_empty_lines(opts) + + local range = opts.range + vim.api.nvim_buf_set_lines(0, range.start_line, range.end_line, false, opts.lines) if not is_same_file then vim.cmd('silent! wq!') diff --git a/lua/orgmode/capture/template.lua b/lua/orgmode/capture/template.lua new file mode 100644 index 000000000..636f78f4c --- /dev/null +++ b/lua/orgmode/capture/template.lua @@ -0,0 +1,85 @@ +---@class TemplateEmptyLines +---@field before integer +---@field after integer +local TemplateEmptyLines = {} + +function TemplateEmptyLines:new(opts) + opts = opts or {} + + vim.validate({ + before = { opts.before, 'number', true }, + after = { opts.after, 'number', true }, + }) + + local this = {} + this.before = opts.before or 0 + this.after = opts.after or 0 + + setmetatable(this, self) + self.__index = self + return this +end + +---@class TemplateProperties +---@field empty_lines TemplateEmptyLines +local TemplateProperties = {} + +function TemplateProperties:new(opts) + opts = opts or {} + + vim.validate({ + empty_lines = { opts.empty_lines, { 'table', 'number' }, true }, + }) + + local empty_lines = opts.empty_lines or {} + if type(empty_lines) == 'number' then + empty_lines = { before = empty_lines, after = empty_lines } + end + + local this = {} + this.empty_lines = TemplateEmptyLines:new(empty_lines) + + setmetatable(this, self) + self.__index = self + return this +end + +---@class Template +---@field description string +---@field template string|string[] +---@field target string? +---@field headline string? +---@field properties TemplateProperties +---@field subtemplates table +local Template = {} + +function Template:new(opts) + opts = opts or {} + + vim.validate({ + description = { opts.description, 'string', true }, + template = { opts.template, { 'string', 'table' }, true }, + target = { opts.target, 'string', true }, + headline = { opts.headline, 'string', true }, + properties = { opts.properties, 'table', true }, + subtemplates = { opts.subtemplates, 'table', true }, + }) + + local this = {} + this.description = opts.description or '' + this.template = opts.template or '' + this.target = opts.target + this.headline = opts.headline + this.properties = TemplateProperties:new(opts.properties) + + this.subtemplates = {} + for key, subtemplate in pairs(opts.subtemplates or {}) do + this.subtemplates[key] = Template:new(subtemplate) + end + + setmetatable(this, self) + self.__index = self + return this +end + +return Template diff --git a/lua/orgmode/capture/templates.lua b/lua/orgmode/capture/templates.lua index 3d0fb69ed..00d5349f8 100644 --- a/lua/orgmode/capture/templates.lua +++ b/lua/orgmode/capture/templates.lua @@ -1,6 +1,8 @@ local config = require('orgmode.config') +local Template = require('orgmode.capture.template') local Date = require('orgmode.objects.date') local utils = require('orgmode.utils') + local expansions = { ['%f'] = function() return vim.fn.expand('%') @@ -34,13 +36,25 @@ local expansions = { ---@see https://orgmode.org/manual/Capture-templates.html ---@class Templates ----@field templates table +---@field templates table local Templates = {} --- TODO Introduce type -function Templates:new() +function Templates:new(templates) local opts = {} - opts.templates = config.org_capture_templates + + vim.validate({ + templates = { templates, 'table', true }, + }) + + opts.templates = {} + for key, template in pairs(templates or config.org_capture_templates) do + if type(template) == 'table' then + opts.templates[key] = Template:new(template) + else + opts.templates[key] = template + end + end + setmetatable(opts, self) self.__index = self return opts diff --git a/lua/orgmode/org/mappings.lua b/lua/orgmode/org/mappings.lua index d13c9216e..9402b7811 100644 --- a/lua/orgmode/org/mappings.lua +++ b/lua/orgmode/org/mappings.lua @@ -43,11 +43,19 @@ function OrgMappings:archive() end local item = file:get_closest_headline() local archive_location = file:get_archive_file_location() + if not archive_location or not item then + return + end + local archive_directory = vim.fn.fnamemodify(archive_location, ':p:h') if vim.fn.isdirectory(archive_directory) == 0 then vim.fn.mkdir(archive_directory, 'p') end - self.capture:refile_file_headline_to_archive(file, item, archive_location) + self.capture:refile_file_headline_to_archive({ + file = archive_location, + item = item, + lines = file:get_headline_lines(item), + }) Files.reload( archive_location, vim.schedule_wrap(function() diff --git a/tests/plenary/capture/capture_spec.lua b/tests/plenary/capture/capture_spec.lua index cfc49c3aa..390d501d9 100644 --- a/tests/plenary/capture/capture_spec.lua +++ b/tests/plenary/capture/capture_spec.lua @@ -1,8 +1,13 @@ local Capture = require('orgmode.capture') +local Templates = require('orgmode.capture.templates') +local Template = require('orgmode.capture.template') +local File = require('orgmode.parser.file') +local helpers = require('tests.plenary.ui.helpers') +local org = require('orgmode') describe('Menu Items', function() it('should create a menu item for each template', function() - local templates = { + local templates = Templates:new({ t = { description = 'todo', }, @@ -12,8 +17,8 @@ describe('Menu Items', function() f = { description = 'file update', }, - } - menu_items = Capture:_create_menu_items(templates) + }) + local menu_items = Capture:_create_menu_items(templates:get_list()) assert.are.same(#menu_items, 3) assert.are.same( { 'b', 'f', 't' }, @@ -24,7 +29,7 @@ describe('Menu Items', function() end) it('should create one entry for multi-key shortcuts', function() - local multikey_templates = { + local multikey_templates = Templates:new({ k = 'Multikey templates', kt = { description = 'multikey todo', @@ -36,8 +41,8 @@ describe('Menu Items', function() kbf = { description = 'file bookmark', }, - } - menu_items = Capture:_create_menu_items(multikey_templates) + }) + local menu_items = Capture:_create_menu_items(multikey_templates:get_list()) assert.are.same(#menu_items, 1) assert.are.same( { 'k' }, @@ -48,7 +53,7 @@ describe('Menu Items', function() end) it('computes the sub templates', function() - local multikey_templates = { + local multikey_templates = Templates:new({ k = 'Multikey templates', kt = { description = 'multikey todo', @@ -60,9 +65,9 @@ describe('Menu Items', function() kbf = { description = 'file bookmark', }, - } - sub_template_items = Capture:_get_subtemplates('k', multikey_templates) - assert.are.same({ + }) + local sub_template_items = Capture:_get_subtemplates('k', multikey_templates:get_list()) + local expected = Templates:new({ b = 'multikey bookmark', bb = { description = 'browser bookmark', @@ -73,14 +78,248 @@ describe('Menu Items', function() t = { description = 'multikey todo', }, - }, sub_template_items) + }):get_list() + assert.are.same(expected, sub_template_items) + end) + + it('should create one entry for multi-key shortcuts with subtemplates', function() + local multikey_templates = Templates:new({ + k = { + description = 'Multikey templates', + subtemplates = { + t = { + description = 'multikey todo', + }, + b = { + description = 'multikey bookmark', + subtemplates = { + b = { + description = 'browser bookmark', + }, + f = { + description = 'file bookmark', + }, + }, + }, + }, + }, + }) + local menu_items = Capture:_create_menu_items(multikey_templates:get_list()) + assert.are.same(#menu_items, 1) + assert.are.same( + { 'k' }, + vim.tbl_map(function(x) + return x.key + end, vim.fn.sort(menu_items)) + ) end) it('adds an ellipses', function() - local template = { + local templates = Templates:new({ k = 'Multikey template', - } - menu_item = Capture:_create_menu_items(template) + }) + local menu_item = Capture:_create_menu_items(templates:get_list()) assert.are.same('Multikey template...', menu_item[1].label) end) end) + +describe('Refile', function() + it('to empty file', function() + local destination_file = helpers.load_file_content({}) + + local capture_lines = { '* foo' } + helpers.load_file_content(capture_lines) + local capture_file = File.from_content(capture_lines, 'capture') + assert(capture_file) + local item = capture_file:get_headlines()[1] + + org.instance().capture:_refile_to_end({ + file = destination_file, + lines = capture_lines, + item = item, + }) + vim.cmd('edit' .. vim.fn.fnameescape(destination_file)) + assert.are.same({ + '* foo', + }, vim.api.nvim_buf_get_lines(0, 0, -1, false)) + end) + + it('to end', function() + local destination_file = helpers.load_file_content({ + '* foobar', + ' ', + '', + '\t\t\t\t', + '', + }) + + local capture_lines = { '** baz' } + helpers.load_file_content(capture_lines) + local capture_file = File.from_content(capture_lines, 'capture') + assert(capture_file) + local item = capture_file:get_headlines()[1] + + org.instance().capture:_refile_to_end({ + file = destination_file, + lines = capture_lines, + item = item, + }) + vim.cmd('edit' .. vim.fn.fnameescape(destination_file)) + assert.are.same({ + '* foobar', + '** baz', + }, vim.api.nvim_buf_get_lines(0, 0, -1, false)) + end) + it('to headline', function() + local destination_file = helpers.load_file_content({ + '* foobar', + ' ', + '', + '\t\t\t\t', + '', + '* barbar', + '', + ' ', + '', + }) + + local capture_lines = { '** baz' } + helpers.load_file_content(capture_lines) + local capture_file = File.from_content(capture_lines, 'capture') + assert(capture_file) + local item = capture_file:get_headlines()[1] + + org.instance().capture:refile_to_headline({ + file = destination_file, + lines = capture_lines, + item = item, + headline = 'foobar', + }) + vim.cmd('edit' .. vim.fn.fnameescape(destination_file)) + assert.are.same({ + '* foobar', + '** baz', + '* barbar', + '', + ' ', + '', + }, vim.api.nvim_buf_get_lines(0, 0, -1, false)) + end) +end) + +describe('Refile with empty lines', function() + it('to empty file', function() + local destination_file = helpers.load_file_content({}) + + local capture_lines = { '* foo' } + helpers.load_file_content(capture_lines) + local capture_file = File.from_content(capture_lines, 'capture') + assert(capture_file) + local item = capture_file:get_headlines()[1] + + org.instance().capture:_refile_to_end({ + file = destination_file, + lines = capture_lines, + item = item, + template = Template:new({ + properties = { + empty_lines = { + before = 2, + after = 1, + }, + }, + }), + }) + vim.cmd('edit' .. vim.fn.fnameescape(destination_file)) + assert.are.same({ + '', + '', + '* foo', + '', + }, vim.api.nvim_buf_get_lines(0, 0, -1, false)) + end) + + it('to end', function() + local destination_file = helpers.load_file_content({ + '* foobar', + ' ', + '', + '\t\t\t\t', + '', + }) + + local capture_lines = { '** baz' } + helpers.load_file_content(capture_lines) + local capture_file = File.from_content(capture_lines, 'capture') + assert(capture_file) + local item = capture_file:get_headlines()[1] + + org.instance().capture:_refile_to_end({ + file = destination_file, + lines = capture_lines, + item = item, + template = Template:new({ + properties = { + empty_lines = { + before = 2, + after = 1, + }, + }, + }), + }) + vim.cmd('edit' .. vim.fn.fnameescape(destination_file)) + assert.are.same({ + '* foobar', + '', + '', + '** baz', + '', + }, vim.api.nvim_buf_get_lines(0, 0, -1, false)) + end) + it('to headline', function() + local destination_file = helpers.load_file_content({ + '* foobar', + ' ', + '', + '\t\t\t\t', + '', + '* barbar', + '', + ' ', + '', + }) + + local capture_lines = { '** baz' } + helpers.load_file_content(capture_lines) + local capture_file = File.from_content(capture_lines, 'capture') + assert(capture_file) + local item = capture_file:get_headlines()[1] + + org.instance().capture:refile_to_headline({ + file = destination_file, + lines = capture_lines, + item = item, + headline = 'foobar', + template = Template:new({ + properties = { + empty_lines = { + before = 2, + after = 1, + }, + }, + }), + }) + vim.cmd('edit' .. vim.fn.fnameescape(destination_file)) + assert.are.same({ + '* foobar', + '', + '', + '** baz', + '', + '* barbar', + '', + ' ', + '', + }, vim.api.nvim_buf_get_lines(0, 0, -1, false)) + end) +end) diff --git a/tests/plenary/ui/mappings/refile_spec.lua b/tests/plenary/ui/mappings/refile_spec.lua index 07db96153..76ae2fdfb 100644 --- a/tests/plenary/ui/mappings/refile_spec.lua +++ b/tests/plenary/ui/mappings/refile_spec.lua @@ -22,7 +22,12 @@ describe('Refile mappings', function() source_file = Files.get_current_file() local item = source_file:get_closest_headline() - org.instance().capture:refile_to_headline(destination_file, source_file:get_headline_lines(item), item, 'foo') + org.instance().capture:refile_to_headline({ + file = destination_file, + lines = source_file:get_headline_lines(item), + item = item, + headline = 'foo', + }) assert.are.same('* not to be refiled', vim.fn.getline(1)) vim.cmd('edit' .. vim.fn.fnameescape(destination_file)) assert.are.same({ @@ -47,7 +52,12 @@ describe('Refile mappings', function() source_file = Files.get_current_file() local item = source_file:get_closest_headline() - org.instance().capture:refile_to_headline(destination_file, source_file:get_headline_lines(item), item, 'foobar') + org.instance().capture:refile_to_headline({ + file = destination_file, + lines = source_file:get_headline_lines(item), + item = item, + headline = 'foobar', + }) assert.are.same('* not to be refiled', vim.fn.getline(1)) vim.cmd('edit' .. vim.fn.fnameescape(destination_file)) assert.are.same({ @@ -72,7 +82,12 @@ describe('Refile mappings', function() source_file = Files.get_current_file() local item = source_file:get_closest_headline() - org.instance().capture:refile_to_headline(destination_file, source_file:get_headline_lines(item), item, 'foobar') + org.instance().capture:refile_to_headline({ + file = destination_file, + lines = source_file:get_headline_lines(item), + item = item, + headline = 'foobar', + }) assert.are.same('* not to be refiled', vim.fn.getline(1)) vim.cmd('edit' .. vim.fn.fnameescape(destination_file)) assert.are.same({