From 339b15af237d866a0f6b89e207fd8342d93ccbcc Mon Sep 17 00:00:00 2001 From: neo451 <412444506@qq.com> Date: Mon, 3 Nov 2025 23:05:15 +0000 Subject: [PATCH 01/32] feat: prototype for code action --- lua/obsidian/lsp/commands/add_file_property.lua | 16 ++++++++++++++++ lua/obsidian/lsp/handlers.lua | 2 ++ lua/obsidian/lsp/handlers/code_action.lua | 13 +++++++++++++ lua/obsidian/lsp/handlers/execute_command.lua | 11 +++++++++++ lua/obsidian/lsp/handlers/initialize.lua | 6 ++++++ 5 files changed, 48 insertions(+) create mode 100644 lua/obsidian/lsp/commands/add_file_property.lua create mode 100644 lua/obsidian/lsp/handlers/code_action.lua create mode 100644 lua/obsidian/lsp/handlers/execute_command.lua diff --git a/lua/obsidian/lsp/commands/add_file_property.lua b/lua/obsidian/lsp/commands/add_file_property.lua new file mode 100644 index 000000000..dd5e1dfb4 --- /dev/null +++ b/lua/obsidian/lsp/commands/add_file_property.lua @@ -0,0 +1,16 @@ +local obsidian = require "obsidian" + +return function() + local note = assert(obsidian.api.current_note(0)) + + local key = obsidian.api.input "key: " + local value = obsidian.api.input "value: " + + ---@diagnostic disable-next-line: param-type-mismatch + if (not key or not value) and (vim.trim(key) ~= "" and vim.trim(value) ~= "") then + return obsidian.log "Aborted" + end + + note:add_field(key, value) + note:update_frontmatter(0) +end diff --git a/lua/obsidian/lsp/handlers.lua b/lua/obsidian/lsp/handlers.lua index 4fb3c5776..bc2b696ee 100644 --- a/lua/obsidian/lsp/handlers.lua +++ b/lua/obsidian/lsp/handlers.lua @@ -8,4 +8,6 @@ return { ["textDocument/references"] = require "obsidian.lsp.handlers.references", ["textDocument/definition"] = require "obsidian.lsp.handlers.definition", ["textDocument/documentSymbol"] = require "obsidian.lsp.handlers.document_symbol", + ["workspace/executeCommand"] = require "obsidian.lsp.handlers.execute_command", + ["textDocument/codeAction"] = require "obsidian.lsp.handlers.code_action", } diff --git a/lua/obsidian/lsp/handlers/code_action.lua b/lua/obsidian/lsp/handlers/code_action.lua new file mode 100644 index 000000000..1894013fe --- /dev/null +++ b/lua/obsidian/lsp/handlers/code_action.lua @@ -0,0 +1,13 @@ +---@param params lsp.CodeActionParams +return function(params, handler) + handler(nil, { + { + title = "add_file_property", + command = { + title = "add_file_property", + command = "add_file_property", + -- arguments + }, + }, + }) +end diff --git a/lua/obsidian/lsp/handlers/execute_command.lua b/lua/obsidian/lsp/handlers/execute_command.lua new file mode 100644 index 000000000..f4b3475ff --- /dev/null +++ b/lua/obsidian/lsp/handlers/execute_command.lua @@ -0,0 +1,11 @@ +---@param params lsp.ExecuteCommandParams +return function(params) + local cmd = params.command + + local ok, fn = pcall(require, "obsidian.lsp.commands." .. cmd) + if ok and fn then + fn() + end + -- return require("obsidian.lsp.handlers.commands." .. cmd)(client, params) + -- return require "obsidian.lsp.handlers.commands.createNote"(client, params) +end diff --git a/lua/obsidian/lsp/handlers/initialize.lua b/lua/obsidian/lsp/handlers/initialize.lua index be7cb405c..8c24b8431 100644 --- a/lua/obsidian/lsp/handlers/initialize.lua +++ b/lua/obsidian/lsp/handlers/initialize.lua @@ -34,6 +34,12 @@ local initializeResult = { }, }, }, + codeActionProvider = true, + executeCommandProvider = { + commands = { + "add_file_property", + }, + }, }, serverInfo = { name = "obsidian-ls", From 5445b9b43939a57061ee5a90c1f039970eb358cb Mon Sep 17 00:00:00 2001 From: neo451 <412444506@qq.com> Date: Tue, 4 Nov 2025 00:04:33 +0000 Subject: [PATCH 02/32] minor --- lua/obsidian/lsp/commands/add_file_property.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lua/obsidian/lsp/commands/add_file_property.lua b/lua/obsidian/lsp/commands/add_file_property.lua index dd5e1dfb4..6c8543f55 100644 --- a/lua/obsidian/lsp/commands/add_file_property.lua +++ b/lua/obsidian/lsp/commands/add_file_property.lua @@ -6,11 +6,14 @@ return function() local key = obsidian.api.input "key: " local value = obsidian.api.input "value: " - ---@diagnostic disable-next-line: param-type-mismatch - if (not key or not value) and (vim.trim(key) ~= "" and vim.trim(value) ~= "") then + if not (key and value) then return obsidian.log "Aborted" end + if vim.trim(key) ~= "" and vim.trim(value) ~= "" then + return obsidian.log "Empty Input" + end + note:add_field(key, value) note:update_frontmatter(0) end From 5d89cecd175da6245170097b876353227ac9393f Mon Sep 17 00:00:00 2001 From: neo451 <412444506@qq.com> Date: Wed, 5 Nov 2025 01:36:38 +0000 Subject: [PATCH 03/32] fix: wrong call --- lua/obsidian/lsp/commands/add_file_property.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/obsidian/lsp/commands/add_file_property.lua b/lua/obsidian/lsp/commands/add_file_property.lua index 6c8543f55..25967e075 100644 --- a/lua/obsidian/lsp/commands/add_file_property.lua +++ b/lua/obsidian/lsp/commands/add_file_property.lua @@ -7,11 +7,11 @@ return function() local value = obsidian.api.input "value: " if not (key and value) then - return obsidian.log "Aborted" + return obsidian.log.info "Aborted" end if vim.trim(key) ~= "" and vim.trim(value) ~= "" then - return obsidian.log "Empty Input" + return obsidian.log.info "Empty Input" end note:add_field(key, value) From 7ed2fce64583b49bff4fca6d8a3039f1df8bbfaf Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Sun, 7 Dec 2025 00:08:04 +0000 Subject: [PATCH 04/32] feat: properly set architecture for code action --- lua/obsidian/lsp/commands/extract_note.lua | 2 + lua/obsidian/lsp/commands/link.lua | 2 + lua/obsidian/lsp/commands/link_new.lua | 66 +++++++++++++++++++ lua/obsidian/lsp/handlers/_code_action.lua | 49 ++++++++++++++ lua/obsidian/lsp/handlers/code_action.lua | 41 +++++++++--- lua/obsidian/lsp/handlers/execute_command.lua | 6 +- lua/obsidian/lsp/handlers/initialize.lua | 5 +- 7 files changed, 157 insertions(+), 14 deletions(-) create mode 100644 lua/obsidian/lsp/commands/extract_note.lua create mode 100644 lua/obsidian/lsp/commands/link.lua create mode 100644 lua/obsidian/lsp/commands/link_new.lua create mode 100644 lua/obsidian/lsp/handlers/_code_action.lua diff --git a/lua/obsidian/lsp/commands/extract_note.lua b/lua/obsidian/lsp/commands/extract_note.lua new file mode 100644 index 000000000..f2bdb436b --- /dev/null +++ b/lua/obsidian/lsp/commands/extract_note.lua @@ -0,0 +1,2 @@ +local obsidian = require "obsidian" +return obsidian.api.extract_note diff --git a/lua/obsidian/lsp/commands/link.lua b/lua/obsidian/lsp/commands/link.lua new file mode 100644 index 000000000..8ed7031c1 --- /dev/null +++ b/lua/obsidian/lsp/commands/link.lua @@ -0,0 +1,2 @@ +local obsidian = require "obsidian" +return obsidian.api.link diff --git a/lua/obsidian/lsp/commands/link_new.lua b/lua/obsidian/lsp/commands/link_new.lua new file mode 100644 index 000000000..8a6d9910e --- /dev/null +++ b/lua/obsidian/lsp/commands/link_new.lua @@ -0,0 +1,66 @@ +local obsidian = require "obsidian" +local has_nvim_0_12 = vim.fn.has "nvim-0.12.0" == 1 + +-- -- TODO: neovim's visual selection is weird +-- local label = vim.api.nvim_buf_get_text( +-- 0, +-- range.start.line, +-- range.start.character, +-- range["end"].line, +-- range["end"].character, +-- {} +-- )[1] +-- + +return { + ---@param range lsp.Range + ---@return lsp.WorkspaceEdit? + edit = function(range) + -- print(range.start.line, range["end"].line) + -- if range.start.line ~= range["end"].line then + -- obsidian.log.err "Only in-line visual selections allowed" + -- return + -- end + -- + -- require("obsidian.note").create { title = label } + -- obsidian.api.make_text_edit() + -- return { + -- documentChanges = { + -- { + -- textDocument = { + -- uri = vim.uri_from_fname(vim.api.nvim_buf_get_name(0)), + -- version = has_nvim_0_12 and vim.NIL or nil, + -- }, + -- edits = { + -- { + -- range = range, + -- newText = label, + -- }, + -- }, + -- }, + -- }, + -- } + local label + + local viz = obsidian.api.get_visual_selection() + if not viz then + obsidian.log.err "`Obsidian link_new` must be called in visual mode" + return + elseif #viz.lines ~= 1 then + obsidian.log.err "Only in-line visual selections allowed" + return + end + + if not label or string.len(label) <= 0 then + label = viz.selection + end + + local note = require("obsidian.note").create { title = label } + local text_edit = obsidian.api.make_text_edit(viz, note:format_link { label = label }) + return { documentChanges = { text_edit } } + end, + command = function() + -- Save file so backlinks search (ripgrep) can find the new link + vim.cmd "silent! write" -- HACK: + end, +} diff --git a/lua/obsidian/lsp/handlers/_code_action.lua b/lua/obsidian/lsp/handlers/_code_action.lua new file mode 100644 index 000000000..52197fbab --- /dev/null +++ b/lua/obsidian/lsp/handlers/_code_action.lua @@ -0,0 +1,49 @@ +local commands = {} + +---@type lsp.CodeAction[] +local actions = {} + +---Register a new command. +---@param name string +---@param config table +local register = function(name, config) + local mod = require("obsidian.lsp.commands." .. name) + actions[#actions + 1] = { + title = config.title, + command = { + title = config.title, + command = name, + -- TODO: kind + }, + data = { + range = config.range, + func = type(mod) == "table" and mod.command or mod, + edit = type(mod) == "table" and mod.edit or nil, + }, + } + commands[#commands + 1] = name +end + +register("add_file_property", { + title = "Add file property", +}) + +register("link", { + title = "Link selection as name for a existing note", + range = true, +}) + +register("link_new", { + title = "Link selection as name for a new note", + range = true, +}) + +register("extract_note", { + title = "Extract selected text to a new note", + range = true, +}) + +return { + commands = commands, + actions = actions, +} diff --git a/lua/obsidian/lsp/handlers/code_action.lua b/lua/obsidian/lsp/handlers/code_action.lua index 1894013fe..fa27ec2f8 100644 --- a/lua/obsidian/lsp/handlers/code_action.lua +++ b/lua/obsidian/lsp/handlers/code_action.lua @@ -1,13 +1,34 @@ +local actions = require("obsidian.lsp.handlers._code_action").actions + +---@param acts lsp.CodeAction[] +---@param in_selection boolean +---@return string[] +local function get_commands_by_context(acts, in_selection) + return vim + .iter(acts) + :filter(function(act) + if in_selection then + return act.data.range ~= nil + else + return act.data.range == nil + end + end) + :totable() +end + ---@param params lsp.CodeActionParams return function(params, handler) - handler(nil, { - { - title = "add_file_property", - command = { - title = "add_file_property", - command = "add_file_property", - -- arguments - }, - }, - }) + local range = params.range + + local in_selection = range.start ~= range["end"] + + local res = get_commands_by_context(actions, in_selection) + + vim.tbl_map(function(act) + if act.data.edit and in_selection then + act.edit = act.data.edit(range) + end + end, res) + + handler(nil, res) end diff --git a/lua/obsidian/lsp/handlers/execute_command.lua b/lua/obsidian/lsp/handlers/execute_command.lua index f4b3475ff..2e794aa2a 100644 --- a/lua/obsidian/lsp/handlers/execute_command.lua +++ b/lua/obsidian/lsp/handlers/execute_command.lua @@ -4,7 +4,11 @@ return function(params) local ok, fn = pcall(require, "obsidian.lsp.commands." .. cmd) if ok and fn then - fn() + if type(fn) == "table" then + pcall(fn.command) + else + pcall(fn) + end end -- return require("obsidian.lsp.handlers.commands." .. cmd)(client, params) -- return require "obsidian.lsp.handlers.commands.createNote"(client, params) diff --git a/lua/obsidian/lsp/handlers/initialize.lua b/lua/obsidian/lsp/handlers/initialize.lua index 8c24b8431..b35520614 100644 --- a/lua/obsidian/lsp/handlers/initialize.lua +++ b/lua/obsidian/lsp/handlers/initialize.lua @@ -8,6 +8,7 @@ local function send_progress(dispatchers, kind, title, percentage) }, }) end +local commands = require("obsidian.lsp.handlers._code_action").commands ---@type lsp.InitializeResult local initializeResult = { @@ -36,9 +37,7 @@ local initializeResult = { }, codeActionProvider = true, executeCommandProvider = { - commands = { - "add_file_property", - }, + commands = commands, }, }, serverInfo = { From a11320eb363702de82d824654643788fe3e68955 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Tue, 6 Jan 2026 01:07:47 +0000 Subject: [PATCH 05/32] improve add_property --- lua/obsidian/api.lua | 33 +++++++++++++++++++ .../lsp/commands/add_file_property.lua | 18 +--------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/lua/obsidian/api.lua b/lua/obsidian/api.lua index b8e034907..a9330151d 100644 --- a/lua/obsidian/api.lua +++ b/lua/obsidian/api.lua @@ -586,4 +586,37 @@ setmetatable(M, { end, }) +M.add_property = function() + local note = assert(M.current_note(0)) + + -- HACK: no native way in lua + -- TODO: complete for existing keys in vault like obsidian app + -- TODO: complete for values + vim.cmd [[ + function! ObsidianPropertyComplete() + return ['aliases', 'tags', 'id'] + endfunction + ]] + + local key = M.input("key: ", { completion = "customlist,ObsidianPropertyComplete" }) + local value = M.input "value: " + + if not (key and value) then + return log.info "Aborted" + end + + if vim.trim(key) == "" or vim.trim(value) == "" then + return log.info "Empty Input" + end + + if key == "tags" then + note:add_tag(value) + elseif key == "aliases" then + note:add_alias(value) + else + note:add_field(key, value) + end + note:update_frontmatter(0) +end + return M diff --git a/lua/obsidian/lsp/commands/add_file_property.lua b/lua/obsidian/lsp/commands/add_file_property.lua index 25967e075..c623a86e5 100644 --- a/lua/obsidian/lsp/commands/add_file_property.lua +++ b/lua/obsidian/lsp/commands/add_file_property.lua @@ -1,19 +1,3 @@ local obsidian = require "obsidian" -return function() - local note = assert(obsidian.api.current_note(0)) - - local key = obsidian.api.input "key: " - local value = obsidian.api.input "value: " - - if not (key and value) then - return obsidian.log.info "Aborted" - end - - if vim.trim(key) ~= "" and vim.trim(value) ~= "" then - return obsidian.log.info "Empty Input" - end - - note:add_field(key, value) - note:update_frontmatter(0) -end +return obsidian.api.add_property From 94fa9933b7246e06f950a1e06be49a71f0154579 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Thu, 29 Jan 2026 18:48:43 +0000 Subject: [PATCH 06/32] reafactor: simplify for first version --- lua/obsidian/actions.lua | 36 ++++++++++++++++ lua/obsidian/api.lua | 33 --------------- lua/obsidian/commands/new_from_template.lua | 4 +- lua/obsidian/commands/template.lua | 42 ++----------------- lua/obsidian/lsp/handlers/_code_action.lua | 13 ++++-- lua/obsidian/lsp/handlers/code_action.lua | 20 +++------ lua/obsidian/lsp/handlers/execute_command.lua | 13 +----- 7 files changed, 59 insertions(+), 102 deletions(-) diff --git a/lua/obsidian/actions.lua b/lua/obsidian/actions.lua index 67a0e51a9..d2b4cc160 100644 --- a/lua/obsidian/actions.lua +++ b/lua/obsidian/actions.lua @@ -622,6 +622,42 @@ M.add_property = function() note:update_frontmatter(0) end +---@param template_name string +M.insert_template = function(template_name) + local templates_dir = api.templates_dir() + if not templates_dir then + return log.err "Templates folder is not defined or does not exist" + end + local templates = require "obsidian.templates" + + -- We need to get this upfront before the picker hijacks the current window. + local insert_location = api.get_active_window_cursor_location() + + local function insert_template(name) + templates.insert_template { + type = "insert_template", + template_name = name, + template_opts = Obsidian.opts.templates, + templates_dir = templates_dir, + location = insert_location, + } + end + + if template_name then + insert_template(template_name) + return + end + + Obsidian.picker.find_files { + prompt_title = "Templates", + dir = templates_dir, + no_default_mappings = true, + callback = function(path) + insert_template(path) + end, + } +end + M.start_presentation = function(buf) local note = Note.from_buffer(buf) require("obsidian.slides").start_presentation(note) diff --git a/lua/obsidian/api.lua b/lua/obsidian/api.lua index a9330151d..b8e034907 100644 --- a/lua/obsidian/api.lua +++ b/lua/obsidian/api.lua @@ -586,37 +586,4 @@ setmetatable(M, { end, }) -M.add_property = function() - local note = assert(M.current_note(0)) - - -- HACK: no native way in lua - -- TODO: complete for existing keys in vault like obsidian app - -- TODO: complete for values - vim.cmd [[ - function! ObsidianPropertyComplete() - return ['aliases', 'tags', 'id'] - endfunction - ]] - - local key = M.input("key: ", { completion = "customlist,ObsidianPropertyComplete" }) - local value = M.input "value: " - - if not (key and value) then - return log.info "Aborted" - end - - if vim.trim(key) == "" or vim.trim(value) == "" then - return log.info "Empty Input" - end - - if key == "tags" then - note:add_tag(value) - elseif key == "aliases" then - note:add_alias(value) - else - note:add_field(key, value) - end - note:update_frontmatter(0) -end - return M diff --git a/lua/obsidian/commands/new_from_template.lua b/lua/obsidian/commands/new_from_template.lua index 88efce6ff..08deb9f59 100644 --- a/lua/obsidian/commands/new_from_template.lua +++ b/lua/obsidian/commands/new_from_template.lua @@ -1,8 +1,8 @@ -local api = require "obsidian.api" +local actions = require "obsidian.actions" ---@param data obsidian.CommandArgs return function(data) local id = table.concat(data.fargs, " ", 1, #data.fargs - 1) local template = data.fargs[#data.fargs] - api.new_from_template(id, template) + actions.new_from_template(id, template) end diff --git a/lua/obsidian/commands/template.lua b/lua/obsidian/commands/template.lua index 08749905b..fc51f44ee 100644 --- a/lua/obsidian/commands/template.lua +++ b/lua/obsidian/commands/template.lua @@ -1,44 +1,10 @@ -local templates = require "obsidian.templates" -local log = require "obsidian.log" -local api = require "obsidian.api" +local actions = require "obsidian.actions" ---@param data obsidian.CommandArgs return function(data) - local templates_dir = api.templates_dir() - if not templates_dir then - return log.err "Templates folder is not defined or does not exist" - end - - -- We need to get this upfront before the picker hijacks the current window. - local insert_location = api.get_active_window_cursor_location() - - local function insert_template(name) - templates.insert_template { - type = "insert_template", - template_name = name, - templates_dir = templates_dir, - location = insert_location, - } - end - + local template_name if string.len(data.args) > 0 then - local template_name = vim.trim(data.args) - insert_template(template_name) - return - end - - local picker = Obsidian.picker - if not picker then - log.err "No picker configured" - return + template_name = vim.trim(data.args) end - - picker.find_files { - prompt_title = "Templates", - dir = templates_dir, - no_default_mappings = true, - callback = function(path) - insert_template(path) - end, - } + actions.insert_template(template_name) end diff --git a/lua/obsidian/lsp/handlers/_code_action.lua b/lua/obsidian/lsp/handlers/_code_action.lua index 52197fbab..8a2c9ed58 100644 --- a/lua/obsidian/lsp/handlers/_code_action.lua +++ b/lua/obsidian/lsp/handlers/_code_action.lua @@ -7,7 +7,7 @@ local actions = {} ---@param name string ---@param config table local register = function(name, config) - local mod = require("obsidian.lsp.commands." .. name) + local mod = require("obsidian.actions")[name] actions[#actions + 1] = { title = config.title, command = { @@ -17,14 +17,19 @@ local register = function(name, config) }, data = { range = config.range, - func = type(mod) == "table" and mod.command or mod, - edit = type(mod) == "table" and mod.edit or nil, + func = mod, + -- TODO: preview edit }, } commands[#commands + 1] = name end -register("add_file_property", { +-- TODO: merge a note to this note, after https://github.com/obsidian-nvim/obsidian.nvim/issues/655 +register("insert_template", { + title = "Insert template at curosr", +}) + +register("add_property", { title = "Add file property", }) diff --git a/lua/obsidian/lsp/handlers/code_action.lua b/lua/obsidian/lsp/handlers/code_action.lua index fa27ec2f8..2f7f6dae8 100644 --- a/lua/obsidian/lsp/handlers/code_action.lua +++ b/lua/obsidian/lsp/handlers/code_action.lua @@ -1,16 +1,16 @@ local actions = require("obsidian.lsp.handlers._code_action").actions ----@param acts lsp.CodeAction[] +---@param code_actions lsp.CodeAction[] ---@param in_selection boolean ---@return string[] -local function get_commands_by_context(acts, in_selection) +local function get_commands_by_context(code_actions, in_selection) return vim - .iter(acts) - :filter(function(act) + .iter(code_actions) + :filter(function(code_action) if in_selection then - return act.data.range ~= nil + return code_action.data.range ~= nil else - return act.data.range == nil + return code_action.data.range == nil end end) :totable() @@ -19,16 +19,8 @@ end ---@param params lsp.CodeActionParams return function(params, handler) local range = params.range - local in_selection = range.start ~= range["end"] - local res = get_commands_by_context(actions, in_selection) - vim.tbl_map(function(act) - if act.data.edit and in_selection then - act.edit = act.data.edit(range) - end - end, res) - handler(nil, res) end diff --git a/lua/obsidian/lsp/handlers/execute_command.lua b/lua/obsidian/lsp/handlers/execute_command.lua index 2e794aa2a..7f24433a2 100644 --- a/lua/obsidian/lsp/handlers/execute_command.lua +++ b/lua/obsidian/lsp/handlers/execute_command.lua @@ -1,15 +1,6 @@ ---@param params lsp.ExecuteCommandParams return function(params) local cmd = params.command - - local ok, fn = pcall(require, "obsidian.lsp.commands." .. cmd) - if ok and fn then - if type(fn) == "table" then - pcall(fn.command) - else - pcall(fn) - end - end - -- return require("obsidian.lsp.handlers.commands." .. cmd)(client, params) - -- return require "obsidian.lsp.handlers.commands.createNote"(client, params) + local fn = require("obsidian.actions")[cmd] + pcall(fn) end From b4e83ae1ae757ebbeeeee19c5c5b02c4af75a7f3 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Thu, 29 Jan 2026 18:59:35 +0000 Subject: [PATCH 07/32] remove --- .../lsp/commands/add_file_property.lua | 3 - lua/obsidian/lsp/commands/extract_note.lua | 2 - lua/obsidian/lsp/commands/link.lua | 2 - lua/obsidian/lsp/commands/link_new.lua | 66 ------------------- 4 files changed, 73 deletions(-) delete mode 100644 lua/obsidian/lsp/commands/add_file_property.lua delete mode 100644 lua/obsidian/lsp/commands/extract_note.lua delete mode 100644 lua/obsidian/lsp/commands/link.lua delete mode 100644 lua/obsidian/lsp/commands/link_new.lua diff --git a/lua/obsidian/lsp/commands/add_file_property.lua b/lua/obsidian/lsp/commands/add_file_property.lua deleted file mode 100644 index c623a86e5..000000000 --- a/lua/obsidian/lsp/commands/add_file_property.lua +++ /dev/null @@ -1,3 +0,0 @@ -local obsidian = require "obsidian" - -return obsidian.api.add_property diff --git a/lua/obsidian/lsp/commands/extract_note.lua b/lua/obsidian/lsp/commands/extract_note.lua deleted file mode 100644 index f2bdb436b..000000000 --- a/lua/obsidian/lsp/commands/extract_note.lua +++ /dev/null @@ -1,2 +0,0 @@ -local obsidian = require "obsidian" -return obsidian.api.extract_note diff --git a/lua/obsidian/lsp/commands/link.lua b/lua/obsidian/lsp/commands/link.lua deleted file mode 100644 index 8ed7031c1..000000000 --- a/lua/obsidian/lsp/commands/link.lua +++ /dev/null @@ -1,2 +0,0 @@ -local obsidian = require "obsidian" -return obsidian.api.link diff --git a/lua/obsidian/lsp/commands/link_new.lua b/lua/obsidian/lsp/commands/link_new.lua deleted file mode 100644 index 8a6d9910e..000000000 --- a/lua/obsidian/lsp/commands/link_new.lua +++ /dev/null @@ -1,66 +0,0 @@ -local obsidian = require "obsidian" -local has_nvim_0_12 = vim.fn.has "nvim-0.12.0" == 1 - --- -- TODO: neovim's visual selection is weird --- local label = vim.api.nvim_buf_get_text( --- 0, --- range.start.line, --- range.start.character, --- range["end"].line, --- range["end"].character, --- {} --- )[1] --- - -return { - ---@param range lsp.Range - ---@return lsp.WorkspaceEdit? - edit = function(range) - -- print(range.start.line, range["end"].line) - -- if range.start.line ~= range["end"].line then - -- obsidian.log.err "Only in-line visual selections allowed" - -- return - -- end - -- - -- require("obsidian.note").create { title = label } - -- obsidian.api.make_text_edit() - -- return { - -- documentChanges = { - -- { - -- textDocument = { - -- uri = vim.uri_from_fname(vim.api.nvim_buf_get_name(0)), - -- version = has_nvim_0_12 and vim.NIL or nil, - -- }, - -- edits = { - -- { - -- range = range, - -- newText = label, - -- }, - -- }, - -- }, - -- }, - -- } - local label - - local viz = obsidian.api.get_visual_selection() - if not viz then - obsidian.log.err "`Obsidian link_new` must be called in visual mode" - return - elseif #viz.lines ~= 1 then - obsidian.log.err "Only in-line visual selections allowed" - return - end - - if not label or string.len(label) <= 0 then - label = viz.selection - end - - local note = require("obsidian.note").create { title = label } - local text_edit = obsidian.api.make_text_edit(viz, note:format_link { label = label }) - return { documentChanges = { text_edit } } - end, - command = function() - -- Save file so backlinks search (ripgrep) can find the new link - vim.cmd "silent! write" -- HACK: - end, -} From 330cbd383c6b01e50b8d4f71fc49a0e0ce18f256 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Thu, 29 Jan 2026 19:08:38 +0000 Subject: [PATCH 08/32] add rename action --- lua/obsidian/actions.lua | 5 +++++ lua/obsidian/commands/new_from_template.lua | 4 +--- lua/obsidian/commands/rename.lua | 6 ++---- lua/obsidian/commands/template.lua | 4 +--- lua/obsidian/lsp/handlers/_code_action.lua | 5 +++++ 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lua/obsidian/actions.lua b/lua/obsidian/actions.lua index d2b4cc160..7916c17c8 100644 --- a/lua/obsidian/actions.lua +++ b/lua/obsidian/actions.lua @@ -730,4 +730,9 @@ M.move_note = function() }) end +---@param new_name string|? +M.rename = function(new_name) + vim.lsp.buf.rename(new_name) +end + return M diff --git a/lua/obsidian/commands/new_from_template.lua b/lua/obsidian/commands/new_from_template.lua index 08deb9f59..6e05669f3 100644 --- a/lua/obsidian/commands/new_from_template.lua +++ b/lua/obsidian/commands/new_from_template.lua @@ -1,8 +1,6 @@ -local actions = require "obsidian.actions" - ---@param data obsidian.CommandArgs return function(data) local id = table.concat(data.fargs, " ", 1, #data.fargs - 1) local template = data.fargs[#data.fargs] - actions.new_from_template(id, template) + require("obsidian.actions").new_from_template(id, template) end diff --git a/lua/obsidian/commands/rename.lua b/lua/obsidian/commands/rename.lua index dbbdcbd90..573be476d 100644 --- a/lua/obsidian/commands/rename.lua +++ b/lua/obsidian/commands/rename.lua @@ -1,9 +1,7 @@ ---@param data obsidian.CommandArgs return function(data) local new_name = vim.trim(data.args) - if #new_name == 0 then - vim.lsp.buf.rename() - else - vim.lsp.buf.rename(new_name) + if string.len(new_name) then + require("obsidian.actions").rename(new_name) end end diff --git a/lua/obsidian/commands/template.lua b/lua/obsidian/commands/template.lua index fc51f44ee..f0da4a1b0 100644 --- a/lua/obsidian/commands/template.lua +++ b/lua/obsidian/commands/template.lua @@ -1,10 +1,8 @@ -local actions = require "obsidian.actions" - ---@param data obsidian.CommandArgs return function(data) local template_name if string.len(data.args) > 0 then template_name = vim.trim(data.args) end - actions.insert_template(template_name) + require("obsidian.actions").insert_template(template_name) end diff --git a/lua/obsidian/lsp/handlers/_code_action.lua b/lua/obsidian/lsp/handlers/_code_action.lua index 8a2c9ed58..010958217 100644 --- a/lua/obsidian/lsp/handlers/_code_action.lua +++ b/lua/obsidian/lsp/handlers/_code_action.lua @@ -25,6 +25,11 @@ local register = function(name, config) end -- TODO: merge a note to this note, after https://github.com/obsidian-nvim/obsidian.nvim/issues/655 + +register("rename", { + title = "Rename current note", +}) + register("insert_template", { title = "Insert template at curosr", }) From 11757ae3c85c21f6d75abe716f8c63e86339762f Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Thu, 29 Jan 2026 19:11:39 +0000 Subject: [PATCH 09/32] fix: rename --- lua/obsidian/commands/rename.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lua/obsidian/commands/rename.lua b/lua/obsidian/commands/rename.lua index 573be476d..e8fe2d122 100644 --- a/lua/obsidian/commands/rename.lua +++ b/lua/obsidian/commands/rename.lua @@ -1,7 +1,8 @@ ---@param data obsidian.CommandArgs return function(data) - local new_name = vim.trim(data.args) - if string.len(new_name) then - require("obsidian.actions").rename(new_name) + local new_name + if string.len(new_name) > 0 then + new_name = vim.trim(data.args) end + require("obsidian.actions").rename(new_name) end From b5d676de63c938574185d95ddbfc08bc7fadaff5 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Thu, 29 Jan 2026 21:42:18 +0000 Subject: [PATCH 10/32] bugs in api --- lua/obsidian/actions.lua | 20 +++--- lua/obsidian/init.lua | 1 + lua/obsidian/lsp/handlers/_code_action.lua | 61 +++++++++++++------ lua/obsidian/lsp/handlers/execute_command.lua | 4 +- 4 files changed, 57 insertions(+), 29 deletions(-) diff --git a/lua/obsidian/actions.lua b/lua/obsidian/actions.lua index 7916c17c8..b576ff734 100644 --- a/lua/obsidian/actions.lua +++ b/lua/obsidian/actions.lua @@ -648,14 +648,20 @@ M.insert_template = function(template_name) return end - Obsidian.picker.find_files { - prompt_title = "Templates", - dir = templates_dir, - no_default_mappings = true, - callback = function(path) - insert_template(path) + ---@type obsidian.PickerEntry + local entries = {} + for path in api.dir(tostring(templates_dir)) do + entries[#entries + 1] = { + filename = path, + text = vim.fs.basename(path), + } + end + + Obsidian.picker.pick(entries, { + callback = function(entry) + insert_template(entry.filename) end, - } + }) end M.start_presentation = function(buf) diff --git a/lua/obsidian/init.lua b/lua/obsidian/init.lua index 220f5b42e..1b0448bb2 100644 --- a/lua/obsidian/init.lua +++ b/lua/obsidian/init.lua @@ -38,6 +38,7 @@ obsidian.get_client = function() end obsidian.register_command = require("obsidian.commands").register +obsidian.register_code_action = require("obsidian.lsp.handlers._code_action").register --- Setup a new Obsidian client. This should only be called once from an Nvim session. --- diff --git a/lua/obsidian/lsp/handlers/_code_action.lua b/lua/obsidian/lsp/handlers/_code_action.lua index 010958217..6b33ade06 100644 --- a/lua/obsidian/lsp/handlers/_code_action.lua +++ b/lua/obsidian/lsp/handlers/_code_action.lua @@ -3,57 +3,78 @@ local commands = {} ---@type lsp.CodeAction[] local actions = {} +---@class obsidian.lsp.CodeActionOpts +---@field name string internal server command name, recommend to keep to snake case +---@field title string text display in code action interface +---@field fn function|? function to run +---@field range boolean|? whether to show action only in visual mode + +local actions_lookup = {} + ---Register a new command. ----@param name string ----@param config table -local register = function(name, config) - local mod = require("obsidian.actions")[name] - actions[#actions + 1] = { +---@param config obsidian.lsp.CodeActionOpts +local register = function(config) + local fn = config.fn or require("obsidian.actions")[config.name] + if not fn then + -- TODO: + return + end + local action = { title = config.title, command = { title = config.title, - command = name, + command = config.name, -- TODO: kind }, data = { range = config.range, - func = mod, + fn = fn, -- TODO: preview edit }, } - commands[#commands + 1] = name + commands[#commands + 1] = config.name + actions[#actions + 1] = action + actions_lookup[config.name] = action end -- TODO: merge a note to this note, after https://github.com/obsidian-nvim/obsidian.nvim/issues/655 -register("rename", { +register { + name = "rename", title = "Rename current note", -}) +} -register("insert_template", { +register { + name = "insert_template", title = "Insert template at curosr", -}) +} -register("add_property", { +register { + name = "add_property", title = "Add file property", -}) +} -register("link", { +register { + name = "link", title = "Link selection as name for a existing note", range = true, -}) +} -register("link_new", { +register { + name = "link_new", title = "Link selection as name for a new note", range = true, -}) +} -register("extract_note", { +register { + name = "extract_note", title = "Extract selected text to a new note", range = true, -}) +} return { commands = commands, actions = actions, + actions_lookup = actions_lookup, + register = register, } diff --git a/lua/obsidian/lsp/handlers/execute_command.lua b/lua/obsidian/lsp/handlers/execute_command.lua index 7f24433a2..6af8fafb1 100644 --- a/lua/obsidian/lsp/handlers/execute_command.lua +++ b/lua/obsidian/lsp/handlers/execute_command.lua @@ -1,6 +1,6 @@ ---@param params lsp.ExecuteCommandParams return function(params) local cmd = params.command - local fn = require("obsidian.actions")[cmd] - pcall(fn) + local config = require("obsidian.lsp.handlers._code_action").actions_lookup[cmd] + pcall(config.data.fn) end From 13f4232ea6143e96ca05096acd4ebfcaa76c77a9 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Sat, 31 Jan 2026 19:15:47 +0000 Subject: [PATCH 11/32] wip: docs --- docs/Actions.md | 14 ++++++++++++++ docs/LSP.md | 15 +++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 docs/Actions.md diff --git a/docs/Actions.md b/docs/Actions.md new file mode 100644 index 000000000..0b59e023f --- /dev/null +++ b/docs/Actions.md @@ -0,0 +1,14 @@ +| name | available mode | arguments | +| ------------------- | -------------- | --------- | +| `follow_link` | `n` | | +| `nav_link` | `n` | | +| `smart_action` | `n` | | +| `new_from_template` | `n` | | +| `add_property` | `n` | | +| `insert_template` | `n` | | +| `rename` | `n` | | +| `toggle_checkbox` | `n`, `v` | | +| `set_checkbox` | `n`, `v` | | +| `link` | `v` | | +| `link_new` | `v` | | +| `extract_note` | `v` | | diff --git a/docs/LSP.md b/docs/LSP.md index 93b8d54ce..6e2b064f1 100644 --- a/docs/LSP.md +++ b/docs/LSP.md @@ -36,3 +36,18 @@ require("obsidian").setup { }, } ``` + +## Code Actions + +obsidian.nvim exposes a small set of LSP code actions for common note operations. You can trigger them using your normal LSP code action keymap, by default neovim maps `vim.lsp.buf.code_action` to `gra`. + +Available actions: + +- Normal mode: + - Rename current note (`rename`) + - Insert template at cursor (`insert_template`) + - Add file property (`add_property`) +- Visual mode: + - Link selection as name for an existing note (`link`) + - Link selection as name for a new note (`link_new`) + - Extract selected text to a new note (`extract_note`) From 223b8b230c081a738de2892c78d17e5cd1af311e Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Sun, 1 Feb 2026 02:03:05 +0000 Subject: [PATCH 12/32] feat: api add and api del --- lua/obsidian/init.lua | 1 + lua/obsidian/lsp/handlers/_code_action.lua | 44 +++++++++++-------- lua/obsidian/lsp/handlers/code_action.lua | 2 +- lua/obsidian/lsp/handlers/execute_command.lua | 4 +- lua/obsidian/lsp/handlers/initialize.lua | 2 +- 5 files changed, 30 insertions(+), 23 deletions(-) diff --git a/lua/obsidian/init.lua b/lua/obsidian/init.lua index 1b0448bb2..03f12806e 100644 --- a/lua/obsidian/init.lua +++ b/lua/obsidian/init.lua @@ -21,6 +21,7 @@ obsidian.util = require "obsidian.util" obsidian.VERSION = require "obsidian.version" obsidian.Workspace = require "obsidian.workspace" obsidian.yaml = require "obsidian.yaml" +obsidian.code_action = require "obsidian.lsp.handlers._code_action" ---@type obsidian.Client|? obsidian._client = nil diff --git a/lua/obsidian/lsp/handlers/_code_action.lua b/lua/obsidian/lsp/handlers/_code_action.lua index 6b33ade06..70199b47c 100644 --- a/lua/obsidian/lsp/handlers/_code_action.lua +++ b/lua/obsidian/lsp/handlers/_code_action.lua @@ -1,19 +1,23 @@ -local commands = {} +---@alias obsidian.lsp.CodeAtionDefaults +---| "rename" +---| "insert_template" +---| "add_property" +---| "link" +---| "link_new" +---| "extract_note" ----@type lsp.CodeAction[] +---@type table local actions = {} ---@class obsidian.lsp.CodeActionOpts ----@field name string internal server command name, recommend to keep to snake case +---@field name string | obsidian.lsp.CodeAtionDefaults internal server command name, recommend to keep to snake case ---@field title string text display in code action interface ---@field fn function|? function to run ---@field range boolean|? whether to show action only in visual mode -local actions_lookup = {} - ---Register a new command. ---@param config obsidian.lsp.CodeActionOpts -local register = function(config) +local add = function(config) local fn = config.fn or require("obsidian.actions")[config.name] if not fn then -- TODO: @@ -32,49 +36,51 @@ local register = function(config) -- TODO: preview edit }, } - commands[#commands + 1] = config.name - actions[#actions + 1] = action - actions_lookup[config.name] = action + actions[config.name] = action end -- TODO: merge a note to this note, after https://github.com/obsidian-nvim/obsidian.nvim/issues/655 -register { +add { name = "rename", title = "Rename current note", } -register { +add { name = "insert_template", - title = "Insert template at curosr", + title = "Insert template at cursor", } -register { +add { name = "add_property", title = "Add file property", } -register { +add { name = "link", title = "Link selection as name for a existing note", range = true, } -register { +add { name = "link_new", title = "Link selection as name for a new note", range = true, } -register { +add { name = "extract_note", title = "Extract selected text to a new note", range = true, } +---@param name string | obsidian.lsp.CodeAtionDefaults +local del = function(name) + actions[name] = nil +end + return { - commands = commands, actions = actions, - actions_lookup = actions_lookup, - register = register, + add = add, + del = del, } diff --git a/lua/obsidian/lsp/handlers/code_action.lua b/lua/obsidian/lsp/handlers/code_action.lua index 2f7f6dae8..417385db0 100644 --- a/lua/obsidian/lsp/handlers/code_action.lua +++ b/lua/obsidian/lsp/handlers/code_action.lua @@ -5,7 +5,7 @@ local actions = require("obsidian.lsp.handlers._code_action").actions ---@return string[] local function get_commands_by_context(code_actions, in_selection) return vim - .iter(code_actions) + .iter(vim.tbl_values(code_actions)) :filter(function(code_action) if in_selection then return code_action.data.range ~= nil diff --git a/lua/obsidian/lsp/handlers/execute_command.lua b/lua/obsidian/lsp/handlers/execute_command.lua index 6af8fafb1..56820c6b8 100644 --- a/lua/obsidian/lsp/handlers/execute_command.lua +++ b/lua/obsidian/lsp/handlers/execute_command.lua @@ -1,6 +1,6 @@ ---@param params lsp.ExecuteCommandParams return function(params) local cmd = params.command - local config = require("obsidian.lsp.handlers._code_action").actions_lookup[cmd] - pcall(config.data.fn) + local action = require("obsidian.lsp.handlers._code_action").actions[cmd] + pcall(action.data.fn) end diff --git a/lua/obsidian/lsp/handlers/initialize.lua b/lua/obsidian/lsp/handlers/initialize.lua index b35520614..7c5a4c3a9 100644 --- a/lua/obsidian/lsp/handlers/initialize.lua +++ b/lua/obsidian/lsp/handlers/initialize.lua @@ -37,7 +37,7 @@ local initializeResult = { }, codeActionProvider = true, executeCommandProvider = { - commands = commands, + commands = vim.tbl_keys(require("obsidian.lsp.handlers._code_action").actions), }, }, serverInfo = { From 9196146ec29e49fb96c18ba9e9f694098d0e68d9 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Sun, 1 Feb 2026 16:07:50 +0000 Subject: [PATCH 13/32] remove old --- lua/obsidian/init.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/lua/obsidian/init.lua b/lua/obsidian/init.lua index 03f12806e..3ea724e14 100644 --- a/lua/obsidian/init.lua +++ b/lua/obsidian/init.lua @@ -39,7 +39,6 @@ obsidian.get_client = function() end obsidian.register_command = require("obsidian.commands").register -obsidian.register_code_action = require("obsidian.lsp.handlers._code_action").register --- Setup a new Obsidian client. This should only be called once from an Nvim session. --- From 9155d17e1f79983e9808ac2f65a4b450586462eb Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Sun, 1 Feb 2026 16:22:27 +0000 Subject: [PATCH 14/32] doc: readme --- CHANGELOG.md | 4 ++++ README.md | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95a0fba13..e51a7400a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `padding_top` option to `Note.insert_text` for configuring blank lines inserted at the top of notes. - `:Obsidian help` has cmdline completion for wiki pages. - Obsidian sync client will emit `ObsidianSyncChanged` autocmd event for better status rendering. +- Support LSP code_action: + - Add code_actions with `require"obsidian".code_action.add`. + - Delete code_actions with `require"obsidian".code_action.del`. ### Fixed @@ -132,6 +135,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opts.daily_notes.date_format` - `opts.daily_notes.alias_format` - `actions.start_presentation`. + <<<<<<< HEAD - Support for template substitution suffix, like `{{date:YY}}` - `actions.new`. diff --git a/README.md b/README.md index af5825e91..e1e100928 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,15 @@ There's one entry point user command for this plugin: `Obsidian` - `:Obsidian link_new [TITLE]` - create a new note and link it to an inline visual selection of text - if title is not given, selected text is used +### LSP code actions + +- Use `gra` or `:=vim.lsp.buf.code_action` to trigger note specific actions. +- See [LSP](https://github.com/obsidian-nvim/obsidian.nvim/wiki/LSP) for more info. + +> [!Waring] +> Some note subcommands that are related to refactoring like `rename`, `template` +> And all the visual mode commands, will be moved to code actions in `3.17.0`. + ## 📝 Requirements ### System requirements From d1244618fadf88911466264fc6e1cd9549a33353 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Sun, 1 Feb 2026 19:55:38 +0000 Subject: [PATCH 15/32] cleanup --- lua/obsidian/lsp/handlers/_code_action.lua | 109 +++++++++++---------- 1 file changed, 56 insertions(+), 53 deletions(-) diff --git a/lua/obsidian/lsp/handlers/_code_action.lua b/lua/obsidian/lsp/handlers/_code_action.lua index 70199b47c..aa32ff700 100644 --- a/lua/obsidian/lsp/handlers/_code_action.lua +++ b/lua/obsidian/lsp/handlers/_code_action.lua @@ -1,86 +1,89 @@ ----@alias obsidian.lsp.CodeAtionDefaults ----| "rename" ----| "insert_template" ----| "add_property" ----| "link" ----| "link_new" ----| "extract_note" +local actions = require "obsidian.actions" ---@type table -local actions = {} +local code_actions = {} ---@class obsidian.lsp.CodeActionOpts ----@field name string | obsidian.lsp.CodeAtionDefaults internal server command name, recommend to keep to snake case +---@field name string internal server command name, recommend to keep to snake case ---@field title string text display in code action interface ----@field fn function|? function to run +---@field fn function function to run ---@field range boolean|? whether to show action only in visual mode ---Register a new command. ----@param config obsidian.lsp.CodeActionOpts -local add = function(config) - local fn = config.fn or require("obsidian.actions")[config.name] - if not fn then - -- TODO: - return - end +---@param opts obsidian.lsp.CodeActionOpts +local add = function(opts) + -- TODO: validate local action = { - title = config.title, + title = opts.title, command = { - title = config.title, - command = config.name, + title = opts.title, + command = opts.name, -- TODO: kind }, data = { - range = config.range, - fn = fn, - -- TODO: preview edit + range = opts.range, + fn = opts.fn, + -- TODO: preview edit with preview_fn }, } - actions[config.name] = action + code_actions[opts.name] = action end --- TODO: merge a note to this note, after https://github.com/obsidian-nvim/obsidian.nvim/issues/655 +---@enum (key) obsidian.lsp.CodeAtionDefaults +local default_actions = { + rename = { + name = "rename", + title = "Rename current note", + fn = actions.rename, + }, -add { - name = "rename", - title = "Rename current note", -} + insert_template = { + name = "insert_template", + title = "Insert template at cursor", + fn = actions.insert_template, + }, -add { - name = "insert_template", - title = "Insert template at cursor", -} + add_property = { + name = "add_property", + title = "Add file property", + fn = actions.add_property, + }, -add { - name = "add_property", - title = "Add file property", -} + link = { + name = "link", + title = "Link selection as name for a existing note", + fn = actions.link, + range = true, + }, -add { - name = "link", - title = "Link selection as name for a existing note", - range = true, -} + link_new = { + name = "link_new", + title = "Link selection as name for a new note", + fn = actions.link_new, + range = true, + }, -add { - name = "link_new", - title = "Link selection as name for a new note", - range = true, + extract_note = { + name = "extract_note", + title = "Extract selected text to a new note", + fn = actions.extract_note, + range = true, + }, } -add { - name = "extract_note", - title = "Extract selected text to a new note", - range = true, -} +-- TODO: merge a note to this note, after https://github.com/obsidian-nvim/obsidian.nvim/issues/655 ---@param name string | obsidian.lsp.CodeAtionDefaults local del = function(name) - actions[name] = nil + code_actions[name] = nil +end + +for _, action in pairs(default_actions) do + add(action) end return { - actions = actions, + actions = code_actions, add = add, del = del, } From c8e8896830e57fc486b5dbdc5d535ffae02ebbd3 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Sun, 1 Feb 2026 22:16:03 +0000 Subject: [PATCH 16/32] doc: more --- README.md | 4 ++-- docs/Actions.md | 30 ++++++++++++++++-------------- docs/LSP.md | 24 ++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e1e100928..9a10fe855 100644 --- a/README.md +++ b/README.md @@ -134,8 +134,8 @@ There's one entry point user command for this plugin: `Obsidian` ### LSP code actions -- Use `gra` or `:=vim.lsp.buf.code_action` to trigger note specific actions. -- See [LSP](https://github.com/obsidian-nvim/obsidian.nvim/wiki/LSP) for more info. +- Use `gra` or `:=vim.lsp.buf.code_action()` to trigger note specific actions. +- See [LSP code actions](https://github.com/obsidian-nvim/obsidian.nvim/wiki/LSP#code-actions) and [Actions](docs/Actions.md) for more info. > [!Waring] > Some note subcommands that are related to refactoring like `rename`, `template` diff --git a/docs/Actions.md b/docs/Actions.md index 0b59e023f..500705d62 100644 --- a/docs/Actions.md +++ b/docs/Actions.md @@ -1,14 +1,16 @@ -| name | available mode | arguments | -| ------------------- | -------------- | --------- | -| `follow_link` | `n` | | -| `nav_link` | `n` | | -| `smart_action` | `n` | | -| `new_from_template` | `n` | | -| `add_property` | `n` | | -| `insert_template` | `n` | | -| `rename` | `n` | | -| `toggle_checkbox` | `n`, `v` | | -| `set_checkbox` | `n`, `v` | | -| `link` | `v` | | -| `link_new` | `v` | | -| `extract_note` | `v` | | +See [LSP code actions](LSP.md#code-actions) for actions exposed via the LSP interface. + +| name | mode | description | arguments | +| ------------------- | -------- | ----------------------------------------- | --------- | +| `follow_link` | `n` | Open the link under the cursor. | | +| `nav_link` | `n` | Navigate through link history. | | +| `smart_action` | `n` | Context-aware action on the cursor. | | +| `new_from_template` | `n` | Create a new note from a template. | | +| `add_property` | `n` | Add frontmatter property. | | +| `insert_template` | `n` | Insert a template at cursor. | `name` | +| `rename` | `n` | Rename current note. | `name` | +| `toggle_checkbox` | `n`, `v` | Toggle checkbox state. | | +| `set_checkbox` | `n`, `v` | Set checkbox state. | `state` | +| `link` | `v` | Link selection to an existing note. | | +| `link_new` | `v` | Create a new note and link selection. | `title` | +| `extract_note` | `v` | Move selection to a new note and link it. | `title` | diff --git a/docs/LSP.md b/docs/LSP.md index 6e2b064f1..34c14edf2 100644 --- a/docs/LSP.md +++ b/docs/LSP.md @@ -51,3 +51,27 @@ Available actions: - Link selection as name for an existing note (`link`) - Link selection as name for a new note (`link_new`) - Extract selected text to a new note (`extract_note`) + +### Code Action API + +You can register custom code actions via `require("obsidian").code_action`. Each action is exposed as an LSP +command, so register actions before calling `require"obsidian".setup{}`. + +API: + +- `require("obsidian").code_action.add(opts)`, and `opts` field have following fields: + - `name`: command id (snake_case recommended). + - `title`: text shown in the code action picker. + - `fn`: function invoked when the action is executed. + - `range` (optional): when `true`, the action only appears for a visual selection. +- `require("obsidian").code_action.del(name)` removes a previously registered action. + + + + + + + + + + From 220a1dff9141870f86864d8880c395ee2447972d Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Sun, 1 Feb 2026 22:55:07 +0000 Subject: [PATCH 17/32] fix: Actions.md --- docs/Actions.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/Actions.md b/docs/Actions.md index 500705d62..eb42c9de8 100644 --- a/docs/Actions.md +++ b/docs/Actions.md @@ -1,16 +1,16 @@ See [LSP code actions](LSP.md#code-actions) for actions exposed via the LSP interface. -| name | mode | description | arguments | -| ------------------- | -------- | ----------------------------------------- | --------- | -| `follow_link` | `n` | Open the link under the cursor. | | -| `nav_link` | `n` | Navigate through link history. | | -| `smart_action` | `n` | Context-aware action on the cursor. | | -| `new_from_template` | `n` | Create a new note from a template. | | -| `add_property` | `n` | Add frontmatter property. | | -| `insert_template` | `n` | Insert a template at cursor. | `name` | -| `rename` | `n` | Rename current note. | `name` | -| `toggle_checkbox` | `n`, `v` | Toggle checkbox state. | | -| `set_checkbox` | `n`, `v` | Set checkbox state. | `state` | -| `link` | `v` | Link selection to an existing note. | | -| `link_new` | `v` | Create a new note and link selection. | `title` | -| `extract_note` | `v` | Move selection to a new note and link it. | `title` | +| name | mode | description | arguments | +| ------------------- | -------- | ------------------------------------- | ----------- | +| `follow_link` | `n` | Open the link under the cursor. | | +| `nav_link` | `n` | Navigate to next/previous link. | `direction` | +| `smart_action` | `n` | Context-aware action on the cursor. | | +| `new_from_template` | `n` | Create a new note from a template. | | +| `add_property` | `n` | Add frontmatter property. | | +| `insert_template` | `n` | Insert a template at cursor. | `name` | +| `rename` | `n` | Rename current note. | `name` | +| `toggle_checkbox` | `n`, `v` | Toggle (cycle) checkbox state. | | +| `set_checkbox` | `n`, `v` | Set to specifi checkbox state. | `state` | +| `link` | `v` | Link selection to an existing note. | | +| `link_new` | `v` | Create a new note and link selection. | `title` | +| `extract_note` | `v` | Move selection to a new note. | `title` | From 106d7f289f29ad313f8aa2900c15bb43091eceaf Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Mon, 2 Feb 2026 00:23:54 +0000 Subject: [PATCH 18/32] readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9a10fe855..cc1eada2c 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,10 @@ There's one entry point user command for this plugin: `Obsidian` - you are in visual mode. - See [Commands](https://github.com/obsidian-nvim/obsidian.nvim/wiki/Commands) for more info. +> [!Warning] +> Note subcommands related to refactoring, like `rename` and `template` +> And all the visual mode commands, will be moved to code actions in `3.17.0`. + #### Top level commands - `:Obsidian check` - check for common issues in your vault and plugin setup @@ -137,10 +141,6 @@ There's one entry point user command for this plugin: `Obsidian` - Use `gra` or `:=vim.lsp.buf.code_action()` to trigger note specific actions. - See [LSP code actions](https://github.com/obsidian-nvim/obsidian.nvim/wiki/LSP#code-actions) and [Actions](docs/Actions.md) for more info. -> [!Waring] -> Some note subcommands that are related to refactoring like `rename`, `template` -> And all the visual mode commands, will be moved to code actions in `3.17.0`. - ## 📝 Requirements ### System requirements From dfc91dab727ec51a32ab83724dddcb8fc16be2fd Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Fri, 6 Feb 2026 21:48:35 +0000 Subject: [PATCH 19/32] type fix --- lua/obsidian/actions.lua | 6 +++--- lua/obsidian/api.lua | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lua/obsidian/actions.lua b/lua/obsidian/actions.lua index b576ff734..0c25c5fb8 100644 --- a/lua/obsidian/actions.lua +++ b/lua/obsidian/actions.lua @@ -637,18 +637,17 @@ M.insert_template = function(template_name) templates.insert_template { type = "insert_template", template_name = name, - template_opts = Obsidian.opts.templates, templates_dir = templates_dir, location = insert_location, } end - if template_name then + if template_name ~= nil then insert_template(template_name) return end - ---@type obsidian.PickerEntry + ---@type obsidian.PickerEntry[] local entries = {} for path in api.dir(tostring(templates_dir)) do entries[#entries + 1] = { @@ -664,6 +663,7 @@ M.insert_template = function(template_name) }) end +---@param buf integer|? M.start_presentation = function(buf) local note = Note.from_buffer(buf) require("obsidian.slides").start_presentation(note) diff --git a/lua/obsidian/api.lua b/lua/obsidian/api.lua index b8e034907..d351db0d5 100644 --- a/lua/obsidian/api.lua +++ b/lua/obsidian/api.lua @@ -132,7 +132,7 @@ M.current_note = function(bufnr, opts) return Note.from_buffer(bufnr, opts) end ----@return [number, number, number, number] tuple containing { buf, win, row, col } +---@return [integer, integer, integer, integer] tuple containing { buf, win, row, col } M.get_active_window_cursor_location = function() local buf = vim.api.nvim_win_get_buf(0) local win = vim.api.nvim_get_current_win() From 4f80f855638091811d3301ca76d222bae3cf491c Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Fri, 6 Feb 2026 22:01:36 +0000 Subject: [PATCH 20/32] register actions on module enabled --- lua/obsidian/lsp/handlers/_code_action.lua | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/lua/obsidian/lsp/handlers/_code_action.lua b/lua/obsidian/lsp/handlers/_code_action.lua index aa32ff700..c5d0b1bea 100644 --- a/lua/obsidian/lsp/handlers/_code_action.lua +++ b/lua/obsidian/lsp/handlers/_code_action.lua @@ -37,12 +37,6 @@ local default_actions = { fn = actions.rename, }, - insert_template = { - name = "insert_template", - title = "Insert template at cursor", - fn = actions.insert_template, - }, - add_property = { name = "add_property", title = "Add file property", @@ -71,6 +65,22 @@ local default_actions = { }, } +if Obsidian.opts.templates.enabled then + default_actions.insert_template = { + name = "insert_template", + title = "Insert template at cursor", + fn = actions.insert_template, + } +end + +if Obsidian.opts.slides.enabled then + default_actions.start_presentation = { + name = "start_presentation", + title = "Start presentation", + fn = actions.start_presentation, + } +end + -- TODO: merge a note to this note, after https://github.com/obsidian-nvim/obsidian.nvim/issues/655 ---@param name string | obsidian.lsp.CodeAtionDefaults From a502b26487e6c50037971045ef4a497d4105a821 Mon Sep 17 00:00:00 2001 From: neo451 <412444506@qq.com> Date: Thu, 9 Apr 2026 02:25:27 +0100 Subject: [PATCH 21/32] fix: rebase --- lua/obsidian/init.lua | 1 - lua/obsidian/lsp/handlers/_code_action.lua | 14 +++++++------- lua/obsidian/lsp/handlers/initialize.lua | 1 - 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/lua/obsidian/init.lua b/lua/obsidian/init.lua index 3ea724e14..220f5b42e 100644 --- a/lua/obsidian/init.lua +++ b/lua/obsidian/init.lua @@ -21,7 +21,6 @@ obsidian.util = require "obsidian.util" obsidian.VERSION = require "obsidian.version" obsidian.Workspace = require "obsidian.workspace" obsidian.yaml = require "obsidian.yaml" -obsidian.code_action = require "obsidian.lsp.handlers._code_action" ---@type obsidian.Client|? obsidian._client = nil diff --git a/lua/obsidian/lsp/handlers/_code_action.lua b/lua/obsidian/lsp/handlers/_code_action.lua index c5d0b1bea..2c7d3ab2e 100644 --- a/lua/obsidian/lsp/handlers/_code_action.lua +++ b/lua/obsidian/lsp/handlers/_code_action.lua @@ -73,13 +73,13 @@ if Obsidian.opts.templates.enabled then } end -if Obsidian.opts.slides.enabled then - default_actions.start_presentation = { - name = "start_presentation", - title = "Start presentation", - fn = actions.start_presentation, - } -end +-- if Obsidian.opts.slides.enabled then +-- default_actions.start_presentation = { +-- name = "start_presentation", +-- title = "Start presentation", +-- fn = actions.start_presentation, +-- } +-- end -- TODO: merge a note to this note, after https://github.com/obsidian-nvim/obsidian.nvim/issues/655 diff --git a/lua/obsidian/lsp/handlers/initialize.lua b/lua/obsidian/lsp/handlers/initialize.lua index 7c5a4c3a9..84e18c708 100644 --- a/lua/obsidian/lsp/handlers/initialize.lua +++ b/lua/obsidian/lsp/handlers/initialize.lua @@ -8,7 +8,6 @@ local function send_progress(dispatchers, kind, title, percentage) }, }) end -local commands = require("obsidian.lsp.handlers._code_action").commands ---@type lsp.InitializeResult local initializeResult = { From 95af090b7f1d7af17675fe00ba987da8c995d46d Mon Sep 17 00:00:00 2001 From: neo451 <412444506@qq.com> Date: Thu, 9 Apr 2026 02:29:06 +0100 Subject: [PATCH 22/32] rebase artifact --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e51a7400a..63171f082 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -135,7 +135,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opts.daily_notes.date_format` - `opts.daily_notes.alias_format` - `actions.start_presentation`. - <<<<<<< HEAD - Support for template substitution suffix, like `{{date:YY}}` - `actions.new`. From 6b57e4d4cb4e84a026a99a00b1f3cb83eccd6f2f Mon Sep 17 00:00:00 2001 From: neo451 <412444506@qq.com> Date: Thu, 9 Apr 2026 18:13:35 +0100 Subject: [PATCH 23/32] refactor: use client side commands for all actions --- lua/obsidian/lsp/handlers/_code_action.lua | 27 ++++++------------- lua/obsidian/lsp/handlers/execute_command.lua | 6 ----- lua/obsidian/lsp/handlers/initialize.lua | 5 +--- lua/obsidian/lsp/init.lua | 6 ++--- 4 files changed, 12 insertions(+), 32 deletions(-) delete mode 100644 lua/obsidian/lsp/handlers/execute_command.lua diff --git a/lua/obsidian/lsp/handlers/_code_action.lua b/lua/obsidian/lsp/handlers/_code_action.lua index 2c7d3ab2e..df51fde62 100644 --- a/lua/obsidian/lsp/handlers/_code_action.lua +++ b/lua/obsidian/lsp/handlers/_code_action.lua @@ -1,12 +1,9 @@ -local actions = require "obsidian.actions" - ---@type table local code_actions = {} ---@class obsidian.lsp.CodeActionOpts ---@field name string internal server command name, recommend to keep to snake case ---@field title string text display in code action interface ----@field fn function function to run ---@field range boolean|? whether to show action only in visual mode ---Register a new command. @@ -22,8 +19,7 @@ local add = function(opts) }, data = { range = opts.range, - fn = opts.fn, - -- TODO: preview edit with preview_fn + -- TODO: preview? }, } code_actions[opts.name] = action @@ -32,52 +28,45 @@ end ---@enum (key) obsidian.lsp.CodeAtionDefaults local default_actions = { rename = { - name = "rename", + name = "obsidian-ls.rename", title = "Rename current note", - fn = actions.rename, }, add_property = { - name = "add_property", + name = "obsidian-ls.add_property", title = "Add file property", - fn = actions.add_property, }, link = { - name = "link", + name = "obsidian-ls.link", title = "Link selection as name for a existing note", - fn = actions.link, range = true, }, link_new = { - name = "link_new", + name = "obsidian-ls.link_new", title = "Link selection as name for a new note", - fn = actions.link_new, range = true, }, extract_note = { - name = "extract_note", + name = "obsidian-ls.extract_note", title = "Extract selected text to a new note", - fn = actions.extract_note, range = true, }, } if Obsidian.opts.templates.enabled then default_actions.insert_template = { - name = "insert_template", + name = "obsidian-ls.insert_template", title = "Insert template at cursor", - fn = actions.insert_template, } end -- if Obsidian.opts.slides.enabled then -- default_actions.start_presentation = { --- name = "start_presentation", +-- name = "obsidian-ls.start_presentation", -- title = "Start presentation", --- fn = actions.start_presentation, -- } -- end diff --git a/lua/obsidian/lsp/handlers/execute_command.lua b/lua/obsidian/lsp/handlers/execute_command.lua deleted file mode 100644 index 56820c6b8..000000000 --- a/lua/obsidian/lsp/handlers/execute_command.lua +++ /dev/null @@ -1,6 +0,0 @@ ----@param params lsp.ExecuteCommandParams -return function(params) - local cmd = params.command - local action = require("obsidian.lsp.handlers._code_action").actions[cmd] - pcall(action.data.fn) -end diff --git a/lua/obsidian/lsp/handlers/initialize.lua b/lua/obsidian/lsp/handlers/initialize.lua index 84e18c708..4d6729b93 100644 --- a/lua/obsidian/lsp/handlers/initialize.lua +++ b/lua/obsidian/lsp/handlers/initialize.lua @@ -19,6 +19,7 @@ local initializeResult = { definitionProvider = true, documentSymbolProvider = true, workspaceSymbolProvider = true, + codeActionProvider = true, workspace = { fileOperations = { didRename = { @@ -34,10 +35,6 @@ local initializeResult = { }, }, }, - codeActionProvider = true, - executeCommandProvider = { - commands = vim.tbl_keys(require("obsidian.lsp.handlers._code_action").actions), - }, }, serverInfo = { name = "obsidian-ls", diff --git a/lua/obsidian/lsp/init.lua b/lua/obsidian/lsp/init.lua index a2c49a0c8..409a8fff0 100644 --- a/lua/obsidian/lsp/init.lua +++ b/lua/obsidian/lsp/init.lua @@ -20,13 +20,13 @@ lsp.start = function(buf) root_dir = tostring(Obsidian.dir), } - local ok, client_id = pcall(vim.lsp.start, lsp_config, { bufnr = buf, silent = false }) + local client_id = vim.lsp.start(lsp_config, { bufnr = buf, silent = false }) - if not ok then + if not client_id then log.err("[obsidian-ls]: failed to start: " .. client_id) + return end - ---@cast client_id integer return client_id end From 42542d1677c07617c774123c4dc828f3f0260821 Mon Sep 17 00:00:00 2001 From: neo451 <412444506@qq.com> Date: Thu, 9 Apr 2026 18:15:40 +0100 Subject: [PATCH 24/32] remove old --- lua/obsidian/lsp/handlers.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/lua/obsidian/lsp/handlers.lua b/lua/obsidian/lsp/handlers.lua index bc2b696ee..a428bb29f 100644 --- a/lua/obsidian/lsp/handlers.lua +++ b/lua/obsidian/lsp/handlers.lua @@ -8,6 +8,5 @@ return { ["textDocument/references"] = require "obsidian.lsp.handlers.references", ["textDocument/definition"] = require "obsidian.lsp.handlers.definition", ["textDocument/documentSymbol"] = require "obsidian.lsp.handlers.document_symbol", - ["workspace/executeCommand"] = require "obsidian.lsp.handlers.execute_command", ["textDocument/codeAction"] = require "obsidian.lsp.handlers.code_action", } From 0d96b8b6209f5a5b366c77be3fe476f2e59392e2 Mon Sep 17 00:00:00 2001 From: neo451 <412444506@qq.com> Date: Thu, 9 Apr 2026 19:32:56 +0100 Subject: [PATCH 25/32] use commmands and merge note --- lua/obsidian/actions.lua | 32 ++++++++++++++++++++++ lua/obsidian/lsp/handlers/_code_action.lua | 21 ++++++-------- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/lua/obsidian/actions.lua b/lua/obsidian/actions.lua index 0c25c5fb8..3cb894a2f 100644 --- a/lua/obsidian/actions.lua +++ b/lua/obsidian/actions.lua @@ -741,4 +741,36 @@ M.rename = function(new_name) vim.lsp.buf.rename(new_name) end +---@param dst_note obsidian.Note +local function merge_note(dst_note) + local current_note = api.current_note() + assert(current_note, "Must be in a note to merge") + + local message = ('Are you sure you want to merge "%s" to "%s"? "%s" will be deleted.'):format( + current_note.id, + dst_note.id, + current_note.id + ) + + if api.confirm(message) == "Yes" then + dst_note:merge(current_note) + dst_note:open { sync = true } + vim.fs.rm(tostring(current_note.path)) + end +end + +---@param dst_note obsidian.Note? +M.merge_note = function(dst_note) + if dst_note then + merge_note(dst_note) + else + Obsidian.picker.find_notes { + callback = function(path) + local note = Note.from_file(path) + merge_note(note) + end, + } + end +end + return M diff --git a/lua/obsidian/lsp/handlers/_code_action.lua b/lua/obsidian/lsp/handlers/_code_action.lua index df51fde62..adf971e91 100644 --- a/lua/obsidian/lsp/handlers/_code_action.lua +++ b/lua/obsidian/lsp/handlers/_code_action.lua @@ -2,19 +2,18 @@ local code_actions = {} ---@class obsidian.lsp.CodeActionOpts ----@field name string internal server command name, recommend to keep to snake case ---@field title string text display in code action interface ---@field range boolean|? whether to show action only in visual mode ---Register a new command. ---@param opts obsidian.lsp.CodeActionOpts -local add = function(opts) +local add = function(name, opts) -- TODO: validate local action = { title = opts.title, command = { title = opts.title, - command = opts.name, + command = "obsidian." .. name, -- TODO: kind }, data = { @@ -22,43 +21,41 @@ local add = function(opts) -- TODO: preview? }, } - code_actions[opts.name] = action + code_actions[name] = action end ---@enum (key) obsidian.lsp.CodeAtionDefaults local default_actions = { rename = { - name = "obsidian-ls.rename", title = "Rename current note", }, add_property = { - name = "obsidian-ls.add_property", title = "Add file property", }, link = { - name = "obsidian-ls.link", title = "Link selection as name for a existing note", range = true, }, link_new = { - name = "obsidian-ls.link_new", title = "Link selection as name for a new note", range = true, }, extract_note = { - name = "obsidian-ls.extract_note", title = "Extract selected text to a new note", range = true, }, + + merge_note = { + title = "Merge this note into another note", + }, } if Obsidian.opts.templates.enabled then default_actions.insert_template = { - name = "obsidian-ls.insert_template", title = "Insert template at cursor", } end @@ -77,8 +74,8 @@ local del = function(name) code_actions[name] = nil end -for _, action in pairs(default_actions) do - add(action) +for name, action in pairs(default_actions) do + add(name, action) end return { From 851bfc6fee88586b46f37b78effc26365a6c7626 Mon Sep 17 00:00:00 2001 From: neo451 <412444506@qq.com> Date: Wed, 15 Apr 2026 22:02:50 +0100 Subject: [PATCH 26/32] refactor(lsp): replace range-based with condition-based code action filtering --- lua/obsidian/lsp/handlers/_code_action.lua | 46 ++++++++++++---------- lua/obsidian/lsp/handlers/code_action.lua | 17 +++----- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/lua/obsidian/lsp/handlers/_code_action.lua b/lua/obsidian/lsp/handlers/_code_action.lua index adf971e91..5d6b4f903 100644 --- a/lua/obsidian/lsp/handlers/_code_action.lua +++ b/lua/obsidian/lsp/handlers/_code_action.lua @@ -3,7 +3,7 @@ local code_actions = {} ---@class obsidian.lsp.CodeActionOpts ---@field title string text display in code action interface ----@field range boolean|? whether to show action only in visual mode +---@field cond? fun(note: obsidian.Note): boolean function used to determine whether code actoin is shown ---Register a new command. ---@param opts obsidian.lsp.CodeActionOpts @@ -17,49 +17,55 @@ local add = function(name, opts) -- TODO: kind }, data = { - range = opts.range, + cond = opts.cond or function() + return true + end, -- TODO: preview? }, } code_actions[name] = action end ----@enum (key) obsidian.lsp.CodeAtionDefaults -local default_actions = { - rename = { - title = "Rename current note", - }, +local function in_visual() + return vim.api.nvim_get_mode().mode:find "v" ~= nil +end +local default_actions = { add_property = { title = "Add file property", }, + merge_note = { + title = "Merge current note into another note", + }, + + move_note = { + title = "Move current note to another folder", + }, + link = { title = "Link selection as name for a existing note", - range = true, + cond = in_visual, }, link_new = { title = "Link selection as name for a new note", - range = true, + cond = in_visual, }, extract_note = { title = "Extract selected text to a new note", - range = true, + cond = in_visual, }, - merge_note = { - title = "Merge this note into another note", + insert_template = { + title = "Insert template at cursor", + cond = function() + return Obsidian.opts.templates.enabled + end, }, } -if Obsidian.opts.templates.enabled then - default_actions.insert_template = { - title = "Insert template at cursor", - } -end - -- if Obsidian.opts.slides.enabled then -- default_actions.start_presentation = { -- name = "obsidian-ls.start_presentation", @@ -67,9 +73,7 @@ end -- } -- end --- TODO: merge a note to this note, after https://github.com/obsidian-nvim/obsidian.nvim/issues/655 - ----@param name string | obsidian.lsp.CodeAtionDefaults +---@param name string local del = function(name) code_actions[name] = nil end diff --git a/lua/obsidian/lsp/handlers/code_action.lua b/lua/obsidian/lsp/handlers/code_action.lua index 417385db0..62bbfe85c 100644 --- a/lua/obsidian/lsp/handlers/code_action.lua +++ b/lua/obsidian/lsp/handlers/code_action.lua @@ -1,26 +1,21 @@ local actions = require("obsidian.lsp.handlers._code_action").actions ---@param code_actions lsp.CodeAction[] ----@param in_selection boolean +---@param note obsidian.Note ---@return string[] -local function get_commands_by_context(code_actions, in_selection) +local function get_commands_by_context(code_actions, note) return vim .iter(vim.tbl_values(code_actions)) :filter(function(code_action) - if in_selection then - return code_action.data.range ~= nil - else - return code_action.data.range == nil - end + return code_action.data.cond(note) end) :totable() end ---@param params lsp.CodeActionParams return function(params, handler) - local range = params.range - local in_selection = range.start ~= range["end"] - local res = get_commands_by_context(actions, in_selection) - + local buf = vim.uri_to_bufnr(params.textDocument.uri) + local note = require("obsidian.note").from_buffer(buf) + local res = get_commands_by_context(actions, note) handler(nil, res) end From 29a2e583c8950f82d7fddb6cb474f614bfc01cf4 Mon Sep 17 00:00:00 2001 From: neo451 <412444506@qq.com> Date: Wed, 15 Apr 2026 23:13:10 +0100 Subject: [PATCH 27/32] slides module and proper register functions --- lua/obsidian/actions.lua | 46 ++++++++++++---------- lua/obsidian/config/default.lua | 8 +++- lua/obsidian/init.lua | 1 + lua/obsidian/lsp/handlers/_code_action.lua | 40 +++++++++++++------ lua/obsidian/types.lua | 2 + 5 files changed, 64 insertions(+), 33 deletions(-) diff --git a/lua/obsidian/actions.lua b/lua/obsidian/actions.lua index 3cb894a2f..b827087c0 100644 --- a/lua/obsidian/actions.lua +++ b/lua/obsidian/actions.lua @@ -691,6 +691,31 @@ M.workspace_symbol = function(query, callback) end) end +---Pick a folder under the vault root. +---@param callback fun(directory: string, text: string) +local function pick_folder(callback) + local root = tostring(Obsidian.workspace.root) + local choices = { { filename = root, text = "/" } } + + for path, t in vim.fs.dir(root, { depth = math.huge }) do + if t == "directory" then + choices[#choices + 1] = { + filename = vim.fs.joinpath(root, path), + text = path .. "/", + } + end + end + + Obsidian.picker.pick(choices, { + callback = function(entry) + callback(entry.filename, entry.text) + end, + format_item = function(v) + return tostring(v.text) + end, + }) +end + ---@param directory string ---@param text string local function move_note(directory, text) @@ -714,26 +739,7 @@ M.move_note = function() log.info "Not in an obsidian buffer" return end - local root = tostring(Obsidian.workspace.root) - local choices = { { filename = root, text = "/" } } - - for path, t in vim.fs.dir(root, { depth = math.huge }) do - if t == "directory" then - choices[#choices + 1] = { - filename = vim.fs.joinpath(root, path), - text = path .. "/", - } - end - end - - Obsidian.picker.pick(choices, { - callback = function(entry) - move_note(entry.filename, entry.text) - end, - format_item = function(v) - return tostring(v.text) - end, - }) + pick_folder(move_note) end ---@param new_name string|? diff --git a/lua/obsidian/config/default.lua b/lua/obsidian/config/default.lua index 53207e2cd..730d8866e 100644 --- a/lua/obsidian/config/default.lua +++ b/lua/obsidian/config/default.lua @@ -409,8 +409,14 @@ return { }, ---@class obsidian.config.CommentOpts - ---@field enabled boolean + ---@field enabled? boolean comment = { enabled = false, }, + + ---@class obsidian.config.SlidesOpts + ---@field enabled? boolean + slides = { + enabled = true, + }, } diff --git a/lua/obsidian/init.lua b/lua/obsidian/init.lua index 220f5b42e..a978c7332 100644 --- a/lua/obsidian/init.lua +++ b/lua/obsidian/init.lua @@ -4,6 +4,7 @@ local obsidian = {} obsidian.api = require "obsidian.api" obsidian.actions = require "obsidian.actions" +obsidian.code_action = require "obsidian.lsp.handlers._code_action" obsidian.async = require "obsidian.async" obsidian.Client = require "obsidian.client" obsidian.commands = require "obsidian.commands" diff --git a/lua/obsidian/lsp/handlers/_code_action.lua b/lua/obsidian/lsp/handlers/_code_action.lua index 5d6b4f903..16dcb1216 100644 --- a/lua/obsidian/lsp/handlers/_code_action.lua +++ b/lua/obsidian/lsp/handlers/_code_action.lua @@ -2,18 +2,20 @@ local code_actions = {} ---@class obsidian.lsp.CodeActionOpts +---@field name string unique name ---@field title string text display in code action interface ---@field cond? fun(note: obsidian.Note): boolean function used to determine whether code actoin is shown +---@field fn? function ---Register a new command. ---@param opts obsidian.lsp.CodeActionOpts -local add = function(name, opts) +local add = function(opts) -- TODO: validate local action = { title = opts.title, command = { title = opts.title, - command = "obsidian." .. name, + command = "obsidian." .. opts.name, -- TODO: kind }, data = { @@ -23,7 +25,13 @@ local add = function(name, opts) -- TODO: preview? }, } - code_actions[name] = action + + if opts.fn then + vim.lsp.commands["obsidian." .. opts.name] = vim.schedule_wrap(function(params) + opts.fn(unpack(params.arguments or {})) + end) + end + code_actions[opts.name] = action end local function in_visual() @@ -64,22 +72,30 @@ local default_actions = { return Obsidian.opts.templates.enabled end, }, -} --- if Obsidian.opts.slides.enabled then --- default_actions.start_presentation = { --- name = "obsidian-ls.start_presentation", --- title = "Start presentation", --- } --- end + start_presentation = { + title = "Start presentation", + cond = function() + return Obsidian.opts.slides.enabled + end, + }, + + new_from_url = { + title = "Create new note from url at cursor", + cond = function() + return not vim.tbl_isempty(vim.ui._get_urls()) + end, + }, +} ---@param name string local del = function(name) code_actions[name] = nil end -for name, action in pairs(default_actions) do - add(name, action) +for name, opts in pairs(default_actions) do + opts.name = name + add(opts) end return { diff --git a/lua/obsidian/types.lua b/lua/obsidian/types.lua index 98fbfcf71..e233e6390 100644 --- a/lua/obsidian/types.lua +++ b/lua/obsidian/types.lua @@ -51,6 +51,7 @@ ---@field link? obsidian.config.LinkOpts ---@field unique_note? obsidian.config.UniqueNoteOpts ---@field sync? obsidian.config.SyncOpts +---@field slides? obsidian.config.SlidesOpts ---@class obsidian.config.Internal ---@field workspaces obsidian.workspace.WorkspaceSpec[] @@ -80,5 +81,6 @@ ---@field link obsidian.config.LinkOpts ---@field unique_note obsidian.config.UniqueNoteOpts ---@field sync obsidian.config.SyncOpts +---@field slides obsidian.config.SlidesOpts ---@alias obsidian.config.NewNotesLocation "current_dir" | "notes_subdir" From 4efe19fa409c27e05998dc5d31ef1677df21ed62 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Mon, 20 Apr 2026 16:46:44 +0100 Subject: [PATCH 28/32] small fixes --- docs/Actions.md | 2 +- docs/LSP.md | 18 +++++++----------- lua/obsidian/actions.lua | 5 ----- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/docs/Actions.md b/docs/Actions.md index eb42c9de8..3a8b90f58 100644 --- a/docs/Actions.md +++ b/docs/Actions.md @@ -10,7 +10,7 @@ See [LSP code actions](LSP.md#code-actions) for actions exposed via the LSP inte | `insert_template` | `n` | Insert a template at cursor. | `name` | | `rename` | `n` | Rename current note. | `name` | | `toggle_checkbox` | `n`, `v` | Toggle (cycle) checkbox state. | | -| `set_checkbox` | `n`, `v` | Set to specifi checkbox state. | `state` | +| `set_checkbox` | `n`, `v` | Set to specific checkbox state. | `state` | | `link` | `v` | Link selection to an existing note. | | | `link_new` | `v` | Create a new note and link selection. | `title` | | `extract_note` | `v` | Move selection to a new note. | `title` | diff --git a/docs/LSP.md b/docs/LSP.md index 34c14edf2..2d2f91e0a 100644 --- a/docs/LSP.md +++ b/docs/LSP.md @@ -44,9 +44,9 @@ obsidian.nvim exposes a small set of LSP code actions for common note operations Available actions: - Normal mode: - - Rename current note (`rename`) - Insert template at cursor (`insert_template`) - Add file property (`add_property`) + - TODO: - Visual mode: - Link selection as name for an existing note (`link`) - Link selection as name for a new note (`link_new`) @@ -63,15 +63,11 @@ API: - `name`: command id (snake_case recommended). - `title`: text shown in the code action picker. - `fn`: function invoked when the action is executed. - - `range` (optional): when `true`, the action only appears for a visual selection. + - `cond` (optional): a filter function that gets the current note object, determines whether actions is listed. - `require("obsidian").code_action.del(name)` removes a previously registered action. - - - - - - - - - +Example: + +```lua +-- TODO: +``` diff --git a/lua/obsidian/actions.lua b/lua/obsidian/actions.lua index b827087c0..3537d1961 100644 --- a/lua/obsidian/actions.lua +++ b/lua/obsidian/actions.lua @@ -742,11 +742,6 @@ M.move_note = function() pick_folder(move_note) end ----@param new_name string|? -M.rename = function(new_name) - vim.lsp.buf.rename(new_name) -end - ---@param dst_note obsidian.Note local function merge_note(dst_note) local current_note = api.current_note() From 7b2a23b0c04d9724383dc7271148e58fad5cd002 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Mon, 20 Apr 2026 21:06:25 +0100 Subject: [PATCH 29/32] final cleanup --- docs/Actions.md | 35 +++++++++++++--------- docs/LSP.md | 15 ++++------ lua/obsidian/lsp/handlers/_code_action.lua | 7 ----- 3 files changed, 26 insertions(+), 31 deletions(-) diff --git a/docs/Actions.md b/docs/Actions.md index 3a8b90f58..d0ef7ab6a 100644 --- a/docs/Actions.md +++ b/docs/Actions.md @@ -1,16 +1,23 @@ See [LSP code actions](LSP.md#code-actions) for actions exposed via the LSP interface. -| name | mode | description | arguments | -| ------------------- | -------- | ------------------------------------- | ----------- | -| `follow_link` | `n` | Open the link under the cursor. | | -| `nav_link` | `n` | Navigate to next/previous link. | `direction` | -| `smart_action` | `n` | Context-aware action on the cursor. | | -| `new_from_template` | `n` | Create a new note from a template. | | -| `add_property` | `n` | Add frontmatter property. | | -| `insert_template` | `n` | Insert a template at cursor. | `name` | -| `rename` | `n` | Rename current note. | `name` | -| `toggle_checkbox` | `n`, `v` | Toggle (cycle) checkbox state. | | -| `set_checkbox` | `n`, `v` | Set to specific checkbox state. | `state` | -| `link` | `v` | Link selection to an existing note. | | -| `link_new` | `v` | Create a new note and link selection. | `title` | -| `extract_note` | `v` | Move selection to a new note. | `title` | +| name | mode | description | arguments | +| -------------------- | -------- | -------------------------------------- | -------------------- | +| `follow_link` | `n` | Open the link under the cursor. | `link` | +| `nav_link` | `n` | Navigate to next/previous link. | `direction` | +| `smart_action` | `n` | Context-aware action on the cursor. | | +| `new` | `n` | Create a new note. | `id` | +| `new_from_template` | `n` | Create a new note from a template. | `id`, `template` | +| `unique_note` | `n` | Create a unique note. | `timestamp` | +| `unique_link` | `n` | Create and insert a unique note link. | `timestamp` | +| `add_property` | `n` | Add frontmatter property. | | +| `insert_template` | `n` | Insert a template at cursor. | `name` | +| `rename` | `n` | Rename current note. | `name` | +| `move_note` | `n` | Move current note to another folder. | | +| `merge_note` | `n` | Merge current note into another note. | `dst_note` | +| `start_presentation` | `n` | Start slide presentation. | `buf` | +| `workspace_symbol` | `n` | Search notes, aliases, and headings. | `query` | +| `toggle_checkbox` | `n`, `v` | Toggle (cycle) checkbox state. | `start_lnum`, `end_lnum` | +| `set_checkbox` | `n`, `v` | Set to specific checkbox state. | `state` | +| `link` | `v` | Link selection to an existing note. | | +| `link_new` | `v` | Create a new note and link selection. | `title` | +| `extract_note` | `v` | Move selection to a new note. | `title` | diff --git a/docs/LSP.md b/docs/LSP.md index 2d2f91e0a..941d319bd 100644 --- a/docs/LSP.md +++ b/docs/LSP.md @@ -44,9 +44,11 @@ obsidian.nvim exposes a small set of LSP code actions for common note operations Available actions: - Normal mode: - - Insert template at cursor (`insert_template`) - Add file property (`add_property`) - - TODO: + - Insert template at cursor (`insert_template`, requires templates enabled) + - Move current note to another folder (`move_note`) + - Merge current note into another note (`merge_note`) + - Start presentation (`start_presentation`, requires slides enabled) - Visual mode: - Link selection as name for an existing note (`link`) - Link selection as name for a new note (`link_new`) @@ -54,8 +56,7 @@ Available actions: ### Code Action API -You can register custom code actions via `require("obsidian").code_action`. Each action is exposed as an LSP -command, so register actions before calling `require"obsidian".setup{}`. +You can register custom code actions via `require("obsidian").code_action` module. API: @@ -65,9 +66,3 @@ API: - `fn`: function invoked when the action is executed. - `cond` (optional): a filter function that gets the current note object, determines whether actions is listed. - `require("obsidian").code_action.del(name)` removes a previously registered action. - -Example: - -```lua --- TODO: -``` diff --git a/lua/obsidian/lsp/handlers/_code_action.lua b/lua/obsidian/lsp/handlers/_code_action.lua index 16dcb1216..dc025212d 100644 --- a/lua/obsidian/lsp/handlers/_code_action.lua +++ b/lua/obsidian/lsp/handlers/_code_action.lua @@ -79,13 +79,6 @@ local default_actions = { return Obsidian.opts.slides.enabled end, }, - - new_from_url = { - title = "Create new note from url at cursor", - cond = function() - return not vim.tbl_isempty(vim.ui._get_urls()) - end, - }, } ---@param name string From 4c03091325abcdc8879365c2c04ee4c025c560d9 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Mon, 20 Apr 2026 21:07:39 +0100 Subject: [PATCH 30/32] rename --- lua/obsidian/commands/rename.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/obsidian/commands/rename.lua b/lua/obsidian/commands/rename.lua index e8fe2d122..53e87fab4 100644 --- a/lua/obsidian/commands/rename.lua +++ b/lua/obsidian/commands/rename.lua @@ -4,5 +4,5 @@ return function(data) if string.len(new_name) > 0 then new_name = vim.trim(data.args) end - require("obsidian.actions").rename(new_name) + vim.lsp.buf.rename(new_name) end From 947930713aaf551e902160b32e49b5d68b503ea4 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Mon, 20 Apr 2026 21:14:42 +0100 Subject: [PATCH 31/32] patch --- lua/obsidian/commands/rename.lua | 2 +- lua/obsidian/lsp/init.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/obsidian/commands/rename.lua b/lua/obsidian/commands/rename.lua index 53e87fab4..e0ec148cc 100644 --- a/lua/obsidian/commands/rename.lua +++ b/lua/obsidian/commands/rename.lua @@ -1,7 +1,7 @@ ---@param data obsidian.CommandArgs return function(data) local new_name - if string.len(new_name) > 0 then + if data.args and string.len(data.args) > 0 then new_name = vim.trim(data.args) end vim.lsp.buf.rename(new_name) diff --git a/lua/obsidian/lsp/init.lua b/lua/obsidian/lsp/init.lua index 409a8fff0..9c12541e7 100644 --- a/lua/obsidian/lsp/init.lua +++ b/lua/obsidian/lsp/init.lua @@ -23,7 +23,7 @@ lsp.start = function(buf) local client_id = vim.lsp.start(lsp_config, { bufnr = buf, silent = false }) if not client_id then - log.err("[obsidian-ls]: failed to start: " .. client_id) + log.err "[obsidian-ls]: failed to start" return end From c992284cc760b05ea95e749e5131f1f286dffdbf Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Mon, 20 Apr 2026 21:16:45 +0100 Subject: [PATCH 32/32] doc: progress --- docs/LSP-Progress.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/LSP-Progress.md b/docs/LSP-Progress.md index c5fb69432..443a4fe82 100644 --- a/docs/LSP-Progress.md +++ b/docs/LSP-Progress.md @@ -14,11 +14,11 @@ Tracking implementation status of [LSP 3.17](https://microsoft.github.io/languag - [x] Document Symbols (`textDocument/documentSymbol`) - returns markdown headings - [x] Rename (`textDocument/rename`) - rename notes and update all references across the vault - [x] Prepare Rename (`textDocument/prepareRename`) +- [x] Code Action (`textDocument/codeAction`) - [ ] Hover (`textDocument/hover`) - [ ] Completion Proposals (`textDocument/completion`) - currently handled outside LSP via nvim-cmp/blink.cmp - [ ] Completion Item Resolve (`completionItem/resolve`) - [ ] Publish Diagnostics (`textDocument/publishDiagnostics`) -- [ ] Code Action (`textDocument/codeAction`) - [ ] Code Action Resolve (`codeAction/resolve`) - [ ] Document Link (`textDocument/documentLink`) - [ ] Document Link Resolve (`documentLink/resolve`)