emacs-lsp / lsp-mode

Emacs client/library for the Language Server Protocol
https://emacs-lsp.github.io/lsp-mode
GNU General Public License v3.0
4.75k stars 873 forks source link

`lsp-lv-message` causes source code buffer to corrupt and be killed when focus lost and regained #4157

Open 0x6d6e647a opened 1 year ago

0x6d6e647a commented 1 year ago

Thank you for the bug report

Bug description

When editing code with lsp-mode enabled and an lsp-lv-message window appears showing signature documentation, if the window focus is changed while the LV window is still visible, then after returning to the original window the contents of the buffer inside that window might be replaced (and possibly saved) with the documentation contents and then the buffer will be killed.

Steps to reproduce

  1. Consider the scenario of two windows in one frame side by side. The first (left-most) window is running a major-mode with lsp-mode enabled. I'll be using python-mode in this example. The second window is running another buffer with contents.
  2. If the first window, code is typed that triggers an lsp-lv-message window to appear. Assuming my cursor is at the end of dict( in the following example.
    #!/usr/bin/env python
    x = dict(
  3. While my cursor is here and the lsp-lv-message display window is open type M-x calls helm-M-x. From Helm I call ace-jump-word-mode. From ace-jump-word-mode I jump into the second buffer. At this point usually the lsp-lv-mesage buffer is no longer visable but the window that it was in is still there.
  4. From here if I return back to the first buffer and do something that would trigger another lsp-lv-message to appear, then the buffer's contents might be overwritten with the signature documentation which may also include saving the file the buffer is visiting with the signature documentation overwriting the file's contents. After this the buffer will be killed, sometimes wihtout a yes-or-no-p prompt.

I have experienced this in multiple different scenarios depending on how I changed focus to a different buffer which lead to variety of different outcomes. Regardless if the buffer's content's being overwritten and/or saved, the buffer will be killed at the end.

Since this does not trigger an error, I made the following changes to lv.el to launch the debugger when kill-buffer is called on a buffer with a name unlike " *LV*".

(defun lv-delete-window ()
  "Delete LV window and kill its buffer."
  (when (window-live-p lv-wnd)
    (let ((buf (window-buffer lv-wnd)))
      ;; +debug
      (unless (string-match-p "\\*LV\\*+$" (buffer-name buf))
        (debug))
      ;; -debug
      (delete-window lv-wnd)
      (kill-buffer buf))))

I also disabled all byte-code and native compilation at the beginning of my init.el in case that made a difference or yielded better back traces:

(setq
 load-suffixes '(".el")
 load-prefer-newer nil)

Expected behavior

Not killing the buffer possibly overwriting the buffer contents and saving over the file.

Which Language Server did you use?

python-lsp-server with the following plugins enabled:

OS

Linux

Error callstack

Debugger entered: nil
  (if (string-match-p "\\*LV\\*+$" (buffer-name buf)) nil (debug))
  (let ((buf (window-buffer lv-wnd))) (if (string-match-p "\\*LV\\*+$" (buffer-name buf)) nil (debug)) (delete-window lv-wnd) (kill-buffer buf))
  (progn (let ((buf (window-buffer lv-wnd))) (if (string-match-p "\\*LV\\*+$" (buffer-name buf)) nil (debug)) (delete-window lv-wnd) (kill-buffer buf)))
  (if (window-live-p lv-wnd) (progn (let ((buf (window-buffer lv-wnd))) (if (string-match-p "\\*LV\\*+$" (buffer-name buf)) nil (debug)) (delete-window lv-wnd) (kill-buffer buf))))
  lv-delete-window()
  (if message (progn (setq lsp--signature-last-buffer (current-buffer)) (let ((lv-force-update t)) (lv-message "%s" message))) (lv-delete-window) (remove-hook 'lv-window-hook #'lsp--setup-page-break-mode-if-present))
  lsp-lv-message(nil)
  funcall(lsp-lv-message nil)
  lsp-signature-stop()
  (if (and lsp--signature-last-buffer (not (equal (current-buffer) lsp--signature-last-buffer))) (lsp-signature-stop) (lsp-request-async "textDocument/signatureHelp" (lsp--text-document-position-params) #'lsp--handle-signature-update :cancel-token :signature))
  lsp-signature()
  read-from-minibuffer("Move to window: " nil (keymap (126 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (125 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (124 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (123 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (122 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (121 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (120 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (119 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (118 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (117 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (116 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (115 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (114 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (113 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (112 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (111 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (110 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (109 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (108 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (107 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (106 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (105 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (104 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (103 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) (102 closure (... ... ... ... ... ... t) nil (interactive) (self-insert-command 1) (exit-minibuffer)) ...))
  (let ((input (read-from-minibuffer (if minibuffer-num (format "Move to window [minibuffer is %s]: " (if switch-window-minibuffer-shortcut (char-to-string switch-window-minibuffer-shortcut) (switch-window--label minibuffer-num))) prompt-message) nil (let ((map (copy-keymap minibuffer-local-map)) (i 32)) (while (< i 127) (define-key map (char-to-string i) #'...) (setq i (1+ i))) map)))) (if (< (length input) 1) (switch-window--restore-eobp eobps) (let ((pos (cl-position input (switch-window--enumerate) :test #'equal)) (extra-function (lookup-key switch-window-extra-map input))) (cond (extra-function (call-interactively extra-function) (if (eq extra-function 'switch-window-resume-auto-resize-window) nil (setq switch-window--temp-disable-auto-resize t))) (pos (setq key (1+ pos))) (t (switch-window--restore-eobp eobps))))))
  (while (not key) (let ((input (read-from-minibuffer (if minibuffer-num (format "Move to window [minibuffer is %s]: " (if switch-window-minibuffer-shortcut ... ...)) prompt-message) nil (let ((map ...) (i 32)) (while (< i 127) (define-key map ... ...) (setq i ...)) map)))) (if (< (length input) 1) (switch-window--restore-eobp eobps) (let ((pos (cl-position input (switch-window--enumerate) :test #'equal)) (extra-function (lookup-key switch-window-extra-map input))) (cond (extra-function (call-interactively extra-function) (if (eq extra-function ...) nil (setq switch-window--temp-disable-auto-resize t))) (pos (setq key (1+ pos))) (t (switch-window--restore-eobp eobps)))))))
  (let (key) (while (not key) (let ((input (read-from-minibuffer (if minibuffer-num (format "Move to window [minibuffer is %s]: " ...) prompt-message) nil (let (... ...) (while ... ... ...) map)))) (if (< (length input) 1) (switch-window--restore-eobp eobps) (let ((pos (cl-position input ... :test ...)) (extra-function (lookup-key switch-window-extra-map input))) (cond (extra-function (call-interactively extra-function) (if ... nil ...)) (pos (setq key ...)) (t (switch-window--restore-eobp eobps))))))) key)
  switch-window--get-minibuffer-input("Move to window: " nil nil)
  (setq key (switch-window--get-minibuffer-input prompt-message minibuffer-num eobps))
  (cond ((eq switch-window-input-style 'read-event) (setq key (switch-window--get-input prompt-message minibuffer-num eobps))) ((member switch-window-input-style '(default minibuffer)) (setq key (switch-window--get-minibuffer-input prompt-message minibuffer-num eobps))))
  (progn (progn (set-default 'cursor-type nil)) (let ((--dolist-tail-- (switch-window--list))) (while --dolist-tail-- (let ((win (car --dolist-tail--))) (setq window-buffers (cons (cons win (window-buffer win)) window-buffers)) (setq window-margins (cons (cons win (window-margins win)) window-margins)) (setq window-points (cons (cons win (window-point win)) window-points)) (if (window-dedicated-p win) (progn (setq dedicated-windows (cons ... dedicated-windows)) (set-window-dedicated-p win nil))) (if (minibuffer-window-active-p win) (setq minibuffer-num num) (setq label-buffers (cons (switch-window--display-number win num) label-buffers))) (setq num (1+ num)) (setq --dolist-tail-- (cdr --dolist-tail--))))) (cond ((eq switch-window-input-style 'read-event) (setq key (switch-window--get-input prompt-message minibuffer-num eobps))) ((member switch-window-input-style '(default minibuffer)) (setq key (switch-window--get-minibuffer-input prompt-message minibuffer-num eobps)))))
  (unwind-protect (progn (progn (set-default 'cursor-type nil)) (let ((--dolist-tail-- (switch-window--list))) (while --dolist-tail-- (let ((win (car --dolist-tail--))) (setq window-buffers (cons (cons win ...) window-buffers)) (setq window-margins (cons (cons win ...) window-margins)) (setq window-points (cons (cons win ...) window-points)) (if (window-dedicated-p win) (progn (setq dedicated-windows ...) (set-window-dedicated-p win nil))) (if (minibuffer-window-active-p win) (setq minibuffer-num num) (setq label-buffers (cons ... label-buffers))) (setq num (1+ num)) (setq --dolist-tail-- (cdr --dolist-tail--))))) (cond ((eq switch-window-input-style 'read-event) (setq key (switch-window--get-input prompt-message minibuffer-num eobps))) ((member switch-window-input-style '(default minibuffer)) (setq key (switch-window--get-minibuffer-input prompt-message minibuffer-num eobps))))) (setq input-method-previous-message nil) (progn (set-default 'cursor-type original-cursor)) (let ((--dolist-tail-- window-buffers)) (while --dolist-tail-- (let ((w (car --dolist-tail--))) (set-window-buffer (car w) (cdr w) t) (setq --dolist-tail-- (cdr --dolist-tail--))))) (let ((--dolist-tail-- window-margins)) (while --dolist-tail-- (let ((w (car --dolist-tail--))) (set-window-margins (car w) (car (cdr w)) (cdr (cdr w))) (setq --dolist-tail-- (cdr --dolist-tail--))))) (let ((--dolist-tail-- window-points)) (while --dolist-tail-- (let ((w (car --dolist-tail--))) (set-window-point (car w) (cdr w)) (setq --dolist-tail-- (cdr --dolist-tail--))))) (let ((--dolist-tail-- dedicated-windows)) (while --dolist-tail-- (let ((w (car --dolist-tail--))) (set-window-dedicated-p (car w) (cdr w)) (setq --dolist-tail-- (cdr --dolist-tail--))))) (mapc 'kill-buffer label-buffers))
  (let ((window-configuration-change-hook (if switch-window-configuration-change-hook-inhibit nil window-configuration-change-hook)) (original-cursor (default-value 'cursor-type)) (eobps (switch-window--list-eobp)) (minibuffer-num nil) (num 1) key label-buffers window-buffers window-margins window-points dedicated-windows) (unwind-protect (progn (progn (set-default 'cursor-type nil)) (let ((--dolist-tail-- (switch-window--list))) (while --dolist-tail-- (let ((win ...)) (setq window-buffers (cons ... window-buffers)) (setq window-margins (cons ... window-margins)) (setq window-points (cons ... window-points)) (if (window-dedicated-p win) (progn ... ...)) (if (minibuffer-window-active-p win) (setq minibuffer-num num) (setq label-buffers ...)) (setq num (1+ num)) (setq --dolist-tail-- (cdr --dolist-tail--))))) (cond ((eq switch-window-input-style 'read-event) (setq key (switch-window--get-input prompt-message minibuffer-num eobps))) ((member switch-window-input-style '(default minibuffer)) (setq key (switch-window--get-minibuffer-input prompt-message minibuffer-num eobps))))) (setq input-method-previous-message nil) (progn (set-default 'cursor-type original-cursor)) (let ((--dolist-tail-- window-buffers)) (while --dolist-tail-- (let ((w (car --dolist-tail--))) (set-window-buffer (car w) (cdr w) t) (setq --dolist-tail-- (cdr --dolist-tail--))))) (let ((--dolist-tail-- window-margins)) (while --dolist-tail-- (let ((w (car --dolist-tail--))) (set-window-margins (car w) (car (cdr w)) (cdr (cdr w))) (setq --dolist-tail-- (cdr --dolist-tail--))))) (let ((--dolist-tail-- window-points)) (while --dolist-tail-- (let ((w (car --dolist-tail--))) (set-window-point (car w) (cdr w)) (setq --dolist-tail-- (cdr --dolist-tail--))))) (let ((--dolist-tail-- dedicated-windows)) (while --dolist-tail-- (let ((w (car --dolist-tail--))) (set-window-dedicated-p (car w) (cdr w)) (setq --dolist-tail-- (cdr --dolist-tail--))))) (mapc 'kill-buffer label-buffers)) key)
  switch-window--prompt("Move to window: ")
  (let ((orig-window (selected-window)) (index (switch-window--prompt prompt)) (eobps (switch-window--list-eobp))) (switch-window--jump-to-window index) (if (functionp function2) (progn (funcall function2))) (if (and return-original-window (window-live-p orig-window)) (progn (switch-window--select-window orig-window))) (switch-window--restore-eobp eobps))
  (if (<= (length (switch-window--list)) (if (numberp threshold) threshold switch-window-threshold)) (if (functionp function1) (progn (funcall function1))) (let ((orig-window (selected-window)) (index (switch-window--prompt prompt)) (eobps (switch-window--list-eobp))) (switch-window--jump-to-window index) (if (functionp function2) (progn (funcall function2))) (if (and return-original-window (window-live-p orig-window)) (progn (switch-window--select-window orig-window))) (switch-window--restore-eobp eobps)))
  switch-window--then("Move to window: " switch-window--other-window-or-frame)
  switch-window()
  funcall-interactively(switch-window)
  command-execute(switch-window)

Anything else?

lsp_workspace_log.txt

lsp_log.txt

0x6d6e647a commented 1 year ago

I tried using lsp-start-plain but it won't run with the following error:

emacs: Terminal type "dumb" is not powerful enough to run Emacs.
0x6d6e647a commented 1 year ago

Workaround to avoid losing work to this bug.

(defun void (&rest args))
(setq lsp-signature-function #'void)
dgutov commented 6 months ago

This might be more easily fixed in the utility package (https://github.com/abo-abo/hydra/blob/master/lv.el).

0x6d6e647a commented 6 months ago

@dgutov Thanks for taking the time to look at this. I don't disagree that the problem might be related to the hydra package if not directly the cause of it. I'll have to do further debugging on the issue but I currently suspect the problem is related to when/how the lv-wnd variable is set when lsp-mode creates the message buffer.

I haven't had time to deep dive on this issue but wanted to submit the bug as soon as possible in order to make people aware due to the potentially catastrophic consequences and offer my work around. I plan on working on this more and will link to this bug if I end up filing a bug against Hydra related to this issue.