From 2fcae17238c6def1bc8c846afe78f4ccc4659934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 24 Apr 2026 10:50:05 +0100 Subject: [PATCH] Eglot: some support for handle "untitled:" URIs (bug#80623) Some language servers use the "untitled:" URI scheme for unsaved documents. Handle these in window/showDocument and workspace/applyEdit by creating suitably named buffers, applying edits to them unconditionally (no diff, no confirmation), and returning early if nothing else remains in the batch. * lisp/progmodes/eglot.el (eglot--untitled-buffer): New helper. (eglot-handle-request): Refactor, handle :untitled URI's. (eglot--apply-workspace-edit): Make so-called 'untitled-text-edit's --- lisp/progmodes/eglot.el | 83 ++++++++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 31 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13d578d550a..8acc5809dba 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2920,29 +2920,35 @@ THINGS are either registrations or unregisterations (sic)." "Handle server request workspace/workspaceFolders." (eglot-workspace-folders server)) +(defun eglot--untitled-buffer (server uri) + "Get or create buffer for `untitled:' URI served by SERVER." + (get-buffer-create (eglot--server-buffer-name + server (substring uri (length "untitled:"))))) + (cl-defmethod eglot-handle-request - (_server (_method (eql window/showDocument)) &key - uri external takeFocus selection) + (server (_method (eql window/showDocument)) &key + uri external takeFocus selection) "Handle request window/showDocument." - (let ((success t) - (filename)) - (cond - ((eq external t) (browse-url uri)) - ((file-readable-p (setq filename (eglot-uri-to-path uri))) - ;; Use run-with-timer to avoid nested client requests like the - ;; "synchronous imenu" floated in bug#62116 presumably caused by - ;; which-func-mode. - (run-with-timer - 0 nil - (lambda () - (with-current-buffer (find-file-noselect filename) - (cond (takeFocus - (pop-to-buffer (current-buffer)) - (select-frame-set-input-focus (selected-frame))) - ((display-buffer (current-buffer)))) - (when selection - (eglot--goto selection)))))) - (t (setq success :json-false))) + (let ((success t) filename) + (cl-macrolet + ((show-buf (buf) + ;; if evaluating `buf' happens to find us an Eglot-managed + ;; file, `run-with-timer' avoids nested requests (bug#62116) + `(run-with-timer + 0 nil (lambda () + (with-current-buffer ,buf + (cond (takeFocus + (pop-to-buffer (current-buffer)) + (select-frame-set-input-focus (selected-frame))) + ((display-buffer (current-buffer)))) + (when selection (eglot--goto selection))))))) + (cond + ((eq external t) (browse-url uri)) + ((string-prefix-p "untitled:" uri) + (show-buf (eglot--untitled-buffer server uri))) + ((file-readable-p (setq filename (eglot-uri-to-path uri))) + (show-buf (find-file-noselect filename))) + (t (setq success :json-false)))) `(:success ,success))) (defun eglot--TextDocumentIdentifier () @@ -4463,14 +4469,20 @@ the edit was attempted and optionally why not." (let ((buf (find-buffer-visiting path))) (when buf (kill-buffer buf))) (delete-file path recursive)))) - (text-edit-op (path edits version) - `(text-edit - ,(format "Change %s (%d change%s)" path (length edits) - (if (> (length edits) 1) "s" "")) - ,(lambda (_op) - (with-current-buffer (find-file-noselect path) - (eglot--apply-text-edits edits version))) - ,path ,edits ,version)) + (text-edit-op (uri edits version &aux (path (pathify uri))) + (if (string-prefix-p "untitled:" uri) + `(untitled-text-edit + "Edit to unsaved `untitled:' buffer" + ,(lambda (_op) + (with-current-buffer (eglot--untitled-buffer server uri) + (eglot--apply-text-edits edits nil)))) + `(text-edit + ,(format "Change %s (%d change%s)" path (length edits) + (if (> (length edits) 1) "s" "")) + ,(lambda (_op) + (with-current-buffer (find-file-noselect path) + (eglot--apply-text-edits edits version))) + ,path ,edits ,version))) (mkfn (doit-fn &rest things) (lambda (op) (apply doit-fn things) @@ -4495,7 +4507,8 @@ the edit was attempted and optionally why not." ;; It's a TextDocumentEdit (no kind field) (eglot--dbind ((TextDocumentEdit) textDocument edits) ch (eglot--dbind ((VersionedTextDocumentIdentifier) uri version) - textDocument (text-edit-op (pathify uri) edits version)))))) + textDocument + (text-edit-op uri edits version)))))) (user-accepts-p () (y-or-n-p (format "[eglot] Server wants to:\n%s\nProceed? " @@ -4513,7 +4526,15 @@ the edit was attempted and optionally why not." (unless (and changes documentChanges) ;; Prefer `documentChanges' over sort-of-deprecated `changes'. (cl-loop for (uri edits) on changes by #'cddr - do (push (text-edit-op (pathify uri) edits nil) prepared))) + do (push (text-edit-op uri edits nil) prepared))) + ;; Apply edits to untitled: buffers unconditionally; they can't + ;; be diffed and need no confirmation. + (cl-loop for op in prepared + if (eq (car op) 'untitled-text-edit) + do (funcall (caddr op) op) + else collect op into rest + finally (setq prepared rest)) + (unless prepared (cl-return-from eglot--apply-workspace-edit `(t nil))) (let* ((decision (eglot--confirm-server-edits origin prepared)) (all-text-edits (cl-loop for (kind . _) in prepared always (eq kind 'text-edit)))