11

When I browse Emacs help for functions via C-h f, I often want to peek into the Elisp/C implementation. I want to enter view-mode automatically when I access source code this way to avoid unnecessary modification. Is there a hook or function I can advise to accomplish this?

1
  • 2
    Here is what I use to prevent accidental modification of any of my files that open in emacs-lisp-mode and I just do C-x C-q if I want to edit the source code. (defun set-buffer-read-only () (setq buffer-read-only t)) (add-hook 'emacs-lisp-mode-hook 'set-buffer-read-only)
    – lawlist
    Nov 18, 2014 at 20:34

5 Answers 5

2

Update (after a night of sleep): This answer has a major flaw: it enables view-mode when navigating to any function, not just Emacs sources. This can be fixed, but you're better off using the answer by @phils.

By doing C-h f describe-function RET and then reading the source code of describe-function I discovered that it creates a "button" of a special type for links to function definitions: help-function-def.

Running zrgrep with this string ("help-function-def") pointed me to help-mode.el.gz.

After all this digging around we can replace this button type with our own (note the comment in the code):

(define-button-type 'help-function-def
  :supertype 'help-xref
  'help-function (lambda (fun file)
               (require 'find-func)
               (when (eq file 'C-source)
                 (setq file
                       (help-C-file-name (indirect-function fun) 'fun)))
               ;; Don't use find-function-noselect because it follows
               ;; aliases (which fails for built-in functions).
               (let ((location
                      (find-function-search-for-symbol fun nil file)))
                 (pop-to-buffer (car location))
                 (if (cdr location)
                     (goto-char (cdr location))
                   (message "Unable to find location in file")))
                   (view-mode t)) ; <= new line: enable view-mode
  'help-echo (purecopy "mouse-2, RET: find function's definition"))

As far as I can tell there is no function to add advice to: Emacs uses a lambda here. On the other hand (as pointed out by @rationalrevolt) one can replace the help-function property of the help-function-def button type:

(require 'help-mode)
(let ((help-func (button-type-get 'help-function-def 'help-function)))
  (button-type-put 'help-function-def 'help-function
                   `(lambda (func file)
                      (funcall ,help-func func file) (view-mode t))))
4
  • 1
    I think I can try to use button-type-get and button-type-put to replace the lambda with my own that forwards to the existing lambda. Nov 18, 2014 at 7:16
  • @rationalrevolt: Good idea! (Seems a bit fragile, but I suppose this is fragile too.) Nov 18, 2014 at 7:19
  • @rationalrevolt: Please see the updated answer. (Can't have newlines in comments, it seems...) Nov 18, 2014 at 8:11
  • Thanks! I was trying something similar but being a newbie to elisp I was bitten by the dynamic binding and couldn't get my closure to work :) Nov 18, 2014 at 8:13
17

You can use directory-local variables to make Emacs' source files read-only by default. (See also C-hig (emacs) Directory Variables RET).

Create a file called .dir-locals.el at the root of the directory tree you wish to protect, with the following contents:

((nil . ((eval . (view-mode 1)))))

Edit: Michał Politowski points out in the comments that enabling view-mode in this way is problematic, because when you dismiss the buffer with q it also disables the mode, meaning that the next time you visit that buffer view-mode will not be enabled.

Edit 2: Constantine has provided a solution to that problem in the comments below:

((nil . ((eval . (when buffer-file-name (view-mode-enter nil #'kill-buffer))))))

This usefully adds a test to ensure that the buffer is already visiting a file, but the key change is the use of view-mode-enter instead of view-mode, as the former takes an EXIT-ACTION argument which determines what to do when q is typed. In this case the exit action is to kill the buffer, ensuring that the next time the file is visited it will again end up in view-mode.

Edit 3: Following that path, we can also see that the specified EXIT-ACTION is ultimately passed to the view-mode-exit function, and its docstring gives us an alternative solution:

view-no-disable-on-exit is a variable defined in `view.el'.
Its value is nil

Documentation:
If non-nil, View mode "exit" commands don't actually disable View mode.
Instead, these commands just switch buffers or windows.
This is set in certain buffers by specialized features such as help commands
that use View mode automatically.

Hence we can use the following:

((nil . ((eval . (when buffer-file-name
                   (setq-local view-no-disable-on-exit t)
                   (view-mode-enter))))))

I use the alternative approach which you can specify entirely in your init file (as opposed to creating a .dir-locals.el file), and I simply make the files read-only rather than using view-mode. My config looks like this:

;; Emacs
(dir-locals-set-class-variables
 'emacs
 '((nil . ((buffer-read-only . t)
           (show-trailing-whitespace . nil)
           (tab-width . 8)
           (eval . (whitespace-mode -1))))))

(dir-locals-set-directory-class "/usr/local/src/emacs" 'emacs)
(dir-locals-set-directory-class "/usr/local/share/emacs" 'emacs)
(dir-locals-set-directory-class "/usr/share/emacs" 'emacs)

Obviously you can do the same thing for your elpa directory, and any other directory which contains third-party source code.

5
  • Great! This is clearly better than what I came up with. (What was I thinking of? I know about and use .dir-locals.el myself...) Nov 18, 2014 at 15:19
  • I've been something along the same lines based on a find-file-hook and a read-only-dirs list, but I like this approach.
    – glucas
    Nov 18, 2014 at 15:35
  • This looks like a very clean approach. I have one minor problem though. With ((nil . ((eval . (view-mode 1))))) what is the simplest way to make View-quit kill the buffers accessed via help? Otherwise after exiting the source view by pressing q, the buffer stays behind and when later accessing sources from the same file from help again view-mode is not started. Nov 18, 2014 at 22:34
  • Michał Politowski: Right you are. I've updated the answer to incorporate that fact, but I don't have a workaround. Constantine's (edited) answer may be the best solution for using view-mode.
    – phils
    Nov 18, 2014 at 23:30
  • 1
    Today I figured out that I need a workaround for the issue pointed out by @MichałPolitowski --- and I found one: use ((nil . ((eval . (when buffer-file-name (view-mode-enter nil #'kill-buffer)))))) (note (view-mode-enter ...) instead of (view-mode 1)). This way pressing q kills the buffer and view-mode is enabled the next time I visit the same file. Jan 23, 2016 at 23:56
0

I think all you need is to add a hook:

(add-hook 'find-function-after-hook 'view-mode)
2
  • This is better than my monstrosity, but still does not really match the question: it turns on view-mode when navigating to any function using C-h f, not just Emacs sources. Nov 18, 2014 at 15:22
  • Experimentally, the help links don't actually run this hook. It looks like only the interactive find-THING commands utilise this hook, and the help buttons bypass it.
    – phils
    Nov 18, 2014 at 23:27
0

This does not address your specific case, but the more general case of switching to view-mode whenever you visit a source file from a help buffer. I'm offering it as an alternative to @Constantine's answer, since it wasn't readable as a comment.

I looks like I originally got this from the EmacsWiki.

(defadvice find-function-search-for-symbol (after view-function-source last (symbol type library) activate)
  "When visiting function source via Help, switch to view-mode"
  (with-current-buffer (car ad-return-value)
    (view-mode 1)))

(defadvice find-variable-noselect (after view-var-source last (variable &optional file) activate)
  "When visiting variable source via Help, switch to view-mode"
  (with-current-buffer (car ad-return-value)
    (view-mode 1)))
0

Here's a solution that works for built-in documentation and an example showing how to extend it to ELPA. It works by matching the path to the current file against some regexes and applying read-only-mode if any of them match.

Note that the buffer is read-only-ified if you visit it through dired as well, not just through help.

I added a hook that runs after entering emacs-lisp-mode that checks if the path to the file matches /\.el\.gz$/, and applies read-only mode if it does.

(defun readonly-if-el-gz ()
  (cond
   ((string-match "\\.el\\.gz\\'" (or (buffer-file-name) ""))
    (read-only-mode +1))))

(add-hook 'emacs-lisp-mode-hook 'readonly-if-el-gz)

Here's an example that checks ELPA too, using the heuristic that any path containing .emacs.d/elpa is actually ELPA code.

(defun readonly-if-internal ()
  (let
      ((name (or (buffer-file-name) "")))
    (cond
     ((string-match "\\.el\\.gz\\'" name) (read-only-mode +1))
     ((string-match "\\.emacs\\.d/elpa" name) (read-only-mode +1)))))

(add-hook 'emacs-lisp-mode-hook 'readonly-if-internal)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.