From d2d01e83e073a76284bf7092eb6f75fc89a486fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=84min=20Baumeler?= Date: Sat, 21 Feb 2026 10:13:47 +0100 Subject: [PATCH] feat: denote directory support --- after/ftplugin/markdown.vim | 2 +- autoload/denote/completion.vim | 4 +- autoload/denote/loclist.vim | 7 ++- autoload/denote/meta.vim | 2 +- plugin/denote.vim | 104 +++++++++++++++++++++++++++++---- 5 files changed, 103 insertions(+), 16 deletions(-) diff --git a/after/ftplugin/markdown.vim b/after/ftplugin/markdown.vim index 2f8a3fa..26727f8 100644 --- a/after/ftplugin/markdown.vim +++ b/after/ftplugin/markdown.vim @@ -16,4 +16,4 @@ setlocal isfname+=: setlocal includeexpr=s:DenoteGotoFile() " Back references command {{{1 -command! DenoteBackReferences :call denote#loclist#references(denote#meta#noteIdFromFile(expand("%"))) +command! DenoteBackReferences :call denote#loclist#references(denote#meta#noteIdFromFile(expand("%:t"))) diff --git a/autoload/denote/completion.vim b/autoload/denote/completion.vim index 5053784..6766c0a 100644 --- a/autoload/denote/completion.vim +++ b/autoload/denote/completion.vim @@ -24,7 +24,7 @@ endfunction " Return completion items given by the base function s:suggestions(base) let prefix = a:base->matchstr('^denote:\zs.*$') - let flist = glob(prefix ? "*" .. prefix .. "*" : "*", 0, v:true) + let flist = glob(t:denote_directory .. prefix ? "*" .. prefix .. "*" : "*", 0, v:true) let res = [] for filename in flist let noteId = denote#meta#noteIdFromFile(filename) @@ -34,7 +34,7 @@ function s:suggestions(base) endif let noteTitle = noteTitle ?? '(no title)' let noteTags = denote#meta#noteTagsFromFile(filename) - let res = res->add({ + call add(res, { \ 'word' : 'denote:' .. noteId, \ 'abbr' : noteTitle, \ 'menu' : noteTags->join(', ') diff --git a/autoload/denote/loclist.vim b/autoload/denote/loclist.vim index 6e6172f..51ed4cd 100644 --- a/autoload/denote/loclist.vim +++ b/autoload/denote/loclist.vim @@ -51,8 +51,13 @@ endfunction " Load all references to the given note into the location list. function denote#loclist#references(noteId) + if !exists('t:denote_directory') + echohl WarningMsg + echom "Denote directory not specified, see |vim-denote|." + return + endif " Populate location list - execute "lvimgrep /\\/gj *" + silent! execute "lvimgrep /\\/gj " .. t:denote_directory .. "*" " Adjust location list: set title and specify display function let file=denote#meta#fileFromNoteId(a:noteId) let noteTitle=denote#meta#noteTitleFromFile(file) diff --git a/autoload/denote/meta.vim b/autoload/denote/meta.vim index 3c5b00d..6150528 100644 --- a/autoload/denote/meta.vim +++ b/autoload/denote/meta.vim @@ -10,7 +10,7 @@ function denote#meta#fileFromNoteId(noteId) " (A) First, we get all files that contain the note id as substring. " (B) Then we ensure that the note id is followed by another field or by the " file extension. - let files = glob("*" .. a:noteId .. "*", 0, v:true) + let files = glob(t:denote_directory .. "*" .. a:noteId .. "*", 0, v:true) \ ->filter('v:val =~ "' .. a:noteId .. '\\(==\\|--\\|__\\|\\.\\)"') \ ->filter('v:val =~ "^' .. a:noteId .. '\\|@@' .. a:noteId .. '"') return empty(files) ? v:false : files[0] diff --git a/plugin/denote.vim b/plugin/denote.vim index ffb1242..cbd355c 100644 --- a/plugin/denote.vim +++ b/plugin/denote.vim @@ -1,5 +1,15 @@ " Global configurations {{{1 +" List of denote directories: +if !exists('g:denote_directories') + let g:denote_directories=[] +endif +call map(g:denote_directories, {_, d -> fnamemodify(d, ':p')}) +" If only one directory has been specified, use that as denote directory +if ! empty(g:denote_directories) + let t:denote_directory = g:denote_directories[0] +endif + " Restrict basic operations to these files: if !exists('g:denote_note_file_extensions') let g:denote_note_file_extensions=['md', 'org', 'txt'] @@ -30,10 +40,15 @@ endif " Put all notes of the given tag to the location list. The tag argument is " mandatory. function s:DenoteNotesByTag(tag) + if !exists('t:denote_directory') + echohl WarningMsg + echom "Denote directory not specified, see |vim-denote|." + return + endif " Clear location list call denote#loclist#clear() " Find files - let files = glob("*_" .. a:tag .. "*", 0, v:true)->filter('v:val =~ "_' .. a:tag .. '\\(==\\|@@\\|__\\|_\\|\\.\\)"') + let files = glob(t:denote_directory .. "*_" .. a:tag .. "*", 0, v:true)->filter('v:val =~ "_' .. a:tag .. '\\(==\\|@@\\|__\\|_\\|\\.\\)"') " Populate location list let locTitle="Denote notes: " .. a:tag call setloclist(0, [], 'r', @@ -52,11 +67,16 @@ endfunction " Put all notes of the given tag to the location list. The search argument may be " empty. For improving search, white spaces are replaced by the * |wildcard|. function s:DenoteNotes(search) + if !exists('t:denote_directory') + echohl WarningMsg + echom "Denote directory not specified, see |vim-denote|." + return + endif let s = substitute(" " .. a:search .. " ", " ", "*", "g") " Clear location list call denote#loclist#clear() " Find files - let files = glob(s, 0, v:true) + let files = glob(t:denote_directory .. s, 0, v:true) " Populate location list let locTitle="Denote notes search:" .. a:search call setloclist(0, [], 'r', @@ -75,11 +95,16 @@ endfunction " This function implements the similar functionality of :vimgrep, but for " denote notes. function s:DenoteGrep(search) + if !exists('t:denote_directory') + echohl WarningMsg + echom "Denote directory not specified, see |vim-denote|." + return + endif " Clear location list call denote#loclist#clear() " Grep all greppable files - let fpat=map(copy(g:denote_note_file_extensions), {_, e -> "*." .. e})->join() - execute "lvimgrep " .. a:search .. " " .. fpat + let fpat=map(copy(g:denote_note_file_extensions), {_, e -> t:denote_directory .. "*." .. e})->join() + execute "silent! lvimgrep " .. a:search .. " " .. fpat " Adjust location list: set title and specify display function call setloclist(0, [], 'r', \ {'title': 'Denote grep: ' .. a:search, @@ -90,7 +115,7 @@ endfunction " This function is used to autocomplete the tags in user commands. function s:tagList(ArgLead, cmdLine, CursorPos) - let files=glob("*", 0, v:true) + let files=glob(t:denote_directory .. "*", 0, v:true) let tags=[] for f in files let tags=extend(tags, denote#meta#noteTagsFromFile(f)) @@ -101,22 +126,79 @@ endfunction " This creates a new denote entry with the given title and of the given " filetype. The title may be empty. function s:DenoteNew(title, ft=g:denote_new_ft) + if !exists('t:denote_directory') + echohl WarningMsg + echom "Denote directory not specified, see |vim-denote|." + return + endif let identifier=denote#meta#identifier_generate() let fn=identifier .. '--' .. a:title \ ->tolower() \ ->substitute('[^[:fname:]]\|/', '-', 'g') \ ->substitute('-\+', '-', 'g') \ ->trim('-') .. '.' .. a:ft - execute "edit " .. fn + execute "edit " .. t:denote_directory .. fn call setline(1, denote#frontmatter#new(a:ft, identifier, a:title)) endfunction +" Transform full path into canonical form WITH trailing '/' +function s:canonicalFullPath(path) + let parts = split(a:path, '/') + let result = [] + for part in parts + if part == '.' || part == '' + continue + elseif part == '..' + if len(result) > 0 + call remove(result, len(result)-1) + endif + else + call add(result, part) + endif + endfor + return empty(result) ? '/' : '/' .. join(result, '/') .. '/' +endfunction + +" Compute the relative path from start to target. Both arguments are given as +" full paths. +function s:relativePath(start, target) + let a = s:canonicalFullPath(a:start) + let b = s:canonicalFullPath(a:target) + " Simple cases first: both paths are the same, or the target is a subpath + " from the start. + if a == b[:strlen(a)-1] + return b[strlen(a):] + endif + " Now, we need to go back. If the following match fails, then we need to go + " back all the way + let l = matchstrpos(a .. b, '^\(/.*\)/\zs.*/\ze\1') + return l[1] == -1 + \ ? substitute(a[1:-2], '[^/]\+', '..', 'g') .. b + \ : substitute(l[0], '[^/]\+', '..', 'g') .. b[l[1]:] +endfunction + +" Complete all paths to the denote directories. This functions completes the +" paths to the configured directories, TODO: but also shows the directories +" accessible from the current position. +function s:denoteDirectoryList(ArgLead, CmdLine, CursorPos) + let prefix = fnamemodify(a:ArgLead ?? '/', ':p') + let res = [] + for dendir in g:denote_directories + if prefix == dendir[:strlen(prefix)-1] + call add(res, substitute(a:ArgLead .. '/' .. s:relativePath(prefix, dendir), '/\+', '/', 'g')) + endif + endfor + " TODO: Append all subdirectories (default directory completion) + return res->join("\n") +endfunction + " Public commands and key mappings {{{1 -command -nargs=* Denote :call DenoteNotes() -command -nargs=1 -complete=custom,tagList DenoteTag :call DenoteNotesByTag() -command -nargs=+ DenoteGrep :call DenoteGrep() -command -nargs=1 DenoteNew :call DenoteNew() +command -nargs=1 -complete=custom,denoteDirectoryList Denote let t:denote_directory = fnamemodify(, ':p') +command -nargs=* DenoteSearch call DenoteNotes() +command -nargs=1 -complete=custom,tagList DenoteTag call DenoteNotesByTag() +command -nargs=+ DenoteGrep call DenoteGrep() +command -nargs=1 DenoteNew call DenoteNew() " Useful key mappings -nnoremap DenoteList :Denote:lclose:lopen:resize 20 +nnoremap DenoteList :DenoteSearch:lclose:lopen:resize 20 nnoremap DenoteBackReferences :DenoteBackReferences:lclose:lopen:resize 20