Skip to content
Mohsin Kaleem edited this page May 25, 2020 · 2 revisions

users of hl-todo may be wanting a convenient way to jump to TODO entries in your current project.

Here's a nice way to get that working using ripgrep or ag.

(require 'hl-todo)
(require 'counsel)

(defvar hl-todo-keyword-faces)

(defgroup counsel-todo nil
  "use ivy to jump to TODOs.")

(defcustom counsel-todo-backend 'rg
  "backend to use for `counsel-todo-command'"
  :group 'counsel-todo
  :type 'symbol
  :options '(rg ag))

(defcustom counsel-todo-command
  (cl-case counsel-todo-backend
    ('rg counsel-rg-base-command)
    ('ag counsel-ag-base-command))
  "command string used for searching with `counsel-todo'.")

(defun counsel-todo-function (string &rest _)
  (let ((regexp (counsel--elisp-to-pcre
                 (cl-loop for (keyword . _) in hl-todo-keyword-faces
                          for i from 1 upto (length hl-todo-keyword-faces)
                          unless (= i 1)
                            do (setq keyword (concat "\\|" keyword))
                          end
                          concat keyword))))
    (counsel--async-command
     (format counsel-todo-command
             (shell-quote-argument regexp)))
    '()))

(cl-defun counsel-todo (arg &optional initial-input initial-directory &key caller)
  (interactive "P")
  (when (and arg (not initial-directory))
    (setq initial-directory
          (counsel-read-directory-name "TODOs in directory: ")))

  (counsel-require-program counsel-todo-command)
  (let ((default-directory (or initial-directory
                               (counsel--git-root)
                               (ivy-state-directory ivy-last)
                               default-directory)))
    (ivy-read "TODO: "
              #'counsel-todo-function
              :initial-input initial-input
              ;; :dynamic-collection t
              :keymap counsel-ag-map
              :history 'counsel-git-grep-history
              :action #'counsel-git-grep-action
              :unwind #'counsel-delete-process
              :require-match t
              :caller (or caller 'counsel-todo))))

;; inherit config from counsel-ag
(ivy-configure 'counsel-todo
  :occur #'counsel-ag-occur
  :unwind-fn #'counsel--grep-unwind
  :display-transformer-fn #'counsel-git-grep-transformer
  :grep-p t
  :exit-codes '(1 "No matches found"))

(provide '+counsel-todo)

highlighting TODO keywords

the previous solution just displays matched lines in the default face, this makes it hard to see where the TODOs appear so here's a display-transformer that highlights TODO keywords as well.

(defun counsel-todo--get-face (keyword)
  (hl-todo--combine-face
   (cdr
    (cl-assoc keyword hl-todo-keyword-faces :test #'string-match-p))))

(defun counsel-todo-transformer (str)
  (save-match-data
    (let ((regex (hl-todo--regexp))
          (search-start 0) start end)
      (while (string-match regex str search-start)
        (setq start (match-beginning 1)
              end (match-end 1)
              search-start end)
        (add-face-text-property start end
                                (counsel-todo--get-face (substring str start end))
                                nil str))))
  (counsel-git-grep-transformer str))

(ivy-configure 'counsel-todo
  :display-transformer-fn #'counsel-todo-transformer)