Count Active Region Words - Another Article Mode Enhancement

February 8, 2009 by John Jenkins · Leave a Comment
Filed under: Emacs 

As I mentioned previously I wanted to add a feature to article mode that would count the number of words in an active region. I can’t think of an immediate practical purpose, but I am frequently curious how long a paragraph is. If the feature has been implemented correctly, I can tell you that this one, for example, is around sixty-nine words long depending on your definition of a word.

To fit in with the rest of the mode, it would be nice if the information was automatically displayed when the region is active. There are two likely candidate hooks - activate-mark-hook and deactivate-mark-hook which look promising. However, on experimentation with activate-mark-hook, it didn’t really work as I wanted.

activate-mark-hook documentation

Hook run when the mark becomes active.
It is also run at the end of a command, if the mark is active and
it is possible that the region may have changed.

What I really want is that when a region is active I may need to recalculate the number of words in that region whenever a command is executed.

When the mark is active,
  record the start and end of the region and
  restart the timer to update the info window.

post-command-hook is ideal for this and I can literally express my intent line for line in the code. I’m not happy with my naming convention for the functions that are run by the hook variables as they can be mistaken for hooks themselves but I’m not feeling very inspired.

(defun am-post-command-hook ()
  (when mark-active
    (setq am-region-beginning (region-beginning))
    (setq am-region-end (region-end))
    (am-restart-timer)))

am-region-beginning and am-region-end are variables we defined earlier and I decided to immediately remove the region word count when the mark becomes inactive.

(defvar am-region-beginning nil)
(defvar am-region-end nil)

(defun am-deactivate-mark-hook ()
  (setq am-region-beginning nil)
  (setq am-region-end nil)
  (when (timerp am-timer) (cancel-timer am-timer))
  (am-update-info))

am-update-info is then updated to add the new information if it is available. The statistics need to be gathered before we switch into the info buffer as otherwise we are uselessly measuring the statistics of the info buffer itself. It might have been nicer to write this all into a string before calling a function that just updates the info buffer here. But, hey it works.

(defun am-update-info ()
      ...
    (let ((words (count-matches *am-re-word* (point-min) (point-max)))
          (keywords (if (= (length am-keywords-regex) 0) 0
                      (count-matches am-keywords-regex
                                     (point-min)
                                     (point-max)))))

      ...

      (insert (format "# Words      : (%s%s)\n"
                      (if region-words (format "%s / " region-words) "")
                      words))

      ...)

The code for restarting the timer is abstracted out into its own function as I seem to be calling it from multiple places. The DRY folks should be pleased.

(defun am-restart-timer ()
  (when (timerp am-timer) (cancel-timer am-timer))
  (setq am-timer
        (run-with-timer am-update-time nil 'am-run-timer-hook)))

All that now remains is to add the hooks in article-mode-start and remove the hooks in article-mode-stop.

  (add-hook 'post-command-hook 'am-post-command-hook nil t)
  (add-hook 'deactivate-mark-hook 'am-deactivate-mark-hook nil t)

  (remove-hook 'post-command-hook 'am-post-command-hook)
  (remove-hook 'deactivate-mark-hook 'am-deactivate-mark-hook)

The latest code is in the usual place. Thanks for reading.

Speak Your Mind

Tell us what you're thinking...
and oh, if you want a pic to show with your comment, go get a gravatar!