Download Git Homepage
(defgroup so-long nil
"Prevent unacceptable performance degradation with very long lines."
:prefix "so-long"
:group 'convenience)
(defcustom so-long-threshold 250
"Maximum line length permitted before invoking `so-long-mode'.
See `so-long-line-detected-p' for details."
:type 'integer
:group 'so-long)
(defcustom so-long-max-lines 5
"Number of non-blank, non-comment lines to test for excessive length.
See `so-long-line-detected-p' for details."
:type 'integer
:group 'so-long)
(defcustom so-long-target-modes
'(prog-mode css-mode sgml-mode nxml-mode)
"`so-long-mode' affects only these modes and their derivatives.
Our primary use-case is minified programming code, so `prog-mode' covers
most cases, but there are some exceptions to this."
:type '(repeat symbol)
:group 'so-long)
(defcustom so-long-minor-modes
'(font-lock-mode
highlight-changes-mode hi-lock-mode hl-line-mode linum-mode nlinum-mode
prettify-symbols-mode visual-line-mode whitespace-mode)
"List of buffer-local minor modes to explicitly disable in `so-long-mode'.
The modes are disabled by calling them with a single numeric argument of zero.
This happens during `after-change-major-mode-hook', and after any globalized
minor modes have acted, so that buffer-local modes controlled by globalized
modes can also be targeted.
`so-long-hook' can be used where more custom behaviour is desired.
See also `so-long-mode-hook'."
:type '(repeat symbol)
:group 'so-long)
(defcustom so-long-hook '(so-long-make-buffer-read-only)
"List of functions to call after `so-long-mode'.
This hook runs during `after-change-major-mode-hook', and after any globalized
minor modes have acted.
See also `so-long-mode-hook' and `so-long-minor-modes'."
:type '(repeat function)
:group 'so-long)
(defcustom so-long-revert-hook '(so-long-revert-buffer-read-only)
"List of functions to call after `so-long-mode-revert'."
:type '(repeat function)
:group 'so-long)
(defvar so-long-mode-enabled t
"Set to nil to prevent `so-long-mode' from being triggered.")
(defvar so-long-mode--inhibited nil)
(make-variable-buffer-local 'so-long-mode--inhibited)
(put 'so-long-mode--inhibited 'permanent-local t)
(defvar-local so-long-original-values nil
"Alist holding the buffer's original `major-mode' value, and other data.
Any values to be restored by `so-long-revert' can be stored here during
`change-major-mode-hook' and reinstated during `so-long-revert-hook'.
See also `so-long-remember' and `so-long-original'.")
(put 'so-long-original-values 'permanent-local t)
(defun so-long-original (key &optional exists)
"Return the current value for KEY in `so-long-original-values'.
If you need to differentiate between a stored value of nil and no stored value
at all, make EXISTS non-nil. This then returns the result of `assq' directly:
nil if no value was set, and a cons cell otherwise."
(if exists
(assq key so-long-original-values)
(cdr (assq key so-long-original-values))))
(defun so-long-remember (variable)
"Push the `symbol-value' for VARIABLE to `so-long-original-values'."
(push (cons variable (symbol-value variable))
so-long-original-values))
(add-hook 'change-major-mode-hook 'so-long-change-major-mode)
(defun so-long-change-major-mode ()
"Ensures that `so-long-mode' knows the original `major-mode'
even when invoked interactively.
Called by default during `change-major-mode-hook'."
(unless (eq major-mode 'so-long-mode)
(so-long-remember 'major-mode)
(so-long-remember 'buffer-read-only)))
(defun so-long-line-detected-p ()
"Following any initial comments and blank lines, the next N lines of the
buffer will be tested for excessive length (where \"excessive\" means above
`so-long-threshold', and N is `so-long-max-lines').
Returns non-nil if any such excessive-length line is detected."
(let ((count 0))
(save-excursion
(goto-char (point-min))
(while (comment-forward))
(catch 'excessive
(while (< count so-long-max-lines)
(if (> (- (line-end-position 1) (point))
so-long-threshold)
(throw 'excessive t)
(forward-line)
(setq count (1+ count))))))))
(defcustom so-long-mode-hook '(so-long-inhibit-global-hl-line-mode)
""
:type '(repeat function)
:group 'so-long)
(define-derived-mode so-long-mode nil "So long"
"This mode is used if line lengths exceed `so-long-threshold'.
Many Emacs modes struggle with buffers which contain excessively long lines,
and may consequently cause unacceptable performance issues.
This is commonly on account of 'minified' code (i.e. code has been compacted
into the smallest file size possible, which often entails removing newlines
should they not be strictly necessary). These kinds of files are typically
not intended to be edited, so not providing the usual editing mode in these
cases will rarely be an issue.
When such files are detected, we invoke this mode. This happens after
`set-auto-mode' has set the major mode, should the selected major mode be
a member (or derivative of a member) of `so-long-target-modes'.
After changing modes, any active minor modes listed in `so-long-minor-modes'
are disabled for the current buffer, and finally `so-long-hook' is run.
These two steps occur as part of `after-change-major-mode-hook', so that
modes controlled by globalized minor modes are also visible.
Some globalized minor modes may be inhibited by acting in `so-long-mode-hook'.
By default this mode is essentially equivalent to `fundamental-mode', and
exists mainly to provide information to the user as to why the expected mode
was not used, and to facilitate hooks for other so-long functionality.
To revert to the original mode despite any potential performance issues,
type \\[so-long-mode-revert], or else re-invoke it manually."
(add-hook 'after-change-major-mode-hook
'so-long-after-change-major-mode :append :local)
(message "Changed to %s (from %s) on account of line length. %s to revert."
major-mode
(or (so-long-original 'major-mode) "<unknown>")
(substitute-command-keys "\\[so-long-mode-revert]")))
(put 'so-long-mode-hook 'variable-documentation
"List of functions to call when `so-long-mode' is invoked.
This is the standard mode hook for `so-long-mode' which runs between
`change-major-mode-after-body-hook' and `after-change-major-mode-hook'.
Note that globalized minor modes have not yet acted.
See also `so-long-hook' and `so-long-minor-modes'.")
(defun so-long-after-change-major-mode ()
"Disable modes in `so-long-minor-modes' and run `so-long-hook' functions.
This happens during `after-change-major-mode-hook'."
(mapc (lambda (mode)
(when (and (boundp mode) mode)
(funcall mode 0)))
so-long-minor-modes)
(run-hooks 'so-long-hook))
(defun so-long-mode-revert ()
"Call the `major-mode' which was selected before `so-long-mode' replaced it,
and re-process the local variables. Lastly run `so-long-revert-hook'."
(interactive)
(let ((so-long-original-mode (so-long-original 'major-mode)))
(unless so-long-original-mode
(error "Original mode unknown."))
(funcall so-long-original-mode)
(hack-local-variables)
(run-hooks 'so-long-revert-hook)))
(define-key so-long-mode-map (kbd "C-c C-c") 'so-long-mode-revert)
(defun so-long-make-buffer-read-only ()
"Make a so-long buffer read-only.
Called by default as the final action of `so-long-hook', as some earlier
actions may generate warnings when performed in a read-only buffer (e.g.
disabling `highlight-changes-mode').
As such, making the buffer read-only should be the final action taken,
to avoid any potential errors."
(setq buffer-read-only t))
(defun so-long-revert-buffer-read-only ()
"Restore `buffer-read-only' to its original value.
Called by default in `so-long-revert-hook'."
(let ((readonly (so-long-original 'buffer-read-only :exists)))
(when readonly
(setq buffer-read-only (cdr readonly)))))
(defun so-long-inhibit-global-hl-line-mode ()
"Prevent `global-hl-line-mode' from activating.
Called by default during `so-long-mode-hook'."
(setq-local global-hl-line-mode nil))
(defun so-long-check-header-modes ()
"Handles the header-comments processing in `set-auto-mode'.
`set-auto-mode' has some special-case code to handle the 'mode' pseudo-variable
when set in the header comment. This runs outside of `hack-local-variables'
and cannot be conveniently intercepted, so we are forced to replicate it here.
This special-case code will ultimately be removed from Emacs, as it exists to
deal with a deprecated feature
to inhibit our own behaviour in the presence of a header comment 'mode'
declaration."
(let ((try-locals (not (inhibit-local-variables-p)))
end done mode modes)
(save-excursion
(goto-char (point-min))
(skip-chars-forward " \t\n")
(and enable-local-variables
try-locals
(setq end (set-auto-mode-1))
(if (save-excursion (search-forward ":" end t))
(while (let ((case-fold-search t))
(or (and (looking-at "mode:")
(goto-char (match-end 0)))
(re-search-forward "[ \t end t)))
(skip-chars-forward " \t")
(let ((beg (point)))
(if (search-forward " end t)
(forward-char -1)
(goto-char end))
(skip-chars-backward " \t")
(push (intern (concat (downcase (buffer-substring beg (point))) "-mode"))
modes)))
(push (intern (concat (downcase (buffer-substring (point) end))
"-mode"))
modes))))
(setq so-long-mode--inhibited modes)))
(defadvice hack-local-variables (after so-long--file-local-mode disable)
"Ensure that `so-long-mode' defers to file-local mode declarations.
This advice acts after any initial MODE-ONLY call to `hack-local-variables',
and ensures that we do not change to `so-long-mode' in that scenario.
File-local header comments are currently an exception (see the commentary
for details). The file-local mode will ultimately still be used, however
`so-long-mode' still runs first, thus displaying a misleading message.
This issue will eventually be resolved in Emacs."
(when (ad-get-arg 0)
(setq so-long-mode--inhibited ad-return-value)))
(defadvice set-auto-mode (around so-long--set-auto-mode disable)
"Maybe change to `so-long-mode' for files with very long lines.
This advice acts after `set-auto-mode' has set the buffer's major mode.
We can't act before this point, because some major modes must be exempt
from `so-long-mode' (binary file modes, for example). Instead, we act
only when the selected major mode is a member (or derivative of a member)
of `so-long-target-modes'.
`so-long-line-detected-p' then determines whether the mode change is needed.
Local variables are not processed after changing to `so-long-mode', as
they might negatively affect performance. (Local variables are processed
again if `so-long-mode-revert' is called, however.)"
(setq so-long-mode--inhibited nil)
(when so-long-mode-enabled
(so-long-check-header-modes))
ad-do-it
(when so-long-mode-enabled
(unless so-long-mode--inhibited
(when (and (apply 'derived-mode-p so-long-target-modes)
(so-long-line-detected-p))
(so-long-mode)))))
(defun so-long-enable ()
"Enable the so-long library's functionality."
(interactive)
(ad-enable-advice 'hack-local-variables 'after 'so-long--file-local-mode)
(ad-enable-advice 'set-auto-mode 'around 'so-long--set-auto-mode)
(ad-activate 'hack-local-variables)
(ad-activate 'set-auto-mode)
(setq so-long-mode-enabled t))
(defun so-long-disable ()
"Disable the so-long library's functionality."
(interactive)
(ad-disable-advice 'hack-local-variables 'after 'so-long--file-local-mode)
(ad-disable-advice 'set-auto-mode 'around 'so-long--set-auto-mode)
(ad-activate 'hack-local-variables)
(ad-activate 'set-auto-mode)
(setq so-long-mode-enabled nil))
(defun so-long-unload-function ()
(so-long-disable)
nil)
(provide 'so-long)