Creating and linking Zettelkasten notes in Vim
This post is part of the Workflow series.
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 “
nnoremap <leader>nz :NewZettel
Done! Now let’s solve the real problem of effortlessly linking to this note. Warning: it gets pretty sexy ahead.
Using fuzzy finding (CtrlP) to create formatted Markdown links to files ¶
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 l:filename_wo_timestamp = fnameescape(fnamemodify(a:line, ':t:s/\(^\d\+-\)\?\(.*\)\..\{1,3\}/\2/'))
let l:filename_wo_timestamp = substitute(l:filename_wo_timestamp, "_", " ", "g")
" 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." )"
put=mdlink
else
" Use CtrlP's default file opening function
call call('ctrlp#acceptfile', [a:action, a:line])
endif
endfunction
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.
EDIT 09/02/2021: a previous version of the post did not escape the +
regex modifier, which is necessary in the vim regex dialect.
As a result, the timestamp was not correctly removed in the created link descriptions.
The screencast below uses the old incorrect version.
EDIT 22/09/2022: I updated the regular expression and additionally remove underscores from file paths.
The behavior of the regex is as follows: 20210340-note.md
becomes [note]( 20210340-note.md )
; note.md
becomes [note]( note.md )
and index_notes.md
becomes [index notes]( index_notes.md )
.
Comments
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:
Reply to Fernando
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:
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?
Thanks
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:
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
tocall 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 callfzf#vim#complete
usingrg
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.
Code
Edwin on Sunday, May 10, 2020
In reply to Fernando
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:
rg
. What are the up- and downsides?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. https://www.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:
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’sgf
command no longer works. In order to overcome this I use the plasticboy/vim-markdown plug which allows to usege
instead ofgf
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!
Jared on Friday, Jul 31, 2020
In reply to Fernando
A little bit that I added to the fzf function that maybe someone will find useful. (everything before put, the first five lines, is exactly the same)
In short this is two way linking. So if file A is open and we insert a link to file B with the script B my addition would also read B and append to the file a link to file A A
For the uninitiated the % in vim represents the current file, but when using it in a function it is treated differently so you have to use the expand function to expand the variable to the filename. The final line is the one that does the writing to the other file. readfile grabs the contents of a file and turns it into a list object. Add appends to a list, and writefile takes a list and writes it to a file.
Final bit you could also use the file completion mapping to call the function (though I would recommend moving your things to a ftplugin file then so it doesn’t map it that way everywhere). Basically for our fzf function that would look like this. (the only works inside ftplugin so if you put this in your general vimrc remove the )
Also an alternate way of doing your new zettel command could be to do this instead.
I prefer this way because I am used to hitting : to deal with files, rather than my leader key which I use to do other things. There are some slight drawbacks in comparison. For one if you type nz anywhere in the command line it will replace the nz with the whole command. Could be annoying if you have notes on New Zealand. Second you would still have to type .md but I know I would do that anyway if I used your command and I would end up with a lot of files ending with .md.md.
Just to break one thing down for those that don’t know it Ctrl-r is how you access your registers (where you copy paste from) when you aren’t in normal mode. and the “=” register just takes a function and pastes the output in place.
Personally I write my notes before I am ready to save them and I am already in the directory so mine looks like this.
Edwin on Sunday, Aug 2, 2020
In reply to Fernando
@Jared, great stuff! Thanks for putting effort into the thorough explanation! It’s time I’ll look into fzf myself, now that I have some free time on my hand. I’ll definitely have a closer look at your contribution!
P.S. sorry for the late acceptation of your comment; I do moderation by hand and I’ve been travelling.
Jared on Sunday, Aug 2, 2020
In reply to Fernando
Okay so on my above comment I screwed up some sentences because I didn’t account for the markdown syntax.
That first paragraph after the first codeblock the links in there were me trying to show an example of the markdown link. Of course those got translated into html links so that doesn’t show. Also in the paragraph before the second codeblock I was trying to warn people not to include the buffer statement if it is in your general vimrc rather than ftplugin. the buffer is between two triangle brackets so the markdown disappeared the word which is why the sentence trails off.
@edwin thanks for saying I gave a through explanation after looking at my comments I am beginning to think I might be long winded.
alex on Friday, Apr 24, 2020:
Reply to alex
Edwin on Friday, Apr 24, 2020
In reply to alex
alex on Friday, Apr 24, 2020:
Reply to alex
Edwin on Friday, Apr 24, 2020
In reply to alex
Robert on Monday, May 11, 2020:
Reply to Robert
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.
Jared on Saturday, Aug 1, 2020:
Its me again. A couple other things I thought could be helpful. I grabbed this bit of code from a commit to the markdown syntax file that never made it in. You would place this in .vim/after/syntax/markdown.vim. It is a syntax file that turns concealing on for markdown links. So your link would go from this A.md to looking like this [A.md]. The stuff inside () is still there of course it is just “concealed”. When the cursor is in the same line it it would expand out to the full set again, or be revealed. Although if you don’t want it displayed at all you could also add, to the code below, setlocal concealcursor=nvic.
Final bit you can add this into an ftplugin to change this mapping only for markdown files, or only for you notes directory if you set up an ftdetect. Basically the mapping takes the follow file commands and alters them to move to the next ( and then do the command. This way if your cursor is on a link inside [] it will move to the file inside () and follow that file. This even works if you use the conceal cursor from above and are unable to see the ().
Reply to Jared
Edwin on Sunday, Aug 2, 2020
In reply to Jared
Welcome back! A while back I also wrote code for concealing markdown syntax, but borrowing it from a commit like you did would have been more efficient :-). Before that I tried the vim-markdown plugin, which also has a conceal function, but I really disliked that it forced some stylistic choices on me. However, ultimately I found out that the pandoc plugin I use also already offers proper concealing of markdown links (duh!). I use that now.
If I remember correctly, this should already do the trick (with
set conceallevel=1
):With regards to your second tip: awesome! Honestly, it’s so straightforward and yet I didn’t think of doing it myself like that. There are plugins for just following markdown links, but I also didn’t like it (although I don’t remember my particular issues with it). So thanks!
Small remark, for the future: in your comments you used four backticks for code blocks instead of three, which broke the conversion to html :-)
Jared on Sunday, Aug 2, 2020
In reply to Jared
Ha so I was talking about markdown syntax for links on a forum that takes markdown syntax so a couple of my comments are confusing because the syntax got converted into links. I assume code block should fix that So this sentence.
Is supposed to read like this. (I only used three ticks this time.) :)
Yeah I felt clever getting it from the commit. Yeah I used that pandoc plugin for awhile but then I realized I only used like two of its features so I got rid of it,
You are right conceallevel 1 is enough.
I tried to take the link following function out of the vim-markdown plugin but my head kind of exploded trying to take his function for that and get it to work independently. So I figured I should just come up with an easy solution. I like tinkering and pretending to be an elitist so I try to take code snippets, or do things myself instead of installing plugins.
Bruce Dillahunty on Saturday, Jan 2, 2021
In reply to Jared
Just another options for the fzf users is to use just the basic fzf.vim plugin with a couple of tweaks to let you just copy the filename instead of open it. Right now I’m going this way as I already have fzf tweaked with the preview window settings and all that I like and am used to.
The downside of this is that (so far :-)) it doesn’t actually create the md formatted link, but requires you to paste it back in manually. I want to automate that a bit more, but…
Mostly this just comes from here:
This just adds a “Ctrl-Y” option to copy the filename(s) that you have selected.
anon on Monday, Sep 7, 2020:
Reply to anon
Edwin on Tuesday, Sep 8, 2020
In reply to anon
Anton on Saturday, Sep 19, 2020:
Hi Edwin, great blog series! How are the updates coming along? Still working on the sneak preview you gave us at the beginning of the post?
I made a back-link viewer:
It just does a full text search with ripgrep for markdown links to the current file and opens all results in a quickfix list. This makes it very easy to see which files link to the file you have currently open. What do you think?
Cheers
Reply to Anton
Edwin on Sunday, Sep 20, 2020
In reply to Anton
Great stuff! Your approach definitely makes sense and with ripgrep it’s definitely fast enough. Are you on a Unix-like system? Your command didn’t work for me on my Windows desktop and it took me a while to figure out why. It was very silly: I had to switch the single and double quotes around. It probably has to do with powershell handling quotes differently (sigh, oh well). This is the new command:
command! -nargs=0 Ngrepl :execute 'grep -F "' . ' ]( ' . expand("%:t") . ' )' . '"' | :copen 1
What’s nice is that the regular grep command should also work, because it also has the -F flag (didn’t test though). One potential caviat is that the above assumes you use spaces around the filename. I enforce that anyways in the automatic linking function because then
gf
will work as intended. But still, it may be something to take into account for others. If it turns out to be an issue for some reason the function could be simplified by just searching for the exact filename, but this may return too much.Anyways, thanks for your interest! I did in fact write a working solution for managing backlinks in a dedicated markdown section in the notes themselves. These are generated/updated for all notes by running a single command, which takes about a second to complete. An advantage is that you then no longer have to do any grepping. Instead you can just follow the links with
gf
as usual. Another edge case I considered is that people may want to automatically convert their notes into a website. Then it’s also handy to have all links in the files themselves.I’ve been a bit hesitant to publish the code because it’s an external Python script. So far I managed to not rely much on external tools. What I should do, really, is rewrite the whole thing using Python within Vim and offer it to readers of this blog as a plugin. It’s possible to write Vim plugins in Python but I’ve never done it before. I can of course also just publish the WIP, because readers enjoy hacking it to fit their own preferences anyways :-)!
But part of why I haven’t published yet is also that I’ve simply been lazy this summer ;-)!
Anton on Saturday, Sep 26, 2020
In reply to Anton
Haha that sucks, yea I’m on a Mac. Great point about having the links in the note. There’s always room for improvement :).
Take it easy, I’ll be waiting for the next post. It would be interesting to see a post about the custom plugin, never done that before.