Skip to content

Commit

Permalink
Add support for completing markdown links (#237)
Browse files Browse the repository at this point in the history
* Add support for completing markdown links

Closes #75.

* update readme
  • Loading branch information
epwalsh committed Nov 19, 2023
1 parent 52ba55d commit bfa9437
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 45 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Added `:ObsidianPasteImg` for pasting images from the clipboard into notes. See the `attachments` configuration option for customizing the behavior of this command. Inspired by [md-img-paste.vim](https://github.com/ferrine/md-img-paste.vim) and [clipboard-image.nvim](https://github.com/ekickx/clipboard-image.nvim).
- Added `completion.prepend_note_path` and `completion.use_path_only` options (mutually exclusive with each other and `completion.prepend_note_id`).
- Added support for completing traditional markdown links instead of just wiki links.

### Changed

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Built for people who love the concept of Obsidian -- a simple, markdown-based no

## Features

- ▶️ Autocompletion for note references via [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) (triggered by typing `[[`)
- ▶️ Autocompletion for note references via [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) (triggered by typing `[[` for wiki links or just `[` for markdown links)
- 🏃 Optional passthrough for `gf` to enable Obsidian links without interfering with existing functionality
- 📷 Paste images into notes
- 💅 Additional markdown syntax highlighting, concealing, and extmarks for references and check-boxes
Expand Down Expand Up @@ -211,6 +211,8 @@ This is a complete list of all of the options that can be passed to `require("ob
-- * "notes_subdir" - put new notes in the default notes subdirectory.
new_notes_location = "current_dir",

-- Control how wiki links are completed with these options:

-- Whether to add the note ID during completion.
-- E.g. "[[Foo" completes to "[[foo|Foo]]" assuming "foo" is the ID of the note.
-- Mutually exclusive with 'prepend_note_path' and 'use_path_only'.
Expand Down
52 changes: 34 additions & 18 deletions lua/cmp_obsidian.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ source.get_keyword_pattern = completion.get_keyword_pattern
source.complete = function(self, request, callback)
local opts = self:option(request)
local client = obsidian.new(opts)
local can_complete, search, insert_start, insert_end = completion.can_complete(request)
local can_complete, search, insert_start, insert_end, ref_type = completion.can_complete(request)

if can_complete and search ~= nil and #search >= opts.completion.min_chars then
local function search_callback(results)
Expand Down Expand Up @@ -55,30 +55,46 @@ source.complete = function(self, request, callback)

---@type string
local label
if client.opts.completion.use_path_only then
label = "[[" .. rel_path .. "]]"
elseif opts.completion.prepend_note_path then
label = "[[" .. rel_path
if option ~= tostring(note.id) then
label = label .. "|" .. option .. "]]"
if ref_type == completion.RefType.Wiki then
if client.opts.completion.use_path_only then
label = "[[" .. rel_path .. "]]"
elseif opts.completion.prepend_note_path then
label = "[[" .. rel_path
if option ~= tostring(note.id) then
label = label .. "|" .. option .. "]]"
else
label = label .. "]]"
end
elseif opts.completion.prepend_note_id then
label = "[[" .. tostring(note.id)
if option ~= tostring(note.id) then
label = label .. "|" .. option .. "]]"
else
label = label .. "]]"
end
else
label = label .. "]]"
end
elseif opts.completion.prepend_note_id then
label = "[[" .. tostring(note.id)
if option ~= tostring(note.id) then
label = label .. "|" .. option .. "]]"
else
label = label .. "]]"
echo.err("Invalid completion options", client.opts.log_level)
return
end
elseif ref_type == completion.RefType.Markdown then
label = "[" .. option .. "](" .. rel_path .. ".md)"
else
echo.err("Invalid completion options", client.opts.log_level)
return
error "not implemented"
end

if not labels_seen[label] then
---@type string
local sort_text
if ref_type == completion.RefType.Wiki then
sort_text = "[[" .. option
elseif ref_type == completion.RefType.Markdown then
sort_text = "[" .. option
else
error "not implemented"
end

table.insert(items, {
sortText = "[[" .. option,
sortText = sort_text,
label = label,
kind = 18,
textEdit = {
Expand Down
37 changes: 25 additions & 12 deletions lua/cmp_obsidian_new.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ source.get_keyword_pattern = completion.get_keyword_pattern
source.complete = function(self, request, callback)
local opts = self:option(request)
local client = obsidian.new(opts)
local can_complete, search, insert_start, insert_end = completion.can_complete(request)
local can_complete, search, insert_start, insert_end, ref_type = completion.can_complete(request)

---@type string|Path|?
local dir
Expand All @@ -45,24 +45,37 @@ source.complete = function(self, request, callback)
rel_path = string.sub(rel_path, 1, -4)
end

if opts.completion.use_path_only then
new_title = rel_path
elseif opts.completion.prepend_note_path then
new_title = rel_path .. "|" .. new_title
elseif opts.completion.prepend_note_id then
new_title = new_id .. "|" .. new_title
---@type string, string
local sort_text, label, new_text
if ref_type == completion.RefType.Wiki then
if opts.completion.use_path_only then
new_title = rel_path
elseif opts.completion.prepend_note_path then
new_title = rel_path .. "|" .. new_title
elseif opts.completion.prepend_note_id then
new_title = new_id .. "|" .. new_title
else
echo.err("Invalid completion options", client.opts.log_level)
return
end
sort_text = "[[" .. search
label = "Create: [[" .. new_title .. "]]"
new_text = "[[" .. new_title .. "]]"
elseif ref_type == completion.RefType.Markdown then
sort_text = "[" .. search
label = "Create: [" .. new_title .. "](" .. rel_path .. ".md)"
new_text = "[" .. new_title .. "](" .. rel_path .. ".md)"
else
echo.err("Invalid completion options", client.opts.log_level)
return
error "not implemented"
end

local items = {
{
sortText = "[[" .. search,
label = "Create: [[" .. new_title .. "]]",
sortText = sort_text,
label = label,
kind = 18,
textEdit = {
newText = "[[" .. new_title .. "]]",
newText = new_text,
range = {
start = {
line = request.context.cursor.row - 1,
Expand Down
39 changes: 25 additions & 14 deletions lua/obsidian/completion.lua
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
local completion = {}

---Backtrack through a string to find the first occurence of '[['.
---@enum obsidian.completion.RefType
completion.RefType = {
Wiki = 1,
Markdown = 2,
}

---Backtrack through a string to find the first occurrence of '[['.
---
---@param input string
---@return string|?
---@return string|?, string|?, obsidian.completion.RefType|?
completion._find_search_start = function(input)
for i = string.len(input), 1, -1 do
local substr = string.sub(input, i)
if vim.startswith(substr, "]") or vim.endswith(substr, "]") then
return nil
elseif vim.startswith(substr, "[[") then
return substr
return substr, string.sub(substr, 3)
elseif vim.startswith(substr, "[") and string.sub(substr, i - 1, i - 1) ~= "[" then
return substr, string.sub(substr, 2)
end
end
return nil
Expand All @@ -21,24 +29,27 @@ end
---items should be inserted.
---
---@return boolean
---@return string|?
---@return integer|?
---@return integer|?
---@return string|?, integer|?, integer|?, obsidian.completion.RefType|?
completion.can_complete = function(request)
local input = completion._find_search_start(request.context.cursor_before_line)
if input == nil then
return false, nil, nil, nil
local input, search = completion._find_search_start(request.context.cursor_before_line)
if input == nil or search == nil then
return false
elseif string.len(search) == 0 then
return false
end

local suffix = string.sub(request.context.cursor_after_line, 1, 2)
local search = string.sub(input, 3)

if string.len(search) > 0 and vim.startswith(input, "[[") then
if vim.startswith(input, "[[") then
local cursor_col = request.context.cursor.col
local insert_end_offset = suffix == "]]" and 1 or -1
return true, search, cursor_col - 1 - #input, cursor_col + insert_end_offset
return true, search, cursor_col - 1 - #input, cursor_col + insert_end_offset, completion.RefType.Wiki
elseif vim.startswith(input, "[") then
local cursor_col = request.context.cursor.col
local insert_end_offset = suffix == "]" and 1 or -1
return true, search, cursor_col - 1 - #input, cursor_col + insert_end_offset, completion.RefType.Markdown
else
return false, nil, nil, nil
return false
end
end

Expand All @@ -50,7 +61,7 @@ completion.get_keyword_pattern = function()
-- Note that this is a vim pattern, not a Lua pattern. See ':help pattern'.
-- The enclosing [=[ ... ]=] is just a way to mark the boundary of a
-- string in Lua.
return [=[\%(^\|[^\[]\)\zs\[\{2}[^\]]\+\]\{,2}]=]
return [=[\%(^\|[^\[]\)\zs\[\{,2}[^\]]\+\]\{,2}]=]
end

return completion

0 comments on commit bfa9437

Please sign in to comment.