Skip to content

More Configuration for Templates #184

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ tmp.*
doc/tags
_neovim
_runtime
deps/
2 changes: 2 additions & 0 deletions .luacheckrc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ self = false
exclude_files = {
"_neovim/*",
"_runtime/*",
"deps/*",
}

-- Glorious list of warnings: https://luacheck.readthedocs.io/en/stable/warnings.html
Expand All @@ -24,4 +25,5 @@ read_globals = {
"it",
"describe",
"before_each",
"after_each",
}
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added

- Allow custom directory and ID logic for templates

### Changed

### Fixed

- Fixed improper tmp-file creation during template tests

## [v3.12.0](https://github.com/obsidian-nvim/obsidian.nvim/releases/tag/v3.12.0) - 2025-06-05

### Added
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,9 @@ require("obsidian").setup {
time_format = "%H:%M",
-- A map for custom variables, the key should be the variable and the value a function
substitutions = {},

-- A map for configuring unique directories and paths for specific templates
customizations = {},
},

-- Sets how you follow URLs
Expand Down
31 changes: 21 additions & 10 deletions lua/obsidian/commands/new_from_template.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
local util = require "obsidian.util"
local log = require "obsidian.log"
local templates = require "obsidian.templates"

---@param client obsidian.Client
---@param data CommandArgs
Expand All @@ -14,30 +15,40 @@ return function(client, data)
local template = data.fargs[#data.fargs]

if title ~= nil and template ~= nil then
templates.load_template_customizations(template, client)
local note = client:create_note { title = title, template = template, no_write = false }
client:open_note(note, { sync = true })
templates.restore_client_configurations(client)
return
end

if title == nil or title == "" then
title = util.input("Enter title or path (optional): ", { completion = "file" })
if not title then
log.warn "Aborted"
return
elseif title == "" then
title = nil
end
end

picker:find_templates {
callback = function(name)
templates.load_template_customizations(name, client)
if title == nil or title == "" then
-- Must use pcall in case of KeyboardInterrupt
-- We cannot place `title` where `safe_title` is because it would be redeclaring it
local success, safe_title = pcall(util.input, "Enter title or path (optional): ", { completion = "file" })
title = safe_title
if not success or not safe_title then
log.warn "Aborted"
templates.restore_client_configurations(client)
return
elseif safe_title == "" then
title = nil
end
end

if name == nil or name == "" then
log.warn "Aborted"
templates.restore_client_configurations(client)
return
end

---@type obsidian.Note
local note = client:create_note { title = title, template = name, no_write = false }
client:open_note(note, { sync = false })
templates.restore_client_configurations(client)
end,
}
end
7 changes: 7 additions & 0 deletions lua/obsidian/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,7 @@ end
---@field date_format string|?
---@field time_format string|?
---@field substitutions table<string, function|string>|?
---@field customizations table<string, obsidian.config.CustomTemplateOpts>
config.TemplateOpts = {}

--- Get defaults.
Expand All @@ -463,9 +464,15 @@ config.TemplateOpts.default = function()
date_format = nil,
time_format = nil,
substitutions = {},
customizations = {},
}
end

---@class obsidian.config.CustomTemplateOpts
---
---@field dir string|?
---@field note_id_func function|?

---@class obsidian.config.UIOpts
---
---@field enable boolean
Expand Down
56 changes: 56 additions & 0 deletions lua/obsidian/templates.lua
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
local Path = require "obsidian.path"
local Note = require "obsidian.note"
local util = require "obsidian.util"
local config = require "obsidian.config"

local M = {}
local restore_client_key = "__restore_client_key"

--- Resolve a template name to a path.
---
Expand Down Expand Up @@ -197,4 +199,58 @@ M.insert_template = function(opts)
return Note.from_buffer(buf)
end

--- Loads the client with customizations for a template identified by `template_name` if present
---
--- @param template_name string The template name
--- @param client obsidian.Client The client
M.load_template_customizations = function(template_name, client)
local success, template_path = pcall(resolve_template, template_name, client)

if not success then
return
end

--- @type obsidian.config.CustomTemplateOpts|?
local customization = nil

-- Check if the configuration has a custom key for this template
for template_key, template_config in pairs(client.opts.templates.customizations) do
if template_key:lower() == template_path.stem:lower() then
customization = template_config
break
end
end

if not customization then
return
end

local restore_values = {
dir = client.opts.notes_subdir,
note_id_func = client.opts.note_id_func,
new_notes_location = client.opts.new_notes_location,
}

client[restore_client_key] = restore_values
client.opts.notes_subdir = customization.dir
client.opts.note_id_func = customization.note_id_func
client.opts.new_notes_location = config.NewNotesLocation.notes_subdir
return nil
end

--- Restores the client's configuration if saved previously (during `load_template_customizations`), does nothing otherwise
--- @param client obsidian.Client The client
M.restore_client_configurations = function(client)
--- @type unknown
local restore_values = rawget(client, restore_client_key)

if not restore_values then
return
end

client.opts.notes_subdir = restore_values.dir
client.opts.note_id_func = restore_values.note_id_func
client.opts.new_notes_location = restore_values.new_notes_location
end

return M
29 changes: 29 additions & 0 deletions lua/test_utils.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
local M = {}

local obsidian = require "obsidian"

---Get a client in a temporary directory.
---@param testid string A unique ID for the tests which prevents directory collision
---@param templates_dir string The template directory
---@return obsidian.Client
M.get_tmp_client = function(testid, templates_dir)
templates_dir = templates_dir or "templates"

local tmpdir = "tmp-vault-" .. testid

vim.loop.fs_mkdir(tmpdir, 448) -- <-- octal representation of 700 (RWX)
vim.loop.fs_mkdir(tmpdir .. "/" .. templates_dir, 448)

local client = obsidian.new_from_dir(tmpdir)
client.opts.templates.folder = "templates"
return client
end

--- Clean up a client, removing any resources created during `get_tmp_client`
--- @param client obsidian.Client The Client
M.cleanup_tmp_client = function(client)
local path = client.dir:resolve()
vim.fs.rm(tostring(path), { recursive = true, force = true })
end

return M
83 changes: 83 additions & 0 deletions test/obsidian/commands/new_from_template_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
local get_tmp_client = require("test_utils").get_tmp_client
local cleanup_tmp_client = require("test_utils").cleanup_tmp_client
local new_from_template = require "obsidian.commands.new_from_template"
local spy = require "luassert.spy"

local templates_dir = "templates"

--- @type obsidian.config.CustomTemplateOpts
local zettelConfig = {
dir = "31 Atomic",
note_id_func = function(title)
return "31.01 - " .. title
end,
}

describe("new_from_template", function()
--- @type obsidian.Client
local client = nil

before_each(function()
client = get_tmp_client("new_from_template", templates_dir)
vim.loop.fs_mkdir(tostring(client.dir) .. "/" .. zettelConfig.dir, 448)
client.picker = function(_)
return {
find_templates = function(_, opts)
opts.callback()
end,
}
end
client.opts.templates.customizations = {
Zettel = zettelConfig,
}
client.opts.new_notes_location = "notes_subdir"
end)

after_each(function()
if client then
cleanup_tmp_client(client)
end
end)

it("should always try to load and restore template configurations", function()
-- Arrange
local templates = require "obsidian.templates"
client:create_note { dir = templates_dir, id = "zettel" }
spy.on(templates, "load_template_customizations")
spy.on(templates, "restore_client_configurations")

-- Act
---@diagnostic disable-next-line: missing-fields
new_from_template(client, { fargs = { "Special Title", "zettel" } })

-- Assert
assert.spy(templates.load_template_customizations).was.called()
assert.spy(templates.restore_client_configurations).was.called()
end)

it("should place matched templates in the custom directory", function()
-- Arrange
client:create_note { dir = templates_dir, id = "zettel" }
local expectedDir = client.dir / zettelConfig.dir
local title = "The Big Bang"
local id = zettelConfig.note_id_func(title)
local expected = string.format("%s/%s.md", expectedDir, id)
client.picker = function(_)
return {
find_templates = function(_, opts)
opts.callback "zettel"
end,
}
end

-- Act

---@diagnostic disable-next-line: missing-fields
new_from_template(client, { fargs = { title, "zettel" } })
local f = io.open(expected, "r")

-- Assert
assert.truthy(f)
io.close(f)
end)
end)
Loading