Compare commits

...

27 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
13 changed files with 1275 additions and 445 deletions

145
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.
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
is not the first attempt to handle denote notes within vim.
implementation is yet incomplete; more features are expected. Note that this is
not the first attempt to handle denote notes within vim.
[Conan](https://zansh.in/) developed a [bash
script](https://github.com/shuckster/denote-md) for denote and an accompanying
[vim plugin](https://github.com/shuckster/vim-denote-md). Also
@@ -14,92 +14,107 @@ script](https://github.com/shuckster/denote-md) for denote and an accompanying
### Why?
The present package aims at handling denote notes the _vim way._ For instance,
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
[|'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.
Also, the present package aims at remaining flexible.
Again, both plugins require the denote note identifiers to follow the rigid
format `YYYYMMDDTHHMMSS`. Per denote manual, the identify may be [any
Also, the present package aims at remaining flexible. Again, both
above-mentioned plugins require the denote note identifiers to follow the rigid
format `YYYYMMDDTHHMMSS`. Per denote manual, however, the identify may be [any
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
You may use any of your favorite plugin managers, or, place a copy of this
package in `~/.vim/pack/tools/start`. Then, run `:helptags ALL` to regenerate
the help files. This will allow you to get more help using `:help denote` or
You may use any of your favorite plugin managers, or, manually place a copy of
this package in, e.g., `~/.vim/pack/tools/start`. For such a manual
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.
### Usage
For _following links,_ simply move your cursor to the denote link, and press
`gf`.
vim needs to know where your denote entries are stored. To do so, run the
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.
This package also provides denote _link completion_ using the
[|'omnifunc'|](https://vimhelp.org/options.txt.html#%27omnifunc%27) option. So,
the link is completed after pressing `<C-X><C-O>` while typing any prefix of a
denote link `denote:<identifier>`. 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.
After specifying the denote directory, the following functionalities are
available.
_Browsing_ and _searching notes_ is implemented using the [location
list](https://vimhelp.org/quickfix.txt.html#location-list).
This means that 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.
_Following links:_ Simply place your cursor on a denote link and press `gf` or
similar keys.
#### Commands
This package defines the following user commands:
_Link completion:_ When editing a note, write any prefix of a denote link
(`denote:<identifier>`) and invoke [Omni
completion](https://vimhelp.org/insert.txt.html#compl-omni) by pressing
`<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.
- `:Denote [{keyword ..}]`:
This command places all denote entries in the location list.
You may supply any number of arguments to filter the notes by the given
keywords.
_Browsing and searching notes:_ Browsing and searching is done using vim's
[location list](https://vimhelp.org/quickfix.txt.html#location-list). The
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).
- `:DenoteTag {tag}`:
With this, all notes of a given tag are listed.
The `{tag}` may be autocompleted by hitting the tab key.
_Adding notes:_ Run `:DenoteNew {title}` to add a new note with the given
title. You may also copy a file (any file) to your denote directory. To do so,
run `:DenoteCopy {file}`.
- `:DenoteGrep /{pattern}/[g][j][f]`:
This command builds around
[:vimgrep](https://vimhelp.org/quickfix.txt.html#%3Avimgrep) and can be used to
search for patterns within your notes.
_Managing entries:_ You can change the title of denote entries using
`:DenoteSetTitle {newtitle}`. Also, you can add and remove tags using
`: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.
- `:DenoteBackReferences`:
With this, you may populate the location list with all links to the currently
opened note.
[^1]: Run `:help g:denote_directories` for more information.
- `:DenoteNew {title}`:
With this, a new note entry is generated with the supplied title.
### Basic keys
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`)
#### Key mappings
This package also defines the following interface for key mappings, which
automatically open the location window:
- `<Plug>DenoteList` to list all denote notes, and
- `<Plug>DenoteBackReferences` to list all back references.
So, you may want to configure your keys like this:
```
nnoremap <silent> <Leader>d <Plug>DenoteList;
nnoremap <silent> <Leader>D <Plug>DenoteBackReferences;
```
You may also find the following keys favorable for navigation:
```
### Example setup
You get a possibly useful setup with a single default denote directory at
`~/Documents/notes/` by placing these lines in your vimrc file:
```vim
au VimEnter * DenoteDirectory ~/Documents/notes/
nnoremap <silent> <Leader>d :Denote<CR>
nnoremap <silent> <Leader>D :DenoteBackReferences<CR>
nnoremap ]l :lnext<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
You can customize the behavior of this package using several global variables.
Customization is explained in the [help file](doc/denote.txt), also accessible
through `:help denote-settings`.
You can customize the behavior of this package using several
global variables. Customization is explained in the [help
file](doc/denote.txt), also accessible through `:help denote-settings`.
### Future features
These features are planned:
- Tag manipulation
- Title manipulation
- Signature handling?
- Note deletion?
- Subdirectories
- Denote query links

View File

@@ -1,19 +1,87 @@
" Link completion {{{1
set omnifunc=denote#completion#get
" Run this plugin only if the denote package has been setup
if !exists('g:denote_directory')
finish
endif
" Go to file command |gf| adjustments {{{1
" This resolves denote links. The function has access to the variable v:fname,
" which corresponds to the filename under the cursor.
function s:DenoteGotoFile()
return v:fname !~ "^denote:"
\ ? v:fname
\ : denote#meta#fileFromNoteId(v:fname[7:]) ?? v:fname
" ... and if this filetype is specified as denote-note file
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
" 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.
setlocal isfname+=:
" 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
command! DenoteBackReferences :call denote#loclist#references(denote#meta#noteIdFromFile(expand("%")))
" Back references command
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.
setlocal nospell
if getwininfo(win_getid())[0].loclist == 0
" Run this plugin only if the denote package has been setup
if !exists('g:denote_directory')
finish
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> 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

@@ -1,59 +1,9 @@
" Find the column where the completion starts. This must be between 1 and
" col('.'). Denote links are of this form: `denote:<identifier>`.
function s:column()
" Get the substring from the start of the line until col('.')
let l = getline(".")[:col('.')]
" Take the shortest prefix of a denote link. This may be any of
" \<d$
" ..
" \<denote:$
" \<denote:\f$
" \<denote:\f\f$
" etc.
let res = l->matchstrpos('\<denote:\f*$')
if res[1] >= 0
return res[1]
endif
let res = l->matchstrpos('\<d\f*$')
if res[1] == -1
return -3
endif
return 'denote:' =~ '^' .. res[0] ? res[1] : -3
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 res = []
for filename in flist
let noteId = denote#meta#noteIdFromFile(filename)
let noteTitle = denote#meta#noteTitleFromFile(filename)
if noteId == v:false || (noteId !~ '^' .. prefix && noteTitle !~ prefix)
continue
endif
let noteTitle = noteTitle ?? '(no title)'
let noteTags = denote#meta#noteTagsFromFile(filename)
let res = res->add({
\ 'word' : 'denote:' .. noteId,
\ 'abbr' : noteTitle,
\ 'menu' : noteTags->join(', ')
\ })
" 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 res
endfunction
function! Myomni(findstart, base)
if a:findstart == 1
let tmp = s:column()
return tmp
else
let tmp = s:suggestions(a:base)
return tmp
endif
endfunction
" Completion function for denote links
function denote#completion#get(findstart, base)
return a:findstart == 1 ? s:column() : s:suggestions(a:base)
return uniq(sort(l:tags))->join("\n")
endfunction

View File

@@ -1,59 +0,0 @@
" Functions to create the front matter {{{1
" Helper function to 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
" Create front matter
function denote#frontmatter#new(ft, id, title, tags=[])
return a:ft == 'org' ? s:org_new(a:id, a:title, a:tags)
\ : a:ft == 'plain' ? s:plain_new(a:id, a:title, a:tags)
\ : g:denote_fm_md_type == 'toml' ? s:md_new_toml(a:id, a:title, a:tags)
\ : s:md_new_yaml(a:id, a:title, a:tags)
endfunction

View File

@@ -5,10 +5,10 @@ endfunction
" Local helper function to retrieve and format the title.
function s:titleFromBuf(buf)
let name=denote#meta#noteTitleFromFile(bufname(a:buf))
return strchars(name, 1) <= g:denote_loc_title_columns
\ ? printf('%' .. g:denote_loc_title_columns .. 's', name)
\ : printf('%.' .. (g:denote_loc_title_columns - 1) .. 's', name) .. '…'
let l:name = libdenote#scheme_metadata(bufname(a:buf)).title
return strchars(l:name, 1) <= g:denote_loc_title_columns
\ ? printf('%' .. g:denote_loc_title_columns .. 's', l:name)
\ : printf('%.' .. (g:denote_loc_title_columns - 1) .. 's', l:name) .. '…'
endfunction
" 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
" This modifies the location list for pretty display.
function denote#loclist#textReferences(info)
let items=getloclist(a:info.winid)
let l=[]
let width=winwidth(0) - g:denote_loc_title_columns - 19
function s:textReferences(info)
let l:items=getloclist(a:info.winid)
let l:l=[]
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)
let e=items[idx]
let name=s:titleFromBuf(e.bufnr)
let lnum=printf('%5d', e.lnum)
let col=printf('%3d', e.col)
call add(l, name ..
\ ' | ' .. lnum .. ' col ' .. col ..
\ ' | ' .. s:formatText(items[idx].text, col, width))
let l:e=l:items[idx]
let l:name=s:titleFromBuf(l:e.bufnr)
let l:lnum=printf('%5d', l:e.lnum)
let l:col=printf('%3d', l:e.col)
call add(l:l, l:name ..
\ ' | ' .. l:lnum .. ' l:col ' .. l:col ..
\ ' | ' .. s:formatText(l:items[idx].text, l:col, l:width))
endfor
return l
return l:l
endfunction
" This modifies the location list for pretty display.
function denote#loclist#textNoteList(info)
let items=getloclist(a:info.winid)
let l=[]
function s:textNoteList(info)
let l:items=getloclist(a:info.winid)
let l:l=[]
for idx in range(a:info.start_idx - 1, a:info.end_idx - 1)
let e=items[idx]
let name=s:titleFromBuf(e.bufnr)
let ntags=denote#meta#noteTagsFromFile(bufname(e.bufnr))->join()
call add(l, name .. ' | ' .. ntags)
let l:e=l:items[idx]
let l:name=s:titleFromBuf(l:e.bufnr)
let l:ntags = libdenote#scheme_metadata(bufname(l:e.bufnr)).tags->join()
call add(l:l, l:name .. ' | ' .. l:ntags)
endfor
return l
return l:l
endfunction
" Load all references to the given note into the location list.
function denote#loclist#references(noteId)
" Populate location list
execute "lvimgrep /\\<denote:" .. a:noteId .. "\\>/gj *"
" Adjust location list: set title and specify display function
let file=denote#meta#fileFromNoteId(a:noteId)
let noteTitle=denote#meta#noteTitleFromFile(file)
" Re-populate location list with denote entries
function denote#loclist#fill(title, files, Gfun)
" Clear first
call setloclist(0, [], ' ')
" Set properties
call setloclist(0, [], 'r',
\ {'title': 'References to ' .. noteTitle .. ' (' .. a:noteId .. ')',
\ 'quickfixtextfunc' : 'denote#loclist#textReferences'})
\ {'title': a:title,
\ '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

View File

@@ -1,44 +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.
function denote#meta#noteTitleFromFile(filename)
return a:filename->matchstr("--\\zs.\\{-\\}\\ze\\(==\\|@@\\|__\\|\\..\\)")->substitute("-", " ", "g")
endfunction
" Return the note tags from the filename as a list.
function denote#meta#noteTagsFromFile(filename)
return a:filename->matchstr("__\\zs.\\{-\\}\\ze\\(==\\|@@\\|--\\|\\..\\)")->split("_")
endfunction
" Identifier creation
function denote#meta#identifier_generate()
if g:denote_identifier_fun
return execute "call " .. g:denote_identifier_fun .. "()"
endif
return exists("*strftime")
\ ? strftime("%Y%m%dT%H%M%S")
\ : rand()
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 18
*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|
2. Commands |denote-commands|
2.1 Universal commands |denote-universal-commands|
2.2 Note command |denote-note-command|
2.3 Denote-list commands |denote-list-commands|
3. Settings |denote-settings|
4. Mappings |denote-mappings|
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 ~
INTRODUCTION *denote-intro*
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
@@ -21,87 +37,385 @@ do adjusting the tags or changing the title of a note. The official manual is
available online:
https://protesilaos.com/emacs/denote
The denote plugin adds denote functionality to vim --- _the vim way._ We are
aware of two other plugins, but we fear that these plugins are not flexible
enough, and more importantly, do not follow the vim philosophy for handling
denote notes. These mentioned plugins are available here:
The denote plugin adds denote functionality to vim the vim way. We are aware
of two other plugins, but we fear that these plugins are not flexible enough,
and more importantly, do not follow the vim philosophy for handling denote
notes. These mentioned plugins are available here:
https://github.com/shuckster/denote-md
https://git.sr.ht/~ashton314/vim-denote
In contrast to these plugins, the present package relies on the
|location-list| features for displaying denote entries, and on the options
|'quickfixtextfunc'|, |'includeexpr'|, and |'omnifunc'|. With these, denote
links can be followed using |gf|, and completed using omni comletion (see,
'quickfixtextfunc', 'includeexpr', and 'omnifunc'. With these, denote links
can be followed using |gf|, and completed using omni completion (see,
|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.
*denote-commands*
Commands ~
*:Denote*
Populate the location list of the current window with the denote entries
present in the current directory. This command may be supplemented with any
number of arguments that filter the entries.
*:DenoteTag*
This command takes as argument a tag, and populates the location list with all
denote entries of that are tagged accordingly. The tags are autocompleted
(see, |c_<Tab>|).
==============================================================================
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*
:Denote [{keyword ..}]
Populate the location list of the current window with the denote
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.
*:DenoteByTag*
:DenoteByTag {tag}
This command takes as argument a tag, and populates the location list
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*
This command is a wrapper around |:lvimgrep| to search for a pattern in the
denote entries. The required argument is a pattern as required by |:vimgrep|,
i.e., /{pattern}/[g][j][f].
:DenoteGrep /{pattern}/[g][j][f]
This command is a wrapper around |:lvimgrep| to search for a pattern
in the denote entries. The required argument is a pattern as required
by |:vimgrep|, i.e., /{pattern}/[g][j][f].
*:DenoteNew*
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|.
: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*
When called from an opened denote entry, this command populates the location
list with all references to the current note.
:DenoteBackReferences
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-list-commands*
Denote-list commands~
The denote list may be opened using one of the commands |:Denote| or
|: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
>
let g:denote_directories = []
<
*denote-settings*
Settings ~
*g:denote_note_file_extension*
With this setting you may specify the file extensions of all denote entries
within which |:DenoteGrep| will search for the provided pattern. If left
unspecified, it is set to the following default value:
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_note_file_extension = ['md', 'org', 'txt']
let g:denote_note_file_extension = ['md', 'org', 'txt']
<
*g:denote_loc_title_columns*
This integer specifies the number of columns used to display the titles of
denote entries. Per default, it is set to:
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:
>
g:denote_loc_title_columns = 60
let g:denote_loc_title_columns = 60
<
*g:denote_new_ft*
Newly created notes are of this file type. Possible values are 'md', 'org', or
'txt', with the following default:
g:denote_new_ft string
Newly created notes are of this file type. Possible values are 'md',
'org', or 'txt', with the following default:
>
g:denote_new_ft = 'md'
let g:denote_new_ft = 'md'
<
*g:denote_fm_md_type*
The front matter of 'md' notes is given as 'yaml' or as 'toml'. By default,
this package uses 'yaml':
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':
>
g:denote_fm_md_type = 'yaml'
let g:denote_fm_md_type = 'yaml'
<
*g:denote_identifier_fun*
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.
*denote-mappings*
Mappings ~
*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') }
<
<Plug>DenoteList "Populate and open the location list with all
denote entries".
<Plug>DenoteBackReferences "Populate and open the location list with all
denote entries that link to the currently opened one".
==============================================================================
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,18 +1,28 @@
" 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')
let g:denote_note_file_extensions = ['md', 'org', 'txt']
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')
let g:denote_loc_title_columns = 40
endif
" Default filetype for newly created denote entries
if !exists('g:denote_new_ft')
let g:denote_new_ft='md'
if !exists('g:denote_new_ext')
let g:denote_new_ext = 'md'
endif
" Default front-matter type for markdown notes, may be one of 'yaml' or 'toml'
@@ -22,101 +32,88 @@ endif
" By using the following global variable, the user may specify a custom
" function for creating identifiers.
if !exists('g:denote_identifier_fun')
let g:denote_identifier_fun=''
if !exists('g:Denote_identifier_fun')
let g:Denote_identifier_fun = function('libdenote#scheme_idgen')
endif
" Local functions {{{1
" Put all notes of the given tag to the location list. The tag argument is
" mandatory.
function s:DenoteNotesByTag(tag)
" Clear location list
call denote#loclist#clear()
" Find files
let files = glob("*_" .. a:tag .. "*", 0, v:true)->filter('v:val =~ "_' .. a:tag .. '\\(==\\|@@\\|__\\|_\\|\\.\\)"')
" Populate location list
let locTitle="Denote notes: " .. a:tag
call setloclist(0, [], 'r',
\ {'title': locTitle,
\ 'quickfixtextfunc' : 'denote#loclist#textNoteList'})
let notes=[]
for f in files
call add(notes, {
\ 'filename' : f,
\ 'lnum' : 1
\ })
" Transform full path into canonical form WITH trailing '/'
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
call setloclist(0, notes, 'a')
return empty(l:result) ? '/' : '/' .. join(l:result, '/') .. '/'
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)
let s = substitute(" " .. a:search .. " ", " ", "*", "g")
" Clear location list
call denote#loclist#clear()
" Find files
let files = glob(s, 0, v:true)
" Populate location list
let locTitle="Denote notes search:" .. a:search
call setloclist(0, [], 'r',
\ {'title': locTitle,
\ 'quickfixtextfunc' : 'denote#loclist#textNoteList'})
let notes=[]
for f in files
call add(notes, {
\ 'filename' : f,
\ 'lnum' : 1
\ })
" Compute the relative path from start to target. Both arguments are given as
" full paths.
function s:relativePath(start, target)
let l:a = s:canonicalFullPath(a:start)
let l:b = s:canonicalFullPath(a:target)
" Simple cases first: both paths are the same, or the target is l:a subpath
" from the start.
if l:a == l:b[:strlen(l:a)-1]
return l:b[strlen(l:a):]
endif
" Now, we need to go back. If the following match fails, then we need to go
" back all the way
let l:l = matchstrpos(l:a .. l:b, '^\(/.*\)/\zs.*/\ze\1')
return l:l[1] == -1
\ ? substitute(l:a[1:-2], '[^/]\+', '..', 'g') .. l:b
\ : substitute(l:l[0], '[^/]\+', '..', 'g') .. l:b[l:l[1]:]
endfunction
" 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
call setloclist(0, notes, 'a')
endfunction
" This function implements the similar functionality of :vimgrep, but for
" denote notes.
function s:DenoteGrep(search)
" 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))
else
let l:leaddir = s:canonicalFullPath(fnamemodify(l:argleaddir, ':p'))
for ldir in g:denote_directories
if l:leaddir == ldir[:strlen(l:leaddir)-1]
call add(l:res, l:argleaddir .. s:relativePath(l:leaddir, ldir))
endif
endfor
return uniq(sort(tags))->join("\n")
endif
return l:res->join("\n")
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)
let identifier=denote#meta#identifier_generate()
let fn=identifier .. '--' .. a:title
\ ->tolower()
\ ->substitute('[^[:fname:]]\|/', '-', 'g')
\ ->substitute('-\+', '-', 'g')
\ ->trim('-') .. '.' .. a:ft
execute "edit " .. fn
call setline(1, denote#frontmatter#new(a:ft, identifier, a:title))
" Setup denote plugin, i.e.,
" - set the denote directory
" - register user commands
" - 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
" Public commands and key mappings {{{1
command -nargs=* Denote :call <SID>DenoteNotes(<q-args>)
command -nargs=1 -complete=custom,<SID>tagList DenoteTag :call <SID>DenoteNotesByTag(<q-args>)
command -nargs=+ DenoteGrep :call <SID>DenoteGrep(<q-args>)
command -nargs=1 DenoteNew :call <SID>DenoteNew(<q-args>)
" Useful key mappings
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>
command -nargs=1 -bang -complete=custom,s:denoteDirectoryCompletion DenoteDirectory call s:setup(<f-args>)