2

Emacs 26.1 user here.

I know some common-lisp but new to elisp, so I wonder if I'm missing something obvious. I'm trying a rather simple frame setup with a function I wrote, and there's this strange behaviour. My local variable is getting set to nil after switch-to-buffer call. Here's the simplified version that has the same issue.

(defun my-frame-test1 ()
  (let ((frame (make-frame '((name . "test-frame")
                             (width . 300)
                             (visibility . nil)
                             (height . 100))))
        left-window edit-window-left edit-window-right)
    (set-frame-position frame 100 100)
    (make-frame-visible frame)
    (select-frame frame)
    (setq-local left-window (selected-window))
    (setq-local edit-window-left (split-window-right 30))
    (select-window edit-window-left)
    (setq-local edit-window-right (split-window-right))
    ;; ---- edit-window-right has the expected value at this point ----

    (dolist (f '("/tmp/file1.lisp" "/tmp/file2.lisp"))
      (find-file-noselect f t))
    (switch-to-buffer (get-file-buffer "/tmp/file1.lisp")) ;; after this line, edit-window-right has changed to nil
    (select-window edit-window-right) ;; <-- thus, this call doesn't work.
    (switch-to-buffer (get-file-buffer "/tmp/file2.lisp"))
    ))

I tried using setq instead of a let block, using different variable name, for example 'amp-edit-window-right, calling (switch-to-buffer (get-file-buffer "/tmp/file1.lisp") nil t), and all behave the same.

When I do a line by line execution by using the new frame and M-: it is the same. And the window that edit-window-right is set to (#<window 18 on some-file.el>) still has the same print value after edit-window-right is set to nil (i.e. the same window still exists).

Why would this happen?

4
  • 1
    When you switch to the other buffer, it's likely put in some (major) mode. Every function that switches to a major mode calls kill-all-local-variables. See C-h f for that function.
    – Drew
    May 5 at 1:59
  • 3
    I'm finding the question difficult to follow, but I think you're misunderstanding what the "local" in setq-local means, which is buffer-local. If you set a buffer-local variable and then switch to a different buffer; in the new buffer you naturally won't see the value which was local to the original buffer.
    – phils
    May 5 at 2:05
  • @phils, I was also trying the same with setq instead of setq-locals, but you're right, I don't have enough grasp on variable scope for elisp. May 5 at 2:36
  • I just tried defvars for the same variables and it works correctly. So @Drew, you are right, major mode is changing. Care to make it an answer? May 5 at 2:37
3

Annotating parts of your code:

(let (left-window edit-window-left edit-window-right)
  ;; ^ these variables go out of scope at the end of the `let' form,
  ;;
  ;; If you were using `setq' below instead of `setq-local' then you would
  ;; be manipulating these values; but because you're setting buffer-local
  ;; values instead, these let-bound variables never change from their `nil'
  ;; default values.
  ;;
  ;; We are currently in some arbitrary buffer X.
  (setq-local left-window (selected-window))
  (setq-local edit-window-left (split-window-right 30))
  ;; ^ These values are local to buffer X.
  (select-window edit-window-left)
  (setq-local edit-window-right (split-window-right))
  ;; ^ This value is also local to buffer X (because `edit-window-left' is
  ;; still showing buffer X).
  (dolist (f '("/tmp/file1.lisp" "/tmp/file2.lisp"))
    (find-file-noselect f t))
  ;; ^ These are quite likely to be different buffers to X.
  (switch-to-buffer (get-file-buffer "/tmp/file1.lisp"))
  ;; ^ We can no longer see the values which were local to X
  ;; (unless this buffer was X), so we will see the `nil' values
  ;; from your `let' bindings.
  (select-window edit-window-right)
  (switch-to-buffer (get-file-buffer "/tmp/file2.lisp"))
  ;; ^ As before -- we probably see the `let' bound values in here.
  )
;; The `let' ends here, and so now *only* buffer X sees values for
;; `left-window', `edit-window-left', and `edit-window-right'.
4
  1. I'm guessing that you really want to use setq, not setq-local in that code. Did you intend to set buffer-local values of those let-bound variables?

    As @phils points out, setq-local sets a buffer-local variable. And the Elisp manual tells us, in node Creating Buffer Local:

Making a variable buffer-local within a let-binding for that variable does not work reliably, unless the buffer in which you do this is not current either on entry to or exit from the let. This is because let does not distinguish between different kinds of bindings; it knows only which variable the binding was made for.

  1. What's more, when you change the current buffer in that code by visiting a file, the buffer you switch to turns on a major mode, and that invokes kill-all-local-variables, which does this:

kill-all-local-variables is a built-in function in ‘C source code’.

(kill-all-local-variables)

Switch to Fundamental mode by killing current buffer's local variables.

Most local variable bindings are eliminated so that the default values become effective once more. Also, the syntax table is set from standard-syntax-table, the local keymap is set to nil, and the abbrev table from fundamental-mode-abbrev-table. This function also forces redisplay of the mode line.

Every function to select a new major mode starts by calling this function.

As a special exception, local variables whose names have a non-nil permanent-local property are not eliminated by this function.

The first thing this function does is run the normal hook change-major-mode-hook.

  1. setq-local is really set applied to a buffer-local variable. (setq-local foo bar) macroexpands to (set (make-local-variable 'foo) bar). So setq-local always applies to a dynamic binding.

    Node Setting Variables of the Elisp manual tells us:

When dynamic variable binding is in effect (the default), set has the same effect as setq, apart from the fact that set evaluates its SYMBOL argument whereas setq does not. But when a variable is lexically bound, set affects its dynamic value, whereas setq affects its current (lexical) value. See Variable Scoping.

2
  • For clarification (as the "when you change the current buffer in that code" part is a bit misleading) it's the find-file-noselect process which sets the major mode for those buffers in that code, not the switch-to-buffer.
    – phils
    May 5 at 3:30
  • @phils: Yes, you're right. Visiting a file (in this case) sets the major mode. I've reworded that.
    – Drew
    May 5 at 3:40
3

You've been mislead by the unusual terminology in Emacs Lisp. A buffer-local variable is not a local variable! It's a global variable which has (potentially) different values in every buffer. “Buffer-local variable” is a shortcut for “variable with buffer-local bindings”. A buffer-local variable doesn't have to be global, but it usually is.

The macro setq-local sets the buffer-local value of the specified global variable. To set the value of a local variable, call setq.

1
  • To expand slightly on this crucial point, the main difference between CL and EL is not the kind of thing that differentiates, say, Scheme or Clojure (Lisp-1, TCO, immutability, etc); it's that EL has one concept that is alien to all other Lisps, that of a default data structure -- when nothing else is specified, the default in EL is to operate on the current buffer. Buffer-local variables are one of the artifacts of this architecture. May 11 at 20:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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