Compare commits

...

33 Commits

Author SHA1 Message Date
270be048dc added key o for other files 2026-03-04 11:38:04 +01:00
e1c3ea09da feat: open non-note files using "open" 2026-03-04 11:16:59 +01:00
5df2b79938 handle variable-length front matter 2026-03-04 10:56:18 +01:00
320bc0b6c0 md: use of ft-local functions instead of script-local functions 2026-03-04 09:08:13 +01:00
5b7825d358 readme and doc: minor imprv 2026-03-03 23:12:28 +01:00
66e032ee38 cleaned 2026-03-03 23:03:34 +01:00
2270ab9069 libdenote added (not used yet) 2026-03-02 16:50:09 +01:00
c3ed08e42f note deletion with bang 2026-02-28 22:31:10 +01:00
040d6397cb documentation: updated to current version 2026-02-28 22:23:53 +01:00
0f0a86d1f9 visual keys 2026-02-28 15:24:22 +01:00
1afd45b91a feat: note deletion 2026-02-28 15:16:47 +01:00
dfc15def2b copy files to denote directory 2026-02-28 15:02:09 +01:00
4fb6e977ff fix: tag removal; keys: tag mod 2026-02-28 14:31:00 +01:00
5d631a5506 add and remove tags 2026-02-28 14:21:39 +01:00
55bbeecb0a rename notes/and files 2026-02-27 21:57:19 +01:00
553cf41c96 doc: mention q key 2026-02-26 14:23:21 +01:00
864b678b9e feat: reload key 2026-02-26 14:20:03 +01:00
7a82fe32db bugfix: use of funcref in settings 2026-02-26 13:44:44 +01:00
d6bc3cbb04 doc/help: adjusted to current version 2026-02-26 11:27:57 +01:00
2015c0e32c code: cleaned up 2026-02-25 16:11:35 +01:00
ba939661c7 removed old files 2026-02-25 15:47:34 +01:00
e8ff0daf16 fixed ft-specifics for new files 2026-02-25 15:43:43 +01:00
caf21ab060 refacotred and split 2026-02-25 15:32:05 +01:00
d4ad71543d removed todo: done 2026-02-23 13:26:11 +01:00
ac75a8c679 fixed denote directory support 2026-02-23 13:07:33 +01:00
d2d01e83e0 feat: denote directory support 2026-02-21 10:13:47 +01:00
6d8f8c720b doc: minor fix 2026-02-19 16:24:26 +01:00
24cadefafc doc: denote link completion 2026-02-19 16:19:18 +01:00
83ea4f2bcf feat: denote link completion 2026-02-19 15:56:08 +01:00
00af1c53b9 documentation: new notes 2026-02-18 17:24:20 +01:00
717f44425b functionality to add new notes 2026-02-18 17:08:24 +01:00
81c5d3f849 add txt files to g:denote_note_file_extension 2026-02-17 19:49:10 +01:00
1fd1e9aaa0 indent fixes 2026-02-17 18:21:44 +01:00
12 changed files with 1296 additions and 274 deletions

137
README.md
View File

@@ -4,8 +4,8 @@ a straightforward and handy file-naming scheme for all kinds of files. This,
e.g., allows altering the title of a note without breaking the web of links. e.g., allows altering the title of a note without breaking the web of links.
The present vim package reproduces some of the denote features for vim. The The present vim package reproduces some of the denote features for vim. The
implementation is not complete, and more features are expected. Note that this implementation is yet incomplete; more features are expected. Note that this is
is not the first attempt to handle denote notes within vim. not the first attempt to handle denote notes within vim.
[Conan](https://zansh.in/) developed a [bash [Conan](https://zansh.in/) developed a [bash
script](https://github.com/shuckster/denote-md) for denote and an accompanying script](https://github.com/shuckster/denote-md) for denote and an accompanying
[vim plugin](https://github.com/shuckster/vim-denote-md). Also [vim plugin](https://github.com/shuckster/vim-denote-md). Also
@@ -14,84 +14,107 @@ script](https://github.com/shuckster/denote-md) for denote and an accompanying
### Why? ### Why?
The present package aims at handling denote notes the _vim way._ For instance, The present package aims at handling denote notes the _vim way._ For instance,
both plugins do not bind the default key combination both plugins do not bind the default key combination
[|gf|](https://vimhelp.org/editing.txt.html#gf) to follow links. [`gf`](https://vimhelp.org/editing.txt.html#gf) to follow links.
The vim option The vim option
[|'includeexpr'|](https://vimhelp.org/options.txt.html#%27includeexpr%27), [`'includeexpr'`](https://vimhelp.org/options.txt.html#%27includeexpr%27),
however, exists precisely for solving the problem at hand: following custom-made links. however, exists precisely for solving the problem at hand: following custom-made links.
Also, the present package aims at remaining flexible. Also, the present package aims at remaining flexible. Again, both
Again, both plugins require the denote note identifiers to follow the rigid above-mentioned plugins require the denote note identifiers to follow the rigid
format `YYYYMMDDTHHMMSS`. Per denote manual, the identify may be [any format `YYYYMMDDTHHMMSS`. Per denote manual, however, the identify may be [any
string](https://protesilaos.com/emacs/denote#h:3048f558-7d84-45d6-9ef2-53055483e801) string](https://protesilaos.com/emacs/denote#h:3048f558-7d84-45d6-9ef2-53055483e801)
(free of field delimiters, of course). (free of field delimiters, of course). This package offers great flexibility in
configuration (see `:help denote-settings`).
### Installation ### Installation
You may use any of your favorite plugin managers, or, place a copy of this You may use any of your favorite plugin managers, or, manually place a copy of
package in `~/.vim/pack/tools/start`. Then, run `:helptags ALL` to regenerate this package in, e.g., `~/.vim/pack/tools/start`. For such a manual
the help files. This will allow you to get more help using `:help denote` or installation, you may want to run `:helptags
~/.vim/pack/tools/start/vim-denote/doc` to regenerate the help files. This
allows you to display the package documentation using `:help vim-denote` and
similar commands. similar commands.
### Usage ### Usage
For following links, simply move your cursor to the denote link, and press vim needs to know where your denote entries are stored. To do so, run the
`gf`. command `:DenoteDirectory! <path>` where `<path>` points to your denote
directory. You may also define the `g:denote_directories`[^1] variable in your
[vimrc](https://vimhelp.org/starting.txt.html#vimrc) as a list of paths to
denote directories. The benefit of doing so is that the `:DenoteDirectory`
command _(without bang)_ auto completes the paths to these directories.
Browsing and searching notes is implemented using the [location After specifying the denote directory, the following functionalities are
list](https://vimhelp.org/quickfix.txt.html#location-list). available.
So, after running any of the following commands, you may navigate your notes
using, e.g., `:lopen` to open the list, `:lnext` and `:lprev`, to move to the next or
previous entry (the list does not need to be opened for that), and `:lclose`
for closing the location list.
#### Commands _Following links:_ Simply place your cursor on a denote link and press `gf` or
This package defines the following user commands: similar keys.
- `:Denote [{keyword ..}]`: _Link completion:_ When editing a note, write any prefix of a denote link
This command places all denote entries in the location list. (`denote:<identifier>`) and invoke [Omni
You may supply any number of arguments to filter the notes by the given completion](https://vimhelp.org/insert.txt.html#compl-omni) by pressing
keywords. `<C-X><C-O>`. Even more so, if this completion is invoked after typing
`denote:<str>`, then the link is completed for denote entries that have `<str>`
as part of the title.
- `:DenoteTag {tag}`: _Browsing and searching notes:_ Browsing and searching is done using vim's
With this, all notes of a given tag are listed. [location list](https://vimhelp.org/quickfix.txt.html#location-list). The
The `{tag}` may be autocompleted by hitting the tab key. benefit of doing so is that commands like `:lnext`, `:lprev` etc. become
available.
- The command `:Denote [{keyword ..}]` lists all denote entries. The optional
argument filters these entries by the appearance of the keyword in the file
name.
- The command `:DenoteByTag {tag}` lists all denote entries of a given tag.
This command come with auto completion.
- On an open note, the command `DenoteBackReferences` lists all
references to that note.
- Finally, the command `:DenoteGrep /{pattern}/[g][j][f]` searches within the
denote entries for the given
[pattern](https://vimhelp.org/quickfix.txt.html#%3Avimgrep).
- `:DenoteGrep /{pattern}/[g][j][f]`: _Adding notes:_ Run `:DenoteNew {title}` to add a new note with the given
This command builds around title. You may also copy a file (any file) to your denote directory. To do so,
[:vimgrep](https://vimhelp.org/quickfix.txt.html#%3Avimgrep) and can be used to run `:DenoteCopy {file}`.
search for patterns within your notes.
- `:DenoteBackReferences`: _Managing entries:_ You can change the title of denote entries using
With this, you may populate the location list with all links to the currently `:DenoteSetTitle {newtitle}`. Also, you can add and remove tags using
opened note. `:DenoteTagAdd {tag}` and `:DenoteTagRm {tag}`, respectively. These latter two
commands offer auto completion for the tags. Also, they accept line ranges that
may be given via, e.g., visual selection. Finally, denote entries are deleted
using `:DenoteDelete`. This command again accepts line ranges.
#### Key mappings [^1]: Run `:help g:denote_directories` for more information.
This package also defines the following interface for key mappings, which
automatically open the location window:
- `<Plug>DenoteList` to list all denote notes, and ### Basic keys
- `<Plug>DenoteBackReferences` to list all back references. The denote location-list window comes with a set of keys:
- `q`: close the list
- `r`: reload the list
- `C`: rename the selected entry
- `+`: add a tag to the entry (also in visual mode)
- `-`: remove a tag from the entry (also in visual mode)
- `dd`: delete the selected entry (also in visual mode, using a single `d`)
So, you may want to configure your keys like this: ### Example setup
``` You get a possibly useful setup with a single default denote directory at
nnoremap <silent> <Leader>d <Plug>DenoteList; `~/Documents/notes/` by placing these lines in your vimrc file:
nnoremap <silent> <Leader>D <Plug>DenoteBackReferences; ```vim
``` au VimEnter * DenoteDirectory ~/Documents/notes/
nnoremap <silent> <Leader>d :Denote<CR>
You may also find the following keys favorable for navigation: nnoremap <silent> <Leader>D :DenoteBackReferences<CR>
```
nnoremap ]l :lnext<CR> nnoremap ]l :lnext<CR>
nnoremap [l :lprevious<CR> nnoremap [l :lprevious<CR>
``` ```
After starting vim (in any directory), you can display all denote entries by
using the `<Leader>d` key, move forwards and backwards in the list using the
`]l` and `[l` keys, and list all back references to an opened note with the
`<Leader>D` key (again, you can jump around the references using `[l` and
`]l`).
### Customization ### Customization
You can customize the behavior of this package using the following two global You can customize the behavior of this package using several
variables. The variable `g:denote_note_file_extensions` defaults to `['md', global variables. Customization is explained in the [help
'org']` and lists the file extensions in which `:DenoteGrep` will look for the file](doc/denote.txt), also accessible through `:help denote-settings`.
given pattern. The other variable `g:denote_loc_title_columns` defaults to `40`
and specifies the number of columns used to display the titles of your notes.
### Future features ### Future features
These features are planned:
- Note creation (and deletion?)
- Tag manipulation
- Title manipulation
- Signature handling? - Signature handling?
- Subdirectories
- Denote query links

View File

@@ -1,16 +1,87 @@
" Go to file command |gf| adjustments {{{1 " Run this plugin only if the denote package has been setup
" This resolves denote links in markdown and org files. The function has access if !exists('g:denote_directory')
" to the variable v:fname, which corresponds to the filename under the cursor. finish
function s:DenoteGotoFile() endif
return v:fname !~ "^denote:"
\ ? v:fname " ... and if this filetype is specified as denote-note file
\ : denote#meta#fileFromNoteId(v:fname[7:]) ?? v:fname if index(g:denote_note_file_extensions, expand('%:e')) == -1
finish
endif
" ... and if the file is in the denote directory
if g:denote_directory != expand('%:p:h')
finish
endif
" Load only once per buffer
if exists('b:loaded_denote_ftplugin_notes')
finish
endif
let b:loaded_denote_ftplugin_notes = 1
" Link completion
" This works by using the functions s:column(), s:suggestions(),
" s:complete_link(), and by setting 'omnifunc'.
" Find the column where the completion starts. This must be between 1 and
" col('.'). Denote links are of this form: `denote:<identifier>`.
function DenoteNoteCompleteLinkColumn()
" Get the substring from the start of the line until col('.')
let l:x = max([1, col('.')-2])
let l:l = getline('.')[:l:x]
" Take the shortest prefix of a denote link. This may be any of
" \<d$
" ..
" \<denote:$
" \<denote:\f$
" \<denote:\f\f$
" etc.
let l:res = l:l->matchstrpos('\<denote:\f*$')
if l:res[1] >= 0
return l:res[1]
endif
let l:res = l:l->matchstrpos('\<d\f*$')
if l:res[1] == -1
return -3
endif
return 'denote:' =~ '^' .. l:res[0] ? l:res[1] : -3
endfunction endfunction
" Return completion items given by the base
function DenoteNoteCompleteLinkSuggestions(base)
let l:prefix = a:base->matchstr('^denote:\zs.*$')
let l:flist = glob(g:denote_directory .. '/' .. (l:prefix ? '*' .. l:prefix .. '*' : '*'), 0, v:true)
let l:res = []
for filename in l:flist
let l:meta = libdenote#scheme_metadata(filename)
if l:meta.id == v:false || (l:meta.id !~ '^' .. l:prefix && l:meta.title !~ l:prefix)
continue
endif
let l:meta.title = l:meta.title ?? '(no title)'
call add(l:res, {
\ 'word' : 'denote:' .. l:meta.id,
\ 'abbr' : l:meta.title,
\ 'menu' : l:meta.tags->join(', ')
\ })
endfor
return l:res
endfunction
" Completion function for denote links
function DenoteNoteCompleteLink(findstart, base)
return a:findstart == 1 ? DenoteNoteCompleteLinkColumn() : DenoteNoteCompleteLinkSuggestions(a:base)
endfunction
setlocal omnifunc=DenoteNoteCompleteLink
" Denote links are of the form 'denote:<note id>'; we require the column. " Denote links are of the form 'denote:<note id>'; we require the column.
setlocal isfname+=: setlocal isfname+=:
" Set the function to resolve the filename under the cursor (see |gf|). " Set the function to resolve the filename under the cursor (see |gf|).
setlocal includeexpr=s:DenoteGotoFile() function DenoteNoteGotoFile()
return v:fname !~ '^denote:'
\ ? v:fname
\ : libdenote#scheme_find(g:denote_directory, v:fname[7:]) ?? v:fname
endfunction
setlocal includeexpr=DenoteNoteGotoFile()
" Back references command {{{1 " Back references command
command! DenoteBackReferences :call denote#loclist#references(denote#meta#noteIdFromFile(expand("%"))) let b:meta = libdenote#scheme_metadata(expand('%:t'))
exe 'command! -buffer DenoteBackReferences DenoteGrep /\<denote:' .. b:meta.id .. '\>/gj'

View File

@@ -1,8 +1,60 @@
" Disable spell checking and set up custom mappings. " Run this plugin only if the denote package has been setup
setlocal nospell if !exists('g:denote_directory')
if getwininfo(win_getid())[0].loclist == 0
finish finish
endif endif
" Load only once per buffer
if exists('b:loaded_denote_ftplugin_qf')
finish
endif
let b:loaded_denote_ftplugin_qf = 1
" This will be called for every location and quickfix, when data is loaded into
" the list. This does nothing for such lists that are note 'denote' lists.
let b:context = getloclist(0, {'context': 1})['context']
if type(b:context) != v:t_dict || !has_key(b:context, 'denote')
" Clear settings
set spell<
nmapclear <buffer>
finish
endif
setlocal nospell
nnoremap <buffer> q :lclose<CR> nnoremap <buffer> q :lclose<CR>
nnoremap <buffer> dd :lremove<CR>
" Reload capability
if has_key(b:context, 'gfun')
function DenoteLocListReload()
let curl = line('.')
let Gfun = b:context['gfun']
call denote#loclist#jumptowindow()
exe 'lclose'
call Gfun()
exe 'lwindow'
exe curl
endfunction
nnoremap <buffer> <silent> r :call DenoteLocListReload()<CR>
endif
" Denote-list specific configuration
if b:context['denote'] == 'list'
function OpenDenoteEntry()
let l:item = getloclist(0, {'items': 1})['items'][line('.')-1]
let l:bufnr = l:item['bufnr']
let l:filename = bufname(l:bufnr)
call system('open ' .. shellescape(l:filename))
endfunction
command! -nargs=1 -range -buffer DenoteSetTitle :call denote#notes#settitle(<line1>, <q-args>) | :normal r
command! -nargs=1 -range -buffer -complete=custom,denote#completion#tags DenoteTagAdd :call denote#notes#tagmod(<line1>, <line2>, <q-args>, v:true) | :normal r
command! -nargs=1 -range -buffer -complete=custom,denote#completion#tags DenoteTagRm :call denote#notes#tagmod(<line1>, <line2>, <q-args>, v:false) | :normal r
command! -range -buffer -bang DenoteDelete :call denote#notes#rm(<line1>, <line2>, <bang>0) | :normal r
nnoremap <buffer> C :DenoteSetTitle
nnoremap <buffer> + :DenoteTagAdd
nnoremap <buffer> - :DenoteTagRm
nnoremap <buffer> dd :DenoteDelete<CR>r
nnoremap <buffer> o :call OpenDenoteEntry()<CR>
xnoremap <buffer> + :DenoteTagAdd
xnoremap <buffer> - :DenoteTagRm
xnoremap <buffer> d :DenoteDelete<CR>
endif

1
after/ftplugin/text.vim Symbolic link
View File

@@ -0,0 +1 @@
markdown.vim

View File

@@ -0,0 +1,13 @@
" Public commands
function denote#commands#load()
if exists('g:denote_commands_loaded') && g:denote_commands_loaded
return
endif
let g:denote_commands_loaded = 1
" Register user commands
command -nargs=* Denote call denote#notes#list(<q-args>) | lcl | lopen
command -nargs=1 -complete=custom,denote#completion#tags DenoteByTag call denote#notes#bytag(<q-args>) | lcl | lopen
command -nargs=+ DenoteGrep call denote#notes#grep(<q-args>) | lcl | lopen
command -nargs=1 DenoteNew call denote#notes#new(<q-args>)
command -nargs=1 -complete=file DenoteCopy call denote#notes#copy(<q-args>)
endfunction

View File

@@ -0,0 +1,9 @@
" Completion function for denote tags
function denote#completion#tags(ArgLead, CmdLine, CursorPos)
let l:files=glob(g:denote_directory .. '/*', 0, v:true)
let l:tags=[]
for f in l:files
let l:tags=extend(l:tags, libdenote#scheme_metadata(f).tags)
endfor
return uniq(sort(l:tags))->join("\n")
endfunction

View File

@@ -5,10 +5,10 @@ endfunction
" Local helper function to retrieve and format the title. " Local helper function to retrieve and format the title.
function s:titleFromBuf(buf) function s:titleFromBuf(buf)
let name=denote#meta#noteTitleFromFile(bufname(a:buf)) let l:name = libdenote#scheme_metadata(bufname(a:buf)).title
return strchars(name, 1) <= g:denote_loc_title_columns return strchars(l:name, 1) <= g:denote_loc_title_columns
\ ? printf('%' .. g:denote_loc_title_columns .. 's', name) \ ? printf('%' .. g:denote_loc_title_columns .. 's', l:name)
\ : printf('%.' .. (g:denote_loc_title_columns - 1) .. 's', name) .. '…' \ : printf('%.' .. (g:denote_loc_title_columns - 1) .. 's', l:name) .. '…'
endfunction endfunction
" Local helper function to truncate text with match at the given column as a " Local helper function to truncate text with match at the given column as a
@@ -20,43 +20,85 @@ function s:formatText(text, col, width)
endfunction endfunction
" This modifies the location list for pretty display. " This modifies the location list for pretty display.
function denote#loclist#textReferences(info) function s:textReferences(info)
let items=getloclist(a:info.winid) let l:items=getloclist(a:info.winid)
let l=[] let l:l=[]
let width=winwidth(0) - g:denote_loc_title_columns - 19 let l:width=winwidth(0) - g:denote_loc_title_columns - 19
for idx in range(a:info.start_idx - 1, a:info.end_idx - 1) for idx in range(a:info.start_idx - 1, a:info.end_idx - 1)
let e=items[idx] let l:e=l:items[idx]
let name=s:titleFromBuf(e.bufnr) let l:name=s:titleFromBuf(l:e.bufnr)
let lnum=printf('%5d', e.lnum) let l:lnum=printf('%5d', l:e.lnum)
let col=printf('%3d', e.col) let l:col=printf('%3d', l:e.col)
call add(l, name .. call add(l:l, l:name ..
\ ' | ' .. lnum .. ' col ' .. col .. \ ' | ' .. l:lnum .. ' l:col ' .. l:col ..
\ ' | ' .. s:formatText(items[idx].text, col, width)) \ ' | ' .. s:formatText(l:items[idx].text, l:col, l:width))
endfor endfor
return l return l:l
endfunction endfunction
" This modifies the location list for pretty display. " This modifies the location list for pretty display.
function denote#loclist#textNoteList(info) function s:textNoteList(info)
let items=getloclist(a:info.winid) let l:items=getloclist(a:info.winid)
let l=[] let l:l=[]
for idx in range(a:info.start_idx - 1, a:info.end_idx - 1) for idx in range(a:info.start_idx - 1, a:info.end_idx - 1)
let e=items[idx] let l:e=l:items[idx]
let name=s:titleFromBuf(e.bufnr) let l:name=s:titleFromBuf(l:e.bufnr)
let ntags=denote#meta#noteTagsFromFile(bufname(e.bufnr))->join() let l:ntags = libdenote#scheme_metadata(bufname(l:e.bufnr)).tags->join()
call add(l, name .. ' | ' .. ntags) call add(l:l, l:name .. ' | ' .. l:ntags)
endfor endfor
return l return l:l
endfunction endfunction
" Load all references to the given note into the location list. " Re-populate location list with denote entries
function denote#loclist#references(noteId) function denote#loclist#fill(title, files, Gfun)
" Populate location list " Clear first
execute "lvimgrep /\\<denote:" .. a:noteId .. "\\>/gj *" call setloclist(0, [], ' ')
" Adjust location list: set title and specify display function " Set properties
let file=denote#meta#fileFromNoteId(a:noteId)
let noteTitle=denote#meta#noteTitleFromFile(file)
call setloclist(0, [], 'r', call setloclist(0, [], 'r',
\ {'title': 'References to ' .. noteTitle .. ' (' .. a:noteId .. ')', \ {'title': a:title,
\ 'quickfixtextfunc' : 'denote#loclist#textReferences'}) \ 'quickfixtextfunc' : 's:textNoteList',
\ 'context' : {'denote': 'list', 'gfun': a:Gfun}})
" Populate
let l:notes=[]
for f in a:files
call add(l:notes, {
\ 'filename' : f,
\ 'lnum' : 1
\ })
endfor
call setloclist(0, l:notes, 'a')
endfunction
" Specify location list as denote-grep list
function denote#loclist#setgrep(title, Gfun)
call setloclist(0, [], 'r',
\ {'title': a:title,
\ 'quickfixtextfunc' : 's:textReferences',
\ 'context' : {'denote': 'grep', 'gfun': a:Gfun}})
endfunction
" Reload location list
function denote#loclist#reload()
let l:context = getloclist(0, {'context': 1})['context']
if has_key(l:context, 'denote') && has_key(l:context, 'gfun')
call denote#loclist#jumptowindow()
exe 'lclose'
call l:context['gfun']()
exe 'lwindow'
endif
endfunction
" Jump to window this location list belongs to
function denote#loclist#jumptowindow()
let l:locprop = getloclist(0, {'filewinid': 0})
if type(l:locprop) != v:t_dict || !has_key(l:locprop, 'filewinid')
return
endif
let l:winid = l:locprop['filewinid']
if l:winid == 0
exe 'new'
exe 'wincmd K'
else
call win_gotoid(l:winid)
endif
endfunction endfunction

View File

@@ -1,36 +0,0 @@
" Return the filename of the note with id `noteId`. If the file is note found,
" then v:false is returned.
function denote#meta#fileFromNoteId(noteId)
" According to the file-naming scheme, the note id is prefixed with '@@'. If
" the note id appears at the beginning of the filename, then this prefix is
" optional. There may exist another field (such as the signature, title, or
" keywords field) after the note id. These are prefixed with '==', '--', or
" '__'. If the note id is the last field, then the id is followed by the file
" extension, e.g., '.md'.
" (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)
\ ->filter('v:val =~ "' .. a:noteId .. '\\(==\\|--\\|__\\|\\.\\)"')
\ ->filter('v:val =~ "^' .. a:noteId .. '\\|@@' .. a:noteId .. '"')
return empty(files) ? v:false : files[0]
endfunction
" Return the note id from the filename. On failure, v:false is returned.
function denote#meta#noteIdFromFile(filename)
return a:filename->matchstr("@@\\zs.\\{-\\}\\ze\\(==\\|--\\|__\\|\\..\\)")
\ ?? a:filename->matchstr("^.\\{-\\}\\ze\\(==\\|--\\|__\\|\\..\\)")
\ ?? v:false
endfunction
" Return the note title from the filename. On failure, v:false is returned.
function denote#meta#noteTitleFromFile(filename)
return a:filename->matchstr("--\\zs.\\{-\\}\\ze\\(==\\|@@\\|__\\|\\..\\)")->substitute("-", " ", "g")
\ ?? v:false
endfunction
" Return the note tags from the filename as a list.
function denote#meta#noteTagsFromFile(filename)
return a:filename->matchstr("__\\zs.\\{-\\}\\ze\\(==\\|@@\\|--\\|\\..\\)")->split("_")
\ ?? []
endfunction

279
autoload/denote/notes.vim Normal file
View File

@@ -0,0 +1,279 @@
" Script-local function that retrieves the front-matter of a file.
function s:getfrontmatter(filename)
let l:ext = fnamemodify(a:filename, ':e')
if index(g:denote_note_file_extensions, l:ext) == -1
return []
endif
" The following code aims to be as robust as possible. This is achieved by
" parsing each file-type (extension) separately. A common signature of the
" front matter is the empty line that separates the front matter from the
" rest of the text. As fail safe, we assume that the front matter is shorter
" than l:max lines.
" Markdown:
" - The first line is either '---' or '+++'.
" - The last line is the same as the first line.
" - All entries (apart first and last lines) are of the form '^\w\+:\?'.
" - Contains the entry '^identifier'.
" Org:
" - All lines start with '^#+\w\+:\s*'.
" - Contains the entry '^#identifier:
" Text:
" - The last line is '^-\+$.
" - All lines start with '^\w\+:\s*.
" - Contains the entry '^identifier:'
"
" Note: getbufline() returns an empty list if no more lines are available.
let l:max = 50
call bufload(a:filename)
let l:fmt = []
let l:lnr = 1
let l:identifier_seen = v:false
if l:ext == 'md'
let l:separator = getbufline(a:filename, l:lnr)[0]
if index(['---', '+++'], l:separator) == -1
return []
endif
call add(l:fmt, l:separator)
let l:lnr += 1
while l:lnr < l:max
let l:line = getbufline(a:filename, l:lnr)[0]
if l:line == l:separator
call add(l:fmt, l:line)
return l:identifier_seen && getbufline(a:filename, l:lnr + 1)[0] == '' ? l:fmt : []
endif
if l:line !~ '^\w\+:\?'
return []
endif
call add(l:fmt, l:line)
if l:line =~ '^identifier'
let l:identifier_seen = v:true
endif
let l:lnr += 1
endwhile
elseif l:ext == 'org'
while l:lnr < l:max
let l:line = getbufline(a:filename, l:lnr)[0]
if l:line == ''
return l:identifier_seen ? l:fmt : []
endif
if l:line !~ '^#+\w\+:'
return []
endif
call add(l:fmt, l:line)
if l:line =~ '^#+identifier:'
let l:identifier_seen = v:true
endif
let l:lnr += 1
endwhile
elseif l:ext == 'txt'
while l:lnr < l:max
let l:line = getbufline(a:filename, l:lnr)[0]
if l:line =~ '^-\+$'
call add(l:fmt, l:line)
return l:identifier_seen && getbufline(a:filename, l:lnr + 1)[0] == '' ? l:fmt : []
endif
if l:line !~ '^\w\+:'
return []
endif
call add(l:fmt, l:line)
if l:line =~ '^identifier:'
let l:identifier_seen = v:true
endif
let l:lnr += 1
endwhile
endif
return []
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 denote#notes#list(search)
let l:s = substitute(' ' .. a:search .. ' ', ' ', '*', 'g')
let l:files = glob(g:denote_directory .. '/' .. l:s, 0, v:true)
let l:title = 'Denote notes search:' .. a:search
let l:Gfun = function('denote#notes#list', [a:search])
call denote#loclist#fill(l:title, l:files, l:Gfun)
endfunction
" Put all notes of the given tag to the location list. The tag argument is
" mandatory.
function denote#notes#bytag(tag)
let l:files = glob(g:denote_directory .. '/*_' .. a:tag .. '*', 0, v:true)->filter('v:val->split("/")[-1] =~ "_' .. a:tag .. '\\(==\\|@@\\|__\\|_\\|\\.\\)"')
let l:title = 'Denote notes: ' .. a:tag
let l:Gfun = function('denote#notes#bytag', [a:tag])
call denote#loclist#fill(l:title, l:files, l:Gfun)
endfunction
" Search in denote notes
function denote#notes#grep(re)
let l:title = 'Grep results for: ' .. a:re
let l:fpat=map(copy(g:denote_note_file_extensions), {_, e -> g:denote_directory .. '/*.' .. e})->join()
exe 'silent! lvimgrep ' .. a:re .. ' ' .. l:fpat
let l:Gfun = function('denote#notes#grep', [a:re])
call denote#loclist#setgrep(l:title, l:Gfun)
endfunction
" This creates a new denote entry with the given title and of the given
" extension. The title may be empty.
function denote#notes#new(title, ext=g:denote_new_ext)
let l:identifier = g:Denote_identifier_fun()
let l:fn = g:denote_directory .. '/' .. libdenote#scheme_filename(a:ext, l:identifier, a:title)
" Jump to window this location list belongs to
call denote#loclist#jumptowindow()
" Open file and write front matter
exe 'edit ' l:fn
call setline(1, libdenote#fm_gen(a:ext, l:identifier, a:title, [], g:denote_fm_md_type))
endfunction
" Function to set the title of the selected entry
function denote#notes#settitle(linenr, title)
" Get file first!
let l:items = getloclist(0, {'items': 1})['items']
if empty(l:items)
return
endif
let l:item = l:items[a:linenr-1]
let l:bufnr = l:item['bufnr']
let l:filename = bufname(l:bufnr)
let l:meta = libdenote#scheme_metadata(l:filename)
let l:ext = fnamemodify(l:filename, ':e')
let l:newfilename = g:denote_directory .. '/' .. libdenote#scheme_filename(l:ext, l:meta.id, a:title, l:meta.tags, l:meta.sig)
" If this note has a front matter, we rewrite the front matter and rename the
" file. Otherwise, we rename the file only.
if index(g:denote_note_file_extensions, l:ext) >= 0
" Handle front matter
call bufload(l:filename)
let l:frontmatter = s:getfrontmatter(l:filename)
if empty(l:frontmatter)
" Write fresh front matter
let l:frontmatter = libdenote#fm_gen(l:ext, l:meta.id, l:meta.title, l:meta.tags, g:denote_fm_md_type)
call add(l:frontmatter, '')
call appendbufline(l:filename, 0, l:frontmatter)
else
" Modify front matter
let l:frontmatter = libdenote#fm_alter(l:frontmatter, {'title': a:title})
call setbufline(l:filename, 1, l:frontmatter)
endif
let curl = line('.')
call denote#loclist#jumptowindow()
exe 'silent buf ' .. l:bufnr
exe 'silent file ' .. l:newfilename
exe 'silent w'
exe 'lopen'
exe curl
if fnamemodify(l:filename, ':t') != fnamemodify(l:newfilename, ':t')
call delete(l:filename)
endif
else
if fnamemodify(l:filename, ':t') == fnamemodify(l:newfilename, ':t')
return
endif
call rename(l:filename, l:newfilename)
endif
endfunction
" Function to add or remove a tag to the selected entries. The last argument
" a:add, is set to v:true to add, and v:false to remove.
function denote#notes#tagmod(line1, line2, tag, add)
" Get file first!
let l:items = getloclist(0, {'items': 1})['items']
if empty(l:items)
return
endif
for i in range(a:line1, a:line2)
let l:item = l:items[i-1]
let l:bufnr = l:item['bufnr']
let l:filename = bufname(l:bufnr)
let l:meta = libdenote#scheme_metadata(l:filename)
let l:idx = index(l:meta.tags, a:tag)
if a:add
if l:idx >= 0
continue
endif
call add(l:meta.tags, a:tag)
else
if l:idx == -1
continue
endif
call remove(l:meta.tags, l:idx)
endif
let l:ext = fnamemodify(l:filename, ':e')
let l:newfilename = g:denote_directory .. '/' .. libdenote#scheme_filename(l:ext, l:meta.id, l:meta.title, l:meta.tags, l:meta.sig)
if index(g:denote_note_file_extensions, l:ext) >= 0
" Handle front matter
call bufload(l:filename)
let l:frontmatter = s:getfrontmatter(l:filename)
if empty(l:frontmatter)
" Write fresh front matter
let l:frontmatter = libdenote#fm_gen(l:ext, l:meta.id, l:meta.title, l:meta.tags, g:denote_fm_md_type)
call add(l:frontmatter, '')
call appendbufline(l:filename, 0, l:frontmatter)
else
" Modify front matter
let l:frontmatter = libdenote#fm_alter(l:frontmatter, {'tags': l:meta.tags})
call setbufline(l:filename, 1, l:frontmatter)
endif
let curl = line('.')
call denote#loclist#jumptowindow()
exe 'silent buf ' .. l:bufnr
exe 'silent file ' .. l:newfilename
exe 'silent w'
exe 'lopen'
exe curl
call delete(l:filename)
else
if fnamemodify(l:filename, ':t') == fnamemodify(l:newfilename, ':t')
return
endif
call rename(l:filename, l:newfilename)
endif
endfor
endfunction
" Add file to denote directory
function denote#notes#copy(origfile)
if !filereadable(a:origfile)
echohl WarningMsg
echom 'Cannot copy specified file to denote directory.'
return
endif
" Derive title from origfile
let l:title = fnamemodify(a:origfile, ':t:r')
let l:ext = fnamemodify(a:origfile, ':e')
let l:identifier = g:Denote_identifier_fun()
let l:filename = g:denote_directory .. '/' .. libdenote#scheme_filename(l:ext, l:identifier, l:title)
call system('cp ' .. shellescape(a:origfile) .. ' ' .. shellescape(l:filename))
" Write front matter, if this is supported
if index(g:denote_note_file_extensions, l:ext) >= 0
call denote#loclist#jumptowindow()
exe 'edit ' .. fnameescape(l:filename)
call appendbufline(l:filename, 0, libdenote#fm_gen(l:ext, l:identifier, l:title), [], g:denote_fm_md_type)
exe 'w'
endif
endfunction
" Delete notes from denote directory
function denote#notes#rm(line1, line2, bang)
let l:items = getloclist(0, {'items': 1})['items']
if empty(l:items)
return
endif
for i in range(a:line1, a:line2)
let l:item = l:items[i-1]
let l:bufnr = l:item['bufnr']
let l:filename = bufname(l:bufnr)
let l:title = libdenote#scheme_metadata(l:filename).title
if a:bang == v:false
let l:answer = confirm('Are you sure you want to delete the note "' .. l:title .. '"?', "&Yes\n&No\n", 2, 'Question')
if l:answer != 1
continue
endif
endif
" Wipe buffer, if it exists
if bufexists(l:filename)
exe 'silent bwipe ' .. fnameescape(l:filename)
endif
" Delete file
call delete(l:filename)
endfor
endfunction

202
autoload/libdenote.vim Normal file
View File

@@ -0,0 +1,202 @@
" libdenote plugin for vim
" This plugin describes basic denote functions. Each one of these functions
" deals wither with the file-naming scheme (prefixed with scheme_), or with the
" front matter (prefixed with fm_). All functions are pure, i.e., have no side
" affects.
"
" FILE-NAMING SCHEME {{{1
"
" Script-local functions {{{2
"
" This function removes illegal characters in parts of the filename.
function s:santizefnpart(part)
return a:part->tolower()
\ ->substitute('[^[:alnum:]]', '-', 'g')
\ ->substitute('-\+', '-', 'g')
\ ->substitute('_\+', '_', 'g')
\ ->substitute('=\+', '=', 'g')
\ ->substitute('@\+', '@', 'g')
\ ->trim('-_@=')
endfunction
" API {{{2
" Identifier creation
" This unction generates a fresh denote identifier
function libdenote#scheme_idgen()
return exists('*strftime')
\ ? strftime('%Y%m%dT%H%M%S')
\ : rand()
endfunction
" Generate file name
" This function returns the Filename given all components in the file-naming
" scheme
"
" @argument a:ext string: File extension
" @argument a:id string: Identifier of denote entry
" @argument a:title string: Title of denote entry
" @argument a:tags list[string]: List of strings that specify the tags
" @argument a:sig string: Signature string of denote entry
function libdenote#scheme_filename(ext, identifier, title='', tags=[], sig='')
let l:f = s:santizefnpart(a:identifier)
let l:f ..= len(a:sig) > 0 ? ('==' .. s:santizefnpart(a:sig)) : ''
let l:f ..= len(a:title) > 0 ? ('--' .. s:santizefnpart(a:title)) : ''
let l:f ..= len(a:tags) > 0 ? ('__' .. map(copy(a:tags), {_, v -> s:santizefnpart(v)})->join('_')) : ''
return l:f .. '.' .. a:ext
endfunction
" Retrieve metadata from file name
" This function returns a dict with the entries 'id', 'title', 'tags', and
" 'sig.' The value type of 'tags' is a list, the other entries hold strings.
"
" @argument a:filename string: Filename or path to file
function libdenote#scheme_metadata(filename)
return {
\ 'id': a:filename->fnamemodify(':t')->matchstr('@@\zs.\{-\}\ze\(==\|--\|__\|\..\)')
\ ?? a:filename->fnamemodify(':t')->matchstr('^.\{-\}\ze\(==\|--\|__\|\..\)'),
\ 'title': a:filename->fnamemodify(':t')->matchstr('--\zs.\{-\}\ze\(==\|@@\|__\|\..\)')
\ ->substitute('-', ' ', 'g'),
\ 'tags': a:filename->fnamemodify(':t')->matchstr('__\zs.\{-\}\ze\(==\|@@\|--\|\..\)')
\ ->split('_'),
\ 'sig': a:filename->fnamemodify(':t')->matchstr('==\zs.\{-\}\ze\(==\|@@\|__\|\..\)')
\}
endfunction
" Get path to file from denote identifier
" This function returns the path to the file the note corresponds to, if it
" exists, and v:false otherwise.
"
" @argument a:dir string: Path to denote directory
" @argument a:id string: Identifier of denote entry
function libdenote#scheme_find(dir, id)
" According to the file-naming scheme, the note id is prefixed with '@@'. If
" the note id appears at the beginning of the file name, then this prefix is
" optional. There may exist another field (such as the signature, title, or
" keywords field) after the note id. These are prefixed with '==', '--', or
" '__'. If the note id is the last field, then the id is followed by the file
" extension, e.g., '.md'.
" (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 l:files = glob(a:dir .. '/*' .. a:id .. '*', 0, v:true)
\ ->filter('v:val->split("/")[-1] =~ "' .. a:id .. '\\(==\\|--\\|__\\|\\.\\)"')
\ ->filter('v:val->split("/")[-1] =~ "^' .. a:id .. '\\|@@' .. a:id .. '"')
return empty(l:files) ? v:false : l:files[0]
endfunction
" FRONT-MATTER HANDLING {{{1
"
" Script-local functions {{{2
"
" Put string in double quotes, and remove inside double quotes.
function s:escapeDQ(s)
return '"' .. substitute(a:s, '"', '', 'g') .. '"'
endfunction
" Create front matter (yaml)
function s:md_new_yaml(id, title, tags)
return [
\ '---',
\ 'title: ' .. s:escapeDQ(a:title),
\ 'date: ' .. strftime('%FT%T%z'),
\ 'tags: [' .. map(copy(a:tags), {_, t -> s:escapeDQ(t) })->join(', ') .. ']',
\ 'identifier: ' .. s:escapeDQ(a:id),
\ '---'
\ ]
endfunction
" Create front matter (toml)
function s:md_new_toml(id, title, tags)
return [
\ '+++',
\ 'title ' .. s:escapeDQ(a:title),
\ 'date ' .. strftime('%FT%T%z'),
\ 'tags [' .. map(copy(a:tags), {_, t -> s:escapeDQ(t) })->join(', ') .. ']',
\ 'identifier ' .. s:escapeDQ(a:id),
\ '+++'
\ ]
endfunction
" Create front matter (plain)
function s:plain_new(id, title, tags)
return [
\ 'title: ' .. a:title,
\ 'date: ' .. strftime('%F'),
\ 'tags: ' .. a:tags->join(' '),
\ 'identifier: ' .. a:id,
\ '---------------------------',
\ ]
endfunction
" Create front matter (org)
function s:org_new(id, title, tags)
return [
\ '#+title: ' .. a:title,
\ '#+date: [' .. strftime('%F %a %R') .. ']',
\ '#+filetags: :' .. map(copy(a:tags), {_, t -> substitute(t, ':', '', 'g') })->join(':') .. ':',
\ '#+identifier: ' .. a:id,
\ ]
endfunction
" API {{{2
" Create front matter
" This function generates a front matter. It may use the global variable
" g:denote_fm_md_type.
"
" @argument a:ext string: Generate front matter for a file of this
" extension
" @argument a:id string: Identifier of the denote note
" @argument a:title string: Title of the denote note
" @argument a:tags list[string]: List of strings that specify the tags
" @argument a:md_type string: Any of 'yaml' (default) or 'toml', for
" markdown front matter
" @return: List of strings with a line-per item that describes the front matter
function libdenote#fm_gen(ext, id, title, tags, md_type='yaml')
return a:ext == 'org' ? s:org_new(a:id, a:title, a:tags)
\ : a:ext == 'txt' ? s:plain_new(a:id, a:title, a:tags)
\ : a:md_type == 'toml' ? s:md_new_toml(a:id, a:title, a:tags)
\ : s:md_new_yaml(a:id, a:title, a:tags)
endfunction
" Alter front matter
" This function returns the (modified) a:fm front matter. The argument a:mod
" of type dict specifies the fields to be updated. If a:mod contains the key
" 'date', then the date field will be updated. If it contains the key 'id',
" then the identifier will be updated with the value a:mod.id. Similar for the
" title (key 'title'), and the tags (key 'tags'). For the tags, the value type
" is a list of strings.
"
" @argument a:fm list[string] List of strings describes a frontmatter
" @argument a:mod dict Dictionary to control updates
" @return: List of strings with a line-per item that describes the front matter
function libdenote#fm_alter(fm, mod)
let l:md_type = a:fm[0] == '+++' ? 'toml' : 'yaml'
let l:ext = { n -> n == 4 ? 'org' : n == 5 ? 'txt' : 'md'}(len(a:fm))
let l:new = copy(a:fm)
let l:repl = libdenote#fm_gen(l:ext,
\ has_key(a:mod, 'id') ? a:mod.id : '',
\ has_key(a:mod, 'title') ? a:mod.title : '',
\ has_key(a:mod, 'tags') ? a:mod.tags : [],
\ g:denote_fm_md_type)
if has_key(a:mod, 'date')
let l:ididx = indexof(l:repl, 'v:val =~ "^\\(#+\\)\\?date"')
call map(l:new, {_, v -> v =~ "^\\(#+\\)\\?date" ? l:repl[l:ididx] : v})
endif
if has_key(a:mod, 'id')
let l:ididx = indexof(l:repl, 'v:val =~ "^\\(#+\\)\\?identifier"')
call map(l:new, {_, v -> v =~ "^\\(#+\\)\\?identifier" ? l:repl[l:ididx] : v})
endif
if has_key(a:mod, 'title')
let l:titleidx = indexof(l:repl, 'v:val =~ "^\\(#+\\)\\?title"')
call map(l:new, {_, v -> v =~ "^\\(#+\\)\\?title" ? l:repl[l:titleidx] : v})
endif
if has_key(a:mod, 'tags')
let l:tagsidx = indexof(l:repl, 'v:val =~ "^\\(#+file\\)\\?tags"')
call map(l:new, {_, v -> v =~ "^\\(#+file\\)\\?tags" ? l:repl[l:tagsidx] : v})
endif
return l:new
endfunction

View File

@@ -1,18 +1,34 @@
*denote.txt* For Vim version 9.0. Last change: 2026 Feb 17 *denote.txt* Handling denote entries the vim way
This is the documentation for the denote plugin. Last change: 2026 Mar 4
This is the documentation for the denote package. This package also introduces
the |libdenote| plugin in the file autoload/libdenote.vim, which may be of
independent interest. If this package is loaded automatically, but you prefer
to opt-out, then add this line to your vimrc:
>
let g:loaded_denote = 1
<
============================================================================== ==============================================================================
CONTENTS *denote.vim* *denote* CONTENTS *vim-denote* *denote*
1. Introduction |denote-intro| 1. Introduction |denote-intro|
2. Commands |denote-commands| 2. Commands |denote-commands|
3. Settings |denote-settings| 2.1 Universal commands |denote-universal-commands|
4. Mappings |denote-mappings| 2.2 Note command |denote-note-command|
2.3 Denote-list commands |denote-list-commands|
3. Settings |denote-settings|
4. Keys |denote-keys|
Appendices
A. The libdenote plugin |libdenote|
A.1 File-naming functions |libdentoe-filenaming|
A.2 Front-matter functions |libdenote-frontmatter|
B. Package API |denote-api|
============================================================================== ==============================================================================
*denote-intro* INTRODUCTION *denote-intro*
Introduction ~
Denote is a file-naming scheme developed by Protesilaos Stavrou and an Emacs Denote is a file-naming scheme developed by Protesilaos Stavrou and an Emacs
tool for handling such files. Notes and other files that follow this scheme tool for handling such files. Notes and other files that follow this scheme
@@ -21,62 +37,385 @@ do adjusting the tags or changing the title of a note. The official manual is
available online: available online:
https://protesilaos.com/emacs/denote https://protesilaos.com/emacs/denote
The denote plugin adds denote-functionality to vim --- the vim way. We are The denote plugin adds denote functionality to vim the vim way. We are aware
aware of two other plugins, but we fear that these plugins are not flexible of two other plugins, but we fear that these plugins are not flexible enough,
enough, and more importantly, do not follow the vim philosophy for handling and more importantly, do not follow the vim philosophy for handling denote
denote notes. These mentioned plugins are available here: notes. These mentioned plugins are available here:
https://github.com/shuckster/denote-md https://github.com/shuckster/denote-md
https://git.sr.ht/~ashton314/vim-denote https://git.sr.ht/~ashton314/vim-denote
In contrast to these plugins, the present package relies on the In contrast to these plugins, the present package relies on the
|location-list| features for displaying denote entries, and on and |location-list| features for displaying denote entries, and on the options
|'includeexpr'| option for following the links (see, |gf|). This plugin also 'quickfixtextfunc', 'includeexpr', and 'omnifunc'. With these, denote links
provides custom location-list formatting using the option can be followed using |gf|, and completed using omni completion (see,
|'quickfixtextfunc'|. |compl-omni|). For link completion, press |i_CTRL-X_CTRL-O| after typing any
prefix of a denote link. This completion also works with titles: by invoking
omni completion after typing "denote:foo", denote links are completed for
entries containing "foo" in the title.
==============================================================================
COMMANDS *denote-commands*
After loading this package, only the first of the following commands is
available. This first command, as descried below, sets the directory to be
used for the denote entries. After that command has been issued, several other
commands become possible, such as |:DenoteTag|. Some command are available
within any window, others only in denote notes, and yet other again only in
the location-list that lists denote entries.
*denote-universal-commands*
Universal commands~
*:DenoteDirectory*
*:DenoteDirectory!*
:DenoteDirectory[!] {path}
Set the provided {path} as the denote directory that will be used by
all following commands. The {path} argument is autocompleted. With the
bang, the autocompletion works for all directories available on your
system. Without the bang, only directories listed in the
|g:denote_directories| variable are autocompleted.
After the denote directory has been specified, the following commands become
available. They all operate on that specified directory.
*denote-commands*
Commands ~
*:Denote* *:Denote*
Populate the location list of the current window with the denote entries :Denote [{keyword ..}]
present in the current directory. This command may be supplemented with any Populate the location list of the current window with the denote
number of arguments that filter the entries. entries present in the specified directory. This command may be
supplemented with any number of arguments that filter the entries
according to the appearance of the keywords in the file name.
*:DenoteTag* *:DenoteByTag*
This command takes as argument a tag, and populates the location list with all :DenoteByTag {tag}
denote entries of that are tagged accordingly. The tags are autocompleted This command takes as argument a tag, and populates the location list
(see, |c_<Tab>|). with all denote entries of that are tagged accordingly. The tags are
autocompleted (see, |c_<Tab>|). Also fuzzy matching is possible when
"fuzzy" is contained in 'wildoptions'.
*:DenoteGrep* *:DenoteGrep*
This command is a wrapper around |:lvimgrep| to search for a pattern in the :DenoteGrep /{pattern}/[g][j][f]
denote entries. The required argument is a pattern as required by |:vimgrep|, This command is a wrapper around |:lvimgrep| to search for a pattern
i.e., /{pattern}/[g][j][f]. in the denote entries. The required argument is a pattern as required
by |:vimgrep|, i.e., /{pattern}/[g][j][f].
*:DenoteNew*
:DenoteNew {title}
This command takes as argument a note title, and generates a new
denote entry with the specified title. The entry file type is
controlled by the setting |g:denote_new_ft|.
*:DenoteCopy*
:DenoteCopy {file}
With this, the specified file is copied into the denote directory. The
file name is taken as the title, and the identifier is freshly
generated. Any file, i.e., not only those specified using
|g:denote_note_file_extension| may be chosen.
*denote-note-command*
Note command~
This is the only command exclusive to the windows that hold denote note
entries, i.e., files with an extension in |g:denote_note_file_extension|.
*:DenoteBackReferences* *:DenoteBackReferences*
When called from an opened denote entry, this command populates the location :DenoteBackReferences
list with all references to the current note. This command populates the location list with all references to the
current note. This command can only be called from an opened denote
note.
*denote-settings*
Settings ~ *denote-list-commands*
*'g:denote_note_file_extension'* Denote-list commands~
With this setting you may specify the file extensions of all denote entries
within which |:DenoteGrep| will search for the provided pattern. If left The denote list may be opened using one of the commands |:Denote| or
unspecified, it is set to the following default value: |:DenoteByTag|. Also the commands |:DenoteGrep| and |:DenoteBackReferences|
open the location list, but not as a list of entries, but as a list of
matching patterns. The following commands are available in the denote list
(first case).
*:DenoteSetTitle*
:DenoteSetTitle {string}
The title of the selected entry is changed as specified.
*:DenoteTagAdd*
:DenoteTagAdd {tag}
With this, the provided tag is added to the list of tags of the
selected entry. The tag may be autocompleted. This command also
accepts a range, resulting in adding the tag to every entry within
that range.
*:DenoteTagRm*
:DenoteTagRm {tag}
Just as above, but for removing the specified tag.
*:DenoteDelete*
:DenoteDelete[!]
With this, the selected entry is deleted from the denote directory
(and from the disk). This command also accepts a range, to delete a
bunch of entries. After the deletion command has been invoked, the
user is asked to confirm the deletion. With the optional bang, no
confirmation will be asked.
==============================================================================
SETTINGS *denote-settings*
All settings described below are set to default values. For this package to
function, it is not necessary to specify a setting manually.
*g:denote_directories*
g:denote_directories list
With this option, may may specify a list of your denote directories.
This list is used for the autocompletion of the |:DenoteDirectory|
command. The default value is
> >
g:denote_note_file_extension = ['md', 'org'] let g:denote_directories = []
< <
*'g:denote_loc_title_columns'*
This integer specifies the number of columns used to display the titles of *g:denote_note_file_extension*
denote entries. Per default, it is set to: g:denote_note_file_extension list
With this setting, you may specify the file extensions of all denote
entries within which |:DenoteGrep| will search, and for which files
denote link completion and the |:DenoteBackReferences| command will be
available. If left unspecified, it is set to the following default
value:
> >
g:denote_loc_title_columns = 60 let g:denote_note_file_extension = ['md', 'org', 'txt']
< <
*denote-mappings*
Mappings ~
<Plug>DenoteList "Populate and open the location list with all denote *g:denote_loc_title_columns*
entries". g:denote_loc_title_columns number
This integer specifies the number of columns used to display the
titles of denote entries. Per default, it is set to:
>
let g:denote_loc_title_columns = 60
<
<Plug>DenoteBackReferences "Populate and open the location list with all *g:denote_new_ft*
denote entries that link to the currently opened one". g:denote_new_ft string
Newly created notes are of this file type. Possible values are 'md',
'org', or 'txt', with the following default:
>
let g:denote_new_ft = 'md'
<
vim:tw=78:sw=4:ts=8:noet:ft=help:norl: *g:denote_fm_md_type*
g:denote_fm_md_type string
The front matter of 'md' notes is given as 'yaml' or as 'toml'. By
default, this package uses 'yaml':
>
let g:denote_fm_md_type = 'yaml'
<
*g:Denote_identifier_fun*
g:Denote_identifier_fun Funcref
Denote allows the use of custom identifiers. This variable, if set,
points to a function that generates identifiers for newly created
notes. The function is supposed to return a unique string. By default,
the variable set as
>
let g:Denote_identifier_fun = function('denote#meta#identifier_generate')
<
which calls |strftime('%Y%m%dT%H%M%S')| if |strftime()| is available,
and |rand()| otherwise. Another reasonable setting would be to use
some uuid generator and to specify, e.g.,
>
let g:Denote_identifier_fun = { -> system('uuidgen')
\ ->substitute('[^[:xdigit:]]', '', 'g') }
<
==============================================================================
KEYS *denote-keys*
The location lists generated by the present package, e.g., through the command
|:DenoteGrep| or |:DenoteByTag|, have the following key key-bindings.
*denote-r*
r Reload the location list.
*denote-q*
q Close the location list.
In addition, denote lists (see |denote-list-commands|) come with the following
keys:
*denote-list-C*
C Change the title of the selected entry.
*denote-list-+*
+ Add a tag to the selected entries (also in visual mode).
*denote-list--*
- As |denote-list-+| but for removing tags.
*denote-list-dd*
dd Delete the selected entry.
{Visual}d Delete the visually selected entries.
*denote-list-o*
o Open the file using the system "open" command.
==============================================================================
APPENDIX A - THE LIBDENOTE PLUGIN *libdenote*
The present package has as constituent the libdenote plugin for basic
denote-centered operations. That plugin is given by the file
autoload/libdenote.vim. Each function in this plugin is pure, i.e., produces
no side effects. There are two classes of functions: functions concerning the
file-naming scheme (prefixed with scheme_, see |libdenote-filenaming|) and
functions concerning the front matter (prefixed with fm_, see
|libdenote-frontmatter|).
*libdenote-filenaming*
File-naming functions~
*libdenote#scheme_idgen()*
libdenote#scheme_idgen()
This function generates a new identifier. If the function |strftime()|
is available, than strftime(%Y%m%dT%H%M%S) is used, otherwise a random
identifier is generated using |rand()|.
*libdenote#scheme_filename()*
libdenote#scheme_filename({ext}, {id}, {title}, {tags}, {sig})
With this, the file name of the denote entry with the given metadata
is returned. The parameter {ext} describes the extension of the file
is is one of 'md', 'txt', or 'org'. The parameter {id} is the denote
identifier, possibly generated via |libdenote#scheme_idgen()|. The
{title} parameter is optional and describes the title of the note. By
default, it is set to the empty string ''. The {tags} parameter is
optional as well, is a list of tags associated to the note. The
default value is the empty list []. Also, the {sig} parameter is
optional, and used to describe the signature of the entry.
*libdenote#scheme_metadata()*
libdenote#scheme_metadata({filename})
This function returns the metadata of the file given by {filename}.
This parameter may describe the path to the file, or the tail only.
The returned |dict| has the keys 'id' for the identifier, 'title' for
the title, 'tags' for the list of tags, and 'sig' for the signature.
*libdenote-frontmatter*
Front-matter functions~
*libdenote#fm_gen()*
libdenote#fm_gen({ext}, {id}, {title}, {tags}, {mdtype})
This function returns the list of lines for the front matter that
stores the given metadata. The paramter {ext} is the extension of the
file, the parameter {id} the identifier, the parameter {title}, the
title of the note, and {tags} the optional parameter as a list of tags
associated to the note. The argument {mdtype} descries the format to
be used in markdown. For markdown files (extension 'md'), two formats
are possible: 'yaml' and 'toml'. The former format is used per
default.
*libdenote#fm_alter()*
libdenote#fm_alter({fm}, {mod})
Similar to |libdenote#fm_gen()|, this function returns a list
containing the lines of a front matter. In contrast to the above
function, this functions takes a front matter {fm} as base (again,
given as list of lines), and updates the fields specified by the
|dict| {mod}. If {mod} contains the key "date", then the date string
will be updated. If {mod} contains the key "id", then the identifier
will be updated with the value a:mod.id. Similarly, the keys "title"
and "tags" are used to update the title and tag list of the front
matter.
==============================================================================
APPENDIX B - PACKAGE API *denote-api*
Here, we list and briefly describe all functions that come with this package.
The aim of providing this information is to keep vim-denote hackable.
*denote#commands#load()*
denote#commands#load()
This function initializes the user commands (see, |denote-commands|)
that are globally available.
*denote#completion#tags()*
denote#completion#tags({ArgLead}, {CmdLine}, {CursorPos})
This is the autocompletion function for tag arguments in the user
commands |:DenoteByTag|, |:DenoteTagAdd|, and |:DenoteTagRm|.
*denote#loclist#clear()*
denote#loclist#clear()
With this, the location list is cleared.
*denote#loclist*fill()*
denote#loclist*fill({title}, {files}, {Gfun})
This function populates the location list with denote entries. The
{title} argument specifies the title of the location list. The {files}
argument is a list of filenames of denote entries. Finally, the {Gfun}
argument is a Funcref to the function that reloads the location list —
this is the function (with all arguments set) that invoked this call
to |denote#loclist#fill()|.
*denote#loclist#setgrep()*
denote#loclist#setgrep({title}, {Gfun})
This function makes the location list fit for showing the result of
|:DenoteGrep|. The {title} and {Gfun} arguments are as in
|denote#loclist#fill()|.
*denote#loclist#reload()*
denote#loclist#reload()
This function reruns the location-list generating function (see {Gfun}
in |denote#loclist#fill()|.
*denote#loclist#jumptowindow()*
denote#loclist#jumptowindow()
With this, the cursor is moved from the location list to the window
the location list belongs to.
*denote#notes#list()*
denote#notes#list({search})
This function searches for denote entries that contain {search} in the
path, and displays the results in the location list. The command
|:Denote| is bound to this function.
*denote#notes#bytag()*
denote#notes#bytag({tag})
This is similar to |denote#notes#list()|, but only entries with the
tag {tag} are put to the location list. This is used by the command
|:DenoteByTag|.
*denote#notes#grep()*
denote#notes#grep({re})
This is the function used by |:DenoteGrep| and |:DenoteBackReferences|
to search for patterns within the denote files.
*denote#notes#new()*
denote#notes#new({title}, {ext})
This function is used by |:DenoteNew|. Here, {title} is the title of
the new note, and {ext} the file extension. This second argument is
optional and has the default value |g:denote_new_ext|.
*denote#notes#settitle()*
denote#notes#settitle({linenr}, {title})
With this, the title of the entry on line {linenr} of the location
list is set to {title}. This is used by |:DenoteSetTitle|.
*denote#notes#tagmod()*
denote#notes#tagmod({line1}, {line2}, {tag}, {add})
This function modifies the tags of the denote entries from line
{line1} to line {line2} of the location list. In each of these
entries, the tag {tag} is added if {add} is 1, and removed if {add} is
0.
*denote#notes#copy()*
denote#notes#copy({origfile})
This copies the file {origfile} to the denote directory, and renames
the copy to make it denote compatible. This function is used by
|:DenoteCopy|.
*denote#notes#rm()*
denote#notes#rm({line1}, {line2}, {bang})
With this, the files that correspond to the entries from line {line1}
to line {line2} of the location list are deleted. If the {bang} is set
to 1, then no confirmation will be asked. Otherwise, the user is asked
to confirm every deletion. This function is used by |:DenoteDelete|.
vim:tw=78:sw=4:ts=8:noet:ft=help:norl:

View File

@@ -1,92 +1,119 @@
" Global configurations {{{1 " Global configurations
if exists('g:loaded_denote')
finish
endif
let g:loaded_denote = 1
" Restrict basic operations to these files: " List of denote directories
if !exists('g:denote_directories')
let g:denote_directories = []
endif
call map(g:denote_directories, {_, d -> fnamemodify(d, ':p')})
" Restrict basic operations to these files
if !exists('g:denote_note_file_extensions') if !exists('g:denote_note_file_extensions')
let g:denote_note_file_extensions=['md', 'org'] let g:denote_note_file_extensions = ['md', 'org', 'txt']
endif endif
" Number of columns used for the title in the location window. " Number of columns used for the title in the location window
if !exists('g:denote_loc_title_columns') if !exists('g:denote_loc_title_columns')
let g:denote_loc_title_columns=40 let g:denote_loc_title_columns = 40
endif endif
" Local functions {{{1 " Default filetype for newly created denote entries
" Put all notes of the given tag to the location list. The tag argument is if !exists('g:denote_new_ext')
" mandatory. let g:denote_new_ext = 'md'
function s:DenoteNotesByTag(tag) endif
" Clear location list
call denote#loclist#clear() " Default front-matter type for markdown notes, may be one of 'yaml' or 'toml'
" Find files if !exists('g:denote_fm_md_type')
let files = glob("*_" .. a:tag .. "*", 0, v:true)->filter('v:val =~ "_' .. a:tag .. '\\(==\\|@@\\|__\\|_\\|\\.\\)"') let g:denote_fm_md_type = 'yaml'
" Populate location list endif
let locTitle="Denote notes: " .. a:tag
call setloclist(0, [], 'r', " By using the following global variable, the user may specify a custom
\ {'title': locTitle, " function for creating identifiers.
\ 'quickfixtextfunc' : 'denote#loclist#textNoteList'}) if !exists('g:Denote_identifier_fun')
let notes=[] let g:Denote_identifier_fun = function('libdenote#scheme_idgen')
for f in files endif
call add(notes, {
\ 'filename' : f, " Transform full path into canonical form WITH trailing '/'
\ 'lnum' : 1 function s:canonicalFullPath(path)
\ }) let l:parts = split(a:path, '/')
let l:result = []
for part in l:parts
if part == '.' || part == ''
continue
elseif part == '..'
if len(l:result) > 0
call remove(l:result, len(l:result)-1)
endif
else
call add(l:result, part)
endif
endfor endfor
call setloclist(0, notes, 'a') return empty(l:result) ? '/' : '/' .. join(l:result, '/') .. '/'
endfunction endfunction
" Put all notes of the given tag to the location list. The search argument may be " Compute the relative path from start to target. Both arguments are given as
" empty. For improving search, white spaces are replaced by the * |wildcard|. " full paths.
function s:DenoteNotes(search) function s:relativePath(start, target)
let s = substitute(" " .. a:search .. " ", " ", "*", "g") let l:a = s:canonicalFullPath(a:start)
" Clear location list let l:b = s:canonicalFullPath(a:target)
call denote#loclist#clear() " Simple cases first: both paths are the same, or the target is l:a subpath
" Find files " from the start.
let files = glob(s, 0, v:true) if l:a == l:b[:strlen(l:a)-1]
" Populate location list return l:b[strlen(l:a):]
let locTitle="Denote notes search:" .. a:search endif
call setloclist(0, [], 'r', " Now, we need to go back. If the following match fails, then we need to go
\ {'title': locTitle, " back all the way
\ 'quickfixtextfunc' : 'denote#loclist#textNoteList'}) let l:l = matchstrpos(l:a .. l:b, '^\(/.*\)/\zs.*/\ze\1')
let notes=[] return l:l[1] == -1
for f in files \ ? substitute(l:a[1:-2], '[^/]\+', '..', 'g') .. l:b
call add(notes, { \ : substitute(l:l[0], '[^/]\+', '..', 'g') .. l:b[l:l[1]:]
\ 'filename' : f, endfunction
\ 'lnum' : 1
\ }) " Complete all paths to the denote directories. This functions completes the
" paths to the configured directories.
function s:denoteDirectoryCompletion(ArgLead, CmdLine, CursorPos)
let l:res = []
let l:bang = match(a:CmdLine, '^\S\+!') >= 0
let l:argleaddir = isdirectory(expand(a:ArgLead)) ? a:ArgLead : matchstr(a:ArgLead, '^.*/')
if strlen(l:argleaddir) == 0
let l:argleaddir = '~'
endif
if l:argleaddir !~ '/$'
let l:argleaddir = l:argleaddir .. '/'
endif
if l:bang
for ldir in (glob(expand(l:argleaddir) .. '/*', v:true, v:true)->filter('isdirectory(v:val)'))
call add(l:res, l:argleaddir .. fnamemodify(ldir, ':t'))
endfor endfor
call setloclist(0, notes, 'a') else
endfunction let l:leaddir = s:canonicalFullPath(fnamemodify(l:argleaddir, ':p'))
for ldir in g:denote_directories
" This function implements the similar functionality of :vimgrep, but for if l:leaddir == ldir[:strlen(l:leaddir)-1]
" denote notes. call add(l:res, l:argleaddir .. s:relativePath(l:leaddir, ldir))
function s:DenoteGrep(search) 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
" Adjust location list: set title and specify display function
call setloclist(0, [], 'r',
\ {'title': 'Denote grep: ' .. a:search,
\ 'quickfixtextfunc' : 'denote#loclist#textReferences'})
lclose
lopen
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 tags=[]
for f in files
let tags=extend(tags, denote#meta#noteTagsFromFile(f))
endfor endfor
return uniq(sort(tags))->join("\n") endif
return l:res->join("\n")
endfunction endfunction
" Public commands and key mappings {{{1 " Setup denote plugin, i.e.,
command -nargs=* Denote :call <SID>DenoteNotes(<q-args>) " - set the denote directory
command -nargs=1 -complete=custom,<SID>tagList DenoteTag :call <SID>DenoteNotesByTag(<q-args>) " - register user commands
command -nargs=+ DenoteGrep :call <SID>DenoteGrep(<q-args>) " - register auto commands
function s:setup(dir)
" Set the denote directory
let l:tmp = s:canonicalFullPath(fnamemodify(a:dir, ':p'))[:-2]
if !isdirectory(l:tmp)
echohl WarningMsg
echom "The specified argument is not a directory (" .. a:dir .. ")"
return
endif
let g:denote_directory = l:tmp
" Register user commands and auto commands
call denote#commands#load()
endfunction
" Useful key mappings command -nargs=1 -bang -complete=custom,s:denoteDirectoryCompletion DenoteDirectory call s:setup(<f-args>)
nnoremap <silent> <Plug>DenoteList :Denote<CR>:lclose<CR>:lopen<CR>:resize 20<CR>
nnoremap <silent> <Plug>DenoteBackReferences :DenoteBackReferences<CR>:lclose<CR>:lopen<CR>:resize 20<CR>