Emacs: Automating Table of Contents Update for Markdown Documents (e.g., README.md)

When working with markdown files in Emacs (e.g., README.md), users may need to manually update the table of contents. Automating this process saves time and ensures that the table of contents remains consistent with the document structure. This article presents an Emacs Lisp code snippet that uses:

  • The markdown-toc package to automatically generate or refresh a table of contents,
  • A custom function (my-markdown-toc-gen-if-present) that runs before markdown files are saved. It performs the following actions:
    • Updates the table of contents if one is already present.
    • Ensures that both the window start and cursor position remain unchanged, addressing a common issue with the markdown-toc package, which can disrupt the editing flow by moving the cursor and/or changing the window start. This behavior can be frustrating, as it interrupts the user’s focus and requires them to navigate back to their original position.

The code snippet that updates the table of contents and ensures that the cursor and window start remain unchanged

The following code snippet updates the table of contents while ensuring that the cursor and window start remain unchanged:

(The table of contents will be updated only if it is already present, so it is necessary to generate it at least once using the (markdown-toc-generate-toc) function. Additionally, ensure that the markdown-mode Emacs package is installed.)

;; Author: James Cherti
;; URL: https://www.jamescherti.com/emacs-markdown-table-of-contents-update-before-save/
;; License: MIT

;; Configure the markdown-toc package
(use-package markdown-toc
  :ensure t
  :defer t
  :commands (markdown-toc-generate-toc
             markdown-toc-generate-or-refresh-toc
             markdown-toc-delete-toc
             markdown-toc--toc-already-present-p))

;; The following functions and hooks guarantee that any existing table of
;; contents remains current whenever changes are made to the markdown file,
;; while also ensuring that both the window start and cursor position remain
;; unchanged.
(defun my-markdown-toc-gen-if-present ()
    (when (markdown-toc--toc-already-present-p)
      (let* ((window (selected-window))
             (buffer-in-selected-window (eq (window-buffer window)
                                            (current-buffer)))
             (window-hscroll nil)
             (lines-before nil))
        (when buffer-in-selected-window
          (setq window-hscroll (window-hscroll))
          (setq lines-before (count-screen-lines
                              (save-excursion (goto-char (window-start))
                                              (vertical-motion 0)
                                              (point))
                              (save-excursion (vertical-motion 0)
                                              (point))
                              nil
                              window)))
        (unwind-protect
            (markdown-toc-generate-toc)
          (when buffer-in-selected-window
            (set-window-start window
                              (save-excursion
                                (vertical-motion 0)
                                (line-move-visual (* -1 lines-before))
                                (vertical-motion 0)
                                (point)))
            (set-window-hscroll window window-hscroll))))))

  (defun my-setup-markdown-toc ()
    "Setup the markdown-toc package."
    (add-hook 'before-save-hook #'my-markdown-toc-gen-if-present -100 t))

  (add-hook 'markdown-mode-hook #'my-setup-markdown-toc)
  (add-hook 'markdown-ts-mode-hook #'my-setup-markdown-toc)
  (add-hook 'gfm-mode-hook #'my-setup-markdown-toc)Code language: Lisp (lisp)

The above code snippet leverages the markdown-toc Emacs package to automate the generation of a table of contents within markdown documents. This package includes functions such as markdown-toc-generate-toc and markdown-toc-generate-or-refresh-toc, which facilitate the creation and updating of the table of contents as needed.

I implemented the my-setup-markdown-toc function to establish a hook that triggers my-markdown-toc-gen-if-present each time a markdown buffer is saved. This function guarantees that any existing table of contents remains current whenever changes are made to the markdown file, while also ensuring that both the window start and cursor position remain unchanged.

Requirement: Markdown Mode Setup

The table of contents update code snippet above will only function correctly if the markdown-mode package is installed:

(use-package markdown-mode
  :ensure t
  :defer t
  :commands (gfm-mode gfm-view-mode markdown-mode markdown-view-mode)
  :mode (("\\.markdown\\'" . markdown-mode)
         ("\\.md\\'"       . markdown-mode)
         ("README\\.md\\'" . gfm-mode))) Code language: Lisp (lisp)

Conclusion

The my-markdown-toc-gen-if-present function automates the generation of a table of contents, ensuring that markdown documents consistently remain up-to-date with minimal effort.

Vim: Open documentation in a new tab for the word under the cursor (Vim help, Python, man pages, Markdown, Ansible…)

The following Vim script (VimL) function can be used to make Vim open the documentation of the word under the cursor in a new tab for various languages and tools such as Vim help (:help), Python (Pydoc), Markdown (sdcv dictionary), man pages (Vim’s built-in ‘:Man’), and Ansible (ansible-doc).

The VimL function is also extensible, meaning that you can adapt it to work with any other documentation tool. By default, the key mapping upper-case “K” can be used to open the documentation for the word under the cursor in a new tab.

" Language: Vim script
" Author: James Cherti
" License: MIT
" Description: Vim: open help/documentation in a new tab 
"              (Vim script, Python, Markdown, man pages, Ansible...).
"              Press upper-case K to open help for the word under the cursor.
" URL: https://www.jamescherti.com/vim-open-help-documentation-in-a-new-tab/

function! TabHelp(word) abort
  let l:cmd = ''

  let l:tabhelpprg = get(b:, 'tabhelpprg', '')
  if l:tabhelpprg ==# ''
    normal! K
    return
  endif

  if l:tabhelpprg[0] ==# ':'
    if stridx(l:tabhelpprg, '%s') ==# -1
      execute l:tabhelpprg
    else
      execute printf(l:tabhelpprg, fnameescape(a:word))
    endif
    return
  else
    let l:cmd = 'silent read! '
    if stridx(l:tabhelpprg, '%s') ==# -1
      let l:cmd .= l:tabhelpprg
    else
      let l:cmd .= printf(l:tabhelpprg, shellescape(a:word))
    endif
  endif

  execute 'silent tabnew help:' . fnameescape(a:word)

  setlocal modifiable
  silent normal! ggdG
  silent normal! 1Gdd
  if l:cmd !=# ''
    execute l:cmd
  endif
  silent normal! gg0
  setlocal nomodifiable
  setlocal noswapfile
  setlocal nowrap
  setlocal nonumber
  setlocal nomodified
  setlocal buftype=nofile
  setlocal bufhidden=delete
  if exists('&relativenumber')
    setlocal norelativenumber
  endif
  if exists('&signcolumn')
    setlocal signcolumn=no
  endif
  setlocal nofoldenable
  setlocal foldcolumn=0
endfunction

augroup TabHelp
  autocmd!
  autocmd FileType vim let b:tabhelpprg = ':tab help %s'
  autocmd FileType sh,zsh,csh if ! exists(':Man') | runtime ftplugin/man.vim | endif | let b:tabhelpprg = ':tab Man %s'
  autocmd FileType yaml.ansible if executable('ansible-doc') | let b:tabhelpprg = 'ansible-doc %s' | endif
  autocmd FileType markdown if executable('sdcv') | let b:tabhelpprg = 'sdcv %s' | endif
  autocmd FileType vim,sh,zsh,csh,yaml.ansible,markdown nnoremap <silent> <buffer> K :call TabHelp(expand('<cword>'))<CR>
augroup ENDCode language: Vim Script (vim)