Introducing a Word Diff Counter in Emacs

How do you measure productivity in writing? Or, how do you know when is a good time to stop working and take a break?

If I rush through a piece of writing in a sprint to the finish, the output quality starts to degrade.

This is why writers like to set word count goals. For example, 500 to 1000 words daily can ensure solid daily output without burning out.

A word count goal does a few things well:

  1. Provides a simple system for daily work
  2. Measures output
  3. Signals stopping time

It’s an easy approach and gets the job done. It cuts down on procrastination by giving you a simple daily target.

Problems With Word Counts

But word count goals have a few problems.

They only count addition. So they’re no good when you start editing.

For some writers, constant rereading and editing is part of the process. (This is how I like to write.)

In this case, word count goals only represent a fraction of the daily work.

What rewriters need is a counter that always counts up whether you are adding or deleting. This metric would account for change rather than raw addition. By incorporating Git, we can achieve this in Emacs.

The Theory Behind a Word Diff Mode

The Git version control program can count differences between versions of your document. There’s the normal diff that counts a changed line, and a word diff that counts differences at the word level for more granularity.

If you had a script that would count the word diff between commits, you could get a number that tells you how many changes you’ve made.

Then, if you could get that number into your Emacs modeline, you have a counter that functions like a word counter, but it would be counting additions and deletions.

The Source Code

I was able to get a working script that provides a word diff counter in my Emacs modeline.

I’m not a programmer by trade, so I had help from a language model in writing this code. I can vouch that the code works, but I can’t vouch for the quality. I was hesitant to include the code for this reason, but I decided to include in the hope that other users might find it helpful or suggest improvements.

(defvar word-diff-count "")
(defvar word-diff-debug-info "")
(defvar word-diff-mode nil)

(defun get-git-root ()
  "Get the root directory of the current Git repository."
  (let ((root (vc-git-root default-directory)))
    (if root
        (expand-file-name root)
      nil)))

(defun run-git-command (command)
  "Run a git command and return its output and exit code."
  (with-temp-buffer
    (let ((exit-code (call-process-shell-command command nil (current-buffer) nil)))
      (cons (buffer-string) exit-code))))

(defun update-word-diff-count ()
  "Update the word diff count and display it in the mode line."
  (interactive)
  (when word-diff-mode
    (let ((git-root (get-git-root)))
      (if git-root
          (let ((default-directory git-root))
            (let* ((diff-command "git diff --word-diff=porcelain HEAD | grep -e '^+[^+]' -e '^-[^-]' | wc -w")
                   (diff-result (run-git-command diff-command))
                   (diff-output (string-trim (car diff-result)))
                   (diff-exit-code (cdr diff-result)))
              (if (= diff-exit-code 0)
                  (setq word-diff-count diff-output
                        word-diff-debug-info "Success")
                (setq word-diff-count "Error"
                      word-diff-debug-info (format "Command failed with exit code %d. Output: %s" diff-exit-code diff-output)))))
        (setq word-diff-count "N/A"
              word-diff-debug-info "Not in a Git repository")))
    (force-mode-line-update)))

(defun word-diff-count-modeline ()
  "Return the word diff count for display in the mode line."
  (if (and word-diff-mode (not (string-empty-p word-diff-count)))
      (format " WD:%s" word-diff-count)
    ""))

(defun toggle-word-diff-mode ()
  "Toggle the word diff count mode."
  (interactive)
  (setq word-diff-mode (not word-diff-mode))
  (if word-diff-mode
      (progn
        (add-hook 'after-save-hook 'update-word-diff-count)
        (update-word-diff-count)
        (message "Word diff mode enabled"))
    (remove-hook 'after-save-hook 'update-word-diff-count)
    (setq word-diff-count "")
    (force-mode-line-update)
    (message "Word diff mode disabled")))

;; Add the word diff count to the mode line
(setq-default mode-line-format
              (append mode-line-format
                      '((:eval (word-diff-count-modeline)))))

;; Debug function
(defun debug-word-diff ()
  "Print debug information for word-diff functionality."
  (interactive)
  (message "Word Diff Mode: %s\nWord Diff Count: %s\nDebug Info: %s"
           (if word-diff-mode "Enabled" "Disabled")
           word-diff-count
           word-diff-debug-info))

How It Works

The script works pretty simply.

You will first need to initialize a Git repository in your working directory and commit your initial changes.

Regular commits are an important part of the process because this is what the script is measuring against your current changes. The current state of the working directory is measured against the most recent commit.

Enable the word diff counter using the function: toggle-word-diff-mode.

Whenever you save the file (or files) the diff counter will run the script and count changes, presenting the result in the modeline.


I’ll reiterate that the code sample provided may provide different results depending on your system. If you have any issues or questions or suggestions for improvement, please drop a comment below.

Be the first to comment

Leave a Reply

Your email address will not be published.


*