home   Archives  About  Etc  Authors

Creating and linking Zettelkasten notes in Vim

This is the third post in a series of sorts about note-taking in Vim. I have silently kept playing around with the system outlined in the previous posts (-1,-2). Some things I have abandoned, some are improved and some are changed. I have inserted several updates (marked as “UPDATE”) in the previous posts in case you are curious. If we have reached some sort of equilibrium at the end of this series I’ll make sure to create a place where people can easily download all relevant configuration and used scripts, but for now everything is a matter of “voortschrijdend inzicht,” a beautiful Dutch phrase that’s hard to translate and certainly hard to pronounce for most of my readers. Given the fact that my previous posts on Vim are well-received and several people are trying it out, it’s time to pick up writing again and start chipping away at the backlog.

To give you a taster of what’s to come:

  • various surprising uses of back-linking between files
  • a script for automatically updating them in-place in a dedicated section each note
  • exploring methods of visualizing relations between notes …
  • … and ideally using this visualizing for some free-roaming navigation as well.

In this post, I want to discuss a seemingly minor issue that will nevertheless potentially have a big impact on your workflow. It concerns the quick creation of new timestamped notes in your note directory or Zettelkasten, and then easily creating a correctly formatted Markdown link to it from another note. If you are impatient, you can have a look at the screencast below. If not, let me give a brief introduction to show you where the potential workflow issue is.

What’s the issue I’m trying to fix?

The authors of this nice Zettelkasten blog argue that you should give up trying to categorize your notes in hierarchical folders and instead should throw everything into one big flat Zettelkasten. This is scary, because notes that do not have many interconnections with other notes may be forgotten when the Zettelkasten gets big (it will be forgotten by you for sure, but also “forgotten” by the Zettelkasten itself if it lacks links). Nevertheless, I’m making the transition because I want to commit to the idea of my note collection being dynamic, organic, an entity of its own, rather than it being a static dump. In order to make this transition, you start to fully rely on your tools. Since I’m hacking together my own tools some issues came up, in this case with using timestamps in filenames.

The O.G. Zettelkasten of Luhmann had an extensive naming convention for organizing notes, but it was more of a necessary evil because computers were not in the picture yet. Given that we now have digital means of naming, searching, and linking notes, a strict naming convention for the notes is an unnecessary complication that blindly applies an “analogue” mindset to a digital solution. The authors from Zettelkasten.de are however strong proponents of the more superficial organization of notes by their time of creation, which they do by inserting a unique timestamp at the beginning of the filename. For me, the best argument for this approach is that unique timestamps are a good way of recovering links through potential filename changes. The main reason I did not use them was however that I used Vim’s default filename/path completion (C-x C-f in insert mode) when making Markdown links. This worked fine for me as long as filenames are meaningful, but this just doesn’t cut it anymore when all filenames start with a timestamp, as you would have to manually start typing the timestamp. An early adapter of my Vim experiment, Boris, did however use long complex timestamps and noticed interlinking was getting in the way of his workflow. Since he now makes all his notes for his PhD in Vim, I certainly do not want to be responsible for trouble! So here it goes …

Create a timestamped Markdown note in your Zettelkasten

Before we solve the bigger issue, let’s add some convenience. When using timestamps, manually typing out the date and time is a pain in the ass. Each timestamp needs to be a unique identifier, so this means you at least also want to include the time of day, potentially up to the amount of seconds if you regularly make multiple notes within a minute. I don’t personally, but the code below is very easy to adjust to your own needs.

First, we declare a variable that holds the location of our Zettelkasten, so we may use it in multiple places without having to retype the whole path.

let g:zettelkasten = "/home/edwin/Notes/Zettelkasten/"

Second, we want to define our own custom command that 1) pre-fills all the stuff we don’t want to type, namely the timestamp and the extension (I always use markdown), and 2) that prompts you for the name of your note:

command! -nargs=1 NewZettel :execute ":e" zettelkasten . strftime("%Y%m%d%H%M") . "-<args>.md"

This will produce a filename like “201704051731-my_awesome_note.md”. Don’t bother with understanding this. Writing it certainly gave me a headache because I’m new to Vimscript. What is interesting for you is “%Y%m%d%H%M” because it indicates how you want to format your datetime. You can read about this by typing :help strftime and otherwise this is a good resource.

Now all we have to do is declare a mapping to call our command. I use the “n” prefix for all note-related stuff I write myself, so I choose “nz”, which just also happens to be a mnemonic for new zettel.

nnoremap <leader>nz :NewZettel 

Done! Now let’s solve the real problem of effortlessly linking to this note. Warning: it gets pretty sexy ahead.

The main issue was that we never want to type timestamps in order to reap the benefits of path completion to get a Markdown link to the file we want. Now that we are at it, having to format a Markdown link like [description](link) also takes time, so let’s automatize that as well.

My new solution is to rely on my fuzzy file finder to find a file and automatically create a markdown link to it. I use CtrlP with ripgrep, but fzf is also a great choice, see fernando’s comment for a fzf solution. This is a great solution because the fuzzy nature of it allows you to ignore the timestamp altogether. But it also allows you to search on a partial fragment of the time and a part of the note title. I can imagine you for example remember making a note about Zettelkasten somewhere in 2020, but you don’t quite remember the exact date (unless you are Rain Man) and neither the precise name of the file. No problemo! Boot up CtrlP and search on “2020Zettelkasten”. We can extend CtrlP to then automatically create a markdown link to the matching file, with Ctrl-X. Have a look at the short screencast I made.

I started with code provided in this StackExchange post and adjusted it to create valid Markdown links:

" CtrlP function for inserting a markdown link with Ctrl-X
function! CtrlPOpenFunc(action, line)
   if a:action =~ '^h$'    
      " Get the filename
      let filename = fnameescape(fnamemodify(a:line, ':t'))
	  let filename_wo_timestamp = fnameescape(fnamemodify(a:line, ':t:s/\d+-//'))

      " Close CtrlP
      call ctrlp#exit()
      call ctrlp#mrufiles#add(filename)

      " Insert the markdown link to the file in the current buffer
	  let mdlink = "[ ".filename_wo_timestamp." ]( ".filename." )"
      " Use CtrlP's default file opening function
      call call('ctrlp#acceptfile', [a:action, a:line])

let g:ctrlp_open_func = { 
         \ 'files': 'CtrlPOpenFunc',
         \ 'mru files': 'CtrlPOpenFunc' 
         \ }

I just love it. Irregardless of whether I will use timestamps in my filenames, this will greatly speed up interlinking notes in my Zettelkasten.


Latest: Russell on AI in technocracy and surveillance

Next: Russell on AI in technocracy and surveillance

Previous: Setting up your own tilde club (UNIX)


Do you want to link a webmention to this page?
Provide the URL of your response for it to show up here.


Fernando on Thursday, Apr 23, 2020:

For those using fzf instead of CtrlP, with help from Zorzi (https://vi.stackexchange.com/questions/24779/how-can-i-get-a-path-with-fzf-vim-and-use-to-insert-a-snippet)

you can do:

function! HandleFZF(file)
    "let filename = fnameescape(fnamemodify(a:file, ":t"))
    "why only the tail ?  I believe the whole filename must be linked unless everything is flat ...
    let filename = fnameescape(a:file)
    let filename_wo_timestamp = fnameescape(fnamemodify(a:file, ":t:s/^[0-9]*-//"))
     " Insert the markdown link to the file in the current buffer
    let mdlink = "[ ".filename_wo_timestamp." ]( ".filename." )"

command! -nargs=1 HandleFZF :call HandleFZF(<f-args>)

Edwin on Friday, Apr 24, 2020
In reply to Fernando

That’s very useful! That’s my official debute on stack exchange, by the way, so thanks for that. I’ve actually meant to mention in the post that this should be easy in fzf as well. This is a great addition, because I know most people prefer fzf. The only reason I do not use fzf for this notetaking system is that I require it to be cross-platform and fzf doesn’t work great on Windows. I’ll refer to your comment in the post 😊.

Just as a sidenote, one fzf-related thing I’d like to try out (but I didn’t yet) is fzf notational velocity.

Edwin on Friday, Apr 24, 2020
In reply to Fernando

Oh and I see I overlooked a question hidden in the code. “Why only the tail?”. That’s actually a better question than you may initially suspect. I’ve struggled a bit with this and there’s multiple layers to my answer.

First the practical answer: for Vim it’s not necessary to include the whole (relative) path if you set Vim up this way:

" Include subfolders
set path+=**

This way, Vim will find your file anyways with gf, even if the note is in a different folder, assuming you follow the convention I use that your working directory is the root directory of all your notes.

The real answer is a bit more intricate, because I additionally want all Markdown links to work when I export all my files as a website, or preview them on GitHub. With your current solution, the link would look like this if I’m not mistaken “Zettelkasten/somenote.md”. This will work for Vim because your working directory is the root directory of your notes, but it won’t for example on GitHub. It would look for Zettelkasten/Zettelkasten/somenote.md . You could fix this by instead using “../Zettelkasten/somenote.md”, but this will break your linking in Vim!

TLDR; unless my reasoning is flawed, this will force you into some hybrid linking convention where you’ll maintain references relative to your Vim working directory and links that function anywhere, relative to your specific note. You could use the Vim link (just the filename with the path setting from above) as the link description, and the other as the actual link.

This all made me commit to the Zettelkasten method because they work with a flat directory anyways, which solved everything. Additionally, having a flat directory makes writing my own tools so much easier.

Robert on Thursday, May 7, 2020
In reply to Fernando

Thanks for creating the code for fzf. But I can’t figure out how to use it.

I added to my vimrc and when I use the :HandleFZF I see it needs an argument after the command. I can type something after and a link is created with the text I typed but I am not sure how to to use FZF with it.

Could you provide a little explanation on how to use it?


Edwin on Thursday, May 7, 2020
In reply to Fernando

Hi Robert, I haven’t tried this myself but what you want is to let fzf autofill the argument of the HandleFZF function and call it. So you are right that you are not supposed to call it manually. Fernando left out the following line:

call fzf#run({'sink': 'HandleFZF'})

This should dump the currently selected file into the HandleFZF function. As I don’t use FZF myself I’m not sure if there is a default keychord that triggers this hook. What should work in any case is defining a key mapping with nnoremap to call fzf#run({'sink': 'HandleFZF'}). Please let us know if this works so that others won’t run into the same issue! :-)

Robert on Thursday, May 7, 2020
In reply to Fernando

Edwin, Your addition was perfect the script now runs. Thanks for the help.

I am new to vimscript but I hope to take your link idea along with Fernando’s and add Silver Searcher to it.

fzf.vim along with Ag (silver sercher) allows you to search for a term(s) in documents and gives you a preview of the document(s) before you open it(them). I open to take that and add the capability to create a link from the selected documents into the current buffer.

Edwin on Thursday, May 7, 2020
In reply to Fernando

Great! That’s cool! It sounds like you are looking for notational-fzf-vim. Vimscript can be a hassle by the way. For a less trivial things I tend to switch to Python (you can also use Python from within Vimscript, just so you know).

If you have a nice workflow going at some point, I would be glad to hear it!

hbenevides on Sunday, May 10, 2020
In reply to Fernando

Hey guys,

Based on your comments I wrote a script that inserts links using a custom fuzzy completion (from fzf.vim, so don’t need to exit vim insert mode to create links. First, I mapped <c-l>z in insert mode to call fzf#vim#complete using rg to match any line starting with a # (title of notes). The result, then, is send to another function which creates the link.

P.S: I use only timestamps in my filenames.


" make_note_link: List -> Str
" returned string: [Title](YYYYMMDDHH.md)
function! s:make_note_link(l)
        " fzf#vim#complete returns a list with all info in index 0
        let line = split(a:l[0], ':')
        let ztk_id = l:line[0]
        let ztk_title = substitute(l:line[1], '\#\s\+', '', 'g')
        let mdlink = "[" . ztk_title ."](". ztk_id .")"
        return mdlink

" mnemonic link zettel
inoremap <expr> <c-l>z fzf#vim#complete({
  \ 'source':  'rg --no-heading --smart-case  ^\#',
  \ 'reducer': function('<sid>make_note_link'),
  \ 'options': '--multi --reverse --margin 15%,0',
  \ 'up':    5})

Edwin on Sunday, May 10, 2020
In reply to Fernando

@hbenevides thanks for sharing! This would be a great guest post, if you care to explain your rationale :-) Feel free to email me if you are interested.

hbenevides on Sunday, May 10, 2020
In reply to Fernando

@Edwin, that would be cool. I’m not sure if I have a rationale. Perhaps with this it is possible to reduce friction and also have good-looking notes =)

BTW, great job in this series of posts! Useful content expressed in good written.

(Apologies for the typos in the previous comment =|)

Edwin on Monday, May 11, 2020
In reply to Fernando

Thanks for the compliment! It is perfectly fine to keep a post very simple and minimal, even better I would say. You could write it as a response/addition. I think it would already be valuable to:

I’m still struggling with the best naming convention, so I’m really interested in the whole timestamp-for-filename thing. If I would switch, I would also need a method for finding the correct note based on its title. (So assuming you write a guest post, I may end up responding to it with a CtrlP version!).

In short, I strongly encourage you to give it a try! I’m sure other people would be interested as well and above all, it’s fun!

Robert on Monday, May 11, 2020
In reply to Fernando

Edwin and hbenevides,

I took hbenevides script and modified it so it works with silver searcher and presents a preview box. Rather than throw the script in the comments I posted it on my blog. http://frrobert.com/blog/linkingzettelkasten-2020-05-11-0735 I hope it helps.

Edwin on Monday, May 11, 2020
In reply to Fernando

@Robert That’s great! I have to step up my game now to match the combined effort of all of you! I’m curious where CtrlP will fall short of fzf when it comes to customization. Great minimalistic website, I subscribed to your RSS so I’ll be following you (many years ago since I translated some Greek, hey, maybe I pick some up again).

I am still interested in having this information on my website in a place that is more accessible than the comments, because I think it’s a great addition to this post. It would be easier to find for people. It could be a cross-post of your blog @Robert, or a new write-up by @hbenevides. I could try out fzf myself and do it, but since it is a combined effort of all of you, you should take the credit :-)

nv2lt on Saturday, May 23, 2020
In reply to Fernando

This is an excellent post Edwin. Well done! In reply to your comment in 24 Apr 2020:

The real answer is a bit more intricate, because I additionally want all Markdown links to work when I export all my files as a website, or preview them on GitHub. With your current solution, the link would look like this if I’m not mistaken “Zettelkasten/somenote.md”. This will work for Vim because your working directory is the …

In order to work in Gitlab (I assume that the same applies for Github) I made the following modification to Fernardo’s script: let mdlink = "[".filename_wo_timestamp[:-4]."](./".filename[:-4].")" This removes .md suffix and also adds the ./ in order to make it work for Gitlab wiki pages. By doing this you can have nested folders and all links in Gitlab wiki will work. The problem now is that by removing .md suffix the vim’s gf command no longer works. In order to overcome this I use the plasticboy/vim-markdown plug which allows to use ge instead of gf to follow markdown link inside vim. It also allows to have link to Headings inside a file like file. In this scenario you can have the same pattern work both inside vim and in gitlab (probably github also).

Congrats again!

Edwin on Saturday, May 23, 2020
In reply to Fernando

@nv2lt thanks for your reply and your addition! I personally prefer to not rely on plasticboy/vim-markdown (I like vim-pandoc), but this is a great suggestion for those that prefer to keep nested directories! This again shows the beauty of the Vim-route: everyone can find a way to adjust it to their preferences.

Thanks a lot for dropping by!

alex on Friday, Apr 24, 2020:

Strong post! A technical post that is practical, yet unheard of. Unheard of in the way of introducing a new idea that might be valuable to you. 👏

Edwin on Friday, Apr 24, 2020
In reply to alex

That’s a great compliment, thanks!

alex on Friday, Apr 24, 2020:

Now make a graphly-linked note taking plugin for vim :p

Edwin on Friday, Apr 24, 2020
In reply to alex

Well that will take me a while :-D. However, I am experimenting with different ways of visualizing and graphically browsing notes, so you can expect a post about that (at some point in the not so near future). If you have time for something like that, it would be a great guest contribution to this project! ;-)

Robert on Monday, May 11, 2020:

@Edwin In terms of controlP vs fzf with ripgrep or silver searcher, fzf, ripgrep, and silver searchers are command line programs that can be used in vim via a wrapper or plugin. I was already using fzf and silver searcher on the command line before I started using it in Vim. FZF is a Swiss Army Knife type of application, I can use it in Vim, to select music for mpd, or many other tasks. I use it because that is what I heard of first and it is so versatile.

Edwin on Monday, May 11, 2020
In reply to Robert

Oh yeah fzf is great! My comment about customization above wasn’t that clear. The only reason I don’t use rely on fzf.vim is because I want my setup to work effortlessly on any OS. fzf works amazing on Linux, but I had trouble with it on Windows (I don’t necessarily blame fzf for that, by the way). The mileage of command-line tools is just less on Windows because the command line works different, although there is improvement in that area. Anyways: that’s what I meant with customization. CtrlP actually doesn’t have external dependencies by default and works in any Vim install. So I was wondering if a setup that’s very dependent on external tools would work that well on an OS like Windows. Additionally, I suspect some nice functionality of fzf.vim may not be in CtrlP, but I should explore that a bit more.

Having said that, only one way to find out. I should give the fzf + ag setup a go on Windows.

Leave a comment

Thank you

Your post has been submitted and will be published once it has been approved.


Something went wrong!

Your comment has not been submitted. Return to the page by clicking OK.