This repository has been archived on 2024-10-22. You can view files and clone it, but cannot push or open issues or pull requests.
spacemacs/layers/+vim/vim-empty-lines/local/vim-empty-lines-mode/vim-empty-lines-mode.el

288 lines
11 KiB
EmacsLisp
Raw Normal View History

;;; vim-empty-lines-mode.el --- Vim-like empty line indicator at end of files.
;; Copyright (C) 2015 Jonne Mickelin
;; Author: Jonne Mickelin <jonne@ljhms.com>
;; Created: 06 Jan 2015
;; Version: 0.1
;; Keywords: emulations
;; URL: https://github.com/jmickelin/vim-empty-lines-mode
;; Package-Requires: ((emacs "23"))
;; This file is not part of GNU Emacs.
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; This mode emulates the way that vim indicates the end of a file,
;; that is by putting a "tilde" character (~) on any empty line that
;; follows the end of file.
;; Emacs provides a similar functionality that inserts a bitmap in the
;; fringe on the extraneous lines. By customizing
;; `indicate-empty-lines' and `fringe-indicator-alist' it is possible
;; to nearly emulate the vim behaviour.
;; However, there is a slight difference in what the two editors
;; consider to be "empty lines".
;; A line in vim can be considered to contain the newline character
;; that ended the previous line, as well as the other visible
;; characters. Equivalently, a line is empty only if it contains no
;; text and the previous line has no trailing whitespace.
;; Example:
;; foo <- not empty
;; \nbar <- not empty
;; \n <- not empty
;; ~ <- empty
;; ~ <- empty
;;
;; foo <- not empty
;; \nbar <- not empty
;; ~ <- empty
;; ~ <- empty
;; A line in emacs, on the other hand, will contain the newline
;; character that breaks it. Thus a line is empty even if the previous
;; line has a trailing linebreak.
;; Example:
;; foo\n <- not empty
;; bar\n <- not empty
;; ~ <- empty
;; ~ <- empty
;; ;
;; foo\n <- not empty
;; bar <- not empty
;; ~ <- empty
;; ~ <- empty
;; Note that Emacs displays the two cases identically!
;; There is currently (as of Emacs 24.4) no way to implement the
;; vim-like behaviour for `indicate-empty-lines' short of modifying
;; the Emacs core.
;; This module emulates the vim-like behaviour using a different
;; approach, namely by inserting at the end of the buffer a read-only
;; overlay containing the indicators for the empty lines. This has the
;; added advantage that it's trivial to customize the indicator to an
;; arbitrary string, and customize its text properties.
;; To enable `vim-empty-lines-mode' in a buffer, run
;; (vim-empty-lines-mode)
;; To enable it globally, run
;; (global-vim-empty-lines-mode)
;; The string that indicates an empty line can be customized, e.g.
;; (setq vim-empty-lines-indicator "**********************")
;; The face that is used to display the indicators is `vim-empty-lines-face'.
;;; Code:
(defgroup vim-empty-lines-mode
nil
"Vim-like empty line indicators."
:group 'emulations
:prefix "vim-empty-lines")
(defface vim-empty-lines-face
'((t (:inherit font-lock-comment-face)))
"Face for empty lines in `vim-empty-lines-mode'."
:group 'vim-empty-lines-mode)
(defcustom vim-empty-lines-indicator "~"
"String to display on lines following end-of-buffer in `vim-empty-lines-mode'.
Must not contain '\\n'."
:group 'vim-empty-lines-mode)
(defvar vim-empty-lines-overlay nil
"Overlay that displays the empty line indicators.")
(put 'vim-empty-lines-overlay 'permanent-local t)
(defun vim-empty-lines-create-overlay ()
(setq-local vim-empty-lines-overlay (make-overlay (point-max)
(point-max)
nil
t t))
(overlay-put vim-empty-lines-overlay 'window t))
(defun vim-empty-lines-count-screen-lines (beg end &optional max)
"Return the number of screen lines in the region.
Taken from `count-screen-lines' and quite stripped down.
Unlike `count-screen-lines', calls `vertical-motion' with MAX as the
argument for efficiencies. It is too expensive calling `vertical-motion'
with `buffer-size' if the buffer is large."
(let (count-final-newline
window)
(if (= beg end)
0
(save-excursion
(save-restriction
(widen)
(narrow-to-region (min beg end)
(if (and (not count-final-newline)
(= ?\n (char-before (max beg end))))
(1- (max beg end))
(max beg end)))
(goto-char (point-min))
(1+ (vertical-motion (or max (buffer-size)) ; XXX: changed
window)))))))
(defun vim-empty-lines-nlines-after-buffer-end (window &optional window-start)
(with-current-buffer (window-buffer window)
(if (and window-start ;; chance to optimize for some cases.
(or (and (window-end) (= (point-max) (window-end)))
(not (pos-visible-in-window-p (point-max) window))))
0
(vim-empty-lines-nlines-after-buffer-end-aux window))))
(defun vim-empty-lines-nlines-after-buffer-end-aux (window)
(save-selected-window
(with-selected-window window
(with-current-buffer (window-buffer)
(let ((screen-height (- (window-height)
1 ;; mode-line
(if header-line-format 1 0))))
(- screen-height
(1- (vim-empty-lines-count-screen-lines
(window-start) (point-max) screen-height))))))))
(defun vim-empty-lines-update-overlay (&optional window window-start)
(let ((w (or window
(let ((w (selected-window)))
(and (window-valid-p w) w)))))
;; `w' could be nil but it's ok for `window-height', `window-start' etc.
(when (overlayp vim-empty-lines-overlay)
(vim-empty-lines-update-overlay-aux
(apply 'max
(vim-empty-lines-nlines-after-buffer-end w window-start)
(mapcar 'vim-empty-lines-nlines-after-buffer-end
(remq w (get-buffer-window-list nil nil t)))))
(move-overlay vim-empty-lines-overlay (point-max) (point-max)))))
(defun vim-empty-lines-update-overlay-aux (nlines-after-buffer-end)
(when (> nlines-after-buffer-end 1)
(save-excursion
(let ((indicators
(apply 'concat
(make-list nlines-after-buffer-end
(concat "\n" vim-empty-lines-indicator)))))
(overlay-put vim-empty-lines-overlay
'after-string
(concat (propertize " "
;; Forbid movement past
;; the beginning of the
;; after-string.
'cursor nlines-after-buffer-end)
(propertize indicators
'face 'vim-empty-lines-face)))))))
(defun vim-empty-lines-update-overlay-windows ()
(with-selected-frame (selected-frame)
(save-selected-window
(mapc (lambda (w)
(with-selected-window w
(vim-empty-lines-update-overlay w)))
(window-list (selected-frame) -1)))))
(defun vim-empty-lines-hide-overlay ()
(when (overlayp vim-empty-lines-overlay)
(let ((ov vim-empty-lines-overlay))
(overlay-put ov 'invisible nil)
(overlay-put ov 'display nil)
(overlay-put ov 'after-string nil))))
(defvar vim-empty-lines-initialize-p nil)
(defun vim-empty-lines-initialize-maybe ()
"Setup some advices to emacs primitives for workarounds"
(unless vim-empty-lines-initialize-p
(setq vim-empty-lines-initialize-p t)
;; A kludge to bug#19553
;; fixed in b544ab561fcb575790c963a2eda51524fa366409
;; XXX: The fix is in the emacs-24 branch only at this time.
(unless (and (version< emacs-version "25")
(version< "24.4.51" emacs-version))
(eval-when-compile
(defmacro vim-empty-lines-advice-add-overlay-handling (&rest functions)
`(progn
,@(mapcar
(lambda (function)
`(defadvice ,function (around vim-empty-lines activate)
(if (not (overlayp vim-empty-lines-overlay))
ad-do-it
(let ((inhibit-redisplay t) ;;Hope not to break anything
(p (overlay-start vim-empty-lines-overlay)))
(delete-overlay vim-empty-lines-overlay)
(unwind-protect ad-do-it
(move-overlay vim-empty-lines-overlay p p))))))
functions))))
(vim-empty-lines-advice-add-overlay-handling vertical-motion
move-to-window-line))))
;;;###autoload
(define-minor-mode vim-empty-lines-mode
"Display `vim-empty-lines-indicator' on visible lines after the end of the buffer.
This differs from `indicate-empty-lines' in the way that it deals
with trailing newlines."
:lighter " ~"
:global nil
(if vim-empty-lines-mode
(progn
(unless (overlayp vim-empty-lines-overlay)
(vim-empty-lines-create-overlay))
(vim-empty-lines-initialize-maybe)
(make-local-variable 'vim-empty-lines-overlay)
(vim-empty-lines-create-overlay)
(vim-empty-lines-update-overlay)
(add-hook 'post-command-hook 'vim-empty-lines-update-overlay t)
(add-hook 'window-scroll-functions 'vim-empty-lines-update-overlay t)
(add-hook 'window-configuration-change-hook
'vim-empty-lines-update-overlay-windows t))
(remove-hook 'post-command-hook 'vim-empty-lines-update-overlay t)
(remove-hook 'window-scroll-functions 'vim-empty-lines-update-overlay t)
(remove-hook 'window-configuration-change-hook
'vim-empty-lines-update-overlay-windows t)
(when (overlayp vim-empty-lines-overlay)
(delete-overlay vim-empty-lines-overlay)
(setq vim-empty-lines-overlay nil))))
;;;###autoload
(define-global-minor-mode global-vim-empty-lines-mode
vim-empty-lines-mode
(lambda ()
(unless (or (minibufferp)
;; Is there really no built-in function for detecting
;; the echo area?
(string-match-p "\\*Echo Area [0-9]+\\*" (buffer-name)))
(vim-empty-lines-mode +1)))
:group 'vim-empty-lines-mode
:require 'vim-empty-lines-mode)
(provide 'vim-empty-lines-mode)
;;; vim-empty-lines-mode.el ends here