From 5c990f04bd7ce24da80e0376850551b9d0c2877a Mon Sep 17 00:00:00 2001 From: Adam Tajti Date: Fri, 24 Jan 2025 21:15:34 +0100 Subject: [PATCH 01/41] refactor: move nvim-cmp sources from root --- .../sources/nvim_cmp}/cmp_obsidian.lua | 1 + .../sources/nvim_cmp}/cmp_obsidian_new.lua | 29 +++++++++++++------ .../sources/nvim_cmp}/cmp_obsidian_tags.lua | 0 lua/obsidian/init.lua | 7 +++-- 4 files changed, 25 insertions(+), 12 deletions(-) rename lua/{ => obsidian/completion/sources/nvim_cmp}/cmp_obsidian.lua (99%) rename lua/{ => obsidian/completion/sources/nvim_cmp}/cmp_obsidian_new.lua (84%) rename lua/{ => obsidian/completion/sources/nvim_cmp}/cmp_obsidian_tags.lua (100%) diff --git a/lua/cmp_obsidian.lua b/lua/obsidian/completion/sources/nvim_cmp/cmp_obsidian.lua similarity index 99% rename from lua/cmp_obsidian.lua rename to lua/obsidian/completion/sources/nvim_cmp/cmp_obsidian.lua index 9d940585..fa419b61 100644 --- a/lua/cmp_obsidian.lua +++ b/lua/obsidian/completion/sources/nvim_cmp/cmp_obsidian.lua @@ -31,6 +31,7 @@ source.complete = function(_, request, callback) return end + -- Different from cmp_obsidian_new local in_buffer_only = false ---@type string|? diff --git a/lua/cmp_obsidian_new.lua b/lua/obsidian/completion/sources/nvim_cmp/cmp_obsidian_new.lua similarity index 84% rename from lua/cmp_obsidian_new.lua rename to lua/obsidian/completion/sources/nvim_cmp/cmp_obsidian_new.lua index 2889b0d3..da5817ce 100644 --- a/lua/cmp_obsidian_new.lua +++ b/lua/obsidian/completion/sources/nvim_cmp/cmp_obsidian_new.lua @@ -3,6 +3,8 @@ local completion = require "obsidian.completion.refs" local obsidian = require "obsidian" local util = require "obsidian.util" local LinkStyle = require("obsidian.config").LinkStyle +local Note = require "obsidian.note" +local Path = require "obsidian.path" ---@class cmp_obsidian_new.Source : obsidian.ABC local source = abc.new_class() @@ -15,10 +17,14 @@ source.get_trigger_characters = completion.get_trigger_characters source.get_keyword_pattern = completion.get_keyword_pattern -source.complete = function(_, request, callback) +---Invoke completion (required). +---@param params cmp.SourceCompletionApiParams +---@param callback fun(response: lsp.CompletionResponse|nil) +source.complete = function(_, params, callback) local client = assert(obsidian.get_client()) - local can_complete, search, insert_start, insert_end, ref_type = completion.can_complete(request) + local can_complete, search, insert_start, insert_end, ref_type = completion.can_complete(params) + -- Different from cmp_obsidian if search ~= nil then search = util.lstrip_whitespace(search) end @@ -124,11 +130,11 @@ source.complete = function(_, request, callback) newText = new_text, range = { start = { - line = request.context.cursor.row - 1, + line = params.context.cursor.row - 1, character = insert_start, }, ["end"] = { - line = request.context.cursor.row - 1, + line = params.context.cursor.row - 1, character = insert_end, }, }, @@ -146,12 +152,17 @@ source.complete = function(_, request, callback) } end -source.execute = function(_, item, callback) - local Note = require "obsidian.note" - local Path = require "obsidian.path" - +---Creates a new note using the default template for the completion item. +---Executed after the item was selected. +---@param completion_item lsp.CompletionItem +---@param callback fun(completion_item: lsp.CompletionItem|nil) +source.execute = function(_, completion_item, callback) local client = assert(obsidian.get_client()) - local data = item.data + local data = completion_item.data + + if data == nil then + return callback(nil) + end -- Make sure `data.note` is actually an `obsidian.Note` object. If it gets serialized at some -- point (seems to happen on Linux), it will lose its metatable. diff --git a/lua/cmp_obsidian_tags.lua b/lua/obsidian/completion/sources/nvim_cmp/cmp_obsidian_tags.lua similarity index 100% rename from lua/cmp_obsidian_tags.lua rename to lua/obsidian/completion/sources/nvim_cmp/cmp_obsidian_tags.lua diff --git a/lua/obsidian/init.lua b/lua/obsidian/init.lua index b5e13910..8963fcc0 100644 --- a/lua/obsidian/init.lua +++ b/lua/obsidian/init.lua @@ -103,9 +103,10 @@ obsidian.setup = function(opts) if opts.completion.nvim_cmp then local cmp = require "cmp" - cmp.register_source("obsidian", require("cmp_obsidian").new()) - cmp.register_source("obsidian_new", require("cmp_obsidian_new").new()) - cmp.register_source("obsidian_tags", require("cmp_obsidian_tags").new()) + cmp.register_source("obsidian", require("obsidian.completion.sources.nvim_cmp.cmp_obsidian").new()) + cmp.register_source("obsidian_new", require("obsidian.completion.sources.nvim_cmp.cmp_obsidian_new").new()) + cmp.register_source("obsidian_tags", require("obsidian.completion.sources.nvim_cmp.cmp_obsidian_tags").new()) + end end local group = vim.api.nvim_create_augroup("obsidian_setup", { clear = true }) From 5c14b9aaf92263bbfd814a507da100b51a14988b Mon Sep 17 00:00:00 2001 From: Adam Tajti Date: Mon, 27 Jan 2025 21:08:04 +0100 Subject: [PATCH 02/41] blink support --- lua/obsidian/commands/debug.lua | 60 ++- .../completion/plugin_initializers/blink.lua | 189 +++++++++ .../plugin_initializers/nvim_cmp.lua | 30 ++ lua/obsidian/completion/sources/base/new.lua | 217 ++++++++++ lua/obsidian/completion/sources/base/refs.lua | 381 ++++++++++++++++++ lua/obsidian/completion/sources/base/tags.lua | 115 ++++++ .../completion/sources/base/types.lua | 14 + lua/obsidian/completion/sources/blink/new.lua | 31 ++ .../completion/sources/blink/refs.lua | 30 ++ .../completion/sources/blink/tags.lua | 31 ++ .../completion/sources/blink/util.lua | 38 ++ .../sources/nvim_cmp/cmp_obsidian.lua | 310 -------------- .../sources/nvim_cmp/cmp_obsidian_new.lua | 178 -------- .../sources/nvim_cmp/cmp_obsidian_tags.lua | 67 --- .../completion/sources/nvim_cmp/new.lua | 34 ++ .../completion/sources/nvim_cmp/refs.lua | 29 ++ .../completion/sources/nvim_cmp/tags.lua | 28 ++ .../completion/sources/nvim_cmp/util.lua | 10 + lua/obsidian/completion/tags.lua | 7 +- lua/obsidian/config.lua | 1 + lua/obsidian/init.lua | 30 +- 21 files changed, 1243 insertions(+), 587 deletions(-) create mode 100644 lua/obsidian/completion/plugin_initializers/blink.lua create mode 100644 lua/obsidian/completion/plugin_initializers/nvim_cmp.lua create mode 100644 lua/obsidian/completion/sources/base/new.lua create mode 100644 lua/obsidian/completion/sources/base/refs.lua create mode 100644 lua/obsidian/completion/sources/base/tags.lua create mode 100644 lua/obsidian/completion/sources/base/types.lua create mode 100644 lua/obsidian/completion/sources/blink/new.lua create mode 100644 lua/obsidian/completion/sources/blink/refs.lua create mode 100644 lua/obsidian/completion/sources/blink/tags.lua create mode 100644 lua/obsidian/completion/sources/blink/util.lua delete mode 100644 lua/obsidian/completion/sources/nvim_cmp/cmp_obsidian.lua delete mode 100644 lua/obsidian/completion/sources/nvim_cmp/cmp_obsidian_new.lua delete mode 100644 lua/obsidian/completion/sources/nvim_cmp/cmp_obsidian_tags.lua create mode 100644 lua/obsidian/completion/sources/nvim_cmp/new.lua create mode 100644 lua/obsidian/completion/sources/nvim_cmp/refs.lua create mode 100644 lua/obsidian/completion/sources/nvim_cmp/tags.lua create mode 100644 lua/obsidian/completion/sources/nvim_cmp/util.lua diff --git a/lua/obsidian/commands/debug.lua b/lua/obsidian/commands/debug.lua index 3013bc38..3d364964 100644 --- a/lua/obsidian/commands/debug.lua +++ b/lua/obsidian/commands/debug.lua @@ -5,7 +5,7 @@ local util = require "obsidian.util" local VERSION = require "obsidian.version" ---@return { available: boolean, refs: boolean|?, tags: boolean|?, new: boolean|?, sources: string[]|? } -local function check_completion() +local function check_completion_with_nvim_cmp() local ok, cmp = pcall(require, "cmp") if not ok then return { available = false } @@ -30,6 +30,28 @@ local function check_completion() return { available = true, refs = cmp_refs, tags = cmp_tags, new = cmp_new, sources = sources } end +---@return { available: boolean, refs: boolean|?, tags: boolean|?, new: boolean|?, sources: string[]|? } +local function check_completion_with_blink() + local ok, blink_sources_lib = pcall(require, "blink.cmp.sources.lib") + if not ok then + return { available = false } + end + + local cmp_refs = pcall(blink_sources_lib.get_provider_by_id, "obsidian") + local cmp_tags = pcall(blink_sources_lib.get_provider_by_id, "obsidian_tags") + local cmp_new = pcall(blink_sources_lib.get_provider_by_id, "obsidian_new") + + local sources = {} + local ok, providers = pcall(blink_sources_lib.get_all_providers) + if ok then + vim.tbl_map(function(provider) + table.insert(sources, provider.name) + end, providers) + end + + return { available = true, refs = cmp_refs ~= nil, tags = cmp_tags, new = cmp_new, sources = sources } +end + ---@param client obsidian.Client return function(client, data) data = data or {} @@ -51,7 +73,8 @@ return function(client, data) end log.lazy_info "Dependencies:" - for _, plugin in ipairs { "plenary.nvim", "nvim-cmp", "telescope.nvim", "fzf-lua", "mini.pick" } do + + for _, plugin in ipairs { "plenary.nvim", "nvim-cmp", "blink.cmp", "telescope.nvim", "fzf-lua", "mini.pick" } do local plugin_info = util.get_plugin_info(plugin) if plugin_info ~= nil then log.lazy_info(" ✓ %s: %s", plugin, plugin_info.commit or "unknown") @@ -62,18 +85,37 @@ return function(client, data) log.lazy_info(" ✓ picker: %s", client:picker()) if client.opts.completion.nvim_cmp then - local cmp_status = check_completion() - if cmp_status.available then + local nvim_cmp_status = check_completion_with_nvim_cmp() + if nvim_cmp_status.available then log.lazy_info( " ✓ completion: enabled (nvim-cmp) %s refs, %s tags, %s new", - cmp_status.refs and "✓" or "✗", - cmp_status.tags and "✓" or "✗", - cmp_status.new and "✓" or "✗" + nvim_cmp_status.refs and "✓" or "✗", + nvim_cmp_status.tags and "✓" or "✗", + nvim_cmp_status.new and "✓" or "✗" + ) + + if nvim_cmp_status.sources then + log.lazy_info " all sources:" + for _, source in ipairs(nvim_cmp_status.sources) do + log.lazy_info(" • %s", source) + end + end + else + log.lazy_info " ✓ completion: unavailable" + end + elseif client.opts.completion.blink then + local blink_status = check_completion_with_blink() + if blink_status.available then + log.lazy_info( + " ✓ completion: enabled (blink) %s refs, %s tags, %s new", + blink_status.refs and "✓" or "✗", + blink_status.tags and "✓" or "✗", + blink_status.new and "✓" or "✗" ) - if cmp_status.sources then + if blink_status.sources then log.lazy_info " all sources:" - for _, source in ipairs(cmp_status.sources) do + for _, source in ipairs(blink_status.sources) do log.lazy_info(" • %s", source) end end diff --git a/lua/obsidian/completion/plugin_initializers/blink.lua b/lua/obsidian/completion/plugin_initializers/blink.lua new file mode 100644 index 00000000..c6f4246e --- /dev/null +++ b/lua/obsidian/completion/plugin_initializers/blink.lua @@ -0,0 +1,189 @@ +local util = require "obsidian.util" +local obsidian = require "obsidian" + +local M = {} + +M.injected_once = false + +M.providers = { + { name = "obsidian", module = "obsidian.completion.sources.blink.refs" }, + { name = "obsidian_tags", module = "obsidian.completion.sources.blink.tags" }, + { name = "obsidian_new", module = "obsidian.completion.sources.blink.new" }, +} + +local function add_provider(blink, provider_name, proivder_module) + blink.add_provider(provider_name, { + name = provider_name, + module = proivder_module, + async = true, + opts = {}, + enabled = function() + -- Enable only in markdown buffers. + return vim.tbl_contains({ "markdown" }, vim.bo.filetype) + and vim.bo.buftype ~= "prompt" + and vim.b.completion ~= false + end, + }) +end + +-- Ran once on the plugin startup +function M.register_providers() + local blink = require "blink.cmp" + + for _, provider in pairs(M.providers) do + add_provider(blink, provider.name, provider.module) + end +end + +local function add_element_to_list_if_not_exists(list, element) + if not vim.tbl_contains(list, element) then + table.insert(list, 1, element) + end +end + +local function should_return_if_not_in_workspace() + local current_file_path = vim.api.nvim_buf_get_name(0) + local buf_dir = vim.fs.dirname(current_file_path) + + local obsidian_client = assert(obsidian.get_client()) + local workspace = obsidian.Workspace.get_workspace_for_dir(buf_dir, obsidian_client.opts.workspaces) + if not workspace then + return true + else + return false + end +end + +local function log_unexpected_type(config_path, unexpected_type, expected_type) + vim.notify( + "blink.cmp's `" + .. config_path + .. "` configuration appears to be an '" + .. unexpected_type + .. "' type, but it " + .. "should be '" + .. expected_type + .. "'. Obsidian won't update this configuration, and " + .. "completion won't work with blink.cmp", + vim.log.levels.ERROR + ) +end + +---Attempts to inject the Obsidian sources into per_filetype if that's what the user seems to use for markdown +---@param blink_sources_per_filetype table +---@return boolean true if it obsidian sources were injected into the sources.per_filetype +local function try_inject_blink_sources_into_per_filetype(blink_sources_per_filetype) + -- If the per_filetype is an empty object, then it's probably not utilized by the user + if vim.deep_equal(blink_sources_per_filetype, {}) then + return false + end + + local markdown_config = blink_sources_per_filetype["markdown"] + + -- If the markdown key is not used, then per_filetype it's probably not utilized by the user + if markdown_config == nil then + return false + end + + local markdown_config_type = type(markdown_config) + if markdown_config_type == "table" and util.tbl_is_array(markdown_config) then + for _, provider in pairs(M.providers) do + add_element_to_list_if_not_exists(markdown_config, provider.name) + end + return true + elseif markdown_config_type == "function" then + local original_func = markdown_config + markdown_config = function() + local original_results = original_func() + + if should_return_if_not_in_workspace() then + return original_results + end + + for _, provider in pairs(M.providers) do + add_element_to_list_if_not_exists(original_results, provider.name) + end + return original_results + end + + -- Overwrite the original config function with the newly generated one + require("blink.cmp.config").sources.per_filetype["markdown"] = markdown_config + return true + else + log_unexpected_type( + ".sources.per_filetype['markdown']", + markdown_config_type, + "a list or a function that returns a list of sources" + ) + return true -- logged the error, returns as if this was successful to avoid further errors + end +end + +---Attempts to inject the Obsidian sources into default if that's what the user seems to use for markdown +---@param blink_sources_default (fun():string[])|(string[]) +---@return boolean true if it obsidian sources were injected into the sources.default +local function try_inject_blink_sources_into_default(blink_sources_default) + local blink_default_type = type(blink_sources_default) + if blink_default_type == "function" then + local original_func = blink_sources_default + blink_sources_default = function() + local original_results = original_func() + + if should_return_if_not_in_workspace() then + return original_results + end + + for _, provider in pairs(M.providers) do + add_element_to_list_if_not_exists(original_results, provider.name) + end + return original_results + end + + -- Overwrite the original config function with the newly generated one + require("blink.cmp.config").sources.default = blink_sources_default + return true + elseif blink_default_type == "table" and util.tbl_is_array(blink_sources_default) then + for _, provider in pairs(M.providers) do + add_element_to_list_if_not_exists(blink_sources_default, provider.name) + end + + return true + elseif blink_default_type == "table" then + log_unexpected_type(".sources.default", blink_default_type, "a list") + return true -- logged the error, returns as if this was successful to avoid further errors + else + log_unexpected_type(".sources.default", blink_default_type, "a list or a function that returns a list") + return true -- logged the error, returns as if this was successful to avoid further errors + end +end + +-- Triggered for each opened markdown buffer that's in a workspace. nvm_cmp had the capability to configure the sources +-- per buffer, but blink.cmp doesn't have that capability. Instead, we have to inject the sources into the global +-- configuration and set a boolean on the module to return early the next time this function is called. +-- +-- In-case the user used functions to configure their sources, the completion will properly work just for the markdown +-- files that are in a workspace. Otherwise, the completion will work for all markdown files. +function M.inject_sources() + if M.injected_once then + return + end + + M.injected_once = true + + local blink_config = require "blink.cmp.config" + -- 'per_filetype' sources has priority over 'default' sources. + -- 'per_filetype' can be a table or a function which returns a table (["filetype"] = { "a", "b" }) + -- 'per_filetype' has the default value of {} (even if it's not configured by the user) + local blink_sources_per_filetype = blink_config.sources.per_filetype + if try_inject_blink_sources_into_per_filetype(blink_sources_per_filetype) then + return + end + + -- 'default' can be a list/array or a function which returns a list/array ({ "a", "b"}) + local blink_sources_default = blink_config.sources["default"] + if try_inject_blink_sources_into_default(blink_sources_default) then + return + end +end + +return M diff --git a/lua/obsidian/completion/plugin_initializers/nvim_cmp.lua b/lua/obsidian/completion/plugin_initializers/nvim_cmp.lua new file mode 100644 index 00000000..a5c75c7a --- /dev/null +++ b/lua/obsidian/completion/plugin_initializers/nvim_cmp.lua @@ -0,0 +1,30 @@ +local M = {} + +-- Ran once on the plugin startup +function M.register_sources() + local cmp = require "cmp" + + cmp.register_source("obsidian", require("obsidian.completion.sources.nvim_cmp.refs").new()) + cmp.register_source("obsidian_new", require("obsidian.completion.sources.nvim_cmp.new").new()) + cmp.register_source("obsidian_tags", require("obsidian.completion.sources.nvim_cmp.tags").new()) +end + +-- Triggered for each opened markdown buffer that's in a workspace and configures nvim_cmp sources for the current buffer. +function M.inject_sources() + local cmp = require "cmp" + + local sources = { + { name = "obsidian" }, + { name = "obsidian_new" }, + { name = "obsidian_tags" }, + } + for _, source in pairs(cmp.get_config().sources) do + if source.name ~= "obsidian" and source.name ~= "obsidian_new" and source.name ~= "obsidian_tags" then + table.insert(sources, source) + end + end + ---@diagnostic disable-next-line: missing-fields + cmp.setup.buffer { sources = sources } +end + +return M diff --git a/lua/obsidian/completion/sources/base/new.lua b/lua/obsidian/completion/sources/base/new.lua new file mode 100644 index 00000000..bb19575b --- /dev/null +++ b/lua/obsidian/completion/sources/base/new.lua @@ -0,0 +1,217 @@ +local abc = require "obsidian.abc" +local completion = require "obsidian.completion.refs" +local obsidian = require "obsidian" +local util = require "obsidian.util" +local LinkStyle = require("obsidian.config").LinkStyle +local Note = require "obsidian.note" +local Path = require "obsidian.path" + +---Used to track variables that are used between reusable method calls. This is required, because each +---call to the sources's completion hook won't create a new source object, but will reuse the same one. +---@class obsidian.completion.sources.base.NewNoteSourceCompletionContext : obsidian.ABC +---@field client obsidian.Client +---@field completion_resolve_callback (fun(self: any)) blink or nvim_cmp completion resolve callback +---@field request obsidian.completion.sources.base.Request +---@field search string|? +---@field insert_start integer|? +---@field insert_end integer|? +---@field ref_type obsidian.completion.RefType|? +local NewNoteSourceCompletionContext = abc.new_class() + +NewNoteSourceCompletionContext.new = function() + return NewNoteSourceCompletionContext.init() +end + +---@class obsidian.completion.sources.base.NewNoteSourceBase : obsidian.ABC +---@field incomplete_response table +---@field complete_response table +local NewNoteSourceBase = abc.new_class() + +---@return obsidian.completion.sources.base.NewNoteSourceBase +NewNoteSourceBase.new = function() + return NewNoteSourceBase.init() +end + +NewNoteSourceBase.get_trigger_characters = completion.get_trigger_characters + +---Sets up a new completion context that is used to pass around variables between completion source methods +---@param completion_resolve_callback (fun(self: any)) blink or nvim_cmp completion resolve callback +---@param request obsidian.completion.sources.base.Request +---@return obsidian.completion.sources.base.NewNoteSourceCompletionContext +function NewNoteSourceBase:new_completion_context(completion_resolve_callback, request) + local completion_context = NewNoteSourceCompletionContext.new() + + -- Sets up the completion callback, which will be called when the (possibly incomplete) completion items are ready + completion_context.completion_resolve_callback = completion_resolve_callback + + -- This request object will be used to determine the current cursor location and the text around it + completion_context.request = request + + completion_context.client = assert(obsidian.get_client()) + + return completion_context +end + +--- Runs a generalized version of the complete (nvim_cmp) or get_completions (blink) methods +---@param cc obsidian.completion.sources.base.NewNoteSourceCompletionContext +function NewNoteSourceBase:process_completion(cc) + if not self:can_complete_request(cc) then + return + end + + ---@type string|? + local block_link + cc.search, block_link = util.strip_block_links(cc.search) + + ---@type string|? + local anchor_link + cc.search, anchor_link = util.strip_anchor_links(cc.search) + + -- If block link is incomplete, do nothing. + if not block_link and vim.endswith(cc.search, "#^") then + cc.completion_resolve_callback(self.incomplete_response) + return + end + + -- If anchor link is incomplete, do nothing. + if not anchor_link and vim.endswith(cc.search, "#") then + cc.completion_resolve_callback(self.incomplete_response) + return + end + + -- Probably just a block/anchor link within current note. + if string.len(cc.search) == 0 then + cc.completion_resolve_callback(self.incomplete_response) + return + end + + -- Create a mock block. + ---@type obsidian.note.Block|? + local block + if block_link then + block = { block = "", id = util.standardize_block(block_link), line = 1 } + end + + -- Create a mock anchor. + ---@type obsidian.note.HeaderAnchor|? + local anchor + if anchor_link then + anchor = { anchor = anchor_link, header = string.sub(anchor_link, 2), level = 1, line = 1 } + end + + ---@type { label: string, note: obsidian.Note, template: string|? }[] + local new_notes_opts = {} + + local note = cc.client:create_note { title = cc.search, no_write = true } + if note.title and string.len(note.title) > 0 then + new_notes_opts[#new_notes_opts + 1] = { label = cc.search, note = note } + end + + -- Check for datetime macros. + for _, dt_offset in ipairs(util.resolve_date_macro(cc.search)) do + if dt_offset.cadence == "daily" then + note = cc.client:daily(dt_offset.offset, { no_write = true }) + if not note:exists() then + new_notes_opts[#new_notes_opts + 1] = + { label = dt_offset.macro, note = note, template = cc.client.opts.daily_notes.template } + end + end + end + + -- Completion items. + local items = {} + + for _, new_note_opts in ipairs(new_notes_opts) do + local new_note = new_note_opts.note + + assert(new_note.path) + + ---@type obsidian.config.LinkStyle, string + local link_style, label + if cc.ref_type == completion.RefType.Wiki then + link_style = LinkStyle.wiki + label = string.format("[[%s]] (create)", new_note_opts.label) + elseif cc.ref_type == completion.RefType.Markdown then + link_style = LinkStyle.markdown + label = string.format("[%s](…) (create)", new_note_opts.label) + else + error "not implemented" + end + + local new_text = cc.client:format_link(new_note, { link_style = link_style, anchor = anchor, block = block }) + local documentation = { + kind = "markdown", + value = new_note:display_info { + label = "Create: " .. new_text, + }, + } + + items[#items + 1] = { + documentation = documentation, + sortText = new_note_opts.label, + label = label, + kind = vim.lsp.protocol.CompletionItemKind.Reference, + textEdit = { + newText = new_text, + range = { + start = { + line = cc.request.context.cursor.row - 1, + character = cc.insert_start, + }, + ["end"] = { + line = cc.request.context.cursor.row - 1, + character = cc.insert_end, + }, + }, + }, + data = { + note = new_note, + template = new_note_opts.template, + }, + } + end + + cc.completion_resolve_callback(vim.tbl_deep_extend("force", self.complete_response, { items = items })) +end + +--- Returns whatever it's possible to complete the search and sets up the search related variables in cc +---@param cc obsidian.completion.sources.base.NewNoteSourceCompletionContext +---@return boolean success provides a chance to return early if the request didn't meet the requirements +function NewNoteSourceBase:can_complete_request(cc) + local can_complete + can_complete, cc.search, cc.insert_start, cc.insert_end, cc.ref_type = completion.can_complete(cc.request) + + if cc.search ~= nil then + cc.search = util.lstrip_whitespace(cc.search) + end + + if not (can_complete and cc.search ~= nil and #cc.search >= cc.client.opts.completion.min_chars) then + cc.completion_resolve_callback(self.incomplete_response) + return false + end + return true +end + +--- Runs a generalized version of the execute method +---@param item any +---@return table|? callback_return_value +function NewNoteSourceBase:process_execute(item) + local client = assert(obsidian.get_client()) + local data = item.data + + if data == nil then + return nil + end + + -- Make sure `data.note` is actually an `obsidian.Note` object. If it gets serialized at some + -- point (seems to happen on Linux), it will lose its metatable. + if not Note.is_note_obj(data.note) then + data.note = setmetatable(data.note, Note.mt) + data.note.path = setmetatable(data.note.path, Path.mt) + end + + client:write_note(data.note, { template = data.template }) + return {} +end + +return NewNoteSourceBase diff --git a/lua/obsidian/completion/sources/base/refs.lua b/lua/obsidian/completion/sources/base/refs.lua new file mode 100644 index 00000000..602ada96 --- /dev/null +++ b/lua/obsidian/completion/sources/base/refs.lua @@ -0,0 +1,381 @@ +local abc = require "obsidian.abc" +local completion = require "obsidian.completion.refs" +local LinkStyle = require("obsidian.config").LinkStyle +local obsidian = require "obsidian" +local util = require "obsidian.util" +local iter = require("obsidian.itertools").iter + +---Used to track variables that are used between reusable method calls. This is required, because each +---call to the sources's completion hook won't create a new source object, but will reuse the same one. +---@class obsidian.completion.sources.base.RefsSourceCompletionContext : obsidian.ABC +---@field client obsidian.Client +---@field completion_resolve_callback (fun(self: any)) blink or nvim_cmp completion resolve callback +---@field request obsidian.completion.sources.base.Request +---@field in_buffer_only boolean +---@field search string|? +---@field insert_start integer|? +---@field insert_end integer|? +---@field ref_type obsidian.completion.RefType|? +---@field block_link string|? +---@field anchor_link string|? +---@field new_text_to_option table +local RefsSourceCompletionContext = abc.new_class() + +RefsSourceCompletionContext.new = function() + return RefsSourceCompletionContext.init() +end + +---@class obsidian.completion.sources.base.RefsSourceBase : obsidian.ABC +---@field incomplete_response table +---@field complete_response table +local RefsSourceBase = abc.new_class() + +---@return obsidian.completion.sources.base.RefsSourceBase +RefsSourceBase.new = function() + return RefsSourceBase.init() +end + +RefsSourceBase.get_trigger_characters = completion.get_trigger_characters + +---Sets up a new completion context that is used to pass around variables between completion source methods +---@param completion_resolve_callback (fun(self: any)) blink or nvim_cmp completion resolve callback +---@param request obsidian.completion.sources.base.Request +---@return obsidian.completion.sources.base.RefsSourceCompletionContext +function RefsSourceBase:new_completion_context(completion_resolve_callback, request) + local completion_context = RefsSourceCompletionContext.new() + + -- Sets up the completion callback, which will be called when the (possibly incomplete) completion items are ready + completion_context.completion_resolve_callback = completion_resolve_callback + + -- This request object will be used to determine the current cursor location and the text around it + completion_context.request = request + + completion_context.client = assert(obsidian.get_client()) + + completion_context.in_buffer_only = false + + return completion_context +end + +--- Runs a generalized version of the complete (nvim_cmp) or get_completions (blink) methods +---@param cc obsidian.completion.sources.base.RefsSourceCompletionContext +function RefsSourceBase:process_completion(cc) + if not self:can_complete_request(cc) then + return + end + + self:strip_links(cc) + self:determine_buffer_only_search_scope(cc) + + if cc.in_buffer_only then + local note = cc.client:current_note(0, { collect_anchor_links = true, collect_blocks = true }) + if note then + self:process_search_results(cc, { note }) + else + cc.completion_resolve_callback(self.incomplete_response) + end + else + local search_ops = cc.client.search_defaults() + search_ops.ignore_case = true + + cc.client:find_notes_async(cc.search, function(results) + self:process_search_results(cc, results) + end, { + search = search_ops, + notes = { collect_anchor_links = cc.anchor_link ~= nil, collect_blocks = cc.block_link ~= nil }, + }) + end +end + +--- Returns whatever it's possible to complete the search and sets up the search related variables in cc +---@param cc obsidian.completion.sources.base.RefsSourceCompletionContext +---@return boolean success provides a chance to return early if the request didn't meet the requirements +function RefsSourceBase:can_complete_request(cc) + local can_complete + can_complete, cc.search, cc.insert_start, cc.insert_end, cc.ref_type = completion.can_complete(cc.request) + + if not (can_complete and cc.search ~= nil and #cc.search >= cc.client.opts.completion.min_chars) then + cc.completion_resolve_callback(self.incomplete_response) + return false + end + + return true +end + +---Collect matching block links. +---@param note obsidian.Note +---@param block_link string? +---@return obsidian.note.Block[]|? +function RefsSourceBase:collect_matching_blocks(note, block_link) + ---@type obsidian.note.Block[]|? + local matching_blocks + if block_link then + assert(note.blocks) + matching_blocks = {} + for block_id, block_data in pairs(note.blocks) do + if vim.startswith("#" .. block_id, block_link) then + table.insert(matching_blocks, block_data) + end + end + + if #matching_blocks == 0 then + -- Unmatched, create a mock one. + table.insert(matching_blocks, { id = util.standardize_block(block_link), line = 1 }) + end + end + + return matching_blocks +end + +---Collect matching anchor links. +---@param note obsidian.Note +---@param anchor_link string? +---@return obsidian.note.HeaderAnchor[]? +function RefsSourceBase:collect_matching_anchors(note, anchor_link) + ---@type obsidian.note.HeaderAnchor[]|? + local matching_anchors + if anchor_link then + assert(note.anchor_links) + matching_anchors = {} + for anchor, anchor_data in pairs(note.anchor_links) do + if vim.startswith(anchor, anchor_link) then + table.insert(matching_anchors, anchor_data) + end + end + + if #matching_anchors == 0 then + -- Unmatched, create a mock one. + table.insert(matching_anchors, { anchor = anchor_link, header = string.sub(anchor_link, 2), level = 1, line = 1 }) + end + end + + return matching_anchors +end + +--- Strips block and anchor links from the current search string +---@param cc obsidian.completion.sources.base.RefsSourceCompletionContext +function RefsSourceBase:strip_links(cc) + cc.search, cc.block_link = util.strip_block_links(cc.search) + cc.search, cc.anchor_link = util.strip_anchor_links(cc.search) + + -- If block link is incomplete, we'll match against all block links. + if not cc.block_link and vim.endswith(cc.search, "#^") then + cc.block_link = "#^" + cc.search = string.sub(cc.search, 1, -3) + end + + -- If anchor link is incomplete, we'll match against all anchor links. + if not cc.anchor_link and vim.endswith(cc.search, "#") then + cc.anchor_link = "#" + cc.search = string.sub(cc.search, 1, -2) + end +end + +--- Determines whatever the in_buffer_only should be enabled +---@param cc obsidian.completion.sources.base.RefsSourceCompletionContext +function RefsSourceBase:determine_buffer_only_search_scope(cc) + if (cc.anchor_link or cc.block_link) and string.len(cc.search) == 0 then + -- Search over headers/blocks in current buffer only. + cc.in_buffer_only = true + end +end + +---@param cc obsidian.completion.sources.base.RefsSourceCompletionContext +---@param results obsidian.Note[] +function RefsSourceBase:process_search_results(cc, results) + assert(cc) + assert(results) + + local completion_items = {} + + cc.new_text_to_option = {} + + for note in iter(results) do + ---@cast note obsidian.Note + + local matching_blocks = self:collect_matching_blocks(note, cc.block_link) + local matching_anchors = self:collect_matching_anchors(note, cc.anchor_link) + + if cc.in_buffer_only then + self:update_completion_options(cc, nil, nil, matching_anchors, matching_blocks, note) + else + -- Collect all valid aliases for the note, including ID, title, and filename. + ---@type string[] + local aliases + if not cc.in_buffer_only then + aliases = util.tbl_unique { tostring(note.id), note:display_name(), unpack(note.aliases) } + if note.title ~= nil then + table.insert(aliases, note.title) + end + end + + for alias in iter(aliases) do + self:update_completion_options(cc, alias, nil, matching_anchors, matching_blocks, note) + local alias_case_matched = util.match_case(cc.search, alias) + + if + alias_case_matched ~= nil + and alias_case_matched ~= alias + and not util.tbl_contains(note.aliases, alias_case_matched) + then + self:update_completion_options(cc, alias_case_matched, nil, matching_anchors, matching_blocks, note) + end + end + + if note.alt_alias ~= nil then + self:update_completion_options(cc, note:display_name(), note.alt_alias, matching_anchors, matching_blocks, note) + end + end + end + + for _, option in pairs(cc.new_text_to_option) do + -- TODO: need a better label, maybe just the note's display name? + ---@type string + local label + if cc.ref_type == completion.RefType.Wiki then + label = string.format("[[%s]]", option.label) + elseif cc.ref_type == completion.RefType.Markdown then + label = string.format("[%s](…)", option.label) + else + error "not implemented" + end + + table.insert(completion_items, { + documentation = option.documentation, + sortText = option.sort_text, + label = label, + kind = vim.lsp.protocol.CompletionItemKind.Reference, + textEdit = { + newText = option.new_text, + range = { + ["start"] = { + line = cc.request.context.cursor.row - 1, + character = cc.insert_start, + }, + ["end"] = { + line = cc.request.context.cursor.row - 1, + character = cc.insert_end + 1, + }, + }, + }, + }) + end + + cc.completion_resolve_callback(vim.tbl_deep_extend("force", self.complete_response, { items = completion_items })) +end + +---@param cc obsidian.completion.sources.base.RefsSourceCompletionContext +---@param label string|? +---@param alt_label string|? +---@param note obsidian.Note +function RefsSourceBase:update_completion_options(cc, label, alt_label, matching_anchors, matching_blocks, note) + ---@type { label: string|?, alt_label: string|?, anchor: obsidian.note.HeaderAnchor|?, block: obsidian.note.Block|? }[] + local new_options = {} + if matching_anchors ~= nil then + for anchor in iter(matching_anchors) do + table.insert(new_options, { label = label, alt_label = alt_label, anchor = anchor }) + end + elseif matching_blocks ~= nil then + for block in iter(matching_blocks) do + table.insert(new_options, { label = label, alt_label = alt_label, block = block }) + end + else + if label then + table.insert(new_options, { label = label, alt_label = alt_label }) + end + + -- Add all blocks and anchors, let cmp sort it out. + for _, anchor_data in pairs(note.anchor_links or {}) do + table.insert(new_options, { label = label, alt_label = alt_label, anchor = anchor_data }) + end + for _, block_data in pairs(note.blocks or {}) do + table.insert(new_options, { label = label, alt_label = alt_label, block = block_data }) + end + end + + -- De-duplicate options relative to their `new_text`. + for _, option in ipairs(new_options) do + ---@type obsidian.config.LinkStyle + local link_style + if cc.ref_type == completion.RefType.Wiki then + link_style = LinkStyle.wiki + elseif cc.ref_type == completion.RefType.Markdown then + link_style = LinkStyle.markdown + else + error "not implemented" + end + + ---@type string, string, string, table|? + local final_label, sort_text, new_text, documentation + if option.label then + new_text = cc.client:format_link( + note, + { label = option.label, link_style = link_style, anchor = option.anchor, block = option.block } + ) + + final_label = assert(option.alt_label or option.label) + if option.anchor then + final_label = final_label .. option.anchor.anchor + elseif option.block then + final_label = final_label .. "#" .. option.block.id + end + sort_text = final_label + + documentation = { + kind = "markdown", + value = note:display_info { + label = new_text, + anchor = option.anchor, + block = option.block, + }, + } + elseif option.anchor then + -- In buffer anchor link. + -- TODO: allow users to customize this? + if cc.ref_type == completion.RefType.Wiki then + new_text = "[[#" .. option.anchor.header .. "]]" + elseif cc.ref_type == completion.RefType.Markdown then + new_text = "[#" .. option.anchor.header .. "](" .. option.anchor.anchor .. ")" + else + error "not implemented" + end + + final_label = option.anchor.anchor + sort_text = final_label + + documentation = { + kind = "markdown", + value = string.format("`%s`", new_text), + } + elseif option.block then + -- In buffer block link. + -- TODO: allow users to customize this? + if cc.ref_type == completion.RefType.Wiki then + new_text = "[[#" .. option.block.id .. "]]" + elseif cc.ref_type == completion.RefType.Markdown then + new_text = "[#" .. option.block.id .. "](#" .. option.block.id .. ")" + else + error "not implemented" + end + + final_label = "#" .. option.block.id + sort_text = final_label + + documentation = { + kind = "markdown", + value = string.format("`%s`", new_text), + } + else + error "should not happen" + end + + if cc.new_text_to_option[new_text] then + cc.new_text_to_option[new_text].sort_text = cc.new_text_to_option[new_text].sort_text .. " " .. sort_text + else + cc.new_text_to_option[new_text] = + { label = final_label, new_text = new_text, sort_text = sort_text, documentation = documentation } + end + end +end + +return RefsSourceBase diff --git a/lua/obsidian/completion/sources/base/tags.lua b/lua/obsidian/completion/sources/base/tags.lua new file mode 100644 index 00000000..db01a139 --- /dev/null +++ b/lua/obsidian/completion/sources/base/tags.lua @@ -0,0 +1,115 @@ +local abc = require "obsidian.abc" +local completion = require "obsidian.completion.tags" +local iter = require("obsidian.itertools").iter +local obsidian = require "obsidian" +local util = require "obsidian.util" + +---Used to track variables that are used between reusable method calls. This is required, because each +---call to the sources's completion hook won't create a new source object, but will reuse the same one. +---@class obsidian.completion.sources.base.TagsSourceCompletionContext : obsidian.ABC +---@field client obsidian.Client +---@field completion_resolve_callback (fun(self: any)) blink or nvim_cmp completion resolve callback +---@field request obsidian.completion.sources.base.Request +---@field search string|? +---@field in_frontmatter boolean|? +local TagsSourceCompletionContext = abc.new_class() + +TagsSourceCompletionContext.new = function() + return TagsSourceCompletionContext.init() +end + +---@class obsidian.completion.sources.base.TagsSourceBase : obsidian.ABC +---@field incomplete_response table +---@field complete_response table +local TagsSourceBase = abc.new_class() + +---@return obsidian.completion.sources.base.TagsSourceBase +TagsSourceBase.new = function() + return TagsSourceBase.init() +end + +TagsSourceBase.get_trigger_characters = completion.get_trigger_characters + +---Sets up a new completion context that is used to pass around variables between completion source methods +---@param completion_resolve_callback (fun(self: any)) blink or nvim_cmp completion resolve callback +---@param request obsidian.completion.sources.base.Request +---@return obsidian.completion.sources.base.TagsSourceCompletionContext +function TagsSourceBase:new_completion_context(completion_resolve_callback, request) + local completion_context = TagsSourceCompletionContext.new() + + -- Sets up the completion callback, which will be called when the (possibly incomplete) completion items are ready + completion_context.completion_resolve_callback = completion_resolve_callback + + -- This request object will be used to determine the current cursor location and the text around it + completion_context.request = request + + completion_context.client = assert(obsidian.get_client()) + + return completion_context +end + +--- Runs a generalized version of the complete (nvim_cmp) or get_completions (blink) methods +---@param cc obsidian.completion.sources.base.TagsSourceCompletionContext +function TagsSourceBase:process_completion(cc) + if not self:can_complete_request(cc) then + return + end + + local search_opts = cc.client.search_defaults() + search_opts.sort = false + + cc.client:find_tags_async(cc.search, function(tag_locs) + local tags = {} + for tag_loc in iter(tag_locs) do + tags[tag_loc.tag] = true + end + + local items = {} + for tag, _ in pairs(tags) do + items[#items + 1] = { + sortText = "#" .. tag, + label = "Tag: #" .. tag, + kind = vim.lsp.protocol.CompletionItemKind.Text, + insertText = "#" .. tag, + data = { + bufnr = cc.request.context.bufnr, + in_frontmatter = cc.in_frontmatter, + line = cc.request.context.cursor.line, + tag = tag, + }, + } + end + + cc.completion_resolve_callback(vim.tbl_deep_extend("force", self.complete_response, { items = items })) + end, { search = search_opts }) +end + +--- Returns whatever it's possible to complete the search and sets up the search related variables in cc +---@param cc obsidian.completion.sources.base.TagsSourceCompletionContext +---@return boolean success provides a chance to return early if the request didn't meet the requirements +function TagsSourceBase:can_complete_request(cc) + local can_complete + can_complete, cc.search, cc.in_frontmatter = completion.can_complete(cc.request) + + if not (can_complete and cc.search ~= nil and #cc.search >= cc.client.opts.completion.min_chars) then + cc.completion_resolve_callback(self.incomplete_response) + return false + end + + return true +end + +--- Runs a generalized version of the execute method +---@param item any +function TagsSourceBase:process_execute(item) + if item.data.in_frontmatter then + -- Remove the '#' at the start of the tag. + -- TODO: ideally we should be able to do this by specifying the completion item in the right way, + -- but I haven't figured out how to do that. + local line = vim.api.nvim_buf_get_lines(item.data.bufnr, item.data.line, item.data.line + 1, true)[1] + line = util.string_replace(line, "#" .. item.data.tag, item.data.tag, 1) + vim.api.nvim_buf_set_lines(item.data.bufnr, item.data.line, item.data.line + 1, true, { line }) + end +end + +return TagsSourceBase diff --git a/lua/obsidian/completion/sources/base/types.lua b/lua/obsidian/completion/sources/base/types.lua new file mode 100644 index 00000000..16e1ce14 --- /dev/null +++ b/lua/obsidian/completion/sources/base/types.lua @@ -0,0 +1,14 @@ +---@class obsidian.completion.sources.base.Request.Context.Position +---@field public col integer +---@field public row integer + +---A request context class that partially matches cmp.Context to serve as a common interface for completion sources +---@class obsidian.completion.sources.base.Request.Context +---@field public bufnr integer +---@field public cursor obsidian.completion.sources.base.Request.Context.Position|lsp.Position +---@field public cursor_after_line string +---@field public cursor_before_line string + +---A request class that partially matches cmp.Request to serve as a common interface for completion sources +---@class obsidian.completion.sources.base.Request +---@field public context obsidian.completion.sources.base.Request.Context diff --git a/lua/obsidian/completion/sources/blink/new.lua b/lua/obsidian/completion/sources/blink/new.lua new file mode 100644 index 00000000..05f2edca --- /dev/null +++ b/lua/obsidian/completion/sources/blink/new.lua @@ -0,0 +1,31 @@ +local NewNoteSourceBase = require "obsidian.completion.sources.base.new" +local abc = require "obsidian.abc" +local blink_util = require "obsidian.completion.sources.blink.util" + +---@class obsidian.completion.sources.blink.NewNoteSource : obsidian.completion.sources.base.NewNoteSourceBase +local NewNoteSource = abc.new_class() + +NewNoteSource.incomplete_response = blink_util.incomplete_response +NewNoteSource.complete_response = blink_util.complete_response + +function NewNoteSource.new() + return NewNoteSource.init(NewNoteSourceBase) +end + +---Implement the get_completions method of the completion provider +---@param context blink.cmp.Context +---@param resolve fun(self: blink.cmp.CompletionResponse): nil +function NewNoteSource:get_completions(context, resolve) + local request = blink_util.generate_completion_request_from_editor_state(context) + local cc = self:new_completion_context(resolve, request) + self:process_completion(cc) +end + +---Implements the execute method of the completion provider +---@param _ blink.cmp.Context +---@param item blink.cmp.CompletionItem +function NewNoteSource:execute(_, item) + self:process_execute(item) +end + +return NewNoteSource diff --git a/lua/obsidian/completion/sources/blink/refs.lua b/lua/obsidian/completion/sources/blink/refs.lua new file mode 100644 index 00000000..1740e316 --- /dev/null +++ b/lua/obsidian/completion/sources/blink/refs.lua @@ -0,0 +1,30 @@ +local RefsSourceBase = require "obsidian.completion.sources.base.refs" +local abc = require "obsidian.abc" +local blink_util = require "obsidian.completion.sources.blink.util" + +---@class obsidian.completion.sources.blink.CompletionItem +---@field label string +---@field new_text string +---@field sort_text string +---@field documentation table|? + +---@class obsidian.completion.sources.blink.RefsSource : obsidian.completion.sources.base.RefsSourceBase +local RefsSource = abc.new_class() + +RefsSource.incomplete_response = blink_util.incomplete_response +RefsSource.complete_response = blink_util.complete_response + +function RefsSource.new() + return RefsSource.init(RefsSourceBase) +end + +---Implement the get_completions method of the completion provider +---@param context blink.cmp.Context +---@param resolve fun(self: blink.cmp.CompletionResponse): nil +function RefsSource:get_completions(context, resolve) + local request = blink_util.generate_completion_request_from_editor_state(context) + local cc = self:new_completion_context(resolve, request) + self:process_completion(cc) +end + +return RefsSource diff --git a/lua/obsidian/completion/sources/blink/tags.lua b/lua/obsidian/completion/sources/blink/tags.lua new file mode 100644 index 00000000..864dc4ac --- /dev/null +++ b/lua/obsidian/completion/sources/blink/tags.lua @@ -0,0 +1,31 @@ +local TagsSourceBase = require "obsidian.completion.sources.base.tags" +local abc = require "obsidian.abc" +local blink_util = require "obsidian.completion.sources.blink.util" + +---@class obsidian.completion.sources.blink.TagsSource : obsidian.completion.sources.base.TagsSourceBase +local TagsSource = abc.new_class() + +TagsSource.incomplete_response = blink_util.incomplete_response +TagsSource.complete_response = blink_util.complete_response + +function TagsSource.new() + return TagsSource.init(TagsSourceBase) +end + +---Implements the get_completions method of the completion provider +---@param context blink.cmp.Context +---@param resolve fun(self: blink.cmp.CompletionResponse): nil +function TagsSource:get_completions(context, resolve) + local request = blink_util.generate_completion_request_from_editor_state(context) + local cc = self:new_completion_context(resolve, request) + self:process_completion(cc) +end + +---Implements the execute method of the completion provider +---@param _ blink.cmp.Context +---@param item blink.cmp.CompletionItem +function TagsSource:execute(_, item) + self:process_execute(item) +end + +return TagsSource diff --git a/lua/obsidian/completion/sources/blink/util.lua b/lua/obsidian/completion/sources/blink/util.lua new file mode 100644 index 00000000..79b1b43a --- /dev/null +++ b/lua/obsidian/completion/sources/blink/util.lua @@ -0,0 +1,38 @@ +local M = {} + +---Generates the completion request from a blink context +---@param context blink.cmp.Context +---@return obsidian.completion.sources.base.Request +M.generate_completion_request_from_editor_state = function(context) + local row = context.cursor[1] + local col = context.cursor[2] + local cursor_before_line = context.line:sub(1, col) + local cursor_after_line = context.line:sub(col + 1) + + return { + context = { + bufnr = context.bufnr, + cursor_before_line = cursor_before_line, + cursor_after_line = cursor_after_line, + cursor = { + row = row, + col = col, + line = row + 1, + }, + }, + } +end + +M.incomplete_response = { + is_incomplete_forward = true, + is_incomplete_backward = true, + items = {}, +} + +M.complete_response = { + is_incomplete_forward = true, + is_incomplete_backward = false, + items = {}, +} + +return M diff --git a/lua/obsidian/completion/sources/nvim_cmp/cmp_obsidian.lua b/lua/obsidian/completion/sources/nvim_cmp/cmp_obsidian.lua deleted file mode 100644 index fa419b61..00000000 --- a/lua/obsidian/completion/sources/nvim_cmp/cmp_obsidian.lua +++ /dev/null @@ -1,310 +0,0 @@ -local abc = require "obsidian.abc" -local completion = require "obsidian.completion.refs" -local obsidian = require "obsidian" -local util = require "obsidian.util" -local iter = require("obsidian.itertools").iter -local LinkStyle = require("obsidian.config").LinkStyle - ----@class cmp_obsidian.CompletionItem ----@field label string ----@field new_text string ----@field sort_text string ----@field documentation table|? - ----@class cmp_obsidian.Source : obsidian.ABC -local source = abc.new_class() - -source.new = function() - return source.init() -end - -source.get_trigger_characters = completion.get_trigger_characters - -source.get_keyword_pattern = completion.get_keyword_pattern - -source.complete = function(_, request, callback) - local client = assert(obsidian.get_client()) - local can_complete, search, insert_start, insert_end, ref_type = completion.can_complete(request) - - if not (can_complete and search ~= nil and #search >= client.opts.completion.min_chars) then - callback { isIncomplete = true } - return - end - - -- Different from cmp_obsidian_new - local in_buffer_only = false - - ---@type string|? - local block_link - search, block_link = util.strip_block_links(search) - - ---@type string|? - local anchor_link - search, anchor_link = util.strip_anchor_links(search) - - -- If block link is incomplete, we'll match against all block links. - if not block_link and vim.endswith(search, "#^") then - block_link = "#^" - search = string.sub(search, 1, -3) - end - - -- If anchor link is incomplete, we'll match against all anchor links. - if not anchor_link and vim.endswith(search, "#") then - anchor_link = "#" - search = string.sub(search, 1, -2) - end - - if (anchor_link or block_link) and string.len(search) == 0 then - -- Search over headers/blocks in current buffer only. - in_buffer_only = true - end - - ---@param results obsidian.Note[] - local function search_callback(results) - -- Completion items. - local items = {} - - ---@type table - local new_text_to_option = {} - - for note in iter(results) do - ---@cast note obsidian.Note - - -- Collect matching block links. - ---@type obsidian.note.Block[]|? - local matching_blocks - if block_link then - assert(note.blocks) - matching_blocks = {} - for block_id, block_data in pairs(note.blocks) do - if vim.startswith("#" .. block_id, block_link) then - table.insert(matching_blocks, block_data) - end - end - - if #matching_blocks == 0 then - -- Unmatched, create a mock one. - table.insert(matching_blocks, { id = util.standardize_block(block_link), line = 1 }) - end - end - - -- Collect matching anchor links. - ---@type obsidian.note.HeaderAnchor[]|? - local matching_anchors - if anchor_link then - assert(note.anchor_links) - matching_anchors = {} - for anchor, anchor_data in pairs(note.anchor_links) do - if vim.startswith(anchor, anchor_link) then - table.insert(matching_anchors, anchor_data) - end - end - - if #matching_anchors == 0 then - -- Unmatched, create a mock one. - table.insert( - matching_anchors, - { anchor = anchor_link, header = string.sub(anchor_link, 2), level = 1, line = 1 } - ) - end - end - - ---@param label string|? - ---@param alt_label string|? - local function update_completion_options(label, alt_label) - ---@type { label: string|?, alt_label: string|?, anchor: obsidian.note.HeaderAnchor|?, block: obsidian.note.Block|? }[] - local new_options = {} - if matching_anchors ~= nil then - for anchor in iter(matching_anchors) do - table.insert(new_options, { label = label, alt_label = alt_label, anchor = anchor }) - end - elseif matching_blocks ~= nil then - for block in iter(matching_blocks) do - table.insert(new_options, { label = label, alt_label = alt_label, block = block }) - end - else - if label then - table.insert(new_options, { label = label, alt_label = alt_label }) - end - - -- Add all blocks and anchors, let cmp sort it out. - for _, anchor_data in pairs(note.anchor_links or {}) do - table.insert(new_options, { label = label, alt_label = alt_label, anchor = anchor_data }) - end - for _, block_data in pairs(note.blocks or {}) do - table.insert(new_options, { label = label, alt_label = alt_label, block = block_data }) - end - end - - -- De-duplicate options relative to their `new_text`. - for _, option in ipairs(new_options) do - ---@type obsidian.config.LinkStyle - local link_style - if ref_type == completion.RefType.Wiki then - link_style = LinkStyle.wiki - elseif ref_type == completion.RefType.Markdown then - link_style = LinkStyle.markdown - else - error "not implemented" - end - - ---@type string, string, string, table|? - local final_label, sort_text, new_text, documentation - if option.label then - new_text = client:format_link( - note, - { label = option.label, link_style = link_style, anchor = option.anchor, block = option.block } - ) - - final_label = assert(option.alt_label or option.label) - if option.anchor then - final_label = final_label .. option.anchor.anchor - elseif option.block then - final_label = final_label .. "#" .. option.block.id - end - sort_text = final_label - - documentation = { - kind = "markdown", - value = note:display_info { - label = new_text, - anchor = option.anchor, - block = option.block, - }, - } - elseif option.anchor then - -- In buffer anchor link. - -- TODO: allow users to customize this? - if ref_type == completion.RefType.Wiki then - new_text = "[[#" .. option.anchor.header .. "]]" - elseif ref_type == completion.RefType.Markdown then - new_text = "[#" .. option.anchor.header .. "](" .. option.anchor.anchor .. ")" - else - error "not implemented" - end - - final_label = option.anchor.anchor - sort_text = final_label - - documentation = { - kind = "markdown", - value = string.format("`%s`", new_text), - } - elseif option.block then - -- In buffer block link. - -- TODO: allow users to customize this? - if ref_type == completion.RefType.Wiki then - new_text = "[[#" .. option.block.id .. "]]" - elseif ref_type == completion.RefType.Markdown then - new_text = "[#" .. option.block.id .. "](#" .. option.block.id .. ")" - else - error "not implemented" - end - - final_label = "#" .. option.block.id - sort_text = final_label - - documentation = { - kind = "markdown", - value = string.format("`%s`", new_text), - } - else - error "should not happen" - end - - if new_text_to_option[new_text] then - new_text_to_option[new_text].sort_text = new_text_to_option[new_text].sort_text .. " " .. sort_text - else - new_text_to_option[new_text] = - { label = final_label, new_text = new_text, sort_text = sort_text, documentation = documentation } - end - end - end - - if in_buffer_only then - update_completion_options() - else - -- Collect all valid aliases for the note, including ID, title, and filename. - ---@type string[] - local aliases - if not in_buffer_only then - aliases = util.tbl_unique { tostring(note.id), note:display_name(), unpack(note.aliases) } - if note.title ~= nil then - table.insert(aliases, note.title) - end - end - - for alias in iter(aliases) do - update_completion_options(alias) - local alias_case_matched = util.match_case(search, alias) - - if - alias_case_matched ~= nil - and alias_case_matched ~= alias - and not util.tbl_contains(note.aliases, alias_case_matched) - then - update_completion_options(alias_case_matched) - end - end - - if note.alt_alias ~= nil then - update_completion_options(note:display_name(), note.alt_alias) - end - end - end - - for _, option in pairs(new_text_to_option) do - -- TODO: need a better label, maybe just the note's display name? - ---@type string - local label - if ref_type == completion.RefType.Wiki then - label = string.format("[[%s]]", option.label) - elseif ref_type == completion.RefType.Markdown then - label = string.format("[%s](…)", option.label) - else - error "not implemented" - end - - table.insert(items, { - documentation = option.documentation, - sortText = option.sort_text, - label = label, - kind = 18, -- "Reference" - textEdit = { - newText = option.new_text, - range = { - start = { - line = request.context.cursor.row - 1, - character = insert_start, - }, - ["end"] = { - line = request.context.cursor.row - 1, - character = insert_end, - }, - }, - }, - }) - end - - callback { - items = items, - isIncomplete = true, - } - end - - if in_buffer_only then - local note = client:current_note(0, { collect_anchor_links = true, collect_blocks = true }) - if note then - search_callback { note } - else - callback { isIncomplete = true } - end - else - client:find_notes_async(search, search_callback, { - search = { ignore_case = true }, - notes = { collect_anchor_links = anchor_link ~= nil, collect_blocks = block_link ~= nil }, - }) - end -end - -return source diff --git a/lua/obsidian/completion/sources/nvim_cmp/cmp_obsidian_new.lua b/lua/obsidian/completion/sources/nvim_cmp/cmp_obsidian_new.lua deleted file mode 100644 index da5817ce..00000000 --- a/lua/obsidian/completion/sources/nvim_cmp/cmp_obsidian_new.lua +++ /dev/null @@ -1,178 +0,0 @@ -local abc = require "obsidian.abc" -local completion = require "obsidian.completion.refs" -local obsidian = require "obsidian" -local util = require "obsidian.util" -local LinkStyle = require("obsidian.config").LinkStyle -local Note = require "obsidian.note" -local Path = require "obsidian.path" - ----@class cmp_obsidian_new.Source : obsidian.ABC -local source = abc.new_class() - -source.new = function() - return source.init() -end - -source.get_trigger_characters = completion.get_trigger_characters - -source.get_keyword_pattern = completion.get_keyword_pattern - ----Invoke completion (required). ----@param params cmp.SourceCompletionApiParams ----@param callback fun(response: lsp.CompletionResponse|nil) -source.complete = function(_, params, callback) - local client = assert(obsidian.get_client()) - local can_complete, search, insert_start, insert_end, ref_type = completion.can_complete(params) - - -- Different from cmp_obsidian - if search ~= nil then - search = util.lstrip_whitespace(search) - end - - if not (can_complete and search ~= nil and #search >= client.opts.completion.min_chars) then - callback { isIncomplete = true } - return - end - - ---@type string|? - local block_link - search, block_link = util.strip_block_links(search) - - ---@type string|? - local anchor_link - search, anchor_link = util.strip_anchor_links(search) - - -- If block link is incomplete, do nothing. - if not block_link and vim.endswith(search, "#^") then - callback { isIncomplete = true } - return - end - - -- If anchor link is incomplete, do nothing. - if not anchor_link and vim.endswith(search, "#") then - callback { isIncomplete = true } - return - end - - -- Probably just a block/anchor link within current note. - if string.len(search) == 0 then - callback { isIncomplete = false } - return - end - - -- Create a mock block. - ---@type obsidian.note.Block|? - local block - if block_link then - block = { block = "", id = util.standardize_block(block_link), line = 1 } - end - - -- Create a mock anchor. - ---@type obsidian.note.HeaderAnchor|? - local anchor - if anchor_link then - anchor = { anchor = anchor_link, header = string.sub(anchor_link, 2), level = 1, line = 1 } - end - - ---@type { label: string, note: obsidian.Note, template: string|? }[] - local new_notes_opts = {} - - local note = client:create_note { title = search, no_write = true } - if note.title and string.len(note.title) > 0 then - new_notes_opts[#new_notes_opts + 1] = { label = search, note = note } - end - - -- Check for datetime macros. - for _, dt_offset in ipairs(util.resolve_date_macro(search)) do - if dt_offset.cadence == "daily" then - note = client:daily(dt_offset.offset, { no_write = true }) - if not note:exists() then - new_notes_opts[#new_notes_opts + 1] = - { label = dt_offset.macro, note = note, template = client.opts.daily_notes.template } - end - end - end - - -- Completion items. - local items = {} - - for _, new_note_opts in ipairs(new_notes_opts) do - local new_note = new_note_opts.note - - assert(new_note.path) - - ---@type obsidian.config.LinkStyle, string - local link_style, label - if ref_type == completion.RefType.Wiki then - link_style = LinkStyle.wiki - label = string.format("[[%s]] (create)", new_note_opts.label) - elseif ref_type == completion.RefType.Markdown then - link_style = LinkStyle.markdown - label = string.format("[%s](…) (create)", new_note_opts.label) - else - error "not implemented" - end - - local new_text = client:format_link(new_note, { link_style = link_style, anchor = anchor, block = block }) - local documentation = { - kind = "markdown", - value = new_note:display_info { - label = "Create: " .. new_text, - }, - } - - items[#items + 1] = { - documentation = documentation, - sortText = new_note_opts.label, - label = label, - kind = 18, - textEdit = { - newText = new_text, - range = { - start = { - line = params.context.cursor.row - 1, - character = insert_start, - }, - ["end"] = { - line = params.context.cursor.row - 1, - character = insert_end, - }, - }, - }, - data = { - note = new_note, - template = new_note_opts.template, - }, - } - end - - return callback { - items = items, - isIncomplete = true, - } -end - ----Creates a new note using the default template for the completion item. ----Executed after the item was selected. ----@param completion_item lsp.CompletionItem ----@param callback fun(completion_item: lsp.CompletionItem|nil) -source.execute = function(_, completion_item, callback) - local client = assert(obsidian.get_client()) - local data = completion_item.data - - if data == nil then - return callback(nil) - end - - -- Make sure `data.note` is actually an `obsidian.Note` object. If it gets serialized at some - -- point (seems to happen on Linux), it will lose its metatable. - if not Note.is_note_obj(data.note) then - data.note = setmetatable(data.note, Note.mt) - data.note.path = setmetatable(data.note.path, Path.mt) - end - - client:write_note(data.note, { template = data.template }) - return callback {} -end - -return source diff --git a/lua/obsidian/completion/sources/nvim_cmp/cmp_obsidian_tags.lua b/lua/obsidian/completion/sources/nvim_cmp/cmp_obsidian_tags.lua deleted file mode 100644 index 4033f227..00000000 --- a/lua/obsidian/completion/sources/nvim_cmp/cmp_obsidian_tags.lua +++ /dev/null @@ -1,67 +0,0 @@ -local abc = require "obsidian.abc" -local obsidian = require "obsidian" -local completion = require "obsidian.completion.tags" -local util = require "obsidian.util" -local iter = require("obsidian.itertools").iter - ----@class cmp_obsidian_tags.Source : obsidian.ABC -local source = abc.new_class() - -source.new = function() - return source.init() -end - -source.get_trigger_characters = completion.get_trigger_characters - -source.get_keyword_pattern = completion.get_keyword_pattern - -source.complete = function(_, request, callback) - local client = assert(obsidian.get_client()) - local can_complete, search, in_frontmatter = completion.can_complete(request) - - if not (can_complete and search ~= nil and #search >= client.opts.completion.min_chars) then - return callback { isIncomplete = true } - end - - client:find_tags_async(search, function(tag_locs) - local tags = {} - for tag_loc in iter(tag_locs) do - tags[tag_loc.tag] = true - end - - local items = {} - for tag, _ in pairs(tags) do - items[#items + 1] = { - sortText = "#" .. tag, - label = "Tag: #" .. tag, - kind = 1, -- "Text" - insertText = "#" .. tag, - data = { - bufnr = request.context.bufnr, - in_frontmatter = in_frontmatter, - line = request.context.cursor.line, - tag = tag, - }, - } - end - - return callback { - items = items, - isIncomplete = false, - } - end, { search = { sort = false } }) -end - -source.execute = function(_, item, callback) - if item.data.in_frontmatter then - -- Remove the '#' at the start of the tag. - -- TODO: ideally we should be able to do this by specifying the completion item in the right way, - -- but I haven't figured out how to do that. - local line = vim.api.nvim_buf_get_lines(item.data.bufnr, item.data.line, item.data.line + 1, true)[1] - line = util.string_replace(line, "#" .. item.data.tag, item.data.tag, 1) - vim.api.nvim_buf_set_lines(item.data.bufnr, item.data.line, item.data.line + 1, true, { line }) - end - return callback {} -end - -return source diff --git a/lua/obsidian/completion/sources/nvim_cmp/new.lua b/lua/obsidian/completion/sources/nvim_cmp/new.lua new file mode 100644 index 00000000..62e8952f --- /dev/null +++ b/lua/obsidian/completion/sources/nvim_cmp/new.lua @@ -0,0 +1,34 @@ +local NewNoteSourceBase = require "obsidian.completion.sources.base.new" +local abc = require "obsidian.abc" +local completion = require "obsidian.completion.refs" +local nvim_cmp_util = require "obsidian.completion.sources.nvim_cmp.util" + +---@class obsidian.completion.sources.nvim_cmp.NewNoteSource : obsidian.completion.sources.base.NewNoteSourceBase +local NewNoteSource = abc.new_class() + +NewNoteSource.new = function() + return NewNoteSource.init(NewNoteSourceBase) +end + +NewNoteSource.get_keyword_pattern = completion.get_keyword_pattern + +NewNoteSource.incomplete_response = nvim_cmp_util.incomplete_response +NewNoteSource.complete_response = nvim_cmp_util.complete_response + +---Invoke completion (required). +---@param request cmp.SourceCompletionApiParams +---@param callback fun(response: lsp.CompletionResponse|nil) +function NewNoteSource:complete(request, callback) + local cc = self:new_completion_context(callback, request) + self:process_completion(cc) +end + +---Creates a new note using the default template for the completion item. +---Executed after the item was selected. +---@param completion_item lsp.CompletionItem +---@param callback fun(completion_item: lsp.CompletionItem|nil) +function NewNoteSource:execute(completion_item, callback) + return callback(self:process_execute(completion_item)) +end + +return NewNoteSource diff --git a/lua/obsidian/completion/sources/nvim_cmp/refs.lua b/lua/obsidian/completion/sources/nvim_cmp/refs.lua new file mode 100644 index 00000000..95f54348 --- /dev/null +++ b/lua/obsidian/completion/sources/nvim_cmp/refs.lua @@ -0,0 +1,29 @@ +local RefsSourceBase = require "obsidian.completion.sources.base.refs" +local abc = require "obsidian.abc" +local completion = require "obsidian.completion.refs" +local nvim_cmp_util = require "obsidian.completion.sources.nvim_cmp.util" + +---@class obsidian.completion.sources.nvim_cmp.CompletionItem +---@field label string +---@field new_text string +---@field sort_text string +---@field documentation table|? + +---@class obsidian.completion.sources.nvim_cmp.RefsSource : obsidian.completion.sources.base.RefsSourceBase +local RefsSource = abc.new_class() + +RefsSource.new = function() + return RefsSource.init(RefsSourceBase) +end + +RefsSource.get_keyword_pattern = completion.get_keyword_pattern + +RefsSource.incomplete_response = nvim_cmp_util.incomplete_response +RefsSource.complete_response = nvim_cmp_util.complete_response + +function RefsSource:complete(request, callback) + local cc = self:new_completion_context(callback, request) + self:process_completion(cc) +end + +return RefsSource diff --git a/lua/obsidian/completion/sources/nvim_cmp/tags.lua b/lua/obsidian/completion/sources/nvim_cmp/tags.lua new file mode 100644 index 00000000..476e8707 --- /dev/null +++ b/lua/obsidian/completion/sources/nvim_cmp/tags.lua @@ -0,0 +1,28 @@ +local TagsSourceBase = require "obsidian.completion.sources.base.tags" +local abc = require "obsidian.abc" +local completion = require "obsidian.completion.tags" +local nvim_cmp_util = require "obsidian.completion.sources.nvim_cmp.util" + +---@class obsidian.completion.sources.nvim_cmp.TagsSource : obsidian.completion.sources.base.TagsSourceBase +local TagsSource = abc.new_class() + +TagsSource.new = function() + return TagsSource.init(TagsSourceBase) +end + +TagsSource.get_keyword_pattern = completion.get_keyword_pattern + +TagsSource.incomplete_response = nvim_cmp_util.incomplete_response +TagsSource.complete_response = nvim_cmp_util.complete_response + +function TagsSource:complete(request, callback) + local cc = self:new_completion_context(callback, request) + self:process_completion(cc) +end + +function TagsSource:execute(item, callback) + self:process_execute(item) + return callback {} +end + +return TagsSource diff --git a/lua/obsidian/completion/sources/nvim_cmp/util.lua b/lua/obsidian/completion/sources/nvim_cmp/util.lua new file mode 100644 index 00000000..1c2e16bd --- /dev/null +++ b/lua/obsidian/completion/sources/nvim_cmp/util.lua @@ -0,0 +1,10 @@ +local M = {} + +M.incomplete_response = { isIncomplete = true } + +M.complete_response = { + isIncomplete = true, + items = {}, +} + +return M diff --git a/lua/obsidian/completion/tags.lua b/lua/obsidian/completion/tags.lua index 0f70476e..a757e595 100644 --- a/lua/obsidian/completion/tags.lua +++ b/lua/obsidian/completion/tags.lua @@ -39,7 +39,12 @@ M.can_complete = function(request) local in_frontmatter = false local line = request.context.cursor.line local frontmatter_start, frontmatter_end = get_frontmatter_boundaries(request.context.bufnr) - if frontmatter_start ~= nil and frontmatter_start <= line and frontmatter_end ~= nil and line <= frontmatter_end then + if + frontmatter_start ~= nil + and frontmatter_start <= (line + 1) + and frontmatter_end ~= nil + and (line + 1) <= frontmatter_end + then in_frontmatter = true end diff --git a/lua/obsidian/config.lua b/lua/obsidian/config.lua index 4f3ae84d..9740250b 100644 --- a/lua/obsidian/config.lua +++ b/lua/obsidian/config.lua @@ -280,6 +280,7 @@ config.LinkStyle = { ---@class obsidian.config.CompletionOpts --- ---@field nvim_cmp boolean +---@field blink boolean ---@field min_chars integer config.CompletionOpts = {} diff --git a/lua/obsidian/init.lua b/lua/obsidian/init.lua index 8963fcc0..7cc8dab8 100644 --- a/lua/obsidian/init.lua +++ b/lua/obsidian/init.lua @@ -99,14 +99,11 @@ obsidian.setup = function(opts) -- These will be available across all buffers, not just note buffers in the vault. obsidian.commands.install(client) - -- Register cmp sources. + -- Register completion sources, providers if opts.completion.nvim_cmp then - local cmp = require "cmp" - - cmp.register_source("obsidian", require("obsidian.completion.sources.nvim_cmp.cmp_obsidian").new()) - cmp.register_source("obsidian_new", require("obsidian.completion.sources.nvim_cmp.cmp_obsidian_new").new()) - cmp.register_source("obsidian_tags", require("obsidian.completion.sources.nvim_cmp.cmp_obsidian_tags").new()) - end + require("obsidian.completion.plugin_initializers.nvim_cmp").register_sources() + elseif opts.completion.blink then + require("obsidian.completion.plugin_initializers.blink").register_providers() end local group = vim.api.nvim_create_augroup("obsidian_setup", { clear = true }) @@ -140,22 +137,11 @@ obsidian.setup = function(opts) vim.keymap.set("n", mapping_keys, mapping_config.action, mapping_config.opts) end - -- Inject Obsidian as a cmp source. + -- Inject completion sources, providers to their plugin configurations if opts.completion.nvim_cmp then - local cmp = require "cmp" - - local sources = { - { name = "obsidian" }, - { name = "obsidian_new" }, - { name = "obsidian_tags" }, - } - for _, source in pairs(cmp.get_config().sources) do - if source.name ~= "obsidian" and source.name ~= "obsidian_new" and source.name ~= "obsidian_tags" then - table.insert(sources, source) - end - end - ---@diagnostic disable-next-line: missing-fields - cmp.setup.buffer { sources = sources } + require("obsidian.completion.plugin_initializers.nvim_cmp").inject_sources() + elseif opts.completion.blink then + require("obsidian.completion.plugin_initializers.blink").inject_sources() end -- Run enter-note callback. From a8d8d548638f3ae3ce3a19db64ad91d851a28d1a Mon Sep 17 00:00:00 2001 From: Adam Tajti Date: Mon, 27 Jan 2025 21:30:06 +0100 Subject: [PATCH 03/41] docs --- CHANGELOG.md | 1 + README.md | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b585ed0e..87c77c67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `opts.follow_img_func` option for customizing how to handle image paths. - Added better handling for undefined template fields, which will now be prompted for. +- Added support for the [`blink.cmp`](https://github.com/Saghen/blink.cmp) completion plugin. ### Changed diff --git a/README.md b/README.md index d5d382b6..f20955c5 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ _Keep in mind this plugin is not meant to replace Obsidian, but to complement it ## Features -▶️ **Completion:** Ultra-fast, asynchronous autocompletion for note references and tags via [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) (triggered by typing `[[` for wiki links, `[` for markdown links, or `#` for tags), powered by [`ripgrep`](https://github.com/BurntSushi/ripgrep). +▶️ **Completion:** Ultra-fast, asynchronous autocompletion for note references and tags via [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) or [blink.cmp](https://github.com/Saghen/blink.cmp) (triggered by typing `[[` for wiki links, `[` for markdown links, or `#` for tags), powered by [`ripgrep`](https://github.com/BurntSushi/ripgrep). [![See this screenshot](https://github.com/epwalsh/obsidian.nvim/assets/8812459/90d5f218-06cd-4ebb-b00b-b59c2f5c3cc1)](https://github.com/epwalsh/obsidian.nvim/assets/8812459/90d5f218-06cd-4ebb-b00b-b59c2f5c3cc1) @@ -195,6 +195,7 @@ The only **required** plugin dependency is [plenary.nvim](https://github.com/nvi **Completion:** - **[recommended]** [hrsh7th/nvim-cmp](https://github.com/hrsh7th/nvim-cmp): for completion of note references. +- [blink.cmp](https://github.com/Saghen/blink.cmp) (new): for completion of note references. **Pickers:** @@ -266,8 +267,10 @@ This is a complete list of all of the options that can be passed to `require("ob -- Optional, completion of wiki links, local markdown links, and tags using nvim-cmp. completion = { - -- Set to false to disable completion. + -- Enables completion using nvim_cmp nvim_cmp = true, + -- Enables completion using blink.cmp + blink = false, -- Trigger completion at 2 chars. min_chars = 2, }, @@ -612,7 +615,9 @@ See [using obsidian.nvim outside of a workspace / Obsidian vault](#usage-outside #### Completion -obsidian.nvim will set itself up as an nvim-cmp source automatically when you enter a markdown buffer within your vault directory, you do **not** need to specify this plugin as a cmp source manually. +obsidian.nvim supports nvim_cmp and blink.cmp completion plugins. + +obsidian.nvim will set itself up automatically when you enter a markdown buffer within your vault directory, you do **not** need to specify this plugin as a cmp source manually. Note that in order to trigger completion for tags _within YAML frontmatter_ you still need to type the "#" at the start of the tag. obsidian.nvim will remove the "#" when you hit enter on the tag completion item. From 658f548192ac0b97b7a0e2e56dbec817187fb8bd Mon Sep 17 00:00:00 2001 From: Adam Tajti Date: Mon, 27 Jan 2025 21:35:23 +0100 Subject: [PATCH 04/41] linter care --- lua/obsidian/commands/debug.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lua/obsidian/commands/debug.lua b/lua/obsidian/commands/debug.lua index 3d364964..ebc039f6 100644 --- a/lua/obsidian/commands/debug.lua +++ b/lua/obsidian/commands/debug.lua @@ -32,8 +32,8 @@ end ---@return { available: boolean, refs: boolean|?, tags: boolean|?, new: boolean|?, sources: string[]|? } local function check_completion_with_blink() - local ok, blink_sources_lib = pcall(require, "blink.cmp.sources.lib") - if not ok then + local require_ok, blink_sources_lib = pcall(require, "blink.cmp.sources.lib") + if not require_ok then return { available = false } end @@ -42,8 +42,8 @@ local function check_completion_with_blink() local cmp_new = pcall(blink_sources_lib.get_provider_by_id, "obsidian_new") local sources = {} - local ok, providers = pcall(blink_sources_lib.get_all_providers) - if ok then + local get_providers_ok, providers = pcall(blink_sources_lib.get_all_providers) + if get_providers_ok then vim.tbl_map(function(provider) table.insert(sources, provider.name) end, providers) From 726bf89c3385dc06bbe518e99543acd1ad7751b5 Mon Sep 17 00:00:00 2001 From: Steve Beaulac Date: Sun, 9 Feb 2025 10:58:44 -0500 Subject: [PATCH 05/41] implementing snack picker --- lua/obsidian/config.lua | 1 + lua/obsidian/pickers/_snacks.lua | 118 +++++++++++++++++++++++++++++++ lua/obsidian/pickers/init.lua | 4 +- 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 lua/obsidian/pickers/_snacks.lua diff --git a/lua/obsidian/config.lua b/lua/obsidian/config.lua index 4f3ae84d..1d32f361 100644 --- a/lua/obsidian/config.lua +++ b/lua/obsidian/config.lua @@ -343,6 +343,7 @@ config.Picker = { telescope = "telescope.nvim", fzf_lua = "fzf-lua", mini = "mini.pick", + snacks = "snacks.pick", } ---@class obsidian.config.PickerOpts diff --git a/lua/obsidian/pickers/_snacks.lua b/lua/obsidian/pickers/_snacks.lua new file mode 100644 index 00000000..97b749fd --- /dev/null +++ b/lua/obsidian/pickers/_snacks.lua @@ -0,0 +1,118 @@ +local snacks_picker = require "snacks.picker" +local snacks = require "snacks" + + +local Path = require "obsidian.path" +local abc = require "obsidian.abc" +local Picker = require "obsidian.pickers.picker" + +---@param entry string +---@return string +local function clean_path(entry) + if type(entry) == "string" then + local path_end = assert(string.find(entry, ":", 1, true)) + return string.sub(entry, 1, path_end - 1) + end + vim.notify("entry: " .. table.concat(vim.tbl_keys(entry), ", ")) + return "" +end + +---@class obsidian.pickers.SnacksPicker : obsidian.Picker +local SnacksPicker = abc.new_class({ + ---@diagnostic disable-next-line: unused-local + __tostring = function(self) + return "SnacksPicker()" + end, +}, Picker) + +SnacksPicker.find_files = function(self, opts) + opts = opts and opts or {} + + ---@type obsidian.Path + local dir = opts.dir and Path:new(opts.dir) or self.client.dir + + local result = snacks_picker.pick("files", { + cwd = tostring(dir), + }) + + if result and opts.callback then + local path = clean_path(result) + opts.callback(tostring(dir / path)) + end +end + +SnacksPicker.grep = function(self, opts) + opts = opts and opts or {} + + ---@type obsidian.Path + local dir = opts.dir and Path:new(opts.dir) or self.client.dir + + local result = snacks_picker.pick("grep", { + cwd = tostring(dir), + }) + + if result and opts.callback then + local path = clean_path(result) + opts.callback(tostring(dir / path)) + end +end + +SnacksPicker.pick = function(self, values, opts) + + self.calling_bufnr = vim.api.nvim_get_current_buf() + + local buf = opts.buf or vim.api.nvim_get_current_buf() + + opts = opts and opts or {} + + local entries = {} + for _, value in ipairs(values) do + if type(value) == "string" then + table.insert(entries, { + text = value, + value = value, + }) + elseif value.valid ~= false then + local name = self:_make_display(value) + table.insert(entries, { + text = name, + buf = buf, + filename = value.filename, + value = value.value, + pos = { value.lnum, value.col }, + }) + end + end + + snacks_picker({ + tilte = opts.prompt_title, + items = entries, + layout = { + preview = false + }, + format = function(item, _) + local ret = {} + local a = snacks_picker.util.align + ret[#ret + 1] = { a(item.text, 20) } + return ret + end, + confirm = function(picker, item) + picker:close() + if item then + if opts.callback then + opts.callback(item.value) + elseif item then + vim.schedule(function() + if item["buf"] then + vim.api.nvim_set_current_buf(item["buf"]) + end + vim.api.nvim_win_set_cursor(0, {item["pos"][1], 0}) + end) + end + end + end, + -- sort = require("snacks.picker.sort").idx(), + }) +end + +return SnacksPicker diff --git a/lua/obsidian/pickers/init.lua b/lua/obsidian/pickers/init.lua index 7b2decd3..51d5ad88 100644 --- a/lua/obsidian/pickers/init.lua +++ b/lua/obsidian/pickers/init.lua @@ -13,7 +13,7 @@ M.get = function(client, picker_name) if picker_name then picker_name = string.lower(picker_name) else - for _, name in ipairs { PickerName.telescope, PickerName.fzf_lua, PickerName.mini } do + for _, name in ipairs { PickerName.telescope, PickerName.fzf_lua, PickerName.mini, PickerName.snacks } do local ok, res = pcall(M.get, client, name) if ok then return res @@ -28,6 +28,8 @@ M.get = function(client, picker_name) return require("obsidian.pickers._mini").new(client) elseif picker_name == string.lower(PickerName.fzf_lua) then return require("obsidian.pickers._fzf").new(client) + elseif picker_name == string.lower(PickerName.snacks) then + return require("obsidian.pickers._snacks").new(client) elseif picker_name then error("not implemented for " .. picker_name) end From d6490d5a05b7d5361484d363cf216b634207e839 Mon Sep 17 00:00:00 2001 From: Steve Beaulac Date: Wed, 12 Feb 2025 01:22:00 -0500 Subject: [PATCH 06/41] added documentation --- README.md | 3 ++- doc/obsidian.txt | 3 ++- lua/obsidian/commands/debug.lua | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d5d382b6..29647609 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,7 @@ The only **required** plugin dependency is [plenary.nvim](https://github.com/nvi - **[recommended]** [nvim-telescope/telescope.nvim](https://github.com/nvim-telescope/telescope.nvim): for search and quick-switch functionality. - [Mini.Pick](https://github.com/echasnovski/mini.pick) from the mini.nvim library: an alternative to telescope for search and quick-switch functionality. - [ibhagwan/fzf-lua](https://github.com/ibhagwan/fzf-lua): another alternative to telescope for search and quick-switch functionality. +- [Snacks.Picker](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md) from the snacks.nvim library: an alternative to mini and telescope for search and quick-switch functionality. **Syntax highlighting:** @@ -412,7 +413,7 @@ This is a complete list of all of the options that can be passed to `require("ob open_app_foreground = false, picker = { - -- Set your preferred picker. Can be one of 'telescope.nvim', 'fzf-lua', or 'mini.pick'. + -- Set your preferred picker. Can be one of 'telescope.nvim', 'fzf-lua', 'mini.pick' or 'snacks.pick'. name = "telescope.nvim", -- Optional, configure key mappings for the picker. These are the defaults. -- Not all pickers support all mappings. diff --git a/doc/obsidian.txt b/doc/obsidian.txt index fc053994..9e5c1006 100644 --- a/doc/obsidian.txt +++ b/doc/obsidian.txt @@ -252,6 +252,7 @@ dependencies that enhance the obsidian.nvim experience. - **[recommended]** nvim-telescope/telescope.nvim : for search and quick-switch functionality. - Mini.Pick from the mini.nvim library: an alternative to telescope for search and quick-switch functionality. - ibhagwan/fzf-lua : another alternative to telescope for search and quick-switch functionality. +- Snacks.Pick : another alternative to telescope for search and quick-switch functionality. **Syntax highlighting:** @@ -468,7 +469,7 @@ carefully and customize it to your needs: open_app_foreground = false, picker = { - -- Set your preferred picker. Can be one of 'telescope.nvim', 'fzf-lua', or 'mini.pick'. + -- Set your preferred picker. Can be one of 'telescope.nvim', 'fzf-lua', 'mini.pick' or 'snacks.pick'. name = "telescope.nvim", -- Optional, configure key mappings for the picker. These are the defaults. -- Not all pickers support all mappings. diff --git a/lua/obsidian/commands/debug.lua b/lua/obsidian/commands/debug.lua index 3013bc38..e1d855a6 100644 --- a/lua/obsidian/commands/debug.lua +++ b/lua/obsidian/commands/debug.lua @@ -51,7 +51,7 @@ return function(client, data) end log.lazy_info "Dependencies:" - for _, plugin in ipairs { "plenary.nvim", "nvim-cmp", "telescope.nvim", "fzf-lua", "mini.pick" } do + for _, plugin in ipairs { "plenary.nvim", "nvim-cmp", "telescope.nvim", "fzf-lua", "mini.pick", "snacks.pick" } do local plugin_info = util.get_plugin_info(plugin) if plugin_info ~= nil then log.lazy_info(" ✓ %s: %s", plugin, plugin_info.commit or "unknown") From 1e1c912e5a3932f4e5ccbd300e8d61c84dc13f49 Mon Sep 17 00:00:00 2001 From: Steve Beaulac Date: Thu, 13 Feb 2025 16:33:34 -0500 Subject: [PATCH 07/41] refactor(pickers): improve snacks picker implementation and callback handling feat: add table utility functions for debugging fix: update grep and find_files to properly handle callbacks refactor: streamline picker options and configuration style: improve code organization and consistency Breaking changes: - Modified behavior of grep and find_files to prioritize callbacks --- lua/obsidian/pickers/_snacks.lua | 193 +++++++++++++++++++++---------- 1 file changed, 130 insertions(+), 63 deletions(-) diff --git a/lua/obsidian/pickers/_snacks.lua b/lua/obsidian/pickers/_snacks.lua index 97b749fd..b96eb25a 100644 --- a/lua/obsidian/pickers/_snacks.lua +++ b/lua/obsidian/pickers/_snacks.lua @@ -6,6 +6,45 @@ local Path = require "obsidian.path" local abc = require "obsidian.abc" local Picker = require "obsidian.pickers.picker" + +function print_table(t, indent) + indent = indent or 0 + local padding = string.rep(" ", indent) + + for key, value in pairs(t) do + if type(value) == "table" then + print(padding .. tostring(key) .. " = {") + print_table(value, indent + 1) + print(padding .. "}") + else + print(padding .. tostring(key) .. " = " .. tostring(value)) + end + end +end + +function table_to_string(t, indent) + if type(t) ~= "table" then return tostring(t) end + + indent = indent or 0 + local padding = string.rep(" ", indent) + local parts = {} + + for k, v in pairs(t) do + local key = type(k) == "number" and "[" .. k .. "]" or k + local value + if type(v) == "table" then + value = "{\n" .. table_to_string(v, indent + 1) .. padding .. "}" + elseif type(v) == "string" then + value = string.format("%q", v) + else + value = tostring(v) + end + parts[#parts + 1] = padding .. key .. " = " .. value + end + + return table.concat(parts, ",\n") .. "\n" +end + ---@param entry string ---@return string local function clean_path(entry) @@ -17,6 +56,24 @@ local function clean_path(entry) return "" end +local function map_actions(action) + if type(action) == "table" then + opts = { win = { input = { keys = {} } }, actions = {} }; + for k, v in pairs(action) do + local name = string.gsub(v.desc, " ", "_") + opts.win.input.keys = { + [k] = { name, mode = { "n", "i" }, desc = v.desc } + } + opts.actions[name] = function(picker, item) + vim.notify("action item: " .. table_to_string(item)) + v.callback({args: item.text}) + end + end + return opts + end + return {} +end + ---@class obsidian.pickers.SnacksPicker : obsidian.Picker local SnacksPicker = abc.new_class({ ---@diagnostic disable-next-line: unused-local @@ -26,93 +83,103 @@ local SnacksPicker = abc.new_class({ }, Picker) SnacksPicker.find_files = function(self, opts) - opts = opts and opts or {} + opts = opts or {} ---@type obsidian.Path local dir = opts.dir and Path:new(opts.dir) or self.client.dir - local result = snacks_picker.pick("files", { - cwd = tostring(dir), + local pick_opts = vim.tbl_extend("force", map or {}, { + source = "files", + title = opts.prompt_title, + cwd = opts.dir.filename, + confirm = function(picker, item, action) + picker:close() + if item then + if opts.callback then + opts.callback(item._path) + else + snacks_picker.actions.jump(picker, item, action) + end + end + end, }) - - if result and opts.callback then - local path = clean_path(result) - opts.callback(tostring(dir / path)) - end + snacks_picker.pick(pick_opts) end -SnacksPicker.grep = function(self, opts) - opts = opts and opts or {} +SnacksPicker.grep = function(self, opts, action) + opts = opts or {} ---@type obsidian.Path local dir = opts.dir and Path:new(opts.dir) or self.client.dir - local result = snacks_picker.pick("grep", { - cwd = tostring(dir), + local pick_opts = vim.tbl_extend("force", map or {}, { + source = "grep", + title = opts.prompt_title, + cwd = opts.dir.filename, + confirm = function(picker, item, action) + picker:close() + if item then + if opts.callback then + opts.callback(item._path) + else + snacks_picker.actions.jump(picker, item, action) + end + end + end, }) - - if result and opts.callback then - local path = clean_path(result) - opts.callback(tostring(dir / path)) - end + snacks_picker.pick(pick_opts) end SnacksPicker.pick = function(self, values, opts) - - self.calling_bufnr = vim.api.nvim_get_current_buf() - - local buf = opts.buf or vim.api.nvim_get_current_buf() - - opts = opts and opts or {} - - local entries = {} - for _, value in ipairs(values) do - if type(value) == "string" then - table.insert(entries, { - text = value, - value = value, - }) - elseif value.valid ~= false then - local name = self:_make_display(value) - table.insert(entries, { + self.calling_bufnr = vim.api.nvim_get_current_buf() + + opts = opts or {} + + local buf = opts.buf or vim.api.nvim_get_current_buf() + + -- local map = vim.tbl_deep_extend("force", {}, + -- map_actions(opts.selection_mappings), + -- map_actions(opts.query_mappings)) + + local entries = {} + for _, value in ipairs(values) do + if type(value) == "string" then + table.insert(entries, { + text = value, + value = value, + }) + elseif value.valid ~= false then + local name = self:_make_display(value) + table.insert(entries, { text = name, buf = buf, filename = value.filename, value = value.value, pos = { value.lnum, value.col }, - }) + }) + end end - end - snacks_picker({ - tilte = opts.prompt_title, - items = entries, - layout = { + local pick_opts = vim.tbl_extend("force", map or {}, { + tilte = opts.prompt_title, + items = entries, + layout = { preview = false - }, - format = function(item, _) - local ret = {} - local a = snacks_picker.util.align - ret[#ret + 1] = { a(item.text, 20) } - return ret - end, - confirm = function(picker, item) - picker:close() - if item then - if opts.callback then - opts.callback(item.value) - elseif item then - vim.schedule(function() - if item["buf"] then - vim.api.nvim_set_current_buf(item["buf"]) + }, + format = "text", + confirm = function(picker, item) + picker:close() + if item and opts.callback then + if type(item) == "string" then + opts.callback(item) + else + opts.callback(item.value) end - vim.api.nvim_win_set_cursor(0, {item["pos"][1], 0}) - end) - end - end - end, - -- sort = require("snacks.picker.sort").idx(), - }) + end + end, + }) + + local entry = snacks_picker.pick(pick_opts) end return SnacksPicker From 62c546a6e866427e83c5bbbb9a41588d6a4f8788 Mon Sep 17 00:00:00 2001 From: Steve Beaulac Date: Fri, 14 Feb 2025 11:49:11 -0500 Subject: [PATCH 08/41] refactor(pickers): improve snacks picker implementation and error handling - Remove unused debug helper functions - Add proper type annotations for picker options - Improve action mapping logic with notes_mappings - Fix path handling in file and grep operations - Add debug logging for better troubleshooting - Enhance callback handling for picker actions --- lua/obsidian/pickers/_snacks.lua | 119 +++++++++++++------------------ 1 file changed, 48 insertions(+), 71 deletions(-) diff --git a/lua/obsidian/pickers/_snacks.lua b/lua/obsidian/pickers/_snacks.lua index b96eb25a..76bb1c43 100644 --- a/lua/obsidian/pickers/_snacks.lua +++ b/lua/obsidian/pickers/_snacks.lua @@ -1,72 +1,29 @@ local snacks_picker = require "snacks.picker" -local snacks = require "snacks" - local Path = require "obsidian.path" local abc = require "obsidian.abc" local Picker = require "obsidian.pickers.picker" - -function print_table(t, indent) - indent = indent or 0 - local padding = string.rep(" ", indent) - - for key, value in pairs(t) do - if type(value) == "table" then - print(padding .. tostring(key) .. " = {") - print_table(value, indent + 1) - print(padding .. "}") - else - print(padding .. tostring(key) .. " = " .. tostring(value)) - end - end -end - -function table_to_string(t, indent) - if type(t) ~= "table" then return tostring(t) end - - indent = indent or 0 - local padding = string.rep(" ", indent) - local parts = {} - - for k, v in pairs(t) do - local key = type(k) == "number" and "[" .. k .. "]" or k - local value - if type(v) == "table" then - value = "{\n" .. table_to_string(v, indent + 1) .. padding .. "}" - elseif type(v) == "string" then - value = string.format("%q", v) - else - value = tostring(v) - end - parts[#parts + 1] = padding .. key .. " = " .. value - end - - return table.concat(parts, ",\n") .. "\n" +local function debug_once(msg, ...) +-- vim.notify(msg .. vim.inspect(...)) end ----@param entry string ----@return string -local function clean_path(entry) - if type(entry) == "string" then - local path_end = assert(string.find(entry, ":", 1, true)) - return string.sub(entry, 1, path_end - 1) - end - vim.notify("entry: " .. table.concat(vim.tbl_keys(entry), ", ")) - return "" -end - -local function map_actions(action) - if type(action) == "table" then +---@param mapping table +---@return table +local function notes_mappings(mapping) + if type(mapping) == "table" then opts = { win = { input = { keys = {} } }, actions = {} }; - for k, v in pairs(action) do + for k, v in pairs(mapping) do local name = string.gsub(v.desc, " ", "_") opts.win.input.keys = { [k] = { name, mode = { "n", "i" }, desc = v.desc } } opts.actions[name] = function(picker, item) - vim.notify("action item: " .. table_to_string(item)) - v.callback({args: item.text}) + debug_once("mappings :", item) + picker:close() + vim.schedule(function() + v.callback(item.value or item._path) + end) end end return opts @@ -82,46 +39,60 @@ local SnacksPicker = abc.new_class({ end, }, Picker) +---@param opts obsidian.PickerFindOpts|? Options. SnacksPicker.find_files = function(self, opts) opts = opts or {} ---@type obsidian.Path - local dir = opts.dir and Path:new(opts.dir) or self.client.dir + local dir = opts.dir.filename and Path:new(opts.dir.filename) or self.client.dir + + local map = vim.tbl_deep_extend("force", {}, + notes_mappings(opts.selection_mappings)) local pick_opts = vim.tbl_extend("force", map or {}, { source = "files", title = opts.prompt_title, - cwd = opts.dir.filename, + cwd = tostring(dir), confirm = function(picker, item, action) picker:close() if item then if opts.callback then + debug_once("find files callback: ", item) opts.callback(item._path) else + debug_once("find files jump: ", item) snacks_picker.actions.jump(picker, item, action) end end end, }) - snacks_picker.pick(pick_opts) + local t = snacks_picker.pick(pick_opts) end -SnacksPicker.grep = function(self, opts, action) +---@param opts obsidian.PickerGrepOpts|? Options. +SnacksPicker.grep = function(self, opts) opts = opts or {} + debug_once("grep opts : ", opts) + ---@type obsidian.Path local dir = opts.dir and Path:new(opts.dir) or self.client.dir + local map = vim.tbl_deep_extend("force", {}, + notes_mappings(opts.selection_mappings)) + local pick_opts = vim.tbl_extend("force", map or {}, { source = "grep", title = opts.prompt_title, - cwd = opts.dir.filename, + cwd = dir, confirm = function(picker, item, action) picker:close() if item then if opts.callback then - opts.callback(item._path) + debug_once("grep callback: ", item) + opts.callback(item._path or item.filename) else + debug_once("grep jump: ", item) snacks_picker.actions.jump(picker, item, action) end end @@ -130,16 +101,17 @@ SnacksPicker.grep = function(self, opts, action) snacks_picker.pick(pick_opts) end +---@param values string[]|obsidian.PickerEntry[] +---@param opts obsidian.PickerPickOpts|? Options. +---@diagnostic disable-next-line: unused-local SnacksPicker.pick = function(self, values, opts) self.calling_bufnr = vim.api.nvim_get_current_buf() opts = opts or {} - local buf = opts.buf or vim.api.nvim_get_current_buf() + debug_once("pick opts: ", opts) - -- local map = vim.tbl_deep_extend("force", {}, - -- map_actions(opts.selection_mappings), - -- map_actions(opts.query_mappings)) + local buf = opts.buf or vim.api.nvim_get_current_buf() local entries = {} for _, value in ipairs(values) do @@ -155,11 +127,14 @@ SnacksPicker.pick = function(self, values, opts) buf = buf, filename = value.filename, value = value.value, - pos = { value.lnum, value.col }, + pos = { value.lnum, value.col or 0 }, }) end end + local map = vim.tbl_deep_extend("force", {}, + notes_mappings(opts.selection_mappings)) + local pick_opts = vim.tbl_extend("force", map or {}, { tilte = opts.prompt_title, items = entries, @@ -167,13 +142,15 @@ SnacksPicker.pick = function(self, values, opts) preview = false }, format = "text", - confirm = function(picker, item) + confirm = function(picker, item, action) picker:close() - if item and opts.callback then - if type(item) == "string" then - opts.callback(item) - else + if item then + if opts.callback then + debug_once("pick callback: ", item) opts.callback(item.value) + else + debug_once("pick jump: ", item) + snacks_picker.actions.jump(picker, item, action) end end end, From f7ba7ae51184e3cf83be00364bd5e6cae01ca919 Mon Sep 17 00:00:00 2001 From: Steve Beaulac Date: Fri, 14 Feb 2025 12:38:35 -0500 Subject: [PATCH 09/41] fix: correct Path handling in grep picker - access dir.filename property instead of dir directly - convert Path object to string for cwd option --- lua/obsidian/pickers/_snacks.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/obsidian/pickers/_snacks.lua b/lua/obsidian/pickers/_snacks.lua index 76bb1c43..4bf632bf 100644 --- a/lua/obsidian/pickers/_snacks.lua +++ b/lua/obsidian/pickers/_snacks.lua @@ -76,7 +76,7 @@ SnacksPicker.grep = function(self, opts) debug_once("grep opts : ", opts) ---@type obsidian.Path - local dir = opts.dir and Path:new(opts.dir) or self.client.dir + local dir = opts.dir.filename and Path:new(opts.dir.filename) or self.client.dir local map = vim.tbl_deep_extend("force", {}, notes_mappings(opts.selection_mappings)) @@ -84,7 +84,7 @@ SnacksPicker.grep = function(self, opts) local pick_opts = vim.tbl_extend("force", map or {}, { source = "grep", title = opts.prompt_title, - cwd = dir, + cwd = tostring(dir), confirm = function(picker, item, action) picker:close() if item then From 2fe5fad73a0399e19f4b642b00abf6a24a933ee1 Mon Sep 17 00:00:00 2001 From: guspix Date: Fri, 28 Feb 2025 18:19:29 +0100 Subject: [PATCH 10/41] Changed the name of the linux binary in github actions Neovim 0.10.4 changed the name of the Linux binary in their realeases, which was breaking the github actions worflow. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 11692f71..0cfa36eb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -57,7 +57,7 @@ jobs: include: - os: ubuntu-latest - nvim_url: https://github.com/neovim/neovim/releases/download/nightly/nvim-linux64.tar.gz + nvim_url: https://github.com/neovim/neovim/releases/download/nightly/nvim-linux-x86_64.tar.gz packages: luarocks ripgrep manager: sudo apt-get From ea293e8a26f20ba59b2ccb93cef22741732a24ae Mon Sep 17 00:00:00 2001 From: guspix Date: Fri, 28 Feb 2025 18:26:22 +0100 Subject: [PATCH 11/41] Changed the name of the github binary for the docs action --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0be5a8b0..e1313d5b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,7 +16,7 @@ env: runtime: ~/.local/share/nvim/site/pack/vendor/start minidoc-git: https://github.com/echasnovski/mini.doc minidoc-path: ~/.local/share/nvim/site/pack/vendor/start/mini.doc - nvim_url: https://github.com/neovim/neovim/releases/download/nightly/nvim-linux64.tar.gz + nvim_url: https://github.com/neovim/neovim/releases/download/nightly/nvim-linux-x86_64.tar.gz jobs: docs: From ee7943ea40bbb1dccfb1476a24ae53a5d58c3f3c Mon Sep 17 00:00:00 2001 From: guspix Date: Fri, 28 Feb 2025 18:53:59 +0100 Subject: [PATCH 12/41] Trying to fix github actions when merging to main There is no GH_PAT configured in the secrets for this new repo. Changed the variable to GITHUB_TOKEN to check if it has enough permissions, if it doesn't we will need to create a PAT and add it to the repo secrets. --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e1313d5b..e28452b5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: if: github.event_name != 'pull_request' uses: actions/checkout@v4 with: - token: ${{ secrets.GH_PAT }} + token: ${{ secrets.GITHUB_TOKEN }} - name: Checkout without token if: github.event_name == 'pull_request' From 90bbcafd9839edd411f695934f2dc22594370cc4 Mon Sep 17 00:00:00 2001 From: guspix Date: Fri, 28 Feb 2025 19:20:28 +0100 Subject: [PATCH 13/41] Revert "Trying to fix github actions when merging to main" The GH_PAT was indeed necessary so I created it and added it to the repo's secrets --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e28452b5..e1313d5b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: if: github.event_name != 'pull_request' uses: actions/checkout@v4 with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.GH_PAT }} - name: Checkout without token if: github.event_name == 'pull_request' From dbd6529604f3877c0ec4bd4cbabd684eafcecb03 Mon Sep 17 00:00:00 2001 From: guspix <33852783+guspix@users.noreply.github.com> Date: Fri, 28 Feb 2025 18:23:39 +0000 Subject: [PATCH 14/41] chore(docs): auto generate docs --- doc/obsidian_api.txt | 402 +++++++++++++++++++++---------------------- 1 file changed, 201 insertions(+), 201 deletions(-) diff --git a/doc/obsidian_api.txt b/doc/obsidian_api.txt index 1a288ef6..78297115 100644 --- a/doc/obsidian_api.txt +++ b/doc/obsidian_api.txt @@ -21,9 +21,9 @@ Class ~ {obsidian.SearchOpts} : obsidian.ABC Fields ~ -{sort} `(boolean|?)` -{include_templates} `(boolean|?)` -{ignore_case} `(boolean|?)` +{sort} `(boolean|)`? +{include_templates} `(boolean|)`? +{ignore_case} `(boolean|)`? ------------------------------------------------------------------------------ *obsidian.SearchOpts.from_tbl()* @@ -32,13 +32,13 @@ Parameters ~ {opts} `(obsidian.SearchOpts|table)` Return ~ -obsidian.SearchOpts +`(obsidian.SearchOpts)` ------------------------------------------------------------------------------ *obsidian.SearchOpts.default()* `SearchOpts.default`() Return ~ -obsidian.SearchOpts +`(obsidian.SearchOpts)` ------------------------------------------------------------------------------ *obsidian.Client* @@ -52,13 +52,13 @@ Class ~ {obsidian.Client} : obsidian.ABC Fields ~ -{current_workspace} obsidian.Workspace The current workspace. -{dir} obsidian.Path The root of the vault for the current workspace. -{opts} obsidian.config.ClientOpts The client config. -{buf_dir} obsidian.Path|? The parent directory of the current buffer. -{callback_manager} obsidian.CallbackManager -{log} obsidian.Logger -{_default_opts} obsidian.config.ClientOpts +{current_workspace} `(obsidian.Workspace)` The current workspace. +{dir} `(obsidian.Path)` The root of the vault for the current workspace. +{opts} `(obsidian.config.ClientOpts)` The client config. +{buf_dir} `(obsidian.Path|? The)` parent directory of the current buffer. +{callback_manager} `(obsidian.CallbackManager)` +{log} `(obsidian.Logger)` +{_default_opts} `(obsidian.config.ClientOpts)` {_quiet} `(boolean)` ------------------------------------------------------------------------------ @@ -71,17 +71,17 @@ client through: `require("obsidian").get_client()` Parameters ~ -{opts} obsidian.config.ClientOpts +{opts} `(obsidian.config.ClientOpts)` Return ~ -obsidian.Client +`(obsidian.Client)` ------------------------------------------------------------------------------ *obsidian.Client.set_workspace()* `Client.set_workspace`({self}, {workspace}, {opts}) Parameters ~ -{workspace} obsidian.Workspace -{opts} { lock: `(boolean|?)` }|? +{workspace} `(obsidian.Workspace)` +{opts} `({ lock: boolean|? }|)`? ------------------------------------------------------------------------------ *obsidian.Client.opts_for_workspace()* @@ -89,10 +89,10 @@ Parameters ~ Get the normalize opts for a given workspace. Parameters ~ -{workspace} obsidian.Workspace|? +{workspace} `(obsidian.Workspace|)`? Return ~ -obsidian.config.ClientOpts +`(obsidian.config.ClientOpts)` ------------------------------------------------------------------------------ *obsidian.Client.switch_workspace()* @@ -101,7 +101,7 @@ Switch to a different workspace. Parameters ~ {workspace} `(obsidian.Workspace|string)` The workspace object or the name of an existing workspace. -{opts} { lock: `(boolean|?)` }|? +{opts} `({ lock: boolean|? }|)`? ------------------------------------------------------------------------------ *obsidian.Client.path_is_note()* @@ -110,7 +110,7 @@ Check if a path represents a note in the workspace. Parameters ~ {path} `(string|obsidian.Path)` -{workspace} obsidian.Workspace|? +{workspace} `(obsidian.Workspace|)`? Return ~ `(boolean)` @@ -122,10 +122,10 @@ Get the absolute path to the root of the Obsidian vault for the given workspace current workspace. Parameters ~ -{workspace} obsidian.Workspace|? +{workspace} `(obsidian.Workspace|)`? Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ *obsidian.Client.vault_name()* @@ -142,10 +142,10 @@ Make a path relative to the vault root, if possible. Parameters ~ {path} `(string|obsidian.Path)` -{opts} { strict: `(boolean|?)` }|? +{opts} `({ strict: boolean|? }|)`? Return ~ -obsidian.Path| `(optional)` +`(obsidian.Path| `(optional))`` ------------------------------------------------------------------------------ *obsidian.Client.templates_dir()* @@ -153,10 +153,10 @@ obsidian.Path| `(optional)` Get the templates folder. Parameters ~ -{workspace} obsidian.Workspace|? +{workspace} `(obsidian.Workspace|)`? Return ~ -obsidian.Path| `(optional)` +`(obsidian.Path| `(optional))`` ------------------------------------------------------------------------------ *obsidian.Client.should_save_frontmatter()* @@ -164,7 +164,7 @@ obsidian.Path| `(optional)` Determines whether a note's frontmatter is managed by obsidian.nvim. Parameters ~ -{note} obsidian.Note +{note} `(obsidian.Note)` Return ~ `(boolean)` @@ -179,7 +179,7 @@ Usage ~ Parameters ~ {cmd_name} `(string)` The name of the command. -{cmd_data} `(table|?)` The payload for the command. +{cmd_data} `(table|? The)` payload for the command. ------------------------------------------------------------------------------ *obsidian.Client.search_defaults()* @@ -187,7 +187,7 @@ Parameters ~ Get the default search options. Return ~ -obsidian.SearchOpts +`(obsidian.SearchOpts)` ------------------------------------------------------------------------------ *obsidian.Client.find_notes()* @@ -196,10 +196,10 @@ Find notes matching the given term. Notes are searched based on ID, title, filen Parameters ~ {term} `(string)` The term to search for -{opts} { search: obsidian.SearchOpts|?, notes: obsidian.note.LoadOpts|?, timeout: `(integer|?)` }|? +{opts} `({ search: obsidian.SearchOpts|?, notes: obsidian.note.LoadOpts|?, timeout: integer|? }|)`? Return ~ -obsidian.Note[] +`(obsidian.Note[])` ------------------------------------------------------------------------------ *obsidian.Client.find_notes_async()* @@ -209,7 +209,7 @@ An async version of `find_notes()` that runs the callback with an array of all m Parameters ~ {term} `(string)` The term to search for {callback} `(fun(notes: obsidian.Note[]))` -{opts} { search: obsidian.SearchOpts|?, notes: obsidian.note.LoadOpts|? }|? +{opts} `({ search: obsidian.SearchOpts|?, notes: obsidian.note.LoadOpts|? }|)`? ------------------------------------------------------------------------------ *obsidian.Client.find_files()* @@ -218,10 +218,10 @@ Find non-markdown files in the vault. Parameters ~ {term} `(string)` The search term. -{opts} { search: obsidian.SearchOpts, timeout: `(integer|?)` }|? +{opts} `({ search: obsidian.SearchOpts, timeout: integer|? }|)`? Return ~ -obsidian.Path[] +`(obsidian.Path[])` ------------------------------------------------------------------------------ *obsidian.Client.find_files_async()* @@ -231,7 +231,7 @@ An async version of `find_files`. Parameters ~ {term} `(string)` The search term. {callback} `(fun(paths: obsidian.Path[]))` -{opts} { search: obsidian.SearchOpts }|? +{opts} `({ search: obsidian.SearchOpts }|)`? ------------------------------------------------------------------------------ *obsidian.Client.resolve_note()* @@ -241,10 +241,10 @@ The 'query' can be a path, filename, note ID, alias, title, etc. Parameters ~ {query} `(string)` -{opts} { timeout: `(integer|?,)` notes: obsidian.note.LoadOpts|? }|? +{opts} `({ timeout: integer|?, notes: obsidian.note.LoadOpts|? }|)`? Return ~ -obsidian.Note `(...)` +`(obsidian.Note)` ... ------------------------------------------------------------------------------ *obsidian.Client.resolve_note_async()* @@ -254,10 +254,10 @@ An async version of `resolve_note()`. Parameters ~ {query} `(string)` {callback} `(fun(...: obsidian.Note))` -{opts} { notes: obsidian.note.LoadOpts|? }|? +{opts} `({ notes: obsidian.note.LoadOpts|? }|)`? Return ~ -obsidian.Note| `(optional)` +`(obsidian.Note| `(optional))`` ------------------------------------------------------------------------------ *obsidian.Client.resolve_note_async_with_picker_fallback()* @@ -268,10 +268,10 @@ there are multiple matches. Parameters ~ {query} `(string)` {callback} `(fun(obsidian.Note))` -{opts} { notes: obsidian.note.LoadOpts|?, prompt_title: `(string|?)` }|? +{opts} `({ notes: obsidian.note.LoadOpts|?, prompt_title: string|? }|)`? Return ~ -obsidian.Note| `(optional)` +`(obsidian.Note| `(optional))`` ------------------------------------------------------------------------------ Class ~ @@ -280,14 +280,14 @@ Class ~ Fields ~ {location} `(string)` {name} `(string)` -{link_type} obsidian.search.RefTypes -{path} obsidian.Path|? -{note} obsidian.Note|? -{url} `(string|?)` -{line} `(integer|?)` -{col} `(integer|?)` -{anchor} obsidian.note.HeaderAnchor|? -{block} obsidian.note.Block|? +{link_type} `(obsidian.search.RefTypes)` +{path} `(obsidian.Path|)`? +{note} `(obsidian.Note|)`? +{url} `(string|)`? +{line} `(integer|)`? +{col} `(integer|)`? +{anchor} `(obsidian.note.HeaderAnchor|)`? +{block} `(obsidian.note.Block|)`? ------------------------------------------------------------------------------ *obsidian.Client.resolve_link_async()* @@ -295,7 +295,7 @@ Fields ~ Resolve a link. If the link argument is `nil` we attempt to resolve a link under the cursor. Parameters ~ -{link} `(string|?)` +{link} `(string|)`? {callback} `(fun(...: obsidian.ResolveLinkResult))` ------------------------------------------------------------------------------ @@ -304,8 +304,8 @@ Parameters ~ Follow a link. If the link argument is `nil` we attempt to follow a link under the cursor. Parameters ~ -{link} `(string|?)` -{opts} { open_strategy: obsidian.config.OpenStrategy|? }|? +{link} `(string|)`? +{opts} `({ open_strategy: obsidian.config.OpenStrategy|? }|)`? ------------------------------------------------------------------------------ *obsidian.Client.open_note()* @@ -314,7 +314,7 @@ Open a note in a buffer. Parameters ~ {note_or_path} `(string|obsidian.Path|obsidian.Note)` -{opts} { line: `(integer|?,)` col: integer|?, open_strategy: obsidian.config.OpenStrategy|?, sync: boolean|?, callback: fun(bufnr: integer)|? }|? +{opts} `({ line: integer|?, col: integer|?, open_strategy: obsidian.config.OpenStrategy|?, sync: boolean|?, callback: fun(bufnr: integer)|? }|)`? ------------------------------------------------------------------------------ *obsidian.Client.current_note()* @@ -322,11 +322,11 @@ Parameters ~ Get the current note from a buffer. Parameters ~ -{bufnr} `(integer|?)` -{opts} obsidian.note.LoadOpts|? +{bufnr} `(integer|)`? +{opts} `(obsidian.note.LoadOpts|)`? Return ~ -obsidian.Note| `(optional)` +`(obsidian.Note| `(optional))`` ------------------------------------------------------------------------------ Class ~ @@ -334,12 +334,12 @@ Class ~ Fields ~ {tag} `(string)` The tag found. -{note} obsidian.Note The note instance where the tag was found. +{note} `(obsidian.Note)` The note instance where the tag was found. {path} `(string|obsidian.Path)` The path to the note where the tag was found. {line} `(integer)` The line number (1-indexed) where the tag was found. {text} `(string)` The text (with whitespace stripped) of the line where the tag was found. -{tag_start} `(integer|?)` The index within 'text' where the tag starts. -{tag_end} `(integer|?)` The index within 'text' where the tag ends. +{tag_start} `(integer|? The)` index within 'text' where the tag starts. +{tag_end} `(integer|? The)` index within 'text' where the tag ends. ------------------------------------------------------------------------------ *obsidian.Client.find_tags()* @@ -348,10 +348,10 @@ Find all tags starting with the given search term(s). Parameters ~ {term} `(string|string[])` The search term. -{opts} { search: obsidian.SearchOpts|?, timeout: `(integer|?)` }|? +{opts} `({ search: obsidian.SearchOpts|?, timeout: integer|? }|)`? Return ~ -obsidian.TagLocation[] +`(obsidian.TagLocation[])` ------------------------------------------------------------------------------ *obsidian.Client.find_tags_async()* @@ -361,16 +361,16 @@ An async version of 'find_tags()'. Parameters ~ {term} `(string|string[])` The search term. {callback} `(fun(tags: obsidian.TagLocation[]))` -{opts} { search: obsidian.SearchOpts }|? +{opts} `({ search: obsidian.SearchOpts }|)`? ------------------------------------------------------------------------------ Class ~ {obsidian.BacklinkMatches} Fields ~ -{note} obsidian.Note The note instance where the backlinks were found. +{note} `(obsidian.Note)` The note instance where the backlinks were found. {path} `(string|obsidian.Path)` The path to the note where the backlinks were found. -{matches} obsidian.BacklinkMatch[] The backlinks within the note. +{matches} `(obsidian.BacklinkMatch[])` The backlinks within the note. ------------------------------------------------------------------------------ Class ~ @@ -386,11 +386,11 @@ Fields ~ Find all backlinks to a note. Parameters ~ -{note} obsidian.Note The note to find backlinks for. -{opts} { search: obsidian.SearchOpts|?, timeout: `(integer|?,)` anchor: string|?, block: string|? }|? +{note} `(obsidian.Note)` The note to find backlinks for. +{opts} `({ search: obsidian.SearchOpts|?, timeout: integer|?, anchor: string|?, block: string|? }|)`? Return ~ -obsidian.BacklinkMatches[] +`(obsidian.BacklinkMatches[])` ------------------------------------------------------------------------------ *obsidian.Client.find_backlinks_async()* @@ -398,9 +398,9 @@ obsidian.BacklinkMatches[] An async version of 'find_backlinks()'. Parameters ~ -{note} obsidian.Note The note to find backlinks for. +{note} `(obsidian.Note)` The note to find backlinks for. {callback} `(fun(backlinks: obsidian.BacklinkMatches[]))` -{opts} { search: obsidian.SearchOpts, anchor: `(string|?,)` block: string|? }|? +{opts} `({ search: obsidian.SearchOpts, anchor: string|?, block: string|? }|)`? ------------------------------------------------------------------------------ *obsidian.Client.list_tags()* @@ -409,8 +409,8 @@ Gather a list of all tags in the vault. If 'term' is provided, only tags that pa term will be included. Parameters ~ -{term} `(string|?)` An optional search term to match tags -{timeout} `(integer|?)` Timeout in milliseconds +{term} `(string|? An)` optional search term to match tags +{timeout} `(integer|? Timeout)` in milliseconds Return ~ `(string[])` @@ -421,7 +421,7 @@ Return ~ An async version of 'list_tags()'. Parameters ~ -{term} `(string|?)` +{term} `(string|)`? {callback} `(fun(tags: string[]))` ------------------------------------------------------------------------------ @@ -431,7 +431,7 @@ Apply a function over all notes in the current vault. Parameters ~ {on_note} `(fun(note: obsidian.Note))` -{opts} { on_done: `(fun()|?,)` timeout: integer|?, pattern: string|? }|? +{opts} `({ on_done: fun()|?, timeout: integer|?, pattern: string|? }|)`? Options: - `on_done`: A function to call when all notes have been processed. @@ -445,7 +445,7 @@ Like apply, but the callback takes a path instead of a note instance. Parameters ~ {on_path} `(fun(path: string))` -{opts} { on_done: `(fun()|?,)` timeout: integer|?, pattern: string|? }|? +{opts} `({ on_done: fun()|?, timeout: integer|?, pattern: string|? }|)`? Options: - `on_done`: A function to call when all paths have been processed. @@ -459,7 +459,7 @@ Generate a unique ID for a new note. This respects the user's `note_id_func` if otherwise falls back to generated a Zettelkasten style ID. Parameters ~ -{title} `(string|?)` +{title} `(string|)`? Return ~ `(string)` @@ -472,10 +472,10 @@ This respects the user's `note_path_func` if configured, otherwise essentially f `spec.dir / (spec.id .. ".md")`. Parameters ~ -{spec} { id: `(string,)` dir: obsidian.Path, title: string|? } +{spec} `({ id: string, dir: obsidian.Path, title: string|? })` Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ *obsidian.Client.parse_title_id_path()* @@ -483,12 +483,12 @@ obsidian.Path Parse the title, ID, and path for a new note. Parameters ~ -{title} `(string|?)` -{id} `(string|?)` -{dir} `(string|obsidian.Path|?)` +{title} `(string|)`? +{id} `(string|)`? +{dir} `(string|obsidian.Path|)`? Return ~ -`(string|)` `(optional)`,string,obsidian.Path +`(string| `(optional))``,string,obsidian.Path ------------------------------------------------------------------------------ *obsidian.Client.new_note()* @@ -497,26 +497,26 @@ Create and save a new note. Deprecated: prefer `Client:create_note()` instead. Parameters ~ -{title} `(string|?)` The title for the note. -{id} `(string|?)` An optional ID for the note. If not provided one will be generated. -{dir} `(string|obsidian.Path|?)` An optional directory to place the note. If this is a relative path it will be interpreted relative the workspace / vault root. -{aliases} `(string[]|?)` Additional aliases to assign to the note. +{title} `(string|? The)` title for the note. +{id} `(string|? An)` optional ID for the note. If not provided one will be generated. +{dir} `(string|obsidian.Path|? An)` optional directory to place the note. If this is a relative path it will be interpreted relative the workspace / vault root. +{aliases} `(string[]|? Additional)` aliases to assign to the note. Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ Class ~ {obsidian.CreateNoteOpts} Fields ~ -{title} `(string|?)` -{id} `(string|?)` -{dir} `(string|obsidian.Path|?)` -{aliases} `(string[]|?)` -{tags} `(string[]|?)` -{no_write} `(boolean|?)` -{template} `(string|?)` +{title} `(string|)`? +{id} `(string|)`? +{dir} `(string|obsidian.Path|)`? +{aliases} `(string[]|)`? +{tags} `(string[]|)`? +{no_write} `(boolean|)`? +{template} `(string|)`? ------------------------------------------------------------------------------ *obsidian.Client.create_note()* @@ -524,7 +524,7 @@ Fields ~ Create a new note with the following options. Parameters ~ -{opts} obsidian.CreateNoteOpts|? Options. +{opts} `(obsidian.CreateNoteOpts|? Options.)` Options: - `title`: A title to assign the note. @@ -538,7 +538,7 @@ Options: - `template`: The name of a template to apply when writing the note to disk. Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ *obsidian.Client.write_note()* @@ -546,8 +546,8 @@ obsidian.Note Write the note to disk. Parameters ~ -{note} obsidian.Note -{opts} { path: `(string|obsidian.Path,)` template: string|?, update_content: (fun(lines: string[]): string[])|? }|? Options. +{note} `(obsidian.Note)` +{opts} `({ path: string|obsidian.Path, template: string|?, update_content: (fun(lines: string[]): string[])|? }|? Options.)` Options: - `path`: Override the path to write to. @@ -557,7 +557,7 @@ Options: actually be written (again excluding frontmatter). Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ *obsidian.Client.write_note_to_buffer()* @@ -565,8 +565,8 @@ obsidian.Note Write the note to a buffer. Parameters ~ -{note} obsidian.Note -{opts} { bufnr: `(integer|?,)` template: string|? }|? Options. +{note} `(obsidian.Note)` +{opts} `({ bufnr: integer|?, template: string|? }|? Options.)` Options: - `bufnr`: Override the buffer to write to. Defaults to current buffer. @@ -581,8 +581,8 @@ Return ~ Update the frontmatter in a buffer for the note. Parameters ~ -{note} obsidian.Note -{bufnr} `(integer|?)` +{note} `(obsidian.Note)` +{bufnr} `(integer|)`? Return ~ `(boolean)` updated If the the frontmatter was updated. @@ -593,10 +593,10 @@ Return ~ Get the path to a daily note. Parameters ~ -{datetime} `(integer|?)` +{datetime} `(integer|)`? Return ~ -obsidian.Path, `(string)` (Path, ID) The path and ID of the note. +`(obsidian.Path)`, string (Path, ID) The path and ID of the note. ------------------------------------------------------------------------------ *obsidian.Client.today()* @@ -604,7 +604,7 @@ obsidian.Path, `(string)` (Path, ID) The path and ID of the note. Open (or create) the daily note for today. Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ *obsidian.Client.yesterday()* @@ -612,7 +612,7 @@ obsidian.Note Open (or create) the daily note from the last weekday. Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ *obsidian.Client.tomorrow()* @@ -620,7 +620,7 @@ obsidian.Note Open (or create) the daily note for the next weekday. Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ *obsidian.Client.daily()* @@ -628,11 +628,11 @@ obsidian.Note Open (or create) the daily note for today + `offset_days`. Parameters ~ -{offset_days} `(integer|?)` -{opts} { no_write: `(boolean|?,)` load: obsidian.note.LoadOpts|? }|? +{offset_days} `(integer|)`? +{opts} `({ no_write: boolean|?, load: obsidian.note.LoadOpts|? }|)`? Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ *obsidian.Client.update_ui()* @@ -640,7 +640,7 @@ obsidian.Note Manually update extmarks in a buffer. Parameters ~ -{bufnr} `(integer|?)` +{bufnr} `(integer|)`? ------------------------------------------------------------------------------ *obsidian.Client.format_link()* @@ -649,7 +649,7 @@ Create a formatted markdown / wiki link for a note. Parameters ~ {note} `(obsidian.Note|obsidian.Path|string)` The note/path to link to. -{opts} { label: `(string|?,)` link_style: obsidian.config.LinkStyle|?, id: string|integer|?, anchor: obsidian.note.HeaderAnchor|?, block: obsidian.note.Block|? }|? Options. +{opts} `({ label: string|?, link_style: obsidian.config.LinkStyle|?, id: string|integer|?, anchor: obsidian.note.HeaderAnchor|?, block: obsidian.note.Block|? }|? Options.)` Return ~ `(string)` @@ -660,10 +660,10 @@ Return ~ Get the Picker. Parameters ~ -{picker_name} obsidian.config.Picker|? +{picker_name} `(obsidian.config.Picker|)`? Return ~ -obsidian.Picker| `(optional)` +`(obsidian.Picker| `(optional))`` ------------------------------------------------------------------------------ Class ~ @@ -674,7 +674,7 @@ Fields ~ {header} `(string)` {level} `(integer)` {line} `(integer)` -{parent} obsidian.note.HeaderAnchor|? +{parent} `(obsidian.note.HeaderAnchor|)`? ------------------------------------------------------------------------------ Class ~ @@ -696,17 +696,17 @@ Class ~ Fields ~ {id} `(string|integer)` {aliases} `(string[])` -{title} `(string|?)` +{title} `(string|)`? {tags} `(string[])` -{path} obsidian.Path|? -{metadata} `(table|?)` -{has_frontmatter} `(boolean|?)` -{frontmatter_end_line} `(integer|?)` -{contents} `(string[]|?)` -{anchor_links} `(table|?)` +{path} `(obsidian.Path|)`? +{metadata} `(table|)`? +{has_frontmatter} `(boolean|)`? +{frontmatter_end_line} `(integer|)`? +{contents} `(string[]|)`? +{anchor_links} `(table|)`? {blocks} `(table?)` -{alt_alias} `(string|?)` -{bufnr} `(integer|?)` +{alt_alias} `(string|)`? +{bufnr} `(integer|)`? ------------------------------------------------------------------------------ *obsidian.Note.new()* @@ -719,10 +719,10 @@ Parameters ~ {id} `(string|number)` {aliases} `(string[])` {tags} `(string[])` -{path} `(string|obsidian.Path|?)` +{path} `(string|obsidian.Path|)`? Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ *obsidian.Note.display_info()* @@ -730,7 +730,7 @@ obsidian.Note Get markdown display info about the note. Parameters ~ -{opts} { label: `(string|?,)` anchor: obsidian.note.HeaderAnchor|?, block: obsidian.note.Block|? }|? +{opts} `({ label: string|?, anchor: obsidian.note.HeaderAnchor|?, block: obsidian.note.Block|? }|)`? Return ~ `(string)` @@ -749,7 +749,7 @@ Return ~ Get the filename associated with the note. Return ~ -`(string|)` `(optional)` +`(string| `(optional))`` ------------------------------------------------------------------------------ *obsidian.Note.reference_ids()* @@ -757,7 +757,7 @@ Return ~ Get a list of all of the different string that can identify this note via references, including the ID, aliases, and filename. Parameters ~ -{opts} { lowercase: `(boolean|?)` }|? +{opts} `({ lowercase: boolean|? }|)`? Return ~ `(string[])` @@ -829,10 +829,10 @@ Return ~ Class ~ {obsidian.note.LoadOpts} Fields ~ -{max_lines} `(integer|?)` -{load_contents} `(boolean|?)` -{collect_anchor_links} `(boolean|?)` -{collect_blocks} `(boolean|?)` +{max_lines} `(integer|)`? +{load_contents} `(boolean|)`? +{collect_anchor_links} `(boolean|)`? +{collect_blocks} `(boolean|)`? ------------------------------------------------------------------------------ *obsidian.Note.from_file()* @@ -841,10 +841,10 @@ Initialize a note from a file. Parameters ~ {path} `(string|obsidian.Path)` -{opts} obsidian.note.LoadOpts|? +{opts} `(obsidian.note.LoadOpts|)`? Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ *obsidian.Note.from_file_async()* @@ -853,10 +853,10 @@ An async version of `.from_file()`, i.e. it needs to be called in an async conte Parameters ~ {path} `(string|obsidian.Path)` -{opts} obsidian.note.LoadOpts|? +{opts} `(obsidian.note.LoadOpts|)`? Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ *obsidian.Note.from_file_with_contents_async()* @@ -865,10 +865,10 @@ Like `.from_file_async()` but also returns the contents of the file as a list of Parameters ~ {path} `(string|obsidian.Path)` -{opts} obsidian.note.LoadOpts|? +{opts} `(obsidian.note.LoadOpts|)`? Return ~ -`(obsidian.Note,string[])` +`(obsidian.Note)`,string[] ------------------------------------------------------------------------------ *obsidian.Note.from_buffer()* @@ -876,11 +876,11 @@ Return ~ Initialize a note from a buffer. Parameters ~ -{bufnr} `(integer|?)` -{opts} obsidian.note.LoadOpts|? +{bufnr} `(integer|)`? +{opts} `(obsidian.note.LoadOpts|)`? Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ *obsidian.Note.display_name()* @@ -898,10 +898,10 @@ Initialize a note from an iterator of lines. Parameters ~ {lines} `(fun(): string|?)` {path} `(string|obsidian.Path)` -{opts} obsidian.note.LoadOpts|? +{opts} `(obsidian.note.LoadOpts|)`? Return ~ -obsidian.Note +`(obsidian.Note)` ------------------------------------------------------------------------------ *obsidian.Note.frontmatter()* @@ -917,8 +917,8 @@ Return ~ Get frontmatter lines that can be written to a buffer. Parameters ~ -{eol} `(boolean|?)` -{frontmatter} `(table|?)` +{eol} `(boolean|)`? +{frontmatter} `(table|)`? Return ~ `(string[])` @@ -931,7 +931,7 @@ In general this only updates the frontmatter and header, leaving the rest of the unless you use the `update_content()` callback. Parameters ~ -{opts} { path: `(string|obsidian.Path|?,)` insert_frontmatter: boolean|?, frontmatter: table|?, update_content: (fun(lines: string[]): string[])|? }|? Options. +{opts} `({ path: string|obsidian.Path|?, insert_frontmatter: boolean|?, frontmatter: table|?, update_content: (fun(lines: string[]): string[])|? }|? Options.)` Options: - `path`: Specify a path to save to. Defaults to `self.path`. @@ -947,7 +947,7 @@ Options: Save frontmatter to the given buffer. Parameters ~ -{opts} { bufnr: `(integer|?,)` insert_frontmatter: boolean|?, frontmatter: table|? }|? Options. +{opts} `({ bufnr: integer|?, insert_frontmatter: boolean|?, frontmatter: table|? }|? Options.)` Return ~ `(boolean)` updated True if the buffer lines were updated, false otherwise. @@ -960,7 +960,7 @@ Try to resolve an anchor link to a line number in the note's file. Parameters ~ {anchor_link} `(string)` Return ~ -obsidian.note.HeaderAnchor| `(optional)` +`(obsidian.note.HeaderAnchor| `(optional))`` ------------------------------------------------------------------------------ *obsidian.Note.resolve_block()* @@ -971,26 +971,26 @@ Parameters ~ {block_id} `(string)` Return ~ -obsidian.note.Block| `(optional)` +`(obsidian.note.Block| `(optional))`` ------------------------------------------------------------------------------ Class ~ {obsidian.workspace.WorkspaceSpec} Fields ~ -{path} `(string|obsidian.Path|(fun():)` string|obsidian.Path) -{name} `(string|?)` -{strict} `(boolean|?)` If true, the workspace root will be fixed to 'path' instead of the vault root (if different). -{overrides} `(table|obsidian.config.ClientOpts|?)` +{path} `(string|obsidian.Path|(fun(): string|obsidian.Path))` +{name} `(string|)`? +{strict} `(boolean|? If)` true, the workspace root will be fixed to 'path' instead of the vault root (if different). +{overrides} `(table|obsidian.config.ClientOpts|)`? ------------------------------------------------------------------------------ Class ~ {obsidian.workspace.WorkspaceOpts} Fields ~ -{name} `(string|?)` -{strict} `(boolean|?)` If true, the workspace root will be fixed to 'path' instead of the vault root (if different). -{overrides} `(table|obsidian.config.ClientOpts|?)` +{name} `(string|)`? +{strict} `(boolean|? If)` true, the workspace root will be fixed to 'path' instead of the vault root (if different). +{overrides} `(table|obsidian.config.ClientOpts|)`? ------------------------------------------------------------------------------ *obsidian.Workspace* @@ -1006,10 +1006,10 @@ Class ~ Fields ~ {name} `(string)` An arbitrary name for the workspace. -{path} obsidian.Path The normalized path to the workspace. -{root} obsidian.Path The normalized path to the vault root of the workspace. This usually matches 'path'. -{overrides} `(table|obsidian.config.ClientOpts|?)` -{locked} `(boolean|?)` +{path} `(obsidian.Path)` The normalized path to the workspace. +{root} `(obsidian.Path)` The normalized path to the vault root of the workspace. This usually matches 'path'. +{overrides} `(table|obsidian.config.ClientOpts|)`? +{locked} `(boolean|)`? ------------------------------------------------------------------------------ *obsidian.find_vault_root()* @@ -1023,7 +1023,7 @@ Parameters ~ {base_dir} `(string|obsidian.Path)` Return ~ -obsidian.Path| `(optional)` +`(obsidian.Path| `(optional))`` ------------------------------------------------------------------------------ *obsidian.Workspace.new()* @@ -1032,10 +1032,10 @@ Create a new 'Workspace' object. This assumes the workspace already exists on th Parameters ~ {path} `(string|obsidian.Path)` Workspace path. -{opts} obsidian.workspace.WorkspaceOpts|? +{opts} `(obsidian.workspace.WorkspaceOpts|)`? Return ~ -obsidian.Workspace +`(obsidian.Workspace)` ------------------------------------------------------------------------------ *obsidian.Workspace.new_from_spec()* @@ -1043,10 +1043,10 @@ obsidian.Workspace Initialize a new 'Workspace' object from a workspace spec. Parameters ~ -{spec} obsidian.workspace.WorkspaceSpec +{spec} `(obsidian.workspace.WorkspaceSpec)` Return ~ -obsidian.Workspace +`(obsidian.Workspace)` ------------------------------------------------------------------------------ *obsidian.Workspace.new_from_cwd()* @@ -1054,10 +1054,10 @@ obsidian.Workspace Initialize a 'Workspace' object from the current working directory. Parameters ~ -{opts} obsidian.workspace.WorkspaceOpts|? +{opts} `(obsidian.workspace.WorkspaceOpts|)`? Return ~ -obsidian.Workspace +`(obsidian.Workspace)` ------------------------------------------------------------------------------ *obsidian.Workspace.new_from_buf()* @@ -1065,11 +1065,11 @@ obsidian.Workspace Initialize a 'Workspace' object from the parent directory of the current buffer. Parameters ~ -{bufnr} `(integer|?)` -{opts} obsidian.workspace.WorkspaceOpts|? +{bufnr} `(integer|)`? +{opts} `(obsidian.workspace.WorkspaceOpts|)`? Return ~ -obsidian.Workspace +`(obsidian.Workspace)` ------------------------------------------------------------------------------ *obsidian.Workspace.lock()* @@ -1089,10 +1089,10 @@ is one. Parameters ~ {cur_dir} `(string|obsidian.Path)` -{workspaces} obsidian.workspace.WorkspaceSpec[] +{workspaces} `(obsidian.workspace.WorkspaceSpec[])` Return ~ -obsidian.Workspace| `(optional)` +`(obsidian.Workspace| `(optional))`` ------------------------------------------------------------------------------ *obsidian.Workspace.get_workspace_for_cwd()* @@ -1101,10 +1101,10 @@ Get the workspace corresponding to the current working directory (or a parent of is one. Parameters ~ -{workspaces} obsidian.workspace.WorkspaceSpec[] +{workspaces} `(obsidian.workspace.WorkspaceSpec[])` Return ~ -obsidian.Workspace| `(optional)` +`(obsidian.Workspace| `(optional))`` ------------------------------------------------------------------------------ *obsidian.Workspace.get_default_workspace()* @@ -1112,7 +1112,7 @@ obsidian.Workspace| `(optional)` Returns the default workspace. Parameters ~ -{workspaces} obsidian.workspace.WorkspaceSpec[] +{workspaces} `(obsidian.workspace.WorkspaceSpec[])` Return ~ `(obsidian.Workspace|nil)` @@ -1123,10 +1123,10 @@ Return ~ Resolves current workspace from the client config. Parameters ~ -{opts} obsidian.config.ClientOpts +{opts} `(obsidian.config.ClientOpts)` Return ~ -obsidian.Workspace| `(optional)` +`(obsidian.Workspace| `(optional))`` ------------------------------------------------------------------------------ *obsidian.cached_get()* @@ -1147,10 +1147,10 @@ Class ~ Fields ~ {filename} `(string)` The underlying filename as a string. -{name} `(string|?)` The final path component, if any. -{suffix} `(string|?)` The final extension of the path, if any. +{name} `(string|? The)` final path component, if any. +{suffix} `(string|? The)` final extension of the path, if any. {suffixes} `(string[])` A list of all of the path's extensions. -{stem} `(string|?)` The final path component, without its suffix. +{stem} `(string|? The)` final path component, without its suffix. ------------------------------------------------------------------------------ *obsidian.Path.is_path_obj()* @@ -1176,7 +1176,7 @@ Parameters ~ {...} `(string|obsidian.Path)` Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ *obsidian.Path.temp()* @@ -1184,10 +1184,10 @@ obsidian.Path Get a temporary path with a unique name. Parameters ~ -{opts} { suffix: `(string|?)` }|? +{opts} `({ suffix: string|? }|)`? Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ *obsidian.Path.cwd()* @@ -1195,7 +1195,7 @@ obsidian.Path Get a path corresponding to the current working directory as given by `vim.loop.cwd()`. Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ *obsidian.Path.buffer()* @@ -1203,10 +1203,10 @@ obsidian.Path Get a path corresponding to a buffer. Parameters ~ -{bufnr} `(integer|?)` The buffer number or `0` / `nil` for the current buffer. +{bufnr} `(integer|? The)` buffer number or `0` / `nil` for the current buffer. Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ *obsidian.Path.buf_dir()* @@ -1214,10 +1214,10 @@ obsidian.Path Get a path corresponding to the parent of a buffer. Parameters ~ -{bufnr} `(integer|?)` The buffer number or `0` / `nil` for the current buffer. +{bufnr} `(integer|? The)` buffer number or `0` / `nil` for the current buffer. Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ @@ -1232,7 +1232,7 @@ Parameters ~ {suffix} `(string)` Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ *obsidian.Path.is_absolute()* @@ -1248,7 +1248,7 @@ Return ~ Parameters ~ {...} `(obsidian.Path|string)` Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ *obsidian.Path.relative_to()* @@ -1260,7 +1260,7 @@ Parameters ~ {other} `(obsidian.Path|string)` Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ *obsidian.Path.parent()* @@ -1268,7 +1268,7 @@ obsidian.Path The logical parent of the path. Return ~ -obsidian.Path| `(optional)` +`(obsidian.Path| `(optional))`` ------------------------------------------------------------------------------ *obsidian.Path.parents()* @@ -1276,7 +1276,7 @@ obsidian.Path| `(optional)` Get a list of the parent directories. Return ~ -obsidian.Path[] +`(obsidian.Path[])` ------------------------------------------------------------------------------ *obsidian.Path.is_parent_of()* @@ -1302,10 +1302,10 @@ Make the path absolute, resolving any symlinks. If `strict` is true and the path doesn't exist, an error is raised. Parameters ~ -{opts} { strict: `(boolean)` }|? +{opts} `({ strict: boolean }|)`? Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ *obsidian.Path.stat()* @@ -1313,7 +1313,7 @@ obsidian.Path Get OS stat results. Return ~ -`(table|)` `(optional)` +`(table| `(optional))`` ------------------------------------------------------------------------------ *obsidian.Path.exists()* @@ -1345,7 +1345,7 @@ Return ~ Create a new directory at the given path. Parameters ~ -{opts} { mode: `(integer|?,)` parents: boolean|?, exist_ok: boolean|? }|? +{opts} `({ mode: integer|?, parents: boolean|?, exist_ok: boolean|? }|)`? ------------------------------------------------------------------------------ *obsidian.Path.rmdir()* @@ -1363,7 +1363,7 @@ Recursively remove an entire directory and its contents. Create a file at this given path. Parameters ~ -{opts} { mode: `(integer|?,)` exist_ok: boolean|? }|? +{opts} `({ mode: integer|?, exist_ok: boolean|? }|)`? ------------------------------------------------------------------------------ *obsidian.Path.rename()* @@ -1374,7 +1374,7 @@ Parameters ~ {target} `(obsidian.Path|string)` Return ~ -obsidian.Path +`(obsidian.Path)` ------------------------------------------------------------------------------ *obsidian.Path.unlink()* @@ -1382,6 +1382,6 @@ obsidian.Path Remove the file. Parameters ~ -{opts} { missing_ok: `(boolean|?)` }|? +{opts} `({ missing_ok: boolean|? }|)`? vim:tw=78:ts=8:noet:ft=help:norl: \ No newline at end of file From abe7987d81ca264c24bb6ffac2c64b929271e897 Mon Sep 17 00:00:00 2001 From: guspix Date: Sat, 1 Mar 2025 22:33:14 +0100 Subject: [PATCH 15/41] Fixing linting, formatting and CHANGELOG for github actions --- CHANGELOG.md | 1 + lua/obsidian/pickers/_snacks.lua | 249 +++++++++++++++---------------- 2 files changed, 124 insertions(+), 126 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b585ed0e..ae1cf8cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `opts.follow_img_func` option for customizing how to handle image paths. - Added better handling for undefined template fields, which will now be prompted for. +- Added support for the [`snacks.picker`](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md) picker ### Changed diff --git a/lua/obsidian/pickers/_snacks.lua b/lua/obsidian/pickers/_snacks.lua index 4bf632bf..d01e42a0 100644 --- a/lua/obsidian/pickers/_snacks.lua +++ b/lua/obsidian/pickers/_snacks.lua @@ -5,158 +5,155 @@ local abc = require "obsidian.abc" local Picker = require "obsidian.pickers.picker" local function debug_once(msg, ...) --- vim.notify(msg .. vim.inspect(...)) + -- vim.notify(msg .. vim.inspect(...)) end ---@param mapping table ---@return table local function notes_mappings(mapping) - if type(mapping) == "table" then - opts = { win = { input = { keys = {} } }, actions = {} }; - for k, v in pairs(mapping) do - local name = string.gsub(v.desc, " ", "_") - opts.win.input.keys = { - [k] = { name, mode = { "n", "i" }, desc = v.desc } - } - opts.actions[name] = function(picker, item) - debug_once("mappings :", item) - picker:close() - vim.schedule(function() - v.callback(item.value or item._path) - end) - end - end - return opts + if type(mapping) == "table" then + local opts = { win = { input = { keys = {} } }, actions = {} } + for k, v in pairs(mapping) do + local name = string.gsub(v.desc, " ", "_") + opts.win.input.keys = { + [k] = { name, mode = { "n", "i" }, desc = v.desc }, + } + opts.actions[name] = function(picker, item) + debug_once("mappings :", item) + picker:close() + vim.schedule(function() + v.callback(item.value or item._path) + end) + end end - return {} + return opts + end + return {} end ---@class obsidian.pickers.SnacksPicker : obsidian.Picker local SnacksPicker = abc.new_class({ - ---@diagnostic disable-next-line: unused-local - __tostring = function(self) - return "SnacksPicker()" - end, + ---@diagnostic disable-next-line: unused-local + __tostring = function(self) + return "SnacksPicker()" + end, }, Picker) ---@param opts obsidian.PickerFindOpts|? Options. SnacksPicker.find_files = function(self, opts) - opts = opts or {} - - ---@type obsidian.Path - local dir = opts.dir.filename and Path:new(opts.dir.filename) or self.client.dir - - local map = vim.tbl_deep_extend("force", {}, - notes_mappings(opts.selection_mappings)) - - local pick_opts = vim.tbl_extend("force", map or {}, { - source = "files", - title = opts.prompt_title, - cwd = tostring(dir), - confirm = function(picker, item, action) - picker:close() - if item then - if opts.callback then - debug_once("find files callback: ", item) - opts.callback(item._path) - else - debug_once("find files jump: ", item) - snacks_picker.actions.jump(picker, item, action) - end - end - end, - }) - local t = snacks_picker.pick(pick_opts) + opts = opts or {} + + ---@type obsidian.Path + local dir = opts.dir.filename and Path:new(opts.dir.filename) or self.client.dir + + local map = vim.tbl_deep_extend("force", {}, notes_mappings(opts.selection_mappings)) + + local pick_opts = vim.tbl_extend("force", map or {}, { + source = "files", + title = opts.prompt_title, + cwd = tostring(dir), + confirm = function(picker, item, action) + picker:close() + if item then + if opts.callback then + debug_once("find files callback: ", item) + opts.callback(item._path) + else + debug_once("find files jump: ", item) + snacks_picker.actions.jump(picker, item, action) + end + end + end, + }) + snacks_picker.pick(pick_opts) end ---@param opts obsidian.PickerGrepOpts|? Options. SnacksPicker.grep = function(self, opts) - opts = opts or {} - - debug_once("grep opts : ", opts) - - ---@type obsidian.Path - local dir = opts.dir.filename and Path:new(opts.dir.filename) or self.client.dir - - local map = vim.tbl_deep_extend("force", {}, - notes_mappings(opts.selection_mappings)) - - local pick_opts = vim.tbl_extend("force", map or {}, { - source = "grep", - title = opts.prompt_title, - cwd = tostring(dir), - confirm = function(picker, item, action) - picker:close() - if item then - if opts.callback then - debug_once("grep callback: ", item) - opts.callback(item._path or item.filename) - else - debug_once("grep jump: ", item) - snacks_picker.actions.jump(picker, item, action) - end - end - end, - }) - snacks_picker.pick(pick_opts) + opts = opts or {} + + debug_once("grep opts : ", opts) + + ---@type obsidian.Path + local dir = opts.dir.filename and Path:new(opts.dir.filename) or self.client.dir + + local map = vim.tbl_deep_extend("force", {}, notes_mappings(opts.selection_mappings)) + + local pick_opts = vim.tbl_extend("force", map or {}, { + source = "grep", + title = opts.prompt_title, + cwd = tostring(dir), + confirm = function(picker, item, action) + picker:close() + if item then + if opts.callback then + debug_once("grep callback: ", item) + opts.callback(item._path or item.filename) + else + debug_once("grep jump: ", item) + snacks_picker.actions.jump(picker, item, action) + end + end + end, + }) + snacks_picker.pick(pick_opts) end ---@param values string[]|obsidian.PickerEntry[] ---@param opts obsidian.PickerPickOpts|? Options. ---@diagnostic disable-next-line: unused-local SnacksPicker.pick = function(self, values, opts) - self.calling_bufnr = vim.api.nvim_get_current_buf() - - opts = opts or {} - - debug_once("pick opts: ", opts) - - local buf = opts.buf or vim.api.nvim_get_current_buf() - - local entries = {} - for _, value in ipairs(values) do - if type(value) == "string" then - table.insert(entries, { - text = value, - value = value, - }) - elseif value.valid ~= false then - local name = self:_make_display(value) - table.insert(entries, { - text = name, - buf = buf, - filename = value.filename, - value = value.value, - pos = { value.lnum, value.col or 0 }, - }) - end + self.calling_bufnr = vim.api.nvim_get_current_buf() + + opts = opts or {} + + debug_once("pick opts: ", opts) + + local buf = opts.buf or vim.api.nvim_get_current_buf() + + local entries = {} + for _, value in ipairs(values) do + if type(value) == "string" then + table.insert(entries, { + text = value, + value = value, + }) + elseif value.valid ~= false then + local name = self:_make_display(value) + table.insert(entries, { + text = name, + buf = buf, + filename = value.filename, + value = value.value, + pos = { value.lnum, value.col or 0 }, + }) end + end + + local map = vim.tbl_deep_extend("force", {}, notes_mappings(opts.selection_mappings)) + + local pick_opts = vim.tbl_extend("force", map or {}, { + tilte = opts.prompt_title, + items = entries, + layout = { + preview = false, + }, + format = "text", + confirm = function(picker, item, action) + picker:close() + if item then + if opts.callback then + debug_once("pick callback: ", item) + opts.callback(item.value) + else + debug_once("pick jump: ", item) + snacks_picker.actions.jump(picker, item, action) + end + end + end, + }) - local map = vim.tbl_deep_extend("force", {}, - notes_mappings(opts.selection_mappings)) - - local pick_opts = vim.tbl_extend("force", map or {}, { - tilte = opts.prompt_title, - items = entries, - layout = { - preview = false - }, - format = "text", - confirm = function(picker, item, action) - picker:close() - if item then - if opts.callback then - debug_once("pick callback: ", item) - opts.callback(item.value) - else - debug_once("pick jump: ", item) - snacks_picker.actions.jump(picker, item, action) - end - end - end, - }) - - local entry = snacks_picker.pick(pick_opts) + snacks_picker.pick(pick_opts) end return SnacksPicker From e0c781e1fc73be33fc95773b16175384fe03d624 Mon Sep 17 00:00:00 2001 From: guspix <33852783+guspix@users.noreply.github.com> Date: Sun, 2 Mar 2025 17:13:10 +0000 Subject: [PATCH 16/41] chore(docs): auto generate docs --- doc/obsidian.txt | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/doc/obsidian.txt b/doc/obsidian.txt index fc053994..37a90095 100644 --- a/doc/obsidian.txt +++ b/doc/obsidian.txt @@ -40,9 +40,10 @@ own as well. You don’t necessarily need to use it alongside the Obsidian app. 2. Features *obsidian-features* ▶️ **Completion:** Ultra-fast, asynchronous autocompletion for note -references and tags via nvim-cmp -(triggered by typing `[[` for wiki links, `[` for markdown links, or `#` for -tags), powered by `ripgrep` . +references and tags via nvim-cmp or +blink.cmp (triggered by typing `[[` for +wiki links, `[` for markdown links, or `#` for tags), powered by `ripgrep` +. @@ -246,6 +247,7 @@ dependencies that enhance the obsidian.nvim experience. **Completion:** - **[recommended]** hrsh7th/nvim-cmp : for completion of note references. +- blink.cmp (new): for completion of note references. **Pickers:** @@ -322,8 +324,10 @@ carefully and customize it to your needs: -- Optional, completion of wiki links, local markdown links, and tags using nvim-cmp. completion = { - -- Set to false to disable completion. + -- Enables completion using nvim_cmp nvim_cmp = true, + -- Enables completion using blink.cmp + blink = false, -- Trigger completion at 2 chars. min_chars = 2, }, @@ -683,9 +687,11 @@ plugin’s functionality on markdown files outside of your "fixed" vaults. See COMPLETION ~ -obsidian.nvim will set itself up as an nvim-cmp source automatically when you -enter a markdown buffer within your vault directory, you do **not** need to -specify this plugin as a cmp source manually. +obsidian.nvim supports nvim_cmp and blink.cmp completion plugins. + +obsidian.nvim will set itself up automatically when you enter a markdown buffer +within your vault directory, you do **not** need to specify this plugin as a +cmp source manually. Note that in order to trigger completion for tags _within YAML frontmatter_ you still need to type the "#" at the start of the tag. obsidian.nvim will remove From 2cbf2eab0e90e9d9b4976fcc58fad9aebd860ef5 Mon Sep 17 00:00:00 2001 From: guspix <33852783+guspix@users.noreply.github.com> Date: Sun, 2 Mar 2025 17:23:09 +0000 Subject: [PATCH 17/41] chore(docs): auto generate docs --- doc/obsidian.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/obsidian.txt b/doc/obsidian.txt index 532de118..a809552d 100644 --- a/doc/obsidian.txt +++ b/doc/obsidian.txt @@ -254,7 +254,7 @@ dependencies that enhance the obsidian.nvim experience. - **[recommended]** nvim-telescope/telescope.nvim : for search and quick-switch functionality. - Mini.Pick from the mini.nvim library: an alternative to telescope for search and quick-switch functionality. - ibhagwan/fzf-lua : another alternative to telescope for search and quick-switch functionality. -- Snacks.Pick : another alternative to telescope for search and quick-switch functionality. +- Snacks.Picker from the snacks.nvim library: an alternative to mini and telescope for search and quick-switch functionality. **Syntax highlighting:** From 843aefe37a7e970964380ca1139a3725e2718e5f Mon Sep 17 00:00:00 2001 From: guspix Date: Mon, 3 Mar 2025 12:53:14 +0100 Subject: [PATCH 18/41] Changed README to point to the org's repos and releases --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 83800921..9c5e3fb4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

obsidian.nvim

- -
Latest release Last commit Latest Neovim Made with Lua Buy me a coffee
+ +
Latest release Last commit Latest Neovim Made with Lua Buy me a coffee

A Neovim plugin for writing and navigating [Obsidian](https://obsidian.md) vaults, written in Lua. @@ -116,13 +116,13 @@ Search functionality (e.g. via the `:ObsidianSearch` and `:ObsidianQuickSwitch` To configure obsidian.nvim you just need to call `require("obsidian").setup({ ... })` with the desired options. Here are some examples using different plugin managers. The full set of [plugin dependencies](#plugin-dependencies) and [configuration options](#configuration-options) are listed below. -> ⚠️ WARNING: if you install from the latest release (recommended for stability) instead of `main`, be aware that the README on `main` may reference features that haven't been released yet. For that reason I recommend viewing the README on the tag for the [latest release](https://github.com/epwalsh/obsidian.nvim/releases) instead of `main`. +> ⚠️ WARNING: if you install from the latest release (recommended for stability) instead of `main`, be aware that the README on `main` may reference features that haven't been released yet. For that reason I recommend viewing the README on the tag for the [latest release](https://github.com/obsidian-nvim/obsidian.nvim/releases) instead of `main`. #### Using [`lazy.nvim`](https://github.com/folke/lazy.nvim) ```lua return { - "epwalsh/obsidian.nvim", + "obsidian-nvim/obsidian.nvim", version = "*", -- recommended, use latest release instead of latest commit lazy = true, ft = "markdown", @@ -161,7 +161,7 @@ return { ```lua use({ - "epwalsh/obsidian.nvim", + "obsidian-nvim/obsidian.nvim", tag = "*", -- recommended, use latest release instead of latest commit requires = { -- Required. @@ -774,7 +774,7 @@ And keep in mind that to reset a configuration option to `nil` you'll have to us ## Contributing -Please read the [CONTRIBUTING](https://github.com/epwalsh/obsidian.nvim/blob/main/.github/CONTRIBUTING.md) guide before submitting a pull request. +Please read the [CONTRIBUTING](https://github.com/obsidian-nvim/obsidian.nvim/blob/main/.github/CONTRIBUTING.md) guide before submitting a pull request. And if you're feeling especially generous I always appreciate some coffee funds! ❤️ From 0d9111fde2968c8015e3a192bba2b3e825d1525a Mon Sep 17 00:00:00 2001 From: guspix Date: Mon, 3 Mar 2025 12:58:45 +0100 Subject: [PATCH 19/41] Changed changelog script to reference the org's repo + epwalsh dir in test --- scripts/prepare_changelog.py | 2 +- test/manual/client_spec.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/prepare_changelog.py b/scripts/prepare_changelog.py index 5ba56ff0..9feda4b9 100644 --- a/scripts/prepare_changelog.py +++ b/scripts/prepare_changelog.py @@ -30,7 +30,7 @@ def main(): lines.insert(insert_index, "\n") lines.insert( insert_index + 1, - f"## [v{VERSION}](https://github.com/epwalsh/obsidian.nvim/releases/tag/v{VERSION}) - " + f"## [v{VERSION}](https://github.com/obsidian-nvim/obsidian.nvim/releases/tag/v{VERSION}) - " f"{datetime.now().strftime('%Y-%m-%d')}\n", ) diff --git a/test/manual/client_spec.lua b/test/manual/client_spec.lua index 304c5369..8538c40b 100644 --- a/test/manual/client_spec.lua +++ b/test/manual/client_spec.lua @@ -4,7 +4,7 @@ local obsidian = require "obsidian" -local client = obsidian.setup { dir = "~/epwalsh-notes/notes" } ---@diagnostic disable-line: missing-fields +local client = obsidian.setup { dir = "~/obsidian-nvim/notes" } ---@diagnostic disable-line: missing-fields for _, note in ipairs(client:find_notes("allennlp", { search = { sort = false } })) do print(note.id) end From 9a3bfd35996d3da9ce5f31797ff9641a5dc4a016 Mon Sep 17 00:00:00 2001 From: guspix <33852783+guspix@users.noreply.github.com> Date: Mon, 3 Mar 2025 12:30:14 +0000 Subject: [PATCH 20/41] chore(docs): auto generate docs --- doc/obsidian.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/obsidian.txt b/doc/obsidian.txt index a809552d..0ed475c5 100644 --- a/doc/obsidian.txt +++ b/doc/obsidian.txt @@ -164,13 +164,13 @@ managers. The full set of |obsidian-plugin-dependencies| and stability) instead of `main`, be aware that the README on `main` may reference features that haven’t been released yet. For that reason I recommend viewing the README on the tag for the latest release - instead of `main`. + instead of `main`. USING LAZY.NVIM ~ >lua return { - "epwalsh/obsidian.nvim", + "obsidian-nvim/obsidian.nvim", version = "*", -- recommended, use latest release instead of latest commit lazy = true, ft = "markdown", @@ -210,7 +210,7 @@ USING PACKER.NVIM ~ >lua use({ - "epwalsh/obsidian.nvim", + "obsidian-nvim/obsidian.nvim", tag = "*", -- recommended, use latest release instead of latest commit requires = { -- Required. @@ -892,7 +892,7 @@ option to `nil` you’ll have to use `vim.NIL` there instead of the builtin Lua 4. Contributing *obsidian-contributing* Please read the CONTRIBUTING - + guide before submitting a pull request. And if you’re feeling especially generous I always appreciate some coffee From 8fc0dbf6ef103fcc37b6ed6014cecf19910f612d Mon Sep 17 00:00:00 2001 From: guspix Date: Mon, 3 Mar 2025 15:27:01 +0100 Subject: [PATCH 21/41] Changed changelog reference to epwalsh --- .github/CONTRIBUTING.md | 4 ++-- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 51c90fca..b0dd231c 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -18,7 +18,7 @@ Make sure that the path there to plenary is correct for you. ## Keeping the CHANGELOG up-to-date -This project tries hard to adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and we maintain a [`CHANGELOG`](https://github.com/epwalsh/obsidian.nvim/blob/main/CHANGELOG.md) with a format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +This project tries hard to adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and we maintain a [`CHANGELOG`](https://github.com/obsidian-nvim/obsidian.nvim/blob/main/CHANGELOG.md) with a format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). If your PR addresses a bug or makes any other substantial change, please be sure to add an entry under the "Unreleased" section at the top of `CHANGELOG.md`. Entries should always be in the form of a list item under a level-3 header of either "Added", "Fixed", "Changed", or "Removed" for the most part. If the corresponding level-3 header for your item does not already exist in the "Unreleased" section, you should add it. @@ -49,7 +49,7 @@ However you can test how changes to the README will affect the Vim doc by runnin To do this you'll need install `pandoc` (e.g. `brew install pandoc` on Mac) and clone [panvimdoc](https://github.com/kdheepak/panvimdoc). Then from the panvimdoc repo root, run: ```bash -./panvimdoc.sh --project-name obsidian --input-file ../../epwalsh/obsidian.nvim/README.md --description 'a plugin for writing and navigating an Obsidian vault' --toc 'false' --vim-version 'NVIM v0.8.0' --demojify 'false' --dedup-subheadings 'false' --shift-heading-level-by '-1' && mv doc/obsidian.txt /tmp/ +./panvimdoc.sh --project-name obsidian --input-file ../../obsidian-nvim/obsidian.nvim/README.md --description 'a plugin for writing and navigating an Obsidian vault' --toc 'false' --vim-version 'NVIM v0.8.0' --demojify 'false' --dedup-subheadings 'false' --shift-heading-level-by '-1' && mv doc/obsidian.txt /tmp/ ``` This will build the Vim documentation to `/tmp/obsidian.txt`. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index e071bd1d..4c2baeab 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -6,7 +6,7 @@ body: - type: markdown attributes: value: > - #### Before submitting a bug, please make sure the issue hasn't been already addressed by searching through [the existing and past issues](https://github.com/epwalsh/obsidian.nvim/issues?q=is%3Aissue+sort%3Acreated-desc+). + #### Before submitting a bug, please make sure the issue hasn't been already addressed by searching through [the existing and past issues](https://github.com/obsidian-nvim/obsidian.nvim/issues?q=is%3Aissue+sort%3Acreated-desc+). - type: textarea attributes: label: 🐛 Describe the bug From a954102fa10be40ef89e30bc68934b803e9b14ee Mon Sep 17 00:00:00 2001 From: n451 <2020200706@ruc.edu.cn> Date: Tue, 4 Mar 2025 20:23:49 +0800 Subject: [PATCH 22/41] doc: add instruction for rocks.nvim, warning for packer.nvim doc: remove some verbose description in plugin dependencies --- README.md | 64 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 9c5e3fb4..35e8cfc4 100644 --- a/README.md +++ b/README.md @@ -120,10 +120,12 @@ Here are some examples using different plugin managers. The full set of [plugin #### Using [`lazy.nvim`](https://github.com/folke/lazy.nvim) +
Click for install snippet + ```lua return { "obsidian-nvim/obsidian.nvim", - version = "*", -- recommended, use latest release instead of latest commit + version = "*", -- recommended, use latest release instead of latest commit lazy = true, ft = "markdown", -- Replace the above line with this if you only want to load obsidian.nvim for markdown files in your vault: @@ -157,12 +159,28 @@ return { } ``` +
+ +#### Using [`rocks.nvim`](https://github.com/nvim-neorocks/rocks.nvim) + +
Click for install snippet + +```vim +:Rocks install obsidian +``` + +
+ #### Using [`packer.nvim`](https://github.com/wbthomason/packer.nvim) +It is not recommended because packer.nvim is currently unmaintained + +
Click for install snippet + ```lua -use({ +use { "obsidian-nvim/obsidian.nvim", - tag = "*", -- recommended, use latest release instead of latest commit + tag = "*", -- recommended, use latest release instead of latest commit requires = { -- Required. "nvim-lua/plenary.nvim", @@ -170,7 +188,7 @@ use({ -- see below for full list of optional dependencies 👇 }, config = function() - require("obsidian").setup({ + require("obsidian").setup { workspaces = { { name = "personal", @@ -183,31 +201,34 @@ use({ }, -- see below for full list of options 👇 - }) + } end, -}) +} ``` +
+ ### Plugin dependencies The only **required** plugin dependency is [plenary.nvim](https://github.com/nvim-lua/plenary.nvim), but there are a number of optional dependencies that enhance the obsidian.nvim experience. **Completion:** -- **[recommended]** [hrsh7th/nvim-cmp](https://github.com/hrsh7th/nvim-cmp): for completion of note references. -- [blink.cmp](https://github.com/Saghen/blink.cmp) (new): for completion of note references. +- **[recommended]** [hrsh7th/nvim-cmp](https://github.com/hrsh7th/nvim-cmp) +- [blink.cmp](https://github.com/Saghen/blink.cmp) (new) **Pickers:** -- **[recommended]** [nvim-telescope/telescope.nvim](https://github.com/nvim-telescope/telescope.nvim): for search and quick-switch functionality. -- [Mini.Pick](https://github.com/echasnovski/mini.pick) from the mini.nvim library: an alternative to telescope for search and quick-switch functionality. -- [ibhagwan/fzf-lua](https://github.com/ibhagwan/fzf-lua): another alternative to telescope for search and quick-switch functionality. -- [Snacks.Picker](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md) from the snacks.nvim library: an alternative to mini and telescope for search and quick-switch functionality. +- **[recommended]** [nvim-telescope/telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) +- [ibhagwan/fzf-lua](https://github.com/ibhagwan/fzf-lua) +- [Mini.Pick](https://github.com/echasnovski/mini.pick) from the mini.nvim library +- [Snacks.Picker](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md) from the snacks.nvim library **Syntax highlighting:** -- **[recommended]** [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter): for base markdown syntax highlighting. See [syntax highlighting](#syntax-highlighting) for more details. -- [preservim/vim-markdown](https://github.com/preservim/vim-markdown): an alternative to nvim-treesitter for syntax highlighting (see [syntax highlighting](#syntax-highlighting) for more details), plus other cool features. +- **[recommended]** [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter): for base markdown syntax highlighting. +- [preservim/vim-markdown](https://github.com/preservim/vim-markdown) +- See [syntax highlighting](#syntax-highlighting) for more details. **Miscellaneous:** @@ -217,7 +238,7 @@ If you choose to use any of these you should include them in the "dependencies" ### Configuration options -This is a complete list of all of the options that can be passed to `require("obsidian").setup()`. The settings below are *not necessarily the defaults, but represent reasonable default settings*. Please read each option carefully and customize it to your needs: +This is a complete list of all of the options that can be passed to `require("obsidian").setup()`. The settings below are _not necessarily the defaults, but represent reasonable default settings_. Please read each option carefully and customize it to your needs: ```lua { @@ -564,7 +585,7 @@ config = { name = "personal", path = "~/vaults/personal", }, - } + }, } ``` @@ -591,13 +612,12 @@ config = { -- ... }, }, - } + }, } ``` obsidian.nvim also supports "dynamic" workspaces. These are simply workspaces where the `path` is set to a Lua function (that returns a path) instead of a hard-coded path. This can be useful in several scenarios, such as when you want a workspace whose `path` is always set to the parent directory of the current buffer: - ```lua config = { workspaces = { @@ -607,7 +627,7 @@ config = { return assert(vim.fs.dirname(vim.api.nvim_buf_get_name(0))) end, }, - } + }, } ``` @@ -627,12 +647,12 @@ Note that in order to trigger completion for tags _within YAML frontmatter_ you If you're using [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter/blob/master/README.md) you're configuration should include both "markdown" and "markdown_inline" sources: ```lua -require("nvim-treesitter.configs").setup({ +require("nvim-treesitter.configs").setup { ensure_installed = { "markdown", "markdown_inline", ... }, highlight = { enable = true, }, -}) +} ``` If you use `vim-markdown` you'll probably want to disable its frontmatter syntax highlighting (`vim.g.vim_markdown_frontmatter = 1`) which I've found doesn't work very well. @@ -734,7 +754,7 @@ templates = { ### Usage outside of a workspace or vault -It's possible to configure obsidian.nvim to work on individual markdown files outside of a regular workspace / Obsidian vault by configuring a "dynamic" workspace. To do so you just need to add a special workspace with a function for the `path` field (instead of a string), which should return a *parent* directory of the current buffer. This tells obsidian.nvim to use that directory as the workspace `path` and `root` (vault root) when the buffer is not located inside another fixed workspace. +It's possible to configure obsidian.nvim to work on individual markdown files outside of a regular workspace / Obsidian vault by configuring a "dynamic" workspace. To do so you just need to add a special workspace with a function for the `path` field (instead of a string), which should return a _parent_ directory of the current buffer. This tells obsidian.nvim to use that directory as the workspace `path` and `root` (vault root) when the buffer is not located inside another fixed workspace. For example, to extend the configuration above this way: From b5a15a394d0c2f54555a5b047091a60f3628d9fd Mon Sep 17 00:00:00 2001 From: n451 <2020200706@ruc.edu.cn> Date: Tue, 4 Mar 2025 20:43:29 +0800 Subject: [PATCH 23/41] fix: offload markdown rendering to render-markdown/markview --- README.md | 11 ++++++++--- lua/obsidian/client.lua | 3 ++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 35e8cfc4..5b612e11 100644 --- a/README.md +++ b/README.md @@ -226,9 +226,14 @@ The only **required** plugin dependency is [plenary.nvim](https://github.com/nvi **Syntax highlighting:** -- **[recommended]** [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter): for base markdown syntax highlighting. -- [preservim/vim-markdown](https://github.com/preservim/vim-markdown) -- See [syntax highlighting](#syntax-highlighting) for more details. +See [syntax highlighting](#syntax-highlighting) for more details. + +- For base syntax highlighting: + - **[recommended]** [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter) + - [preservim/vim-markdown](https://github.com/preservim/vim-markdown) +- For additional syntax features: + - [render-markdown.nvim](https://github.com/MeanderingProgrammer/render-markdown.nvim) + - [markview.nvim](https://github.com/OXY2DEV/markview.nvim) **Miscellaneous:** diff --git a/lua/obsidian/client.lua b/lua/obsidian/client.lua index 00c09c77..c6492bdc 100644 --- a/lua/obsidian/client.lua +++ b/lua/obsidian/client.lua @@ -125,7 +125,8 @@ Client.set_workspace = function(self, workspace, opts) self.callback_manager = CallbackManager.new(self, self.opts.callbacks) -- Setup UI add-ons. - if self.opts.ui.enable then + local has_no_renderer = not (util.get_plugin_info "render-markdown.nvim" or util.get_plugin_info "markview.nvim") + if has_no_renderer and self.opts.ui.enable then require("obsidian.ui").setup(self.current_workspace, self.opts.ui) end From 39ff5a03a657d26f48737cafd9913a54e9853814 Mon Sep 17 00:00:00 2001 From: n451 <2020200706@ruc.edu.cn> Date: Tue, 4 Mar 2025 20:50:53 +0800 Subject: [PATCH 24/41] update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f2a2061..0b7a9af8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Renamed `opts.image_name_func` to `opts.attachments.img_name_func`. +- Default to not activate ui render when `render-markdown.nvim` or `markview.nvim` is present ### Fixed From 4c2f1ce16feb34a23034359c32658b9af0d72f8f Mon Sep 17 00:00:00 2001 From: bosvik Date: Thu, 6 Mar 2025 09:30:58 +0100 Subject: [PATCH 25/41] Fix bug where ObsidianNewFromTemplate does not respect note_id_func --- CHANGELOG.md | 11 +++++++---- lua/obsidian/client.lua | 1 + lua/obsidian/templates.lua | 8 +++++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f2a2061..bb2d0f1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed an edge case with collecting backlinks. - Fixed typo in `ObsidianPasteImg`'s command description - Fixed the case when `opts.attachments` is `nil`. +- Fixed bug where `ObsidianNewFromTemplate` did not respect `note_id_func` ## [v3.9.0](https://github.com/epwalsh/obsidian.nvim/releases/tag/v3.9.0) - 2024-07-11 @@ -191,6 +192,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [v3.7.0](https://github.com/epwalsh/obsidian.nvim/releases/tag/v3.7.0) - 2024-03-08 There's a lot of new features and improvements here that I'm really excited about 🥳 They've improved my workflow a ton and I hope they do for you too. To highlight the 3 biggest additions: + 1. 🔗 Full support for header anchor links and block links! That means both for following links and completion of links. Various forms of anchor/block links are support. Here are a few examples: - Typical Obsidian-style wiki links, e.g. `[[My note#Heading 1]]`, `[[My note#Heading 1#Sub heading]]`, `[[My note#^block-123]]`. - Wiki links with a label, e.g. `[[my-note#heading-1|Heading 1 in My Note]]`. @@ -199,7 +201,7 @@ There's a lot of new features and improvements here that I'm really excited abou We also support links to headers within the same note, like for a table of contents, e.g. `[[#Heading 1]]`, `[[#heading-1|Heading]]`, `[[#^block-1]]`. 2. 📲 A basic callback system to let you easily customize obisidian.nvim's behavior even more. There are currently 4 events: `post_setup`, `enter_note`, `pre_write_note`, and `post_set_workspace`. You can define a function for each of these in your config. -3. 🔭 Improved picker integrations (especially for telescope), particular for the `:ObsidianTags` command. See https://github.com/epwalsh/obsidian.nvim/discussions/450 for a demo. +3. 🔭 Improved picker integrations (especially for telescope), particular for the `:ObsidianTags` command. See for a demo. Full changelog below 👇 @@ -439,7 +441,7 @@ Minor internal improvements. ### Fixed -- Fixed parsing header with trailing whitespace (https://github.com/epwalsh/obsidian.nvim/issues/341#issuecomment-1925445271). +- Fixed parsing header with trailing whitespace (). ## [v2.9.0](https://github.com/epwalsh/obsidian.nvim/releases/tag/v2.9.0) - 2024-01-31 @@ -469,7 +471,7 @@ Minor internal improvements. ### Fixed - Fixed a YAML parsing issue with unquoted URLs in an array item. -- Fixed an issue on Windows when cloning a template into a new note. The root cause was this bug in plenary: https://github.com/nvim-lua/plenary.nvim/issues/489. We've added a work-around. +- Fixed an issue on Windows when cloning a template into a new note. The root cause was this bug in plenary: . We've added a work-around. ## [v2.7.1](https://github.com/epwalsh/obsidian.nvim/releases/tag/v2.7.1) - 2024-01-23 @@ -799,6 +801,7 @@ Major internal refactoring to bring performance improvements through async execu - Added `mappings` configuration field. - Added `open_notes_in` configuration field - Added `backlinks` options to the config. The default is + ```lua backlinks = { -- The default height of the backlinks pane. @@ -900,7 +903,7 @@ Major internal refactoring to bring performance improvements through async execu ### Added - Added support for [fzf-lua](https://github.com/ibhagwan/fzf-lua) as one of the possible fallbacks for the `:ObsidianQuickSwitch` command. -- Added `:ObsidianQuickSwitch` to fuzzy-find a note by name in telescope/fzf _a la_ in Obsidian. +- Added `:ObsidianQuickSwitch` to fuzzy-find a note by name in telescope/fzf *a la* in Obsidian. - Added support for [fzf-lua](https://github.com/ibhagwan/fzf-lua) as one of the possible fallbacks for the `:ObsidianSearch` command. - Added `:ObsidianFollowLink` and companion function `util.cursor_on_markdown_link` - Added `:ObsidianLink` and `:ObsidianLinkNew` commands. diff --git a/lua/obsidian/client.lua b/lua/obsidian/client.lua index 00c09c77..d0a14ddb 100644 --- a/lua/obsidian/client.lua +++ b/lua/obsidian/client.lua @@ -1875,6 +1875,7 @@ Client.write_note_to_buffer = function(self, note, opts) if opts.template and util.buffer_is_empty(opts.bufnr) then note = insert_template { + note = note, template_name = opts.template, client = self, location = util.get_active_window_cursor_location(), diff --git a/lua/obsidian/templates.lua b/lua/obsidian/templates.lua index ac8a83f2..d7e9f3e7 100644 --- a/lua/obsidian/templates.lua +++ b/lua/obsidian/templates.lua @@ -151,12 +151,14 @@ end ---Insert a template at the given location. --- ----@param opts { template_name: string|obsidian.Path, client: obsidian.Client, location: { [1]: integer, [2]: integer, [3]: integer, [4]: integer } } Options. +---@param opts { note: obsidian.Note|?, template_name: string|obsidian.Path, client: obsidian.Client, location: { [1]: integer, [2]: integer, [3]: integer, [4]: integer } } Options. --- ---@return obsidian.Note M.insert_template = function(opts) local buf, win, row, _ = unpack(opts.location) - local note = Note.from_buffer(buf) + if opts.note == nil then + opts.note = Note.from_buffer(buf) + end local template_path = resolve_template(opts.template_name, opts.client) @@ -165,7 +167,7 @@ M.insert_template = function(opts) if template_file then local lines = template_file:lines() for line in lines do - local new_lines = M.substitute_template_variables(line, opts.client, note) + local new_lines = M.substitute_template_variables(line, opts.client, opts.note) if string.find(new_lines, "[\r\n]") then local line_start = 1 for line_end in util.gfind(new_lines, "[\r\n]") do From 87d7b4e63b12a3ec5be67620ee2b01aac5157058 Mon Sep 17 00:00:00 2001 From: guspix <33852783+guspix@users.noreply.github.com> Date: Thu, 6 Mar 2025 08:45:44 +0000 Subject: [PATCH 26/41] chore(docs): auto generate docs --- doc/obsidian.txt | 59 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/doc/obsidian.txt b/doc/obsidian.txt index 0ed475c5..9b9a23bb 100644 --- a/doc/obsidian.txt +++ b/doc/obsidian.txt @@ -168,10 +168,12 @@ managers. The full set of |obsidian-plugin-dependencies| and USING LAZY.NVIM ~ +Click for install snippet ~ + >lua return { "obsidian-nvim/obsidian.nvim", - version = "*", -- recommended, use latest release instead of latest commit + version = "*", -- recommended, use latest release instead of latest commit lazy = true, ft = "markdown", -- Replace the above line with this if you only want to load obsidian.nvim for markdown files in your vault: @@ -206,12 +208,25 @@ USING LAZY.NVIM ~ < +USING ROCKS.NVIM ~ + +Click for install snippet ~ + +>vim + :Rocks install obsidian +< + + USING PACKER.NVIM ~ +It is not recommended because packer.nvim is currently unmaintained + +Click for install snippet ~ + >lua - use({ + use { "obsidian-nvim/obsidian.nvim", - tag = "*", -- recommended, use latest release instead of latest commit + tag = "*", -- recommended, use latest release instead of latest commit requires = { -- Required. "nvim-lua/plenary.nvim", @@ -219,7 +234,7 @@ USING PACKER.NVIM ~ -- see below for full list of optional dependencies 👇 }, config = function() - require("obsidian").setup({ + require("obsidian").setup { workspaces = { { name = "personal", @@ -232,9 +247,9 @@ USING PACKER.NVIM ~ }, -- see below for full list of options 👇 - }) + } end, - }) + } < @@ -246,20 +261,26 @@ dependencies that enhance the obsidian.nvim experience. **Completion:** -- **[recommended]** hrsh7th/nvim-cmp : for completion of note references. -- blink.cmp (new): for completion of note references. +- **[recommended]** hrsh7th/nvim-cmp +- blink.cmp (new) **Pickers:** -- **[recommended]** nvim-telescope/telescope.nvim : for search and quick-switch functionality. -- Mini.Pick from the mini.nvim library: an alternative to telescope for search and quick-switch functionality. -- ibhagwan/fzf-lua : another alternative to telescope for search and quick-switch functionality. -- Snacks.Picker from the snacks.nvim library: an alternative to mini and telescope for search and quick-switch functionality. +- **[recommended]** nvim-telescope/telescope.nvim +- ibhagwan/fzf-lua +- Mini.Pick from the mini.nvim library +- Snacks.Picker from the snacks.nvim library **Syntax highlighting:** -- **[recommended]** nvim-treesitter : for base markdown syntax highlighting. See |obsidian-syntax-highlighting| for more details. -- preservim/vim-markdown : an alternative to nvim-treesitter for syntax highlighting (see |obsidian-syntax-highlighting| for more details), plus other cool features. +See |obsidian-syntax-highlighting| for more details. + +- For base syntax highlighting: + - **[recommended]** nvim-treesitter + - preservim/vim-markdown +- For additional syntax features: + - render-markdown.nvim + - markview.nvim **Miscellaneous:** @@ -626,7 +647,7 @@ the `workspaces` field in your config would look like this: name = "personal", path = "~/vaults/personal", }, - } + }, } < @@ -658,7 +679,7 @@ example: -- ... }, }, - } + }, } < @@ -677,7 +698,7 @@ buffer: return assert(vim.fs.dirname(vim.api.nvim_buf_get_name(0))) end, }, - } + }, } < @@ -707,12 +728,12 @@ you’re configuration should include both "markdown" and "markdown_inline" sources: >lua - require("nvim-treesitter.configs").setup({ + require("nvim-treesitter.configs").setup { ensure_installed = { "markdown", "markdown_inline", ... }, highlight = { enable = true, }, - }) + } < If you use `vim-markdown` you’ll probably want to disable its frontmatter From e6ca9e8eca3ffe977d1bc7b79de15d5662c9663f Mon Sep 17 00:00:00 2001 From: ffricken Date: Thu, 6 Mar 2025 18:54:00 +0100 Subject: [PATCH 27/41] feat: explain the fork in readme --- README.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5b612e11..b9464931 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,21 @@

obsidian.nvim

-
Latest release Last commit Latest Neovim Made with Lua Buy me a coffee
+
Latest release Last commit Latest Neovim Made with Lua

-A Neovim plugin for writing and navigating [Obsidian](https://obsidian.md) vaults, written in Lua. +A **community fork** of the Neovim plugin for writing and navigating [Obsidian](https://obsidian.md) vaults, written in Lua, created by [epwalsh](https://github.com/epwalsh). Built for people who love the concept of Obsidian -- a simple, markdown-based notes app -- but love Neovim too much to stand typing characters into anything else. -If you're new to Obsidian I highly recommend watching [this excellent YouTube video](https://youtu.be/5ht8NYkU9wQ?si=8nbnNsRVnw0xfX2S) for a great overview. +If you're new to Obsidian we highly recommend watching [this excellent YouTube video](https://youtu.be/5ht8NYkU9wQ?si=8nbnNsRVnw0xfX2S) for a great overview. _Keep in mind this plugin is not meant to replace Obsidian, but to complement it._ The Obsidian app is very powerful in its own way; it comes with a mobile app and has a lot of functionality that's not feasible to implement in Neovim, such as the graph explorer view. That said, this plugin stands on its own as well. You don't necessarily need to use it alongside the Obsidian app. +## About the fork +The original project has not been actively maintained for quite a while and with the ever-changing Neovim ecosystem, new widely used tools such as [blink.cmp](https://github.com/Saghen/blink.cmp) or [snacks.picker](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md) were not supported. +With bugs, issues and pull requests piling up, people from the community decided to fork and maintain the project. +The fork aims to stay close to the original, but fix bugs, include and merge useful improvements, and ensure long term robustness. + ## Table of contents - 👉 [Features](#features) @@ -801,6 +806,5 @@ And keep in mind that to reset a configuration option to `nil` you'll have to us Please read the [CONTRIBUTING](https://github.com/obsidian-nvim/obsidian.nvim/blob/main/.github/CONTRIBUTING.md) guide before submitting a pull request. -And if you're feeling especially generous I always appreciate some coffee funds! ❤️ - -[![BuyMeACoffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/epwalsh) +## Acknowledgement +We would like to thank [epwalsh](https://github.com/epwalsh) for creating this beautiful plugin. If you're feeling especially generous, [he still appreciates some coffee funds! ❤️](https://www.buymeacoffee.com/epwalsh). From 5b3cbe293231ff4c8eb4807442b7c2e7a44ac9c1 Mon Sep 17 00:00:00 2001 From: guspix <33852783+guspix@users.noreply.github.com> Date: Sat, 8 Mar 2025 10:23:02 +0000 Subject: [PATCH 28/41] chore(docs): auto generate docs --- doc/obsidian.txt | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/doc/obsidian.txt b/doc/obsidian.txt index 9b9a23bb..772ea2d1 100644 --- a/doc/obsidian.txt +++ b/doc/obsidian.txt @@ -2,14 +2,15 @@ obsidian.nvim -A Neovim plugin for writing and navigating Obsidian -vaults, written in Lua. +A **community fork** of the Neovim plugin for writing and navigating Obsidian + vaults, written in Lua, created by epwalsh +. Built for people who love the concept of Obsidian – a simple, markdown-based notes app – but love Neovim too much to stand typing characters into anything else. -If you’re new to Obsidian I highly recommend watching this excellent YouTube +If you’re new to Obsidian we highly recommend watching this excellent YouTube video for a great overview. _Keep in mind this plugin is not meant to replace Obsidian, but to complement @@ -20,7 +21,20 @@ own as well. You don’t necessarily need to use it alongside the Obsidian app. ============================================================================== -1. Table of contents *obsidian-table-of-contents* +1. About the fork *obsidian-about-the-fork* + +The original project has not been actively maintained for quite a while and +with the ever-changing Neovim ecosystem, new widely used tools such as +blink.cmp or snacks.picker + were not +supported. With bugs, issues and pull requests piling up, people from the +community decided to fork and maintain the project. The fork aims to stay close +to the original, but fix bugs, include and merge useful improvements, and +ensure long term robustness. + + +============================================================================== +2. Table of contents *obsidian-table-of-contents* - 👉 |obsidian-features| - |obsidian-commands| @@ -37,7 +51,7 @@ own as well. You don’t necessarily need to use it alongside the Obsidian app. ============================================================================== -2. Features *obsidian-features* +3. Features *obsidian-features* ▶️ **Completion:** Ultra-fast, asynchronous autocompletion for note references and tags via nvim-cmp or @@ -130,7 +144,7 @@ DEMO *obsidian-demo* ============================================================================== -3. Setup *obsidian-setup* +4. Setup *obsidian-setup* SYSTEM REQUIREMENTS *obsidian-system-requirements* @@ -910,24 +924,26 @@ option to `nil` you’ll have to use `vim.NIL` there instead of the builtin Lua ============================================================================== -4. Contributing *obsidian-contributing* +5. Contributing *obsidian-contributing* Please read the CONTRIBUTING guide before submitting a pull request. -And if you’re feeling especially generous I always appreciate some coffee -funds! ❤️ - +============================================================================== +6. Acknowledgement *obsidian-acknowledgement* + +We would like to thank epwalsh for creating this +beautiful plugin. If you’re feeling especially generous, he still appreciates +some coffee funds! ❤️ . ============================================================================== -5. Links *obsidian-links* +7. Links *obsidian-links* 1. *See this screenshot*: https://github.com/epwalsh/obsidian.nvim/assets/8812459/90d5f218-06cd-4ebb-b00b-b59c2f5c3cc1 2. *See this screenshot*: https://github.com/epwalsh/obsidian.nvim/assets/8812459/e74f5267-21b5-49bc-a3bb-3b9db5fa6687 3. *2024-01-31 14 22 52*: https://github.com/epwalsh/obsidian.nvim/assets/8812459/2986e1d2-13e8-40e2-9c9e-75691a3b662e -4. *BuyMeACoffee*: https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black Generated by panvimdoc From 5eea5d2b32719d6b5064af8cd26a49409d564b16 Mon Sep 17 00:00:00 2001 From: neo451 <111681693+neo451@users.noreply.github.com> Date: Sat, 8 Mar 2025 21:43:51 +0800 Subject: [PATCH 29/41] fix: parser treats "Nan" as a number instead of a string (#22) * fix: parser treats "Nan" as a number instead of a string --- CHANGELOG.md | 1 + lua/obsidian/util.lua | 8 ++++++++ lua/obsidian/yaml/parser.lua | 2 +- test/obsidian/yaml/parser_spec.lua | 4 ++++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b4c8641..9e740c11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed typo in `ObsidianPasteImg`'s command description - Fixed the case when `opts.attachments` is `nil`. - Fixed bug where `ObsidianNewFromTemplate` did not respect `note_id_func` +- Fixed bug where parser treats "Nan" as a number instead of a string ## [v3.9.0](https://github.com/epwalsh/obsidian.nvim/releases/tag/v3.9.0) - 2024-07-11 diff --git a/lua/obsidian/util.lua b/lua/obsidian/util.lua index 1db3d044..1bd3e6da 100644 --- a/lua/obsidian/util.lua +++ b/lua/obsidian/util.lua @@ -1341,4 +1341,12 @@ util.buffer_is_empty = function(bufnr) end end +---Check if a string is NaN +--- +---@param v any +---@return boolean +util.isNan = function(v) + return tostring(v) == tostring(0 / 0) +end + return util diff --git a/lua/obsidian/yaml/parser.lua b/lua/obsidian/yaml/parser.lua index 3373ae38..a8e3a53e 100644 --- a/lua/obsidian/yaml/parser.lua +++ b/lua/obsidian/yaml/parser.lua @@ -555,7 +555,7 @@ end ---@diagnostic disable-next-line: unused-local Parser._parse_number = function(self, i, text) local out = tonumber(text) - if out == nil then + if out == nil or util.isNan(out) then return false, nil, nil else return true, nil, out diff --git a/test/obsidian/yaml/parser_spec.lua b/test/obsidian/yaml/parser_spec.lua index a96103c2..618f78d9 100644 --- a/test/obsidian/yaml/parser_spec.lua +++ b/test/obsidian/yaml/parser_spec.lua @@ -30,6 +30,10 @@ describe("Parser class", function() return parser:parse_number(str) end, "foo") assert.is_false(ok) + ok, _ = pcall(function(str) + return parser:parse_number(str) + end, "Nan") + assert.is_false(ok) end) it("should parse booleans while trimming whitespace", function() From 06a32cccc06a4fc2139d8a5d3a159c3d01cdbf63 Mon Sep 17 00:00:00 2001 From: Jost Alemann Date: Sun, 9 Mar 2025 14:53:55 +0100 Subject: [PATCH 30/41] nit: remove tracking from youtube URL --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b9464931..1e928092 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A **community fork** of the Neovim plugin for writing and navigating [Obsidian]( Built for people who love the concept of Obsidian -- a simple, markdown-based notes app -- but love Neovim too much to stand typing characters into anything else. -If you're new to Obsidian we highly recommend watching [this excellent YouTube video](https://youtu.be/5ht8NYkU9wQ?si=8nbnNsRVnw0xfX2S) for a great overview. +If you're new to Obsidian we highly recommend watching [this excellent YouTube video](https://youtu.be/5ht8NYkU9wQ) for a great overview. _Keep in mind this plugin is not meant to replace Obsidian, but to complement it._ The Obsidian app is very powerful in its own way; it comes with a mobile app and has a lot of functionality that's not feasible to implement in Neovim, such as the graph explorer view. That said, this plugin stands on its own as well. You don't necessarily need to use it alongside the Obsidian app. From 7481090eb9d6644e9b821d936d75c24527be07a2 Mon Sep 17 00:00:00 2001 From: ottersome Date: Thu, 20 Feb 2025 09:31:55 -0600 Subject: [PATCH 31/41] Add text/uri-list support for `paste_img` --- CHANGELOG.md | 1 + lua/obsidian/img_paste.lua | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e740c11..da4188fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added better handling for undefined template fields, which will now be prompted for. - Added support for the [`snacks.picker`](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md) picker - Added support for the [`blink.cmp`](https://github.com/Saghen/blink.cmp) completion plugin. +- Added support `text/uri-list` to `ObsidianPasteImg`. ### Changed diff --git a/lua/obsidian/img_paste.lua b/lua/obsidian/img_paste.lua index edd5df90..5cdda53e 100644 --- a/lua/obsidian/img_paste.lua +++ b/lua/obsidian/img_paste.lua @@ -40,7 +40,13 @@ local function clipboard_is_img() -- See: [Data URI scheme](https://en.wikipedia.org/wiki/Data_URI_scheme) local this_os = util.get_os() if this_os == util.OSType.Linux or this_os == util.OSType.FreeBSD then - return vim.tbl_contains(content, "image/png") + if vim.tbl_contains(content, "image/png") then + return true + elseif vim.tbl_contains(content, "text/uri-list") then + local success = + os.execute "wl-paste --type text/uri-list | sed 's|file://||' | head -n1 | tr -d '[:space:]' | xargs -I{} sh -c 'wl-copy < \"$1\"' _ {}" + return success == 0 + end elseif this_os == util.OSType.Darwin then return string.sub(content[1], 1, 9) == "iVBORw0KG" -- Magic png number in base64 elseif this_os == util.OSType.Windows or this_os == util.OSType.Wsl then From 15b8c5fc730625a3f162817b13db144c5bba3a9f Mon Sep 17 00:00:00 2001 From: guspix <33852783+guspix@users.noreply.github.com> Date: Thu, 13 Mar 2025 22:50:29 +0000 Subject: [PATCH 32/41] chore(docs): auto generate docs --- doc/obsidian.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/obsidian.txt b/doc/obsidian.txt index 772ea2d1..89960519 100644 --- a/doc/obsidian.txt +++ b/doc/obsidian.txt @@ -11,7 +11,7 @@ notes app – but love Neovim too much to stand typing characters into anything else. If you’re new to Obsidian we highly recommend watching this excellent YouTube -video for a great overview. +video for a great overview. _Keep in mind this plugin is not meant to replace Obsidian, but to complement it._ The Obsidian app is very powerful in its own way; it comes with a mobile From 5079e0a142c8cb3b050e30df4f2d37d5b0fabe24 Mon Sep 17 00:00:00 2001 From: Stefan Otte Date: Sun, 30 Mar 2025 16:17:28 +0200 Subject: [PATCH 33/41] feat: `smart_action` shows tag picker when cursor is on a tag (#36) --- CHANGELOG.md | 1 + README.md | 2 +- doc/obsidian.txt | 2 +- lua/obsidian/util.lua | 5 +++++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e740c11..8a2db4bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Renamed `opts.image_name_func` to `opts.attachments.img_name_func`. - Default to not activate ui render when `render-markdown.nvim` or `markview.nvim` is present +- `smart_action` shows picker for tags (`ObsidianTag`) when cursor is on a tag ### Fixed diff --git a/README.md b/README.md index 1e928092..a03a91c0 100644 --- a/README.md +++ b/README.md @@ -324,7 +324,7 @@ This is a complete list of all of the options that can be passed to `require("ob end, opts = { buffer = true }, }, - -- Smart action depending on context, either follow link or toggle checkbox. + -- Smart action depending on context: follow link, show notes with tag, or toggle checkbox. [""] = { action = function() return require("obsidian").util.smart_action() diff --git a/doc/obsidian.txt b/doc/obsidian.txt index 89960519..c59e5272 100644 --- a/doc/obsidian.txt +++ b/doc/obsidian.txt @@ -385,7 +385,7 @@ carefully and customize it to your needs: end, opts = { buffer = true }, }, - -- Smart action depending on context, either follow link or toggle checkbox. + -- Smart action depending on context: follow link, show notes with tag, or toggle checkbox. [""] = { action = function() return require("obsidian").util.smart_action() diff --git a/lua/obsidian/util.lua b/lua/obsidian/util.lua index 1bd3e6da..39bb336d 100644 --- a/lua/obsidian/util.lua +++ b/lua/obsidian/util.lua @@ -757,6 +757,11 @@ util.smart_action = function() return "ObsidianFollowLink" end + -- show notes with tag if possible + if util.cursor_tag(nil, nil) then + return "ObsidianTag" + end + -- toggle task if possible -- cycles through your custom UI checkboxes, default: [ ] [~] [>] [x] return "ObsidianToggleCheckbox" From 78a02d849ff39e5781d907999000602b5cb5ce6f Mon Sep 17 00:00:00 2001 From: neo451 <111681693+neo451@users.noreply.github.com> Date: Sun, 30 Mar 2025 14:17:50 +0000 Subject: [PATCH 34/41] chore(docs): auto generate docs --- doc/obsidian_api.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/obsidian_api.txt b/doc/obsidian_api.txt index 78297115..00b72f1c 100644 --- a/doc/obsidian_api.txt +++ b/doc/obsidian_api.txt @@ -6,13 +6,13 @@ The Obsidian.nvim Lua API. Table of contents - obsidian.Client............................................|obsidian.Client| + obsidian.Client .......................................... |obsidian.Client| - obsidian.Note................................................|obsidian.Note| + obsidian.Note .............................................. |obsidian.Note| - obsidian.Workspace......................................|obsidian.Workspace| + obsidian.Workspace .................................... |obsidian.Workspace| - obsidian.Path................................................|obsidian.Path| + obsidian.Path .............................................. |obsidian.Path| ------------------------------------------------------------------------------ *obsidian.SearchOpts* From 9879280b6b03c6d005c6792dac74316720ff68ee Mon Sep 17 00:00:00 2001 From: Horia Gug Date: Mon, 31 Mar 2025 08:16:50 +0300 Subject: [PATCH 35/41] Fix: blink.add_provider is deprecated - use add_source_provider instead (#30) * update deprecated method * update for backwards compatibility --- lua/obsidian/completion/plugin_initializers/blink.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/obsidian/completion/plugin_initializers/blink.lua b/lua/obsidian/completion/plugin_initializers/blink.lua index c6f4246e..5d01886f 100644 --- a/lua/obsidian/completion/plugin_initializers/blink.lua +++ b/lua/obsidian/completion/plugin_initializers/blink.lua @@ -12,7 +12,8 @@ M.providers = { } local function add_provider(blink, provider_name, proivder_module) - blink.add_provider(provider_name, { + local add_source_provider = blink.add_source_provider or blink.add_provider + add_source_provider(provider_name, { name = provider_name, module = proivder_module, async = true, From 57742368ed0995d370e2ab1bb3a29ed247223deb Mon Sep 17 00:00:00 2001 From: Sebastian Stark Date: Sun, 6 Apr 2025 17:11:25 +0200 Subject: [PATCH 36/41] Search for templates folder in vault_root first (#46) If your templates folder is configured as e. g. 'Templates' and you happen to have a folder of the same name in $CWD, this should guard against picking the wrong folder. --- lua/obsidian/client.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/obsidian/client.lua b/lua/obsidian/client.lua index b456fbfb..8e11adbd 100644 --- a/lua/obsidian/client.lua +++ b/lua/obsidian/client.lua @@ -266,7 +266,7 @@ Client.templates_dir = function(self, workspace) return nil end - local paths_to_check = { Path.new(opts.templates.folder), self:vault_root(workspace) / opts.templates.folder } + local paths_to_check = { self:vault_root(workspace) / opts.templates.folder, Path.new(opts.templates.folder) } for _, path in ipairs(paths_to_check) do if path:is_dir() then return path From 36df07bfd24874d3b6ce84d89a7dad820249bc58 Mon Sep 17 00:00:00 2001 From: Stefan Otte Date: Wed, 9 Apr 2025 18:11:06 +0200 Subject: [PATCH 37/41] ObsidianToggleCheckbox works with numbered lists (#48) * feat: add types and improve docs for util.toggle_checkbox() * feat: add tests for toggle_checkbox * feat: add util.is_checkbox() (and tests) This is in preparation to refactor and fix toggle_checkbox. Also add doc+typet to util.is_img() * refactor: make toggle_checkbox tests more uniform * feat: add toggle_checkbox test case for lists starting with `*` Note: they currently fail as only `-` lists are supported * feat: toggle_checkbox supports numbered lists (with tests) Use the preciously introduced helper functions. * refactor: toggle_checkbox tests are more explicit Less logic in the tests; easier to read. * doc: changelog * feat: is_checkbox() supports `1) item` and `+ item` With tests again. * feat: more tests for toggle_checkbox() --- CHANGELOG.md | 1 + lua/obsidian/util.lua | 54 +++++++++++++----- test/obsidian/util_spec.lua | 111 ++++++++++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a2db4bc..202533d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Renamed `opts.image_name_func` to `opts.attachments.img_name_func`. - Default to not activate ui render when `render-markdown.nvim` or `markview.nvim` is present - `smart_action` shows picker for tags (`ObsidianTag`) when cursor is on a tag +- `ObsidianToggleCheckbox` now works with numbered lists ### Fixed diff --git a/lua/obsidian/util.lua b/lua/obsidian/util.lua index 39bb336d..a737ea81 100644 --- a/lua/obsidian/util.lua +++ b/lua/obsidian/util.lua @@ -176,6 +176,30 @@ util.escape_magic_characters = function(text) return text:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1") end +---Check if a string is a checkbox list item +--- +---Supported checboox lists: +--- - [ ] foo +--- - [x] foo +--- + [x] foo +--- * [ ] foo +--- 1. [ ] foo +--- 1) [ ] foo +--- +---@param s string +---@return boolean +util.is_checkbox = function(s) + -- - [ ] and * [ ] and + [ ] + if string.match(s, "^%s*[-+*]%s+%[.%]") ~= nil then + return true + end + -- 1. [ ] and 1) [ ] + if string.match(s, "^%s*%d+[%.%)]%s+%[.%]") ~= nil then + return true + end + return false +end + ---Check if a string is a valid URL. ---@param s string ---@return boolean @@ -193,6 +217,10 @@ util.is_url = function(s) end end +---Checks if a given string represents an image file based on its suffix. +--- +---@param s string: The input string to check. +---@return boolean: Returns true if the string ends with a supported image suffix, false otherwise. util.is_img = function(s) for _, suffix in ipairs { ".png", ".jpg", ".jpeg", ".heic", ".gif", ".svg", ".ico" } do if vim.endswith(s, suffix) then @@ -503,32 +531,32 @@ util.zettel_id = function() return tostring(os.time()) .. "-" .. suffix end ----Toggle the checkbox on the line that the cursor is on. +---Toggle the checkbox on the current line. +--- +---@param opts table|nil Optional table containing checkbox states (e.g., {" ", "x"}). +---@param line_num number|nil Optional line number to toggle the checkbox on. Defaults to the current line. util.toggle_checkbox = function(opts, line_num) -- Allow line_num to be optional, defaulting to the current line if not provided line_num = line_num or unpack(vim.api.nvim_win_get_cursor(0)) local line = vim.api.nvim_buf_get_lines(0, line_num - 1, line_num, false)[1] - local checkbox_pattern = "^%s*- %[.] " local checkboxes = opts or { " ", "x" } - if not string.match(line, checkbox_pattern) then + if util.is_checkbox(line) then + for i, check_char in enumerate(checkboxes) do + if string.match(line, "^.* %[" .. util.escape_magic_characters(check_char) .. "%].*") then + i = i % #checkboxes + line = util.string_replace(line, "[" .. check_char .. "]", "[" .. checkboxes[i + 1] .. "]", 1) + break + end + end + else local unordered_list_pattern = "^(%s*)[-*+] (.*)" if string.match(line, unordered_list_pattern) then line = string.gsub(line, unordered_list_pattern, "%1- [ ] %2") else line = string.gsub(line, "^(%s*)", "%1- [ ] ") end - else - for i, check_char in enumerate(checkboxes) do - if string.match(line, "^%s*- %[" .. util.escape_magic_characters(check_char) .. "%].*") then - if i == #checkboxes then - i = 0 - end - line = util.string_replace(line, "- [" .. check_char .. "]", "- [" .. checkboxes[i + 1] .. "]", 1) - break - end - end end -- 0-indexed vim.api.nvim_buf_set_lines(0, line_num - 1, line_num, true, { line }) diff --git a/test/obsidian/util_spec.lua b/test/obsidian/util_spec.lua index c4cb7e0e..3bcc10ee 100644 --- a/test/obsidian/util_spec.lua +++ b/test/obsidian/util_spec.lua @@ -389,3 +389,114 @@ describe("util.markdown_link()", function() ) end) end) + +describe("util.toggle_checkbox", function() + before_each(function() + vim.cmd "bwipeout!" -- wipe out the buffer to avoid unsaved changes + vim.cmd "enew" -- create a new empty buffer + vim.bo.bufhidden = "wipe" -- and wipe it after use + end) + + it("should toggle between default states with - lists", function() + vim.api.nvim_buf_set_lines(0, 0, -1, false, { "- [ ] dummy" }) + local custom_states = nil + + util.toggle_checkbox(custom_states) + assert.equals("- [x] dummy", vim.api.nvim_get_current_line()) + + util.toggle_checkbox(custom_states) + assert.equals("- [ ] dummy", vim.api.nvim_get_current_line()) + end) + + it("should toggle between default states with * lists", function() + vim.api.nvim_buf_set_lines(0, 0, -1, false, { "* [ ] dummy" }) + local custom_states = nil + + util.toggle_checkbox(custom_states) + assert.equals("* [x] dummy", vim.api.nvim_get_current_line()) + + util.toggle_checkbox(custom_states) + assert.equals("* [ ] dummy", vim.api.nvim_get_current_line()) + end) + + it("should toggle between default states with numbered lists with .", function() + vim.api.nvim_buf_set_lines(0, 0, -1, false, { "1. [ ] dummy" }) + local custom_states = nil + + util.toggle_checkbox(custom_states) + assert.equals("1. [x] dummy", vim.api.nvim_get_current_line()) + + util.toggle_checkbox(custom_states) + assert.equals("1. [ ] dummy", vim.api.nvim_get_current_line()) + end) + + it("should toggle between default states with numbered lists with )", function() + vim.api.nvim_buf_set_lines(0, 0, -1, false, { "1) [ ] dummy" }) + local custom_states = nil + + util.toggle_checkbox(custom_states) + assert.equals("1) [x] dummy", vim.api.nvim_get_current_line()) + + util.toggle_checkbox(custom_states) + assert.equals("1) [ ] dummy", vim.api.nvim_get_current_line()) + end) + + it("should use custom states if provided", function() + local custom_states = { " ", "!", "x" } + vim.api.nvim_buf_set_lines(0, 0, -1, false, { "- [ ] dummy" }) + + util.toggle_checkbox(custom_states) + assert.equals("- [!] dummy", vim.api.nvim_get_current_line()) + + util.toggle_checkbox(custom_states) + assert.equals("- [x] dummy", vim.api.nvim_get_current_line()) + + util.toggle_checkbox(custom_states) + assert.equals("- [ ] dummy", vim.api.nvim_get_current_line()) + + util.toggle_checkbox(custom_states) + assert.equals("- [!] dummy", vim.api.nvim_get_current_line()) + end) +end) + +describe("util.is_checkbox", function() + it("should return true for valid checkbox list items", function() + assert.is_true(util.is_checkbox "- [ ] Task 1") + assert.is_true(util.is_checkbox "- [x] Task 1") + assert.is_true(util.is_checkbox "+ [ ] Task 1") + assert.is_true(util.is_checkbox "+ [x] Task 1") + assert.is_true(util.is_checkbox "* [ ] Task 2") + assert.is_true(util.is_checkbox "* [x] Task 2") + assert.is_true(util.is_checkbox "1. [ ] Task 3") + assert.is_true(util.is_checkbox "1. [x] Task 3") + assert.is_true(util.is_checkbox "2. [ ] Task 3") + assert.is_true(util.is_checkbox "10. [ ] Task 3") + assert.is_true(util.is_checkbox "1) [ ] Task") + assert.is_true(util.is_checkbox "10) [ ] Task") + end) + + it("should return false for non-checkbox list items", function() + assert.is_false(util.is_checkbox "- Task 1") + assert.is_false(util.is_checkbox "-- Task 1") + assert.is_false(util.is_checkbox "-- [ ] Task 1") + assert.is_false(util.is_checkbox "* Task 2") + assert.is_false(util.is_checkbox "++ [ ] Task 2") + assert.is_false(util.is_checkbox "1. Task 3") + assert.is_false(util.is_checkbox "1.1 Task 3") + assert.is_false(util.is_checkbox "1.1 [ ] Task 3") + assert.is_false(util.is_checkbox "1)1 Task 3") + assert.is_false(util.is_checkbox "Random text") + end) + + it("should handle leading spaces correctly", function() + -- true + assert.is_true(util.is_checkbox " - [ ] Task 1") + assert.is_true(util.is_checkbox " * [ ] Task 2") + assert.is_true(util.is_checkbox " 5. [ ] Task 2") + + -- false + assert.is_false(util.is_checkbox " - Task 1") + assert.is_false(util.is_checkbox " * Task 1") + assert.is_false(util.is_checkbox " 1. Task 1") + end) +end) From e3c14f27ccd48a59f7954136214fac6239744921 Mon Sep 17 00:00:00 2001 From: neo451 <111681693+neo451@users.noreply.github.com> Date: Sat, 12 Apr 2025 02:47:26 +0800 Subject: [PATCH 38/41] feat: healthcheck and minimal_sandbox (#50) --- CHANGELOG.md | 121 ++++++++++++++++++++-------------------- README.md | 6 ++ lua/obsidian/health.lua | 90 ++++++++++++++++++++++++++++++ minimal.lua | 61 ++++++++++++++++++++ 4 files changed, 219 insertions(+), 59 deletions(-) create mode 100644 lua/obsidian/health.lua create mode 100644 minimal.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 202533d0..2260b610 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) with respect to the public API, which currently includes the installation steps, dependencies, configuration, keymappings, commands, and other plugin functionality. At the moment this does *not* include the Lua `Client` API, although in the future it will once that API stabilizes. +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) with respect to the public API, which currently includes the installation steps, dependencies, configuration, keymappings, commands, and other plugin functionality. At the moment this does _not_ include the Lua `Client` API, although in the future it will once that API stabilizes. ## Unreleased @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added better handling for undefined template fields, which will now be prompted for. - Added support for the [`snacks.picker`](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md) picker - Added support for the [`blink.cmp`](https://github.com/Saghen/blink.cmp) completion plugin. +- Added health check module +- Added a minimal sandbox script `minimal.lua` ### Changed @@ -198,11 +200,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 There's a lot of new features and improvements here that I'm really excited about 🥳 They've improved my workflow a ton and I hope they do for you too. To highlight the 3 biggest additions: 1. 🔗 Full support for header anchor links and block links! That means both for following links and completion of links. Various forms of anchor/block links are support. Here are a few examples: - - Typical Obsidian-style wiki links, e.g. `[[My note#Heading 1]]`, `[[My note#Heading 1#Sub heading]]`, `[[My note#^block-123]]`. - - Wiki links with a label, e.g. `[[my-note#heading-1|Heading 1 in My Note]]`. - - Markdown links, e.g. `[Heading 1 in My Note](my-note.md#heading-1)`. - We also support links to headers within the same note, like for a table of contents, e.g. `[[#Heading 1]]`, `[[#heading-1|Heading]]`, `[[#^block-1]]`. + - Typical Obsidian-style wiki links, e.g. `[[My note#Heading 1]]`, `[[My note#Heading 1#Sub heading]]`, `[[My note#^block-123]]`. + - Wiki links with a label, e.g. `[[my-note#heading-1|Heading 1 in My Note]]`. + - Markdown links, e.g. `[Heading 1 in My Note](my-note.md#heading-1)`. + + We also support links to headers within the same note, like for a table of contents, e.g. `[[#Heading 1]]`, `[[#heading-1|Heading]]`, `[[#^block-1]]`. 2. 📲 A basic callback system to let you easily customize obisidian.nvim's behavior even more. There are currently 4 events: `post_setup`, `enter_note`, `pre_write_note`, and `post_set_workspace`. You can define a function for each of these in your config. 3. 🔭 Improved picker integrations (especially for telescope), particular for the `:ObsidianTags` command. See for a demo. @@ -213,44 +216,44 @@ Full changelog below 👇 - Added a configurable callback system to further customize obsidian.nvim's behavior. Callbacks are defined through the `callbacks` field in the config: - ```lua - callbacks = { - -- Runs at the end of `require("obsidian").setup()`. - ---@param client obsidian.Client - post_setup = function(client) end, - - -- Runs anytime you enter the buffer for a note. - ---@param client obsidian.Client - ---@param note obsidian.Note - enter_note = function(client, note) end, - - -- Runs anytime you leave the buffer for a note. - ---@param client obsidian.Client - ---@param note obsidian.Note - leave_note = function(client, note) end, - - -- Runs right before writing the buffer for a note. - ---@param client obsidian.Client - ---@param note obsidian.Note - pre_write_note = function(client, note) end, - - -- Runs anytime the workspace is set/changed. - ---@param client obsidian.Client - ---@param workspace obsidian.Workspace - post_set_workspace = function(client, workspace) end, - } - ``` + ```lua + callbacks = { + -- Runs at the end of `require("obsidian").setup()`. + ---@param client obsidian.Client + post_setup = function(client) end, + + -- Runs anytime you enter the buffer for a note. + ---@param client obsidian.Client + ---@param note obsidian.Note + enter_note = function(client, note) end, + + -- Runs anytime you leave the buffer for a note. + ---@param client obsidian.Client + ---@param note obsidian.Note + leave_note = function(client, note) end, + + -- Runs right before writing the buffer for a note. + ---@param client obsidian.Client + ---@param note obsidian.Note + pre_write_note = function(client, note) end, + + -- Runs anytime the workspace is set/changed. + ---@param client obsidian.Client + ---@param workspace obsidian.Workspace + post_set_workspace = function(client, workspace) end, + } + ``` - Added configuration option `note_path_func(spec): obsidian.Path` for customizing how file names for new notes are generated. This takes a single argument, a table that looks like `{ id: string, dir: obsidian.Path, title: string|? }`, and returns an `obsidian.Path` object. The default behavior is equivalent to this: - ```lua - ---@param spec { id: string, dir: obsidian.Path, title: string|? } - ---@return string|obsidian.Path The full path to the new note. - note_path_func = function(spec) - local path = spec.dir / tostring(spec.id) - return path:with_suffix(".md") - end - ``` + ```lua + ---@param spec { id: string, dir: obsidian.Path, title: string|? } + ---@return string|obsidian.Path The full path to the new note. + note_path_func = function(spec) + local path = spec.dir / tostring(spec.id) + return path:with_suffix ".md" + end + ``` - Added config option `picker.tag_mappings`, analogous to `picker.note_mappings`. - Added `log` field to `obsidian.Client` for easier access to the logger. @@ -500,7 +503,7 @@ Minor internal improvements. ### Added -- Added extmarks that conceal "-", "*", or "+" with "•" by default. This can turned off by setting `.ui.bullets` to `nil` in your config. +- Added extmarks that conceal "-", "\*", or "+" with "•" by default. This can turned off by setting `.ui.bullets` to `nil` in your config. ### Fixed @@ -556,26 +559,26 @@ Minor internal improvements. - Added Lua API methods `Client:set_workspace(workspace: obsidian.Workspace)` and `Client:switch_workspace(workspace: string|obsidian.Workspace)`. - Added the ability to override settings per workspace by providing the `overrides` field in a workspace definition. For example: - ```lua - require("obsidian").setup({ - workspaces = { - { - name = "personal", - path = "~/vaults/personal", - }, - { - name = "work", - path = "~/vaults/work", - -- Optional, override certain settings. - overrides = { - notes_subdir = "notes", - }, + ```lua + require("obsidian").setup { + workspaces = { + { + name = "personal", + path = "~/vaults/personal", + }, + { + name = "work", + path = "~/vaults/work", + -- Optional, override certain settings. + overrides = { + notes_subdir = "notes", }, }, + }, - -- ... other options ... - }) - ``` + -- ... other options ... + } + ``` ### Fixed @@ -907,7 +910,7 @@ Major internal refactoring to bring performance improvements through async execu ### Added - Added support for [fzf-lua](https://github.com/ibhagwan/fzf-lua) as one of the possible fallbacks for the `:ObsidianQuickSwitch` command. -- Added `:ObsidianQuickSwitch` to fuzzy-find a note by name in telescope/fzf *a la* in Obsidian. +- Added `:ObsidianQuickSwitch` to fuzzy-find a note by name in telescope/fzf _a la_ in Obsidian. - Added support for [fzf-lua](https://github.com/ibhagwan/fzf-lua) as one of the possible fallbacks for the `:ObsidianSearch` command. - Added `:ObsidianFollowLink` and companion function `util.cursor_on_markdown_link` - Added `:ObsidianLink` and `:ObsidianLinkNew` commands. diff --git a/README.md b/README.md index a03a91c0..bccbd7b1 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ If you're new to Obsidian we highly recommend watching [this excellent YouTube v _Keep in mind this plugin is not meant to replace Obsidian, but to complement it._ The Obsidian app is very powerful in its own way; it comes with a mobile app and has a lot of functionality that's not feasible to implement in Neovim, such as the graph explorer view. That said, this plugin stands on its own as well. You don't necessarily need to use it alongside the Obsidian app. ## About the fork + The original project has not been actively maintained for quite a while and with the ever-changing Neovim ecosystem, new widely used tools such as [blink.cmp](https://github.com/Saghen/blink.cmp) or [snacks.picker](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md) were not supported. With bugs, issues and pull requests piling up, people from the community decided to fork and maintain the project. The fork aims to stay close to the original, but fix bugs, include and merge useful improvements, and ensure long term robustness. @@ -123,6 +124,10 @@ Here are some examples using different plugin managers. The full set of [plugin > ⚠️ WARNING: if you install from the latest release (recommended for stability) instead of `main`, be aware that the README on `main` may reference features that haven't been released yet. For that reason I recommend viewing the README on the tag for the [latest release](https://github.com/obsidian-nvim/obsidian.nvim/releases) instead of `main`. +> [!NOTE] +> To see you installation status, run `:checkhealth obsidian` +> To try out or debug this plugin, use `minimal.lua` in the repo to run a clean instance of obsidian.nvim + #### Using [`lazy.nvim`](https://github.com/folke/lazy.nvim)
Click for install snippet @@ -807,4 +812,5 @@ And keep in mind that to reset a configuration option to `nil` you'll have to us Please read the [CONTRIBUTING](https://github.com/obsidian-nvim/obsidian.nvim/blob/main/.github/CONTRIBUTING.md) guide before submitting a pull request. ## Acknowledgement + We would like to thank [epwalsh](https://github.com/epwalsh) for creating this beautiful plugin. If you're feeling especially generous, [he still appreciates some coffee funds! ❤️](https://www.buymeacoffee.com/epwalsh). diff --git a/lua/obsidian/health.lua b/lua/obsidian/health.lua new file mode 100644 index 00000000..e6bd07a8 --- /dev/null +++ b/lua/obsidian/health.lua @@ -0,0 +1,90 @@ +local M = {} +local VERSION = require "obsidian.version" +local util = require "obsidian.util" + +local error = vim.health.error +local warn = vim.health.warn +local ok = vim.health.ok + +local function info(...) + local t = { ... } + local format = table.remove(t, 1) + local str = #t == 0 and format or string.format(format, unpack(t)) + return ok(str) +end + +---@private +---@param name string +local function start(name) + vim.health.start(string.format("obsidian.nvim [%s]", name)) +end + +---@param plugin string +---@param optional boolean +---@return boolean +local function has_plugin(plugin, optional) + local plugin_info = util.get_plugin_info(plugin) + if plugin_info then + info(" ✓ %s: %s", plugin, plugin_info.commit or "unknown") + return true + else + if not optional then + vim.health.error(" " .. plugin .. " not installed") + end + return false + end +end + +---@param plugins string[] +local function has_one_of(plugins) + local found + for _, plugin in ipairs(plugins) do + if has_plugin(plugin, true) then + found = true + end + end + if not found then + vim.health.warn("It is recommended to install at least one of " .. vim.inspect(plugins)) + end +end + +---@param minimum string +---@param recommended string +local function neovim(minimum, recommended) + if vim.fn.has("nvim-" .. minimum) == 0 then + error("neovim < " .. minimum) + elseif vim.fn.has("nvim-" .. recommended) == 0 then + warn("neovim < " .. recommended .. " some features will not work") + else + ok("neovim >= " .. recommended) + end +end + +function M.check() + neovim("0.8", "0.11") + start "Version" + info("Obsidian.nvim v%s (%s)", VERSION, util.get_plugin_info("obsidian.nvim").commit) + + start "Pickers" + + has_one_of { + "telescope.nvim", + "fzf-lua", + "mini.nvim", + "mini.pick", + "snacks.nvim", + } + + start "Completion" + + has_one_of { + "nvim-cmp", + "blink.cmp", + } + + start "Dependencies" + info(" ✓ rg: %s", util.get_external_dependency_info "rg" or "not found") + has_plugin("plenary.nvim", false) +end + +return M diff --git a/minimal.lua b/minimal.lua new file mode 100644 index 00000000..74fa4410 --- /dev/null +++ b/minimal.lua @@ -0,0 +1,61 @@ +vim.env.LAZY_STDPATH = ".repro" +load(vim.fn.system "curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua")() + +vim.fn.mkdir(".repro/vault", "p") + +vim.o.conceallevel = 2 + +local plugins = { + { + "obsidian-nvim/obsidian.nvim", + dependencies = { "nvim-lua/plenary.nvim" }, + opts = { + completion = { + blink = true, + nvim_cmp = false, + }, + workspaces = { + { + name = "test", + path = vim.fs.joinpath(vim.uv.cwd(), ".repro", "vault"), + }, + }, + }, + }, + + -- **Choose your renderer** + { "MeanderingProgrammer/render-markdown.nvim", dependencies = { "echasnovski/mini.icons" }, opts = {} }, + -- { "OXY2DEV/markview.nvim", lazy = false }, + + -- **Choose your picker** + "nvim-telescope/telescope.nvim", + -- "folke/snacks.nvim", + -- "ibhagwan/fzf-lua", + -- "echasnovski/mini.pick", + + { + "hrsh7th/nvim-cmp", + config = function() + local cmp = require "cmp" + cmp.setup { + mapping = cmp.mapping.preset.insert { + [""] = cmp.mapping.abort(), + [""] = cmp.mapping.confirm { select = true }, + }, + } + end, + }, + { + "saghen/blink.cmp", + opts = { + fuzzy = { implementation = "lua" }, -- no need to build binary + keymap = { + preset = "default", + }, + }, + }, +} + +require("lazy.minit").repro { spec = plugins } + +vim.cmd "checkhealth obsidian" From 80a483c2eebac1afa6e40c4a46ffae2cc538e1cc Mon Sep 17 00:00:00 2001 From: neo451 <111681693+neo451@users.noreply.github.com> Date: Fri, 11 Apr 2025 18:47:46 +0000 Subject: [PATCH 39/41] chore(docs): auto generate docs --- doc/obsidian.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/obsidian.txt b/doc/obsidian.txt index c59e5272..5648bb69 100644 --- a/doc/obsidian.txt +++ b/doc/obsidian.txt @@ -180,6 +180,10 @@ managers. The full set of |obsidian-plugin-dependencies| and the README on the tag for the latest release instead of `main`. + [!NOTE] To see you installation status, run `:checkhealth obsidian` To try out + or debug this plugin, use `minimal.lua` in the repo to run a clean instance of + obsidian.nvim + USING LAZY.NVIM ~ Click for install snippet ~ From 32c7ffd131644386e5ba2f204cb3f7e497ab6136 Mon Sep 17 00:00:00 2001 From: Stefan Otte Date: Sat, 12 Apr 2025 17:02:42 +0200 Subject: [PATCH 40/41] feat: makefile is friendlier (#49) * feat: makefile is friendlier - it is self documenting: just call `make` to see all targets - it gets missing dependencies * fix(CICD): adjust github CI to new makefile logic --- .github/workflows/docs.yml | 7 ++---- CHANGELOG.md | 1 + Makefile | 48 ++++++++++++++++++++++++++++---------- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e1313d5b..c10877e3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,7 +3,7 @@ name: Docs on: pull_request: branches: - - '*' + - "*" push: branches: [main] workflow_dispatch: @@ -14,8 +14,6 @@ concurrency: env: runtime: ~/.local/share/nvim/site/pack/vendor/start - minidoc-git: https://github.com/echasnovski/mini.doc - minidoc-path: ~/.local/share/nvim/site/pack/vendor/start/mini.doc nvim_url: https://github.com/neovim/neovim/releases/download/nightly/nvim-linux-x86_64.tar.gz jobs: @@ -38,7 +36,6 @@ jobs: mkdir -p ${{ env.runtime }} mkdir -p _neovim curl -sL ${{ env.nvim_url }} | tar xzf - --strip-components=1 -C "${PWD}/_neovim" - git clone --depth 1 ${{ env.minidoc-git }} ${{ env.minidoc-path }} ln -s $(pwd) ${{ env.runtime }} - name: Generate API docs @@ -74,5 +71,5 @@ jobs: if: github.event_name != 'pull_request' with: commit_user_name: github-actions[bot] - commit_message: 'chore(docs): auto generate docs' + commit_message: "chore(docs): auto generate docs" branch: ${{ github.head_ref }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 2260b610..b774516e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Default to not activate ui render when `render-markdown.nvim` or `markview.nvim` is present - `smart_action` shows picker for tags (`ObsidianTag`) when cursor is on a tag - `ObsidianToggleCheckbox` now works with numbered lists +- `Makefile` is friendlier: self-documenting and automatically gets dependencies ### Fixed diff --git a/Makefile b/Makefile index 78516378..399458d4 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,31 @@ +SHELL:=/usr/bin/env bash + +.DEFAULT_GOAL:=help +PROJECT_NAME = "obsidian.nvim" TEST = test/obsidian -# This is where you have plenary installed locally. Override this at runtime if yours is elsewhere. +# Depending on your setup you have to override the locations at runtime. PLENARY = ~/.local/share/nvim/lazy/plenary.nvim/ MINIDOC = ~/.local/share/nvim/lazy/mini.doc/ -.PHONY : all -all : style lint test -.PHONY : test -test : +################################################################################ +##@ Developmment +.PHONY: chores +chores: style lint test ## Run all develoment tasks + +.PHONY: test +test: $(PLENARY) ## Run unit tests PLENARY=$(PLENARY) nvim \ --headless \ --noplugin \ -u test/minimal_init.vim \ -c "PlenaryBustedDirectory $(TEST) { minimal_init = './test/minimal_init.vim' }" +$(PLENARY): + git clone --depth 1 https://github.com/nvim-lua/plenary.nvim.git $(PLENARY) + .PHONY: api-docs -api-docs : +api-docs: $(MINIDOC) ## Generate API documentation with mini.doc MINIDOC=$(MINIDOC) nvim \ --headless \ --noplugin \ @@ -23,14 +33,28 @@ api-docs : -c "luafile scripts/generate_api_docs.lua" \ -c "qa!" -.PHONY : lint -lint : +$(MINIDOC): + git clone --depth 1 https://github.com/echasnovski/mini.doc $(MINIDOC) + +.PHONY: lint +lint: ## Lint the code luacheck . -.PHONY : style -style : +.PHONY: style +style: ## format the code stylua --check . -.PHONY : version -version : + +################################################################################ +##@ Helpers +.PHONY: version +version: ## Print the obsidian.nvim version @nvim --headless -c 'lua print("v" .. require("obsidian").VERSION)' -c q 2>&1 + +.PHONY: help +help: ## Display this help + @echo "Welcome to $$(tput bold)${PROJECT_NAME}$$(tput sgr0) 🥳📈🎉" + @echo "" + @echo "To get started:" + @echo " >>> $$(tput bold)make chores$$(tput sgr0)" + @awk 'BEGIN {FS = ":.*##"; printf "\033[36m\033[0m"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) From e0eb92c5afcacf5bf11e4735079a538fd1486ea9 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Sat, 12 Apr 2025 23:22:02 +0800 Subject: [PATCH 41/41] chore(release): bump version to v3.10.0 for release --- CHANGELOG.md | 2 ++ lua/obsidian/version.lua | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b774516e..25e6868c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## [v3.10.0](https://github.com/obsidian-nvim/obsidian.nvim/releases/tag/v3.10.0) - 2025-04-12 + ### Added - Added `opts.follow_img_func` option for customizing how to handle image paths. diff --git a/lua/obsidian/version.lua b/lua/obsidian/version.lua index 96d97ee7..8123fcca 100644 --- a/lua/obsidian/version.lua +++ b/lua/obsidian/version.lua @@ -1 +1 @@ -return "3.9.0" +return "3.10.0"