3

I'm writing a minor mode for emacs which, at the very least, will calculate a numeric value for each line in a buffer. I want to display this visually, preferable neatly before each line.

I know some minor modes draw to the fringe, and I know overlays are an option too (are these related?), but I can't find a good example of what I want anywhere.

Basically, I want to have something like the line numbers from linum-mode, but they will need to change every time the buffer is modified (actually, only whenever the line they're on changes). Something like a character counter for each line would be a good example. And I'd like it to not break linum-mode, but not depend on it, etc, if possible.

bgutt3r
  • 558
  • 3
  • 10
  • 1
    You may want to read the source code for modes that do this. `linum` is a built-in that should be good. `M-x find-library linum RET` should show you the source code. Third-party libraries that might be good include `git-gutter` and `git-gutter-fringe`. The fringe version is compatible with `linum`, and apparently the non-fringe version now has experimental support for working with `linum`. – ChrisGPT was on strike Jul 13 '14 at 17:07
  • 1
    In addition to the suggestions by Chris, you may wish to consider looking at libraries such as `vline-mode`, `col-highlight`, and `fill-column-indicator` so that you have an idea how to use both `re-search-forward\backward` for `\n` and you may wish to consider `vertical-motion 1\-1` to visit each line; and to see how to handle local variables. You will be using `before-string` instead of `after-string`. You may also wish to consider taking a look at my test-minor-mode for an example of determining `window-start` and `window-end` before redisplay: http://stackoverflow.com/a/24216247/2112489 – lawlist Jul 13 '14 at 18:36
  • 1
    Your options with the fringe will likely be limited to bitmap images, so depending upon your usage, you may need to write your own bitmaps if you choose to use the fringe. The following link contains an interesting discussion regarding using the fringe: http://stackoverflow.com/questions/16114700/is-it-possible-to-replace-fringe-bitmaps-with-text-in-emacs And, here is the link to the fringe-helper library: https://github.com/nschum/fringe-helper.el – lawlist Jul 13 '14 at 19:08

2 Answers2

4

Here is a quick example of one way to put an overlay after linum-mode numbers and before the line of text. I will need to give some thought about right-alignment of the character count.

NOTE:  This method contemplates that the linum-mode numbers are generated before the code that follows in this example. If the post-command-hook or the widow-scroll-functions hook is used to implement this proposed method, then those additions to the hooks would need to follow in time subsequently to the linum-mode functions attached to those same hooks.

The following example could be implemented with the post-command-hook and the window-scroll-functions hook. See the following link for an example of how to determine window-start and window-end before a redisplay occurs: https://stackoverflow.com/a/24216247/2112489

EDIT:  Added right-alignment of character count -- contemplates a maximum of three digits (i.e., up to 999 characters per line). The text after the character count overlays are now left-aligned.

(save-excursion
  (let* (
      (window-start (window-start))
      (window-end (window-end)))
    (goto-char window-end)
    (while (re-search-backward "\n" window-start t)
      (let* (
          (pbol (point-at-bol))
          (peol (point-at-eol))
          (raw-char-count (abs (- peol pbol)))
          (starting-column
            (propertize (char-to-string ?\uE001)
              'display
              `((space :align-to 1)
                (space :width 0))))
          (colored-char-count
            (propertize (number-to-string raw-char-count)
              'face '(:background "gray50" :foreground "black")
              'cursor t))
          (one-spacer
            (propertize (char-to-string ?\uE001)
              'display
              `((space :width 1))))
          (two-spacers
            (propertize (char-to-string ?\uE001)
              'display
              `((space :width 2))))
          (final-char-count
            (cond
              ((and
                  (< raw-char-count 100)
                  (> raw-char-count 9))
                (concat one-spacer colored-char-count))
              ((< raw-char-count 10)
                (concat two-spacers colored-char-count))
              (t colored-char-count))) )
        (overlay-put (make-overlay pbol pbol)
          'before-string
          (concat starting-column final-char-count two-spacers) )))))
Community
  • 1
  • 1
lawlist
  • 13,099
  • 3
  • 49
  • 158
3
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; M-x char-count-mode

(defvar char-count-p nil
"When `char-count-p` is non-`nil`, the overlays are present.")
(make-variable-buffer-local 'char-count-p)

(defvar char-count-this-command nil
"This local variable is set within the `post-command-hook`; and,
is also used by the `window-scroll-functions` hook.")
(make-variable-buffer-local 'char-count-this-command)

(defvar char-count-overlay-list nil
"List used to store overlays until they are removed.")
(make-variable-buffer-local 'char-count-overlay-list)

(defun char-count-post-command-hook ()
"Doc-string."
  (setq char-count-this-command this-command)
  (character-count-function))

(defun character-count-window-scroll-functions (win _start)
"Doc-string."
  (character-count-function))

(defun equal-including-properties--remove-overlays (beg end name val)
  "Remove the overlays using `equal`, instead of `eq`."
  (when (and beg end name val)
    (overlay-recenter end)
    (dolist (o (overlays-in beg end))
      (when (equal-including-properties (overlay-get o name) val)
        (delete-overlay o)))))

(defun character-count-function ()
"Doc-string for the character-count-function."
  (when
      (and
        char-count-mode
        char-count-this-command
        (window-live-p (get-buffer-window (current-buffer)))
        (not (minibufferp))
        (pos-visible-in-window-p (point)
          (get-buffer-window (current-buffer) (selected-frame)) t) )
    (remove-char-count-overlays)
    (save-excursion
      (let* (
          counter
          (selected-window (selected-window))
          (window-start (window-start selected-window))
          (window-end (window-end selected-window t)) )
        (goto-char window-end)
        (catch 'done
          (while t
            (when counter
              (re-search-backward "\n" window-start t))
            (when (not counter)
              (setq counter t))
          (let* (
              (pbol (point-at-bol))
              (peol (point-at-eol))
              (raw-char-count (abs (- peol pbol)))
              (starting-column
                (propertize (char-to-string ?\uE001)
                  'display
                  `((space :align-to 1) (space :width 0))))
              (colored-char-count
                (propertize (number-to-string raw-char-count)
                  'face '(:background "gray50" :foreground "black")))
              (one-spacer
                (propertize (char-to-string ?\uE001)
                  'display
                  `((space :width 1))))
              (two-spacers
                (propertize (char-to-string ?\uE001)
                  'display
                  `((space :width 2))))
              (final-char-count
                (cond
                  ((and
                      (< raw-char-count 100)
                      (> raw-char-count 9))
                    (concat one-spacer colored-char-count))
                  ((< raw-char-count 10)
                    (concat two-spacers colored-char-count))
                  (t colored-char-count)))
              (ov-string (concat starting-column final-char-count two-spacers)) )
            (push ov-string char-count-overlay-list)
            (overlay-put (make-overlay pbol pbol) 'before-string ov-string) 
            (when (<= pbol window-start)
              (throw 'done nil)) )))
        (setq char-count-p t)))
     (setq char-count-this-command nil) ))

(defun remove-char-count-overlays ()
  (when char-count-p
    (require 'cl)
    (setq char-count-overlay-list
      (remove-duplicates char-count-overlay-list
        :test (lambda (x y) (or (null y) (equal-including-properties x y)))
        :from-end t))
    (dolist (description char-count-overlay-list)
      (equal-including-properties--remove-overlays (point-min) (point-max) 'before-string description))
    (setq char-count-p nil) ))

(defun turn-off-char-count-mode ()
  (char-count-mode -1))

(define-minor-mode char-count-mode
"A minor-mode that places the character count at the beginning of the line."
  :init-value nil
  :lighter " Char-Count"
  :keymap nil
  :global nil
  :group nil
  (cond
    (char-count-mode
      (setq scroll-conservatively 101)
      (add-hook 'post-command-hook 'char-count-post-command-hook t t)
      (add-hook 'window-scroll-functions
        'character-count-window-scroll-functions t t)
      (add-hook 'change-major-mode-hook 'turn-off-char-count-mode nil t)
      (message "Turned ON `char-count-mode`."))
    (t
      (remove-char-count-overlays)
      (remove-hook 'post-command-hook 'char-count-post-command-hook t)
      (remove-hook 'window-scroll-functions
        'character-count-window-scroll-functions t)
      (remove-hook 'change-major-mode-hook 'turn-off-char-count-mode t)
      (kill-local-variable 'scroll-conservatively)
      (message "Turned OFF `char-count-mode`.") )))

(provide 'char-count)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
lawlist
  • 13,099
  • 3
  • 49
  • 158
  • Is there a reason you use 50 individual variables instead of a vector? But this is otherwise exactly what I wanted! – bgutt3r Sep 26 '14 at 14:13
  • It's been a while since I last looked at this example. Off hand, I'd say that it contemplates 50 lines of visible text and that potentially the value for each variable is going to be different. The value of the variable is stored so that the precise overlay matching that value can be deleted without affecting other overlays. If you have a better way to store between 50 to 75 individual values and later target them one by one with an overlay removal function, I'd be very interested in learning how. Note: most people prefer to just move overlays and delete unused, so that may be better. – lawlist Sep 26 '14 at 15:35
  • At the very least you could use a macro to generate all the variables. – Joseph Garvin Dec 20 '14 at 22:04
  • @Joseph Garvin -- Thank you for the helpful suggestion regarding using a vector to store the overlay strings. The revised draft now uses a list instead of individual buffer-local variables. I incorporated your idea into my own custom minor mode that tracks the cursor position and eliminated a zillion buffer local variables, and increased the overall speed by a little bit. – lawlist Feb 02 '15 at 20:51