Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ A powerful yet simple Neovim plugin for managing daily notes and quick notes wit
- **⚡ Quick Notes**: Create random-named notes in an inbox for rapid capture
- **🎨 Flexible Templates**: Progressive enhancement from zero-config to fully customizable
- **📝 Smart Frontmatter**: Automatic YAML frontmatter with timestamps and metadata
- **➕ Add Frontmatter**: Add frontmatter to any existing markdown file
- **🔄 Auto-timestamps**: Automatically update modified timestamps on save
- **🎯 Smart Completion**: Intelligent date completion with fuzzy matching
- **🛡️ Robust Error Handling**: Helpful error messages with actionable suggestions
Expand Down Expand Up @@ -54,8 +55,9 @@ return {
{ '<leader>nd', function() require('notes').daily_note() end, desc = 'Open daily note' },
{ '<leader>nt', function() require('notes').tomorrow_note() end, desc = 'Open tomorrow note' },
{ '<leader>nn', function() require('notes').quick_note() end, desc = 'Create quick note' },
{ '<leader>nf', function() require('notes').add_frontmatter() end, desc = 'Add frontmatter' },
},
cmd = { 'DailyNote', 'TomorrowNote', 'QuickNote' },
cmd = { 'DailyNote', 'TomorrowNote', 'QuickNote', 'AddFrontmatter' },
ft = "markdown",
}
```
Expand All @@ -77,8 +79,9 @@ return {
{ '<leader>nd', function() require('notes').daily_note() end, desc = 'Open daily note' },
{ '<leader>nt', function() require('notes').tomorrow_note() end, desc = 'Open tomorrow note' },
{ '<leader>nn', function() require('notes').quick_note() end, desc = 'Create quick note' },
{ '<leader>nf', function() require('notes').add_frontmatter() end, desc = 'Add frontmatter' },
},
cmd = { 'DailyNote', 'TomorrowNote', 'QuickNote' },
cmd = { 'DailyNote', 'TomorrowNote', 'QuickNote', 'AddFrontmatter' },
}
```

Expand Down Expand Up @@ -221,6 +224,7 @@ require('notes').setup({
use_frontmatter = true, -- Enable/disable frontmatter
auto_update_modified = true, -- Auto-update modified timestamp on save
scan_lines = 20, -- Lines to scan for frontmatter (1-100)
overwrite_frontmatter = false, -- Allow :AddFrontmatter to replace existing frontmatter
fields = {
id = true, -- Include ID field
created = true, -- Include created timestamp
Expand All @@ -243,6 +247,19 @@ require('notes').setup({
})
```

## ➕ Adding Frontmatter to Existing Files

Add frontmatter to any markdown file with `:AddFrontmatter`:

```vim
:AddFrontmatter " Adds frontmatter with random ID and empty tags
```

- Generates random ID (same as quick notes)
- Uses empty tags `[]` by default
- Respects your `frontmatter.fields` settings
- Won't overwrite existing frontmatter unless `overwrite_frontmatter = true`

## 📁 Directory Structure

Daily notes are organized as:
Expand All @@ -261,6 +278,7 @@ PKM_DIR/+Inbox/randomname.md
- `:DailyNote [date]` - Open daily note (supports smart date input)
- `:TomorrowNote` - Open tomorrow's daily note
- `:QuickNote` - Create a new quick note
- `:AddFrontmatter` - Add frontmatter to current markdown file

### Examples
```vim
Expand All @@ -279,6 +297,7 @@ require('notes').daily_note()
require('notes').tomorrow_note()
require('notes').quick_note()
require('notes').dynamic_daily_note('next monday')
require('notes').add_frontmatter()
```

## 🔧 Template Context
Expand Down
3 changes: 2 additions & 1 deletion lua/notes/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ M.defaults = {
use_frontmatter = true,
auto_update_modified = true,
scan_lines = 20,
overwrite_frontmatter = false,
fields = {
id = true,
created = true,
Expand Down Expand Up @@ -87,7 +88,7 @@ local function validate_frontmatter(frontmatter)
validate_type(frontmatter, "table", "frontmatter")

-- Validate boolean fields
local boolean_fields = { "use_frontmatter", "auto_update_modified" }
local boolean_fields = { "use_frontmatter", "auto_update_modified", "overwrite_frontmatter" }
for _, field in ipairs(boolean_fields) do
if frontmatter[field] ~= nil then
validate_type(frontmatter[field], "boolean", "frontmatter." .. field)
Expand Down
8 changes: 8 additions & 0 deletions lua/notes/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ function M.setup_commands(opts)
vim.api.nvim_create_user_command("QuickNote", function()
daily.quick_note(opts)
end, { desc = "Create a new quick note" })

vim.api.nvim_create_user_command("AddFrontmatter", function()
utils.add_frontmatter_to_current_buffer(opts)
end, { desc = "Add frontmatter to current markdown file" })
end

-- Helper function to ensure setup has been called
Expand Down Expand Up @@ -83,4 +87,8 @@ M.dynamic_daily_note = function(input)
return daily.dynamic_daily_note(input, ensure_setup())
end

M.add_frontmatter = function()
return utils.add_frontmatter_to_current_buffer(ensure_setup())
end

return M
46 changes: 46 additions & 0 deletions lua/notes/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,50 @@ function M.update_modified_timestamp(config)
end
end

-- Helper function to add frontmatter to current buffer
function M.add_frontmatter_to_current_buffer(config)
local errors = require("notes.errors")
local buf = vim.api.nvim_get_current_buf()
local scan_lines = config.frontmatter.scan_lines
local lines = vim.api.nvim_buf_get_lines(buf, 0, scan_lines, false)

-- Check if frontmatter already exists
if #lines >= 1 and lines[1] == "---" then
-- Find the closing frontmatter delimiter
local frontmatter_end = nil
for i = 2, #lines do
if lines[i] == "---" then
frontmatter_end = i
break
end
end

if frontmatter_end then
-- Frontmatter exists
if not config.frontmatter.overwrite_frontmatter then
errors.user_error(
"Frontmatter already exists",
"Set overwrite_frontmatter = true in config to replace existing frontmatter"
)
return
end

-- Remove existing frontmatter
vim.api.nvim_buf_set_lines(buf, 0, frontmatter_end, false, {})
end
end

-- Generate random ID using quick note id_length
local id_length = (config.templates.quick and config.templates.quick.id_length) or 8
local random_id = M.create_id(id_length)

-- Generate frontmatter lines
local frontmatter = M.generate_frontmatter(random_id, "[]", config)

-- Insert frontmatter at the beginning of the buffer
vim.api.nvim_buf_set_lines(buf, 0, 0, false, frontmatter)

vim.notify("notes.nvim: Frontmatter added successfully", vim.log.levels.INFO)
end

return M
1 change: 1 addition & 0 deletions tests/run_tests.lua
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ local test_suites = {
{ path = "tests.spec.templates_spec", name = "Template System" },
{ path = "tests.spec.config_spec", name = "Configuration Validation" },
{ path = "tests.spec.errors_spec", name = "Error Handling" },
{ path = "tests.spec.add_frontmatter_spec", name = "Add Frontmatter" },
}

for _, suite in ipairs(test_suites) do
Expand Down
217 changes: 217 additions & 0 deletions tests/spec/add_frontmatter_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
-- Add frontmatter functionality tests

-- Setup path and mocks
package.path = package.path .. ";./lua/?.lua;./lua/?/init.lua"
local vim_mocks = require("tests.helpers.vim_mocks")
local test_utils = require("tests.helpers.test_utils")

vim_mocks.setup()

-- Mock buffer for testing
local mock_buffer = {}
local mock_buffer_lines = {}

-- Add buffer API mocks
vim.api.nvim_get_current_buf = function()
return 1
end

vim.api.nvim_buf_get_lines = function(buf, start, end_line, strict)
local lines = {}
for i = start + 1, (end_line == -1 and #mock_buffer_lines or end_line) do
if mock_buffer_lines[i] then
table.insert(lines, mock_buffer_lines[i])
end
end
return lines
end

vim.api.nvim_buf_set_lines = function(buf, start, end_line, strict, lines)
-- nvim uses 0-based indexing, Lua uses 1-based
-- start is 0-based, inclusive
-- end_line is 0-based, exclusive

-- Remove lines from start to end (convert to 1-based)
for i = end_line, start + 1, -1 do
table.remove(mock_buffer_lines, i)
end

-- Insert new lines at start position (convert to 1-based)
for i = #lines, 1, -1 do
table.insert(mock_buffer_lines, start + 1, lines[i])
end
end

local function reset_buffer(initial_lines)
mock_buffer_lines = {}
if initial_lines then
for _, line in ipairs(initial_lines) do
table.insert(mock_buffer_lines, line)
end
end
end

local function get_buffer_lines()
-- Simple copy of lines table
local copy = {}
for i, line in ipairs(mock_buffer_lines) do
copy[i] = line
end
return copy
end

local utils = require("notes.utils")
local config = require("notes.config")

local function test_add_frontmatter_to_buffer_without_frontmatter()
reset_buffer({ "# My Note", "", "Some content here" })

config.setup({
pkm_dir = "/tmp/test_pkm",
frontmatter = {
overwrite_frontmatter = false,
},
})

utils.add_frontmatter_to_current_buffer(config.options)

local lines = get_buffer_lines()
test_utils.assert_equal("---", lines[1], "Should start with frontmatter delimiter")
test_utils.assert_matches("^id: %w+", lines[2], "Should have ID field")
test_utils.assert_matches("^created: %d%d%d%d%-", lines[3], "Should have created timestamp")
test_utils.assert_matches("^modified: %d%d%d%d%-", lines[4], "Should have modified timestamp")
test_utils.assert_equal("tags: []", lines[5], "Should have empty tags")
test_utils.assert_equal("---", lines[6], "Should close frontmatter")
test_utils.assert_equal("", lines[7], "Should have blank line after frontmatter")
test_utils.assert_equal("# My Note", lines[8], "Should preserve original content")
end

local function test_preserves_existing_content()
reset_buffer({ "Line 1", "Line 2", "Line 3" })

config.setup({
pkm_dir = "/tmp/test_pkm",
})

utils.add_frontmatter_to_current_buffer(config.options)

local lines = get_buffer_lines()
-- Find where content starts (after frontmatter)
local content_start = 8 -- After: ---, id, created, modified, tags, ---, blank line
test_utils.assert_equal("Line 1", lines[content_start], "Should preserve first line")
test_utils.assert_equal("Line 2", lines[content_start + 1], "Should preserve second line")
test_utils.assert_equal("Line 3", lines[content_start + 2], "Should preserve third line")
end

local function test_error_when_frontmatter_exists_and_no_overwrite()
reset_buffer({
"---",
"id: existing-id",
"created: 2025-01-01T00:00:00",
"---",
"",
"Content",
})

config.setup({
pkm_dir = "/tmp/test_pkm",
frontmatter = {
overwrite_frontmatter = false,
},
})

-- Clear previous notifications
_G._test_notifications = {}

utils.add_frontmatter_to_current_buffer(config.options)

-- Check that error notification was sent
local notifications = vim_mocks.get_notifications()
test_utils.assert_true(#notifications > 0, "Should send notification")
test_utils.assert_matches("already exists", notifications[1].message, "Should mention existing frontmatter")

-- Verify buffer wasn't modified
local lines = get_buffer_lines()
test_utils.assert_equal("existing-id", lines[2]:match("id: (.+)"), "Should not change existing ID")
end

local function test_replaces_frontmatter_when_overwrite_enabled()
reset_buffer({
"---",
"id: old-id",
"created: 2020-01-01T00:00:00",
"modified: 2020-01-01T00:00:00",
"tags: [#old]",
"---",
"",
"Content here",
})

config.setup({
pkm_dir = "/tmp/test_pkm",
frontmatter = {
overwrite_frontmatter = true,
},
})

utils.add_frontmatter_to_current_buffer(config.options)

local lines = get_buffer_lines()
test_utils.assert_equal("---", lines[1], "Should start with frontmatter delimiter")

-- Verify old ID is replaced with new random ID
local new_id = lines[2]:match("id: (.+)")
test_utils.assert_false(new_id == "old-id", "Should generate new ID")
test_utils.assert_matches("^%w+$", new_id, "New ID should be alphanumeric")

-- Verify tags are reset to empty
test_utils.assert_equal("tags: []", lines[5], "Should reset to empty tags")

-- Verify content is preserved
-- Original buffer: --- id created modified tags --- blank Content (8 lines)
-- After removing frontmatter (lines 1-6): blank Content (2 lines)
-- After adding new frontmatter (7 lines): --- id created modified tags --- blank blank Content
-- Content should be at line 9
test_utils.assert_not_nil(lines[9], "Should have content preserved")
if lines[9] then
test_utils.assert_equal("Content here", lines[9], "Should preserve content after frontmatter")
end
end

local function test_incomplete_frontmatter_handled()
reset_buffer({
"---",
"id: incomplete",
"Some content without closing delimiter",
})

config.setup({
pkm_dir = "/tmp/test_pkm",
frontmatter = {
overwrite_frontmatter = false,
},
})

-- Should not detect this as valid frontmatter since there's no closing ---
-- Therefore it should add frontmatter
utils.add_frontmatter_to_current_buffer(config.options)

local lines = get_buffer_lines()
test_utils.assert_equal("---", lines[1], "Should add frontmatter at start")
test_utils.assert_matches("^id: %w+", lines[2], "Should have new ID field")
end

-- Run all tests
local tests = {
["add frontmatter to buffer without frontmatter"] = test_add_frontmatter_to_buffer_without_frontmatter,
["preserve existing content"] = test_preserves_existing_content,
["error when frontmatter exists and no overwrite"] = test_error_when_frontmatter_exists_and_no_overwrite,
["replace frontmatter when overwrite enabled"] = test_replaces_frontmatter_when_overwrite_enabled,
["handle incomplete frontmatter"] = test_incomplete_frontmatter_handled,
}

local passed, total = test_utils.run_test_suite("Add Frontmatter Tests", tests)

vim_mocks.cleanup()

return { passed = passed, total = total }
Loading
Loading