From 8f9cf5dc8163bbb8c53aaade060a0706d769bc47 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Sat, 12 Apr 2025 03:07:18 +0800 Subject: [PATCH 01/22] feat: obsidian_ls --- CHANGELOG.md | 1 + lua/obsidian/lsp/code_action.lua | 5 ++ lua/obsidian/lsp/handlers.lua | 15 +++++ lua/obsidian/lsp/handlers/completion.lua | 57 +++++++++++++++++++ lua/obsidian/lsp/handlers/config.lua | 3 + lua/obsidian/lsp/handlers/hover.lua | 25 ++++++++ lua/obsidian/lsp/handlers/initialize.lua | 50 ++++++++++++++++ lua/obsidian/lsp/handlers/rename.lua | 5 ++ lua/obsidian/lsp/handlers/resolve.lua | 9 +++ .../lsp/handlers/workplace_diagnostics.lua | 4 ++ lua/obsidian/lsp/init.lua | 37 ++++++++++++ 11 files changed, 211 insertions(+) create mode 100644 lua/obsidian/lsp/code_action.lua create mode 100644 lua/obsidian/lsp/handlers.lua create mode 100644 lua/obsidian/lsp/handlers/completion.lua create mode 100644 lua/obsidian/lsp/handlers/config.lua create mode 100644 lua/obsidian/lsp/handlers/hover.lua create mode 100644 lua/obsidian/lsp/handlers/initialize.lua create mode 100644 lua/obsidian/lsp/handlers/rename.lua create mode 100644 lua/obsidian/lsp/handlers/resolve.lua create mode 100644 lua/obsidian/lsp/handlers/workplace_diagnostics.lua create mode 100644 lua/obsidian/lsp/init.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 571277fe..6a885830 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added `makefile types` target to check types via lua-ls +- Added a builtin lsp to handle completion, hover, diagnostics and code actions ### Changed diff --git a/lua/obsidian/lsp/code_action.lua b/lua/obsidian/lsp/code_action.lua new file mode 100644 index 00000000..4a094fe4 --- /dev/null +++ b/lua/obsidian/lsp/code_action.lua @@ -0,0 +1,5 @@ +-- ObsidianExtractNote +-- ObsidianQuickSwitch +-- ObsidianNew +-- ObsidianBacklinks +-- ObsidianDailies diff --git a/lua/obsidian/lsp/handlers.lua b/lua/obsidian/lsp/handlers.lua new file mode 100644 index 00000000..d0726707 --- /dev/null +++ b/lua/obsidian/lsp/handlers.lua @@ -0,0 +1,15 @@ +local ms = vim.lsp.protocol.Methods + +return setmetatable({ + [ms.initialize] = require "obsidian.lsp.handlers.initialize", + [ms.textDocument_completion] = require "obsidian.lsp.handlers.completion", + [ms.completionItem_resolve] = require "obsidian.lsp.handlers.resolve", + [ms.textDocument_hover] = require "obsidian.lsp.handlers.hover", + [ms.workspace_diagnostic] = require "obsidian.lsp.handlers.workplace_diagnostics", + [ms.textDocument_rename] = require "obsidian.lsp.handlers.rename", +}, { + __index = function(t, k) + print("obsidian_ls does not support method " .. k .. " yet") + return function() end + end, +}) diff --git a/lua/obsidian/lsp/handlers/completion.lua b/lua/obsidian/lsp/handlers/completion.lua new file mode 100644 index 00000000..467d846b --- /dev/null +++ b/lua/obsidian/lsp/handlers/completion.lua @@ -0,0 +1,57 @@ +local obsidian_client = require("obsidian").get_client() +local link_style = obsidian_client.opts.preferred_link_style + +local function calc_insert_text(note, text_after_cursor) + -- TODO: consider incomplete + if link_style == "markdown" then + return note.title .. "](" .. note.path.filename .. ")" + else + return note.title .. "]]" + end +end + +return function(_, params, handler, _) + local uri = params.textDocument.uri + local line_num = params.position.line + local char_num = params.position.character + + local file_path = vim.uri_to_fname(uri) + local buf = vim.fn.bufnr(file_path, false) + + local line_text = (vim.api.nvim_buf_get_lines(buf, line_num, line_num + 1, false)[1] or "") + local text_before_cursor = line_text:sub(1, char_num) + + local trigger_pattern = "%[%[.*$" + if link_style == "markdown" then + trigger_pattern = "%[.*$" + end + + local bracket_start = text_before_cursor:find(trigger_pattern) + + if not bracket_start then + handler(nil, { items = {} }, params.context) + return + end + + local partial = text_before_cursor:sub(bracket_start + 2) + + local items = {} + obsidian_client:find_notes_async( + partial, + vim.schedule_wrap(function(notes) + for _, note in ipairs(notes) do + local title = note.title + if title and title:lower():find(partial:lower(), 1, true) then + table.insert(items, { + kind = "File", + label = title, + filterText = title, + insertText = calc_insert_text(note), + labelDetails = { description = "Obsidian" }, + }) + end + end + handler(nil, { items = items }) + end) + ) +end diff --git a/lua/obsidian/lsp/handlers/config.lua b/lua/obsidian/lsp/handlers/config.lua new file mode 100644 index 00000000..d73cc6fd --- /dev/null +++ b/lua/obsidian/lsp/handlers/config.lua @@ -0,0 +1,3 @@ +return { + complete = true, +} diff --git a/lua/obsidian/lsp/handlers/hover.lua b/lua/obsidian/lsp/handlers/hover.lua new file mode 100644 index 00000000..3ea1b51b --- /dev/null +++ b/lua/obsidian/lsp/handlers/hover.lua @@ -0,0 +1,25 @@ +local obsidian_client = require("obsidian").get_client() + +return function(_, params, handler, _) + --- TODO: more precise sense of node under cursor + --- TODO: hover on tags? + --- TODO: not work on frontmatter? + local note_name = vim.fn.expand "" + obsidian_client:find_notes_async( + note_name, + vim.schedule_wrap(function(notes) + for i, note in ipairs(notes) do + if vim.uri_from_fname(note.path.filename) == params.textDocument.uri then + table.remove(notes, i) + end + end + local note = notes[1] + if note then + local content = table.concat(vim.fn.readfile(note.path.filename), "\n") + handler(nil, { contents = content }) + else + vim.notify("No notes found", 3) + end + end) + ) +end diff --git a/lua/obsidian/lsp/handlers/initialize.lua b/lua/obsidian/lsp/handlers/initialize.lua new file mode 100644 index 00000000..f3da9d9f --- /dev/null +++ b/lua/obsidian/lsp/handlers/initialize.lua @@ -0,0 +1,50 @@ +local config = require "obsidian.lsp.handlers.config" + +local completion_options + +if config.complete then + completion_options = { + triggerCharacters = { "[[" }, + resolveProvider = true, + completionItem = { + labelDetailsSupport = true, + }, + } +else + completion_options = false +end + +local initializeResult = { + capabilities = { + hoverProvider = true, + definitionProvider = true, + implementationProvider = true, + declarationProvider = true, + signatureHelpProvider = { + triggerCharacters = { "(", "," }, + retriggerCharacters = {}, + }, + -- Add diagnostic support + diagnosticProvider = { + interFileDependencies = false, + workspaceDiagnostics = true, + }, + typeDefinitionProvider = true, + renameProvider = true, + referencesProvider = true, + documentSymbolProvider = true, + completionProvider = completion_options, + textDocumentSync = { + openClose = true, + change = 2, + }, + }, + serverInfo = { + name = "obsidian-ls", + version = "1.0.0", + }, +} + +return function(_, params, handler, _) + return handler(nil, initializeResult, params.context) +end diff --git a/lua/obsidian/lsp/handlers/rename.lua b/lua/obsidian/lsp/handlers/rename.lua new file mode 100644 index 00000000..fef7b531 --- /dev/null +++ b/lua/obsidian/lsp/handlers/rename.lua @@ -0,0 +1,5 @@ +local obsidian_client = require("obsidian").get_client() + +return function(_, param, _, _) + require "obsidian.commands.rename"(obsidian_client, { args = param.newName }) +end diff --git a/lua/obsidian/lsp/handlers/resolve.lua b/lua/obsidian/lsp/handlers/resolve.lua new file mode 100644 index 00000000..c13530c3 --- /dev/null +++ b/lua/obsidian/lsp/handlers/resolve.lua @@ -0,0 +1,9 @@ +return function(_, params, handler, _) + params.documentation = { + value = [[# Heading 1 +[link](https://example.com) + ]], + kind = "markdown", + } + handler(nil, params) +end diff --git a/lua/obsidian/lsp/handlers/workplace_diagnostics.lua b/lua/obsidian/lsp/handlers/workplace_diagnostics.lua new file mode 100644 index 00000000..44067112 --- /dev/null +++ b/lua/obsidian/lsp/handlers/workplace_diagnostics.lua @@ -0,0 +1,4 @@ +return function(_, params, handler, _) + print "diagnosticing workspace" + return +end diff --git a/lua/obsidian/lsp/init.lua b/lua/obsidian/lsp/init.lua new file mode 100644 index 00000000..a175118b --- /dev/null +++ b/lua/obsidian/lsp/init.lua @@ -0,0 +1,37 @@ +-- reference: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/ +-- reference: https://github.com/zk-org/zk/blob/main/internal/adapter/lsp/server.go +local obsidian_client = require("obsidian").get_client() +local handlers = require "obsidian.lsp.handlers" + +local obsidian_ls = {} +local capabilities = vim.lsp.protocol.make_client_capabilities() +local has_blink, blink = pcall(require, "blink.cmp") +if has_blink then + capabilities = blink.get_lsp_capabilities({}, true) +end + +---@return integer? client_id +obsidian_ls.start = function() + local client_id = vim.lsp.start { + name = "obsidian-ls", + capabilities = capabilities, + cmd = function(dispatchers) + local _ = dispatchers + local members = { + request = function(method, params, handler, _) + print(method) + handlers[method](method, params, handler, _) + end, + notify = function() end, -- Handle notify events + is_closing = function() end, + terminate = function() end, + } + return members + end, + init_options = {}, + root_dir = tostring(obsidian_client.dir), + } + return client_id +end + +return obsidian_ls From 7f29c66ced2114f92c54daaae31db8e029e4ef95 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Sat, 12 Apr 2025 17:52:01 +0800 Subject: [PATCH 02/22] feat: tags completion, better hover link recognition --- lua/obsidian/init.lua | 16 +++-- lua/obsidian/lsp/handlers/completion.lua | 87 +++++++++++++++--------- lua/obsidian/lsp/handlers/hover.lua | 54 +++++++++------ lua/obsidian/lsp/handlers/initialize.lua | 8 +-- 4 files changed, 102 insertions(+), 63 deletions(-) diff --git a/lua/obsidian/init.lua b/lua/obsidian/init.lua index 669ea56f..120fc37b 100644 --- a/lua/obsidian/init.lua +++ b/lua/obsidian/init.lua @@ -145,11 +145,15 @@ obsidian.setup = function(opts) end -- Inject completion sources, providers to their plugin configurations - if opts.completion.nvim_cmp then - require("obsidian.completion.plugin_initializers.nvim_cmp").inject_sources() - elseif opts.completion.blink then - require("obsidian.completion.plugin_initializers.blink").inject_sources() - end + -- if opts.completion.nvim_cmp then + -- require("obsidian.completion.plugin_initializers.nvim_cmp").inject_sources() + -- elseif opts.completion.blink then + -- require("obsidian.completion.plugin_initializers.blink").inject_sources() + -- end + + vim.bo[ev.buf].omnifunc = "v:lua.vim.lsp.omnifunc" + vim.bo[ev.buf].completeopt = "menu,menuone,noselect" + require("obsidian.lsp").start() local win = vim.api.nvim_get_current_win() @@ -164,7 +168,7 @@ obsidian.setup = function(opts) -- Run enter-note callback. client.callback_manager:enter_note(function() - return obsidian.Note.from_buffer(ev.bufnr) + return obsidian.Note.from_buffer(ev.buf) end) end, }) diff --git a/lua/obsidian/lsp/handlers/completion.lua b/lua/obsidian/lsp/handlers/completion.lua index 467d846b..5f9d4767 100644 --- a/lua/obsidian/lsp/handlers/completion.lua +++ b/lua/obsidian/lsp/handlers/completion.lua @@ -1,52 +1,28 @@ local obsidian_client = require("obsidian").get_client() local link_style = obsidian_client.opts.preferred_link_style -local function calc_insert_text(note, text_after_cursor) - -- TODO: consider incomplete +local function calc_insert_text(note, partial) + local title = note.title if link_style == "markdown" then - return note.title .. "](" .. note.path.filename .. ")" + return title .. "](" .. note.path.filename .. ")" else - return note.title .. "]]" + return title .. "]]" end end -return function(_, params, handler, _) - local uri = params.textDocument.uri - local line_num = params.position.line - local char_num = params.position.character - - local file_path = vim.uri_to_fname(uri) - local buf = vim.fn.bufnr(file_path, false) - - local line_text = (vim.api.nvim_buf_get_lines(buf, line_num, line_num + 1, false)[1] or "") - local text_before_cursor = line_text:sub(1, char_num) - - local trigger_pattern = "%[%[.*$" - if link_style == "markdown" then - trigger_pattern = "%[.*$" - end - - local bracket_start = text_before_cursor:find(trigger_pattern) - - if not bracket_start then - handler(nil, { items = {} }, params.context) - return - end - - local partial = text_before_cursor:sub(bracket_start + 2) - +local function build_ref_items(partial, handler) local items = {} obsidian_client:find_notes_async( partial, vim.schedule_wrap(function(notes) for _, note in ipairs(notes) do local title = note.title - if title and title:lower():find(partial:lower(), 1, true) then + if title and title:lower():find(vim.pesc(partial:lower())) then table.insert(items, { kind = "File", label = title, filterText = title, - insertText = calc_insert_text(note), + insertText = calc_insert_text(note, partial), labelDetails = { description = "Obsidian" }, }) end @@ -55,3 +31,52 @@ return function(_, params, handler, _) end) ) end + +local function build_tag_items(partial, handler) + local items = {} + local tags = obsidian_client:list_tags_async(partial, function(tags) + for _, tag in ipairs(tags) do + if tag and tag:lower():find(vim.pesc(partial:lower())) then + table.insert(items, { + kind = "File", + label = tag, + filterText = tag, + insertText = tag, + labelDetails = { description = "ObsidianTag" }, + }) + end + end + handler(nil, { + items = items, + }) + end) +end + +return function(_, params, handler, _) + local uri = params.textDocument.uri + local line_num = params.position.line + local char_num = params.position.character + + local file_path = vim.uri_to_fname(uri) + local buf = vim.fn.bufnr(file_path, false) + + local line_text = (vim.api.nvim_buf_get_lines(buf, line_num, line_num + 1, false)[1] or "") + local text_before_cursor = line_text:sub(1, char_num) + + local trigger_pattern = "[[" + if link_style == "markdown" then + trigger_pattern = "[" + end + + local hastag_start = text_before_cursor:find("#", 1, true) + + local bracket_start = text_before_cursor:find(vim.pesc(trigger_pattern)) + + if bracket_start then + local partial = text_before_cursor:sub(bracket_start + 2) + build_ref_items(partial, handler) + elseif hastag_start then + local partial = text_before_cursor:sub(hastag_start + 1) + build_tag_items(partial, handler) + end +end diff --git a/lua/obsidian/lsp/handlers/hover.lua b/lua/obsidian/lsp/handlers/hover.lua index 3ea1b51b..cceb20ba 100644 --- a/lua/obsidian/lsp/handlers/hover.lua +++ b/lua/obsidian/lsp/handlers/hover.lua @@ -1,25 +1,39 @@ local obsidian_client = require("obsidian").get_client() +local util = require "obsidian.util" + +--- TODO: hover on tags +--- TODO: should not work on frontmatter? + +local function read_file(file) + local fd = assert(io.open(file, "r")) + ---@type string + local data = fd:read "*a" + fd:close() + return data +end return function(_, params, handler, _) - --- TODO: more precise sense of node under cursor - --- TODO: hover on tags? - --- TODO: not work on frontmatter? - local note_name = vim.fn.expand "" - obsidian_client:find_notes_async( - note_name, - vim.schedule_wrap(function(notes) - for i, note in ipairs(notes) do - if vim.uri_from_fname(note.path.filename) == params.textDocument.uri then - table.remove(notes, i) + local term = util.parse_cursor_link() + if term then + obsidian_client:find_notes_async( + term, + vim.schedule_wrap(function(notes) + for i, note in ipairs(notes) do + if vim.uri_from_fname(note.path.filename) == params.textDocument.uri then + table.remove(notes, i) + end + end + local note = notes[1] + if note then + handler(nil, { + contents = read_file(note.path.filename), + }) + else + vim.notify("No notes found", 3) end - end - local note = notes[1] - if note then - local content = table.concat(vim.fn.readfile(note.path.filename), "\n") - handler(nil, { contents = content }) - else - vim.notify("No notes found", 3) - end - end) - ) + end) + ) + else + vim.notify("No notes found", 3) + end end diff --git a/lua/obsidian/lsp/handlers/initialize.lua b/lua/obsidian/lsp/handlers/initialize.lua index f3da9d9f..465e992c 100644 --- a/lua/obsidian/lsp/handlers/initialize.lua +++ b/lua/obsidian/lsp/handlers/initialize.lua @@ -4,7 +4,7 @@ local completion_options if config.complete then completion_options = { - triggerCharacters = { "[[" }, + triggerCharacters = { "[", "#" }, resolveProvider = true, completionItem = { labelDetailsSupport = true, @@ -20,11 +20,7 @@ local initializeResult = { definitionProvider = true, implementationProvider = true, declarationProvider = true, - signatureHelpProvider = { - triggerCharacters = { "(", "," }, - retriggerCharacters = {}, - }, - -- Add diagnostic support + -- TODO: Add diagnostic support diagnosticProvider = { interFileDependencies = false, workspaceDiagnostics = true, From 9d3f9244e10863c47022bfeaf8c2bc7d2abcbd9f Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Sat, 12 Apr 2025 19:48:43 +0800 Subject: [PATCH 03/22] initial references impl for tags and backlinks --- lua/obsidian/lsp/handlers.lua | 3 +- lua/obsidian/lsp/handlers/completion.lua | 38 +++++---- lua/obsidian/lsp/handlers/hover.lua | 2 +- lua/obsidian/lsp/handlers/initialize.lua | 2 +- lua/obsidian/lsp/handlers/references.lua | 79 +++++++++++++++++++ lua/obsidian/lsp/handlers/rename.lua | 2 +- lua/obsidian/lsp/handlers/resolve.lua | 42 ++++++++-- .../lsp/handlers/workplace_diagnostics.lua | 2 +- lua/obsidian/lsp/init.lua | 3 +- 9 files changed, 142 insertions(+), 31 deletions(-) create mode 100644 lua/obsidian/lsp/handlers/references.lua diff --git a/lua/obsidian/lsp/handlers.lua b/lua/obsidian/lsp/handlers.lua index d0726707..3b3aa6c7 100644 --- a/lua/obsidian/lsp/handlers.lua +++ b/lua/obsidian/lsp/handlers.lua @@ -7,8 +7,9 @@ return setmetatable({ [ms.textDocument_hover] = require "obsidian.lsp.handlers.hover", [ms.workspace_diagnostic] = require "obsidian.lsp.handlers.workplace_diagnostics", [ms.textDocument_rename] = require "obsidian.lsp.handlers.rename", + [ms.textDocument_references] = require "obsidian.lsp.handlers.references", }, { - __index = function(t, k) + __index = function(_, k) print("obsidian_ls does not support method " .. k .. " yet") return function() end end, diff --git a/lua/obsidian/lsp/handlers/completion.lua b/lua/obsidian/lsp/handlers/completion.lua index 5f9d4767..0fa55501 100644 --- a/lua/obsidian/lsp/handlers/completion.lua +++ b/lua/obsidian/lsp/handlers/completion.lua @@ -1,6 +1,9 @@ local obsidian_client = require("obsidian").get_client() local link_style = obsidian_client.opts.preferred_link_style +-- TODO: completion for anchor, blocks +-- TODO: complete wiki format like nvim-cmp source and obsidan app + local function calc_insert_text(note, partial) local title = note.title if link_style == "markdown" then @@ -34,25 +37,28 @@ end local function build_tag_items(partial, handler) local items = {} - local tags = obsidian_client:list_tags_async(partial, function(tags) - for _, tag in ipairs(tags) do - if tag and tag:lower():find(vim.pesc(partial:lower())) then - table.insert(items, { - kind = "File", - label = tag, - filterText = tag, - insertText = tag, - labelDetails = { description = "ObsidianTag" }, - }) + obsidian_client:list_tags_async( + partial, + vim.schedule_wrap(function(tags) + for _, tag in ipairs(tags) do + if tag and tag:lower():find(vim.pesc(partial:lower())) then + table.insert(items, { + kind = "File", + label = tag, + filterText = tag, + insertText = tag, + labelDetails = { description = "ObsidianTag" }, + }) + end end - end - handler(nil, { - items = items, - }) - end) + handler(nil, { + items = items, + }) + end) + ) end -return function(_, params, handler, _) +return function(params, handler, _) local uri = params.textDocument.uri local line_num = params.position.line local char_num = params.position.character diff --git a/lua/obsidian/lsp/handlers/hover.lua b/lua/obsidian/lsp/handlers/hover.lua index cceb20ba..91f09080 100644 --- a/lua/obsidian/lsp/handlers/hover.lua +++ b/lua/obsidian/lsp/handlers/hover.lua @@ -12,7 +12,7 @@ local function read_file(file) return data end -return function(_, params, handler, _) +return function(params, handler, _) local term = util.parse_cursor_link() if term then obsidian_client:find_notes_async( diff --git a/lua/obsidian/lsp/handlers/initialize.lua b/lua/obsidian/lsp/handlers/initialize.lua index 465e992c..8fddfa18 100644 --- a/lua/obsidian/lsp/handlers/initialize.lua +++ b/lua/obsidian/lsp/handlers/initialize.lua @@ -41,6 +41,6 @@ local initializeResult = { }, } -return function(_, params, handler, _) +return function(params, handler, _) return handler(nil, initializeResult, params.context) end diff --git a/lua/obsidian/lsp/handlers/references.lua b/lua/obsidian/lsp/handlers/references.lua new file mode 100644 index 00000000..bc8e3794 --- /dev/null +++ b/lua/obsidian/lsp/handlers/references.lua @@ -0,0 +1,79 @@ +---@diagnostic disable: missing-fields + +local obsidian_client = require("obsidian").get_client() +local util = require "obsidian.util" +local Note = require "obsidian.note" + +-- TODO: references for anchor, blocks + +return function(_, handler, _) + local tag = util.cursor_tag(nil, nil) + if tag then + obsidian_client:find_tags_async(tag, function(tag_locations) + local entries = {} + for _, tag_loc in ipairs(tag_locations) do + if tag_loc.tag == tag or vim.startswith(tag_loc.tag, tag .. "/") then + local line = tag_loc.line - 1 -- lsp wants zero-indexed + local tag_start = (tag_loc.tag_start or 1) - 1 + entries[#entries + 1] = { + uri = vim.uri_from_fname(tostring(tag_loc.path)), + range = { + start = { line = line, character = tag_start }, + ["end"] = { line = line, character = tag_start }, + }, + } + end + end + if vim.tbl_isempty(entries) then + vim.notify("Tag not found", 3) + return + end + handler(nil, entries) + end, { search = { sort = true } }) + end + + local buf = vim.api.nvim_get_current_buf() + + local note = Note.from_buffer(buf) + + obsidian_client:find_backlinks_async( + note, + vim.schedule_wrap(function(backlinks) + -- if vim.tbl_isempty(backlinks) then + -- if opts.anchor then + -- log.info("No backlinks found for anchor '%s' in note '%s'", opts.anchor, note.id) + -- elseif opts.block then + -- log.info("No backlinks found for block '%s' in note '%s'", opts.block, note.id) + -- else + -- log.info("No backlinks found for note '%s'", note.id) + -- end + -- return + -- end + -- + local entries = {} + for _, matches in ipairs(backlinks) do + for _, match in ipairs(matches.matches) do + entries[#entries + 1] = { + uri = vim.uri_from_fname(tostring(matches.path)), + range = { + start = { line = match.line, character = 1 }, + ["end"] = { line = match.line, character = 1 }, + }, + } + end + end + + handler(nil, entries) + + -- ---@type string + -- local prompt_title + -- if opts.anchor then + -- prompt_title = string.format("Backlinks to '%s%s'", note.id, opts.anchor) + -- elseif opts.block then + -- prompt_title = string.format("Backlinks to '%s#%s'", note.id, util.standardize_block(opts.block)) + -- else + -- prompt_title = string.format("Backlinks to '%s'", note.id) + -- end + end) + ) +end diff --git a/lua/obsidian/lsp/handlers/rename.lua b/lua/obsidian/lsp/handlers/rename.lua index fef7b531..d87306b7 100644 --- a/lua/obsidian/lsp/handlers/rename.lua +++ b/lua/obsidian/lsp/handlers/rename.lua @@ -1,5 +1,5 @@ local obsidian_client = require("obsidian").get_client() -return function(_, param, _, _) +return function(param, _, _) require "obsidian.commands.rename"(obsidian_client, { args = param.newName }) end diff --git a/lua/obsidian/lsp/handlers/resolve.lua b/lua/obsidian/lsp/handlers/resolve.lua index c13530c3..666fa6c7 100644 --- a/lua/obsidian/lsp/handlers/resolve.lua +++ b/lua/obsidian/lsp/handlers/resolve.lua @@ -1,9 +1,35 @@ -return function(_, params, handler, _) - params.documentation = { - value = [[# Heading 1 -[link](https://example.com) - ]], - kind = "markdown", - } - handler(nil, params) +local obsidian_client = require("obsidian").get_client() + +-- TODO: not working + +return function(params, handler, _) + local buf = vim.api.nvim_get_current_buf() + local current_uri = vim.uri_from_bufnr(buf) + obsidian_client:find_notes_async( + params.label, + vim.schedule_wrap(function(notes) + for i, note in ipairs(notes) do + if vim.uri_from_fname(note.path.filename) == current_uri then + table.remove(notes, i) + end + end + local note = notes[1] + if note then + local content = table.concat(vim.fn.readfile(note.path.filename), "\n") + handler(nil, { + value = content, + kind = "markdown", + }) + else + vim.notify("No notes found", 3) + end + end) + ) + -- params.documentation = { + -- value = [[# Heading 1 + -- [link](https://example.com) + -- ]], + -- kind = "markdown", + -- } + -- handler(nil, params) end diff --git a/lua/obsidian/lsp/handlers/workplace_diagnostics.lua b/lua/obsidian/lsp/handlers/workplace_diagnostics.lua index 44067112..f13c9282 100644 --- a/lua/obsidian/lsp/handlers/workplace_diagnostics.lua +++ b/lua/obsidian/lsp/handlers/workplace_diagnostics.lua @@ -1,4 +1,4 @@ -return function(_, params, handler, _) +return function(params, handler, _) print "diagnosticing workspace" return end diff --git a/lua/obsidian/lsp/init.lua b/lua/obsidian/lsp/init.lua index a175118b..ac0d4ca9 100644 --- a/lua/obsidian/lsp/init.lua +++ b/lua/obsidian/lsp/init.lua @@ -19,8 +19,7 @@ obsidian_ls.start = function() local _ = dispatchers local members = { request = function(method, params, handler, _) - print(method) - handlers[method](method, params, handler, _) + handlers[method](params, handler, _) end, notify = function() end, -- Handle notify events is_closing = function() end, From d250ac72579f256d46718392648b2c29256fabe0 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Sat, 12 Apr 2025 20:26:33 +0800 Subject: [PATCH 04/22] pass the same client instance to all handlers --- lua/obsidian/lsp/code_action.lua | 1 + lua/obsidian/lsp/handlers/completion.lua | 107 +++++++++--------- lua/obsidian/lsp/handlers/hover.lua | 3 +- lua/obsidian/lsp/handlers/initialize.lua | 2 +- lua/obsidian/lsp/handlers/references.lua | 3 +- lua/obsidian/lsp/handlers/rename.lua | 4 +- lua/obsidian/lsp/handlers/resolve.lua | 6 +- .../lsp/handlers/workplace_diagnostics.lua | 2 +- lua/obsidian/lsp/init.lua | 2 +- 9 files changed, 62 insertions(+), 68 deletions(-) diff --git a/lua/obsidian/lsp/code_action.lua b/lua/obsidian/lsp/code_action.lua index 4a094fe4..07845adc 100644 --- a/lua/obsidian/lsp/code_action.lua +++ b/lua/obsidian/lsp/code_action.lua @@ -3,3 +3,4 @@ -- ObsidianNew -- ObsidianBacklinks -- ObsidianDailies +-- ObsidianLinkNew diff --git a/lua/obsidian/lsp/handlers/completion.lua b/lua/obsidian/lsp/handlers/completion.lua index 0fa55501..14a7ac20 100644 --- a/lua/obsidian/lsp/handlers/completion.lua +++ b/lua/obsidian/lsp/handlers/completion.lua @@ -1,64 +1,63 @@ -local obsidian_client = require("obsidian").get_client() -local link_style = obsidian_client.opts.preferred_link_style - -- TODO: completion for anchor, blocks -- TODO: complete wiki format like nvim-cmp source and obsidan app -local function calc_insert_text(note, partial) - local title = note.title - if link_style == "markdown" then - return title .. "](" .. note.path.filename .. ")" - else - return title .. "]]" +return function(obsidian_client, params, handler, _) + local link_style = obsidian_client.opts.preferred_link_style + + local function calc_insert_text(note, partial) + local title = note.title + if link_style == "markdown" then + return title .. "](" .. note.path.filename .. ")" + else + return title .. "]]" + end end -end -local function build_ref_items(partial, handler) - local items = {} - obsidian_client:find_notes_async( - partial, - vim.schedule_wrap(function(notes) - for _, note in ipairs(notes) do - local title = note.title - if title and title:lower():find(vim.pesc(partial:lower())) then - table.insert(items, { - kind = "File", - label = title, - filterText = title, - insertText = calc_insert_text(note, partial), - labelDetails = { description = "Obsidian" }, - }) + local function build_ref_items(partial) + local items = {} + obsidian_client:find_notes_async( + partial, + vim.schedule_wrap(function(notes) + for _, note in ipairs(notes) do + local title = note.title + if title and title:lower():find(vim.pesc(partial:lower())) then + table.insert(items, { + kind = "File", + label = title, + filterText = title, + insertText = calc_insert_text(note, partial), + labelDetails = { description = "Obsidian" }, + }) + end end - end - handler(nil, { items = items }) - end) - ) -end + handler(nil, { items = items }) + end) + ) + end -local function build_tag_items(partial, handler) - local items = {} - obsidian_client:list_tags_async( - partial, - vim.schedule_wrap(function(tags) - for _, tag in ipairs(tags) do - if tag and tag:lower():find(vim.pesc(partial:lower())) then - table.insert(items, { - kind = "File", - label = tag, - filterText = tag, - insertText = tag, - labelDetails = { description = "ObsidianTag" }, - }) + local function build_tag_items(partial) + local items = {} + obsidian_client:list_tags_async( + partial, + vim.schedule_wrap(function(tags) + for _, tag in ipairs(tags) do + if tag and tag:lower():find(vim.pesc(partial:lower())) then + table.insert(items, { + kind = "File", + label = tag, + filterText = tag, + insertText = tag, + labelDetails = { description = "ObsidianTag" }, + }) + end end - end - handler(nil, { - items = items, - }) - end) - ) -end + handler(nil, { + items = items, + }) + end) + ) + end -return function(params, handler, _) local uri = params.textDocument.uri local line_num = params.position.line local char_num = params.position.character @@ -80,9 +79,9 @@ return function(params, handler, _) if bracket_start then local partial = text_before_cursor:sub(bracket_start + 2) - build_ref_items(partial, handler) + build_ref_items(partial) elseif hastag_start then local partial = text_before_cursor:sub(hastag_start + 1) - build_tag_items(partial, handler) + build_tag_items(partial) end end diff --git a/lua/obsidian/lsp/handlers/hover.lua b/lua/obsidian/lsp/handlers/hover.lua index 91f09080..887e3cd1 100644 --- a/lua/obsidian/lsp/handlers/hover.lua +++ b/lua/obsidian/lsp/handlers/hover.lua @@ -1,4 +1,3 @@ -local obsidian_client = require("obsidian").get_client() local util = require "obsidian.util" --- TODO: hover on tags @@ -12,7 +11,7 @@ local function read_file(file) return data end -return function(params, handler, _) +return function(obsidian_client, params, handler, _) local term = util.parse_cursor_link() if term then obsidian_client:find_notes_async( diff --git a/lua/obsidian/lsp/handlers/initialize.lua b/lua/obsidian/lsp/handlers/initialize.lua index 8fddfa18..465e992c 100644 --- a/lua/obsidian/lsp/handlers/initialize.lua +++ b/lua/obsidian/lsp/handlers/initialize.lua @@ -41,6 +41,6 @@ local initializeResult = { }, } -return function(params, handler, _) +return function(_, params, handler, _) return handler(nil, initializeResult, params.context) end diff --git a/lua/obsidian/lsp/handlers/references.lua b/lua/obsidian/lsp/handlers/references.lua index bc8e3794..7c2a3ee0 100644 --- a/lua/obsidian/lsp/handlers/references.lua +++ b/lua/obsidian/lsp/handlers/references.lua @@ -1,12 +1,11 @@ ---@diagnostic disable: missing-fields -local obsidian_client = require("obsidian").get_client() local util = require "obsidian.util" local Note = require "obsidian.note" -- TODO: references for anchor, blocks -return function(_, handler, _) +return function(obsidian_client, _, handler, _) local tag = util.cursor_tag(nil, nil) if tag then obsidian_client:find_tags_async(tag, function(tag_locations) diff --git a/lua/obsidian/lsp/handlers/rename.lua b/lua/obsidian/lsp/handlers/rename.lua index d87306b7..ea1aed7d 100644 --- a/lua/obsidian/lsp/handlers/rename.lua +++ b/lua/obsidian/lsp/handlers/rename.lua @@ -1,5 +1,3 @@ -local obsidian_client = require("obsidian").get_client() - -return function(param, _, _) +return function(obsidian_client, param, _, _) require "obsidian.commands.rename"(obsidian_client, { args = param.newName }) end diff --git a/lua/obsidian/lsp/handlers/resolve.lua b/lua/obsidian/lsp/handlers/resolve.lua index 666fa6c7..4889602a 100644 --- a/lua/obsidian/lsp/handlers/resolve.lua +++ b/lua/obsidian/lsp/handlers/resolve.lua @@ -1,8 +1,6 @@ -local obsidian_client = require("obsidian").get_client() +-- FIXME: not working --- TODO: not working - -return function(params, handler, _) +return function(obsidian_client, params, handler, _) local buf = vim.api.nvim_get_current_buf() local current_uri = vim.uri_from_bufnr(buf) obsidian_client:find_notes_async( diff --git a/lua/obsidian/lsp/handlers/workplace_diagnostics.lua b/lua/obsidian/lsp/handlers/workplace_diagnostics.lua index f13c9282..dbc8c7ed 100644 --- a/lua/obsidian/lsp/handlers/workplace_diagnostics.lua +++ b/lua/obsidian/lsp/handlers/workplace_diagnostics.lua @@ -1,4 +1,4 @@ -return function(params, handler, _) +return function(obsidian_client, params, handler, _) print "diagnosticing workspace" return end diff --git a/lua/obsidian/lsp/init.lua b/lua/obsidian/lsp/init.lua index ac0d4ca9..f937eba4 100644 --- a/lua/obsidian/lsp/init.lua +++ b/lua/obsidian/lsp/init.lua @@ -19,7 +19,7 @@ obsidian_ls.start = function() local _ = dispatchers local members = { request = function(method, params, handler, _) - handlers[method](params, handler, _) + handlers[method](obsidian_client, params, handler, _) end, notify = function() end, -- Handle notify events is_closing = function() end, From e85aaca79699fffda2c5038ed49dfe525c836c06 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Tue, 15 Apr 2025 16:17:13 +0800 Subject: [PATCH 05/22] add workplace symbols as TOC and add handler signature --- lua/obsidian/lsp/handlers.lua | 1 + lua/obsidian/lsp/handlers/completion.lua | 3 + lua/obsidian/lsp/handlers/hover.lua | 3 + lua/obsidian/lsp/handlers/initialize.lua | 5 +- lua/obsidian/lsp/handlers/references.lua | 3 + lua/obsidian/lsp/handlers/rename.lua | 9 ++- lua/obsidian/lsp/handlers/resolve.lua | 3 + lua/obsidian/lsp/handlers/symbols.lua | 64 +++++++++++++++++++ .../lsp/handlers/workplace_diagnostics.lua | 3 + 9 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 lua/obsidian/lsp/handlers/symbols.lua diff --git a/lua/obsidian/lsp/handlers.lua b/lua/obsidian/lsp/handlers.lua index 3b3aa6c7..ab5073b1 100644 --- a/lua/obsidian/lsp/handlers.lua +++ b/lua/obsidian/lsp/handlers.lua @@ -8,6 +8,7 @@ return setmetatable({ [ms.workspace_diagnostic] = require "obsidian.lsp.handlers.workplace_diagnostics", [ms.textDocument_rename] = require "obsidian.lsp.handlers.rename", [ms.textDocument_references] = require "obsidian.lsp.handlers.references", + [ms.textDocument_documentSymbol] = require "obsidian.lsp.handlers.symbols", }, { __index = function(_, k) print("obsidian_ls does not support method " .. k .. " yet") diff --git a/lua/obsidian/lsp/handlers/completion.lua b/lua/obsidian/lsp/handlers/completion.lua index 14a7ac20..6bd5cea0 100644 --- a/lua/obsidian/lsp/handlers/completion.lua +++ b/lua/obsidian/lsp/handlers/completion.lua @@ -1,6 +1,9 @@ -- TODO: completion for anchor, blocks -- TODO: complete wiki format like nvim-cmp source and obsidan app +---@param obsidian_client obsidian.Client +---@param params table +---@param handler function return function(obsidian_client, params, handler, _) local link_style = obsidian_client.opts.preferred_link_style diff --git a/lua/obsidian/lsp/handlers/hover.lua b/lua/obsidian/lsp/handlers/hover.lua index 887e3cd1..2fb8535b 100644 --- a/lua/obsidian/lsp/handlers/hover.lua +++ b/lua/obsidian/lsp/handlers/hover.lua @@ -11,6 +11,9 @@ local function read_file(file) return data end +---@param obsidian_client obsidian.Client +---@param params table +---@param handler function return function(obsidian_client, params, handler, _) local term = util.parse_cursor_link() if term then diff --git a/lua/obsidian/lsp/handlers/initialize.lua b/lua/obsidian/lsp/handlers/initialize.lua index 465e992c..4f61d401 100644 --- a/lua/obsidian/lsp/handlers/initialize.lua +++ b/lua/obsidian/lsp/handlers/initialize.lua @@ -41,6 +41,9 @@ local initializeResult = { }, } -return function(_, params, handler, _) +---@param obsidian_client obsidian.Client +---@param params table +---@param handler function +return function(obsidian_client, params, handler, _) return handler(nil, initializeResult, params.context) end diff --git a/lua/obsidian/lsp/handlers/references.lua b/lua/obsidian/lsp/handlers/references.lua index 7c2a3ee0..edf47656 100644 --- a/lua/obsidian/lsp/handlers/references.lua +++ b/lua/obsidian/lsp/handlers/references.lua @@ -5,6 +5,9 @@ local Note = require "obsidian.note" -- TODO: references for anchor, blocks +---@param obsidian_client obsidian.Client +---@param _ table +---@param handler function return function(obsidian_client, _, handler, _) local tag = util.cursor_tag(nil, nil) if tag then diff --git a/lua/obsidian/lsp/handlers/rename.lua b/lua/obsidian/lsp/handlers/rename.lua index ea1aed7d..d14370f9 100644 --- a/lua/obsidian/lsp/handlers/rename.lua +++ b/lua/obsidian/lsp/handlers/rename.lua @@ -1,3 +1,8 @@ -return function(obsidian_client, param, _, _) - require "obsidian.commands.rename"(obsidian_client, { args = param.newName }) +---TODO: move to the idea of textEdits + +---@param obsidian_client obsidian.Client +---@param params table +---@param handler function +return function(obsidian_client, params, handler, _) + require "obsidian.commands.rename"(obsidian_client, { args = params.newName }) end diff --git a/lua/obsidian/lsp/handlers/resolve.lua b/lua/obsidian/lsp/handlers/resolve.lua index 4889602a..6f8de8f3 100644 --- a/lua/obsidian/lsp/handlers/resolve.lua +++ b/lua/obsidian/lsp/handlers/resolve.lua @@ -1,5 +1,8 @@ -- FIXME: not working +---@param obsidian_client obsidian.Client +---@param params table +---@param handler function return function(obsidian_client, params, handler, _) local buf = vim.api.nvim_get_current_buf() local current_uri = vim.uri_from_bufnr(buf) diff --git a/lua/obsidian/lsp/handlers/symbols.lua b/lua/obsidian/lsp/handlers/symbols.lua new file mode 100644 index 00000000..1ac1abef --- /dev/null +++ b/lua/obsidian/lsp/handlers/symbols.lua @@ -0,0 +1,64 @@ +local ts = vim.treesitter + +local heading_query = [[ + (setext_heading + heading_content: (_) @h1 + (setext_h1_underline)) + (setext_heading + heading_content: (_) @h2 + (setext_h2_underline)) + (atx_heading + (atx_h1_marker) + heading_content: (_) @h1) + (atx_heading + (atx_h2_marker) + heading_content: (_) @h2) + (atx_heading + (atx_h3_marker) + heading_content: (_) @h3) + (atx_heading + (atx_h4_marker) + heading_content: (_) @h4) + (atx_heading + (atx_h5_marker) + heading_content: (_) @h5) + (atx_heading + (atx_h6_marker) + heading_content: (_) @h6) + ]] + +return function(_, params, handler) + --- Extract headings from buffer + --- @param bufnr integer buffer to extract headings from + --- @return table TODO: + local get_headings = function(bufnr) + local lang = ts.language.get_lang(vim.bo[bufnr].filetype) + if not lang then + return {} + end + local parser = assert(ts.get_parser(bufnr, lang, { error = false })) + local query = ts.query.parse(lang, heading_query) + local root = parser:parse()[1]:root() + local headings = {} + for id, node, _, _ in query:iter_captures(root, bufnr) do + local text = string.rep("#", id) .. " " .. ts.get_node_text(node, bufnr) + local row, _ = node:start() + table.insert(headings, { + kind = 15, + range = { + start = { line = row, character = 1 }, + ["end"] = { line = row, character = 1 }, + }, + selectionRange = { + start = { line = row, character = 1 }, + ["end"] = { line = row, character = 1 }, + }, + name = text, + }) + end + return headings + end + + local bufnr = vim.uri_to_bufnr(params.textDocument.uri) + handler(nil, get_headings(bufnr)) +end diff --git a/lua/obsidian/lsp/handlers/workplace_diagnostics.lua b/lua/obsidian/lsp/handlers/workplace_diagnostics.lua index dbc8c7ed..7de4a52b 100644 --- a/lua/obsidian/lsp/handlers/workplace_diagnostics.lua +++ b/lua/obsidian/lsp/handlers/workplace_diagnostics.lua @@ -1,3 +1,6 @@ +---@param obsidian_client obsidian.Client +---@param params table +---@param handler function return function(obsidian_client, params, handler, _) print "diagnosticing workspace" return From 4a63a5a855d0dd6ccc2c6d76318739926e1f4a38 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Tue, 15 Apr 2025 18:01:16 +0800 Subject: [PATCH 06/22] use client:format_link to format completions --- lua/obsidian/lsp/handlers/completion.lua | 53 +++++++++++------------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/lua/obsidian/lsp/handlers/completion.lua b/lua/obsidian/lsp/handlers/completion.lua index 6bd5cea0..1a6fdcb4 100644 --- a/lua/obsidian/lsp/handlers/completion.lua +++ b/lua/obsidian/lsp/handlers/completion.lua @@ -1,24 +1,24 @@ -- TODO: completion for anchor, blocks -- TODO: complete wiki format like nvim-cmp source and obsidan app ----@param obsidian_client obsidian.Client +---@param client obsidian.Client ---@param params table ---@param handler function -return function(obsidian_client, params, handler, _) - local link_style = obsidian_client.opts.preferred_link_style +return function(client, params, handler, _) + local link_style = client.opts.preferred_link_style - local function calc_insert_text(note, partial) - local title = note.title + local function calc_insert_text(note) + local link_text = client:format_link(note) if link_style == "markdown" then - return title .. "](" .. note.path.filename .. ")" + return link_text:sub(2) else - return title .. "]]" + return link_text:sub(3) end end - local function build_ref_items(partial) + local function handle_ref(partial) local items = {} - obsidian_client:find_notes_async( + client:find_notes_async( partial, vim.schedule_wrap(function(notes) for _, note in ipairs(notes) do @@ -28,7 +28,7 @@ return function(obsidian_client, params, handler, _) kind = "File", label = title, filterText = title, - insertText = calc_insert_text(note, partial), + insertText = calc_insert_text(note), labelDetails = { description = "Obsidian" }, }) end @@ -38,9 +38,9 @@ return function(obsidian_client, params, handler, _) ) end - local function build_tag_items(partial) + local function handle_tag(partial) local items = {} - obsidian_client:list_tags_async( + client:list_tags_async( partial, vim.schedule_wrap(function(tags) for _, tag in ipairs(tags) do @@ -54,9 +54,7 @@ return function(obsidian_client, params, handler, _) }) end end - handler(nil, { - items = items, - }) + handler(nil, { items = items }) end) ) end @@ -64,27 +62,24 @@ return function(obsidian_client, params, handler, _) local uri = params.textDocument.uri local line_num = params.position.line local char_num = params.position.character - - local file_path = vim.uri_to_fname(uri) - local buf = vim.fn.bufnr(file_path, false) + local buf = vim.uri_to_bufnr(uri) local line_text = (vim.api.nvim_buf_get_lines(buf, line_num, line_num + 1, false)[1] or "") local text_before_cursor = line_text:sub(1, char_num) - local trigger_pattern = "[[" + local ref_trigger_pattern = "[[" if link_style == "markdown" then - trigger_pattern = "[" + ref_trigger_pattern = "[" end - local hastag_start = text_before_cursor:find("#", 1, true) - - local bracket_start = text_before_cursor:find(vim.pesc(trigger_pattern)) + local tag_start = text_before_cursor:find("#", 1, true) + local ref_start = text_before_cursor:find(ref_trigger_pattern, 1, true) - if bracket_start then - local partial = text_before_cursor:sub(bracket_start + 2) - build_ref_items(partial) - elseif hastag_start then - local partial = text_before_cursor:sub(hastag_start + 1) - build_tag_items(partial) + if ref_start then + local partial = text_before_cursor:sub(ref_start + 2) + handle_ref(partial) + elseif tag_start then + local partial = text_before_cursor:sub(tag_start + 1) + handle_tag(partial) end end From 2eece7c93f5f82b462005122a3fc887384631760 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Tue, 15 Apr 2025 21:19:21 +0800 Subject: [PATCH 07/22] fix native completion for -, impl for documentLink --- lua/obsidian/init.lua | 1 + lua/obsidian/lsp/handlers.lua | 5 +- .../{resolve.lua => completion_resolve.lua} | 0 lua/obsidian/lsp/handlers/config.lua | 1 + lua/obsidian/lsp/handlers/document_link.lua | 10 ++ lua/obsidian/lsp/handlers/document_symbol.lua | 9 ++ lua/obsidian/lsp/handlers/initialize.lua | 1 + lua/obsidian/lsp/handlers/rename.lua | 2 +- lua/obsidian/lsp/handlers/symbols.lua | 64 -------- lua/obsidian/lsp/util.lua | 152 ++++++++++++++++++ 10 files changed, 178 insertions(+), 67 deletions(-) rename lua/obsidian/lsp/handlers/{resolve.lua => completion_resolve.lua} (100%) create mode 100644 lua/obsidian/lsp/handlers/document_link.lua create mode 100644 lua/obsidian/lsp/handlers/document_symbol.lua delete mode 100644 lua/obsidian/lsp/handlers/symbols.lua create mode 100644 lua/obsidian/lsp/util.lua diff --git a/lua/obsidian/init.lua b/lua/obsidian/init.lua index 120fc37b..1da72322 100644 --- a/lua/obsidian/init.lua +++ b/lua/obsidian/init.lua @@ -153,6 +153,7 @@ obsidian.setup = function(opts) vim.bo[ev.buf].omnifunc = "v:lua.vim.lsp.omnifunc" vim.bo[ev.buf].completeopt = "menu,menuone,noselect" + vim.bo[ev.buf].iskeyword = "@,48-57,192-255" -- HACK: so that completion for note names with `-` in it works in native completion require("obsidian.lsp").start() local win = vim.api.nvim_get_current_win() diff --git a/lua/obsidian/lsp/handlers.lua b/lua/obsidian/lsp/handlers.lua index ab5073b1..8b583d6e 100644 --- a/lua/obsidian/lsp/handlers.lua +++ b/lua/obsidian/lsp/handlers.lua @@ -3,12 +3,13 @@ local ms = vim.lsp.protocol.Methods return setmetatable({ [ms.initialize] = require "obsidian.lsp.handlers.initialize", [ms.textDocument_completion] = require "obsidian.lsp.handlers.completion", - [ms.completionItem_resolve] = require "obsidian.lsp.handlers.resolve", + [ms.completionItem_resolve] = require "obsidian.lsp.handlers.completion_resolve", [ms.textDocument_hover] = require "obsidian.lsp.handlers.hover", [ms.workspace_diagnostic] = require "obsidian.lsp.handlers.workplace_diagnostics", [ms.textDocument_rename] = require "obsidian.lsp.handlers.rename", [ms.textDocument_references] = require "obsidian.lsp.handlers.references", - [ms.textDocument_documentSymbol] = require "obsidian.lsp.handlers.symbols", + [ms.textDocument_documentSymbol] = require "obsidian.lsp.handlers.document_symbol", + [ms.textDocument_documentLink] = require "obsidian.lsp.handlers.document_link", }, { __index = function(_, k) print("obsidian_ls does not support method " .. k .. " yet") diff --git a/lua/obsidian/lsp/handlers/resolve.lua b/lua/obsidian/lsp/handlers/completion_resolve.lua similarity index 100% rename from lua/obsidian/lsp/handlers/resolve.lua rename to lua/obsidian/lsp/handlers/completion_resolve.lua diff --git a/lua/obsidian/lsp/handlers/config.lua b/lua/obsidian/lsp/handlers/config.lua index d73cc6fd..5d316a81 100644 --- a/lua/obsidian/lsp/handlers/config.lua +++ b/lua/obsidian/lsp/handlers/config.lua @@ -1,3 +1,4 @@ return { complete = true, + -- option to only show first few links on hover, and completion doc } diff --git a/lua/obsidian/lsp/handlers/document_link.lua b/lua/obsidian/lsp/handlers/document_link.lua new file mode 100644 index 00000000..5c6a9e8d --- /dev/null +++ b/lua/obsidian/lsp/handlers/document_link.lua @@ -0,0 +1,10 @@ +local util = require "obsidian.lsp.util" + +---@param client obsidian.Client +---@param params table +---@param handler function +return function(client, params, handler, _) + local bufnr = vim.uri_to_bufnr(params.textDocument.uri) + local links = util.get_links(client, bufnr) + handler(nil, links) +end diff --git a/lua/obsidian/lsp/handlers/document_symbol.lua b/lua/obsidian/lsp/handlers/document_symbol.lua new file mode 100644 index 00000000..9e6ab076 --- /dev/null +++ b/lua/obsidian/lsp/handlers/document_symbol.lua @@ -0,0 +1,9 @@ +local util = require "obsidian.lsp.util" + +---@param client obsidian.Client +---@param params table +---@param handler function +return function(client, params, handler) + local bufnr = vim.uri_to_bufnr(params.textDocument.uri) + handler(nil, util.get_headings(client, bufnr)) +end diff --git a/lua/obsidian/lsp/handlers/initialize.lua b/lua/obsidian/lsp/handlers/initialize.lua index 4f61d401..5118c103 100644 --- a/lua/obsidian/lsp/handlers/initialize.lua +++ b/lua/obsidian/lsp/handlers/initialize.lua @@ -20,6 +20,7 @@ local initializeResult = { definitionProvider = true, implementationProvider = true, declarationProvider = true, + documentLinkProvider = true, -- TODO: Add diagnostic support diagnosticProvider = { interFileDependencies = false, diff --git a/lua/obsidian/lsp/handlers/rename.lua b/lua/obsidian/lsp/handlers/rename.lua index d14370f9..537d718b 100644 --- a/lua/obsidian/lsp/handlers/rename.lua +++ b/lua/obsidian/lsp/handlers/rename.lua @@ -1,4 +1,4 @@ ----TODO: move to the idea of textEdits +-- TODO: move to the idea of textEdits ---@param obsidian_client obsidian.Client ---@param params table diff --git a/lua/obsidian/lsp/handlers/symbols.lua b/lua/obsidian/lsp/handlers/symbols.lua deleted file mode 100644 index 1ac1abef..00000000 --- a/lua/obsidian/lsp/handlers/symbols.lua +++ /dev/null @@ -1,64 +0,0 @@ -local ts = vim.treesitter - -local heading_query = [[ - (setext_heading - heading_content: (_) @h1 - (setext_h1_underline)) - (setext_heading - heading_content: (_) @h2 - (setext_h2_underline)) - (atx_heading - (atx_h1_marker) - heading_content: (_) @h1) - (atx_heading - (atx_h2_marker) - heading_content: (_) @h2) - (atx_heading - (atx_h3_marker) - heading_content: (_) @h3) - (atx_heading - (atx_h4_marker) - heading_content: (_) @h4) - (atx_heading - (atx_h5_marker) - heading_content: (_) @h5) - (atx_heading - (atx_h6_marker) - heading_content: (_) @h6) - ]] - -return function(_, params, handler) - --- Extract headings from buffer - --- @param bufnr integer buffer to extract headings from - --- @return table TODO: - local get_headings = function(bufnr) - local lang = ts.language.get_lang(vim.bo[bufnr].filetype) - if not lang then - return {} - end - local parser = assert(ts.get_parser(bufnr, lang, { error = false })) - local query = ts.query.parse(lang, heading_query) - local root = parser:parse()[1]:root() - local headings = {} - for id, node, _, _ in query:iter_captures(root, bufnr) do - local text = string.rep("#", id) .. " " .. ts.get_node_text(node, bufnr) - local row, _ = node:start() - table.insert(headings, { - kind = 15, - range = { - start = { line = row, character = 1 }, - ["end"] = { line = row, character = 1 }, - }, - selectionRange = { - start = { line = row, character = 1 }, - ["end"] = { line = row, character = 1 }, - }, - name = text, - }) - end - return headings - end - - local bufnr = vim.uri_to_bufnr(params.textDocument.uri) - handler(nil, get_headings(bufnr)) -end diff --git a/lua/obsidian/lsp/util.lua b/lua/obsidian/lsp/util.lua new file mode 100644 index 00000000..49399ec8 --- /dev/null +++ b/lua/obsidian/lsp/util.lua @@ -0,0 +1,152 @@ +local M = {} +local ts = vim.treesitter + +local heading_query = [[ + (setext_heading + heading_content: (_) @h1 + (setext_h1_underline)) + (setext_heading + heading_content: (_) @h2 + (setext_h2_underline)) + (atx_heading + (atx_h1_marker) + heading_content: (_) @h1) + (atx_heading + (atx_h2_marker) + heading_content: (_) @h2) + (atx_heading + (atx_h3_marker) + heading_content: (_) @h3) + (atx_heading + (atx_h4_marker) + heading_content: (_) @h4) + (atx_heading + (atx_h5_marker) + heading_content: (_) @h5) + (atx_heading + (atx_h6_marker) + heading_content: (_) @h6) + ]] + +local link_query = [[ +( + (inline_link + (link_text) @text + (link_destination) @url + ) @link + ) +( + (reference_link + (link_text) @text + (link_reference) @url + ) @link + ) +]] + +---Extract headings from buffer +---@param client obsidian.Client +---@param bufnr integer +---@return table TODO: +M.get_headings = function(client, bufnr) + local lang = ts.language.get_lang(vim.bo[bufnr].filetype) + if not lang then + return {} + end + local parser = assert(ts.get_parser(bufnr, lang, { error = false })) + local query = ts.query.parse(lang, heading_query) + local root = parser:parse()[1]:root() + local headings = {} + for id, node, _, _ in query:iter_captures(root, bufnr) do + local text = string.rep("#", id) .. " " .. ts.get_node_text(node, bufnr) + local row, _ = node:start() + table.insert(headings, { + kind = 15, + range = { + start = { line = row, character = 1 }, + ["end"] = { line = row, character = 1 }, + }, + selectionRange = { + start = { line = row, character = 1 }, + ["end"] = { line = row, character = 1 }, + }, + name = text, + }) + end + return headings +end + +---@param client obsidian.Client +---@param bufnr integer +---@param lsp.DocumentLink[] +M.get_links = function(client, bufnr) + bufnr = bufnr or vim.api.nvim_get_current_buf() + local links = {} + local lang = ts.language.get_lang(vim.bo[bufnr].filetype) + + -- Tree-sitter part for Markdown links + if lang then + local parser = ts.get_parser(bufnr, lang) + if parser then + local query_success, query = pcall(ts.query.parse, lang, link_query) + + if query_success and query then + local root = parser:parse()[1]:root() + for _, match, _ in query:iter_matches(root, bufnr) do + local link_info = {} + for id, node in pairs(match) do + local capture_name = query.captures[id] + if capture_name == "text" then + link_info.text = ts.get_node_text(node, bufnr) + elseif capture_name == "url" then + link_info.url = ts.get_node_text(node, bufnr) + elseif capture_name == "link" then + local start_row, start_col = node:start() + link_info.line = start_row + link_info.column = start_col + end + end + if link_info.text and link_info.url then + table.insert(links, { + text = link_info.text, + url = link_info.url, + line = link_info.line, + column = link_info.column, + type = "markdown", + }) + end + end + end + end + end + + -- Regex part for wiki-style links [[...]] + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + for line_idx, line in ipairs(lines) do + local col = 0 + while true do + local start, end_ = line:find("%[%[.-%]%]", col) + if not start then + break + end + + local content = line:sub(start + 2, end_ - 2) + local target, text = content:match "^([^|]*)%|?(.*)$" + text = text ~= "" and text or target + + table.insert(links, { + kind = 15, + target = target, -- TODO: resolve to path + range = { + start = { line = line_idx - 1, character = 1 }, + ["end"] = { line = line_idx - 1, character = 1 }, + }, + }) + + col = end_ + 1 + end + end + + return links +end + +return M From 8215b18d179cd93160d80aa70d795f8a2edbb5f6 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Wed, 16 Apr 2025 00:39:55 +0800 Subject: [PATCH 08/22] completion supporting min_chars, and resolve, basic diagnostic --- lua/obsidian/init.lua | 8 ++- lua/obsidian/lsp/diagnostic.lua | 32 ++++++++++++ lua/obsidian/lsp/handlers.lua | 4 +- lua/obsidian/lsp/handlers/completion.lua | 49 ++++++++++++------- .../lsp/handlers/completion_resolve.lua | 49 +++++++------------ lua/obsidian/lsp/handlers/did_change.lua | 8 +++ lua/obsidian/lsp/handlers/did_open.lua | 8 +++ lua/obsidian/lsp/handlers/initialize.lua | 10 +++- lua/obsidian/lsp/init.lua | 7 +-- lua/obsidian/lsp/util.lua | 8 +++ 10 files changed, 127 insertions(+), 56 deletions(-) create mode 100644 lua/obsidian/lsp/diagnostic.lua create mode 100644 lua/obsidian/lsp/handlers/did_change.lua create mode 100644 lua/obsidian/lsp/handlers/did_open.lua diff --git a/lua/obsidian/init.lua b/lua/obsidian/init.lua index 1da72322..5fc9ff10 100644 --- a/lua/obsidian/init.lua +++ b/lua/obsidian/init.lua @@ -154,7 +154,13 @@ obsidian.setup = function(opts) vim.bo[ev.buf].omnifunc = "v:lua.vim.lsp.omnifunc" vim.bo[ev.buf].completeopt = "menu,menuone,noselect" vim.bo[ev.buf].iskeyword = "@,48-57,192-255" -- HACK: so that completion for note names with `-` in it works in native completion - require("obsidian.lsp").start() + + local client_id = require("obsidian.lsp").start() + assert(client_id) + + if not pcall(require, "blink.cmp") then + vim.lsp.completion.enable(true, client_id, ev.buf, { autotrigger = true }) + end local win = vim.api.nvim_get_current_win() diff --git a/lua/obsidian/lsp/diagnostic.lua b/lua/obsidian/lsp/diagnostic.lua new file mode 100644 index 00000000..adef7795 --- /dev/null +++ b/lua/obsidian/lsp/diagnostic.lua @@ -0,0 +1,32 @@ +local util = require "obsidian.lsp.util" + +local ns = vim.api.nvim_create_namespace "obsidian-ls.diagnostics" + +-- IDEAD: inject markdownlint like none-ls +-- https://github.com/nvimtools/none-ls.nvim/blob/main/lua/null-ls/builtins/diagnostics/markdownlint.lua +-- https://github.com/nvimtools/none-ls.nvim/blob/main/lua/null-ls/builtins/diagnostics/markdownlint_cli2.lua + +return function(client, params) + local uri = params.textDocument.uri + local buf = vim.uri_to_bufnr(uri) + local diagnostics = {} + + local client_id = assert(vim.lsp.get_clients({ name = "obsidian-ls" })[1]) + + local links = util.get_links(client, buf) + + for _, link in ipairs(links) do + if link.target == "error" then + table.insert(diagnostics, { + lnum = link.range.start.line, + col = link.range.start.character, + severity = vim.lsp.protocol.DiagnosticSeverity.Warning, + message = "This is an error", + source = "obsidian-ls", + code = "ERROR", + }) + end + end + + vim.diagnostic.set(ns, buf, diagnostics, { client_id = client_id }) +end diff --git a/lua/obsidian/lsp/handlers.lua b/lua/obsidian/lsp/handlers.lua index 8b583d6e..40b3316f 100644 --- a/lua/obsidian/lsp/handlers.lua +++ b/lua/obsidian/lsp/handlers.lua @@ -10,9 +10,11 @@ return setmetatable({ [ms.textDocument_references] = require "obsidian.lsp.handlers.references", [ms.textDocument_documentSymbol] = require "obsidian.lsp.handlers.document_symbol", [ms.textDocument_documentLink] = require "obsidian.lsp.handlers.document_link", + [ms.textDocument_didOpen] = require "obsidian.lsp.handlers.did_open", + [ms.textDocument_didChange] = require "obsidian.lsp.handlers.did_change", }, { __index = function(_, k) - print("obsidian_ls does not support method " .. k .. " yet") + vim.notify("obsidian_ls does not support method " .. k .. " yet", 3) return function() end end, }) diff --git a/lua/obsidian/lsp/handlers/completion.lua b/lua/obsidian/lsp/handlers/completion.lua index 1a6fdcb4..13a8e0ce 100644 --- a/lua/obsidian/lsp/handlers/completion.lua +++ b/lua/obsidian/lsp/handlers/completion.lua @@ -1,18 +1,26 @@ -- TODO: completion for anchor, blocks --- TODO: complete wiki format like nvim-cmp source and obsidan app +-- TODO: create item + +local ref_trigger_pattern = { + wiki = "[[", + markdown = "[", +} + +local find, sub, lower = string.find, string.sub, string.lower ---@param client obsidian.Client ---@param params table ---@param handler function return function(client, params, handler, _) local link_style = client.opts.preferred_link_style + local min_chars = client.opts.completion.min_chars local function calc_insert_text(note) local link_text = client:format_link(note) if link_style == "markdown" then - return link_text:sub(2) + return sub(link_text, 2) else - return link_text:sub(3) + return sub(link_text, 3) end end @@ -23,13 +31,18 @@ return function(client, params, handler, _) vim.schedule_wrap(function(notes) for _, note in ipairs(notes) do local title = note.title - if title and title:lower():find(vim.pesc(partial:lower())) then + local pattern = vim.pesc(lower(partial)) + if title and find(lower(title), pattern) then table.insert(items, { - kind = "File", + kind = 17, label = title, filterText = title, insertText = calc_insert_text(note), labelDetails = { description = "Obsidian" }, + data = { + file = note.path.filename, + kind = "ref", + }, }) end end @@ -51,6 +64,9 @@ return function(client, params, handler, _) filterText = tag, insertText = tag, labelDetails = { description = "ObsidianTag" }, + data = { + kind = "tag", + }, }) end end @@ -65,21 +81,20 @@ return function(client, params, handler, _) local buf = vim.uri_to_bufnr(uri) local line_text = (vim.api.nvim_buf_get_lines(buf, line_num, line_num + 1, false)[1] or "") - local text_before_cursor = line_text:sub(1, char_num) + local text_before_cursor = string.sub(line_text, 1, char_num) - local ref_trigger_pattern = "[[" - if link_style == "markdown" then - ref_trigger_pattern = "[" - end - - local tag_start = text_before_cursor:find("#", 1, true) - local ref_start = text_before_cursor:find(ref_trigger_pattern, 1, true) + local tag_start = find(text_before_cursor, "#", 1, true) + local ref_start = find(text_before_cursor, ref_trigger_pattern[link_style], 1, true) if ref_start then - local partial = text_before_cursor:sub(ref_start + 2) - handle_ref(partial) + local partial = sub(text_before_cursor, ref_start + 2) + if #partial >= min_chars then + handle_ref(partial) + end elseif tag_start then - local partial = text_before_cursor:sub(tag_start + 1) - handle_tag(partial) + local partial = sub(text_before_cursor, tag_start + 1) + if #partial >= min_chars then + handle_tag(partial) + end end end diff --git a/lua/obsidian/lsp/handlers/completion_resolve.lua b/lua/obsidian/lsp/handlers/completion_resolve.lua index 6f8de8f3..7e0df8e1 100644 --- a/lua/obsidian/lsp/handlers/completion_resolve.lua +++ b/lua/obsidian/lsp/handlers/completion_resolve.lua @@ -1,36 +1,21 @@ --- FIXME: not working +local util = require "obsidian.lsp.util" ----@param obsidian_client obsidian.Client +---@param _ obsidian.Client ---@param params table ---@param handler function -return function(obsidian_client, params, handler, _) - local buf = vim.api.nvim_get_current_buf() - local current_uri = vim.uri_from_bufnr(buf) - obsidian_client:find_notes_async( - params.label, - vim.schedule_wrap(function(notes) - for i, note in ipairs(notes) do - if vim.uri_from_fname(note.path.filename) == current_uri then - table.remove(notes, i) - end - end - local note = notes[1] - if note then - local content = table.concat(vim.fn.readfile(note.path.filename), "\n") - handler(nil, { - value = content, - kind = "markdown", - }) - else - vim.notify("No notes found", 3) - end - end) - ) - -- params.documentation = { - -- value = [[# Heading 1 - -- [link](https://example.com) - -- ]], - -- kind = "markdown", - -- } - -- handler(nil, params) +return function(_, params, handler, _) + local kind = params.data.kind + + if kind == "ref" then + local content = util.read_file(params.data.file) + if content then + params.documentation = { + value = content, + kind = "markdown", + } + handler(nil, params) + else + vim.notify("No notes found", 3) + end + end end diff --git a/lua/obsidian/lsp/handlers/did_change.lua b/lua/obsidian/lsp/handlers/did_change.lua new file mode 100644 index 00000000..bdb600d0 --- /dev/null +++ b/lua/obsidian/lsp/handlers/did_change.lua @@ -0,0 +1,8 @@ +local diagnostic = require "obsidian.lsp.diagnostic" + +---@param client obsidian.Client +---@param params table +---@param handler function +return function(client, params, handler, _) + diagnostic(client, params) +end diff --git a/lua/obsidian/lsp/handlers/did_open.lua b/lua/obsidian/lsp/handlers/did_open.lua new file mode 100644 index 00000000..bdb600d0 --- /dev/null +++ b/lua/obsidian/lsp/handlers/did_open.lua @@ -0,0 +1,8 @@ +local diagnostic = require "obsidian.lsp.diagnostic" + +---@param client obsidian.Client +---@param params table +---@param handler function +return function(client, params, handler, _) + diagnostic(client, params) +end diff --git a/lua/obsidian/lsp/handlers/initialize.lua b/lua/obsidian/lsp/handlers/initialize.lua index 5118c103..cb65b630 100644 --- a/lua/obsidian/lsp/handlers/initialize.lua +++ b/lua/obsidian/lsp/handlers/initialize.lua @@ -2,9 +2,15 @@ local config = require "obsidian.lsp.handlers.config" local completion_options +-- TODO: what about non-english chars, or emojis? +local chars = {} +for i = 32, 126 do + table.insert(chars, string.char(i)) +end + if config.complete then completion_options = { - triggerCharacters = { "[", "#" }, + triggerCharacters = chars, resolveProvider = true, completionItem = { labelDetailsSupport = true, @@ -33,7 +39,7 @@ local initializeResult = { completionProvider = completion_options, textDocumentSync = { openClose = true, - change = 2, + change = 1, }, }, serverInfo = { diff --git a/lua/obsidian/lsp/init.lua b/lua/obsidian/lsp/init.lua index f937eba4..7f3060fd 100644 --- a/lua/obsidian/lsp/init.lua +++ b/lua/obsidian/lsp/init.lua @@ -15,13 +15,14 @@ obsidian_ls.start = function() local client_id = vim.lsp.start { name = "obsidian-ls", capabilities = capabilities, - cmd = function(dispatchers) - local _ = dispatchers + cmd = function() local members = { request = function(method, params, handler, _) handlers[method](obsidian_client, params, handler, _) end, - notify = function() end, -- Handle notify events + notify = function(method, params, handler, _) + handlers[method](obsidian_client, params, handler, _) + end, is_closing = function() end, terminate = function() end, } diff --git a/lua/obsidian/lsp/util.lua b/lua/obsidian/lsp/util.lua index 49399ec8..11984615 100644 --- a/lua/obsidian/lsp/util.lua +++ b/lua/obsidian/lsp/util.lua @@ -149,4 +149,12 @@ M.get_links = function(client, bufnr) return links end +local function read_file(file) + local fd = assert(io.open(file, "r")) + ---@type string + local data = fd:read "*a" + fd:close() + return data +end + return M From 197e09d2b34abc0836e53cbf0d6ca3dd15d769e2 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Wed, 16 Apr 2025 01:22:06 +0800 Subject: [PATCH 09/22] add readfile util --- lua/obsidian/lsp/util.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/obsidian/lsp/util.lua b/lua/obsidian/lsp/util.lua index 11984615..3744db5d 100644 --- a/lua/obsidian/lsp/util.lua +++ b/lua/obsidian/lsp/util.lua @@ -149,7 +149,9 @@ M.get_links = function(client, bufnr) return links end -local function read_file(file) +-- TODO: make async in the future? + +function M.read_file(file) local fd = assert(io.open(file, "r")) ---@type string local data = fd:read "*a" From 5079a47068b7ba99b01cf34056cfc212c4aeba66 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Wed, 16 Apr 2025 15:07:11 +0800 Subject: [PATCH 10/22] hover on tags, custom preview callback --- lua/obsidian/lsp/config.lua | 20 ++++++++ lua/obsidian/lsp/handlers/config.lua | 4 -- lua/obsidian/lsp/handlers/hover.lua | 59 +++++++++++------------- lua/obsidian/lsp/handlers/initialize.lua | 2 +- lua/obsidian/lsp/util.lua | 20 ++++++++ lua/obsidian/path.lua | 10 ++++ 6 files changed, 77 insertions(+), 38 deletions(-) create mode 100644 lua/obsidian/lsp/config.lua delete mode 100644 lua/obsidian/lsp/handlers/config.lua diff --git a/lua/obsidian/lsp/config.lua b/lua/obsidian/lsp/config.lua new file mode 100644 index 00000000..ed700113 --- /dev/null +++ b/lua/obsidian/lsp/config.lua @@ -0,0 +1,20 @@ +-- TODO:eventaully move to config + +return { + complete = true, + preview = { + tag = function(tag_locs, params) + return ([[Tag used in %d notes]]):format(#tag_locs) + end, + note = function(notes, params) + for i, note in ipairs(notes) do + if vim.uri_from_fname(note.path.filename) == params.textDocument.uri then + table.remove(notes, i) + end + end + local note = notes[1] + return note.path:read() + end, + }, + -- option to only show first few links on hover, and completion doc +} diff --git a/lua/obsidian/lsp/handlers/config.lua b/lua/obsidian/lsp/handlers/config.lua deleted file mode 100644 index 5d316a81..00000000 --- a/lua/obsidian/lsp/handlers/config.lua +++ /dev/null @@ -1,4 +0,0 @@ -return { - complete = true, - -- option to only show first few links on hover, and completion doc -} diff --git a/lua/obsidian/lsp/handlers/hover.lua b/lua/obsidian/lsp/handlers/hover.lua index 2fb8535b..58f09be2 100644 --- a/lua/obsidian/lsp/handlers/hover.lua +++ b/lua/obsidian/lsp/handlers/hover.lua @@ -1,41 +1,34 @@ local util = require "obsidian.util" +local lsp_util = require "obsidian.lsp.util" ---- TODO: hover on tags ---- TODO: should not work on frontmatter? +--- TODO: tag hover should also work on frontmatter -local function read_file(file) - local fd = assert(io.open(file, "r")) - ---@type string - local data = fd:read "*a" - fd:close() - return data -end - ----@param obsidian_client obsidian.Client +---@param client obsidian.Client ---@param params table ---@param handler function -return function(obsidian_client, params, handler, _) - local term = util.parse_cursor_link() - if term then - obsidian_client:find_notes_async( - term, - vim.schedule_wrap(function(notes) - for i, note in ipairs(notes) do - if vim.uri_from_fname(note.path.filename) == params.textDocument.uri then - table.remove(notes, i) - end - end - local note = notes[1] - if note then - handler(nil, { - contents = read_file(note.path.filename), - }) - else - vim.notify("No notes found", 3) - end - end) - ) +return function(client, params, handler, _) + local cursor_ref = util.parse_cursor_link() -- TODO: use title to validate if note is right + local cursor_tag = util.cursor_tag() + if cursor_ref then + lsp_util.preview_ref(client, params, cursor_ref, function(content) + if content then + handler(nil, { + contents = content, + }) + vim.notify("No note found", 3) + end + end) + elseif cursor_tag then + lsp_util.preview_tag(client, params, cursor_tag, function(content) + if content then + handler(nil, { + contents = content, + }) + else + vim.notify("No tag found", 3) + end + end) else - vim.notify("No notes found", 3) + vim.notify("No note or tag found", 3) end end diff --git a/lua/obsidian/lsp/handlers/initialize.lua b/lua/obsidian/lsp/handlers/initialize.lua index cb65b630..a05392cc 100644 --- a/lua/obsidian/lsp/handlers/initialize.lua +++ b/lua/obsidian/lsp/handlers/initialize.lua @@ -1,4 +1,4 @@ -local config = require "obsidian.lsp.handlers.config" +local config = require "obsidian.lsp.config" local completion_options diff --git a/lua/obsidian/lsp/util.lua b/lua/obsidian/lsp/util.lua index 3744db5d..36360bfa 100644 --- a/lua/obsidian/lsp/util.lua +++ b/lua/obsidian/lsp/util.lua @@ -1,5 +1,25 @@ local M = {} local ts = vim.treesitter +local server_config = require "obsidian.lsp.config" + +M.preview_tag = function(client, _, term, cb) + client:find_tags_async( + term, + vim.schedule_wrap(function(tag_locs) + cb(server_config.preview.tag(tag_locs, _)) + end) + ) +end + +-- TODO: if term is file path then just read the file +M.preview_ref = function(client, params, term, cb) + client:find_notes_async( + term, + vim.schedule_wrap(function(notes) + cb(server_config.preview.note(notes, params)) + end) + ) +end local heading_query = [[ (setext_heading diff --git a/lua/obsidian/path.lua b/lua/obsidian/path.lua index 92c526e6..c3c0d0ac 100644 --- a/lua/obsidian/path.lua +++ b/lua/obsidian/path.lua @@ -601,4 +601,14 @@ Path.unlink = function(self, opts) end end +--- Read the file +Path.read = function(self, _) + local file = tostring(self) + local fd = assert(io.open(file, "r")) + ---@type string + local data = fd:read "*a" + fd:close() + return data +end + return Path From a4ed94debb06eac3a0b1eb17217b69374522a59d Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Sat, 19 Apr 2025 19:15:38 +0800 Subject: [PATCH 11/22] feat: implement toggle_checkbox as a server command --- lua/obsidian/init.lua | 8 ++ lua/obsidian/lsp/config.lua | 4 + lua/obsidian/lsp/handlers.lua | 2 + .../lsp/handlers/commands/toggleCheckbox.lua | 46 +++++++ lua/obsidian/lsp/handlers/completion.lua | 126 +++++++++--------- .../lsp/handlers/completion_resolve.lua | 12 +- lua/obsidian/lsp/handlers/execute_command.lua | 5 + lua/obsidian/lsp/handlers/initialize.lua | 5 + lua/obsidian/lsp/handlers/initialized.lua | 6 + 9 files changed, 150 insertions(+), 64 deletions(-) create mode 100644 lua/obsidian/lsp/handlers/commands/toggleCheckbox.lua create mode 100644 lua/obsidian/lsp/handlers/execute_command.lua create mode 100644 lua/obsidian/lsp/handlers/initialized.lua diff --git a/lua/obsidian/init.lua b/lua/obsidian/init.lua index 5fc9ff10..9c105716 100644 --- a/lua/obsidian/init.lua +++ b/lua/obsidian/init.lua @@ -158,6 +158,14 @@ obsidian.setup = function(opts) local client_id = require("obsidian.lsp").start() assert(client_id) + vim.keymap.set("n", "cH", function() + local lsp_client = assert(vim.lsp.get_client_by_id(client_id)) + lsp_client:exec_cmd({ + title = "toggle checkbox", + command = "toggleCheckbox", + }, { bufnr = ev.buf }) + end, { buffer = ev.buf }) + if not pcall(require, "blink.cmp") then vim.lsp.completion.enable(true, client_id, ev.buf, { autotrigger = true }) end diff --git a/lua/obsidian/lsp/config.lua b/lua/obsidian/lsp/config.lua index ed700113..36a9fb28 100644 --- a/lua/obsidian/lsp/config.lua +++ b/lua/obsidian/lsp/config.lua @@ -2,6 +2,10 @@ return { complete = true, + checkboxs = { + ---@type "- [ ] " | "* [ ] " | "+ [ ] " | "1. [ ] " | "1) [ ] " + style = "- [ ] ", + }, preview = { tag = function(tag_locs, params) return ([[Tag used in %d notes]]):format(#tag_locs) diff --git a/lua/obsidian/lsp/handlers.lua b/lua/obsidian/lsp/handlers.lua index 40b3316f..c379422f 100644 --- a/lua/obsidian/lsp/handlers.lua +++ b/lua/obsidian/lsp/handlers.lua @@ -12,6 +12,8 @@ return setmetatable({ [ms.textDocument_documentLink] = require "obsidian.lsp.handlers.document_link", [ms.textDocument_didOpen] = require "obsidian.lsp.handlers.did_open", [ms.textDocument_didChange] = require "obsidian.lsp.handlers.did_change", + [ms.initialized] = require "obsidian.lsp.handlers.initialized", + [ms.workspace_executeCommand] = require "obsidian.lsp.handlers.execute_command", }, { __index = function(_, k) vim.notify("obsidian_ls does not support method " .. k .. " yet", 3) diff --git a/lua/obsidian/lsp/handlers/commands/toggleCheckbox.lua b/lua/obsidian/lsp/handlers/commands/toggleCheckbox.lua new file mode 100644 index 00000000..5772550b --- /dev/null +++ b/lua/obsidian/lsp/handlers/commands/toggleCheckbox.lua @@ -0,0 +1,46 @@ +local util = require "obsidian.util" +local config = require "obsidian.lsp.config" + +local gen_checkbox_edit = function(uri, newText, line, start) + local edit = { + range = { + start = { line = line, character = start or 0 }, + ["end"] = { line = line, character = (start or -1) + 1 }, + }, + newText = newText, + } + return { + changes = { + [uri] = { edit }, + }, + } +end + +---@param client obsidian.Client +local gen_checkbox_edits = function(client, buf) + local line_num = unpack(vim.api.nvim_win_get_cursor(0)) - 1 -- 0-indexed + local line = vim.api.nvim_buf_get_lines(buf, line_num, line_num + 1, false)[1] + local uri = vim.uri_from_bufnr(buf) + + local checkboxes = vim.tbl_keys(client.opts.ui.checkboxes) + + local defualt_checkbox_style = config.checkboxs.style + + if util.is_checkbox(line) then + for i, check_char in ipairs(checkboxes) do + local start = string.find(line, "%[" .. vim.pesc(check_char)) + if start then + i = i % #checkboxes + return gen_checkbox_edit(uri, checkboxes[i + 1], line_num, start) + end + end + else + return gen_checkbox_edit(uri, defualt_checkbox_style, line_num, nil) + end +end + +return function(client, params) + local buf = vim.api.nvim_get_current_buf() + local edits = gen_checkbox_edits(client, buf) + vim.lsp.util.apply_workspace_edit(edits, "utf-8") +end diff --git a/lua/obsidian/lsp/handlers/completion.lua b/lua/obsidian/lsp/handlers/completion.lua index 13a8e0ce..43d24ef6 100644 --- a/lua/obsidian/lsp/handlers/completion.lua +++ b/lua/obsidian/lsp/handlers/completion.lua @@ -1,5 +1,6 @@ -- TODO: completion for anchor, blocks -- TODO: create item +-- TODO: memoize? local ref_trigger_pattern = { wiki = "[[", @@ -8,72 +9,73 @@ local ref_trigger_pattern = { local find, sub, lower = string.find, string.sub, string.lower ----@param client obsidian.Client ----@param params table ----@param handler function -return function(client, params, handler, _) +local function calc_insert_text(client, note) local link_style = client.opts.preferred_link_style - local min_chars = client.opts.completion.min_chars - - local function calc_insert_text(note) - local link_text = client:format_link(note) - if link_style == "markdown" then - return sub(link_text, 2) - else - return sub(link_text, 3) - end + local link_text = client:format_link(note) + if link_style == "markdown" then + return sub(link_text, 2) + else + return sub(link_text, 3) end +end - local function handle_ref(partial) - local items = {} - client:find_notes_async( - partial, - vim.schedule_wrap(function(notes) - for _, note in ipairs(notes) do - local title = note.title - local pattern = vim.pesc(lower(partial)) - if title and find(lower(title), pattern) then - table.insert(items, { - kind = 17, - label = title, - filterText = title, - insertText = calc_insert_text(note), - labelDetails = { description = "Obsidian" }, - data = { - file = note.path.filename, - kind = "ref", - }, - }) - end +local function handle_ref(client, partial, handler) + local items = {} + client:find_notes_async( + partial, + vim.schedule_wrap(function(notes) + for _, note in ipairs(notes) do + local title = note.title + local pattern = vim.pesc(lower(partial)) + if title and find(lower(title), pattern) then + table.insert(items, { + kind = 17, + label = title, + filterText = title, + insertText = calc_insert_text(client, note), + labelDetails = { description = "Obsidian" }, + data = { + file = note.path.filename, + kind = "ref", + }, + }) end - handler(nil, { items = items }) - end) - ) - end + end + handler(nil, { items = items }) + end) + ) +end - local function handle_tag(partial) - local items = {} - client:list_tags_async( - partial, - vim.schedule_wrap(function(tags) - for _, tag in ipairs(tags) do - if tag and tag:lower():find(vim.pesc(partial:lower())) then - table.insert(items, { - kind = "File", - label = tag, - filterText = tag, - insertText = tag, - labelDetails = { description = "ObsidianTag" }, - data = { - kind = "tag", - }, - }) - end +local function handle_tag(client, partial, handler) + local items = {} + client:list_tags_async( + partial, + vim.schedule_wrap(function(tags) + for _, tag in ipairs(tags) do + if tag and tag:lower():find(vim.pesc(partial:lower())) then + table.insert(items, { + kind = "File", + label = tag, + filterText = tag, + insertText = tag, + labelDetails = { description = "ObsidianTag" }, + data = { + kind = "tag", + }, + }) end - handler(nil, { items = items }) - end) - ) - end + end + handler(nil, { items = items }) + end) + ) +end + +---@param client obsidian.Client +---@param params table +---@param handler function +return function(client, params, handler, _) + local link_style = client.opts.preferred_link_style + local min_chars = client.opts.completion.min_chars local uri = params.textDocument.uri local line_num = params.position.line @@ -89,12 +91,12 @@ return function(client, params, handler, _) if ref_start then local partial = sub(text_before_cursor, ref_start + 2) if #partial >= min_chars then - handle_ref(partial) + handle_ref(client, partial, handler) end elseif tag_start then local partial = sub(text_before_cursor, tag_start + 1) if #partial >= min_chars then - handle_tag(partial) + handle_tag(client, partial, handler) end end end diff --git a/lua/obsidian/lsp/handlers/completion_resolve.lua b/lua/obsidian/lsp/handlers/completion_resolve.lua index 7e0df8e1..0af5d425 100644 --- a/lua/obsidian/lsp/handlers/completion_resolve.lua +++ b/lua/obsidian/lsp/handlers/completion_resolve.lua @@ -1,9 +1,9 @@ local util = require "obsidian.lsp.util" ----@param _ obsidian.Client +---@param client obsidian.Client ---@param params table ---@param handler function -return function(_, params, handler, _) +return function(client, params, handler, _) local kind = params.data.kind if kind == "ref" then @@ -17,5 +17,13 @@ return function(_, params, handler, _) else vim.notify("No notes found", 3) end + elseif kind == "tag" then + util.preview_tag(client, params, params.label, function(content) + params.documentation = { + value = content, + kind = "plaintext", + } + handler(nil, params) + end) end end diff --git a/lua/obsidian/lsp/handlers/execute_command.lua b/lua/obsidian/lsp/handlers/execute_command.lua new file mode 100644 index 00000000..02061e2a --- /dev/null +++ b/lua/obsidian/lsp/handlers/execute_command.lua @@ -0,0 +1,5 @@ +---@param client obsidian.Client +---@param params table +return function(client, params) + return require "obsidian.lsp.handlers.commands.toggleCheckbox"(client, params) +end diff --git a/lua/obsidian/lsp/handlers/initialize.lua b/lua/obsidian/lsp/handlers/initialize.lua index a05392cc..64d45241 100644 --- a/lua/obsidian/lsp/handlers/initialize.lua +++ b/lua/obsidian/lsp/handlers/initialize.lua @@ -36,6 +36,11 @@ local initializeResult = { renameProvider = true, referencesProvider = true, documentSymbolProvider = true, + executeCommandProvider = { + commands = { + "toggleCheckbox", + }, + }, completionProvider = completion_options, textDocumentSync = { openClose = true, diff --git a/lua/obsidian/lsp/handlers/initialized.lua b/lua/obsidian/lsp/handlers/initialized.lua new file mode 100644 index 00000000..2c94bec2 --- /dev/null +++ b/lua/obsidian/lsp/handlers/initialized.lua @@ -0,0 +1,6 @@ +---@param obsidian_client obsidian.Client +---@param params table +---@param handler function +return function(obsidian_client, params, handler, _) + return +end From 35bfbfddd6163eab8bc061a5afad1ea40537fee2 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Sat, 19 Apr 2025 19:39:11 +0800 Subject: [PATCH 12/22] fix: preview tags in completion --- lua/obsidian/lsp/handlers/completion.lua | 41 +++++++++---------- .../lsp/handlers/completion_resolve.lua | 13 +++--- lua/obsidian/lsp/util.lua | 6 +++ 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/lua/obsidian/lsp/handlers/completion.lua b/lua/obsidian/lsp/handlers/completion.lua index 43d24ef6..62c2f1e2 100644 --- a/lua/obsidian/lsp/handlers/completion.lua +++ b/lua/obsidian/lsp/handlers/completion.lua @@ -21,29 +21,26 @@ end local function handle_ref(client, partial, handler) local items = {} - client:find_notes_async( - partial, - vim.schedule_wrap(function(notes) - for _, note in ipairs(notes) do - local title = note.title - local pattern = vim.pesc(lower(partial)) - if title and find(lower(title), pattern) then - table.insert(items, { - kind = 17, - label = title, - filterText = title, - insertText = calc_insert_text(client, note), - labelDetails = { description = "Obsidian" }, - data = { - file = note.path.filename, - kind = "ref", - }, - }) - end + client:find_notes_async(partial, function(notes) + for _, note in ipairs(notes) do + local title = note.title + local pattern = vim.pesc(lower(partial)) + if title and find(lower(title), pattern) then + table.insert(items, { + kind = 17, + label = title, + filterText = title, + insertText = calc_insert_text(client, note), + labelDetails = { description = "Obsidian" }, + data = { + file = note.path.filename, + kind = "ref", + }, + }) end - handler(nil, { items = items }) - end) - ) + end + handler(nil, { items = items }) + end) end local function handle_tag(client, partial, handler) diff --git a/lua/obsidian/lsp/handlers/completion_resolve.lua b/lua/obsidian/lsp/handlers/completion_resolve.lua index 0af5d425..b48f22b6 100644 --- a/lua/obsidian/lsp/handlers/completion_resolve.lua +++ b/lua/obsidian/lsp/handlers/completion_resolve.lua @@ -18,12 +18,11 @@ return function(client, params, handler, _) vim.notify("No notes found", 3) end elseif kind == "tag" then - util.preview_tag(client, params, params.label, function(content) - params.documentation = { - value = content, - kind = "plaintext", - } - handler(nil, params) - end) + local content = util.preview_tag_sync(client, params, params.label) + params.documentation = { + value = content, + kind = "markdown", + } + handler(nil, params) end end diff --git a/lua/obsidian/lsp/util.lua b/lua/obsidian/lsp/util.lua index 36360bfa..1f82f782 100644 --- a/lua/obsidian/lsp/util.lua +++ b/lua/obsidian/lsp/util.lua @@ -11,6 +11,12 @@ M.preview_tag = function(client, _, term, cb) ) end +M.preview_tag_sync = function(client, _, term) + local tag_locs = client:find_tags(term, { timeout = 4000 }) + + return server_config.preview.tag(tag_locs, _) +end + -- TODO: if term is file path then just read the file M.preview_ref = function(client, params, term, cb) client:find_notes_async( From 8ddaa58cfd2b2f1b15aa977fcedac18035698efc Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Sat, 19 Apr 2025 22:06:10 +0800 Subject: [PATCH 13/22] fix: completion use TextEdits to be more accurate --- lua/obsidian/client.lua | 39 ++++++++ lua/obsidian/init.lua | 18 +--- lua/obsidian/lsp/handlers/completion.lua | 110 ++++++++++++++--------- lua/obsidian/lsp/init.lua | 37 -------- 4 files changed, 111 insertions(+), 93 deletions(-) delete mode 100644 lua/obsidian/lsp/init.lua diff --git a/lua/obsidian/client.lua b/lua/obsidian/client.lua index 3e66a8e9..fb508168 100644 --- a/lua/obsidian/client.lua +++ b/lua/obsidian/client.lua @@ -2125,4 +2125,43 @@ Client.statusline = function(self) timer:start(0, 1000, vim.schedule_wrap(refresh)) end +--- Start the lsp client +--- +---@return integer +Client.lsp_start = function(self) + local handlers = require "obsidian.lsp.handlers" + local has_blink, blink = pcall(require, "blink.cmp") + local has_cmp, cmp_lsp = pcall(require, "cmp_nvim_lsp") + + local capabilities + if has_blink then + capabilities = blink.get_lsp_capabilities({}, true) + elseif has_cmp then + capabilities = cmp_lsp.default_capabilities() + else + capabilities = vim.lsp.protocol.make_client_capabilities() + end + local client_id = vim.lsp.start { + name = "obsidian-ls", + capabilities = capabilities, + cmd = function() + return { + request = function(method, params, handler, _) + handlers[method](self, params, handler, _) + end, + notify = function(method, params, handler, _) + handlers[method](self, params, handler, _) + end, + is_closing = function() end, + terminate = function() end, + } + end, + init_options = {}, + root_dir = tostring(self.dir), + } + assert(client_id, "failed to start obsidian_ls") + + return client_id +end + return Client diff --git a/lua/obsidian/init.lua b/lua/obsidian/init.lua index 9c105716..5e4be848 100644 --- a/lua/obsidian/init.lua +++ b/lua/obsidian/init.lua @@ -144,29 +144,19 @@ obsidian.setup = function(opts) vim.keymap.set("n", mapping_keys, mapping_config.action, mapping_config.opts) end - -- Inject completion sources, providers to their plugin configurations - -- if opts.completion.nvim_cmp then - -- require("obsidian.completion.plugin_initializers.nvim_cmp").inject_sources() - -- elseif opts.completion.blink then - -- require("obsidian.completion.plugin_initializers.blink").inject_sources() - -- end - vim.bo[ev.buf].omnifunc = "v:lua.vim.lsp.omnifunc" vim.bo[ev.buf].completeopt = "menu,menuone,noselect" vim.bo[ev.buf].iskeyword = "@,48-57,192-255" -- HACK: so that completion for note names with `-` in it works in native completion - local client_id = require("obsidian.lsp").start() - assert(client_id) + local client_id = client:lsp_start() + -- place holders vim.keymap.set("n", "cH", function() local lsp_client = assert(vim.lsp.get_client_by_id(client_id)) - lsp_client:exec_cmd({ - title = "toggle checkbox", - command = "toggleCheckbox", - }, { bufnr = ev.buf }) + lsp_client:exec_cmd({ title = "toggle checkbox", command = "toggleCheckbox" }, { bufnr = ev.buf }) end, { buffer = ev.buf }) - if not pcall(require, "blink.cmp") then + if not (pcall(require, "blink.cmp") or pcall(require, "cmp")) then vim.lsp.completion.enable(true, client_id, ev.buf, { autotrigger = true }) end diff --git a/lua/obsidian/lsp/handlers/completion.lua b/lua/obsidian/lsp/handlers/completion.lua index 62c2f1e2..bcb63d50 100644 --- a/lua/obsidian/lsp/handlers/completion.lua +++ b/lua/obsidian/lsp/handlers/completion.lua @@ -7,40 +7,71 @@ local ref_trigger_pattern = { markdown = "[", } +local util = require "obsidian.util" + local find, sub, lower = string.find, string.sub, string.lower -local function calc_insert_text(client, note) - local link_style = client.opts.preferred_link_style - local link_text = client:format_link(note) - if link_style == "markdown" then - return sub(link_text, 2) - else - return sub(link_text, 3) - end +---@param note obsidian.Note +---@param insert_text string +---@param insert_start integer +---@param insert_end integer +---@param line_num integer +---@return lsp.CompletionItem +local function calc_ref_item(note, insert_text, insert_start, insert_end, line_num) + return { + kind = 17, + label = note.title, + filterText = note.title, + textEdit = { + range = { + start = { line = line_num, character = insert_start }, + ["end"] = { line = line_num, character = insert_end }, + }, + newText = insert_text, + }, + labelDetails = { description = "Obsidian" }, + data = { + file = note.path.filename, + kind = "ref", + }, + } end -local function handle_ref(client, partial, handler) +local function handle_ref(client, partial, ref_start, cursor_col, line_num, handler) + ---@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) + local items = {} - client:find_notes_async(partial, function(notes) - for _, note in ipairs(notes) do - local title = note.title - local pattern = vim.pesc(lower(partial)) - if title and find(lower(title), pattern) then - table.insert(items, { - kind = 17, - label = title, - filterText = title, - insertText = calc_insert_text(client, note), - labelDetails = { description = "Obsidian" }, - data = { - file = note.path.filename, - kind = "ref", - }, - }) + client:find_notes_async( + partial, + vim.schedule_wrap(function(notes) + for _, note in ipairs(notes) do + local title = note.title + local pattern = vim.pesc(lower(partial)) + if title and find(lower(title), pattern) then + local link_text = client:format_link(note) + items[#items + 1] = calc_ref_item(note, link_text, ref_start, cursor_col, line_num) + end end - end - handler(nil, { items = items }) - end) + handler(nil, { items = items }) + end) + ) +end + +local function calc_tag_item(tag) + return { + kind = "File", + label = tag, + filterText = tag, + insertText = tag, + labelDetails = { description = "ObsidianTag" }, + data = { kind = "tag" }, + } end local function handle_tag(client, partial, handler) @@ -50,16 +81,7 @@ local function handle_tag(client, partial, handler) vim.schedule_wrap(function(tags) for _, tag in ipairs(tags) do if tag and tag:lower():find(vim.pesc(partial:lower())) then - table.insert(items, { - kind = "File", - label = tag, - filterText = tag, - insertText = tag, - labelDetails = { description = "ObsidianTag" }, - data = { - kind = "tag", - }, - }) + items[#items + 1] = calc_tag_item(tag) end end handler(nil, { items = items }) @@ -77,18 +99,22 @@ return function(client, params, handler, _) local uri = params.textDocument.uri local line_num = params.position.line local char_num = params.position.character + local buf = vim.uri_to_bufnr(uri) + local line_text = vim.api.nvim_buf_get_lines(buf, line_num, line_num + 1, false)[1] + + print(util.strip_anchor_links(line_text)) + print(util.strip_block_links(line_text)) - local line_text = (vim.api.nvim_buf_get_lines(buf, line_num, line_num + 1, false)[1] or "") - local text_before_cursor = string.sub(line_text, 1, char_num) + local text_before_cursor = sub(line_text, 1, char_num) local tag_start = find(text_before_cursor, "#", 1, true) local ref_start = find(text_before_cursor, ref_trigger_pattern[link_style], 1, true) if ref_start then - local partial = sub(text_before_cursor, ref_start + 2) + local partial = sub(text_before_cursor, ref_start + #ref_trigger_pattern[link_style]) if #partial >= min_chars then - handle_ref(client, partial, handler) + handle_ref(client, partial, ref_start - 1, char_num, line_num, handler) end elseif tag_start then local partial = sub(text_before_cursor, tag_start + 1) diff --git a/lua/obsidian/lsp/init.lua b/lua/obsidian/lsp/init.lua deleted file mode 100644 index 7f3060fd..00000000 --- a/lua/obsidian/lsp/init.lua +++ /dev/null @@ -1,37 +0,0 @@ --- reference: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/ --- reference: https://github.com/zk-org/zk/blob/main/internal/adapter/lsp/server.go -local obsidian_client = require("obsidian").get_client() -local handlers = require "obsidian.lsp.handlers" - -local obsidian_ls = {} -local capabilities = vim.lsp.protocol.make_client_capabilities() -local has_blink, blink = pcall(require, "blink.cmp") -if has_blink then - capabilities = blink.get_lsp_capabilities({}, true) -end - ----@return integer? client_id -obsidian_ls.start = function() - local client_id = vim.lsp.start { - name = "obsidian-ls", - capabilities = capabilities, - cmd = function() - local members = { - request = function(method, params, handler, _) - handlers[method](obsidian_client, params, handler, _) - end, - notify = function(method, params, handler, _) - handlers[method](obsidian_client, params, handler, _) - end, - is_closing = function() end, - terminate = function() end, - } - return members - end, - init_options = {}, - root_dir = tostring(obsidian_client.dir), - } - return client_id -end - -return obsidian_ls From f492596ba257c082636cd9ee4d8f0a6cf0b05095 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Sun, 20 Apr 2025 16:29:37 +0800 Subject: [PATCH 14/22] feat: better interact with pickers --- lua/obsidian/commands/__toc.lua | 59 +++++++ lua/obsidian/commands/backlinks.lua | 153 ++++-------------- lua/obsidian/lsp/handlers/document_symbol.lua | 3 +- lua/obsidian/lsp/handlers/references.lua | 1 + lua/obsidian/lsp/util.lua | 9 +- 5 files changed, 98 insertions(+), 127 deletions(-) create mode 100644 lua/obsidian/commands/__toc.lua diff --git a/lua/obsidian/commands/__toc.lua b/lua/obsidian/commands/__toc.lua new file mode 100644 index 00000000..8127900a --- /dev/null +++ b/lua/obsidian/commands/__toc.lua @@ -0,0 +1,59 @@ +-- TODO: all pickers should do on_list callbacks + +local telescope_on_list = function(data) + local pickers = require "telescope.pickers" + local finders = require "telescope.finders" + pickers + .new({}, { + prompt_title = "Table of Contents", + finder = finders.new_table { + results = data.items, + entry_maker = function(value) + return { + value = value, + display = value.text, + path = value.filename, + ordinal = value.text, + } + end, + }, + }) + :find() +end + +local function mini_pick_on_list(data) + local ok, pick = pcall(require, "mini.pick") + if not ok then + vim.notify("no mini.pick found", 3) + return + end + + local items = data.items + for _, item in ipairs(data.items) do + item.path = item.filename + end + + pick.start { + source = { items = items }, + name = "Table of Contents", + show = pick.default_show, + choose = pick.default_choose, + } +end + +---@param client obsidian.Client +---@param _ CommandArgs +return function(client, _) + local picker_name = tostring(client:picker()) + if picker_name == "TelescopePicker()" then + vim.lsp.buf.document_symbol { on_list = telescope_on_list } + elseif picker_name == "SnacksPicker()" then + require("snacks.picker").lsp_symbols() + elseif picker_name == "FzfPicker()" then + require("fzf-lua").lsp_document_symbols() + elseif picker_name == "MiniPicker()" then + vim.lsp.buf.document_symbol { on_list = mini_pick_on_list } + else + vim.lsp.buf.document_symbol { loclist = false } + end +end diff --git a/lua/obsidian/commands/backlinks.lua b/lua/obsidian/commands/backlinks.lua index 30a3ca5f..358e1c98 100644 --- a/lua/obsidian/commands/backlinks.lua +++ b/lua/obsidian/commands/backlinks.lua @@ -1,56 +1,25 @@ -local util = require "obsidian.util" local log = require "obsidian.log" -local RefTypes = require("obsidian.search").RefTypes ----@param client obsidian.Client ----@param picker obsidian.Picker ----@param note obsidian.Note ----@param opts { anchor: string|?, block: string|? }|? -local function collect_backlinks(client, picker, note, opts) - opts = opts or {} - - client:find_backlinks_async(note, function(backlinks) - if vim.tbl_isempty(backlinks) then - if opts.anchor then - log.info("No backlinks found for anchor '%s' in note '%s'", opts.anchor, note.id) - elseif opts.block then - log.info("No backlinks found for block '%s' in note '%s'", opts.block, note.id) - else - log.info("No backlinks found for note '%s'", note.id) - end - return - end - - local entries = {} - for _, matches in ipairs(backlinks) do - for _, match in ipairs(matches.matches) do - entries[#entries + 1] = { - value = { path = matches.path, line = match.line }, - filename = tostring(matches.path), - lnum = match.line, - } - end - end - - ---@type string - local prompt_title - if opts.anchor then - prompt_title = string.format("Backlinks to '%s%s'", note.id, opts.anchor) - elseif opts.block then - prompt_title = string.format("Backlinks to '%s#%s'", note.id, util.standardize_block(opts.block)) - else - prompt_title = string.format("Backlinks to '%s'", note.id) - end - - vim.schedule(function() - picker:pick(entries, { - prompt_title = prompt_title, - callback = function(value) - util.open_buffer(value.path, { line = value.line }) +local telescope_on_list = function(data) + local pickers = require "telescope.pickers" + local finders = require "telescope.finders" + pickers + .new({}, { + prompt_title = "References", + finder = finders.new_table { + results = data.items, + entry_maker = function(value) + return { + value = value, + display = value.text, + path = value.filename, + ordinal = value.text, + lnum = value.lnum, + } end, - }) - end) - end, { search = { sort = true }, anchor = opts.anchor, block = opts.block }) + }, + }) + :find() end ---@param client obsidian.Client @@ -60,77 +29,19 @@ return function(client) log.err "No picker configured" return end - - local location, _, ref_type = util.parse_cursor_link { include_block_ids = true } - - if - location ~= nil - and ref_type ~= RefTypes.NakedUrl - and ref_type ~= RefTypes.FileUrl - and ref_type ~= RefTypes.BlockID - then - -- Remove block links from the end if there are any. - -- TODO: handle block links. - ---@type string|? - local block_link - location, block_link = util.strip_block_links(location) - - -- Remove anchor links from the end if there are any. - ---@type string|? - local anchor_link - location, anchor_link = util.strip_anchor_links(location) - - -- Assume 'location' is current buffer path if empty, like for TOCs. - if string.len(location) == 0 then - location = vim.api.nvim_buf_get_name(0) - end - - local opts = { anchor = anchor_link, block = block_link } - - client:resolve_note_async(location, function(...) - ---@type obsidian.Note[] - local notes = { ... } - - if #notes == 0 then - log.err("No notes matching '%s'", location) - return - elseif #notes == 1 then - return collect_backlinks(client, picker, notes[1], opts) - else - return vim.schedule(function() - picker:pick_note(notes, { - prompt_title = "Select note", - callback = function(note) - collect_backlinks(client, picker, note, opts) - end, - }) - end) - end - end) + local picker_name = tostring(picker) + + if picker_name == "TelescopePicker()" then + vim.lsp.buf.references({ + includeDeclaration = false, + }, { on_list = telescope_on_list }) + elseif picker_name == "SnacksPicker()" then + require("snacks.picker").lsp_symbols() + elseif picker_name == "FzfPicker()" then + require("fzf-lua").lsp_document_symbols() + elseif picker_name == "MiniPicker()" then + -- vim.lsp.buf.document_symbol { on_list = mini_pick_on_list } else - ---@type { anchor: string|?, block: string|? } - local opts = {} - ---@type obsidian.note.LoadOpts - local load_opts = {} - - if ref_type == RefTypes.BlockID then - opts.block = location - else - load_opts.collect_anchor_links = true - end - - local note = client:current_note(0, load_opts) - - -- Check if cursor is on a header, if so, use that anchor. - local header_match = util.parse_header(vim.api.nvim_get_current_line()) - if header_match then - opts.anchor = header_match.anchor - end - - if note == nil then - log.err "Current buffer does not appear to be a note inside the vault" - else - collect_backlinks(client, picker, note, opts) - end + vim.lsp.buf.document_symbol { loclist = false } end end diff --git a/lua/obsidian/lsp/handlers/document_symbol.lua b/lua/obsidian/lsp/handlers/document_symbol.lua index 9e6ab076..37484216 100644 --- a/lua/obsidian/lsp/handlers/document_symbol.lua +++ b/lua/obsidian/lsp/handlers/document_symbol.lua @@ -5,5 +5,6 @@ local util = require "obsidian.lsp.util" ---@param handler function return function(client, params, handler) local bufnr = vim.uri_to_bufnr(params.textDocument.uri) - handler(nil, util.get_headings(client, bufnr)) + local symbols = util.get_headings(bufnr) + handler(nil, symbols) end diff --git a/lua/obsidian/lsp/handlers/references.lua b/lua/obsidian/lsp/handlers/references.lua index edf47656..fb4f0cbc 100644 --- a/lua/obsidian/lsp/handlers/references.lua +++ b/lua/obsidian/lsp/handlers/references.lua @@ -52,6 +52,7 @@ return function(obsidian_client, _, handler, _) -- return -- end -- + local entries = {} for _, matches in ipairs(backlinks) do for _, match in ipairs(matches.matches) do diff --git a/lua/obsidian/lsp/util.lua b/lua/obsidian/lsp/util.lua index 1f82f782..d25777a9 100644 --- a/lua/obsidian/lsp/util.lua +++ b/lua/obsidian/lsp/util.lua @@ -70,10 +70,9 @@ local link_query = [[ ]] ---Extract headings from buffer ----@param client obsidian.Client ---@param bufnr integer ----@return table TODO: -M.get_headings = function(client, bufnr) +---@return lsp.DocumentSymbol[] +M.get_headings = function(bufnr) local lang = ts.language.get_lang(vim.bo[bufnr].filetype) if not lang then return {} @@ -85,7 +84,7 @@ M.get_headings = function(client, bufnr) for id, node, _, _ in query:iter_captures(root, bufnr) do local text = string.rep("#", id) .. " " .. ts.get_node_text(node, bufnr) local row, _ = node:start() - table.insert(headings, { + headings[#headings + 1] = { kind = 15, range = { start = { line = row, character = 1 }, @@ -96,7 +95,7 @@ M.get_headings = function(client, bufnr) ["end"] = { line = row, character = 1 }, }, name = text, - }) + } end return headings end From 86e6a069e7078a1a0de774f32dc730f8fa13962d Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Tue, 22 Apr 2025 01:25:51 +0800 Subject: [PATCH 15/22] feat: use snippet marker to jump to anchor/block pos on accept --- lua/obsidian/lsp/handlers/completion.lua | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/lua/obsidian/lsp/handlers/completion.lua b/lua/obsidian/lsp/handlers/completion.lua index bcb63d50..926c0561 100644 --- a/lua/obsidian/lsp/handlers/completion.lua +++ b/lua/obsidian/lsp/handlers/completion.lua @@ -11,23 +11,32 @@ local util = require "obsidian.util" local find, sub, lower = string.find, string.sub, string.lower +local function insert_snippet_marker(text, style) + if style == "markdown" then + local pos = text:find "]" + local a, b = sub(text, 1, pos - 1), sub(text, pos) + return a .. "$1" .. b + end +end + ---@param note obsidian.Note ---@param insert_text string ---@param insert_start integer ---@param insert_end integer ---@param line_num integer ---@return lsp.CompletionItem -local function calc_ref_item(note, insert_text, insert_start, insert_end, line_num) +local function calc_ref_item(note, insert_text, insert_start, insert_end, line_num, style) return { kind = 17, label = note.title, filterText = note.title, + insertTextFormat = 2, -- is snippet textEdit = { range = { start = { line = line_num, character = insert_start }, ["end"] = { line = line_num, character = insert_end }, }, - newText = insert_text, + newText = insert_snippet_marker(insert_text, style), }, labelDetails = { description = "Obsidian" }, data = { @@ -55,7 +64,8 @@ local function handle_ref(client, partial, ref_start, cursor_col, line_num, hand local pattern = vim.pesc(lower(partial)) if title and find(lower(title), pattern) then local link_text = client:format_link(note) - items[#items + 1] = calc_ref_item(note, link_text, ref_start, cursor_col, line_num) + local style = client.opts.preferred_link_style + items[#items + 1] = calc_ref_item(note, link_text, ref_start, cursor_col, line_num, style) end end handler(nil, { items = items }) @@ -103,9 +113,9 @@ return function(client, params, handler, _) local buf = vim.uri_to_bufnr(uri) local line_text = vim.api.nvim_buf_get_lines(buf, line_num, line_num + 1, false)[1] - print(util.strip_anchor_links(line_text)) - print(util.strip_block_links(line_text)) - + -- print(util.strip_anchor_links(line_text)) + -- print(util.strip_block_links(line_text)) + -- local text_before_cursor = sub(line_text, 1, char_num) local tag_start = find(text_before_cursor, "#", 1, true) From c1c4ebb40462d2321546748c44bcfa1133829f53 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Tue, 22 Apr 2025 15:31:02 +0800 Subject: [PATCH 16/22] feat: initial try to do anchor links --- lua/obsidian/lsp/handlers/completion.lua | 139 +++++++++++++++++++---- 1 file changed, 120 insertions(+), 19 deletions(-) diff --git a/lua/obsidian/lsp/handlers/completion.lua b/lua/obsidian/lsp/handlers/completion.lua index 926c0561..5bb23845 100644 --- a/lua/obsidian/lsp/handlers/completion.lua +++ b/lua/obsidian/lsp/handlers/completion.lua @@ -46,31 +46,113 @@ local function calc_ref_item(note, insert_text, insert_start, insert_end, line_n } end +local state = { + ---@type obsidian.Note + current_note = nil, +} + +---Collect matching anchor links. +---@param note obsidian.Note +---@param anchor_link string? +---@return obsidian.note.HeaderAnchor[]? +local function 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 + +---@client obsidian.Client local function handle_ref(client, partial, ref_start, cursor_col, line_num, handler) ---@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) + ---@type string|? + local anchor_link + partial, anchor_link = util.strip_anchor_links(partial) + print(partial) local items = {} - client:find_notes_async( - partial, - vim.schedule_wrap(function(notes) - for _, note in ipairs(notes) do - local title = note.title - local pattern = vim.pesc(lower(partial)) - if title and find(lower(title), pattern) then - local link_text = client:format_link(note) - local style = client.opts.preferred_link_style - items[#items + 1] = calc_ref_item(note, link_text, ref_start, cursor_col, line_num, style) + if not anchor_link then + client:find_notes_async( + partial, + vim.schedule_wrap(function(notes) + for _, note in ipairs(notes) do + local title = note.title + local pattern = vim.pesc(lower(partial)) + if title and find(lower(title), pattern) then + local link_text = client:format_link(note) + local style = client.opts.preferred_link_style + items[#items + 1] = calc_ref_item(note, link_text, ref_start, cursor_col, line_num, style) + end + handler(nil, { items = items }) end - end - handler(nil, { items = items }) - end) - ) + end) + ) + else + local Note = require "obsidian.note" + -- state.current_note = state.current_note or client:find_notes(partial)[2] + -- TODO: calc current_note once + -- TODO: handle two cases: + -- 1. typing partial note name, no completeed text after cursor, insert the full link + -- 2. jumped to heading, only insert anchor + -- TODO: need to do more textEdit to insert additional #title to path so that app supports? + client:find_notes_async( + partial, + vim.schedule_wrap(function(notes) + for _, note in ipairs(notes) do + local title = note.title + local pattern = vim.pesc(lower(partial)) + if title and find(lower(title), pattern) then + local note2 = Note.from_file(note.path.filename, { collect_anchor_links = true }) + + local note_anchors = collect_matching_anchors(note2, anchor_link) + if not note_anchors then + return + end + for _, anchor in ipairs(note_anchors) do + items[#items + 1] = { + kind = 17, + label = anchor.header, + filterText = anchor.header, + insertText = anchor.header, + -- insertTextFormat = 2, -- is snippet + -- textEdit = { + -- range = { + -- start = { line = line_num, character = insert_start }, + -- ["end"] = { line = line_num, character = insert_end }, + -- }, + -- newText = insert_snippet_marker(insert_text, style), + -- }, + labelDetails = { description = "ObsidianAnchor" }, + data = { + file = note.path.filename, + kind = "anchor", + }, + } + end + end + handler(nil, { items = items }) + end + end) + ) + vim.print(state.current_note) + end end local function calc_tag_item(tag) @@ -99,6 +181,18 @@ local function handle_tag(client, partial, handler) ) end +local function handle_heading(client) + -- TODO: client:find_headings_async + -- client:find_ +end + +-- util.BLOCK_PATTERN = "%^[%w%d][%w%d-]*" +local anchor_trigger_pattern = { + markdown = "%[%S+#(%w*)", +} + +local heading_trigger_pattern = "[##" + ---@param client obsidian.Client ---@param params table ---@param handler function @@ -118,10 +212,16 @@ return function(client, params, handler, _) -- local text_before_cursor = sub(line_text, 1, char_num) - local tag_start = find(text_before_cursor, "#", 1, true) local ref_start = find(text_before_cursor, ref_trigger_pattern[link_style], 1, true) + local tag_start = find(text_before_cursor, "#", 1, true) + local heading_start = find(text_before_cursor, heading_trigger_pattern, 1, true) - if ref_start then + if heading_start then + local partial = sub(text_before_cursor, heading_start + #heading_trigger_pattern) + -- if #partial >= min_chars then + -- handle_heading(client, partial, ref_start - 1, char_num, line_num, handler) + -- end + elseif ref_start then local partial = sub(text_before_cursor, ref_start + #ref_trigger_pattern[link_style]) if #partial >= min_chars then handle_ref(client, partial, ref_start - 1, char_num, line_num, handler) @@ -131,5 +231,6 @@ return function(client, params, handler, _) if #partial >= min_chars then handle_tag(client, partial, handler) end + else end end From e0944a59d5a8cf44b8d017cf93a28fddd9c7a44c Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Sat, 26 Apr 2025 13:55:19 +0800 Subject: [PATCH 17/22] feat: create note item --- lua/obsidian/client.lua | 1 + lua/obsidian/init.lua | 17 +++ .../lsp/handlers/commands/createNote.lua | 6 + lua/obsidian/lsp/handlers/completion.lua | 107 ++++++++++++------ lua/obsidian/lsp/handlers/execute_command.lua | 3 +- lua/obsidian/lsp/handlers/initialize.lua | 1 + 6 files changed, 100 insertions(+), 35 deletions(-) create mode 100644 lua/obsidian/lsp/handlers/commands/createNote.lua diff --git a/lua/obsidian/client.lua b/lua/obsidian/client.lua index fb508168..0ce660e6 100644 --- a/lua/obsidian/client.lua +++ b/lua/obsidian/client.lua @@ -2150,6 +2150,7 @@ Client.lsp_start = function(self) handlers[method](self, params, handler, _) end, notify = function(method, params, handler, _) + print(method) handlers[method](self, params, handler, _) end, is_closing = function() end, diff --git a/lua/obsidian/init.lua b/lua/obsidian/init.lua index 5e4be848..bc3db7a4 100644 --- a/lua/obsidian/init.lua +++ b/lua/obsidian/init.lua @@ -150,6 +150,23 @@ obsidian.setup = function(opts) local client_id = client:lsp_start() + vim.keymap.set("n", "ii", function() + local lsp_client = assert(vim.lsp.get_client_by_id(client_id)) + + vim.ui.input({}, function(input) + if not input then + return + end + lsp_client:exec_cmd({ + arguments = { + input, + }, + title = "create note", + command = "createNote", + }, { bufnr = ev.buf }) + end, { buffer = ev.buf }) + end) + -- place holders vim.keymap.set("n", "cH", function() local lsp_client = assert(vim.lsp.get_client_by_id(client_id)) diff --git a/lua/obsidian/lsp/handlers/commands/createNote.lua b/lua/obsidian/lsp/handlers/commands/createNote.lua new file mode 100644 index 00000000..d638ec67 --- /dev/null +++ b/lua/obsidian/lsp/handlers/commands/createNote.lua @@ -0,0 +1,6 @@ +---@param client obsidian.Client +return function(client, params) + local name = params.arguments[1] + print("creating note " .. name) + return client:create_note { title = name } +end diff --git a/lua/obsidian/lsp/handlers/completion.lua b/lua/obsidian/lsp/handlers/completion.lua index 5bb23845..2320e2d7 100644 --- a/lua/obsidian/lsp/handlers/completion.lua +++ b/lua/obsidian/lsp/handlers/completion.lua @@ -1,5 +1,4 @@ -- TODO: completion for anchor, blocks --- TODO: create item -- TODO: memoize? local ref_trigger_pattern = { @@ -11,6 +10,7 @@ local util = require "obsidian.util" local find, sub, lower = string.find, string.sub, string.lower +-- TODO: local function insert_snippet_marker(text, style) if style == "markdown" then local pos = text:find "]" @@ -19,33 +19,6 @@ local function insert_snippet_marker(text, style) end end ----@param note obsidian.Note ----@param insert_text string ----@param insert_start integer ----@param insert_end integer ----@param line_num integer ----@return lsp.CompletionItem -local function calc_ref_item(note, insert_text, insert_start, insert_end, line_num, style) - return { - kind = 17, - label = note.title, - filterText = note.title, - insertTextFormat = 2, -- is snippet - textEdit = { - range = { - start = { line = line_num, character = insert_start }, - ["end"] = { line = line_num, character = insert_end }, - }, - newText = insert_snippet_marker(insert_text, style), - }, - labelDetails = { description = "Obsidian" }, - data = { - file = note.path.filename, - kind = "ref", - }, - } -end - local state = { ---@type obsidian.Note current_note = nil, @@ -76,6 +49,60 @@ local function collect_matching_anchors(note, anchor_link) return matching_anchors end +-- A more generic pure function, don't require label to exist +local function format_link(label, format_func) + local path = util.urlencode(label) .. ".md" + local opts = { label = label, path = path } + return format_func(opts) +end + +---@param label string +---@param path string +---@param new_text string +---@param range lsp.Range +---@return lsp.CompletionItem +local function gen_ref_item(label, path, new_text, range, style, is_snippet) + return { + kind = 17, + label = label, + filterText = label, + insertTextFormat = 2, -- is snippet TODO: extract to config option + textEdit = { + range = range, + newText = insert_snippet_marker(new_text, style), + }, + labelDetails = { description = "Obsidian" }, + data = { + file = path, + kind = "ref", + }, + } +end + +---@param label string +---@param range lsp.Range +---@param format_func function +---@return lsp.CompletionItem +local function gen_create_item(label, range, format_func) + return { + kind = 17, + label = label .. " (create)", + filterText = label, + textEdit = { + range = range, + newText = format_link(label, format_func), + }, + labelDetails = { description = "Obsidian" }, + command = { -- runs after accept + command = "createNote", + arguments = { label }, + }, + data = { + kind = "ref_create", -- TODO: resolve to a tooltip window + }, + } +end + ---@client obsidian.Client local function handle_ref(client, partial, ref_start, cursor_col, line_num, handler) ---@type string|? @@ -85,23 +112,35 @@ local function handle_ref(client, partial, ref_start, cursor_col, line_num, hand ---@type string|? local anchor_link partial, anchor_link = util.strip_anchor_links(partial) - print(partial) + local style = client.opts.preferred_link_style + + local range = { + start = { line = line_num, character = ref_start }, + ["end"] = { line = line_num, character = cursor_col }, -- if auto parired + } + + local format_func + if style == "markdown" then + format_func = client.opts.markdown_link_func + else + format_func = client.opts.wiki_link_func + end - local items = {} if not anchor_link then client:find_notes_async( partial, vim.schedule_wrap(function(notes) - for _, note in ipairs(notes) do + local items = {} + for _, note in ipairs(notes or {}) do local title = note.title local pattern = vim.pesc(lower(partial)) if title and find(lower(title), pattern) then local link_text = client:format_link(note) - local style = client.opts.preferred_link_style - items[#items + 1] = calc_ref_item(note, link_text, ref_start, cursor_col, line_num, style) + items[#items + 1] = gen_ref_item(note.title, note.path.filename, link_text, range, style) end - handler(nil, { items = items }) end + items[#items + 1] = gen_create_item(partial, range, format_func) + handler(nil, { items = items }) end) ) else diff --git a/lua/obsidian/lsp/handlers/execute_command.lua b/lua/obsidian/lsp/handlers/execute_command.lua index 02061e2a..13c703f0 100644 --- a/lua/obsidian/lsp/handlers/execute_command.lua +++ b/lua/obsidian/lsp/handlers/execute_command.lua @@ -1,5 +1,6 @@ ---@param client obsidian.Client ---@param params table return function(client, params) - return require "obsidian.lsp.handlers.commands.toggleCheckbox"(client, params) + -- return require "obsidian.lsp.handlers.commands.toggleCheckbox"(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 64d45241..cf40a30c 100644 --- a/lua/obsidian/lsp/handlers/initialize.lua +++ b/lua/obsidian/lsp/handlers/initialize.lua @@ -39,6 +39,7 @@ local initializeResult = { executeCommandProvider = { commands = { "toggleCheckbox", + "createNote", }, }, completionProvider = completion_options, From ed038febc69fc435398bc0908b5a8261e6ff5ce1 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Fri, 2 May 2025 13:04:47 +0800 Subject: [PATCH 18/22] fix: start method takes a buf --- lua/obsidian/client.lua | 11 +++++++++-- lua/obsidian/init.lua | 6 +----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lua/obsidian/client.lua b/lua/obsidian/client.lua index 0ce660e6..6a5aef14 100644 --- a/lua/obsidian/client.lua +++ b/lua/obsidian/client.lua @@ -2128,7 +2128,7 @@ end --- Start the lsp client --- ---@return integer -Client.lsp_start = function(self) +Client.lsp_start = function(self, buf) local handlers = require "obsidian.lsp.handlers" local has_blink, blink = pcall(require, "blink.cmp") local has_cmp, cmp_lsp = pcall(require, "cmp_nvim_lsp") @@ -2141,6 +2141,7 @@ Client.lsp_start = function(self) else capabilities = vim.lsp.protocol.make_client_capabilities() end + local client_id = vim.lsp.start { name = "obsidian-ls", capabilities = capabilities, @@ -2150,7 +2151,6 @@ Client.lsp_start = function(self) handlers[method](self, params, handler, _) end, notify = function(method, params, handler, _) - print(method) handlers[method](self, params, handler, _) end, is_closing = function() end, @@ -2162,6 +2162,13 @@ Client.lsp_start = function(self) } assert(client_id, "failed to start obsidian_ls") + if not (has_blink or has_cmp) then + vim.lsp.completion.enable(true, client_id, buf, { autotrigger = true }) + vim.bo[buf].omnifunc = "v:lua.vim.lsp.omnifunc" + vim.bo[buf].completeopt = "menu,menuone,noselect" + vim.bo[buf].iskeyword = "@,48-57,192-255" -- HACK: so that completion for note names with `-` in it works in native completion + end + return client_id end diff --git a/lua/obsidian/init.lua b/lua/obsidian/init.lua index bc3db7a4..960ed76c 100644 --- a/lua/obsidian/init.lua +++ b/lua/obsidian/init.lua @@ -144,11 +144,7 @@ obsidian.setup = function(opts) vim.keymap.set("n", mapping_keys, mapping_config.action, mapping_config.opts) end - vim.bo[ev.buf].omnifunc = "v:lua.vim.lsp.omnifunc" - vim.bo[ev.buf].completeopt = "menu,menuone,noselect" - vim.bo[ev.buf].iskeyword = "@,48-57,192-255" -- HACK: so that completion for note names with `-` in it works in native completion - - local client_id = client:lsp_start() + local client_id = client:lsp_start(ev.buf) vim.keymap.set("n", "ii", function() local lsp_client = assert(vim.lsp.get_client_by_id(client_id)) From e8ae8a5d214cd5d3a03360707bc7a9c7b487d55f Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Tue, 6 May 2025 22:53:56 +0800 Subject: [PATCH 19/22] feat: initial try at implementing rename --- lua/obsidian/lsp/handlers/rename.lua | 144 ++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 3 deletions(-) diff --git a/lua/obsidian/lsp/handlers/rename.lua b/lua/obsidian/lsp/handlers/rename.lua index 537d718b..8112a16c 100644 --- a/lua/obsidian/lsp/handlers/rename.lua +++ b/lua/obsidian/lsp/handlers/rename.lua @@ -1,8 +1,146 @@ -- TODO: move to the idea of textEdits ----@param obsidian_client obsidian.Client +local lsp = vim.lsp +local ms = lsp.protocol.Methods + +---@type lsp.WorkspaceEdit +local edits = { + documentChanges = { + { + kind = "rename", + oldUri = vim.uri_from_bufnr(vim.api.nvim_get_current_buf()), + newUri = vim.uri_from_fname "/home/n451/test3.lua", + }, + }, +} + +-- lsp.util.apply_workspace_edit(edits, "utf-8") + +-- local function rename() +-- lsp.buf_request(0, ms.workspace_didRenameFiles, params, function(...) +-- vim.print(...) +-- end) +-- end + +-- Search notes on disk for any references to `cur_note_id`. +-- We look for the following forms of references: +-- * '[[cur_note_id]]' +-- * '[[cur_note_id|ALIAS]]' +-- * '[[cur_note_id\|ALIAS]]' (a wiki link within a table) +-- * '[ALIAS](cur_note_id)' +-- And all of the above with relative paths (from the vault root) to the note instead of just the note ID, +-- with and without the ".md" suffix. +-- Another possible form is [[ALIAS]], but we don't change the note's aliases when renaming +-- so those links will still be valid. +---@param ref_link string +---@return string[] +local function get_ref_forms(ref_link) + return { + "[[" .. ref_link .. "]]", + "[[" .. ref_link .. "|", + "[[" .. ref_link .. "\\|", + "[[" .. ref_link .. "#", + "](" .. ref_link .. ")", + "](" .. ref_link .. "#", + } +end + +local function rename_file(old_uri, new_uri) + ---@type lsp.WorkspaceEdit + local edit = { + documentChanges = { + { + kind = "rename", + oldUri = old_uri, + newUri = new_uri, + }, + }, + } + + vim.lsp.util.apply_workspace_edit(edit, "utf-8") +end + +local Path = require "obsidian.path" +local Note = require "obsidian.note" +local search = require "obsidian.search" + +---@param client obsidian.Client +---@param params table +local function rename_current_note(client, params) + local new_note_id = params.newName + local uri = params.textDocument.uri + local current_file = vim.uri_to_fname(uri) + local dirname = vim.fs.dirname(current_file) + + local new_path = vim.fs.joinpath(dirname, new_note_id) .. ".md" + local new_note_path = Path.new(new_path) + + local cur_note_bufnr = vim.uri_to_bufnr(uri) + local cur_note_path = Path.buffer(cur_note_bufnr) + local cur_note = Note.from_file(cur_note_path) + local cur_note_id = tostring(cur_note.id) + + local cur_note_rel_path = tostring(client:vault_relative_path(cur_note_path, { strict = true })) + local new_note_rel_path = tostring(client:vault_relative_path(new_note_path, { strict = true })) + + local pats = { + "[[%s]]", -- wiki + "[[%s|", -- wiki with display + "[[%s\\|", -- ? + "[[%s#", -- wiki with heading + "](%s)", -- markdown + "](%s#", -- markdown with heading + } + + local replace_lookup = {} + + for _, pat in ipairs(pats) do + replace_lookup[pat:format(cur_note_id)] = pat:format(new_note_id) + replace_lookup[pat:format(cur_note_rel_path)] = pat:format(new_note_rel_path) + replace_lookup[pat:format(cur_note_rel_path:sub(1, -4))] = pat:format(new_note_rel_path:sub(1, -4)) + end + + local reference_forms = vim.tbl_keys(replace_lookup) + + -- search.search_async( + -- client.dir, + -- reference_forms, + -- search.SearchOpts.from_tbl { fixed_strings = true, max_count_per_file = 1 }, + -- function(match) + -- local file = match.path.text + -- local line = match.line_number + -- local start, _end = match.submatches[1].start, match.submatches[1]["end"] + -- local matched = match.submatches[1].match.text + -- + -- handler(nil, { + -- changes = { + -- [vim.uri_from_fname(file)] = { + -- range = { + -- start = { line = line, character = start }, + -- ["end"] = { line = line, character = _end }, + -- }, + -- newText = replace_lookup[matched], + -- }, + -- }, + -- }) + -- end, + -- function(_) + -- -- all_tasks_submitted = true + -- end + -- ) + + rename_file(uri, vim.uri_from_fname(new_path)) +end + +local function rename_note_at_cursor(params) end + +---@param client obsidian.Client ---@param params table ---@param handler function -return function(obsidian_client, params, handler, _) - require "obsidian.commands.rename"(obsidian_client, { args = params.newName }) +return function(client, params, handler, _) + local position = params.position + + rename_current_note(client, params) + + -- require "obsidian.commands.rename"(obsidian_client, { args = params.newName }) end From bbc3895073950fbdf7ec3e2f2655a23a8ced5b45 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Tue, 6 May 2025 23:36:27 +0800 Subject: [PATCH 20/22] fix: cleaned up rename logic, ref replace working --- lua/obsidian/lsp/handlers/completion.lua | 3 +- lua/obsidian/lsp/handlers/rename.lua | 152 ++++++++++------------- 2 files changed, 66 insertions(+), 89 deletions(-) diff --git a/lua/obsidian/lsp/handlers/completion.lua b/lua/obsidian/lsp/handlers/completion.lua index 2320e2d7..c85e2eef 100644 --- a/lua/obsidian/lsp/handlers/completion.lua +++ b/lua/obsidian/lsp/handlers/completion.lua @@ -69,7 +69,8 @@ local function gen_ref_item(label, path, new_text, range, style, is_snippet) insertTextFormat = 2, -- is snippet TODO: extract to config option textEdit = { range = range, - newText = insert_snippet_marker(new_text, style), + newText = new_text, + -- insert_snippet_marker(new_text, style), }, labelDetails = { description = "Obsidian" }, data = { diff --git a/lua/obsidian/lsp/handlers/rename.lua b/lua/obsidian/lsp/handlers/rename.lua index 8112a16c..2d4b75fe 100644 --- a/lua/obsidian/lsp/handlers/rename.lua +++ b/lua/obsidian/lsp/handlers/rename.lua @@ -1,50 +1,10 @@ --- TODO: move to the idea of textEdits - local lsp = vim.lsp -local ms = lsp.protocol.Methods - ----@type lsp.WorkspaceEdit -local edits = { - documentChanges = { - { - kind = "rename", - oldUri = vim.uri_from_bufnr(vim.api.nvim_get_current_buf()), - newUri = vim.uri_from_fname "/home/n451/test3.lua", - }, - }, -} - --- lsp.util.apply_workspace_edit(edits, "utf-8") - --- local function rename() --- lsp.buf_request(0, ms.workspace_didRenameFiles, params, function(...) --- vim.print(...) --- end) --- end - --- Search notes on disk for any references to `cur_note_id`. --- We look for the following forms of references: --- * '[[cur_note_id]]' --- * '[[cur_note_id|ALIAS]]' --- * '[[cur_note_id\|ALIAS]]' (a wiki link within a table) --- * '[ALIAS](cur_note_id)' --- And all of the above with relative paths (from the vault root) to the note instead of just the note ID, --- with and without the ".md" suffix. --- Another possible form is [[ALIAS]], but we don't change the note's aliases when renaming --- so those links will still be valid. ----@param ref_link string ----@return string[] -local function get_ref_forms(ref_link) - return { - "[[" .. ref_link .. "]]", - "[[" .. ref_link .. "|", - "[[" .. ref_link .. "\\|", - "[[" .. ref_link .. "#", - "](" .. ref_link .. ")", - "](" .. ref_link .. "#", - } -end +local Path = require "obsidian.path" +local Note = require "obsidian.note" +local search = require "obsidian.search" +---@param old_uri string +---@param new_uri string local function rename_file(old_uri, new_uri) ---@type lsp.WorkspaceEdit local edit = { @@ -57,12 +17,27 @@ local function rename_file(old_uri, new_uri) }, } - vim.lsp.util.apply_workspace_edit(edit, "utf-8") + lsp.util.apply_workspace_edit(edit, "utf-8") end -local Path = require "obsidian.path" -local Note = require "obsidian.note" -local search = require "obsidian.search" +-- Search notes on disk for any references to `cur_note_id`. +-- We look for the following forms of references: +-- * '[[cur_note_id]]' +-- * '[[cur_note_id|ALIAS]]' +-- * '[[cur_note_id\|ALIAS]]' (a wiki link within a table) +-- * '[ALIAS](cur_note_id)' +-- And all of the above with relative paths (from the vault root) to the note instead of just the note ID, +-- with and without the ".md" suffix. +-- Another possible form is [[ALIAS]], but we don't change the note's aliases when renaming +-- so those links will still be valid. +local ref_patterns = { + "[[%s]]", -- wiki + "[[%s|", -- wiki with alias + "[[%s\\|", -- wiki link within a table + "[[%s#", -- wiki with heading + "](%s)", -- markdown + "](%s#", -- markdown with heading +} ---@param client obsidian.Client ---@param params table @@ -83,18 +58,9 @@ local function rename_current_note(client, params) local cur_note_rel_path = tostring(client:vault_relative_path(cur_note_path, { strict = true })) local new_note_rel_path = tostring(client:vault_relative_path(new_note_path, { strict = true })) - local pats = { - "[[%s]]", -- wiki - "[[%s|", -- wiki with display - "[[%s\\|", -- ? - "[[%s#", -- wiki with heading - "](%s)", -- markdown - "](%s#", -- markdown with heading - } - local replace_lookup = {} - for _, pat in ipairs(pats) do + for _, pat in ipairs(ref_patterns) do replace_lookup[pat:format(cur_note_id)] = pat:format(new_note_id) replace_lookup[pat:format(cur_note_rel_path)] = pat:format(new_note_rel_path) replace_lookup[pat:format(cur_note_rel_path:sub(1, -4))] = pat:format(new_note_rel_path:sub(1, -4)) @@ -102,44 +68,54 @@ local function rename_current_note(client, params) local reference_forms = vim.tbl_keys(replace_lookup) - -- search.search_async( - -- client.dir, - -- reference_forms, - -- search.SearchOpts.from_tbl { fixed_strings = true, max_count_per_file = 1 }, - -- function(match) - -- local file = match.path.text - -- local line = match.line_number - -- local start, _end = match.submatches[1].start, match.submatches[1]["end"] - -- local matched = match.submatches[1].match.text - -- - -- handler(nil, { - -- changes = { - -- [vim.uri_from_fname(file)] = { - -- range = { - -- start = { line = line, character = start }, - -- ["end"] = { line = line, character = _end }, - -- }, - -- newText = replace_lookup[matched], - -- }, - -- }, - -- }) - -- end, - -- function(_) - -- -- all_tasks_submitted = true - -- end - -- ) - + search.search_async( + client.dir, + reference_forms, + search.SearchOpts.from_tbl { fixed_strings = true, max_count_per_file = 1 }, + vim.schedule_wrap(function(match) + local file = match.path.text + local line = match.line_number - 1 + local start, _end = match.submatches[1].start, match.submatches[1]["end"] + local matched = match.submatches[1].match.text + local edit = { + documentChanges = { + { + textDocument = { + uri = vim.uri_from_fname(file), + }, + edits = { + { + range = { + start = { line = line, character = start }, + ["end"] = { line = line, character = _end }, + }, + newText = replace_lookup[matched], + }, + }, + }, + }, + } + lsp.util.apply_workspace_edit(edit, "utf-8") + end), + function(_) + -- TODO: conclude the rename + -- all_tasks_submitted = true + end + ) rename_file(uri, vim.uri_from_fname(new_path)) + + local note = client:current_note() + note.id = new_note_id end local function rename_note_at_cursor(params) end ---@param client obsidian.Client ---@param params table ----@param handler function -return function(client, params, handler, _) +return function(client, params, _, _) local position = params.position + -- TODO: check if cursor on link rename_current_note(client, params) -- require "obsidian.commands.rename"(obsidian_client, { args = params.newName }) From fd4730a4a39b994170c4bf0aa25d3145b397b6c2 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Sat, 10 May 2025 00:59:54 +0800 Subject: [PATCH 21/22] feat: code action for all sub-commands --- lua/obsidian/init.lua | 23 ---------- lua/obsidian/lsp/code_action.lua | 6 --- lua/obsidian/lsp/config.lua | 18 +++++++- lua/obsidian/lsp/handlers.lua | 1 + .../lsp/handlers/commands/createNote.lua | 6 --- .../lsp/handlers/commands/toggleCheckbox.lua | 46 ------------------- lua/obsidian/lsp/handlers/completion.lua | 2 +- lua/obsidian/lsp/handlers/execute_command.lua | 10 +++- lua/obsidian/lsp/handlers/initialize.lua | 11 +++-- 9 files changed, 34 insertions(+), 89 deletions(-) delete mode 100644 lua/obsidian/lsp/code_action.lua delete mode 100644 lua/obsidian/lsp/handlers/commands/createNote.lua delete mode 100644 lua/obsidian/lsp/handlers/commands/toggleCheckbox.lua diff --git a/lua/obsidian/init.lua b/lua/obsidian/init.lua index 960ed76c..399cb023 100644 --- a/lua/obsidian/init.lua +++ b/lua/obsidian/init.lua @@ -146,29 +146,6 @@ obsidian.setup = function(opts) local client_id = client:lsp_start(ev.buf) - vim.keymap.set("n", "ii", function() - local lsp_client = assert(vim.lsp.get_client_by_id(client_id)) - - vim.ui.input({}, function(input) - if not input then - return - end - lsp_client:exec_cmd({ - arguments = { - input, - }, - title = "create note", - command = "createNote", - }, { bufnr = ev.buf }) - end, { buffer = ev.buf }) - end) - - -- place holders - vim.keymap.set("n", "cH", function() - local lsp_client = assert(vim.lsp.get_client_by_id(client_id)) - lsp_client:exec_cmd({ title = "toggle checkbox", command = "toggleCheckbox" }, { bufnr = ev.buf }) - end, { buffer = ev.buf }) - if not (pcall(require, "blink.cmp") or pcall(require, "cmp")) then vim.lsp.completion.enable(true, client_id, ev.buf, { autotrigger = true }) end diff --git a/lua/obsidian/lsp/code_action.lua b/lua/obsidian/lsp/code_action.lua deleted file mode 100644 index 07845adc..00000000 --- a/lua/obsidian/lsp/code_action.lua +++ /dev/null @@ -1,6 +0,0 @@ --- ObsidianExtractNote --- ObsidianQuickSwitch --- ObsidianNew --- ObsidianBacklinks --- ObsidianDailies --- ObsidianLinkNew diff --git a/lua/obsidian/lsp/config.lua b/lua/obsidian/lsp/config.lua index 36a9fb28..6c014e2d 100644 --- a/lua/obsidian/lsp/config.lua +++ b/lua/obsidian/lsp/config.lua @@ -1,6 +1,7 @@ -- TODO:eventaully move to config -return { +local defualt = { + actions = {}, complete = true, checkboxs = { ---@type "- [ ] " | "* [ ] " | "+ [ ] " | "1. [ ] " | "1) [ ] " @@ -22,3 +23,18 @@ return { }, -- option to only show first few links on hover, and completion doc } + +local cmds = require "obsidian.commands" + +-- TODO: make context aware +for _, cmd in ipairs(vim.tbl_keys(cmds.commands)) do + defualt.actions[cmd] = { + title = cmd, + command = cmd, + fn = function() + vim.cmd.Obsidian(cmd) + end, + } +end + +return defualt diff --git a/lua/obsidian/lsp/handlers.lua b/lua/obsidian/lsp/handlers.lua index c379422f..d4893ba9 100644 --- a/lua/obsidian/lsp/handlers.lua +++ b/lua/obsidian/lsp/handlers.lua @@ -14,6 +14,7 @@ return setmetatable({ [ms.textDocument_didChange] = require "obsidian.lsp.handlers.did_change", [ms.initialized] = require "obsidian.lsp.handlers.initialized", [ms.workspace_executeCommand] = require "obsidian.lsp.handlers.execute_command", + [ms.textDocument_codeAction] = require "obsidian.lsp.handlers.code_action", }, { __index = function(_, k) vim.notify("obsidian_ls does not support method " .. k .. " yet", 3) diff --git a/lua/obsidian/lsp/handlers/commands/createNote.lua b/lua/obsidian/lsp/handlers/commands/createNote.lua deleted file mode 100644 index d638ec67..00000000 --- a/lua/obsidian/lsp/handlers/commands/createNote.lua +++ /dev/null @@ -1,6 +0,0 @@ ----@param client obsidian.Client -return function(client, params) - local name = params.arguments[1] - print("creating note " .. name) - return client:create_note { title = name } -end diff --git a/lua/obsidian/lsp/handlers/commands/toggleCheckbox.lua b/lua/obsidian/lsp/handlers/commands/toggleCheckbox.lua deleted file mode 100644 index 5772550b..00000000 --- a/lua/obsidian/lsp/handlers/commands/toggleCheckbox.lua +++ /dev/null @@ -1,46 +0,0 @@ -local util = require "obsidian.util" -local config = require "obsidian.lsp.config" - -local gen_checkbox_edit = function(uri, newText, line, start) - local edit = { - range = { - start = { line = line, character = start or 0 }, - ["end"] = { line = line, character = (start or -1) + 1 }, - }, - newText = newText, - } - return { - changes = { - [uri] = { edit }, - }, - } -end - ----@param client obsidian.Client -local gen_checkbox_edits = function(client, buf) - local line_num = unpack(vim.api.nvim_win_get_cursor(0)) - 1 -- 0-indexed - local line = vim.api.nvim_buf_get_lines(buf, line_num, line_num + 1, false)[1] - local uri = vim.uri_from_bufnr(buf) - - local checkboxes = vim.tbl_keys(client.opts.ui.checkboxes) - - local defualt_checkbox_style = config.checkboxs.style - - if util.is_checkbox(line) then - for i, check_char in ipairs(checkboxes) do - local start = string.find(line, "%[" .. vim.pesc(check_char)) - if start then - i = i % #checkboxes - return gen_checkbox_edit(uri, checkboxes[i + 1], line_num, start) - end - end - else - return gen_checkbox_edit(uri, defualt_checkbox_style, line_num, nil) - end -end - -return function(client, params) - local buf = vim.api.nvim_get_current_buf() - local edits = gen_checkbox_edits(client, buf) - vim.lsp.util.apply_workspace_edit(edits, "utf-8") -end diff --git a/lua/obsidian/lsp/handlers/completion.lua b/lua/obsidian/lsp/handlers/completion.lua index c85e2eef..8bc01940 100644 --- a/lua/obsidian/lsp/handlers/completion.lua +++ b/lua/obsidian/lsp/handlers/completion.lua @@ -95,7 +95,7 @@ local function gen_create_item(label, range, format_func) }, labelDetails = { description = "Obsidian" }, command = { -- runs after accept - command = "createNote", + command = "create_note", arguments = { label }, }, data = { diff --git a/lua/obsidian/lsp/handlers/execute_command.lua b/lua/obsidian/lsp/handlers/execute_command.lua index 13c703f0..728e9ac2 100644 --- a/lua/obsidian/lsp/handlers/execute_command.lua +++ b/lua/obsidian/lsp/handlers/execute_command.lua @@ -1,6 +1,12 @@ +local Config = require "obsidian.lsp.config" + ---@param client obsidian.Client ---@param params table return function(client, params) - -- return require "obsidian.lsp.handlers.commands.toggleCheckbox"(client, params) - return require "obsidian.lsp.handlers.commands.createNote"(client, params) + local cmd = params.command + + Config.actions[cmd].fn() + + -- 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 cf40a30c..b80badcd 100644 --- a/lua/obsidian/lsp/handlers/initialize.lua +++ b/lua/obsidian/lsp/handlers/initialize.lua @@ -1,4 +1,4 @@ -local config = require "obsidian.lsp.config" +local Config = require "obsidian.lsp.config" local completion_options @@ -8,7 +8,7 @@ for i = 32, 126 do table.insert(chars, string.char(i)) end -if config.complete then +if Config.complete then completion_options = { triggerCharacters = chars, resolveProvider = true, @@ -22,6 +22,7 @@ end local initializeResult = { capabilities = { + codeActionProvider = true, hoverProvider = true, definitionProvider = true, implementationProvider = true, @@ -54,9 +55,11 @@ local initializeResult = { }, } ----@param obsidian_client obsidian.Client +---@param client obsidian.Client ---@param params table ---@param handler function -return function(obsidian_client, params, handler, _) +return function(client, params, handler, _) + vim.list_extend(initializeResult.capabilities.executeCommandProvider.commands, vim.tbl_keys(Config.actions)) + return handler(nil, initializeResult, params.context) end From 763dca7076df216641e88efe42df299616cd6184 Mon Sep 17 00:00:00 2001 From: "zizhou teng (n451)" <2020200706@ruc.edu.cn> Date: Tue, 13 May 2025 22:16:15 +0800 Subject: [PATCH 22/22] fix: cleaning completion logic --- lua/obsidian/lsp/handlers/completion.lua | 130 +++++++++++++---------- 1 file changed, 76 insertions(+), 54 deletions(-) diff --git a/lua/obsidian/lsp/handlers/completion.lua b/lua/obsidian/lsp/handlers/completion.lua index 8bc01940..77c7559f 100644 --- a/lua/obsidian/lsp/handlers/completion.lua +++ b/lua/obsidian/lsp/handlers/completion.lua @@ -1,14 +1,18 @@ -- TODO: completion for anchor, blocks -- TODO: memoize? +local util = require "obsidian.util" +local find, sub, lower = string.find, string.sub, string.lower local ref_trigger_pattern = { wiki = "[[", markdown = "[", } -local util = require "obsidian.util" - -local find, sub, lower = string.find, string.sub, string.lower +-- TODO: remove +local state = { + ---@type obsidian.Note + current_note = nil, +} -- TODO: local function insert_snippet_marker(text, style) @@ -19,11 +23,6 @@ local function insert_snippet_marker(text, style) end end -local state = { - ---@type obsidian.Note - current_note = nil, -} - ---Collect matching anchor links. ---@param note obsidian.Note ---@param anchor_link string? @@ -104,12 +103,31 @@ local function gen_create_item(label, range, format_func) } end ----@client obsidian.Client +local handle_bare_links = function(client, partial, range, handler, format_func) + local pattern = vim.pesc(lower(partial)) + + local function match(title) + return title and find(lower(title), pattern) + end + + local style = client.opts.preferred_link_style + + client:find_notes_async(partial, function(notes) + local items = {} + for _, note in ipairs(notes or {}) do + local title = note.title + if match(title) then + local link_text = client:format_link(note) + items[#items + 1] = gen_ref_item(note.title, note.path.filename, link_text, range, style) + end + end + items[#items + 1] = gen_create_item(partial, range, format_func) + handler(nil, { items = items }) + end) +end + +---@param client obsidian.Client local function handle_ref(client, partial, ref_start, cursor_col, line_num, handler) - ---@type string|? - -- local block_link - -- cc.search, block_link = util.strip_block_links(cc.search) - -- ---@type string|? local anchor_link partial, anchor_link = util.strip_anchor_links(partial) @@ -128,22 +146,7 @@ local function handle_ref(client, partial, ref_start, cursor_col, line_num, hand end if not anchor_link then - client:find_notes_async( - partial, - vim.schedule_wrap(function(notes) - local items = {} - for _, note in ipairs(notes or {}) do - local title = note.title - local pattern = vim.pesc(lower(partial)) - if title and find(lower(title), pattern) then - local link_text = client:format_link(note) - items[#items + 1] = gen_ref_item(note.title, note.path.filename, link_text, range, style) - end - end - items[#items + 1] = gen_create_item(partial, range, format_func) - handler(nil, { items = items }) - end) - ) + handle_bare_links(client, partial, range, handler, format_func) else local Note = require "obsidian.note" -- state.current_note = state.current_note or client:find_notes(partial)[2] @@ -152,6 +155,7 @@ local function handle_ref(client, partial, ref_start, cursor_col, line_num, hand -- 1. typing partial note name, no completeed text after cursor, insert the full link -- 2. jumped to heading, only insert anchor -- TODO: need to do more textEdit to insert additional #title to path so that app supports? + local items = {} client:find_notes_async( partial, vim.schedule_wrap(function(notes) @@ -191,7 +195,6 @@ local function handle_ref(client, partial, ref_start, cursor_col, line_num, hand end end) ) - vim.print(state.current_note) end end @@ -233,6 +236,41 @@ local anchor_trigger_pattern = { local heading_trigger_pattern = "[##" +local CmpType = { + ref = 1, + tag = 2, + anchor = 3, +} + +---@param text string +---@param style obsidian.config.LinkStyle +---@param min_char integer +---@return integer? +---@return string? +---@return integer? +local function get_type(text, style, min_char) + local ref_start = find(text, ref_trigger_pattern[style], 1, true) + local tag_start = find(text, "#", 1, true) + -- local heading_start = find(text, heading_trigger_pattern, 1, true) + + if ref_start then + local partial = sub(text, ref_start + #ref_trigger_pattern[style]) + if #partial >= min_char then + return CmpType.ref, partial, ref_start + end + elseif tag_start then + local partial = sub(text, tag_start + 1) + if #partial >= min_char then + return CmpType.tag, partial, tag_start + end + -- elseif heading_start then + -- local partial = sub(text, heading_start + #heading_trigger_pattern) + -- if #partial >= min_char then + -- return CmpType.anchor, partial, heading_start + -- end + end +end + ---@param client obsidian.Client ---@param params table ---@param handler function @@ -246,31 +284,15 @@ return function(client, params, handler, _) local buf = vim.uri_to_bufnr(uri) local line_text = vim.api.nvim_buf_get_lines(buf, line_num, line_num + 1, false)[1] + local text_before = sub(line_text, 1, char_num) + local t, partial, start = get_type(text_before, link_style, min_chars) - -- print(util.strip_anchor_links(line_text)) - -- print(util.strip_block_links(line_text)) - -- - local text_before_cursor = sub(line_text, 1, char_num) - - local ref_start = find(text_before_cursor, ref_trigger_pattern[link_style], 1, true) - local tag_start = find(text_before_cursor, "#", 1, true) - local heading_start = find(text_before_cursor, heading_trigger_pattern, 1, true) - - if heading_start then - local partial = sub(text_before_cursor, heading_start + #heading_trigger_pattern) - -- if #partial >= min_chars then - -- handle_heading(client, partial, ref_start - 1, char_num, line_num, handler) - -- end - elseif ref_start then - local partial = sub(text_before_cursor, ref_start + #ref_trigger_pattern[link_style]) - if #partial >= min_chars then - handle_ref(client, partial, ref_start - 1, char_num, line_num, handler) - end - elseif tag_start then - local partial = sub(text_before_cursor, tag_start + 1) - if #partial >= min_chars then - handle_tag(client, partial, handler) - end + if t == CmpType.ref then + handle_ref(client, partial, start - 1, char_num, line_num, handler) + elseif t == CmpType.tag then + handle_tag(client, partial, handler) + elseif t == CmpType.anchor then else + return end end