commit f61db42fc580fb671016c77be942506d9081ac2c (HEAD, refs/remotes/origin/master) Author: Eli Zaretskii Date: Thu Oct 20 22:03:12 2022 +0300 ; * etc/NEWS: Mention Eglot. diff --git a/etc/NEWS b/etc/NEWS index be90d1beef..f1a0662f55 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -2759,6 +2759,13 @@ name. * New Modes and Packages in Emacs 29.1 ++++ +** Eglot: Emacs Client for the Language Server Protocol. +Emacs now comes with the Eglot package, which enhances various Emacs +features, such as completion, documentation, error detection, etc., +based on data provided by language servers using the Language Server +Protocol (LSP). + +++ ** New commands 'image-crop' and 'image-cut. These commands allow interactively cropping/cutting the image at commit 937ae0cf55d31c332fba3d96061e2ac3653e5437 Author: Filipp Gunbin Date: Thu Oct 20 20:41:00 2022 +0300 Fix ldapsearch output parsing in ldap-search-internal * lisp/net/ldap.el (ldap-search-internal): When parsing output, make sure that file:// matched before opening the file. (bug#58605) diff --git a/lisp/net/ldap.el b/lisp/net/ldap.el index 062ff05d69..ccad8c4edb 100644 --- a/lisp/net/ldap.el +++ b/lisp/net/ldap.el @@ -715,14 +715,14 @@ an alist of attribute/value pairs." (eq (string-match "/\\(.:.*\\)$" value) 0)) (setq value (match-string 1 value))) ;; Do not try to open non-existent files - (if (equal value "") - (setq value " ") - (with-current-buffer bufval + (if (match-string 3) + (with-current-buffer bufval (erase-buffer) (set-buffer-multibyte nil) (insert-file-contents-literally value) (delete-file value) - (setq value (buffer-string)))) + (setq value (buffer-string))) + (setq value " ")) (setq record (cons (list name value) record)) (forward-line 1)) commit 1324baea728a11bf650303698881c682105155da Author: Eli Zaretskii Date: Thu Oct 20 20:50:34 2022 +0300 Add Eglot to the menu bar * lisp/progmodes/eglot.el (eglot): Improve the doc string. * lisp/menu-bar.el (menu-bar-tools-menu): Add Eglot to the menu. diff --git a/lisp/menu-bar.el b/lisp/menu-bar.el index 526bccbbac..849e0f7723 100644 --- a/lisp/menu-bar.el +++ b/lisp/menu-bar.el @@ -1847,6 +1847,10 @@ mail status in mode line")) :help "Toggle automatic parsing in source code buffers (Semantic mode)" :button (:toggle . (bound-and-true-p semantic-mode)))) + (bindings--define-key menu [eglot] + '(menu-item "Language Server Support (Eglot)" eglot + :help "Start language server suitable for this buffer's major-mode")) + (bindings--define-key menu [ede] '(menu-item "Project Support (EDE)" global-ede-mode diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0a7cb2a9aa..ada8b01fec 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1039,26 +1039,30 @@ suitable root directory for a given LSP server's purposes." ;;;###autoload (defun eglot (managed-major-mode project class contact language-id &optional interactive) - "Manage a project with a Language Server Protocol (LSP) server. + "Start LSP server in support of PROJECT's buffers under MANAGED-MAJOR-MODE. -The LSP server of CLASS is started (or contacted) via CONTACT. -If this operation is successful, current *and future* file -buffers of MANAGED-MAJOR-MODE inside PROJECT become \"managed\" -by the LSP server, meaning information about their contents is -exchanged periodically to provide enhanced code-analysis via -`xref-find-definitions', `flymake-mode', `eldoc-mode', -`completion-at-point', among others. +This starts a Language Server Protocol (LSP) server suitable for the +buffers of PROJECT whose `major-mode' is MANAGED-MAJOR-MODE. +CLASS is the class of the LSP server to start and CONTACT specifies +how to connect to the server. Interactively, the command attempts to guess MANAGED-MAJOR-MODE -from current buffer, CLASS and CONTACT from -`eglot-server-programs' and PROJECT from +from the current buffer's `major-mode', CLASS and CONTACT from +`eglot-server-programs' looked up by the major mode, and PROJECT from `project-find-functions'. The search for active projects in this context binds `eglot-lsp-context' (which see). -If it can't guess, the user is prompted. With a single -\\[universal-argument] prefix arg, it always prompt for COMMAND. -With two \\[universal-argument] prefix args, also prompts for -MANAGED-MAJOR-MODE. +If it can't guess, it prompts the user for the mode and the server. +With a single \\[universal-argument] prefix arg, it always prompts for COMMAND. +With two \\[universal-argument], it also always prompts for MANAGED-MAJOR-MODE. + +The LSP server of CLASS is started (or contacted) via CONTACT. +If this operation is successful, current *and future* file +buffers of MANAGED-MAJOR-MODE inside PROJECT become \"managed\" +by the LSP server, meaning the information about their contents is +exchanged periodically with the server to provide enhanced +code-analysis via `xref-find-definitions', `flymake-mode', +`eldoc-mode', and `completion-at-point', among others. PROJECT is a project object as returned by `project-current'. commit 3bab83289415f5169e312064c8fc46a0674b8d9e Author: Eli Zaretskii Date: Thu Oct 20 20:17:45 2022 +0300 Mention Eglot in the Emacs user manual * doc/emacs/maintaining.texi (Xref): * doc/emacs/programs.texi (Symbol Completion, Imenu): Mention Eglot. diff --git a/doc/emacs/maintaining.texi b/doc/emacs/maintaining.texi index ad4a3ea350..94171b3a08 100644 --- a/doc/emacs/maintaining.texi +++ b/doc/emacs/maintaining.texi @@ -2094,6 +2094,13 @@ definitions of symbols. (One disadvantage of this kind of backend is that it only knows about subunits that were loaded into the interpreter.) +@item +If Eglot is activated for the current buffer's project +(@pxref{Projects}) and the current buffer's major mode, Eglot consults +an external language server program and provides the data supplied by +the server regarding the definitions of the identifiers in the +project. @xref{Eglot Features,,, eglot, Eglot: The Emacs LSP Client}. + @item An external program can extract references by scanning the relevant files, and build a database of these references. A backend can then diff --git a/doc/emacs/programs.texi b/doc/emacs/programs.texi index 818deb3941..b5e577d96a 100644 --- a/doc/emacs/programs.texi +++ b/doc/emacs/programs.texi @@ -287,6 +287,13 @@ they occur in the buffer; if you want alphabetic sorting, use the symbol @code{imenu--sort-by-name} as the value. You can also define your own comparison function by writing Lisp code. + If Eglot is activated for the current buffer's project +(@pxref{Projects}) and the current buffer's major mode, Eglot provides +its own facility for producing the buffer's index based on the +analysis of the program source by the language-server which manages +the current buffer. @xref{Eglot Features,,, eglot, Eglot: The Emacs +LSP Client}. + Imenu provides the information to guide Which Function mode @ifnottex (@pxref{Which Function}). @@ -1438,6 +1445,13 @@ uses the available support facilities to come up with the completion candidates: @itemize @bullet +@item +If Eglot is activated for the current buffer's project +(@pxref{Projects}) and the current buffer's major mode, the command +tries to use the corresponding language server for producing the list +of completion candidates. @xref{Eglot Features,,, eglot, Eglot: The +Emacs LSP Client}. + @item If Semantic mode is enabled (@pxref{Semantic}), the command tries to use the Semantic parser data for completion. commit 5c99112e8940d8d4ffef393a3fd05d553b43861b Author: Eli Zaretskii Date: Thu Oct 20 19:02:14 2022 +0300 ; Minor copyedits to eglot.texi. diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index e5c38a6e3d..033464f990 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -62,14 +62,20 @@ modify this GNU manual.'' @cindex language server protocol Eglot is the Emacs client for the @dfn{Language Server Protocol} (@acronym{LSP}). The name ``Eglot'' is an acronym that stands for -``@emph{E}macs Poly@emph{glot}''.@footnote{ +@ifhtml +``@emph{E}macs Poly@emph{glot}''. +@end ifhtml +@ifnothtml +``Emacs polyGLOT''. +@end ifnothtml +@footnote{ A @dfn{polyglot} is a person who is able to use several languages. } Eglot provides infrastructure and a set of commands for enriching the source code editing capabilities of Emacs via LSP. LSP is a standardized communications protocol between source code editors (such -as Emacs) and language servers, programs external to Emacs for -analyzing source code on behalf of Emacs. The protocol allows Emacs +as Emacs) and language servers---programs external to Emacs which +analyze the source code on behalf of Emacs. The protocol allows Emacs to receive various source code services from the server, such as description and location of functions calls, types of variables, class definitions, syntactic errors, etc. This way, Emacs doesn't need to @@ -417,9 +423,10 @@ activated automatically as you type. @item If a completion package such as @code{company-mode}, a popular -third-party completion package, is installed, Eglot enhances it by -providing completion candidates based on the language-server analysis -of the source code. (@code{company-mode} can be installed from GNU ELPA.) +third-party completion package (or any other completion package), is +installed, Eglot enhances it by providing completion candidates based +on the language-server analysis of the source code. +(@code{company-mode} can be installed from GNU ELPA.) @item If @code{yasnippet}, a popular third-party package for automatic @@ -432,8 +439,9 @@ completion package to instantiate these snippets using If the popular third-party package @code{markdown-mode} is installed, and the server provides at-point documentation formatted as Markdown in addition to plain text, Eglot arranges for the ElDoc package to -enrich this text with e.g. fontification before displaying it to the -user. +enrich this text with fontifications and other nice formatting before +displaying it to the user. This makes the documentation shown by +ElDoc look nicer on display. @item In addition to enabling and enhancing other features and packages, @@ -777,7 +785,9 @@ unexpectedly. The default value 3 means to attempt reconnection only if the previous successful connection lasted for more than that number of seconds; a different positive value changes the minimal length of the connection to trigger reconnection. A value of @code{t} means -always reconnect automatically, and @code{nil} means never reconnect. +always reconnect automatically, and @code{nil} means never reconnect +(in which case you will need to reconnect manually using @kbd{M-x +eglot}). @item eglot-connect-timeout This specifies the number of seconds before connection attempt to a @@ -798,8 +808,7 @@ all during the waiting period. This determines the size of the Eglot events buffer. @xref{Eglot Commands, eglot-events-buffer}, for how to display that buffer. If the value is changed, for it to take effect the connection should be -restarted using @kbd{eglot-shutdown} followed by -@kbd{eglot-reconnect}. +restarted using @kbd{M-x eglot-reconnect}. @c FIXME: Shouldn't the defcustom do this by itself using the :set @c attribute? @xref{Troubleshooting Eglot}, for when this could be useful. @@ -854,6 +863,7 @@ connections, are documented in @ref{Customizing Eglot}. @chapter Customizing Eglot @cindex customizing Eglot +Eglot itself has a relatively small number of customization options. A large part of customizing Eglot to your needs and preferences should actually be done via options of the Emacs packages and features which Eglot supports and enhances (@pxref{Eglot Features}). For example: commit c44ea4548da12ad5d43cacbc1f26831bb8c1f7fe Author: Eli Zaretskii Date: Thu Oct 20 17:08:15 2022 +0300 ; * doc/misc/eglot.texi: Fix a typo. diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index 25c04940c9..e5c38a6e3d 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -122,7 +122,7 @@ you want to use Eglot, you just need to make sure those servers are installed on your system. Alternatively, install one or more servers of your choice and add them to the value of @code{eglot-server-programs}, as described in @ref{Setting Up LSP -Server}. +Servers}. @item Turn on Eglot for your project. commit a549316c7dce18a47ef88d35aca7d867468432a1 Author: Eli Zaretskii Date: Thu Oct 20 17:05:01 2022 +0300 ; * doc/misc/eglot.texi: Undo some recent "fixes" to the Eglot manual. diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index 6a4127bed7..25c04940c9 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -122,7 +122,7 @@ you want to use Eglot, you just need to make sure those servers are installed on your system. Alternatively, install one or more servers of your choice and add them to the value of @code{eglot-server-programs}, as described in @ref{Setting Up LSP -Servers}. +Server}. @item Turn on Eglot for your project. @@ -185,10 +185,8 @@ further discussed in this manual; refer to the documentation of the particular server(s) you want to install. To use a language server, Eglot must know how to start it and which -programming languages each server supports. Eglot comes with a fairly -complete set of associations of major-modes to popular language -servers predefined. This information is provided by the -@code{eglot-server-programs} variable. +programming languages each server supports. This information is +provided by the variable @code{eglot-server-programs}. @defvar eglot-server-programs This variable associates major modes with names and command-line @@ -203,13 +201,13 @@ The value of the variable is an alist, whose elements are of the form The @var{major-mode} of the alist elements can be either a symbol of an Emacs major mode or a list of the form @w{@code{(@var{mode} :language-id @var{id})}}, with @var{mode} being a major-mode symbol -and @var{id} a string that identifies the language to the server. The -latter form should be used if Eglot cannot by itself convert the -major-mode to the language identifier string required by the server. -In addition, @var{major-mode} can be a list of several major modes -specified in one of the above forms -- this means a running instance -of the associated server is responsible for files of multiple major -modes or languages in the project. +and @var{id} a string that identifies the language to the server (if +Eglot cannot by itself convert the major-mode to the language +identifier string required by the server). In addition, +@var{major-mode} can be a list of several major modes specified in one +of the above forms -- this means a running instance of the associated +server is responsible for files of multiple major modes or languages +in the project. The @var{server} part of the alist elements can be one of the following: @@ -251,11 +249,13 @@ arguments. @end defvar -If you need to add server associations to the default list, use -@code{add-to-list}. For example, if there is a hypothetical language -server program @command{fools} for the language @code{Foo} which is -supported by an Emacs major-mode @code{foo-mode}, you can add it to -the alist like this: +Eglot comes with a fairly complete set of associations of major-modes +to popular language servers predefined. If you need to add server +associations to the default list, use @code{add-to-list}. For +example, if there is a hypothetical language server program +@command{fools} for the language @code{Foo} which is supported by an +Emacs major-mode @code{foo-mode}, you can add it to the alist like +this: @lisp (add-to-list 'eglot-server-programs @@ -372,7 +372,8 @@ commands and variables. Once Eglot is enabled in a buffer, it uses LSP and the language-server capabilities to activate, enable, and enhance modern IDE features in Emacs. The features themselves are usually provided via other Emacs -packages. These are the main features that Eglot enables and provides: +packages. Here's the list of the main features that Eglot enables and +provides: @itemize @bullet @item @@ -395,7 +396,7 @@ emacs, GNU Emacs Manual}). Eglot provides a backend for the Xref capabilities which uses the language-server understanding of the program source. In particular, it eliminates the need to generate tags tables (@pxref{Tags tables,,, emacs, GNU Emacs Manual}) for -languages that are only supported by the @code{etags} backend. +languages which are only supported by the @code{etags} backend. @item Buffer navigation by name of function, class, method, etc., via Imenu @@ -507,7 +508,7 @@ directory. @item A VC project: source files in a directory hierarchy under some VCS, -e.g.@: a Git repository (@pxref{Version Control,,, emacs, GNU Emacs +e.g.@: a VCS repository (@pxref{Version Control,,, emacs, GNU Emacs Manual}). @item @@ -620,9 +621,8 @@ will be added to those managed by an existing server session. The command attempts to figure out the buffer's major mode and the suitable language server; in case it fails, it might prompt for the major mode to use and for the server program to start. If invoked -with a prefix argument @kbd{C-u}, it always prompts for the server -program, and if invoked with @kbd{C-u C-u}, also prompt for the major -mode. +with @kbd{C-u}, it always prompts for the server program, and if +invoked with @kbd{C-u C-u}, it also prompts for the major mode. If the language server is successfully started and contacted, this command arranges for any other buffers belonging to the same project @@ -637,13 +637,13 @@ Emacs features will be configured to use Eglot, use the @code{eglot-stay-out-of} option (@pxref{Customizing Eglot}). @item M-x eglot-reconnect -Shuts down an the current connection to the language server and -immediately restarts it using the same options used originally. This -can sometimes be useful to unclog a partially malfunctioning server -connection. +This command shuts down the current connection to the language +server and immediately restarts it using the same options used +originally. This can sometimes be useful to unclog a partially +malfunctioning server connection. @item M-x eglot-shutdown -Shuts down a language server. This commands prompts for a language +This command shuts down a language server. It prompts for a language server to shut down (unless there's only one server session, and it manages the current buffer). Then the command shuts down the server and stops managing the buffers the server was used for. Emacs @@ -1126,4 +1126,3 @@ that used Eglot to communicate with the language server. @printindex cp @bye - commit 83fbda715973f57dc49fe002d255ecaff8273154 Merge: 6f3ade1c08 8b3a700327 Author: João Távora Date: Thu Oct 20 13:50:09 2022 +0100 Merge branch 'feature/eglot2emacs' commit 8b3a7003274de7b184b71c4552e6c4518948bcfe Author: João Távora Date: Thu Oct 20 13:49:49 2022 +0100 ; fix warning about order of defvaralias/defconst * lisp/progmodes/eglot.el (eglot-{}): Declare alias before thing being aliased. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 901bf30d4b..0a7cb2a9aa 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -406,8 +406,8 @@ This can be useful when using docker to run a language server.") `((1 . eglot-diagnostic-tag-unnecessary-face) (2 . eglot-diagnostic-tag-deprecated-face))) -(defconst eglot--{} (make-hash-table :size 1) "The empty JSON object.") (defvaralias 'eglot-{} 'eglot--{}) +(defconst eglot--{} (make-hash-table :size 1) "The empty JSON object.") (defun eglot--executable-find (command &optional remote) "Like Emacs 27's `executable-find', ignore REMOTE on Emacs 26." commit 785197a07634050b8cb79f1b0c93a16712336529 Author: João Távora Date: Thu Oct 20 12:59:22 2022 +0100 Minor fixes to doc/misc/eglot.texi * doc/misc/eglot.texi (eglot-workspace-configuration): Correct markup of eglot-{} (Quick Start): Fix section cross reference. diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index c1de1e818d..6a4127bed7 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -122,7 +122,7 @@ you want to use Eglot, you just need to make sure those servers are installed on your system. Alternatively, install one or more servers of your choice and add them to the value of @code{eglot-server-programs}, as described in @ref{Setting Up LSP -Server}. +Servers}. @item Turn on Eglot for your project. @@ -969,7 +969,7 @@ object. @var{plist} may be arbitrarity complex, generally containing other keywork-value property sublists corresponding to JSON subobjects. The JSON values @code{true}, @code{false}, @code{null} and @code{@{@}} are represented by the Lisp values @code{t}, @code{:json-false}, -@code{nil}, and @{eglot-@{@}}, respectively. +@code{nil}, and @code{eglot-@{@}}, respectively. @findex eglot-show-workspace-configuration When experimenting with workspace settings, you can use the command @@ -1126,3 +1126,4 @@ that used Eglot to communicate with the language server. @printindex cp @bye + commit 69abb438b80c18c6409c7423b1fddec8d3da4165 Author: João Távora Date: Thu Oct 20 12:45:31 2022 +0100 * lisp/info-look.el (mapc): Add Eglot manual's index. diff --git a/lisp/info-look.el b/lisp/info-look.el index ce0a08dcbe..2eec6f49f5 100644 --- a/lisp/info-look.el +++ b/lisp/info-look.el @@ -1051,6 +1051,7 @@ Return nil if there is nothing appropriate in the buffer near point." ("eieio" "Function Index") ("gnutls" "(emacs-gnutls)Variable Index" "(emacs-gnutls)Function Index") ("mm" "(emacs-mime)Index") + ("eglot" "Index") ("epa" "Variable Index" "Function Index") ("ert" "Index") ("eshell" "Function and Variable Index") commit 6f3ade1c08c6cbf56c0dc0d12e9508c261eb42bf Author: Po Lu Date: Sun Aug 7 13:46:52 2022 +0800 Work around problems setting input focus when a frame is in the background * src/xterm.c (server_timestamp_predicate, x_get_server_time): New functions. (x_ewmh_activate_frame, x_focus_frame, syms_of_xterm): Apply various workarounds for window manager "focus stealing prevention". (bug#57012) diff --git a/src/xterm.c b/src/xterm.c index ade5600f4d..8b3d6f77a6 100644 --- a/src/xterm.c +++ b/src/xterm.c @@ -27134,6 +27134,64 @@ xembed_request_focus (struct frame *f) XEMBED_REQUEST_FOCUS, 0, 0, 0); } +static Bool +server_timestamp_predicate (Display *display, XEvent *xevent, + XPointer arg) +{ + XID *args = (XID *) arg; + + if (xevent->type == PropertyNotify + && xevent->xproperty.window == args[0] + && xevent->xproperty.atom == args[1]) + return True; + + return False; +} + +/* Get the server time. The X server is guaranteed to deliver the + PropertyNotify event, so there is no reason to use x_if_event. */ + +static Time +x_get_server_time (struct frame *f) +{ + Atom property_atom; + XEvent property_dummy; + struct x_display_info *dpyinfo; + XID client_data[2]; +#if defined HAVE_XSYNC && !defined USE_GTK && defined HAVE_CLOCK_GETTIME + uint_fast64_t current_monotonic_time; +#endif + + /* If the server time is the same as the monotonic time, avoid a + roundtrip by using that instead. */ + +#if defined HAVE_XSYNC && !defined USE_GTK && defined HAVE_CLOCK_GETTIME + if (FRAME_DISPLAY_INFO (f)->server_time_monotonic_p) + { + current_monotonic_time = x_sync_current_monotonic_time (); + + if (current_monotonic_time) + /* Truncate the time to CARD32. */ + return (current_monotonic_time / 1000) & X_ULONG_MAX; + } +#endif + + dpyinfo = FRAME_DISPLAY_INFO (f); + property_atom = dpyinfo->Xatom_EMACS_SERVER_TIME_PROP; + client_data[0] = FRAME_OUTER_WINDOW (f); + client_data[1] = property_atom; + + XChangeProperty (dpyinfo->display, FRAME_OUTER_WINDOW (f), + property_atom, XA_ATOM, 32, + PropModeReplace, + (unsigned char *) &property_atom, 1); + + XIfEvent (dpyinfo->display, &property_dummy, + server_timestamp_predicate, (XPointer) client_data); + + return property_dummy.xproperty.time; +} + /* Activate frame with Extended Window Manager Hints */ static void @@ -27141,6 +27199,7 @@ x_ewmh_activate_frame (struct frame *f) { XEvent msg; struct x_display_info *dpyinfo; + Time time; dpyinfo = FRAME_DISPLAY_INFO (f); @@ -27161,6 +27220,43 @@ x_ewmh_activate_frame (struct frame *f) msg.xclient.data.l[3] = 0; msg.xclient.data.l[4] = 0; + /* No frame is currently focused on that display, so apply any + bypass for focus stealing prevention that the user has + specified. */ + if (!dpyinfo->x_focus_frame) + { + if (EQ (Vx_allow_focus_stealing, Qimitate_pager)) + msg.xclient.data.l[0] = 2; + else if (EQ (Vx_allow_focus_stealing, Qnewer_time)) + { + block_input (); + time = x_get_server_time (f); +#ifdef USE_GTK + x_set_gtk_user_time (f, time); +#endif + /* Temporarily override dpyinfo->x_focus_frame so the + user time property is set on the right window. */ + dpyinfo->x_focus_frame = f; + x_display_set_last_user_time (dpyinfo, time, true, true); + dpyinfo->x_focus_frame = NULL; + unblock_input (); + + msg.xclient.data.l[1] = time; + } + else if (EQ (Vx_allow_focus_stealing, Qraise_and_focus)) + { + time = x_get_server_time (f); + + x_ignore_errors_for_next_request (dpyinfo); + XSetInputFocus (FRAME_X_DISPLAY (f), FRAME_OUTER_WINDOW (f), + RevertToParent, time); + XRaiseWindow (FRAME_X_DISPLAY (f), FRAME_OUTER_WINDOW (f)); + x_stop_ignoring_errors (dpyinfo); + + return; + } + } + XSendEvent (dpyinfo->display, dpyinfo->root_window, False, (SubstructureRedirectMask | SubstructureNotifyMask), &msg); @@ -30649,6 +30745,9 @@ With MS Windows, Haiku windowing or Nextstep, the value is t. */); Fput (Qsuper, Qmodifier_value, make_fixnum (super_modifier)); DEFSYM (QXdndSelection, "XdndSelection"); DEFSYM (Qx_selection_alias_alist, "x-selection-alias-alist"); + DEFSYM (Qimitate_pager, "imitate-pager"); + DEFSYM (Qnewer_time, "newer-time"); + DEFSYM (Qraise_and_focus, "raise-and-focus"); DEFVAR_LISP ("x-ctrl-keysym", Vx_ctrl_keysym, doc: /* Which keys Emacs uses for the ctrl modifier. @@ -30902,4 +31001,24 @@ connection setup. */); /* The default value of this variable is chosen so that updating the tool bar does not require a call to _XReply. */ Vx_fast_selection_list = list1 (QCLIPBOARD); + + DEFVAR_LISP ("x-allow-focus-stealing", Vx_allow_focus_stealing, + doc: /* How to bypass window manager focus stealing prevention. + +Some window managers prevent `x-focus-frame' from activating the given +frame when Emacs is in the background, which is especially prone to +cause problems when the Emacs server wants to activate itself. This +variable specifies the strategy used to activate frames when that is +the case, and has several valid values (any other value means to not +bypass window manager focus stealing prevention): + + - The symbol `imitate-pager', which means to pretend that Emacs is a + pager. + + - The symbol `newer-time', which means to fetch the current time + from the X server and use it to activate the frame. + + - The symbol `raise-and-focus', which means to raise the window and + focus it manually. */); + Vx_allow_focus_stealing = Qnewer_time; } commit e0616f2d3cbc559560bf15346dd8a1824603bd80 Author: Po Lu Date: Sun Aug 7 13:49:59 2022 +0800 * etc/PROBLEMS: Document window manager focus problems. diff --git a/etc/PROBLEMS b/etc/PROBLEMS index ed2bc1ae05..aaecc41f6e 100644 --- a/etc/PROBLEMS +++ b/etc/PROBLEMS @@ -1229,6 +1229,17 @@ you should use an Emacs input method instead. ** X keyboard problems +*** `x-focus-frame' fails to activate the frame. + +Some window managers prevent `x-focus-frame' from activating the given +frame when Emacs is in the background. + +Emacs tries to work around this problem by default, but the workaround +does not work on all window managers. You can try different +workarounds by changing the value of `x-allow-focus-stealing' (see its +doc string for more details). The value `imitate-pager' may be +required on some versions of KWin. + *** You "lose characters" after typing Compose Character key. This is because the Compose Character key is defined as the keysym commit df31a36f69d29502bba6e32047d660dc014668ab Merge: 0186faf2a1 0e7361a5cc Author: João Távora Date: Thu Oct 20 12:30:49 2022 +0100 Merge branch 'feature/eglot-texi-manual' into feature/eglot2emacs commit 0186faf2a15cc918de0e7f57d05a14cab33465c3 Merge: 7ee5b0f85f 806734c1b1 Author: João Távora Date: Thu Oct 20 12:16:19 2022 +0100 ; Merge from https://github.com/joaotavora/eglot commit 0e7361a5ccce084613d54d6ba3954ffca6074817 (refs/remotes/origin/feature/eglot-texi-manual) Author: João Távora Date: Thu Oct 20 11:42:25 2022 +0100 Revert "eglot.texi: Make example more realistic" This quest for realism ignores the fact that a previous example for a hypothetical language Foo and a language server "fools" already exists. It also undermines the intended generality of the instructions. This reverts commit 16986a9cc42ef4de580456f4acc5feba682ac8b1. diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index a05e7fd7ee..c1de1e818d 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -525,13 +525,13 @@ directory, which is usually the top-level directory of the project's directory hierarchy. This ensures the language server has the same comprehensive view of the project's files as you do. -For example, if you visit the file @file{~/projects/fooey/lib/x.c} -and @file{x.c} belongs to a project rooted at +For example, if you visit the file @file{~/projects/fooey/lib/x.foo} +and @file{x.foo} belongs to a project rooted at @file{~/projects/fooey} (perhaps because a @file{.git} directory exists there), then @kbd{M-x eglot} causes the server program to start with that root as the current working directory. The server then will -analyze not only the file @file{lib/x.c} you visited, but likely -also all the other @file{*.c} files under the +analyze not only the file @file{lib/x.foo} you visited, but likely +also all the other @file{*.foo} files under the @file{~/projects/fooey} directory. In some cases, additional information specific to a given project will @@ -594,8 +594,8 @@ When you visit a file under the same project, whether an existing or a new file, its buffer is automatically added to the set of buffers managed by Eglot, and the server which supports the buffer's major-mode is notified about that. Thus, visiting a non-existent file -@file{/home/joe/projects/fooey/lib/y.c} in the above example will -notify the server of the @file{*.c} files' language that a new file +@file{/home/joe/projects/fooey/lib/y.foo} in the above example will +notify the server of the @file{*.foo} files' language that a new file was added to the project, even before the file appears on disk. The special Eglot minor mode is also turned on automatically in the buffer visiting the file. commit 2a9797b422fe8a28bfaab49974ea2be21ae2502e Author: João Távora Date: Thu Oct 20 11:20:30 2022 +0100 Fix Eglot manual's description of eglot-workspace-configuration * doc/misc/eglot.texi (Customizing Eglot) (eglot-workspace-configuration): Explain that plist may be arbitrarily complex and correctly identify nil as the Elisp equivalent to JSON null. diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index caf09769b4..a05e7fd7ee 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -964,11 +964,12 @@ this variable should be a property list of the following format: @noindent Here @code{:@var{server}} identifies a particular language server and @var{plist} is the corresponding keyword-value property list of one or -more parameter settings for that server. That list of parameters is -serialized to JSON by Eglot and sent to the server. For that reason -JSON values @code{true}, @code{false}, and @code{@{@}} should be -represented in the property lists as Lisp symbols @code{t}, -@code{:json-false}, and @code{nil}, respectively. +more parameter settings for that server, serialized by Eglot as a JSON +object. @var{plist} may be arbitrarity complex, generally containing +other keywork-value property sublists corresponding to JSON subobjects. +The JSON values @code{true}, @code{false}, @code{null} and @code{@{@}} +are represented by the Lisp values @code{t}, @code{:json-false}, +@code{nil}, and @{eglot-@{@}}, respectively. @findex eglot-show-workspace-configuration When experimenting with workspace settings, you can use the command commit 7ee5b0f85f32dd6fd874a12a033798ac56ecbacc Author: Mauro Aranda Date: Thu Oct 20 07:34:38 2022 -0300 Improve HERE document detection in perl-mode * lisp/progmodes/perl-mode.el (perl-syntax-propertize-function): Detect indented HERE documents when using a bare identifier. (perl--syntax-exp-intro-keywords): Recognize HERE documents that come after die, warn and eval. (perl--syntax-exp-intro-regexp): Identify HERE documents when printing to a filehandle with printf? and when they appear after a fat comma. * test/lisp/progmodes/cperl-mode-resources/here-docs.pl: Add more tests. diff --git a/lisp/progmodes/perl-mode.el b/lisp/progmodes/perl-mode.el index 7b7a2cdf01..c5d5d703fc 100644 --- a/lisp/progmodes/perl-mode.el +++ b/lisp/progmodes/perl-mode.el @@ -215,11 +215,16 @@ (eval-and-compile (defconst perl--syntax-exp-intro-keywords '("split" "if" "unless" "until" "while" "print" "printf" - "grep" "map" "not" "or" "and" "for" "foreach" "return")) + "grep" "map" "not" "or" "and" "for" "foreach" "return" "die" + "warn" "eval")) (defconst perl--syntax-exp-intro-regexp (concat "\\(?:\\(?:^\\|[^$@&%[:word:]]\\)" (regexp-opt perl--syntax-exp-intro-keywords) + ;; A HERE document as an argument to printf? + ;; when printing to a filehandle. + "\\|printf?[ \t]*$?[_[:alpha:]][_[:alnum:]]*" + "\\|=>" "\\|[?:.,;|&*=!~({[]" "\\|[^-+][-+]" ;Bug#42168: `+' is intro but `++' isn't! "\\|\\(^\\)\\)[ \t\n]*"))) @@ -335,7 +340,7 @@ "<<\\(~\\)?[ \t]*\\('[^'\n]*'\\|\"[^\"\n]*\"\\|\\\\[[:alpha:]][[:alnum:]]*\\)" ;; The < <<~HERE +look-here +HERE + ); + +$noindent = "New statement in this line"; + +=head2 Test case 10 + +A HERE document as an argument to die. + +=cut + +1 or die < Date: Thu Oct 20 11:06:44 2022 +0100 Expose eglot-{} to be used in eglot-workspace-configuration * eglot.el (eglot-{}): New variable alias. GitHub-reference: per https://github.com/joaotavora/eglot/issues/1084 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1520d991ff..901bf30d4b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -407,6 +407,7 @@ This can be useful when using docker to run a language server.") (2 . eglot-diagnostic-tag-deprecated-face))) (defconst eglot--{} (make-hash-table :size 1) "The empty JSON object.") +(defvaralias 'eglot-{} 'eglot--{}) (defun eglot--executable-find (command &optional remote) "Like Emacs 27's `executable-find', ignore REMOTE on Emacs 26." commit 9801e217f9842190f2303e46f6d41202cfe6b546 Author: João Távora Date: Thu Oct 20 10:48:11 2022 +0100 Rework header of eglot.el * eglot.el (Commentary): Rework. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 18523067fa..1520d991ff 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1,4 +1,4 @@ -;;; eglot.el --- Client for Language Server Protocol (LSP) servers -*- lexical-binding: t; -*- +;;; eglot.el --- The Emacs Client for LSP servers -*- lexical-binding: t; -*- ;; Copyright (C) 2018-2022 Free Software Foundation, Inc. @@ -7,11 +7,11 @@ ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.14") (flymake "1.2.1") (project "0.3.0") (xref "1.0.1") (eldoc "1.11.0") (seq "2.23")) +;; Package-Requires: ((emacs "26.3") (jsonrpc "1.0.14") (flymake "1.2.1") (project "0.3.0") (xref "1.0.1") (eldoc "1.11.0") (seq "2.23")) -;; This is (or will soon) be a GNU ELPA :core package. Avoid using -;; functionality that not compatible with the version of Emacs -;; recorded above. +;; This is is a GNU ELPA :core package. Avoid adding functionality +;; that is not available in the version of Emacs recorded above or any +;; of the package dependencies. ;; This file is part of GNU Emacs. @@ -33,32 +33,63 @@ ;; Eglot ("Emacs Polyglot") is an Emacs LSP client that stays out of ;; your way. ;; -;; Typing M-x eglot should be enough to get you started, but here's a -;; little info (see the accompanying README.md or the URL for more). +;; Typing M-x eglot in some source file is often enough to get you +;; started, if the language server you're looking to use is installed +;; in your system. Please refer to the manual, available from +;; https://joaotavora.github.io/eglot/ or from M-x info for more usage +;; instructions. ;; -;; M-x eglot starts a server via a shell command guessed from -;; `eglot-server-programs', using the current major mode (for whatever -;; language you're programming in) as a hint. If it can't guess, it -;; prompts you in the minibuffer for these things. Actually, the -;; server does not need to be running locally: you can connect to a -;; running server via TCP by entering a syntax. +;; If you wish to contribute changes to Eglot, please do read the user +;; manual first. Additionally, take the following in consideration: + +;; * Eglot's main job is to hook up the information that language +;; servers offer via LSP to Emacs's UI facilities: Xref for +;; definition-chasing, Flymake for diagnostics, Eldoc for at-point +;; documentation, etc. Eglot's job is generally *not* to provide +;; such a UI itself, though a small number of simple +;; counter-examples do exist, for example in the `eglot-rename' +;; command. When a new UI is evidently needed, consider adding a +;; new package to Emacs, or extending an existing one. ;; -;; If the connection is successful, you should see an `eglot' -;; indicator pop up in your mode-line. More importantly, this means -;; that current *and future* file buffers of that major mode *inside -;; your current project* automatically become \"managed\" by the LSP -;; server. In other words, information about their content is -;; exchanged periodically to provide enhanced code analysis using -;; `xref-find-definitions', `flymake-mode', `eldoc-mode', -;; `completion-at-point', among others. +;; * Eglot was designed to function with just the UI facilities found +;; in the latest Emacs core, as long as those facilities are also +;; available as GNU ELPA :core packages. Historically, a number of +;; :core packages were added or reworked in Emacs to make this +;; possible. This principle should be upheld when adding new LSP +;; features or tweaking exising ones. Design any new facilities in +;; a way that they could work in the absence of LSP or using some +;; different protocol, then make sure Eglot can link up LSP +;; information to it. + +;; * There are few Eglot configuration variables. This principle +;; should also be upheld. If Eglot had these variables, it could be +;; duplicating configuration found elsewhere, bloating itself up, +;; and making it generally hard to integrate with the ever growing +;; set of LSP features and Emacs packages. For instance, this is +;; why one finds a single variable +;; `eglot-ignored-server-capabilities' instead of a number of +;; capability-specific flags, or why customizing the display of +;; LSP-provided documentation is done via ElDoc's variables, not +;; Eglot's. ;; -;; To "unmanage" these buffers, shutdown the server with -;; M-x eglot-shutdown. +;; * Linking up LSP information to other libraries is generally done +;; in the `eglot--managed-mode' minor mode function, by +;; buffer-locally setting the other library's variables to +;; Eglot-specific versions. When deciding what to set the variable +;; to, the general idea is to choose a good default for beginners +;; that doesn't clash with Emacs's defaults. The settings are only +;; in place during Eglot's LSP-enriched tenure over a project. Even +;; so, some of those decisions will invariably aggravate a minority +;; of Emacs power users, but these users can use `eglot-stay-out-of' +;; and `eglot-managed-mode-hook' to quench their OCD. ;; -;; To start an eglot session automatically when a foo-mode buffer is -;; visited, you can put this in your init file: -;; -;; (add-hook 'foo-mode-hook 'eglot-ensure) +;; * On occasion, to enable new features, Eglot can have soft +;; dependencies on popular libraries that are not in Emacs core. +;; "Soft" means that the dependency doesn't impair any other use of +;; Eglot beyond that feature. Such is the case of the snippet +;; functionality, via the Yasnippet package, Markdown formatting of +;; at-point documentation via the markdown-mode package, and nicer +;; looking completions when the Company package is used. ;;; Code: commit eb9d6281b58f50927afdc2fdb2fcebf76e2ffe23 Author: João Távora Date: Mon Oct 10 13:57:26 2022 +0100 Do use eglot-connect-timeout if eglot-sync-connect is t Reported by Eli Zaretskii * eglot.el (eglot--connect): Use eglot-connect-timeout in the case eglot-sync-connect is t. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d3f5935a9e..18523067fa 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1282,7 +1282,8 @@ in project `%s'." (cond ((numberp eglot-sync-connect) (accept-process-output nil eglot-sync-connect)) (eglot-sync-connect - (while t (accept-process-output nil 30))))))) + (while t (accept-process-output + nil eglot-connect-timeout))))))) (pcase retval (`(error . ,msg) (eglot--error msg)) (`nil (eglot--message "Waiting in background for server `%s'" commit 5d73bc5c69f56ce1b22c950d19ef6406b662949f Author: Stefan Kangas Date: Thu Oct 20 11:43:08 2022 +0200 eglot.texi: Explain where to find third-party packages * doc/misc/eglot.texi (Eglot Features): Improve description on third-party packages. diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index e5f2606082..caf09769b4 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -418,19 +418,21 @@ activated automatically as you type. If a completion package such as @code{company-mode}, a popular third-party completion package, is installed, Eglot enhances it by providing completion candidates based on the language-server analysis -of the source code. +of the source code. (@code{company-mode} can be installed from GNU ELPA.) @item -If @code{yasnippet}, a popular package for automatic insertion of code -templates (snippets), is installed, and the language server supports -snippet completion candidates, Eglot arranges for the completion -package to instantiate these snippets using @code{yasnippet}. +If @code{yasnippet}, a popular third-party package for automatic +insertion of code templates (snippets), is installed, and the language +server supports snippet completion candidates, Eglot arranges for the +completion package to instantiate these snippets using +@code{yasnippet}. (@code{yasnippet} can be installed from GNU ELPA.) @item -If the popular package @code{markdown-mode} is installed, and the -server provides at-point documentation formatted as Markdown in -addition to plain text, Eglot arranges for the ElDoc package to enrich -this text with e.g. fontification before displaying it to the user. +If the popular third-party package @code{markdown-mode} is installed, +and the server provides at-point documentation formatted as Markdown +in addition to plain text, Eglot arranges for the ElDoc package to +enrich this text with e.g. fontification before displaying it to the +user. @item In addition to enabling and enhancing other features and packages, commit 16986a9cc42ef4de580456f4acc5feba682ac8b1 Author: Stefan Kangas Date: Thu Oct 20 11:42:29 2022 +0200 eglot.texi: Make example more realistic * doc/misc/eglot.texi (Eglot and Buffers): Prefer more realistic *.c instead of *.foo in example. diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index 9ed482cec6..e5f2606082 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -523,13 +523,13 @@ directory, which is usually the top-level directory of the project's directory hierarchy. This ensures the language server has the same comprehensive view of the project's files as you do. -For example, if you visit the file @file{~/projects/fooey/lib/x.foo} -and @file{x.foo} belongs to a project rooted at +For example, if you visit the file @file{~/projects/fooey/lib/x.c} +and @file{x.c} belongs to a project rooted at @file{~/projects/fooey} (perhaps because a @file{.git} directory exists there), then @kbd{M-x eglot} causes the server program to start with that root as the current working directory. The server then will -analyze not only the file @file{lib/x.foo} you visited, but likely -also all the other @file{*.foo} files under the +analyze not only the file @file{lib/x.c} you visited, but likely +also all the other @file{*.c} files under the @file{~/projects/fooey} directory. In some cases, additional information specific to a given project will @@ -592,8 +592,8 @@ When you visit a file under the same project, whether an existing or a new file, its buffer is automatically added to the set of buffers managed by Eglot, and the server which supports the buffer's major-mode is notified about that. Thus, visiting a non-existent file -@file{/home/joe/projects/fooey/lib/y.foo} in the above example will -notify the server of the @file{*.foo} files' language that a new file +@file{/home/joe/projects/fooey/lib/y.c} in the above example will +notify the server of the @file{*.c} files' language that a new file was added to the project, even before the file appears on disk. The special Eglot minor mode is also turned on automatically in the buffer visiting the file. commit 2d2cdb4741a3c1e42c8ed771303a878fd428911b Author: Stefan Kangas Date: Thu Oct 20 11:40:37 2022 +0200 eglot.texi: Move sentence on LSP Servers earlier * doc/misc/eglot.texi (Setting Up LSP Servers): Move explanation on the (lack of) need for customizing servers earlier. diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index bd92582f90..9ed482cec6 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -185,8 +185,10 @@ further discussed in this manual; refer to the documentation of the particular server(s) you want to install. To use a language server, Eglot must know how to start it and which -programming languages each server supports. This information is -provided by the @code{eglot-server-programs} variable. +programming languages each server supports. Eglot comes with a fairly +complete set of associations of major-modes to popular language +servers predefined. This information is provided by the +@code{eglot-server-programs} variable. @defvar eglot-server-programs This variable associates major modes with names and command-line @@ -249,9 +251,7 @@ arguments. @end defvar -Eglot comes with a fairly complete set of associations of major-modes -to popular language servers predefined in this variable. If you need -to add server associations to the default list, use +If you need to add server associations to the default list, use @code{add-to-list}. For example, if there is a hypothetical language server program @command{fools} for the language @code{Foo} which is supported by an Emacs major-mode @code{foo-mode}, you can add it to commit 4725c123f33eb9579b695748fa9f85c9af3eb01a Author: Stefan Kangas Date: Thu Oct 20 11:38:18 2022 +0200 ; eglot.texi: Fix typos and minor inconsistencies * doc/misc/eglot.texi: Fix typos and minor inconsistencies. diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index 64f4c84dfd..bd92582f90 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -89,7 +89,7 @@ This manual documents how to configure, use, and customize Eglot. @menu * Quick Start:: For the impatient. -* Eglot and LSP Servers:: How to work with language servers +* Eglot and LSP Servers:: How to work with language servers. * Using Eglot:: Important Eglot commands and variables. * Customizing Eglot:: Eglot customization and advanced features. * Troubleshooting Eglot:: Troubleshooting and reporting bugs. @@ -201,13 +201,13 @@ The value of the variable is an alist, whose elements are of the form The @var{major-mode} of the alist elements can be either a symbol of an Emacs major mode or a list of the form @w{@code{(@var{mode} :language-id @var{id})}}, with @var{mode} being a major-mode symbol -and @var{id} a string that identifies the language to the server (if -Eglot cannot by itself convert the major-mode to the language -identifier string required by the server). In addition, -@var{major-mode} can be a list of several major mode specified in one -of the above forms -- this means a running instance of the associated -server is responsible for files of multiple major modes or languages -in the project. +and @var{id} a string that identifies the language to the server. The +latter form should be used if Eglot cannot by itself convert the +major-mode to the language identifier string required by the server. +In addition, @var{major-mode} can be a list of several major modes +specified in one of the above forms -- this means a running instance +of the associated server is responsible for files of multiple major +modes or languages in the project. The @var{server} part of the alist elements can be one of the following: @@ -327,12 +327,12 @@ Emacs session, it runs the hook @code{eglot-connect-hook} When Eglot is turned on, it arranges for turning itself off automatically if the language server process terminates. Turning off -Eglot means it shuts down the server connection, ceases its management -of all the buffers that use the server connection which was +Eglot means that it shuts down the server connection, ceases its +management of all the buffers that use the server connection which was terminated, deactivates its minor mode, and restores the original values of the Emacs variables that Eglot changed when it was turned -on. @xref{Eglot and Buffers}, for more details of what does Eglot -management of a buffer entail. +on. @xref{Eglot and Buffers}, for more details of what Eglot +management of a buffer entails. @findex eglot-shutdown You can also shut down a language server manually, by using the @@ -372,7 +372,7 @@ commands and variables. Once Eglot is enabled in a buffer, it uses LSP and the language-server capabilities to activate, enable, and enhance modern IDE features in Emacs. The features themselves are usually provided via other Emacs -packages. Here are the main features Eglot enables and provides: +packages. These are the main features that Eglot enables and provides: @itemize @bullet @item @@ -395,7 +395,7 @@ emacs, GNU Emacs Manual}). Eglot provides a backend for the Xref capabilities which uses the language-server understanding of the program source. In particular, it eliminates the need to generate tags tables (@pxref{Tags tables,,, emacs, GNU Emacs Manual}) for -languages which are only supported by the @code{etags} backend. +languages that are only supported by the @code{etags} backend. @item Buffer navigation by name of function, class, method, etc., via Imenu @@ -416,7 +416,7 @@ activated automatically as you type. @item If a completion package such as @code{company-mode}, a popular -3rd-party completion package, is installed, Eglot enhances it by +third-party completion package, is installed, Eglot enhances it by providing completion candidates based on the language-server analysis of the source code. @@ -505,7 +505,7 @@ directory. @item A VC project: source files in a directory hierarchy under some VCS, -i.e.@: a VCS repository (@pxref{Version Control,,, emacs, GNU Emacs +e.g.@: a Git repository (@pxref{Version Control,,, emacs, GNU Emacs Manual}). @item @@ -618,8 +618,9 @@ will be added to those managed by an existing server session. The command attempts to figure out the buffer's major mode and the suitable language server; in case it fails, it might prompt for the major mode to use and for the server program to start. If invoked -with @kbd{C-u}, it always prompts for the server program, and if -invoked with @kbd{C-u C-u}, also prompt for the major mode. +with a prefix argument @kbd{C-u}, it always prompts for the server +program, and if invoked with @kbd{C-u C-u}, also prompt for the major +mode. If the language server is successfully started and contacted, this command arranges for any other buffers belonging to the same project @@ -640,7 +641,7 @@ can sometimes be useful to unclog a partially malfunctioning server connection. @item M-x eglot-shutdown -Shut down a language server. This commands prompts for a language +Shuts down a language server. This commands prompts for a language server to shut down (unless there's only one server session, and it manages the current buffer). Then the command shuts down the server and stops managing the buffers the server was used for. Emacs @@ -863,7 +864,7 @@ customize the Flymake faces @code{flymake-error} and @item To configure the amount of space taken up by documentation in the -echo area, the customize the ElDoc variable +echo area, customize the ElDoc variable @code{eldoc-echo-area-use-multiline-p}. @item @@ -1008,7 +1009,7 @@ Alternatively, the same configuration could be defined as follows: This is an equivalent setup which sets the value for all the major-modes inside the project; Eglot will use for each server only -the section of the parameters intended for that server +the section of the parameters intended for that server. As yet another alternative, you can set the value of @code{eglot-workspace-configuration} programmatically, via the commit 0c7024d0d9172322052de2ee571ba64afff905f0 Author: Stefan Kangas Date: Thu Oct 20 11:02:09 2022 +0200 * doc/misc/Makefile.in (INFO_COMMON): Add eglot. diff --git a/doc/misc/Makefile.in b/doc/misc/Makefile.in index 1d881a5fc7..b6eef7ea79 100644 --- a/doc/misc/Makefile.in +++ b/doc/misc/Makefile.in @@ -68,7 +68,7 @@ DOCMISC_W32 = @DOCMISC_W32@ ## Info files to build and install on all platforms. INFO_COMMON = auth autotype bovine calc ccmode cl \ - dbus dired-x ebrowse ede ediff edt eieio \ + dbus dired-x ebrowse ede ediff edt eglot eieio \ emacs-mime epa erc ert eshell eudc efaq eww \ flymake forms gnus emacs-gnutls htmlfontify idlwave ido info.info \ mairix-el message mh-e modus-themes newsticker nxml-mode octave-mode \ commit 25cf39162e0a78406409842b96164c813eb8c337 Author: Stefan Kangas Date: Thu Oct 20 09:32:58 2022 +0200 Prefer defvar-keymap in modula2.el * lisp/progmodes/modula2.el (m2-mode-map): Prefer defvar-keymap. diff --git a/lisp/progmodes/modula2.el b/lisp/progmodes/modula2.el index e668570ba1..09cb848fd5 100644 --- a/lisp/progmodes/modula2.el +++ b/lisp/progmodes/modula2.el @@ -65,39 +65,36 @@ "Column for aligning the end of a comment, in Modula-2." :type 'integer) -;;; Added by TEP -(defvar m2-mode-map - (let ((map (make-sparse-keymap))) - ;; FIXME: Many of those bindings are contrary to coding conventions. - (define-key map "\C-cb" #'m2-begin) - (define-key map "\C-cc" #'m2-case) - (define-key map "\C-cd" #'m2-definition) - (define-key map "\C-ce" #'m2-else) - (define-key map "\C-cf" #'m2-for) - (define-key map "\C-ch" #'m2-header) - (define-key map "\C-ci" #'m2-if) - (define-key map "\C-cm" #'m2-module) - (define-key map "\C-cl" #'m2-loop) - (define-key map "\C-co" #'m2-or) - (define-key map "\C-cp" #'m2-procedure) - (define-key map "\C-c\C-w" #'m2-with) - (define-key map "\C-cr" #'m2-record) - (define-key map "\C-cs" #'m2-stdio) - (define-key map "\C-ct" #'m2-type) - (define-key map "\C-cu" #'m2-until) - (define-key map "\C-cv" #'m2-var) - (define-key map "\C-cw" #'m2-while) - (define-key map "\C-cx" #'m2-export) - (define-key map "\C-cy" #'m2-import) - (define-key map "\C-c{" #'m2-begin-comment) - (define-key map "\C-c}" #'m2-end-comment) - (define-key map "\C-c\C-z" #'suspend-emacs) - (define-key map "\C-c\C-v" #'m2-visit) - (define-key map "\C-c\C-t" #'m2-toggle) - (define-key map "\C-c\C-l" #'m2-link) - (define-key map "\C-c\C-c" #'m2-compile) - map) - "Keymap used in Modula-2 mode.") +(defvar-keymap m2-mode-map + :doc "Keymap used in Modula-2 mode." + ;; FIXME: Many of those bindings are contrary to coding conventions. + "C-c b" #'m2-begin + "C-c c" #'m2-case + "C-c d" #'m2-definition + "C-c e" #'m2-else + "C-c f" #'m2-for + "C-c h" #'m2-header + "C-c i" #'m2-if + "C-c m" #'m2-module + "C-c l" #'m2-loop + "C-c o" #'m2-or + "C-c p" #'m2-procedure + "C-c C-w" #'m2-with + "C-c r" #'m2-record + "C-c s" #'m2-stdio + "C-c t" #'m2-type + "C-c u" #'m2-until + "C-c v" #'m2-var + "C-c w" #'m2-while + "C-c x" #'m2-export + "C-c y" #'m2-import + "C-c {" #'m2-begin-comment + "C-c }" #'m2-end-comment + "C-c C-z" #'suspend-emacs + "C-c C-v" #'m2-visit + "C-c C-t" #'m2-toggle + "C-c C-l" #'m2-link + "C-c C-c" #'m2-compile) (defcustom m2-indent 5 "This variable gives the indentation in Modula-2 mode." commit 20d44771201ef96fcfd6aebffe05aa50c5fa8074 Author: João Távora Date: Wed Oct 19 13:23:14 2022 +0100 Remove spurious trailing ':' in doc/misc/eglot.texi * doc/misc/eglot.texi (Customizing Eglot): Remove spurious ':'. diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index 286607738b..64f4c84dfd 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -885,7 +885,7 @@ supported major mode, and how to invoke that server's program. @xref{Setting Up LSP Servers}, for the details. @vindex eglot-strict-mode -@item eglot-strict-mode: +@item eglot-strict-mode This is @code{nil} by default, meaning that Eglot is generally lenient about non-conforming servers. If you need to debug a server, set this to @w{@code{(disallow-non-standard-keys enforce-required-keys)}}. commit 7a551e92005dc9964c1a74a2896b5dbf0ca231b9 Author: João Távora Date: Wed Oct 19 13:21:40 2022 +0100 Tweak some node names in doc/misc/eglot.texi "Shutting Down LSP Server" -> "Shutting Down LSP Servers" "Setting Up LSP Server" -> "Setting Up LSP Servers" * doc/misc/eglot.texi: Rework node names. diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index 3e79e89492..286607738b 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -88,11 +88,11 @@ This manual documents how to configure, use, and customize Eglot. @insertcopying @menu -* Quick Start:: For the impatient. -* Eglot and LSP Servers:: How to work with language servers -* Using Eglot:: Important Eglot commands and variables. -* Customizing Eglot:: Eglot customization and advanced features. -* Troubleshooting Eglot:: Troubleshooting and reporting bugs. +* Quick Start:: For the impatient. +* Eglot and LSP Servers:: How to work with language servers +* Using Eglot:: Important Eglot commands and variables. +* Customizing Eglot:: Eglot customization and advanced features. +* Troubleshooting Eglot:: Troubleshooting and reporting bugs. * GNU Free Documentation License:: The license for this manual. * Index:: @end menu @@ -164,13 +164,13 @@ This chapter describes how to set up Eglot for your needs, and how to start it. @menu -* Setting Up LSP Server:: How to configure LSP servers for your needs. -* Starting Eglot:: Ways of starting Eglot for your project. -* Shutting Down LSP Server:: +* Setting Up LSP Servers:: How to configure LSP servers for your needs. +* Starting Eglot:: Ways of starting Eglot for your project. +* Shutting Down LSP Servers:: @end menu -@node Setting Up LSP Server -@section Setting Up LSP Server +@node Setting Up LSP Servers +@section Setting Up LSP Servers @cindex setting up LSP server for Eglot @cindex language server for Eglot @@ -321,8 +321,8 @@ When Eglot connects to a language server for the first time in an Emacs session, it runs the hook @code{eglot-connect-hook} (@pxref{Eglot Variables}). -@node Shutting Down LSP Server -@section Shutting Down LSP Server +@node Shutting Down LSP Servers +@section Shutting Down LSP Servers @cindex shutting down LSP server When Eglot is turned on, it arranges for turning itself off @@ -803,7 +803,7 @@ restarted using @kbd{eglot-shutdown} followed by @item eglot-autoshutdown If this is non-@code{nil}, Eglot shuts down a language server when the -last buffer managed by it is killed. @xref{Shutting Down LSP Server}. +last buffer managed by it is killed. @xref{Shutting Down LSP Servers}. The default is @code{nil}; if you want to shut down a server, use @kbd{M-x eglot-shutdown} (@pxref{Eglot Commands}). @@ -882,7 +882,7 @@ connections and the server features to be used by Eglot. @item eglot-server-programs This variable determines which language server to start for each supported major mode, and how to invoke that server's program. -@xref{Setting Up LSP Server}, for the details. +@xref{Setting Up LSP Servers}, for the details. @vindex eglot-strict-mode @item eglot-strict-mode: commit c681f374788235cbaf1dca062450202e90fd2a86 Author: João Távora Date: Tue Oct 18 14:46:27 2022 +0100 More minor fixes to doc/misc/eglot.texi Most, if not all of these, were previously discussed with Eli. * doc/misc/eglot.texi (Setting Up LSP Server): Fix repetition of "This variable". (Setting Up LSP Server): Explain that single a running instance supports multiple major modes. (Starting Eglot, Eglot and Buffers): Correctly describe mode-line indication. (Eglot Features): Suggest that company-mode is just one of the possible packages. Explain that Eglot arranges for the completion package to "instantiate" snippets. Could have used "expand". Mention benefits of having the popular markdown-mode available. (Eglot Commands): Explain how eglot-reconnect and eglot-clear-status are useful. (Eglot Variables): Clarify when eglot-autoreconnect is useful. Clarify how eglot-sync-connect and eglot-connect-timeout relate to each other. Clarify semantics of eglot-confirm-server-initiated-edits. diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index df8509aa21..3e79e89492 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -191,9 +191,9 @@ provided by the @code{eglot-server-programs} variable. @defvar eglot-server-programs This variable associates major modes with names and command-line arguments of the language server programs corresponding to the -programming language of each major mode. This variable provides all -the information that Eglot needs to know about the programming -language of the source you are editing. +programming language of each major mode. It provides all the +information that Eglot needs to know about the programming language of +the source you are editing. The value of the variable is an alist, whose elements are of the form @w{@code{(@var{major-mode} . @var{server})}}. @@ -205,8 +205,9 @@ and @var{id} a string that identifies the language to the server (if Eglot cannot by itself convert the major-mode to the language identifier string required by the server). In addition, @var{major-mode} can be a list of several major mode specified in one -of the above forms -- this means the server can support more than one -major mode. +of the above forms -- this means a running instance of the associated +server is responsible for files of multiple major modes or languages +in the project. The @var{server} part of the alist elements can be one of the following: @@ -280,17 +281,14 @@ The most common way to start Eglot is to simply visit a source file of a given language and use the command @kbd{M-x eglot}. This starts the language server suitable for the visited file's major-mode, and attempts to connect to it. If the connection to the language server -is successful, you will see the @code{eglot:@var{server}} indicator on -the mode line which reflects the server that was started. If the +is successful, you will see the @code{[eglot:@var{project}]} indicator +on the mode line which reflects the server that was started. If the server program couldn't be started or connection to it failed, you will see an error message; in that case, try to troubleshoot the -problem as described in @ref{Troubleshooting Eglot}. -@c FIXME: Is the mode-line indication just eglot:server, or -@c egloit:serve/project, as described farther down? - -Once a language server was successfully started and Eglot connected to -it, you can immediately start using the Emacs features supported by -Eglot, as described in @ref{Eglot Features}. +problem as described in @ref{Troubleshooting Eglot}. Once a language +server was successfully started and Eglot connected to it, you can +immediately start using the Emacs features supported by Eglot, as +described in @ref{Eglot Features}. A single Eglot session for a certain major-mode usually serves all the buffers under that mode which visit files from the same project, so @@ -417,16 +415,22 @@ to what the @code{eglot-format} command does (see below), but is activated automatically as you type. @item -If @code{company-mode}, a popular 3rd-party completion package, is -installed, Eglot enhances it by providing completion candidates based -on the language-server analysis of the source code. +If a completion package such as @code{company-mode}, a popular +3rd-party completion package, is installed, Eglot enhances it by +providing completion candidates based on the language-server analysis +of the source code. @item If @code{yasnippet}, a popular package for automatic insertion of code -templates, is installed, and the language server supports -template-based completion, Eglot provides to @code{yasnippet} -completion templates based on the language-server analysis and -knowledge bases. +templates (snippets), is installed, and the language server supports +snippet completion candidates, Eglot arranges for the completion +package to instantiate these snippets using @code{yasnippet}. + +@item +If the popular package @code{markdown-mode} is installed, and the +server provides at-point documentation formatted as Markdown in +addition to plain text, Eglot arranges for the ElDoc package to enrich +this text with e.g. fontification before displaying it to the user. @item In addition to enabling and enhancing other features and packages, @@ -547,7 +551,7 @@ are served by a single language-server connection. (If the project uses several programming languages, there will usually be a separate server connection for each group of files written in the same language and using the same Emacs major-mode.) Eglot adds the -@samp{eglot:@var{server}/@var{project}} indication to the mode line of +@samp{[eglot:@var{project}]} indication to the mode line of each such buffer, where @var{server} is the name of the server and @var{project} identifies the project by its root directory. Clicking the mouse on the Eglot mode-line indication activates a menu with @@ -630,8 +634,10 @@ Emacs features will be configured to use Eglot, use the @code{eglot-stay-out-of} option (@pxref{Customizing Eglot}). @item M-x eglot-reconnect -@c FIXME: When and why would this command be useful? Move to less common? -Reconnect to current language server. +Shuts down an the current connection to the language server and +immediately restarts it using the same options used originally. This +can sometimes be useful to unclog a partially malfunctioning server +connection. @item M-x eglot-shutdown Shut down a language server. This commands prompts for a language @@ -706,7 +712,13 @@ the current value of the variable @code{eglot-workspace-configuration} @item M-x eglot-clear-status Clear the last JSONRPC error for the server of the current buffer. -@c FIXME: Better description of the need and the effect. +Eglot keeps track of erroneous situations encountered by the server in +its mode-line indication so that the user may inspect the +communication leading up to it (@pxref{Troubleshooting Eglot}). If +the situation is deemed uninteresting or temporary, this command can +be used to ``forget'' the error. Note that the command @code{M-x +eglot-reconnect} can sometimes be used to unclog a temporarily +malfunctioning server. @end ftable As described in @ref{Eglot Features} most features associated with @@ -757,13 +769,12 @@ This section provides a reference of the Eglot' user options. @vtable @code @item eglot-autoreconnect This option controls the ability to reconnect automatically to the -language server. The default value 3 means to attempt reconnection -only if the previous successful connection lasted for more than that -number of seconds; a different positive value changes the minimal -length of the connection to trigger reconnection. A value of @code{t} -means always reconnect automatically, and @code{nil} means never -reconnect. The alternative to automatic reconnection is the command -@code{eglot-reconnect} (@pxref{Eglot Commands}). +language server when Eglot detects that the server process terminated +unexpectedly. The default value 3 means to attempt reconnection only +if the previous successful connection lasted for more than that number +of seconds; a different positive value changes the minimal length of +the connection to trigger reconnection. A value of @code{t} means +always reconnect automatically, and @code{nil} means never reconnect. @item eglot-connect-timeout This specifies the number of seconds before connection attempt to a @@ -771,14 +782,14 @@ language server times out. The value of @code{nil} means never time out. The default is 30 seconds. @item eglot-sync-connect -This controls whether attempts to connect to language servers should -be blocking. The setting is mainly important for slow connections. -The default value is 3; a positive value means block for that many -seconds, then wait for the connection in the background. The value -of @code{t} means block for @code{eglot-connect-timeout} seconds. The -value of @code{nil} or zero means don't block at all. -@c FIXME: the code doesn't use eglot-connect-timeout, it uses a -@c hard-coded value of 30. +This setting is mainly important for connections which are slow to +establish. Whereas the variable @code{eglot-connect-timeout} controls +how long to wait for, this variable controls whether to block Emacs's +user interface while waiting. The default value is 3; a positive +value means block for that many seconds, then wait for the connection +in the background. The value of @code{t} means block during the whole +waiting period. The value of @code{nil} or zero means don't block at +all during the waiting period. @item eglot-events-buffer-size This determines the size of the Eglot events buffer. @xref{Eglot @@ -800,11 +811,8 @@ The default is @code{nil}; if you want to shut down a server, use Various Eglot commands and code actions result in the language server sending editing commands to Emacs. If this option's value is non-@code{nil} (the default), Eglot will ask for confirmation before -performing the edits requested by the server. -@c FIXME: Not clear: is the confirmation required for each individual -@c edit, or for as group? for each buffer or just once? And what -@c about on-type reformatting -- does that require confirmation as -@c well (which would be annoying)? +performing edits initiated by the server or edits whose scope affects +buffers other than the one where the user initiated the request. @item eglot-ignored-server-capabilities This variable's value is a list of language server capabilities that commit b92a5174939fba17ffb5235dd926c7063c13b1d1 Author: João Távora Date: Tue Oct 18 12:46:48 2022 +0100 Minor stylistic fixes to introduction of doc/misc/eglot.texi * doc/misc/eglot.texi (title): Add "the". (Top): Fix sentence structure in top-level introduction. diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index 761964334b..df8509aa21 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -1,7 +1,7 @@ \input texinfo @c -*-texinfo-*- @c %**start of header @setfilename ../../eglot.info -@settitle Eglot: The Emacs Client for Language Server Protocol +@settitle Eglot: The Emacs Client for the Language Server Protocol @include docstyle.texi @syncodeindex vr cp @syncodeindex fn cp @@ -60,24 +60,24 @@ modify this GNU manual.'' @cindex LSP @cindex language server protocol -Eglot is the Emacs client for Language Server Protocol. The name -``Eglot'' is an acronym that stands for ``@emph{E}macs -Poly@emph{glot}''.@footnote{ -A @dfn{polyglot} is a person who is able to use several languages. +Eglot is the Emacs client for the @dfn{Language Server Protocol} +(@acronym{LSP}). The name ``Eglot'' is an acronym that stands for +``@emph{E}macs Poly@emph{glot}''.@footnote{ +A @dfn{polyglot} is a +person who is able to use several languages. } Eglot provides infrastructure and a set of commands for enriching -Emacs source code editing capabilities using the @dfn{Language Server -Protocol} (@acronym{LSP}) -- a standardized communications protocol -between source code editors (such as Emacs) and language servers -- -programs external to Emacs that analyze the source code on behalf of -Emacs. The protocol allows Emacs to receive various source code -services from the server, such as description and location of -functions calls, types of variables, class definitions, syntactic -errors, etc. This way, Emacs doesn't need to implement the -language-specific parsing and analysis capabilities in its own code, -but is still capable of providing sophisticated editing features that -rely on such capabilities, such as automatic code completion, go-to -definition of function/class, documentation of symbol at-point, -refactoring, on-the-fly diagnostics, and more. +the source code editing capabilities of Emacs via LSP. LSP is a +standardized communications protocol between source code editors (such +as Emacs) and language servers, programs external to Emacs for +analyzing source code on behalf of Emacs. The protocol allows Emacs +to receive various source code services from the server, such as +description and location of functions calls, types of variables, class +definitions, syntactic errors, etc. This way, Emacs doesn't need to +implement the language-specific parsing and analysis capabilities in +its own code, but is still capable of providing sophisticated editing +features that rely on such capabilities, such as automatic code +completion, go-to definition of function/class, documentation of +symbol at-point, refactoring, on-the-fly diagnostics, and more. Eglot itself is completely language-agnostic, but it can support any programming language for which there is a language server and an Emacs commit 3e5856b21a83a52dda8c0b3ab541d106d809d625 Author: Eli Zaretskii Date: Tue Oct 18 12:17:51 2022 +0100 Add new Texinfo manual for the Eglot LSP client * doc/misc/eglot.texi: New file. diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi new file mode 100644 index 0000000000..761964334b --- /dev/null +++ b/doc/misc/eglot.texi @@ -0,0 +1,1116 @@ +\input texinfo @c -*-texinfo-*- +@c %**start of header +@setfilename ../../eglot.info +@settitle Eglot: The Emacs Client for Language Server Protocol +@include docstyle.texi +@syncodeindex vr cp +@syncodeindex fn cp +@c %**end of header + +@copying +This manual is for Eglot, the Emacs LSP client. + +Copyright @copyright{} 2022 Free Software Foundation, Inc. + +@quotation +Permission is granted to copy, distribute and/or modify this document +under the terms of the GNU Free Documentation License, Version 1.3 or +any later version published by the Free Software Foundation; with no +Invariant Sections, with the Front-Cover Texts being ``A GNU Manual'', +and with the Back-Cover Texts as in (a) below. A copy of the license +is included in the section entitled ``GNU Free Documentation License''. + +(a) The FSF's Back-Cover Text is: ``You have the freedom to copy and +modify this GNU manual.'' +@end quotation +@end copying + +@dircategory Emacs misc features +@direntry +* Eglot: (eglot). Language Server Protocol client for Emacs. +@end direntry + +@titlepage +@sp 4 +@c The title is printed in a large font. +@center @titlefont{User's Guide} +@sp 1 +@center @titlefont{to} +@sp 1 +@center @titlefont{Eglot: The Emacs LSP Client} +@ignore +@sp 2 +@center release 1.8 +@c -release- +@end ignore +@sp 3 +@center Jo@~ao T@'avora & Eli Zaretskii +@c -date- + +@page +@vskip 0pt plus 1filll +@insertcopying +@end titlepage + +@contents + +@ifnottex +@node Top +@top Eglot + +@cindex LSP +@cindex language server protocol +Eglot is the Emacs client for Language Server Protocol. The name +``Eglot'' is an acronym that stands for ``@emph{E}macs +Poly@emph{glot}''.@footnote{ +A @dfn{polyglot} is a person who is able to use several languages. +} Eglot provides infrastructure and a set of commands for enriching +Emacs source code editing capabilities using the @dfn{Language Server +Protocol} (@acronym{LSP}) -- a standardized communications protocol +between source code editors (such as Emacs) and language servers -- +programs external to Emacs that analyze the source code on behalf of +Emacs. The protocol allows Emacs to receive various source code +services from the server, such as description and location of +functions calls, types of variables, class definitions, syntactic +errors, etc. This way, Emacs doesn't need to implement the +language-specific parsing and analysis capabilities in its own code, +but is still capable of providing sophisticated editing features that +rely on such capabilities, such as automatic code completion, go-to +definition of function/class, documentation of symbol at-point, +refactoring, on-the-fly diagnostics, and more. + +Eglot itself is completely language-agnostic, but it can support any +programming language for which there is a language server and an Emacs +major mode. + +This manual documents how to configure, use, and customize Eglot. + +@insertcopying + +@menu +* Quick Start:: For the impatient. +* Eglot and LSP Servers:: How to work with language servers +* Using Eglot:: Important Eglot commands and variables. +* Customizing Eglot:: Eglot customization and advanced features. +* Troubleshooting Eglot:: Troubleshooting and reporting bugs. +* GNU Free Documentation License:: The license for this manual. +* Index:: +@end menu +@end ifnottex + +@node Quick Start +@chapter Quick Start +@cindex quick start + +This chapter provides concise instructions for setting up and using +Eglot with your programming project in common usage scenarios. For +more detailed instructions regarding Eglot setup, @pxref{Eglot and LSP +Servers}. @xref{Using Eglot}, for detailed description of using Eglot, +and see @ref{Customizing Eglot}, for adapting Eglot to less common use +patterns. + +Here's how to start using Eglot with your programming project: + +@enumerate +@item +Select and install a language server. + +Eglot comes pre-configured with many popular language servers, see the +value of @code{eglot-server-programs}. If the server(s) mentioned +there satisfy your needs for the programming language(s) with which +you want to use Eglot, you just need to make sure those servers are +installed on your system. Alternatively, install one or more servers +of your choice and add them to the value of +@code{eglot-server-programs}, as described in @ref{Setting Up LSP +Server}. + +@item +Turn on Eglot for your project. + +To start using Eglot for a project, type @kbd{M-x eglot @key{RET}} in +a buffer visiting any file that belongs to the project. This starts +the language server configured for the programming language of that +buffer, and causes Eglot to start managing all the files of the +project which use the same programming language. The notion of a +``project'' used by Eglot is the same Emacs uses (@pxref{Projects,,, +emacs, GNU Emacs Manual}): in the simplest case, the ``project'' is +the single file you are editing, but it can also be all the files in a +single directory or a directory tree under some version control +system, such as Git. + +Alternatively, you can start Eglot automatically from the major-mode +hook of the mode used for the programming language; see @ref{Starting +Eglot}. + +@item +Use Eglot. + +Most Eglot facilities are integrated into Emacs features, such as +ElDoc, Flymake, Xref, and Imenu. However, Eglot also provides +commands of its own, mainly to perform tasks by the LSP server, such +as @kbd{M-x eglot-rename} (to rename an identifier across the entire +project), @kbd{M-x eglot-format} (to reformat and reindent code), and +some others. @xref{Eglot Commands}, for the detailed list of Eglot +commands. + +@item +That's it! +@end enumerate + +@node Eglot and LSP Servers +@chapter Eglot and LSP Servers + +This chapter describes how to set up Eglot for your needs, and how to +start it. + +@menu +* Setting Up LSP Server:: How to configure LSP servers for your needs. +* Starting Eglot:: Ways of starting Eglot for your project. +* Shutting Down LSP Server:: +@end menu + +@node Setting Up LSP Server +@section Setting Up LSP Server +@cindex setting up LSP server for Eglot +@cindex language server for Eglot + +For Eglot to be useful, it must first be combined with a suitable +language server. Usually, that means running the server program +locally as a child process of Emacs (@pxref{Processes,,, elisp, GNU +Emacs Lisp Reference Manual}) and communicating with it via the +standard input and output streams. + +The language server program must be installed separately, and is not +further discussed in this manual; refer to the documentation of the +particular server(s) you want to install. + +To use a language server, Eglot must know how to start it and which +programming languages each server supports. This information is +provided by the @code{eglot-server-programs} variable. + +@defvar eglot-server-programs +This variable associates major modes with names and command-line +arguments of the language server programs corresponding to the +programming language of each major mode. This variable provides all +the information that Eglot needs to know about the programming +language of the source you are editing. + +The value of the variable is an alist, whose elements are of the form +@w{@code{(@var{major-mode} . @var{server})}}. + +The @var{major-mode} of the alist elements can be either a symbol of +an Emacs major mode or a list of the form @w{@code{(@var{mode} +:language-id @var{id})}}, with @var{mode} being a major-mode symbol +and @var{id} a string that identifies the language to the server (if +Eglot cannot by itself convert the major-mode to the language +identifier string required by the server). In addition, +@var{major-mode} can be a list of several major mode specified in one +of the above forms -- this means the server can support more than one +major mode. + +The @var{server} part of the alist elements can be one of the +following: + +@table @code +@item (@var{program} @var{args}@dots{}) +This says to invoke @var{program} with zero or more arguments +@var{args}; the program is expected to communicate with Emacs via the +standard input and standard output streams. + +@item (@var{program} @var{args}@dots{} :initializationOptions @var{options}@dots{}) +Like above, but with @var{options} specifying the options to be +used for constructing the @samp{initializationOptions} JSON object for +the server. @var{options} can also be a function of one argument, in +which case it will be called with the server instance as the argument, +and should return the JSON object to use for initialization. + +@item (@var{host} @var{port} @var{args}@dots{}) +Here @var{host} is a string and @var{port} is a positive integer +specifying a TCP connection to a remote server. The @var{args} are +passed to @code{open-network-stream}, e.g.@: if the connection needs +to use encryption or other non-default parameters (@pxref{Network,,, +elisp, GNU Emacs Lisp Reference Manual}). + +@item (@var{program} @var{args}@dots{} :autoport @var{moreargs}@dots{}) +@var{program} is started with a command line constructed from +@var{args} followed by an available server port and the rest of +arguments in @var{moreargs}; Eglot then establishes a TCP connection +with the server via that port on the local host. + +@item @var{function} +This should be a function of a single argument: non-@code{nil} if the +connection was requested interactively (e.g., by the @code{eglot} +command), otherwise @code{nil}. The function should return a value of +any of the forms described above. This allows interaction with the +user for determining the program to start and its command-line +arguments. +@end table + +@end defvar + +Eglot comes with a fairly complete set of associations of major-modes +to popular language servers predefined in this variable. If you need +to add server associations to the default list, use +@code{add-to-list}. For example, if there is a hypothetical language +server program @command{fools} for the language @code{Foo} which is +supported by an Emacs major-mode @code{foo-mode}, you can add it to +the alist like this: + +@lisp +(add-to-list 'eglot-server-programs + '(foo-mode . ("fools" "--stdio"))) +@end lisp + +This will invoke the program @command{fools} with the command-line +argument @option{--stdio} in support of editing source files for which +Emacs turns on @code{foo-mode}, and will communicate with the program +via the standard streams. As usual with invoking programs, the +executable file @file{fools} should be in one of the directories +mentioned by the @code{exec-path} variable (@pxref{Subprocess +Creation,,, elisp, GNU Emacs Lisp Reference Manual}), for Eglot to be +able to find it. + +@node Starting Eglot +@section Starting Eglot +@cindex starting Eglot +@cindex activating Eglot for a project + +@findex eglot +The most common way to start Eglot is to simply visit a source file of +a given language and use the command @kbd{M-x eglot}. This starts the +language server suitable for the visited file's major-mode, and +attempts to connect to it. If the connection to the language server +is successful, you will see the @code{eglot:@var{server}} indicator on +the mode line which reflects the server that was started. If the +server program couldn't be started or connection to it failed, you +will see an error message; in that case, try to troubleshoot the +problem as described in @ref{Troubleshooting Eglot}. +@c FIXME: Is the mode-line indication just eglot:server, or +@c egloit:serve/project, as described farther down? + +Once a language server was successfully started and Eglot connected to +it, you can immediately start using the Emacs features supported by +Eglot, as described in @ref{Eglot Features}. + +A single Eglot session for a certain major-mode usually serves all the +buffers under that mode which visit files from the same project, so +you don't need to invoke @kbd{M-x eglot} again when you visit another +file from the same project which is edited using the same major-mode. +This is because Eglot uses the Emacs project infrastructure, as +described in @ref{Eglot and Buffers}, and this knows about files that +belong to the same project. Thus, after starting an Eglot session for +some buffer, that session is automatically reused when visiting files +in the same project with the same major-mode. + +@findex eglot-ensure +Alternatively, you could configure Eglot to start automatically for +one or more major-modes from the respective mode hooks. Here's an +example for a hypothetical @code{foo-mode}: + +@lisp + (add-hook 'foo-mode-hook 'eglot-ensure) +@end lisp + +@noindent +The function @code{eglot-ensure} will start an Eglot session for each +buffer in which @code{foo-mode} is turned on, if there isn't already +an Eglot session that handles the buffer. Note that this variant of +starting an Eglot session is non-interactive, so it should be used +only when you are confident that Eglot can be started reliably for any +file which may be visited with the major-mode in question. + +When Eglot connects to a language server for the first time in an +Emacs session, it runs the hook @code{eglot-connect-hook} +(@pxref{Eglot Variables}). + +@node Shutting Down LSP Server +@section Shutting Down LSP Server +@cindex shutting down LSP server + +When Eglot is turned on, it arranges for turning itself off +automatically if the language server process terminates. Turning off +Eglot means it shuts down the server connection, ceases its management +of all the buffers that use the server connection which was +terminated, deactivates its minor mode, and restores the original +values of the Emacs variables that Eglot changed when it was turned +on. @xref{Eglot and Buffers}, for more details of what does Eglot +management of a buffer entail. + +@findex eglot-shutdown +You can also shut down a language server manually, by using the +command @kbd{M-x eglot-shutdown}. This prompts for the server (unless +there's only one connection and it's used in the current buffer), and +then shuts it down. By default, it also kills the server's events +buffer (@pxref{Troubleshooting Eglot}), but a prefix argument prevents +that. + +Alternatively, you can customize the variable +@code{eglot-autoshutdown} to a non-@code{nil} value, in which case +Eglot will automatically shut down the language server process when +the last buffer served by that language server is killed. The default +of this variable is @code{nil}, so that visiting another file would +automatically activate Eglot even when the project which started Eglot +with the server no longer has any buffer associated with it. This +default allows you to start a server only once in each Emacs session. + +@node Using Eglot +@chapter Using Eglot + +This chapter describes in detail the features that Eglot provides and +how it does that. It also provides reference sections for Eglot +commands and variables. + +@menu +* Eglot Features:: +* Eglot and Buffers:: +* Eglot Commands:: +* Eglot Variables:: +@end menu + +@node Eglot Features +@section Eglot Features +@cindex features in buffers supported by Eglot + +Once Eglot is enabled in a buffer, it uses LSP and the language-server +capabilities to activate, enable, and enhance modern IDE features in +Emacs. The features themselves are usually provided via other Emacs +packages. Here are the main features Eglot enables and provides: + +@itemize @bullet +@item +At-point documentation: when point is at or near a symbol or an +identifier, the information about the symbol/identifier, such as the +signature of a function or class method and server-generated +diagnostics, is made available via the ElDoc package (@pxref{Lisp +Doc,,, emacs, GNU Emacs Manual}). This allows major modes to provide +extensive help and documentation about the program identifiers. + +@item +On-the-fly diagnostic annotations with server-suggested fixes, via the +Flymake package (@pxref{Top,,, flymake, GNU Flymake manual}). This +improves and enhances the Flymake diagnostics, replacing the other +Flymake backends. + +@item +Finding definitions and uses of identifiers, via Xref (@pxref{Xref,,, +emacs, GNU Emacs Manual}). Eglot provides a backend for the Xref +capabilities which uses the language-server understanding of the +program source. In particular, it eliminates the need to generate +tags tables (@pxref{Tags tables,,, emacs, GNU Emacs Manual}) for +languages which are only supported by the @code{etags} backend. + +@item +Buffer navigation by name of function, class, method, etc., via Imenu +(@pxref{Imenu,,, emacs, GNU Emacs Manual}). Eglot provides its own +variant of @code{imenu-create-index-function}, which generates the +index for the buffer based on language-server program source analysis. + +@item +Enhanced completion of symbol at point by the +@code{completion-at-point} command (@pxref{Symbol Completion,,, emacs, +GNU Emacs Manual}). This uses the language-server's parser data for +the completion candidates. + +@item +Automatic reformatting of source code as you type it. This is similar +to what the @code{eglot-format} command does (see below), but is +activated automatically as you type. + +@item +If @code{company-mode}, a popular 3rd-party completion package, is +installed, Eglot enhances it by providing completion candidates based +on the language-server analysis of the source code. + +@item +If @code{yasnippet}, a popular package for automatic insertion of code +templates, is installed, and the language server supports +template-based completion, Eglot provides to @code{yasnippet} +completion templates based on the language-server analysis and +knowledge bases. + +@item +In addition to enabling and enhancing other features and packages, +Eglot also provides a small number of user commands based directly on +the capabilities of language servers. These commands are: + +@table @kbd +@item M-x eglot-rename +This prompts for a new name for the symbol at point, and then modifies +all the project source files to rename the symbol to the new name, +based on editing data received from the language-server. @xref{Eglot +and Buffers}, for the details of how project files are defined. + +@item M-x eglot-format +This reformats and prettifies the current active region according to +source formatting rules of the language-server. If the region is not +active, it reformats the entire buffer instead. + +@item M-x eglot-format-buffer +This reformats and prettifies the current buffer according to source +formatting rules of the language-server. + +@cindex code actions +@item M-x eglot-code-actions +@itemx M-x eglot-code-action-organize-imports +@itemx M-x eglot-code-action-quickfix +@itemx M-x eglot-code-action-extract +@itemx M-x eglot-code-action-inline +@itemx M-x eglot-code-action-rewrite +These command allow you to invoke the so-called @dfn{code actions}: +requests for the language-server to provide editing commands for +various code fixes, typically either to fix an error diagnostic or to +beautify/refactor code. For example, +@code{eglot-code-action-organize-imports} rearranges the program +@dfn{imports}---declarations of modules whose capabilities the program +uses. These commands affect all the files that belong to the +project. The command @kbd{M-x eglot-code-actions} will pop up a menu +of code applicable actions at point. +@end table + +@end itemize + +Not all servers support the full set of LSP capabilities, but most of +them support enough to enable the basic set of features mentioned +above. Conversely, some servers offer capabilities for which no +equivalent Emacs package exists yet, and so Eglot cannot (yet) expose +these capabilities to Emacs users. + +@node Eglot and Buffers +@section Buffers, Projects, and Eglot +@cindex buffers managed by Eglot +@cindex projects and Eglot + +@cindex workspace +One of the main strong points of using a language server is that a +language server has a broad view of the program: it considers more +than just the single source file you are editing. Ideally, the +language server should know about all the source files of your program +which are written in the language supported by the server. In the +language-server parlance, the set of the source files of a program is +known as a @dfn{workspace}. The Emacs equivalent of a workspace is a +@dfn{project} (@pxref{Projects,,, emacs, GNU Emacs Manual}). Eglot +fully supports Emacs projects, and considers the file in whose buffer +Eglot is turned on as belonging to a project. In the simplest case, +that file is the entire project, i.e.@: your project consists of a +single file. But there are other more complex projects: + +@itemize @bullet +@item +A single-directory project: several source files in a single common +directory. + +@item +A VC project: source files in a directory hierarchy under some VCS, +i.e.@: a VCS repository (@pxref{Version Control,,, emacs, GNU Emacs +Manual}). + +@item +An EDE project: source files in a directory hierarchy managed via the +Emacs Development Environment (@pxref{EDE,,, emacs, GNU Emacs +Manual}). +@end itemize + +Eglot uses the Emacs's project management infrastructure to figure out +which files and buffers belong to what project, so any kind of project +supported by that infrastructure is automatically supported by Eglot. + +When Eglot starts a server program, it does so in the project's root +directory, which is usually the top-level directory of the project's +directory hierarchy. This ensures the language server has the same +comprehensive view of the project's files as you do. + +For example, if you visit the file @file{~/projects/fooey/lib/x.foo} +and @file{x.foo} belongs to a project rooted at +@file{~/projects/fooey} (perhaps because a @file{.git} directory +exists there), then @kbd{M-x eglot} causes the server program to start +with that root as the current working directory. The server then will +analyze not only the file @file{lib/x.foo} you visited, but likely +also all the other @file{*.foo} files under the +@file{~/projects/fooey} directory. + +In some cases, additional information specific to a given project will +need to be provided to the language server when starting it. The +variable @code{eglot-workspace-configuration} (@pxref{Customizing +Eglot}) exists for that purpose. It specifies the parameters and +their values to communicate to each language server which needs that. + +When Eglot is active for a project, it performs several background +activities on behalf of the project and its buffers: + +@itemize @bullet +@cindex mode-line indication of language server +@cindex mouse clicks on mode-line, and Eglot +@vindex eglot-menu +@item +All of the project's file-visiting buffers under the same major-mode +are served by a single language-server connection. (If the project +uses several programming languages, there will usually be a separate +server connection for each group of files written in the same language +and using the same Emacs major-mode.) Eglot adds the +@samp{eglot:@var{server}/@var{project}} indication to the mode line of +each such buffer, where @var{server} is the name of the server and +@var{project} identifies the project by its root directory. Clicking +the mouse on the Eglot mode-line indication activates a menu with +server-specific items. + +@item +For each buffer in which Eglot is active, it notifies the language +server that Eglot is @dfn{managing} the file visited by that buffer. +This tells the language server that the file's contents on disk may no +longer be up-to-date due to unsaved edits. Eglot reports to the +server any changes in the text of each managed buffer, to make the +server aware of unsaved changes. This includes your editing of the +buffer and also changes done automatically by other Emacs features and +commands. Killing a buffer relinquishes its management by Eglot and +notifies the server that the file on disk is up-to-date. + +@vindex eglot-managed-mode-hook +@vindex eglot-managed-p +@item +Eglot turns on a special minor mode in each buffer it manages. This +minor mode ensures the server is notified about files Eglot manages, +and also arranges for other Emacs features supported by Eglot +(@pxref{Eglot Features}) to receive information from the language +server, by changing the settings of these features. Unlike other +minor-modes, this special minor mode is not activated manually by the +user, but automatically as result of starting an Eglot session for the +buffer. However, this minor mode provides a hook variable +@code{eglot-managed-mode-hook} that can be used to customize the Eglot +management of the buffer. This hook is run both when the minor mode +is turned on and when it's turned off; use the variable +@code{eglot-managed-p} to tell if current buffer is still being +managed or not. When Eglot stops managing the buffer, this minor mode +is turned off, and all the settings that Eglot changed are restored to +their original values. + +@item +When you visit a file under the same project, whether an existing or a +new file, its buffer is automatically added to the set of buffers +managed by Eglot, and the server which supports the buffer's +major-mode is notified about that. Thus, visiting a non-existent file +@file{/home/joe/projects/fooey/lib/y.foo} in the above example will +notify the server of the @file{*.foo} files' language that a new file +was added to the project, even before the file appears on disk. The +special Eglot minor mode is also turned on automatically in the buffer +visiting the file. +@end itemize + +@node Eglot Commands +@section Eglot Commands +@cindex commands, Eglot + +This section provides a reference of the most commonly used Eglot +commands: + +@ftable @code +@item M-x eglot +This command adds the current buffer and the file it visits to the +group of buffers and files managed by Eglot on behalf of a suitable +language server. If a language server for the buffer's +@code{major-mode} (@pxref{Major Modes,,, emacs, GNU Emacs Manual}) is +not yet running, it will be started; otherwise the buffer and its file +will be added to those managed by an existing server session. + +The command attempts to figure out the buffer's major mode and the +suitable language server; in case it fails, it might prompt for the +major mode to use and for the server program to start. If invoked +with @kbd{C-u}, it always prompts for the server program, and if +invoked with @kbd{C-u C-u}, also prompt for the major mode. + +If the language server is successfully started and contacted, this +command arranges for any other buffers belonging to the same project +and using the same major mode to use the same language-server session. +That includes any buffers created by visiting files after this command +succeeds to connect to a language server. + +All the Emacs features that are capable of using Eglot services +(@pxref{Eglot Features}) are automatically configured by this command +to start using the language server via Eglot. To customize which +Emacs features will be configured to use Eglot, use the +@code{eglot-stay-out-of} option (@pxref{Customizing Eglot}). + +@item M-x eglot-reconnect +@c FIXME: When and why would this command be useful? Move to less common? +Reconnect to current language server. + +@item M-x eglot-shutdown +Shut down a language server. This commands prompts for a language +server to shut down (unless there's only one server session, and it +manages the current buffer). Then the command shuts down the server +and stops managing the buffers the server was used for. Emacs +features (@pxref{Eglot Features}) that Eglot configured to work with +the language server are restored back to their original configuration. + +Normally, this command kills the buffers used for communicating with +the language server, but if invoked with a prefix argument @kbd{C-u}, +the command doesn't kill those buffers, allowing them to be used for +diagnostics and problem reporting (@pxref{Troubleshooting Eglot}). + +@item M-x eglot-shutdown-all +This command shuts down all the language servers active in the current +Emacs session. As with @code{eglot-shutdown}, invoking this command +with a prefix argument avoids killing the buffers used for +communications with the language servers. + +@item M-x eglot-rename +This command renames the program symbol (a.k.a.@: @dfn{identifier}) at +point to another name. It prompts for the new name of the symbol, and +then modifies all the files in the project which arte managed by the +language server of the current buffer to implement the renaming. + +@item M-x eglot-format +This command reformats the active region according to the +language-server rules. If no region is active, it reformats the +entire current buffer. + +@item M-x eglot-format-buffer +This command reformats the current buffer, in the same manner as +@code{eglot-format} does. + +@item M-x eglot-code-actions +@itemx mouse-1 +This command asks the server for any @dfn{code actions} applicable at +point. It can also be invoked by @kbd{mouse-1} clicking on +diagnostics provided by the server. + +@item M-x eglot-code-action-organize-imports +@itemx M-x eglot-code-action-quickfix +@itemx M-x eglot-code-action-extract +@itemx M-x eglot-code-action-inline +@itemx M-x eglot-code-action-rewrite +These commands invoke specific code actions supported by the language +server. +@c FIXME: Need more detailed description of each action. +@end ftable + +The following Eglot commands are used less commonly, mostly for +diagnostic and troubleshooting purposes: + +@ftable @code +@item M-x eglot-events-buffer +This command pops up the events buffer used for communication with the +language server of the current buffer. + +@item M-x eglot-stderr-buffer +This command pops up the buffer with the debug info printed by the +language server to its standard error stream. + +@item M-x eglot-forget-pending-continuations +Forget pending requests for the server of the current buffer. +@c FIXME: Better description of the need. + +@item M-x eglot-signal-didChangeConfiguration +This command updates the language server configuration according to +the current value of the variable @code{eglot-workspace-configuration} +(@pxref{Customizing Eglot}). + +@item M-x eglot-clear-status +Clear the last JSONRPC error for the server of the current buffer. +@c FIXME: Better description of the need and the effect. +@end ftable + +As described in @ref{Eglot Features} most features associated with +Eglot are actually provided by other Emacs packages and features, and +Eglot only enhances them by allowing them to use the information +coming from the language servers. For completeness, here's the list +of commands of those other packages that are very commonly used in +Eglot-managed buffers: + +@c Not @ftable, because the index entries should mention Eglot +@table @code +@cindex eldoc, and Eglot +@cindex documentation using Eglot +@item M-x eldoc +Ask the ElDoc system for help at point. + +@cindex flymake, and Eglot +@cindex on-the-fly diagnostics using Eglot +@item M-x flymake-show-buffer-diagnostics +Ask Flymake system to display diagnostics for the current buffer. + +@item M-x flymake-show-project-diagnostics +Ask Flymake to list diagnostics for all the files in the current +project. + +@cindex xref, and Eglot +@cindex finding definitions of identifiers using Eglot +@item M-x xref-find-definitions +Ask Xref to go the definition of the identifier at point. + +@cindex imenu navigation using Eglot +@item M-x imenu +Let the user navigate the program source code using buffer index, +categorizing program elements by syntactic class (class, method, +variable, etc.) and offering completion. + +@cindex symbol completion using Eglot +@item M-x completion-at-point +Request completion of the symbol at point. +@end table + +@node Eglot Variables +@section Eglot Variables +@cindex variables, Eglot + +This section provides a reference of the Eglot' user options. + +@vtable @code +@item eglot-autoreconnect +This option controls the ability to reconnect automatically to the +language server. The default value 3 means to attempt reconnection +only if the previous successful connection lasted for more than that +number of seconds; a different positive value changes the minimal +length of the connection to trigger reconnection. A value of @code{t} +means always reconnect automatically, and @code{nil} means never +reconnect. The alternative to automatic reconnection is the command +@code{eglot-reconnect} (@pxref{Eglot Commands}). + +@item eglot-connect-timeout +This specifies the number of seconds before connection attempt to a +language server times out. The value of @code{nil} means never time +out. The default is 30 seconds. + +@item eglot-sync-connect +This controls whether attempts to connect to language servers should +be blocking. The setting is mainly important for slow connections. +The default value is 3; a positive value means block for that many +seconds, then wait for the connection in the background. The value +of @code{t} means block for @code{eglot-connect-timeout} seconds. The +value of @code{nil} or zero means don't block at all. +@c FIXME: the code doesn't use eglot-connect-timeout, it uses a +@c hard-coded value of 30. + +@item eglot-events-buffer-size +This determines the size of the Eglot events buffer. @xref{Eglot +Commands, eglot-events-buffer}, for how to display that buffer. If +the value is changed, for it to take effect the connection should be +restarted using @kbd{eglot-shutdown} followed by +@kbd{eglot-reconnect}. +@c FIXME: Shouldn't the defcustom do this by itself using the :set +@c attribute? +@xref{Troubleshooting Eglot}, for when this could be useful. + +@item eglot-autoshutdown +If this is non-@code{nil}, Eglot shuts down a language server when the +last buffer managed by it is killed. @xref{Shutting Down LSP Server}. +The default is @code{nil}; if you want to shut down a server, use +@kbd{M-x eglot-shutdown} (@pxref{Eglot Commands}). + +@item eglot-confirm-server-initiated-edits +Various Eglot commands and code actions result in the language server +sending editing commands to Emacs. If this option's value is +non-@code{nil} (the default), Eglot will ask for confirmation before +performing the edits requested by the server. +@c FIXME: Not clear: is the confirmation required for each individual +@c edit, or for as group? for each buffer or just once? And what +@c about on-type reformatting -- does that require confirmation as +@c well (which would be annoying)? + +@item eglot-ignored-server-capabilities +This variable's value is a list of language server capabilities that +Eglot should not use. The default is @code{nil}: Eglot uses all of +the capabilities supported by each server. + +@item eglot-extend-to-xref +If this is non-@code{nil}, and @kbd{M-.} +(@code{xref-find-definitions}) lands you in a file outside of your +project, such as a system-installed library or header file, +transiently consider that file as managed by the same language server. +That file is still outside your project (i.e. @code{project-find-file} +won't find it), but Eglot and the server will consider it to be part +of the workspace. The default is @code{nil}. + +@item eglot-mode-map +This variable is the keymap for binding Eglot-related command. It is +in effect only as long as the buffer is managed by Eglot. By default, +it is empty, with the single exception: @kbd{C-h .} is remapped to +invoke @code{eldoc-doc-buffer}. You can bind additional commands in +this map. For example: + +@lisp + (define-key eglot-mode-map (kbd "C-c r") 'eglot-rename) + (define-key eglot-mode-map (kbd "C-c o") 'eglot-code-action-organize-imports) + (define-key eglot-mode-map (kbd "C-c h") 'eldoc) + (define-key eglot-mode-map (kbd "") 'xref-find-definitions) +@end lisp + +@end vtable + +Additional variables, which are relevant for customizing the server +connections, are documented in @ref{Customizing Eglot}. + +@node Customizing Eglot +@chapter Customizing Eglot +@cindex customizing Eglot + +A large part of customizing Eglot to your needs and preferences should +actually be done via options of the Emacs packages and features which +Eglot supports and enhances (@pxref{Eglot Features}). For example: + +@itemize @bullet +@item +To configure the face used for server-derived errors and warnings, +customize the Flymake faces @code{flymake-error} and +@code{flymake-error}. + +@item +To configure the amount of space taken up by documentation in the +echo area, the customize the ElDoc variable +@code{eldoc-echo-area-use-multiline-p}. + +@item +To completely change how ElDoc displays the at-point documentation +destination, customize the ElDoc variable +@code{eldoc-display-functions}. +@end itemize + +For this reason, this manual describes only how to customize the +Eglot's own operation, which mainly has to do with the server +connections and the server features to be used by Eglot. + +@c @table, not @vtable, because some of the variables are indexed +@c elsewhere +@table @code +@item eglot-server-programs +This variable determines which language server to start for each +supported major mode, and how to invoke that server's program. +@xref{Setting Up LSP Server}, for the details. + +@vindex eglot-strict-mode +@item eglot-strict-mode: +This is @code{nil} by default, meaning that Eglot is generally lenient +about non-conforming servers. If you need to debug a server, set this +to @w{@code{(disallow-non-standard-keys enforce-required-keys)}}. + +@vindex eglot-server-initialized-hook +@item eglot-server-initialized-hook +A hook run after the server object is successfully initialized. + +@vindex eglot-connect-hook +@item eglot-connect-hook +A hook run after connection to the server is successfully +established. @xref{Starting Eglot}. + +@item eglot-managed-mode-hook +A hook run after Eglot started or stopped managing a buffer. +@xref{Eglot and Buffers}, for details of its usage. + +@vindex eglot-stay-out-of +@item eglot-stay-out-of +This variable's value lists Emacs features that Eglot shouldn't +automatically try to manage on user's behalf. It is useful, for +example, when you need to use non-LSP Flymake or Company back-ends. +To have Eglot stay away of some Emacs feature, add that feature's +symbol or a regexp that will match a symbol's name to the list: for +example, the symbol @code{xref} to leave Xref alone, or the string +@samp{company} to stay away of your Company customizations. Here's an +example: + +@lisp +(add-to-list 'eglot-stay-out-of 'flymake) +@end lisp + +Note that you can still configure the excluded Emacs features manually +to use Eglot in your @code{eglot-managed-mode-hook} or via some other +mechanism. + +@vindex eglot-workspace-configuration +@cindex server workspace configuration +@item eglot-workspace-configuration +This variable is meant to be set in the @file{.dir-locals.el} file, to +provide per-project settings, as described below in more detail. +@end table + +Some language servers need to know project-specific settings, which +the LSP calls @dfn{workspace configuration}. Eglot allows such fine +tuning of per-project settings via the variable +@code{eglot-workspace-configuration}. Eglot sends the portion of the +settings contained in this variable to each server for which such +settings were defined in the variable. These settings are +communicated to the server initially (upon establishing the +connection) or when the settings are changed, or in response to the +configuration request from the server. + +In many cases, servers can be configured globally using a +configuration file in the user's home directory or in the project +directory, which the language server reads. For example, the +@command{pylsp} server for Python reads the file +@file{~/.config/pycodestyle} and the @command{clangd} server reads the +file @file{.clangd} anywhere in the current project's directory tree. +If possible, we recommend to use these configuration files that are +independent of Eglot and Emacs; they have the advantage that they will +work with other LSP clients as well. + +If you do need to provide Emacs-specific configuration for a language +server, we recommend to define the appropriate value in the +@file{.dir-locals.el} file in the project's directory. The value of +this variable should be a property list of the following format: + +@lisp + (:@var{server} @var{plist}@dots{}) +@end lisp + +@noindent +Here @code{:@var{server}} identifies a particular language server and +@var{plist} is the corresponding keyword-value property list of one or +more parameter settings for that server. That list of parameters is +serialized to JSON by Eglot and sent to the server. For that reason +JSON values @code{true}, @code{false}, and @code{@{@}} should be +represented in the property lists as Lisp symbols @code{t}, +@code{:json-false}, and @code{nil}, respectively. + +@findex eglot-show-workspace-configuration +When experimenting with workspace settings, you can use the command +@kbd{M-x eglot-show-workspace-configuration} to inspect and debug the +JSON value to be sent to the server. This helper command works even +before actually connecting to the server. + +Here's an example of defining the workspace-configuration settings for +a project that uses two different language servers, one for Python, +whose server is @command{pylsp}, the other one for Go, with +@command{gopls} as its server (presumably, the project is written in a +combination of these two languages): + +@lisp +((python-mode + . ((eglot-workspace-configuration + . (:pylsp (:plugins (:jedi_completion (:include_params t + :fuzzy t) + :pylint (:enabled :json-false))))))) + (go-mode + . ((eglot-workspace-configuration + . (:gopls (:usePlaceholders t)))))) +@end lisp + +@noindent +This should go into the @file{.dir-locals.el} file in the project's +root directory. It sets up the value of +@code{eglot-workspace-configuration} separately for each major mode. + +Alternatively, the same configuration could be defined as follows: + +@lisp +((nil + . ((eglot-workspace-configuration + . (:pylsp (:plugins (:jedi_completion (:include_params t + :fuzzy t) + :pylint (:enabled :json-false))) + :gopls (:usePlaceholders t)))))) +@end lisp + +This is an equivalent setup which sets the value for all the +major-modes inside the project; Eglot will use for each server only +the section of the parameters intended for that server + +As yet another alternative, you can set the value of +@code{eglot-workspace-configuration} programmatically, via the +@code{dir-locals-set-class-variables} function, @pxref{Directory Local +Variables,,, elisp, GNU Emacs Lisp Reference Manual}. + +Finally, if one needs to determine the workspace configuration based +on some dynamic context, @code{eglot-workspace-configuration} can be +set to a function. The function is called with the +@code{eglot-lsp-server} instance of the connected server (if any) and +with @code{default-directory} set to the root of the project. The +function should return a value of the form described above. + +Some servers need special hand-holding to operate correctly. If your +server has some quirks or non-conformity, it's possible to extend +Eglot via Elisp to adapt to it, by defining a suitable +@code{eglot-initialization-options} method via @code{cl-defmethod} +(@pxref{Generic Functions,,, elisp, GNU Emacs Lisp Reference Manual}). + +Here's an example: + +@lisp +(add-to-list 'eglot-server-programs + '((c++ mode c-mode) . (eglot-cquery "cquery"))) + +(defclass eglot-cquery (eglot-lsp-server) () + :documentation "A custom class for cquery's C/C++ langserver.") + +(cl-defmethod eglot-initialization-options ((server eglot-cquery)) + "Passes through required cquery initialization options" + (let* ((root (car (project-roots (eglot--project server)))) + (cache (expand-file-name ".cquery_cached_index/" root))) + (list :cacheDirectory (file-name-as-directory cache) + :progressReportFrequencyMs -1))) +@end lisp + +@noindent +See the doc string of @code{eglot-initialization-options} for more +details. +@c FIXME: The doc string of eglot-initialization-options should be +@c enhanced and extended. + +@node Troubleshooting Eglot +@chapter Troubleshooting Eglot +@cindex troubleshooting Eglot + +This section documents commands and variables that can be used to +troubleshoot Eglot problems. It also provides guidelines for +reporting Eglot bugs in a way that facilitates their resolution. + +When you encounter problems with Eglot, try first using the commands +@kbd{M-x eglot-events-server} and @kbd{M-x eglot-stderr-buffer}. They +pop up special buffers that can be used to inspect the communications +between the Eglot and language server. In many cases, this will +indicate the problems or at least provide a hint. + +A common and easy-to-fix cause of performance problems is the length +of these two buffers. If Eglot is operating correctly but slowly, +customize the variable @code{eglot-events-buffer-size} (@pxref{Eglot +Variables}) to limit logging, and thus speed things up. + +If you need to report an Eglot bug, please keep in mind that, because +there are so many variables involved, it is generally both very +@emph{difficult} and @emph{absolutely essential} to reproduce bugs +exactly as they happened to you, the user. Therefore, every bug +report should include: + +@enumerate +@item +The transcript of events obtained from the buffer popped up by +@kbd{M-x eglot-events-buffer}. If the transcript can be narrowed down +to show the problematic exchange, so much the better. This is +invaluable for the investigation and reproduction of the problem. + +@item +If Emacs signaled an error (an error message was seen or heard), make +sure to repeat the process after toggling @code{debug-on-error} on +(via @kbd{M-x toggle-debug-on-error}). This normally produces a +backtrace of the error that should also be attached to the bug report. + +@item +An explanation how to obtain and install the language server you used. +If possible, try to replicate the problem with the C/C@t{++} or Python +servers, as these are very easy to install. + +@item +A description of how to setup the @emph{minimal} project (one or two +files and their contents) where the problem happens. + +@item +A recipe to replicate the problem with @emph{a clean Emacs run}. This +means @kbd{emacs -Q} invocation or a very minimal (no more that 10 +lines) @file{.emacs} initialization file. @code{eglot-ensure} and +@code{use-package} calls are generally @emph{not} needed. + +@item +Make sure to double check all the above elements and re-run the +recipe to see that the problem is reproducible. +@end enumerate + +Please keep in mind that some problems reported against Eglot may +actually be bugs in the language server or the Emacs feature/package +that used Eglot to communicate with the language server. + +@node GNU Free Documentation License +@appendix GNU Free Documentation License +@include doclicense.texi + +@node Index +@unnumbered Index +@printindex cp + +@bye commit 4071eaf8ad9ce7e1a19f9f12c20430a77c1807c9 Author: João Távora Date: Tue Oct 11 12:02:46 2022 +0100 * eglot.el (version): actually bump to 1.9 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0cba701f51..d3f5935a9e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018-2022 Free Software Foundation, Inc. -;; Version: 1.8 +;; Version: 1.9 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot commit 0848387fa23c2cf00e7fae7192b21cad91e5b921 Author: João Távora Date: Sat Oct 8 11:19:10 2022 +0100 Fix docstring of eglot-list-connections-mode * eglot.el (eglot-list-connections-mode): Fix mistaken reference to similar non-Eglot code. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0dff2b0d86..0cba701f51 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -3330,9 +3330,8 @@ If NOERROR, return predicate, else erroring function." ;;; List connections mode (define-derived-mode eglot-list-connections-mode tabulated-list-mode - "" "Eglot Connection List Mode - -\\{sly-connection-list-mode-map}" + "" "Eglot mode for listing server connections +\\{eglot-list-connections-mode-map}" (setq-local tabulated-list-format `[("Language server" 16) ("Project name" 16) ("Modes handled" 16)]) (tabulated-list-init-header)) commit add2926de85a48e102cd11017f160f9f5ebe410d Author: João Távora Date: Wed Oct 5 20:53:26 2022 +0100 Make eglot-code-actions usable non-interactively * eglot.el (eglot--read-execute-code-action): New helper. (eglot-code-actions): Use new helper. Offer non-interactive version. GitHub-reference: per https://github.com/joaotavora/eglot/issues/1070 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 918bba6210..0dff2b0d86 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -3124,8 +3124,9 @@ Returns a list as described in docstring of `imenu--index-alist'." (let ((boftap (bounds-of-thing-at-point 'sexp))) (list (car boftap) (cdr boftap))))) -(defun eglot-code-actions (beg &optional end action-kind) - "Offer to execute actions of ACTION-KIND between BEG and END. +(defun eglot-code-actions (beg &optional end action-kind interactive) + "Find LSP code actions of type ACTION-KIND between BEG and END. +Interactively, offer to execute them. If ACTION-KIND is nil, consider all kinds of actions. Interactively, default BEG and END to region's bounds else BEG is point and END is nil, which results in a request for code actions @@ -3135,8 +3136,10 @@ at point. With prefix argument, prompt for ACTION-KIND." ,(and current-prefix-arg (completing-read "[eglot] Action kind: " '("quickfix" "refactor.extract" "refactor.inline" - "refactor.rewrite" "source.organizeImports"))))) - (unless (eglot--server-capable :codeActionProvider) + "refactor.rewrite" "source.organizeImports"))) + t)) + (unless (or (not interactive) + (eglot--server-capable :codeActionProvider)) (eglot--error "Server can't execute code actions!")) (let* ((server (eglot--current-server-or-lose)) (actions @@ -3154,13 +3157,20 @@ at point. With prefix argument, prompt for ACTION-KIND." collect it)] ,@(when action-kind `(:only [,action-kind])))) :deferred t)) - (menu-items - (or (cl-loop for action across actions - ;; Do filtering ourselves, in case the `:only' - ;; didn't go through. - when (or (not action-kind) - (equal action-kind (plist-get action :kind))) - collect (cons (plist-get action :title) action)) + ;; Redo filtering, in case the `:only' didn't go through. + (actions (cl-loop for a across actions + when (or (not action-kind) + (equal action-kind (plist-get a :kind))) + collect a))) + (if interactive + (eglot--read-execute-code-action actions server action-kind) + actions))) + +(defun eglot--read-execute-code-action (actions server &optional action-kind) + "Helper for interactive calls to `eglot-code-actions'" + (let* ((menu-items + (or (cl-loop for a in actions + collect (cons (plist-get a :title) a)) (apply #'eglot--error (if action-kind `("No \"%s\" code actions here" ,action-kind) `("No code actions here"))))) @@ -3169,7 +3179,7 @@ at point. With prefix argument, prompt for ACTION-KIND." (plist-get (cdr menu-item) :isPreferred)) menu-items)) (default-action (car (or preferred-action (car menu-items)))) - (action (if (and action-kind (null (cadr menu-items))) + (chosen (if (and action-kind (null (cadr menu-items))) (cdr (car menu-items)) (if (listp last-nonmenu-event) (x-popup-menu last-nonmenu-event `("Eglot code actions:" @@ -3179,7 +3189,7 @@ at point. With prefix argument, prompt for ACTION-KIND." default-action) menu-items nil t nil nil default-action) menu-items)))))) - (eglot--dcase action + (eglot--dcase chosen (((Command) command arguments) (eglot-execute-command server (intern command) arguments)) (((CodeAction) edit command) commit b07fa37d04d98fcd9856f9f29fc1323850c230b7 Author: João Távora Date: Tue Oct 4 21:11:38 2022 +0100 Add half-baked m-x eglot-list-connections Not very useful for now, but more functionality could be added later, like bindings for disconnecting a given connection, switching to its events buffers, or just listing some details like capabilities. * eglot.el (eglot-list-connections-mode, eglot-list-connections): New mode and function. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 010f8c86c7..918bba6210 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -3316,6 +3316,40 @@ If NOERROR, return predicate, else erroring function." (when (eq ?! (aref arg 1)) (aset arg 1 ?^)) `(,self () (re-search-forward ,(concat "\\=" arg)) (,next))) + +;;; List connections mode + +(define-derived-mode eglot-list-connections-mode tabulated-list-mode + "" "Eglot Connection List Mode + +\\{sly-connection-list-mode-map}" + (setq-local tabulated-list-format + `[("Language server" 16) ("Project name" 16) ("Modes handled" 16)]) + (tabulated-list-init-header)) + +(defun eglot-list-connections () + "List currently active Eglot connections." + (interactive) + (with-current-buffer + (get-buffer-create "*EGLOT connections*") + (let ((inhibit-read-only t)) + (erase-buffer) + (eglot-list-connections-mode) + (setq-local tabulated-list-entries + (mapcar + (lambda (server) + (list server + `[,(or (plist-get (eglot--server-info server) :name) + (jsonrpc-name server)) + ,(eglot-project-nickname server) + ,(mapconcat #'symbol-name + (eglot--major-modes server) + ", ")])) + (cl-reduce #'append + (hash-table-values eglot--servers-by-project)))) + (revert-buffer) + (pop-to-buffer (current-buffer))))) + ;;; Hacks ;;; commit b633c29648d0f9b4f36c81c6c0eac4c6e2362a76 Author: João Távora Date: Tue Oct 4 19:32:02 2022 +0100 Rename "eglot -> eglot" in docstrings * eglot.el (eglot-mode-line, eglot-client-capabilities) (eglot--stay-out-of-p, eglot-managed-p) (eglot-managed-mode-hook, eglot--managed-mode) (eglot-current-server, eglot--current-server-or-lose) (eglot--mode-line-format, eglot-xref-backend) (eglot-imenu): Rename "EGLOT" -> "Eglot" diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 58a150cf48..010f8c86c7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -273,7 +273,7 @@ CONTACT can be: (defface eglot-mode-line '((t (:inherit font-lock-constant-face :weight bold))) - "Face for package-name in EGLOT's mode line.") + "Face for package-name in Eglot's mode line.") (defface eglot-diagnostic-tag-unnecessary-face '((t (:inherit shadow))) @@ -671,7 +671,7 @@ treated as in `eglot-dbind'." method))) (cl-defgeneric eglot-client-capabilities (server) - "What the EGLOT LSP client supports for SERVER." + "What the Eglot LSP client supports for SERVER." (:method (s) (list :workspace (list @@ -1131,7 +1131,7 @@ Each function is passed the server as an argument") contact)) (defvar-local eglot--cached-server nil - "A cached reference to the current EGLOT server.") + "A cached reference to the current Eglot server.") (defun eglot--connect (managed-modes project class contact language-id) "Connect to MANAGED-MODES, LANGUAGE-ID, PROJECT, CLASS and CONTACT. @@ -1625,7 +1625,7 @@ For example, to keep your Company customization, add the symbol `company' to this variable.") (defun eglot--stay-out-of-p (symbol) - "Tell if EGLOT should stay of of SYMBOL." + "Tell if Eglot should stay of of SYMBOL." (cl-find (symbol-name symbol) eglot-stay-out-of :test (lambda (s thing) (let ((re (if (symbolp thing) (symbol-name thing) thing))) @@ -1637,15 +1637,15 @@ For example, to keep your Company customization, add the symbol (setq-local ,symbol ,binding))) (defun eglot-managed-p () - "Tell if current buffer is managed by EGLOT." + "Tell if current buffer is managed by Eglot." eglot--managed-mode) (defvar eglot-managed-mode-hook nil - "A hook run by EGLOT after it started/stopped managing a buffer. + "A hook run by Eglot after it started/stopped managing a buffer. Use `eglot-managed-p' to determine if current buffer is managed.") (define-minor-mode eglot--managed-mode - "Mode for source buffers managed by some EGLOT project." + "Mode for source buffers managed by some Eglot project." :init-value nil :lighter nil :keymap eglot-mode-map (cond (eglot--managed-mode @@ -1715,7 +1715,7 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (eglot--managed-mode -1)) (defun eglot-current-server () - "Return logical EGLOT server for current buffer, nil if none." + "Return logical Eglot server for current buffer, nil if none." (setq eglot--cached-server (or eglot--cached-server (cl-find major-mode @@ -1728,7 +1728,7 @@ Use `eglot-managed-p' to determine if current buffer is managed.") eglot--servers-by-xrefed-file))))) (defun eglot--current-server-or-lose () - "Return current logical EGLOT server connection or error." + "Return current logical Eglot server connection or error." (or (eglot-current-server) (jsonrpc-error "No current JSON-RPC connection"))) @@ -1859,7 +1859,7 @@ Uses THING, FACE, DEFS and PREPEND." mouse-face mode-line-highlight)))) (defun eglot--mode-line-format () - "Compose the EGLOT's mode-line." + "Compose the Eglot's mode-line." (pcase-let* ((server (eglot-current-server)) (nick (and server (eglot-project-nickname server))) (pending (and server (hash-table-count @@ -2416,7 +2416,7 @@ may be called multiple times (respecting the protocol of :region (cons (point-min) (point-max)))) (setq eglot--diagnostics diags)) -(defun eglot-xref-backend () "EGLOT xref backend." 'eglot) +(defun eglot-xref-backend () "Eglot xref backend." 'eglot) (defvar eglot--temp-location-buffers (make-hash-table :test #'equal) "Helper variable for `eglot--handling-xrefs'.") @@ -2666,7 +2666,7 @@ for which LSP on-type-formatting should be requested." :deferred method)))) (defun eglot-completion-at-point () - "EGLOT's `completion-at-point' function." + "Eglot's `completion-at-point' function." ;; Commit logs for this function help understand what's going on. (when-let (completion-capability (eglot--server-capable :completionProvider)) (let* ((server (eglot--current-server-or-lose)) @@ -2983,7 +2983,7 @@ for which LSP on-type-formatting should be requested." nil))) (defun eglot-imenu () - "EGLOT's `imenu-create-index-function'. + "Eglot's `imenu-create-index-function'. Returns a list as described in docstring of `imenu--index-alist'." (cl-labels ((unfurl (obj) commit 1780b93d664e6f4b4678d240333d7011e1a9f1ad Author: João Távora Date: Mon Sep 26 13:35:34 2022 +0100 Make clojure-lsp handle more major modes at once Suggested-by: Witoslaw Koczewski * eglot.el (eglot-server-programs): Enhance clojure-specific section. GitHub-reference: per https://github.com/joaotavora/eglot/issues/682 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6a1eb1282c..58a150cf48 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -197,7 +197,8 @@ language-server/bin/php-language-server.php")) (html-mode . ,(eglot-alternatives '(("vscode-html-language-server" "--stdio") ("html-languageserver" "--stdio")))) (json-mode . ,(eglot-alternatives '(("vscode-json-language-server" "--stdio") ("json-languageserver" "--stdio")))) (dockerfile-mode . ("docker-langserver" "--stdio")) - (clojure-mode . ("clojure-lsp")) + ((clojure-mode clojurescript-mode clojurec-mode) + . ("clojure-lsp")) (csharp-mode . ("omnisharp" "-lsp")) (purescript-mode . ("purescript-language-server" "--stdio")) (perl-mode . ("perl" "-MPerl::LanguageServer" "-e" "Perl::LanguageServer::run")) commit 5b902b5cbb1de5f3852856d194deaf996c2623cc Author: João Távora Date: Mon Sep 26 11:43:39 2022 +0100 Add support for "single server, multiple modes" Previously, if an entry such as: ((c++-mode c-mode) . ("clangd)") were found in eglot-server-programs, it meant that opening a .cpp file and a .c file in the same project and enabling eglot for both would lead to two clangd instances. Now only one instance is created to handle all buffers of those major modes, as long as they are in the same project. This change accomplishes this with minimal changes and NO modification to the already complicated syntax of eglot-server-programs. Naturally, this means that a subtle backward-incompatibility was introduced. If, instead of "clangd", someone is using some kind "c++-or-c-but-not-both-at-once" server, this commit now breaks that person's configuration. After analysing the entries of this variable, an educated guess was made that this situation is rare. If it's not rare, then some change to the syntax of eglot-server-programs will have to ensue. * eglot.el (eglot-server-programs): Update docstring. (eglot-lsp-server): Replace major-mode -> major-modes. (eglot--lookup-mode): Rework. (eglot--guess-contact): Rework. (eglot--connect): Reword first parameter. (eglot-reconnect): Use eglot--major-modes. (eglot--read-server): Rework. (eglot--ensure-list): New helper. (eglot-current-server): Rework. (eglot-handle-request workspace/configuration): Use first of managed major modes. * NEWS.md: Mention change. GitHub-reference: per https://github.com/joaotavora/eglot/issues/681 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fa29c60617..6a1eb1282c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -218,7 +218,8 @@ MAJOR-MODE can be: * A list combining the previous two alternatives, meaning multiple major modes will be associated with a single server - program. + program. This association is such that the same resulting + server process will manage buffers of different major modes. CONTACT can be: @@ -760,9 +761,9 @@ treated as in `eglot-dbind'." :documentation "Short nickname for the associated project." :accessor eglot--project-nickname :reader eglot-project-nickname) - (major-mode - :documentation "Major mode symbol." - :accessor eglot--major-mode) + (major-modes + :documentation "Major modes server is responsible for in a given project." + :accessor eglot--major-modes) (language-id :documentation "Language ID string for the mode." :accessor eglot--language-id) @@ -879,16 +880,31 @@ PRESERVE-BUFFERS as in `eglot-shutdown', which see." (defun eglot--lookup-mode (mode) "Lookup `eglot-server-programs' for MODE. -Return (LANGUAGE-ID . CONTACT-PROXY). If not specified, -LANGUAGE-ID is determined from MODE." +Return (MANAGED-MODES LANGUAGE-ID CONTACT-PROXY). + +MANAGED-MODES is a list with MODE as its first elements. +Subsequent elements are other major modes also potentially +managed by the server that is to manage MODE. + +If not specified in `eglot-server-programs' (which see), +LANGUAGE-ID is determined from MODE's name. + +CONTACT-PROXY is the value of the corresponding +`eglot-server-programs' entry." (cl-loop for (modes . contact) in eglot-server-programs + for mode-symbols = (cons mode + (delete mode + (mapcar #'car + (mapcar #'eglot--ensure-list + (eglot--ensure-list modes))))) thereis (cl-some (lambda (spec) (cl-destructuring-bind (probe &key language-id &allow-other-keys) - (if (consp spec) spec (list spec)) + (eglot--ensure-list spec) (and (provided-mode-derived-p mode probe) - (cons + (list + mode-symbols (or language-id (or (get mode 'eglot-language-id) (get spec 'eglot-language-id) @@ -903,7 +919,7 @@ Return (MANAGED-MODE PROJECT CLASS CONTACT LANG-ID). If INTERACTIVE is non-nil, maybe prompt user, else error as soon as something can't be guessed." (let* ((guessed-mode (if buffer-file-name major-mode)) - (managed-mode + (main-mode (cond ((and interactive (or (>= (prefix-numeric-value current-prefix-arg) 16) @@ -916,10 +932,11 @@ be guessed." ((not guessed-mode) (eglot--error "Can't guess mode to manage for `%s'" (current-buffer))) (t guessed-mode))) - (lang-id-and-guess (eglot--lookup-mode guessed-mode)) - (language-id (or (car lang-id-and-guess) + (triplet (eglot--lookup-mode main-mode)) + (managed-modes (car triplet)) + (language-id (or (cadr triplet) (string-remove-suffix "-mode" (symbol-name guessed-mode)))) - (guess (cdr lang-id-and-guess)) + (guess (caddr triplet)) (guess (if (functionp guess) (funcall guess interactive) guess)) @@ -945,7 +962,7 @@ be guessed." (cond (current-prefix-arg base-prompt) ((null guess) (format "[eglot] Sorry, couldn't guess for `%s'!\n%s" - managed-mode base-prompt)) + main-mode base-prompt)) ((and program (not (file-name-absolute-p program)) (not (eglot--executable-find program t))) @@ -967,7 +984,7 @@ be guessed." full-program-invocation 'eglot-command-history))) guess))) - (list managed-mode (eglot--current-project) class contact language-id))) + (list managed-modes (eglot--current-project) class contact language-id))) (defvar eglot-lsp-context) (put 'eglot-lsp-context 'variable-documentation @@ -1038,7 +1055,7 @@ INTERACTIVE is t if called interactively." (interactive (list (eglot--current-server-or-lose) t)) (when (jsonrpc-running-p server) (ignore-errors (eglot-shutdown server interactive nil 'preserve-buffers))) - (eglot--connect (eglot--major-mode server) + (eglot--connect (eglot--major-modes server) (eglot--project server) (eieio-object-class-name server) (eglot--saved-initargs server) @@ -1115,12 +1132,12 @@ Each function is passed the server as an argument") (defvar-local eglot--cached-server nil "A cached reference to the current EGLOT server.") -(defun eglot--connect (managed-major-mode project class contact language-id) - "Connect to MANAGED-MAJOR-MODE, LANGUAGE-ID, PROJECT, CLASS and CONTACT. +(defun eglot--connect (managed-modes project class contact language-id) + "Connect to MANAGED-MODES, LANGUAGE-ID, PROJECT, CLASS and CONTACT. This docstring appeases checkdoc, that's all." (let* ((default-directory (project-root project)) (nickname (file-name-base (directory-file-name default-directory))) - (readable-name (format "EGLOT (%s/%s)" nickname managed-major-mode)) + (readable-name (format "EGLOT (%s/%s)" nickname managed-modes)) autostart-inferior-process server-info (contact (if (functionp contact) (funcall contact) contact)) @@ -1180,7 +1197,7 @@ This docstring appeases checkdoc, that's all." (setf (eglot--saved-initargs server) initargs) (setf (eglot--project server) project) (setf (eglot--project-nickname server) nickname) - (setf (eglot--major-mode server) managed-major-mode) + (setf (eglot--major-modes server) (eglot--ensure-list managed-modes)) (setf (eglot--language-id server) language-id) (setf (eglot--inferior-process server) autostart-inferior-process) (run-hook-with-args 'eglot-server-initialized-hook server) @@ -1236,7 +1253,7 @@ This docstring appeases checkdoc, that's all." (setf (eglot--inhibit-autoreconnect server) (null eglot-autoreconnect))))))) (let ((default-directory (project-root project)) - (major-mode managed-major-mode)) + (major-mode (car managed-modes))) (hack-dir-local-variables-non-file-buffer) (run-hook-with-args 'eglot-connect-hook server)) (eglot--message @@ -1244,7 +1261,7 @@ This docstring appeases checkdoc, that's all." in project `%s'." (or (plist-get serverInfo :name) (jsonrpc-name server)) - managed-major-mode + managed-modes (eglot-project-nickname server)) (when tag (throw tag t)))) :timeout eglot-connect-timeout @@ -1545,8 +1562,8 @@ and just return it. PROMPT shouldn't end with a question mark." being hash-values of eglot--servers-by-project append servers)) (name (lambda (srv) - (format "%s/%s" (eglot-project-nickname srv) - (eglot--major-mode srv))))) + (format "%s %s" (eglot-project-nickname srv) + (eglot--major-modes srv))))) (cond ((null servers) (eglot--error "No servers!")) ((or (cdr servers) (not dont-if-just-the-one)) @@ -1570,6 +1587,8 @@ and just return it. PROMPT shouldn't end with a question mark." (defun eglot--plist-keys (plist) "Get keys of a plist." (cl-loop for (k _v) on plist by #'cddr collect k)) +(defun eglot--ensure-list (x) (if (listp x) x (list x))) + ;;; Minor modes ;;; @@ -1700,7 +1719,8 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (or eglot--cached-server (cl-find major-mode (gethash (eglot--current-project) eglot--servers-by-project) - :key #'eglot--major-mode) + :key #'eglot--major-modes + :test #'memq) (and eglot-extend-to-xref buffer-file-name (gethash (expand-file-name buffer-file-name) @@ -2288,7 +2308,7 @@ When called interactively, use the currently active server" (file-directory-p uri-path)) (file-name-as-directory uri-path) (project-root (eglot--project server))))) - (setq-local major-mode (eglot--major-mode server)) + (setq-local major-mode (car (eglot--major-modes server))) (hack-dir-local-variables-non-file-buffer) (cl-loop for (wsection o) on (eglot--workspace-configuration-plist server) commit f06a837f291004e4d25804d78dd8ae17eba2fe14 Author: João Távora Date: Mon Sep 26 10:23:53 2022 +0100 Shoosh byte-compilation warnings about line numbering functions Also add warning at the top of file about not using functionality incompatible with 26.3 * eglot.el: (eglot-current-column, eglot-current-column): Use line-beginning-position (eglot--xref-make-match): Use line-beginning-position, line-end-position, line-number-at-pos diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 650b4ccccc..fa29c60617 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -9,6 +9,10 @@ ;; Keywords: convenience, languages ;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.14") (flymake "1.2.1") (project "0.3.0") (xref "1.0.1") (eldoc "1.11.0") (seq "2.23")) +;; This is (or will soon) be a GNU ELPA :core package. Avoid using +;; functionality that not compatible with the version of Emacs +;; recorded above. + ;; This file is part of GNU Emacs. ;; GNU Emacs is free software: you can redistribute it and/or modify @@ -1336,7 +1340,7 @@ CONNECT-ARGS are passed as additional arguments to (let ((warning-minimum-level :error)) (display-warning 'eglot (apply #'format format args) :warning))) -(defun eglot-current-column () (- (point) (point-at-bol))) +(defun eglot-current-column () (- (point) (line-beginning-position))) (defvar eglot-current-column-function #'eglot-lsp-abiding-column "Function to calculate the current column. @@ -1985,10 +1989,10 @@ COMMAND is a symbol naming the command." (eglot--widening (goto-char (point-min)) (setq beg - (point-at-bol + (line-beginning-position (1+ (plist-get (plist-get range :start) :line)))) (setq end - (point-at-eol + (line-end-position (1+ (plist-get (plist-get range :end) :line))))))) (eglot--make-diag (current-buffer) beg end @@ -2422,14 +2426,14 @@ Try to visit the target file for a richer summary line." (collect (lambda () (eglot--widening (pcase-let* ((`(,beg . ,end) (eglot--range-region range)) - (bol (progn (goto-char beg) (point-at-bol))) - (substring (buffer-substring bol (point-at-eol))) + (bol (progn (goto-char beg) (line-beginning-position))) + (substring (buffer-substring bol (line-end-position))) (hi-beg (- beg bol)) - (hi-end (- (min (point-at-eol) end) bol))) + (hi-end (- (min (line-end-position) end) bol))) (add-face-text-property hi-beg hi-end 'xref-match t substring) - (list substring (1+ (current-line)) (eglot-current-column) - (- end beg)))))) + (list substring (line-number-at-pos (point) t) + (eglot-current-column) (- end beg)))))) (`(,summary ,line ,column ,length) (cond (visiting (with-current-buffer visiting (funcall collect))) commit b2054790358ec89c3c3e0e79afda4adbce7f1dd6 Author: João Távora Date: Sat Sep 24 09:54:04 2022 +0100 Fix blunder in eglot--guess-contact * eglot.el (eglot--guess-contact): Add back 'split-string-and-unquote' lost in https://github.com/joaotavora/eglot/issues/940 fix. GitHub-reference: per https://github.com/joaotavora/eglot/issues/940 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 718a42dbd7..650b4ccccc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -957,10 +957,11 @@ be guessed." program guess)))))) (contact (or (and prompt - (read-shell-command - prompt - full-program-invocation - 'eglot-command-history)) + (split-string-and-unquote + (read-shell-command + prompt + full-program-invocation + 'eglot-command-history))) guess))) (list managed-mode (eglot--current-project) class contact language-id))) commit 0829d5e7a2e7a958cb55df8cbc983bfcf7d4b500 Author: João Távora Date: Mon Sep 19 16:19:33 2022 +0100 Revert "fix jdtls support" This reverts commit e5b021c01fceea02b7e6622cde0a347b842ca6f3. GitHub-reference: per https://github.com/joaotavora/eglot/issues/1008 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0867e43e07..718a42dbd7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -172,7 +172,7 @@ language-server/bin/php-language-server.php")) (go-mode . ("gopls")) ((R-mode ess-r-mode) . ("R" "--slave" "-e" "languageserver::run()")) - (java-mode . ("jdtls" "-data" ".jdtls-cache")) + (java-mode . ("jdtls")) (dart-mode . ("dart" "language-server" "--client-id" "emacs.eglot-dart")) (elixir-mode . ("language_server.sh")) commit 68b9c03b44a056e7a1454879b7f1b9cf050f7ddb Author: João Távora Date: Mon Sep 19 16:04:31 2022 +0100 Don't use three-argument plist-get * eglot.el (eglot-handle-request): Don't use three-argument plist-get. GitHub-reference: per https://github.com/joaotavora/eglot/issues/1024 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7c1e849389..0867e43e07 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2285,13 +2285,15 @@ When called interactively, use the currently active server" (project-root (eglot--project server))))) (setq-local major-mode (eglot--major-mode server)) (hack-dir-local-variables-non-file-buffer) - (plist-get (eglot--workspace-configuration-plist server) section - (lambda (section wsection) - (string= - (if (keywordp wsection) - (substring (symbol-name wsection) 1) - wsection) - section)))))) + (cl-loop for (wsection o) + on (eglot--workspace-configuration-plist server) + by #'cddr + when (string= + (if (keywordp wsection) + (substring (symbol-name wsection) 1) + wsection) + section) + return o)))) items))) (defun eglot--signal-textDocument/didChange () commit ec7d63cbe75b2de4a8e85852f077c7ab04d87524 Author: João Távora Date: Sun Sep 18 11:19:00 2022 +0100 Don't return hash tables from e-w-configuration-plist * eglot.el (eglot-signal-didChangeConfiguration): Adjust. (eglot-handle-request workspace-configuration): Adjust. (eglot--workspace-configuration-plist): Don't return a hashtable. GitHub-reference: per https://github.com/joaotavora/eglot/issues/1033 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c2db7e817f..7c1e849389 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2257,8 +2257,7 @@ format described above.") collect (if (keywordp section) section (intern (format ":%s" section))) collect v)) - val - eglot--{}))) + val))) (defun eglot-signal-didChangeConfiguration (server) "Send a `:workspace/didChangeConfiguration' signal to SERVER. @@ -2268,7 +2267,8 @@ When called interactively, use the currently active server" server :workspace/didChangeConfiguration (list :settings - (eglot--workspace-configuration-plist server)))) + (or (eglot--workspace-configuration-plist server) + eglot--{})))) (cl-defmethod eglot-handle-request (server (_method (eql workspace/configuration)) &key items) commit a5983527506d0d79137003dc9b5eb15dfd4c7365 Author: João Távora Date: Sun Sep 18 00:37:31 2022 +0100 Allow eglot-workspace-configuration to be a plist Suggested-by: Augusto Stoffel * NEWS.md: Mention change. * README.md (eglot-workspace-configuration): Update yet again. Update examples to use pylsp. * eglot.el (eglot--workspace-configuration-plist): Noop if already a plist. (eglot-handle-request workspace/configuration): Use eglot--workspace-configuration-plist. (eglot-workspace-configuration): Document variable. GitHub-reference: per https://github.com/joaotavora/eglot/issues/590 GitHub-reference: per https://github.com/joaotavora/eglot/issues/790 GitHub-reference: per https://github.com/joaotavora/eglot/issues/1033 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 038847c78f..c2db7e817f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2198,14 +2198,32 @@ Records BEG, END and PRE-CHANGE-LENGTH locally." '((name . eglot--signal-textDocument/didChange))) (defvar-local eglot-workspace-configuration () - "Alist of (SECTION . VALUE) entries configuring the LSP server. -SECTION should be a keyword or a string. VALUE is a -plist or a primitive type converted to JSON. + "Configure LSP servers specifically for a given project. + +This variable's value should be a plist (SECTION VALUE ...). +SECTION is a keyword naming a parameter section relevant to a +particular server. VALUE is a plist or a primitive type +converted to JSON also understood by that server. + +Instead of a plist, an alist ((SECTION . VALUE) ...) can be used +instead, but this variant is less reliable and not recommended. + +This variable should be set as a directory-local variable. See +See info node `(emacs)Directory Variables' for various ways to to +that. + +Here's an example value that establishes two sections relevant to +the Pylsp and Gopls LSP servers: + + (:pylsp (:plugins (:jedi_completion (:include_params t + :fuzzy t) + :pylint (:enabled :json-false))) + :gopls (:usePlaceholders t)) The value of this variable can also be a unary function of a single argument, which will be a connected `eglot-lsp-server' instance. The function runs with `default-directory' set to the -root of the current project. It should return an alist of the +root of the current project. It should return an object of the format described above.") ;;;###autoload @@ -2232,12 +2250,15 @@ format described above.") eglot-workspace-configuration)) (defun eglot--workspace-configuration-plist (server) - "Returns `eglot-workspace-configuraiton' suitable serialization." - (or (cl-loop for (section . v) in (eglot--workspace-configuration server) - collect (if (keywordp section) section - (intern (format ":%s" section))) - collect v) - eglot--{})) + "Returns `eglot-workspace-configuration' suitable for serialization." + (let ((val (eglot--workspace-configuration server))) + (or (and (consp (car val)) + (cl-loop for (section . v) in val + collect (if (keywordp section) section + (intern (format ":%s" section))) + collect v)) + val + eglot--{}))) (defun eglot-signal-didChangeConfiguration (server) "Send a `:workspace/didChangeConfiguration' signal to SERVER. @@ -2264,9 +2285,8 @@ When called interactively, use the currently active server" (project-root (eglot--project server))))) (setq-local major-mode (eglot--major-mode server)) (hack-dir-local-variables-non-file-buffer) - (alist-get section (eglot--workspace-configuration server) - nil nil - (lambda (wsection section) + (plist-get (eglot--workspace-configuration-plist server) section + (lambda (section wsection) (string= (if (keywordp wsection) (substring (symbol-name wsection) 1) commit 14586fedcf9ac4aafe119b89a72d0b438f6a4d04 Author: João Távora Date: Sun Sep 18 10:15:43 2022 +0100 Don't exceed max-specdl-size in big go projects When invoking client/registerCapability for workspace/didChangeWatchedFiles, Gopls lists each file to watch separately. This makes eglot--glob-emit-{} emit a closure with an 'or' form containing a potentially large number of 're-search-forward' forms. For large Go project such as "Kubernetes", this list becomes so large that -- for some reason I don't understand -- it triggers the 'max-specdl-size' limit. An alternative using `regexp` opt doesn't seem to trigger the error. * eglot.el (eglot--glob-emit-{}): Use regexp-opt. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/633 GitHub-reference: fix https://github.com/joaotavora/eglot/issues/1067 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 493bfcc7d6..038847c78f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -3260,8 +3260,7 @@ If NOERROR, return predicate, else erroring function." (defun eglot--glob-emit-{} (arg self next) (let ((alternatives (split-string (substring arg 1 (1- (length arg))) ","))) `(,self () - (or ,@(cl-loop for alt in alternatives - collect `(re-search-forward ,(concat "\\=" alt) nil t)) + (or (re-search-forward ,(concat "\\=" (regexp-opt alternatives)) nil t) (error "Failed matching any of %s" ',alternatives)) (,next)))) commit 523547321e4caca6fc966bd71ecd7b60a6e98f73 Author: João Távora Date: Sat Sep 17 21:40:34 2022 +0100 Allow :initializationoptions in eglot-server-programs Also see https://github.com/joaotavora/eglot/issues/1038. This feature was poorly tested, and simply wouldn't work when trying to initialize the server object. The simple solution is to ignore :initializationOptions initarg in this context. It is still stored separately as and accessed as the 'eglot--saved-initargs' slot. Another complication arises in eglot--guess-contact, which tried too hard to be able to compose an interactive prompt (when the server program can't be found). The solution is just to give up when :autoport or :initializationOptions is found. It's not easy or practical to have the user provide non-string arguments via a string interface like the minibuffer. * eglot.el (initialize-instance :before eglot-lsp-server): Don't pass :initializationOptions initarg onward. (eglot--guess-contact): Simplify. Don't try heroics with :autoport and :initializationOptions. * eglot-tests.el (eglot-server-programs-simple-missing-executable): Update test. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/940 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ff94d5ca5f..493bfcc7d6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -796,6 +796,9 @@ treated as in `eglot-dbind'." :documentation "Represents a server. Wraps a process for LSP communication.") +(cl-defmethod initialize-instance :before ((_server eglot-lsp-server) &optional args) + (cl-remf args :initializationOptions)) + ;;; Process management (defvar eglot--servers-by-project (make-hash-table :test #'equal) @@ -929,10 +932,10 @@ be guessed." (base-prompt (and interactive "Enter program to execute (or :): ")) - (program-guess + (full-program-invocation (and program - (combine-and-quote-strings (cl-subst ":autoport:" - :autoport guess)))) + (cl-every #'stringp guess) + (combine-and-quote-strings guess))) (prompt (and base-prompt (cond (current-prefix-arg base-prompt) @@ -942,25 +945,23 @@ be guessed." ((and program (not (file-name-absolute-p program)) (not (eglot--executable-find program t))) - (concat (format "[eglot] I guess you want to run `%s'" - program-guess) - (format ", but I can't find `%s' in PATH!" program) - "\n" base-prompt))))) + (if full-program-invocation + (concat (format "[eglot] I guess you want to run `%s'" + full-program-invocation) + (format ", but I can't find `%s' in PATH!" + program) + "\n" base-prompt) + (eglot--error + (concat "`%s' not found in PATH, but can't form" + " an interactive prompt for to fix %s!") + program guess)))))) (contact (or (and prompt - (let ((s (read-shell-command - prompt - program-guess - 'eglot-command-history))) - (if (string-match "^\\([^\s\t]+\\):\\([[:digit:]]+\\)$" - (string-trim s)) - (list (match-string 1 s) - (string-to-number (match-string 2 s))) - (cl-subst - :autoport ":autoport:" (split-string-and-unquote s) - :test #'equal)))) - guess - (eglot--error "Couldn't guess for `%s'!" managed-mode)))) + (read-shell-command + prompt + full-program-invocation + 'eglot-command-history)) + guess))) (list managed-mode (eglot--current-project) class contact language-id))) (defvar eglot-lsp-context) commit bef332a98314c5cbd76cce79ee0a60cb6aa94172 Author: João Távora Date: Sat Sep 17 10:33:26 2022 +0100 Adjust last commit about workspace configuration * README.md (way): Adjust. * eglot.el (json): Don't require needlessly. (eglot-show-workspace-configuration): Don't depend on json-mode. GitHub-reference: per https://github.com/joaotavora/eglot/issues/790 GitHub-reference: per https://github.com/joaotavora/eglot/issues/590 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f02ec73043..ff94d5ca5f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -74,7 +74,6 @@ (require 'filenotify) (require 'ert) (require 'array) -(require 'json) ;; ElDoc is preloaded in Emacs, so `require'-ing won't guarantee we are ;; using the latest version from GNU Elpa when we load eglot.el. Use an @@ -2222,8 +2221,7 @@ format described above.") (insert (jsonrpc--json-encode conf)) (with-no-warnings (require 'json) - (require 'json-mode) - (json-mode) + (when (require 'json-mode nil t) (json-mode)) (json-pretty-print-buffer)) (pop-to-buffer (current-buffer))))) commit e5f77f8ca59b899bcc2b77e8e8eb27a2749a9130 Author: João Távora Date: Sat Sep 17 01:28:52 2022 +0100 Rework readme.md about workspace configuration again Also tweak eglot-show-workspace-configuration a bit. * README.md (Workspace configuration): Rework. * eglot.el (eglot-show-workspace-configuration): Rework. (eglot--workspace-configuration-plist): New helper. GitHub-reference: per https://github.com/joaotavora/eglot/issues/590 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 91733a8d7a..f02ec73043 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -58,7 +58,6 @@ ;;; Code: -(require 'json) (require 'imenu) (require 'cl-lib) (require 'project) @@ -2204,23 +2203,28 @@ SECTION should be a keyword or a string. VALUE is a plist or a primitive type converted to JSON. The value of this variable can also be a unary function of a -`eglot-lsp-server' instance, the server connection requesting the -configuration. It should return an alist of the format described -above.") +single argument, which will be a connected `eglot-lsp-server' +instance. The function runs with `default-directory' set to the +root of the current project. It should return an alist of the +format described above.") ;;;###autoload (put 'eglot-workspace-configuration 'safe-local-variable 'listp) -(defun eglot-show-configuration (server) - "Dump `eglot-workspace-configuration' as json for debugging." - (interactive (list (eglot--read-server "Server configuration" - (eglot-current-server)))) - (let ((conf (eglot--workspace-configuration server))) - (with-current-buffer (get-buffer-create " *eglot configuration*") +(defun eglot-show-workspace-configuration (&optional server) + "Dump `eglot-workspace-configuration' as JSON for debugging." + (interactive (list (and (eglot-current-server) + (eglot--read-server "Server configuration" + (eglot-current-server))))) + (let ((conf (eglot--workspace-configuration-plist server))) + (with-current-buffer (get-buffer-create "*EGLOT workspace configuration*") (erase-buffer) (insert (jsonrpc--json-encode conf)) - (json-mode) - (json-pretty-print-buffer) + (with-no-warnings + (require 'json) + (require 'json-mode) + (json-mode) + (json-pretty-print-buffer)) (pop-to-buffer (current-buffer))))) (defun eglot--workspace-configuration (server) @@ -2228,6 +2232,14 @@ above.") (funcall eglot-workspace-configuration server) eglot-workspace-configuration)) +(defun eglot--workspace-configuration-plist (server) + "Returns `eglot-workspace-configuraiton' suitable serialization." + (or (cl-loop for (section . v) in (eglot--workspace-configuration server) + collect (if (keywordp section) section + (intern (format ":%s" section))) + collect v) + eglot--{})) + (defun eglot-signal-didChangeConfiguration (server) "Send a `:workspace/didChangeConfiguration' signal to SERVER. When called interactively, use the currently active server" @@ -2236,12 +2248,7 @@ When called interactively, use the currently active server" server :workspace/didChangeConfiguration (list :settings - (or (cl-loop for (section . v) in (eglot--workspace-configuration server) - collect (if (keywordp section) - section - (intern (format ":%s" section))) - collect v) - eglot--{})))) + (eglot--workspace-configuration-plist server)))) (cl-defmethod eglot-handle-request (server (_method (eql workspace/configuration)) &key items) commit 51ae66b50c9f73956039fddba72dbd7213926622 Author: Fredrik Bergroth Date: Mon Jan 10 15:09:36 2022 +0100 Add eglot-show-configuration to debug workspace configurations Also see https://github.com/joaotavora/eglot/issues/790, https://github.com/joaotavora/eglot/issues/1033. GitHub-reference: per https://github.com/joaotavora/eglot/issues/590 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e399b29f09..91733a8d7a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -75,6 +75,7 @@ (require 'filenotify) (require 'ert) (require 'array) +(require 'json) ;; ElDoc is preloaded in Emacs, so `require'-ing won't guarantee we are ;; using the latest version from GNU Elpa when we load eglot.el. Use an @@ -2210,6 +2211,18 @@ above.") ;;;###autoload (put 'eglot-workspace-configuration 'safe-local-variable 'listp) +(defun eglot-show-configuration (server) + "Dump `eglot-workspace-configuration' as json for debugging." + (interactive (list (eglot--read-server "Server configuration" + (eglot-current-server)))) + (let ((conf (eglot--workspace-configuration server))) + (with-current-buffer (get-buffer-create " *eglot configuration*") + (erase-buffer) + (insert (jsonrpc--json-encode conf)) + (json-mode) + (json-pretty-print-buffer) + (pop-to-buffer (current-buffer))))) + (defun eglot--workspace-configuration (server) (if (functionp eglot-workspace-configuration) (funcall eglot-workspace-configuration server) commit d2e842bbf51134bfd33943247faa88db99a36842 Author: Theodor Thornhill Date: Fri Sep 9 23:25:50 2022 +0200 Prefer documentchanges to changes in server-initiated edits Some servers return both. PR: https://github.com/joaotavora/eglot/issues/949 * eglot.el (eglot--apply-workspace-edit): When both documentChanges and changes are present, prefer the documentChanges. By doing that we ensure that we don't double edit, rendering the document in an unusable state. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/704 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 80f0b65470..e399b29f09 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -3016,8 +3016,12 @@ Returns a list as described in docstring of `imenu--index-alist'." textDocument (list (eglot--uri-to-path uri) edits version))) documentChanges))) - (cl-loop for (uri edits) on changes by #'cddr - do (push (list (eglot--uri-to-path uri) edits) prepared)) + (unless (and changes documentChanges) + ;; We don't want double edits, and some servers send both + ;; changes and documentChanges. This unless ensures that we + ;; prefer documentChanges over changes. + (cl-loop for (uri edits) on changes by #'cddr + do (push (list (eglot--uri-to-path uri) edits) prepared))) (if (or confirm (cl-notevery #'find-buffer-visiting (mapcar #'car prepared))) commit dd017359e974ff2bbbb0db8ceba8e13de6035900 Author: jgart <47760695+jgarte@users.noreply.github.com> Date: Fri Jul 8 19:16:29 2022 -0500 Add support for jedi-language-server (again) * eglot.el (eglot-server-programs): Add jedi-language-server * README.md: Mention jedi-language-server * NEWS.md: Mention jedi-language-server Copyright-paperwork-exempt: yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/961 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2ac9b0dff6..80f0b65470 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -151,7 +151,7 @@ chosen (interactively or automatically)." (vimrc-mode . ("vim-language-server" "--stdio")) (python-mode . ,(eglot-alternatives - '("pylsp" "pyls" ("pyright-langserver" "--stdio")))) + '("pylsp" "pyls" ("pyright-langserver" "--stdio") "jedi-language-server"))) ((js-mode typescript-mode) . ("typescript-language-server" "--stdio")) (sh-mode . ("bash-language-server" "start")) commit 41a42e631bd798151130097feafa6f535161b9de Author: João Távora Date: Thu Sep 8 11:53:11 2022 +0100 Don't return poorly supported "special elements" in eglot-imenu Fix https://github.com/joaotavora/eglot/issues/758, https://github.com/joaotavora/eglot/issues/536, https://github.com/joaotavora/eglot/issues/535. Eglot's eglot-imenu returned a structure compliant with the rules outlined in imenu--index-alist. In particular, it returned some elements of the form (INDEX-NAME POSITION GOTO-FN ARGUMENTS...) The original intention (mine) must have been to allow fancy highlighting of the position navigated to with a custom GOTO-FN. Not only was access to that fanciness never implemented, but many other imenu frontends do not support such elements. See for example https://github.com/joaotavora/eglot/issues/758, https://github.com/joaotavora/eglot/issues/536, https://github.com/joaotavora/eglot/issues/535. And also related issues in other packages: https://github.com/IvanMalison/flimenu/issues/6 https://github.com/bmag/imenu-list/issues/58 So it's best to remove this problematic feature for now. It can be added back later. * eglot.el (eglot-imenu): Simplify. * NEWS.md: Mention change diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8088490363..2ac9b0dff6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2916,39 +2916,37 @@ for which LSP on-type-formatting should be requested." nil))) (defun eglot-imenu () - "EGLOT's `imenu-create-index-function'." + "EGLOT's `imenu-create-index-function'. +Returns a list as described in docstring of `imenu--index-alist'." (cl-labels - ((visit (_name one-obj-array) - (imenu-default-goto-function - nil (car (eglot--range-region - (eglot--dcase (aref one-obj-array 0) - (((SymbolInformation) location) - (plist-get location :range)) - (((DocumentSymbol) selectionRange) - selectionRange)))))) - (unfurl (obj) - (eglot--dcase obj - (((SymbolInformation)) (list obj)) - (((DocumentSymbol) name children) - (cons obj - (mapcar - (lambda (c) - (plist-put - c :containerName - (let ((existing (plist-get c :containerName))) - (if existing (format "%s::%s" name existing) - name)))) - (mapcan #'unfurl children))))))) + ((unfurl (obj) + (eglot--dcase obj + (((SymbolInformation)) (list obj)) + (((DocumentSymbol) name children) + (cons obj + (mapcar + (lambda (c) + (plist-put + c :containerName + (let ((existing (plist-get c :containerName))) + (if existing (format "%s::%s" name existing) + name)))) + (mapcan #'unfurl children))))))) (mapcar (pcase-lambda (`(,kind . ,objs)) (cons (alist-get kind eglot--symbol-kind-names "Unknown") (mapcan (pcase-lambda (`(,container . ,objs)) - (let ((elems (mapcar (lambda (obj) - (list (plist-get obj :name) - `[,obj] ;; trick - #'visit)) - objs))) + (let ((elems (mapcar + (lambda (obj) + (cons (plist-get obj :name) + (car (eglot--range-region + (eglot--dcase obj + (((SymbolInformation) location) + (plist-get location :range)) + (((DocumentSymbol) selectionRange) + selectionRange)))))) + objs))) (if container (list (cons container elems)) elems))) (seq-group-by (lambda (e) (plist-get e :containerName)) objs)))) commit e5b021c01fceea02b7e6622cde0a347b842ca6f3 Author: Manuel Uberti Date: Thu Sep 8 10:36:56 2022 +0000 Fix jdtls support PR https://github.com/joaotavora/eglot/issues/1026 * eglot.el (eglot-server-programs): Add -data setup for java-mode. GitHub-reference: per https://github.com/joaotavora/eglot/issues/1008 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a04a4f762d..8088490363 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -173,7 +173,7 @@ language-server/bin/php-language-server.php")) (go-mode . ("gopls")) ((R-mode ess-r-mode) . ("R" "--slave" "-e" "languageserver::run()")) - (java-mode . ("jdtls")) + (java-mode . ("jdtls" "-data" ".jdtls-cache")) (dart-mode . ("dart" "language-server" "--client-id" "emacs.eglot-dart")) (elixir-mode . ("language_server.sh")) commit 6ee995fe6bea311147b92aa35051e9a7574fa2a9 Author: Artem Pyanykh Date: Thu Sep 8 11:36:07 2022 +0100 Add marksman server for markdown * eglot.el (eglot-server-programs): Update. * README (Connecting to a server): Add marksman. * NEWS.md: Mention change. Copyright-paperwork-exempt: yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/1013 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 14e7980d38..a04a4f762d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -197,7 +197,8 @@ language-server/bin/php-language-server.php")) (clojure-mode . ("clojure-lsp")) (csharp-mode . ("omnisharp" "-lsp")) (purescript-mode . ("purescript-language-server" "--stdio")) - (perl-mode . ("perl" "-MPerl::LanguageServer" "-e" "Perl::LanguageServer::run"))) + (perl-mode . ("perl" "-MPerl::LanguageServer" "-e" "Perl::LanguageServer::run")) + (markdown-mode . ("marksman" "server"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE identifies the buffers that are to be managed by a specific commit e74e19991213d1298afb22be6af356c91de2a87a Author: Christian Garbs Date: Tue Jul 26 16:05:46 2022 +0200 Add out-of-box support for perl lsp server * eglot.el (eglot-server-programs): Support Perl lsp. * README.md: Update. * NEWS.md: Update. Co-authored-by: João Távora GitHub-reference: close https://github.com/joaotavora/eglot/issues/952 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2e332c470f..14e7980d38 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -196,7 +196,8 @@ language-server/bin/php-language-server.php")) (dockerfile-mode . ("docker-langserver" "--stdio")) (clojure-mode . ("clojure-lsp")) (csharp-mode . ("omnisharp" "-lsp")) - (purescript-mode . ("purescript-language-server" "--stdio"))) + (purescript-mode . ("purescript-language-server" "--stdio")) + (perl-mode . ("perl" "-MPerl::LanguageServer" "-e" "Perl::LanguageServer::run"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE identifies the buffers that are to be managed by a specific commit 0e8a8697ba9cac9cbf2fb8077b80b2b6c4db00f0 Merge: cc5d1a5a72 917e8ffa31 Author: João Távora Date: Sun Jul 24 19:45:30 2022 +0100 Merge commit '7738854e09' to fix fallout of force-push in gnu elpa commit cc5d1a5a72ccceaecc6ef1c3eb905481802a3b03 Author: João Távora Date: Fri Jul 22 01:17:05 2022 +0100 Always default eglot-strict-mode to nil it's mostly useful for developers/debugger. It's better to have the latter remember to set it than users being hindered by it. See https://github.com/joaotavora/eglot/issues/131#issuecomment-1191997167 * eglot.el (eglot-strict-mode): default to nil. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f9a7d2d1e7..2e332c470f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -433,14 +433,12 @@ Here's what an element of this alist might look like: (Command ((:title . string) (:command . string)) (:arguments))")) (eval-and-compile - (defvar eglot-strict-mode (if load-file-name '() - '(disallow-non-standard-keys - ;; Uncomment these two for fun at - ;; compile-time or with flymake-mode. - ;; - ;; enforce-required-keys - ;; enforce-optional-keys - )) + (defvar eglot-strict-mode + '(;; Uncomment next lines for fun and debugging + ;; disallow-non-standard-keys + ;; enforce-required-keys + ;; enforce-optional-keys + ) "How strictly to check LSP interfaces at compile- and run-time. Value is a list of symbols (if the list is empty, no checks are commit b6e041a24be538758727462f042822a8a1d285c7 Author: João Távora Date: Tue Jul 19 17:50:35 2022 +0100 Fix embarrassing paren-matching blunder in eglot.el * eglot.el (eglot-handle-request workspace/applyEdit): Fix parens. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c3ef543e3d..f9a7d2d1e7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2048,7 +2048,7 @@ THINGS are either registrations or unregisterations (sic)." (cl-defmethod eglot-handle-request (_server (_method (eql workspace/applyEdit)) &key _label edit) "Handle server request workspace/applyEdit." - (eglot--apply-workspace-edit edit eglot-confirm-server-initiated-edits)) + (eglot--apply-workspace-edit edit eglot-confirm-server-initiated-edits) `(:applied t)) (cl-defmethod eglot-handle-request commit 1986c4df88400a319362c87c616a44d0259f12fe Author: João Távora Date: Tue Jul 19 13:57:52 2022 +0100 Reply more reasonably to server's workspace/applyedit * eglot.el (eglot-handle-request): Return non-nil (eglot--apply-workspace-edit): Signal jsonrpc-error, not error. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index caebced529..c3ef543e3d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2049,6 +2049,7 @@ THINGS are either registrations or unregisterations (sic)." (_server (_method (eql workspace/applyEdit)) &key _label edit) "Handle server request workspace/applyEdit." (eglot--apply-workspace-edit edit eglot-confirm-server-initiated-edits)) + `(:applied t)) (cl-defmethod eglot-handle-request (server (_method (eql workspace/workspaceFolders))) @@ -3025,7 +3026,7 @@ for which LSP on-type-formatting should be requested." (unless (y-or-n-p (format "[eglot] Server wants to edit:\n %s\n Proceed? " (mapconcat #'identity (mapcar #'car prepared) "\n "))) - (eglot--error "User cancelled server edit"))) + (jsonrpc-error "User cancelled server edit"))) (cl-loop for edit in prepared for (path edits version) = edit do (with-current-buffer (find-file-noselect path) commit 3c6356b037d0046f23abad9a4c2f1ccdec65e585 Author: João Távora Date: Tue Jul 19 13:55:59 2022 +0100 Appease byte-compiler warnings about wrong use of quotes * eglot.el (eglot-stay-out-of, eglot--code-action): Just give it what it wants. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 582ad1fdee..caebced529 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1596,9 +1596,8 @@ However, if you wish for Eglot to stay out of a particular Emacs facility that you'd like to keep control of add an element to this list and Eglot will refrain from setting it. -For example, to keep your Company customization use - -(add-to-list 'eglot-stay-out-of 'company)") +For example, to keep your Company customization, add the symbol +`company' to this variable.") (defun eglot--stay-out-of-p (symbol) "Tell if EGLOT should stay of of SYMBOL." @@ -3122,7 +3121,7 @@ at point. With prefix argument, prompt for ACTION-KIND." (defmacro eglot--code-action (name kind) "Define NAME to execute KIND code action." `(defun ,name (beg &optional end) - ,(format "Execute '%s' code actions between BEG and END." kind) + ,(format "Execute `%s' code actions between BEG and END." kind) (interactive (eglot--region-bounds)) (eglot-code-actions beg end ,kind))) commit 2a12f622dcd69ae7cd2457c8f1fff2b7912dc47d Author: João Távora Date: Mon Jul 18 21:23:32 2022 +0100 Eglot-workspace-configuration can be a function * README.md (Workspace configuration): Renamed from per-project configuration. Rework. * NEWS.md: Mention change. * eglot.el (eglot-workspace-configuration): Overhaul. (eglot-signal-didChangeConfiguration): Use new eglot-workspace-configuration. GitHub-reference: per https://github.com/joaotavora/eglot/issues/967 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0b64cd2301..582ad1fdee 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2199,12 +2199,22 @@ Records BEG, END and PRE-CHANGE-LENGTH locally." (defvar-local eglot-workspace-configuration () "Alist of (SECTION . VALUE) entries configuring the LSP server. -SECTION should be a keyword or a string, value can be anything -that can be converted to JSON.") +SECTION should be a keyword or a string. VALUE is a +plist or a primitive type converted to JSON. + +The value of this variable can also be a unary function of a +`eglot-lsp-server' instance, the server connection requesting the +configuration. It should return an alist of the format described +above.") ;;;###autoload (put 'eglot-workspace-configuration 'safe-local-variable 'listp) +(defun eglot--workspace-configuration (server) + (if (functionp eglot-workspace-configuration) + (funcall eglot-workspace-configuration server) + eglot-workspace-configuration)) + (defun eglot-signal-didChangeConfiguration (server) "Send a `:workspace/didChangeConfiguration' signal to SERVER. When called interactively, use the currently active server" @@ -2213,7 +2223,7 @@ When called interactively, use the currently active server" server :workspace/didChangeConfiguration (list :settings - (or (cl-loop for (section . v) in eglot-workspace-configuration + (or (cl-loop for (section . v) in (eglot--workspace-configuration server) collect (if (keywordp section) section (intern (format ":%s" section))) @@ -2235,7 +2245,7 @@ When called interactively, use the currently active server" (project-root (eglot--project server))))) (setq-local major-mode (eglot--major-mode server)) (hack-dir-local-variables-non-file-buffer) - (alist-get section eglot-workspace-configuration + (alist-get section (eglot--workspace-configuration server) nil nil (lambda (wsection section) (string= commit 6717589c57edb96c7050df7d33cfcdc805a0eaec Author: João Távora Date: Fri Jul 15 12:58:47 2022 +0100 Be more conservative with the lsp identifier guess If the user is not requesting a prompt, opt for the safer approach which is to get the location from textDocument/definition, not from workspace/symbol. Because of things like function overloading, the latter is not always successful in finding exactly the definition of the thing one is invoking M-. on. This requires using an xref-internal symbol, which is kind of unfortunate. * eglot.el (xref-backend-identifier-at-point): Rework. GitHub-reference: per https://github.com/joaotavora/eglot/issues/131 GitHub-reference: per https://github.com/joaotavora/eglot/issues/314 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 22eff41f53..0b64cd2301 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2458,10 +2458,11 @@ If BUFFER, switch to it before." (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) (let ((attempt - (puthash :default - (ignore-errors - (eglot--workspace-symbols (symbol-name (symbol-at-point)))) - eglot--workspace-symbols-cache))) + (and (xref--prompt-p this-command) + (puthash :default + (ignore-errors + (eglot--workspace-symbols (symbol-name (symbol-at-point)))) + eglot--workspace-symbols-cache)))) (if attempt (car attempt) "LSP identifier at point"))) (defvar eglot--lsp-xref-refs nil commit b931d93b1549d41eb11a61724e339a4a34b317d3 Author: João Távora Date: Fri Jul 15 12:01:44 2022 +0100 Guess the "lsp identifier at point" * eglot.el (eglot--workspace-symbols): New helper. (xref-backend-identifier-completion-table): Rework. (xref-backend-identifier-at-point): Rework. GitHub-reference: per https://github.com/joaotavora/eglot/issues/131 GitHub-reference: per https://github.com/joaotavora/eglot/issues/314 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6f9c4f50f2..22eff41f53 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2396,52 +2396,52 @@ Try to visit the target file for a richer summary line." (eglot--current-server-or-lose)) (xref-make-match summary (xref-make-file-location file line column) length))) +(defun eglot--workspace-symbols (pat &optional buffer) + "Ask for :workspace/symbol on PAT, return list of formatted strings. +If BUFFER, switch to it before." + (with-current-buffer (or buffer (current-buffer)) + (unless (eglot--server-capable :workspaceSymbolProvider) + (eglot--error "This LSP server isn't a :workspaceSymbolProvider")) + (mapcar + (lambda (wss) + (eglot--dbind ((WorkspaceSymbol) name containerName kind) wss + (propertize + (format "%s%s %s" + (if (zerop (length containerName)) "" + (concat (propertize containerName 'face 'shadow) " ")) + name + (propertize (alist-get kind eglot--symbol-kind-names "Unknown") + 'face 'shadow)) + 'eglot--lsp-workspaceSymbol wss))) + (jsonrpc-request (eglot--current-server-or-lose) :workspace/symbol + `(:query ,pat))))) + (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) - (if (eglot--server-capable :workspaceSymbolProvider) - (let ((buf (current-buffer))) - (clrhash eglot--workspace-symbols-cache) - (cl-labels ((refresh (pat) - (mapcar - (lambda (wss) - (eglot--dbind - ((WorkspaceSymbol) name containerName kind) wss - (propertize - (format "%s%s %s" - (if (zerop (length containerName)) "" - (concat (propertize containerName - 'face 'shadow) - " ")) - name - (propertize (alist-get - kind - eglot--symbol-kind-names - "Unknown") - 'face 'shadow)) - 'eglot--lsp-workspaceSymbol wss))) - (with-current-buffer buf - (jsonrpc-request (eglot--current-server-or-lose) - :workspace/symbol - `(:query ,pat))))) - (lookup (pat) ;; check cache, else refresh - (let* ((cache eglot--workspace-symbols-cache) - (probe (gethash pat cache :missing))) - (if (eq probe :missing) (puthash pat (refresh pat) cache) - probe))) - (score (c) - (cl-getf (get-text-property - 0 'eglot--lsp-workspaceSymbol c) - :score 0))) - (lambda (string _pred action) - (pcase action - (`metadata `(metadata - (cycle-sort-function - . ,(lambda (completions) - (cl-sort completions #'> :key #'score))) - (category . eglot-indirection-joy))) - (`(eglot--lsp-tryc . ,point) `(eglot--lsp-tryc . (,string . ,point))) - (`(eglot--lsp-allc . ,_point) `(eglot--lsp-allc . ,(lookup string))) - (_ nil))))) - (eglot--error "This LSP server isn't a :workspaceSymbolProvider"))) + "Yet another tricky connection between LSP and Elisp completion semantics." + (let ((buf (current-buffer)) (cache eglot--workspace-symbols-cache)) + (cl-labels ((refresh (pat) (eglot--workspace-symbols pat buf)) + (lookup-1 (pat) ;; check cache, else refresh + (let ((probe (gethash pat cache :missing))) + (if (eq probe :missing) (puthash pat (refresh pat) cache) + probe))) + (lookup (pat) + (let ((res (lookup-1 pat)) + (def (and (string= pat "") (gethash :default cache)))) + (append def res nil))) + (score (c) + (cl-getf (get-text-property + 0 'eglot--lsp-workspaceSymbol c) + :score 0))) + (lambda (string _pred action) + (pcase action + (`metadata `(metadata + (cycle-sort-function + . ,(lambda (completions) + (cl-sort completions #'> :key #'score))) + (category . eglot-indirection-joy))) + (`(eglot--lsp-tryc . ,point) `(eglot--lsp-tryc . (,string . ,point))) + (`(eglot--lsp-allc . ,_point) `(eglot--lsp-allc . ,(lookup string))) + (_ nil)))))) (defun eglot--recover-workspace-symbol-meta (string) "Search `eglot--workspace-symbols-cache' for rich entry of STRING." @@ -2457,11 +2457,12 @@ Try to visit the target file for a richer summary line." '(eglot-indirection-joy (styles . (eglot--lsp-backend-style)))) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) - ;; JT@19/10/09: This is a totally dummy identifier that isn't even - ;; passed to LSP. The reason for this particular wording is to - ;; construct a readable message "No references for LSP identifier at - ;; point.". See https://github.com/joaotavora/eglot/issues/314 - "LSP identifier at point") + (let ((attempt + (puthash :default + (ignore-errors + (eglot--workspace-symbols (symbol-name (symbol-at-point)))) + eglot--workspace-symbols-cache))) + (if attempt (car attempt) "LSP identifier at point"))) (defvar eglot--lsp-xref-refs nil "`xref' objects for overriding `xref-backend-references''s.") commit 9dbc18cbfa5c838453a2036c8d37c673bbc8de1f Author: João Távora Date: Fri Jul 15 10:25:55 2022 +0100 Tweak some details, fix some bugs eglot--recover-workspace-symbol-meta had a bug that still made it choke on improper lists. Also, when simply M-. to the thing at point, let's not lose time on iterating a potentially out-of-date eglot--workspace-symbols-cache. So clear it early in the pre-command-hook. * eglot.el (eglot--workspace-symbols-cache): Move up. (eglot--pre-command-hook): Clear eglot--workspace-symbols-cache here. (eglot--recover-workspace-symbol-meta): Check for consp. GitHub-reference: per https://github.com/joaotavora/eglot/issues/131 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6d3667a84a..6f9c4f50f2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2101,8 +2101,12 @@ THINGS are either registrations or unregisterations (sic)." :key #'seq-first)))) (eglot-format (point) nil last-input-event)))) +(defvar eglot--workspace-symbols-cache (make-hash-table :test #'equal) + "Cache of `workspace/Symbol' results used by `xref-find-definitions'.") + (defun eglot--pre-command-hook () "Reset some temporary variables." + (clrhash eglot--workspace-symbols-cache) (setq eglot--last-inserted-char nil)) (defun eglot--CompletionParams () @@ -2392,9 +2396,6 @@ Try to visit the target file for a richer summary line." (eglot--current-server-or-lose)) (xref-make-match summary (xref-make-file-location file line column) length))) -(defvar eglot--workspace-symbols-cache (make-hash-table :test #'equal) - "Cache of `workspace/Symbol' results used by `xref-find-definitions'.") - (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) (if (eglot--server-capable :workspaceSymbolProvider) (let ((buf (current-buffer))) @@ -2446,10 +2447,10 @@ Try to visit the target file for a richer summary line." "Search `eglot--workspace-symbols-cache' for rich entry of STRING." (catch 'found (maphash (lambda (_k v) - (while v + (while (consp v) ;; Like mess? Ask minibuffer.el about improper lists. (when (equal (car v) string) (throw 'found (car v))) - (setq v (and (consp v) (cdr v))))) + (setq v (cdr v)))) eglot--workspace-symbols-cache))) (add-to-list 'completion-category-overrides commit b59fa2548e5ec86c3f439fd59ad46abf8840a8ea Author: João Távora Date: Thu Jul 14 10:09:27 2022 +0100 Cosmetic decisions guaranteed to tick off someone somewhere (tm) The symbols returned by the LSP server must be converted to unique strings if Emacs is to present them in a list. On the other hand, the search operates on the pattern and is completely controlled by the backend. There is not much Eglot, the LSP client, can do about this. Decided to present the unique string to the user, even though it could be hidden. All the manner of :annotation-function, :affixation-function, :group-funcion etc didn't seem to add much value. Grouping was especially useless, since it makes sense to respect the LSP server's account of sorting score, so that better results bubble up to the top. * eglot.el (xref-backend-identifier-completion-table): Uniquify symbols with containerName and kind. GitHub-reference: per https://github.com/joaotavora/eglot/issues/131 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index eccd67c129..6d3667a84a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2402,11 +2402,20 @@ Try to visit the target file for a richer summary line." (cl-labels ((refresh (pat) (mapcar (lambda (wss) - (eglot--dbind ((WorkspaceSymbol) name containerName) wss + (eglot--dbind + ((WorkspaceSymbol) name containerName kind) wss (propertize - (concat (and (not (zerop (length containerName))) - (format "%s::" containerName)) - name) + (format "%s%s %s" + (if (zerop (length containerName)) "" + (concat (propertize containerName + 'face 'shadow) + " ")) + name + (propertize (alist-get + kind + eglot--symbol-kind-names + "Unknown") + 'face 'shadow)) 'eglot--lsp-workspaceSymbol wss))) (with-current-buffer buf (jsonrpc-request (eglot--current-server-or-lose) @@ -2417,41 +2426,17 @@ Try to visit the target file for a richer summary line." (probe (gethash pat cache :missing))) (if (eq probe :missing) (puthash pat (refresh pat) cache) probe))) - (container (c) - (plist-get (get-text-property - 0 'eglot--lsp-workspaceSymbol c) - :containerName))) + (score (c) + (cl-getf (get-text-property + 0 'eglot--lsp-workspaceSymbol c) + :score 0))) (lambda (string _pred action) (pcase action (`metadata `(metadata (cycle-sort-function . ,(lambda (completions) - (cl-sort completions - #'string-lessp - :key (lambda (c) - (or (container c) - ""))))) - (category . eglot-indirection-joy) - ;; (annotation-function - ;; . ,(lambda (c) - ;; (plist-get (get-text-property - ;; 0 'eglot--lsp-workspaceSymbol c) - ;; :containerName))) - ;; (affixation-function - ;; . ,(lambda (comps) - ;; (mapcar (lambda (c) - ;; (list c - ;; (plist-get (get-text-property - ;; 0 'eglot--lsp-workspaceSymbol c) - ;; :containerName) - ;; " bla")) - ;; comps))) - (group-function - . ,(lambda (c transformp) - (if (not transformp) - (container c) - c))) - )) + (cl-sort completions #'> :key #'score))) + (category . eglot-indirection-joy))) (`(eglot--lsp-tryc . ,point) `(eglot--lsp-tryc . (,string . ,point))) (`(eglot--lsp-allc . ,_point) `(eglot--lsp-allc . ,(lookup string))) (_ nil))))) @@ -2475,7 +2460,7 @@ Try to visit the target file for a richer summary line." ;; passed to LSP. The reason for this particular wording is to ;; construct a readable message "No references for LSP identifier at ;; point.". See https://github.com/joaotavora/eglot/issues/314 - "LSP identifier at point.") + "LSP identifier at point") (defvar eglot--lsp-xref-refs nil "`xref' objects for overriding `xref-backend-references''s.") commit e72fa6d86764027d1c071b73cc83aef8d4d344de Author: João Távora Date: Wed Jul 13 16:42:10 2022 +0100 Experiment with grouping in xref-backend-identifier-completion-table Doesn't look very good. * eglot.el (xref-backend-identifier-completion-table): Add stuff. GitHub-reference: per https://github.com/joaotavora/eglot/issues/131 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b17bfd1b5c..eccd67c129 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2416,12 +2416,42 @@ Try to visit the target file for a richer summary line." (let* ((cache eglot--workspace-symbols-cache) (probe (gethash pat cache :missing))) (if (eq probe :missing) (puthash pat (refresh pat) cache) - probe)))) + probe))) + (container (c) + (plist-get (get-text-property + 0 'eglot--lsp-workspaceSymbol c) + :containerName))) (lambda (string _pred action) (pcase action - (`metadata '(metadata - (display-sort-function . identity) - (category . eglot-indirection-joy))) + (`metadata `(metadata + (cycle-sort-function + . ,(lambda (completions) + (cl-sort completions + #'string-lessp + :key (lambda (c) + (or (container c) + ""))))) + (category . eglot-indirection-joy) + ;; (annotation-function + ;; . ,(lambda (c) + ;; (plist-get (get-text-property + ;; 0 'eglot--lsp-workspaceSymbol c) + ;; :containerName))) + ;; (affixation-function + ;; . ,(lambda (comps) + ;; (mapcar (lambda (c) + ;; (list c + ;; (plist-get (get-text-property + ;; 0 'eglot--lsp-workspaceSymbol c) + ;; :containerName) + ;; " bla")) + ;; comps))) + (group-function + . ,(lambda (c transformp) + (if (not transformp) + (container c) + c))) + )) (`(eglot--lsp-tryc . ,point) `(eglot--lsp-tryc . (,string . ,point))) (`(eglot--lsp-allc . ,_point) `(eglot--lsp-allc . ,(lookup string))) (_ nil))))) commit fd5a5f16d7bb575b0b0323b54028d6a667767519 Author: João Távora Date: Wed Jul 13 00:44:32 2022 +0100 Make c-u m-. work half decently * NEWS.md: Mention change. * eglot.el (eglot--lsp-interface-alist): Add WorkspaceSymbol (eglot--workspace-symbols-cache): New variable. (eglot--recover-workspace-meta): New helper. (xref-backend-identifier-completion-table): Complicate. (xref-backend-definitions): Complicate. (completion-category-overrides): Register a category and a style here. (completion-styles-alist): Add eglot--lsp-backend-style style (eglot--lsp-backend-style-call): New funtion. (eglot--lsp-backend-style-all-completions): New function. (eglot--lsp-backend-style-try-completion): New function. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/131 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1b9c997d25..b17bfd1b5c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -417,7 +417,7 @@ This can be useful when using docker to run a language server.") (TextEdit (:range :newText)) (VersionedTextDocumentIdentifier (:uri :version) ()) (WorkspaceEdit () (:changes :documentChanges)) - ) + (WorkspaceSymbol (:name :kind) (:containerName :location :data))) "Alist (INTERFACE-NAME . INTERFACE) of known external LSP interfaces. INTERFACE-NAME is a symbol designated by the spec as @@ -2102,7 +2102,7 @@ THINGS are either registrations or unregisterations (sic)." (eglot-format (point) nil last-input-event)))) (defun eglot--pre-command-hook () - "Reset `eglot--last-inserted-char'." + "Reset some temporary variables." (setq eglot--last-inserted-char nil)) (defun eglot--CompletionParams () @@ -2392,8 +2392,53 @@ Try to visit the target file for a richer summary line." (eglot--current-server-or-lose)) (xref-make-match summary (xref-make-file-location file line column) length))) +(defvar eglot--workspace-symbols-cache (make-hash-table :test #'equal) + "Cache of `workspace/Symbol' results used by `xref-find-definitions'.") + (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) - (eglot--error "Cannot (yet) provide reliable completion table for LSP symbols")) + (if (eglot--server-capable :workspaceSymbolProvider) + (let ((buf (current-buffer))) + (clrhash eglot--workspace-symbols-cache) + (cl-labels ((refresh (pat) + (mapcar + (lambda (wss) + (eglot--dbind ((WorkspaceSymbol) name containerName) wss + (propertize + (concat (and (not (zerop (length containerName))) + (format "%s::" containerName)) + name) + 'eglot--lsp-workspaceSymbol wss))) + (with-current-buffer buf + (jsonrpc-request (eglot--current-server-or-lose) + :workspace/symbol + `(:query ,pat))))) + (lookup (pat) ;; check cache, else refresh + (let* ((cache eglot--workspace-symbols-cache) + (probe (gethash pat cache :missing))) + (if (eq probe :missing) (puthash pat (refresh pat) cache) + probe)))) + (lambda (string _pred action) + (pcase action + (`metadata '(metadata + (display-sort-function . identity) + (category . eglot-indirection-joy))) + (`(eglot--lsp-tryc . ,point) `(eglot--lsp-tryc . (,string . ,point))) + (`(eglot--lsp-allc . ,_point) `(eglot--lsp-allc . ,(lookup string))) + (_ nil))))) + (eglot--error "This LSP server isn't a :workspaceSymbolProvider"))) + +(defun eglot--recover-workspace-symbol-meta (string) + "Search `eglot--workspace-symbols-cache' for rich entry of STRING." + (catch 'found + (maphash (lambda (_k v) + (while v + ;; Like mess? Ask minibuffer.el about improper lists. + (when (equal (car v) string) (throw 'found (car v))) + (setq v (and (consp v) (cdr v))))) + eglot--workspace-symbols-cache))) + +(add-to-list 'completion-category-overrides + '(eglot-indirection-joy (styles . (eglot--lsp-backend-style)))) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) ;; JT@19/10/09: This is a totally dummy identifier that isn't even @@ -2456,8 +2501,14 @@ Try to visit the target file for a richer summary line." (interactive) (eglot--lsp-xref-helper :textDocument/typeDefinition)) -(cl-defmethod xref-backend-definitions ((_backend (eql eglot)) _identifier) - (eglot--lsp-xrefs-for-method :textDocument/definition)) +(cl-defmethod xref-backend-definitions ((_backend (eql eglot)) id) + (let ((probe (eglot--recover-workspace-symbol-meta id))) + (if probe + (eglot--dbind ((WorkspaceSymbol) name location) + (get-text-property 0 'eglot--lsp-workspaceSymbol probe) + (eglot--dbind ((Location) uri range) location + (list (eglot--xref-make-match name uri range)))) + (eglot--lsp-xrefs-for-method :textDocument/definition)))) (cl-defmethod xref-backend-references ((_backend (eql eglot)) _identifier) (or @@ -3188,6 +3239,43 @@ If NOERROR, return predicate, else erroring function." 'eglot-managed-mode-hook "1.6") (provide 'eglot) + +;;; Backend completion + +;; Written by Stefan Monnier circa 2016. Something to move to +;; minibuffer.el "ASAP" (with all the `eglot--lsp-' replaced by +;; something else. The very same code already in SLY and stable for a +;; long time. + +;; This "completion style" delegates all the work to the "programmable +;; completion" table which is then free to implement its own +;; completion style. Typically this is used to take advantage of some +;; external tool which already has its own completion system and +;; doesn't give you efficient access to the prefix completion needed +;; by other completion styles. The table should recognize the symbols +;; 'eglot--lsp-tryc and 'eglot--lsp-allc as ACTION, reply with +;; (eglot--lsp-tryc COMP...) or (eglot--lsp-allc . (STRING . POINT)), +;; accordingly. tryc/allc names made akward/recognizable on purpose. + +(add-to-list 'completion-styles-alist + '(eglot--lsp-backend-style + eglot--lsp-backend-style-try-completion + eglot--lsp-backend-style-all-completions + "Ad-hoc completion style provided by the completion table.")) + +(defun eglot--lsp-backend-style-call (op string table pred point) + (when (functionp table) + (let ((res (funcall table string pred (cons op point)))) + (when (eq op (car-safe res)) + (cdr res))))) + +(defun eglot--lsp-backend-style-try-completion (string table pred point) + (eglot--lsp-backend-style-call 'eglot--lsp-tryc string table pred point)) + +(defun eglot--lsp-backend-style-all-completions (string table pred point) + (eglot--lsp-backend-style-call 'eglot--lsp-allc string table pred point)) + + ;; Local Variables: ;; bug-reference-bug-regexp: "\\(github#\\([0-9]+\\)\\)" ;; bug-reference-url-format: "https://github.com/joaotavora/eglot/issues/%s" commit 917e8ffa314600b0736ec7117f24ca34bec4e7db Author: jgart <47760695+jgarte@users.noreply.github.com> Date: Fri Jul 8 19:16:29 2022 -0500 Add support for jedi-language-server * eglot.el (eglot-server-programs): Add jedi-language-server * README.md: Mention jedi-language-server * NEWS.md: Mention jedi-language-server Copyright-paperwork-exempt: yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/961 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1b9c997d25..ebbadea801 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -151,7 +151,7 @@ chosen (interactively or automatically)." (vimrc-mode . ("vim-language-server" "--stdio")) (python-mode . ,(eglot-alternatives - '("pylsp" "pyls" ("pyright-langserver" "--stdio")))) + '("pylsp" "pyls" ("pyright-langserver" "--stdio") "jedi-language-server"))) ((js-mode typescript-mode) . ("typescript-language-server" "--stdio")) (sh-mode . ("bash-language-server" "start")) commit 6c8aee268d81ef616169d79ea5bd0331aebc25ee Author: João Távora Date: Thu Jul 7 12:30:03 2022 +0100 Prevent desktop.el from saving/restoring eglot--managed-mode Although desktop.el compatibility is Emacs bughttps://github.com/joaotavora/eglot/issues/56407, the optimal solution agreed to there is a bit more work than what I have time to right now. See e.g. https://debbugs.gnu.org/cgi/bugreport.cgi?bug=bug%2356407https://github.com/joaotavora/eglot/issues/68. For now, just use `with-eval-after-load' * eglot.el (Hacks desktop): Add eglot--managed-mode to desktop-minor-mode-handlers GitHub-reference: fix https://github.com/joaotavora/eglot/issues/990 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index eb5b86ed10..1b9c997d25 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -3169,6 +3169,17 @@ If NOERROR, return predicate, else erroring function." (when (eq ?! (aref arg 1)) (aset arg 1 ?^)) `(,self () (re-search-forward ,(concat "\\=" arg)) (,next))) + +;;; Hacks +;;; +;; FIXME: Although desktop.el compatibility is Emacs bug#56407, the +;; optimal solution agreed to there is a bit more work than what I +;; have time to right now. See +;; e.g. https://debbugs.gnu.org/cgi/bugreport.cgi?bug=bug%2356407#68. +;; For now, just use `with-eval-after-load' +(with-eval-after-load 'desktop + (add-to-list 'desktop-minor-mode-handlers '(eglot--managed-mode . ignore))) + ;;; Obsolete ;;; commit 9ffcd537f82ecb05996c49014c5b72009134a927 Author: João Távora Date: Fri Jun 24 10:35:07 2022 +0100 Apply any additionaltextedits unconditionally * eglot.el (eglot-completion-at-point): Apply any additionalTextEdits unconditionally. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/981 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b058183fb9..eb5b86ed10 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2698,16 +2698,16 @@ for which LSP on-type-formatting should be requested." (eglot--range-region range))) (delete-region beg end) (goto-char beg) - (funcall (or snippet-fn #'insert) newText))) - (when (cl-plusp (length additionalTextEdits)) - (eglot--apply-text-edits additionalTextEdits))) + (funcall (or snippet-fn #'insert) newText)))) (snippet-fn ;; A snippet should be inserted, but using plain ;; `insertText'. This requires us to delete the ;; whole completion, since `insertText' is the full ;; completion's text. (delete-region (- (point) (length proxy)) (point)) - (funcall snippet-fn (or insertText label))))) + (funcall snippet-fn (or insertText label)))) + (when (cl-plusp (length additionalTextEdits)) + (eglot--apply-text-edits additionalTextEdits))) (eglot--signal-textDocument/didChange) (eldoc))))))))) commit f8c8c70f8a7ec5649b5dd3f5af559f59eff65724 Author: jicksaw Date: Thu Jun 30 10:39:33 2022 +0300 Reduce eldoc noise from hover messages Also close https://github.com/joaotavora/eglot/issues/985 Only echo hover response content, without response range. LSP specification says the range is meant to visualize a hover. Maybe echoing the range is useful for some, but it seems non-standard behavior. Example issue: haskell-language-server responds with range set to whole file when hovering a comment -> Large, useless eldoc * eglot.el (eglot--hover-info): Remove text selected by range from output Copyright-paperwork-exempt: Yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/514 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bde4a23f8e..b058183fb9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2711,13 +2711,10 @@ for which LSP on-type-formatting should be requested." (eglot--signal-textDocument/didChange) (eldoc))))))))) -(defun eglot--hover-info (contents &optional range) - (let ((heading (and range (pcase-let ((`(,beg . ,end) (eglot--range-region range))) - (concat (buffer-substring beg end) ": ")))) - (body (mapconcat #'eglot--format-markup - (if (vectorp contents) contents (list contents)) "\n"))) - (when (or heading (cl-plusp (length body))) (concat heading body)))) - +(defun eglot--hover-info (contents &optional _range) + (mapconcat #'eglot--format-markup + (if (vectorp contents) contents (list contents)) "\n")) + (defun eglot--sig-info (sigs active-sig sig-help-active-param) (cl-loop for (sig . moresigs) on (append sigs nil) for i from 0 commit 68fbcbd6207819b9d653362e66236ea5e04e9b7d Author: Basil L. Contovounesios Date: Fri Jun 24 12:39:02 2022 +0300 Reduce memory footprint of eglot--{} * eglot.el (eglot--{}): Specify smallest hash table :size, to spare ~1KiB according to memory-report-object-size. See also https://github.com/joaotavora/eglot/pull/315. GitHub-reference: per https://github.com/joaotavora/eglot/issues/978 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 53ae6fae3c..bde4a23f8e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -368,7 +368,7 @@ This can be useful when using docker to run a language server.") `((1 . eglot-diagnostic-tag-unnecessary-face) (2 . eglot-diagnostic-tag-deprecated-face))) -(defconst eglot--{} (make-hash-table) "The empty JSON object.") +(defconst eglot--{} (make-hash-table :size 1) "The empty JSON object.") (defun eglot--executable-find (command &optional remote) "Like Emacs 27's `executable-find', ignore REMOTE on Emacs 26." commit f8344871a0159f2550fda3c68207219f1513e1f8 Author: Yuan Fu Date: Sun Jun 12 03:04:53 2022 -0700 Update docstring of eglot-events-buffer-size * eglot.el (eglot-events-buffer-size): Mention that you need to restart the connection for 'eglot-events-buffer-size' to take effect. GitHub-reference: close https://github.com/joaotavora/eglot/issues/974 GitHub-reference: close https://github.com/joaotavora/eglot/issues/776 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4e28de1875..53ae6fae3c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -312,7 +312,11 @@ as 0, i.e. don't block at all." "Control the size of the Eglot events buffer. If a number, don't let the buffer grow larger than that many characters. If 0, don't use an event's buffer at all. If nil, -let the buffer grow forever." +let the buffer grow forever. + +For changes on this variable to take effect on a connection +already started, you need to restart the connection. That can be +done by `eglot-reconnect'." :type '(choice (const :tag "No limit" nil) (integer :tag "Number of characters"))) commit 5c6eb3caa903ad1479b3e901cee4b845bada1f1e Author: João Távora Date: Thu May 19 09:59:55 2022 +0100 Don't ignore flymake-no-changes-timeout Also per https://github.com/joaotavora/eglot/issues/957. Only actually and eagerly report LSP diagnotics if the user has Flymake starting automatically on a timer (flymake-no-changes-timeout is a number). By contrast, if flymake-no-changes-timeout is nil, the user starts the diagnostic collection process on-demand via 'M-x flymake-start'. Since the control of such collection is impossible with LSP, we should just hold on to whatever diagnostics we have (which are presumably up-to-date) until the next invocation of 'eglot-flymake-backend'. For now, this doesn't affect Flymake "list-only" diagnostics. Those are reported via the 'flymake-list-only-diagonstics' variable and are always communicated immediately to it. * eglot.el: (eglot-handle-notification textDocument/publishDiagnostics): Consult flymake-no-changes-timeout. Suggested-by: Jim Davis GitHub-reference: fix https://github.com/joaotavora/eglot/issues/508 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e8f060cd64..4e28de1875 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1996,7 +1996,11 @@ COMMAND is a symbol naming the command." collect it))) `((face . ,faces)))))) into diags - finally (cond (eglot--current-flymake-report-fn + finally (cond ((and + ;; only add to current report if Flymake + ;; starts on idle-timer (github#958) + (not (null flymake-no-changes-timeout)) + eglot--current-flymake-report-fn) (eglot--report-to-flymake diags)) (t (setq eglot--diagnostics diags))))) commit 50ff73d753708467621d6ce8495a5d78dc31ae63 Author: Theodor Thornhill Date: Mon May 9 21:04:12 2022 +0200 Use format string instead of concat * eglot.el (eglot-handle-notification): Because diagnostics code can be integer or string, and integer fails the sequencep test, use format to create this string. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/948 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3e3eb3c543..e8f060cd64 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1959,7 +1959,7 @@ COMMAND is a symbol naming the command." ((= sev 2) 'eglot-warning) (t 'eglot-note))) (mess (source code message) - (concat source (and code (concat " [" code "]")) ": " message))) + (concat source (and code (format " [%s]" code)) ": " message))) (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) (with-current-buffer buffer (cl-loop commit 46a480aa8882d4febc2f8eea42dbd84dff18c0d3 Author: João Távora Date: Mon May 9 01:17:58 2022 +0100 Fix egregious thinko in eglot--uri-to-path One shouldn't unhex the URI before parsing it. Just consider a filename with a # character in it. The character is encoded as C%23, after unhexing the file name becomes. /tmp/C#/Program.cs Now, parsing this as the URL will fail completely as the # mean "anchor" in URLs. * eglot.el (eglot--uri-to-path): Fix thinko. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3a33ad3ec2..3e3eb3c543 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1440,7 +1440,7 @@ If optional MARKER, return a marker instead" (when (keywordp uri) (setq uri (substring (symbol-name uri) 1))) (let* ((server (eglot-current-server)) (remote-prefix (and server (eglot--trampish-p server))) - (retval (url-filename (url-generic-parse-url (url-unhex-string uri)))) + (retval (url-unhex-string (url-filename (url-generic-parse-url uri)))) ;; Remove the leading "/" for local MS Windows-style paths. (normalized (if (and (not remote-prefix) (eq system-type 'windows-nt) commit 29f2ec24713984d561881980aef578faa2a83068 Author: João Távora Date: Wed May 4 21:47:21 2022 +0100 Consider diagnostic.code when generating flymake diagnostics Not sure this will please everybody, can almost guess someone is going to ask for a custom switch. Instead this info (and the source) should be passed on to Flymake. That's where the custom switch for controlling formatting of diagnostic messages should exist. But that's too much work right now. * eglot.el (eglot-handle-notification): Consider Diagnostic.code. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 81c545e64f..3a33ad3ec2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1957,14 +1957,16 @@ COMMAND is a symbol naming the command." (cond ((null sev) 'eglot-error) ((<= sev 1) 'eglot-error) ((= sev 2) 'eglot-warning) - (t 'eglot-note)))) + (t 'eglot-note))) + (mess (source code message) + (concat source (and code (concat " [" code "]")) ": " message))) (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) (with-current-buffer buffer (cl-loop for diag-spec across diagnostics - collect (eglot--dbind ((Diagnostic) range message severity source tags) + collect (eglot--dbind ((Diagnostic) range code message severity source tags) diag-spec - (setq message (concat source ": " message)) + (setq message (mess source code message)) (pcase-let ((`(,beg . ,end) (eglot--range-region range))) ;; Fallback to `flymake-diag-region' if server @@ -2001,8 +2003,8 @@ COMMAND is a symbol naming the command." (cl-loop with path = (expand-file-name (eglot--uri-to-path uri)) for diag-spec across diagnostics - collect (eglot--dbind ((Diagnostic) range message severity source) diag-spec - (setq message (concat source ": " message)) + collect (eglot--dbind ((Diagnostic) code range message severity source) diag-spec + (setq message (mess source code message)) (let* ((start (plist-get range :start)) (line (1+ (plist-get start :line))) (char (1+ (plist-get start :character)))) commit 4beab004d98f17f830600b4597315f030a7313c3 Author: rbrtb <104695105+rbrtb@users.noreply.github.com> Date: Tue May 3 09:53:17 2022 +0000 Ensure exit-function of eglot-c-at-point runs on exact match When the completion is exact match, exit-function should still run. Say one is using auto-imports feature of pyright. One types foo, and triggers the completion. There are two candidates: foo and foo_bar. If one chooses foo, the status would be 'exact' instead of 'finished', thus exit-function is not executed, foo is not auto-imported. * eglot.el (eglot-completion-at-point): Consider 'exact status. Copyright-paperwork-exempt: Yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/941 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3d1b19c905..81c545e64f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2649,7 +2649,7 @@ for which LSP on-type-formatting should be requested." (line-beginning-position)))) :exit-function (lambda (proxy status) - (when (eq status 'finished) + (when (memq status '(finished exact)) ;; To assist in using this whole `completion-at-point' ;; function inside `completion-in-region', ensure the exit ;; function runs in the buffer where the completion was commit f5503420594e0e80a1552edacc8b85b7e29f4223 Author: Tomasz Hołubowicz <45176912+alternateved@users.noreply.github.com> Date: Wed Apr 27 13:48:47 2022 +0200 Add out-of-box support for purescript lsp server * eglot.el (eglot-server-programs): Support purescript lsp. * README.md: Update. * NEWS.md: Update. Copyright-paperwork-exempt: Yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/905 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fd82b76c5b..3d1b19c905 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -195,7 +195,8 @@ language-server/bin/php-language-server.php")) (json-mode . ,(eglot-alternatives '(("vscode-json-language-server" "--stdio") ("json-languageserver" "--stdio")))) (dockerfile-mode . ("docker-langserver" "--stdio")) (clojure-mode . ("clojure-lsp")) - (csharp-mode . ("omnisharp" "-lsp"))) + (csharp-mode . ("omnisharp" "-lsp")) + (purescript-mode . ("purescript-language-server" "--stdio"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE identifies the buffers that are to be managed by a specific commit 5e9d78f9cf1ec4da5f24ab32d038d0d67566fbc6 Author: João Távora Date: Wed Apr 27 11:11:52 2022 +0100 Ensure non-null :settings param in didchangeconfiguration notif * eglot.el (eglot-signal-didChangeConfiguration): Use eglot--{} GitHub-reference: fix https://github.com/joaotavora/eglot/issues/936 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 348ae4b42c..fd82b76c5b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2198,11 +2198,12 @@ When called interactively, use the currently active server" server :workspace/didChangeConfiguration (list :settings - (cl-loop for (section . v) in eglot-workspace-configuration - collect (if (keywordp section) - section - (intern (format ":%s" section))) - collect v)))) + (or (cl-loop for (section . v) in eglot-workspace-configuration + collect (if (keywordp section) + section + (intern (format ":%s" section))) + collect v) + eglot--{})))) (cl-defmethod eglot-handle-request (server (_method (eql workspace/configuration)) &key items) commit 73f4555a0dab8d5d516febe25162ca8af5aebfad Author: Troels Henriksen Date: Fri Apr 15 20:22:57 2022 +0200 Add out-of-box support for futhark lsp server * eglot.el (eglot-server-programs): Support futhark lsp. * README.md: Update. * NEWS.md: Update. Copyright-paperwork-exempt: yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/922 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 52f61e80f4..348ae4b42c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -187,6 +187,7 @@ language-server/bin/php-language-server.php")) (nix-mode . ("rnix-lsp")) (gdscript-mode . ("localhost" 6008)) ((fortran-mode f90-mode) . ("fortls")) + (futhark-mode . ("futhark" "lsp")) (lua-mode . ("lua-lsp")) (zig-mode . ("zls")) (css-mode . ,(eglot-alternatives '(("vscode-css-language-server" "--stdio") ("css-languageserver" "--stdio")))) commit 49e56e47d81c0cdfb219d38cb46bfcbdb1c503fd Author: João Távora Date: Wed Apr 6 11:08:12 2022 +0100 Solve flymake diagnostics synchronization problems A diagnostics-lazy server is one who doesn't re-report already reported diagnostics when it received textDocument/didSave. Such is the case of Clangd, for example. Before this change, saving an Eglot/Clang-managed buffer with some diagnostics caused the Flymake indicator to display Wait[0 0] until some change was actually done to the buffer. That is because Flymake, by default, wants diagnostics on buffer save, per flymake-start-on-save-buffer. But it doesn't work to simply turn that off. That's because if one types something and quickly saves, and the LSP diagnostics do come in after the save (for some reason, like server latency), then Flymake sometimes doesn't request any diagnostics at all. The reason for the Flymake behaviour wasn't investigated, but that wasn't a very good solution either Rather this change makes it so that when such a Flymake request comes in, it always gets served immediately with the latest information. The latest information is now always stored in eglot--diagnostics, with eglot--unreported-diagnotics being removed. The up-to-date list is reported to Flymake whenever it requests it. It is updated whenever the LSP server decides to. * eglot.el (eglot--last-reported-diagnostics): Delete. (eglot--unreported-diagnostics): Delete. (eglot--diagnostics): New variable.. (eglot--maybe-activate-editing-mode): Use eglot--diagnostics. (eglot-handle-notification): Set eglot--diaggnostics. (eglot-flymake-backend): Read eglot--diagnostics. Always report. (eglot--report-to-flymake): Set eglot--diagnostics. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d712b06e76..52f61e80f4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1701,8 +1701,8 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (or (eglot-current-server) (jsonrpc-error "No current JSON-RPC connection"))) -(defvar-local eglot--unreported-diagnostics nil - "Unreported Flymake diagnostics for this buffer.") +(defvar-local eglot--diagnostics nil + "Flymake diagnostics for this buffer.") (defvar revert-buffer-preserve-modes) (defun eglot--after-revert-hook () @@ -1717,7 +1717,7 @@ If it is activated, also signal textDocument/didOpen." ;; Called when `revert-buffer-in-progress-p' is t but ;; `revert-buffer-preserve-modes' is nil. (when (and buffer-file-name (eglot-current-server)) - (setq eglot--unreported-diagnostics `(:just-opened . nil)) + (setq eglot--diagnostics nil) (eglot--managed-mode) (eglot--signal-textDocument/didOpen)))) @@ -1995,7 +1995,7 @@ COMMAND is a symbol naming the command." finally (cond (eglot--current-flymake-report-fn (eglot--report-to-flymake diags)) (t - (setq eglot--unreported-diagnostics (cons t diags)))))) + (setq eglot--diagnostics diags))))) (cl-loop with path = (expand-file-name (eglot--uri-to-path uri)) for diag-spec across diagnostics @@ -2305,9 +2305,7 @@ may be called multiple times (respecting the protocol of `flymake-backend-functions')." (cond (eglot--managed-mode (setq eglot--current-flymake-report-fn report-fn) - ;; Report anything unreported - (when eglot--unreported-diagnostics - (eglot--report-to-flymake (cdr eglot--unreported-diagnostics)))) + (eglot--report-to-flymake eglot--diagnostics)) (t (funcall report-fn nil)))) @@ -2322,7 +2320,7 @@ may be called multiple times (respecting the protocol of ;; keyword forces flymake to delete ;; them (github#159). :region (cons (point-min) (point-max)))) - (setq eglot--unreported-diagnostics nil)) + (setq eglot--diagnostics diags)) (defun eglot-xref-backend () "EGLOT xref backend." 'eglot) commit 339ebe7ce467b47ce78404cfae28f29f1bf20ead Author: Billy.Zheng Date: Tue Apr 5 19:45:47 2022 +0800 Update invocation for out-of-box dart ls support * eglot.el (eglot-server-programs): Tweak dart-mode entry. * README.md: Tweak Dart entry. Co-authored-by: João Távora Copyright-paperwork-exempt: Yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/862 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c32560ad8f..d712b06e76 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -174,7 +174,8 @@ language-server/bin/php-language-server.php")) ((R-mode ess-r-mode) . ("R" "--slave" "-e" "languageserver::run()")) (java-mode . ("jdtls")) - (dart-mode . ("dart_language_server")) + (dart-mode . ("dart" "language-server" + "--client-id" "emacs.eglot-dart")) (elixir-mode . ("language_server.sh")) (ada-mode . ("ada_language_server")) (scala-mode . ("metals-emacs")) commit c17c3cfcbffc97dfae76d75ab32ec81d893be97c Author: João Távora Date: Mon Apr 4 11:05:29 2022 +0100 Check textdocumentsync/willsave cap before sending it * eglot.el (eglot--guess-contact): Default language-id to educated guess when eglot--lookup-mode returns nil. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/823 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a1c5ab01eb..c32560ad8f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2277,7 +2277,8 @@ When called interactively, use the currently active server" "Send textDocument/willSave to server." (let ((server (eglot--current-server-or-lose)) (params `(:reason 1 :textDocument ,(eglot--TextDocumentIdentifier)))) - (jsonrpc-notify server :textDocument/willSave params) + (when (eglot--server-capable :textDocumentSync :willSave) + (jsonrpc-notify server :textDocument/willSave params)) (when (eglot--server-capable :textDocumentSync :willSaveWaitUntil) (ignore-errors (eglot--apply-text-edits commit f9cfefcf89cd5d899bd335e9d7674b84cef952f2 Author: João Távora Date: Mon Apr 4 10:41:29 2022 +0100 Guess language-id if manually entering server program * eglot.el (eglot--guess-contact): Default language-id to educated guess when eglot--lookup-mode returns nil. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/837 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6d0b66c2a1..a1c5ab01eb 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -904,7 +904,8 @@ be guessed." (eglot--error "Can't guess mode to manage for `%s'" (current-buffer))) (t guessed-mode))) (lang-id-and-guess (eglot--lookup-mode guessed-mode)) - (language-id (car lang-id-and-guess)) + (language-id (or (car lang-id-and-guess) + (string-remove-suffix "-mode" (symbol-name guessed-mode)))) (guess (cdr lang-id-and-guess)) (guess (if (functionp guess) (funcall guess interactive) commit a63916f92855aa4a0035f60a31d8a0f991194583 Author: João Távora Date: Mon Apr 4 10:23:56 2022 +0100 Tweak eglot mode-line menus * eglot.el (eglot-manual): Rename from eglot-read-documentation (eglot-customize): Delete. (eglot-menu): Rename from eglot-menu-map. Rework. (eglot--mode-line-format): Tweak. (eglot-menu-string): Rename from eglot-mode-line-string. (Flymake customization): New source section. * NEWS.md: Tweak. GitHub-reference: per https://github.com/joaotavora/eglot/issues/792 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c8de62c154..6d0b66c2a1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -322,8 +322,8 @@ let the buffer grow forever." "If non-nil, activate Eglot in cross-referenced non-project files." :type 'boolean) -(defcustom eglot-mode-line-string "eglot" - "String displayed on the mode line when Eglot is active." +(defcustom eglot-menu-string "eglot" + "String displayed in mode line when Eglot is active." :type 'string) (defvar eglot-withhold-process-id nil @@ -1745,98 +1745,71 @@ If it is activated, also signal textDocument/didOpen." (call-interactively what) (force-mode-line-update t)))))) -(defun eglot-read-documentation () - "Open the on-line documentation." - (interactive) - (browse-url "https://github.com/joaotavora/eglot#readme")) +(defun eglot-manual () "Open on-line documentation." + (interactive) (browse-url "https://github.com/joaotavora/eglot#readme")) -(defun eglot-customize () - "Customize Eglot." - (interactive) - (customize-group "eglot")) - -(easy-menu-define eglot-menu-map nil "Eglot" - (let ((action-help - "Get possible code actions for the active region or the point")) - `("Eglot" - ;; Commands for getting information and customization. - ["Read the documentation" eglot-read-documentation - :help "Read the on-line documentation"] - ["Customize Eglot" eglot-customize - :help "Customize Eglot globally"] - "--" - ;; xref like commands. - ["Find definitions" xref-find-definitions - :help "Find definitions of the identifier at point" - :active (eglot--server-capable :definitionProvider)] - ["Find references" xref-find-references - :help "Find references to the identifier at point" - :active (eglot--server-capable :referencesProvider)] - ["Find symbols in workspace (apropos)" xref-find-apropos - :help "Find symbols matching a query" - :active (eglot--server-capable :workspaceSymbolProvider)] - ["Find declaration" eglot-find-declaration - :help "Find declaration for the identifier at point" - :active (eglot--server-capable :declarationProvider)] - ["Find implementation" eglot-find-implementation - :help "Find implementation for the identifier at point" - :active (eglot--server-capable :implementationProvider)] - ["Find type definition" eglot-find-typeDefinition - :help "Find type definition for the identifier at point" - :active (eglot--server-capable :typeDefinitionProvider)] - "--" - ;; LSP-related commands (mostly Eglot's own commands). - ["Rename symbol" eglot-rename - :help "Rename current symbol" - :active (eglot--server-capable :renameProvider)] - ["Format buffer" eglot-format-buffer - :help "Format contents of the buffer" - :active (eglot--server-capable :documentFormattingProvider)] - ["Format region" eglot-format - :help "Format the active region" - :active (and (region-active-p) - (eglot--server-capable :documentRangeFormattingProvider))] - ["Show all diagnostics" flymake-show-buffer-diagnostics - :help "Show diagnostics for current buffer (flymake)"] - ["Show documentation for point" eldoc-doc-buffer - :help "Show documentation for point in a buffer (eldoc)"] - "--" - ;; Code-action commands. - ["All possible code actions" eglot-code-actions - :help ,action-help - :active (eglot--server-capable :codeActionProvider)] - ["Organize imports" eglot-code-action-organize-imports - :help ,action-help - :visible (eglot--server-capable :codeActionProvider)] - ["Extract" eglot-code-action-extract - :help ,action-help - :visible (eglot--server-capable :codeActionProvider)] - ["Inline" eglot-code-action-inline - :help ,action-help - :visible (eglot--server-capable :codeActionProvider)] - ["Rewrite" eglot-code-action-rewrite - :help ,action-help - :visible (eglot--server-capable :codeActionProvider)] - ["Quickfix" eglot-code-action-quickfix - :help ,action-help - :visible (eglot--server-capable :codeActionProvider)]))) - -(easy-menu-define eglot-debug-map nil "Debugging the server communication" +(easy-menu-define eglot-menu nil "Eglot" + `("Eglot" + ;; Commands for getting information and customization. + ["Read manual" eglot-manual] + ["Customize Eglot" (lambda () (interactive) (customize-group "eglot"))] + "--" + ;; xref like commands. + ["Find definitions" xref-find-definitions + :help "Find definitions of identifier at point" + :active (eglot--server-capable :definitionProvider)] + ["Find references" xref-find-references + :help "Find references to identifier at point" + :active (eglot--server-capable :referencesProvider)] + ["Find symbols in workspace (apropos)" xref-find-apropos + :help "Find symbols matching a query" + :active (eglot--server-capable :workspaceSymbolProvider)] + ["Find declaration" eglot-find-declaration + :help "Find declaration for identifier at point" + :active (eglot--server-capable :declarationProvider)] + ["Find implementation" eglot-find-implementation + :help "Find implementation for identifier at point" + :active (eglot--server-capable :implementationProvider)] + ["Find type definition" eglot-find-typeDefinition + :help "Find type definition for identifier at point" + :active (eglot--server-capable :typeDefinitionProvider)] + "--" + ;; LSP-related commands (mostly Eglot's own commands). + ["Rename symbol" eglot-rename + :active (eglot--server-capable :renameProvider)] + ["Format buffer" eglot-format-buffer + :active (eglot--server-capable :documentFormattingProvider)] + ["Format active region" eglot-format + :active (and (region-active-p) + (eglot--server-capable :documentRangeFormattingProvider))] + ["Show Flymake diagnostics for buffer" flymake-show-buffer-diagnostics] + ["Show Flymake diagnostics for project" flymake-show-project-diagnostics] + ["Show Eldoc documentation at point" eldoc-doc-buffer] + "--" + ["All possible code actions" eglot-code-actions + :active (eglot--server-capable :codeActionProvider)] + ["Organize imports" eglot-code-action-organize-imports + :visible (eglot--server-capable :codeActionProvider)] + ["Extract" eglot-code-action-extract + :visible (eglot--server-capable :codeActionProvider)] + ["Inline" eglot-code-action-inline + :visible (eglot--server-capable :codeActionProvider)] + ["Rewrite" eglot-code-action-rewrite + :visible (eglot--server-capable :codeActionProvider)] + ["Quickfix" eglot-code-action-quickfix + :visible (eglot--server-capable :codeActionProvider)])) + +(easy-menu-define eglot-server-menu nil "Monitor server communication" '("Debugging the server communication" - ["Go to events buffer" eglot-events-buffer - :help "Display the log buffer of the server communication"] - ["Go to the stderr buffer" eglot-stderr-buffer - :help "Display the error buffer for current LSP server"] - ["Reconnect to server" eglot-reconnect - :help "Reconnect to the current LSP server"] - ["Quit server" eglot-shutdown - :help "Politely ask the LSP server to quit"] + ["Reconnect to server" eglot-reconnect] + ["Quit server" eglot-shutdown] "--" - ["Customize events buffers" + ["LSP events buffer" eglot-events-buffer] + ["Server stderr buffer" eglot-stderr-buffer] + ["Customize event buffer size" (lambda () (interactive) - (customize-variable 'eglot-events-buffer-size)) - :help "Customize variable eglot-events-buffer-size"])) + (customize-variable 'eglot-events-buffer-size))])) (defun eglot--mode-line-props (thing face defs &optional prepend) "Helper for function `eglot--mode-line-format'. @@ -1862,12 +1835,12 @@ Uses THING, FACE, DEFS and PREPEND." (last-error (and server (jsonrpc-last-error server)))) (append `(,(propertize - eglot-mode-line-string + eglot-menu-string 'face 'eglot-mode-line 'mouse-face 'mode-line-highlight - 'help-echo "Eglot: an LSP client\nmouse-1: Display minor mode menu" + 'help-echo "Eglot: Emacs LSP client\nmouse-1: Display minor mode menu" 'keymap (let ((map (make-sparse-keymap))) - (define-key map [mode-line down-mouse-1] eglot-menu-map) + (define-key map [mode-line down-mouse-1] eglot-menu) map))) (when nick `(":" @@ -1875,9 +1848,9 @@ Uses THING, FACE, DEFS and PREPEND." nick 'face 'eglot-mode-line 'mouse-face 'mode-line-highlight - 'help-echo (format "Project '%s'\nmouse-1: LSP debugging menu" nick) + 'help-echo (format "Project '%s'\nmouse-1: LSP server control menu" nick) 'keymap (let ((map (make-sparse-keymap))) - (define-key map [mode-line down-mouse-1] eglot-debug-map) + (define-key map [mode-line down-mouse-1] eglot-server-menu) map)) ,@(when last-error `("/" ,(eglot--mode-line-props @@ -1899,6 +1872,9 @@ still unanswered LSP requests to the server\n")))))))) (add-to-list 'mode-line-misc-info `(eglot--managed-mode (" [" eglot--mode-line-format "] "))) + +;;; Flymake customization +;;; (put 'eglot-note 'flymake-category 'flymake-note) (put 'eglot-warning 'flymake-category 'flymake-warning) (put 'eglot-error 'flymake-category 'flymake-error) commit 2dad9298d9ba96510fb5f744959284ca46b71968 Author: Felicián Németh Date: Sat Jan 15 18:51:36 2022 +0100 Rework eglot's mode-line Mimic flymake by replacing the old menus of the mode-line with "context menus". List all usefull commands under the main menu (eglot-menu-map), and commands related to LSP debugging under the project menu (eglot-debug-map). * eglot.el (eglot-read-documentation, eglot-customize): New commands. (eglot-mode-line-string): New defcustom. (eglot-menu-map, eglot-debug-map,): New variables. (eglot--mode-line-props): Rework to use eglot-menu-map and eglot-debug-map. (eglot--mode-line-format): Use eglot-mode-line-string. GitHub-reference: close https://github.com/joaotavora/eglot/issues/792 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f739a0d34d..c8de62c154 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -322,6 +322,10 @@ let the buffer grow forever." "If non-nil, activate Eglot in cross-referenced non-project files." :type 'boolean) +(defcustom eglot-mode-line-string "eglot" + "String displayed on the mode line when Eglot is active." + :type 'string) + (defvar eglot-withhold-process-id nil "If non-nil, Eglot will not send the Emacs process id to the language server. This can be useful when using docker to run a language server.") @@ -1741,6 +1745,99 @@ If it is activated, also signal textDocument/didOpen." (call-interactively what) (force-mode-line-update t)))))) +(defun eglot-read-documentation () + "Open the on-line documentation." + (interactive) + (browse-url "https://github.com/joaotavora/eglot#readme")) + +(defun eglot-customize () + "Customize Eglot." + (interactive) + (customize-group "eglot")) + +(easy-menu-define eglot-menu-map nil "Eglot" + (let ((action-help + "Get possible code actions for the active region or the point")) + `("Eglot" + ;; Commands for getting information and customization. + ["Read the documentation" eglot-read-documentation + :help "Read the on-line documentation"] + ["Customize Eglot" eglot-customize + :help "Customize Eglot globally"] + "--" + ;; xref like commands. + ["Find definitions" xref-find-definitions + :help "Find definitions of the identifier at point" + :active (eglot--server-capable :definitionProvider)] + ["Find references" xref-find-references + :help "Find references to the identifier at point" + :active (eglot--server-capable :referencesProvider)] + ["Find symbols in workspace (apropos)" xref-find-apropos + :help "Find symbols matching a query" + :active (eglot--server-capable :workspaceSymbolProvider)] + ["Find declaration" eglot-find-declaration + :help "Find declaration for the identifier at point" + :active (eglot--server-capable :declarationProvider)] + ["Find implementation" eglot-find-implementation + :help "Find implementation for the identifier at point" + :active (eglot--server-capable :implementationProvider)] + ["Find type definition" eglot-find-typeDefinition + :help "Find type definition for the identifier at point" + :active (eglot--server-capable :typeDefinitionProvider)] + "--" + ;; LSP-related commands (mostly Eglot's own commands). + ["Rename symbol" eglot-rename + :help "Rename current symbol" + :active (eglot--server-capable :renameProvider)] + ["Format buffer" eglot-format-buffer + :help "Format contents of the buffer" + :active (eglot--server-capable :documentFormattingProvider)] + ["Format region" eglot-format + :help "Format the active region" + :active (and (region-active-p) + (eglot--server-capable :documentRangeFormattingProvider))] + ["Show all diagnostics" flymake-show-buffer-diagnostics + :help "Show diagnostics for current buffer (flymake)"] + ["Show documentation for point" eldoc-doc-buffer + :help "Show documentation for point in a buffer (eldoc)"] + "--" + ;; Code-action commands. + ["All possible code actions" eglot-code-actions + :help ,action-help + :active (eglot--server-capable :codeActionProvider)] + ["Organize imports" eglot-code-action-organize-imports + :help ,action-help + :visible (eglot--server-capable :codeActionProvider)] + ["Extract" eglot-code-action-extract + :help ,action-help + :visible (eglot--server-capable :codeActionProvider)] + ["Inline" eglot-code-action-inline + :help ,action-help + :visible (eglot--server-capable :codeActionProvider)] + ["Rewrite" eglot-code-action-rewrite + :help ,action-help + :visible (eglot--server-capable :codeActionProvider)] + ["Quickfix" eglot-code-action-quickfix + :help ,action-help + :visible (eglot--server-capable :codeActionProvider)]))) + +(easy-menu-define eglot-debug-map nil "Debugging the server communication" + '("Debugging the server communication" + ["Go to events buffer" eglot-events-buffer + :help "Display the log buffer of the server communication"] + ["Go to the stderr buffer" eglot-stderr-buffer + :help "Display the error buffer for current LSP server"] + ["Reconnect to server" eglot-reconnect + :help "Reconnect to the current LSP server"] + ["Quit server" eglot-shutdown + :help "Politely ask the LSP server to quit"] + "--" + ["Customize events buffers" + (lambda () + (interactive) + (customize-variable 'eglot-events-buffer-size)) + :help "Customize variable eglot-events-buffer-size"])) + (defun eglot--mode-line-props (thing face defs &optional prepend) "Helper for function `eglot--mode-line-format'. Uses THING, FACE, DEFS and PREPEND." @@ -1764,18 +1861,28 @@ Uses THING, FACE, DEFS and PREPEND." (`(,_id ,doing ,done-p ,_detail) (and server (eglot--spinner server))) (last-error (and server (jsonrpc-last-error server)))) (append - `(,(eglot--mode-line-props "eglot" 'eglot-mode-line nil)) + `(,(propertize + eglot-mode-line-string + 'face 'eglot-mode-line + 'mouse-face 'mode-line-highlight + 'help-echo "Eglot: an LSP client\nmouse-1: Display minor mode menu" + 'keymap (let ((map (make-sparse-keymap))) + (define-key map [mode-line down-mouse-1] eglot-menu-map) + map))) (when nick - `(":" ,(eglot--mode-line-props - nick 'eglot-mode-line - '((C-mouse-1 eglot-stderr-buffer "go to stderr buffer") - (mouse-1 eglot-events-buffer "go to events buffer") - (mouse-2 eglot-shutdown "quit server") - (mouse-3 eglot-reconnect "reconnect to server"))) - ,@(when last-error + `(":" + ,(propertize + nick + 'face 'eglot-mode-line + 'mouse-face 'mode-line-highlight + 'help-echo (format "Project '%s'\nmouse-1: LSP debugging menu" nick) + 'keymap (let ((map (make-sparse-keymap))) + (define-key map [mode-line down-mouse-1] eglot-debug-map) + map)) + ,@(when last-error `("/" ,(eglot--mode-line-props "error" 'compilation-mode-line-fail - '((mouse-3 eglot-clear-status "clear this status")) + '((mouse-3 eglot-clear-status "Clear this status")) (format "An error occurred: %s\n" (plist-get last-error :message))))) ,@(when (and doing (not done-p)) @@ -1785,9 +1892,9 @@ Uses THING, FACE, DEFS and PREPEND." `("/" ,(eglot--mode-line-props (format "%d" pending) 'warning '((mouse-3 eglot-forget-pending-continuations - "forget pending continuations")) + "Forget pending continuations")) "Number of outgoing, \ -still unanswered LSP requests to the server")))))))) +still unanswered LSP requests to the server\n")))))))) (add-to-list 'mode-line-misc-info `(eglot--managed-mode (" [" eglot--mode-line-format "] "))) commit ef0da9414e148cafef7443660ac90c571850b629 Author: João Távora Date: Mon Apr 4 09:39:59 2022 +0100 Make eglot--plist-keys a simple (non-map.el) helper again This removes a nagging compilation warning when developing on Emacs master. There's not much point in depending on map.el just for this util. And there' snot much point in making eglot--plist-keys go through a generic dispatching mechanism when we happen to know the thing being dispatched * eglot.el (eglot--plist-keys): Define in helpers section. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 86e798fabd..f739a0d34d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1550,6 +1550,9 @@ and just return it. PROMPT shouldn't end with a question mark." "Tell if SERVER's project root is `file-remote-p'." (file-remote-p (project-root (eglot--project server)))) +(defun eglot--plist-keys (plist) "Get keys of a plist." + (cl-loop for (k _v) on plist by #'cddr collect k)) + ;;; Minor modes ;;; @@ -3078,13 +3081,6 @@ If NOERROR, return predicate, else erroring function." (make-obsolete-variable 'eglot--managed-mode-hook 'eglot-managed-mode-hook "1.6") - -(if (< emacs-major-version 27) - (defun eglot--plist-keys (plist) - (cl-loop for (k _v) on plist by #'cddr collect k)) - ;; Make into an obsolete alias once we drop support for Emacs 26. - (defalias 'eglot--plist-keys #'map-keys)) - (provide 'eglot) ;; Local Variables: commit 1d9542cbe8e4249d185518b746437b83de855810 Author: João Távora Date: Thu Mar 31 14:21:29 2022 +0100 Protect against empty firsttriggercharacter strings Which some LS's like gopls like to send. * eglot.el (eglot--post-self-insert-hook): Beware of empty strings. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/906 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ade8a7c711..86e798fabd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1993,11 +1993,12 @@ THINGS are either registrations or unregisterations (sic)." (setq eglot--last-inserted-char last-input-event) (let ((ot-provider (eglot--server-capable :documentOnTypeFormattingProvider))) (when (and ot-provider - (or (eq last-input-event - (elt (plist-get ot-provider :firstTriggerCharacter) 0)) - (cl-find last-input-event - (plist-get ot-provider :moreTriggerCharacter) - :key #'seq-first))) + (ignore-errors ; github#906, some LS's send empty strings + (or (eq last-input-event + (seq-first (plist-get ot-provider :firstTriggerCharacter))) + (cl-find last-input-event + (plist-get ot-provider :moreTriggerCharacter) + :key #'seq-first)))) (eglot-format (point) nil last-input-event)))) (defun eglot--pre-command-hook () commit 904556f662fa188c97f3b56d3555c58a3dc84afe Author: João Távora Date: Mon Mar 28 11:00:44 2022 +0100 Easier initializationoptions in eglot-server-programs Per https://github.com/joaotavora/eglot/issues/845. * NEWS.md: Update. * eglot.el (eglot-server-programs): Document new syntax. (eglot-initialization-options): Can use initializationOptions from server's saved initargs. (eglot--connect): Allow a plist to be appended to a server contact. GitHub-reference: close https://github.com/joaotavora/eglot/issues/901 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d71e5966a8..ade8a7c711 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -218,17 +218,24 @@ CONTACT can be: PROGRAM is called with ARGS and is expected to serve LSP requests over the standard input/output channels. +* A list (PROGRAM [ARGS...] :initializationOptions OPTIONS), + whereupon PROGRAM is called with ARGS as in the first option, + and the LSP \"initializationOptions\" JSON object is + constructed from OPTIONS. If OPTIONS is a unary function, it + is called with the server instance and should return a JSON + object. + * A list (HOST PORT [TCP-ARGS...]) where HOST is a string and PORT is a positive integer for connecting to a server via TCP. Remaining ARGS are passed to `open-network-stream' for upgrading the connection with encryption or other capabilities. * A list (PROGRAM [ARGS...] :autoport [MOREARGS...]), whereupon a - combination of the two previous options is used. First, an - attempt is made to find an available server port, then PROGRAM - is launched with ARGS; the `:autoport' keyword substituted for - that number; and MOREARGS. Eglot then attempts to establish a - TCP connection to that port number on the localhost. + combination of previous options is used. First, an attempt is + made to find an available server port, then PROGRAM is launched + with ARGS; the `:autoport' keyword substituted for that number; + and MOREARGS. Eglot then attempts to establish a TCP + connection to that port number on the localhost. * A cons (CLASS-NAME . INITARGS) where CLASS-NAME is a symbol designating a subclass of `eglot-lsp-server', for representing @@ -627,7 +634,11 @@ treated as in `eglot-dbind'." (cl-defgeneric eglot-initialization-options (server) "JSON object to send under `initializationOptions'." - (:method (_s) eglot--{})) ; blank default + (:method (s) + (let ((probe (plist-get (eglot--saved-initargs s) :initializationOptions))) + (cond ((functionp probe) (funcall probe s)) + (probe) + (t eglot--{}))))) (cl-defgeneric eglot-register-capability (server method id &rest params) "Ask SERVER to register capability METHOD marked with ID." @@ -1116,18 +1127,22 @@ This docstring appeases checkdoc, that's all." (setq autostart-inferior-process inferior) connection)))) ((stringp (car contact)) - `(:process - ,(lambda () - (let ((default-directory default-directory)) - (make-process - :name readable-name - :command (setq server-info (eglot--cmd contact)) - :connection-type 'pipe - :coding 'utf-8-emacs-unix - :noquery t - :stderr (get-buffer-create - (format "*%s stderr*" readable-name)) - :file-handler t))))))) + (let* ((probe (cl-position-if #'keywordp contact)) + (more-initargs (and probe (cl-subseq contact probe))) + (contact (cl-subseq contact 0 probe))) + `(:process + ,(lambda () + (let ((default-directory default-directory)) + (make-process + :name readable-name + :command (setq server-info (eglot--cmd contact)) + :connection-type 'pipe + :coding 'utf-8-emacs-unix + :noquery t + :stderr (get-buffer-create + (format "*%s stderr*" readable-name)) + :file-handler t))) + ,@more-initargs))))) (spread (lambda (fn) (lambda (server method params) (let ((eglot--cached-server server)) (apply fn server method (append params nil)))))) commit 6d815aaa986f24d356aa08557095cbaab3414142 Author: João Távora Date: Tue Mar 29 00:09:34 2022 +0100 Tweak on-type-formatting code * eglot.el (eglot--post-self-insert-hook): Tweak. (eglot-format): Tweak docstring. GitHub-reference: per https://github.com/joaotavora/eglot/issues/899 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1cf0b7ae63..d71e5966a8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1974,18 +1974,16 @@ THINGS are either registrations or unregisterations (sic)." "If non-nil, value of the last inserted character in buffer.") (defun eglot--post-self-insert-hook () - "Set `eglot--last-inserted-char', call on-type-formatting if necessary." + "Set `eglot--last-inserted-char', maybe call on-type-formatting." (setq eglot--last-inserted-char last-input-event) - (when (or (eq last-input-event - (elt (eglot--server-capable - :documentOnTypeFormattingProvider - :firstTriggerCharacter) - 0)) - (seq-find (lambda (elt) (eq last-input-event (elt elt 0))) - (eglot--server-capable - :documentOnTypeFormattingProvider - :moreTriggerCharacter))) - (eglot-format (point) nil (string last-input-event)))) + (let ((ot-provider (eglot--server-capable :documentOnTypeFormattingProvider))) + (when (and ot-provider + (or (eq last-input-event + (elt (plist-get ot-provider :firstTriggerCharacter) 0)) + (cl-find last-input-event + (plist-get ot-provider :moreTriggerCharacter) + :key #'seq-first))) + (eglot-format (point) nil last-input-event)))) (defun eglot--pre-command-hook () "Reset `eglot--last-inserted-char'." @@ -2373,9 +2371,8 @@ If either BEG or END is nil, format entire buffer. Interactively, format active region, or entire buffer if region is not active. -If ON-TYPE-FORMAT is non-nil, request on-type-formatting from the -server. The argument should be a one-character-long string that -has just been inserted at BEG." +If non-nil, ON-TYPE-FORMAT is a character just inserted at BEG +for which LSP on-type-formatting should be requested." (interactive (and (region-active-p) (list (region-beginning) (region-end)))) (pcase-let ((`(,method ,cap ,args) (cond @@ -2383,7 +2380,7 @@ has just been inserted at BEG." `(:textDocument/onTypeFormatting :documentOnTypeFormattingProvider ,`(:position ,(eglot--pos-to-lsp-position beg) - :ch ,on-type-format))) + :ch ,(string on-type-format)))) ((and beg end) `(:textDocument/rangeFormatting :documentRangeFormattingProvider commit c2d97d22aa535af2640676e1df05a41f9abbd1e3 Author: Felicián Németh Date: Wed Mar 23 21:47:45 2022 +0100 Implement on-type-formatting support * eglot.el (eglot-format): Add new optional argument `on-type-format' to request :textDocument/onTypeFormatting, and ... (eglot--post-self-insert-hook): ... call it from here when necessary. * eglot-tests.el (eglot--simulate-key-event): New helper defun. (rust-on-type-formatting): New test. * NEWS.md: mention feature. GitHub-reference: close https://github.com/joaotavora/eglot/issues/899 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 58ad4588ae..1cf0b7ae63 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1974,8 +1974,18 @@ THINGS are either registrations or unregisterations (sic)." "If non-nil, value of the last inserted character in buffer.") (defun eglot--post-self-insert-hook () - "Set `eglot--last-inserted-char'." - (setq eglot--last-inserted-char last-input-event)) + "Set `eglot--last-inserted-char', call on-type-formatting if necessary." + (setq eglot--last-inserted-char last-input-event) + (when (or (eq last-input-event + (elt (eglot--server-capable + :documentOnTypeFormattingProvider + :firstTriggerCharacter) + 0)) + (seq-find (lambda (elt) (eq last-input-event (elt elt 0))) + (eglot--server-capable + :documentOnTypeFormattingProvider + :moreTriggerCharacter))) + (eglot-format (point) nil (string last-input-event)))) (defun eglot--pre-command-hook () "Reset `eglot--last-inserted-char'." @@ -2357,14 +2367,23 @@ Try to visit the target file for a richer summary line." (interactive) (eglot-format nil nil)) -(defun eglot-format (&optional beg end) +(defun eglot-format (&optional beg end on-type-format) "Format region BEG END. If either BEG or END is nil, format entire buffer. Interactively, format active region, or entire buffer if region -is not active." +is not active. + +If ON-TYPE-FORMAT is non-nil, request on-type-formatting from the +server. The argument should be a one-character-long string that +has just been inserted at BEG." (interactive (and (region-active-p) (list (region-beginning) (region-end)))) (pcase-let ((`(,method ,cap ,args) (cond + ((and beg on-type-format) + `(:textDocument/onTypeFormatting + :documentOnTypeFormattingProvider + ,`(:position ,(eglot--pos-to-lsp-position beg) + :ch ,on-type-format))) ((and beg end) `(:textDocument/rangeFormatting :documentRangeFormattingProvider commit 5d2f6bc667afb8252170690ad9523f327d93bc74 Author: Felicián Németh Date: Sun Mar 27 22:58:44 2022 +0200 Map more emacs variables to lsp formattingoptions fields * eglot.el (eglot-format): Map require-final-newline to insertFinalNewline and delete-trailing-lines to trimFinalNewlines. GitHub-reference: close https://github.com/joaotavora/eglot/issues/900 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1c16240890..58ad4588ae 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2381,7 +2381,9 @@ is not active." (cl-list* :textDocument (eglot--TextDocumentIdentifier) :options (list :tabSize tab-width - :insertSpaces (if indent-tabs-mode :json-false t)) + :insertSpaces (if indent-tabs-mode :json-false t) + :insertFinalNewline (if require-final-newline t :json-false) + :trimFinalNewlines (if delete-trailing-lines t :json-false)) args) :deferred method)))) commit 05418a1d836cc76d3e4c8bf5287b8351da523096 Author: Marcus Swanson Date: Sat Mar 26 22:43:31 2022 +0100 Add omnisharp support for c# * eglot.el (eglot-server-programs): Add omnisharp for C#. * README.md: Document the above change. Copyright-paperwork-exempt: Yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/897 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 83a2945514..1c16240890 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -192,7 +192,8 @@ language-server/bin/php-language-server.php")) (html-mode . ,(eglot-alternatives '(("vscode-html-language-server" "--stdio") ("html-languageserver" "--stdio")))) (json-mode . ,(eglot-alternatives '(("vscode-json-language-server" "--stdio") ("json-languageserver" "--stdio")))) (dockerfile-mode . ("docker-langserver" "--stdio")) - (clojure-mode . ("clojure-lsp"))) + (clojure-mode . ("clojure-lsp")) + (csharp-mode . ("omnisharp" "-lsp"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE identifies the buffers that are to be managed by a specific commit 965e1378f19d36284119324a06840752dfff3990 Author: João Távora Date: Thu Mar 24 16:06:08 2022 +0000 Use bounds of thing at point when asking for code actions * eglot.el (eglot--region-bounds): Consider bounds of things at point. GitHub-reference: per https://github.com/joaotavora/eglot/issues/895 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index daf6c3e237..83a2945514 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2838,8 +2838,11 @@ is not active." :newName ,newname)) current-prefix-arg)) -(defun eglot--region-bounds () "Region bounds if active, else point and nil." - (if (use-region-p) `(,(region-beginning) ,(region-end)) `(,(point) nil))) +(defun eglot--region-bounds () + "Region bounds if active, else bounds of things at point." + (if (use-region-p) `(,(region-beginning) ,(region-end)) + (let ((boftap (bounds-of-thing-at-point 'sexp))) + (list (car boftap) (cdr boftap))))) (defun eglot-code-actions (beg &optional end action-kind) "Offer to execute actions of ACTION-KIND between BEG and END. commit a38ce8b28fef2f50263d827aedb3394b795dac56 Author: Felicián Németh Date: Sun Mar 20 09:50:15 2022 +0100 Add simple support for workspacefolders Close https://github.com/joaotavora/eglot/issues/893. Clients can support workspaceFolders since LSP 3.6. rootUri and rootPath are deprecated. Dynamic changes in folders are not supported, i.e., this patch does not implement workspace/didChangeWorkspaceFolders. * eglot.el (eglot-client-capabilities): Add capability `workspaceFolders'. (eglot-workspace-folders): New cl-defgeneric. (eglot--connect): Add workspaceFolders to initializeParams. (eglot-handle-request workspace/workspaceFolders): New cl-defmethod. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3bd2d844c8..daf6c3e237 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -654,7 +654,8 @@ treated as in `eglot-dbind'." `(:dynamicRegistration ,(if (eglot--trampish-p s) :json-false t)) :symbol `(:dynamicRegistration :json-false) - :configuration t) + :configuration t + :workspaceFolders t) :textDocument (list :synchronization (list @@ -719,6 +720,15 @@ treated as in `eglot-dbind'." #'car eglot--tag-faces)]))) :experimental eglot--{}))) +(cl-defgeneric eglot-workspace-folders (server) + "Return workspaceFolders for SERVER." + (let ((project (eglot--project server))) + (vconcat + (mapcar (lambda (dir) + (list :uri (eglot--path-to-uri dir) + :name (abbreviate-file-name dir))) + `(,(project-root project) ,@(project-external-roots project)))))) + (defclass eglot-lsp-server (jsonrpc-process-connection) ((project-nickname :documentation "Short nickname for the associated project." @@ -1164,7 +1174,8 @@ This docstring appeases checkdoc, that's all." :rootUri (eglot--path-to-uri default-directory) :initializationOptions (eglot-initialization-options server) - :capabilities (eglot-client-capabilities server)) + :capabilities (eglot-client-capabilities server) + :workspaceFolders (eglot-workspace-folders server)) :success-fn (eglot--lambda ((InitializeResult) capabilities serverInfo) (unless cancelled @@ -1924,6 +1935,11 @@ THINGS are either registrations or unregisterations (sic)." "Handle server request workspace/applyEdit." (eglot--apply-workspace-edit edit eglot-confirm-server-initiated-edits)) +(cl-defmethod eglot-handle-request + (server (_method (eql workspace/workspaceFolders))) + "Handle server request workspace/workspaceFolders." + (eglot-workspace-folders server)) + (defun eglot--TextDocumentIdentifier () "Compute TextDocumentIdentifier object for current buffer." `(:uri ,(eglot--path-to-uri (or buffer-file-name commit 6dba74d8cb7e0172641ac6fdd7dfcd80bd22a1c4 Author: João Távora Date: Fri Mar 18 10:54:21 2022 +0000 Defend against broken move-to-column in recent emacs * eglot.el (eglot-lsp-abiding-column): Use (min (point) (point-max)) This is a defensive fix for an Emacs/company-mode problem described below. The problem can be reproduced in Eglot before this commit with: ~/Source/Emacs/emacs/src/emacs -Q -f package-initialize -L \ ~/Source/Emacs/company-mode -l company -f global-company-mode -l \ eglot.el ~/tmp/issue-860/args_out_of_range.c -f eglot -f \ display-line-numbers-mode -f toggle-debug-on-error 1 // args_out_of_range.c 2 struct Book { 3 int id; 4 char title[50] 5 } book = { 1024, "C" }; 6 7 int main(int argc, char *argv[]) 8 { 9 10 // Error when typing the dot to make "book." 11 book 12 return 0; 13 } When one types the dot after the "book" on line 11, company-mode displays a two-line overlay that visually encompasses line 12 after "book", which has the "return 0;" statement. That line happens to also hold a warning about incorrect syntax, one that starts at column 2. Eglot uses 'move-to-column' to go that precise place. In Emacs 27.2, move-to-column is unaffected by previous company-mode overlays, even if the current line is being co-used visually by the overlay. It moves to the right buffer position. In Emacs master, this isn't true. It seems to be confounded by the company-mode overlay and moves to eob, which eventually breaks Eglot with a backtrace such as this one: Debugger entered--Lisp error: (args-out-of-range # 110 124) encode-coding-region(110 124 utf-16 t) (length (encode-coding-region (or lbp (line-beginning-position)) (point) 'utf-16 t)) (- (length (encode-coding-region (or lbp (line-beginning-position)) (point) 'utf-16 t)) 2) (/ (- (length (encode-coding-region (or lbp (line-beginning-position)) (point) 'utf-16 t)) 2) 2) eglot-lsp-abiding-column(110) (- column (eglot-lsp-abiding-column lbp)) (setq diff (- column (eglot-lsp-abiding-column lbp))) (progn (setq diff (- column (eglot-lsp-abiding-column lbp))) (not (= 0 diff))) (while (progn (setq diff (- column (eglot-lsp-abiding-column lbp))) (not (= 0 diff))) (condition-case eob-err (forward-char (/ (if (> diff 0) (1+ diff) (1- diff)) 2)) (end-of-buffer (throw '--cl-block-nil-- eob-err))) (setq --cl-var-- nil)) (let* ((lbp (line-beginning-position)) (diff nil) (--cl-var-- t)) (narrow-to-region lbp (line-end-position)) (move-to-column column) (while (progn (setq diff (- column (eglot-lsp-abiding-column lbp))) (not (= 0 diff))) (condition-case eob-err (forward-char (/ (if (> diff 0) (1+ diff) (1- diff)) 2)) (end-of-buffer (throw '--cl-block-nil-- eob-err))) (setq --cl-var-- nil)) nil) (catch '--cl-block-nil-- (let* ((lbp (line-beginning-position)) (diff nil) (--cl-var-- t)) (narrow-to-region lbp (line-end-position)) (move-to-column column) (while (progn (setq diff (- column (eglot-lsp-abiding-column lbp))) (not (= 0 diff))) (condition-case eob-err (forward-char (/ (if (> diff 0) (1+ diff) (1- diff)) 2)) (end-of-buffer (throw '--cl-block-nil-- eob-err))) (setq --cl-var-- nil)) nil)) (save-restriction (catch '--cl-block-nil-- (let* ((lbp (line-beginning-position)) (diff nil) (--cl-var-- t)) (narrow-to-region lbp (line-end-position)) (move-to-column column) (while (progn (setq diff (- column (eglot-lsp-abiding-column lbp))) (not (= 0 diff))) (condition-case eob-err (forward-char (/ (if ... ... ...) 2)) (end-of-buffer (throw '--cl-block-nil-- eob-err))) (setq --cl-var-- nil)) nil))) eglot-move-to-lsp-abiding-column(2) GitHub-reference: fix https://github.com/joaotavora/eglot/issues/860 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ea9299ab59..3bd2d844c8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1312,7 +1312,8 @@ fully LSP-compliant servers, this should be set to "Calculate current COLUMN as defined by the LSP spec. LBP defaults to `line-beginning-position'." (/ (- (length (encode-coding-region (or lbp (line-beginning-position)) - (point) 'utf-16 t)) + ;; Fix github#860 + (min (point) (point-max)) 'utf-16 t)) 2) 2)) commit 349f6b5f7894667b5acb5a6a3dd93bcf27751c75 Author: João Távora Date: Tue Mar 15 10:20:24 2022 +0000 Don't advertise didchangewatchedfiles on tramp * eglot.el (eglot--trampish-p): New helper. (eglot-client-capabilities): Use it. (eglot--uri-to-path): Use it. GitHub-reference: per https://github.com/joaotavora/eglot/issues/883 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cc0a06f4cb..ea9299ab59 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -644,13 +644,15 @@ treated as in `eglot-dbind'." (cl-defgeneric eglot-client-capabilities (server) "What the EGLOT LSP client supports for SERVER." - (:method (_s) + (:method (s) (list :workspace (list :applyEdit t :executeCommand `(:dynamicRegistration :json-false) :workspaceEdit `(:documentChanges t) - :didChangeWatchedFiles `(:dynamicRegistration t) + :didChangeWatchedFiles + `(:dynamicRegistration + ,(if (eglot--trampish-p s) :json-false t)) :symbol `(:dynamicRegistration :json-false) :configuration t) :textDocument @@ -1401,9 +1403,7 @@ If optional MARKER, return a marker instead" "Convert URI to file path, helped by `eglot--current-server'." (when (keywordp uri) (setq uri (substring (symbol-name uri) 1))) (let* ((server (eglot-current-server)) - (remote-prefix (and server - (file-remote-p - (project-root (eglot--project server))))) + (remote-prefix (and server (eglot--trampish-p server))) (retval (url-filename (url-generic-parse-url (url-unhex-string uri)))) ;; Remove the leading "/" for local MS Windows-style paths. (normalized (if (and (not remote-prefix) @@ -1518,6 +1518,10 @@ and just return it. PROMPT shouldn't end with a question mark." (cl-find read servers :key name :test #'equal))) (t (car servers))))) +(defun eglot--trampish-p (server) + "Tell if SERVER's project root is `file-remote-p'." + (file-remote-p (project-root (eglot--project server)))) + ;;; Minor modes ;;; commit be4755233192f2e27ddab75a75c6b858198bac26 Author: Felicián Németh Date: Sat Mar 12 10:57:53 2022 +0100 Change capability 'documentchanges' to t Eglot does support woskspaceEdit/documentChanges, but failed to advertise this fact. Per https://github.com/joaotavora/eglot/issues/873. * eglot.el (eglot-client-capabilities): Set documentChanges to t. GitHub-reference: per https://github.com/joaotavora/eglot/issues/853 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5b4d419678..cc0a06f4cb 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -649,7 +649,7 @@ treated as in `eglot-dbind'." :workspace (list :applyEdit t :executeCommand `(:dynamicRegistration :json-false) - :workspaceEdit `(:documentChanges :json-false) + :workspaceEdit `(:documentChanges t) :didChangeWatchedFiles `(:dynamicRegistration t) :symbol `(:dynamicRegistration :json-false) :configuration t) commit 77f3157dcd2926ded5a04069ef44242b3c27a2a5 Author: Manuel Uberti Date: Fri Mar 11 13:41:53 2022 +0100 Use new jdtls script for eclipse jdt Per https://github.com/joaotavora/eglot/issues/864. * eglot.el (eglot-server-programs): use new jdtls (eglot--eclipse-jdt-contact, eglot--eclipse-jdt). Remove. (eglot-execute-command eglot-eclipse-jdt): Remove. (eglot-initialization-options eglot-eclipse-jdt): Remove. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/863 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b84e1449d7..5b4d419678 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -173,7 +173,7 @@ language-server/bin/php-language-server.php")) (go-mode . ("gopls")) ((R-mode ess-r-mode) . ("R" "--slave" "-e" "languageserver::run()")) - (java-mode . eglot--eclipse-jdt-contact) + (java-mode . ("jdtls")) (dart-mode . ("dart_language_server")) (elixir-mode . ("language_server.sh")) (ada-mode . ("ada_language_server")) @@ -3013,100 +3013,6 @@ If NOERROR, return predicate, else erroring function." (when (eq ?! (aref arg 1)) (aset arg 1 ?^)) `(,self () (re-search-forward ,(concat "\\=" arg)) (,next))) - -;;; eclipse-jdt-specific -;;; -(defclass eglot-eclipse-jdt (eglot-lsp-server) () - :documentation "Eclipse's Java Development Tools Language Server.") - -(cl-defmethod eglot-initialization-options ((server eglot-eclipse-jdt)) - "Passes through required jdt initialization options." - `(:workspaceFolders - [,@(cl-delete-duplicates - (mapcar #'eglot--path-to-uri - (let* ((root (project-root (eglot--project server)))) - (cons root - (mapcar - #'file-name-directory - (append - (file-expand-wildcards (concat root "*/pom.xml")) - (file-expand-wildcards (concat root "*/build.gradle")) - (file-expand-wildcards (concat root "*/.project"))))))) - :test #'string=)] - ,@(if-let ((home (or (getenv "JAVA_HOME") - (ignore-errors - (expand-file-name - ".." - (file-name-directory - (file-chase-links (executable-find "javac")))))))) - `(:settings (:java (:home ,home))) - (ignore (eglot--warn "JAVA_HOME env var not set"))))) - -(defun eglot--eclipse-jdt-contact (interactive) - "Return a contact for connecting to eclipse.jdt.ls server, as a cons cell. -If INTERACTIVE, prompt user for details." - (cl-labels - ((is-the-jar - (path) - (and (string-match-p - "org\\.eclipse\\.equinox\\.launcher_.*\\.jar$" - (file-name-nondirectory path)) - (file-exists-p path)))) - (let* ((classpath (or (getenv "CLASSPATH") path-separator)) - (cp-jar (cl-find-if #'is-the-jar (split-string classpath path-separator))) - (jar cp-jar) - (dir - (cond - (jar (file-name-as-directory - (expand-file-name ".." (file-name-directory jar)))) - (interactive - (expand-file-name - (read-directory-name - (concat "Path to eclipse.jdt.ls directory (could not" - " find it in CLASSPATH): ") - nil nil t))) - (t (error "Could not find eclipse.jdt.ls jar in CLASSPATH")))) - (repodir - (concat dir - "org.eclipse.jdt.ls.product/target/repository/")) - (repodir (if (file-directory-p repodir) repodir dir)) - (config - (concat - repodir - (cond - ((string= system-type "darwin") "config_mac") - ((string= system-type "windows-nt") "config_win") - (t "config_linux")))) - (workspace - (expand-file-name (md5 (project-root (eglot--current-project))) - (locate-user-emacs-file - "eglot-eclipse-jdt-cache")))) - (unless jar - (setq jar - (cl-find-if #'is-the-jar - (directory-files (concat repodir "plugins") t)))) - (unless (and jar (file-exists-p jar) (file-directory-p config)) - (error "Could not find required eclipse.jdt.ls files (build required?)")) - (when (and interactive (not cp-jar) - (y-or-n-p (concat "Add path to the server program " - "to CLASSPATH environment variable?"))) - (setenv "CLASSPATH" (concat (getenv "CLASSPATH") path-separator jar))) - (unless (file-directory-p workspace) - (make-directory workspace t)) - (cons 'eglot-eclipse-jdt - (list (executable-find "java") - "-Declipse.application=org.eclipse.jdt.ls.core.id1" - "-Dosgi.bundles.defaultStartLevel=4" - "-Declipse.product=org.eclipse.jdt.ls.core.product" - "-jar" jar - "-configuration" config - "-data" workspace))))) - -(cl-defmethod eglot-execute-command - ((_server eglot-eclipse-jdt) (_cmd (eql java.apply.workspaceEdit)) arguments) - "Eclipse JDT breaks spec and replies with edits as arguments." - (mapc #'eglot--apply-workspace-edit arguments)) - ;;; Obsolete ;;; commit cb562118cb14d8bb71140a07232107d83bbbf6d9 Author: Augusto Stoffel Date: Thu Mar 10 12:32:20 2022 +0100 Don't strip invisible text when formatting hover string This was introduced in https://github.com/joaotavora/eglot/issues/482 due to a bad interaction with a specific server. But this solution makes hyperlinks in Eldoc buffers unclickable, because the markdown-mode function that visits a link relies on the invisible text. Per https://github.com/joaotavora/eglot/issues/866 * eglot.el (eglot--format-markup): Use buffer-string instead of filter-buffer-substring GitHub-reference: fix https://github.com/joaotavora/eglot/issues/865 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7bff005973..b84e1449d7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1436,7 +1436,7 @@ Doubles as an indicator of snippet support." (message-log-max nil)) (ignore-errors (delay-mode-hooks (funcall mode)))) (font-lock-ensure) - (string-trim (filter-buffer-substring (point-min) (point-max)))))) + (string-trim (buffer-string))))) (define-obsolete-variable-alias 'eglot-ignored-server-capabilites 'eglot-ignored-server-capabilities "1.8") commit 85ecf46a18c05d4182af3e3a11a8fadd421a28d0 Author: João Távora Date: Sun Mar 6 11:15:18 2022 +0000 Have a couple of lsp faces inherit from basic "shadow" * eglot.el (eglot-diagnostic-tag-unnecessary-face) (eglot-diagnostic-tag-deprecated-face): Inherit from 'shadow'. GitHub-reference: per https://github.com/joaotavora/eglot/issues/858 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3f84b3b7a2..7bff005973 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -258,11 +258,11 @@ CONTACT can be: "Face for package-name in EGLOT's mode line.") (defface eglot-diagnostic-tag-unnecessary-face - '((t . (:weight ultra-light))) + '((t (:inherit shadow))) "Face used to render unused or unnecessary code.") (defface eglot-diagnostic-tag-deprecated-face - '((t . (:strike-through t))) + '((t . (:inherit shadow :strike-through t))) "Face used to render deprecated or obsolete code.") (defcustom eglot-autoreconnect 3 commit 4f1f06375a219178ba681a6101af1fece73024b0 Author: Brian Leung Date: Tue Mar 1 07:59:05 2022 -0800 Prevent empty diagnostic tags vector hiding main fontification * eglot.el (eglot-handle-notification): Require that the resulting list of faces is non-empty and that each face corresponds only to a known tag. For unknown tags, we don't pass any additional face information to Flymake, and instead expect it to make the appropriate overlay with the "severity" property of the Diagnostic. Co-authored-by: João Távora GitHub-reference: fix https://github.com/joaotavora/eglot/issues/851 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5a0a8caba4..3f84b3b7a2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1866,11 +1866,11 @@ COMMAND is a symbol naming the command." (current-buffer) beg end (eglot--diag-type severity) message `((eglot-lsp-diag . ,diag-spec)) - (and tags - `((face - . ,(mapcar (lambda (tag) - (alist-get tag eglot--tag-faces)) - tags))))))) + (when-let ((faces + (cl-loop for tag across tags + when (alist-get tag eglot--tag-faces) + collect it))) + `((face . ,faces)))))) into diags finally (cond (eglot--current-flymake-report-fn (eglot--report-to-flymake diags)) commit 8b31247f1c6626b289e75460e0d0978bf446e05a Author: Brian Leung Date: Sat Jan 22 19:59:06 2022 -0800 Properly check the completionitem.deprecated property * eglot.el (eglot-completion-at-point): Check the :deprecated property is `t'. We do this so that a :deprecated property of :json-false does not cause a completion candidate to be incorrectly marked as deprecated. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 957ddde68c..5a0a8caba4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2472,7 +2472,7 @@ is not active." (when-let ((lsp-item (get-text-property 0 'eglot--lsp-item proxy))) (or (seq-contains-p (plist-get lsp-item :tags) 1) - (plist-get lsp-item :deprecated)))) + (eq t (plist-get lsp-item :deprecated))))) :company-docsig ;; FIXME: autoImportText is specific to the pyright language server (lambda (proxy) commit ed4fd33223e6cc2bafd7323abc91e0bee23accb4 Author: Stefan Kangas Date: Sat Jan 22 04:40:12 2022 +0100 * eglot.el (eglot-handle-notification): silence byte-compiler. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f109fc930f..957ddde68c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1828,8 +1828,8 @@ COMMAND is a symbol naming the command." "Handle notification telemetry/event.") ;; noop, use events buffer (cl-defmethod eglot-handle-notification - (server (_method (eql textDocument/publishDiagnostics)) &key uri diagnostics - &allow-other-keys) ; FIXME: doesn't respect `eglot-strict-mode' + (_server (_method (eql textDocument/publishDiagnostics)) &key uri diagnostics + &allow-other-keys) ; FIXME: doesn't respect `eglot-strict-mode' "Handle notification publishDiagnostics." (cl-flet ((eglot--diag-type (sev) (cond ((null sev) 'eglot-error) commit 97107540806a426c0951e572890b2827d41ebd43 Author: Brian Leung Date: Wed Jan 12 19:06:18 2022 -0800 Add support for optional completionitem.tags * eglot.el (eglot--lsp-interface-alist): Add optional CompletionItem.tags. (eglot-completion-at-point): Add :company-deprecated key and value, checking for either the appropriate tag (1) in the :tags property, or a truthy value for the :deprecated property. (eglot-client-capabilities): Advertise tagSupport (for tag == 1) and deprecatedSupport. Also load an updated version of seq.el in Emacsen < 27. * Makefile (ELPA_DEPS): Require latest seq.el. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d8308e0f97..f109fc930f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -7,7 +7,7 @@ ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.14") (flymake "1.2.1") (project "0.3.0") (xref "1.0.1") (eldoc "1.11.0")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.14") (flymake "1.2.1") (project "0.3.0") (xref "1.0.1") (eldoc "1.11.0") (seq "2.23")) ;; This file is part of GNU Emacs. @@ -62,7 +62,6 @@ (require 'imenu) (require 'cl-lib) (require 'project) -(require 'seq) (require 'url-parse) (require 'url-util) (require 'pcase) @@ -85,6 +84,11 @@ (load "eldoc") (require 'eldoc)) +;; Similar issue as above for Emacs 26.3 and seq.el. +(if (< emacs-major-version 27) + (load "seq") + (require 'seq)) + ;; forward-declare, but don't require (Emacs 28 doesn't seem to care) (defvar markdown-fontify-code-blocks-natively) (defvar company-backends) @@ -365,7 +369,7 @@ This can be useful when using docker to run a language server.") (:kind :detail :documentation :deprecated :preselect :sortText :filterText :insertText :insertTextFormat :textEdit :additionalTextEdits :commitCharacters - :command :data)) + :command :data :tags)) (Diagnostic (:range :message) (:severity :code :source :relatedInformation :codeDescription :tags)) (DocumentHighlight (:range) (:kind)) (FileSystemWatcher (:globPattern) (:kind)) @@ -659,7 +663,9 @@ treated as in `eglot-dbind'." `(:snippetSupport ,(if (eglot--snippet-expansion-fn) t - :json-false)) + :json-false) + :deprecatedSupport t + :tagSupport (:valueSet [1])) :contextSupport t) :hover (list :dynamicRegistration :json-false :contentFormat @@ -2461,6 +2467,12 @@ is not active." (kind (alist-get (plist-get lsp-item :kind) eglot--kind-names))) (intern (downcase kind)))) + :company-deprecated + (lambda (proxy) + (when-let ((lsp-item (get-text-property 0 'eglot--lsp-item proxy))) + (or (seq-contains-p (plist-get lsp-item :tags) + 1) + (plist-get lsp-item :deprecated)))) :company-docsig ;; FIXME: autoImportText is specific to the pyright language server (lambda (proxy) commit 6a6f4c3d27892ee5355d341ae9f586a48d217023 Author: Stefan Kangas Date: Sat Jan 22 04:13:11 2022 +0100 Don't use :exclusive no See https://github.com/joaotavora/eglot/issues/812 for background, in particular: https://github.com/joaotavora/eglot/issues/812#issuecomment-1014821345 * eglot.el (eglot-completion-at-point): Don't use :exclusive no, as it leads to breakage in many cases. GitHub-reference: close https://github.com/joaotavora/eglot/issues/812 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e27ddd7f94..d8308e0f97 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2489,7 +2489,6 @@ is not active." (regexp-opt (cl-coerce (cl-getf completion-capability :triggerCharacters) 'list)) (line-beginning-position)))) - :exclusive 'no :exit-function (lambda (proxy status) (when (eq status 'finished) commit 5b62d0071ec55ab8a38a0ee8f7fbd491f5731e22 Author: Theodor Thornhill Date: Thu Jan 20 23:06:53 2022 +0100 Enable lsp project-wide diagnostics via flymake * eglot.el (eglot-handle-notification): Pass on diagnostics from unvisited files to flymake. Enables project-wide-diagnostics, so that we can view all diagnostics in a given workspace. Uses new functionality from flymake 1.2.1, hence the version bump. * eglot-tests.el (project-wide-diagnostics-typescript): New tests showcasing the possibility to see all related diagnostics in a workspace. * eglot-tests.el (project-wide-diagnostics-rust-analyzer): New tests showcasing the possibility to see all related diagnostics in a workspace. * NEWS.md: Mention the new functionality * README.md: Mention the new functionality diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 10e1616274..e27ddd7f94 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -7,7 +7,7 @@ ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.14") (flymake "1.0.9") (project "0.3.0") (xref "1.0.1") (eldoc "1.11.0")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.14") (flymake "1.2.1") (project "0.3.0") (xref "1.0.1") (eldoc "1.11.0")) ;; This file is part of GNU Emacs. @@ -1825,49 +1825,66 @@ COMMAND is a symbol naming the command." (server (_method (eql textDocument/publishDiagnostics)) &key uri diagnostics &allow-other-keys) ; FIXME: doesn't respect `eglot-strict-mode' "Handle notification publishDiagnostics." - (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) - (with-current-buffer buffer - (cl-loop - for diag-spec across diagnostics - collect (eglot--dbind ((Diagnostic) range message severity source tags) - diag-spec - (setq message (concat source ": " message)) - (pcase-let - ((sev severity) - (`(,beg . ,end) (eglot--range-region range))) - ;; Fallback to `flymake-diag-region' if server - ;; botched the range - (when (= beg end) - (if-let* ((st (plist-get range :start)) - (diag-region - (flymake-diag-region - (current-buffer) (1+ (plist-get st :line)) - (plist-get st :character)))) - (setq beg (car diag-region) end (cdr diag-region)) - (eglot--widening - (goto-char (point-min)) - (setq beg - (point-at-bol - (1+ (plist-get (plist-get range :start) :line)))) - (setq end - (point-at-eol - (1+ (plist-get (plist-get range :end) :line))))))) - (eglot--make-diag (current-buffer) beg end - (cond ((null sev) 'eglot-error) - ((<= sev 1) 'eglot-error) - ((= sev 2) 'eglot-warning) - (t 'eglot-note)) - message `((eglot-lsp-diag . ,diag-spec)) - (and tags - `((face . ,(mapcar (lambda (tag) - (alist-get tag eglot--tag-faces)) - tags))))))) - into diags - finally (cond (eglot--current-flymake-report-fn - (eglot--report-to-flymake diags)) - (t - (setq eglot--unreported-diagnostics (cons t diags)))))) - (jsonrpc--debug server "Diagnostics received for unvisited %s" uri))) + (cl-flet ((eglot--diag-type (sev) + (cond ((null sev) 'eglot-error) + ((<= sev 1) 'eglot-error) + ((= sev 2) 'eglot-warning) + (t 'eglot-note)))) + (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) + (with-current-buffer buffer + (cl-loop + for diag-spec across diagnostics + collect (eglot--dbind ((Diagnostic) range message severity source tags) + diag-spec + (setq message (concat source ": " message)) + (pcase-let + ((`(,beg . ,end) (eglot--range-region range))) + ;; Fallback to `flymake-diag-region' if server + ;; botched the range + (when (= beg end) + (if-let* ((st (plist-get range :start)) + (diag-region + (flymake-diag-region + (current-buffer) (1+ (plist-get st :line)) + (plist-get st :character)))) + (setq beg (car diag-region) end (cdr diag-region)) + (eglot--widening + (goto-char (point-min)) + (setq beg + (point-at-bol + (1+ (plist-get (plist-get range :start) :line)))) + (setq end + (point-at-eol + (1+ (plist-get (plist-get range :end) :line))))))) + (eglot--make-diag + (current-buffer) beg end + (eglot--diag-type severity) + message `((eglot-lsp-diag . ,diag-spec)) + (and tags + `((face + . ,(mapcar (lambda (tag) + (alist-get tag eglot--tag-faces)) + tags))))))) + into diags + finally (cond (eglot--current-flymake-report-fn + (eglot--report-to-flymake diags)) + (t + (setq eglot--unreported-diagnostics (cons t diags)))))) + (cl-loop + with path = (expand-file-name (eglot--uri-to-path uri)) + for diag-spec across diagnostics + collect (eglot--dbind ((Diagnostic) range message severity source) diag-spec + (setq message (concat source ": " message)) + (let* ((start (plist-get range :start)) + (line (1+ (plist-get start :line))) + (char (1+ (plist-get start :character)))) + (eglot--make-diag + path (cons line char) nil (eglot--diag-type severity) message))) + into diags + finally + (setq flymake-list-only-diagnostics + (assoc-delete-all path flymake-list-only-diagnostics #'string=)) + (push (cons path diags) flymake-list-only-diagnostics))))) (cl-defun eglot--register-unregister (server things how) "Helper for `registerCapability'. commit 8a9959e054b1faf8305354e1ff7e6e1e92a216a2 Author: Derek Passen Date: Tue Jan 18 19:56:13 2022 -0600 Add clojure-lsp support for clojure * eglot.el (eglot-server-programs): Add clojure-lsp for Clojure. * README.md: Document the above change. Copyright-paperwork-exempt: Yes diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 219bcc85bd..10e1616274 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -187,7 +187,8 @@ language-server/bin/php-language-server.php")) (css-mode . ,(eglot-alternatives '(("vscode-css-language-server" "--stdio") ("css-languageserver" "--stdio")))) (html-mode . ,(eglot-alternatives '(("vscode-html-language-server" "--stdio") ("html-languageserver" "--stdio")))) (json-mode . ,(eglot-alternatives '(("vscode-json-language-server" "--stdio") ("json-languageserver" "--stdio")))) - (dockerfile-mode . ("docker-langserver" "--stdio"))) + (dockerfile-mode . ("docker-langserver" "--stdio")) + (clojure-mode . ("clojure-lsp"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE identifies the buffers that are to be managed by a specific commit 27ba1994fe50ebdcd927bb46fccc9db0d67e9408 Author: Stefan Kangas Date: Mon Jan 17 23:32:50 2022 +0100 ; fix thinko in last commit diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fb2f1ba100..219bcc85bd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -142,7 +142,7 @@ chosen (interactively or automatically)." when probe return (cons probe args) finally (funcall err))))))) -(defvar eglot-server-programs `((rust-mode . ("rust-analyzer" "rls")) +(defvar eglot-server-programs `((rust-mode . ,(eglot-alternatives '("rust-analyzer" "rls"))) (cmake-mode . ("cmake-language-server")) (vimrc-mode . ("vim-language-server" "--stdio")) (python-mode commit 3aeebe8186dfcc7e4a6df638bd44b4740895427f Author: Stefan Kangas Date: Fri Jan 14 13:03:44 2022 +0100 Change rust language server to rust-analyzer rust-analyzer is the officially blessed Language Server for Rust: https://github.com/rust-lang/rfcs/pull/2912 Also drop the special support code for RLS. * eglot.el (eglot-server-programs): Add rust-mode language server "rust-analyzer" and prefer it to the older "rls". (eglot-rls, jsonrpc-connection-ready-p) (eglot-handle-notification): Delete special support for "rls". * eglot-tests.el (rls-analyzer-watches-files) (rls-analyzer-hover-after-edit): Rename to ... (rust-analyzer-watches-files) (rust-analyzer-hover-after-edit): ... this. Update tests to work with rust-analyzer. * README.md: Update references for RLS to point to rust-analyzer. * NEWS.md: Announce above change. GitHub-reference: per https://github.com/joaotavora/eglot/issues/803 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 14a4d1cd43..fb2f1ba100 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -142,7 +142,7 @@ chosen (interactively or automatically)." when probe return (cons probe args) finally (funcall err))))))) -(defvar eglot-server-programs `((rust-mode . (eglot-rls "rls")) +(defvar eglot-server-programs `((rust-mode . ("rust-analyzer" "rls")) (cmake-mode . ("cmake-language-server")) (vimrc-mode . ("vim-language-server" "--stdio")) (python-mode @@ -2984,25 +2984,6 @@ If NOERROR, return predicate, else erroring function." (when (eq ?! (aref arg 1)) (aset arg 1 ?^)) `(,self () (re-search-forward ,(concat "\\=" arg)) (,next))) - -;;; Rust-specific -;;; -(defclass eglot-rls (eglot-lsp-server) () :documentation "Rustlang's RLS.") - -(cl-defmethod jsonrpc-connection-ready-p ((server eglot-rls) what) - "Except for :completion, RLS isn't ready until Indexing done." - (and (cl-call-next-method) - (or ;; RLS normally ready for this, even if building. - (eq :textDocument/completion what) - (pcase-let ((`(,_id ,what ,done ,_detail) (eglot--spinner server))) - (and (equal "Indexing" what) done))))) - -(cl-defmethod eglot-handle-notification - ((server eglot-rls) (_method (eql window/progress)) - &key id done title message &allow-other-keys) - "Handle notification window/progress." - (setf (eglot--spinner server) (list id title done message))) - ;;; eclipse-jdt-specific ;;; commit bc058058872a13e6ee3d9a04c6d37f2bba5a4852 Author: Stefan Kangas Date: Sun Jan 16 14:13:05 2022 +0100 ; unbreak tests on emacs 26 * eglot.el (eglot--plist-keys): Define in Emacs 26, no longer obsolete in Emacs 27 or later. (eglot--check-object): Go back to eglot--plist-keys. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9c9f055cfa..14a4d1cd43 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -451,11 +451,11 @@ on unknown notifications and errors on unknown requests.")) (eglot--interface interface-name) (when-let ((missing (and enforce-required (cl-set-difference required-keys - (map-keys object))))) + (eglot--plist-keys object))))) (eglot--error "A `%s' must have %s" interface-name missing)) (when-let ((excess (and disallow-non-standard (cl-set-difference - (map-keys object) + (eglot--plist-keys object) (append required-keys optional-keys))))) (eglot--error "A `%s' mustn't have %s" interface-name excess)) (when check-types @@ -580,7 +580,7 @@ treated as in `eglot-dbind'." ;; has all the keys the user wants to destructure. `(null (cl-set-difference ',vars-as-keywords - (map-keys ,obj-once))))) + (eglot--plist-keys ,obj-once))))) collect `(,condition (cl-destructuring-bind (&key ,@vars &allow-other-keys) ,obj-once @@ -3103,7 +3103,12 @@ If INTERACTIVE, prompt user for details." (make-obsolete-variable 'eglot--managed-mode-hook 'eglot-managed-mode-hook "1.6") -(define-obsolete-function-alias 'eglot--plist-keys #'map-keys "1.9") + +(if (< emacs-major-version 27) + (defun eglot--plist-keys (plist) + (cl-loop for (k _v) on plist by #'cddr collect k)) + ;; Make into an obsolete alias once we drop support for Emacs 26. + (defalias 'eglot--plist-keys #'map-keys)) (provide 'eglot) commit 8b0ea132cb5e5a44c345fe97ebdab7d8a0d511fc Author: Stefan Kangas Date: Sun Jan 16 13:05:16 2022 +0100 * eglot.el (eglot--server-capable): don't use obsolete name. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e7aa4ee185..9c9f055cfa 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1466,7 +1466,7 @@ under cursor." (defun eglot--server-capable (&rest feats) "Determine if current server is capable of FEATS." (unless (cl-some (lambda (feat) - (memq feat eglot-ignored-server-capabilites)) + (memq feat eglot-ignored-server-capabilities)) feats) (cl-loop for caps = (eglot--capabilities (eglot--current-server-or-lose)) then (cadr probe) commit 469835a4f20407d92549dd40a53652d107e26571 Author: Stefan Kangas Date: Sun Jan 16 13:05:13 2022 +0100 Obsolete eglot--plist-keys in favor of map-keys * eglot.el (eglot--plist-keys): Make into obsolete function alias for map-keys. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f3710fd3c9..e7aa4ee185 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -439,9 +439,6 @@ destructuring spec doesn't use all optional fields. If the symbol `disallow-unknown-methods' is present, Eglot warns on unknown notifications and errors on unknown requests.")) -(defun eglot--plist-keys (plist) - (cl-loop for (k _v) on plist by #'cddr collect k)) - (cl-defun eglot--check-object (interface-name object &optional @@ -454,11 +451,11 @@ on unknown notifications and errors on unknown requests.")) (eglot--interface interface-name) (when-let ((missing (and enforce-required (cl-set-difference required-keys - (eglot--plist-keys object))))) + (map-keys object))))) (eglot--error "A `%s' must have %s" interface-name missing)) (when-let ((excess (and disallow-non-standard (cl-set-difference - (eglot--plist-keys object) + (map-keys object) (append required-keys optional-keys))))) (eglot--error "A `%s' mustn't have %s" interface-name excess)) (when check-types @@ -583,7 +580,7 @@ treated as in `eglot-dbind'." ;; has all the keys the user wants to destructure. `(null (cl-set-difference ',vars-as-keywords - (eglot--plist-keys ,obj-once))))) + (map-keys ,obj-once))))) collect `(,condition (cl-destructuring-bind (&key ,@vars &allow-other-keys) ,obj-once @@ -3106,6 +3103,7 @@ If INTERACTIVE, prompt user for details." (make-obsolete-variable 'eglot--managed-mode-hook 'eglot-managed-mode-hook "1.6") +(define-obsolete-function-alias 'eglot--plist-keys #'map-keys "1.9") (provide 'eglot) commit 97ded8227795da62d78d8b1952efd7d0351e492f Author: Stefan Kangas Date: Sun Jan 16 13:04:27 2022 +0100 ; * eglot.el: move obsolete definition to new section. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0cc317fa7a..f3710fd3c9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1565,9 +1565,6 @@ For example, to keep your Company customization use "Tell if current buffer is managed by EGLOT." eglot--managed-mode) -(make-obsolete-variable - 'eglot--managed-mode-hook 'eglot-managed-mode-hook "1.6") - (defvar eglot-managed-mode-hook nil "A hook run by EGLOT after it started/stopped managing a buffer. Use `eglot-managed-p' to determine if current buffer is managed.") @@ -3103,11 +3100,19 @@ If INTERACTIVE, prompt user for details." "Eclipse JDT breaks spec and replies with edits as arguments." (mapc #'eglot--apply-workspace-edit arguments)) + +;;; Obsolete +;;; + +(make-obsolete-variable 'eglot--managed-mode-hook + 'eglot-managed-mode-hook "1.6") + (provide 'eglot) -;;; eglot.el ends here ;; Local Variables: ;; bug-reference-bug-regexp: "\\(github#\\([0-9]+\\)\\)" ;; bug-reference-url-format: "https://github.com/joaotavora/eglot/issues/%s" ;; checkdoc-force-docstrings-flag: nil ;; End: + +;;; eglot.el ends here commit abfb193201dd064c99a8c869434ffaacc4e0f66b Author: Stefan Kangas Date: Sun Jan 16 00:12:05 2022 +0100 Remove unnecessary compatibility code * eglot.el (eglot-mode-map): Remove unnecessary compatibility code. We already depend on eldoc 0.11.0. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 72a0103c81..0cc317fa7a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1519,8 +1519,7 @@ and just return it. PROMPT shouldn't end with a question mark." ;;; (defvar eglot-mode-map (let ((map (make-sparse-keymap))) - (when (fboundp 'eldoc-doc-buffer) ; Emacs 28.1 or later - (define-key map [remap display-local-help] #'eldoc-doc-buffer)) + (define-key map [remap display-local-help] #'eldoc-doc-buffer) map)) (defvar-local eglot--current-flymake-report-fn nil commit 0739cdcf20850aa94c7bbfd9791e344fe9202fd1 Author: Stefan Kangas Date: Sat Jan 15 17:37:42 2022 +0100 Improve backwards-compatibility of eglot-mode-map * eglot.el (eglot-mode-map): Only bind to eldoc-doc-buffer in Emacs 28.1 or later. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bcd67205b5..72a0103c81 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1519,7 +1519,8 @@ and just return it. PROMPT shouldn't end with a question mark." ;;; (defvar eglot-mode-map (let ((map (make-sparse-keymap))) - (define-key map [remap display-local-help] 'eldoc-doc-buffer) + (when (fboundp 'eldoc-doc-buffer) ; Emacs 28.1 or later + (define-key map [remap display-local-help] #'eldoc-doc-buffer)) map)) (defvar-local eglot--current-flymake-report-fn nil commit 1616da4f26158e66c32e4f9e5f07517a224a451a Author: Stefan Kangas Date: Sat Jan 15 17:35:19 2022 +0100 * eglot.el (eglot-strict-mode): very minor docfix. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bf3e54c6b4..bcd67205b5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -437,8 +437,7 @@ happens at run-time. At compile-time, a warning is raised if a destructuring spec doesn't use all optional fields. If the symbol `disallow-unknown-methods' is present, Eglot warns -on unknown notifications and errors on unknown requests. -")) +on unknown notifications and errors on unknown requests.")) (defun eglot--plist-keys (plist) (cl-loop for (k _v) on plist by #'cddr collect k)) commit ed9800041ef2cc05145865134371fa3f0b846497 Author: Stefan Kangas Date: Sat Jan 15 13:47:30 2022 +0100 * eglot.el (eglot--connect): display seconds on timeout. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9536a72a26..bf3e54c6b4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1208,7 +1208,8 @@ in project `%s'." :timeout-fn (lambda () (unless cancelled (jsonrpc-shutdown server) - (let ((msg (format "Timed out"))) + (let ((msg (format "Timed out after %s seconds" + eglot-connect-timeout))) (if tag (throw tag `(error . ,msg)) (eglot--error msg)))))) (cond ((numberp eglot-sync-connect) commit 26bd153b9087635ca4a820e253ac0fe6ca21df64 Author: Stefan Kangas Date: Thu Jan 13 00:50:35 2022 +0100 Print server command to events buffer * eglot.el (eglot--connect): Print server command to events buffer. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/476 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b97e3a4e66..9536a72a26 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1078,16 +1078,20 @@ This docstring appeases checkdoc, that's all." (nickname (file-name-base (directory-file-name default-directory))) (readable-name (format "EGLOT (%s/%s)" nickname managed-major-mode)) autostart-inferior-process + server-info (contact (if (functionp contact) (funcall contact) contact)) (initargs (cond ((keywordp (car contact)) contact) ((integerp (cadr contact)) + (setq server-info (list (format "%s:%s" (car contact) + (cadr contact)))) `(:process ,(lambda () (apply #'open-network-stream readable-name nil (car contact) (cadr contact) (cddr contact))))) ((and (stringp (car contact)) (memq :autoport contact)) + (setq server-info (list "")) `(:process ,(lambda () (pcase-let ((`(,connection . ,inferior) (eglot--inferior-bootstrap @@ -1101,7 +1105,7 @@ This docstring appeases checkdoc, that's all." (let ((default-directory default-directory)) (make-process :name readable-name - :command (eglot--cmd contact) + :command (setq server-info (eglot--cmd contact)) :connection-type 'pipe :coding 'utf-8-emacs-unix :noquery t @@ -1122,6 +1126,9 @@ This docstring appeases checkdoc, that's all." initargs)) (cancelled nil) (tag (make-symbol "connected-catch-tag"))) + (when server-info + (jsonrpc--debug server "Running language server: %s" + (string-join server-info " "))) (setf (eglot--saved-initargs server) initargs) (setf (eglot--project server) project) (setf (eglot--project-nickname server) nickname) commit f199060ee023bc47f0c4260ca88e9072750ec2b7 Author: Stefan Kangas Date: Thu Jan 13 22:47:36 2022 +0100 ; * eglot.el (eglot-server-initialized-hook): fix punctuation. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4e92bf09bf..b97e3a4e66 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1046,7 +1046,7 @@ Use current server's or first available Eglot events buffer." '() "Hook run after a `eglot-lsp-server' instance is created. -That is before a connection was established. Use +That is before a connection was established. Use `eglot-connect-hook' to hook into when a connection was successfully established and the server on the other side has received the initializing configuration. commit 5b88ec259ccf24b87a7c8c3d35173a2353dd8517 Author: Theodor Thornhill Date: Mon Dec 30 16:26:22 2019 +0100 Change from symbol-at-point to thing-at-point * eglot.el (eglot-rename): Change from symbol-at-point to thing-at-point to avoid interning a symbol. Add "unknown symbol" to prompt when no symbol is found. Co-authored-by: João Távora GitHub-reference: close https://github.com/joaotavora/eglot/issues/385 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index dc20ef751b..4e92bf09bf 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2774,9 +2774,11 @@ is not active." (defun eglot-rename (newname) "Rename the current symbol to NEWNAME." (interactive - (list (read-from-minibuffer (format "Rename `%s' to: " (symbol-at-point)) - nil nil nil nil - (symbol-name (symbol-at-point))))) + (list (read-from-minibuffer + (format "Rename `%s' to: " (or (thing-at-point 'symbol t) + "unknown symbol")) + nil nil nil nil + (symbol-name (symbol-at-point))))) (unless (eglot--server-capable :renameProvider) (eglot--error "Server can't rename!")) (eglot--apply-workspace-edit commit 0f44d338f17bd426ab43f6a619873c9ac91bc51e Author: Brian Leung Date: Mon Jan 10 21:48:21 2022 -0800 Support optional diagnostic.tags https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#diagnosticTag A DiagnosticTag can be either 1 (DiagnosticTag.Unnecessary) or 2 (DiagnosticTag.Deprecated). Following the rendering suggestions in the protocol, we fade out Unnecessary code and strike-through Deprecated code. * eglot.el (eglot-diagnostic-tag-unnecessary-face) (eglot-diagnostic-tag-deprecated-face): New faces. (eglot--tag-faces): New defconst. (eglot--lsp-interface-alist): Add Diagnostic.tags. (eglot-client-capabilities): Advertise supported tags. (eglot-handle-notification): Assign the appropriate properties. * eglot-tests.el (diagnostic-tags-unnecessary-code): New test. GitHub-reference: per https://github.com/joaotavora/eglot/issues/794 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 813b29775e..dc20ef751b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -252,6 +252,14 @@ CONTACT can be: '((t (:inherit font-lock-constant-face :weight bold))) "Face for package-name in EGLOT's mode line.") +(defface eglot-diagnostic-tag-unnecessary-face + '((t . (:weight ultra-light))) + "Face used to render unused or unnecessary code.") + +(defface eglot-diagnostic-tag-deprecated-face + '((t . (:strike-through t))) + "Face used to render deprecated or obsolete code.") + (defcustom eglot-autoreconnect 3 "Control ability to reconnect automatically to the LSP server. If t, always reconnect automatically (not recommended). If nil, @@ -332,6 +340,10 @@ This can be useful when using docker to run a language server.") (21 . "Constant") (22 . "Struct") (23 . "Event") (24 . "Operator") (25 . "TypeParameter"))) +(defconst eglot--tag-faces + `((1 . eglot-diagnostic-tag-unnecessary-face) + (2 . eglot-diagnostic-tag-deprecated-face))) + (defconst eglot--{} (make-hash-table) "The empty JSON object.") (defun eglot--executable-find (command &optional remote) @@ -353,7 +365,7 @@ This can be useful when using docker to run a language server.") :sortText :filterText :insertText :insertTextFormat :textEdit :additionalTextEdits :commitCharacters :command :data)) - (Diagnostic (:range :message) (:severity :code :source :relatedInformation :codeDescription)) + (Diagnostic (:range :message) (:severity :code :source :relatedInformation :codeDescription :tags)) (DocumentHighlight (:range) (:kind)) (FileSystemWatcher (:globPattern) (:kind)) (Hover (:contents) (:range)) @@ -695,7 +707,11 @@ treated as in `eglot-dbind'." ;; TODO: We can support :codeDescription after ;; adding an appropriate UI to ;; Flymake. - :codeDescriptionSupport :json-false)) + :codeDescriptionSupport :json-false + :tagSupport + `(:valueSet + [,@(mapcar + #'car eglot--tag-faces)]))) :experimental eglot--{}))) (defclass eglot-lsp-server (jsonrpc-process-connection) @@ -1811,7 +1827,7 @@ COMMAND is a symbol naming the command." (with-current-buffer buffer (cl-loop for diag-spec across diagnostics - collect (eglot--dbind ((Diagnostic) range message severity source) + collect (eglot--dbind ((Diagnostic) range message severity source tags) diag-spec (setq message (concat source ": " message)) (pcase-let @@ -1839,7 +1855,11 @@ COMMAND is a symbol naming the command." ((<= sev 1) 'eglot-error) ((= sev 2) 'eglot-warning) (t 'eglot-note)) - message `((eglot-lsp-diag . ,diag-spec))))) + message `((eglot-lsp-diag . ,diag-spec)) + (and tags + `((face . ,(mapcar (lambda (tag) + (alist-get tag eglot--tag-faces)) + tags))))))) into diags finally (cond (eglot--current-flymake-report-fn (eglot--report-to-flymake diags)) commit a905bad63305b456eeaaa5d716b9f7fcb60ec5ab Author: Stefan Kangas Date: Thu Jan 13 12:30:17 2022 +0100 * eglot.el: improve commentary section. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4fa2b073b1..813b29775e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -26,35 +26,36 @@ ;;; Commentary: -;; Simply M-x eglot should be enough to get you started, but here's a +;; Eglot ("Emacs Polyglot") is an Emacs LSP client that stays out of +;; your way. +;; +;; Typing M-x eglot should be enough to get you started, but here's a ;; little info (see the accompanying README.md or the URL for more). ;; -;; M-x eglot starts a server via a shell-command guessed from -;; `eglot-server-programs', using the current major-mode (for whatever +;; M-x eglot starts a server via a shell command guessed from +;; `eglot-server-programs', using the current major mode (for whatever ;; language you're programming in) as a hint. If it can't guess, it -;; prompts you in the mini-buffer for these things. Actually, the -;; server needen't be locally started: you can connect to a running -;; server via TCP by entering a syntax. +;; prompts you in the minibuffer for these things. Actually, the +;; server does not need to be running locally: you can connect to a +;; running server via TCP by entering a syntax. ;; -;; Anyway, if the connection is successful, you should see an `eglot' +;; If the connection is successful, you should see an `eglot' ;; indicator pop up in your mode-line. More importantly, this means -;; current *and future* file buffers of that major mode *inside your -;; current project* automatically become \"managed\" by the LSP -;; server, i.e. information about their contents is exchanged -;; periodically to provide enhanced code analysis via +;; that current *and future* file buffers of that major mode *inside +;; your current project* automatically become \"managed\" by the LSP +;; server. In other words, information about their content is +;; exchanged periodically to provide enhanced code analysis using ;; `xref-find-definitions', `flymake-mode', `eldoc-mode', ;; `completion-at-point', among others. ;; -;; To "unmanage" these buffers, shutdown the server with M-x -;; eglot-shutdown. +;; To "unmanage" these buffers, shutdown the server with +;; M-x eglot-shutdown. ;; -;; You can also do: +;; To start an eglot session automatically when a foo-mode buffer is +;; visited, you can put this in your init file: ;; ;; (add-hook 'foo-mode-hook 'eglot-ensure) -;; -;; To attempt to start an eglot session automatically every time a -;; foo-mode buffer is visited. -;; + ;;; Code: (require 'json) commit b527764963273999c8d466420e18922bc82650e0 Author: Stefan Kangas Date: Thu Jan 13 11:52:29 2022 +0100 Support racket-langserver * eglot.el (eglot-server-programs): Support racket-langserver. * README.md: * NEWS.md: Update for above changes. GitHub-reference: per https://github.com/joaotavora/eglot/issues/694 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1d6d298ce8..4fa2b073b1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -173,6 +173,7 @@ language-server/bin/php-language-server.php")) (elixir-mode . ("language_server.sh")) (ada-mode . ("ada_language_server")) (scala-mode . ("metals-emacs")) + (racket-mode . ("racket" "-l" "racket-langserver")) ((tex-mode context-mode texinfo-mode bibtex-mode) . ("digestif")) (erlang-mode . ("erlang_ls" "--transport" "stdio")) commit c06860b0f47e74a22f61eb2a93e6486efdfb11c7 Author: Stefan Kangas Date: Wed Jan 12 19:35:22 2022 +0100 Bump eglot version to 1.8 * eglot.el (Version): Bump to 1.8. * NEWS.md (1.8): Rename header from "(upcoming)". diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bdd0dcccc5..1d6d298ce8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018-2022 Free Software Foundation, Inc. -;; Version: 1.7 +;; Version: 1.8 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot commit 9adb310e08410b555651d7b819671842bd99f2c1 Author: Brian Leung Date: Mon Jan 10 19:32:19 2022 -0800 Don't error out on unsupported diagnostic.codedescription A codeDescription property is, at the time of writing, an object with an href property (of type URI, or a string), denoting a "URI to open with more information about the diagnostic error". It's not obvious how best to put this into a Flymake diagostic aside from simply appending it to the diagnostic message, so we'll worry about it some other time. * eglot.el (eglot--lsp-interface-alist) (eglot-client-capabilities): Don't error out on unsupported Diagnostic.codeDescription. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/768 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 80eb58f579..bdd0dcccc5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -351,7 +351,7 @@ This can be useful when using docker to run a language server.") :sortText :filterText :insertText :insertTextFormat :textEdit :additionalTextEdits :commitCharacters :command :data)) - (Diagnostic (:range :message) (:severity :code :source :relatedInformation)) + (Diagnostic (:range :message) (:severity :code :source :relatedInformation :codeDescription)) (DocumentHighlight (:range) (:kind)) (FileSystemWatcher (:globPattern) (:kind)) (Hover (:contents) (:range)) @@ -689,7 +689,11 @@ treated as in `eglot-dbind'." :formatting `(:dynamicRegistration :json-false) :rangeFormatting `(:dynamicRegistration :json-false) :rename `(:dynamicRegistration :json-false) - :publishDiagnostics `(:relatedInformation :json-false)) + :publishDiagnostics (list :relatedInformation :json-false + ;; TODO: We can support :codeDescription after + ;; adding an appropriate UI to + ;; Flymake. + :codeDescriptionSupport :json-false)) :experimental eglot--{}))) (defclass eglot-lsp-server (jsonrpc-process-connection) commit 34c7da506d85d7bb160e60957163eef941804364 Author: Fredrik Bergroth Date: Wed Dec 15 13:05:22 2021 +0100 Support autoimporttext from pyright language server * eglot.el (eglot-completion-at-point): show autoImportText via company-docsig. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/769 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 85220f2cd5..80eb58f579 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2416,6 +2416,13 @@ is not active." (kind (alist-get (plist-get lsp-item :kind) eglot--kind-names))) (intern (downcase kind)))) + :company-docsig + ;; FIXME: autoImportText is specific to the pyright language server + (lambda (proxy) + (when-let* ((lsp-comp (get-text-property 0 'eglot--lsp-item proxy)) + (data (plist-get (funcall resolve-maybe lsp-comp) :data)) + (import-text (plist-get data :autoImportText))) + import-text)) :company-doc-buffer (lambda (proxy) (let* ((documentation commit 09c071d3d1feee5c50afb2ba3e80b4f5c79ca793 Author: Felicián Németh Date: Mon Jan 10 20:21:21 2022 +0100 Add tooltip describing pending requests * eglot.el (eglot--mode-line-format): Add tooltip to `pending'. GitHub-reference: per https://github.com/joaotavora/eglot/issues/784 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 922b9225d2..85220f2cd5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1720,7 +1720,9 @@ Uses THING, FACE, DEFS and PREPEND." `("/" ,(eglot--mode-line-props (format "%d" pending) 'warning '((mouse-3 eglot-forget-pending-continuations - "forget pending continuations")))))))))) + "forget pending continuations")) + "Number of outgoing, \ +still unanswered LSP requests to the server")))))))) (add-to-list 'mode-line-misc-info `(eglot--managed-mode (" [" eglot--mode-line-format "] "))) commit f0b9018f521292d970cdaed99c47e02b60026e52 Author: Brian Leung Date: Sat Jan 8 18:08:23 2022 -0800 Properly print error message of eglot-alternatives * eglot.el (eglot-alternatives): Work with the listified form. This allows presumed executables provided as (EXECUTABLE &rest ARGS...) to be displayed in the error. GitHub-reference: per https://github.com/joaotavora/eglot/issues/786 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ef9b371af8..922b9225d2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -114,7 +114,7 @@ chosen (interactively or automatically)." collect (if (listp a) a (list a)))) (err (lambda () (error "None of '%s' are valid executables" - (mapconcat #'identity alternatives ", "))))) + (mapconcat #'car listified ", "))))) (cond (interactive (let* ((augmented (mapcar (lambda (a) (let ((found (eglot--executable-find commit 49e46c3d5333b0aa10f0eeefba03f95b8a4a9862 Author: Brian Leung Date: Sat Jan 8 17:43:23 2022 -0800 Add up-to-date server executables for html/css/json * README.md: Advertise updated executables. * eglot.el (eglot-server-programs): Prioritize the alternatives. The {html,css,json}-languageserver executables that are distributed outside VS Code are not regularly updated by Microsoft; any relevant updates to the VS Code source tree reach VS Code users without the need for VS Code developers to go out of their way to publish new versions of the executables. Consequently, users of other editors who have been using the server executables from the most obvious NPM packages are likely using stale versions. @hrsh7th, a Vim user, created an NPM package with updated versions of these executables taken straight from VS Code's source tree. We therefore prefer to direct users to the corresponding repo, which contains appropriate installation instructions, in the README. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2a8000cce3..ef9b371af8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -182,9 +182,9 @@ language-server/bin/php-language-server.php")) ((fortran-mode f90-mode) . ("fortls")) (lua-mode . ("lua-lsp")) (zig-mode . ("zls")) - (css-mode "css-languageserver" "--stdio") - (html-mode "html-languageserver" "--stdio") - (json-mode "json-languageserver" "--stdio") + (css-mode . ,(eglot-alternatives '(("vscode-css-language-server" "--stdio") ("css-languageserver" "--stdio")))) + (html-mode . ,(eglot-alternatives '(("vscode-html-language-server" "--stdio") ("html-languageserver" "--stdio")))) + (json-mode . ,(eglot-alternatives '(("vscode-json-language-server" "--stdio") ("json-languageserver" "--stdio")))) (dockerfile-mode . ("docker-langserver" "--stdio"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE commit ae7315b5f11dc22ef0ebaa1f9ce1546d2ba34ab8 Author: Stefan Kangas Date: Sun Jan 9 17:41:40 2022 +0100 ; fix license statement Packages in GNU ELPA are considered part of GNU Emacs. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8163b7f918..2a8000cce3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -9,23 +9,25 @@ ;; Keywords: convenience, languages ;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.14") (flymake "1.0.9") (project "0.3.0") (xref "1.0.1") (eldoc "1.11.0")) -;; This program is free software; you can redistribute it and/or modify +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. -;; This program is distributed in the hope that it will be useful, +;; GNU Emacs is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License -;; along with this program. If not, see . +;; along with GNU Emacs. If not, see . ;;; Commentary: ;; Simply M-x eglot should be enough to get you started, but here's a - ;; little info (see the accompanying README.md or the URL for more). +;; little info (see the accompanying README.md or the URL for more). ;; ;; M-x eglot starts a server via a shell-command guessed from ;; `eglot-server-programs', using the current major-mode (for whatever commit eacc40e3bfcf316a52b48185383262821a17820d Author: Stefan Kangas Date: Sun Jan 9 17:38:40 2022 +0100 ; update copyright years diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bf2e9c0fd9..8163b7f918 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1,6 +1,6 @@ ;;; eglot.el --- Client for Language Server Protocol (LSP) servers -*- lexical-binding: t; -*- -;; Copyright (C) 2018-2021 Free Software Foundation, Inc. +;; Copyright (C) 2018-2022 Free Software Foundation, Inc. ;; Version: 1.7 ;; Author: João Távora commit a8b3b6a5263798c990a5f5c46a074b51355513cd Author: Brian Leung Date: Sat Jan 8 18:30:57 2022 -0800 Add vim-language-server for vimrc-mode * README.md: Advertise. * eglot.el (eglot-server-programs): Add vim-language-server. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3cd006d169..bf2e9c0fd9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -141,6 +141,7 @@ chosen (interactively or automatically)." (defvar eglot-server-programs `((rust-mode . (eglot-rls "rls")) (cmake-mode . ("cmake-language-server")) + (vimrc-mode . ("vim-language-server" "--stdio")) (python-mode . ,(eglot-alternatives '("pylsp" "pyls" ("pyright-langserver" "--stdio")))) commit be1e214fb2911e4563298e5542bfa98dc23bf332 Author: Brian Leung Date: Sat Jan 8 18:27:29 2022 -0800 Add cmake-language-server for cmake-mode * README.md: Advertise. * eglot.el (eglot-server-programs): Add cmake-language-server. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 72cf0c44fc..3cd006d169 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -140,6 +140,7 @@ chosen (interactively or automatically)." finally (funcall err))))))) (defvar eglot-server-programs `((rust-mode . (eglot-rls "rls")) + (cmake-mode . ("cmake-language-server")) (python-mode . ,(eglot-alternatives '("pylsp" "pyls" ("pyright-langserver" "--stdio")))) commit a218f52ec9b0324b7026ae449067167ab833d503 Author: Stefan Kangas Date: Sun Jan 9 02:50:38 2022 +0100 Un-reverse references in xref buffer GitHub-reference: fix https://github.com/joaotavora/eglot/issues/763 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 712ad17298..72cf0c44fc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2148,6 +2148,7 @@ may be called multiple times (respecting the protocol of (let (,collected) (cl-flet ((,collector (xref) (push xref ,collected))) ,@body) + (setq ,collected (nreverse ,collected)) (sort ,collected eglot-xref-lessp-function)) (maphash (lambda (_uri buf) (kill-buffer buf)) eglot--temp-location-buffers) (clrhash eglot--temp-location-buffers)))) commit 512d8b9f590df3b553526bffb7b39ee4f0cd76e9 Author: Martin Carlson Date: Tue Aug 31 12:24:34 2021 +0200 Add variable to withhold the init req process id * eglot.el (eglot-withhold-process-id): New defvar. (eglot--connect): Don't send pid to language server if above new defvar has a non-nil value. Copyright-paperwork-exempt: Yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/722 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e1eb9ed4ce..712ad17298 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -295,6 +295,10 @@ let the buffer grow forever." "If non-nil, activate Eglot in cross-referenced non-project files." :type 'boolean) +(defvar eglot-withhold-process-id nil + "If non-nil, Eglot will not send the Emacs process id to the language server. +This can be useful when using docker to run a language server.") + ;; Customizable via `completion-category-overrides'. (when (assoc 'flex completion-styles-alist) (add-to-list 'completion-category-defaults '(eglot (styles flex basic)))) @@ -1110,7 +1114,8 @@ This docstring appeases checkdoc, that's all." server :initialize (list :processId - (unless (or (file-remote-p default-directory) + (unless (or eglot-withhold-process-id + (file-remote-p default-directory) (eq (jsonrpc-process-type server) 'network)) (emacs-pid)) commit a35f6a7f9abe2e67ec76e932bdb9f4424cd12fc2 Author: NA Date: Fri Jan 11 05:58:04 2019 +0200 Support language server for html, css, json and docker * eglot.el (eglot-server-programs): Support html-languageserver, css-languageserver, json-languageserver, and docker-langserver. * README.md: Update documentation for above changes. Copyright-paperwork-exempt: Yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/204 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 234e4f14e7..e1eb9ed4ce 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -177,7 +177,11 @@ language-server/bin/php-language-server.php")) (gdscript-mode . ("localhost" 6008)) ((fortran-mode f90-mode) . ("fortls")) (lua-mode . ("lua-lsp")) - (zig-mode . ("zls"))) + (zig-mode . ("zls")) + (css-mode "css-languageserver" "--stdio") + (html-mode "html-languageserver" "--stdio") + (json-mode "json-languageserver" "--stdio") + (dockerfile-mode . ("docker-langserver" "--stdio"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE identifies the buffers that are to be managed by a specific commit 7c712abf8f8e6f9a85b80339c00316c35b4beb14 Author: Stefan Kangas Date: Sat Jan 8 19:43:29 2022 +0100 Minor checkdoc fixes * eglot.el (eglot, eglot--when-buffer-window, eglot--widening) (eglot-initialization-options, eglot--current-flymake-report-fn) (eglot-handle-notification, eglot-handle-request) (eglot--highlight-piggyback, eglot-register-capability) (eglot-unregister-capability): * eglot-tests.el (auto-detect-running-server, auto-shutdown) (auto-reconnect, eglot--tests-force-full-eldoc, rename-a-symbol) (basic-completions, non-unique-completions, basic-xref) (snippet-completions, snippet-completions-with-company) (eglot-eldoc-after-completions, python-yapf-formatting) (javascript-basic, json-basic, eglot-ensure) (eglot--guessing-contact): Doc fixes; formatting. * eglot.el (xref-backend-identifier-completion-table): Fix error format. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e9f48bb3c3..234e4f14e7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -91,7 +91,7 @@ ;;; User tweakable stuff (defgroup eglot nil - "Interaction with Language Server Protocol servers" + "Interaction with Language Server Protocol servers." :prefix "eglot-" :group 'applications) @@ -577,7 +577,7 @@ treated as in `eglot-dbind'." `(let ((,b ,buf)) (if (buffer-live-p ,b) (with-current-buffer ,b ,@body))))) (cl-defmacro eglot--when-buffer-window (buf &body body) - "Check BUF showing somewhere, then do BODY in it" (declare (indent 1) (debug t)) + "Check BUF showing somewhere, then do BODY in it." (declare (indent 1) (debug t)) (let ((b (cl-gensym))) `(let ((,b ,buf)) ;;notice the exception when testing with `ert' @@ -585,7 +585,7 @@ treated as in `eglot-dbind'." (with-current-buffer ,b ,@body))))) (cl-defmacro eglot--widening (&rest body) - "Save excursion and restriction. Widen. Then run BODY." (declare (debug t)) + "Save excursion and restriction. Widen. Then run BODY." (declare (debug t)) `(save-excursion (save-restriction (widen) ,@body))) (cl-defgeneric eglot-handle-request (server method &rest params) @@ -598,7 +598,7 @@ treated as in `eglot-dbind'." "Ask SERVER to execute COMMAND with ARGUMENTS.") (cl-defgeneric eglot-initialization-options (server) - "JSON object to send under `initializationOptions'" + "JSON object to send under `initializationOptions'." (:method (_s) eglot--{})) ; blank default (cl-defgeneric eglot-register-capability (server method id &rest params) @@ -1481,7 +1481,7 @@ and just return it. PROMPT shouldn't end with a question mark." map)) (defvar-local eglot--current-flymake-report-fn nil - "Current flymake report function for this buffer") + "Current flymake report function for this buffer.") (defvar-local eglot--saved-bindings nil "Bindings saved by `eglot--setq-saving'.") @@ -1734,14 +1734,14 @@ Uses THING, FACE, DEFS and PREPEND." ;;; (cl-defmethod eglot-handle-notification (_server method &key &allow-other-keys) - "Handle unknown notification" + "Handle unknown notification." (unless (or (string-prefix-p "$" (format "%s" method)) (not (memq 'disallow-unknown-methods eglot-strict-mode))) (eglot--warn "Server sent unknown notification method `%s'" method))) (cl-defmethod eglot-handle-request (_server method &key &allow-other-keys) - "Handle unknown request" + "Handle unknown request." (when (memq 'disallow-unknown-methods eglot-strict-mode) (jsonrpc-error "Unknown request method `%s'" method))) @@ -1754,14 +1754,14 @@ COMMAND is a symbol naming the command." (cl-defmethod eglot-handle-notification (_server (_method (eql window/showMessage)) &key type message) - "Handle notification window/showMessage" + "Handle notification window/showMessage." (eglot--message (propertize "Server reports (type=%s): %s" 'face (if (<= type 1) 'error)) type message)) (cl-defmethod eglot-handle-request (_server (_method (eql window/showMessageRequest)) &key type message actions) - "Handle server request window/showMessageRequest" + "Handle server request window/showMessageRequest." (let* ((actions (append actions nil)) ;; gh#627 (label (completing-read (concat @@ -1776,16 +1776,16 @@ COMMAND is a symbol naming the command." (cl-defmethod eglot-handle-notification (_server (_method (eql window/logMessage)) &key _type _message) - "Handle notification window/logMessage") ;; noop, use events buffer + "Handle notification window/logMessage.") ;; noop, use events buffer (cl-defmethod eglot-handle-notification (_server (_method (eql telemetry/event)) &rest _any) - "Handle notification telemetry/event") ;; noop, use events buffer + "Handle notification telemetry/event.") ;; noop, use events buffer (cl-defmethod eglot-handle-notification (server (_method (eql textDocument/publishDiagnostics)) &key uri diagnostics &allow-other-keys) ; FIXME: doesn't respect `eglot-strict-mode' - "Handle notification publishDiagnostics" + "Handle notification publishDiagnostics." (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) (with-current-buffer buffer (cl-loop @@ -1839,18 +1839,18 @@ THINGS are either registrations or unregisterations (sic)." (cl-defmethod eglot-handle-request (server (_method (eql client/registerCapability)) &key registrations) - "Handle server request client/registerCapability" + "Handle server request client/registerCapability." (eglot--register-unregister server registrations 'register)) (cl-defmethod eglot-handle-request (server (_method (eql client/unregisterCapability)) &key unregisterations) ;; XXX: "unregisterations" (sic) - "Handle server request client/unregisterCapability" + "Handle server request client/unregisterCapability." (eglot--register-unregister server unregisterations 'unregister)) (cl-defmethod eglot-handle-request (_server (_method (eql workspace/applyEdit)) &key _label edit) - "Handle server request workspace/applyEdit" + "Handle server request workspace/applyEdit." (eglot--apply-workspace-edit edit eglot-confirm-server-initiated-edits)) (defun eglot--TextDocumentIdentifier () @@ -2180,7 +2180,7 @@ Try to visit the target file for a richer summary line." (xref-make-match summary (xref-make-file-location file line column) length))) (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) - (eglot--error "cannot (yet) provide reliable completion table for LSP symbols")) + (eglot--error "Cannot (yet) provide reliable completion table for LSP symbols")) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) ;; JT@19/10/09: This is a totally dummy identifier that isn't even @@ -2578,7 +2578,7 @@ is not active." (defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") (defun eglot--highlight-piggyback (_cb) - "Request and handle `:textDocument/documentHighlight'" + "Request and handle `:textDocument/documentHighlight'." ;; FIXME: Obviously, this is just piggy backing on eldoc's calls for ;; convenience, as shown by the fact that we just ignore cb. (let ((buf (current-buffer))) @@ -2821,7 +2821,7 @@ at point. With prefix argument, prompt for ACTION-KIND." ;;; (cl-defmethod eglot-register-capability (server (method (eql workspace/didChangeWatchedFiles)) id &key watchers) - "Handle dynamic registration of workspace/didChangeWatchedFiles" + "Handle dynamic registration of workspace/didChangeWatchedFiles." (eglot-unregister-capability server method id) (let* (success (globs (mapcar @@ -2863,7 +2863,7 @@ at point. With prefix argument, prompt for ACTION-KIND." (cl-defmethod eglot-unregister-capability (server (_method (eql workspace/didChangeWatchedFiles)) id) - "Handle dynamic unregistration of workspace/didChangeWatchedFiles" + "Handle dynamic unregistration of workspace/didChangeWatchedFiles." (mapc #'file-notify-rm-watch (gethash id (eglot--file-watches server))) (remhash id (eglot--file-watches server)) (list t "OK")) @@ -2948,7 +2948,7 @@ If NOERROR, return predicate, else erroring function." (cl-defmethod eglot-handle-notification ((server eglot-rls) (_method (eql window/progress)) &key id done title message &allow-other-keys) - "Handle notification window/progress" + "Handle notification window/progress." (setf (eglot--spinner server) (list id title done message))) @@ -2958,7 +2958,7 @@ If NOERROR, return predicate, else erroring function." :documentation "Eclipse's Java Development Tools Language Server.") (cl-defmethod eglot-initialization-options ((server eglot-eclipse-jdt)) - "Passes through required jdt initialization options" + "Passes through required jdt initialization options." `(:workspaceFolders [,@(cl-delete-duplicates (mapcar #'eglot--path-to-uri commit 06f1cd6365916e54b69b3a7fde3eb23c8a54b00b Author: Omar Polo Date: Mon Aug 30 12:32:32 2021 +0000 Add lua-lsp support for lua-mode * eglot.el (eglot-server-programs): Add support for the lua-lsp server for lua. * README.md: Document the above change. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 725fb6c27d..e9f48bb3c3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -176,6 +176,7 @@ language-server/bin/php-language-server.php")) (nix-mode . ("rnix-lsp")) (gdscript-mode . ("localhost" 6008)) ((fortran-mode f90-mode) . ("fortls")) + (lua-mode . ("lua-lsp")) (zig-mode . ("zls"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE commit cf0ba0197a965afd2aca5798ed990a41f2124585 Author: jgart <47760695+jgarte@users.noreply.github.com> Date: Sat Oct 2 00:20:57 2021 -0400 Add support for the mint language server * eglot.el (eglot-server-programs): Add support for the mint language server. * README.md: Document the above change. Copyright-paperwork-exempt: Yes diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 43b27af90f..725fb6c27d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -159,6 +159,7 @@ language-server/bin/php-language-server.php")) (haskell-mode . ("haskell-language-server-wrapper" "--lsp")) (elm-mode . ("elm-language-server")) + (mint-mode . ("mint" "ls")) (kotlin-mode . ("kotlin-language-server")) (go-mode . ("gopls")) ((R-mode ess-r-mode) . ("R" "--slave" "-e" commit bbdfbf0456bcf2fd55b78ea18e8785e64a5a8793 Author: Illia Danko Date: Sat Sep 18 15:13:39 2021 +0300 Add pyright language server support for python-mode * eglot.el (eglot-server-programs): Add pyright support for python-mode. * README.md: Document the above change. Copyright-paperwork-exempt: Yes diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index abcd0781e1..43b27af90f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -141,7 +141,8 @@ chosen (interactively or automatically)." (defvar eglot-server-programs `((rust-mode . (eglot-rls "rls")) (python-mode - . ,(eglot-alternatives '("pylsp" "pyls"))) + . ,(eglot-alternatives + '("pylsp" "pyls" ("pyright-langserver" "--stdio")))) ((js-mode typescript-mode) . ("typescript-language-server" "--stdio")) (sh-mode . ("bash-language-server" "start")) commit 3967d22b9c8b00833ee9b051017b613a788d6217 Author: Stefan Kangas Date: Sat Jan 8 18:18:16 2022 +0100 ; fix typos diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2e9531cffb..abcd0781e1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -50,7 +50,7 @@ ;; ;; (add-hook 'foo-mode-hook 'eglot-ensure) ;; -;; To attempt to start an eglot session automatically everytime a +;; To attempt to start an eglot session automatically every time a ;; foo-mode buffer is visited. ;; ;;; Code: @@ -1490,7 +1490,7 @@ against a variable's name. Examples include the string \"company\" or the symbol `xref'. Before Eglot starts \"managing\" a particular buffer, it -opinionatedly sets some peripheral Emacs facilites, such as +opinionatedly sets some peripheral Emacs facilities, such as Flymake, Xref and Company. These overriding settings help ensure consistent Eglot behaviour and only stay in place until \"managing\" stops (usually via `eglot-shutdown'), whereupon the @@ -1695,7 +1695,7 @@ Uses THING, FACE, DEFS and PREPEND." `("/" ,(eglot--mode-line-props "error" 'compilation-mode-line-fail '((mouse-3 eglot-clear-status "clear this status")) - (format "An error occured: %s\n" (plist-get last-error + (format "An error occurred: %s\n" (plist-get last-error :message))))) ,@(when (and doing (not done-p)) `("/" ,(eglot--mode-line-props doing commit fb8706165c0fed8281d4bc00fac39d86f89c1036 Author: Philipp Edelmann Date: Wed May 13 11:50:12 2020 -0600 Use fortls also for fortran-mode * eglot.el (eglot-server-programs): Use fortls also for fortran-mode. Copyright-paperwork-exempt: Yes diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a25a143709..2e9531cffb 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -173,7 +173,7 @@ language-server/bin/php-language-server.php")) (yaml-mode . ("yaml-language-server" "--stdio")) (nix-mode . ("rnix-lsp")) (gdscript-mode . ("localhost" 6008)) - (f90-mode . ("fortls")) + ((fortran-mode f90-mode) . ("fortls")) (zig-mode . ("zls"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE commit b1c7aa1d17adf1da5f1e76482dad7c46ddadcae7 Author: Brian Leung Date: Mon Oct 4 22:39:12 2021 -0700 Add yaml-language-server for yaml-mode * eglot.el (eglot-server-programs): Add yaml-language-server. * README.md: Mention yaml-language-server. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0f31da9984..a25a143709 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -170,6 +170,7 @@ language-server/bin/php-language-server.php")) ((tex-mode context-mode texinfo-mode bibtex-mode) . ("digestif")) (erlang-mode . ("erlang_ls" "--transport" "stdio")) + (yaml-mode . ("yaml-language-server" "--stdio")) (nix-mode . ("rnix-lsp")) (gdscript-mode . ("localhost" 6008)) (f90-mode . ("fortls")) commit 51fc8fc3d2ea241dc0c7982bb43ed4e1f128f363 Author: lorniu/sz Date: Sun Dec 26 18:37:14 2021 +0800 Use `locate-user-emacs-file` instead of `concat` * eglot.el (eglot--eclipse-jdt-contact): Use locate-user-emacs-file. Copyright-paperwork-exempt: Yes diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 19091e8c3e..0f31da9984 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -3013,8 +3013,8 @@ If INTERACTIVE, prompt user for details." (t "config_linux")))) (workspace (expand-file-name (md5 (project-root (eglot--current-project))) - (concat user-emacs-directory - "eglot-eclipse-jdt-cache")))) + (locate-user-emacs-file + "eglot-eclipse-jdt-cache")))) (unless jar (setq jar (cl-find-if #'is-the-jar commit 426d97f172c34cd10b9aabf0f593f39e24ec502b Author: Stefan Kangas Date: Sat Jan 8 16:02:06 2022 +0100 ; prefer https to http addresses diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 06632fde92..19091e8c3e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -20,7 +20,7 @@ ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License -;; along with this program. If not, see . +;; along with this program. If not, see . ;;; Commentary: @@ -2182,7 +2182,7 @@ Try to visit the target file for a richer summary line." ;; JT@19/10/09: This is a totally dummy identifier that isn't even ;; passed to LSP. The reason for this particular wording is to ;; construct a readable message "No references for LSP identifier at - ;; point.". See http://github.com/joaotavora/eglot/issues/314 + ;; point.". See https://github.com/joaotavora/eglot/issues/314 "LSP identifier at point.") (defvar eglot--lsp-xref-refs nil commit c12a611e44d3223286fc6190d505ee85813bcc6f Author: Fredrik Bergroth Date: Wed Dec 15 22:31:20 2021 +0100 Add missing entries from completionitemkind * eglot.el (eglot--kind-names): update GitHub-reference: fix https://github.com/joaotavora/eglot/issues/772 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5fbf9a7361..06632fde92 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -310,7 +310,9 @@ let the buffer grow forever." (5 . "Field") (6 . "Variable") (7 . "Class") (8 . "Interface") (9 . "Module") (10 . "Property") (11 . "Unit") (12 . "Value") (13 . "Enum") (14 . "Keyword") (15 . "Snippet") (16 . "Color") - (17 . "File") (18 . "Reference"))) + (17 . "File") (18 . "Reference") (19 . "Folder") (20 . "EnumMember") + (21 . "Constant") (22 . "Struct") (23 . "Event") (24 . "Operator") + (25 . "TypeParameter"))) (defconst eglot--{} (make-hash-table) "The empty JSON object.") commit 907bfe2a93ad172a854979cede87ee0460e33ddd Author: Garret Buell Date: Wed Dec 15 13:17:26 2021 -0800 Mark eglot-completion-at-point capf "non-exclusive" Add :exclusive 'no to eglot-completion-at-point results marking it as non-exclusive. This will allow completion to fall back to other less precise completion backends (e.g. dabbrev) if Eglot's returns no results. * eglot.el (eglot-completion-at-point): Set :exclusive to 'no Copyright-paperwork-exempt: Yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/770 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d889020912..5fbf9a7361 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2415,6 +2415,7 @@ is not active." (regexp-opt (cl-coerce (cl-getf completion-capability :triggerCharacters) 'list)) (line-beginning-position)))) + :exclusive 'no :exit-function (lambda (proxy status) (when (eq status 'finished) commit 720eaece6e33d574178cf0c7ac1f16321b661dc1 Merge: c0b74d0b59 19d8085b76 Author: Stephen Leake Date: Tue Nov 16 00:23:39 2021 -0800 Merge pull request from stephe-ada-guru/master Fix issues https://github.com/joaotavora/eglot/issues/755, https://github.com/joaotavora/eglot/issues/401; severity not set in textDocument/publishDiagnostics GitHub-reference: https://github.com/joaotavora/eglot/issues/759 commit 19d8085b762161ef4e54997acf43b87a535accb4 Author: Stephen Leake Date: Sat Nov 13 02:39:59 2021 -0800 Fix issues; severity not set in textdocument/publishdiagnostics * eglot.el (eglot-handle-notification): Handle severity not set. GitHub-reference: https://github.com/joaotavora/eglot/issues/755 GitHub-reference: https://github.com/joaotavora/eglot/issues/401 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bf9cf25c33..d889020912 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1,6 +1,6 @@ ;;; eglot.el --- Client for Language Server Protocol (LSP) servers -*- lexical-binding: t; -*- -;; Copyright (C) 2018-2020 Free Software Foundation, Inc. +;; Copyright (C) 2018-2021 Free Software Foundation, Inc. ;; Version: 1.7 ;; Author: João Távora @@ -1603,7 +1603,7 @@ Use `eglot-managed-p' to determine if current buffer is managed.") :key #'eglot--major-mode) (and eglot-extend-to-xref buffer-file-name - (gethash (expand-file-name buffer-file-name) + (gethash (expand-file-name buffer-file-name) eglot--servers-by-xrefed-file))))) (defun eglot--current-server-or-lose () @@ -1808,7 +1808,8 @@ COMMAND is a symbol naming the command." (point-at-eol (1+ (plist-get (plist-get range :end) :line))))))) (eglot--make-diag (current-buffer) beg end - (cond ((<= sev 1) 'eglot-error) + (cond ((null sev) 'eglot-error) + ((<= sev 1) 'eglot-error) ((= sev 2) 'eglot-warning) (t 'eglot-note)) message `((eglot-lsp-diag . ,diag-spec))))) commit c0b74d0b5938583db829363ebafdd9e0701554a4 Author: Ingo Lohmar Date: Sat Oct 9 21:19:37 2021 +0200 Fix workspace/configuration handling when given scopeuri directory The path returned by eglot--uri-to-path is mostly used for file paths, and therefore does not end with a slash. Such a no-trailing-slash path violates what default-directory demands (per its docstring), which causes hack-dir-local-variables-non-file-buffer to not find the appropriate dir-local vars. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7ebc422451..bf9cf25c33 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2007,7 +2007,7 @@ When called interactively, use the currently active server" (default-directory (if (and (not (string-empty-p uri-path)) (file-directory-p uri-path)) - uri-path + (file-name-as-directory uri-path) (project-root (eglot--project server))))) (setq-local major-mode (eglot--major-mode server)) (hack-dir-local-variables-non-file-buffer) commit d7057441b823a42b1378eadb0b4488d6fb7069f5 Author: João Távora Date: Sun Sep 5 20:05:45 2021 +0100 Fixup last commit Per https://github.com/joaotavora/eglot/issues/726. I'm still not entirely convinced using all-completion here is a good idea. As usual the completion list we get from the server is pre-filtered to whatever the server wishes. Letting the completion style do its own filtering (most completion styles use completion-regexp-list and all-completions themselves) is completely useless here. Let's hope it's not harmful. * eglot.el (eglot-completion-at-point): Fix all-completions call diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e8b7ffbbb1..7ebc422451 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2371,8 +2371,7 @@ is not active." (filterText (plist-get item :filterText))) (and (or (null pred) (funcall pred proxy)) (string-prefix-p - probe (or filterText proxy) completion-ignore-case)))) - (funcall proxies))))) + probe (or filterText proxy) completion-ignore-case)))))))) :annotation-function (lambda (proxy) (eglot--dbind ((CompletionItem) detail kind) commit 67fe1c1ad5a1277f4715ef503f14e8a73a972f78 Author: João Távora Date: Sun Sep 5 09:44:27 2021 +0100 Respect completion-regexp-alist in eglot's completion table See GitHub discussion https://github.com/joaotavora/eglot/issues/726 Suggested-by: Felicián Németh Suggested-by: JD Smith * eglot (eglot-completion-at-point): use all-completions. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5fdc25327f..e8b7ffbbb1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2363,7 +2363,9 @@ is not active." ((null action) ; try-completion (try-completion probe (funcall proxies))) ((eq action t) ; all-completions - (cl-remove-if-not + (all-completions + "" + (funcall proxies) (lambda (proxy) (let* ((item (get-text-property 0 'eglot--lsp-item proxy)) (filterText (plist-get item :filterText))) commit 64ffc80e6fb3f3e77dc55149b24e9e7225ceea74 Author: João Távora Date: Wed Sep 1 10:17:24 2021 +0100 Fix typo in user-visible eglot-ignored-server-capabilities The name with the typo, eglot-ignored-server-capabilites, is still supported. Per https://github.com/joaotavora/eglot/issues/724. * NEWS.md: Mention change * eglot.el (eglot-ignored-server-capabilities): New defcustom. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4667526c37..5fdc25327f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1386,7 +1386,10 @@ Doubles as an indicator of snippet support." (font-lock-ensure) (string-trim (filter-buffer-substring (point-min) (point-max)))))) -(defcustom eglot-ignored-server-capabilites (list) +(define-obsolete-variable-alias 'eglot-ignored-server-capabilites + 'eglot-ignored-server-capabilities "1.8") + +(defcustom eglot-ignored-server-capabilities (list) "LSP server capabilities that Eglot could use, but won't. You could add, for instance, the symbol `:documentHighlightProvider' to prevent automatic highlighting commit 82c3a2eff7c990331edb3dc0281751230edd88f7 Author: João Távora Date: Tue Aug 17 10:12:27 2021 +0100 Fall back to prompting user if eglot-alternatives fails * eglot.el (eglot-alternatives): Don't error in interactive case. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/719 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 71f7d7ea59..4667526c37 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -129,7 +129,10 @@ chosen (interactively or automatically)." nil t nil nil (car (car available))) available #'equal))) ((cdr (car available))) - (t (funcall err))))) + (t + ;; Don't error when used interactively, let the + ;; Eglot prompt the user for alternative (github#719) + nil)))) (t (cl-loop for (p . args) in listified for probe = (eglot--executable-find p t) commit ace1573dfe02fa5969692af545993ccf9475ccaf Author: João Távora Date: Tue Aug 10 20:28:35 2021 +0100 Let eglot-flymake-backend be in flymake-d-functions even if eglot off This is useful when using eglot-stay-out-of and a pattern like: (defun my/js-mode-hook () (add-hook 'flymake-diagnostic-functions 'some-eslint-backend nil t)) (setq-local eglot-stay-out-of '(flymake)) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t)) (add-hook 'js-mode-hook 'my/js-mode-hook) Then, _both_ backends will run unconditionally, but Eglot backend only actually reports diagnostics if Eglot is on. * eglot.el (eglot-flymake-backend): If buffer isn't being managed by Eglot, behave as a noop. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0f367fd220..71f7d7ea59 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2089,10 +2089,13 @@ Calls REPORT-FN (or arranges for it to be called) when the server publishes diagnostics. Between calls to this function, REPORT-FN may be called multiple times (respecting the protocol of `flymake-backend-functions')." - (setq eglot--current-flymake-report-fn report-fn) - ;; Report anything unreported - (when eglot--unreported-diagnostics - (eglot--report-to-flymake (cdr eglot--unreported-diagnostics)))) + (cond (eglot--managed-mode + (setq eglot--current-flymake-report-fn report-fn) + ;; Report anything unreported + (when eglot--unreported-diagnostics + (eglot--report-to-flymake (cdr eglot--unreported-diagnostics)))) + (t + (funcall report-fn nil)))) (defun eglot--report-to-flymake (diags) "Internal helper for `eglot-flymake-backend'." commit 7eb81031cd25562c7c566a34cce616fd0410e51d Author: Brian Leung Date: Sat Jan 30 17:33:08 2021 -0800 Add support for locationlink Fix https://github.com/joaotavora/eglot/issues/711. LocationLink was added in version 3.14 of the protocol and is sometimes used in lieu of Location for definition- and reference-related requests. * eglot.el (eglot--lsp-interface-alist): Update with LocationLink. (eglot-client-capabilities): Advertise textDocument.{definition,declaration,implementation,typeDefinition}.linkSupport. (eglot--lsp-xrefs-for-method): Accept LocationLinks. Co-authored-by: João Távora Date: Sun Jun 13 23:07:42 2021 +0100 Transpose order of "pylsp" and "pyls" alternatives When operating remotely, searching for an executable that don't exist takes longer than usual. Better to put the most likely server first in the list to minimize the slowdown. * eglot.el (eglot-server-programs): Transpose python mode alternatives GitHub-reference: per https://github.com/joaotavora/eglot/issues/703 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 927009a8a9..83bd1024a0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -138,7 +138,7 @@ chosen (interactively or automatically)." (defvar eglot-server-programs `((rust-mode . (eglot-rls "rls")) (python-mode - . ,(eglot-alternatives '("pyls" "pylsp"))) + . ,(eglot-alternatives '("pylsp" "pyls"))) ((js-mode typescript-mode) . ("typescript-language-server" "--stdio")) (sh-mode . ("bash-language-server" "start")) commit 42508de4f628d022e961a353fec1d81e1b3f52d1 Author: João Távora Date: Sun Jun 13 10:55:24 2021 +0100 Don't call eglot--executable-find more than needed * eglot.el (eglot-alternatives): Complexify. (eglot--guess-contact): No need to 'executable-find' if path absolute. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/703 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a6c60e9578..927009a8a9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -99,22 +99,42 @@ "Compute server-choosing function for `eglot-server-programs'. Each element of ALTERNATIVES is a string PROGRAM or a list of strings (PROGRAM ARGS...) where program names an LSP server -program to start with ARGS. Returns a function of one -argument." +program to start with ARGS. Returns a function of one argument. +When invoked, that function will return a list (ABSPATH ARGS), +where ABSPATH is the absolute path of the PROGRAM that was +chosen (interactively or automatically)." (lambda (&optional interactive) + ;; JT@2021-06-13: This function is way more complicated than it + ;; could be because it accounts for the fact that + ;; `eglot--executable-find' may take much longer to execute on + ;; remote files. (let* ((listified (cl-loop for a in alternatives collect (if (listp a) a (list a)))) - (available (cl-remove-if-not (lambda (a) (eglot--executable-find a t)) - listified :key #'car))) - (cond ((and interactive (cdr available)) - (let ((chosen (completing-read - "[eglot] More than one server executable available:" - (mapcar #'car available) - nil t nil nil (car (car available))))) - (assoc chosen available #'equal))) - ((car available)) + (err (lambda () + (error "None of '%s' are valid executables" + (mapconcat #'identity alternatives ", "))))) + (cond (interactive + (let* ((augmented (mapcar (lambda (a) + (let ((found (eglot--executable-find + (car a) t))) + (and found + (cons (car a) (cons found (cdr a)))))) + listified)) + (available (remove nil augmented))) + (cond ((cdr available) + (cdr (assoc + (completing-read + "[eglot] More than one server executable available:" + (mapcar #'car available) + nil t nil nil (car (car available))) + available #'equal))) + ((cdr (car available))) + (t (funcall err))))) (t - (car listified)))))) + (cl-loop for (p . args) in listified + for probe = (eglot--executable-find p t) + when probe return (cons probe args) + finally (funcall err))))))) (defvar eglot-server-programs `((rust-mode . (eglot-rls "rls")) (python-mode @@ -833,7 +853,9 @@ be guessed." ((null guess) (format "[eglot] Sorry, couldn't guess for `%s'!\n%s" managed-mode base-prompt)) - ((and program (not (eglot--executable-find program t))) + ((and program + (not (file-name-absolute-p program)) + (not (eglot--executable-find program t))) (concat (format "[eglot] I guess you want to run `%s'" program-guess) (format ", but I can't find `%s' in PATH!" program) commit 7d1375df484e10668552ba23cea0778bf9e374c4 Author: Liu Hui Date: Sat Jun 12 06:49:19 2021 +0800 Consider tramp in eglot-alternatives * eglot.el (eglot-alternatives): Use eglot--executable-find. Copyright-paperwork-exempt: yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/702 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4ead874eec..a6c60e9578 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -104,7 +104,8 @@ argument." (lambda (&optional interactive) (let* ((listified (cl-loop for a in alternatives collect (if (listp a) a (list a)))) - (available (cl-remove-if-not #'executable-find listified :key #'car))) + (available (cl-remove-if-not (lambda (a) (eglot--executable-find a t)) + listified :key #'car))) (cond ((and interactive (cdr available)) (let ((chosen (completing-read "[eglot] More than one server executable available:" commit b1a379cd774f40f6709fdfbb5c469fc317f22f8d Author: João Távora Date: Wed May 26 18:51:30 2021 +0100 Use project-files to know which directory watchers to skip The directory-finding logic is probably a bit slower than using eglot--directories-recursively, but since it honours `.gitignores` and ignores more directories it's much faster overall. And guaranteed to create less watchers. Thanks to Dmitry Gutov for the idea. * eglot.el (eglot--directories-recursively): Remove. GitHub-reference: per https://github.com/joaotavora/eglot/issues/697 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4c47ad004b..4ead874eec 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2777,7 +2777,9 @@ at point. With prefix argument, prompt for ACTION-KIND." (eglot--glob-compile globPattern t t)) watchers)) (dirs-to-watch - (eglot--directories-recursively default-directory))) + (delete-dups (mapcar #'file-name-directory + (project-files + (eglot--project server)))))) (cl-labels ((handle-event (event) @@ -2878,15 +2880,6 @@ If NOERROR, return predicate, else erroring function." (when (eq ?! (aref arg 1)) (aset arg 1 ?^)) `(,self () (re-search-forward ,(concat "\\=" arg)) (,next))) -(defun eglot--directories-recursively (&optional dir) - "Because `directory-files-recursively' isn't complete in 26.3." - (cons (setq dir (expand-file-name (or dir default-directory))) - (cl-loop with default-directory = dir - with completion-regexp-list = '("^[^.]") - for f in (file-name-all-completions "" dir) - if (and (file-directory-p f) (not (string= "node_modules/" f))) - append (eglot--directories-recursively f)))) - ;;; Rust-specific ;;; commit 02dc70363120beb28e770a872ab3960fb0920fa0 Author: João Távora Date: Wed May 26 15:23:29 2021 +0100 Hard code an exception to "node_modules" directores * eglot.el (eglot--directories-recursively): Fix. GitHub-reference: per https://github.com/joaotavora/eglot/issues/697 GitHub-reference: per https://github.com/joaotavora/eglot/issues/645 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 706fa92522..4c47ad004b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2884,7 +2884,7 @@ If NOERROR, return predicate, else erroring function." (cl-loop with default-directory = dir with completion-regexp-list = '("^[^.]") for f in (file-name-all-completions "" dir) - if (file-directory-p f) + if (and (file-directory-p f) (not (string= "node_modules/" f))) append (eglot--directories-recursively f)))) commit 78e994d85517ab0d0f2c053b5e6c3598bdfe7718 Author: João Távora Date: Wed May 26 15:21:06 2021 +0100 Again speed up directory watching Previously, given a number of globs, Eglot would try to place system watchers only in those subdirectories that could potentially be matched by a glob. This meant traversing the whole tree, which could be impractical. Just place watchers in every subdirectory of the project (you may run out of watchers). * eglot.el (eglot-register-capability): Simplify. (eglot--files-recursively): Delete. (eglot--directories-recursively): Fix. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/697 GitHub-reference: fix https://github.com/joaotavora/eglot/issues/645 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a739419e9d..706fa92522 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2777,7 +2777,7 @@ at point. With prefix argument, prompt for ACTION-KIND." (eglot--glob-compile globPattern t t)) watchers)) (dirs-to-watch - (eglot--directories-matched-by-globs default-directory globs))) + (eglot--directories-recursively default-directory))) (cl-labels ((handle-event (event) @@ -2878,37 +2878,14 @@ If NOERROR, return predicate, else erroring function." (when (eq ?! (aref arg 1)) (aset arg 1 ?^)) `(,self () (re-search-forward ,(concat "\\=" arg)) (,next))) -(defun eglot--files-recursively (&optional dir) - "Because `directory-files-recursively' isn't complete in 26.3." - (cons (setq dir (expand-file-name (or dir default-directory))) - (cl-loop with default-directory = dir - with completion-regexp-list = '("^[^.]") - for f in (file-name-all-completions "" dir) - if (file-directory-p f) append (eglot--files-recursively f) - else collect (expand-file-name f)))) - (defun eglot--directories-recursively (&optional dir) "Because `directory-files-recursively' isn't complete in 26.3." (cons (setq dir (expand-file-name (or dir default-directory))) (cl-loop with default-directory = dir with completion-regexp-list = '("^[^.]") for f in (file-name-all-completions "" dir) - if (file-directory-p f) append (eglot--files-recursively f) - else collect (expand-file-name f)))) - -(defun eglot--directories-matched-by-globs (dir globs) - "Discover subdirectories of DIR with files matched by one of GLOBS. -Each element of GLOBS is either an uncompiled glob-string or a -compiled glob." - (setq globs (cl-loop for g in globs - collect (if (stringp g) (eglot--glob-compile g t t) g))) - (cl-loop for f in (eglot--files-recursively dir) - for fdir = (file-name-directory f) - when (and - (not (member fdir dirs)) - (cl-loop for g in globs thereis (funcall g f))) - collect fdir into dirs - finally (cl-return (delete-dups dirs)))) + if (file-directory-p f) + append (eglot--directories-recursively f)))) ;;; Rust-specific commit 712cf71d9d1b05ea699f6a1a172dc8eefe1efbbf Author: João Távora Date: Sat May 22 11:49:47 2021 +0100 Support multiple servers out-of-box for same mode Also per https://github.com/joaotavora/eglot/issues/537. * eglot.el (eglot-alternatives): new helper. (eglot-server-programs): Use it. Use clangd and pylsp. * NEWS.md: Mention feature. * README.md (Connecting to a server): Mention pylsp and clangd. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/688 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13d9952494..a739419e9d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -95,15 +95,37 @@ :prefix "eglot-" :group 'applications) -(defvar eglot-server-programs '((rust-mode . (eglot-rls "rls")) - (python-mode . ("pyls")) +(defun eglot-alternatives (alternatives) + "Compute server-choosing function for `eglot-server-programs'. +Each element of ALTERNATIVES is a string PROGRAM or a list of +strings (PROGRAM ARGS...) where program names an LSP server +program to start with ARGS. Returns a function of one +argument." + (lambda (&optional interactive) + (let* ((listified (cl-loop for a in alternatives + collect (if (listp a) a (list a)))) + (available (cl-remove-if-not #'executable-find listified :key #'car))) + (cond ((and interactive (cdr available)) + (let ((chosen (completing-read + "[eglot] More than one server executable available:" + (mapcar #'car available) + nil t nil nil (car (car available))))) + (assoc chosen available #'equal))) + ((car available)) + (t + (car listified)))))) + +(defvar eglot-server-programs `((rust-mode . (eglot-rls "rls")) + (python-mode + . ,(eglot-alternatives '("pyls" "pylsp"))) ((js-mode typescript-mode) . ("typescript-language-server" "--stdio")) (sh-mode . ("bash-language-server" "start")) ((php-mode phps-mode) . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php")) - ((c++-mode c-mode) . ("ccls")) + ((c++-mode c-mode) . ,(eglot-alternatives + '("clangd" "ccls"))) (((caml-mode :language-id "ocaml") (tuareg-mode :language-id "ocaml") reason-mode) . ("ocamllsp")) commit 210b52859e71b81c6dc022ea3e07b1a68bc978d3 Author: Augusto Stoffel Date: Sat May 22 12:53:38 2021 +0200 Allow staying out of flymake-mode, eldoc-mode * eglot.el (eglot--managed-mode): don't enable flymake or eldoc when those symbols belong to eglot-stay-out-of. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/671 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index db468d83c1..13d9952494 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1500,8 +1500,8 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (unless (eglot--stay-out-of-p 'imenu) (add-function :before-until (local 'imenu-create-index-function) #'eglot-imenu)) - (flymake-mode 1) - (eldoc-mode 1) + (unless (eglot--stay-out-of-p 'flymake) (flymake-mode 1)) + (unless (eglot--stay-out-of-p 'eldoc) (eldoc-mode 1)) (cl-pushnew (current-buffer) (eglot--managed-buffers (eglot-current-server)))) (t (remove-hook 'after-change-functions 'eglot--after-change t) commit 7eddb6f950b0a067a55347600d499cae608aa728 Author: Michael Livshin Date: Sat Dec 15 01:17:32 2018 +0000 Manage cross-referenced files outside project in same server Close https://github.com/joaotavora/eglot/issues/686, Close https://github.com/joaotavora/eglot/issues/695. Co-authored-by: João Távora * eglot.el (eglot-extend-to-xref): new defcustom, default to nil. (eglot--servers-by-xrefed-file): new hash table, mapping file names to servers. (eglot--managed-mode): use eglot-current-server, instead of eglot--cached-server directly. (eglot--current-server-or-lose): ditto. (eglot--maybe-activate-editing-mode): ditto. (eglot-current-server): move all cached-server update logic here -- if eglot--cached-server is nil, try to find it using current project or (optionally) xref location. (eglot--xref-make-match): record the xref location. * README.md (Customization): Mention new defcustom. * NEWS.md: Mention new feature GitHub-reference: fix https://github.com/joaotavora/eglot/issues/76 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f17e795bfb..db468d83c1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -237,6 +237,10 @@ let the buffer grow forever." :type '(choice (const :tag "Don't show confirmation prompt" nil) (symbol :tag "Show confirmation prompt" 'confirm))) +(defcustom eglot-extend-to-xref nil + "If non-nil, activate Eglot in cross-referenced non-project files." + :type 'boolean) + ;; Customizable via `completion-category-overrides'. (when (assoc 'flex completion-styles-alist) (add-to-list 'completion-category-defaults '(eglot (styles flex basic)))) @@ -832,6 +836,9 @@ be guessed." (put 'eglot-lsp-context 'variable-documentation "Dynamically non-nil when searching for projects in LSP context.") +(defvar eglot--servers-by-xrefed-file + (make-hash-table :test 'equal :weakness 'value)) + (defun eglot--current-project () "Return a project object for Eglot's LSP purposes. This relies on `project-current' and thus on @@ -1495,7 +1502,7 @@ Use `eglot-managed-p' to determine if current buffer is managed.") #'eglot-imenu)) (flymake-mode 1) (eldoc-mode 1) - (cl-pushnew (current-buffer) (eglot--managed-buffers eglot--cached-server))) + (cl-pushnew (current-buffer) (eglot--managed-buffers (eglot-current-server)))) (t (remove-hook 'after-change-functions 'eglot--after-change t) (remove-hook 'before-change-functions 'eglot--before-change t) @@ -1533,11 +1540,19 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (defun eglot-current-server () "Return logical EGLOT server for current buffer, nil if none." - eglot--cached-server) + (setq eglot--cached-server + (or eglot--cached-server + (cl-find major-mode + (gethash (eglot--current-project) eglot--servers-by-project) + :key #'eglot--major-mode) + (and eglot-extend-to-xref + buffer-file-name + (gethash (expand-file-name buffer-file-name) + eglot--servers-by-xrefed-file))))) (defun eglot--current-server-or-lose () "Return current logical EGLOT server connection or error." - (or eglot--cached-server + (or (eglot-current-server) (jsonrpc-error "No current JSON-RPC connection"))) (defvar-local eglot--unreported-diagnostics nil @@ -1555,14 +1570,7 @@ If it is activated, also signal textDocument/didOpen." (unless eglot--managed-mode ;; Called when `revert-buffer-in-progress-p' is t but ;; `revert-buffer-preserve-modes' is nil. - (when (and buffer-file-name - (or - eglot--cached-server - (setq eglot--cached-server - (cl-find major-mode - (gethash (eglot--current-project) - eglot--servers-by-project) - :key #'eglot--major-mode)))) + (when (and buffer-file-name (eglot-current-server)) (setq eglot--unreported-diagnostics `(:just-opened . nil)) (eglot--managed-mode) (eglot--signal-textDocument/didOpen)))) @@ -2101,6 +2109,8 @@ Try to visit the target file for a richer summary line." (start-pos (cl-getf start :character)) (end-pos (cl-getf (cl-getf range :end) :character))) (list name line start-pos (- end-pos start-pos))))))) + (setf (gethash (expand-file-name file) eglot--servers-by-xrefed-file) + (eglot--current-server-or-lose)) (xref-make-match summary (xref-make-file-location file line column) length))) (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) commit fe9d6daa57102c7daebdcced2d7b6253cecb047c Author: Jim Porter Date: Thu May 13 08:55:31 2021 -0700 Correct path/uri when using tramp from ms windows Co-authored-by: João Távora * eglot.el (eglot--connect): Ensure drive letter doesn't sneak into rootPath. (eglot--path-to-uri): Only add a leading "/" for local MS Windows paths. (eglot--uri-to-path): Only remove leading "/" from local MS Windows paths. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/679 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fc82367f8e..f17e795bfb 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1046,8 +1046,8 @@ This docstring appeases checkdoc, that's all." (emacs-pid)) ;; Maybe turn trampy `/ssh:foo@bar:/path/to/baz.py' ;; into `/path/to/baz.py', so LSP groks it. - :rootPath (expand-file-name - (file-local-name default-directory)) + :rootPath (file-local-name + (expand-file-name default-directory)) :rootUri (eglot--path-to-uri default-directory) :initializationOptions (eglot-initialization-options server) @@ -1274,24 +1274,31 @@ If optional MARKER, return a marker instead" (defun eglot--path-to-uri (path) "URIfy PATH." - (concat "file://" (if (eq system-type 'windows-nt) "/") - (url-hexify-string - ;; Again watch out for trampy paths. - (directory-file-name (file-local-name (file-truename path))) - eglot--uri-path-allowed-chars))) + (let ((truepath (file-truename path))) + (concat "file://" + ;; Add a leading "/" for local MS Windows-style paths. + (if (and (eq system-type 'windows-nt) + (not (file-remote-p truepath))) + "/") + (url-hexify-string + ;; Again watch out for trampy paths. + (directory-file-name (file-local-name truepath)) + eglot--uri-path-allowed-chars)))) (defun eglot--uri-to-path (uri) "Convert URI to file path, helped by `eglot--current-server'." (when (keywordp uri) (setq uri (substring (symbol-name uri) 1))) - (let* ((retval (url-filename (url-generic-parse-url (url-unhex-string uri)))) - (normalized (if (and (eq system-type 'windows-nt) - (cl-plusp (length retval))) - (substring retval 1) - retval)) - (server (eglot-current-server)) + (let* ((server (eglot-current-server)) (remote-prefix (and server (file-remote-p - (project-root (eglot--project server)))))) + (project-root (eglot--project server))))) + (retval (url-filename (url-generic-parse-url (url-unhex-string uri)))) + ;; Remove the leading "/" for local MS Windows-style paths. + (normalized (if (and (not remote-prefix) + (eq system-type 'windows-nt) + (cl-plusp (length retval))) + (substring retval 1) + retval))) (concat remote-prefix normalized))) (defun eglot--snippet-expansion-fn () commit 68baa57143ed4f338778053d9aa3120ed5d59300 Author: João Távora Date: Thu May 13 10:09:20 2021 +0100 Provide context for finer project-find-functions * eglot.el (eglot--guess-contact): Use eglot--current-project. (eglot): Adjust docstring. (eglot-lsp-context): New variable. (eglot--current-project): New helper. (eglot--maybe-activate-editing-mode, eglot--eclipse-jdt-contact): Use eglot--current-project. GitHub-reference: per https://github.com/joaotavora/eglot/issues/687 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 29024ad3fb..fc82367f8e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -777,7 +777,6 @@ be guessed." ((not guessed-mode) (eglot--error "Can't guess mode to manage for `%s'" (current-buffer))) (t guessed-mode))) - (project (or (project-current) `(transient . ,default-directory))) (lang-id-and-guess (eglot--lookup-mode guessed-mode)) (language-id (car lang-id-and-guess)) (guess (cdr lang-id-and-guess)) @@ -827,7 +826,21 @@ be guessed." :test #'equal)))) guess (eglot--error "Couldn't guess for `%s'!" managed-mode)))) - (list managed-mode project class contact language-id))) + (list managed-mode (eglot--current-project) class contact language-id))) + +(defvar eglot-lsp-context) +(put 'eglot-lsp-context 'variable-documentation + "Dynamically non-nil when searching for projects in LSP context.") + +(defun eglot--current-project () + "Return a project object for Eglot's LSP purposes. +This relies on `project-current' and thus on +`project-find-functions'. Functions in the latter +variable (which see) can query the value `eglot-lsp-context' to +decide whether a given directory is a project containing a +suitable root directory for a given LSP server's purposes." + (let ((eglot-lsp-context t)) + (or (project-current) `(transient . ,default-directory)))) ;;;###autoload (defun eglot (managed-major-mode project class contact language-id @@ -844,13 +857,16 @@ exchanged periodically to provide enhanced code-analysis via Interactively, the command attempts to guess MANAGED-MAJOR-MODE from current buffer, CLASS and CONTACT from -`eglot-server-programs' and PROJECT from `project-current'. If -it can't guess, the user is prompted. With a single +`eglot-server-programs' and PROJECT from +`project-find-functions'. The search for active projects in this +context binds `eglot-lsp-context' (which see). + +If it can't guess, the user is prompted. With a single \\[universal-argument] prefix arg, it always prompt for COMMAND. With two \\[universal-argument] prefix args, also prompts for MANAGED-MAJOR-MODE. -PROJECT is a project instance as returned by `project-current'. +PROJECT is a project object as returned by `project-current'. CLASS is a subclass of `eglot-lsp-server'. @@ -1537,8 +1553,7 @@ If it is activated, also signal textDocument/didOpen." eglot--cached-server (setq eglot--cached-server (cl-find major-mode - (gethash (or (project-current) - `(transient . ,default-directory)) + (gethash (eglot--current-project) eglot--servers-by-project) :key #'eglot--major-mode)))) (setq eglot--unreported-diagnostics `(:just-opened . nil)) @@ -2939,9 +2954,8 @@ If INTERACTIVE, prompt user for details." ((string= system-type "darwin") "config_mac") ((string= system-type "windows-nt") "config_win") (t "config_linux")))) - (project (or (project-current) `(transient . ,default-directory))) (workspace - (expand-file-name (md5 (project-root project)) + (expand-file-name (md5 (project-root (eglot--current-project))) (concat user-emacs-directory "eglot-eclipse-jdt-cache")))) (unless jar commit ded0aa0bfc2a4213ec7b938f356478d2abd21dc8 Author: Steve Purcell Date: Fri Apr 30 20:32:07 2021 +1200 Fix emacs 28 warning by avoiding positional args in define-minor-mode * eglot.el (eglot--managed-mode): Avoid positional args. Co-authored-by: João Távora GitHub-reference: close https://github.com/joaotavora/eglot/issues/685 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b191763198..29024ad3fb 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1440,7 +1440,7 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (define-minor-mode eglot--managed-mode "Mode for source buffers managed by some EGLOT project." - nil nil eglot-mode-map + :init-value nil :lighter nil :keymap eglot-mode-map (cond (eglot--managed-mode (add-hook 'after-change-functions 'eglot--after-change nil t) commit edf75e87cac1d72745955cd8965af4ce586c1bb8 Author: Steve Purcell Date: Fri Apr 30 11:09:04 2021 +1200 Allow lsp languageid to be overridden via eglot-server-programs Close https://github.com/joaotavora/eglot/issues/678. Per https://github.com/joaotavora/eglot/issues/677 * eglot-tests.el (eglot--guessing-contact): Add GUESSED-LANG-ID-SYM param. (eglot-server-programs-guess-lang): New test. * eglot.el (eglot-server-programs): Augment entries for caml-mode and tuareg-mode. Enhance docstring. (eglot--lookup-mode): New helper. (eglot--guess-contact): Call eglot--lookup-mode. (eglot, eglot-reconnect): Pass language-id to eglot--connect (eglot--connect): Receive LANGUAGE-ID (eglot--TextDocumentItem): Simplify. Use `eglot--current-server-or-lose' * README.md (Handling quirky servers): Mention new feature. Co-authored-by: João Távora diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 122a76b035..b191763198 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -104,7 +104,8 @@ . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php")) ((c++-mode c-mode) . ("ccls")) - ((caml-mode tuareg-mode reason-mode) + (((caml-mode :language-id "ocaml") + (tuareg-mode :language-id "ocaml") reason-mode) . ("ocamllsp")) (ruby-mode . ("solargraph" "socket" "--port" :autoport)) @@ -129,9 +130,23 @@ language-server/bin/php-language-server.php")) (zig-mode . ("zls"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE -is a mode symbol, or a list of mode symbols. The associated -CONTACT specifies how to connect to a server for managing buffers -of those modes. CONTACT can be: +identifies the buffers that are to be managed by a specific +language server. The associated CONTACT specifies how to connect +to a server for those buffers. + +MAJOR-MODE can be: + +* In the most common case, a symbol such as `c-mode'; + +* A list (MAJOR-MODE-SYMBOL :LANGUAGE-ID ID) where + MAJOR-MODE-SYMBOL is the aforementioned symbol and ID is a + string identifying the language to the server; + +* A list combining the previous two alternatives, meaning + multiple major modes will be associated with a single server + program. + +CONTACT can be: * In the most common case, a list of strings (PROGRAM [ARGS...]). PROGRAM is called with ARGS and is expected to serve LSP requests @@ -612,6 +627,9 @@ treated as in `eglot-dbind'." (major-mode :documentation "Major mode symbol." :accessor eglot--major-mode) + (language-id + :documentation "Language ID string for the mode." + :accessor eglot--language-id) (capabilities :documentation "JSON object containing server capabilities." :accessor eglot--capabilities) @@ -720,9 +738,29 @@ PRESERVE-BUFFERS as in `eglot-shutdown', which see." (defvar eglot--command-history nil "History of CONTACT arguments to `eglot'.") +(defun eglot--lookup-mode (mode) + "Lookup `eglot-server-programs' for MODE. +Return (LANGUAGE-ID . CONTACT-PROXY). If not specified, +LANGUAGE-ID is determined from MODE." + (cl-loop + for (modes . contact) in eglot-server-programs + thereis (cl-some + (lambda (spec) + (cl-destructuring-bind (probe &key language-id &allow-other-keys) + (if (consp spec) spec (list spec)) + (and (provided-mode-derived-p mode probe) + (cons + (or language-id + (or (get mode 'eglot-language-id) + (get spec 'eglot-language-id) + (string-remove-suffix "-mode" (symbol-name mode)))) + contact)))) + (if (or (symbolp modes) (keywordp (cadr modes))) + (list modes) modes)))) + (defun eglot--guess-contact (&optional interactive) "Helper for `eglot'. -Return (MANAGED-MODE PROJECT CLASS CONTACT). If INTERACTIVE is +Return (MANAGED-MODE PROJECT CLASS CONTACT LANG-ID). If INTERACTIVE is non-nil, maybe prompt user, else error as soon as something can't be guessed." (let* ((guessed-mode (if buffer-file-name major-mode)) @@ -740,11 +778,9 @@ be guessed." (eglot--error "Can't guess mode to manage for `%s'" (current-buffer))) (t guessed-mode))) (project (or (project-current) `(transient . ,default-directory))) - (guess (cdr (assoc managed-mode eglot-server-programs - (lambda (m1 m2) - (cl-find - m2 (if (listp m1) m1 (list m1)) - :test #'provided-mode-derived-p))))) + (lang-id-and-guess (eglot--lookup-mode guessed-mode)) + (language-id (car lang-id-and-guess)) + (guess (cdr lang-id-and-guess)) (guess (if (functionp guess) (funcall guess interactive) guess)) @@ -791,10 +827,11 @@ be guessed." :test #'equal)))) guess (eglot--error "Couldn't guess for `%s'!" managed-mode)))) - (list managed-mode project class contact))) + (list managed-mode project class contact language-id))) ;;;###autoload -(defun eglot (managed-major-mode project class contact &optional interactive) +(defun eglot (managed-major-mode project class contact language-id + &optional interactive) "Manage a project with a Language Server Protocol (LSP) server. The LSP server of CLASS is started (or contacted) via CONTACT. @@ -821,6 +858,9 @@ CONTACT specifies how to contact the server. It is a keyword-value plist used to initialize CLASS or a plain list as described in `eglot-server-programs', which see. +LANGUAGE-ID is the language ID string to send to the server for +MANAGED-MAJOR-MODE, which matters to a minority of servers. + INTERACTIVE is t if called interactively." (interactive (append (eglot--guess-contact t) '(t))) (let* ((current-server (eglot-current-server)) @@ -830,7 +870,7 @@ INTERACTIVE is t if called interactively." (y-or-n-p "[eglot] Live process found, reconnect instead? ")) (eglot-reconnect current-server interactive) (when live-p (ignore-errors (eglot-shutdown current-server))) - (eglot--connect managed-major-mode project class contact)))) + (eglot--connect managed-major-mode project class contact language-id)))) (defun eglot-reconnect (server &optional interactive) "Reconnect to SERVER. @@ -841,7 +881,8 @@ INTERACTIVE is t if called interactively." (eglot--connect (eglot--major-mode server) (eglot--project server) (eieio-object-class-name server) - (eglot--saved-initargs server)) + (eglot--saved-initargs server) + (eglot--language-id server)) (eglot--message "Reconnected!")) (defvar eglot--managed-mode) ; forward decl @@ -914,8 +955,8 @@ Each function is passed the server as an argument") (defvar-local eglot--cached-server nil "A cached reference to the current EGLOT server.") -(defun eglot--connect (managed-major-mode project class contact) - "Connect to MANAGED-MAJOR-MODE, PROJECT, CLASS and CONTACT. +(defun eglot--connect (managed-major-mode project class contact language-id) + "Connect to MANAGED-MAJOR-MODE, LANGUAGE-ID, PROJECT, CLASS and CONTACT. This docstring appeases checkdoc, that's all." (let* ((default-directory (project-root project)) (nickname (file-name-base (directory-file-name default-directory))) @@ -969,6 +1010,7 @@ This docstring appeases checkdoc, that's all." (setf (eglot--project server) project) (setf (eglot--project-nickname server) nickname) (setf (eglot--major-mode server) managed-major-mode) + (setf (eglot--language-id server) language-id) (setf (eglot--inferior-process server) autostart-inferior-process) (run-hook-with-args 'eglot-server-initialized-hook server) ;; Now start the handshake. To honour `eglot-sync-connect' @@ -1737,11 +1779,7 @@ THINGS are either registrations or unregisterations (sic)." (append (eglot--VersionedTextDocumentIdentifier) (list :languageId - (cond - ((get major-mode 'eglot-language-id)) - ((string-match "\\(.*\\)-mode" (symbol-name major-mode)) - (match-string 1 (symbol-name major-mode))) - (t "unknown")) + (eglot--language-id (eglot--current-server-or-lose)) :text (eglot--widening (buffer-substring-no-properties (point-min) (point-max)))))) commit a1fb92543ccd21375f21143939a29445016d56ee Author: Augusto Stoffel Date: Wed Apr 28 11:41:19 2021 +0100 Add a completion-category-defaults entry Setting completion-styles buffer-locally is harder to customize and can break some completion UIs. Emacs bughttps://github.com/joaotavora/eglot/issues/48073 * eglot.el: Add a completion-category-defaults entry, if applicable. (eglot--managed-mode): Don't set `completion-styles' (eglot-completion-at-point): Add style metadata to completion table. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3348054264..122a76b035 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -222,6 +222,10 @@ let the buffer grow forever." :type '(choice (const :tag "Don't show confirmation prompt" nil) (symbol :tag "Show confirmation prompt" 'confirm))) +;; Customizable via `completion-category-overrides'. +(when (assoc 'flex completion-styles-alist) + (add-to-list 'completion-category-defaults '(eglot (styles flex basic)))) + ;;; Constants ;;; @@ -1421,8 +1425,6 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend)) (eglot--setq-saving company-backends '(company-capf)) (eglot--setq-saving company-tooltip-align-annotations t) - (when (assoc 'flex completion-styles-alist) - (eglot--setq-saving completion-styles '(flex basic))) (unless (eglot--stay-out-of-p 'imenu) (add-function :before-until (local 'imenu-create-index-function) #'eglot-imenu)) @@ -2166,7 +2168,8 @@ is not active." (get-text-property 0 'eglot--lsp-item c) :sortText) ""))))) - (metadata `(metadata . ((display-sort-function . ,sort-completions)))) + (metadata `(metadata (category . eglot) + (display-sort-function . ,sort-completions))) resp items (cached-proxies :none) (proxies (lambda () commit f3e2ca5bd5f4ee3f5a32e85dc9cb27eaa982b41b Author: Steve Purcell Date: Mon Apr 26 10:51:44 2021 +1200 Switch default langserver for ocaml to ocamllsp The repo for ocaml-language-server has been archived and inactive for quite some time: https://github.com/ocaml-lsp/ocaml-language-server Meanwhile, ocaml-lsp is the generally-preferred option, and is actively maintained in the ocaml org itself: https://github.com/ocaml/ocaml-lsp/ * eglot.el (eglot-server-programs): switch caml-mode entry. GitHub-reference: close https://github.com/joaotavora/eglot/issues/677 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3373be2256..3348054264 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -105,7 +105,7 @@ language-server/bin/php-language-server.php")) ((c++-mode c-mode) . ("ccls")) ((caml-mode tuareg-mode reason-mode) - . ("ocaml-language-server" "--stdio")) + . ("ocamllsp")) (ruby-mode . ("solargraph" "socket" "--port" :autoport)) (haskell-mode commit f634580f1bb8c2e329810adfef42f35503cb8848 Author: João Távora Date: Fri Apr 23 08:49:02 2021 +0100 Declare eglot--cached-server before use Per https://github.com/joaotavora/eglot/issues/670. Otherwise the dynamic binding of it in in eglot--connect won't work. * eglot.el (eglot--cached-server): Move up. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/673 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f926709e8c..3373be2256 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -907,6 +907,9 @@ Each function is passed the server as an argument") " ")) contact)) +(defvar-local eglot--cached-server nil + "A cached reference to the current EGLOT server.") + (defun eglot--connect (managed-major-mode project class contact) "Connect to MANAGED-MAJOR-MODE, PROJECT, CLASS and CONTACT. This docstring appeases checkdoc, that's all." @@ -1378,9 +1381,6 @@ For example, to keep your Company customization use (push (cons ',symbol (symbol-value ',symbol)) eglot--saved-bindings) (setq-local ,symbol ,binding))) -(defvar-local eglot--cached-server nil - "A cached reference to the current EGLOT server.") - (defun eglot-managed-p () "Tell if current buffer is managed by EGLOT." eglot--managed-mode) commit 5b33fe06c506d6a83f6ef77851478033de840b2a Author: João Távora Date: Tue Apr 20 18:39:54 2021 +0100 Make eglot-current-server work in notification handlers * eglot.el (eglot--connect): Ensure `eglot--cached-server` bound when calling notification/request methods. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/670 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 20f59956f8..f926709e8c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -945,7 +945,8 @@ This docstring appeases checkdoc, that's all." (format "*%s stderr*" readable-name)) :file-handler t))))))) (spread (lambda (fn) (lambda (server method params) - (apply fn server method (append params nil))))) + (let ((eglot--cached-server server)) + (apply fn server method (append params nil)))))) (server (apply #'make-instance class commit 0d89dd73ff02bd0f70e2fcca0c2dcedf821faeb3 Author: João Távora Date: Tue Apr 13 01:16:31 2021 +0100 Add a passing test demonstrating clangd + tramp works ... It works at least within the minimal, well-controlled reproducible settings of this test. Maybe if we knew something more about the setup of the user who submitted this report we would be able to concoct a failing test, but we don't. * eglot-tests.el (subr-x): Require it (eglot--make-file-or-dir): Return expanded file name. (eglot-tests--lsp-abiding-column-1): New helper. (eglot-lsp-abiding-column): Use it. (eglot--tramp-test): Fix `skip-unless` condition. (eglot--tramp-test-2): New test. GitHub-reference: per https://github.com/joaotavora/eglot/issues/667 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cbde1b7a4c..20f59956f8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -25,7 +25,7 @@ ;;; Commentary: ;; Simply M-x eglot should be enough to get you started, but here's a -;; little info (see the accompanying README.md or the URL for more). + ;; little info (see the accompanying README.md or the URL for more). ;; ;; M-x eglot starts a server via a shell-command guessed from ;; `eglot-server-programs', using the current major-mode (for whatever @@ -2791,6 +2791,15 @@ If NOERROR, return predicate, else erroring function." if (file-directory-p f) append (eglot--files-recursively f) else collect (expand-file-name f)))) +(defun eglot--directories-recursively (&optional dir) + "Because `directory-files-recursively' isn't complete in 26.3." + (cons (setq dir (expand-file-name (or dir default-directory))) + (cl-loop with default-directory = dir + with completion-regexp-list = '("^[^.]") + for f in (file-name-all-completions "" dir) + if (file-directory-p f) append (eglot--files-recursively f) + else collect (expand-file-name f)))) + (defun eglot--directories-matched-by-globs (dir globs) "Discover subdirectories of DIR with files matched by one of GLOBS. Each element of GLOBS is either an uncompiled glob-string or a commit 28159d31c0b35d6b4a210cfc1111ef38ff53656d Merge: 355f1b5f49 7ae862de9e Author: Dmitry Gutov Date: Tue Apr 13 02:30:11 2021 +0300 Merge pull request from mohkale/company-kind (feature): Add :company-kind to eglot-completion-at-point GitHub-reference: https://github.com/joaotavora/eglot/issues/656 commit 7ae862de9e85bf58aba1bda0c119459a6fbf273d Author: Mohsin Kaleem Date: Mon Mar 29 22:32:33 2021 +0100 Add :company-kind to eglot-completion-at-point * eglot.el (eglot-completion-at-point): Add a :company-kind field to the completion-at-point function so that company can associate completion candidates with lsp types. GitHub-reference: close https://github.com/joaotavora/eglot/issues/652 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5914f2ddd4..579ed27913 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2236,6 +2236,13 @@ is not active." (concat " " (propertize annotation 'face 'font-lock-function-name-face)))))) + :company-kind + ;; Associate each lsp-item with a lsp-kind symbol. + (lambda (proxy) + (when-let* ((lsp-item (get-text-property 0 'eglot--lsp-item proxy)) + (kind (alist-get (plist-get lsp-item :kind) + eglot--kind-names))) + (intern (downcase kind)))) :company-doc-buffer (lambda (proxy) (let* ((documentation commit 355f1b5f49cc091ce48a50e534853225f375936a Author: Mohsin Kaleem Date: Mon Mar 29 22:17:07 2021 +0100 Highlight relevant part of xref hits using xref-match face Also close https://github.com/joaotavora/eglot/issues/657. (eglot--xref-make-match): Use face 'xref-match instead of 'highlight. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/650 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 59804da9e4..57c065273b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2020,7 +2020,7 @@ Try to visit the target file for a richer summary line." (substring (buffer-substring bol (point-at-eol))) (hi-beg (- beg bol)) (hi-end (- (min (point-at-eol) end) bol))) - (add-face-text-property hi-beg hi-end 'highlight + (add-face-text-property hi-beg hi-end 'xref-match t substring) (list substring (1+ (current-line)) (eglot-current-column) (- end beg)))))) commit 83b993258b9c96cde165b4055fe1d32820907e99 Author: João Távora Date: Sat Apr 10 14:31:14 2021 +0100 Attempt to speed up initial directory/glob correspondence In https://github.com/joaotavora/eglot/issues/602, not only a new glob processing system was implemented, but also a new, more correct, way to look for directories that might hold files matched by one of these globs. Answering this question is important because the file watchers for 'workspace/didChangeWatchedFiles' are placed on a per-directory basis. Previously, a glob such as /foo/**/bar/*.el would fail to produce practical file-watching effects because /foo/**/bar/ isn't really a directory. However, answering this question is also expensive, as the globs sent by the LSP server are meant to match files, not directories. The only way is to list all files under the project's root directory and test each glob on each one. If it matches at least one file, that file's directory is meant to be watched. We suspect that in https://github.com/joaotavora/eglot/issues/645 and https://github.com/joaotavora/eglot/issues/633 we are falling victim to LSP server who serve a tremendous unoptimized number of globs, one for each file. So instead of sending just '/foo/**/bar/*.el' they send '/foo/**/bar/quux.el', '/foo/**/bar/quuz.el', etc... which would tremendeously slow down the process. But this is only a suspicion. This commit tries some simple optimizations: if a directory is known to be watch-worthy becasue one of its files matched a single glob, no more files under that directory are tried. This should help somewhat. Also fixed a bug in 'eglot--files-recursively', though I suspect that doesn't make that much of a difference. * eglot.el (eglot--directories-matched-by-globs): New helper. (eglot--files-recursively): Fix bug. GitHub-reference: per https://github.com/joaotavora/eglot/issues/645 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c236388676..59804da9e4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2674,10 +2674,7 @@ at point. With prefix argument, prompt for ACTION-KIND." (eglot--glob-compile globPattern t t)) watchers)) (dirs-to-watch - (cl-loop for f in (eglot--files-recursively) - when (cl-loop for g in globs thereis (funcall g f)) - collect (file-name-directory f) into dirs - finally (cl-return (delete-dups dirs))))) + (eglot--directories-matched-by-globs default-directory globs))) (cl-labels ((handle-event (event) @@ -2784,9 +2781,23 @@ If NOERROR, return predicate, else erroring function." (cl-loop with default-directory = dir with completion-regexp-list = '("^[^.]") for f in (file-name-all-completions "" dir) - if (file-name-directory f) append (eglot--files-recursively f) + if (file-directory-p f) append (eglot--files-recursively f) else collect (expand-file-name f)))) +(defun eglot--directories-matched-by-globs (dir globs) + "Discover subdirectories of DIR with files matched by one of GLOBS. +Each element of GLOBS is either an uncompiled glob-string or a +compiled glob." + (setq globs (cl-loop for g in globs + collect (if (stringp g) (eglot--glob-compile g t t) g))) + (cl-loop for f in (eglot--files-recursively dir) + for fdir = (file-name-directory f) + when (and + (not (member fdir dirs)) + (cl-loop for g in globs thereis (funcall g f))) + collect fdir into dirs + finally (cl-return (delete-dups dirs)))) + ;;; Rust-specific ;;; commit dcbb5a8d0bf3936363d1cfcb5b074199f3a67353 Author: João Távora Date: Fri Apr 2 00:21:27 2021 +0100 Generalize eglot-flymake-backend Loosen coupling between eglot-flymake-backend and flymake-mode. The flymake-mode check in 'eglot-handle-notification publishDiagnostics' was a hack (and it wasn't even functioning correctly on M-x eglot-shutdown/eglot-reconnect). This should also allow eglot-flymake-backend to be driven by diagnostic-annotating frontends other than Flymake, such as the popular Flycheck package. * eglot.el (eglot--managed-mode): Use eglot--report-to-flymake. (eglot-handle-notification textDocument/publishDiagnostics): Use eglot--report-to-flymake. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/596 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 03e8baa8b7..c236388676 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1445,7 +1445,9 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (cl-loop for (var . saved-binding) in eglot--saved-bindings do (set (make-local-variable var) saved-binding)) (remove-function (local 'imenu-create-index-function) #'eglot-imenu) - (setq eglot--current-flymake-report-fn nil) + (when eglot--current-flymake-report-fn + (eglot--report-to-flymake nil) + (setq eglot--current-flymake-report-fn nil)) (let ((server eglot--cached-server)) (setq eglot--cached-server nil) (when server @@ -1680,17 +1682,8 @@ COMMAND is a symbol naming the command." (t 'eglot-note)) message `((eglot-lsp-diag . ,diag-spec))))) into diags - finally (cond ((and flymake-mode eglot--current-flymake-report-fn) - (save-restriction - (widen) - (funcall eglot--current-flymake-report-fn diags - ;; If the buffer hasn't changed since last - ;; call to the report function, flymake won't - ;; delete old diagnostics. Using :region - ;; keyword forces flymake to delete - ;; them (github#159). - :region (cons (point-min) (point-max)))) - (setq eglot--unreported-diagnostics nil)) + finally (cond (eglot--current-flymake-report-fn + (eglot--report-to-flymake diags)) (t (setq eglot--unreported-diagnostics (cons t diags)))))) (jsonrpc--debug server "Diagnostics received for unvisited %s" uri))) @@ -1970,13 +1963,28 @@ When called interactively, use the currently active server" :textDocument (eglot--TextDocumentIdentifier)))) (defun eglot-flymake-backend (report-fn &rest _more) - "An EGLOT Flymake backend. -Calls REPORT-FN maybe if server publishes diagnostics in time." + "A Flymake backend for Eglot. +Calls REPORT-FN (or arranges for it to be called) when the server +publishes diagnostics. Between calls to this function, REPORT-FN +may be called multiple times (respecting the protocol of +`flymake-backend-functions')." (setq eglot--current-flymake-report-fn report-fn) ;; Report anything unreported (when eglot--unreported-diagnostics - (funcall report-fn (cdr eglot--unreported-diagnostics)) - (setq eglot--unreported-diagnostics nil))) + (eglot--report-to-flymake (cdr eglot--unreported-diagnostics)))) + +(defun eglot--report-to-flymake (diags) + "Internal helper for `eglot-flymake-backend'." + (save-restriction + (widen) + (funcall eglot--current-flymake-report-fn diags + ;; If the buffer hasn't changed since last + ;; call to the report function, flymake won't + ;; delete old diagnostics. Using :region + ;; keyword forces flymake to delete + ;; them (github#159). + :region (cons (point-min) (point-max)))) + (setq eglot--unreported-diagnostics nil)) (defun eglot-xref-backend () "EGLOT xref backend." 'eglot) commit 602004dbb5286e20fffae9c034b89aab994ad826 Author: rvs314 <71688932+rvs314@users.noreply.github.com> Date: Fri Mar 26 06:08:03 2021 -0400 Offer better control over "languageid" value sent to lsp Handles the issue of languages whose major mode has a different name than the name that the LSP server expects for the language. One can now: (put 'favourite-major-mode 'eglot-language-id "foobarbaz") And "foobarbaz" will be used as the LSP "languageId" value. * eglot.el (eglot--TextDocumentItem): Consult 'eglot-language-id. Copyright-paperwork-exempt: yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/525 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5914f2ddd4..03e8baa8b7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1741,9 +1741,11 @@ THINGS are either registrations or unregisterations (sic)." (append (eglot--VersionedTextDocumentIdentifier) (list :languageId - (if (string-match "\\(.*\\)-mode" (symbol-name major-mode)) - (match-string 1 (symbol-name major-mode)) - "unknown") + (cond + ((get major-mode 'eglot-language-id)) + ((string-match "\\(.*\\)-mode" (symbol-name major-mode)) + (match-string 1 (symbol-name major-mode))) + (t "unknown")) :text (eglot--widening (buffer-substring-no-properties (point-min) (point-max)))))) commit 8a5f63d8bd91a2ff55c0e78ff8e893c79d0f5f94 Author: Johnathan C. Maudlin <13183098+jcmdln@users.noreply.github.com> Date: Fri Mar 19 16:10:43 2021 -0400 Add support for zls, the zig language server * eglot.el (eglot-server-programs): Add zig-mode entry. * README.md (Connecting to a server): Mention zls. Copyright-paperwork-exempt: yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/646 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0341ffdecc..5914f2ddd4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -125,7 +125,8 @@ language-server/bin/php-language-server.php")) (erlang-mode . ("erlang_ls" "--transport" "stdio")) (nix-mode . ("rnix-lsp")) (gdscript-mode . ("localhost" 6008)) - (f90-mode . ("fortls"))) + (f90-mode . ("fortls")) + (zig-mode . ("zls"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated commit a3e6b3b86c41c077b0939bc957d362f68f49f748 Author: bc² Date: Mon Mar 15 06:49:07 2021 -0300 Add new command eglot-shutdown-all Also closes https://github.com/joaotavora/eglot/issues/644 Co-authored-by: João Távora Copyright-paperwork-exempt: yes * NEWS.md: mention new command * README.md (Commands and keybindings): mention new command. Tweak documentation for eglot-shutdown and eglot-reconnect. * eglot.el (eglot-shutdown): Tweak docstring. (eglot-shutdown-all): New command. GitHub-reference: close https://github.com/joaotavora/eglot/issues/643 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 79b90886f6..0341ffdecc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -652,11 +652,12 @@ Interactively, read SERVER from the minibuffer unless there is only one and it's managing the current buffer. Forcefully quit it if it doesn't respond within TIMEOUT seconds. -Don't leave this function with the server still running. +TIMEOUT defaults to 1.5 seconds. Don't leave this function with +the server still running. If PRESERVE-BUFFERS is non-nil (interactively, when called with a prefix argument), do not kill events and output buffers of -SERVER. ." +SERVER." (interactive (list (eglot--read-server "Shutdown which server" (eglot-current-server)) t nil current-prefix-arg)) @@ -670,6 +671,13 @@ SERVER. ." (jsonrpc-shutdown server (not preserve-buffers)) (unless preserve-buffers (kill-buffer (jsonrpc-events-buffer server))))) +(defun eglot-shutdown-all (&optional preserve-buffers) + "Politely ask all language servers to quit, in order. +PRESERVE-BUFFERS as in `eglot-shutdown', which see." + (interactive (list current-prefix-arg)) + (cl-loop for ss being the hash-values of eglot--servers-by-project + do (cl-loop for s in ss do (eglot-shutdown s nil preserve-buffers)))) + (defun eglot--on-shutdown (server) "Called by jsonrpc.el when SERVER is already dead." ;; Turn off `eglot--managed-mode' where appropriate. commit e43c1ee0d46f3af209a0a263fa059b1ed839b0ad Author: João Távora Date: Sat Mar 6 21:20:42 2021 +0000 Autoload eglot-workspace-configuration's safe-l-v spec This is useful for those who edit files in a certain source tree where this directory-local variable is set, but without having yet loaded eglot.el. Those users would be bothered by the usual risky-local-variable prompt. * eglot.el (eglot-workspace-configuration): Add autoload cookie. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/555 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 14c0fbc544..79b90886f6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1848,6 +1848,7 @@ Records BEG, END and PRE-CHANGE-LENGTH locally." SECTION should be a keyword or a string, value can be anything that can be converted to JSON.") +;;;###autoload (put 'eglot-workspace-configuration 'safe-local-variable 'listp) (defun eglot-signal-didChangeConfiguration (server) commit 21b8ebf585498c0376e264f869ebeedb6ae4683a Author: Brian Leung Date: Sat Mar 6 13:17:07 2021 -0800 Indicate support for activeparameter * eglot.el (eglot-client-capabilities): Indicate :activeParameterSupport. Fixup of commit bdf57d5d4e888a6a7b4066b87497da8a8d9e36de. GitHub-reference: per https://github.com/joaotavora/eglot/issues/605 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f7632cb599..14c0fbc544 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -569,7 +569,8 @@ treated as in `eglot-dbind'." :signatureHelp (list :dynamicRegistration :json-false :signatureInformation `(:parameterInformation - (:labelOffsetSupport t))) + (:labelOffsetSupport t) + :activeParameterSupport t)) :references `(:dynamicRegistration :json-false) :definition `(:dynamicRegistration :json-false) :declaration `(:dynamicRegistration :json-false) commit 88b8b9364331573646cfc5b481d47ecd3323f0d8 Author: João Távora Date: Sat Mar 6 21:15:33 2021 +0000 Simplify eglot--apply-workspace-edit Suggested by Brian Leung. * eglot.el (eglot--apply-workspace-edit): simplify GitHub-reference: fix https://github.com/joaotavora/eglot/issues/620 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 964658a8b2..f7632cb599 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2530,8 +2530,7 @@ is not active." (eglot--dbind ((VersionedTextDocumentIdentifier) uri version) textDocument (list (eglot--uri-to-path uri) edits version))) - documentChanges)) - edit) + documentChanges))) (cl-loop for (uri edits) on changes by #'cddr do (push (list (eglot--uri-to-path uri) edits) prepared)) (if (or confirm @@ -2541,17 +2540,11 @@ is not active." (format "[eglot] Server wants to edit:\n %s\n Proceed? " (mapconcat #'identity (mapcar #'car prepared) "\n "))) (eglot--error "User cancelled server edit"))) - (while (setq edit (car prepared)) - (pcase-let ((`(,path ,edits ,version) edit)) - (with-current-buffer (find-file-noselect path) - (eglot--apply-text-edits edits version)) - (pop prepared)) - t) - (unwind-protect - (if prepared (eglot--warn "Caution: edits of files %s failed." - (mapcar #'car prepared)) - (eldoc) - (eglot--message "Edit successful!")))))) + (cl-loop for edit in prepared + for (path edits version) = edit + do (with-current-buffer (find-file-noselect path) + (eglot--apply-text-edits edits version)) + finally (eldoc) (eglot--message "Edit successful!"))))) (defun eglot-rename (newname) "Rename the current symbol to NEWNAME." commit 8c0b2ca7cf76ec5215029ff0ab15f4bec1b1f12e Author: Augusto Stoffel Date: Thu Feb 25 15:48:41 2021 +0100 Remove highlight overlays immediately when symbol edited * eglot.el (eglot--highlight-piggyback): Add modification-hooks property to the created overlays. Co-authored-by: João Távora GitHub-reference: fix https://github.com/joaotavora/eglot/issues/626 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d4300e186b..964658a8b2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2421,7 +2421,8 @@ is not active." (eglot--range-region range))) (let ((ov (make-overlay beg end))) (overlay-put ov 'face 'eglot-highlight-symbol-face) - (overlay-put ov 'evaporate t) + (overlay-put ov 'modification-hooks + `(,(lambda (o &rest _) (delete-overlay o)))) ov))) highlights)))) :deferred :textDocument/documentHighlight) commit ff91ba70cd24e7469c7c171234363c82b1566cd6 Author: Theodor Thornhill Date: Sat Mar 6 21:18:48 2021 +0100 Convert colon to hex in uri On windows, in the path portion of the URI, ':' must be hexified to '%3A'. In the URL scheme, the ':' stays. * eglot.el (eglot--uri-path-allowed-chars): define what characters are allowed in path portion of URI. * eglot.el (eglot--path-to-uri): ensure colon in 'file://' stays, but and others are hexified. Co-authored-by: João Távora GitHub-reference: fix https://github.com/joaotavora/eglot/issues/638 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e0896c8501..d4300e186b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1190,13 +1190,19 @@ If optional MARKER, return a marker instead" (funcall eglot-move-to-column-function col))) (if marker (copy-marker (point-marker)) (point))))) +(defconst eglot--uri-path-allowed-chars + (let ((vec (copy-sequence url-path-allowed-chars))) + (aset vec ?: nil) ;; see github#639 + vec) + "Like `url-path-allows-chars' but more restrictive.") + (defun eglot--path-to-uri (path) "URIfy PATH." - (url-hexify-string - (concat "file://" (if (eq system-type 'windows-nt) "/") + (concat "file://" (if (eq system-type 'windows-nt) "/") + (url-hexify-string ;; Again watch out for trampy paths. - (directory-file-name (file-local-name (file-truename path)))) - url-path-allowed-chars)) + (directory-file-name (file-local-name (file-truename path))) + eglot--uri-path-allowed-chars))) (defun eglot--uri-to-path (uri) "Convert URI to file path, helped by `eglot--current-server'." commit 2076d345655206254f6749cf710c150dfec313dd Author: Brian Cully Date: Tue Mar 2 16:13:07 2021 -0500 Add tramp support Also close https://github.com/joaotavora/eglot/issues/463, close https://github.com/joaotavora/eglot/issues/84. Thanks to Brian Cully for the original simple idea. The basic technique is to pass :file-handler t to make-process, then tweak eglot--uri-to-path and eglot--path-to-uri, along with some other functions, to be aware of "trampy" paths". Crucially, a "stty hack" was needed. It has been encapsulated in a new a new eglot--cmd helper, which contains a comment explaining the hack. Co-authored-by: João Távora * eglot.el (eglot--executable-find): Shim two-arg executable-find function only available on Emacs 27. (eglot--guess-contact): Use eglot--executable-find. (eglot--cmd): New helper. (eglot--connect): Use eglot--cmd. Use :file-handler arg to make-process. (eglot--connect, eglot--path-to-uri): Be aware of trampy file names. * eglot-tests.el (eglot-tests--auto-detect-running-server-1): New helper. (eglot--guessing-contact): Better mock for executable-find. (eglot--tramp-test): New test. * NEWS.md: mention TRAMP support. * README.md: mention TRAMP support. GitHub-reference: close https://github.com/joaotavora/eglot/issues/637 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 610e57b5ad..e0896c8501 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -244,6 +244,10 @@ let the buffer grow forever." (defconst eglot--{} (make-hash-table) "The empty JSON object.") +(defun eglot--executable-find (command &optional remote) + "Like Emacs 27's `executable-find', ignore REMOTE on Emacs 26." + (if (>= emacs-major-version 27) (executable-find command remote) + (executable-find command))) ;;; Message verification helpers @@ -753,7 +757,7 @@ be guessed." ((null guess) (format "[eglot] Sorry, couldn't guess for `%s'!\n%s" managed-mode base-prompt)) - ((and program (not (executable-find program))) + ((and program (not (eglot--executable-find program t))) (concat (format "[eglot] I guess you want to run `%s'" program-guess) (format ", but I can't find `%s' in PATH!" program) @@ -878,6 +882,21 @@ received the initializing configuration. Each function is passed the server as an argument") +(defun eglot--cmd (contact) + "Helper for `eglot--connect'." + (if (file-remote-p default-directory) + ;; TODO: this seems like a bug, although it’s everywhere. For + ;; some reason, for remote connections only, over a pipe, we + ;; need to turn off line buffering on the tty. + ;; + ;; Not only does this seem like there should be a better way, + ;; but it almost certainly doesn’t work on non-unix systems. + (list "sh" "-c" + (string-join (cons "stty raw > /dev/null;" + (mapcar #'shell-quote-argument contact)) + " ")) + contact)) + (defun eglot--connect (managed-major-mode project class contact) "Connect to MANAGED-MAJOR-MODE, PROJECT, CLASS and CONTACT. This docstring appeases checkdoc, that's all." @@ -908,12 +927,13 @@ This docstring appeases checkdoc, that's all." (let ((default-directory default-directory)) (make-process :name readable-name - :command contact + :command (eglot--cmd contact) :connection-type 'pipe :coding 'utf-8-emacs-unix :noquery t :stderr (get-buffer-create - (format "*%s stderr*" readable-name))))))))) + (format "*%s stderr*" readable-name)) + :file-handler t))))))) (spread (lambda (fn) (lambda (server method params) (apply fn server method (append params nil))))) (server @@ -943,10 +963,15 @@ This docstring appeases checkdoc, that's all." (jsonrpc-async-request server :initialize - (list :processId (unless (eq (jsonrpc-process-type server) - 'network) - (emacs-pid)) - :rootPath (expand-file-name default-directory) + (list :processId + (unless (or (file-remote-p default-directory) + (eq (jsonrpc-process-type server) + 'network)) + (emacs-pid)) + ;; Maybe turn trampy `/ssh:foo@bar:/path/to/baz.py' + ;; into `/path/to/baz.py', so LSP groks it. + :rootPath (expand-file-name + (file-local-name default-directory)) :rootUri (eglot--path-to-uri default-directory) :initializationOptions (eglot-initialization-options server) @@ -1169,15 +1194,23 @@ If optional MARKER, return a marker instead" "URIfy PATH." (url-hexify-string (concat "file://" (if (eq system-type 'windows-nt) "/") - (directory-file-name (file-truename path))) + ;; Again watch out for trampy paths. + (directory-file-name (file-local-name (file-truename path)))) url-path-allowed-chars)) (defun eglot--uri-to-path (uri) - "Convert URI to a file path." + "Convert URI to file path, helped by `eglot--current-server'." (when (keywordp uri) (setq uri (substring (symbol-name uri) 1))) - (let ((retval (url-filename (url-generic-parse-url (url-unhex-string uri))))) - (if (and (eq system-type 'windows-nt) (cl-plusp (length retval))) - (substring retval 1) retval))) + (let* ((retval (url-filename (url-generic-parse-url (url-unhex-string uri)))) + (normalized (if (and (eq system-type 'windows-nt) + (cl-plusp (length retval))) + (substring retval 1) + retval)) + (server (eglot-current-server)) + (remote-prefix (and server + (file-remote-p + (project-root (eglot--project server)))))) + (concat remote-prefix normalized))) (defun eglot--snippet-expansion-fn () "Compute a function to expand snippets. commit a6229c50e8e00b895eed733f2626e6209043f504 Author: Theodor Thornhill Date: Wed Mar 3 11:08:24 2021 +0100 Correctly protect against zero-length completion items Close https://github.com/joaotavora/eglot/issues/636. * eglot.el (eglot-completion-at-point): check for zero length string in proxy rather than the item. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/635 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0cb5839f6d..610e57b5ad 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2133,7 +2133,7 @@ is not active." insertText) (t (string-trim-left label))))) - (unless (zerop (length item)) + (unless (zerop (length proxy)) (put-text-property 0 1 'eglot--lsp-item item proxy)) proxy)) items))))) commit fa3ab318fa0970b9590db92bf8230336fcbc61a7 Author: Theodor Thornhill Date: Sat Feb 27 11:19:35 2021 +0100 Protect against empty uris on windows Per https://github.com/joaotavora/eglot/issues/630. * eglot.el (eglot--uri-to-path): Check string length Co-authored-by: João Távora GitHub-reference: fix https://github.com/joaotavora/eglot/issues/610 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 017b8200bf..0cb5839f6d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1176,7 +1176,8 @@ If optional MARKER, return a marker instead" "Convert URI to a file path." (when (keywordp uri) (setq uri (substring (symbol-name uri) 1))) (let ((retval (url-filename (url-generic-parse-url (url-unhex-string uri))))) - (if (eq system-type 'windows-nt) (substring retval 1) retval))) + (if (and (eq system-type 'windows-nt) (cl-plusp (length retval))) + (substring retval 1) retval))) (defun eglot--snippet-expansion-fn () "Compute a function to expand snippets. commit 5a4ca5fdf3082426a5d6aa8595492b3b1d50fd1b Author: João Távora Date: Fri Feb 26 20:11:08 2021 +0000 Urify better See also https://microsoft.github.io/language-server-protocol/specifications/specification-current/#uri. * eglot.el (eglot--path-to-uri): use directory-file-name. GitHub-reference: per https://github.com/joaotavora/eglot/issues/627 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 86e0d01118..017b8200bf 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1168,7 +1168,8 @@ If optional MARKER, return a marker instead" (defun eglot--path-to-uri (path) "URIfy PATH." (url-hexify-string - (concat "file://" (if (eq system-type 'windows-nt) "/") (file-truename path)) + (concat "file://" (if (eq system-type 'windows-nt) "/") + (directory-file-name (file-truename path))) url-path-allowed-chars)) (defun eglot--uri-to-path (uri) commit 7c66a3e78957eea2fd5c80ffceeee4ad0c899927 Author: João Távora Date: Fri Feb 26 19:30:30 2021 +0000 Fixup last commit to fix * eglot.el (eglot-handle-request): Fixup. Use let* GitHub-reference: https://github.com/joaotavora/eglot/issues/627 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 98a1059e15..86e0d01118 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1572,16 +1572,16 @@ COMMAND is a symbol naming the command." (cl-defmethod eglot-handle-request (_server (_method (eql window/showMessageRequest)) &key type message actions) "Handle server request window/showMessageRequest" - (let ((actions (append actions nil)) ;; gh#627 - (label (completing-read - (concat - (format (propertize "[eglot] Server reports (type=%s): %s" - 'face (if (<= type 1) 'error)) - type message) - "\nChoose an option: ") - (or (mapcar (lambda (obj) (plist-get obj :title)) actions) - '("OK")) - nil t (plist-get (elt actions 0) :title)))) + (let* ((actions (append actions nil)) ;; gh#627 + (label (completing-read + (concat + (format (propertize "[eglot] Server reports (type=%s): %s" + 'face (if (<= type 1) 'error)) + type message) + "\nChoose an option: ") + (or (mapcar (lambda (obj) (plist-get obj :title)) actions) + '("OK")) + nil t (plist-get (elt actions 0) :title)))) (if label `(:title ,label) :null))) (cl-defmethod eglot-handle-notification commit b3f31e0b657706baed13eeee2b6ae3eb1bd04049 Author: João Távora Date: Fri Feb 26 18:49:59 2021 +0000 Handle empty actions array in window/showmessagerequest * eglot.el (eglot-handle-request window/showMessageRequest): Handle empty actions. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/627 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 851f2e68ef..98a1059e15 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1572,7 +1572,8 @@ COMMAND is a symbol naming the command." (cl-defmethod eglot-handle-request (_server (_method (eql window/showMessageRequest)) &key type message actions) "Handle server request window/showMessageRequest" - (let ((label (completing-read + (let ((actions (append actions nil)) ;; gh#627 + (label (completing-read (concat (format (propertize "[eglot] Server reports (type=%s): %s" 'face (if (<= type 1) 'error)) commit 93cbf54609909591af1bb864e12beda4141a4d89 Author: João Távora Date: Wed Feb 24 11:27:59 2021 +0000 Handle null reply for textdocument/definition * eglot.el (eglot--lsp-xrefs-for-method): Handle null response from textDocument/definition & friends. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/625 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13fe74a905..851f2e68ef 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2008,7 +2008,7 @@ Try to visit the target file for a richer summary line." (eglot--lambda ((Location) uri range) (collect (eglot--xref-make-match (symbol-name (symbol-at-point)) uri range))) - (if (vectorp response) response (list response)))))) + (if (vectorp response) response (and response (list response))))))) (cl-defun eglot--lsp-xref-helper (method &key extra-params capability ) "Helper for `eglot-find-declaration' & friends." commit 514f80333b5d1aaff5c1ed1f03b3a49321381073 Author: Jonathan del Strother Date: Sun Feb 21 10:07:57 2021 +0000 Silence messages while formatting markup Also fix https://github.com/joaotavora/eglot/issues/501. Prior to this, activating gfm-view-mode could echo messages like "markdown-mode math support enabled" to the minibuffer. Message are both silenced from from the minibuffer and the *Messaages* log. Co-authored-by: João Távora Copyright-paperwork-exempt: yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/502 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 80780f57f9..13fe74a905 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1196,7 +1196,9 @@ Doubles as an indicator of snippet support." (with-temp-buffer (setq-local markdown-fontify-code-blocks-natively t) (insert string) - (ignore-errors (delay-mode-hooks (funcall mode))) + (let ((inhibit-message t) + (message-log-max nil)) + (ignore-errors (delay-mode-hooks (funcall mode)))) (font-lock-ensure) (string-trim (filter-buffer-substring (point-min) (point-max)))))) commit e6fac3807870cc46ed2c2b97447265d1cb4c0cb0 Author: João Távora Date: Wed Feb 3 10:41:40 2021 +0000 Simplify dir-watching strategy of w/didchangewatchedfiles Instead of massaging the globPattern to match directories instead of files, which is fragile, gather the list of directoris to watch by matching the globPattern against every file recursively (except hidden files and dirs). This is still not 100% correct, but should do the right thing is most cases. Notably, if the correct dirs are being watched, the glob pattern is matched against all existing and new files in those directories, which does include hidden files. * eglot.el (eglot-register-capability): match file globs against files only. (eglot--files-recursively): Rename from eglot--directories-recursively. GitHub-reference: per https://github.com/joaotavora/eglot/issues/602 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 90d973c6b6..80780f57f9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2612,25 +2612,20 @@ at point. With prefix argument, prompt for ACTION-KIND." (let* (success (globs (mapcar (eglot--lambda ((FileSystemWatcher) globPattern) - (cons - (eglot--glob-compile globPattern t t) - (eglot--glob-compile - (replace-regexp-in-string "/[^/]*$" "/" globPattern) t t))) + (eglot--glob-compile globPattern t t)) watchers)) (dirs-to-watch - (cl-loop for dir in (eglot--directories-recursively) - when (cl-loop for g in globs - thereis (ignore-errors (funcall (cdr g) dir))) - collect dir))) + (cl-loop for f in (eglot--files-recursively) + when (cl-loop for g in globs thereis (funcall g f)) + collect (file-name-directory f) into dirs + finally (cl-return (delete-dups dirs))))) (cl-labels ((handle-event (event) (pcase-let ((`(,desc ,action ,file ,file1) event)) (cond ((and (memq action '(created changed deleted)) - (cl-find file (mapcar #'car globs) - :test (lambda (f glob) - (funcall glob f)))) + (cl-find file globs :test (lambda (f g) (funcall g f)))) (jsonrpc-notify server :workspace/didChangeWatchedFiles `(:changes ,(vector `(:uri ,(eglot--path-to-uri file) @@ -2724,14 +2719,14 @@ If NOERROR, return predicate, else erroring function." (when (eq ?! (aref arg 1)) (aset arg 1 ?^)) `(,self () (re-search-forward ,(concat "\\=" arg)) (,next))) -(defun eglot--directories-recursively (&optional dir) +(defun eglot--files-recursively (&optional dir) "Because `directory-files-recursively' isn't complete in 26.3." (cons (setq dir (expand-file-name (or dir default-directory))) - (cl-loop - with default-directory = dir - with completion-regexp-list = '("^[^.]") - for f in (file-name-all-completions "" dir) - when (file-directory-p f) append (eglot--directories-recursively f)))) + (cl-loop with default-directory = dir + with completion-regexp-list = '("^[^.]") + for f in (file-name-all-completions "" dir) + if (file-name-directory f) append (eglot--files-recursively f) + else collect (expand-file-name f)))) ;;; Rust-specific commit 14d901c58880f50712585951163bcd7a6567d718 Author: João Távora Date: Wed Feb 3 00:43:35 2021 +0000 Tweak glob-parsing grammar Alternative groups {} don't bork on forward slash. * eglot.el (eglot--glob-parse): Tweak {} grammar. GitHub-reference: per https://github.com/joaotavora/eglot/issues/602 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9944b18a2e..90d973c6b6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2671,7 +2671,7 @@ at point. With prefix argument, prompt for ACTION-KIND." with grammar = '((:** "\\*\\*/?" eglot--glob-emit-**) (:* "\\*" eglot--glob-emit-*) (:? "\\?" eglot--glob-emit-?) - (:{} "{[^][/*{}]+}" eglot--glob-emit-{}) + (:{} "{[^][*{}]+}" eglot--glob-emit-{}) (:range "\\[\\^?[^][/,*{}]+\\]" eglot--glob-emit-range) (:literal "[^][,*?{}]+" eglot--glob-emit-self)) until (eobp) commit c453d8df3603a7bacad30647778e92e960ead18b Author: Philip Kaludercic Date: Mon Feb 1 18:20:37 2021 +0000 Make eglot-ignored-server-capabilites defcustom a set Co-authored-by: João Távora * eglot.el (eglot-ignored-server-capabilites): Now a set. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/467 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 50fb695319..9944b18a2e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1205,30 +1205,29 @@ Doubles as an indicator of snippet support." You could add, for instance, the symbol `:documentHighlightProvider' to prevent automatic highlighting under cursor." - :type '(repeat - (choice - (const :tag "Documentation on hover" :hoverProvider) - (const :tag "Code completion" :completionProvider) - (const :tag "Function signature help" :signatureHelpProvider) - (const :tag "Go to definition" :definitionProvider) - (const :tag "Go to type definition" :typeDefinitionProvider) - (const :tag "Go to implementation" :implementationProvider) - (const :tag "Go to declaration" :implementationProvider) - (const :tag "Find references" :referencesProvider) - (const :tag "Highlight symbols automatically" :documentHighlightProvider) - (const :tag "List symbols in buffer" :documentSymbolProvider) - (const :tag "List symbols in workspace" :workspaceSymbolProvider) - (const :tag "Execute code actions" :codeActionProvider) - (const :tag "Code lens" :codeLensProvider) - (const :tag "Format buffer" :documentFormattingProvider) - (const :tag "Format portion of buffer" :documentRangeFormattingProvider) - (const :tag "On-type formatting" :documentOnTypeFormattingProvider) - (const :tag "Rename symbol" :renameProvider) - (const :tag "Highlight links in document" :documentLinkProvider) - (const :tag "Decorate color references" :colorProvider) - (const :tag "Fold regions of buffer" :foldingRangeProvider) - (const :tag "Execute custom commands" :executeCommandProvider) - (symbol :tag "Other")))) + :type '(set + :tag "Tick the ones you're not interested in" + (const :tag "Documentation on hover" :hoverProvider) + (const :tag "Code completion" :completionProvider) + (const :tag "Function signature help" :signatureHelpProvider) + (const :tag "Go to definition" :definitionProvider) + (const :tag "Go to type definition" :typeDefinitionProvider) + (const :tag "Go to implementation" :implementationProvider) + (const :tag "Go to declaration" :implementationProvider) + (const :tag "Find references" :referencesProvider) + (const :tag "Highlight symbols automatically" :documentHighlightProvider) + (const :tag "List symbols in buffer" :documentSymbolProvider) + (const :tag "List symbols in workspace" :workspaceSymbolProvider) + (const :tag "Execute code actions" :codeActionProvider) + (const :tag "Code lens" :codeLensProvider) + (const :tag "Format buffer" :documentFormattingProvider) + (const :tag "Format portion of buffer" :documentRangeFormattingProvider) + (const :tag "On-type formatting" :documentOnTypeFormattingProvider) + (const :tag "Rename symbol" :renameProvider) + (const :tag "Highlight links in document" :documentLinkProvider) + (const :tag "Decorate color references" :colorProvider) + (const :tag "Fold regions of buffer" :foldingRangeProvider) + (const :tag "Execute custom commands" :executeCommandProvider))) (defun eglot--server-capable (&rest feats) "Determine if current server is capable of FEATS." commit aa4e58409c6c7a394a9a1292b1f5e34d5377323d Author: João Távora Date: Mon Feb 1 17:23:07 2021 +0000 Speed up glob matching 2x with-temp-buffer was taking a lot of time, presumably because it kills the buffer. Since emacs is single-threaded, we can safely reuse a single buffer. * eglot.el (eglot--glob-parse): Simplify grammar. (eglot--glob-compile): Don't with-temp-buffer. GitHub-reference: per https://github.com/joaotavora/eglot/issues/602 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 44648ae41b..50fb695319 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2672,10 +2672,9 @@ at point. With prefix argument, prompt for ACTION-KIND." with grammar = '((:** "\\*\\*/?" eglot--glob-emit-**) (:* "\\*" eglot--glob-emit-*) (:? "\\?" eglot--glob-emit-?) - (:/ "/" eglot--glob-emit-self) (:{} "{[^][/*{}]+}" eglot--glob-emit-{}) (:range "\\[\\^?[^][/,*{}]+\\]" eglot--glob-emit-range) - (:literal "[^][/,*?{}]+" eglot--glob-emit-self)) + (:literal "[^][,*?{}]+" eglot--glob-emit-self)) until (eobp) collect (cl-loop for (_token regexp emitter) in grammar @@ -2687,7 +2686,8 @@ at point. With prefix argument, prompt for ACTION-KIND." "Convert GLOB into Elisp function. Maybe BYTE-COMPILE it. If NOERROR, return predicate, else erroring function." (let* ((states (eglot--glob-parse glob)) - (body `(with-temp-buffer + (body `(with-current-buffer (get-buffer-create " *eglot-glob-matcher*") + (erase-buffer) (save-excursion (insert string)) (cl-labels ,(cl-loop for (this that) on states for (self emit text) = this commit 176a6df74e0bbf3e360f2af72c02a70211fd5431 Author: Felicián Németh Date: Mon Feb 1 17:02:58 2021 +0100 Support phps-mode phps-mode is available from ELPA, php-mode isn't. * eglot.el (eglot-server-programs): Recognize phps-mode as a PHP mode. Co-authored-by: João Távora GitHub-reference: fix https://github.com/joaotavora/eglot/issues/418 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e1387235ff..44648ae41b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -97,11 +97,10 @@ (defvar eglot-server-programs '((rust-mode . (eglot-rls "rls")) (python-mode . ("pyls")) - ((js-mode - typescript-mode) + ((js-mode typescript-mode) . ("typescript-language-server" "--stdio")) (sh-mode . ("bash-language-server" "start")) - (php-mode + ((php-mode phps-mode) . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php")) ((c++-mode c-mode) . ("ccls")) commit d64ea753f97e96093c148d72b295c6beb1a6443f Author: Brian Leung Date: Mon Feb 1 07:44:52 2021 -0800 Remove duplicate entry for "registration" lsp type * eglot.el (eglot--lsp-interface-alist): Remove extra Registration entry. Copyright-paperwork-exempt: yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/612 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 158a108499..e1387235ff 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -272,7 +272,6 @@ let the buffer grow forever." (Position (:line :character)) (Range (:start :end)) (Registration (:id :method) (:registerOptions)) - (Registration (:id :method) (:registerOptions)) (ResponseError (:code :message) (:data)) (ShowMessageParams (:type :message)) (ShowMessageRequestParams (:type :message) (:actions)) commit 5e3fa130baccc66e551d62b9b3daab848bcbc6ef Author: João Távora Date: Mon Feb 1 15:39:15 2021 +0000 Prefer typescript-language-server for js&ts * README.md (Connecting to a server): Prefer typescript-language-server. * eglot.el (eglot-server-programs): Use typescript-language-server. GitHub-reference: close https://github.com/joaotavora/eglot/issues/566 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3ea8a2fa7e..158a108499 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -99,7 +99,7 @@ (python-mode . ("pyls")) ((js-mode typescript-mode) - . ("javascript-typescript-stdio")) + . ("typescript-language-server" "--stdio")) (sh-mode . ("bash-language-server" "start")) (php-mode . ("php" "vendor/felixfbecker/\ commit 60724b8c522ff2ef30429170223c1595c47a4d17 Author: João Távora Date: Mon Feb 1 14:03:23 2021 +0000 Also override global flymake-diagnostic-functions The global value of the flymake-diagnostic-functions is likely to be of little use in Eglot-managed buffers, so don't run it. Likely the value flymake-proc-legacy-flymake is there which is not only likely of little uses but also causes trouble in some situations. The user can easily avert this by leveraging the variable eglot-stay-out-of. * eglot.el (eglot--managed-mode): Don't run global flymake-diagnostic-functions. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/616 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f3b006da1a..3ea8a2fa7e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1367,7 +1367,7 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (eglot--setq-saving eldoc-documentation-strategy #'eldoc-documentation-enthusiast) (eglot--setq-saving xref-prompt-for-identifier nil) - (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend t)) + (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend)) (eglot--setq-saving company-backends '(company-capf)) (eglot--setq-saving company-tooltip-align-annotations t) (when (assoc 'flex completion-styles-alist) commit c758ba1a4c38f3edb9779082a7ac808e59774848 Author: Brian Leung Date: Mon Feb 1 02:44:38 2021 -0800 Explicitly require seq.el `seq-empty-p' is not autoloaded in Emacs >= 26.3, so it must be explicitly required. * eglot.el: Require seq.el. GitHub-reference: close https://github.com/joaotavora/eglot/issues/613 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 27094a2bd5..f3b006da1a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -59,6 +59,7 @@ (require 'imenu) (require 'cl-lib) (require 'project) +(require 'seq) (require 'url-parse) (require 'url-util) (require 'pcase) commit bdf57d5d4e888a6a7b4066b87497da8a8d9e36de Author: Brian Leung Date: Sun Jan 31 17:28:49 2021 -0800 Support activeparameter property for signatureinformation SignatureInformation.activeParameter is new in version 3.16.0 of the protocol. When non-nil, it is used in place of SignatureHelp.activeParameter. The latter was deemed insufficient in languages where multiple signatures for the same function may exist with arbitrary order of parameters, like Python. Co-authored-by: João Távora * eglot.el (eglot--lsp-interface-alist): Add SignatureInformation.activeParameter. * eglot.el (eglot--sig-info): Prioritize SignatureInformation.activeParameter over SignatureHelp.activeParameter. GitHub-reference: close https://github.com/joaotavora/eglot/issues/605 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 51ed1c49a9..27094a2bd5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -276,7 +276,7 @@ let the buffer grow forever." (ShowMessageParams (:type :message)) (ShowMessageRequestParams (:type :message) (:actions)) (SignatureHelp (:signatures) (:activeSignature :activeParameter)) - (SignatureInformation (:label) (:documentation :parameters)) + (SignatureInformation (:label) (:documentation :parameters :activeParameter)) (SymbolInformation (:name :kind :location) (:deprecated :containerName)) (DocumentSymbol (:name :range :selectionRange :kind) @@ -2265,14 +2265,15 @@ is not active." (if (vectorp contents) contents (list contents)) "\n"))) (when (or heading (cl-plusp (length body))) (concat heading body)))) -(defun eglot--sig-info (sigs active-sig active-param) +(defun eglot--sig-info (sigs active-sig sig-help-active-param) (cl-loop for (sig . moresigs) on (append sigs nil) for i from 0 concat - (eglot--dbind ((SignatureInformation) label documentation parameters) sig + (eglot--dbind ((SignatureInformation) label documentation parameters activeParameter) sig (with-temp-buffer (save-excursion (insert label)) - (let (params-start params-end) + (let ((active-param (or activeParameter sig-help-active-param)) + params-start params-end) ;; Ad-hoc attempt to parse label as () (when (looking-at "\\([^(]+\\)(\\([^)]+\\))") (setq params-start (match-beginning 2) params-end (match-end 2)) commit 89fccba0088f765ba6a4d02b7ca4bf53633b43be Author: João Távora Date: Sun Jan 31 18:18:02 2021 +0000 Fully handle lsp glob syntax Thanks to Brian Leung and Dan Peterson for testing and helping me spot bugs. * eglot-tests.el (eglot--glob-match): New test. * eglot.el (eglot--wildcard-to-regexp): Delete. (eglot-register-capability): Rework. (eglot--glob-parse, eglot--glob-compile, eglot--glob-emit-self) (eglot--glob-emit-**, eglot--glob-emit-*, eglot--glob-emit-?) (eglot--glob-emit-{}, eglot--glob-emit-range) (eglot--directories-recursively): New helpers. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/602 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8403e5dfdb..51ed1c49a9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2606,40 +2606,32 @@ at point. With prefix argument, prompt for ACTION-KIND." ;;; Dynamic registration ;;; -(defun eglot--wildcard-to-regexp (wildcard) - "(Very lame attempt to) convert WILDCARD to a Elisp regexp." - (cl-loop - with substs = '(("{" . "\\\\(") - ("}" . "\\\\)") - ("," . "\\\\|")) - with string = (wildcard-to-regexp wildcard) - for (pattern . rep) in substs - for target = string then result - for result = (replace-regexp-in-string pattern rep target) - finally return result)) - (cl-defmethod eglot-register-capability (server (method (eql workspace/didChangeWatchedFiles)) id &key watchers) "Handle dynamic registration of workspace/didChangeWatchedFiles" (eglot-unregister-capability server method id) (let* (success - (globs (mapcar (eglot--lambda ((FileSystemWatcher) globPattern) - globPattern) - watchers)) - (glob-dirs - (delete-dups (mapcar #'file-name-directory - (mapcan #'file-expand-wildcards globs))))) + (globs (mapcar + (eglot--lambda ((FileSystemWatcher) globPattern) + (cons + (eglot--glob-compile globPattern t t) + (eglot--glob-compile + (replace-regexp-in-string "/[^/]*$" "/" globPattern) t t))) + watchers)) + (dirs-to-watch + (cl-loop for dir in (eglot--directories-recursively) + when (cl-loop for g in globs + thereis (ignore-errors (funcall (cdr g) dir))) + collect dir))) (cl-labels ((handle-event (event) (pcase-let ((`(,desc ,action ,file ,file1) event)) (cond ((and (memq action '(created changed deleted)) - (cl-find file globs + (cl-find file (mapcar #'car globs) :test (lambda (f glob) - (string-match (eglot--wildcard-to-regexp - (expand-file-name glob)) - f)))) + (funcall glob f)))) (jsonrpc-notify server :workspace/didChangeWatchedFiles `(:changes ,(vector `(:uri ,(eglot--path-to-uri file) @@ -2652,13 +2644,13 @@ at point. With prefix argument, prompt for ACTION-KIND." (handle-event `(,desc 'created ,file1))))))) (unwind-protect (progn - (dolist (dir glob-dirs) + (dolist (dir dirs-to-watch) (push (file-notify-add-watch dir '(change) #'handle-event) (gethash id (eglot--file-watches server)))) (setq success `(:message ,(format "OK, watching %s directories in %s watchers" - (length glob-dirs) (length watchers))))) + (length dirs-to-watch) (length watchers))))) (unless success (eglot-unregister-capability server method id)))))) @@ -2669,6 +2661,79 @@ at point. With prefix argument, prompt for ACTION-KIND." (remhash id (eglot--file-watches server)) (list t "OK")) + +;;; Glob heroics +;;; +(defun eglot--glob-parse (glob) + "Compute list of (STATE-SYM EMITTER-FN PATTERN)." + (with-temp-buffer + (save-excursion (insert glob)) + (cl-loop + with grammar = '((:** "\\*\\*/?" eglot--glob-emit-**) + (:* "\\*" eglot--glob-emit-*) + (:? "\\?" eglot--glob-emit-?) + (:/ "/" eglot--glob-emit-self) + (:{} "{[^][/*{}]+}" eglot--glob-emit-{}) + (:range "\\[\\^?[^][/,*{}]+\\]" eglot--glob-emit-range) + (:literal "[^][/,*?{}]+" eglot--glob-emit-self)) + until (eobp) + collect (cl-loop + for (_token regexp emitter) in grammar + thereis (and (re-search-forward (concat "\\=" regexp) nil t) + (list (cl-gensym "state-") emitter (match-string 0))) + finally (error "Glob '%s' invalid at %s" (buffer-string) (point)))))) + +(defun eglot--glob-compile (glob &optional byte-compile noerror) + "Convert GLOB into Elisp function. Maybe BYTE-COMPILE it. +If NOERROR, return predicate, else erroring function." + (let* ((states (eglot--glob-parse glob)) + (body `(with-temp-buffer + (save-excursion (insert string)) + (cl-labels ,(cl-loop for (this that) on states + for (self emit text) = this + for next = (or (car that) 'eobp) + collect (funcall emit text self next)) + (or (,(caar states)) + (error "Glob done but more unmatched text: '%s'" + (buffer-substring (point) (point-max))))))) + (form `(lambda (string) ,(if noerror `(ignore-errors ,body) body)))) + (if byte-compile (byte-compile form) form))) + +(defun eglot--glob-emit-self (text self next) + `(,self () (re-search-forward ,(concat "\\=" (regexp-quote text))) (,next))) + +(defun eglot--glob-emit-** (_ self next) + `(,self () (or (ignore-errors (save-excursion (,next))) + (and (re-search-forward "\\=/?[^/]+/?") (,self))))) + +(defun eglot--glob-emit-* (_ self next) + `(,self () (re-search-forward "\\=[^/]") + (or (ignore-errors (save-excursion (,next))) (,self)))) + +(defun eglot--glob-emit-? (_ self next) + `(,self () (re-search-forward "\\=[^/]") (,next))) + +(defun eglot--glob-emit-{} (arg self next) + (let ((alternatives (split-string (substring arg 1 (1- (length arg))) ","))) + `(,self () + (or ,@(cl-loop for alt in alternatives + collect `(re-search-forward ,(concat "\\=" alt) nil t)) + (error "Failed matching any of %s" ',alternatives)) + (,next)))) + +(defun eglot--glob-emit-range (arg self next) + (when (eq ?! (aref arg 1)) (aset arg 1 ?^)) + `(,self () (re-search-forward ,(concat "\\=" arg)) (,next))) + +(defun eglot--directories-recursively (&optional dir) + "Because `directory-files-recursively' isn't complete in 26.3." + (cons (setq dir (expand-file-name (or dir default-directory))) + (cl-loop + with default-directory = dir + with completion-regexp-list = '("^[^.]") + for f in (file-name-all-completions "" dir) + when (file-directory-p f) append (eglot--directories-recursively f)))) + ;;; Rust-specific ;;; commit c266aa6b360a683f50b3370942fc7ebfcafc747a Author: João Távora Date: Sat Jan 30 18:01:19 2021 +0000 Flush pending changes to server before code actions request Otherwise the actions returned by the server might be stale when the user selects them. * eglot.el (eglot-code-actions): Issue jsonrpc-request with deferred=t. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/609 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 98e0bcf14f..8403e5dfdb 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2554,7 +2554,8 @@ at point. With prefix argument, prompt for ACTION-KIND." when (cdr (assoc 'eglot-lsp-diag (eglot--diag-data diag))) collect it)] - ,@(when action-kind `(:only [,action-kind])))))) + ,@(when action-kind `(:only [,action-kind])))) + :deferred t)) (menu-items (or (cl-loop for action across actions ;; Do filtering ourselves, in case the `:only' commit 93eb72de229a2774198364be7acb7131a71222cb Author: João Távora Date: Fri Jan 29 16:27:55 2021 +0000 #fix 608: fix bug in eglot-code-actions Suggested by GitHub user "vconcat". * eglot.el (eglot-code-actions): Use a vector for transmitting action-kind. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/606 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b5f6e6f73a..98e0bcf14f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2554,7 +2554,7 @@ at point. With prefix argument, prompt for ACTION-KIND." when (cdr (assoc 'eglot-lsp-diag (eglot--diag-data diag))) collect it)] - ,@(when action-kind `(:only ,action-kind)))))) + ,@(when action-kind `(:only [,action-kind])))))) (menu-items (or (cl-loop for action across actions ;; Do filtering ourselves, in case the `:only' commit 470447e22a2a2625cca10791ee49ef6a11764a5c Author: João Távora Date: Thu Jan 28 18:56:22 2021 +0000 * eglot.el (eglot): tweak docstring grammar. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 276cd1aeb3..b5f6e6f73a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -780,13 +780,13 @@ be guessed." (defun eglot (managed-major-mode project class contact &optional interactive) "Manage a project with a Language Server Protocol (LSP) server. -The LSP server of CLASS started (or contacted) via CONTACT. If -this operation is successful, current *and future* file buffers -of MANAGED-MAJOR-MODE inside PROJECT automatically become -\"managed\" by the LSP server, meaning information about their -contents is exchanged periodically to provide enhanced -code-analysis via `xref-find-definitions', `flymake-mode', -`eldoc-mode', `completion-at-point', among others. +The LSP server of CLASS is started (or contacted) via CONTACT. +If this operation is successful, current *and future* file +buffers of MANAGED-MAJOR-MODE inside PROJECT become \"managed\" +by the LSP server, meaning information about their contents is +exchanged periodically to provide enhanced code-analysis via +`xref-find-definitions', `flymake-mode', `eldoc-mode', +`completion-at-point', among others. Interactively, the command attempts to guess MANAGED-MAJOR-MODE from current buffer, CLASS and CONTACT from @@ -798,7 +798,7 @@ MANAGED-MAJOR-MODE. PROJECT is a project instance as returned by `project-current'. -CLASS is a subclass of symbol `eglot-lsp-server'. +CLASS is a subclass of `eglot-lsp-server'. CONTACT specifies how to contact the server. It is a keyword-value plist used to initialize CLASS or a plain list as commit 49327fb04c173249346edb30ac9a8a7cd8aade53 Author: Andrii Kolomoiets Date: Thu Jan 28 20:36:11 2021 +0200 Offer shortcut commands to commonly invoked code actions See also https://github.com/joaotavora/eglot/issues/598. Make eglot-code-actions accept a new action-kind argument. If there is only one action of that kind, apply it. This allows us to create actions shortcuts like eglot-code-action-organize-imports, etc. * eglot.el (eglot-code-actions): Accept new argument action-kind. (eglot--code-action): New function-defining helper macro. (eglot-code-action-organize-imports) (eglot-code-action-extract) (eglot-code-action-inline) (eglot-code-action-rewrite) (eglot-code-action-quickfix): New commands. * README.md: Mention new feature. * NEWS.md: Mention new feature. Co-authored-by: João Távora GitHub-reference: close https://github.com/joaotavora/eglot/issues/411 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8cfe9918d2..276cd1aeb3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2523,14 +2523,21 @@ is not active." :newName ,newname)) current-prefix-arg)) - -(defun eglot-code-actions (beg &optional end) - "Offer to execute code actions between BEG and END. -Interactively, if a region is active, BEG and END are its bounds, -else BEG is point and END is nil, which results in a request for -code actions at point" +(defun eglot--region-bounds () "Region bounds if active, else point and nil." + (if (use-region-p) `(,(region-beginning) ,(region-end)) `(,(point) nil))) + +(defun eglot-code-actions (beg &optional end action-kind) + "Offer to execute actions of ACTION-KIND between BEG and END. +If ACTION-KIND is nil, consider all kinds of actions. +Interactively, default BEG and END to region's bounds else BEG is +point and END is nil, which results in a request for code actions +at point. With prefix argument, prompt for ACTION-KIND." (interactive - (if (region-active-p) `(,(region-beginning) ,(region-end)) `(,(point) nil))) + `(,@(eglot--region-bounds) + ,(and current-prefix-arg + (completing-read "[eglot] Action kind: " + '("quickfix" "refactor.extract" "refactor.inline" + "refactor.rewrite" "source.organizeImports"))))) (unless (eglot--server-capable :codeActionProvider) (eglot--error "Server can't execute code actions!")) (let* ((server (eglot--current-server-or-lose)) @@ -2544,27 +2551,35 @@ code actions at point" :context `(:diagnostics [,@(cl-loop for diag in (flymake-diagnostics beg end) - when (cdr (assoc 'eglot-lsp-diag (eglot--diag-data diag))) - collect it)])))) + when (cdr (assoc 'eglot-lsp-diag + (eglot--diag-data diag))) + collect it)] + ,@(when action-kind `(:only ,action-kind)))))) (menu-items - (or (mapcar (jsonrpc-lambda (&rest all &key title &allow-other-keys) - (cons title all)) - actions) - (eglot--error "No code actions here"))) + (or (cl-loop for action across actions + ;; Do filtering ourselves, in case the `:only' + ;; didn't go through. + when (or (not action-kind) + (equal action-kind (plist-get action :kind))) + collect (cons (plist-get action :title) action)) + (apply #'eglot--error + (if action-kind `("No \"%s\" code actions here" ,action-kind) + `("No code actions here"))))) (preferred-action (cl-find-if - (jsonrpc-lambda (&key isPreferred &allow-other-keys) - isPreferred) - actions)) - (menu `("Eglot code actions:" ("dummy" ,@menu-items))) - (action (if (listp last-nonmenu-event) - (x-popup-menu last-nonmenu-event menu) - (cdr (assoc (completing-read "[eglot] Pick an action: " - menu-items nil t - nil nil (or (plist-get - preferred-action - :title) - (car menu-items))) - menu-items))))) + (lambda (menu-item) + (plist-get (cdr menu-item) :isPreferred)) + menu-items)) + (default-action (car (or preferred-action (car menu-items)))) + (action (if (and action-kind (null (cadr menu-items))) + (cdr (car menu-items)) + (if (listp last-nonmenu-event) + (x-popup-menu last-nonmenu-event `("Eglot code actions:" + ("dummy" ,@menu-items))) + (cdr (assoc (completing-read + (format "[eglot] Pick an action (default %s): " + default-action) + menu-items nil t nil nil default-action) + menu-items)))))) (eglot--dcase action (((Command) command arguments) (eglot-execute-command server (intern command) arguments)) @@ -2574,6 +2589,18 @@ code actions at point" (eglot--dbind ((Command) command arguments) command (eglot-execute-command server (intern command) arguments))))))) +(defmacro eglot--code-action (name kind) + "Define NAME to execute KIND code action." + `(defun ,name (beg &optional end) + ,(format "Execute '%s' code actions between BEG and END." kind) + (interactive (eglot--region-bounds)) + (eglot-code-actions beg end ,kind))) + +(eglot--code-action eglot-code-action-organize-imports "source.organizeImports") +(eglot--code-action eglot-code-action-extract "refactor.extract") +(eglot--code-action eglot-code-action-inline "refactor.inline") +(eglot--code-action eglot-code-action-rewrite "refactor.rewrite") +(eglot--code-action eglot-code-action-quickfix "quickfix") ;;; Dynamic registration commit ae361b0a49d0db70aedfa481070d88752fe62f35 Author: ssnnoo <43703153+ssnnoo@users.noreply.github.com> Date: Wed Jan 27 09:10:07 2021 +0000 Add fortls for fotran (f90-mode) * eglot.el (eglot-server-programs): Add fortls * README.md: mention fortls Copyright-paperwork-exempt: yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/603 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a0c5dc5203..8cfe9918d2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -124,7 +124,8 @@ language-server/bin/php-language-server.php")) . ("digestif")) (erlang-mode . ("erlang_ls" "--transport" "stdio")) (nix-mode . ("rnix-lsp")) - (gdscript-mode . ("localhost" 6008))) + (gdscript-mode . ("localhost" 6008)) + (f90-mode . ("fortls"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated commit 8b4896f6d2a2245c5794fa8c9ecca1507648daed Author: Brian Leung Date: Wed Jan 27 14:43:15 2021 -0800 Add rnix-lsp server for nix-mode, community suggestion * eglot.el (eglot-server-programs): Add rnix-lsp * README.md: mention rnix-lsp Copyright-paperwork-exempt: yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/599 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 98fa4d9af6..a0c5dc5203 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -123,6 +123,7 @@ language-server/bin/php-language-server.php")) ((tex-mode context-mode texinfo-mode bibtex-mode) . ("digestif")) (erlang-mode . ("erlang_ls" "--transport" "stdio")) + (nix-mode . ("rnix-lsp")) (gdscript-mode . ("localhost" 6008))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE commit 581dfb79bffc25a2ef8879aee4862817f6c21e79 Author: Felicián Németh Date: Sat Jan 11 20:12:26 2020 +0100 Fix eglot-completion-at-point for multiple matches The test-completion case shouldn't return t when there are multiple matches. Similarly, the try-completion should return t only if the match is exact. See (info "(elisp)Programmed Completion"). * eglot.el (eglot-completion-at-point): Instead of testing memberships, use test-completion and try-completion suggested by (info "(elisp)Programmed Completion"). * eglot-tests.el (non-unique-completions): Add new test. Co-authored-by: João Távora GitHub-reference: fix https://github.com/joaotavora/eglot/issues/365 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0447bc8504..98fa4d9af6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2154,10 +2154,10 @@ is not active." (cond ((eq action 'metadata) metadata) ; metadata ((eq action 'lambda) ; test-completion - (member probe (funcall proxies))) + (test-completion probe (funcall proxies))) ((eq (car-safe action) 'boundaries) nil) ; boundaries - ((and (null action) ; try-completion - (member probe (funcall proxies)) t)) + ((null action) ; try-completion + (try-completion probe (funcall proxies))) ((eq action t) ; all-completions (cl-remove-if-not (lambda (proxy) commit 6d731fab9c07cd49f83e49ad3e6588ddb47fe98e Author: Andrii Kolomoiets Date: Sat Jan 16 13:42:32 2021 +0200 Run exit-function only for finished completion Per https://github.com/joaotavora/eglot/issues/594. * eglot.el (eglot-completion-at-point): Respect 'status' argument in completion's exit function. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/592 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b8db7f9057..0447bc8504 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2203,54 +2203,58 @@ is not active." (cl-coerce (cl-getf completion-capability :triggerCharacters) 'list)) (line-beginning-position)))) :exit-function - (lambda (proxy _status) - ;; To assist in using this whole `completion-at-point' - ;; function inside `completion-in-region', ensure the exit - ;; function runs in the buffer where the completion was - ;; triggered from. This should probably be in Emacs itself. - ;; (github#505) - (with-current-buffer (if (minibufferp) - (window-buffer (minibuffer-selected-window)) - (current-buffer)) - (eglot--dbind ((CompletionItem) insertTextFormat - insertText textEdit additionalTextEdits label) - (funcall - resolve-maybe - (or (get-text-property 0 'eglot--lsp-item proxy) - ;; When selecting from the *Completions* - ;; buffer, `proxy' won't have any properties. - ;; A lookup should fix that (github#148) - (get-text-property - 0 'eglot--lsp-item - (cl-find proxy (funcall proxies) :test #'string=)))) - (let ((snippet-fn (and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)))) - (cond (textEdit - ;; Undo (yes, undo) the newly inserted completion. - ;; If before completion the buffer was "foo.b" and - ;; now is "foo.bar", `proxy' will be "bar". We - ;; want to delete only "ar" (`proxy' minus the - ;; symbol whose bounds we've calculated before) - ;; (github#160). - (delete-region (+ (- (point) (length proxy)) - (if bounds (- (cdr bounds) (car bounds)) 0)) - (point)) - (eglot--dbind ((TextEdit) range newText) textEdit - (pcase-let ((`(,beg . ,end) (eglot--range-region range))) - (delete-region beg end) - (goto-char beg) - (funcall (or snippet-fn #'insert) newText))) - (when (cl-plusp (length additionalTextEdits)) - (eglot--apply-text-edits additionalTextEdits))) - (snippet-fn - ;; A snippet should be inserted, but using plain - ;; `insertText'. This requires us to delete the - ;; whole completion, since `insertText' is the full - ;; completion's text. - (delete-region (- (point) (length proxy)) (point)) - (funcall snippet-fn (or insertText label))))) - (eglot--signal-textDocument/didChange) - (eldoc)))))))) + (lambda (proxy status) + (when (eq status 'finished) + ;; To assist in using this whole `completion-at-point' + ;; function inside `completion-in-region', ensure the exit + ;; function runs in the buffer where the completion was + ;; triggered from. This should probably be in Emacs itself. + ;; (github#505) + (with-current-buffer (if (minibufferp) + (window-buffer (minibuffer-selected-window)) + (current-buffer)) + (eglot--dbind ((CompletionItem) insertTextFormat + insertText textEdit additionalTextEdits label) + (funcall + resolve-maybe + (or (get-text-property 0 'eglot--lsp-item proxy) + ;; When selecting from the *Completions* + ;; buffer, `proxy' won't have any properties. + ;; A lookup should fix that (github#148) + (get-text-property + 0 'eglot--lsp-item + (cl-find proxy (funcall proxies) :test #'string=)))) + (let ((snippet-fn (and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)))) + (cond (textEdit + ;; Undo (yes, undo) the newly inserted completion. + ;; If before completion the buffer was "foo.b" and + ;; now is "foo.bar", `proxy' will be "bar". We + ;; want to delete only "ar" (`proxy' minus the + ;; symbol whose bounds we've calculated before) + ;; (github#160). + (delete-region (+ (- (point) (length proxy)) + (if bounds + (- (cdr bounds) (car bounds)) + 0)) + (point)) + (eglot--dbind ((TextEdit) range newText) textEdit + (pcase-let ((`(,beg . ,end) + (eglot--range-region range))) + (delete-region beg end) + (goto-char beg) + (funcall (or snippet-fn #'insert) newText))) + (when (cl-plusp (length additionalTextEdits)) + (eglot--apply-text-edits additionalTextEdits))) + (snippet-fn + ;; A snippet should be inserted, but using plain + ;; `insertText'. This requires us to delete the + ;; whole completion, since `insertText' is the full + ;; completion's text. + (delete-region (- (point) (length proxy)) (point)) + (funcall snippet-fn (or insertText label))))) + (eglot--signal-textDocument/didChange) + (eldoc))))))))) (defun eglot--hover-info (contents &optional range) (let ((heading (and range (pcase-let ((`(,beg . ,end) (eglot--range-region range))) commit 26b10c6dafd126d347fa1f835dc9a974953e3282 Author: Augusto Stoffel Date: Wed Jan 13 19:43:08 2021 +0100 Use `path-separator', not ":", in eclipse/jdt custom code This is needed on Windows. * eglot.el (eglot--eclipse-jdt-contact): Replace literal ":" by `path-separator'. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/513 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6d51fc74fc..b8db7f9057 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2693,8 +2693,8 @@ If INTERACTIVE, prompt user for details." "org\\.eclipse\\.equinox\\.launcher_.*\\.jar$" (file-name-nondirectory path)) (file-exists-p path)))) - (let* ((classpath (or (getenv "CLASSPATH") ":")) - (cp-jar (cl-find-if #'is-the-jar (split-string classpath ":"))) + (let* ((classpath (or (getenv "CLASSPATH") path-separator)) + (cp-jar (cl-find-if #'is-the-jar (split-string classpath path-separator))) (jar cp-jar) (dir (cond @@ -2732,7 +2732,7 @@ If INTERACTIVE, prompt user for details." (when (and interactive (not cp-jar) (y-or-n-p (concat "Add path to the server program " "to CLASSPATH environment variable?"))) - (setenv "CLASSPATH" (concat (getenv "CLASSPATH") ":" jar))) + (setenv "CLASSPATH" (concat (getenv "CLASSPATH") path-separator jar))) (unless (file-directory-p workspace) (make-directory workspace t)) (cons 'eglot-eclipse-jdt commit 2abd7be6b3e23b6fb9ef424352cdfb54496a1b47 Author: Augusto Stoffel Date: Wed Jan 13 19:41:10 2021 +0100 Define a face for symbol highlight Also per https://github.com/joaotavora/eglot/issues/583. * eglot.el (eglot-highlight-symbol-face): New face. (eglot--highlight-piggyback): Use it. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/584 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f843c2dba6..6d51fc74fc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -166,6 +166,10 @@ of those modes. CONTACT can be: should not ask the user for any input, and return nil or signal an error if it can't produce a valid CONTACT.") +(defface eglot-highlight-symbol-face + '((t (:inherit bold))) + "Face used to highlight the symbol at point.") + (defface eglot-mode-line '((t (:inherit font-lock-constant-face :weight bold))) "Face for package-name in EGLOT's mode line.") @@ -2367,7 +2371,7 @@ is not active." (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (let ((ov (make-overlay beg end))) - (overlay-put ov 'face 'highlight) + (overlay-put ov 'face 'eglot-highlight-symbol-face) (overlay-put ov 'evaporate t) ov))) highlights)))) commit 9622f03b5c679e1d69337c0758507ff3a3928207 Author: João Távora Date: Wed Jan 13 15:13:32 2021 +0000 Unbreak interactivee eglot--connect for complex contact specs The previous commit for https://github.com/joaotavora/eglot/issues/526 was completely botched. One has to check current-prefix-arg for the presence of C-u, not eglot--guess-contact INTERACTIVE arg. * eglot.el (eglot--guess-contact): Be more careful when processing guess. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/593 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5bd087c435..f843c2dba6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -725,9 +725,9 @@ be guessed." (guess (if (functionp guess) (funcall guess interactive) guess)) - (class (or (and (not interactive) - (consp guess) (symbolp (car guess)) - (prog1 (car guess) (setq guess (cdr guess)))) + (class (or (and (consp guess) (symbolp (car guess)) + (prog1 (unless current-prefix-arg (car guess)) + (setq guess (cdr guess)))) 'eglot-lsp-server)) (program (and (listp guess) (stringp (car guess)) commit 30139cc1f482c925861c54e9a6ea06effab02ff1 Author: João Távora Date: Sun Jan 10 16:42:59 2021 +0000 C-u m-x eglot discards class guessed by eglot--guess-contact This will prevent C-u M-x eglot RET rust-analyzer from bringing into play the eglot-rls server class, which is only valid for the default RLS server. Found when testing for https://github.com/joaotavora/eglot/issues/526, though this bug might not necessarily be the problem being reported there. * eglot.el (eglot--guess-contact): Don't assume guessed class if INTERACTIVE. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9267a138bd..5bd087c435 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -725,7 +725,8 @@ be guessed." (guess (if (functionp guess) (funcall guess interactive) guess)) - (class (or (and (consp guess) (symbolp (car guess)) + (class (or (and (not interactive) + (consp guess) (symbolp (car guess)) (prog1 (car guess) (setq guess (cdr guess)))) 'eglot-lsp-server)) (program (and (listp guess) commit 40453c991e12d966a950dc6ec2fe522e57c5a5f2 Author: Jürgen Hötzel Date: Tue Jan 5 11:56:06 2021 +0100 Flex completion style is not available on emacs < 27 * eglot.el (eglot--managed-mode): check if flex style available Copyright-paperwork-exempt: yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/582 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c7f70ea141..9267a138bd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1362,7 +1362,8 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend t)) (eglot--setq-saving company-backends '(company-capf)) (eglot--setq-saving company-tooltip-align-annotations t) - (eglot--setq-saving completion-styles '(flex basic)) + (when (assoc 'flex completion-styles-alist) + (eglot--setq-saving completion-styles '(flex basic))) (unless (eglot--stay-out-of-p 'imenu) (add-function :before-until (local 'imenu-create-index-function) #'eglot-imenu)) commit 7443bcf612c8df48004a9729f3a3514b985913bf Author: João Távora Date: Tue Dec 22 17:35:08 2020 +0000 Don't block in eglot-imenu if performing non-essential task eglot-imenu is used by imenu which in turn is used by which-func-mode called from an idle timer. We don't want it to block in that situation. Latest which-func mode now sets "non-essential" when performing its duties, so we leverage that in eglot-imenu. * eglot.el (eglot-imenu): Use non-essential. GitHub-reference: close https://github.com/joaotavora/eglot/issues/212 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1a53c16dfc..c7f70ea141 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2415,7 +2415,8 @@ is not active." (jsonrpc-request (eglot--current-server-or-lose) :textDocument/documentSymbol `(:textDocument - ,(eglot--TextDocumentIdentifier)))))))) + ,(eglot--TextDocumentIdentifier)) + :cancel-on-input non-essential)))))) (defun eglot--apply-text-edits (edits &optional version) "Apply EDITS for current buffer if at VERSION, or if it's nil." commit e36fe5985153effff3cba7328ae3b6f0bd771147 Author: João Távora Date: Tue Dec 22 17:31:52 2020 +0000 Use flex completion if available by default * eglot.el (eglot--managed-mode): Set completion-styles. GitHub-reference: close https://github.com/joaotavora/eglot/issues/575 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 35c959bb20..1a53c16dfc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1362,6 +1362,7 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend t)) (eglot--setq-saving company-backends '(company-capf)) (eglot--setq-saving company-tooltip-align-annotations t) + (eglot--setq-saving completion-styles '(flex basic)) (unless (eglot--stay-out-of-p 'imenu) (add-function :before-until (local 'imenu-create-index-function) #'eglot-imenu)) commit 2a88cffd69e54e759537f3b04d91158843d2ca32 Author: João Távora Date: Wed Dec 16 16:40:41 2020 +0000 Bump eglot version to 1.7 * eglot.el (Version): Bump to 1.7. (Package-Requires): Bump dependency versions. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c240c4251b..35c959bb20 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,12 +2,12 @@ ;; Copyright (C) 2018-2020 Free Software Foundation, Inc. -;; Version: 1.6 +;; Version: 1.7 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.9") (flymake "1.0.9") (project "0.3.0") (xref "1.0.1") (eldoc "1.5.0")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.14") (flymake "1.0.9") (project "0.3.0") (xref "1.0.1") (eldoc "1.11.0")) ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by commit ad47072c322224c382f2b4ed05d025fa9217a69e Author: João Távora Date: Wed Dec 16 16:37:31 2020 +0000 Allow eglot to stay out of xref configuration * eglot.el (eglot-stay-out-of): Rework docstring. (eglot--managed-mode): Can now stay out of xref. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/569 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 404ed19049..c240c4251b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1288,6 +1288,10 @@ and just return it. PROMPT shouldn't end with a question mark." (defvar eglot-stay-out-of '() "List of Emacs things that Eglot should try to stay of. +Each element is a string, a symbol, or a regexp which is matched +against a variable's name. Examples include the string +\"company\" or the symbol `xref'. + Before Eglot starts \"managing\" a particular buffer, it opinionatedly sets some peripheral Emacs facilites, such as Flymake, Xref and Company. These overriding settings help ensure @@ -1296,9 +1300,8 @@ consistent Eglot behaviour and only stay in place until previous settings are restored. However, if you wish for Eglot to stay out of a particular Emacs -facility that you'd like to keep control of, add a string, a -symbol, or a regexp here that will be matched against the -variable's name, and Eglot will refrain from setting it. +facility that you'd like to keep control of add an element to +this list and Eglot will refrain from setting it. For example, to keep your Company customization use @@ -1338,13 +1341,14 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'before-change-functions 'eglot--before-change nil t) (add-hook 'kill-buffer-hook #'eglot--managed-mode-off nil t) - ;; Prepend "didClose" to the hook after the "onoff", so it will run first + ;; Prepend "didClose" to the hook after the "nonoff", so it will run first (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'before-revert-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'after-revert-hook 'eglot--after-revert-hook nil t) (add-hook 'before-save-hook 'eglot--signal-textDocument/willSave nil t) (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) - (add-hook 'xref-backend-functions 'eglot-xref-backend nil t) + (unless (eglot--stay-out-of-p 'xref) + (add-hook 'xref-backend-functions 'eglot-xref-backend nil t)) (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) (add-hook 'change-major-mode-hook #'eglot--managed-mode-off nil t) (add-hook 'post-self-insert-hook 'eglot--post-self-insert-hook nil t) commit d18f546844350d7fd91477dcaf0a65772828b463 Author: João Távora Date: Wed Dec 16 10:22:56 2020 +0000 Cosmetic whitespace fix (indentation, long lines, tabs->spaces) * eglot.el (eglot-server-programs, for, eglot--after-change) (eglot-code-actions, eglot-register-capability) (eglot-register-capability): Fix whitespace. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e148403590..404ed19049 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -100,15 +100,16 @@ typescript-mode) . ("javascript-typescript-stdio")) (sh-mode . ("bash-language-server" "start")) - (php-mode . ("php" "vendor/felixfbecker/\ + (php-mode + . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php")) ((c++-mode c-mode) . ("ccls")) ((caml-mode tuareg-mode reason-mode) . ("ocaml-language-server" "--stdio")) (ruby-mode - . ("solargraph" "socket" "--port" - :autoport)) - (haskell-mode . ("haskell-language-server-wrapper" "--lsp")) + . ("solargraph" "socket" "--port" :autoport)) + (haskell-mode + . ("haskell-language-server-wrapper" "--lsp")) (elm-mode . ("elm-language-server")) (kotlin-mode . ("kotlin-language-server")) (go-mode . ("gopls")) @@ -1522,7 +1523,7 @@ Uses THING, FACE, DEFS and PREPEND." (priority . ,(+ 50 i)) (keymap . ,(let ((map (make-sparse-keymap))) (define-key map [mouse-1] - (eglot--mouse-call 'eglot-code-actions)) + (eglot--mouse-call 'eglot-code-actions)) map))))) @@ -1763,8 +1764,8 @@ Records BEG, END and PRE-CHANGE-LENGTH locally." ,(buffer-substring-no-properties b-beg-marker b-end-marker))) (setcar eglot--recent-changes - `(,lsp-beg ,lsp-end ,pre-change-length - ,(buffer-substring-no-properties beg end))))) + `(,lsp-beg ,lsp-end ,pre-change-length + ,(buffer-substring-no-properties beg end))))) (_ (setf eglot--recent-changes :emacs-messup))) (when eglot--change-idle-timer (cancel-timer eglot--change-idle-timer)) (let ((buf (current-buffer))) @@ -2533,9 +2534,9 @@ code actions at point" actions) (eglot--error "No code actions here"))) (preferred-action (cl-find-if - (jsonrpc-lambda (&key isPreferred &allow-other-keys) - isPreferred) - actions)) + (jsonrpc-lambda (&key isPreferred &allow-other-keys) + isPreferred) + actions)) (menu `("Eglot code actions:" ("dummy" ,@menu-items))) (action (if (listp last-nonmenu-event) (x-popup-menu last-nonmenu-event menu) @@ -2572,16 +2573,16 @@ code actions at point" finally return result)) (cl-defmethod eglot-register-capability - (server (method (eql workspace/didChangeWatchedFiles)) id &key watchers) + (server (method (eql workspace/didChangeWatchedFiles)) id &key watchers) "Handle dynamic registration of workspace/didChangeWatchedFiles" (eglot-unregister-capability server method id) (let* (success (globs (mapcar (eglot--lambda ((FileSystemWatcher) globPattern) globPattern) watchers)) - (glob-dirs - (delete-dups (mapcar #'file-name-directory - (mapcan #'file-expand-wildcards globs))))) + (glob-dirs + (delete-dups (mapcar #'file-name-directory + (mapcan #'file-expand-wildcards globs))))) (cl-labels ((handle-event (event) @@ -2605,13 +2606,13 @@ code actions at point" (handle-event `(,desc 'created ,file1))))))) (unwind-protect (progn - (dolist (dir glob-dirs) - (push (file-notify-add-watch dir '(change) #'handle-event) - (gethash id (eglot--file-watches server)))) - (setq - success - `(:message ,(format "OK, watching %s directories in %s watchers" - (length glob-dirs) (length watchers))))) + (dolist (dir glob-dirs) + (push (file-notify-add-watch dir '(change) #'handle-event) + (gethash id (eglot--file-watches server)))) + (setq + success + `(:message ,(format "OK, watching %s directories in %s watchers" + (length glob-dirs) (length watchers))))) (unless success (eglot-unregister-capability server method id)))))) commit d8b863651e2181cbf5c6d9e29e87ff6f7822b227 Author: TANIGUCHI Kohei Date: Wed Dec 16 19:19:30 2020 +0900 Use haskell-language-server in eglot-server-programs Use haskell-language-server instead of deprecated Haskell IDE Engine https://github.com/haskell/haskell-language-server https://github.com/haskell/haskell-ide-engine#deprecated * README.md: Replace Haskell IDE Engine with haskell-language-server * eglot.el (eglot-server-programs): Replace hie-wrapper with haskell-language-server-wrapper Copyright-paperwork-exempt: yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/572 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3ebdd04d9f..e148403590 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -108,7 +108,7 @@ language-server/bin/php-language-server.php")) (ruby-mode . ("solargraph" "socket" "--port" :autoport)) - (haskell-mode . ("hie-wrapper" "--lsp")) + (haskell-mode . ("haskell-language-server-wrapper" "--lsp")) (elm-mode . ("elm-language-server")) (kotlin-mode . ("kotlin-language-server")) (go-mode . ("gopls")) commit 73b1707c411b57f217b21da9ac68cf063b4e963e Author: João Távora Date: Tue Dec 15 12:24:13 2020 +0000 Robustify previous fix of onchange breakage From the in-code comments: ;; githubhttps://github.com/joaotavora/eglot/issues/259 and githubhttps://github.com/joaotavora/eglot/issues/367: With `capitalize-word' or somesuch, ;; `before-change-functions' always records the whole word's `b-beg' ;; and `b-end'. Similarly, when coalescing two lines into one, ;; `fill-paragraph' they mark the end of the first line up to the end ;; of the second line. In both situations, args received here ;; contradict that information: `beg' and `end' will differ by 1 and ;; will likely only encompass the letter that was capitalized or, in ;; the sentence-joining situation, the replacement of the newline with ;; a space. That's we keep markers _and_ positions so we're able to ;; detect and correct this. We ignore `beg', `len' and ;; `pre-change-len' and send "fuller" information about the region ;; from the markers. I've also experimented with doing this ;; unconditionally but it seems to break when newlines are added. * eglot.el (eglot--after-change): Robustify fix. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/367 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index aa89ae98c6..3ebdd04d9f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1729,8 +1729,8 @@ THINGS are either registrations or unregisterations (sic)." ;; (github#259) (push `(,(eglot--pos-to-lsp-position beg) ,(eglot--pos-to-lsp-position end) - (,beg . ,(copy-marker beg)) - (,end . ,(copy-marker end))) + (,beg . ,(copy-marker beg nil)) + (,end . ,(copy-marker end t))) eglot--recent-changes))) (defun eglot--after-change (beg end pre-change-length) @@ -1742,23 +1742,29 @@ Records BEG, END and PRE-CHANGE-LENGTH locally." (`(,lsp-beg ,lsp-end (,b-beg . ,b-beg-marker) (,b-end . ,b-end-marker)) - ;; github#259: With `upcase-word' or somesuch, + ;; github#259 and github#367: With `capitalize-word' or somesuch, ;; `before-change-functions' always records the whole word's - ;; `beg' and `end'. Not only is this longer than needed but - ;; conflicts with the args received here, which encompass just - ;; the parts of the word that changed (if any). We detect this - ;; using markers recorded earlier and at looking - ;; `pre-change-len'. We also ensure that the before bounds - ;; indeed belong to the same line (if we don't, we get could get - ;; #367). - (when (and (= b-end b-end-marker) (= b-beg b-beg-marker) - (not (zerop pre-change-length)) - (= (plist-get lsp-beg :line) (plist-get lsp-end :line))) - (setq lsp-end (eglot--pos-to-lsp-position end) - lsp-beg (eglot--pos-to-lsp-position beg))) - (setcar eglot--recent-changes - `(,lsp-beg ,lsp-end ,pre-change-length - ,(buffer-substring-no-properties beg end)))) + ;; `b-beg' and `b-end'. Similarly, when coalescing two lines + ;; into one, `fill-paragraph' they mark the end of the first line + ;; up to the end of the second line. In both situations, args + ;; received here contradict that information: `beg' and `end' + ;; will differ by 1 and will likely only encompass the letter + ;; that was capitalized or, in the sentence-joining situation, + ;; the replacement of the newline with a space. That's we keep + ;; markers _and_ positions so we're able to detect and correct + ;; this. We ignore `beg', `len' and `pre-change-len' and send + ;; "fuller" information about the region from the markers. I've + ;; also experimented with doing this unconditionally but it seems + ;; to break when newlines are added. + (if (and (= b-end b-end-marker) (= b-beg b-beg-marker) + (or (/= beg b-beg) (/= end b-end))) + (setcar eglot--recent-changes + `(,lsp-beg ,lsp-end ,(- b-end-marker b-beg-marker) + ,(buffer-substring-no-properties b-beg-marker + b-end-marker))) + (setcar eglot--recent-changes + `(,lsp-beg ,lsp-end ,pre-change-length + ,(buffer-substring-no-properties beg end))))) (_ (setf eglot--recent-changes :emacs-messup))) (when eglot--change-idle-timer (cancel-timer eglot--change-idle-timer)) (let ((buf (current-buffer))) commit e609841f6f987698e41e362b397c0130fa4103e5 Author: João Távora Date: Mon Dec 14 17:08:26 2020 +0000 Don't let m-x fill-paragraph break didchange M-x fill-paragraph represents some paragraph-fillling changes very summarily. Filling 1 // foo 2 bar Into 1 // foo bar Only makes two changes: a deletion of the "// " and a replacement of a newline with a space character. The second change fooled Eglot's fix for https://github.com/joaotavora/eglot/issues/259, by making a change similar to the one it is made to detect and correct. That fix should taget things that happen on the same line, this not being one of those things. * eglot.el (eglot--after-change): Only apply fix to https://github.com/joaotavora/eglot/issues/259 if case-fiddling happens on same line. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/367 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b0bd213b48..aa89ae98c6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1745,10 +1745,15 @@ Records BEG, END and PRE-CHANGE-LENGTH locally." ;; github#259: With `upcase-word' or somesuch, ;; `before-change-functions' always records the whole word's ;; `beg' and `end'. Not only is this longer than needed but - ;; conflicts with the args received here. Detect this using - ;; markers recorded earlier and `pre-change-len', then fix it. + ;; conflicts with the args received here, which encompass just + ;; the parts of the word that changed (if any). We detect this + ;; using markers recorded earlier and at looking + ;; `pre-change-len'. We also ensure that the before bounds + ;; indeed belong to the same line (if we don't, we get could get + ;; #367). (when (and (= b-end b-end-marker) (= b-beg b-beg-marker) - (not (zerop pre-change-length))) + (not (zerop pre-change-length)) + (= (plist-get lsp-beg :line) (plist-get lsp-end :line))) (setq lsp-end (eglot--pos-to-lsp-position end) lsp-beg (eglot--pos-to-lsp-position beg))) (setcar eglot--recent-changes commit 5646b874b2f340cd159429cd141df9f68e0e12be Author: João Távora Date: Tue Nov 3 10:26:03 2020 +0000 Don't force eglot-strict-mode completely in eglot--dcase Doing so was by design, since there's much ambiguity between the CodeAction and Command objects. But 'disallow-non-standard-keys is not necessary to disambiguate, and proved harmful in this bug. * eglot.el (eglot--dcase): Don't disallow (eglot--check-dspec): Fix docstring. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/558 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bd3bb5337a..b0bd213b48 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -373,7 +373,7 @@ on unknown notifications and errors on unknown requests. :optional-keys (mapcar #'car optional)))) (defun eglot--check-dspec (interface-name dspec) - "Check if variables in DSPEC " + "Check destructuring spec DSPEC against INTERFACE-NAME." (cl-destructuring-bind (&key required-keys optional-keys &allow-other-keys) (eglot--interface interface-name) (cond ((or required-keys optional-keys) @@ -457,10 +457,14 @@ treated as in `eglot-dbind'." (cond (interface-name (eglot--check-dspec interface-name vars) ;; In this mode, in runtime, we assume - ;; `eglot-strict-mode' is fully on, otherwise we + ;; `eglot-strict-mode' is partially on, otherwise we ;; can't disambiguate between certain types. `(ignore-errors - (eglot--check-object ',interface-name ,obj-once))) + (eglot--check-object + ',interface-name ,obj-once + t + (memq 'disallow-non-standard-keys eglot-strict-mode) + t))) (t ;; In this interface-less mode we don't check ;; `eglot-strict-mode' at all: just check that the object commit e4f4762e7acd7ee235aada8c51177a08502f5c85 Author: Damien Merenne Date: Wed Oct 28 21:40:32 2020 +0100 Handle lsp 3.15's ispreferred code action property * eglot.el (eglot--lsp-interface-alist): Add :isPreferred to CodeAction. (eglot-client-capabilities): Announce :isPreferredSupport. (eglot-code-actions): Consider preferred CodeAction item. Co-authored-by: João Távora GitHub-reference: close https://github.com/joaotavora/eglot/issues/558 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 60bc56e8aa..bd3bb5337a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -244,7 +244,7 @@ let the buffer grow forever." (eval-and-compile (defvar eglot--lsp-interface-alist `( - (CodeAction (:title) (:kind :diagnostics :edit :command)) + (CodeAction (:title) (:kind :diagnostics :edit :command :isPreferred)) (ConfigurationItem () (:scopeUri :section)) (Command ((:title . string) (:command . string)) (:arguments)) (CompletionItem (:label) @@ -576,7 +576,8 @@ treated as in `eglot-dbind'." ["quickfix" "refactor" "refactor.extract" "refactor.inline" "refactor.rewrite" - "source" "source.organizeImports"]))) + "source" "source.organizeImports"])) + :isPreferredSupport t) :formatting `(:dynamicRegistration :json-false) :rangeFormatting `(:dynamicRegistration :json-false) :rename `(:dynamicRegistration :json-false) @@ -2516,12 +2517,19 @@ code actions at point" (cons title all)) actions) (eglot--error "No code actions here"))) + (preferred-action (cl-find-if + (jsonrpc-lambda (&key isPreferred &allow-other-keys) + isPreferred) + actions)) (menu `("Eglot code actions:" ("dummy" ,@menu-items))) (action (if (listp last-nonmenu-event) (x-popup-menu last-nonmenu-event menu) (cdr (assoc (completing-read "[eglot] Pick an action: " menu-items nil t - nil nil (car menu-items)) + nil nil (or (plist-get + preferred-action + :title) + (car menu-items))) menu-items))))) (eglot--dcase action (((Command) command arguments) commit 7d506b0cd5a9afe68b66d6553520074d9c06c8ea Author: João Távora Date: Tue Sep 1 19:10:55 2020 +0100 Don't send json null (elisp nil) down the wire * eglot.el (eglot-initialization-options) (eglot-client-capabilities): Use eglot--{}, not nil. GitHub-reference: per https://github.com/joaotavora/eglot/issues/300 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c752322b58..60bc56e8aa 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -508,7 +508,7 @@ treated as in `eglot-dbind'." (cl-defgeneric eglot-initialization-options (server) "JSON object to send under `initializationOptions'" - (:method (_s) nil)) ; blank default + (:method (_s) eglot--{})) ; blank default (cl-defgeneric eglot-register-capability (server method id &rest params) "Ask SERVER to register capability METHOD marked with ID." @@ -581,7 +581,7 @@ treated as in `eglot-dbind'." :rangeFormatting `(:dynamicRegistration :json-false) :rename `(:dynamicRegistration :json-false) :publishDiagnostics `(:relatedInformation :json-false)) - :experimental (list)))) + :experimental eglot--{}))) (defclass eglot-lsp-server (jsonrpc-process-connection) ((project-nickname commit 9ca5b69dc6a832d64a09e777b12fe320821d6c7f Author: R Primus Date: Sun Aug 30 13:54:14 2020 +0100 Unbreak haskell's hie-wrapper built-in incantation * eglot.el (eglot-server-programs): Add required argument for hie-wrapper Copyright-paperwork-exempt: yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/528 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 64543d7bb8..c752322b58 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -100,7 +100,7 @@ typescript-mode) . ("javascript-typescript-stdio")) (sh-mode . ("bash-language-server" "start")) - (php-mode . ("php" "vendor/felixfbecker/\ + (php-mode . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php")) ((c++-mode c-mode) . ("ccls")) ((caml-mode tuareg-mode reason-mode) @@ -108,7 +108,7 @@ language-server/bin/php-language-server.php")) (ruby-mode . ("solargraph" "socket" "--port" :autoport)) - (haskell-mode . ("hie-wrapper")) + (haskell-mode . ("hie-wrapper" "--lsp")) (elm-mode . ("elm-language-server")) (kotlin-mode . ("kotlin-language-server")) (go-mode . ("gopls")) commit c37c30f134e03d0d982aef714b94bda56fe8d860 Author: Paul M. Rodriguez Date: Fri Aug 21 17:27:40 2020 -0500 Provide suitable default to m-x eglot-rename Copyright-paperwork-exempt: Yes * eglot (eglot-rename): Provide a default value. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/524 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a4ba1f9f93..64543d7bb8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2477,7 +2477,9 @@ is not active." (defun eglot-rename (newname) "Rename the current symbol to NEWNAME." (interactive - (list (read-from-minibuffer (format "Rename `%s' to: " (symbol-at-point))))) + (list (read-from-minibuffer (format "Rename `%s' to: " (symbol-at-point)) + nil nil nil nil + (symbol-name (symbol-at-point))))) (unless (eglot--server-capable :renameProvider) (eglot--error "Server can't rename!")) (eglot--apply-workspace-edit commit 07c8219fa24f15213b1e1899a6824f1146954ef5 Author: João Távora Date: Sun Aug 16 19:10:23 2020 +0100 Correct paren mismatch blunder introduced by earlier commit Per https://github.com/joaotavora/eglot/issues/512. * eglot.el (eglot-server-programs): properly close parenthesis. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/521 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 023c0dfe7a..a4ba1f9f93 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -122,7 +122,7 @@ language-server/bin/php-language-server.php")) ((tex-mode context-mode texinfo-mode bibtex-mode) . ("digestif")) (erlang-mode . ("erlang_ls" "--transport" "stdio")) - (gdscript-mode . ("localhost" 6008)) + (gdscript-mode . ("localhost" 6008))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated commit 9511280768fb6239f88fc3eaa7398c972fa9dd00 Author: Steven vanZyl Date: Fri Aug 14 10:44:38 2020 -0400 Add built-int support for godot engine Copyright-paperwork-exempt: yes Co-authored-by: João Távora * README.md: mention Godot * eglot.el (eglot-server-programs): Add godot engine via port GitHub-reference: close https://github.com/joaotavora/eglot/issues/511 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0b233377b1..023c0dfe7a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -121,7 +121,8 @@ language-server/bin/php-language-server.php")) (scala-mode . ("metals-emacs")) ((tex-mode context-mode texinfo-mode bibtex-mode) . ("digestif")) - (erlang-mode . ("erlang_ls" "--transport" "stdio"))) + (erlang-mode . ("erlang_ls" "--transport" "stdio")) + (gdscript-mode . ("localhost" 6008)) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated commit 836127f39447fb93ffc9f2af2d0b0abbc2b4f824 Author: Andrew Schwartzmeyer Date: Sat Jun 20 21:47:04 2020 -0700 Ensure completion terminates in correct buffer To design a completion-in-region-function replacement that leverages the elements in completion-at-point-functions, we must ensure that their :exit-function parts execute in the correct buffer. That is the buffer where the text to be completed lives, not necessarily the buffer being used for user interaction. Later on, this guarantee should be provided by Emacs itself, perhaps by putting the correct with-current-buffer call in completion--done. Copyright-paperwork-exempt: yes Co-authored-by: João Távora * eglot.el (eglot-completion-at-point): Ensure :exit-function's buffer is where the source is. GitHub-reference: close https://github.com/joaotavora/eglot/issues/505 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c0f3143ba1..0b233377b1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2175,45 +2175,53 @@ is not active." (line-beginning-position)))) :exit-function (lambda (proxy _status) - (eglot--dbind ((CompletionItem) insertTextFormat - insertText textEdit additionalTextEdits label) - (funcall - resolve-maybe - (or (get-text-property 0 'eglot--lsp-item proxy) - ;; When selecting from the *Completions* - ;; buffer, `proxy' won't have any properties. - ;; A lookup should fix that (github#148) - (get-text-property - 0 'eglot--lsp-item - (cl-find proxy (funcall proxies) :test #'string=)))) - (let ((snippet-fn (and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)))) - (cond (textEdit - ;; Undo (yes, undo) the newly inserted completion. - ;; If before completion the buffer was "foo.b" and - ;; now is "foo.bar", `proxy' will be "bar". We - ;; want to delete only "ar" (`proxy' minus the - ;; symbol whose bounds we've calculated before) - ;; (github#160). - (delete-region (+ (- (point) (length proxy)) - (if bounds (- (cdr bounds) (car bounds)) 0)) - (point)) - (eglot--dbind ((TextEdit) range newText) textEdit - (pcase-let ((`(,beg . ,end) (eglot--range-region range))) - (delete-region beg end) - (goto-char beg) - (funcall (or snippet-fn #'insert) newText))) - (when (cl-plusp (length additionalTextEdits)) - (eglot--apply-text-edits additionalTextEdits))) - (snippet-fn - ;; A snippet should be inserted, but using plain - ;; `insertText'. This requires us to delete the - ;; whole completion, since `insertText' is the full - ;; completion's text. - (delete-region (- (point) (length proxy)) (point)) - (funcall snippet-fn (or insertText label))))) - (eglot--signal-textDocument/didChange) - (eldoc))))))) + ;; To assist in using this whole `completion-at-point' + ;; function inside `completion-in-region', ensure the exit + ;; function runs in the buffer where the completion was + ;; triggered from. This should probably be in Emacs itself. + ;; (github#505) + (with-current-buffer (if (minibufferp) + (window-buffer (minibuffer-selected-window)) + (current-buffer)) + (eglot--dbind ((CompletionItem) insertTextFormat + insertText textEdit additionalTextEdits label) + (funcall + resolve-maybe + (or (get-text-property 0 'eglot--lsp-item proxy) + ;; When selecting from the *Completions* + ;; buffer, `proxy' won't have any properties. + ;; A lookup should fix that (github#148) + (get-text-property + 0 'eglot--lsp-item + (cl-find proxy (funcall proxies) :test #'string=)))) + (let ((snippet-fn (and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)))) + (cond (textEdit + ;; Undo (yes, undo) the newly inserted completion. + ;; If before completion the buffer was "foo.b" and + ;; now is "foo.bar", `proxy' will be "bar". We + ;; want to delete only "ar" (`proxy' minus the + ;; symbol whose bounds we've calculated before) + ;; (github#160). + (delete-region (+ (- (point) (length proxy)) + (if bounds (- (cdr bounds) (car bounds)) 0)) + (point)) + (eglot--dbind ((TextEdit) range newText) textEdit + (pcase-let ((`(,beg . ,end) (eglot--range-region range))) + (delete-region beg end) + (goto-char beg) + (funcall (or snippet-fn #'insert) newText))) + (when (cl-plusp (length additionalTextEdits)) + (eglot--apply-text-edits additionalTextEdits))) + (snippet-fn + ;; A snippet should be inserted, but using plain + ;; `insertText'. This requires us to delete the + ;; whole completion, since `insertText' is the full + ;; completion's text. + (delete-region (- (point) (length proxy)) (point)) + (funcall snippet-fn (or insertText label))))) + (eglot--signal-textDocument/didChange) + (eldoc)))))))) (defun eglot--hover-info (contents &optional range) (let ((heading (and range (pcase-let ((`(,beg . ,end) (eglot--range-region range))) commit ad701795985377401eb007a2eeb5ece65d47b0dc Author: João Távora Date: Mon Jul 13 23:59:41 2020 +0100 Use a hash-table for storing resolved completions * eglot.el (eglot-completion-at-point): use a hash-table for storing resolved completions. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/510 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 865ca03dd3..c0f3143ba1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2103,20 +2103,20 @@ is not active." (put-text-property 0 1 'eglot--lsp-item item proxy)) proxy)) items))))) - resolved + (resolved (make-hash-table)) (resolve-maybe ;; Maybe completion/resolve JSON object `lsp-comp' into ;; another JSON object, if at all possible. Otherwise, ;; just return lsp-comp. (lambda (lsp-comp) - (cond (resolved resolved) - ((and (eglot--server-capable :completionProvider - :resolveProvider) - (plist-get lsp-comp :data)) - (setq resolved - (jsonrpc-request server :completionItem/resolve - lsp-comp :cancel-on-input t))) - (t lsp-comp)))) + (or (gethash lsp-comp resolved) + (setf (gethash lsp-comp resolved) + (if (and (eglot--server-capable :completionProvider + :resolveProvider) + (plist-get lsp-comp :data)) + (jsonrpc-request server :completionItem/resolve + lsp-comp :cancel-on-input t) + lsp-comp))))) (bounds (bounds-of-thing-at-point 'symbol))) (list (or (car bounds) (point)) commit 34ecaa4b16c6ec4a12e069e2c51616c667d07d51 Author: João Távora Date: Mon Jul 13 23:43:35 2020 +0100 Reload eldoc if needed on emacs < 28 ElDoc is preloaded in Emacs, so `require`-ing won't guarantee we are using the latest version from GNU Elpa when we load eglot.el. Use an heuristic to see if we need to `load` it in Emacs < 28. * eglot.el (Package-Requires): Require eldoc 1.5.0 (top): Sometimes load eldoc diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 87fd9c8902..865ca03dd3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -7,7 +7,7 @@ ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.9") (flymake "1.0.9") (project "0.3.0") (xref "1.0.1") (eldoc "1.2.0")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.9") (flymake "1.0.9") (project "0.3.0") (xref "1.0.1") (eldoc "1.5.0")) ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by @@ -72,6 +72,15 @@ (require 'filenotify) (require 'ert) (require 'array) + +;; ElDoc is preloaded in Emacs, so `require'-ing won't guarantee we are +;; using the latest version from GNU Elpa when we load eglot.el. Use an +;; heuristic to see if we need to `load' it in Emacs < 28. +(if (and (< emacs-major-version 28) + (not (boundp 'eldoc-documentation-strategy))) + (load "eldoc") + (require 'eldoc)) + ;; forward-declare, but don't require (Emacs 28 doesn't seem to care) (defvar markdown-fontify-code-blocks-natively) (defvar company-backends) commit 814da26d35db42576f7085bef953f5fe2352baef Author: João Távora Date: Sat Jul 11 00:41:43 2020 +0800 Fix sorting of completion items This fixes a problem pointed out by Yuwei Tian . * eglot.el (eglot-completion-at-point): Fix getting :sortText content of the completion item. GitHub-reference: closes https://github.com/joaotavora/eglot/issues/509 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3e9c964139..87fd9c8902 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2053,12 +2053,15 @@ is not active." ;; Commit logs for this function help understand what's going on. (when-let (completion-capability (eglot--server-capable :completionProvider)) (let* ((server (eglot--current-server-or-lose)) - (sort-completions (lambda (completions) - (sort completions - (lambda (a b) - (string-lessp - (or (get-text-property 0 :sortText a) "") - (or (get-text-property 0 :sortText b) "")))))) + (sort-completions + (lambda (completions) + (cl-sort completions + #'string-lessp + :key (lambda (c) + (or (plist-get + (get-text-property 0 'eglot--lsp-item c) + :sortText) + ""))))) (metadata `(metadata . ((display-sort-function . ,sort-completions)))) resp items (cached-proxies :none) (proxies commit 1d4caae44fc0ad08e126b9ba4b0594ef9c524b07 Author: João Távora Date: Fri Jul 10 00:28:19 2020 +0100 * eglot.el (package-requires): require flymake 1.0.9 and eldoc 1.2.0 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b94fcc31e1..3e9c964139 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -7,7 +7,7 @@ ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.9") (flymake "1.0.8") (project "0.3.0") (xref "1.0.1") (eldoc "1.1.0")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.9") (flymake "1.0.9") (project "0.3.0") (xref "1.0.1") (eldoc "1.2.0")) ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by commit 8afdc3d2d16139c2be5d17588d0741861cb294cc Author: João Távora Date: Wed Jun 3 18:40:58 2020 +0100 Delegate "hover" and "signature" doc synchronization efforts to eldoc Uses Eldoc's eldoc-documentation-functions variable. In Eldoc v1.0.0 that variable was already available as a way of handling/composing multiple docstrings from different sources, but it didn't work practically with mutiple concurrent async sources. This was fixed in 1.1.0, which Eglot now requires. This fixes the synchronization problems reported in https://github.com/joaotavora/eglot/issues/494 and also issue https://github.com/joaotavora/eglot/issues/439. It is likely that some of the exact doc-composing functionality in Eglot, (developed during those issues) was lost, and has to be remade, quite likely in Eldoc itself. Flymake is now also an Eldoc producer, and therefore the problems of github issues https://github.com/joaotavora/eglot/issues/481 and https://github.com/joaotavora/eglot/issues/454 will also soon be fixed as soon as Eglot starts using the upcoming Flymake 1.0.9. * NEWS.md: New entry. * README.md (eglot-put-doc-in-help-buffer) (eglot-auto-display-help-buffer): Remove mention to these options. * eglot.el (Package-Requires:) Require eldoc.el 1.1.0. (eglot--when-live-buffer): Rename from eglot--with-live-buffer. (eglot--when-buffer-window): New macro. (eglot--after-change, eglot--on-shutdown, eglot-ensure): Use eglot--when-live-buffer. (eglot--managed-mode): Use eglot-documentation-functions and eldoc-documentation-strategy. (eglot--highlights): Move down. (eglot-signature-eldoc-function, eglot-hover-eldoc-function) (eglot--highlight-piggyback): New eldoc functions. (eglot--help-buffer, eglot--update-doc) (eglot-auto-display-help-buffer, eglot-put-doc-in-help-buffer) (eglot--truncate-string, eglot-doc-too-large-for-echo-area) (eglot-help-at-point): Remove all of this. (eglot--apply-workspace-edit): Call eldoc manually after an edit. (eglot-mode-map): Remap display-local-help to eldoc-doc-buffer diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 733b69c395..b94fcc31e1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -7,7 +7,7 @@ ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.9") (flymake "1.0.8") (project "0.3.0") (xref "1.0.1") (eldoc "1.0.0")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.9") (flymake "1.0.8") (project "0.3.0") (xref "1.0.1") (eldoc "1.1.0")) ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by @@ -470,11 +470,19 @@ treated as in `eglot-dbind'." ;;; API (WORK-IN-PROGRESS!) ;;; -(cl-defmacro eglot--with-live-buffer (buf &rest body) +(cl-defmacro eglot--when-live-buffer (buf &rest body) "Check BUF live, then do BODY in it." (declare (indent 1) (debug t)) (let ((b (cl-gensym))) `(let ((,b ,buf)) (if (buffer-live-p ,b) (with-current-buffer ,b ,@body))))) +(cl-defmacro eglot--when-buffer-window (buf &body body) + "Check BUF showing somewhere, then do BODY in it" (declare (indent 1) (debug t)) + (let ((b (cl-gensym))) + `(let ((,b ,buf)) + ;;notice the exception when testing with `ert' + (when (or (get-buffer-window ,b) (ert-running-test)) + (with-current-buffer ,b ,@body))))) + (cl-defmacro eglot--widening (&rest body) "Save excursion and restriction. Widen. Then run BODY." (declare (debug t)) `(save-excursion (save-restriction (widen) ,@body))) @@ -642,7 +650,7 @@ SERVER. ." (dolist (buffer (eglot--managed-buffers server)) (let (;; Avoid duplicate shutdowns (github#389) (eglot-autoshutdown nil)) - (eglot--with-live-buffer buffer (eglot--managed-mode-off)))) + (eglot--when-live-buffer buffer (eglot--managed-mode-off)))) ;; Kill any expensive watches (maphash (lambda (_id watches) (mapcar #'file-notify-rm-watch watches)) @@ -806,7 +814,7 @@ INTERACTIVE is t if called interactively." ((maybe-connect () (remove-hook 'post-command-hook #'maybe-connect nil) - (eglot--with-live-buffer buffer + (eglot--when-live-buffer buffer (unless eglot--managed-mode (apply #'eglot--connect (eglot--guess-contact)))))) (when buffer-file-name @@ -1253,7 +1261,7 @@ and just return it. PROMPT shouldn't end with a question mark." ;;; (defvar eglot-mode-map (let ((map (make-sparse-keymap))) - (define-key map [remap display-local-help] 'eglot-help-at-point) + (define-key map [remap display-local-help] 'eldoc-doc-buffer) map)) (defvar-local eglot--current-flymake-report-fn nil @@ -1325,7 +1333,11 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (add-hook 'change-major-mode-hook #'eglot--managed-mode-off nil t) (add-hook 'post-self-insert-hook 'eglot--post-self-insert-hook nil t) (add-hook 'pre-command-hook 'eglot--pre-command-hook nil t) - (eglot--setq-saving eldoc-documentation-function #'eglot-eldoc-function) + (eglot--setq-saving eldoc-documentation-functions + '(eglot-signature-eldoc-function + eglot-hover-eldoc-function)) + (eglot--setq-saving eldoc-documentation-strategy + #'eldoc-documentation-enthusiast) (eglot--setq-saving xref-prompt-for-identifier nil) (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend t)) (eglot--setq-saving company-backends '(company-capf)) @@ -1733,7 +1745,7 @@ Records BEG, END and PRE-CHANGE-LENGTH locally." (setq eglot--change-idle-timer (run-with-idle-timer eglot-send-changes-idle-time - nil (lambda () (eglot--with-live-buffer buf + nil (lambda () (eglot--when-live-buffer buf (when eglot--managed-mode (eglot--signal-textDocument/didChange) (setq eglot--change-idle-timer nil)))))))) @@ -2189,9 +2201,7 @@ is not active." (delete-region (- (point) (length proxy)) (point)) (funcall snippet-fn (or insertText label))))) (eglot--signal-textDocument/didChange) - (eglot-eldoc-function))))))) - -(defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") + (eldoc))))))) (defun eglot--hover-info (contents &optional range) (let ((heading (and range (pcase-let ((`(,beg . ,end) (eglot--range-region range))) @@ -2256,177 +2266,68 @@ is not active." (buffer-string)))) when moresigs concat "\n")) -(defvar eglot--help-buffer nil) +(defun eglot-signature-eldoc-function (cb) + "A member of `eldoc-documentation-functions', for signatures." + (when (eglot--server-capable :signatureHelpProvider) + (let ((buf (current-buffer))) + (jsonrpc-async-request + (eglot--current-server-or-lose) + :textDocument/signatureHelp (eglot--TextDocumentPositionParams) + :success-fn + (eglot--lambda ((SignatureHelp) + signatures activeSignature activeParameter) + (eglot--when-buffer-window buf + (funcall cb + (unless (seq-empty-p signatures) + (eglot--sig-info signatures + activeSignature + activeParameter))))) + :deferred :textDocument/signatureHelp)) + t)) -(defun eglot--help-buffer () - (or (and (buffer-live-p eglot--help-buffer) - eglot--help-buffer) - (setq eglot--help-buffer (generate-new-buffer "*eglot-help*")))) +(defun eglot-hover-eldoc-function (cb) + "A member of `eldoc-documentation-functions', for hover." + (when (eglot--server-capable :hoverProvider) + (let ((buf (current-buffer))) + (jsonrpc-async-request + (eglot--current-server-or-lose) + :textDocument/hover (eglot--TextDocumentPositionParams) + :success-fn (eglot--lambda ((Hover) contents range) + (eglot--when-buffer-window buf + (let ((info (unless (seq-empty-p contents) + (eglot--hover-info contents range)))) + (funcall cb info :buffer t)))) + :deferred :textDocument/hover)) + (eglot--highlight-piggyback cb) + t)) -(defun eglot-help-at-point () - "Request documentation for the thing at point." - (interactive) - (eglot--dbind ((Hover) contents range) - (jsonrpc-request (eglot--current-server-or-lose) :textDocument/hover - (eglot--TextDocumentPositionParams)) - (let ((blurb (and (not (seq-empty-p contents)) - (eglot--hover-info contents range))) - (hint (thing-at-point 'symbol))) - (if blurb - (with-current-buffer (eglot--help-buffer) - (with-help-window (current-buffer) - (rename-buffer (format "*eglot-help for %s*" hint)) - (with-current-buffer standard-output (insert blurb)) - (setq-local nobreak-char-display nil))) - (display-local-help))))) - -(cl-defun eglot-doc-too-large-for-echo-area - (string &optional (height max-mini-window-height)) - "Return non-nil if STRING won't fit in echo area of height HEIGHT. -HEIGHT defaults to `max-mini-window-height' (which see) and is -interpreted like that variable. If non-nil, the return value is -the number of lines available." - (let ((available-lines (cl-typecase height - (float (truncate (* (frame-height) height))) - (integer height) - (t 1)))) - (when (> (1+ (cl-count ?\n string)) available-lines) - available-lines))) - -(cl-defun eglot--truncate-string (string height &optional (width (frame-width))) - "Return as much from STRING as fits in HEIGHT and WIDTH. -WIDTH, if non-nil, truncates last line to those columns." - (cl-flet ((maybe-trunc - (str) (if width (truncate-string-to-width str width - nil nil "...") - str))) - (cl-loop - repeat height - for i from 1 - for break-pos = (cl-position ?\n string) - for (line . rest) = (and break-pos - (cons (substring string 0 break-pos) - (substring string (1+ break-pos)))) - concat (cond (line (if (= i height) (maybe-trunc line) (concat line "\n"))) - (t (maybe-trunc string))) - while rest do (setq string rest)))) - -(defcustom eglot-put-doc-in-help-buffer - ;; JT@2020-05-21: TODO: this variable should be renamed and the - ;; decision somehow be in eldoc.el itself. - #'eglot-doc-too-large-for-echo-area - "If non-nil, put \"hover\" documentation in separate `*eglot-help*' buffer. -If nil, use whatever `eldoc-message-function' decides, honouring -`eldoc-echo-area-use-multiline-p'. If t, use `*eglot-help*' -unconditionally. If a function, it is called with the -documentation string to display and returns a generalized boolean -interpreted as one of the two preceding values." - :type '(choice (const :tag "Never use `*eglot-help*'" nil) - (const :tag "Always use `*eglot-help*'" t) - (function :tag "Ask a function"))) - -(defcustom eglot-auto-display-help-buffer nil - "If non-nil, automatically display `*eglot-help*' buffer. -Buffer is displayed with `display-buffer', which obeys -`display-buffer-alist' & friends." - :type 'boolean) +(defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") -(defun eglot--update-doc (string hint) - "Put updated documentation STRING where it belongs. -HINT is used to potentially rename EGLOT's help buffer. If -STRING is nil, the echo area cleared of any previous -documentation. Honour `eglot-put-doc-in-help-buffer', -`eglot-auto-display-help-buffer' and -`eldoc-echo-area-use-multiline-p'." - (cond ((null string) (eldoc-message nil)) - ((or (eq t eglot-put-doc-in-help-buffer) - (and eglot-put-doc-in-help-buffer - (funcall eglot-put-doc-in-help-buffer string))) - (with-current-buffer (eglot--help-buffer) - (let ((inhibit-read-only t) - (name (format "*eglot-help for %s*" hint))) - (unless (string= name (buffer-name)) - (rename-buffer (format "*eglot-help for %s*" hint)) - (erase-buffer) - (insert string) - (goto-char (point-min))) - (help-mode))) - (if eglot-auto-display-help-buffer - (display-buffer eglot--help-buffer) - (unless (get-buffer-window eglot--help-buffer t) - ;; Hand-tweaked to print two lines. Should it print - ;; 1? Or honour max-mini-window-height? - (eglot--message - "%s\n(Truncated, %sfull help in buffer %s)" - (eglot--truncate-string string 1 (- (frame-width) 9)) - (if-let (key (car (where-is-internal 'eglot-help-at-point))) - (format "use %s to see " (key-description key)) "") - (buffer-name eglot--help-buffer))))) - ((eq eldoc-echo-area-use-multiline-p t) - (if-let ((available (eglot-doc-too-large-for-echo-area string))) - (eldoc-message (eglot--truncate-string string available)) - (eldoc-message string))) - ((eq eldoc-echo-area-use-multiline-p 'truncate-sym-name-if-fit) - (eldoc-message (eglot--truncate-string string 1 nil))) - (t - ;; Can't (yet?) honour non-t non-nil values of this var - (eldoc-message (eglot--truncate-string string 1))))) - -(defun eglot-eldoc-function () - "EGLOT's `eldoc-documentation-function' function." - (let* ((buffer (current-buffer)) - (server (eglot--current-server-or-lose)) - (position-params (eglot--TextDocumentPositionParams)) - sig-showing - (thing-at-point (thing-at-point 'symbol))) - (cl-macrolet ((when-buffer-window - (&body body) ; notice the exception when testing with `ert' - `(when (or (get-buffer-window buffer) (ert-running-test)) - (with-current-buffer buffer ,@body)))) - (when (eglot--server-capable :signatureHelpProvider) - (jsonrpc-async-request - server :textDocument/signatureHelp position-params - :success-fn - (eglot--lambda ((SignatureHelp) - signatures activeSignature activeParameter) - (when-buffer-window - (when (cl-plusp (length signatures)) - (setq sig-showing t) - (eglot--update-doc (eglot--sig-info signatures - activeSignature - activeParameter) - thing-at-point)))) - :deferred :textDocument/signatureHelp)) - (when (eglot--server-capable :hoverProvider) - (jsonrpc-async-request - server :textDocument/hover position-params - :success-fn (eglot--lambda ((Hover) contents range) - (unless sig-showing - (when-buffer-window - (eglot--update-doc (and (not (seq-empty-p contents)) - (eglot--hover-info contents - range)) - thing-at-point)))) - :deferred :textDocument/hover)) - (when (eglot--server-capable :documentHighlightProvider) - (jsonrpc-async-request - server :textDocument/documentHighlight position-params - :success-fn - (lambda (highlights) - (mapc #'delete-overlay eglot--highlights) - (setq eglot--highlights - (when-buffer-window - (mapcar - (eglot--lambda ((DocumentHighlight) range) - (pcase-let ((`(,beg . ,end) - (eglot--range-region range))) - (let ((ov (make-overlay beg end))) - (overlay-put ov 'face 'highlight) - (overlay-put ov 'evaporate t) - ov))) - highlights)))) - :deferred :textDocument/documentHighlight)))) - eldoc-last-message) +(defun eglot--highlight-piggyback (_cb) + "Request and handle `:textDocument/documentHighlight'" + ;; FIXME: Obviously, this is just piggy backing on eldoc's calls for + ;; convenience, as shown by the fact that we just ignore cb. + (let ((buf (current-buffer))) + (when (eglot--server-capable :documentHighlightProvider) + (jsonrpc-async-request + (eglot--current-server-or-lose) + :textDocument/documentHighlight (eglot--TextDocumentPositionParams) + :success-fn + (lambda (highlights) + (mapc #'delete-overlay eglot--highlights) + (setq eglot--highlights + (eglot--when-buffer-window buf + (mapcar + (eglot--lambda ((DocumentHighlight) range) + (pcase-let ((`(,beg . ,end) + (eglot--range-region range))) + (let ((ov (make-overlay beg end))) + (overlay-put ov 'face 'highlight) + (overlay-put ov 'evaporate t) + ov))) + highlights)))) + :deferred :textDocument/documentHighlight) + nil))) (defun eglot-imenu () "EGLOT's `imenu-create-index-function'." @@ -2549,7 +2450,7 @@ documentation. Honour `eglot-put-doc-in-help-buffer', (unwind-protect (if prepared (eglot--warn "Caution: edits of files %s failed." (mapcar #'car prepared)) - (eglot-eldoc-function) + (eldoc) (eglot--message "Edit successful!")))))) (defun eglot-rename (newname) commit 29dbbcc185471136a8dc874619dbf8f2ffba8fa6 Author: João Távora Date: Sun May 31 11:49:51 2020 +0100 Fix small problems around eglot's help buffer Specifically: - correctly format the message shown to the user about doc being truncated - don't show message if the buffer is showing in some frame's window - correctly name the help buffer switched to with `C-h .'. This is still not ideal: - When the `C-h .' suggestion is shown to the user, typing that keybinding shouldn't result in a new LSP request to fetch probably the same info; - All this functionality belongs in eldoc.el. * eglot.el (eglot-help-at-point): Fix buffer name. (eglot--update-doc): Provide more help. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4b25368d73..733b69c395 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2270,12 +2270,12 @@ is not active." (jsonrpc-request (eglot--current-server-or-lose) :textDocument/hover (eglot--TextDocumentPositionParams)) (let ((blurb (and (not (seq-empty-p contents)) - (eglot--hover-info contents range)))) + (eglot--hover-info contents range))) + (hint (thing-at-point 'symbol))) (if blurb (with-current-buffer (eglot--help-buffer) (with-help-window (current-buffer) - (rename-buffer (format "*eglot-help for %s*" - (thing-at-point 'symbol))) + (rename-buffer (format "*eglot-help for %s*" hint)) (with-current-buffer standard-output (insert blurb)) (setq-local nobreak-char-display nil))) (display-local-help))))) @@ -2350,16 +2350,18 @@ documentation. Honour `eglot-put-doc-in-help-buffer', (erase-buffer) (insert string) (goto-char (point-min))) - (if eglot-auto-display-help-buffer - (display-buffer (current-buffer)) - (unless (get-buffer-window (current-buffer)) - ;; This prints two lines. Should it print 1? Or - ;; honour max-mini-window-height? - (eglot--message - "%s\n(...truncated. Full help is in `%s')" - (eglot--truncate-string string 1 (- (frame-width) 8)) - (buffer-name eglot--help-buffer)))) - (help-mode)))) + (help-mode))) + (if eglot-auto-display-help-buffer + (display-buffer eglot--help-buffer) + (unless (get-buffer-window eglot--help-buffer t) + ;; Hand-tweaked to print two lines. Should it print + ;; 1? Or honour max-mini-window-height? + (eglot--message + "%s\n(Truncated, %sfull help in buffer %s)" + (eglot--truncate-string string 1 (- (frame-width) 9)) + (if-let (key (car (where-is-internal 'eglot-help-at-point))) + (format "use %s to see " (key-description key)) "") + (buffer-name eglot--help-buffer))))) ((eq eldoc-echo-area-use-multiline-p t) (if-let ((available (eglot-doc-too-large-for-echo-area string))) (eldoc-message (eglot--truncate-string string available)) commit a56c77183a9b59409701cfd78e38b5a50970857d Author: Andrii Kolomoiets Date: Wed May 13 10:48:26 2020 +0300 Simplify eglot-code-actions If no region is active, ask for code actions at point, even if there are no diagnostics at point. Co-authored-by: João Távora * eglot.el (eglot-code-actions): Simplify. GitHub-reference: close https://github.com/joaotavora/eglot/issues/473 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8bb610bb4e..4b25368d73 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2563,15 +2563,13 @@ documentation. Honour `eglot-put-doc-in-help-buffer', current-prefix-arg)) -(defun eglot-code-actions (&optional beg end) - "Get and offer to execute code actions between BEG and END." +(defun eglot-code-actions (beg &optional end) + "Offer to execute code actions between BEG and END. +Interactively, if a region is active, BEG and END are its bounds, +else BEG is point and END is nil, which results in a request for +code actions at point" (interactive - (let (diags) - (cond ((region-active-p) (list (region-beginning) (region-end))) - ((setq diags (flymake-diagnostics (point))) - (list (cl-reduce #'min (mapcar #'flymake-diagnostic-beg diags)) - (cl-reduce #'max (mapcar #'flymake-diagnostic-end diags)))) - (t (list (point-min) (point-max)))))) + (if (region-active-p) `(,(region-beginning) ,(region-end)) `(,(point) nil))) (unless (eglot--server-capable :codeActionProvider) (eglot--error "Server can't execute code actions!")) (let* ((server (eglot--current-server-or-lose)) commit 5e0cd484ecd9abc9b1a9e1b14468620b9de9c9cc Author: Andrii Kolomoiets Date: Tue May 26 23:03:39 2020 +0300 Use filter-buffer-substring to get buffer text This way modes used to represent hover info text, such as gfm-view-mode can e.g. filter out invisible text by providing own `filter-buffer-substring-function'. * eglot.el (eglot--format-markup): Use `filter-buffer-substring'. GitHub-reference: close https://github.com/joaotavora/eglot/issues/482 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index dc0200d617..8bb610bb4e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1165,10 +1165,10 @@ Doubles as an indicator of snippet support." (_ major-mode)))))) (with-temp-buffer (setq-local markdown-fontify-code-blocks-natively t) - (insert (string-trim string)) + (insert string) (ignore-errors (delay-mode-hooks (funcall mode))) (font-lock-ensure) - (buffer-string)))) + (string-trim (filter-buffer-substring (point-min) (point-max)))))) (defcustom eglot-ignored-server-capabilites (list) "LSP server capabilities that Eglot could use, but won't. commit e14934fef20460716c9e4588749c46e979dc154f Author: João Távora Date: Mon May 25 11:18:21 2020 +0100 Replace uses of project-roots with project-root * eglot.el (Package-Requires): Require project 0.3.0. (eglot--connect, eglot-handle-request) (eglot-initialization-options, eglot--eclipse-jdt-contact): Use project-root. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fe8eeee9ae..dc0200d617 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -7,7 +7,7 @@ ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.9") (flymake "1.0.8") (package "0.1.1") (xref "1.0.1") (eldoc "1.0.0")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.9") (flymake "1.0.8") (project "0.3.0") (xref "1.0.1") (eldoc "1.0.0")) ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by @@ -851,7 +851,7 @@ Each function is passed the server as an argument") (defun eglot--connect (managed-major-mode project class contact) "Connect to MANAGED-MAJOR-MODE, PROJECT, CLASS and CONTACT. This docstring appeases checkdoc, that's all." - (let* ((default-directory (car (project-roots project))) + (let* ((default-directory (project-root project)) (nickname (file-name-base (directory-file-name default-directory))) (readable-name (format "EGLOT (%s/%s)" nickname managed-major-mode)) autostart-inferior-process @@ -947,7 +947,7 @@ This docstring appeases checkdoc, that's all." (lambda () (setf (eglot--inhibit-autoreconnect server) (null eglot-autoreconnect))))))) - (let ((default-directory (car (project-roots project))) + (let ((default-directory (project-root project)) (major-mode managed-major-mode)) (hack-dir-local-variables-non-file-buffer) (run-hook-with-args 'eglot-connect-hook server)) @@ -1781,7 +1781,7 @@ When called interactively, use the currently active server" (if (and (not (string-empty-p uri-path)) (file-directory-p uri-path)) uri-path - (car (project-roots (eglot--project server)))))) + (project-root (eglot--project server))))) (setq-local major-mode (eglot--major-mode server)) (hack-dir-local-variables-non-file-buffer) (alist-get section eglot-workspace-configuration @@ -2705,16 +2705,14 @@ documentation. Honour `eglot-put-doc-in-help-buffer', `(:workspaceFolders [,@(cl-delete-duplicates (mapcar #'eglot--path-to-uri - (let* ((roots (project-roots (eglot--project server))) - (root (car roots))) - (append - roots - (mapcar - #'file-name-directory - (append - (file-expand-wildcards (concat root "*/pom.xml")) - (file-expand-wildcards (concat root "*/build.gradle")) - (file-expand-wildcards (concat root "*/.project"))))))) + (let* ((root (project-root (eglot--project server)))) + (cons root + (mapcar + #'file-name-directory + (append + (file-expand-wildcards (concat root "*/pom.xml")) + (file-expand-wildcards (concat root "*/build.gradle")) + (file-expand-wildcards (concat root "*/.project"))))))) :test #'string=)] ,@(if-let ((home (or (getenv "JAVA_HOME") (ignore-errors @@ -2762,7 +2760,7 @@ If INTERACTIVE, prompt user for details." (t "config_linux")))) (project (or (project-current) `(transient . ,default-directory))) (workspace - (expand-file-name (md5 (car (project-roots project))) + (expand-file-name (md5 (project-root project)) (concat user-emacs-directory "eglot-eclipse-jdt-cache")))) (unless jar commit a4c7fdfd2d4b512043899a0b1ce88d84aa231f53 Author: Gary Oberbrunner Date: Tue May 26 14:13:44 2020 +0100 Also consider label of a completionitem for snippets Copyright-paperwork-exempt: yes * eglot.el (eglot-completion-at-point): Consider label when expanding snippets. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/480 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cc6029079a..fe8eeee9ae 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2152,9 +2152,7 @@ is not active." :exit-function (lambda (proxy _status) (eglot--dbind ((CompletionItem) insertTextFormat - insertText - textEdit - additionalTextEdits) + insertText textEdit additionalTextEdits label) (funcall resolve-maybe (or (get-text-property 0 'eglot--lsp-item proxy) @@ -2189,7 +2187,7 @@ is not active." ;; whole completion, since `insertText' is the full ;; completion's text. (delete-region (- (point) (length proxy)) (point)) - (funcall snippet-fn insertText)))) + (funcall snippet-fn (or insertText label))))) (eglot--signal-textDocument/didChange) (eglot-eldoc-function))))))) commit 43f9294773a9d8928d179c7a4d206a312b869a90 Author: Andrii Kolomoiets Date: Wed May 6 22:06:35 2020 +0300 Rework computation of string given to eldoc (again) Co-authored-by: Andreii Kolomoiets Also do some refactoring to join similar logic in eglot-doc-too-large-for-echo-area and eglot--truncate-string. * eglot.el (eglot-doc-too-large-for-echo-area): Now returns the number of lines available. (eglot--truncate-string): New helper. (eglot--first-line-of-doc, eglot--top-lines-of-doc): Remove. (eglot--update-doc): Use new helpers. * eglot-tests.el (hover-multiline-doc-locus): New test GitHub-reference: close https://github.com/joaotavora/eglot/issues/459 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 15fa2a189f..cc6029079a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2282,24 +2282,47 @@ is not active." (setq-local nobreak-char-display nil))) (display-local-help))))) -(defun eglot-doc-too-large-for-echo-area (string) - "Return non-nil if STRING won't fit in echo area. -Respects `max-mini-window-height' (which see)." - (let ((max-height - (cond ((floatp max-mini-window-height) (* (frame-height) - max-mini-window-height)) - ((integerp max-mini-window-height) max-mini-window-height) - (t 1)))) - (> (cl-count ?\n string) max-height))) +(cl-defun eglot-doc-too-large-for-echo-area + (string &optional (height max-mini-window-height)) + "Return non-nil if STRING won't fit in echo area of height HEIGHT. +HEIGHT defaults to `max-mini-window-height' (which see) and is +interpreted like that variable. If non-nil, the return value is +the number of lines available." + (let ((available-lines (cl-typecase height + (float (truncate (* (frame-height) height))) + (integer height) + (t 1)))) + (when (> (1+ (cl-count ?\n string)) available-lines) + available-lines))) + +(cl-defun eglot--truncate-string (string height &optional (width (frame-width))) + "Return as much from STRING as fits in HEIGHT and WIDTH. +WIDTH, if non-nil, truncates last line to those columns." + (cl-flet ((maybe-trunc + (str) (if width (truncate-string-to-width str width + nil nil "...") + str))) + (cl-loop + repeat height + for i from 1 + for break-pos = (cl-position ?\n string) + for (line . rest) = (and break-pos + (cons (substring string 0 break-pos) + (substring string (1+ break-pos)))) + concat (cond (line (if (= i height) (maybe-trunc line) (concat line "\n"))) + (t (maybe-trunc string))) + while rest do (setq string rest)))) (defcustom eglot-put-doc-in-help-buffer + ;; JT@2020-05-21: TODO: this variable should be renamed and the + ;; decision somehow be in eldoc.el itself. #'eglot-doc-too-large-for-echo-area "If non-nil, put \"hover\" documentation in separate `*eglot-help*' buffer. If nil, use whatever `eldoc-message-function' decides, honouring `eldoc-echo-area-use-multiline-p'. If t, use `*eglot-help*' -unconditionally. If a function, it is called with the docstring -to display and should a boolean producing one of the two previous -values." +unconditionally. If a function, it is called with the +documentation string to display and returns a generalized boolean +interpreted as one of the two preceding values." :type '(choice (const :tag "Never use `*eglot-help*'" nil) (const :tag "Always use `*eglot-help*'" t) (function :tag "Ask a function"))) @@ -2310,11 +2333,6 @@ Buffer is displayed with `display-buffer', which obeys `display-buffer-alist' & friends." :type 'boolean) -(defun eglot--first-line-of-doc (string) - (truncate-string-to-width - (replace-regexp-in-string "\\(.*\\)\n.*" "\\1" string) - (frame-width) nil nil "...")) - (defun eglot--update-doc (string hint) "Put updated documentation STRING where it belongs. HINT is used to potentially rename EGLOT's help buffer. If @@ -2337,16 +2355,22 @@ documentation. Honour `eglot-put-doc-in-help-buffer', (if eglot-auto-display-help-buffer (display-buffer (current-buffer)) (unless (get-buffer-window (current-buffer)) + ;; This prints two lines. Should it print 1? Or + ;; honour max-mini-window-height? (eglot--message "%s\n(...truncated. Full help is in `%s')" - (eglot--first-line-of-doc string) + (eglot--truncate-string string 1 (- (frame-width) 8)) (buffer-name eglot--help-buffer)))) (help-mode)))) - (eldoc-echo-area-use-multiline-p - ;; Can't really honour non-t non-nil values if this var - (eldoc-message string)) + ((eq eldoc-echo-area-use-multiline-p t) + (if-let ((available (eglot-doc-too-large-for-echo-area string))) + (eldoc-message (eglot--truncate-string string available)) + (eldoc-message string))) + ((eq eldoc-echo-area-use-multiline-p 'truncate-sym-name-if-fit) + (eldoc-message (eglot--truncate-string string 1 nil))) (t - (eldoc-message (eglot--first-line-of-doc string))))) + ;; Can't (yet?) honour non-t non-nil values of this var + (eldoc-message (eglot--truncate-string string 1))))) (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function." commit b2dd04ddbe94b5a10884fd01a8309a0ea8f6b887 Author: Philipp Stephani Date: Mon May 25 11:30:32 2020 +0200 Fix type error in eglot--xref-make-match Its first argument is passed to xref-make-match, which expects a string as its SUMMARY argument, but symbol-at-point returns a symbol. Co-authored-by: João Távora * eglot.el (eglot--lsp-xrefs-for-method): use symbol-name. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/488 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c38620edf9..15fa2a189f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1954,7 +1954,8 @@ Try to visit the target file for a richer summary line." (eglot--collecting-xrefs (collect) (mapc (eglot--lambda ((Location) uri range) - (collect (eglot--xref-make-match (symbol-at-point) uri range))) + (collect (eglot--xref-make-match (symbol-name (symbol-at-point)) + uri range))) (if (vectorp response) response (list response)))))) (cl-defun eglot--lsp-xref-helper (method &key extra-params capability ) commit 6cc6392546a69efaa3089790dcd2d5d0815ae2b3 Author: Rudolf Schlatte Date: Thu May 21 11:20:41 2020 +0200 Add support for erlang_ls * README.md: Mention erlang_ls * eglot.el (eglot-server-programs): Add erlang_ls GitHub-reference: close https://github.com/joaotavora/eglot/issues/471 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 21a2496572..c38620edf9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -111,7 +111,8 @@ language-server/bin/php-language-server.php")) (ada-mode . ("ada_language_server")) (scala-mode . ("metals-emacs")) ((tex-mode context-mode texinfo-mode bibtex-mode) - . ("digestif"))) + . ("digestif")) + (erlang-mode . ("erlang_ls" "--transport" "stdio"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated commit a5a1559e7a5906575325b72cae217a805be1a4de Author: João Távora Date: Mon May 18 13:10:13 2020 +0100 Correctly place diagnostics in narrowed buffers * eglot.el (eglot--lsp-position-to-point) (eglot-handle-notification): save-restriction and widen GitHub-reference: fix https://github.com/joaotavora/eglot/issues/479 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f3501cbbcf..21a2496572 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1118,19 +1118,21 @@ be set to `eglot-move-to-lsp-abiding-column' (the default), and "Convert LSP position POS-PLIST to Emacs point. If optional MARKER, return a marker instead" (save-excursion - (goto-char (point-min)) - (forward-line (min most-positive-fixnum - (plist-get pos-plist :line))) - (unless (eobp) ;; if line was excessive leave point at eob - (let ((tab-width 1) - (col (plist-get pos-plist :character))) - (unless (wholenump col) - (eglot--warn - "Caution: LSP server sent invalid character position %s. Using 0 instead." - col) - (setq col 0)) - (funcall eglot-move-to-column-function col))) - (if marker (copy-marker (point-marker)) (point)))) + (save-restriction + (widen) + (goto-char (point-min)) + (forward-line (min most-positive-fixnum + (plist-get pos-plist :line))) + (unless (eobp) ;; if line was excessive leave point at eob + (let ((tab-width 1) + (col (plist-get pos-plist :character))) + (unless (wholenump col) + (eglot--warn + "Caution: LSP server sent invalid character position %s. Using 0 instead." + col) + (setq col 0)) + (funcall eglot-move-to-column-function col))) + (if marker (copy-marker (point-marker)) (point))))) (defun eglot--path-to-uri (path) "URIfy PATH." @@ -1585,13 +1587,15 @@ COMMAND is a symbol naming the command." message `((eglot-lsp-diag . ,diag-spec))))) into diags finally (cond ((and flymake-mode eglot--current-flymake-report-fn) - (funcall eglot--current-flymake-report-fn diags - ;; If the buffer hasn't changed since last - ;; call to the report function, flymake won't - ;; delete old diagnostics. Using :region - ;; keyword forces flymake to delete - ;; them (github#159). - :region (cons (point-min) (point-max))) + (save-restriction + (widen) + (funcall eglot--current-flymake-report-fn diags + ;; If the buffer hasn't changed since last + ;; call to the report function, flymake won't + ;; delete old diagnostics. Using :region + ;; keyword forces flymake to delete + ;; them (github#159). + :region (cons (point-min) (point-max)))) (setq eglot--unreported-diagnostics nil)) (t (setq eglot--unreported-diagnostics (cons t diags)))))) commit c57ee29fb45dd06a71b9acb87712397f38ca22d8 Author: João Távora Date: Thu May 14 23:10:48 2020 +0100 Require xref, project and eldoc from gnu elpa * Makefile (ELPADEPS): Install Xref, Project and Eldoc. * eglot.el (Package-Requires): Require Xref, Project and Eldoc from GNU ELPA. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8a1d162c6d..f3501cbbcf 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -7,7 +7,7 @@ ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.9") (flymake "1.0.8")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.9") (flymake "1.0.8") (package "0.1.1") (xref "1.0.1") (eldoc "1.0.0")) ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by commit cc8bbf69ffbafc99296e5ccce17d39ea945cde63 Author: Dan Davison Date: Wed May 13 20:06:35 2020 -0400 Prompt for executable if supplied name does not exist * eglot.el (eglot--guess-contact): Interpret a list containing a single string as an executable when forming the interactive prompt. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/474 GitHub-reference: fix https://github.com/joaotavora/eglot/issues/478 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 112959b356..8a1d162c6d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -704,7 +704,11 @@ be guessed." (prog1 (car guess) (setq guess (cdr guess)))) 'eglot-lsp-server)) (program (and (listp guess) - (stringp (car guess)) (stringp (cadr guess)) (car guess))) + (stringp (car guess)) + ;; A second element might be the port of a (host, port) + ;; pair, but in that case it is not a string. + (or (null (cdr guess)) (stringp (cadr guess))) + (car guess))) (base-prompt (and interactive "Enter program to execute (or :): ")) commit f97e9aa75c794f11442167c239439244c88d343f Author: Dan Davison Date: Tue May 12 11:50:56 2020 -0400 Don't call flymake report function if flymake is disabled Also fix https://github.com/joaotavora/eglot/issues/472. Copyright-paperwork-exempt: yes * eglot.el (eglot-handle-notification): Check that flymake-mode is active before calling flymake report function. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/468 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8dae0b7026..112959b356 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1580,7 +1580,7 @@ COMMAND is a symbol naming the command." (t 'eglot-note)) message `((eglot-lsp-diag . ,diag-spec))))) into diags - finally (cond (eglot--current-flymake-report-fn + finally (cond ((and flymake-mode eglot--current-flymake-report-fn) (funcall eglot--current-flymake-report-fn diags ;; If the buffer hasn't changed since last ;; call to the report function, flymake won't commit 11c911574972976fbf39cdfb392d469d61f688da Author: Tobias Rittweiler Date: Fri May 8 00:51:44 2020 +0200 Fix "free variable" warning * eglot.el (eglot-events-buffer): Use `eglot-current-server' instead of `eglot--cached-server' because the latter is declared later in the file. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/460 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b5f05ce7e3..8dae0b7026 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -810,7 +810,7 @@ INTERACTIVE is t if called interactively." (defun eglot-events-buffer (server) "Display events buffer for SERVER. Use current server's or first available Eglot events buffer." - (interactive (list eglot--cached-server)) + (interactive (list (eglot-current-server))) (let ((buffer (if server (jsonrpc-events-buffer server) (cl-find "\\*EGLOT.*events\\*" (buffer-list) commit 8cf6f4f292ec8ae469daa80e1b155240d845d73b Author: Andrii Kolomoiets Date: Thu May 7 00:35:48 2020 +0300 Remove trailing whitespaces * eglot.el (defvar company-backends, eglot-code-actions): Remove trailing whitespace diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 03609a128c..b5f05ce7e3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -74,7 +74,7 @@ (require 'array) ;; forward-declare, but don't require (Emacs 28 doesn't seem to care) (defvar markdown-fontify-code-blocks-natively) -(defvar company-backends) +(defvar company-backends) (defvar company-tooltip-align-annotations) @@ -2563,7 +2563,7 @@ documentation. Honour `eglot-put-doc-in-help-buffer', (menu `("Eglot code actions:" ("dummy" ,@menu-items))) (action (if (listp last-nonmenu-event) (x-popup-menu last-nonmenu-event menu) - (cdr (assoc (completing-read "[eglot] Pick an action: " + (cdr (assoc (completing-read "[eglot] Pick an action: " menu-items nil t nil nil (car menu-items)) menu-items))))) commit 1091226b3a65d9e4a324691a65db46b47e76d9b1 Author: João Távora Date: Sun May 3 21:59:29 2020 +0100 Simplify bug-reporting instructions We assume the user has a recent enough jsonrpc.el that consolidates events and stderr int the same transcript. * README.md (Reporting bugs): Simplify instructions. * eglot.el (eglot-events-buffer): Can work with no server. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d172f4c7cc..03609a128c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -808,9 +808,15 @@ INTERACTIVE is t if called interactively." (add-hook 'post-command-hook #'maybe-connect 'append nil))))) (defun eglot-events-buffer (server) - "Display events buffer for SERVER." - (interactive (list (eglot--current-server-or-lose))) - (display-buffer (jsonrpc-events-buffer server))) + "Display events buffer for SERVER. +Use current server's or first available Eglot events buffer." + (interactive (list eglot--cached-server)) + (let ((buffer (if server (jsonrpc-events-buffer server) + (cl-find "\\*EGLOT.*events\\*" + (buffer-list) + :key #'buffer-name :test #'string-match)))) + (if buffer (display-buffer buffer) + (eglot--error "Can't find an Eglot events buffer!")))) (defun eglot-stderr-buffer (server) "Display stderr buffer for SERVER." commit 3cef1072ad4973c5055e6719deac27b9d9efc5f4 Author: João Távora Date: Sun May 3 21:37:42 2020 +0100 Survive hover responses with empty markdown strings * eglot.el (eglot-help-at-point): Protect against null eglot--hover-info GitHub-reference: fix https://github.com/joaotavora/eglot/issues/433 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8fadd5f7ab..d172f4c7cc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2255,15 +2255,16 @@ is not active." (eglot--dbind ((Hover) contents range) (jsonrpc-request (eglot--current-server-or-lose) :textDocument/hover (eglot--TextDocumentPositionParams)) - (if (seq-empty-p contents) - (display-local-help) - (let ((blurb (eglot--hover-info contents range)) - (sym (thing-at-point 'symbol))) - (with-current-buffer (eglot--help-buffer) - (with-help-window (current-buffer) - (rename-buffer (format "*eglot-help for %s*" sym)) - (with-current-buffer standard-output (insert blurb)) - (setq-local nobreak-char-display nil))))))) + (let ((blurb (and (not (seq-empty-p contents)) + (eglot--hover-info contents range)))) + (if blurb + (with-current-buffer (eglot--help-buffer) + (with-help-window (current-buffer) + (rename-buffer (format "*eglot-help for %s*" + (thing-at-point 'symbol))) + (with-current-buffer standard-output (insert blurb)) + (setq-local nobreak-char-display nil))) + (display-local-help))))) (defun eglot-doc-too-large-for-echo-area (string) "Return non-nil if STRING won't fit in echo area. commit a2af2e126817cff818d8d56ceb6c689cd7fb025d Author: João Távora Date: Sun May 3 21:10:05 2020 +0100 Remap display-local-help (c-h .) to eglot-help-at-point * eglot.el (eglot-help-at-point): Fallback to display-local-help if no hover doc (eglot-mode-map): Remap display-local-help to eglot-help-at-point.. GitHub-reference: per https://github.com/joaotavora/eglot/issues/437 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 09be1f8dc6..8fadd5f7ab 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1238,7 +1238,10 @@ and just return it. PROMPT shouldn't end with a question mark." ;;; Minor modes ;;; -(defvar eglot-mode-map (make-sparse-keymap)) +(defvar eglot-mode-map + (let ((map (make-sparse-keymap))) + (define-key map [remap display-local-help] 'eglot-help-at-point) + map)) (defvar-local eglot--current-flymake-report-fn nil "Current flymake report function for this buffer") @@ -2247,19 +2250,20 @@ is not active." (setq eglot--help-buffer (generate-new-buffer "*eglot-help*")))) (defun eglot-help-at-point () - "Request \"hover\" information for the thing at point." + "Request documentation for the thing at point." (interactive) (eglot--dbind ((Hover) contents range) (jsonrpc-request (eglot--current-server-or-lose) :textDocument/hover (eglot--TextDocumentPositionParams)) - (when (seq-empty-p contents) (eglot--error "No hover info here")) - (let ((blurb (eglot--hover-info contents range)) - (sym (thing-at-point 'symbol))) - (with-current-buffer (eglot--help-buffer) - (with-help-window (current-buffer) - (rename-buffer (format "*eglot-help for %s*" sym)) - (with-current-buffer standard-output (insert blurb)) - (setq-local nobreak-char-display nil)))))) + (if (seq-empty-p contents) + (display-local-help) + (let ((blurb (eglot--hover-info contents range)) + (sym (thing-at-point 'symbol))) + (with-current-buffer (eglot--help-buffer) + (with-help-window (current-buffer) + (rename-buffer (format "*eglot-help for %s*" sym)) + (with-current-buffer standard-output (insert blurb)) + (setq-local nobreak-char-display nil))))))) (defun eglot-doc-too-large-for-echo-area (string) "Return non-nil if STRING won't fit in echo area. commit 1914356c60e7308175121dbae8340dbbc9847ffc Author: Theodor Thornhill Date: Sun May 3 11:20:27 2020 +0200 Tweak handling of eldoc-echo-area-use-multiline-p Also close https://github.com/joaotavora/eglot/issues/453 Co-authored-by: João Távora * eglot.el (eglot--first-line-of-doc): New helper. (eglot--update-doc): Tweak docstring. Simplify. (eglot-put-doc-in-help-buffer): Tweak docstring GitHub-reference: per https://github.com/joaotavora/eglot/issues/443 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 22a8aefa31..09be1f8dc6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2274,10 +2274,11 @@ Respects `max-mini-window-height' (which see)." (defcustom eglot-put-doc-in-help-buffer #'eglot-doc-too-large-for-echo-area "If non-nil, put \"hover\" documentation in separate `*eglot-help*' buffer. -If nil, use whatever `eldoc-message-function' decides (usually -the echo area). If t, use `*eglot-help*' unconditionally. If a -function, it is called with the docstring to display and should a -boolean producing one of the two previous values." +If nil, use whatever `eldoc-message-function' decides, honouring +`eldoc-echo-area-use-multiline-p'. If t, use `*eglot-help*' +unconditionally. If a function, it is called with the docstring +to display and should a boolean producing one of the two previous +values." :type '(choice (const :tag "Never use `*eglot-help*'" nil) (const :tag "Always use `*eglot-help*'" t) (function :tag "Ask a function"))) @@ -2288,15 +2289,22 @@ Buffer is displayed with `display-buffer', which obeys `display-buffer-alist' & friends." :type 'boolean) +(defun eglot--first-line-of-doc (string) + (truncate-string-to-width + (replace-regexp-in-string "\\(.*\\)\n.*" "\\1" string) + (frame-width) nil nil "...")) + (defun eglot--update-doc (string hint) "Put updated documentation STRING where it belongs. -Honours `eglot-put-doc-in-help-buffer'. HINT is used to -potentially rename EGLOT's help buffer. If STRING is nil, the -echo area cleared of any previous documentation." - (cond ((and string - (or (eq t eglot-put-doc-in-help-buffer) - (and eglot-put-doc-in-help-buffer - (funcall eglot-put-doc-in-help-buffer string)))) +HINT is used to potentially rename EGLOT's help buffer. If +STRING is nil, the echo area cleared of any previous +documentation. Honour `eglot-put-doc-in-help-buffer', +`eglot-auto-display-help-buffer' and +`eldoc-echo-area-use-multiline-p'." + (cond ((null string) (eldoc-message nil)) + ((or (eq t eglot-put-doc-in-help-buffer) + (and eglot-put-doc-in-help-buffer + (funcall eglot-put-doc-in-help-buffer string))) (with-current-buffer (eglot--help-buffer) (let ((inhibit-read-only t) (name (format "*eglot-help for %s*" hint))) @@ -2310,19 +2318,14 @@ echo area cleared of any previous documentation." (unless (get-buffer-window (current-buffer)) (eglot--message "%s\n(...truncated. Full help is in `%s')" - (truncate-string-to-width - (replace-regexp-in-string "\\(.*\\)\n.*" "\\1" string) - (frame-width) nil nil "...") + (eglot--first-line-of-doc string) (buffer-name eglot--help-buffer)))) (help-mode)))) (eldoc-echo-area-use-multiline-p + ;; Can't really honour non-t non-nil values if this var (eldoc-message string)) (t - (eldoc-message - (and string - (if (string-match "\n" string) - (substring string (match-end 0)) - string)))))) + (eldoc-message (eglot--first-line-of-doc string))))) (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function." commit c8efef647e2705665a8e3b9de3eedccc209fc981 Author: João Távora Date: Sun May 3 01:00:04 2020 +0100 Fontify markdown source code blocks by default * eglot.el (eglot--format-markup): Set markdown-fontify-code-blocks-natively to t locally. GitHub-reference: per https://github.com/joaotavora/eglot/issues/408 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 596a82d6a8..22a8aefa31 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -72,7 +72,9 @@ (require 'filenotify) (require 'ert) (require 'array) -(defvar company-backends) ; forward-declare, but don't require company +;; forward-declare, but don't require (Emacs 28 doesn't seem to care) +(defvar markdown-fontify-code-blocks-natively) +(defvar company-backends) (defvar company-tooltip-align-annotations) @@ -1149,6 +1151,7 @@ Doubles as an indicator of snippet support." ("plaintext" 'text-mode) (_ major-mode)))))) (with-temp-buffer + (setq-local markdown-fontify-code-blocks-natively t) (insert (string-trim string)) (ignore-errors (delay-mode-hooks (funcall mode))) (font-lock-ensure) commit 5d00eac56495ce6786f374785cab034ff82962fc Author: Felicián Németh Date: Sat Jan 11 19:08:59 2020 +0100 Declare markdown support iff gfm-view-mode installed * eglot.el (eglot-client-capabilities): Support markdown only when gfm-view-mode is installed. GitHub-reference: close https://github.com/joaotavora/eglot/issues/408 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bdc4cd0aaa..596a82d6a8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -527,7 +527,10 @@ treated as in `eglot-dbind'." :json-false)) :contextSupport t) :hover (list :dynamicRegistration :json-false - :contentFormat ["markdown" "plaintext"]) + :contentFormat + (if (fboundp 'gfm-view-mode) + ["markdown" "plaintext"] + ["plaintext"])) :signatureHelp (list :dynamicRegistration :json-false :signatureInformation `(:parameterInformation commit a88cc9210bf9ca31003954a6af043a08c462fc1c Author: Theodor Thornhill Date: Wed Apr 29 10:09:24 2020 +0200 Always string-trim markup Co-authored-by: João Távora * eglot.el: (eglot--format-markup): Factor string trim out so we string-trim for all cases GitHub-reference: close https://github.com/joaotavora/eglot/issues/450 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 436e5bfe44..bdc4cd0aaa 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1139,15 +1139,14 @@ Doubles as an indicator of snippet support." (defun eglot--format-markup (markup) "Format MARKUP according to LSP's spec." (pcase-let ((`(,string ,mode) - (if (stringp markup) (list (string-trim markup) - (intern "gfm-view-mode")) + (if (stringp markup) (list markup 'gfm-view-mode) (list (plist-get markup :value) (pcase (plist-get markup :kind) ("markdown" 'gfm-view-mode) ("plaintext" 'text-mode) (_ major-mode)))))) (with-temp-buffer - (insert string) + (insert (string-trim string)) (ignore-errors (delay-mode-hooks (funcall mode))) (font-lock-ensure) (buffer-string)))) commit d285e0060a721d84a35a06c1e9784cd8d8756c7a Author: João Távora Date: Sun May 3 00:43:00 2020 +0100 Kind of honour eldoc-echo-area-use-multiline-p A reworking of an idea and original implementation by Andrii Kolomoiets . It doesn't honor it completely because the semantics for a non-t, non-nil value are tricky. And we don't always exactly know what the symbol prefix reliably. * eglot.el (eglot--update-doc): Kind of honour eldoc-echo-area-use-multiline-p. GitHub-reference: close https://github.com/joaotavora/eglot/issues/443 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index adfd4e29f8..436e5bfe44 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2288,29 +2288,36 @@ Buffer is displayed with `display-buffer', which obeys Honours `eglot-put-doc-in-help-buffer'. HINT is used to potentially rename EGLOT's help buffer. If STRING is nil, the echo area cleared of any previous documentation." - (if (and string - (or (eq t eglot-put-doc-in-help-buffer) - (and eglot-put-doc-in-help-buffer - (funcall eglot-put-doc-in-help-buffer string)))) - (with-current-buffer (eglot--help-buffer) - (let ((inhibit-read-only t) - (name (format "*eglot-help for %s*" hint))) - (unless (string= name (buffer-name)) - (rename-buffer (format "*eglot-help for %s*" hint)) - (erase-buffer) - (insert string) - (goto-char (point-min))) - (if eglot-auto-display-help-buffer - (display-buffer (current-buffer)) - (unless (get-buffer-window (current-buffer)) - (eglot--message - "%s\n(...truncated. Full help is in `%s')" - (truncate-string-to-width - (replace-regexp-in-string "\\(.*\\)\n.*" "\\1" string) - (frame-width) nil nil "...") - (buffer-name eglot--help-buffer)))) - (help-mode))) - (eldoc-message string))) + (cond ((and string + (or (eq t eglot-put-doc-in-help-buffer) + (and eglot-put-doc-in-help-buffer + (funcall eglot-put-doc-in-help-buffer string)))) + (with-current-buffer (eglot--help-buffer) + (let ((inhibit-read-only t) + (name (format "*eglot-help for %s*" hint))) + (unless (string= name (buffer-name)) + (rename-buffer (format "*eglot-help for %s*" hint)) + (erase-buffer) + (insert string) + (goto-char (point-min))) + (if eglot-auto-display-help-buffer + (display-buffer (current-buffer)) + (unless (get-buffer-window (current-buffer)) + (eglot--message + "%s\n(...truncated. Full help is in `%s')" + (truncate-string-to-width + (replace-regexp-in-string "\\(.*\\)\n.*" "\\1" string) + (frame-width) nil nil "...") + (buffer-name eglot--help-buffer)))) + (help-mode)))) + (eldoc-echo-area-use-multiline-p + (eldoc-message string)) + (t + (eldoc-message + (and string + (if (string-match "\n" string) + (substring string (match-end 0)) + string)))))) (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function." commit e91a40007682491e2f675a6a84151564baaf5c39 Author: Dan Davison Date: Thu Apr 30 20:04:24 2020 -0400 Unbreak eglot--guess-contact for host-and-port case * eglot.el (eglot--guess-contact): Fix bug in (host port) connection case. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/446 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6db0a09f8f..adfd4e29f8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -698,7 +698,8 @@ be guessed." (class (or (and (consp guess) (symbolp (car guess)) (prog1 (car guess) (setq guess (cdr guess)))) 'eglot-lsp-server)) - (program (and (listp guess) (stringp (car guess)) (car guess))) + (program (and (listp guess) + (stringp (car guess)) (stringp (cadr guess)) (car guess))) (base-prompt (and interactive "Enter program to execute (or :): ")) commit 3773b2638f35e39c7040ba86497dcfe62bff1217 Author: Dan Davison Date: Sat Apr 25 22:26:14 2020 -0400 Tweak docstring of eglot-server-programs Co-authored-by: João Távora * eglot.el (eglot-server-programs): Fix typos and phrasing. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index afb7063c64..6db0a09f8f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -120,27 +120,28 @@ of those modes. CONTACT can be: PROGRAM is called with ARGS and is expected to serve LSP requests over the standard input/output channels. -* A list (HOST PORT [TCP-ARGS...]) where HOST is a string and PORT is - na positive integer number for connecting to a server via TCP. +* A list (HOST PORT [TCP-ARGS...]) where HOST is a string and + PORT is a positive integer for connecting to a server via TCP. Remaining ARGS are passed to `open-network-stream' for upgrading the connection with encryption or other capabilities. -* A list (PROGRAM [ARGS...] :autoport [MOREARGS...]), whereby a - combination of the two previous options is used.. First, an +* A list (PROGRAM [ARGS...] :autoport [MOREARGS...]), whereupon a + combination of the two previous options is used. First, an attempt is made to find an available server port, then PROGRAM is launched with ARGS; the `:autoport' keyword substituted for - that number; and MOREARGS. Eglot then attempts to to establish - a TCP connection to that port number on the localhost. + that number; and MOREARGS. Eglot then attempts to establish a + TCP connection to that port number on the localhost. * A cons (CLASS-NAME . INITARGS) where CLASS-NAME is a symbol designating a subclass of `eglot-lsp-server', for representing experimental LSP servers. INITARGS is a keyword-value plist - used to initialize CLASS-NAME, or a plain list interpreted as - the previous descriptions of CONTACT, in which case it is - converted to produce a plist with a suitable :PROCESS initarg - to CLASS-NAME. The class `eglot-lsp-server' descends - `jsonrpc-process-connection', which you should see for the - semantics of the mandatory :PROCESS argument. + used to initialize the object of CLASS-NAME, or a plain list + interpreted as the previous descriptions of CONTACT. In the + latter case that plain list is used to produce a plist with a + suitable :PROCESS initarg to CLASS-NAME. The class + `eglot-lsp-server' descends from `jsonrpc-process-connection', + which you should see for the semantics of the mandatory + :PROCESS argument. * A function of a single argument producing any of the above values for CONTACT. The argument's value is non-nil if the commit ed162088f3036cbb9e964cc2775a2e12c0b6e9c4 Author: Ingo Lohmar Date: Fri Sep 20 18:39:23 2019 +0200 Support hierarchical documentsymbol in eglot-imenu A reworking of an original implementation by Ingo Lohmar * eglot.el (eglot-client-capabilities, defvar): Add DocumentSymbol. (eglot-client-capabilities): Add :hierarchicalDocumentSymbolSupport. (eglot--parse-DocumentSymbol): Remove. (eglot-imenu): Rewrite. * NEWS.md (1.7): Mention new feature GitHub-reference: close https://github.com/joaotavora/eglot/issues/303 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 42fca9be52..afb7063c64 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -56,6 +56,7 @@ ;;; Code: (require 'json) +(require 'imenu) (require 'cl-lib) (require 'project) (require 'url-parse) @@ -255,7 +256,12 @@ let the buffer grow forever." (ShowMessageRequestParams (:type :message) (:actions)) (SignatureHelp (:signatures) (:activeSignature :activeParameter)) (SignatureInformation (:label) (:documentation :parameters)) - (SymbolInformation (:name :kind :location) (:deprecated :containerName)) + (SymbolInformation (:name :kind :location) + (:deprecated :containerName)) + (DocumentSymbol (:name :range :selectionRange :kind) + ;; `:containerName' isn't really allowed , but + ;; it simplifies the impl of `eglot-imenu'. + (:detail :deprecated :children :containerName)) (TextDocumentEdit (:textDocument :edits) ()) (TextEdit (:range :newText)) (VersionedTextDocumentIdentifier (:uri :version) ()) @@ -532,6 +538,7 @@ treated as in `eglot-dbind'." :typeDefinition `(:dynamicRegistration :json-false) :documentSymbol (list :dynamicRegistration :json-false + :hierarchicalDocumentSymbolSupport t :symbolKind `(:valueSet [,@(mapcar #'car eglot--symbol-kind-names)])) @@ -2361,36 +2368,48 @@ echo area cleared of any previous documentation." (defun eglot-imenu () "EGLOT's `imenu-create-index-function'." - (let ((entries - (and - (eglot--server-capable :documentSymbolProvider) - (mapcar - (eglot--lambda - ((SymbolInformation) name kind location containerName) - (cons (propertize - name - :kind (alist-get kind eglot--symbol-kind-names - "Unknown") - :containerName (and (stringp containerName) - (not (string-empty-p containerName)) - containerName)) - (eglot--lsp-position-to-point - (plist-get (plist-get location :range) :start)))) - (jsonrpc-request (eglot--current-server-or-lose) - :textDocument/documentSymbol - `(:textDocument ,(eglot--TextDocumentIdentifier))))))) + (cl-labels + ((visit (_name one-obj-array) + (imenu-default-goto-function + nil (car (eglot--range-region + (eglot--dcase (aref one-obj-array 0) + (((SymbolInformation) location) + (plist-get location :range)) + (((DocumentSymbol) selectionRange) + selectionRange)))))) + (unfurl (obj) + (eglot--dcase obj + (((SymbolInformation)) (list obj)) + (((DocumentSymbol) name children) + (cons obj + (mapcar + (lambda (c) + (plist-put + c :containerName + (let ((existing (plist-get c :containerName))) + (if existing (format "%s::%s" name existing) + name)))) + (mapcan #'unfurl children))))))) (mapcar - (pcase-lambda (`(,kind . ,syms)) - (let ((syms-by-scope (seq-group-by - (lambda (e) - (get-text-property 0 :containerName (car e))) - syms))) - (cons kind (cl-loop for (scope . elems) in syms-by-scope - append (if scope - (list (cons scope elems)) - elems))))) - (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) - entries)))) + (pcase-lambda (`(,kind . ,objs)) + (cons + (alist-get kind eglot--symbol-kind-names "Unknown") + (mapcan (pcase-lambda (`(,container . ,objs)) + (let ((elems (mapcar (lambda (obj) + (list (plist-get obj :name) + `[,obj] ;; trick + #'visit)) + objs))) + (if container (list (cons container elems)) elems))) + (seq-group-by + (lambda (e) (plist-get e :containerName)) objs)))) + (seq-group-by + (lambda (obj) (plist-get obj :kind)) + (mapcan #'unfurl + (jsonrpc-request (eglot--current-server-or-lose) + :textDocument/documentSymbol + `(:textDocument + ,(eglot--TextDocumentIdentifier)))))))) (defun eglot--apply-text-edits (edits &optional version) "Apply EDITS for current buffer if at VERSION, or if it's nil." commit bbf8a0d0f65aecdd617ea2d07b7c9e7f4053a79c Author: João Távora Date: Sat May 2 10:30:28 2020 +0100 Also check types when destructuring lsp objects The problem in this issue is that the disambiguation between Command and CodeAction objects can only be performed by checking the types of the keys involved. So we added that to the spec and check it at runtime. * eglot.el (eglot--lsp-interface-alist): Add types to Command. Tweak docstring. (eglot--check-object): Renamed from eglot--call-with-interface. (eglot--ensure-type): New helper. (eglot--interface): New helper. (eglot--check-dspec): Renamed from eglot--check-interface. (eglot--dbind): Simplify. (eglot-code-actions): Adjust indentation. * eglot-tests.el (eglot-dcase-issue-452): New test. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/452 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c485b4e2dd..42fca9be52 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -231,7 +231,7 @@ let the buffer grow forever." `( (CodeAction (:title) (:kind :diagnostics :edit :command)) (ConfigurationItem () (:scopeUri :section)) - (Command (:title :command) (:arguments)) + (Command ((:title . string) (:command . string)) (:arguments)) (CompletionItem (:label) (:kind :detail :documentation :deprecated :preselect :sortText :filterText :insertText :insertTextFormat @@ -265,13 +265,15 @@ let the buffer grow forever." INTERFACE-NAME is a symbol designated by the spec as \"interface\". INTERFACE is a list (REQUIRED OPTIONAL) where -REQUIRED and OPTIONAL are lists of keyword symbols designating -field names that must be, or may be, respectively, present in a -message adhering to that interface. +REQUIRED and OPTIONAL are lists of KEYWORD designating field +names that must be, or may be, respectively, present in a message +adhering to that interface. KEY can be a keyword or a cons (SYM +TYPE), where type is used by `cl-typep' to check types at +runtime. Here's what an element of this alist might look like: - (CreateFile . ((:kind :uri) (:options)))")) + (Command ((:title . string) (:command . string)) (:arguments))")) (eval-and-compile (defvar eglot-strict-mode (if load-file-name '() @@ -308,46 +310,69 @@ on unknown notifications and errors on unknown requests. (defun eglot--plist-keys (plist) (cl-loop for (k _v) on plist by #'cddr collect k)) -(defun eglot--call-with-interface (interface object fn) - "Call FN, checking that OBJECT conforms to INTERFACE." - (when-let ((missing (and (memq 'enforce-required-keys eglot-strict-mode) - (cl-set-difference (car (cdr interface)) - (eglot--plist-keys object))))) - (eglot--error "A `%s' must have %s" (car interface) missing)) - (when-let ((excess (and (memq 'disallow-non-standard-keys eglot-strict-mode) - (cl-set-difference - (eglot--plist-keys object) - (append (car (cdr interface)) (cadr (cdr interface))))))) - (eglot--error "A `%s' mustn't have %s" (car interface) excess)) - (funcall fn)) +(cl-defun eglot--check-object (interface-name + object + &optional + (enforce-required t) + (disallow-non-standard t) + (check-types t)) + "Check that OBJECT conforms to INTERFACE. Error otherwise." + (cl-destructuring-bind + (&key types required-keys optional-keys &allow-other-keys) + (eglot--interface interface-name) + (when-let ((missing (and enforce-required + (cl-set-difference required-keys + (eglot--plist-keys object))))) + (eglot--error "A `%s' must have %s" interface-name missing)) + (when-let ((excess (and disallow-non-standard + (cl-set-difference + (eglot--plist-keys object) + (append required-keys optional-keys))))) + (eglot--error "A `%s' mustn't have %s" interface-name excess)) + (when check-types + (cl-loop + for (k v) on object by #'cddr + for type = (or (cdr (assoc k types)) t) ;; FIXME: enforce nil type? + unless (cl-typep v type) + do (eglot--error "A `%s' must have a %s as %s, but has %s" + interface-name ))) + t)) (eval-and-compile (defun eglot--keywordize-vars (vars) (mapcar (lambda (var) (intern (format ":%s" var))) vars)) - (defun eglot--check-interface (interface-name vars) - (let ((interface - (assoc interface-name eglot--lsp-interface-alist))) - (cond (interface + (defun eglot--ensure-type (k) (if (consp k) k (cons k t))) + + (defun eglot--interface (interface-name) + (let* ((interface (assoc interface-name eglot--lsp-interface-alist)) + (required (mapcar #'eglot--ensure-type (car (cdr interface)))) + (optional (mapcar #'eglot--ensure-type (cadr (cdr interface))))) + (list :types (append required optional) + :required-keys (mapcar #'car required) + :optional-keys (mapcar #'car optional)))) + + (defun eglot--check-dspec (interface-name dspec) + "Check if variables in DSPEC " + (cl-destructuring-bind (&key required-keys optional-keys &allow-other-keys) + (eglot--interface interface-name) + (cond ((or required-keys optional-keys) (let ((too-many (and (memq 'disallow-non-standard-keys eglot-strict-mode) (cl-set-difference - (eglot--keywordize-vars vars) - (append (car (cdr interface)) - (cadr (cdr interface)))))) + (eglot--keywordize-vars dspec) + (append required-keys optional-keys)))) (ignored-required (and (memq 'enforce-required-keys eglot-strict-mode) (cl-set-difference - (car (cdr interface)) - (eglot--keywordize-vars vars)))) + required-keys (eglot--keywordize-vars dspec)))) (missing-out (and (memq 'enforce-optional-keys eglot-strict-mode) (cl-set-difference - (cadr (cdr interface)) - (eglot--keywordize-vars vars))))) + optional-keys (eglot--keywordize-vars dspec))))) (when too-many (byte-compile-warn "Destructuring for %s has extraneous %s" interface-name too-many)) @@ -361,7 +386,7 @@ on unknown notifications and errors on unknown requests. (byte-compile-warn "Unknown LSP interface %s" interface-name)))))) (cl-defmacro eglot--dbind (vars object &body body) - "Destructure OBJECT of binding VARS in BODY. + "Destructure OBJECT, binding VARS in BODY. VARS is ([(INTERFACE)] SYMS...) Honour `eglot-strict-mode'." (declare (indent 2) (debug (sexp sexp &rest form))) @@ -370,13 +395,14 @@ Honour `eglot-strict-mode'." (object-once (make-symbol "object-once")) (fn-once (make-symbol "fn-once"))) (cond (interface-name - (eglot--check-interface interface-name vars) + (eglot--check-dspec interface-name vars) `(let ((,object-once ,object)) (cl-destructuring-bind (&key ,@vars &allow-other-keys) ,object-once - (eglot--call-with-interface (assoc ',interface-name - eglot--lsp-interface-alist) - ,object-once (lambda () - ,@body))))) + (eglot--check-object ',interface-name ,object-once + (memq 'enforce-required-keys eglot-strict-mode) + (memq 'disallow-non-standard-keys eglot-strict-mode) + (memq 'check-types eglot-strict-mode)) + ,@body))) (t `(let ((,object-once ,object) (,fn-once (lambda (,@vars) ,@body))) @@ -409,20 +435,12 @@ treated as in `eglot-dbind'." (car (pop vars))) for condition = (cond (interface-name - (eglot--check-interface interface-name vars) + (eglot--check-dspec interface-name vars) ;; In this mode, in runtime, we assume ;; `eglot-strict-mode' is fully on, otherwise we ;; can't disambiguate between certain types. - `(let* ((interface - (or (assoc ',interface-name eglot--lsp-interface-alist) - (eglot--error "Unknown LSP interface %s" - ',interface-name))) - (object-keys (eglot--plist-keys ,obj-once)) - (required-keys (car (cdr interface)))) - (and (null (cl-set-difference required-keys object-keys)) - (null (cl-set-difference - (cl-set-difference object-keys required-keys) - (cadr (cdr interface))))))) + `(ignore-errors + (eglot--check-object ',interface-name ,obj-once))) (t ;; In this interface-less mode we don't check ;; `eglot-strict-mode' at all: just check that the object @@ -435,7 +453,7 @@ treated as in `eglot-dbind'." ,obj-once ,@body))) (t - (eglot--error "%s didn't match any of %s" + (eglot--error "%S didn't match any of %S" ,obj-once ',(mapcar #'car clauses))))))) @@ -2499,12 +2517,12 @@ echo area cleared of any previous documentation." (action (if (listp last-nonmenu-event) (x-popup-menu last-nonmenu-event menu) (cdr (assoc (completing-read "[eglot] Pick an action: " - menu-items nil t - nil nil (car menu-items)) + menu-items nil t + nil nil (car menu-items)) menu-items))))) (eglot--dcase action - (((Command) command arguments) - (eglot-execute-command server (intern command) arguments)) + (((Command) command arguments) + (eglot-execute-command server (intern command) arguments)) (((CodeAction) edit command) (when edit (eglot--apply-workspace-edit edit)) (when command commit 60914d2ca402a26f099ab27564611bb4d7e22e67 Author: Andrii Kolomoiets Date: Thu Apr 23 10:44:12 2020 +0300 Hide eldoc-message on empty hover info Co-authored-by: João Távora * eglot.el (eglot-eldoc-function): Pass nil to eglot--update-doc on empty hover info. (eglot--update-doc): Skip update eglot help buffer if string is nil. GitHub-reference: close https://github.com/joaotavora/eglot/issues/439 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13571d4315..c485b4e2dd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2259,10 +2259,12 @@ Buffer is displayed with `display-buffer', which obeys (defun eglot--update-doc (string hint) "Put updated documentation STRING where it belongs. Honours `eglot-put-doc-in-help-buffer'. HINT is used to -potentially rename EGLOT's help buffer." - (if (or (eq t eglot-put-doc-in-help-buffer) - (and eglot-put-doc-in-help-buffer - (funcall eglot-put-doc-in-help-buffer string))) +potentially rename EGLOT's help buffer. If STRING is nil, the +echo area cleared of any previous documentation." + (if (and string + (or (eq t eglot-put-doc-in-help-buffer) + (and eglot-put-doc-in-help-buffer + (funcall eglot-put-doc-in-help-buffer string)))) (with-current-buffer (eglot--help-buffer) (let ((inhibit-read-only t) (name (format "*eglot-help for %s*" hint))) @@ -2314,10 +2316,10 @@ potentially rename EGLOT's help buffer." :success-fn (eglot--lambda ((Hover) contents range) (unless sig-showing (when-buffer-window - (when-let (info (and (not (seq-empty-p contents)) - (eglot--hover-info contents - range))) - (eglot--update-doc info thing-at-point))))) + (eglot--update-doc (and (not (seq-empty-p contents)) + (eglot--hover-info contents + range)) + thing-at-point)))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) (jsonrpc-async-request commit 047f99896c127008287e87583750ac4b159c0696 Author: Tobias Rittweiler Date: Sun Apr 26 17:30:11 2020 +0200 Tests: print contents of *eglot ...* buffers in batch mode. Useful for the CI on github. To be able to see more of the context of a failure. * eglot.el (eglot-server-initialized-hook): Changed semantics. Now called when an instance of `eglot-lsp-server' is created as part of the "connect to server" flow. Previously, there was no difference between this hook and `eglot-connect-hook' which continues to be run once a connection was successfully established. The `eglot-server-initialized-hook' will now capture ALL server instances including those that failed to be started. This change was necessary to make the test suite be able to dump the output of processes that fail to start when running the test suite in batch mode ("make check" and the CI.) In PR https://github.com/joaotavora/eglot/issues/448 it was decided that it is ok to change the semantics of this hook rather than introducing a new hook. (eglot--connect): Change place of where the hook is run. (eglot-connect-hook): Initialized now with `eglot-signal-didChangeConfiguration' which was kept in `eglot-server-initialized-hook' before. * eglot-tests.el (eglot--call-with-fixture): Use `eglot-server-initialized-hook' rather than `eglot-connect-hook'. And dump the contents of the *EGLOT ...* buffers when run in `noninteractive' (i.e. batch) mode. (eglot--cleanup-after-test): New auxiliary function. Extracted verbatim out of `eglot--call-with-fixture` in order to lower the latter's LOC. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c7c4551812..13571d4315 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -790,11 +790,19 @@ INTERACTIVE is t if called interactively." (interactive (list (eglot--current-server-or-lose))) (jsonrpc-forget-pending-continuations server)) -(defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") +(defvar eglot-connect-hook + '(eglot-signal-didChangeConfiguration) + "Hook run after connecting in `eglot--connect'.") (defvar eglot-server-initialized-hook - '(eglot-signal-didChangeConfiguration) - "Hook run after server is successfully initialized. + '() + "Hook run after a `eglot-lsp-server' instance is created. + +That is before a connection was established. Use +`eglot-connect-hook' to hook into when a connection was +successfully established and the server on the other side has +received the initializing configuration. + Each function is passed the server as an argument") (defun eglot--connect (managed-major-mode project class contact) @@ -851,6 +859,7 @@ This docstring appeases checkdoc, that's all." (setf (eglot--project-nickname server) nickname) (setf (eglot--major-mode server) managed-major-mode) (setf (eglot--inferior-process server) autostart-inferior-process) + (run-hook-with-args 'eglot-server-initialized-hook server) ;; Now start the handshake. To honour `eglot-sync-connect' ;; maybe-sync-maybe-async semantics we use `jsonrpc-async-request' ;; and mimic most of `jsonrpc-request'. @@ -898,8 +907,7 @@ This docstring appeases checkdoc, that's all." (let ((default-directory (car (project-roots project))) (major-mode managed-major-mode)) (hack-dir-local-variables-non-file-buffer) - (run-hook-with-args 'eglot-connect-hook server) - (run-hook-with-args 'eglot-server-initialized-hook server)) + (run-hook-with-args 'eglot-connect-hook server)) (eglot--message "Connected! Server `%s' now managing `%s' buffers \ in project `%s'." commit 4d3cf3330671a3df4433d1f35efda779ebb72460 Author: Trevor Murphy Date: Fri Apr 24 11:52:01 2020 -0700 Create match xrefs when possible "Match xrefs" are created with `xref-make-match' instead of `xref-make'. Match xrefs support `xref-query-replace-in-results' from the results buffer. * eglot.el (eglot--xref-make-match): Calculate xref match length from the eglot range. GitHub-reference: close https://github.com/joaotavora/eglot/issues/435 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3ea839115c..c7c4551812 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1833,8 +1833,8 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." (maphash (lambda (_uri buf) (kill-buffer buf)) eglot--temp-location-buffers) (clrhash eglot--temp-location-buffers)))) -(defun eglot--xref-make (name uri range) - "Like `xref-make' but with LSP's NAME, URI and RANGE. +(defun eglot--xref-make-match (name uri range) + "Like `xref-make-match' but with LSP's NAME, URI and RANGE. Try to visit the target file for a richer summary line." (pcase-let* ((file (eglot--uri-to-path uri)) @@ -1849,8 +1849,9 @@ Try to visit the target file for a richer summary line." (hi-end (- (min (point-at-eol) end) bol))) (add-face-text-property hi-beg hi-end 'highlight t substring) - (list substring (1+ (current-line)) (eglot-current-column)))))) - (`(,summary ,line ,column) + (list substring (1+ (current-line)) (eglot-current-column) + (- end beg)))))) + (`(,summary ,line ,column ,length) (cond (visiting (with-current-buffer visiting (funcall collect))) ((file-readable-p file) (with-current-buffer @@ -1859,9 +1860,12 @@ Try to visit the target file for a richer summary line." (insert-file-contents file) (funcall collect))) (t ;; fall back to the "dumb strategy" - (let ((start (cl-getf range :start))) - (list name (1+ (cl-getf start :line)) (cl-getf start :character))))))) - (xref-make summary (xref-make-file-location file line column)))) + (let* ((start (cl-getf range :start)) + (line (1+ (cl-getf start :line))) + (start-pos (cl-getf start :character)) + (end-pos (cl-getf (cl-getf range :end) :character))) + (list name line start-pos (- end-pos start-pos))))))) + (xref-make-match summary (xref-make-file-location file line column) length))) (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) (eglot--error "cannot (yet) provide reliable completion table for LSP symbols")) @@ -1892,7 +1896,7 @@ Try to visit the target file for a richer summary line." (eglot--collecting-xrefs (collect) (mapc (eglot--lambda ((Location) uri range) - (collect (eglot--xref-make (symbol-at-point) uri range))) + (collect (eglot--xref-make-match (symbol-at-point) uri range))) (if (vectorp response) response (list response)))))) (cl-defun eglot--lsp-xref-helper (method &key extra-params capability ) @@ -1935,7 +1939,7 @@ Try to visit the target file for a richer summary line." (mapc (eglot--lambda ((SymbolInformation) name location) (eglot--dbind ((Location) uri range) location - (collect (eglot--xref-make name uri range)))) + (collect (eglot--xref-make-match name uri range)))) (jsonrpc-request (eglot--current-server-or-lose) :workspace/symbol `(:query ,pattern)))))) commit 23878a9404fcefcc3ffc32bcbf9456a6c2cf06c2 Author: João Távora Date: Fri Apr 24 16:33:36 2020 +0100 * eglot.el (eglot-put-doc-in-help-buffer): tiny docstring fix. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ce4f19c65c..3ea839115c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2231,7 +2231,7 @@ Respects `max-mini-window-height' (which see)." #'eglot-doc-too-large-for-echo-area "If non-nil, put \"hover\" documentation in separate `*eglot-help*' buffer. If nil, use whatever `eldoc-message-function' decides (usually -the echo area). If t, use `*eglot-help; unconditionally. If a +the echo area). If t, use `*eglot-help*' unconditionally. If a function, it is called with the docstring to display and should a boolean producing one of the two previous values." :type '(choice (const :tag "Never use `*eglot-help*'" nil) commit 81385edb7132461500f229919c074e329f42b009 Author: João Távora Date: Fri Apr 24 15:39:16 2020 +0100 Don't reupdate help buffer if already rendered * eglot.el (eglot--update-doc): Don't reupdate if doc buffer already exists. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/445 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6d6f91f55a..ce4f19c65c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2252,11 +2252,13 @@ potentially rename EGLOT's help buffer." (and eglot-put-doc-in-help-buffer (funcall eglot-put-doc-in-help-buffer string))) (with-current-buffer (eglot--help-buffer) - (rename-buffer (format "*eglot-help for %s*" hint)) - (let ((inhibit-read-only t)) - (erase-buffer) - (insert string) - (goto-char (point-min)) + (let ((inhibit-read-only t) + (name (format "*eglot-help for %s*" hint))) + (unless (string= name (buffer-name)) + (rename-buffer (format "*eglot-help for %s*" hint)) + (erase-buffer) + (insert string) + (goto-char (point-min))) (if eglot-auto-display-help-buffer (display-buffer (current-buffer)) (unless (get-buffer-window (current-buffer)) commit da888370b4d61025cb9d3d5dbd591b9d04a6f5dd Author: Andrii Kolomoiets Date: Thu Apr 23 23:41:22 2020 +0300 Use text-mode for plaintext markup * eglot.el (eglot--format-markup): Use text-mode for plaintext markup. GitHub-reference: close https://github.com/joaotavora/eglot/issues/444 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13ee30723b..6d6f91f55a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1109,6 +1109,7 @@ Doubles as an indicator of snippet support." (list (plist-get markup :value) (pcase (plist-get markup :kind) ("markdown" 'gfm-view-mode) + ("plaintext" 'text-mode) (_ major-mode)))))) (with-temp-buffer (insert string) commit 2a0e0433cd15d3a0b9de220b141c2c12b549c67b Author: Felicián Németh Date: Tue Jan 21 19:35:34 2020 +0100 Update dependencies and copyright years * eglot.el: Update dependencies and copyright years. GitHub-reference: close https://github.com/joaotavora/eglot/issues/413 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 58980a1147..13ee30723b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1,13 +1,13 @@ ;;; eglot.el --- Client for Language Server Protocol (LSP) servers -*- lexical-binding: t; -*- -;; Copyright (C) 2018 Free Software Foundation, Inc. +;; Copyright (C) 2018-2020 Free Software Foundation, Inc. ;; Version: 1.6 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.7") (flymake "1.0.5")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.9") (flymake "1.0.8")) ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by commit 6bfa6e2532c4d81fa717ff7cb3b9a2a1fdce2f19 Author: Theodor Thornhill Date: Fri Jan 3 21:42:08 2020 +0100 Simplify a bit of code Co-authored-by: João Távora * eglot.el (eglot-move-to-lsp-abiding-column): use already existing function to refer to lsp-abiding-column GitHub-reference: close https://github.com/joaotavora/eglot/issues/397 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index eafe22e224..58980a1147 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1012,9 +1012,10 @@ fully LSP-compliant servers, this should be set to `eglot-lsp-abiding-column' (the default), and `eglot-current-column' for all others.") -(defun eglot-lsp-abiding-column () - "Calculate current COLUMN as defined by the LSP spec." - (/ (- (length (encode-coding-region (line-beginning-position) +(defun eglot-lsp-abiding-column (&optional lbp) + "Calculate current COLUMN as defined by the LSP spec. +LBP defaults to `line-beginning-position'." + (/ (- (length (encode-coding-region (or lbp (line-beginning-position)) (point) 'utf-16 t)) 2) 2)) @@ -1057,9 +1058,7 @@ be set to `eglot-move-to-lsp-abiding-column' (the default), and (narrow-to-region lbp (line-end-position)) (move-to-column column) for diff = (- column - (/ (- (length (encode-coding-region lbp (point) 'utf-16 t)) - 2) - 2)) + (eglot-lsp-abiding-column lbp)) until (zerop diff) do (condition-case eob-err (forward-char (/ (if (> diff 0) (1+ diff) (1- diff)) 2)) commit 1c2dc32a6e5a7266371535ab8b71f730cfc1f6f2 Author: João Távora Date: Thu Apr 16 10:31:01 2020 +0100 * eglot.el (version): bump to 1.6 * NEWS.md: Bump to 1.6 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5d363ed303..eafe22e224 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 1.5 +;; Version: 1.6 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot commit f901fa91bc61454363a07989e458a3a05bca080f Author: Felicián Németh Date: Sun Jan 19 11:13:20 2020 +0100 Abide by lsp when reporting and moving to columns * eglot.el (eglot-current-column-function): Set to eglot-lsp-abiding-column. (eglot-move-to-column-function): Set to eglot-move-to-lsp-abiding-column. * NEWS.md: Log the change here as well. Co-authored-by: João Távora GitHub-reference: fix https://github.com/joaotavora/eglot/issues/361 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2a50611d36..5d363ed303 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1002,15 +1002,15 @@ CONNECT-ARGS are passed as additional arguments to (defun eglot-current-column () (- (point) (point-at-bol))) -(defvar eglot-current-column-function #'eglot-current-column +(defvar eglot-current-column-function #'eglot-lsp-abiding-column "Function to calculate the current column. This is the inverse operation of `eglot-move-to-column-function' (which see). It is a function of no arguments returning a column number. For buffers managed by fully LSP-compliant servers, this should be set to -`eglot-lsp-abiding-column', and `eglot-current-column' (the default) -for all others.") +`eglot-lsp-abiding-column' (the default), and +`eglot-current-column' for all others.") (defun eglot-lsp-abiding-column () "Calculate current COLUMN as defined by the LSP spec." @@ -1026,7 +1026,7 @@ for all others.") :character (progn (when pos (goto-char pos)) (funcall eglot-current-column-function))))) -(defvar eglot-move-to-column-function #'eglot-move-to-column +(defvar eglot-move-to-column-function #'eglot-move-to-lsp-abiding-column "Function to move to a column reported by the LSP server. According to the standard, LSP column/character offsets are based @@ -1036,8 +1036,8 @@ where X is a multi-byte character, it actually means `b', not `c'. However, many servers don't follow the spec this closely. For buffers managed by fully LSP-compliant servers, this should -be set to `eglot-move-to-lsp-abiding-column', and -`eglot-move-to-column' (the default) for all others.") +be set to `eglot-move-to-lsp-abiding-column' (the default), and +`eglot-move-to-column' for all others.") (defun eglot-move-to-column (column) "Move to COLUMN without closely following the LSP spec." commit c93c90842a96f8695ec81f1e34ff4ca951737f4d Author: João Távora Date: Sun Jan 19 11:02:55 2020 +0100 Fix eglot-move-to-lsp-abiding-column () Ensure conformance with the this part of the specification: "if the character value is greater than the line length it defaults back to the line length." * eglot.el: (eglot-move-to-lsp-abiding-column): Don't move beyond line-end. GitHub-reference: https://github.com/joaotavora/eglot/issues/361 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3149cd5cc0..2a50611d36 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1050,15 +1050,20 @@ be set to `eglot-move-to-lsp-abiding-column', and (defun eglot-move-to-lsp-abiding-column (column) "Move to COLUMN abiding by the LSP spec." - (cl-loop - initially (move-to-column column) - with lbp = (line-beginning-position) - for diff = (- column - (/ (- (length (encode-coding-region lbp (point) 'utf-16 t)) - 2) - 2)) - until (zerop diff) - do (forward-char (/ (if (> diff 0) (1+ diff) (1- diff)) 2)))) + (save-restriction + (cl-loop + with lbp = (line-beginning-position) + initially + (narrow-to-region lbp (line-end-position)) + (move-to-column column) + for diff = (- column + (/ (- (length (encode-coding-region lbp (point) 'utf-16 t)) + 2) + 2)) + until (zerop diff) + do (condition-case eob-err + (forward-char (/ (if (> diff 0) (1+ diff) (1- diff)) 2)) + (end-of-buffer (cl-return eob-err)))))) (defun eglot--lsp-position-to-point (pos-plist &optional marker) "Convert LSP position POS-PLIST to Emacs point. commit 6b59dcf652301a98d96e539509fad26a873db6f8 Author: Felicián Németh Date: Thu Apr 16 09:38:31 2020 +0200 Send shutdown and exit messages without arguments Fix regression introduced in 70e6157b (https://github.com/joaotavora/eglot/issues/315). According to the LSP specification the exit notification and the shutdown request shouldn't have arguments ("params: void"). Note that jsonrpc.el send nil as null on the wire. * eglot.el (eglot-shutdown): Change back the arguments of :shutdown and :exit to nil. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/430 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fe06e0c9ad..3149cd5cc0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -598,9 +598,8 @@ SERVER. ." (unwind-protect (progn (setf (eglot--shutdown-requested server) t) - (jsonrpc-request server :shutdown eglot--{} - :timeout (or timeout 1.5)) - (jsonrpc-notify server :exit eglot--{})) + (jsonrpc-request server :shutdown nil :timeout (or timeout 1.5)) + (jsonrpc-notify server :exit nil)) ;; Now ask jsonrpc.el to shut down the server. (jsonrpc-shutdown server (not preserve-buffers)) (unless preserve-buffers (kill-buffer (jsonrpc-events-buffer server))))) commit 595ca62d1c7aec15e364011c99ff986ea2e930ae Author: Felicián Németh Date: Fri Mar 20 09:42:44 2020 +0100 Ignore empty hover info This just mimics a similar check in `eglot-help-at-point'. * eglot.el (eglot-eldoc-function): Check emptiness of `contents' more carefully. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/425 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8d1d2d7157..fe06e0c9ad 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2296,7 +2296,7 @@ potentially rename EGLOT's help buffer." :success-fn (eglot--lambda ((Hover) contents range) (unless sig-showing (when-buffer-window - (when-let (info (and contents + (when-let (info (and (not (seq-empty-p contents)) (eglot--hover-info contents range))) (eglot--update-doc info thing-at-point))))) commit e81e6a24adf9eaaab0e31c4056ae7bef7a94db72 Author: Felicián Németh Date: Fri Nov 22 18:35:01 2019 +0100 Make a public reader for project-nickname Close https://github.com/joaotavora/eglot/issues/399. * eglot.el (eglot-lsp-server): Add a public reader for project-nickname as eglot-project-nickname. (eglot--connect, eglot--read-server, eglot--mode-line-format): Use the public variant. GitHub-reference: per https://github.com/joaotavora/eglot/issues/354 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4c65af0a24..8d1d2d7157 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -536,7 +536,8 @@ treated as in `eglot-dbind'." (defclass eglot-lsp-server (jsonrpc-process-connection) ((project-nickname :documentation "Short nickname for the associated project." - :accessor eglot--project-nickname) + :accessor eglot--project-nickname + :reader eglot-project-nickname) (major-mode :documentation "Major mode symbol." :accessor eglot--major-mode) @@ -906,7 +907,7 @@ in project `%s'." (or (plist-get serverInfo :name) (jsonrpc-name server)) managed-major-mode - (eglot--project-nickname server)) + (eglot-project-nickname server)) (when tag (throw tag t)))) :timeout eglot-connect-timeout :error-fn (eglot--lambda ((ResponseError) code message) @@ -1172,7 +1173,7 @@ and just return it. PROMPT shouldn't end with a question mark." being hash-values of eglot--servers-by-project append servers)) (name (lambda (srv) - (format "%s/%s" (eglot--project-nickname srv) + (format "%s/%s" (eglot-project-nickname srv) (eglot--major-mode srv))))) (cond ((null servers) (eglot--error "No servers!")) @@ -1388,7 +1389,7 @@ Uses THING, FACE, DEFS and PREPEND." (defun eglot--mode-line-format () "Compose the EGLOT's mode-line." (pcase-let* ((server (eglot-current-server)) - (nick (and server (eglot--project-nickname server))) + (nick (and server (eglot-project-nickname server))) (pending (and server (hash-table-count (jsonrpc--request-continuations server)))) (`(,_id ,doing ,done-p ,_detail) (and server (eglot--spinner server))) commit fbc29353667b88c208b2796a4e1e120edb708673 Author: Felicián Németh Date: Fri Nov 22 16:55:04 2019 +0100 Add public hook eglot-managed-mode-hook Per https://github.com/joaotavora/eglot/issues/354. * eglot.el (eglot-managed-p): New function. (eglot--managed-mode-hook): Obsolete it. (eglot-managed-mode-hook): New hook variable. (eglot--managed-mode): Run the new hook. * README.md (Customization): Mention the new hook. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/182 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7f506c8c9f..4c65af0a24 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1234,6 +1234,17 @@ For example, to keep your Company customization use (defvar-local eglot--cached-server nil "A cached reference to the current EGLOT server.") +(defun eglot-managed-p () + "Tell if current buffer is managed by EGLOT." + eglot--managed-mode) + +(make-obsolete-variable + 'eglot--managed-mode-hook 'eglot-managed-mode-hook "1.6") + +(defvar eglot-managed-mode-hook nil + "A hook run by EGLOT after it started/stopped managing a buffer. +Use `eglot-managed-p' to determine if current buffer is managed.") + (define-minor-mode eglot--managed-mode "Mode for source buffers managed by some EGLOT project." nil nil eglot-mode-map @@ -1289,7 +1300,9 @@ For example, to keep your Company customization use (delq (current-buffer) (eglot--managed-buffers server))) (when (and eglot-autoshutdown (null (eglot--managed-buffers server))) - (eglot-shutdown server))))))) + (eglot-shutdown server)))))) + ;; Note: the public hook runs before the internal eglot--managed-mode-hook. + (run-hooks 'eglot-managed-mode-hook)) (defun eglot--managed-mode-off () "Turn off `eglot--managed-mode' unconditionally." commit f6a72c5541f3e1564476109a06f3899f5a663882 Author: Vladimir Panteleev Date: Tue Nov 19 02:04:43 2019 +0000 Introduce and use eglot--{}, the empty json object * eglot.el (Constants): Add eglot--{}. (eglot-shutdown, eglot--connect): Use it. Copyright-paperwork-exempt: yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/315 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1bdca12642..7f506c8c9f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -220,6 +220,8 @@ let the buffer grow forever." (13 . "Enum") (14 . "Keyword") (15 . "Snippet") (16 . "Color") (17 . "File") (18 . "Reference"))) +(defconst eglot--{} (make-hash-table) "The empty JSON object.") + ;;; Message verification helpers @@ -595,9 +597,9 @@ SERVER. ." (unwind-protect (progn (setf (eglot--shutdown-requested server) t) - (jsonrpc-request server :shutdown (make-hash-table) + (jsonrpc-request server :shutdown eglot--{} :timeout (or timeout 1.5)) - (jsonrpc-notify server :exit (make-hash-table))) + (jsonrpc-notify server :exit eglot--{})) ;; Now ask jsonrpc.el to shut down the server. (jsonrpc-shutdown server (not preserve-buffers)) (unless preserve-buffers (kill-buffer (jsonrpc-events-buffer server))))) @@ -874,7 +876,7 @@ This docstring appeases checkdoc, that's all." (gethash project eglot--servers-by-project)) (setf (eglot--capabilities server) capabilities) (setf (eglot--server-info server) serverInfo) - (jsonrpc-notify server :initialized (make-hash-table)) + (jsonrpc-notify server :initialized eglot--{}) (dolist (buffer (buffer-list)) (with-current-buffer buffer ;; No need to pass SERVER as an argument: it has commit 70e6157b56a6d1534148f6fd9706232754e9ea9e Author: Vladimir Panteleev Date: Sun Oct 6 16:10:32 2019 +0000 Call shutdown/exit methods with params:{}, not null "null" is not a valid JSON value for "params" according to the JSON-RPC specification. * eglot.el (eglot-shutdown): Do the same thing as for "initialized", and use an empty hash table to be serialized to {}. Copyright-paperwork-exempt: yes GitHub-reference: per https://github.com/joaotavora/eglot/issues/315 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1decb2695f..1bdca12642 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -595,8 +595,9 @@ SERVER. ." (unwind-protect (progn (setf (eglot--shutdown-requested server) t) - (jsonrpc-request server :shutdown nil :timeout (or timeout 1.5)) - (jsonrpc-notify server :exit nil)) + (jsonrpc-request server :shutdown (make-hash-table) + :timeout (or timeout 1.5)) + (jsonrpc-notify server :exit (make-hash-table))) ;; Now ask jsonrpc.el to shut down the server. (jsonrpc-shutdown server (not preserve-buffers)) (unless preserve-buffers (kill-buffer (jsonrpc-events-buffer server))))) commit edf382a98222cf8a133bdc119563aba22a849bd9 Author: Felicián Németh Date: Thu Jan 9 13:28:08 2020 -0500 Support bug-reference-prog-mode * eglot.el (Local Variables): Add bug-reference-bug-regexp and bug-reference-url-format. GitHub-reference: close https://github.com/joaotavora/eglot/issues/405 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 888eacc690..1decb2695f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2661,5 +2661,7 @@ If INTERACTIVE, prompt user for details." ;;; eglot.el ends here ;; Local Variables: +;; bug-reference-bug-regexp: "\\(github#\\([0-9]+\\)\\)" +;; bug-reference-url-format: "https://github.com/joaotavora/eglot/issues/%s" ;; checkdoc-force-docstrings-flag: nil ;; End: commit 4ff8f1ed8f36dc8987819424f02133da526899b0 Author: Felicián Németh Date: Wed Jan 8 18:02:07 2020 +0100 Revert the last change about column calculation diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5e83118554..888eacc690 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -999,15 +999,15 @@ CONNECT-ARGS are passed as additional arguments to (defun eglot-current-column () (- (point) (point-at-bol))) -(defvar eglot-current-column-function #'eglot-lsp-abiding-column +(defvar eglot-current-column-function #'eglot-current-column "Function to calculate the current column. This is the inverse operation of `eglot-move-to-column-function' (which see). It is a function of no arguments returning a column number. For buffers managed by fully LSP-compliant servers, this should be set to -`eglot-lsp-abiding-column' (the default), and -`eglot-current-column' for all others.") +`eglot-lsp-abiding-column', and `eglot-current-column' (the default) +for all others.") (defun eglot-lsp-abiding-column () "Calculate current COLUMN as defined by the LSP spec." @@ -1023,7 +1023,7 @@ fully LSP-compliant servers, this should be set to :character (progn (when pos (goto-char pos)) (funcall eglot-current-column-function))))) -(defvar eglot-move-to-column-function #'eglot-move-to-lsp-abiding-column +(defvar eglot-move-to-column-function #'eglot-move-to-column "Function to move to a column reported by the LSP server. According to the standard, LSP column/character offsets are based @@ -1033,8 +1033,8 @@ where X is a multi-byte character, it actually means `b', not `c'. However, many servers don't follow the spec this closely. For buffers managed by fully LSP-compliant servers, this should -be set to `eglot-move-to-lsp-abiding-column' (the default), and -`eglot-move-to-column' for all others.") +be set to `eglot-move-to-lsp-abiding-column', and +`eglot-move-to-column' (the default) for all others.") (defun eglot-move-to-column (column) "Move to COLUMN without closely following the LSP spec." commit 6e0ad2ac68820ef197116ea4149ee60f40797a32 Author: Felicián Németh Date: Wed Jan 8 16:51:09 2020 +0100 Document the changes in column calculation * eglot.el (eglot-current-column-function) (eglot-move-to-column-function): Document the change of the default value. * NEWS.md: Log the change here as well. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cbfb926036..5e83118554 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1006,8 +1006,8 @@ This is the inverse operation of `eglot-move-to-column-function' (which see). It is a function of no arguments returning a column number. For buffers managed by fully LSP-compliant servers, this should be set to -`eglot-lsp-abiding-column', and `eglot-current-column' (the default) -for all others.") +`eglot-lsp-abiding-column' (the default), and +`eglot-current-column' for all others.") (defun eglot-lsp-abiding-column () "Calculate current COLUMN as defined by the LSP spec." @@ -1033,8 +1033,8 @@ where X is a multi-byte character, it actually means `b', not `c'. However, many servers don't follow the spec this closely. For buffers managed by fully LSP-compliant servers, this should -be set to `eglot-move-to-lsp-abiding-column', and -`eglot-move-to-column' (the default) for all others.") +be set to `eglot-move-to-lsp-abiding-column' (the default), and +`eglot-move-to-column' for all others.") (defun eglot-move-to-column (column) "Move to COLUMN without closely following the LSP spec." commit f9b59cf71f9aa31296642231856536be1618035f Author: João Távora Date: Fri Nov 29 23:42:58 2019 +0000 Abide by lsp when reporting and moving to columns * eglot.el (eglot-current-column-function): Set to eglot-lsp-abiding-column. (eglot-move-to-column-function): Set to eglot-move-to-lsp-abiding-column. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/361 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 888eacc690..cbfb926036 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -999,7 +999,7 @@ CONNECT-ARGS are passed as additional arguments to (defun eglot-current-column () (- (point) (point-at-bol))) -(defvar eglot-current-column-function #'eglot-current-column +(defvar eglot-current-column-function #'eglot-lsp-abiding-column "Function to calculate the current column. This is the inverse operation of @@ -1023,7 +1023,7 @@ for all others.") :character (progn (when pos (goto-char pos)) (funcall eglot-current-column-function))))) -(defvar eglot-move-to-column-function #'eglot-move-to-column +(defvar eglot-move-to-column-function #'eglot-move-to-lsp-abiding-column "Function to move to a column reported by the LSP server. According to the standard, LSP column/character offsets are based commit 2d1a7b21d1c32b92a51088e320a33260e68a4b4f Merge: edbc24d9cd 03ac6a10ac Author: Felicián Németh Date: Sun Jan 5 08:32:15 2020 -0500 Merge pull request from joaotavora/scratch/fix-277-exit-notification Fix https://github.com/joaotavora/eglot/issues/277: Send exit as a notification GitHub-reference: https://github.com/joaotavora/eglot/issues/400 commit 03ac6a10acc4fa677f9dbff468a72ff4fbf06052 Author: Felicián Németh Date: Sat Jan 4 16:55:00 2020 +0100 Send exit as a notification This is what the specification requires. @PerMildner, thanks for reporting and analyzing the issue. * eglot.el (eglot-shutdown): Use `notify' instead of `request' for the `exit' LSP method. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/277 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e952b912d1..888eacc690 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -596,11 +596,8 @@ SERVER. ." (progn (setf (eglot--shutdown-requested server) t) (jsonrpc-request server :shutdown nil :timeout (or timeout 1.5)) - ;; this one is supposed to always fail, because it asks the - ;; server to exit itself. Hence ignore-errors. - (ignore-errors (jsonrpc-request server :exit nil :timeout 1))) - ;; Now ask jsonrpc.el to shut down the server (which under normal - ;; conditions should return immediately). + (jsonrpc-notify server :exit nil)) + ;; Now ask jsonrpc.el to shut down the server. (jsonrpc-shutdown server (not preserve-buffers)) (unless preserve-buffers (kill-buffer (jsonrpc-events-buffer server))))) commit edbc24d9cd6e05291531a65a78bb960439ceaae2 Author: João Távora Date: Wed Jan 1 22:05:29 2020 +0000 Avoid double shutdowns and simplify shutdown logic * eglot.el (eglot-shutdown): Don't turn off eglot--managed-mode here. (eglot--on-shutdown): Rather here, but without autoshutdown. (eglot--managed-mode): Don't check eglot--shutdown-requested. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/389 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d1ff7da5d9..e952b912d1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -599,9 +599,6 @@ SERVER. ." ;; this one is supposed to always fail, because it asks the ;; server to exit itself. Hence ignore-errors. (ignore-errors (jsonrpc-request server :exit nil :timeout 1))) - ;; Turn off `eglot--managed-mode' where appropriate. - (dolist (buffer (eglot--managed-buffers server)) - (eglot--with-live-buffer buffer (eglot--managed-mode-off))) ;; Now ask jsonrpc.el to shut down the server (which under normal ;; conditions should return immediately). (jsonrpc-shutdown server (not preserve-buffers)) @@ -611,7 +608,9 @@ SERVER. ." "Called by jsonrpc.el when SERVER is already dead." ;; Turn off `eglot--managed-mode' where appropriate. (dolist (buffer (eglot--managed-buffers server)) - (eglot--with-live-buffer buffer (eglot--managed-mode-off))) + (let (;; Avoid duplicate shutdowns (github#389) + (eglot-autoshutdown nil)) + (eglot--with-live-buffer buffer (eglot--managed-mode-off)))) ;; Kill any expensive watches (maphash (lambda (_id watches) (mapcar #'file-notify-rm-watch watches)) @@ -1289,8 +1288,7 @@ For example, to keep your Company customization use (setf (eglot--managed-buffers server) (delq (current-buffer) (eglot--managed-buffers server))) (when (and eglot-autoshutdown - (not (eglot--shutdown-requested server)) - (not (eglot--managed-buffers server))) + (null (eglot--managed-buffers server))) (eglot-shutdown server))))))) (defun eglot--managed-mode-off () commit 234bbd10321cb668df253323a9033cf692642c0c Author: Theodor Thornhill Date: Thu Jan 2 10:33:26 2020 +0100 Use completing-read in eglot-code-actions See also https://github.com/joaotavora/eglot/issues/386. * eglot.el (eglot-code-actions): Replace tmm with completing-read Copyright-paperwork-exempt: yes Co-authored-by: João Távora GitHub-reference: close https://github.com/joaotavora/eglot/issues/393 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0d2433e9c0..d1ff7da5d9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2466,12 +2466,10 @@ potentially rename EGLOT's help buffer." (menu `("Eglot code actions:" ("dummy" ,@menu-items))) (action (if (listp last-nonmenu-event) (x-popup-menu last-nonmenu-event menu) - (let ((never-mind (gensym)) retval) - (setcdr (cadr menu) - (cons `("never mind..." . ,never-mind) (cdadr menu))) - (if (eq (setq retval (tmm-prompt menu)) never-mind) - (keyboard-quit) - retval))))) + (cdr (assoc (completing-read "[eglot] Pick an action: " + menu-items nil t + nil nil (car menu-items)) + menu-items))))) (eglot--dcase action (((Command) command arguments) (eglot-execute-command server (intern command) arguments)) commit d8a8bf448aed3a58f31f3a1556b11e8d7de8ea41 Author: Evgeni Kolev Date: Mon Dec 30 11:13:08 2019 +0200 * eglot.el (eglot-eldoc-function): fix outdated docstring. Fix https://github.com/joaotavora/eglot/issues/387 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5abdfc54af..0d2433e9c0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2254,8 +2254,7 @@ potentially rename EGLOT's help buffer." (eldoc-message string))) (defun eglot-eldoc-function () - "EGLOT's `eldoc-documentation-function' function. -If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." + "EGLOT's `eldoc-documentation-function' function." (let* ((buffer (current-buffer)) (server (eglot--current-server-or-lose)) (position-params (eglot--TextDocumentPositionParams)) commit 1668a22f0208223f103616dc93a7963ff870e0d8 Author: Steve Purcell Date: Sun Dec 29 02:08:19 2019 +1300 Add elm-language-server as the language server for elm * README.md (Connecting to a server): Add elm-language-server * eglot.el (eglot-server-programs): Add elm-language-server Co-authored-by: João Távora GitHub-reference: close https://github.com/joaotavora/eglot/issues/383 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index df8df13410..5abdfc54af 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -97,6 +97,7 @@ language-server/bin/php-language-server.php")) . ("solargraph" "socket" "--port" :autoport)) (haskell-mode . ("hie-wrapper")) + (elm-mode . ("elm-language-server")) (kotlin-mode . ("kotlin-language-server")) (go-mode . ("gopls")) ((R-mode ess-r-mode) . ("R" "--slave" "-e" commit be17d1824fe1f6cfd863c2ac7ddad224f7d251cb Author: Theodor Thornhill Date: Thu Dec 26 09:39:33 2019 +0100 New eglot-confirm-server-initiated-edits defcustom * eglot.el (eglot-confirm-server-initiated-edits): New defcustom. Copyright-paperwork-exempt: yes Co-authored-by: João Távora GitHub-reference: close https://github.com/joaotavora/eglot/issues/382 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4c0160a570..df8df13410 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -193,6 +193,11 @@ let the buffer grow forever." :type '(choice (const :tag "No limit" nil) (integer :tag "Number of characters"))) +(defcustom eglot-confirm-server-initiated-edits 'confirm + "Non-nil if server-initiated edits should be confirmed with user." + :type '(choice (const :tag "Don't show confirmation prompt" nil) + (symbol :tag "Show confirmation prompt" 'confirm))) + ;;; Constants ;;; @@ -1547,7 +1552,7 @@ THINGS are either registrations or unregisterations (sic)." (cl-defmethod eglot-handle-request (_server (_method (eql workspace/applyEdit)) &key _label edit) "Handle server request workspace/applyEdit" - (eglot--apply-workspace-edit edit 'confirm)) + (eglot--apply-workspace-edit edit eglot-confirm-server-initiated-edits)) (defun eglot--TextDocumentIdentifier () "Compute TextDocumentIdentifier object for current buffer." commit 76a658aba15103a60cc427463b677429b0a67eb5 Author: Augusto Stoffel Date: Wed Dec 18 13:44:28 2019 +0100 Add built-in support for tex and friends plain-tex-mode and latex-mode are derived from tex-mode. Some other TeX-related modes are not, so they require an explicit mention in eglot-server-programs. * README.md (Connecting to a server): Add Digestif to the list * eglot.el (eglot-server-programs): Add Digestif for TeX-related modes Copyright-paperwork-exempt: yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/379 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3e8eb2d14f..4c0160a570 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -105,7 +105,9 @@ language-server/bin/php-language-server.php")) (dart-mode . ("dart_language_server")) (elixir-mode . ("language_server.sh")) (ada-mode . ("ada_language_server")) - (scala-mode . ("metals-emacs"))) + (scala-mode . ("metals-emacs")) + ((tex-mode context-mode texinfo-mode bibtex-mode) + . ("digestif"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated commit 5c235178991116612a8fa51babe332a4e2fa9553 Author: Antoine Kalmbach Date: Tue Dec 17 21:34:29 2019 +0200 Add metals as the language server for scala * README.md (Connecting to a server): Add metals to the list * eglot.el (eglot-server-programs): Add metals for scala-mode Copyright-paperwork-exempt: yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/376 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 74f06699f2..3e8eb2d14f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -104,7 +104,8 @@ language-server/bin/php-language-server.php")) (java-mode . eglot--eclipse-jdt-contact) (dart-mode . ("dart_language_server")) (elixir-mode . ("language_server.sh")) - (ada-mode . ("ada_language_server"))) + (ada-mode . ("ada_language_server")) + (scala-mode . ("metals-emacs"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated commit 084970d188d0adf6c96f93c852caee6088432416 Author: João Távora Date: Sat Nov 30 00:16:12 2019 +0000 Allow non-standard keys in textdocument/publishdiagnostics. * eglot.el (eglot-handle-notification): Allow other keys for textDocument/publishDiagnostics. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/357 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b3a7f51826..74f06699f2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1470,7 +1470,8 @@ COMMAND is a symbol naming the command." "Handle notification telemetry/event") ;; noop, use events buffer (cl-defmethod eglot-handle-notification - (server (_method (eql textDocument/publishDiagnostics)) &key uri diagnostics) + (server (_method (eql textDocument/publishDiagnostics)) &key uri diagnostics + &allow-other-keys) ; FIXME: doesn't respect `eglot-strict-mode' "Handle notification publishDiagnostics" (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) (with-current-buffer buffer commit 86da1f615c0d1087b504c4b52347ac1b765834a2 Author: João Távora Date: Fri Nov 29 23:33:12 2019 +0000 Unbreak window/showmessagerequest * eglot.el (eglot-handle-request): Answer with a proper MessageActionItem. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/362 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 90f2c68474..b3a7f51826 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1450,16 +1450,16 @@ COMMAND is a symbol naming the command." (cl-defmethod eglot-handle-request (_server (_method (eql window/showMessageRequest)) &key type message actions) "Handle server request window/showMessageRequest" - (or (completing-read - (concat - (format (propertize "[eglot] Server reports (type=%s): %s" - 'face (if (<= type 1) 'error)) - type message) - "\nChoose an option: ") - (or (mapcar (lambda (obj) (plist-get obj :title)) actions) - '("OK")) - nil t (plist-get (elt actions 0) :title)) - (jsonrpc-error :code -32800 :message "User cancelled"))) + (let ((label (completing-read + (concat + (format (propertize "[eglot] Server reports (type=%s): %s" + 'face (if (<= type 1) 'error)) + type message) + "\nChoose an option: ") + (or (mapcar (lambda (obj) (plist-get obj :title)) actions) + '("OK")) + nil t (plist-get (elt actions 0) :title)))) + (if label `(:title ,label) :null))) (cl-defmethod eglot-handle-notification (_server (_method (eql window/logMessage)) &key _type _message) commit 8f4b1d97dc7124d4768e3dae4047e52d8d9cfa71 Author: João Távora Date: Wed Nov 20 22:55:29 2019 +0000 Resolve compilation warnings * eglot.el (company-tooltip-align-annotations): Forward declare. (eglot--cached-server): Renamed from eglot--cached-current-server. (eglot--managed-mode, eglot-current-server) (eglot--current-server-or-lose) (eglot--maybe-activate-editing-mode): use it. (eglot-completion-at-point): Don't use insertTextFormat. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a327655a0e..90f2c68474 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -71,7 +71,8 @@ (require 'filenotify) (require 'ert) (require 'array) -(defvar company-backends) ; forward-declare, but don't require company yet +(defvar company-backends) ; forward-declare, but don't require company +(defvar company-tooltip-align-annotations) @@ -1222,6 +1223,9 @@ For example, to keep your Company customization use (push (cons ',symbol (symbol-value ',symbol)) eglot--saved-bindings) (setq-local ,symbol ,binding))) +(defvar-local eglot--cached-server nil + "A cached reference to the current EGLOT server.") + (define-minor-mode eglot--managed-mode "Mode for source buffers managed by some EGLOT project." nil nil eglot-mode-map @@ -1251,7 +1255,7 @@ For example, to keep your Company customization use #'eglot-imenu)) (flymake-mode 1) (eldoc-mode 1) - (cl-pushnew (current-buffer) (eglot--managed-buffers eglot--cached-current-server))) + (cl-pushnew (current-buffer) (eglot--managed-buffers eglot--cached-server))) (t (remove-hook 'after-change-functions 'eglot--after-change t) (remove-hook 'before-change-functions 'eglot--before-change t) @@ -1270,8 +1274,8 @@ For example, to keep your Company customization use do (set (make-local-variable var) saved-binding)) (remove-function (local 'imenu-create-index-function) #'eglot-imenu) (setq eglot--current-flymake-report-fn nil) - (let ((server eglot--cached-current-server)) - (setq eglot--cached-current-server nil) + (let ((server eglot--cached-server)) + (setq eglot--cached-server nil) (when server (setf (eglot--managed-buffers server) (delq (current-buffer) (eglot--managed-buffers server))) @@ -1284,16 +1288,13 @@ For example, to keep your Company customization use "Turn off `eglot--managed-mode' unconditionally." (eglot--managed-mode -1)) -(defvar-local eglot--cached-current-server nil - "A cached reference to the current EGLOT server.") - (defun eglot-current-server () "Return logical EGLOT server for current buffer, nil if none." - eglot--cached-current-server) + eglot--cached-server) (defun eglot--current-server-or-lose () "Return current logical EGLOT server connection or error." - (or eglot--cached-current-server + (or eglot--cached-server (jsonrpc-error "No current JSON-RPC connection"))) (defvar-local eglot--unreported-diagnostics nil @@ -1313,8 +1314,8 @@ If it is activated, also signal textDocument/didOpen." ;; `revert-buffer-preserve-modes' is nil. (when (and buffer-file-name (or - eglot--cached-current-server - (setq eglot--cached-current-server + eglot--cached-server + (setq eglot--cached-server (cl-find major-mode (gethash (or (project-current) `(transient . ,default-directory)) @@ -2026,7 +2027,7 @@ is not active." (funcall proxies))))) :annotation-function (lambda (proxy) - (eglot--dbind ((CompletionItem) detail kind insertTextFormat) + (eglot--dbind ((CompletionItem) detail kind) (get-text-property 0 'eglot--lsp-item proxy) (let* ((detail (and (stringp detail) (not (string= detail "")) commit 111973220fd5822298beac47f18a8292b1e92d10 Author: João Távora Date: Wed Nov 20 22:51:42 2019 +0000 Locally tweak imenu-create-index-function * eglot.el (eglot--managed-mode): locally tweak imenu-create-index-function. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/351 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c25b7b7f08..a327655a0e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1247,7 +1247,8 @@ For example, to keep your Company customization use (eglot--setq-saving company-backends '(company-capf)) (eglot--setq-saving company-tooltip-align-annotations t) (unless (eglot--stay-out-of-p 'imenu) - (add-function :before-until imenu-create-index-function #'eglot-imenu)) + (add-function :before-until (local 'imenu-create-index-function) + #'eglot-imenu)) (flymake-mode 1) (eldoc-mode 1) (cl-pushnew (current-buffer) (eglot--managed-buffers eglot--cached-current-server))) @@ -1267,6 +1268,7 @@ For example, to keep your Company customization use (remove-hook 'pre-command-hook 'eglot--pre-command-hook t) (cl-loop for (var . saved-binding) in eglot--saved-bindings do (set (make-local-variable var) saved-binding)) + (remove-function (local 'imenu-create-index-function) #'eglot-imenu) (setq eglot--current-flymake-report-fn nil) (let ((server eglot--cached-current-server)) (setq eglot--cached-current-server nil) commit 0453a2186634b5e9eca8e86748cdfe4002c3b855 Author: Felicián Németh Date: Mon Nov 18 12:23:37 2019 +0100 Fail when eglot-find-* finds no references * eglot.el (eglot--lsp-xref-helper): Display message when no references have been found instead of calling xref-find-references. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/339 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7171911b0e..c25b7b7f08 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1872,7 +1872,9 @@ Try to visit the target file for a richer summary line." method :extra-params extra-params :capability capability))) - (xref-find-references "LSP identifier at point."))) + (if eglot--lsp-xref-refs + (xref-find-references "LSP identifier at point.") + (eglot--message "%s returned no references" method)))) (defun eglot-find-declaration () "Find declaration for SYM, the identifier at point." commit 46aa1aafd179353a98dc1582ca523bf649e3371c Author: Xu Chunyang <4550353+xuchunyang@users.noreply.github.com> Date: Sun Nov 17 21:17:47 2019 +0800 Waste less space in completion annotations * eglot.el (eglot-completion-at-point): don't add "(snippet)" Copyright-paperwork-exempt: yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/349 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1baa3393dc..7171911b0e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2033,10 +2033,7 @@ is not active." (when annotation (concat " " (propertize annotation - 'face 'font-lock-function-name-face) - (and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn) - " (snippet)")))))) + 'face 'font-lock-function-name-face)))))) :company-doc-buffer (lambda (proxy) (let* ((documentation commit 751abfe9b4a8f882eca6a8903126641c2fb39489 Author: João Távora Date: Sun Nov 17 13:07:08 2019 +0000 * eglot.el (eglot-completion-at-point): remove spurious unrelated change. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b30a236ed1..1baa3393dc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1963,9 +1963,7 @@ is not active." :textDocument/completion (eglot--CompletionParams) :deferred :textDocument/completion - :cancel-on-input (prog1 non-essential - (when non-essential - (message "OH IT'S NON ESSENTIAL"))))) + :cancel-on-input t)) (setq items (append (if (vectorp resp) resp (plist-get resp :items)) nil)) commit 9101970ccf77c755a12d20d147187cff20929df4 Author: João Távora Date: Fri Nov 15 16:04:55 2019 +0000 Ensure process starts in project's root Also fix https://github.com/joaotavora/eglot/issues/347. * eglot.el (eglot--connect): Bind default-directory around make process. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/330 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 51d0dee140..b30a236ed1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -813,15 +813,17 @@ This docstring appeases checkdoc, that's all." (setq autostart-inferior-process inferior) connection)))) ((stringp (car contact)) - `(:process ,(lambda () - (make-process - :name readable-name - :command contact - :connection-type 'pipe - :coding 'utf-8-emacs-unix - :noquery t - :stderr (get-buffer-create - (format "*%s stderr*" readable-name)))))))) + `(:process + ,(lambda () + (let ((default-directory default-directory)) + (make-process + :name readable-name + :command contact + :connection-type 'pipe + :coding 'utf-8-emacs-unix + :noquery t + :stderr (get-buffer-create + (format "*%s stderr*" readable-name))))))))) (spread (lambda (fn) (lambda (server method params) (apply fn server method (append params nil))))) (server @@ -1961,7 +1963,9 @@ is not active." :textDocument/completion (eglot--CompletionParams) :deferred :textDocument/completion - :cancel-on-input t)) + :cancel-on-input (prog1 non-essential + (when non-essential + (message "OH IT'S NON ESSENTIAL"))))) (setq items (append (if (vectorp resp) resp (plist-get resp :items)) nil)) commit 1f784797d86dd86625be3ed14226e795f031d2cd Author: João Távora Date: Tue Nov 12 08:51:09 2019 +0000 Let other imenu functions work if lsp server's doesn't * eglot.el (eglot--stay-out-of-p): New helper. (eglot--setq-saving): Use it. (eglot--managed-mode): Use add-function :before-until for imenu-create-index-function. (eglot-imenu): Don't error. Fix indentation. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/343 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a7b5d59004..51d0dee140 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1208,17 +1208,16 @@ For example, to keep your Company customization use (add-to-list 'eglot-stay-out-of 'company)") +(defun eglot--stay-out-of-p (symbol) + "Tell if EGLOT should stay of of SYMBOL." + (cl-find (symbol-name symbol) eglot-stay-out-of + :test (lambda (s thing) + (let ((re (if (symbolp thing) (symbol-name thing) thing))) + (string-match re s))))) + (defmacro eglot--setq-saving (symbol binding) - `(when (and (boundp ',symbol) - (not (cl-find (symbol-name ',symbol) - eglot-stay-out-of - :test - (lambda (s thing) - (let ((re (if (symbolp thing) (symbol-name thing) - thing))) - (string-match re s)))))) - (push (cons ',symbol (symbol-value ',symbol)) - eglot--saved-bindings) + `(unless (or (not (boundp ',symbol)) (eglot--stay-out-of-p ',symbol)) + (push (cons ',symbol (symbol-value ',symbol)) eglot--saved-bindings) (setq-local ,symbol ,binding))) (define-minor-mode eglot--managed-mode @@ -1245,7 +1244,8 @@ For example, to keep your Company customization use (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend t)) (eglot--setq-saving company-backends '(company-capf)) (eglot--setq-saving company-tooltip-align-annotations t) - (eglot--setq-saving imenu-create-index-function #'eglot-imenu) + (unless (eglot--stay-out-of-p 'imenu) + (add-function :before-until imenu-create-index-function #'eglot-imenu)) (flymake-mode 1) (eldoc-mode 1) (cl-pushnew (current-buffer) (eglot--managed-buffers eglot--cached-current-server))) @@ -2298,37 +2298,36 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (defun eglot-imenu () "EGLOT's `imenu-create-index-function'." - (unless (eglot--server-capable :documentSymbolProvider) - (eglot--error "Server isn't a :documentSymbolProvider")) (let ((entries - (mapcar - (eglot--lambda - ((SymbolInformation) name kind location containerName) - (cons (propertize - name - :kind (alist-get kind eglot--symbol-kind-names - "Unknown") - :containerName (and (stringp containerName) - (not (string-empty-p containerName)) - containerName)) - (eglot--lsp-position-to-point - (plist-get (plist-get location :range) :start)))) - (jsonrpc-request (eglot--current-server-or-lose) - :textDocument/documentSymbol - `(:textDocument ,(eglot--TextDocumentIdentifier)))))) - (mapcar - (pcase-lambda (`(,kind . ,syms)) - (let ((syms-by-scope (seq-group-by - (lambda (e) - (get-text-property 0 :containerName (car e))) - syms))) - (cons kind (cl-loop for (scope . elems) in syms-by-scope - append (if scope - (list (cons scope elems)) - elems))))) - (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) - entries))) - ) + (and + (eglot--server-capable :documentSymbolProvider) + (mapcar + (eglot--lambda + ((SymbolInformation) name kind location containerName) + (cons (propertize + name + :kind (alist-get kind eglot--symbol-kind-names + "Unknown") + :containerName (and (stringp containerName) + (not (string-empty-p containerName)) + containerName)) + (eglot--lsp-position-to-point + (plist-get (plist-get location :range) :start)))) + (jsonrpc-request (eglot--current-server-or-lose) + :textDocument/documentSymbol + `(:textDocument ,(eglot--TextDocumentIdentifier))))))) + (mapcar + (pcase-lambda (`(,kind . ,syms)) + (let ((syms-by-scope (seq-group-by + (lambda (e) + (get-text-property 0 :containerName (car e))) + syms))) + (cons kind (cl-loop for (scope . elems) in syms-by-scope + append (if scope + (list (cons scope elems)) + elems))))) + (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) + entries)))) (defun eglot--apply-text-edits (edits &optional version) "Apply EDITS for current buffer if at VERSION, or if it's nil." commit b5f02979b6c4a6f8b57279768258a5884e1fac5a Author: Felicián Németh Date: Tue Nov 12 13:50:11 2019 +0100 Support serverinfo of lsp 3.15.0 Add support for serverInfo from the upcoming specification. This changeset just stores the info sent by the server and slightly changes a greeting message. But it opens up the possibility to identify servers even when eglot uses a TCP connection and therefore makes possible to implement server specific features (in eglot-x). Old message: ``` Connected! Server `EGLOT (test-ccls/c++-mode)' now managing `c++-mode' buffers in project `test-ccls'. ``` New message: ``` Connected! Server `ccls' now managing `c++-mode' buffers in project `test-ccls'. ``` * eglot.el (eglot--lsp-interface-alist): Extend it with serverInfo. (eglot-lsp-server): Add member variable server-info. (eglot--connect): Store server-info and display server's name when connected. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0022737fb9..a7b5d59004 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -229,7 +229,7 @@ let the buffer grow forever." (DocumentHighlight (:range) (:kind)) (FileSystemWatcher (:globPattern) (:kind)) (Hover (:contents) (:range)) - (InitializeResult (:capabilities)) + (InitializeResult (:capabilities) (:serverInfo)) (Location (:uri :range)) (LogMessageParams (:type :message)) (MarkupContent (:kind :value)) @@ -531,6 +531,9 @@ treated as in `eglot-dbind'." (capabilities :documentation "JSON object containing server capabilities." :accessor eglot--capabilities) + (server-info + :documentation "JSON object containing server info." + :accessor eglot--server-info) (shutdown-requested :documentation "Flag set when server is shutting down." :accessor eglot--shutdown-requested) @@ -856,11 +859,12 @@ This docstring appeases checkdoc, that's all." server) :capabilities (eglot-client-capabilities server)) :success-fn - (eglot--lambda ((InitializeResult) capabilities) + (eglot--lambda ((InitializeResult) capabilities serverInfo) (unless cancelled (push server (gethash project eglot--servers-by-project)) (setf (eglot--capabilities server) capabilities) + (setf (eglot--server-info server) serverInfo) (jsonrpc-notify server :initialized (make-hash-table)) (dolist (buffer (buffer-list)) (with-current-buffer buffer @@ -888,7 +892,9 @@ This docstring appeases checkdoc, that's all." (eglot--message "Connected! Server `%s' now managing `%s' buffers \ in project `%s'." - (jsonrpc-name server) managed-major-mode + (or (plist-get serverInfo :name) + (jsonrpc-name server)) + managed-major-mode (eglot--project-nickname server)) (when tag (throw tag t)))) :timeout eglot-connect-timeout commit fbcb55168f1b0c718536fb35b68cad687d311636 Author: r-zip Date: Tue Nov 12 13:44:01 2019 -0500 Set nobreak-char-display to nil in *eglot-help* * eglot.el (eglot-help-at-point): set nobreak-char-display Copyright-paperwork-exempt: yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/345 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 80c65cea5c..0022737fb9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2178,7 +2178,8 @@ is not active." (with-current-buffer (eglot--help-buffer) (with-help-window (current-buffer) (rename-buffer (format "*eglot-help for %s*" sym)) - (with-current-buffer standard-output (insert blurb))))))) + (with-current-buffer standard-output (insert blurb)) + (setq-local nobreak-char-display nil)))))) (defun eglot-doc-too-large-for-echo-area (string) "Return non-nil if STRING won't fit in echo area. commit c4f5e40ddb1fe1e39d3b86875e04aedf46ec8bd0 Author: João Távora Date: Wed Nov 6 15:11:12 2019 +0000 Protect against empty-string inserttext in completions * eglot.el (eglot-completion-at-point): Don't use insertText as a proxy. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/341 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 281a293c89..80c65cea5c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1968,8 +1968,11 @@ is not active." (cond ((and (eql insertTextFormat 2) (eglot--snippet-expansion-fn)) (string-trim-left label)) + ((and insertText + (not (string-empty-p insertText))) + insertText) (t - (or insertText (string-trim-left label)))))) + (string-trim-left label))))) (unless (zerop (length item)) (put-text-property 0 1 'eglot--lsp-item item proxy)) proxy)) commit a6799b92ce6208a12d17291673aa09e8963e61be Author: João Távora Date: Sat Nov 9 22:58:08 2019 +0000 Provide stable eglot-current-server helper It's better if eglot--current-server is removed, since it was being abused by other packages, and has side effects. The only place where it was really needed was eglot--maybe-activate-editing-mode, so the find-and-cache logic has been moved there. All other places that can handle a nil server now use eglot-current-server, the external version. * eglot.el (eglot-shutdown, eglot, eglot--read-server) (eglot--mode-line-format): Use eglot-current-server. (eglot--connect): Update comment. (eglot--current-server): Remove. (eglot-current-server): New helper. (eglot--maybe-activate-editing-mode): find and cache the server here. * eglot-tests.el (auto-detect-running-server) (auto-shutdown, auto-reconnect, eglot-ensure) (slow-async-connection): Use eglot-current-server. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/342 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7133426021..281a293c89 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -576,7 +576,7 @@ If PRESERVE-BUFFERS is non-nil (interactively, when called with a prefix argument), do not kill events and output buffers of SERVER. ." (interactive (list (eglot--read-server "Shutdown which server" - (eglot--current-server)) + (eglot-current-server)) t nil current-prefix-arg)) (eglot--message "Asking %s politely to terminate" (jsonrpc-name server)) (unwind-protect @@ -726,7 +726,7 @@ described in `eglot-server-programs', which see. INTERACTIVE is t if called interactively." (interactive (append (eglot--guess-contact t) '(t))) - (let* ((current-server (eglot--current-server)) + (let* ((current-server (eglot-current-server)) (live-p (and current-server (jsonrpc-running-p current-server)))) (if (and live-p interactive @@ -866,8 +866,9 @@ This docstring appeases checkdoc, that's all." (with-current-buffer buffer ;; No need to pass SERVER as an argument: it has ;; been registered in `eglot--servers-by-project', - ;; so that it can be obtained from the function - ;; `eglot--current-server' in any managed buffer. + ;; so that it can be found (and cached) from + ;; `eglot--maybe-activate-editing-mode' in any + ;; managed buffer. (eglot--maybe-activate-editing-mode))) (setf (eglot--inhibit-autoreconnect server) (cond @@ -1159,7 +1160,7 @@ and just return it. PROMPT shouldn't end with a question mark." (cond ((null servers) (eglot--error "No servers!")) ((or (cdr servers) (not dont-if-just-the-one)) - (let* ((default (when-let ((current (eglot--current-server))) + (let* ((default (when-let ((current (eglot-current-server))) (funcall name current))) (read (completing-read (if default @@ -1276,20 +1277,13 @@ For example, to keep your Company customization use (defvar-local eglot--cached-current-server nil "A cached reference to the current EGLOT server.") -(defun eglot--current-server () - "Find and cache logical EGLOT server for current buffer." - (or - eglot--cached-current-server - (setq eglot--cached-current-server - (cl-find major-mode - (gethash (or (project-current) - `(transient . ,default-directory)) - eglot--servers-by-project) - :key #'eglot--major-mode)))) +(defun eglot-current-server () + "Return logical EGLOT server for current buffer, nil if none." + eglot--cached-current-server) (defun eglot--current-server-or-lose () "Return current logical EGLOT server connection or error." - (or (eglot--current-server) + (or eglot--cached-current-server (jsonrpc-error "No current JSON-RPC connection"))) (defvar-local eglot--unreported-diagnostics nil @@ -1307,7 +1301,15 @@ If it is activated, also signal textDocument/didOpen." (unless eglot--managed-mode ;; Called when `revert-buffer-in-progress-p' is t but ;; `revert-buffer-preserve-modes' is nil. - (when (and buffer-file-name (eglot--current-server)) + (when (and buffer-file-name + (or + eglot--cached-current-server + (setq eglot--cached-current-server + (cl-find major-mode + (gethash (or (project-current) + `(transient . ,default-directory)) + eglot--servers-by-project) + :key #'eglot--major-mode)))) (setq eglot--unreported-diagnostics `(:just-opened . nil)) (eglot--managed-mode) (eglot--signal-textDocument/didOpen)))) @@ -1354,7 +1356,7 @@ Uses THING, FACE, DEFS and PREPEND." (defun eglot--mode-line-format () "Compose the EGLOT's mode-line." - (pcase-let* ((server (eglot--current-server)) + (pcase-let* ((server (eglot-current-server)) (nick (and server (eglot--project-nickname server))) (pending (and server (hash-table-count (jsonrpc--request-continuations server)))) commit 8b4e81cdcf57a1eca09523c84bb3389d3549131d Author: João Távora Date: Tue Nov 5 23:53:35 2019 +0000 Don't choke on workspace/configuration with no scopeuri * eglot.el (eglot-handle-request): Don't choke on nil scopeUri. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/340 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a0f57f4160..7133426021 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1678,20 +1678,23 @@ When called interactively, use the currently active server" (apply #'vector (mapcar (eglot--lambda ((ConfigurationItem) scopeUri section) - (let* ((path (eglot--uri-to-path scopeUri))) - (when (file-directory-p path) - (with-temp-buffer - (let ((default-directory path)) - (setq-local major-mode (eglot--major-mode server)) - (hack-dir-local-variables-non-file-buffer) - (alist-get section eglot-workspace-configuration - nil nil - (lambda (wsection section) - (string= - (if (keywordp wsection) - (substring (symbol-name wsection) 1) - wsection) - section)))))))) + (with-temp-buffer + (let* ((uri-path (eglot--uri-to-path scopeUri)) + (default-directory + (if (and (not (string-empty-p uri-path)) + (file-directory-p uri-path)) + uri-path + (car (project-roots (eglot--project server)))))) + (setq-local major-mode (eglot--major-mode server)) + (hack-dir-local-variables-non-file-buffer) + (alist-get section eglot-workspace-configuration + nil nil + (lambda (wsection section) + (string= + (if (keywordp wsection) + (substring (symbol-name wsection) 1) + wsection) + section)))))) items))) (defun eglot--signal-textDocument/didChange () commit 19653f1e891d8a23afc46675579f9a3140fe582d Author: Ingo Lohmar Date: Sun Oct 20 12:21:37 2019 +0200 Only set eglot--cached-current-server by (more aggressive) caching * eglot.el (eglot--current-server): Always set cache value. (eglot--maybe-activate-editing-mode): No need to set cached server. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e304ebd2b2..a0f57f4160 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1277,13 +1277,15 @@ For example, to keep your Company customization use "A cached reference to the current EGLOT server.") (defun eglot--current-server () - "Find the current logical EGLOT server." + "Find and cache logical EGLOT server for current buffer." (or eglot--cached-current-server - (let* ((probe (or (project-current) - `(transient . ,default-directory)))) - (cl-find major-mode (gethash probe eglot--servers-by-project) - :key #'eglot--major-mode)))) + (setq eglot--cached-current-server + (cl-find major-mode + (gethash (or (project-current) + `(transient . ,default-directory)) + eglot--servers-by-project) + :key #'eglot--major-mode)))) (defun eglot--current-server-or-lose () "Return current logical EGLOT server connection or error." @@ -1305,12 +1307,10 @@ If it is activated, also signal textDocument/didOpen." (unless eglot--managed-mode ;; Called when `revert-buffer-in-progress-p' is t but ;; `revert-buffer-preserve-modes' is nil. - (let ((server (and buffer-file-name (eglot--current-server)))) - (when server - (setq eglot--unreported-diagnostics `(:just-opened . nil)) - (setq eglot--cached-current-server server) - (eglot--managed-mode) - (eglot--signal-textDocument/didOpen))))) + (when (and buffer-file-name (eglot--current-server)) + (setq eglot--unreported-diagnostics `(:just-opened . nil)) + (eglot--managed-mode) + (eglot--signal-textDocument/didOpen)))) (add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) (add-hook 'after-change-major-mode-hook 'eglot--maybe-activate-editing-mode) commit 08532c1b92bcf43923273ef5f662705fe94e9126 Author: Ingo Lohmar Date: Fri Oct 18 21:19:46 2019 +0200 Simplify "maybe"-activation, dump "server" arg * eglot.el (eglot--maybe-activate-editing-mode): Remove `server' arg. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c4b83a0210..e304ebd2b2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -864,7 +864,11 @@ This docstring appeases checkdoc, that's all." (jsonrpc-notify server :initialized (make-hash-table)) (dolist (buffer (buffer-list)) (with-current-buffer buffer - (eglot--maybe-activate-editing-mode server))) + ;; No need to pass SERVER as an argument: it has + ;; been registered in `eglot--servers-by-project', + ;; so that it can be obtained from the function + ;; `eglot--current-server' in any managed buffer. + (eglot--maybe-activate-editing-mode))) (setf (eglot--inhibit-autoreconnect server) (cond ((booleanp eglot-autoreconnect) @@ -1294,28 +1298,20 @@ For example, to keep your Company customization use "Eglot's `after-revert-hook'." (when revert-buffer-preserve-modes (eglot--signal-textDocument/didOpen))) -(defun eglot--maybe-activate-editing-mode (&optional server) - "Maybe activate mode function `eglot--managed-mode'. -If SERVER is supplied, do it only if BUFFER is managed by it. In -that case, also signal textDocument/didOpen." +(defun eglot--maybe-activate-editing-mode () + "Maybe activate `eglot--managed-mode'. + +If it is activated, also signal textDocument/didOpen." (unless eglot--managed-mode - (unless server - (when eglot--cached-current-server - (display-warning - :eglot "`eglot--cached-current-server' is non-nil, but it shouldn't be!\n\ -Please report this as a possible bug.") - (setq eglot--cached-current-server nil))) ;; Called when `revert-buffer-in-progress-p' is t but ;; `revert-buffer-preserve-modes' is nil. - (let* ((cur (and buffer-file-name (eglot--current-server))) - (server (or (and (null server) cur) (and server (eq server cur) cur)))) + (let ((server (and buffer-file-name (eglot--current-server)))) (when server (setq eglot--unreported-diagnostics `(:just-opened . nil)) (setq eglot--cached-current-server server) (eglot--managed-mode) (eglot--signal-textDocument/didOpen))))) - (add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) (add-hook 'after-change-major-mode-hook 'eglot--maybe-activate-editing-mode) commit 27e0aa7333f84e2ea450309a801a5613fc9ede95 Author: Ingo Lohmar Date: Fri Oct 18 21:11:59 2019 +0200 Merge -onoff proxy code into minor mode function This simplifies bookkeeping and keeping the state of locally cached servers, their managed buffers, and the buffer-local mode consistent. The "on" case of the -onoff code now expects that `eglot--cached-current-server' has been set already, the "off" case uses the same value. * eglot.el (eglot--managed-mode-onoff): Remove. (eglot--managed-mode): Adopt code. (eglot--managed-mode-off): New minimal wrapper. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bce4ee08b1..c4b83a0210 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -588,7 +588,7 @@ SERVER. ." (ignore-errors (jsonrpc-request server :exit nil :timeout 1))) ;; Turn off `eglot--managed-mode' where appropriate. (dolist (buffer (eglot--managed-buffers server)) - (eglot--with-live-buffer buffer (eglot--managed-mode-onoff server nil))) + (eglot--with-live-buffer buffer (eglot--managed-mode-off))) ;; Now ask jsonrpc.el to shut down the server (which under normal ;; conditions should return immediately). (jsonrpc-shutdown server (not preserve-buffers)) @@ -598,7 +598,7 @@ SERVER. ." "Called by jsonrpc.el when SERVER is already dead." ;; Turn off `eglot--managed-mode' where appropriate. (dolist (buffer (eglot--managed-buffers server)) - (eglot--with-live-buffer buffer (eglot--managed-mode-onoff server nil))) + (eglot--with-live-buffer buffer (eglot--managed-mode-off))) ;; Kill any expensive watches (maphash (lambda (_id watches) (mapcar #'file-notify-rm-watch watches)) @@ -1217,7 +1217,7 @@ For example, to keep your Company customization use (eglot--managed-mode (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'before-change-functions 'eglot--before-change nil t) - (add-hook 'kill-buffer-hook 'eglot--managed-mode-onoff nil t) + (add-hook 'kill-buffer-hook #'eglot--managed-mode-off nil t) ;; Prepend "didClose" to the hook after the "onoff", so it will run first (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'before-revert-hook 'eglot--signal-textDocument/didClose nil t) @@ -1226,7 +1226,7 @@ For example, to keep your Company customization use (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) (add-hook 'xref-backend-functions 'eglot-xref-backend nil t) (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) - (add-hook 'change-major-mode-hook 'eglot--managed-mode-onoff nil t) + (add-hook 'change-major-mode-hook #'eglot--managed-mode-off nil t) (add-hook 'post-self-insert-hook 'eglot--post-self-insert-hook nil t) (add-hook 'pre-command-hook 'eglot--pre-command-hook nil t) (eglot--setq-saving eldoc-documentation-function #'eglot-eldoc-function) @@ -1236,10 +1236,12 @@ For example, to keep your Company customization use (eglot--setq-saving company-tooltip-align-annotations t) (eglot--setq-saving imenu-create-index-function #'eglot-imenu) (flymake-mode 1) - (eldoc-mode 1)) + (eldoc-mode 1) + (cl-pushnew (current-buffer) (eglot--managed-buffers eglot--cached-current-server))) (t (remove-hook 'after-change-functions 'eglot--after-change t) (remove-hook 'before-change-functions 'eglot--before-change t) + (remove-hook 'kill-buffer-hook #'eglot--managed-mode-off t) (remove-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose t) (remove-hook 'before-revert-hook 'eglot--signal-textDocument/didClose t) (remove-hook 'after-revert-hook 'eglot--after-revert-hook t) @@ -1247,37 +1249,28 @@ For example, to keep your Company customization use (remove-hook 'after-save-hook 'eglot--signal-textDocument/didSave t) (remove-hook 'xref-backend-functions 'eglot-xref-backend t) (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) - (remove-hook 'change-major-mode-hook #'eglot--managed-mode-onoff t) + (remove-hook 'change-major-mode-hook #'eglot--managed-mode-off t) (remove-hook 'post-self-insert-hook 'eglot--post-self-insert-hook t) (remove-hook 'pre-command-hook 'eglot--pre-command-hook t) (cl-loop for (var . saved-binding) in eglot--saved-bindings do (set (make-local-variable var) saved-binding)) - (setq eglot--current-flymake-report-fn nil)))) + (setq eglot--current-flymake-report-fn nil) + (let ((server eglot--cached-current-server)) + (setq eglot--cached-current-server nil) + (when server + (setf (eglot--managed-buffers server) + (delq (current-buffer) (eglot--managed-buffers server))) + (when (and eglot-autoshutdown + (not (eglot--shutdown-requested server)) + (not (eglot--managed-buffers server))) + (eglot-shutdown server))))))) -(defvar-local eglot--cached-current-server nil - "A cached reference to the current EGLOT server. -Reset in `eglot--managed-mode-onoff'.") +(defun eglot--managed-mode-off () + "Turn off `eglot--managed-mode' unconditionally." + (eglot--managed-mode -1)) -(defun eglot--managed-mode-onoff (&optional server turn-on) - "Proxy for function `eglot--managed-mode' with TURN-ON and SERVER." - (let ((buf (current-buffer))) - (cond ((and server turn-on) - (eglot--managed-mode 1) - (setq eglot--cached-current-server server) - (cl-pushnew buf (eglot--managed-buffers server))) - (t - (eglot--managed-mode -1) - (let ((server - (or server - eglot--cached-current-server))) - (setq eglot--cached-current-server nil) - (when server - (setf (eglot--managed-buffers server) - (delq buf (eglot--managed-buffers server))) - (when (and eglot-autoshutdown - (not (eglot--shutdown-requested server)) - (not (eglot--managed-buffers server))) - (eglot-shutdown server)))))))) +(defvar-local eglot--cached-current-server nil + "A cached reference to the current EGLOT server.") (defun eglot--current-server () "Find the current logical EGLOT server." @@ -1318,7 +1311,8 @@ Please report this as a possible bug.") (server (or (and (null server) cur) (and server (eq server cur) cur)))) (when server (setq eglot--unreported-diagnostics `(:just-opened . nil)) - (eglot--managed-mode-onoff server t) + (setq eglot--cached-current-server server) + (eglot--managed-mode) (eglot--signal-textDocument/didOpen))))) commit ee794a8d5ec095fabe0ce4b682b665cb97d3301d Author: João Távora Date: Wed Oct 30 01:24:10 2019 +0000 Use completionitem/resolve more abundantly It was already used to resolve documentation bits of completions, but it can also be useful to resolve snippet templates and such. To resolve a completion, you need some part of a completion to start with. If it has a :data field exists and the server supports :resolveProvider, fetch the new object, otherwise use whatever we had already. * eglot.el (eglot-completion-at-point): Add another local function for resolving completions. GitHub-reference: per https://github.com/joaotavora/eglot/issues/50 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1f7a396ef9..bce4ee08b1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1979,6 +1979,20 @@ is not active." (put-text-property 0 1 'eglot--lsp-item item proxy)) proxy)) items))))) + resolved + (resolve-maybe + ;; Maybe completion/resolve JSON object `lsp-comp' into + ;; another JSON object, if at all possible. Otherwise, + ;; just return lsp-comp. + (lambda (lsp-comp) + (cond (resolved resolved) + ((and (eglot--server-capable :completionProvider + :resolveProvider) + (plist-get lsp-comp :data)) + (setq resolved + (jsonrpc-request server :completionItem/resolve + lsp-comp :cancel-on-input t))) + (t lsp-comp)))) (bounds (bounds-of-thing-at-point 'symbol))) (list (or (car bounds) (point)) @@ -2021,13 +2035,7 @@ is not active." (lambda (proxy) (let* ((documentation (let ((lsp-comp (get-text-property 0 'eglot--lsp-item proxy))) - (or (plist-get lsp-comp :documentation) - (and (eglot--server-capable :completionProvider - :resolveProvider) - (plist-get - (jsonrpc-request server :completionItem/resolve - lsp-comp :cancel-on-input t) - :documentation))))) + (plist-get (funcall resolve-maybe lsp-comp) :documentation))) (formatted (and documentation (eglot--format-markup documentation)))) (when formatted @@ -2050,13 +2058,15 @@ is not active." insertText textEdit additionalTextEdits) - (or (get-text-property 0 'eglot--lsp-item proxy) - ;; When selecting from the *Completions* - ;; buffer, `proxy' won't have any properties. A - ;; lookup should fix that (github#148) - (get-text-property - 0 'eglot--lsp-item - (cl-find proxy (funcall proxies) :test #'string=))) + (funcall + resolve-maybe + (or (get-text-property 0 'eglot--lsp-item proxy) + ;; When selecting from the *Completions* + ;; buffer, `proxy' won't have any properties. + ;; A lookup should fix that (github#148) + (get-text-property + 0 'eglot--lsp-item + (cl-find proxy (funcall proxies) :test #'string=)))) (let ((snippet-fn (and (eql insertTextFormat 2) (eglot--snippet-expansion-fn)))) (cond (textEdit commit c8ea2c269a5452753875482a06f0de59b576dd96 Author: Xu Chunyang <4550353+xuchunyang@users.noreply.github.com> Date: Mon Oct 28 23:29:03 2019 +0800 Support markdown for textdocument/hover () * eglot.el (eglot-client-capabilities): annouce markdown support for hover. (eglot--format-markup): Format hover info with Markdown. Fixes: https://github.com/joaotavora/eglot/issues/328 Copyright-paperwork-exempt: yes GitHub-reference: https://github.com/joaotavora/eglot/issues/329 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 285dc50ac8..1f7a396ef9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -489,7 +489,8 @@ treated as in `eglot-dbind'." t :json-false)) :contextSupport t) - :hover `(:dynamicRegistration :json-false) + :hover (list :dynamicRegistration :json-false + :contentFormat ["markdown" "plaintext"]) :signatureHelp (list :dynamicRegistration :json-false :signatureInformation `(:parameterInformation @@ -1080,7 +1081,9 @@ Doubles as an indicator of snippet support." (if (stringp markup) (list (string-trim markup) (intern "gfm-view-mode")) (list (plist-get markup :value) - major-mode)))) + (pcase (plist-get markup :kind) + ("markdown" 'gfm-view-mode) + (_ major-mode)))))) (with-temp-buffer (insert string) (ignore-errors (delay-mode-hooks (funcall mode))) commit 7cda807726c633df17a08d53308d8a59220ec083 Author: Xu Chunyang <4550353+xuchunyang@users.noreply.github.com> Date: Sun Oct 27 23:41:53 2019 +0800 Don't run mode hooks in eglot--format-markup * eglot.el (eglot--format-markup): Use delay-mode-hooks. Copyright-paperwork-exempt: yes diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b9496e166d..285dc50ac8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1083,7 +1083,9 @@ Doubles as an indicator of snippet support." major-mode)))) (with-temp-buffer (insert string) - (ignore-errors (funcall mode)) (font-lock-ensure) (buffer-string)))) + (ignore-errors (delay-mode-hooks (funcall mode))) + (font-lock-ensure) + (buffer-string)))) (defcustom eglot-ignored-server-capabilites (list) "LSP server capabilities that Eglot could use, but won't. commit 4f6e4dc7a162ef2808de55cedc349843feda3d59 Author: João Távora Date: Sat Oct 26 22:51:57 2019 +0100 Support workspace/configuration This helps users configure servers such as Gopls, which doesn't support didChangeConfiguration signals. * README.md (Per-project server configuration): New section. * eglot.el (eglot-workspace-configuration): Fix docstring. (eglot-signal-didChangeConfiguration): Rename a variable. (eglot-handle-request workspace/configuration): New request handler. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/326 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 75ce937610..b9496e166d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -218,6 +218,7 @@ let the buffer grow forever." (defvar eglot--lsp-interface-alist `( (CodeAction (:title) (:kind :diagnostics :edit :command)) + (ConfigurationItem () (:scopeUri :section)) (Command (:title :command) (:arguments)) (CompletionItem (:label) (:kind :detail :documentation :deprecated :preselect @@ -474,7 +475,8 @@ treated as in `eglot-dbind'." :executeCommand `(:dynamicRegistration :json-false) :workspaceEdit `(:documentChanges :json-false) :didChangeWatchedFiles `(:dynamicRegistration t) - :symbol `(:dynamicRegistration :json-false)) + :symbol `(:dynamicRegistration :json-false) + :configuration t) :textDocument (list :synchronization (list @@ -1655,9 +1657,9 @@ Records BEG, END and PRE-CHANGE-LENGTH locally." '((name . eglot--signal-textDocument/didChange))) (defvar-local eglot-workspace-configuration () - "Alist of (SETTING . VALUE) entries configuring the LSP server. -Setting should be a keyword, value can be any value that can be -converted to JSON.") + "Alist of (SECTION . VALUE) entries configuring the LSP server. +SECTION should be a keyword or a string, value can be anything +that can be converted to JSON.") (put 'eglot-workspace-configuration 'safe-local-variable 'listp) @@ -1669,12 +1671,34 @@ When called interactively, use the currently active server" server :workspace/didChangeConfiguration (list :settings - (cl-loop for (k . v) in eglot-workspace-configuration - collect (if (keywordp k) - k - (intern (format ":%s" k))) + (cl-loop for (section . v) in eglot-workspace-configuration + collect (if (keywordp section) + section + (intern (format ":%s" section))) collect v)))) +(cl-defmethod eglot-handle-request + (server (_method (eql workspace/configuration)) &key items) + "Handle server request workspace/configuration." + (apply #'vector + (mapcar + (eglot--lambda ((ConfigurationItem) scopeUri section) + (let* ((path (eglot--uri-to-path scopeUri))) + (when (file-directory-p path) + (with-temp-buffer + (let ((default-directory path)) + (setq-local major-mode (eglot--major-mode server)) + (hack-dir-local-variables-non-file-buffer) + (alist-get section eglot-workspace-configuration + nil nil + (lambda (wsection section) + (string= + (if (keywordp wsection) + (substring (symbol-name wsection) 1) + wsection) + section)))))))) + items))) + (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." (when eglot--recent-changes commit 615bd6ce378880ff3cb4bc7f2fc370495ab31017 Author: João Távora Date: Thu Oct 24 12:32:51 2019 +0100 Expand readme.md section on handling quirky servers Also remove explicit cquery support (cquery seems to be dead anyway). * README.md (Handling quirky servers): New section. * eglot.el (eglot-initialization-options eglot-cquery): Remove. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9a0960d7a9..75ce937610 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2511,19 +2511,6 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." "Handle notification window/progress" (setf (eglot--spinner server) (list id title done message))) - -;;; cquery-specific -;;; -(defclass eglot-cquery (eglot-lsp-server) () - :documentation "Cquery's C/C++ langserver.") - -(cl-defmethod eglot-initialization-options ((server eglot-cquery)) - "Passes through required cquery initialization options" - (let* ((root (car (project-roots (eglot--project server)))) - (cache (expand-file-name ".cquery_cached_index/" root))) - (list :cacheDirectory (file-name-as-directory cache) - :progressReportFrequencyMs -1))) - ;;; eclipse-jdt-specific ;;; commit 66f5a1a8ee247ee3f314f5c12f539ea75aeeb66b Author: João Távora Date: Tue Oct 22 12:18:53 2019 +0100 Unbreak imenu * eglot.el (eglot-imenu): Unbreak. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e29d888577..9a0960d7a9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2252,10 +2252,11 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." :deferred :textDocument/documentHighlight)))) eldoc-last-message) -(defun eglot-imenu (oldfun) - "EGLOT's `imenu-create-index-function' overriding OLDFUN." - (if (eglot--server-capable :documentSymbolProvider) - (let ((entries +(defun eglot-imenu () + "EGLOT's `imenu-create-index-function'." + (unless (eglot--server-capable :documentSymbolProvider) + (eglot--error "Server isn't a :documentSymbolProvider")) + (let ((entries (mapcar (eglot--lambda ((SymbolInformation) name kind location containerName) @@ -2283,7 +2284,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." elems))))) (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) entries))) - (funcall oldfun))) + ) (defun eglot--apply-text-edits (edits &optional version) "Apply EDITS for current buffer if at VERSION, or if it's nil." commit 3352f2b095c65e52962d53caa7c0f9f536063ebf Author: João Távora Date: Tue Oct 22 01:44:54 2019 +0100 Force company to align completion annotations in eglot sessions * eglot.el (eglot--managed-mode): force company-tooltip-align-annotations to t. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 92a35cab43..e29d888577 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1226,6 +1226,7 @@ For example, to keep your Company customization use (eglot--setq-saving xref-prompt-for-identifier nil) (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend t)) (eglot--setq-saving company-backends '(company-capf)) + (eglot--setq-saving company-tooltip-align-annotations t) (eglot--setq-saving imenu-create-index-function #'eglot-imenu) (flymake-mode 1) (eldoc-mode 1)) commit 72b9ef98652e00b47066dff4dca7934a89c16d5b Author: João Távora Date: Mon Oct 21 22:25:13 2019 +0100 Fix race condition when company-completing quickly For some reason, probably related to the way that Eglot tries to maintain the responsiveness of Company completion tooltips (see below), the user's explicit input will sometimes be surprisingly deleted by Company, leading to a horrible completion experience. This is sometimes hard to reproduce, but appears to match this description perfectly: https://github.com/joaotavora/eglot/issues/319#issuecomment-542955432 Fortunately, Company has a good fix for this, which is to pass `:company-require-match 'never` in the completion-at-point function. This is the fix applied in this commit. However, this line shouldn't be required since the default value for `company-require-match` is `company-explicit-action-p`, presumably meaning that the auto-deletion should never take place for characters typed by the user. This points to a bug in Company, or at least something which may have been exacerbated by the way that Eglot aggressively fetches completions from the server by passing :cancel-on-input to `jsonrpc-request`, discarding out-of-date replies. Perhaps that discarding step bears with it some side-effects that make the `company-explicit-action-p` test return `nil` instead of the correct `t`. Company interprets this as carte blanche to delete the last inserted character. * eglot.el (eglot-completion-at-point): Use :company-require-match 'never. GitHub-reference: per https://github.com/joaotavora/eglot/issues/319 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bd5f1ac8d4..92a35cab43 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2005,6 +2005,7 @@ is not active." (erase-buffer) (insert formatted) (current-buffer))))) + :company-require-match 'never :company-prefix-length (save-excursion (when (car bounds) (goto-char (car bounds))) commit 0816da8e78f1d0d643fdd1259a2f307c0817e316 Author: João Távora Date: Mon Oct 21 16:07:38 2019 +0100 Unbreak m-x vc-revert, which reverts preserving modes Unlike the normal revert-buffer command, vc-revert, doesn't re-apply the major mode, meaning it was missing a didOpen to pair with the didClose that is unconditionally sent on both commands. Needed to use the dynamic variable revert-buffer-preserve-modes, and, curiously, also forward-declare it to appease the byte compiler. * eglot.el (eglot--managed-mode): Use after-revert-hook. (revert-buffer-preserve-modes): Forward declare. (eglot--after-revert-hook): Signal didOpen when preserving-modes. (eglot--maybe-activate-editing-mode): Tweak comment. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e1c7b48c8d..bd5f1ac8d4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1214,6 +1214,7 @@ For example, to keep your Company customization use ;; Prepend "didClose" to the hook after the "onoff", so it will run first (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'before-revert-hook 'eglot--signal-textDocument/didClose nil t) + (add-hook 'after-revert-hook 'eglot--after-revert-hook nil t) (add-hook 'before-save-hook 'eglot--signal-textDocument/willSave nil t) (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) (add-hook 'xref-backend-functions 'eglot-xref-backend nil t) @@ -1233,6 +1234,7 @@ For example, to keep your Company customization use (remove-hook 'before-change-functions 'eglot--before-change t) (remove-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose t) (remove-hook 'before-revert-hook 'eglot--signal-textDocument/didClose t) + (remove-hook 'after-revert-hook 'eglot--after-revert-hook t) (remove-hook 'before-save-hook 'eglot--signal-textDocument/willSave t) (remove-hook 'after-save-hook 'eglot--signal-textDocument/didSave t) (remove-hook 'xref-backend-functions 'eglot-xref-backend t) @@ -1286,6 +1288,11 @@ Reset in `eglot--managed-mode-onoff'.") (defvar-local eglot--unreported-diagnostics nil "Unreported Flymake diagnostics for this buffer.") +(defvar revert-buffer-preserve-modes) +(defun eglot--after-revert-hook () + "Eglot's `after-revert-hook'." + (when revert-buffer-preserve-modes (eglot--signal-textDocument/didOpen))) + (defun eglot--maybe-activate-editing-mode (&optional server) "Maybe activate mode function `eglot--managed-mode'. If SERVER is supplied, do it only if BUFFER is managed by it. In @@ -1297,7 +1304,8 @@ that case, also signal textDocument/didOpen." :eglot "`eglot--cached-current-server' is non-nil, but it shouldn't be!\n\ Please report this as a possible bug.") (setq eglot--cached-current-server nil))) - ;; Called even when revert-buffer-in-progress-p + ;; Called when `revert-buffer-in-progress-p' is t but + ;; `revert-buffer-preserve-modes' is nil. (let* ((cur (and buffer-file-name (eglot--current-server))) (server (or (and (null server) cur) (and server (eq server cur) cur)))) (when server commit f71716e91427d893773ca68d5d3dec417a963571 Author: João Távora Date: Mon Oct 21 13:08:45 2019 +0100 (again): fix issue with replace-buffer-contents Manually calling the before/after change hooks for Emacs 26.1's buggy replace-buffer-contents must be done with absolute positions, not markers. * eglot.el (eglot--apply-text-edits): Call change hooks with buffer positions, not markers. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/259 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e78d2c4588..e1c7b48c8d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2307,7 +2307,9 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." ;; https://debbugs.gnu.org/cgi/bugreport.cgi?bug=32237 ;; https://debbugs.gnu.org/cgi/bugreport.cgi?bug=32278 (let ((inhibit-modification-hooks t) - (length (- end beg))) + (length (- end beg)) + (beg (marker-position beg)) + (end (marker-position end))) (run-hook-with-args 'before-change-functions beg end) (replace-buffer-contents temp) commit 703a09113cf3e21a01c6c14b86faa97233c8f0b1 Author: João Távora Date: Sun Oct 20 12:56:41 2019 +0100 * eglot.el (version): bump to 1.5 * NEWS.md: update. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7a22b5c33b..e78d2c4588 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 1.4 +;; Version: 1.5 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot commit 1002d7aeaf4306f2024e552a1ddca52961c2c56e Author: João Távora Date: Sat Oct 19 23:07:47 2019 +0100 Let user keep control of some variables during eglot sessions * NEWS.md: Mention new variable eglot-stay-out-of * eglot.el (eglot-stay-out-of): New variable. (eglot--setq-saving): Use it. (eglot--managed-mode): Use eglot--setq-saving for imenu. No need to remove 'eglot-flymake-backend from diagnostic functions. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/324 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 09c1461a06..7a22b5c33b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1172,8 +1172,33 @@ and just return it. PROMPT shouldn't end with a question mark." (defvar-local eglot--saved-bindings nil "Bindings saved by `eglot--setq-saving'.") +(defvar eglot-stay-out-of '() + "List of Emacs things that Eglot should try to stay of. +Before Eglot starts \"managing\" a particular buffer, it +opinionatedly sets some peripheral Emacs facilites, such as +Flymake, Xref and Company. These overriding settings help ensure +consistent Eglot behaviour and only stay in place until +\"managing\" stops (usually via `eglot-shutdown'), whereupon the +previous settings are restored. + +However, if you wish for Eglot to stay out of a particular Emacs +facility that you'd like to keep control of, add a string, a +symbol, or a regexp here that will be matched against the +variable's name, and Eglot will refrain from setting it. + +For example, to keep your Company customization use + +(add-to-list 'eglot-stay-out-of 'company)") + (defmacro eglot--setq-saving (symbol binding) - `(when (boundp ',symbol) + `(when (and (boundp ',symbol) + (not (cl-find (symbol-name ',symbol) + eglot-stay-out-of + :test + (lambda (s thing) + (let ((re (if (symbolp thing) (symbol-name thing) + thing))) + (string-match re s)))))) (push (cons ',symbol (symbol-value ',symbol)) eglot--saved-bindings) (setq-local ,symbol ,binding))) @@ -1200,11 +1225,10 @@ and just return it. PROMPT shouldn't end with a question mark." (eglot--setq-saving xref-prompt-for-identifier nil) (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend t)) (eglot--setq-saving company-backends '(company-capf)) - (add-function :around (local 'imenu-create-index-function) #'eglot-imenu) + (eglot--setq-saving imenu-create-index-function #'eglot-imenu) (flymake-mode 1) (eldoc-mode 1)) (t - (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) (remove-hook 'before-change-functions 'eglot--before-change t) (remove-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose t) commit 5690e3a659b36717971e4f62108f82bda1b871d0 Author: João Távora Date: Fri Oct 18 16:43:50 2019 +0100 Don't immediately request completions in eglot-completion-at-point Yet another adjustment to this function. According to the documentation of completion-at-point-functions, we should strive to make functions like eglot-completion-at-point "cheap to run". Requesting completion from the server immediately after calling the function goes against that. The reason we were doing it is that it might have helped compute more accurate "bounds" for the return value (START and END) from possible TextEdit completion items. But I've decided it's not worth the effort, at least for now. * eglot.el (eglot-completion-at-point): Request completions asynchronously. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cba8c341b3..09c1461a06 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1889,47 +1889,46 @@ is not active." (or (get-text-property 0 :sortText a) "") (or (get-text-property 0 :sortText b) "")))))) (metadata `(metadata . ((display-sort-function . ,sort-completions)))) - (response (jsonrpc-request server - :textDocument/completion - (eglot--CompletionParams) - :deferred :textDocument/completion - :cancel-on-input t)) - (items (append ; coerce to list - (if (vectorp response) response (plist-get response :items)) - nil)) + resp items (cached-proxies :none) (proxies - (mapcar (jsonrpc-lambda - (&rest item &key label insertText insertTextFormat - &allow-other-keys) - (let ((proxy - (cond ((and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)) - (string-trim-left label)) - (t - (or insertText (string-trim-left label)))))) - (unless (zerop (length proxy)) - (put-text-property 0 1 'eglot--lsp-item item proxy)) - proxy)) - items)) - (bounds - (cl-loop with probe = - (plist-get (plist-get (car items) :textEdit) :range) - for item in (cdr items) - for range = (plist-get (plist-get item :textEdit) :range) - unless (and range (equal range probe)) - return (bounds-of-thing-at-point 'symbol) - finally (cl-return (or (and probe - (eglot--range-region probe)) - (bounds-of-thing-at-point 'symbol)))))) + (lambda () + (if (listp cached-proxies) cached-proxies + (setq resp + (jsonrpc-request server + :textDocument/completion + (eglot--CompletionParams) + :deferred :textDocument/completion + :cancel-on-input t)) + (setq items (append + (if (vectorp resp) resp (plist-get resp :items)) + nil)) + (setq cached-proxies + (mapcar + (jsonrpc-lambda + (&rest item &key label insertText insertTextFormat + &allow-other-keys) + (let ((proxy + (cond ((and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)) + (string-trim-left label)) + (t + (or insertText (string-trim-left label)))))) + (unless (zerop (length item)) + (put-text-property 0 1 'eglot--lsp-item item proxy)) + proxy)) + items))))) + (bounds (bounds-of-thing-at-point 'symbol))) (list (or (car bounds) (point)) (or (cdr bounds) (point)) (lambda (probe pred action) (cond ((eq action 'metadata) metadata) ; metadata - ((eq action 'lambda) (member probe proxies)) ; test-completion + ((eq action 'lambda) ; test-completion + (member probe (funcall proxies))) ((eq (car-safe action) 'boundaries) nil) ; boundaries - ((and (null action) (member probe proxies) t)) ; try-completion + ((and (null action) ; try-completion + (member probe (funcall proxies)) t)) ((eq action t) ; all-completions (cl-remove-if-not (lambda (proxy) @@ -1938,7 +1937,7 @@ is not active." (and (or (null pred) (funcall pred proxy)) (string-prefix-p probe (or filterText proxy) completion-ignore-case)))) - proxies)))) + (funcall proxies))))) :annotation-function (lambda (proxy) (eglot--dbind ((CompletionItem) detail kind insertTextFormat) @@ -1993,7 +1992,8 @@ is not active." ;; buffer, `proxy' won't have any properties. A ;; lookup should fix that (github#148) (get-text-property - 0 'eglot--lsp-item (cl-find proxy proxies :test #'string=))) + 0 'eglot--lsp-item + (cl-find proxy (funcall proxies) :test #'string=))) (let ((snippet-fn (and (eql insertTextFormat 2) (eglot--snippet-expansion-fn)))) (cond (textEdit commit 21c2bb18d898b5d70750688a51346b2b867e3265 Author: João Távora Date: Wed Oct 16 23:13:09 2019 +0100 Protect against zero-length completions Apparently the Vue Language Server sends such things (see https://github.com/joaotavora/eglot/issues/319). * eglot.el (eglot-completion-at-point): Protect against zero-length completions. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index aa64fe6a2a..cba8c341b3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1907,7 +1907,8 @@ is not active." (string-trim-left label)) (t (or insertText (string-trim-left label)))))) - (put-text-property 0 1 'eglot--lsp-item item proxy) + (unless (zerop (length proxy)) + (put-text-property 0 1 'eglot--lsp-item item proxy)) proxy)) items)) (bounds commit 5d1c5c64b17a127daa0e69a14bce1e68362f36ef Author: João Távora Date: Wed Oct 16 19:25:51 2019 +0100 Don't choke on single-location reply to td/definition * eglot.el (eglot--lsp-xrefs-for-method): Accept non-vector Location. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/321 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bb5d5f8169..aa64fe6a2a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1791,14 +1791,15 @@ Try to visit the target file for a richer summary line." (cadr (split-string (symbol-name method) "/")))))) (eglot--error "Sorry, this server doesn't do %s" method)) - (eglot--collecting-xrefs (collect) - (mapc - (eglot--lambda ((Location) uri range) - (collect (eglot--xref-make (symbol-at-point) uri range))) - (jsonrpc-request - (eglot--current-server-or-lose) method (append - (eglot--TextDocumentPositionParams) - extra-params))))) + (let ((response + (jsonrpc-request + (eglot--current-server-or-lose) + method (append (eglot--TextDocumentPositionParams) extra-params)))) + (eglot--collecting-xrefs (collect) + (mapc + (eglot--lambda ((Location) uri range) + (collect (eglot--xref-make (symbol-at-point) uri range))) + (if (vectorp response) response (list response)))))) (cl-defun eglot--lsp-xref-helper (method &key extra-params capability ) "Helper for `eglot-find-declaration' & friends." commit 1aa5d0b528996dd56ab20a04235d3cd032c6e616 Author: João Távora Date: Wed Oct 16 16:46:03 2019 +0100 Unbreak eglot--setq-saving if symbol is unbound * eglot.el (eglot--setq-saving): check if symbol is bound diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0220969e6f..bb5d5f8169 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1173,9 +1173,10 @@ and just return it. PROMPT shouldn't end with a question mark." "Bindings saved by `eglot--setq-saving'.") (defmacro eglot--setq-saving (symbol binding) - `(progn (push (cons ',symbol (symbol-value ',symbol)) - eglot--saved-bindings) - (setq-local ,symbol ,binding))) + `(when (boundp ',symbol) + (push (cons ',symbol (symbol-value ',symbol)) + eglot--saved-bindings) + (setq-local ,symbol ,binding))) (define-minor-mode eglot--managed-mode "Mode for source buffers managed by some EGLOT project." commit 024bbfc6163acbd2545f74d4537f801ae7bf012f Author: João Távora Date: Wed Oct 16 16:29:41 2019 +0100 Use of company-capf backend in eglot-managed buffers * eglot.el (company-backends): forward-declare (eglot--managed-mode): Force company-backends to company-capf diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9460e4b3c6..0220969e6f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -71,6 +71,8 @@ (require 'filenotify) (require 'ert) (require 'array) +(defvar company-backends) ; forward-declare, but don't require company yet + ;;; User tweakable stuff @@ -1196,6 +1198,7 @@ and just return it. PROMPT shouldn't end with a question mark." (eglot--setq-saving eldoc-documentation-function #'eglot-eldoc-function) (eglot--setq-saving xref-prompt-for-identifier nil) (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend t)) + (eglot--setq-saving company-backends '(company-capf)) (add-function :around (local 'imenu-create-index-function) #'eglot-imenu) (flymake-mode 1) (eldoc-mode 1)) commit c85ee68e29a986aab4c6c3a825cd40506667b884 Author: João Távora Date: Wed Oct 16 14:16:52 2019 +0100 Play along with lsp's filtertext hacks Reworked important parts of eglot-completion-at-point. One of the tasks was to cleanup the nomenclature so it's easier to spot how LSP and Emacs's views of completion techniques differ. When reading this rather long function, remember an "item" is a plist representing the LSP completionItem object, and "proxy" is a propertized string that Emacs's frontends will use to represent that completion. When the completion is close to done, the :exit-function is called, to potentially rework the inserted text so that the final result might be quite different from the proxy (it might be a snippet, or even a suprising text edit). The most important change in this commit reworks the way the completion "bounds" are calculated in the buffer. This is the region that Emacs needs to know that is being targeted for the completion. A server can specify this region by using textEdit-based completions all consistently pointing to the same range. If it does so, Emacs will use that region instead of its own understanding of symbol boundaries (provided by thingatpt.el and syntax tables). To implement server-side completion filtering, the server can also provide a filterText "cookie" in each completion, which, when prefix-matched to the intended region, selects or rejects the completion. Given the feedback in https://github.com/microsoft/language-server-protocol/issues/651, we have no choice but to play along with that inneficient and grotesque strategy to implement flex-style matching. Like ever in LSP, we do so while being backward-compatible to all previously supported behaviour. * eglot.el (eglot-completion-at-point): rework. GitHub-reference: close https://github.com/joaotavora/eglot/issues/235 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ce3705a3a3..9460e4b3c6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1874,57 +1874,69 @@ is not active." (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." - (let* ((bounds (bounds-of-thing-at-point 'symbol)) - (server (eglot--current-server-or-lose)) - (completion-capability (eglot--server-capable :completionProvider)) - (sort-completions (lambda (completions) - (sort completions - (lambda (a b) - (string-lessp - (or (get-text-property 0 :sortText a) "") - (or (get-text-property 0 :sortText b) "")))))) - (metadata `(metadata . ((display-sort-function . ,sort-completions)))) - completions) - (when completion-capability + ;; Commit logs for this function help understand what's going on. + (when-let (completion-capability (eglot--server-capable :completionProvider)) + (let* ((server (eglot--current-server-or-lose)) + (sort-completions (lambda (completions) + (sort completions + (lambda (a b) + (string-lessp + (or (get-text-property 0 :sortText a) "") + (or (get-text-property 0 :sortText b) "")))))) + (metadata `(metadata . ((display-sort-function . ,sort-completions)))) + (response (jsonrpc-request server + :textDocument/completion + (eglot--CompletionParams) + :deferred :textDocument/completion + :cancel-on-input t)) + (items (append ; coerce to list + (if (vectorp response) response (plist-get response :items)) + nil)) + (proxies + (mapcar (jsonrpc-lambda + (&rest item &key label insertText insertTextFormat + &allow-other-keys) + (let ((proxy + (cond ((and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)) + (string-trim-left label)) + (t + (or insertText (string-trim-left label)))))) + (put-text-property 0 1 'eglot--lsp-item item proxy) + proxy)) + items)) + (bounds + (cl-loop with probe = + (plist-get (plist-get (car items) :textEdit) :range) + for item in (cdr items) + for range = (plist-get (plist-get item :textEdit) :range) + unless (and range (equal range probe)) + return (bounds-of-thing-at-point 'symbol) + finally (cl-return (or (and probe + (eglot--range-region probe)) + (bounds-of-thing-at-point 'symbol)))))) (list (or (car bounds) (point)) (or (cdr bounds) (point)) - (lambda (comp pred action) + (lambda (probe pred action) (cond - ((eq action 'metadata) metadata) ; metadata - ((eq action 'lambda) (member comp completions)) ; test-completion - ((eq (car-safe action) 'boundaries) nil) ; boundaries - ((and (null action) (member comp completions) t)) ; try-completion - ((eq action t) ; all-completions - (let* ((resp (jsonrpc-request server - :textDocument/completion - (eglot--CompletionParams) - :deferred :textDocument/completion - :cancel-on-input t)) - (items (if (vectorp resp) resp (plist-get resp :items)))) - (setq - completions - (all-completions ; <-stuck with prefix-comp because LSP - comp - (mapcar - (jsonrpc-lambda - (&rest all &key label insertText insertTextFormat - &allow-other-keys) - (let ((completion - (cond ((and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)) - (string-trim-left label)) - (t - (or insertText (string-trim-left label)))))) - (put-text-property 0 1 'eglot--lsp-completion - all completion) - completion)) - items) - pred)))))) + ((eq action 'metadata) metadata) ; metadata + ((eq action 'lambda) (member probe proxies)) ; test-completion + ((eq (car-safe action) 'boundaries) nil) ; boundaries + ((and (null action) (member probe proxies) t)) ; try-completion + ((eq action t) ; all-completions + (cl-remove-if-not + (lambda (proxy) + (let* ((item (get-text-property 0 'eglot--lsp-item proxy)) + (filterText (plist-get item :filterText))) + (and (or (null pred) (funcall pred proxy)) + (string-prefix-p + probe (or filterText proxy) completion-ignore-case)))) + proxies)))) :annotation-function - (lambda (obj) + (lambda (proxy) (eglot--dbind ((CompletionItem) detail kind insertTextFormat) - (get-text-property 0 'eglot--lsp-completion obj) + (get-text-property 0 'eglot--lsp-item proxy) (let* ((detail (and (stringp detail) (not (string= detail "")) detail)) @@ -1939,10 +1951,9 @@ is not active." (eglot--snippet-expansion-fn) " (snippet)")))))) :company-doc-buffer - (lambda (obj) + (lambda (proxy) (let* ((documentation - (let ((lsp-comp - (get-text-property 0 'eglot--lsp-completion obj))) + (let ((lsp-comp (get-text-property 0 'eglot--lsp-item proxy))) (or (plist-get lsp-comp :documentation) (and (eglot--server-capable :completionProvider :resolveProvider) @@ -1966,46 +1977,45 @@ is not active." (cl-coerce (cl-getf completion-capability :triggerCharacters) 'list)) (line-beginning-position)))) :exit-function - (lambda (comp _status) - (let ((comp (if (get-text-property 0 'eglot--lsp-completion comp) - comp - ;; When selecting from the *Completions* - ;; buffer, `comp' won't have any properties. A - ;; lookup should fix that (github#148) - (cl-find comp completions :test #'string=)))) - (eglot--dbind ((CompletionItem) insertTextFormat - insertText - textEdit - additionalTextEdits) - (get-text-property 0 'eglot--lsp-completion comp) - (let ((snippet-fn (and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)))) - (cond (textEdit - ;; Undo the just the completed bit. If before - ;; completion the buffer was "foo.b" and now is - ;; "foo.bar", `comp' will be "bar". We want to - ;; delete only "ar" (`comp' minus the symbol - ;; whose bounds we've calculated before) - ;; (github#160). - (delete-region (+ (- (point) (length comp)) - (if bounds (- (cdr bounds) (car bounds)) 0)) - (point)) - (eglot--dbind ((TextEdit) range newText) textEdit - (pcase-let ((`(,beg . ,end) (eglot--range-region range))) - (delete-region beg end) - (goto-char beg) - (funcall (or snippet-fn #'insert) newText))) - (when (cl-plusp (length additionalTextEdits)) - (eglot--apply-text-edits additionalTextEdits))) - (snippet-fn - ;; A snippet should be inserted, but using plain - ;; `insertText'. This requires us to delete the - ;; whole completion, since `insertText' is the full - ;; completion's text. - (delete-region (- (point) (length comp)) (point)) - (funcall snippet-fn insertText)))) - (eglot--signal-textDocument/didChange) - (eglot-eldoc-function)))))))) + (lambda (proxy _status) + (eglot--dbind ((CompletionItem) insertTextFormat + insertText + textEdit + additionalTextEdits) + (or (get-text-property 0 'eglot--lsp-item proxy) + ;; When selecting from the *Completions* + ;; buffer, `proxy' won't have any properties. A + ;; lookup should fix that (github#148) + (get-text-property + 0 'eglot--lsp-item (cl-find proxy proxies :test #'string=))) + (let ((snippet-fn (and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)))) + (cond (textEdit + ;; Undo (yes, undo) the newly inserted completion. + ;; If before completion the buffer was "foo.b" and + ;; now is "foo.bar", `proxy' will be "bar". We + ;; want to delete only "ar" (`proxy' minus the + ;; symbol whose bounds we've calculated before) + ;; (github#160). + (delete-region (+ (- (point) (length proxy)) + (if bounds (- (cdr bounds) (car bounds)) 0)) + (point)) + (eglot--dbind ((TextEdit) range newText) textEdit + (pcase-let ((`(,beg . ,end) (eglot--range-region range))) + (delete-region beg end) + (goto-char beg) + (funcall (or snippet-fn #'insert) newText))) + (when (cl-plusp (length additionalTextEdits)) + (eglot--apply-text-edits additionalTextEdits))) + (snippet-fn + ;; A snippet should be inserted, but using plain + ;; `insertText'. This requires us to delete the + ;; whole completion, since `insertText' is the full + ;; completion's text. + (delete-region (- (point) (length proxy)) (point)) + (funcall snippet-fn insertText)))) + (eglot--signal-textDocument/didChange) + (eglot-eldoc-function))))))) (defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") commit ca9649c6b39abe75e90822936b214d6d4eb10ec3 Author: João Távora Date: Wed Oct 16 09:49:09 2019 +0100 Always filter completions client-side by prefix Prefix completion is all we get in LSP because there are some servers that send *all* completions everytime. This is horrible, but it's the currently defined behaviour. See https://github.com/microsoft/language-server-protocol/issues/651. * eglot.el (eglot-completion-at-point): Use all-completions. GitHub-reference: per https://github.com/joaotavora/eglot/issues/319 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f757a8ac4f..ce3705a3a3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1889,7 +1889,7 @@ is not active." (list (or (car bounds) (point)) (or (cdr bounds) (point)) - (lambda (comp _pred action) + (lambda (comp pred action) (cond ((eq action 'metadata) metadata) ; metadata ((eq action 'lambda) (member comp completions)) ; test-completion @@ -1904,20 +1904,23 @@ is not active." (items (if (vectorp resp) resp (plist-get resp :items)))) (setq completions - (mapcar - (jsonrpc-lambda - (&rest all &key label insertText insertTextFormat - &allow-other-keys) - (let ((completion - (cond ((and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)) - (string-trim-left label)) - (t - (or insertText (string-trim-left label)))))) - (put-text-property 0 1 'eglot--lsp-completion - all completion) - completion)) - items)))))) + (all-completions ; <-stuck with prefix-comp because LSP + comp + (mapcar + (jsonrpc-lambda + (&rest all &key label insertText insertTextFormat + &allow-other-keys) + (let ((completion + (cond ((and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)) + (string-trim-left label)) + (t + (or insertText (string-trim-left label)))))) + (put-text-property 0 1 'eglot--lsp-completion + all completion) + completion)) + items) + pred)))))) :annotation-function (lambda (obj) (eglot--dbind ((CompletionItem) detail kind insertTextFormat) commit 320356985a9315453c3551ee835a22979c543dd5 Author: João Távora Date: Tue Oct 15 18:42:33 2019 +0100 Fix bug in workspace/didchangewatchedfiles * eglot.el (eglot-register-capability): Fix a bug and a couple of warnings. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 970f024db1..f757a8ac4f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2415,8 +2415,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (changed 2) (deleted 3))))))) ((eq action 'renamed) - (handle-event '(desc 'deleted file)) - (handle-event '(desc 'created file1))))))) + (handle-event `(,desc 'deleted ,file)) + (handle-event `(,desc 'created ,file1))))))) (unwind-protect (progn (dolist (dir glob-dirs) commit 471fff254f6babaad608da4383f6d94be10e665e Author: Tom Tromey Date: Tue Oct 15 10:32:57 2019 -0600 Add support for the ada language server * eglot.el (eglot-server-programs): Add ada-mode entry. * README.md (Connecting to a server): Add Ada entry. GitHub-reference: close https://github.com/joaotavora/eglot/issues/316 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 421941f195..970f024db1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -100,7 +100,8 @@ language-server/bin/php-language-server.php")) "languageserver::run()")) (java-mode . eglot--eclipse-jdt-contact) (dart-mode . ("dart_language_server")) - (elixir-mode . ("language_server.sh"))) + (elixir-mode . ("language_server.sh")) + (ada-mode . ("ada_language_server"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated commit c2e084bc23f3e717605d54b334ead0816f6d445d Author: João Távora Date: Sat Oct 12 01:57:00 2019 +0100 Fix eglot-completion-at-point to work with bare completion-at-point Fixes https://github.com/joaotavora/eglot/issues/313, fixes https://github.com/joaotavora/eglot/issues/311, fixes https://github.com/joaotavora/eglot/issues/279 As is well known, LSP's and Emacs's completion mechanics don't fit very well together, mostly because Emacs expects completion to be a something of a pure function of a string argument and LSP treats as a function of a concrete buffer position. A further complication arises because some completion frontends like "bare" completion-at-point make Emacs modify the buffer's contents during the completion process, while other (notably company-mode) don't do that. Thus, 'eglot-completion-at-point' must take extra care to answer to the questions listed in the "(elisp)Programmed Completion" info node based on its (quite hacky) "completions" local var and _not_ based on the intermediate buffer contents. That var is also used to cache the last LSP response and allow the :exit-function callback to retrieve much more than just the completion text in In yet another related problem, :exit-function won't be called at all with completion-at-point if the completion table doesn't answer properly to test-completion. A previous use of completion-table-dynamic was found to be unsuitable here: we must answer all the requests separately. * eglot.el (eglot-completion-at-point): Rework. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7b0e3e28a6..421941f195 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1883,38 +1883,40 @@ is not active." (or (get-text-property 0 :sortText a) "") (or (get-text-property 0 :sortText b) "")))))) (metadata `(metadata . ((display-sort-function . ,sort-completions)))) - strings) + completions) (when completion-capability (list (or (car bounds) (point)) (or (cdr bounds) (point)) - (lambda (string pred action) - (if (eq action 'metadata) metadata - (funcall - (completion-table-dynamic - (lambda (_ignored) - (let* ((resp (jsonrpc-request server - :textDocument/completion - (eglot--CompletionParams) - :deferred :textDocument/completion - :cancel-on-input t)) - (items (if (vectorp resp) resp (plist-get resp :items)))) - (setq - strings - (mapcar - (jsonrpc-lambda - (&rest all &key label insertText insertTextFormat - &allow-other-keys) - (let ((completion - (cond ((and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)) - (string-trim-left label)) - (t - (or insertText (string-trim-left label)))))) - (put-text-property 0 1 'eglot--lsp-completion all completion) - completion)) - items))))) - string pred action))) + (lambda (comp _pred action) + (cond + ((eq action 'metadata) metadata) ; metadata + ((eq action 'lambda) (member comp completions)) ; test-completion + ((eq (car-safe action) 'boundaries) nil) ; boundaries + ((and (null action) (member comp completions) t)) ; try-completion + ((eq action t) ; all-completions + (let* ((resp (jsonrpc-request server + :textDocument/completion + (eglot--CompletionParams) + :deferred :textDocument/completion + :cancel-on-input t)) + (items (if (vectorp resp) resp (plist-get resp :items)))) + (setq + completions + (mapcar + (jsonrpc-lambda + (&rest all &key label insertText insertTextFormat + &allow-other-keys) + (let ((completion + (cond ((and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)) + (string-trim-left label)) + (t + (or insertText (string-trim-left label)))))) + (put-text-property 0 1 'eglot--lsp-completion + all completion) + completion)) + items)))))) :annotation-function (lambda (obj) (eglot--dbind ((CompletionItem) detail kind insertTextFormat) @@ -1966,7 +1968,7 @@ is not active." ;; When selecting from the *Completions* ;; buffer, `comp' won't have any properties. A ;; lookup should fix that (github#148) - (cl-find comp strings :test #'string=)))) + (cl-find comp completions :test #'string=)))) (eglot--dbind ((CompletionItem) insertTextFormat insertText textEdit commit 0aaaea5ae9be07e979cbcd636226a088c1650017 Author: João Távora Date: Sun Oct 13 22:32:52 2019 +0100 Unbreak xref-find-definitions * eglot-tests.el (basic-xref): New test. * eglot.el (eglot--collecting-xrefs): Add an edebug spec. (eglot--lsp-xrefs-for-method): Actually collect xref. (xref-backend-apropos): Fix indentation slightly. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/318 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9d5c546cef..7b0e3e28a6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1724,6 +1724,7 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." (cl-defmacro eglot--collecting-xrefs ((collector) &rest body) "Sort and handle xrefs collected with COLLECTOR in BODY." + (declare (indent 1) (debug (sexp &rest form))) (let ((collected (cl-gensym "collected"))) `(unwind-protect (let (,collected) @@ -1786,13 +1787,13 @@ Try to visit the target file for a richer summary line." "/")))))) (eglot--error "Sorry, this server doesn't do %s" method)) (eglot--collecting-xrefs (collect) - (mapc - (eglot--lambda ((Location) uri range) - (eglot--xref-make (symbol-at-point) uri range)) - (jsonrpc-request - (eglot--current-server-or-lose) method (append - (eglot--TextDocumentPositionParams) - extra-params))))) + (mapc + (eglot--lambda ((Location) uri range) + (collect (eglot--xref-make (symbol-at-point) uri range))) + (jsonrpc-request + (eglot--current-server-or-lose) method (append + (eglot--TextDocumentPositionParams) + extra-params))))) (cl-defun eglot--lsp-xref-helper (method &key extra-params capability ) "Helper for `eglot-find-declaration' & friends." @@ -1829,13 +1830,13 @@ Try to visit the target file for a richer summary line." (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) (eglot--collecting-xrefs (collect) - (mapc - (eglot--lambda ((SymbolInformation) name location) - (eglot--dbind ((Location) uri range) location - (collect (eglot--xref-make name uri range)))) - (jsonrpc-request (eglot--current-server-or-lose) - :workspace/symbol - `(:query ,pattern)))))) + (mapc + (eglot--lambda ((SymbolInformation) name location) + (eglot--dbind ((Location) uri range) location + (collect (eglot--xref-make name uri range)))) + (jsonrpc-request (eglot--current-server-or-lose) + :workspace/symbol + `(:query ,pattern)))))) (defun eglot-format-buffer () "Format contents of current buffer." commit c848af80d5fc58bf5560a87c192366e467abf56c Author: João Távora Date: Wed Oct 9 19:30:27 2019 +0100 Misc improvements to the xref glue code * eglot.el (eglot-xref-backend): Don't check capability here. (eglot--collecting-xrefs): Reworked from eglot--handling-xrefs. (eglot--handling-xrefs): Remove. (xref-backend-apropos, eglot--lsp-xrefs-for-method): Use eglot--collecting-xrefs. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 51914d9e4e..9d5c546cef 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1714,9 +1714,7 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." (funcall report-fn (cdr eglot--unreported-diagnostics)) (setq eglot--unreported-diagnostics nil))) -(defun eglot-xref-backend () - "EGLOT xref backend." - (when (eglot--server-capable :definitionProvider) 'eglot)) +(defun eglot-xref-backend () "EGLOT xref backend." 'eglot) (defvar eglot--temp-location-buffers (make-hash-table :test #'equal) "Helper variable for `eglot--handling-xrefs'.") @@ -1724,12 +1722,16 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." (defvar eglot-xref-lessp-function #'ignore "Compare two `xref-item' objects for sorting.") -(defmacro eglot--handling-xrefs (&rest body) - "Properly sort and handle xrefs produced and returned by BODY." - `(unwind-protect - (sort (progn ,@body) eglot-xref-lessp-function) - (maphash (lambda (_uri buf) (kill-buffer buf)) eglot--temp-location-buffers) - (clrhash eglot--temp-location-buffers))) +(cl-defmacro eglot--collecting-xrefs ((collector) &rest body) + "Sort and handle xrefs collected with COLLECTOR in BODY." + (let ((collected (cl-gensym "collected"))) + `(unwind-protect + (let (,collected) + (cl-flet ((,collector (xref) (push xref ,collected))) + ,@body) + (sort ,collected eglot-xref-lessp-function)) + (maphash (lambda (_uri buf) (kill-buffer buf)) eglot--temp-location-buffers) + (clrhash eglot--temp-location-buffers)))) (defun eglot--xref-make (name uri range) "Like `xref-make' but with LSP's NAME, URI and RANGE. @@ -1783,8 +1785,8 @@ Try to visit the target file for a richer summary line." (cadr (split-string (symbol-name method) "/")))))) (eglot--error "Sorry, this server doesn't do %s" method)) - (eglot--handling-xrefs - (mapcar + (eglot--collecting-xrefs (collect) + (mapc (eglot--lambda ((Location) uri range) (eglot--xref-make (symbol-at-point) uri range)) (jsonrpc-request @@ -1792,9 +1794,12 @@ Try to visit the target file for a richer summary line." (eglot--TextDocumentPositionParams) extra-params))))) -(defun eglot--lsp-xref-helper (method) +(cl-defun eglot--lsp-xref-helper (method &key extra-params capability ) "Helper for `eglot-find-declaration' & friends." - (let ((eglot--lsp-xref-refs (eglot--lsp-xrefs-for-method method))) + (let ((eglot--lsp-xref-refs (eglot--lsp-xrefs-for-method + method + :extra-params extra-params + :capability capability))) (xref-find-references "LSP identifier at point."))) (defun eglot-find-declaration () @@ -1823,11 +1828,11 @@ Try to visit the target file for a richer summary line." (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) - (eglot--handling-xrefs - (mapcar + (eglot--collecting-xrefs (collect) + (mapc (eglot--lambda ((SymbolInformation) name location) (eglot--dbind ((Location) uri range) location - (eglot--xref-make name uri range))) + (collect (eglot--xref-make name uri range)))) (jsonrpc-request (eglot--current-server-or-lose) :workspace/symbol `(:query ,pattern)))))) commit 9bb0331d04635002972b5208db6394ad931a4b86 Author: João Távora Date: Sun Oct 6 16:10:33 2019 +0100 Rework and correct major part of xref glue code See comments of https://github.com/joaotavora/eglot/pull/314. Up to now, xref-backend-indentifier-completion-table was a gross hack that only worked sometimes. It relied on some fugly gymnastics to cache a response from :textDocument/documentSymbol and somehow used that information to build a completion table. But it doesn't work well. Summarily, LSP doesn't lend itself well to the xref interface of prompting for an arbitrary identifier and then go look for whichever type of references of that identifier. All the LSP :textDocument/{definition,references,implementation,...} methods expect to know the exact context of the search the user is about to perform, in the form of a document location. That conflicts with the xref "arbitrary string" requirement. Therefore, the slightly limited, but much more correct way, for Eglot to function is to override the user's preference of xref-prompt-for-identifier, temporarily setting it to nil in eglot--managed-mode (ideally, though, xref-prompt-for-identifier should be a function of the backend.) Later on, a possibly better behaved identifier completion table can be built on top of the :workspace/symbol LSP method. * eglot.el (xref-backend-identifier-at-point): Rewrite. (eglot--lsp-xrefs-for-method): New helper. (eglot--lsp-xref-helper): Use eglot--lsp-xrefs-for-method. (eglot--xref-definitions-method): Delete. (eglot--lsp-xref-refs): New variable. (xref-backend-references, xref-backend-definitions): Use eglot--lsp-xrefs-for-method. (eglot--managed-mode): Set xref-prompt-for-identifier to nil. (eglot--xref-reset-known-symbols, eglot--xref-known-symbols): Delete (xref-backend-identifier-completion-table): Nullify. (eglot-find-declaration, eglot-find-implementation) (eglot-find-typeDefinition): Use eglot--lsp-xref-helper. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e3ead96361..51914d9e4e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1193,6 +1193,7 @@ and just return it. PROMPT shouldn't end with a question mark." (add-hook 'post-self-insert-hook 'eglot--post-self-insert-hook nil t) (add-hook 'pre-command-hook 'eglot--pre-command-hook nil t) (eglot--setq-saving eldoc-documentation-function #'eglot-eldoc-function) + (eglot--setq-saving xref-prompt-for-identifier nil) (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend t)) (add-function :around (local 'imenu-create-index-function) #'eglot-imenu) (flymake-mode 1) @@ -1717,16 +1718,6 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." "EGLOT xref backend." (when (eglot--server-capable :definitionProvider) 'eglot)) -(defvar eglot--xref-known-symbols nil) - -(defun eglot--xref-reset-known-symbols (&rest _dummy) - "Reset `eglot--xref-reset-known-symbols'. -DUMMY is ignored." - (setq eglot--xref-known-symbols nil)) - -(advice-add 'xref-find-definitions :after #'eglot--xref-reset-known-symbols) -(advice-add 'xref-find-references :after #'eglot--xref-reset-known-symbols) - (defvar eglot--temp-location-buffers (make-hash-table :test #'equal) "Helper variable for `eglot--handling-xrefs'.") @@ -1771,102 +1762,64 @@ Try to visit the target file for a richer summary line." (xref-make summary (xref-make-file-location file line column)))) (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) - (when (eglot--server-capable :documentSymbolProvider) - (let ((server (eglot--current-server-or-lose)) - (text-id (eglot--TextDocumentIdentifier))) - (completion-table-with-cache - (lambda (string) - (setq eglot--xref-known-symbols - (mapcar - (eglot--lambda - ((SymbolInformation) name kind location containerName) - (propertize name - :textDocumentPositionParams - (list :textDocument text-id - :position (plist-get - (plist-get location :range) - :start)) - :locations (vector location) - :kind kind - :containerName containerName)) - (jsonrpc-request server - :textDocument/documentSymbol - `(:textDocument ,text-id)))) - (all-completions string eglot--xref-known-symbols)))))) + (eglot--error "cannot (yet) provide reliable completion table for LSP symbols")) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) - (when-let ((symatpt (symbol-at-point))) - (propertize (symbol-name symatpt) - :textDocumentPositionParams - (eglot--TextDocumentPositionParams)))) - -(defvar eglot--xref-definitions-method :textDocument/definition - "The LSP method to map xref-find-definitions call.") + ;; JT@19/10/09: This is a totally dummy identifier that isn't even + ;; passed to LSP. The reason for this particular wording is to + ;; construct a readable message "No references for LSP identifier at + ;; point.". See http://github.com/joaotavora/eglot/issues/314 + "LSP identifier at point.") + +(defvar eglot--lsp-xref-refs nil + "`xref' objects for overriding `xref-backend-references''s.") + +(cl-defun eglot--lsp-xrefs-for-method (method &key extra-params capability) + "Make `xref''s for METHOD, EXTRA-PARAMS, check CAPABILITY." + (unless (eglot--server-capable + (or capability + (intern + (format ":%sProvider" + (cadr (split-string (symbol-name method) + "/")))))) + (eglot--error "Sorry, this server doesn't do %s" method)) + (eglot--handling-xrefs + (mapcar + (eglot--lambda ((Location) uri range) + (eglot--xref-make (symbol-at-point) uri range)) + (jsonrpc-request + (eglot--current-server-or-lose) method (append + (eglot--TextDocumentPositionParams) + extra-params))))) + +(defun eglot--lsp-xref-helper (method) + "Helper for `eglot-find-declaration' & friends." + (let ((eglot--lsp-xref-refs (eglot--lsp-xrefs-for-method method))) + (xref-find-references "LSP identifier at point."))) (defun eglot-find-declaration () - "Find the declaration for the identifier at point. -See `xref-find-definitions' and `xref-prompt-for-identifier'." + "Find declaration for SYM, the identifier at point." (interactive) - (eglot--find-location 'declaration)) + (eglot--lsp-xref-helper :textDocument/declaration)) (defun eglot-find-implementation () - "Find the implementation for the identifier at point. -See `xref-find-definitions' and `xref-prompt-for-identifier'." + "Find implementation for SYM, the identifier at point." (interactive) - (eglot--find-location 'implementation)) + (eglot--lsp-xref-helper :textDocument/implementation)) (defun eglot-find-typeDefinition () - "Find the type definition for the identifier at point. -See `xref-find-definitions' and `xref-prompt-for-identifier'." + "Find type definition for SYM, the identifier at point." (interactive) - (eglot--find-location 'typeDefinition)) - -(defun eglot--find-location (kind) - (let* ((method-name (symbol-name kind)) - (method (intern (concat ":textDocument/" method-name))) - (capability (intern (concat ":" method-name "Provider")))) - (if (eglot--server-capable capability) - (let ((eglot--xref-definitions-method method)) - (call-interactively #'xref-find-definitions)) - (eglot--error "Server is not a %sProvider" method-name)))) - -(cl-defmethod xref-backend-definitions ((_backend (eql eglot)) identifier) - (let* ((rich-identifier - (car (member identifier eglot--xref-known-symbols))) - (definitions - (if rich-identifier - (get-text-property 0 :locations rich-identifier) - (jsonrpc-request (eglot--current-server-or-lose) - eglot--xref-definitions-method - (get-text-property - 0 :textDocumentPositionParams identifier)))) - (locations - (and definitions - (if (vectorp definitions) definitions (vector definitions))))) - (eglot--handling-xrefs - (mapcar (eglot--lambda ((Location) uri range) - (eglot--xref-make identifier uri range)) - locations)))) - -(cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) - (when (eglot--server-capable :referencesProvider) - (let ((params - (or (get-text-property 0 :textDocumentPositionParams identifier) - (let ((rich (car (member identifier eglot--xref-known-symbols)))) - (and rich - (get-text-property 0 :textDocumentPositionParams rich)))))) - (unless params - (eglot--error "Don' know where %s is in the workspace!" identifier)) - (eglot--handling-xrefs - (mapcar - (eglot--lambda ((Location) uri range) - (eglot--xref-make identifier uri range)) - (jsonrpc-request (eglot--current-server-or-lose) - :textDocument/references - (append - params - (list :context - (list :includeDeclaration t))))))))) + (eglot--lsp-xref-helper :textDocument/typeDefinition)) + +(cl-defmethod xref-backend-definitions ((_backend (eql eglot)) _identifier) + (eglot--lsp-xrefs-for-method :textDocument/definition)) + +(cl-defmethod xref-backend-references ((_backend (eql eglot)) _identifier) + (or + eglot--lsp-xref-refs + (eglot--lsp-xrefs-for-method + :textDocument/references :extra-params `(:context (:includeDeclaration t))))) (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) commit 83ed46b6a30ae4067df1436f6aef6f539adf5e77 Author: Felicián Németh Date: Thu Oct 3 21:11:18 2019 +0200 Support goto-{declaration, implementation, typedefinition} Closes https://github.com/joaotavora/eglot/issues/302. * eglot.el (eglot--xref-definitions-method): New variable. (xref-backend-definitions): Use it. (eglot-find-declaration, eglot-find-implementation, eglot-find-typeDefinition): New functions. * README.md (Language features): Add new capabilities. * eglot.el (eglot-client-capabilities): Add new capabilities. (eglot-ignored-server-capabilites): Add new capability. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1dc5711398..e3ead96361 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -491,6 +491,9 @@ treated as in `eglot-dbind'." (:labelOffsetSupport t))) :references `(:dynamicRegistration :json-false) :definition `(:dynamicRegistration :json-false) + :declaration `(:dynamicRegistration :json-false) + :implementation `(:dynamicRegistration :json-false) + :typeDefinition `(:dynamicRegistration :json-false) :documentSymbol (list :dynamicRegistration :json-false :symbolKind `(:valueSet @@ -1090,6 +1093,7 @@ under cursor." (const :tag "Go to definition" :definitionProvider) (const :tag "Go to type definition" :typeDefinitionProvider) (const :tag "Go to implementation" :implementationProvider) + (const :tag "Go to declaration" :implementationProvider) (const :tag "Find references" :referencesProvider) (const :tag "Highlight symbols automatically" :documentHighlightProvider) (const :tag "List symbols in buffer" :documentSymbolProvider) @@ -1796,6 +1800,36 @@ Try to visit the target file for a richer summary line." :textDocumentPositionParams (eglot--TextDocumentPositionParams)))) +(defvar eglot--xref-definitions-method :textDocument/definition + "The LSP method to map xref-find-definitions call.") + +(defun eglot-find-declaration () + "Find the declaration for the identifier at point. +See `xref-find-definitions' and `xref-prompt-for-identifier'." + (interactive) + (eglot--find-location 'declaration)) + +(defun eglot-find-implementation () + "Find the implementation for the identifier at point. +See `xref-find-definitions' and `xref-prompt-for-identifier'." + (interactive) + (eglot--find-location 'implementation)) + +(defun eglot-find-typeDefinition () + "Find the type definition for the identifier at point. +See `xref-find-definitions' and `xref-prompt-for-identifier'." + (interactive) + (eglot--find-location 'typeDefinition)) + +(defun eglot--find-location (kind) + (let* ((method-name (symbol-name kind)) + (method (intern (concat ":textDocument/" method-name))) + (capability (intern (concat ":" method-name "Provider")))) + (if (eglot--server-capable capability) + (let ((eglot--xref-definitions-method method)) + (call-interactively #'xref-find-definitions)) + (eglot--error "Server is not a %sProvider" method-name)))) + (cl-defmethod xref-backend-definitions ((_backend (eql eglot)) identifier) (let* ((rich-identifier (car (member identifier eglot--xref-known-symbols))) @@ -1803,7 +1837,7 @@ Try to visit the target file for a richer summary line." (if rich-identifier (get-text-property 0 :locations rich-identifier) (jsonrpc-request (eglot--current-server-or-lose) - :textDocument/definition + eglot--xref-definitions-method (get-text-property 0 :textDocumentPositionParams identifier)))) (locations commit 374ce4e29f1b025feed77aa2cb7e90a67864760e Author: ambihelical Date: Sat May 11 11:41:52 2019 -0700 Allow user to set idle time to wait before processing changes * eglot.el (eglot-send-changes-idle-time): New defcustom. (eglot--after-change): Use it. Co-authored-by: João Távora Copyright-paperwork-exempt: yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/258 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0a9da05b1c..1dc5711398 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -174,6 +174,10 @@ as 0, i.e. don't block at all." "If non-nil, shut down server after killing last managed buffer." :type 'boolean) +(defcustom eglot-send-changes-idle-time 0.5 + "Don't tell server of changes before Emacs's been idle for this many seconds." + :type 'number) + (defcustom eglot-events-buffer-size 2000000 "Control the size of the Eglot events buffer. If a number, don't let the buffer grow larger than that many @@ -1591,10 +1595,11 @@ Records BEG, END and PRE-CHANGE-LENGTH locally." (let ((buf (current-buffer))) (setq eglot--change-idle-timer (run-with-idle-timer - 0.5 nil (lambda () (eglot--with-live-buffer buf - (when eglot--managed-mode - (eglot--signal-textDocument/didChange) - (setq eglot--change-idle-timer nil)))))))) + eglot-send-changes-idle-time + nil (lambda () (eglot--with-live-buffer buf + (when eglot--managed-mode + (eglot--signal-textDocument/didChange) + (setq eglot--change-idle-timer nil)))))))) ;; HACK! Launching a deferred sync request with outstanding changes is a ;; bad idea, since that might lead to the request never having a commit 1c8d062c5f474ef33ffb056ab73cafaeaae2d7fd Author: João Távora Date: Sat Oct 5 12:32:23 2019 +0100 Much less noisy mode line * eglot.el (eglot--mode-line-format): Simplify. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/236 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cfcfaa7d68..0a9da05b1c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1314,7 +1314,7 @@ Uses THING, FACE, DEFS and PREPEND." (nick (and server (eglot--project-nickname server))) (pending (and server (hash-table-count (jsonrpc--request-continuations server)))) - (`(,_id ,doing ,done-p ,detail) (and server (eglot--spinner server))) + (`(,_id ,doing ,done-p ,_detail) (and server (eglot--spinner server))) (last-error (and server (jsonrpc-last-error server)))) (append `(,(eglot--mode-line-props "eglot" 'eglot-mode-line nil)) @@ -1332,15 +1332,13 @@ Uses THING, FACE, DEFS and PREPEND." (format "An error occured: %s\n" (plist-get last-error :message))))) ,@(when (and doing (not done-p)) - `("/" ,(eglot--mode-line-props - (format "%s%s" doing - (if detail (format ":%s" detail) "")) - 'compilation-mode-line-run '()))) + `("/" ,(eglot--mode-line-props doing + 'compilation-mode-line-run '()))) ,@(when (cl-plusp pending) `("/" ,(eglot--mode-line-props - (format "%d outstanding requests" pending) 'warning + (format "%d" pending) 'warning '((mouse-3 eglot-forget-pending-continuations - "fahgettaboudit")))))))))) + "forget pending continuations")))))))))) (add-to-list 'mode-line-misc-info `(eglot--managed-mode (" [" eglot--mode-line-format "] "))) commit 0e7e66fe2739a203dd2f2e5138e27f8b8dd04fb0 Author: João Távora Date: Mon Sep 30 18:06:48 2019 +0200 Unbreak elm language server which does use :triggercharacters Only query completionProvider -> triggerCharacter information if the server has provided it. Elm's, and probaly other's, do not provide it, which doesn't mean they don't support completion. * eglot.el (eglot-completion-at-point): Check that completion capability is a list before treating it like one. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/285 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d1a1a3d0b2..cfcfaa7d68 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1958,10 +1958,11 @@ is not active." :company-prefix-length (save-excursion (when (car bounds) (goto-char (car bounds))) - (looking-back - (regexp-opt - (cl-coerce (cl-getf completion-capability :triggerCharacters) 'list)) - (line-beginning-position))) + (when (listp completion-capability) + (looking-back + (regexp-opt + (cl-coerce (cl-getf completion-capability :triggerCharacters) 'list)) + (line-beginning-position)))) :exit-function (lambda (comp _status) (let ((comp (if (get-text-property 0 'eglot--lsp-completion comp) commit fe37d7e3ce796a2bee21af6a0b34de59aef1daca Author: João Távora Date: Sat Oct 5 11:46:08 2019 +0100 Revert "treat null/nil server capabilities as false" This reverts commit 645bcfc6e57181c39dae1f238758e76c1759a765. A capability of "null" is downright invalid, and must NOT be mistaken for a value of "{}" (which indicates the presence of the capability) or "False" (which indicates its asence). See https://github.com/microsoft/language-server-protocol/issues/830#issuecomment-537849292 for a clarification from the LSP maintainer. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ef7f8f0a3e..d1a1a3d0b2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1113,9 +1113,6 @@ under cursor." for probe = (plist-member caps feat) if (not probe) do (cl-return nil) if (eq (cadr probe) :json-false) do (cl-return nil) - ;; If the server specifies null as the value of the capability, it - ;; makes sense to treat it like false. - if (null (cadr probe)) do (cl-return nil) if (not (listp (cadr probe))) do (cl-return (if more nil (cadr probe))) finally (cl-return (or (cadr probe) t))))) commit 471434e068816b7d28616aeefa9fe53a5130eca1 Author: Vladimir Panteleev Date: Fri Oct 4 10:13:32 2019 +0000 Don't send dummy json object in "initialized" notification () Eglot uses a JSON object { __dummy__ : true } as a placeholder instead of the empty object {}. It does this out of necessity, since encoding an empty object can't currently be easily using the current jsonrpc.el library. However, this also causes the parameter to be actually sent to the server. Since the JSON-RPC specification states "The names MUST match exactly, including case, to the method's expected parameters" this is non-conforming to the protocol. The LSP specification does not seem to indicate how servers should handle method calls with parameters they do not support. As such, ignoring the parameter, or reporting an error, or crashing all seem to be "valid" behaviors as far as the specification is concerned. We can avoid this by using an empty hash table instead of a dummy parameter. Currently, an empty hash table is the only Emacs Lisp object which jsonrpc.el serializes to an empty JSON object in jsonrpc--json-encode. * eglot.el (eglot--connect): Use make-hash-table instead of dummy object. Copyright-paperwork-exempt: yes GitHub-reference: https://github.com/joaotavora/eglot/issues/312 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cf2f371acc..ef7f8f0a3e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -848,7 +848,7 @@ This docstring appeases checkdoc, that's all." (push server (gethash project eglot--servers-by-project)) (setf (eglot--capabilities server) capabilities) - (jsonrpc-notify server :initialized `(:__dummy__ t)) + (jsonrpc-notify server :initialized (make-hash-table)) (dolist (buffer (buffer-list)) (with-current-buffer buffer (eglot--maybe-activate-editing-mode server))) commit ad1cc3b1c21188c5fe3264d6b4e2b1f93a9161b6 Author: Ingo Lohmar Date: Wed Oct 2 18:05:15 2019 +0200 Optionally shutdown after killing last buffer of managed project () This should close issue https://github.com/joaotavora/eglot/issues/217, also cf. https://github.com/joaotavora/eglot/issues/270. * eglot.el (eglot-autoshutdown): New defcustom. (eglot--managed-mode-onoff): Shutdown if so configured and no managed buffers left. Co-authored-by: João Távora GitHub-reference: https://github.com/joaotavora/eglot/issues/309 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 279fbeedad..cf2f371acc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -170,6 +170,10 @@ as 0, i.e. don't block at all." :type '(choice (boolean :tag "Whether to inhibit autoreconnection") (integer :tag "Number of seconds"))) +(defcustom eglot-autoshutdown nil + "If non-nil, shut down server after killing last managed buffer." + :type 'boolean) + (defcustom eglot-events-buffer-size 2000000 "Control the size of the Eglot events buffer. If a number, don't let the buffer grow larger than that many @@ -1224,7 +1228,11 @@ Reset in `eglot--managed-mode-onoff'.") (setq eglot--cached-current-server nil) (when server (setf (eglot--managed-buffers server) - (delq buf (eglot--managed-buffers server))))))))) + (delq buf (eglot--managed-buffers server))) + (when (and eglot-autoshutdown + (not (eglot--shutdown-requested server)) + (not (eglot--managed-buffers server))) + (eglot-shutdown server)))))))) (defun eglot--current-server () "Find the current logical EGLOT server." commit 14f69da41711f2826af60a511155b2d1a5025e4a Author: Ingo Lohmar Date: Wed Oct 2 18:03:48 2019 +0200 On buffer kill, first send didclose then teardown local structures It used to be the reverse way around, which doesn't make sense. * eglot.el (eglot-managed-mode): Fix order in `kill-buffer-hook' Co-authored-by: João Távora diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 723ac3bc89..279fbeedad 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1172,8 +1172,9 @@ and just return it. PROMPT shouldn't end with a question mark." (eglot--managed-mode (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'before-change-functions 'eglot--before-change nil t) - (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'kill-buffer-hook 'eglot--managed-mode-onoff nil t) + ;; Prepend "didClose" to the hook after the "onoff", so it will run first + (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'before-revert-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'before-save-hook 'eglot--signal-textDocument/willSave nil t) (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) commit 051bc27a101f0c9f3376194ec53842c87d31e94e Author: galeo Date: Thu Sep 26 20:04:13 2019 +0800 Also use signature label offsets for parameter info According to the LSP specification, a parameter of a callable-signature has a label and a optional doc-commet. The label of a parameter information is either a string or an inclusive start and exclusive end offsets within its containing signature label. Previously, this was only taken in account for highlighting the parameter in the definition signature. * eglot.el (eglot--sig-info): Handle signature label offsets when printing the signature parameter information. Copyright-paperwork-exempt: yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/272 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3155135b30..723ac3bc89 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2055,7 +2055,10 @@ is not active." (goto-char (point-max)) (insert "\n" (propertize - label 'face 'eldoc-highlight-function-argument) + (if (stringp label) + label + (apply #'buffer-substring (mapcar #'1+ label))) + 'face 'eldoc-highlight-function-argument) ": " (eglot--format-markup documentation)))))) (buffer-string)))) when moresigs concat "\n")) commit 2b3e8deb4fe4b15b3c88f94fc25ed01db043737a Merge: c537770301 645bcfc6e5 Author: Felicián Németh Date: Tue Sep 24 17:47:58 2019 +0200 Merge pull request from jorams/nil-capabilities-as-false Treat null/nil server capabilities as false GitHub-reference: https://github.com/joaotavora/eglot/issues/298 commit c53777030187d7273e05b84e242f2b3b5cfe513f Author: Ingo Lohmar Date: Mon Sep 23 20:05:55 2019 +0200 Use gopls server as the default for go () Developers recommend it: see https://github.com/sourcegraph/go-langserver/blob/master/README.md * eglot (eglot-server-programs): Use gopls. * README.md: mention gopls instead of go-langserver. GitHub-reference: https://github.com/joaotavora/eglot/issues/304 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b87d5bb7c3..d160b57700 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -95,8 +95,7 @@ language-server/bin/php-language-server.php")) :autoport)) (haskell-mode . ("hie-wrapper")) (kotlin-mode . ("kotlin-language-server")) - (go-mode . ("go-langserver" "-mode=stdio" - "-gocodecompletion")) + (go-mode . ("gopls")) ((R-mode ess-r-mode) . ("R" "--slave" "-e" "languageserver::run()")) (java-mode . eglot--eclipse-jdt-contact) commit 645bcfc6e57181c39dae1f238758e76c1759a765 Author: Joram Schrijver Date: Fri Sep 13 11:48:10 2019 +0200 Treat null/nil server capabilities as false Some language servers may specify null for some capabilities in the list of server capabilities. This does not conform to the specification, but treating it as false is more reasonable than treating it as true. A current example is the PHP language server. which specifies null for every capability it does not handle, like documentHighlightProvider. This would cause Eglot to send constant textDocument/documentHighlight requests, which all timed out. * eglot.el (eglot--server-capable): Change the handling of null values for capabilities to treat them as false instead of true. Copyright-paperwork-exempt: yes diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 23d53edc2b..845f0294df 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1101,6 +1101,9 @@ under cursor." for probe = (plist-member caps feat) if (not probe) do (cl-return nil) if (eq (cadr probe) :json-false) do (cl-return nil) + ;; If the server specifies null as the value of the capability, it + ;; makes sense to treat it like false. + if (null (cadr probe)) do (cl-return nil) if (not (listp (cadr probe))) do (cl-return (if more nil (cadr probe))) finally (cl-return (or (cadr probe) t))))) commit e62b6395ee5284825abccb3bde4255ed894e0c7a Author: Felicián Németh Date: Tue Sep 10 15:31:16 2019 +0200 Change the default of eglot-move-to-column-function Previous default (move-to-column) works on visual columns, the LSP specification and the new default (eglot-move-to-column) use "real" columns. Fixes https://github.com/joaotavora/eglot/issues/293 and https://github.com/joaotavora/eglot/issues/297. * eglot.el (eglot-move-to-column): New function. (eglot-move-to-column-function): Use it as default. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 23d53edc2b..b87d5bb7c3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -988,7 +988,7 @@ for all others.") :character (progn (when pos (goto-char pos)) (funcall eglot-current-column-function))))) -(defvar eglot-move-to-column-function #'move-to-column +(defvar eglot-move-to-column-function #'eglot-move-to-column "Function to move to a column reported by the LSP server. According to the standard, LSP column/character offsets are based @@ -999,7 +999,16 @@ where X is a multi-byte character, it actually means `b', not For buffers managed by fully LSP-compliant servers, this should be set to `eglot-move-to-lsp-abiding-column', and -`move-to-column' (the default) for all others.") +`eglot-move-to-column' (the default) for all others.") + +(defun eglot-move-to-column (column) + "Move to COLUMN without closely following the LSP spec." + ;; We cannot use `move-to-column' here, because it moves to *visual* + ;; columns, which can be different from LSP columns in case of + ;; `whitespace-mode', `prettify-symbols-mode', etc. (github#296, + ;; github#297) + (goto-char (min (+ (line-beginning-position) column) + (line-end-position)))) (defun eglot-move-to-lsp-abiding-column (column) "Move to COLUMN abiding by the LSP spec." commit 4a1d60dd6d6340b215d3f2b9376619cf92e8a2d8 Author: David Florness Date: Sun Aug 18 18:09:13 2019 -0600 Require array package to use current-line () The jsonrpc package (one of eglot's dependencies) recently updated and removed the line requiring the array package. Since current-line is provided by array and is used by eglot, require array explicitly. Here's jsonrpc's guilty commit: https://git.savannah.gnu.org/cgit/emacs.git/commit/lisp/jsonrpc.el?id=c676444a43e4634c1f98ec286b5bd9e46b23216b Copyright-paperwork-exempt: Yes * eglot.el (array): Require it. GitHub-reference: https://github.com/joaotavora/eglot/issues/294 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6078e97cdc..23d53edc2b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -70,6 +70,7 @@ (require 'jsonrpc) (require 'filenotify) (require 'ert) +(require 'array) ;;; User tweakable stuff commit f18137499d46ab4e2210d8190d09cea84834f8d3 Author: Jürgen Hötzel Date: Mon Aug 12 22:13:47 2019 +0200 Expand directory watcher globs containing ** () Previously, if the server requested a glob pattern like foo/**/* to be watched, we would just error. Now we watch foo/bar/ and foo/baz/ as if the server had requested those two watchers instead of just the one with the **. As a limitation, the implementation of file-expand-wildcards doesn't fully handle ** globstars (** matches at most one path segment). * eglot.el (eglot-register-capability workspace/didChangeWatchedFiles): Use file-expand-wildcards to make a ** glob into multiple **-less globs. GitHub-reference: https://github.com/joaotavora/eglot/issues/293 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a80be22e6b..6078e97cdc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2371,7 +2371,10 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (let* (success (globs (mapcar (eglot--lambda ((FileSystemWatcher) globPattern) globPattern) - watchers))) + watchers)) + (glob-dirs + (delete-dups (mapcar #'file-name-directory + (mapcan #'file-expand-wildcards globs))))) (cl-labels ((handle-event (event) @@ -2394,13 +2397,14 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (handle-event '(desc 'deleted file)) (handle-event '(desc 'created file1))))))) (unwind-protect - (progn (dolist (dir (delete-dups (mapcar #'file-name-directory globs))) - (push (file-notify-add-watch dir '(change) #'handle-event) - (gethash id (eglot--file-watches server)))) - (setq - success - `(:message ,(format "OK, watching %s watchers" - (length watchers))))) + (progn + (dolist (dir glob-dirs) + (push (file-notify-add-watch dir '(change) #'handle-event) + (gethash id (eglot--file-watches server)))) + (setq + success + `(:message ,(format "OK, watching %s directories in %s watchers" + (length glob-dirs) (length watchers))))) (unless success (eglot-unregister-capability server method id)))))) commit 3a9221c7b82ff7ce5d76ff1c791743255440d740 Author: Jürgen Hötzel Date: Thu Jul 18 21:36:56 2019 +0200 Fix invalid guess for php language server () * eglot.el (eglot-server-programs): Change the position of the php language server, otherwise it will always be hidden by the c-mode server (php-mode is derived from c-mode). Copyright-paperwork-exempt: yes GitHub-reference: https://github.com/joaotavora/eglot/issues/288 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 25c8fbe707..a80be22e6b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -84,14 +84,14 @@ typescript-mode) . ("javascript-typescript-stdio")) (sh-mode . ("bash-language-server" "start")) + (php-mode . ("php" "vendor/felixfbecker/\ +language-server/bin/php-language-server.php")) ((c++-mode c-mode) . ("ccls")) ((caml-mode tuareg-mode reason-mode) . ("ocaml-language-server" "--stdio")) (ruby-mode . ("solargraph" "socket" "--port" :autoport)) - (php-mode . ("php" "vendor/felixfbecker/\ -language-server/bin/php-language-server.php")) (haskell-mode . ("hie-wrapper")) (kotlin-mode . ("kotlin-language-server")) (go-mode . ("go-langserver" "-mode=stdio" commit 6ed1f50cde74a4ae1a8225abbac37e6dd007da3f Author: haqle314 <16577773+haqle314@users.noreply.github.com> Date: Tue Jul 2 16:58:41 2019 -0500 Fix a typo * eglot.el (eglot--lsp-position-to-point): fix eglot--warn call Copyright-paperwork-exempt: yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/273 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index aec975c1e6..25c8fbe707 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1024,7 +1024,7 @@ If optional MARKER, return a marker instead" (col (plist-get pos-plist :character))) (unless (wholenump col) (eglot--warn - :eglot "Caution: LSP server sent invalid character position %s. Using 0 instead." + "Caution: LSP server sent invalid character position %s. Using 0 instead." col) (setq col 0)) (funcall eglot-move-to-column-function col))) commit d79232df76b8fc583cf340703d448fb54bb1bbc3 Author: Ingo Lohmar Date: Thu Jun 27 18:56:45 2019 +0200 Simplify eldoc usage () * eglot-tests.el (hover-after-completions): Protect test. Rewrite docstring. * eglot.el (eglot--managed-mode): Don't mess with eldoc-message-function. (eglot--eldoc-hint): Remove. (eglot--update-doc): Rename and rewrite from eglot--eldoc-message. (eglot-eldoc-function): Don't set eglot--eldoc-hint. Call eglot--update-doc. GitHub-reference: https://github.com/joaotavora/eglot/issues/269 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d4705551f4..aec975c1e6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1172,7 +1172,6 @@ and just return it. PROMPT shouldn't end with a question mark." (add-hook 'pre-command-hook 'eglot--pre-command-hook nil t) (eglot--setq-saving eldoc-documentation-function #'eglot-eldoc-function) (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend t)) - (add-function :before-until (local 'eldoc-message-function) #'eglot--eldoc-message) (add-function :around (local 'imenu-create-index-function) #'eglot-imenu) (flymake-mode 1) (eldoc-mode 1)) @@ -1189,7 +1188,6 @@ and just return it. PROMPT shouldn't end with a question mark." (remove-hook 'change-major-mode-hook #'eglot--managed-mode-onoff t) (remove-hook 'post-self-insert-hook 'eglot--post-self-insert-hook t) (remove-hook 'pre-command-hook 'eglot--pre-command-hook t) - (remove-function (local 'eldoc-message-function) #'eglot--eldoc-message) (cl-loop for (var . saved-binding) in eglot--saved-bindings do (set (make-local-variable var) saved-binding)) (setq eglot--current-flymake-report-fn nil)))) @@ -2050,8 +2048,6 @@ is not active." (buffer-string)))) when moresigs concat "\n")) -(defvar eglot--eldoc-hint nil) - (defvar eglot--help-buffer nil) (defun eglot--help-buffer () @@ -2100,29 +2096,30 @@ Buffer is displayed with `display-buffer', which obeys `display-buffer-alist' & friends." :type 'boolean) -(defun eglot--eldoc-message (format &rest args) - (when format - (let ((string (apply #'format format args))) ;; FIXME: overworking? - (when (or (eq t eglot-put-doc-in-help-buffer) - (and eglot-put-doc-in-help-buffer - (funcall eglot-put-doc-in-help-buffer string))) - (with-current-buffer (eglot--help-buffer) - (rename-buffer (format "*eglot-help for %s*" eglot--eldoc-hint)) - (let ((inhibit-read-only t)) - (erase-buffer) - (insert string) - (goto-char (point-min)) - (if eglot-auto-display-help-buffer - (display-buffer (current-buffer)) - (unless (get-buffer-window (current-buffer)) - (eglot--message - "%s\n(...truncated. Full help is in `%s')" - (truncate-string-to-width - (replace-regexp-in-string "\\(.*\\)\n.*" "\\1" string) - (frame-width) nil nil "...") - (buffer-name eglot--help-buffer)))) - (help-mode) - t)))))) +(defun eglot--update-doc (string hint) + "Put updated documentation STRING where it belongs. +Honours `eglot-put-doc-in-help-buffer'. HINT is used to +potentially rename EGLOT's help buffer." + (if (or (eq t eglot-put-doc-in-help-buffer) + (and eglot-put-doc-in-help-buffer + (funcall eglot-put-doc-in-help-buffer string))) + (with-current-buffer (eglot--help-buffer) + (rename-buffer (format "*eglot-help for %s*" hint)) + (let ((inhibit-read-only t)) + (erase-buffer) + (insert string) + (goto-char (point-min)) + (if eglot-auto-display-help-buffer + (display-buffer (current-buffer)) + (unless (get-buffer-window (current-buffer)) + (eglot--message + "%s\n(...truncated. Full help is in `%s')" + (truncate-string-to-width + (replace-regexp-in-string "\\(.*\\)\n.*" "\\1" string) + (frame-width) nil nil "...") + (buffer-name eglot--help-buffer)))) + (help-mode))) + (eldoc-message string))) (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function. @@ -2132,7 +2129,6 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (position-params (eglot--TextDocumentPositionParams)) sig-showing (thing-at-point (thing-at-point 'symbol))) - (setq eglot--eldoc-hint thing-at-point) (cl-macrolet ((when-buffer-window (&body body) ; notice the exception when testing with `ert' `(when (or (get-buffer-window buffer) (ert-running-test)) @@ -2146,10 +2142,10 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (when-buffer-window (when (cl-plusp (length signatures)) (setq sig-showing t) - (let ((eglot--eldoc-hint thing-at-point)) - (eldoc-message (eglot--sig-info signatures - activeSignature - activeParameter)))))) + (eglot--update-doc (eglot--sig-info signatures + activeSignature + activeParameter) + thing-at-point)))) :deferred :textDocument/signatureHelp)) (when (eglot--server-capable :hoverProvider) (jsonrpc-async-request @@ -2160,8 +2156,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (when-let (info (and contents (eglot--hover-info contents range))) - (let ((eglot--eldoc-hint thing-at-point)) - (eldoc-message info)))))) + (eglot--update-doc info thing-at-point))))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) (jsonrpc-async-request commit 8f80ae14559389ae49843efb98bbb4d5ef09cf41 Author: João Távora Date: Thu Jun 27 17:55:05 2019 +0100 Leniently handle invalid positions sent by some servers * eglot.el (eglot--lsp-position-to-point): Leniently squash invalid character positions to 0. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/273 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ca63723881..d4705551f4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1020,8 +1020,14 @@ If optional MARKER, return a marker instead" (forward-line (min most-positive-fixnum (plist-get pos-plist :line))) (unless (eobp) ;; if line was excessive leave point at eob - (let ((tab-width 1)) - (funcall eglot-move-to-column-function (plist-get pos-plist :character)))) + (let ((tab-width 1) + (col (plist-get pos-plist :character))) + (unless (wholenump col) + (eglot--warn + :eglot "Caution: LSP server sent invalid character position %s. Using 0 instead." + col) + (setq col 0)) + (funcall eglot-move-to-column-function col))) (if marker (copy-marker (point-marker)) (point)))) (defun eglot--path-to-uri (path) commit 08d5a9dfde8b42407aa7cf81deef588270cb4a10 Author: Akash Hiremath Date: Sun May 12 16:17:37 2019 +0530 Add built-in support for elixir's elixir-ls () * README.md: add elixir-ls. * eglot.el (eglot-server-programs): add elixir-ls. Copyright-paperwork-exempt: yes GitHub-reference: https://github.com/joaotavora/eglot/issues/264 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0c2f4e97bd..ca63723881 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -99,7 +99,8 @@ language-server/bin/php-language-server.php")) ((R-mode ess-r-mode) . ("R" "--slave" "-e" "languageserver::run()")) (java-mode . eglot--eclipse-jdt-contact) - (dart-mode . ("dart_language_server"))) + (dart-mode . ("dart_language_server")) + (elixir-mode . ("language_server.sh"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated commit faa0500ff769e14365492ea6b1cc80c7e9d23725 Author: João Távora Date: Thu May 9 11:38:58 2019 +0100 Work around a bug in emacs's change detection When using capitalize-word, or any case-fiddling function, before-change-functions will record e.g. the whole word's start and end, even though only the first character has changed. Not only is this longer than needed but also conflicts with what we get in after-change-functions, which records just the one-char-long change. Also, if the word didn't need any fiddling at all then before-change-function will run but after-change-functions won't: an "orphan" before-change will erroneously be sent to the server. * eglot.el (eglot--after-change): Detect problematic case and fix change description. (eglot--before-change): Store markers of changed region. (eglot--signal-textDocument/didChange): Weed out orphan changes. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/259 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b9fc25c01e..0c2f4e97bd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1530,26 +1530,42 @@ THINGS are either registrations or unregisterations (sic)." (defvar-local eglot--change-idle-timer nil "Idle timer for didChange signals.") -(defun eglot--before-change (start end) - "Hook onto `before-change-functions' with START and END." - ;; Records START and END, crucially convert them into LSP - ;; (line/char) positions before that information is lost (because - ;; the after-change thingy doesn't know if newlines were - ;; deleted/added) +(defun eglot--before-change (beg end) + "Hook onto `before-change-functions' with BEG and END." (when (listp eglot--recent-changes) - (push `(,(eglot--pos-to-lsp-position start) - ,(eglot--pos-to-lsp-position end)) + ;; Records BEG and END, crucially convert them into LSP + ;; (line/char) positions before that information is lost (because + ;; the after-change thingy doesn't know if newlines were + ;; deleted/added). Also record markers of BEG and END + ;; (github#259) + (push `(,(eglot--pos-to-lsp-position beg) + ,(eglot--pos-to-lsp-position end) + (,beg . ,(copy-marker beg)) + (,end . ,(copy-marker end))) eglot--recent-changes))) -(defun eglot--after-change (start end pre-change-length) +(defun eglot--after-change (beg end pre-change-length) "Hook onto `after-change-functions'. -Records START, END and PRE-CHANGE-LENGTH locally." +Records BEG, END and PRE-CHANGE-LENGTH locally." (cl-incf eglot--versioned-identifier) - (if (and (listp eglot--recent-changes) - (null (cddr (car eglot--recent-changes)))) - (setf (cddr (car eglot--recent-changes)) - `(,pre-change-length ,(buffer-substring-no-properties start end))) - (setf eglot--recent-changes :emacs-messup)) + (pcase (and (listp eglot--recent-changes) + (car eglot--recent-changes)) + (`(,lsp-beg ,lsp-end + (,b-beg . ,b-beg-marker) + (,b-end . ,b-end-marker)) + ;; github#259: With `upcase-word' or somesuch, + ;; `before-change-functions' always records the whole word's + ;; `beg' and `end'. Not only is this longer than needed but + ;; conflicts with the args received here. Detect this using + ;; markers recorded earlier and `pre-change-len', then fix it. + (when (and (= b-end b-end-marker) (= b-beg b-beg-marker) + (not (zerop pre-change-length))) + (setq lsp-end (eglot--pos-to-lsp-position end) + lsp-beg (eglot--pos-to-lsp-position beg))) + (setcar eglot--recent-changes + `(,lsp-beg ,lsp-end ,pre-change-length + ,(buffer-substring-no-properties beg end)))) + (_ (setf eglot--recent-changes :emacs-messup))) (when eglot--change-idle-timer (cancel-timer eglot--change-idle-timer)) (let ((buf (current-buffer))) (setq eglot--change-idle-timer @@ -1609,6 +1625,12 @@ When called interactively, use the currently active server" (buffer-substring-no-properties (point-min) (point-max))))) (cl-loop for (beg end len text) in (reverse eglot--recent-changes) + ;; github#259: `capitalize-word' and commands based + ;; on `casify_region' will cause multiple duplicate + ;; empty entries in `eglot--before-change' calls + ;; without an `eglot--after-change' reciprocal. + ;; Weed them out here. + when (numberp len) vconcat `[,(list :range `(:start ,beg :end ,end) :rangeLength len :text text)])))) (setq eglot--recent-changes nil) commit c90f33dc212259220aea1cd9edf73cf1fe2c0826 Author: João Távora Date: Thu May 9 19:49:31 2019 +0100 Fix case when eglot-put-doc-in-help-buffer is nil * eglot.el (eglot--eldoc-message): Check eglot-put-doc-in-help-buffer. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/263 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f5d81e4aa4..b9fc25c01e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2075,7 +2075,8 @@ Buffer is displayed with `display-buffer', which obeys (when format (let ((string (apply #'format format args))) ;; FIXME: overworking? (when (or (eq t eglot-put-doc-in-help-buffer) - (funcall eglot-put-doc-in-help-buffer string)) + (and eglot-put-doc-in-help-buffer + (funcall eglot-put-doc-in-help-buffer string))) (with-current-buffer (eglot--help-buffer) (rename-buffer (format "*eglot-help for %s*" eglot--eldoc-hint)) (let ((inhibit-read-only t)) commit 68d95c8125327b7cef0ce496a70abb407bbf9b7f Author: Michal Krzywkowski Date: Wed May 8 13:33:34 2019 +0200 Only consider eglot's own diagnostics in eglot-code-actions * eglot.el (eglot-code-actions): Filter out non-eglot diagnostics before sending a request to the server. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/260 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ef0cb08342..f5d81e4aa4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2297,10 +2297,9 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." :end (eglot--pos-to-lsp-position end)) :context `(:diagnostics - [,@(mapcar (lambda (diag) - (cdr (assoc 'eglot-lsp-diag - (eglot--diag-data diag)))) - (flymake-diagnostics beg end))])))) + [,@(cl-loop for diag in (flymake-diagnostics beg end) + when (cdr (assoc 'eglot-lsp-diag (eglot--diag-data diag))) + collect it)])))) (menu-items (or (mapcar (jsonrpc-lambda (&rest all &key title &allow-other-keys) (cons title all)) commit 9383a2cd5c7fb9883c087725ccb00de712aad58a Author: vjoki Date: Tue Apr 30 12:35:24 2019 +0300 Fix local function call in directory watcher () Copyright-paperwork-exempt: yes * eglot.el (eglot-register-capability workspace/didChangeWatchFiles): fix call to handle-event. GitHub-reference: https://github.com/joaotavora/eglot/issues/255 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 06600e2e97..ef0cb08342 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2367,8 +2367,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (changed 2) (deleted 3))))))) ((eq action 'renamed) - (handle-event desc 'deleted file) - (handle-event desc 'created file1)))))) + (handle-event '(desc 'deleted file)) + (handle-event '(desc 'created file1))))))) (unwind-protect (progn (dolist (dir (delete-dups (mapcar #'file-name-directory globs))) (push (file-notify-add-watch dir '(change) #'handle-event) commit 80433528c2349a38aca5c2b884b71040978ead97 Author: João Távora Date: Wed Feb 13 09:24:02 2019 +0000 Unbreak build Messed up the name of eglot-xref-lessp-function. * eglot.el (eglot--handling-xrefs): Use eglot-xref-lessp-function GitHub-reference: per https://github.com/joaotavora/eglot/issues/220 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index df6a1b3ecb..06600e2e97 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1684,7 +1684,7 @@ DUMMY is ignored." (defmacro eglot--handling-xrefs (&rest body) "Properly sort and handle xrefs produced and returned by BODY." `(unwind-protect - (sort (progn ,@body) eglot-xref-sort-function) + (sort (progn ,@body) eglot-xref-lessp-function) (maphash (lambda (_uri buf) (kill-buffer buf)) eglot--temp-location-buffers) (clrhash eglot--temp-location-buffers))) commit aed8e9732b1cff2c9d24b0010ae47337afb072c7 Author: João Távora Date: Mon Feb 11 21:34:50 2019 +0000 * eglot.el (xref-backend-references): don't use return-from. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4fdd5c4e5b..df6a1b3ecb 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1767,24 +1767,24 @@ Try to visit the target file for a richer summary line." locations)))) (cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) - (unless (eglot--server-capable :referencesProvider) - (cl-return-from xref-backend-references nil)) - (let ((params - (or (get-text-property 0 :textDocumentPositionParams identifier) - (let ((rich (car (member identifier eglot--xref-known-symbols)))) - (and rich (get-text-property 0 :textDocumentPositionParams rich)))))) - (unless params - (eglot--error "Don' know where %s is in the workspace!" identifier)) - (eglot--handling-xrefs - (mapcar - (eglot--lambda ((Location) uri range) - (eglot--xref-make identifier uri range)) - (jsonrpc-request (eglot--current-server-or-lose) - :textDocument/references - (append - params - (list :context - (list :includeDeclaration t)))))))) + (when (eglot--server-capable :referencesProvider) + (let ((params + (or (get-text-property 0 :textDocumentPositionParams identifier) + (let ((rich (car (member identifier eglot--xref-known-symbols)))) + (and rich + (get-text-property 0 :textDocumentPositionParams rich)))))) + (unless params + (eglot--error "Don' know where %s is in the workspace!" identifier)) + (eglot--handling-xrefs + (mapcar + (eglot--lambda ((Location) uri range) + (eglot--xref-make identifier uri range)) + (jsonrpc-request (eglot--current-server-or-lose) + :textDocument/references + (append + params + (list :context + (list :includeDeclaration t))))))))) (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) commit e123f41b9b2bc1b0c3ae9728362217bfc5a4770c Author: João Távora Date: Mon Feb 11 21:33:49 2019 +0000 Don't sort xref's by default But use a eglot-xref-lessp-function in case someone wants to tweak this. * eglot.el (eglot-xref-lessp-function): New variable. (eglot--handling-xrefs): Use it. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/220 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 79e414646b..4fdd5c4e5b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1678,13 +1678,13 @@ DUMMY is ignored." (defvar eglot--temp-location-buffers (make-hash-table :test #'equal) "Helper variable for `eglot--handling-xrefs'.") +(defvar eglot-xref-lessp-function #'ignore + "Compare two `xref-item' objects for sorting.") + (defmacro eglot--handling-xrefs (&rest body) "Properly sort and handle xrefs produced and returned by BODY." `(unwind-protect - (sort (progn ,@body) - (lambda (a b) - (< (xref-location-line (xref-item-location a)) - (xref-location-line (xref-item-location b))))) + (sort (progn ,@body) eglot-xref-sort-function) (maphash (lambda (_uri buf) (kill-buffer buf)) eglot--temp-location-buffers) (clrhash eglot--temp-location-buffers))) commit 232289d25c0851e85b1798d976ed8c368b83cea3 Author: João Távora Date: Mon Feb 11 21:21:49 2019 +0000 Use a less buggy flymake * eglot.el (Package-Requires) Require flymake 1.0.5 GitHub-reference: fix https://github.com/joaotavora/eglot/issues/223 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a4b7f8f8e6..79e414646b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -7,7 +7,7 @@ ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.7") (flymake "1.0.2")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.7") (flymake "1.0.5")) ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by commit 36f294c2d5fb975f888539f9aef88100bf2db0ab Author: João Távora Date: Tue Feb 5 21:49:38 2019 +0000 Don't teardown company if started via trigger chars Reported by zhanghj in https://github.com/company-mode/company-mode/issues/866 * eglot.el (eglot-completion-at-point): More carefully calculate :company-prefix-length diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2d1c3676f2..a4b7f8f8e6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1911,9 +1911,12 @@ is not active." (insert formatted) (current-buffer))))) :company-prefix-length - (cl-some #'looking-back - (mapcar #'regexp-quote - (plist-get completion-capability :triggerCharacters))) + (save-excursion + (when (car bounds) (goto-char (car bounds))) + (looking-back + (regexp-opt + (cl-coerce (cl-getf completion-capability :triggerCharacters) 'list)) + (line-beginning-position))) :exit-function (lambda (comp _status) (let ((comp (if (get-text-property 0 'eglot--lsp-completion comp) commit 5292c4b6f01c2ac680eb5a032100486ce6047412 Author: João Távora Date: Mon Jan 14 15:31:26 2019 +0000 Consider mode derivation when guessing servers * eglot.el (eglot-server-programs): Remove js2-mode and rjsx-mode. (eglot--guess-contact): Use provided-mode-derived-p GitHub-reference: per https://github.com/joaotavora/eglot/issues/177 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 799ab10fa2..2d1c3676f2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -81,8 +81,6 @@ (defvar eglot-server-programs '((rust-mode . (eglot-rls "rls")) (python-mode . ("pyls")) ((js-mode - js2-mode - rjsx-mode typescript-mode) . ("javascript-typescript-stdio")) (sh-mode . ("bash-language-server" "start")) @@ -635,8 +633,9 @@ be guessed." (project (or (project-current) `(transient . ,default-directory))) (guess (cdr (assoc managed-mode eglot-server-programs (lambda (m1 m2) - (or (eq m1 m2) - (and (listp m1) (memq m2 m1))))))) + (cl-find + m2 (if (listp m1) m1 (list m1)) + :test #'provided-mode-derived-p))))) (guess (if (functionp guess) (funcall guess interactive) guess)) commit 1eb7535511add8a828290e0353ac6c9719b7af7c Author: João Távora Date: Mon Jan 14 14:15:20 2019 +0000 Protect against null messages from eldoc * eglot.el (eglot--eldoc-message): Protect against nil FORMAT. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/209 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d251bcb7c0..799ab10fa2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2070,26 +2070,27 @@ Buffer is displayed with `display-buffer', which obeys :type 'boolean) (defun eglot--eldoc-message (format &rest args) - (let ((string (apply #'format format args))) ;; FIXME: overworking? - (when (or (eq t eglot-put-doc-in-help-buffer) - (funcall eglot-put-doc-in-help-buffer string)) - (with-current-buffer (eglot--help-buffer) - (rename-buffer (format "*eglot-help for %s*" eglot--eldoc-hint)) - (let ((inhibit-read-only t)) - (erase-buffer) - (insert string) - (goto-char (point-min)) - (if eglot-auto-display-help-buffer - (display-buffer (current-buffer)) - (unless (get-buffer-window (current-buffer)) - (eglot--message - "%s\n(...truncated. Full help is in `%s')" - (truncate-string-to-width - (replace-regexp-in-string "\\(.*\\)\n.*" "\\1" string) - (frame-width) nil nil "...") - (buffer-name eglot--help-buffer)))) - (help-mode) - t))))) + (when format + (let ((string (apply #'format format args))) ;; FIXME: overworking? + (when (or (eq t eglot-put-doc-in-help-buffer) + (funcall eglot-put-doc-in-help-buffer string)) + (with-current-buffer (eglot--help-buffer) + (rename-buffer (format "*eglot-help for %s*" eglot--eldoc-hint)) + (let ((inhibit-read-only t)) + (erase-buffer) + (insert string) + (goto-char (point-min)) + (if eglot-auto-display-help-buffer + (display-buffer (current-buffer)) + (unless (get-buffer-window (current-buffer)) + (eglot--message + "%s\n(...truncated. Full help is in `%s')" + (truncate-string-to-width + (replace-regexp-in-string "\\(.*\\)\n.*" "\\1" string) + (frame-width) nil nil "...") + (buffer-name eglot--help-buffer)))) + (help-mode) + t)))))) (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function. commit d0c8c6011e774c91a35d14086506b34fb070682e Author: Sergey Kostyaev Date: Fri Jan 11 02:45:30 2019 +0700 Fix bug introduced by commit fixing this issue * eglot.el (eglot--sig-info): Protect against invalid label. GitHub-reference: per https://github.com/joaotavora/eglot/issues/121 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 18067f61b5..d251bcb7c0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2005,9 +2005,10 @@ is not active." params-end t) (list (match-beginning 0) (match-end 0)))) (mapcar #'1+ (append label nil))))) - (add-face-text-property - beg end - 'eldoc-highlight-function-argument))) + (if (and beg end) + (add-face-text-property + beg end + 'eldoc-highlight-function-argument)))) ;; ...and/or maybe add its doc on a line by its own. (when documentation (goto-char (point-max)) commit 1da5b8e1a31f3032baf26573f5ccb09a26a67375 Author: Brady Trainor Date: Tue Dec 25 16:21:53 2018 -0800 Add built-in support for dart's dart_language_server Closes https://github.com/joaotavora/eglot/issues/194. Copyright-paperwork-exempt: yes * README.md (Connecting to a server): Add dart_language_server. * eglot.el (eglot-server-programs): Add dart_language_server. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index befa6cdf45..18067f61b5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -100,7 +100,8 @@ language-server/bin/php-language-server.php")) "-gocodecompletion")) ((R-mode ess-r-mode) . ("R" "--slave" "-e" "languageserver::run()")) - (java-mode . eglot--eclipse-jdt-contact)) + (java-mode . eglot--eclipse-jdt-contact) + (dart-mode . ("dart_language_server"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated commit 9cedae50a27f0d0d6adee82976654d63c9f67602 Author: João Távora Date: Wed Jan 9 21:09:35 2019 +0000 Handle label offsets in parameterinformation At least ccls uses this. * eglot.el (eglot-client-capabilities): Declare support for :labelOffsetSupport. (eglot--sig-info): Handle label offsets in ParameterInformation GitHub-reference: fix https://github.com/joaotavora/eglot/issues/201 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2ab4bd864a..befa6cdf45 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -477,7 +477,10 @@ treated as in `eglot-dbind'." :json-false)) :contextSupport t) :hover `(:dynamicRegistration :json-false) - :signatureHelp `(:dynamicRegistration :json-false) + :signatureHelp (list :dynamicRegistration :json-false + :signatureInformation + `(:parameterInformation + (:labelOffsetSupport t))) :references `(:dynamicRegistration :json-false) :definition `(:dynamicRegistration :json-false) :documentSymbol (list @@ -1992,12 +1995,18 @@ is not active." ;; ...perhaps highlight it in the formals list (when params-start (goto-char params-start) - (let ((regex (concat "\\<" (regexp-quote label) "\\>")) - (case-fold-search nil)) - (when (re-search-forward regex params-end t) - (add-face-text-property - (match-beginning 0) (match-end 0) - 'eldoc-highlight-function-argument)))) + (pcase-let + ((`(,beg ,end) + (if (stringp label) + (let ((case-fold-search nil)) + (and (re-search-forward + (concat "\\<" (regexp-quote label) "\\>") + params-end t) + (list (match-beginning 0) (match-end 0)))) + (mapcar #'1+ (append label nil))))) + (add-face-text-property + beg end + 'eldoc-highlight-function-argument))) ;; ...and/or maybe add its doc on a line by its own. (when documentation (goto-char (point-max)) commit 58d4aff894c650ef4b3f601292314af53b961ce3 Author: João Távora Date: Mon Jan 7 08:44:37 2019 +0000 Display truncated docstring if too large for echo area * eglot.el (eglot--eldoc-message): Display first line doc. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9631c192e6..2ab4bd864a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2071,8 +2071,12 @@ Buffer is displayed with `display-buffer', which obeys (if eglot-auto-display-help-buffer (display-buffer (current-buffer)) (unless (get-buffer-window (current-buffer)) - (eglot--message "Help for %s is in %s buffer" eglot--eldoc-hint - (buffer-name eglot--help-buffer)))) + (eglot--message + "%s\n(...truncated. Full help is in `%s')" + (truncate-string-to-width + (replace-regexp-in-string "\\(.*\\)\n.*" "\\1" string) + (frame-width) nil nil "...") + (buffer-name eglot--help-buffer)))) (help-mode) t))))) commit 791a117c5fab6f55ca5fbc03be0225444c4cd9c7 Author: João Távora Date: Sun Jan 6 22:14:27 2019 +0000 Rename new defcustoms with friendlier names * eglot.el (eglot-doc-too-large-for-echo-area): Rename from eglot-eldoc-extra-buffer-if-too-large. (eglot-put-doc-in-help-buffer): Rename from eglot-eldoc-extra-buffer. (eglot-auto-display-help-buffer): Rename from eglot-auto-display-eldoc-extra-buffer. (eglot--eldoc-message): Use new variable names. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a7740e5690..9631c192e6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2031,7 +2031,7 @@ is not active." (rename-buffer (format "*eglot-help for %s*" sym)) (with-current-buffer standard-output (insert blurb))))))) -(defun eglot-eldoc-extra-buffer-if-too-large (string) +(defun eglot-doc-too-large-for-echo-area (string) "Return non-nil if STRING won't fit in echo area. Respects `max-mini-window-height' (which see)." (let ((max-height @@ -2041,18 +2041,18 @@ Respects `max-mini-window-height' (which see)." (t 1)))) (> (cl-count ?\n string) max-height))) -(defcustom eglot-eldoc-extra-buffer - #'eglot-eldoc-extra-buffer-if-too-large - "If non-nil, put eldoc docstrings in separate `*eglot-help*' buffer. +(defcustom eglot-put-doc-in-help-buffer + #'eglot-doc-too-large-for-echo-area + "If non-nil, put \"hover\" documentation in separate `*eglot-help*' buffer. If nil, use whatever `eldoc-message-function' decides (usually the echo area). If t, use `*eglot-help; unconditionally. If a function, it is called with the docstring to display and should a -boolean." +boolean producing one of the two previous values." :type '(choice (const :tag "Never use `*eglot-help*'" nil) (const :tag "Always use `*eglot-help*'" t) (function :tag "Ask a function"))) -(defcustom eglot-auto-display-eldoc-extra-buffer nil +(defcustom eglot-auto-display-help-buffer nil "If non-nil, automatically display `*eglot-help*' buffer. Buffer is displayed with `display-buffer', which obeys `display-buffer-alist' & friends." @@ -2060,20 +2060,19 @@ Buffer is displayed with `display-buffer', which obeys (defun eglot--eldoc-message (format &rest args) (let ((string (apply #'format format args))) ;; FIXME: overworking? - (when (or (eq t eglot-eldoc-extra-buffer) - (funcall eglot-eldoc-extra-buffer string)) + (when (or (eq t eglot-put-doc-in-help-buffer) + (funcall eglot-put-doc-in-help-buffer string)) (with-current-buffer (eglot--help-buffer) (rename-buffer (format "*eglot-help for %s*" eglot--eldoc-hint)) (let ((inhibit-read-only t)) (erase-buffer) (insert string) (goto-char (point-min)) - (cond (eglot-auto-display-eldoc-extra-buffer - (display-buffer (current-buffer))) - (t - (unless (get-buffer-window (current-buffer)) - (eglot--message "Help for %s is in %s buffer" eglot--eldoc-hint - (buffer-name eglot--help-buffer))))) + (if eglot-auto-display-help-buffer + (display-buffer (current-buffer)) + (unless (get-buffer-window (current-buffer)) + (eglot--message "Help for %s is in %s buffer" eglot--eldoc-hint + (buffer-name eglot--help-buffer)))) (help-mode) t))))) commit 84234b25ba59dd593e0f77bb4a54c03281519567 Author: João Távora Date: Sun Jan 6 12:55:21 2019 +0000 Fix test failure introduced by previous commit Remove the hack of unsetting eldoc-last-message in eglot--eldoc-message. This allows any subsequent eglot-eldoc-function calls (prompted by simple cursor movement) to return it immediately, thus refreshing the help buffer with the same contents. For this to work, we also have to set eglot--eldoc-hint globally in eglot-eldoc-function. An alternative to making the test pass would be to keep the hack of unsetting eldoc-last-message only in the case that we actually get to display the help buffer. This would actually be more efficient, but potentially more hacky. The bottom line here is that eldoc doesn't have a good API to deal with asynchronous docstring fetching. See this thread: https://lists.gnu.org/archive/html/emacs-devel/2018-05/msg00151.html * eglot.el (eglot--eldoc-message): Don't unset eldoc-last-message. (eglot-eldoc-function): Set eglot--eldoc-hint for synchronous operation too. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fb002b85a8..a7740e5690 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2068,12 +2068,12 @@ Buffer is displayed with `display-buffer', which obeys (erase-buffer) (insert string) (goto-char (point-min)) - (setq eldoc-last-message nil) - (if eglot-auto-display-eldoc-extra-buffer - (display-buffer (current-buffer)) - (unless (get-buffer-window (current-buffer)) - (eglot--message "Help for %s in in %s buffer" eglot--eldoc-hint - (buffer-name eglot--help-buffer)))) + (cond (eglot-auto-display-eldoc-extra-buffer + (display-buffer (current-buffer))) + (t + (unless (get-buffer-window (current-buffer)) + (eglot--message "Help for %s is in %s buffer" eglot--eldoc-hint + (buffer-name eglot--help-buffer))))) (help-mode) t))))) @@ -2085,6 +2085,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (position-params (eglot--TextDocumentPositionParams)) sig-showing (thing-at-point (thing-at-point 'symbol))) + (setq eglot--eldoc-hint thing-at-point) (cl-macrolet ((when-buffer-window (&body body) ; notice the exception when testing with `ert' `(when (or (get-buffer-window buffer) (ert-running-test)) commit aedb0d33de82595822a6fe56f40fdf608adc37c7 Author: João Távora Date: Sat Jan 5 22:47:43 2019 +0000 Show large docs in help buffer instead of echo are by default * eglot.el (eglot--managed-mode): Add and remove from eglot--eldoc-message (eglot--eldoc-hint, eglot--help-buffer): New helpers. (eglot-eldoc-extra-buffer) (eglot-auto-display-eldoc-extra-buffer): New defcustoms. (eglot--eldoc-message): New helper. (eglot-eldoc-function): Set eglot--eldoc-hint. (eglot-help-at-point): Use new helpers. (eglot-eldoc-extra-buffer-if-too-large): New predicate. GitHub-reference: per https://github.com/joaotavora/eglot/issues/198 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 98032b4a3e..fb002b85a8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1162,6 +1162,7 @@ and just return it. PROMPT shouldn't end with a question mark." (add-hook 'pre-command-hook 'eglot--pre-command-hook nil t) (eglot--setq-saving eldoc-documentation-function #'eglot-eldoc-function) (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend t)) + (add-function :before-until (local 'eldoc-message-function) #'eglot--eldoc-message) (add-function :around (local 'imenu-create-index-function) #'eglot-imenu) (flymake-mode 1) (eldoc-mode 1)) @@ -1178,6 +1179,7 @@ and just return it. PROMPT shouldn't end with a question mark." (remove-hook 'change-major-mode-hook #'eglot--managed-mode-onoff t) (remove-hook 'post-self-insert-hook 'eglot--post-self-insert-hook t) (remove-hook 'pre-command-hook 'eglot--pre-command-hook t) + (remove-function (local 'eldoc-message-function) #'eglot--eldoc-message) (cl-loop for (var . saved-binding) in eglot--saved-bindings do (set (make-local-variable var) saved-binding)) (setq eglot--current-flymake-report-fn nil)))) @@ -2006,6 +2008,15 @@ is not active." (buffer-string)))) when moresigs concat "\n")) +(defvar eglot--eldoc-hint nil) + +(defvar eglot--help-buffer nil) + +(defun eglot--help-buffer () + (or (and (buffer-live-p eglot--help-buffer) + eglot--help-buffer) + (setq eglot--help-buffer (generate-new-buffer "*eglot-help*")))) + (defun eglot-help-at-point () "Request \"hover\" information for the thing at point." (interactive) @@ -2013,9 +2024,58 @@ is not active." (jsonrpc-request (eglot--current-server-or-lose) :textDocument/hover (eglot--TextDocumentPositionParams)) (when (seq-empty-p contents) (eglot--error "No hover info here")) - (let ((blurb (eglot--hover-info contents range))) - (with-help-window "*eglot help*" - (with-current-buffer standard-output (insert blurb)))))) + (let ((blurb (eglot--hover-info contents range)) + (sym (thing-at-point 'symbol))) + (with-current-buffer (eglot--help-buffer) + (with-help-window (current-buffer) + (rename-buffer (format "*eglot-help for %s*" sym)) + (with-current-buffer standard-output (insert blurb))))))) + +(defun eglot-eldoc-extra-buffer-if-too-large (string) + "Return non-nil if STRING won't fit in echo area. +Respects `max-mini-window-height' (which see)." + (let ((max-height + (cond ((floatp max-mini-window-height) (* (frame-height) + max-mini-window-height)) + ((integerp max-mini-window-height) max-mini-window-height) + (t 1)))) + (> (cl-count ?\n string) max-height))) + +(defcustom eglot-eldoc-extra-buffer + #'eglot-eldoc-extra-buffer-if-too-large + "If non-nil, put eldoc docstrings in separate `*eglot-help*' buffer. +If nil, use whatever `eldoc-message-function' decides (usually +the echo area). If t, use `*eglot-help; unconditionally. If a +function, it is called with the docstring to display and should a +boolean." + :type '(choice (const :tag "Never use `*eglot-help*'" nil) + (const :tag "Always use `*eglot-help*'" t) + (function :tag "Ask a function"))) + +(defcustom eglot-auto-display-eldoc-extra-buffer nil + "If non-nil, automatically display `*eglot-help*' buffer. +Buffer is displayed with `display-buffer', which obeys +`display-buffer-alist' & friends." + :type 'boolean) + +(defun eglot--eldoc-message (format &rest args) + (let ((string (apply #'format format args))) ;; FIXME: overworking? + (when (or (eq t eglot-eldoc-extra-buffer) + (funcall eglot-eldoc-extra-buffer string)) + (with-current-buffer (eglot--help-buffer) + (rename-buffer (format "*eglot-help for %s*" eglot--eldoc-hint)) + (let ((inhibit-read-only t)) + (erase-buffer) + (insert string) + (goto-char (point-min)) + (setq eldoc-last-message nil) + (if eglot-auto-display-eldoc-extra-buffer + (display-buffer (current-buffer)) + (unless (get-buffer-window (current-buffer)) + (eglot--message "Help for %s in in %s buffer" eglot--eldoc-hint + (buffer-name eglot--help-buffer)))) + (help-mode) + t))))) (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function. @@ -2023,7 +2083,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (let* ((buffer (current-buffer)) (server (eglot--current-server-or-lose)) (position-params (eglot--TextDocumentPositionParams)) - sig-showing) + sig-showing + (thing-at-point (thing-at-point 'symbol))) (cl-macrolet ((when-buffer-window (&body body) ; notice the exception when testing with `ert' `(when (or (get-buffer-window buffer) (ert-running-test)) @@ -2037,9 +2098,10 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (when-buffer-window (when (cl-plusp (length signatures)) (setq sig-showing t) - (eldoc-message (eglot--sig-info signatures - activeSignature - activeParameter))))) + (let ((eglot--eldoc-hint thing-at-point)) + (eldoc-message (eglot--sig-info signatures + activeSignature + activeParameter)))))) :deferred :textDocument/signatureHelp)) (when (eglot--server-capable :hoverProvider) (jsonrpc-async-request @@ -2050,7 +2112,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (when-let (info (and contents (eglot--hover-info contents range))) - (eldoc-message info))))) + (let ((eglot--eldoc-hint thing-at-point)) + (eldoc-message info)))))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) (jsonrpc-async-request commit f399be6d122d9f23da8a13df8050f213c0617369 Author: João Távora Date: Sat Jan 5 14:35:01 2019 +0000 Prevent eldoc flicker when moving around * eglot.el (eglot-eldoc-function): Return eldoc-last-message immediately. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/198 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 031a657fe9..98032b4a3e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2070,7 +2070,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." ov))) highlights)))) :deferred :textDocument/documentHighlight)))) - nil) + eldoc-last-message) (defun eglot-imenu (oldfun) "EGLOT's `imenu-create-index-function' overriding OLDFUN." commit a47618f19f6dec030b08e04717e10a573a1ce14f Author: João Távora Date: Sat Jan 5 13:32:13 2019 +0000 Handle (un)registercapability requests via generic functions * eglot.el (Version): Bump to 1.4 (eglot-register-capability, eglot-unregister-capability): New generic functions. (eglot--register-unregister): Call eglot-register-capability, eglot-unregister-capability. (eglot-register-capability s (eql workspace/didChangeWatchedFiles)): Rename from eglot--register-workspace/didChangeWatchedFiles. (eglot-unregister-capability s (eql workspace/didChangeWatchedFiles)): Rename from eglot--unregister-workspace/didChangeWatchedFiles. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 53e60fd6ef..031a657fe9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 1.3 +;; Version: 1.4 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot @@ -440,6 +440,20 @@ treated as in `eglot-dbind'." "JSON object to send under `initializationOptions'" (:method (_s) nil)) ; blank default +(cl-defgeneric eglot-register-capability (server method id &rest params) + "Ask SERVER to register capability METHOD marked with ID." + (:method + (_s method _id &rest _params) + (eglot--warn "Server tried to register unsupported capability `%s'" + method))) + +(cl-defgeneric eglot-unregister-capability (server method id &rest params) + "Ask SERVER to register capability METHOD marked with ID." + (:method + (_s method _id &rest _params) + (eglot--warn "Server tried to unregister unsupported capability `%s'" + method))) + (cl-defgeneric eglot-client-capabilities (server) "What the EGLOT LSP client supports for SERVER." (:method (_s) @@ -1423,12 +1437,14 @@ COMMAND is a symbol naming the command." (cl-defun eglot--register-unregister (server things how) "Helper for `registerCapability'. -THINGS are either registrations or unregisterations." +THINGS are either registrations or unregisterations (sic)." (cl-loop for thing in (cl-coerce things 'list) do (eglot--dbind ((Registration) id method registerOptions) thing - (apply (intern (format "eglot--%s-%s" how method)) - server :id id registerOptions)))) + (apply (cl-ecase how + (register 'eglot-register-capability) + (unregister 'eglot-unregister-capability)) + server (intern method) id registerOptions)))) (cl-defmethod eglot-handle-request (server (_method (eql client/registerCapability)) &key registrations) @@ -2243,9 +2259,10 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." for result = (replace-regexp-in-string pattern rep target) finally return result)) -(cl-defun eglot--register-workspace/didChangeWatchedFiles (server &key id watchers) +(cl-defmethod eglot-register-capability + (server (method (eql workspace/didChangeWatchedFiles)) id &key watchers) "Handle dynamic registration of workspace/didChangeWatchedFiles" - (eglot--unregister-workspace/didChangeWatchedFiles server :id id) + (eglot-unregister-capability server method id) (let* (success (globs (mapcar (eglot--lambda ((FileSystemWatcher) globPattern) globPattern) @@ -2280,9 +2297,10 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." `(:message ,(format "OK, watching %s watchers" (length watchers))))) (unless success - (eglot--unregister-workspace/didChangeWatchedFiles server :id id)))))) + (eglot-unregister-capability server method id)))))) -(cl-defun eglot--unregister-workspace/didChangeWatchedFiles (server &key id) +(cl-defmethod eglot-unregister-capability + (server (_method (eql workspace/didChangeWatchedFiles)) id) "Handle dynamic unregistration of workspace/didChangeWatchedFiles" (mapc #'file-notify-rm-watch (gethash id (eglot--file-watches server))) (remhash id (eglot--file-watches server)) commit 6ee4328ca1495afb90fa8b3377781e1b3667ec5d Author: João Távora Date: Sat Jan 5 13:03:01 2019 +0000 Appease checkdoc * eglot.el (eglot--post-self-insert-hook) (eglot--pre-command-hook, eglot--before-change) (eglot--eclipse-jdt-contact): Fix docstrings. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6de3adf976..53e60fd6ef 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1481,11 +1481,11 @@ THINGS are either registrations or unregisterations." "If non-nil, value of the last inserted character in buffer.") (defun eglot--post-self-insert-hook () - "Set `eglot--last-inserted-char.'" + "Set `eglot--last-inserted-char'." (setq eglot--last-inserted-char last-input-event)) (defun eglot--pre-command-hook () - "Reset `eglot--last-inserted-char.'" + "Reset `eglot--last-inserted-char'." (setq eglot--last-inserted-char nil)) (defun eglot--CompletionParams () @@ -1510,7 +1510,7 @@ THINGS are either registrations or unregisterations." (defvar-local eglot--change-idle-timer nil "Idle timer for didChange signals.") (defun eglot--before-change (start end) - "Hook onto `before-change-functions'." + "Hook onto `before-change-functions' with START and END." ;; Records START and END, crucially convert them into LSP ;; (line/char) positions before that information is lost (because ;; the after-change thingy doesn't know if newlines were @@ -2352,7 +2352,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (ignore (eglot--warn "JAVA_HOME env var not set"))))) (defun eglot--eclipse-jdt-contact (interactive) - "Return a contact for connecting to eclipse.jdt.ls server, as a cons cell." + "Return a contact for connecting to eclipse.jdt.ls server, as a cons cell. +If INTERACTIVE, prompt user for details." (cl-labels ((is-the-jar (path) commit 36b2fa8e7f799679c1ba6995e097ea3bd0fbc72d Author: João Távora Date: Wed Jan 2 20:34:09 2019 +0000 * eglot.el (eglot-workspace-configuration): safe when listp. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 39f484220a..6de3adf976 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1553,6 +1553,8 @@ Records START, END and PRE-CHANGE-LENGTH locally." Setting should be a keyword, value can be any value that can be converted to JSON.") +(put 'eglot-workspace-configuration 'safe-local-variable 'listp) + (defun eglot-signal-didChangeConfiguration (server) "Send a `:workspace/didChangeConfiguration' signal to SERVER. When called interactively, use the currently active server" commit f3914c266fcf81bb735e733222d6a4aa70948548 Author: João Távora Date: Wed Jan 2 17:12:36 2019 +0000 Run connection hooks with proper dir-locals eglot-connect-hook and eglot-server-initialized-hook must run in a buffer with properly setup directory-local variables for the project. This is crucial for things like eglot-signal-didChangeConfiguration, which needs a properly setup value of eglot-workspace-configuration to succeed. I could have chosen any of the buffers where Eglot is activating itself, but the approach using hack-dir-local-variables-non-file-buffer seems more correct, despite the name. * eglot.el (eglot--connect): Run connection hooks with proper dir-locals. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/196 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4416e52c70..39f484220a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -840,8 +840,11 @@ This docstring appeases checkdoc, that's all." (lambda () (setf (eglot--inhibit-autoreconnect server) (null eglot-autoreconnect))))))) - (run-hook-with-args 'eglot-connect-hook server) - (run-hook-with-args 'eglot-server-initialized-hook server) + (let ((default-directory (car (project-roots project))) + (major-mode managed-major-mode)) + (hack-dir-local-variables-non-file-buffer) + (run-hook-with-args 'eglot-connect-hook server) + (run-hook-with-args 'eglot-server-initialized-hook server)) (eglot--message "Connected! Server `%s' now managing `%s' buffers \ in project `%s'." commit 355c9c4a656719a94e0a25a04eb913470d46b7dc Author: João Távora Date: Tue Jan 1 14:56:46 2019 +0000 Allow read-only modes for markup rendering gfm-mode is read-only, so it must be set after the string has been inserted in the temporary buffer. * eglot.el (eglot--format-markup): Insert string before setting mode. GitHub-reference: close https://github.com/joaotavora/eglot/issues/197 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2411c6a89c..4416e52c70 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1030,8 +1030,8 @@ Doubles as an indicator of snippet support." (list (plist-get markup :value) major-mode)))) (with-temp-buffer - (ignore-errors (funcall mode)) - (insert string) (font-lock-ensure) (buffer-string)))) + (insert string) + (ignore-errors (funcall mode)) (font-lock-ensure) (buffer-string)))) (defcustom eglot-ignored-server-capabilites (list) "LSP server capabilities that Eglot could use, but won't. commit 0c432de4cf64dca3869a4c7578e9d21fe3bef491 Author: João Távora Date: Thu Dec 27 18:31:42 2018 +0000 Remove a hard dependency on flymake-mode * eglot.el (eglot-handle-notification): Don't specifically check for flymake-mode before reporting diagnostics. GitHub-reference: close https://github.com/joaotavora/eglot/issues/195 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a335f1675e..2411c6a89c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1405,7 +1405,7 @@ COMMAND is a symbol naming the command." (t 'eglot-note)) message `((eglot-lsp-diag . ,diag-spec))))) into diags - finally (cond ((and flymake-mode eglot--current-flymake-report-fn) + finally (cond (eglot--current-flymake-report-fn (funcall eglot--current-flymake-report-fn diags ;; If the buffer hasn't changed since last ;; call to the report function, flymake won't commit 5df556bb94d4dcd7aedc1dd8d282b553f67f15e5 Author: João Távora Date: Sun Dec 23 14:01:38 2018 +0000 Slightly simplify eglot-completion-at-point * eglot.el (eglot-completion-at-point): Don't propertize completion string with all LSP properties. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d60fc0776a..a335f1675e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1844,7 +1844,6 @@ is not active." (string-trim-left label)) (t (or insertText (string-trim-left label)))))) - (add-text-properties 0 1 all completion) (put-text-property 0 1 'eglot--lsp-completion all completion) completion)) items))))) @@ -1869,15 +1868,15 @@ is not active." :company-doc-buffer (lambda (obj) (let* ((documentation - (or (get-text-property 0 :documentation obj) - (and (eglot--server-capable :completionProvider - :resolveProvider) - (plist-get - (jsonrpc-request server :completionItem/resolve - (get-text-property - 0 'eglot--lsp-completion obj) - :cancel-on-input t) - :documentation)))) + (let ((lsp-comp + (get-text-property 0 'eglot--lsp-completion obj))) + (or (plist-get lsp-comp :documentation) + (and (eglot--server-capable :completionProvider + :resolveProvider) + (plist-get + (jsonrpc-request server :completionItem/resolve + lsp-comp :cancel-on-input t) + :documentation))))) (formatted (and documentation (eglot--format-markup documentation)))) (when formatted commit 9ffa6a91cd1b7c84134997e22295664eba1925d6 Author: João Távora Date: Sat Dec 22 15:23:41 2018 +0000 Actually make completion sorting work * eglot.el (eglot-completion-at-point): Complicate severely. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/190 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5861b05fcd..d60fc0776a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1806,37 +1806,49 @@ is not active." (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." - (let ((bounds (bounds-of-thing-at-point 'symbol)) + (let* ((bounds (bounds-of-thing-at-point 'symbol)) (server (eglot--current-server-or-lose)) (completion-capability (eglot--server-capable :completionProvider)) + (sort-completions (lambda (completions) + (sort completions + (lambda (a b) + (string-lessp + (or (get-text-property 0 :sortText a) "") + (or (get-text-property 0 :sortText b) "")))))) + (metadata `(metadata . ((display-sort-function . ,sort-completions)))) strings) (when completion-capability (list (or (car bounds) (point)) (or (cdr bounds) (point)) - (completion-table-dynamic - (lambda (_ignored) - (let* ((resp (jsonrpc-request server - :textDocument/completion - (eglot--CompletionParams) - :deferred :textDocument/completion - :cancel-on-input t)) - (items (if (vectorp resp) resp (plist-get resp :items)))) - (setq - strings - (mapcar - (jsonrpc-lambda (&rest all &key label insertText insertTextFormat - &allow-other-keys) - (let ((completion - (cond ((and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)) - (string-trim-left label)) - (t - (or insertText (string-trim-left label)))))) - (add-text-properties 0 1 all completion) - (put-text-property 0 1 'eglot--lsp-completion all completion) - completion)) - items))))) + (lambda (string pred action) + (if (eq action 'metadata) metadata + (funcall + (completion-table-dynamic + (lambda (_ignored) + (let* ((resp (jsonrpc-request server + :textDocument/completion + (eglot--CompletionParams) + :deferred :textDocument/completion + :cancel-on-input t)) + (items (if (vectorp resp) resp (plist-get resp :items)))) + (setq + strings + (mapcar + (jsonrpc-lambda + (&rest all &key label insertText insertTextFormat + &allow-other-keys) + (let ((completion + (cond ((and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)) + (string-trim-left label)) + (t + (or insertText (string-trim-left label)))))) + (add-text-properties 0 1 all completion) + (put-text-property 0 1 'eglot--lsp-completion all completion) + completion)) + items))))) + string pred action))) :annotation-function (lambda (obj) (eglot--dbind ((CompletionItem) detail kind insertTextFormat) @@ -1854,12 +1866,6 @@ is not active." (and (eql insertTextFormat 2) (eglot--snippet-expansion-fn) " (snippet)")))))) - :display-sort-function - (lambda (items) - (sort items (lambda (a b) - (string-lessp - (or (get-text-property 0 :sortText a) "") - (or (get-text-property 0 :sortText b) ""))))) :company-doc-buffer (lambda (obj) (let* ((documentation commit f5e3279958eb78c90da8df3a159b931b7a6b3134 Author: João Távora Date: Sat Dec 22 15:27:27 2018 +0000 Fix previous commit where workaround had been removed Do remove the workaround, but not more than that. * eglot.el (eglot-completion-at-point): set local var strings. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 43bc023509..5861b05fcd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1822,19 +1822,21 @@ is not active." :deferred :textDocument/completion :cancel-on-input t)) (items (if (vectorp resp) resp (plist-get resp :items)))) - (mapcar - (jsonrpc-lambda (&rest all &key label insertText insertTextFormat - &allow-other-keys) - (let ((completion - (cond ((and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)) - (string-trim-left label)) - (t - (or insertText (string-trim-left label)))))) - (add-text-properties 0 1 all completion) - (put-text-property 0 1 'eglot--lsp-completion all completion) - completion)) - items)))) + (setq + strings + (mapcar + (jsonrpc-lambda (&rest all &key label insertText insertTextFormat + &allow-other-keys) + (let ((completion + (cond ((and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)) + (string-trim-left label)) + (t + (or insertText (string-trim-left label)))))) + (add-text-properties 0 1 all completion) + (put-text-property 0 1 'eglot--lsp-completion all completion) + completion)) + items))))) :annotation-function (lambda (obj) (eglot--dbind ((CompletionItem) detail kind insertTextFormat) commit d255e51c78fe99a0e2736305f0baa2155cb9ca70 Author: João Távora Date: Sat Dec 22 15:18:55 2018 +0000 Use gfm-view-mode * eglot.el (eglot--format-markup): Use gfm-view-mode instead of gfm-mode. GitHub-reference: per https://github.com/joaotavora/eglot/issues/188 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 53a52bfc42..43bc023509 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1026,7 +1026,7 @@ Doubles as an indicator of snippet support." "Format MARKUP according to LSP's spec." (pcase-let ((`(,string ,mode) (if (stringp markup) (list (string-trim markup) - (intern "gfm-mode")) + (intern "gfm-view-mode")) (list (plist-get markup :value) major-mode)))) (with-temp-buffer commit b699fc7a195d7e1ae983cca5839010033915a3c9 Author: João Távora Date: Sat Dec 22 15:17:41 2018 +0000 Remove workaround for company bug that has been fixed See https://github.com/company-mode/company-mode/pull/845. * eglot.el (eglot-completion-at-point): Remove workaround for company-mode bug. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 108e7f0d92..53a52bfc42 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1822,22 +1822,19 @@ is not active." :deferred :textDocument/completion :cancel-on-input t)) (items (if (vectorp resp) resp (plist-get resp :items)))) - (setq - strings - (mapcar - (jsonrpc-lambda (&rest all &key label insertText insertTextFormat - &allow-other-keys) - (let ((completion - (cond ((and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)) - (string-trim-left label)) - (t - (or insertText (string-trim-left label)))))) - (add-text-properties 0 1 all completion) - (put-text-property 0 1 'eglot--completion-bounds bounds completion) - (put-text-property 0 1 'eglot--lsp-completion all completion) - completion)) - items))))) + (mapcar + (jsonrpc-lambda (&rest all &key label insertText insertTextFormat + &allow-other-keys) + (let ((completion + (cond ((and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)) + (string-trim-left label)) + (t + (or insertText (string-trim-left label)))))) + (add-text-properties 0 1 all completion) + (put-text-property 0 1 'eglot--lsp-completion all completion) + completion)) + items)))) :annotation-function (lambda (obj) (eglot--dbind ((CompletionItem) detail kind insertTextFormat) @@ -1898,12 +1895,7 @@ is not active." additionalTextEdits) (get-text-property 0 'eglot--lsp-completion comp) (let ((snippet-fn (and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn))) - ;; FIXME: it would have been much easier to fetch - ;; these from the lexical environment, but we can't - ;; in company because of - ;; https://github.com/company-mode/company-mode/pull/845 - (bounds (get-text-property 0 'eglot--completion-bounds comp))) + (eglot--snippet-expansion-fn)))) (cond (textEdit ;; Undo the just the completed bit. If before ;; completion the buffer was "foo.b" and now is commit 00fb3a184abd39dbdb9316faa83b4045fdf075a4 Author: João Távora Date: Wed Dec 19 21:57:00 2018 +0000 * eglot.el (package-requires): require jsonrpc 1.0.7. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7f3eea4748..108e7f0d92 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -7,7 +7,7 @@ ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.6") (flymake "1.0.2")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.7") (flymake "1.0.2")) ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by commit d9e4306e2d96e17cd99d610f865c79fdf034e98c Author: João Távora Date: Sun Dec 16 19:03:06 2018 +0000 Take over flymake and eldoc completely while managing buffers Take a pragmatic approach and override all other Flymake and Eglot backends while Eglot is enabled. Restore previous values after eglot-shutdown. Certainly cases might arise where using more than one datasource besides LSP while Eglot is managing the buffer is useful. But currently contrary, it confuses users enabling Eglot in buffers that already have flymake/eldoc backends configured. The reasons are slightly different for Eldoc and Flymake: - For Eldoc the :before-until strategy only makes sense for synchronous backends, which Eglot isn't. This conflicts with python.el default python-eldoc-function, which is also asynchronous. - For Flymake, the default backends in Emacs (python-mode, c-mode, and a few others) are mainly repetitions of what LSP does. The global value is still run though (in case you want to put, say, a spell-checking backend there). * eglot.el (eglot--saved-bindings, eglot--setq-saving): New helpers. (eglot--managed-mode): Use them. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 60dad452d6..7f3eea4748 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1118,6 +1118,14 @@ and just return it. PROMPT shouldn't end with a question mark." (defvar-local eglot--current-flymake-report-fn nil "Current flymake report function for this buffer") +(defvar-local eglot--saved-bindings nil + "Bindings saved by `eglot--setq-saving'.") + +(defmacro eglot--setq-saving (symbol binding) + `(progn (push (cons ',symbol (symbol-value ',symbol)) + eglot--saved-bindings) + (setq-local ,symbol ,binding))) + (define-minor-mode eglot--managed-mode "Mode for source buffers managed by some EGLOT project." nil nil eglot-mode-map @@ -1125,7 +1133,6 @@ and just return it. PROMPT shouldn't end with a question mark." (eglot--managed-mode (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'before-change-functions 'eglot--before-change nil t) - (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'kill-buffer-hook 'eglot--managed-mode-onoff nil t) (add-hook 'before-revert-hook 'eglot--signal-textDocument/didClose nil t) @@ -1136,8 +1143,8 @@ and just return it. PROMPT shouldn't end with a question mark." (add-hook 'change-major-mode-hook 'eglot--managed-mode-onoff nil t) (add-hook 'post-self-insert-hook 'eglot--post-self-insert-hook nil t) (add-hook 'pre-command-hook 'eglot--pre-command-hook nil t) - (add-function :before-until (local 'eldoc-documentation-function) - #'eglot-eldoc-function) + (eglot--setq-saving eldoc-documentation-function #'eglot-eldoc-function) + (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend t)) (add-function :around (local 'imenu-create-index-function) #'eglot-imenu) (flymake-mode 1) (eldoc-mode 1)) @@ -1154,9 +1161,8 @@ and just return it. PROMPT shouldn't end with a question mark." (remove-hook 'change-major-mode-hook #'eglot--managed-mode-onoff t) (remove-hook 'post-self-insert-hook 'eglot--post-self-insert-hook t) (remove-hook 'pre-command-hook 'eglot--pre-command-hook t) - (remove-function (local 'eldoc-documentation-function) - #'eglot-eldoc-function) - (remove-function (local 'imenu-create-index-function) #'eglot-imenu) + (cl-loop for (var . saved-binding) in eglot--saved-bindings + do (set (make-local-variable var) saved-binding)) (setq eglot--current-flymake-report-fn nil)))) (defvar-local eglot--cached-current-server nil commit 24a1a7ffeed8d2e4b45a29a51b0378a87584e562 Author: João Távora Date: Sun Dec 16 14:33:14 2018 +0000 Be more careful when making xref summaries * eglot.el (eglot--xref-make): Only highlight to end-of-line at most. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/187 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ee40329ca1..60dad452d6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1667,8 +1667,10 @@ Try to visit the target file for a richer summary line." (eglot--widening (pcase-let* ((`(,beg . ,end) (eglot--range-region range)) (bol (progn (goto-char beg) (point-at-bol))) - (substring (buffer-substring bol (point-at-eol)))) - (add-face-text-property (- beg bol) (- end bol) 'highlight + (substring (buffer-substring bol (point-at-eol))) + (hi-beg (- beg bol)) + (hi-end (- (min (point-at-eol) end) bol))) + (add-face-text-property hi-beg hi-end 'highlight t substring) (list substring (1+ (current-line)) (eglot-current-column)))))) (`(,summary ,line ,column) commit 1d72360e03a219ffeb1799263a640f92c4e0c0c0 Author: João Távora Date: Sun Dec 16 14:22:57 2018 +0000 Don't make bogus responses to client/(un)registercapability * eglot.el (eglot--register-unregister): Response is void. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e83562fd20..ee40329ca1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1417,11 +1417,9 @@ COMMAND is a symbol naming the command." THINGS are either registrations or unregisterations." (cl-loop for thing in (cl-coerce things 'list) - collect (eglot--dbind ((Registration) id method registerOptions) thing - (apply (intern (format "eglot--%s-%s" how method)) - server :id id registerOptions)) - into results - finally return `(:ok ,@results))) + do (eglot--dbind ((Registration) id method registerOptions) thing + (apply (intern (format "eglot--%s-%s" how method)) + server :id id registerOptions)))) (cl-defmethod eglot-handle-request (server (_method (eql client/registerCapability)) &key registrations) commit 47f5fdcae6b3f2177b55ec5898a8277b0d541a83 Author: João Távora Date: Sun Dec 16 13:51:14 2018 +0000 Add edebug specs to destructuring macros * eglot.el (eglot--dbind, eglot--lambda, eglot--dcase): Add edebug specs. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 429c329c82..e83562fd20 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -340,7 +340,7 @@ on unknown notifications and errors on unknown requests. "Destructure OBJECT of binding VARS in BODY. VARS is ([(INTERFACE)] SYMS...) Honour `eglot-strict-mode'." - (declare (indent 2)) + (declare (indent 2) (debug (sexp sexp &rest form))) (let ((interface-name (if (consp (car vars)) (car (pop vars)))) (object-once (make-symbol "object-once")) @@ -366,7 +366,7 @@ Honour `eglot-strict-mode'." (cl-defmacro eglot--lambda (cl-lambda-list &body body) "Function of args CL-LAMBDA-LIST for processing INTERFACE objects. Honour `eglot-strict-mode'." - (declare (indent 1)) + (declare (indent 1) (debug (sexp &rest form))) (let ((e (cl-gensym "jsonrpc-lambda-elem"))) `(lambda (,e) (eglot--dbind ,cl-lambda-list ,e ,@body)))) @@ -374,7 +374,7 @@ Honour `eglot-strict-mode'." "Like `pcase', but for the LSP object OBJ. CLAUSES is a list (DESTRUCTURE FORMS...) where DESTRUCTURE is treated as in `eglot-dbind'." - (declare (indent 1)) + (declare (indent 1) (debug (sexp &rest (sexp &rest form)))) (let ((obj-once (make-symbol "obj-once"))) `(let ((,obj-once ,obj)) (cond commit cdb3de6bc6f9a1d4f642d1613019f947e7138c7b Author: João Távora Date: Sun Dec 16 13:48:57 2018 +0000 Rewrite eglot--sig-info a bit for readability * eglot.el (eglot--sig-info): Rewrite a bit. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 01f6960047..429c329c82 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1941,37 +1941,42 @@ is not active." (eglot--dbind ((SignatureInformation) label documentation parameters) sig (with-temp-buffer (save-excursion (insert label)) - (let ((params-start (point-min)) - (params-end (point-max))) - (when (looking-at "\\([^(]+\\)(") - (setq params-start (match-end 0)) + (let (params-start params-end) + ;; Ad-hoc attempt to parse label as () + (when (looking-at "\\([^(]+\\)(\\([^)]+\\))") + (setq params-start (match-beginning 2) params-end (match-end 2)) (add-face-text-property (match-beginning 1) (match-end 1) 'font-lock-function-name-face)) - - (when (and (stringp documentation) (eql i active-sig) - (string-match "[[:space:]]*\\([^.\r\n]+[.]?\\)" - documentation)) - (setq documentation (match-string 1 documentation)) - (unless (string-prefix-p (string-trim documentation) label) - (goto-char (point-max)) - (insert ": " (eglot--format-markup documentation)))) - (when (and (eql i active-sig) active-param - (< -1 active-param (length parameters))) - (eglot--dbind ((ParameterInformation) label documentation) - (aref parameters active-param) - (goto-char params-start) - (let ((regex (concat "\\<" (regexp-quote label) "\\>")) - (case-fold-search nil)) - (when (re-search-forward regex params-end t) - (add-face-text-property - (- (point) (length label)) (point) - 'eldoc-highlight-function-argument))) - (when documentation + (when (eql i active-sig) + ;; Decide whether to add one-line-summary to signature line + (when (and (stringp documentation) + (string-match "[[:space:]]*\\([^.\r\n]+[.]?\\)" + documentation)) + (setq documentation (match-string 1 documentation)) + (unless (string-prefix-p (string-trim documentation) label) (goto-char (point-max)) - (insert "\n" - (propertize - label 'face 'eldoc-highlight-function-argument) - ": " (eglot--format-markup documentation))))) + (insert ": " (eglot--format-markup documentation)))) + ;; Decide what to do with the active parameter... + (when (and (eql i active-sig) active-param + (< -1 active-param (length parameters))) + (eglot--dbind ((ParameterInformation) label documentation) + (aref parameters active-param) + ;; ...perhaps highlight it in the formals list + (when params-start + (goto-char params-start) + (let ((regex (concat "\\<" (regexp-quote label) "\\>")) + (case-fold-search nil)) + (when (re-search-forward regex params-end t) + (add-face-text-property + (match-beginning 0) (match-end 0) + 'eldoc-highlight-function-argument)))) + ;; ...and/or maybe add its doc on a line by its own. + (when documentation + (goto-char (point-max)) + (insert "\n" + (propertize + label 'face 'eldoc-highlight-function-argument) + ": " (eglot--format-markup documentation)))))) (buffer-string)))) when moresigs concat "\n")) commit f0a2747ab50b9b0b944655c43ae230ccfb7db1f8 Author: Fredrik Bergroth Date: Thu Dec 13 13:03:42 2018 +0100 Adjust active param highlighting in first line of signature (3/3) Highlight only first active parameter match (even if there are many) Copyright-paperwork-exempt: yes * eglot.el (eglot--sig-info): Simplify. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ae18493f11..01f6960047 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1962,13 +1962,10 @@ is not active." (goto-char params-start) (let ((regex (concat "\\<" (regexp-quote label) "\\>")) (case-fold-search nil)) - (cl-loop for nmatches from 0 - while (re-search-forward regex params-end t) - finally do - (when (= 1 nmatches) - (add-face-text-property - (- (point) (length label)) (point) - 'eldoc-highlight-function-argument)))) + (when (re-search-forward regex params-end t) + (add-face-text-property + (- (point) (length label)) (point) + 'eldoc-highlight-function-argument))) (when documentation (goto-char (point-max)) (insert "\n" commit d050540fefed7547a85be3c9eb2e71581a98724b Author: Fredrik Bergroth Date: Thu Dec 13 13:02:15 2018 +0100 Adjust active param highlighting in first line of signature (2/3) Use regex with word boundaries when scanning for active param, to avoid matching substrings. Copyright-paperwork-exempt: yes * eglot.el (eglot--sig-info): Use `re-search-forward`. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 46cfb595fe..ae18493f11 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1960,10 +1960,10 @@ is not active." (eglot--dbind ((ParameterInformation) label documentation) (aref parameters active-param) (goto-char params-start) - (let ((case-fold-search nil)) + (let ((regex (concat "\\<" (regexp-quote label) "\\>")) + (case-fold-search nil)) (cl-loop for nmatches from 0 - while (and (not (string-empty-p label)) - (search-forward label params-end t)) + while (re-search-forward regex params-end t) finally do (when (= 1 nmatches) (add-face-text-property commit cdee0e4674d837469b12b9378e24950bea82a555 Author: Fredrik Bergroth Date: Thu Dec 13 12:59:57 2018 +0100 Adjust active param highlighting in first line of signature (1/3) JT@2018/12/16: Previously, the whole first line of the rendered documentation was searched for, potentially highlighting params in the wrong place. Copyright-paperwork-exempt: yes * eglot.el (eglot--sig-info): Search for active parameter within `params-start` and `params-end`. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2b51c2155b..46cfb595fe 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1941,38 +1941,41 @@ is not active." (eglot--dbind ((SignatureInformation) label documentation parameters) sig (with-temp-buffer (save-excursion (insert label)) - (when (looking-at "\\([^(]+\\)(") - (add-face-text-property (match-beginning 1) (match-end 1) - 'font-lock-function-name-face)) - - (when (and (stringp documentation) (eql i active-sig) - (string-match "[[:space:]]*\\([^.\r\n]+[.]?\\)" - documentation)) - (setq documentation (match-string 1 documentation)) - (unless (string-prefix-p (string-trim documentation) label) - (goto-char (point-max)) - (insert ": " (eglot--format-markup documentation)))) - (when (and (eql i active-sig) active-param - (< -1 active-param (length parameters))) - (eglot--dbind ((ParameterInformation) label documentation) - (aref parameters active-param) - (goto-char (point-min)) - (let ((case-fold-search nil)) - (cl-loop for nmatches from 0 - while (and (not (string-empty-p label)) - (search-forward label nil t)) - finally do - (when (= 1 nmatches) - (add-face-text-property - (- (point) (length label)) (point) - 'eldoc-highlight-function-argument)))) - (when documentation + (let ((params-start (point-min)) + (params-end (point-max))) + (when (looking-at "\\([^(]+\\)(") + (setq params-start (match-end 0)) + (add-face-text-property (match-beginning 1) (match-end 1) + 'font-lock-function-name-face)) + + (when (and (stringp documentation) (eql i active-sig) + (string-match "[[:space:]]*\\([^.\r\n]+[.]?\\)" + documentation)) + (setq documentation (match-string 1 documentation)) + (unless (string-prefix-p (string-trim documentation) label) (goto-char (point-max)) - (insert "\n" - (propertize - label 'face 'eldoc-highlight-function-argument) - ": " (eglot--format-markup documentation))))) - (buffer-string))) + (insert ": " (eglot--format-markup documentation)))) + (when (and (eql i active-sig) active-param + (< -1 active-param (length parameters))) + (eglot--dbind ((ParameterInformation) label documentation) + (aref parameters active-param) + (goto-char params-start) + (let ((case-fold-search nil)) + (cl-loop for nmatches from 0 + while (and (not (string-empty-p label)) + (search-forward label params-end t)) + finally do + (when (= 1 nmatches) + (add-face-text-property + (- (point) (length label)) (point) + 'eldoc-highlight-function-argument)))) + (when documentation + (goto-char (point-max)) + (insert "\n" + (propertize + label 'face 'eldoc-highlight-function-argument) + ": " (eglot--format-markup documentation))))) + (buffer-string)))) when moresigs concat "\n")) (defun eglot-help-at-point () commit d44340815666c633e0d20eb32959f45140664dc9 Author: Fredrik Bergroth Date: Thu Dec 13 12:55:28 2018 +0100 Apply eglot--format-markup to signature documentation Copyright-paperwork-exempt: yes * eglot.el (eglot--sig-info): Call eglot--format-markup on signature documentation. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index db4fbb4a7a..2b51c2155b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1951,7 +1951,7 @@ is not active." (setq documentation (match-string 1 documentation)) (unless (string-prefix-p (string-trim documentation) label) (goto-char (point-max)) - (insert ": " documentation))) + (insert ": " (eglot--format-markup documentation)))) (when (and (eql i active-sig) active-param (< -1 active-param (length parameters))) (eglot--dbind ((ParameterInformation) label documentation) commit 89e8803f61839e5914344db846ae212be429f988 Author: João Távora Date: Mon Dec 10 00:10:57 2018 +0000 * eglot.el (version): bump to 1.3 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f442b2fb24..db4fbb4a7a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 1.2 +;; Version: 1.3 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot commit 7e7a9483a62be14bb62e7c4af1ccbfae81421002 Author: João Távora Date: Fri Dec 7 22:51:40 2018 +0000 Be lenient by default to unknown methods or notifications * eglot.el (eglot-strict-mode): Describe meaning of disallow-non-standard-keys. (eglot-handle-notification, eglot-handle-request): Check eglot-strict-mode. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 05971e13cd..f442b2fb24 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -260,7 +260,8 @@ Here's what an element of this alist might look like: )) "How strictly to check LSP interfaces at compile- and run-time. -Value is a list of symbols: +Value is a list of symbols (if the list is empty, no checks are +performed). If the symbol `disallow-non-standard-keys' is present, an error is raised if any extraneous fields are sent by the server. At @@ -276,9 +277,9 @@ If the symbol `enforce-optional-keys' is present, nothing special happens at run-time. At compile-time, a warning is raised if a destructuring spec doesn't use all optional fields. -If the list is empty, any non-standard fields sent by the server -and missing required fields are accepted (which may or may not -cause problems in Eglot's functioning later on).")) +If the symbol `disallow-unknown-methods' is present, Eglot warns +on unknown notifications and errors on unknown requests. +")) (defun eglot--plist-keys (plist) (cl-loop for (k _v) on plist by #'cddr collect k)) @@ -1316,13 +1317,15 @@ Uses THING, FACE, DEFS and PREPEND." (cl-defmethod eglot-handle-notification (_server method &key &allow-other-keys) "Handle unknown notification" - (unless (string-prefix-p "$" (format "%s" method)) + (unless (or (string-prefix-p "$" (format "%s" method)) + (not (memq 'disallow-unknown-methods eglot-strict-mode))) (eglot--warn "Server sent unknown notification method `%s'" method))) (cl-defmethod eglot-handle-request (_server method &key &allow-other-keys) "Handle unknown request" - (jsonrpc-error "Unknown request method `%s'" method)) + (when (memq 'disallow-unknown-methods eglot-strict-mode) + (jsonrpc-error "Unknown request method `%s'" method))) (cl-defmethod eglot-execute-command (server command arguments) commit 6d3310d83ca568daf4092b9118b872feef5d12b0 Author: João Távora Date: Fri Dec 7 22:43:13 2018 +0000 Handle array params to server notification or requests * eglot.el (eglot-handle-notification): Remove extraneous id (eglot--connect): If params is an array, make it a list. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 45529307d6..05971e13cd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -429,7 +429,7 @@ treated as in `eglot-dbind'." (cl-defgeneric eglot-handle-request (server method &rest params) "Handle SERVER's METHOD request with PARAMS.") -(cl-defgeneric eglot-handle-notification (server method id &rest params) +(cl-defgeneric eglot-handle-notification (server method &rest params) "Handle SERVER's METHOD notification with PARAMS.") (cl-defgeneric eglot-execute-command (server command arguments) @@ -783,10 +783,8 @@ This docstring appeases checkdoc, that's all." :noquery t :stderr (get-buffer-create (format "*%s stderr*" readable-name)))))))) - (spread - (lambda (fn) - (lambda (&rest args) - (apply fn (append (butlast args) (car (last args))))))) + (spread (lambda (fn) (lambda (server method params) + (apply fn server method (append params nil))))) (server (apply #'make-instance class commit dfd413c22d235b37f1ad25127b98b5827d2cb1ad Author: João Távora Date: Fri Dec 7 14:46:23 2018 +0000 Scratch/use elpa flymake () Use GNU ELPA's Flymake * eglot.el (Package-Requires): require Flymake 1.0.2. (version< emacs-version "27.0"): Remove horrible hack * Makefile (ELPADEPS): Renamed from JSONRPC. (%.elc): Use it. (eglot-check): Use it. GitHub-reference: https://github.com/joaotavora/eglot/issues/178 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0286f75869..45529307d6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -7,7 +7,7 @@ ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.6")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.6") (flymake "1.0.2")) ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by @@ -2400,40 +2400,6 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." "Eclipse JDT breaks spec and replies with edits as arguments." (mapc #'eglot--apply-workspace-edit arguments)) - -;; FIXME: A horrible hack of Flymake's insufficient API that must go -;; into Emacs master, or better, 26.2 -(when (version< emacs-version "27.0") - (cl-defstruct (eglot--diag (:include flymake--diag) - (:constructor eglot--make-diag-1)) - data-1) - (defsubst eglot--make-diag (buffer beg end type text data) - (let ((sym (alist-get type eglot--diag-error-types-to-old-types))) - (eglot--make-diag-1 :buffer buffer :beg beg :end end :type sym - :text text :data-1 data))) - (defsubst eglot--diag-data (diag) - (and (eglot--diag-p diag) (eglot--diag-data-1 diag))) - (defvar eglot--diag-error-types-to-old-types - '((eglot-error . :error) - (eglot-warning . :warning) - (eglot-note . :note))) - (advice-add - 'flymake--highlight-line :after - (lambda (diag) - (when (eglot--diag-p diag) - (let ((ov (cl-find diag - (overlays-at (flymake-diagnostic-beg diag)) - :key (lambda (ov) - (overlay-get ov 'flymake-diagnostic)))) - (overlay-properties - (get (car (rassoc (flymake-diagnostic-type diag) - eglot--diag-error-types-to-old-types)) - 'flymake-overlay-control))) - (cl-loop for (k . v) in overlay-properties - do (overlay-put ov k v))))) - '((name . eglot-hacking-in-some-per-diag-overlay-properties)))) - - (provide 'eglot) ;;; eglot.el ends here commit 60f45f0f30ae5ba13c913be25166baff30ccba5c Author: João Távora Date: Thu Dec 6 18:26:17 2018 +0000 Warn about suspicious interface usage at compile-time For fun, set eglot-strict-mode to '(disallow-non-standard-keys enforce-required-keys enforce-optional-keys) when compiling, or just use flymake-mode in eglot.el. * eglot.el (eglot--lsp-interface-alist): Use in compile-time. Order alphabetically. Fix a few bugs. (eglot-strict-mode): Disallow non-standard-keys when compiling. Update docstring. (eglot--keywordize-vars, eglot--check-interface): New compile-time-helpers. (eglot--dbind, eglot--dcase): Use new helpers. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ba61de9315..0286f75869 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -203,40 +203,41 @@ let the buffer grow forever." ;;; Message verification helpers ;;; -(defvar eglot--lsp-interface-alist - `((CodeAction (:title) (:kind :diagnostics :edit :command)) - (Command (:title :command) (:arguments)) - (FileSystemWatcher (:globPattern) (:kind)) - (Registration (:id :method) (:registerOptions)) - (Hover (:contents) (:range)) - (SymbolInformation - (:name :kind :location) - (:deprecated :containerName)) - (Position (:line :character)) - (Range (:start :end)) - (Location (:uri :range)) - (Diagnostic (:range :message) - (:severity :code :source :relatedInformation)) - (TextEdit (:range :newText)) - (TextDocumentEdit (:textDocument :edits) ()) - (VersionedTextDocumentIdentifier (:uri :version) ()) - (WorkspaceEdit () (:changes :documentChanges)) - (MarkupContent (:kind :value)) - (InitializeResult (:capabilities)) - (ShowMessageParams (:type :message)) - (ShowMessageRequestParams (:type :message) (:actions)) - (LogMessageParams (:type :message)) - (Registration (:id :method) (:registerOptions)) - (CompletionItem - (:label ) - (:kind :detail :documentation :deprecated :preselect :sortText :filterText - :insertText :insertTextFormat :textEdit :additionalTextEdits? - :commitCharacters :command :data)) - (SignatureHelp (:signatures) (:activeSignature :activeParameter)) - (SignatureInformation (:label) (:documentation :parameters)) - (ParameterInformation (:label) (:documentation)) - (DocumentHighlight) (:range) (:kind)) - "Alist (INTERFACE-NAME . INTERFACE) of known external LSP interfaces. +(eval-and-compile + (defvar eglot--lsp-interface-alist + `( + (CodeAction (:title) (:kind :diagnostics :edit :command)) + (Command (:title :command) (:arguments)) + (CompletionItem (:label) + (:kind :detail :documentation :deprecated :preselect + :sortText :filterText :insertText :insertTextFormat + :textEdit :additionalTextEdits :commitCharacters + :command :data)) + (Diagnostic (:range :message) (:severity :code :source :relatedInformation)) + (DocumentHighlight (:range) (:kind)) + (FileSystemWatcher (:globPattern) (:kind)) + (Hover (:contents) (:range)) + (InitializeResult (:capabilities)) + (Location (:uri :range)) + (LogMessageParams (:type :message)) + (MarkupContent (:kind :value)) + (ParameterInformation (:label) (:documentation)) + (Position (:line :character)) + (Range (:start :end)) + (Registration (:id :method) (:registerOptions)) + (Registration (:id :method) (:registerOptions)) + (ResponseError (:code :message) (:data)) + (ShowMessageParams (:type :message)) + (ShowMessageRequestParams (:type :message) (:actions)) + (SignatureHelp (:signatures) (:activeSignature :activeParameter)) + (SignatureInformation (:label) (:documentation :parameters)) + (SymbolInformation (:name :kind :location) (:deprecated :containerName)) + (TextDocumentEdit (:textDocument :edits) ()) + (TextEdit (:range :newText)) + (VersionedTextDocumentIdentifier (:uri :version) ()) + (WorkspaceEdit () (:changes :documentChanges)) + ) + "Alist (INTERFACE-NAME . INTERFACE) of known external LSP interfaces. INTERFACE-NAME is a symbol designated by the spec as \"interface\". INTERFACE is a list (REQUIRED OPTIONAL) where @@ -246,23 +247,38 @@ message adhering to that interface. Here's what an element of this alist might look like: - (CreateFile . ((:kind :uri) (:options)))") + (CreateFile . ((:kind :uri) (:options)))")) -(defvar eglot-strict-mode '() - "How strictly Eglot vetoes LSP messages from server. +(eval-and-compile + (defvar eglot-strict-mode (if load-file-name '() + '(disallow-non-standard-keys + ;; Uncomment these two for fun at + ;; compile-time or with flymake-mode. + ;; + ;; enforce-required-keys + ;; enforce-optional-keys + )) + "How strictly to check LSP interfaces at compile- and run-time. Value is a list of symbols: -If a list containing the symbol `disallow-non-standard-keys', an -error is raised if any non-standard fields are sent by the -server. +If the symbol `disallow-non-standard-keys' is present, an error +is raised if any extraneous fields are sent by the server. At +compile-time, a warning is raised if a destructuring spec +includes such a field. -If the list containing the symbol `enforce-required-keys', an error -is raised if any required fields are missing from the message. +If the symbol `enforce-required-keys' is present, an error is +raised if any required fields are missing from the message sent +from the server. At compile-time, a warning is raised if a +destructuring spec doesn't use such a field. + +If the symbol `enforce-optional-keys' is present, nothing special +happens at run-time. At compile-time, a warning is raised if a +destructuring spec doesn't use all optional fields. If the list is empty, any non-standard fields sent by the server and missing required fields are accepted (which may or may not -cause problems in Eglot's functioning later on).") +cause problems in Eglot's functioning later on).")) (defun eglot--plist-keys (plist) (cl-loop for (k _v) on plist by #'cddr collect k)) @@ -280,6 +296,45 @@ cause problems in Eglot's functioning later on).") (eglot--error "A `%s' mustn't have %s" (car interface) excess)) (funcall fn)) +(eval-and-compile + (defun eglot--keywordize-vars (vars) + (mapcar (lambda (var) (intern (format ":%s" var))) vars)) + + (defun eglot--check-interface (interface-name vars) + (let ((interface + (assoc interface-name eglot--lsp-interface-alist))) + (cond (interface + (let ((too-many + (and + (memq 'disallow-non-standard-keys eglot-strict-mode) + (cl-set-difference + (eglot--keywordize-vars vars) + (append (car (cdr interface)) + (cadr (cdr interface)))))) + (ignored-required + (and + (memq 'enforce-required-keys eglot-strict-mode) + (cl-set-difference + (car (cdr interface)) + (eglot--keywordize-vars vars)))) + (missing-out + (and + (memq 'enforce-optional-keys eglot-strict-mode) + (cl-set-difference + (cadr (cdr interface)) + (eglot--keywordize-vars vars))))) + (when too-many (byte-compile-warn + "Destructuring for %s has extraneous %s" + interface-name too-many)) + (when ignored-required (byte-compile-warn + "Destructuring for %s ignores required %s" + interface-name ignored-required)) + (when missing-out (byte-compile-warn + "Destructuring for %s is missing out on %s" + interface-name missing-out)))) + (t + (byte-compile-warn "Unknown LSP interface %s" interface-name)))))) + (cl-defmacro eglot--dbind (vars object &body body) "Destructure OBJECT of binding VARS in BODY. VARS is ([(INTERFACE)] SYMS...) @@ -290,8 +345,7 @@ Honour `eglot-strict-mode'." (object-once (make-symbol "object-once")) (fn-once (make-symbol "fn-once"))) (cond (interface-name - ;; jt@2018-11-29: maybe we check some things at compile - ;; time and use `byte-compiler-warn' here + (eglot--check-interface interface-name vars) `(let ((,object-once ,object)) (cl-destructuring-bind (&key ,@vars &allow-other-keys) ,object-once (eglot--call-with-interface (assoc ',interface-name @@ -325,31 +379,32 @@ treated as in `eglot-dbind'." (cond ,@(cl-loop for (vars . body) in clauses - for vars-as-keywords = (mapcar (lambda (var) - (intern (format ":%s" var))) - vars) + for vars-as-keywords = (eglot--keywordize-vars vars) for interface-name = (if (consp (car vars)) (car (pop vars))) for condition = - (if interface-name - ;; In this mode, we assume `eglot-strict-mode' is fully - ;; on, otherwise we can't disambiguate between certain - ;; types. - `(let* ((interface - (or (assoc ',interface-name eglot--lsp-interface-alist) - (eglot--error "Unknown interface %s"))) - (object-keys (eglot--plist-keys ,obj-once)) - (required-keys (car (cdr interface)))) - (and (null (cl-set-difference required-keys object-keys)) - (null (cl-set-difference - (cl-set-difference object-keys required-keys) - (cadr (cdr interface)))))) - ;; In this interface-less mode we don't check - ;; `eglot-strict-mode' at all: just check that the object - ;; has all the keys the user wants to destructure. - `(null (cl-set-difference - ',vars-as-keywords - (eglot--plist-keys ,obj-once)))) + (cond (interface-name + (eglot--check-interface interface-name vars) + ;; In this mode, in runtime, we assume + ;; `eglot-strict-mode' is fully on, otherwise we + ;; can't disambiguate between certain types. + `(let* ((interface + (or (assoc ',interface-name eglot--lsp-interface-alist) + (eglot--error "Unknown LSP interface %s" + ',interface-name))) + (object-keys (eglot--plist-keys ,obj-once)) + (required-keys (car (cdr interface)))) + (and (null (cl-set-difference required-keys object-keys)) + (null (cl-set-difference + (cl-set-difference object-keys required-keys) + (cadr (cdr interface))))))) + (t + ;; In this interface-less mode we don't check + ;; `eglot-strict-mode' at all: just check that the object + ;; has all the keys the user wants to destructure. + `(null (cl-set-difference + ',vars-as-keywords + (eglot--plist-keys ,obj-once))))) collect `(,condition (cl-destructuring-bind (&key ,@vars &allow-other-keys) ,obj-once commit 30ab4e3eedd0ca08e7d4d8ad5c303f58c40e9228 Author: João Távora Date: Wed Dec 5 19:54:55 2018 +0000 Use eglot--dbind and eglot--lambda throughout The default behaviour of these macros is to be lenient towards servers sending unknown keys, which should fix the issue. * eglot.el (eglot--lsp-interface-alist): Add a bunch of new interfaces. (eglot--connect, eglot-handle-notification) (xref-backend-identifier-completion-table) (xref-backend-definitions, xref-backend-apropos) (xref-backend-references, eglot-completion-at-point) (eglot--sig-info, eglot-help-at-point, eglot-eldoc-function) (eglot-imenu, eglot--apply-text-edits) (eglot--apply-workspace-edit) (eglot--register-workspace/didChangeWatchedFiles): Use eglot--dbind and eglot--lambda to destructure LSP objects. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/144 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 727b76166d..ba61de9315 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -208,8 +208,34 @@ let the buffer grow forever." (Command (:title :command) (:arguments)) (FileSystemWatcher (:globPattern) (:kind)) (Registration (:id :method) (:registerOptions)) + (Hover (:contents) (:range)) + (SymbolInformation + (:name :kind :location) + (:deprecated :containerName)) + (Position (:line :character)) + (Range (:start :end)) + (Location (:uri :range)) + (Diagnostic (:range :message) + (:severity :code :source :relatedInformation)) + (TextEdit (:range :newText)) (TextDocumentEdit (:textDocument :edits) ()) - (WorkspaceEdit () (:changes :documentChanges))) + (VersionedTextDocumentIdentifier (:uri :version) ()) + (WorkspaceEdit () (:changes :documentChanges)) + (MarkupContent (:kind :value)) + (InitializeResult (:capabilities)) + (ShowMessageParams (:type :message)) + (ShowMessageRequestParams (:type :message) (:actions)) + (LogMessageParams (:type :message)) + (Registration (:id :method) (:registerOptions)) + (CompletionItem + (:label ) + (:kind :detail :documentation :deprecated :preselect :sortText :filterText + :insertText :insertTextFormat :textEdit :additionalTextEdits? + :commitCharacters :command :data)) + (SignatureHelp (:signatures) (:activeSignature :activeParameter)) + (SignatureInformation (:label) (:documentation :parameters)) + (ParameterInformation (:label) (:documentation)) + (DocumentHighlight) (:range) (:kind)) "Alist (INTERFACE-NAME . INTERFACE) of known external LSP interfaces. INTERFACE-NAME is a symbol designated by the spec as @@ -741,7 +767,7 @@ This docstring appeases checkdoc, that's all." server) :capabilities (eglot-client-capabilities server)) :success-fn - (jsonrpc-lambda (&key capabilities) + (eglot--lambda ((InitializeResult) capabilities) (unless cancelled (push server (gethash project eglot--servers-by-project)) @@ -769,7 +795,7 @@ in project `%s'." (eglot--project-nickname server)) (when tag (throw tag t)))) :timeout eglot-connect-timeout - :error-fn (jsonrpc-lambda (&key code message _data) + :error-fn (eglot--lambda ((ResponseError) code message) (unless cancelled (jsonrpc-shutdown server) (let ((msg (format "%s: %s" code message))) @@ -1288,13 +1314,12 @@ COMMAND is a symbol naming the command." (with-current-buffer buffer (cl-loop for diag-spec across diagnostics - collect (cl-destructuring-bind (&key range ((:severity sev)) _group - _code source message - &allow-other-keys) + collect (eglot--dbind ((Diagnostic) range message severity source) diag-spec (setq message (concat source ": " message)) (pcase-let - ((`(,beg . ,end) (eglot--range-region range))) + ((sev severity) + (`(,beg . ,end) (eglot--range-region range))) ;; Fallback to `flymake-diag-region' if server ;; botched the range (when (= beg end) @@ -1613,8 +1638,8 @@ Try to visit the target file for a richer summary line." (lambda (string) (setq eglot--xref-known-symbols (mapcar - (jsonrpc-lambda - (&key name kind location containerName _deprecated) + (eglot--lambda + ((SymbolInformation) name kind location containerName) (propertize name :textDocumentPositionParams (list :textDocument text-id @@ -1649,7 +1674,7 @@ Try to visit the target file for a richer summary line." (and definitions (if (vectorp definitions) definitions (vector definitions))))) (eglot--handling-xrefs - (mapcar (jsonrpc-lambda (&key uri range) + (mapcar (eglot--lambda ((Location) uri range) (eglot--xref-make identifier uri range)) locations)))) @@ -1664,7 +1689,7 @@ Try to visit the target file for a richer summary line." (eglot--error "Don' know where %s is in the workspace!" identifier)) (eglot--handling-xrefs (mapcar - (jsonrpc-lambda (&key uri range) + (eglot--lambda ((Location) uri range) (eglot--xref-make identifier uri range)) (jsonrpc-request (eglot--current-server-or-lose) :textDocument/references @@ -1677,8 +1702,8 @@ Try to visit the target file for a richer summary line." (when (eglot--server-capable :workspaceSymbolProvider) (eglot--handling-xrefs (mapcar - (jsonrpc-lambda (&key name location &allow-other-keys) - (cl-destructuring-bind (&key uri range) location + (eglot--lambda ((SymbolInformation) name location) + (eglot--dbind ((Location) uri range) location (eglot--xref-make name uri range))) (jsonrpc-request (eglot--current-server-or-lose) :workspace/symbol @@ -1746,16 +1771,15 @@ is not active." (string-trim-left label)) (t (or insertText (string-trim-left label)))))) - (setq all (append all `(:bounds ,bounds))) (add-text-properties 0 1 all completion) + (put-text-property 0 1 'eglot--completion-bounds bounds completion) (put-text-property 0 1 'eglot--lsp-completion all completion) completion)) items))))) :annotation-function (lambda (obj) - (cl-destructuring-bind (&key detail kind insertTextFormat - &allow-other-keys) - (text-properties-at 0 obj) + (eglot--dbind ((CompletionItem) detail kind insertTextFormat) + (get-text-property 0 'eglot--lsp-completion obj) (let* ((detail (and (stringp detail) (not (string= detail "")) detail)) @@ -1806,15 +1830,18 @@ is not active." ;; buffer, `comp' won't have any properties. A ;; lookup should fix that (github#148) (cl-find comp strings :test #'string=)))) - (cl-destructuring-bind (&key insertTextFormat - insertText - textEdit - additionalTextEdits - bounds - &allow-other-keys) - (text-properties-at 0 comp) + (eglot--dbind ((CompletionItem) insertTextFormat + insertText + textEdit + additionalTextEdits) + (get-text-property 0 'eglot--lsp-completion comp) (let ((snippet-fn (and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)))) + (eglot--snippet-expansion-fn))) + ;; FIXME: it would have been much easier to fetch + ;; these from the lexical environment, but we can't + ;; in company because of + ;; https://github.com/company-mode/company-mode/pull/845 + (bounds (get-text-property 0 'eglot--completion-bounds comp))) (cond (textEdit ;; Undo the just the completed bit. If before ;; completion the buffer was "foo.b" and now is @@ -1825,7 +1852,7 @@ is not active." (delete-region (+ (- (point) (length comp)) (if bounds (- (cdr bounds) (car bounds)) 0)) (point)) - (cl-destructuring-bind (&key range newText) textEdit + (eglot--dbind ((TextEdit) range newText) textEdit (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (delete-region beg end) (goto-char beg) @@ -1854,47 +1881,48 @@ is not active." (defun eglot--sig-info (sigs active-sig active-param) (cl-loop for (sig . moresigs) on (append sigs nil) for i from 0 - concat (cl-destructuring-bind (&key label documentation parameters) sig - (with-temp-buffer - (save-excursion (insert label)) - (when (looking-at "\\([^(]+\\)(") - (add-face-text-property (match-beginning 1) (match-end 1) - 'font-lock-function-name-face)) - - (when (and (stringp documentation) (eql i active-sig) - (string-match "[[:space:]]*\\([^.\r\n]+[.]?\\)" - documentation)) - (setq documentation (match-string 1 documentation)) - (unless (string-prefix-p (string-trim documentation) label) - (goto-char (point-max)) - (insert ": " documentation))) - (when (and (eql i active-sig) active-param - (< -1 active-param (length parameters))) - (cl-destructuring-bind (&key label documentation) - (aref parameters active-param) - (goto-char (point-min)) - (let ((case-fold-search nil)) - (cl-loop for nmatches from 0 - while (and (not (string-empty-p label)) - (search-forward label nil t)) - finally do - (when (= 1 nmatches) - (add-face-text-property - (- (point) (length label)) (point) - 'eldoc-highlight-function-argument)))) - (when documentation - (goto-char (point-max)) - (insert "\n" - (propertize - label 'face 'eldoc-highlight-function-argument) - ": " (eglot--format-markup documentation))))) - (buffer-string))) + concat + (eglot--dbind ((SignatureInformation) label documentation parameters) sig + (with-temp-buffer + (save-excursion (insert label)) + (when (looking-at "\\([^(]+\\)(") + (add-face-text-property (match-beginning 1) (match-end 1) + 'font-lock-function-name-face)) + + (when (and (stringp documentation) (eql i active-sig) + (string-match "[[:space:]]*\\([^.\r\n]+[.]?\\)" + documentation)) + (setq documentation (match-string 1 documentation)) + (unless (string-prefix-p (string-trim documentation) label) + (goto-char (point-max)) + (insert ": " documentation))) + (when (and (eql i active-sig) active-param + (< -1 active-param (length parameters))) + (eglot--dbind ((ParameterInformation) label documentation) + (aref parameters active-param) + (goto-char (point-min)) + (let ((case-fold-search nil)) + (cl-loop for nmatches from 0 + while (and (not (string-empty-p label)) + (search-forward label nil t)) + finally do + (when (= 1 nmatches) + (add-face-text-property + (- (point) (length label)) (point) + 'eldoc-highlight-function-argument)))) + (when documentation + (goto-char (point-max)) + (insert "\n" + (propertize + label 'face 'eldoc-highlight-function-argument) + ": " (eglot--format-markup documentation))))) + (buffer-string))) when moresigs concat "\n")) (defun eglot-help-at-point () "Request \"hover\" information for the thing at point." (interactive) - (cl-destructuring-bind (&key contents range) + (eglot--dbind ((Hover) contents range) (jsonrpc-request (eglot--current-server-or-lose) :textDocument/hover (eglot--TextDocumentPositionParams)) (when (seq-empty-p contents) (eglot--error "No hover info here")) @@ -1917,8 +1945,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (jsonrpc-async-request server :textDocument/signatureHelp position-params :success-fn - (jsonrpc-lambda (&key signatures activeSignature - activeParameter) + (eglot--lambda ((SignatureHelp) + signatures activeSignature activeParameter) (when-buffer-window (when (cl-plusp (length signatures)) (setq sig-showing t) @@ -1929,7 +1957,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (when (eglot--server-capable :hoverProvider) (jsonrpc-async-request server :textDocument/hover position-params - :success-fn (jsonrpc-lambda (&key contents range) + :success-fn (eglot--lambda ((Hover) contents range) (unless sig-showing (when-buffer-window (when-let (info (and contents @@ -1946,7 +1974,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (setq eglot--highlights (when-buffer-window (mapcar - (jsonrpc-lambda (&key range _kind _role) + (eglot--lambda ((DocumentHighlight) range) (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (let ((ov (make-overlay beg end))) @@ -1962,8 +1990,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (if (eglot--server-capable :documentSymbolProvider) (let ((entries (mapcar - (jsonrpc-lambda - (&key name kind location containerName _deprecated) + (eglot--lambda + ((SymbolInformation) name kind location containerName) (cons (propertize name :kind (alist-get kind eglot--symbol-kind-names @@ -2030,7 +2058,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." beg (+ beg (length newText)) length)))) (progress-reporter-update reporter (cl-incf done))))))) - (mapcar (jsonrpc-lambda (&key range newText) + (mapcar (eglot--lambda ((TextEdit) range newText) (cons newText (eglot--range-region range 'markers))) (reverse edits))) (undo-amalgamate-change-group change-group) @@ -2041,7 +2069,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (eglot--dbind ((WorkspaceEdit) changes documentChanges) wedit (let ((prepared (mapcar (eglot--lambda ((TextDocumentEdit) textDocument edits) - (cl-destructuring-bind (&key uri version) textDocument + (eglot--dbind ((VersionedTextDocumentIdentifier) uri version) + textDocument (list (eglot--uri-to-path uri) edits version))) documentChanges)) edit) @@ -2055,7 +2084,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (mapconcat #'identity (mapcar #'car prepared) "\n "))) (eglot--error "User cancelled server edit"))) (while (setq edit (car prepared)) - (cl-destructuring-bind (path edits &optional version) edit + (pcase-let ((`(,path ,edits ,version) edit)) (with-current-buffer (find-file-noselect path) (eglot--apply-text-edits edits version)) (pop prepared)) @@ -2153,7 +2182,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (cl-labels ((handle-event (event) - (cl-destructuring-bind (desc action file &optional file1) event + (pcase-let ((`(,desc ,action ,file ,file1) event)) (cond ((and (memq action '(created changed deleted)) (cl-find file globs commit 0918c9d1a585f8c86bdd13c39704240b3ab81fc9 Author: João Távora Date: Tue Dec 4 23:10:35 2018 +0000 Adjust previous fix * eglot.el (eglot--before-change): Don't reset eglot--last-inserted-char here. (eglot--pre-command-hook): Do it here. (eglot--managed-mode): Add/remove eglot--pre-command-hook to/from pre-command-hook. GitHub-reference: per https://github.com/joaotavora/eglot/issues/173 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3bb93dcc4f..727b76166d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1055,6 +1055,7 @@ and just return it. PROMPT shouldn't end with a question mark." (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) (add-hook 'change-major-mode-hook 'eglot--managed-mode-onoff nil t) (add-hook 'post-self-insert-hook 'eglot--post-self-insert-hook nil t) + (add-hook 'pre-command-hook 'eglot--pre-command-hook nil t) (add-function :before-until (local 'eldoc-documentation-function) #'eglot-eldoc-function) (add-function :around (local 'imenu-create-index-function) #'eglot-imenu) @@ -1072,6 +1073,7 @@ and just return it. PROMPT shouldn't end with a question mark." (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) (remove-hook 'change-major-mode-hook #'eglot--managed-mode-onoff t) (remove-hook 'post-self-insert-hook 'eglot--post-self-insert-hook t) + (remove-hook 'pre-command-hook 'eglot--pre-command-hook t) (remove-function (local 'eldoc-documentation-function) #'eglot-eldoc-function) (remove-function (local 'imenu-create-index-function) #'eglot-imenu) @@ -1394,6 +1396,10 @@ THINGS are either registrations or unregisterations." "Set `eglot--last-inserted-char.'" (setq eglot--last-inserted-char last-input-event)) +(defun eglot--pre-command-hook () + "Reset `eglot--last-inserted-char.'" + (setq eglot--last-inserted-char nil)) + (defun eglot--CompletionParams () (append (eglot--TextDocumentPositionParams) @@ -1424,10 +1430,7 @@ THINGS are either registrations or unregisterations." (when (listp eglot--recent-changes) (push `(,(eglot--pos-to-lsp-position start) ,(eglot--pos-to-lsp-position end)) - eglot--recent-changes)) - ;; Also, reset `eglot--last-inserted-char' which might be set later - ;; by `eglot--post-self-insert-hook'. - (setq eglot--last-inserted-char nil)) + eglot--recent-changes))) (defun eglot--after-change (start end pre-change-length) "Hook onto `after-change-functions'. commit f2326f4e13ecf7c244b31aa727cec6981056ace2 Author: João Távora Date: Tue Dec 4 09:47:36 2018 +0000 Fix bug introduced by previous fix * eglot.el (eglot--CompletionParams): Don't use last-input-event. GitHub-reference: per https://github.com/joaotavora/eglot/issues/173 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 578a90d27e..3bb93dcc4f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1399,7 +1399,7 @@ THINGS are either registrations or unregisterations." (eglot--TextDocumentPositionParams) `(:context ,(if-let (trigger (and (characterp eglot--last-inserted-char) - (cl-find last-input-event + (cl-find eglot--last-inserted-char (eglot--server-capable :completionProvider :triggerCharacters) :key (lambda (str) (aref str 0)) @@ -2116,8 +2116,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (keyboard-quit) retval))))) (eglot--dcase action - (((Command) command arguments) - (eglot-execute-command server (intern command) arguments)) + (((Command) command arguments) + (eglot-execute-command server (intern command) arguments)) (((CodeAction) edit command) (when edit (eglot--apply-workspace-edit edit)) (when command commit b873654835710e7c51a0b2073467162800551bb0 Author: João Távora Date: Mon Dec 3 14:07:21 2018 +0000 Handle codeaction/command polymorphism with eglot--dcase * eglot-tests.el (eglot-dcase): Augment test. * eglot.el (eglot--lsp-interface-alist): Add Command interface. (eglot--dcase): Fix indentation. When given interface, always assume strict mode. (eglot-code-actions): Use eglot--dcase. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/164 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e4547c5161..578a90d27e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -204,13 +204,12 @@ let the buffer grow forever." ;;; Message verification helpers ;;; (defvar eglot--lsp-interface-alist - `( - (CodeAction (:title) (:kind :diagnostics :edit :command)) + `((CodeAction (:title) (:kind :diagnostics :edit :command)) + (Command (:title :command) (:arguments)) (FileSystemWatcher (:globPattern) (:kind)) (Registration (:id :method) (:registerOptions)) (TextDocumentEdit (:textDocument :edits) ()) - (WorkspaceEdit () (:changes :documentChanges)) - ) + (WorkspaceEdit () (:changes :documentChanges))) "Alist (INTERFACE-NAME . INTERFACE) of known external LSP interfaces. INTERFACE-NAME is a symbol designated by the spec as @@ -294,6 +293,7 @@ Honour `eglot-strict-mode'." "Like `pcase', but for the LSP object OBJ. CLAUSES is a list (DESTRUCTURE FORMS...) where DESTRUCTURE is treated as in `eglot-dbind'." + (declare (indent 1)) (let ((obj-once (make-symbol "obj-once"))) `(let ((,obj-once ,obj)) (cond @@ -306,19 +306,21 @@ treated as in `eglot-dbind'." (car (pop vars))) for condition = (if interface-name + ;; In this mode, we assume `eglot-strict-mode' is fully + ;; on, otherwise we can't disambiguate between certain + ;; types. `(let* ((interface (or (assoc ',interface-name eglot--lsp-interface-alist) (eglot--error "Unknown interface %s"))) (object-keys (eglot--plist-keys ,obj-once)) (required-keys (car (cdr interface)))) (and (null (cl-set-difference required-keys object-keys)) - (or (null (memq 'disallow-non-standard-keys - eglot-strict-mode)) - (null (cl-set-difference - (cl-set-difference object-keys required-keys) - (cadr (cdr interface))))))) + (null (cl-set-difference + (cl-set-difference object-keys required-keys) + (cadr (cdr interface)))))) ;; In this interface-less mode we don't check - ;; `eglot-strict-mode' at all. + ;; `eglot-strict-mode' at all: just check that the object + ;; has all the keys the user wants to destructure. `(null (cl-set-difference ',vars-as-keywords (eglot--plist-keys ,obj-once)))) @@ -2100,9 +2102,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (eglot--diag-data diag)))) (flymake-diagnostics beg end))])))) (menu-items - (or (mapcar (eglot--lambda ((CodeAction) title edit command arguments) - `(,title . (:command ,command :arguments ,arguments - :edit ,edit))) + (or (mapcar (jsonrpc-lambda (&rest all &key title &allow-other-keys) + (cons title all)) actions) (eglot--error "No code actions here"))) (menu `("Eglot code actions:" ("dummy" ,@menu-items))) @@ -2114,11 +2115,14 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (if (eq (setq retval (tmm-prompt menu)) never-mind) (keyboard-quit) retval))))) - (cl-destructuring-bind (&key _title command arguments edit) action - (when edit - (eglot--apply-workspace-edit edit)) - (when command - (eglot-execute-command server (intern command) arguments))))) + (eglot--dcase action + (((Command) command arguments) + (eglot-execute-command server (intern command) arguments)) + (((CodeAction) edit command) + (when edit (eglot--apply-workspace-edit edit)) + (when command + (eglot--dbind ((Command) command arguments) command + (eglot-execute-command server (intern command) arguments))))))) commit 3b9e5b1a84ada8769bdcd95f0d21674c7fc57714 Author: João Távora Date: Mon Dec 3 12:24:26 2018 +0000 Robustify previous fix against non-standard insertion bindings * eglot.el (eglot--managed-mode): Manage post-self-insert-hook. (eglot--last-inserted-char): New variable. (eglot--post-self-insert-hook): Set it. (eglot--before-change): Reset it. (eglot--CompletionParams): Use it. GitHub-reference: per https://github.com/joaotavora/eglot/issues/173 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 86db5ea322..e4547c5161 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1052,6 +1052,7 @@ and just return it. PROMPT shouldn't end with a question mark." (add-hook 'xref-backend-functions 'eglot-xref-backend nil t) (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) (add-hook 'change-major-mode-hook 'eglot--managed-mode-onoff nil t) + (add-hook 'post-self-insert-hook 'eglot--post-self-insert-hook nil t) (add-function :before-until (local 'eldoc-documentation-function) #'eglot-eldoc-function) (add-function :around (local 'imenu-create-index-function) #'eglot-imenu) @@ -1068,6 +1069,7 @@ and just return it. PROMPT shouldn't end with a question mark." (remove-hook 'xref-backend-functions 'eglot-xref-backend t) (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) (remove-hook 'change-major-mode-hook #'eglot--managed-mode-onoff t) + (remove-hook 'post-self-insert-hook 'eglot--post-self-insert-hook t) (remove-function (local 'eldoc-documentation-function) #'eglot-eldoc-function) (remove-function (local 'imenu-create-index-function) #'eglot-imenu) @@ -1383,12 +1385,18 @@ THINGS are either registrations or unregisterations." (list :textDocument (eglot--TextDocumentIdentifier) :position (eglot--pos-to-lsp-position))) +(defvar-local eglot--last-inserted-char nil + "If non-nil, value of the last inserted character in buffer.") + +(defun eglot--post-self-insert-hook () + "Set `eglot--last-inserted-char.'" + (setq eglot--last-inserted-char last-input-event)) + (defun eglot--CompletionParams () (append (eglot--TextDocumentPositionParams) `(:context - ,(if-let (trigger (and (eq last-command 'self-insert-command) - (characterp last-input-event) + ,(if-let (trigger (and (characterp eglot--last-inserted-char) (cl-find last-input-event (eglot--server-capable :completionProvider :triggerCharacters) @@ -1406,15 +1414,18 @@ THINGS are either registrations or unregisterations." (defvar-local eglot--change-idle-timer nil "Idle timer for didChange signals.") (defun eglot--before-change (start end) - "Hook onto `before-change-functions'. -Records START and END, crucially convert them into -LSP (line/char) positions before that information is -lost (because the after-change thingy doesn't know if newlines -were deleted/added)" + "Hook onto `before-change-functions'." + ;; Records START and END, crucially convert them into LSP + ;; (line/char) positions before that information is lost (because + ;; the after-change thingy doesn't know if newlines were + ;; deleted/added) (when (listp eglot--recent-changes) (push `(,(eglot--pos-to-lsp-position start) ,(eglot--pos-to-lsp-position end)) - eglot--recent-changes))) + eglot--recent-changes)) + ;; Also, reset `eglot--last-inserted-char' which might be set later + ;; by `eglot--post-self-insert-hook'. + (setq eglot--last-inserted-char nil)) (defun eglot--after-change (start end pre-change-length) "Hook onto `after-change-functions'. commit 4a9914c0e66ebb2ba41e894a58d2598e8dc44687 Author: Michal Krzywkowski Date: Mon Dec 3 12:49:34 2018 +0100 Properly clear old diagnostics when making new ones * eglot.el (eglot-handle-notification textDocument/publishDiagnostics): Call flymake report function with :region. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/159 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2e99fc0cdd..86db5ea322 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1313,7 +1313,13 @@ COMMAND is a symbol naming the command." message `((eglot-lsp-diag . ,diag-spec))))) into diags finally (cond ((and flymake-mode eglot--current-flymake-report-fn) - (funcall eglot--current-flymake-report-fn diags) + (funcall eglot--current-flymake-report-fn diags + ;; If the buffer hasn't changed since last + ;; call to the report function, flymake won't + ;; delete old diagnostics. Using :region + ;; keyword forces flymake to delete + ;; them (github#159). + :region (cons (point-min) (point-max))) (setq eglot--unreported-diagnostics nil)) (t (setq eglot--unreported-diagnostics (cons t diags)))))) commit 1db7873cc111ce09d72093f6e76a1ccfea3afdec Author: Mario Rodas Date: Sun Dec 2 14:16:58 2018 -0500 Use javascript-typescript-langserver for typescript-mode () Copyright-paperwork-exempt: Yes * eglot.el (eglot-server-programs): add typescript-mode to javascript-typescript-langserver's contact GitHub-reference: https://github.com/joaotavora/eglot/issues/174 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ac10b434c7..2e99fc0cdd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -82,7 +82,9 @@ (python-mode . ("pyls")) ((js-mode js2-mode - rjsx-mode) . ("javascript-typescript-stdio")) + rjsx-mode + typescript-mode) + . ("javascript-typescript-stdio")) (sh-mode . ("bash-language-server" "start")) ((c++-mode c-mode) . ("ccls")) ((caml-mode tuareg-mode reason-mode) commit 949cf4e7a9725d414760dd02d463c3e67786b102 Author: João Távora Date: Sun Dec 2 10:54:09 2018 +0000 Support completioncontext to help servers like ccls * eglot.el (eglot-client-capabilities): Annouce textDocument/completion/contextSupport. (eglot--CompletionParams): New helper. (eglot-completion-at-point): Use it. GitHub-reference: close https://github.com/joaotavora/eglot/issues/173 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b240e0e860..ac10b434c7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -374,7 +374,8 @@ treated as in `eglot-dbind'." `(:snippetSupport ,(if (eglot--snippet-expansion-fn) t - :json-false))) + :json-false)) + :contextSupport t) :hover `(:dynamicRegistration :json-false) :signatureHelp `(:dynamicRegistration :json-false) :references `(:dynamicRegistration :json-false) @@ -1374,6 +1375,19 @@ THINGS are either registrations or unregisterations." (list :textDocument (eglot--TextDocumentIdentifier) :position (eglot--pos-to-lsp-position))) +(defun eglot--CompletionParams () + (append + (eglot--TextDocumentPositionParams) + `(:context + ,(if-let (trigger (and (eq last-command 'self-insert-command) + (characterp last-input-event) + (cl-find last-input-event + (eglot--server-capable :completionProvider + :triggerCharacters) + :key (lambda (str) (aref str 0)) + :test #'char-equal))) + `(:triggerKind 2 :triggerCharacter ,trigger) `(:triggerKind 1))))) + (defvar-local eglot--recent-changes nil "Recent buffer changes as collected by `eglot--before-change'.") @@ -1693,7 +1707,7 @@ is not active." (lambda (_ignored) (let* ((resp (jsonrpc-request server :textDocument/completion - (eglot--TextDocumentPositionParams) + (eglot--CompletionParams) :deferred :textDocument/completion :cancel-on-input t)) (items (if (vectorp resp) resp (plist-get resp :items)))) commit 92ce9a30f1cfa8a974b9e40dec1126751aef1737 Author: João Távora Date: Sat Dec 1 22:47:56 2018 +0000 Don't break in indirect buffers Indirect buffers, such as the ones created by ediff-regions-wordwise, have eglot enabled but not buffer-file-name. Resort to the finding the file-name of the original buffer for these. * eglot.el (eglot--TextDocumentIdentifier): Work in indirect buffers. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/116 GitHub-reference: fix https://github.com/joaotavora/eglot/issues/150 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8d3977091d..b240e0e860 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1345,7 +1345,10 @@ THINGS are either registrations or unregisterations." (defun eglot--TextDocumentIdentifier () "Compute TextDocumentIdentifier object for current buffer." - `(:uri ,(eglot--path-to-uri buffer-file-name))) + `(:uri ,(eglot--path-to-uri (or buffer-file-name + (ignore-errors + (buffer-file-name + (buffer-base-buffer))))))) (defvar-local eglot--versioned-identifier 0) commit c515075fcb3d1bafed2dc881e059ba6bf1814f12 Author: Michal Krzywkowski Date: Tue Nov 27 23:42:45 2018 +0100 Use eglot--dbind for destructuring * eglot.el (eglot--lsp-interface-alist): Add CodeAction, FileSystemWatcher, Registration, TextDocumentEdit, WorkspaceEdit. (eglot-handle-notification): Use eglot--dbind. (eglot--apply-workspace-edit): Use eglot--dbind and eglot--lambda. (eglot-code-actions): Use eglot--lambda. (eglot--register-workspace/didChangeWatchedFiles): Use eglot--lambda. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 61f9b70a5c..8d3977091d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -201,7 +201,14 @@ let the buffer grow forever." ;;; Message verification helpers ;;; -(defvar eglot--lsp-interface-alist `() +(defvar eglot--lsp-interface-alist + `( + (CodeAction (:title) (:kind :diagnostics :edit :command)) + (FileSystemWatcher (:globPattern) (:kind)) + (Registration (:id :method) (:registerOptions)) + (TextDocumentEdit (:textDocument :edits) ()) + (WorkspaceEdit () (:changes :documentChanges)) + ) "Alist (INTERFACE-NAME . INTERFACE) of known external LSP interfaces. INTERFACE-NAME is a symbol designated by the spec as @@ -1314,7 +1321,7 @@ COMMAND is a symbol naming the command." THINGS are either registrations or unregisterations." (cl-loop for thing in (cl-coerce things 'list) - collect (cl-destructuring-bind (&key id method registerOptions) thing + collect (eglot--dbind ((Registration) id method registerOptions) thing (apply (intern (format "eglot--%s-%s" how method)) server :id id registerOptions)) into results @@ -1990,9 +1997,9 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (defun eglot--apply-workspace-edit (wedit &optional confirm) "Apply the workspace edit WEDIT. If CONFIRM, ask user first." - (cl-destructuring-bind (&key changes documentChanges) wedit + (eglot--dbind ((WorkspaceEdit) changes documentChanges) wedit (let ((prepared - (mapcar (jsonrpc-lambda (&key textDocument edits) + (mapcar (eglot--lambda ((TextDocumentEdit) textDocument edits) (cl-destructuring-bind (&key uri version) textDocument (list (eglot--uri-to-path uri) edits version))) documentChanges)) @@ -2057,8 +2064,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (eglot--diag-data diag)))) (flymake-diagnostics beg end))])))) (menu-items - (or (mapcar (jsonrpc-lambda (&key title command arguments - edit _kind _diagnostics) + (or (mapcar (eglot--lambda ((CodeAction) title edit command arguments) `(,title . (:command ,command :arguments ,arguments :edit ,edit))) actions) @@ -2098,7 +2104,9 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." "Handle dynamic registration of workspace/didChangeWatchedFiles" (eglot--unregister-workspace/didChangeWatchedFiles server :id id) (let* (success - (globs (mapcar (lambda (w) (plist-get w :globPattern)) watchers))) + (globs (mapcar (eglot--lambda ((FileSystemWatcher) globPattern) + globPattern) + watchers))) (cl-labels ((handle-event (event) commit 0bce2e3b2b3fe57cdbb63560241c65d406732252 Author: João Távora Date: Thu Nov 29 22:36:03 2018 +0000 Introduce eglot--dcase * eglot.el (eglot--dcase): New macro. * eglot-tests.el (eglot-dcase-with-interface) (eglot-dcase-no-interface): New tests. GitHub-reference: per https://github.com/joaotavora/eglot/issues/171 GitHub-reference: per https://github.com/joaotavora/eglot/issues/156 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 594a638ad5..61f9b70a5c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -281,6 +281,47 @@ Honour `eglot-strict-mode'." (let ((e (cl-gensym "jsonrpc-lambda-elem"))) `(lambda (,e) (eglot--dbind ,cl-lambda-list ,e ,@body)))) +(cl-defmacro eglot--dcase (obj &rest clauses) + "Like `pcase', but for the LSP object OBJ. +CLAUSES is a list (DESTRUCTURE FORMS...) where DESTRUCTURE is +treated as in `eglot-dbind'." + (let ((obj-once (make-symbol "obj-once"))) + `(let ((,obj-once ,obj)) + (cond + ,@(cl-loop + for (vars . body) in clauses + for vars-as-keywords = (mapcar (lambda (var) + (intern (format ":%s" var))) + vars) + for interface-name = (if (consp (car vars)) + (car (pop vars))) + for condition = + (if interface-name + `(let* ((interface + (or (assoc ',interface-name eglot--lsp-interface-alist) + (eglot--error "Unknown interface %s"))) + (object-keys (eglot--plist-keys ,obj-once)) + (required-keys (car (cdr interface)))) + (and (null (cl-set-difference required-keys object-keys)) + (or (null (memq 'disallow-non-standard-keys + eglot-strict-mode)) + (null (cl-set-difference + (cl-set-difference object-keys required-keys) + (cadr (cdr interface))))))) + ;; In this interface-less mode we don't check + ;; `eglot-strict-mode' at all. + `(null (cl-set-difference + ',vars-as-keywords + (eglot--plist-keys ,obj-once)))) + collect `(,condition + (cl-destructuring-bind (&key ,@vars &allow-other-keys) + ,obj-once + ,@body))) + (t + (eglot--error "%s didn't match any of %s" + ,obj-once + ',(mapcar #'car clauses))))))) + ;;; API (WORK-IN-PROGRESS!) ;;; commit bec802d0032054494911472b51b8bd421b0131df Author: João Távora Date: Wed Nov 28 20:26:37 2018 +0000 Simplify interface of eglot--dbind macro * eglot.el (eglot--dbind): Use new interface. (eglot--lambda): Use new eglot--dbind interface. (eglot--lsp-interface-alist): Fix docstring. (eglot--call-with-interface): Simplify. (eglot--plist-keys): New helper. * eglot-tests.el (eglot-strict-interfaces): Add a new test clause. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2519189ca4..594a638ad5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -204,8 +204,8 @@ let the buffer grow forever." (defvar eglot--lsp-interface-alist `() "Alist (INTERFACE-NAME . INTERFACE) of known external LSP interfaces. -INTERFACE-NAME is a symbol designated by the spec as \"export -interface\". INTERFACE is a list (REQUIRED OPTIONAL) where +INTERFACE-NAME is a symbol designated by the spec as +\"interface\". INTERFACE is a list (REQUIRED OPTIONAL) where REQUIRED and OPTIONAL are lists of keyword symbols designating field names that must be, or may be, respectively, present in a message adhering to that interface. @@ -230,60 +230,56 @@ If the list is empty, any non-standard fields sent by the server and missing required fields are accepted (which may or may not cause problems in Eglot's functioning later on).") +(defun eglot--plist-keys (plist) + (cl-loop for (k _v) on plist by #'cddr collect k)) + (defun eglot--call-with-interface (interface object fn) - "Call FN, but first check that OBJECT conforms to INTERFACE. - -INTERFACE is a key to `eglot--lsp-interface-alist' and OBJECT is - a plist representing an LSP message." - (let* ((entry (assoc interface eglot--lsp-interface-alist)) - (required (car (cdr entry))) - (optional (cadr (cdr entry)))) - (when (memq 'enforce-required-keys eglot-strict-mode) - (cl-loop for req in required - when (eq 'eglot--not-present - (cl-getf object req 'eglot--not-present)) - collect req into missing - finally (when missing - (eglot--error - "A `%s' must have %s" interface missing)))) - (when (and entry (memq 'disallow-non-standard-keys eglot-strict-mode)) - (cl-loop - with allowed = (append required optional) - for (key _val) on object by #'cddr - unless (memq key allowed) collect key into disallowed - finally (when disallowed - (eglot--error - "A `%s' mustn't have %s" interface disallowed)))) - (funcall fn))) - -(cl-defmacro eglot--dbind (interface lambda-list object &body body) - "Destructure OBJECT of INTERFACE as CL-LAMBDA-LIST. + "Call FN, checking that OBJECT conforms to INTERFACE." + (when-let ((missing (and (memq 'enforce-required-keys eglot-strict-mode) + (cl-set-difference (car (cdr interface)) + (eglot--plist-keys object))))) + (eglot--error "A `%s' must have %s" (car interface) missing)) + (when-let ((excess (and (memq 'disallow-non-standard-keys eglot-strict-mode) + (cl-set-difference + (eglot--plist-keys object) + (append (car (cdr interface)) (cadr (cdr interface))))))) + (eglot--error "A `%s' mustn't have %s" (car interface) excess)) + (funcall fn)) + +(cl-defmacro eglot--dbind (vars object &body body) + "Destructure OBJECT of binding VARS in BODY. +VARS is ([(INTERFACE)] SYMS...) Honour `eglot-strict-mode'." - (declare (indent 3)) - (let ((fn-once `(lambda () ,@body)) - (lax-lambda-list (if (memq '&allow-other-keys lambda-list) - lambda-list - (append lambda-list '(&allow-other-keys)))) - (strict-lambda-list (delete '&allow-other-keys lambda-list))) - (if interface - `(cl-destructuring-bind ,lax-lambda-list ,object - (eglot--call-with-interface ',interface ,object ,fn-once)) - (let ((object-once (make-symbol "object-once"))) - `(let ((,object-once ,object)) - (if (memq 'disallow-non-standard-keys eglot-strict-mode) - (cl-destructuring-bind ,strict-lambda-list ,object-once - (funcall ,fn-once)) - (cl-destructuring-bind ,lax-lambda-list ,object-once - (funcall ,fn-once)))))))) - -(cl-defmacro eglot--lambda (interface cl-lambda-list &body body) + (declare (indent 2)) + (let ((interface-name (if (consp (car vars)) + (car (pop vars)))) + (object-once (make-symbol "object-once")) + (fn-once (make-symbol "fn-once"))) + (cond (interface-name + ;; jt@2018-11-29: maybe we check some things at compile + ;; time and use `byte-compiler-warn' here + `(let ((,object-once ,object)) + (cl-destructuring-bind (&key ,@vars &allow-other-keys) ,object-once + (eglot--call-with-interface (assoc ',interface-name + eglot--lsp-interface-alist) + ,object-once (lambda () + ,@body))))) + (t + `(let ((,object-once ,object) + (,fn-once (lambda (,@vars) ,@body))) + (if (memq 'disallow-non-standard-keys eglot-strict-mode) + (cl-destructuring-bind (&key ,@vars) ,object-once + (funcall ,fn-once ,@vars)) + (cl-destructuring-bind (&key ,@vars &allow-other-keys) ,object-once + (funcall ,fn-once ,@vars)))))))) + + +(cl-defmacro eglot--lambda (cl-lambda-list &body body) "Function of args CL-LAMBDA-LIST for processing INTERFACE objects. Honour `eglot-strict-mode'." - (declare (indent 2)) + (declare (indent 1)) (let ((e (cl-gensym "jsonrpc-lambda-elem"))) - `(lambda (,e) - (eglot--dbind ,interface ,cl-lambda-list ,e - ,@body)))) + `(lambda (,e) (eglot--dbind ,cl-lambda-list ,e ,@body)))) ;;; API (WORK-IN-PROGRESS!) commit ad2efe30748d60cfe1e89030a2c649b02ad1ae33 Author: Michal Krzywkowski Date: Wed Nov 28 19:53:35 2018 +0100 Touch up last commit * eglot.el (eglot-current-column): Rename from eglot--current-column. (eglot-current-column-function): Use it as value and mention in docstring. (eglot--xref-make): Use eglot-current-column. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 94de8d1bc4..2519189ca4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -811,16 +811,16 @@ CONNECT-ARGS are passed as additional arguments to (let ((warning-minimum-level :error)) (display-warning 'eglot (apply #'format format args) :warning))) -(defun eglot--current-column () (- (point) (point-at-bol))) +(defun eglot-current-column () (- (point) (point-at-bol))) -(defvar eglot-current-column-function #'eglot--current-column +(defvar eglot-current-column-function #'eglot-current-column "Function to calculate the current column. This is the inverse operation of `eglot-move-to-column-function' (which see). It is a function of no arguments returning a column number. For buffers managed by fully LSP-compliant servers, this should be set to -`eglot-lsp-abiding-column', and `current-column' (the default) +`eglot-lsp-abiding-column', and `eglot-current-column' (the default) for all others.") (defun eglot-lsp-abiding-column () @@ -1506,7 +1506,7 @@ Try to visit the target file for a richer summary line." (substring (buffer-substring bol (point-at-eol)))) (add-face-text-property (- beg bol) (- end bol) 'highlight t substring) - (list substring (1+ (current-line)) (eglot--current-column)))))) + (list substring (1+ (current-line)) (eglot-current-column)))))) (`(,summary ,line ,column) (cond (visiting (with-current-buffer visiting (funcall collect))) commit 1f3499320cca1f3c3ea2722bec5a8c2df003e2b7 Author: Michal Krzywkowski Date: Tue Nov 27 23:28:11 2018 +0100 * eglot.el (eglot--current-column): new helper. (eglot-current-column-function): Set to eglot--current-column. (eglot--pos-to-lsp-position): Don't bind tab-width anymore. (eglot--xref-make): Use eglot--current-column. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1522935281..94de8d1bc4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -811,7 +811,9 @@ CONNECT-ARGS are passed as additional arguments to (let ((warning-minimum-level :error)) (display-warning 'eglot (apply #'format format args) :warning))) -(defvar eglot-current-column-function #'current-column +(defun eglot--current-column () (- (point) (point-at-bol))) + +(defvar eglot-current-column-function #'eglot--current-column "Function to calculate the current column. This is the inverse operation of @@ -833,8 +835,7 @@ for all others.") (eglot--widening (list :line (1- (line-number-at-pos pos t)) ; F!@&#$CKING OFF-BY-ONE :character (progn (when pos (goto-char pos)) - (let ((tab-width 1)) - (funcall eglot-current-column-function)))))) + (funcall eglot-current-column-function))))) (defvar eglot-move-to-column-function #'move-to-column "Function to move to a column reported by the LSP server. @@ -1502,11 +1503,10 @@ Try to visit the target file for a richer summary line." (eglot--widening (pcase-let* ((`(,beg . ,end) (eglot--range-region range)) (bol (progn (goto-char beg) (point-at-bol))) - (substring (buffer-substring bol (point-at-eol))) - (tab-width 1)) + (substring (buffer-substring bol (point-at-eol)))) (add-face-text-property (- beg bol) (- end bol) 'highlight t substring) - (list substring (1+ (current-line)) (current-column)))))) + (list substring (1+ (current-line)) (eglot--current-column)))))) (`(,summary ,line ,column) (cond (visiting (with-current-buffer visiting (funcall collect))) commit e63203a8a70a59e5138740ec447d281b71ce7334 Author: João Távora Date: Tue Nov 27 13:49:30 2018 +0000 Improve performance of xref summary line collection * eglot.el (eglot--temp-location-buffers): New variable. (eglot--handling-xrefs): New macro. (eglot--xref-make): Use eglot--temp-location-buffers. (xref-backend-definitions, xref-backend-references) (xref-backend-apropos): Use eglot--handling-xrefs. GitHub-reference: per https://github.com/joaotavora/eglot/issues/52 GitHub-reference: per https://github.com/joaotavora/eglot/issues/127 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8ba483b167..1522935281 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1478,13 +1478,26 @@ DUMMY is ignored." (advice-add 'xref-find-definitions :after #'eglot--xref-reset-known-symbols) (advice-add 'xref-find-references :after #'eglot--xref-reset-known-symbols) +(defvar eglot--temp-location-buffers (make-hash-table :test #'equal) + "Helper variable for `eglot--handling-xrefs'.") + +(defmacro eglot--handling-xrefs (&rest body) + "Properly sort and handle xrefs produced and returned by BODY." + `(unwind-protect + (sort (progn ,@body) + (lambda (a b) + (< (xref-location-line (xref-item-location a)) + (xref-location-line (xref-item-location b))))) + (maphash (lambda (_uri buf) (kill-buffer buf)) eglot--temp-location-buffers) + (clrhash eglot--temp-location-buffers))) + (defun eglot--xref-make (name uri range) "Like `xref-make' but with LSP's NAME, URI and RANGE. Try to visit the target file for a richer summary line." (pcase-let* - ((`(,beg . ,end) (eglot--range-region range)) - (file (eglot--uri-to-path uri)) - (visiting (find-buffer-visiting file)) + ((file (eglot--uri-to-path uri)) + (visiting (or (find-buffer-visiting file) + (gethash uri eglot--temp-location-buffers))) (collect (lambda () (eglot--widening (pcase-let* ((`(,beg . ,end) (eglot--range-region range)) @@ -1497,19 +1510,16 @@ Try to visit the target file for a richer summary line." (`(,summary ,line ,column) (cond (visiting (with-current-buffer visiting (funcall collect))) - ((file-readable-p file) (with-temp-buffer (insert-file-contents file) - (funcall collect))) + ((file-readable-p file) (with-current-buffer + (puthash uri (generate-new-buffer " *temp*") + eglot--temp-location-buffers) + (insert-file-contents file) + (funcall collect))) (t ;; fall back to the "dumb strategy" (let ((start (cl-getf range :start))) (list name (1+ (cl-getf start :line)) (cl-getf start :character))))))) (xref-make summary (xref-make-file-location file line column)))) -(defun eglot--sort-xrefs (xrefs) - (sort xrefs - (lambda (a b) - (< (xref-location-line (xref-item-location a)) - (xref-location-line (xref-item-location b)))))) - (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) (when (eglot--server-capable :documentSymbolProvider) (let ((server (eglot--current-server-or-lose)) @@ -1553,7 +1563,7 @@ Try to visit the target file for a richer summary line." (locations (and definitions (if (vectorp definitions) definitions (vector definitions))))) - (eglot--sort-xrefs + (eglot--handling-xrefs (mapcar (jsonrpc-lambda (&key uri range) (eglot--xref-make identifier uri range)) locations)))) @@ -1567,7 +1577,7 @@ Try to visit the target file for a richer summary line." (and rich (get-text-property 0 :textDocumentPositionParams rich)))))) (unless params (eglot--error "Don' know where %s is in the workspace!" identifier)) - (eglot--sort-xrefs + (eglot--handling-xrefs (mapcar (jsonrpc-lambda (&key uri range) (eglot--xref-make identifier uri range)) @@ -1580,7 +1590,7 @@ Try to visit the target file for a richer summary line." (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) - (eglot--sort-xrefs + (eglot--handling-xrefs (mapcar (jsonrpc-lambda (&key name location &allow-other-keys) (cl-destructuring-bind (&key uri range) location commit 9e700ebc4c03254a832dce922a89b997b6f29354 Author: João Távora Date: Tue Nov 27 12:42:56 2018 +0000 Use entire line as xref summary when available After an original implementation by Michael Livshin. Also close https://github.com/joaotavora/eglot/issues/127. * eglot.el (eglot--xref-make): Rework. (xref-backend-definitions, xref-backend-references) (xref-backend-apropos): Simplify call to `eglot--xref-make'. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/52 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4996f5b639..8ba483b167 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1478,13 +1478,31 @@ DUMMY is ignored." (advice-add 'xref-find-definitions :after #'eglot--xref-reset-known-symbols) (advice-add 'xref-find-references :after #'eglot--xref-reset-known-symbols) -(defun eglot--xref-make (name uri position) - "Like `xref-make' but with LSP's NAME, URI and POSITION." - (cl-destructuring-bind (&key line character) position - (xref-make name (xref-make-file-location - (eglot--uri-to-path uri) - ;; F!@(#*&#$)CKING OFF-BY-ONE again - (1+ line) character)))) +(defun eglot--xref-make (name uri range) + "Like `xref-make' but with LSP's NAME, URI and RANGE. +Try to visit the target file for a richer summary line." + (pcase-let* + ((`(,beg . ,end) (eglot--range-region range)) + (file (eglot--uri-to-path uri)) + (visiting (find-buffer-visiting file)) + (collect (lambda () + (eglot--widening + (pcase-let* ((`(,beg . ,end) (eglot--range-region range)) + (bol (progn (goto-char beg) (point-at-bol))) + (substring (buffer-substring bol (point-at-eol))) + (tab-width 1)) + (add-face-text-property (- beg bol) (- end bol) 'highlight + t substring) + (list substring (1+ (current-line)) (current-column)))))) + (`(,summary ,line ,column) + (cond + (visiting (with-current-buffer visiting (funcall collect))) + ((file-readable-p file) (with-temp-buffer (insert-file-contents file) + (funcall collect))) + (t ;; fall back to the "dumb strategy" + (let ((start (cl-getf range :start))) + (list name (1+ (cl-getf start :line)) (cl-getf start :character))))))) + (xref-make summary (xref-make-file-location file line column)))) (defun eglot--sort-xrefs (xrefs) (sort xrefs @@ -1537,7 +1555,7 @@ DUMMY is ignored." (if (vectorp definitions) definitions (vector definitions))))) (eglot--sort-xrefs (mapcar (jsonrpc-lambda (&key uri range) - (eglot--xref-make identifier uri (plist-get range :start))) + (eglot--xref-make identifier uri range)) locations)))) (cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) @@ -1552,7 +1570,7 @@ DUMMY is ignored." (eglot--sort-xrefs (mapcar (jsonrpc-lambda (&key uri range) - (eglot--xref-make identifier uri (plist-get range :start))) + (eglot--xref-make identifier uri range)) (jsonrpc-request (eglot--current-server-or-lose) :textDocument/references (append @@ -1566,7 +1584,7 @@ DUMMY is ignored." (mapcar (jsonrpc-lambda (&key name location &allow-other-keys) (cl-destructuring-bind (&key uri range) location - (eglot--xref-make name uri (plist-get range :start)))) + (eglot--xref-make name uri range))) (jsonrpc-request (eglot--current-server-or-lose) :workspace/symbol `(:query ,pattern)))))) commit 6ae6ce8b922bba07219bb8e10b67cc81a4703995 Author: João Távora Date: Fri Nov 23 18:12:14 2018 +0000 Revert "codeaction command can be a command object ()" This reverts commit 1e7f94d75a30628be56ad00de8c6245f5f3e9fdf. The spec doesn't define Command as implemented in the commit. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/164 GitHub-reference: fix https://github.com/joaotavora/eglot/issues/165 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bc89cd28f3..4996f5b639 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2011,12 +2011,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (when edit (eglot--apply-workspace-edit edit)) (when command - (cond ((stringp command) - (eglot-execute-command server (intern command) arguments)) - ((listp command) - (eglot-execute-command server - (intern (plist-get command :command)) - (plist-get command :arguments)))))))) + (eglot-execute-command server (intern command) arguments))))) commit 1e7f94d75a30628be56ad00de8c6245f5f3e9fdf Author: Michal Krzywkowski Date: Fri Nov 23 19:00:00 2018 +0100 Codeaction command can be a command object () * eglot.el (eglot-code-actions): Handle case when the :command field is not a string. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/164 GitHub-reference: fix https://github.com/joaotavora/eglot/issues/165 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4996f5b639..bc89cd28f3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2011,7 +2011,12 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (when edit (eglot--apply-workspace-edit edit)) (when command - (eglot-execute-command server (intern command) arguments))))) + (cond ((stringp command) + (eglot-execute-command server (intern command) arguments)) + ((listp command) + (eglot-execute-command server + (intern (plist-get command :command)) + (plist-get command :arguments)))))))) commit d3fc3ce7e70df0a92931c31fcadb625d81b9f0bc Author: João Távora Date: Fri Nov 23 12:31:15 2018 +0000 Control strictness towards incoming lsp messages A new variable, eglot-strict-mode controls whether Eglot is strict or lax with regard to incoming LSP messages. 1. Bug reports should be tested with eglot-strict-mode set to '(disallow-non-standard-keys enforce-required-keys) 2. Users struggling to get non-standard servers working set this variable to '(), nil. For now, by popular demand, this is the default value. Note that this commit in particular introduces a new infrastructure, but does not yet alter any code in Eglot to use it. Neither is the variable eglot--lsp-interface-alist populated. * eglot-tests.el (eglot-strict-interfaces): New test. * eglot.el (eglot--lsp-interface-alist): New variable. (eglot-strict-mode): New variable. (eglot--call-with-interface): New helper. (eglot--dbind): New macro. (eglot--lambda): New macro. GitHub-reference: per https://github.com/joaotavora/eglot/issues/144 GitHub-reference: per https://github.com/joaotavora/eglot/issues/156 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 365b5d2181..4996f5b639 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -197,6 +197,94 @@ let the buffer grow forever." (13 . "Enum") (14 . "Keyword") (15 . "Snippet") (16 . "Color") (17 . "File") (18 . "Reference"))) + + +;;; Message verification helpers +;;; +(defvar eglot--lsp-interface-alist `() + "Alist (INTERFACE-NAME . INTERFACE) of known external LSP interfaces. + +INTERFACE-NAME is a symbol designated by the spec as \"export +interface\". INTERFACE is a list (REQUIRED OPTIONAL) where +REQUIRED and OPTIONAL are lists of keyword symbols designating +field names that must be, or may be, respectively, present in a +message adhering to that interface. + +Here's what an element of this alist might look like: + + (CreateFile . ((:kind :uri) (:options)))") + +(defvar eglot-strict-mode '() + "How strictly Eglot vetoes LSP messages from server. + +Value is a list of symbols: + +If a list containing the symbol `disallow-non-standard-keys', an +error is raised if any non-standard fields are sent by the +server. + +If the list containing the symbol `enforce-required-keys', an error +is raised if any required fields are missing from the message. + +If the list is empty, any non-standard fields sent by the server +and missing required fields are accepted (which may or may not +cause problems in Eglot's functioning later on).") + +(defun eglot--call-with-interface (interface object fn) + "Call FN, but first check that OBJECT conforms to INTERFACE. + +INTERFACE is a key to `eglot--lsp-interface-alist' and OBJECT is + a plist representing an LSP message." + (let* ((entry (assoc interface eglot--lsp-interface-alist)) + (required (car (cdr entry))) + (optional (cadr (cdr entry)))) + (when (memq 'enforce-required-keys eglot-strict-mode) + (cl-loop for req in required + when (eq 'eglot--not-present + (cl-getf object req 'eglot--not-present)) + collect req into missing + finally (when missing + (eglot--error + "A `%s' must have %s" interface missing)))) + (when (and entry (memq 'disallow-non-standard-keys eglot-strict-mode)) + (cl-loop + with allowed = (append required optional) + for (key _val) on object by #'cddr + unless (memq key allowed) collect key into disallowed + finally (when disallowed + (eglot--error + "A `%s' mustn't have %s" interface disallowed)))) + (funcall fn))) + +(cl-defmacro eglot--dbind (interface lambda-list object &body body) + "Destructure OBJECT of INTERFACE as CL-LAMBDA-LIST. +Honour `eglot-strict-mode'." + (declare (indent 3)) + (let ((fn-once `(lambda () ,@body)) + (lax-lambda-list (if (memq '&allow-other-keys lambda-list) + lambda-list + (append lambda-list '(&allow-other-keys)))) + (strict-lambda-list (delete '&allow-other-keys lambda-list))) + (if interface + `(cl-destructuring-bind ,lax-lambda-list ,object + (eglot--call-with-interface ',interface ,object ,fn-once)) + (let ((object-once (make-symbol "object-once"))) + `(let ((,object-once ,object)) + (if (memq 'disallow-non-standard-keys eglot-strict-mode) + (cl-destructuring-bind ,strict-lambda-list ,object-once + (funcall ,fn-once)) + (cl-destructuring-bind ,lax-lambda-list ,object-once + (funcall ,fn-once)))))))) + +(cl-defmacro eglot--lambda (interface cl-lambda-list &body body) + "Function of args CL-LAMBDA-LIST for processing INTERFACE objects. +Honour `eglot-strict-mode'." + (declare (indent 2)) + (let ((e (cl-gensym "jsonrpc-lambda-elem"))) + `(lambda (,e) + (eglot--dbind ,interface ,cl-lambda-list ,e + ,@body)))) + ;;; API (WORK-IN-PROGRESS!) ;;; commit 333009a5c5e02dd23d6c7204e8b04f15ffff020c Author: João Távora Date: Fri Nov 23 00:12:45 2018 +0000 * eglot.el (version): bump to 1.2 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b408e59f25..365b5d2181 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 1.1 +;; Version: 1.2 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot commit 95d48a3576400d2ffe4de1747f9a0794eb00f6ba Author: João Távora Date: Fri Nov 23 00:11:44 2018 +0000 * eglot.el (eglot-completion-at-point): less chatter. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9ff9cdf6f5..b408e59f25 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1629,7 +1629,8 @@ is not active." (delete-region beg end) (goto-char beg) (funcall (or snippet-fn #'insert) newText))) - (eglot--apply-text-edits additionalTextEdits)) + (when (cl-plusp (length additionalTextEdits)) + (eglot--apply-text-edits additionalTextEdits))) (snippet-fn ;; A snippet should be inserted, but using plain ;; `insertText'. This requires us to delete the commit fbb7d1e9183df446162f1365056a254e5ec4a29e Author: João Távora Date: Thu Nov 22 23:07:18 2018 +0000 Correctly insert textedit-less snippets Fixes a slight regression from https://github.com/joaotavora/eglot/issues/160. * eglot.el (eglot-completion-at-point): When there is plain `insertText' snippet, delete the full completion text. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/167 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f4a02ac7a9..9ff9cdf6f5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1612,24 +1612,31 @@ is not active." bounds &allow-other-keys) (text-properties-at 0 comp) - (let ((fn (and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)))) - (when (or fn textEdit) - ;; Undo the completion. If before completion the buffer was - ;; "foo.b" and now is "foo.bar", `comp' will be "bar". We - ;; want to delete only "ar" (`comp' minus the symbol whose - ;; bounds we've calculated before) (github#160). - (delete-region (+ (- (point) (length comp)) - (if bounds (- (cdr bounds) (car bounds)) 0)) - (point))) + (let ((snippet-fn (and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)))) (cond (textEdit + ;; Undo the just the completed bit. If before + ;; completion the buffer was "foo.b" and now is + ;; "foo.bar", `comp' will be "bar". We want to + ;; delete only "ar" (`comp' minus the symbol + ;; whose bounds we've calculated before) + ;; (github#160). + (delete-region (+ (- (point) (length comp)) + (if bounds (- (cdr bounds) (car bounds)) 0)) + (point)) (cl-destructuring-bind (&key range newText) textEdit (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (delete-region beg end) (goto-char beg) - (funcall (or fn #'insert) newText))) + (funcall (or snippet-fn #'insert) newText))) (eglot--apply-text-edits additionalTextEdits)) - (fn (funcall fn insertText)))) + (snippet-fn + ;; A snippet should be inserted, but using plain + ;; `insertText'. This requires us to delete the + ;; whole completion, since `insertText' is the full + ;; completion's text. + (delete-region (- (point) (length comp)) (point)) + (funcall snippet-fn insertText)))) (eglot--signal-textDocument/didChange) (eglot-eldoc-function)))))))) commit f62f37d1ed8965eee954ad70794484bcc432de24 Author: João Távora Date: Mon Nov 19 23:16:33 2018 +0000 Fix potential security issue fontifying lsp doc Previously, a server could mistankely or maliciously call *-mode functions by in the response to a completion or hover request, specifically in the :documentation field of the response. Although there are plenty of similar avenues of attack in Emacs, it's probably a good idea not to let LSP servers decide which functions to call in an Emacs session running Eglot. * eglot.el (eglot--format-markup): Call major-mode to fontify buffer, not some dynamically constructed function name. (eglot-completion-at-point): Ensure eglot--format-markup runs in source buffer. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/154 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c382c67e54..f4a02ac7a9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -810,7 +810,7 @@ Doubles as an indicator of snippet support." (if (stringp markup) (list (string-trim markup) (intern "gfm-mode")) (list (plist-get markup :value) - (intern (concat (plist-get markup :language) "-mode" )))))) + major-mode)))) (with-temp-buffer (ignore-errors (funcall mode)) (insert string) (font-lock-ensure) (buffer-string)))) @@ -1585,11 +1585,13 @@ is not active." (get-text-property 0 'eglot--lsp-completion obj) :cancel-on-input t) - :documentation))))) - (when documentation + :documentation)))) + (formatted (and documentation + (eglot--format-markup documentation)))) + (when formatted (with-current-buffer (get-buffer-create " *eglot doc*") (erase-buffer) - (insert (eglot--format-markup documentation)) + (insert formatted) (current-buffer))))) :company-prefix-length (cl-some #'looking-back commit a6536ec8b0c20cab5c04edf7c552077a9d3f6b7d Author: Alex Branham Date: Tue Nov 20 15:27:38 2018 -0600 Add support for r's languageserver () Copyright-paperwork-exempt: yes * eglot.el (eglot-server-programs): Add R language server. * README.md (Installation and usage): Mention it. GitHub-reference: https://github.com/joaotavora/eglot/issues/161 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c65d908978..c382c67e54 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -96,6 +96,8 @@ language-server/bin/php-language-server.php")) (kotlin-mode . ("kotlin-language-server")) (go-mode . ("go-langserver" "-mode=stdio" "-gocodecompletion")) + ((R-mode ess-r-mode) . ("R" "--slave" "-e" + "languageserver::run()")) (java-mode . eglot--eclipse-jdt-contact)) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE commit 0097d8d8327a783c77bed2860c4283541c6608b7 Author: Michal Krzywkowski Date: Wed Nov 21 15:54:22 2018 +0100 Properly delete inserted text after completion * eglot.el (eglot-completion-at-point): In :exit-function, delete only the region of buffer that was inserted by completion. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/160 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d9c1c3ab97..c65d908978 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1543,6 +1543,7 @@ is not active." (string-trim-left label)) (t (or insertText (string-trim-left label)))))) + (setq all (append all `(:bounds ,bounds))) (add-text-properties 0 1 all completion) (put-text-property 0 1 'eglot--lsp-completion all completion) completion)) @@ -1604,13 +1605,19 @@ is not active." insertText textEdit additionalTextEdits + bounds &allow-other-keys) (text-properties-at 0 comp) (let ((fn (and (eql insertTextFormat 2) (eglot--snippet-expansion-fn)))) (when (or fn textEdit) - ;; Undo the completion - (delete-region (- (point) (length comp)) (point))) + ;; Undo the completion. If before completion the buffer was + ;; "foo.b" and now is "foo.bar", `comp' will be "bar". We + ;; want to delete only "ar" (`comp' minus the symbol whose + ;; bounds we've calculated before) (github#160). + (delete-region (+ (- (point) (length comp)) + (if bounds (- (cdr bounds) (car bounds)) 0)) + (point))) (cond (textEdit (cl-destructuring-bind (&key range newText) textEdit (pcase-let ((`(,beg . ,end) (eglot--range-region range))) commit 2cf7905887f2137869f44c3383a55636e38b4b81 Author: Michal Krzywkowski Date: Mon Nov 19 21:22:14 2018 +0100 Treat tab characters as 1 column wide in position conversion functions Fixes https://github.com/joaotavora/eglot/issues/158. * eglot.el (eglot--pos-to-lsp-position): Call eglot-current-column-function with tab-width bound to 1. (eglot--lsp-position-to-point): Call eglot-move-to-column-function with tab-width bound to 1. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3ba1b87d80..d9c1c3ab97 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -743,7 +743,8 @@ for all others.") (eglot--widening (list :line (1- (line-number-at-pos pos t)) ; F!@&#$CKING OFF-BY-ONE :character (progn (when pos (goto-char pos)) - (funcall eglot-current-column-function))))) + (let ((tab-width 1)) + (funcall eglot-current-column-function)))))) (defvar eglot-move-to-column-function #'move-to-column "Function to move to a column reported by the LSP server. @@ -778,7 +779,8 @@ If optional MARKER, return a marker instead" (forward-line (min most-positive-fixnum (plist-get pos-plist :line))) (unless (eobp) ;; if line was excessive leave point at eob - (funcall eglot-move-to-column-function (plist-get pos-plist :character))) + (let ((tab-width 1)) + (funcall eglot-move-to-column-function (plist-get pos-plist :character)))) (if marker (copy-marker (point-marker)) (point)))) (defun eglot--path-to-uri (path) commit 46d2bef4b397257ea232af483c45f49d7eab900c Author: Michal Krzywkowski Date: Sat Nov 17 12:12:31 2018 +0100 Format documentation of signature parameters * eglot.el (eglot--sig-info): Call `eglot--format-markup` on parameter :documentation. GitHub-reference: per https://github.com/joaotavora/eglot/issues/144 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2c0b22e500..3ba1b87d80 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1665,7 +1665,7 @@ is not active." (insert "\n" (propertize label 'face 'eldoc-highlight-function-argument) - ": " documentation)))) + ": " (eglot--format-markup documentation))))) (buffer-string))) when moresigs concat "\n")) commit 8df3bdd653010aebd7083669d5c2a946c73a4d4a Author: João Távora Date: Tue Nov 13 22:08:16 2018 +0000 Add ability to report lsp-compliant columns * eglot.el (eglot-current-column-function): New variable. (eglot-lsp-abiding-column): New helper. (eglot--pos-to-lsp-position): Use eglot-current-column-function. (eglot-move-to-column-function): Tweak docstring. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/125 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d4a8b6f8a3..2c0b22e500 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -721,26 +721,42 @@ CONNECT-ARGS are passed as additional arguments to (let ((warning-minimum-level :error)) (display-warning 'eglot (apply #'format format args) :warning))) +(defvar eglot-current-column-function #'current-column + "Function to calculate the current column. + +This is the inverse operation of +`eglot-move-to-column-function' (which see). It is a function of +no arguments returning a column number. For buffers managed by +fully LSP-compliant servers, this should be set to +`eglot-lsp-abiding-column', and `current-column' (the default) +for all others.") + +(defun eglot-lsp-abiding-column () + "Calculate current COLUMN as defined by the LSP spec." + (/ (- (length (encode-coding-region (line-beginning-position) + (point) 'utf-16 t)) + 2) + 2)) + (defun eglot--pos-to-lsp-position (&optional pos) "Convert point POS to LSP position." (eglot--widening (list :line (1- (line-number-at-pos pos t)) ; F!@&#$CKING OFF-BY-ONE - :character (- (goto-char (or pos (point))) - (line-beginning-position))))) + :character (progn (when pos (goto-char pos)) + (funcall eglot-current-column-function))))) (defvar eglot-move-to-column-function #'move-to-column - "How to move to a column reported by the LSP server. + "Function to move to a column reported by the LSP server. According to the standard, LSP column/character offsets are based on a count of UTF-16 code units, not actual visual columns. So when LSP says position 3 of a line containing just \"aXbc\", where X is a multi-byte character, it actually means `b', not -`c'. This is what the function -`eglot-move-to-lsp-abiding-column' does. +`c'. However, many servers don't follow the spec this closely. -However, many servers don't follow the spec this closely, and -thus this variable should be set to `move-to-column' in buffers -managed by those servers.") +For buffers managed by fully LSP-compliant servers, this should +be set to `eglot-move-to-lsp-abiding-column', and +`move-to-column' (the default) for all others.") (defun eglot-move-to-lsp-abiding-column (column) "Move to COLUMN abiding by the LSP spec." commit 5c97238692a5dcecbefeeaf21b8a4344caca3275 Author: Michal Krzywkowski Date: Wed Oct 31 20:59:30 2018 +0100 Add support for textedits in completion * eglot.el (eglot-completion-at-point): Apply the CompletionItem's :textEdit and :additionalTextEdits when they're present. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 24f8c971f9..d4a8b6f8a3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1584,12 +1584,23 @@ is not active." (cl-find comp strings :test #'string=)))) (cl-destructuring-bind (&key insertTextFormat insertText + textEdit + additionalTextEdits &allow-other-keys) (text-properties-at 0 comp) - (when-let ((fn (and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)))) - (delete-region (- (point) (length comp)) (point)) - (funcall fn insertText)) + (let ((fn (and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)))) + (when (or fn textEdit) + ;; Undo the completion + (delete-region (- (point) (length comp)) (point))) + (cond (textEdit + (cl-destructuring-bind (&key range newText) textEdit + (pcase-let ((`(,beg . ,end) (eglot--range-region range))) + (delete-region beg end) + (goto-char beg) + (funcall (or fn #'insert) newText))) + (eglot--apply-text-edits additionalTextEdits)) + (fn (funcall fn insertText)))) (eglot--signal-textDocument/didChange) (eglot-eldoc-function)))))))) commit 35e431c829d7e554e6c0ab38dde59fee870506a0 Author: João Távora Date: Tue Nov 13 09:46:47 2018 +0000 Tweak solution to with a hint from fangrui song * eglot.el (eglot-move-to-lsp-abiding-column): Simplify slightly. GitHub-reference: https://github.com/joaotavora/eglot/issues/125 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 512e9a689f..24f8c971f9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -752,8 +752,7 @@ managed by those servers.") 2) 2)) until (zerop diff) - for offset = (max 1 (abs (/ diff 2))) - do (if (> diff 0) (forward-char offset) (backward-char offset)))) + do (forward-char (/ (if (> diff 0) (1+ diff) (1- diff)) 2)))) (defun eglot--lsp-position-to-point (pos-plist &optional marker) "Convert LSP position POS-PLIST to Emacs point. commit 6393580d7e386787da98fa4fdb0c034bdd06d331 Author: João Távora Date: Mon Nov 12 23:17:22 2018 +0000 Complex completions work when chosen from *completions* * eglot.el (eglot-completion-at-point): Make exit-function work even if its arguments was stripped of its properties. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/148 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f7b9c86974..512e9a689f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1501,7 +1501,8 @@ is not active." "EGLOT's `completion-at-point' function." (let ((bounds (bounds-of-thing-at-point 'symbol)) (server (eglot--current-server-or-lose)) - (completion-capability (eglot--server-capable :completionProvider))) + (completion-capability (eglot--server-capable :completionProvider)) + strings) (when completion-capability (list (or (car bounds) (point)) @@ -1514,19 +1515,21 @@ is not active." :deferred :textDocument/completion :cancel-on-input t)) (items (if (vectorp resp) resp (plist-get resp :items)))) - (mapcar - (jsonrpc-lambda (&rest all &key label insertText insertTextFormat - &allow-other-keys) - (let ((completion - (cond ((and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)) - (string-trim-left label)) - (t - (or insertText (string-trim-left label)))))) - (add-text-properties 0 1 all completion) - (put-text-property 0 1 'eglot--lsp-completion all completion) - completion)) - items)))) + (setq + strings + (mapcar + (jsonrpc-lambda (&rest all &key label insertText insertTextFormat + &allow-other-keys) + (let ((completion + (cond ((and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)) + (string-trim-left label)) + (t + (or insertText (string-trim-left label)))))) + (add-text-properties 0 1 all completion) + (put-text-property 0 1 'eglot--lsp-completion all completion) + completion)) + items))))) :annotation-function (lambda (obj) (cl-destructuring-bind (&key detail kind insertTextFormat @@ -1572,17 +1575,24 @@ is not active." (cl-some #'looking-back (mapcar #'regexp-quote (plist-get completion-capability :triggerCharacters))) - :exit-function (lambda (obj _status) - (cl-destructuring-bind (&key insertTextFormat - insertText - &allow-other-keys) - (text-properties-at 0 obj) - (when-let ((fn (and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)))) - (delete-region (- (point) (length obj)) (point)) - (funcall fn insertText)) - (eglot--signal-textDocument/didChange) - (eglot-eldoc-function))))))) + :exit-function + (lambda (comp _status) + (let ((comp (if (get-text-property 0 'eglot--lsp-completion comp) + comp + ;; When selecting from the *Completions* + ;; buffer, `comp' won't have any properties. A + ;; lookup should fix that (github#148) + (cl-find comp strings :test #'string=)))) + (cl-destructuring-bind (&key insertTextFormat + insertText + &allow-other-keys) + (text-properties-at 0 comp) + (when-let ((fn (and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)))) + (delete-region (- (point) (length comp)) (point)) + (funcall fn insertText)) + (eglot--signal-textDocument/didChange) + (eglot-eldoc-function)))))))) (defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") commit 9f44e74ca9d2b254fbf9fd1d5ccfde64e2b652f1 Author: João Távora Date: Mon Nov 12 22:29:38 2018 +0000 Add ability to move to lsp-precise columns Also close https://github.com/joaotavora/eglot/issues/125. Idea and much of design contributed by Michał Krzywkowski This introduces the variable eglot-move-to-column-function. According to the standard, LSP column/character offsets are based on a count of UTF-16 code units, not actual visual columns. So when LSP says position 3 of a line containing just \"aXbc\", where X is a multi-byte character, it actually means `b', not `c'. This is what the function `eglot-move-to-lsp-abiding-column' does. However, many servers don't follow the spec this closely, and thus this variable should be set to `move-to-column' in buffers managed by those servers. * eglot.el (eglot-move-to-column-function): New variable. (eglot-move-to-lsp-abiding-column): New function. (eglot--lsp-position-to-point): Use eglot-move-to-column-function. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/124 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 576d7f39d3..f7b9c86974 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -728,16 +728,43 @@ CONNECT-ARGS are passed as additional arguments to :character (- (goto-char (or pos (point))) (line-beginning-position))))) +(defvar eglot-move-to-column-function #'move-to-column + "How to move to a column reported by the LSP server. + +According to the standard, LSP column/character offsets are based +on a count of UTF-16 code units, not actual visual columns. So +when LSP says position 3 of a line containing just \"aXbc\", +where X is a multi-byte character, it actually means `b', not +`c'. This is what the function +`eglot-move-to-lsp-abiding-column' does. + +However, many servers don't follow the spec this closely, and +thus this variable should be set to `move-to-column' in buffers +managed by those servers.") + +(defun eglot-move-to-lsp-abiding-column (column) + "Move to COLUMN abiding by the LSP spec." + (cl-loop + initially (move-to-column column) + with lbp = (line-beginning-position) + for diff = (- column + (/ (- (length (encode-coding-region lbp (point) 'utf-16 t)) + 2) + 2)) + until (zerop diff) + for offset = (max 1 (abs (/ diff 2))) + do (if (> diff 0) (forward-char offset) (backward-char offset)))) + (defun eglot--lsp-position-to-point (pos-plist &optional marker) "Convert LSP position POS-PLIST to Emacs point. If optional MARKER, return a marker instead" - (save-excursion (goto-char (point-min)) - (forward-line (min most-positive-fixnum - (plist-get pos-plist :line))) - (forward-char (min (plist-get pos-plist :character) - (- (line-end-position) - (line-beginning-position)))) - (if marker (copy-marker (point-marker)) (point)))) + (save-excursion + (goto-char (point-min)) + (forward-line (min most-positive-fixnum + (plist-get pos-plist :line))) + (unless (eobp) ;; if line was excessive leave point at eob + (funcall eglot-move-to-column-function (plist-get pos-plist :character))) + (if marker (copy-marker (point-marker)) (point)))) (defun eglot--path-to-uri (path) "URIfy PATH." @@ -1040,7 +1067,7 @@ Uses THING, FACE, DEFS and PREPEND." (priority . ,(+ 50 i)) (keymap . ,(let ((map (make-sparse-keymap))) (define-key map [mouse-1] - (eglot--mouse-call 'eglot-code-actions)) + (eglot--mouse-call 'eglot-code-actions)) map))))) commit a0365b6f81864488b249de23c2b399e475f312a0 Author: João Távora Date: Fri Nov 9 01:46:55 2018 +0000 Fix a bug introduced by previous bugfix This commit fixes a bug but introduced another when completing a symbol in xref-find-definitions. commit ee58d92a7de1c8b1914c77177a7d5e755f1de827 Author: Michał Krzywkowski Date: Sun Nov 4 16:59:05 2018 +0100 * eglot.el (xref-backend-identifier-completion-table): Use vector. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e17717452b..576d7f39d3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1374,7 +1374,7 @@ DUMMY is ignored." :position (plist-get (plist-get location :range) :start)) - :locations (list location) + :locations (vector location) :kind kind :containerName containerName)) (jsonrpc-request server commit ee243c0c80eb2c17be1e2606abf156a0d09a74a1 Author: João Távora Date: Wed Nov 7 13:10:29 2018 +0000 Move constants to top instead of forward-declaring * eglot.el (eglot--symbol-kind-names, eglot--kind-names): Move to top of file. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2ce9d086e7..e17717452b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -174,6 +174,28 @@ let the buffer grow forever." :type '(choice (const :tag "No limit" nil) (integer :tag "Number of characters"))) + +;;; Constants +;;; +(defconst eglot--symbol-kind-names + `((1 . "File") (2 . "Module") + (3 . "Namespace") (4 . "Package") (5 . "Class") + (6 . "Method") (7 . "Property") (8 . "Field") + (9 . "Constructor") (10 . "Enum") (11 . "Interface") + (12 . "Function") (13 . "Variable") (14 . "Constant") + (15 . "String") (16 . "Number") (17 . "Boolean") + (18 . "Array") (19 . "Object") (20 . "Key") + (21 . "Null") (22 . "EnumMember") (23 . "Struct") + (24 . "Event") (25 . "Operator") (26 . "TypeParameter"))) + +(defconst eglot--kind-names + `((1 . "Text") (2 . "Method") (3 . "Function") (4 . "Constructor") + (5 . "Field") (6 . "Variable") (7 . "Class") (8 . "Interface") + (9 . "Module") (10 . "Property") (11 . "Unit") (12 . "Value") + (13 . "Enum") (14 . "Keyword") (15 . "Snippet") (16 . "Color") + (17 . "File") (18 . "Reference"))) + + ;;; API (WORK-IN-PROGRESS!) ;;; (cl-defmacro eglot--with-live-buffer (buf &rest body) @@ -198,8 +220,6 @@ let the buffer grow forever." "JSON object to send under `initializationOptions'" (:method (_s) nil)) ; blank default -(defvar eglot--symbol-kind-names) - (cl-defgeneric eglot-client-capabilities (server) "What the EGLOT LSP client supports for SERVER." (:method (_s) @@ -738,24 +758,6 @@ Doubles as an indicator of snippet support." (symbol-value 'yas-minor-mode) 'yas-expand-snippet)) -(defconst eglot--kind-names - `((1 . "Text") (2 . "Method") (3 . "Function") (4 . "Constructor") - (5 . "Field") (6 . "Variable") (7 . "Class") (8 . "Interface") - (9 . "Module") (10 . "Property") (11 . "Unit") (12 . "Value") - (13 . "Enum") (14 . "Keyword") (15 . "Snippet") (16 . "Color") - (17 . "File") (18 . "Reference"))) - -(defconst eglot--symbol-kind-names - `((1 . "File") (2 . "Module") - (3 . "Namespace") (4 . "Package") (5 . "Class") - (6 . "Method") (7 . "Property") (8 . "Field") - (9 . "Constructor") (10 . "Enum") (11 . "Interface") - (12 . "Function") (13 . "Variable") (14 . "Constant") - (15 . "String") (16 . "Number") (17 . "Boolean") - (18 . "Array") (19 . "Object") (20 . "Key") - (21 . "Null") (22 . "EnumMember") (23 . "Struct") - (24 . "Event") (25 . "Operator") (26 . "TypeParameter"))) - (defun eglot--format-markup (markup) "Format MARKUP according to LSP's spec." (pcase-let ((`(,string ,mode) commit 753dddc631b62ec321fab8e1690af05547b02a05 Author: Mario Rodas Date: Wed Nov 7 07:09:58 2018 -0500 Support ocaml-language-server out of the box () Copyright-paperwork-exempt: yes * eglot.el (eglot-server-programs): Add ocaml-language-server. * README.md (Installation and usage): Mention ocaml-language-server GitHub-reference: https://github.com/joaotavora/eglot/issues/149 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0a789702c7..2ce9d086e7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -85,6 +85,8 @@ rjsx-mode) . ("javascript-typescript-stdio")) (sh-mode . ("bash-language-server" "start")) ((c++-mode c-mode) . ("ccls")) + ((caml-mode tuareg-mode reason-mode) + . ("ocaml-language-server" "--stdio")) (ruby-mode . ("solargraph" "socket" "--port" :autoport)) commit 6ea0216c53533b76120574b7892a021b037389cf Author: Michal Krzywkowski Date: Mon Nov 5 19:42:32 2018 +0100 * eglot.el (eglot-client-capabilities): mention supported symbolkinds. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 22f509b81b..0a789702c7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -196,6 +196,8 @@ let the buffer grow forever." "JSON object to send under `initializationOptions'" (:method (_s) nil)) ; blank default +(defvar eglot--symbol-kind-names) + (cl-defgeneric eglot-client-capabilities (server) "What the EGLOT LSP client supports for SERVER." (:method (_s) @@ -221,7 +223,11 @@ let the buffer grow forever." :signatureHelp `(:dynamicRegistration :json-false) :references `(:dynamicRegistration :json-false) :definition `(:dynamicRegistration :json-false) - :documentSymbol `(:dynamicRegistration :json-false) + :documentSymbol (list + :dynamicRegistration :json-false + :symbolKind `(:valueSet + [,@(mapcar + #'car eglot--symbol-kind-names)])) :documentHighlight `(:dynamicRegistration :json-false) :codeAction (list :dynamicRegistration :json-false commit 40e4c88dd3461136e7a447e96a0d8af9732f10f8 Author: Michal Krzywkowski Date: Sun Nov 4 16:34:58 2018 +0100 Make imenu hierarchical * eglot.el (eglot-imenu): Use :containerName to build a nested imenu index alist. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f11a14e11c..22f509b81b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1670,20 +1670,29 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (jsonrpc-lambda (&key name kind location containerName _deprecated) (cons (propertize - (concat - (and (stringp containerName) - (not (string-empty-p containerName)) - (concat containerName "::")) - name) + name :kind (alist-get kind eglot--symbol-kind-names - "(Unknown)")) + "Unknown") + :containerName (and (stringp containerName) + (not (string-empty-p containerName)) + containerName)) (eglot--lsp-position-to-point (plist-get (plist-get location :range) :start)))) (jsonrpc-request (eglot--current-server-or-lose) :textDocument/documentSymbol `(:textDocument ,(eglot--TextDocumentIdentifier)))))) - (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) - entries)) + (mapcar + (pcase-lambda (`(,kind . ,syms)) + (let ((syms-by-scope (seq-group-by + (lambda (e) + (get-text-property 0 :containerName (car e))) + syms))) + (cons kind (cl-loop for (scope . elems) in syms-by-scope + append (if scope + (list (cons scope elems)) + elems))))) + (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) + entries))) (funcall oldfun))) (defun eglot--apply-text-edits (edits &optional version) commit ee58d92a7de1c8b1914c77177a7d5e755f1de827 Author: Michal Krzywkowski Date: Sun Nov 4 16:59:05 2018 +0100 Fix a bug when response to definitions request is a single location It's valid to return just a single Location for a definitions request. * eglot.el (xref-backend-definitions): Coerce response to a vector. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2b683f8874..f11a14e11c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1381,17 +1381,20 @@ DUMMY is ignored." (cl-defmethod xref-backend-definitions ((_backend (eql eglot)) identifier) (let* ((rich-identifier (car (member identifier eglot--xref-known-symbols))) - (location-or-locations + (definitions (if rich-identifier (get-text-property 0 :locations rich-identifier) (jsonrpc-request (eglot--current-server-or-lose) :textDocument/definition (get-text-property - 0 :textDocumentPositionParams identifier))))) + 0 :textDocumentPositionParams identifier)))) + (locations + (and definitions + (if (vectorp definitions) definitions (vector definitions))))) (eglot--sort-xrefs (mapcar (jsonrpc-lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) - location-or-locations)))) + locations)))) (cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) (unless (eglot--server-capable :referencesProvider) commit 9e720cbea4949d65d280881d0e5f45b5a727526d Author: João Távora Date: Sun Nov 4 12:56:37 2018 +0000 Simplify eglot-code-action. fix compilation warning * eglot.el (eglot-code-actions): Simplify. (eglot-client-capabilities): Mention supported codeActionKind's directly. (eglot--code-action-kinds): Remove. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b2e88d8439..2b683f8874 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -226,9 +226,12 @@ let the buffer grow forever." :codeAction (list :dynamicRegistration :json-false :codeActionLiteralSupport - `(:codeActionKind + '(:codeActionKind (:valueSet - [,@eglot--code-action-kinds]))) + ["quickfix" + "refactor" "refactor.extract" + "refactor.inline" "refactor.rewrite" + "source" "source.organizeImports"]))) :formatting `(:dynamicRegistration :json-false) :rangeFormatting `(:dynamicRegistration :json-false) :rename `(:dynamicRegistration :json-false) @@ -745,11 +748,6 @@ Doubles as an indicator of snippet support." (21 . "Null") (22 . "EnumMember") (23 . "Struct") (24 . "Event") (25 . "Operator") (26 . "TypeParameter"))) -(defconst eglot--code-action-kinds - '("quickfix" "refactor" "refactor.extract" - "refactor.inline" "refactor.rewrite" - "source" "source.organizeImports")) - (defun eglot--format-markup (markup) "Format MARKUP according to LSP's spec." (pcase-let ((`(,string ,mode) @@ -1786,41 +1784,40 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (unless (eglot--server-capable :codeActionProvider) (eglot--error "Server can't execute code actions!")) (let* ((server (eglot--current-server-or-lose)) - (actions (jsonrpc-request - server - :textDocument/codeAction - (list :textDocument (eglot--TextDocumentIdentifier) - :range (list :start (eglot--pos-to-lsp-position beg) - :end (eglot--pos-to-lsp-position end)) - :context - `(:diagnostics - [,@(mapcar (lambda (diag) - (cdr (assoc 'eglot-lsp-diag - (eglot--diag-data diag)))) - (flymake-diagnostics beg end))])))) - (menu-items (mapcar (jsonrpc-lambda (&key title command arguments - edit _kind _diagnostics) - `(,title . (:command ,command :arguments ,arguments - :edit ,edit))) - actions)) - (menu (and menu-items `("Eglot code actions:" ("dummy" ,@menu-items)))) - (command-and-args - (and menu - (if (listp last-nonmenu-event) - (x-popup-menu last-nonmenu-event menu) - (let ((never-mind (gensym)) retval) - (setcdr (cadr menu) - (cons `("never mind..." . ,never-mind) (cdadr menu))) - (if (eq (setq retval (tmm-prompt menu)) never-mind) - (keyboard-quit) - retval)))))) - (cl-destructuring-bind (&key _title command arguments edit) command-and-args + (actions + (jsonrpc-request + server + :textDocument/codeAction + (list :textDocument (eglot--TextDocumentIdentifier) + :range (list :start (eglot--pos-to-lsp-position beg) + :end (eglot--pos-to-lsp-position end)) + :context + `(:diagnostics + [,@(mapcar (lambda (diag) + (cdr (assoc 'eglot-lsp-diag + (eglot--diag-data diag)))) + (flymake-diagnostics beg end))])))) + (menu-items + (or (mapcar (jsonrpc-lambda (&key title command arguments + edit _kind _diagnostics) + `(,title . (:command ,command :arguments ,arguments + :edit ,edit))) + actions) + (eglot--error "No code actions here"))) + (menu `("Eglot code actions:" ("dummy" ,@menu-items))) + (action (if (listp last-nonmenu-event) + (x-popup-menu last-nonmenu-event menu) + (let ((never-mind (gensym)) retval) + (setcdr (cadr menu) + (cons `("never mind..." . ,never-mind) (cdadr menu))) + (if (eq (setq retval (tmm-prompt menu)) never-mind) + (keyboard-quit) + retval))))) + (cl-destructuring-bind (&key _title command arguments edit) action (when edit (eglot--apply-workspace-edit edit)) - (if command - (eglot-execute-command server (intern command) arguments) - (unless edit - (eglot--message "No code actions here")))))) + (when command + (eglot-execute-command server (intern command) arguments))))) commit 025c926301f5f8f1c5a39fbf22dec1053c90c972 Author: Michal Krzywkowski Date: Wed Oct 31 22:14:16 2018 +0100 Add support for code action literals Code action literals allow the server to simply return a WorkspaceEdit for a code action, so the client does not have to execute a command. * eglot.el (eglot-client-capabilities): Add :codeActionLiteralSupport. (eglot--code-action-kinds): New variable. (eglot-code-actions): Apply provided WorkspaceEdit. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e15170400d..b2e88d8439 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -223,7 +223,12 @@ let the buffer grow forever." :definition `(:dynamicRegistration :json-false) :documentSymbol `(:dynamicRegistration :json-false) :documentHighlight `(:dynamicRegistration :json-false) - :codeAction `(:dynamicRegistration :json-false) + :codeAction (list + :dynamicRegistration :json-false + :codeActionLiteralSupport + `(:codeActionKind + (:valueSet + [,@eglot--code-action-kinds]))) :formatting `(:dynamicRegistration :json-false) :rangeFormatting `(:dynamicRegistration :json-false) :rename `(:dynamicRegistration :json-false) @@ -740,6 +745,11 @@ Doubles as an indicator of snippet support." (21 . "Null") (22 . "EnumMember") (23 . "Struct") (24 . "Event") (25 . "Operator") (26 . "TypeParameter"))) +(defconst eglot--code-action-kinds + '("quickfix" "refactor" "refactor.extract" + "refactor.inline" "refactor.rewrite" + "source" "source.organizeImports")) + (defun eglot--format-markup (markup) "Format MARKUP according to LSP's spec." (pcase-let ((`(,string ,mode) @@ -1788,8 +1798,10 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (cdr (assoc 'eglot-lsp-diag (eglot--diag-data diag)))) (flymake-diagnostics beg end))])))) - (menu-items (mapcar (jsonrpc-lambda (&key title command arguments) - `(,title . (:command ,command :arguments ,arguments))) + (menu-items (mapcar (jsonrpc-lambda (&key title command arguments + edit _kind _diagnostics) + `(,title . (:command ,command :arguments ,arguments + :edit ,edit))) actions)) (menu (and menu-items `("Eglot code actions:" ("dummy" ,@menu-items)))) (command-and-args @@ -1802,10 +1814,13 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (if (eq (setq retval (tmm-prompt menu)) never-mind) (keyboard-quit) retval)))))) - (cl-destructuring-bind (&key _title command arguments) command-and-args + (cl-destructuring-bind (&key _title command arguments edit) command-and-args + (when edit + (eglot--apply-workspace-edit edit)) (if command (eglot-execute-command server (intern command) arguments) - (eglot--message "No code actions here"))))) + (unless edit + (eglot--message "No code actions here")))))) commit 7f062198c8677028c6ade0b68e07e5f26a3b4867 Author: Michal Krzywkowski Date: Wed Oct 31 14:16:56 2018 +0100 Use the container name of a symbol in imenu * eglot.el (eglot-imenu): Prepend :containerName to each symbol, when provided. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 26ee814755..e15170400d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1657,10 +1657,15 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (let ((entries (mapcar (jsonrpc-lambda - (&key name kind location _containerName _deprecated) + (&key name kind location containerName _deprecated) (cons (propertize - name :kind (alist-get kind eglot--symbol-kind-names - "(Unknown)")) + (concat + (and (stringp containerName) + (not (string-empty-p containerName)) + (concat containerName "::")) + name) + :kind (alist-get kind eglot--symbol-kind-names + "(Unknown)")) (eglot--lsp-position-to-point (plist-get (plist-get location :range) :start)))) (jsonrpc-request (eglot--current-server-or-lose) commit 3d91b57629d3f4ca307a561eb05a587e831d8554 Author: Michal Krzywkowski Date: Wed Oct 31 13:06:59 2018 +0100 Don't ignore unknown symbolkinds in imenu Some servers provide custom SymbolKinds. For example, ccls says that symbols defined with #define are of kind 255. * eglot.el (eglot-imenu): Don't delete elements with unknown symbol kind from the return list, instead put them in `(Unknown)` group. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2b39849f9b..26ee814755 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1658,16 +1658,16 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (mapcar (jsonrpc-lambda (&key name kind location _containerName _deprecated) - (cons (propertize name :kind (cdr (assoc kind eglot--symbol-kind-names))) + (cons (propertize + name :kind (alist-get kind eglot--symbol-kind-names + "(Unknown)")) (eglot--lsp-position-to-point (plist-get (plist-get location :range) :start)))) (jsonrpc-request (eglot--current-server-or-lose) :textDocument/documentSymbol `(:textDocument ,(eglot--TextDocumentIdentifier)))))) - (cl-remove nil - (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) - entries) - :key #'car)) + (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) + entries)) (funcall oldfun))) (defun eglot--apply-text-edits (edits &optional version) commit 5f250e875a7ff176e82290c828496127dfa355b0 Author: Michal Krzywkowski Date: Wed Oct 31 12:54:26 2018 +0100 Remove duplicates from imenu * eglot.el (eglot-imenu): Don't append the result list to itself, which causes duplicates. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a234e17c0b..2b39849f9b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1664,12 +1664,10 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (jsonrpc-request (eglot--current-server-or-lose) :textDocument/documentSymbol `(:textDocument ,(eglot--TextDocumentIdentifier)))))) - (append - (cl-remove nil - (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) - entries) - :key #'car) - entries)) + (cl-remove nil + (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) + entries) + :key #'car)) (funcall oldfun))) (defun eglot--apply-text-edits (edits &optional version) commit d84d55e6e4c239faafc4f4de5e0295a8b99e15af Author: Alex Branham Date: Tue Oct 30 16:59:05 2018 -0500 Require subr-x at compile time () if-let and when-let are macros that the byte compiler can expand at compile time. No need to require subr-x at run time. * eglot.el (subr-x): Require only when compiling. GitHub-reference: https://github.com/joaotavora/eglot/issues/139 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 297d6f4f0e..a234e17c0b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -65,7 +65,8 @@ (require 'warnings) (require 'flymake) (require 'xref) -(require 'subr-x) +(eval-when-compile + (require 'subr-x)) (require 'jsonrpc) (require 'filenotify) (require 'ert) commit 1f865ee560c75cb06807b0598043303d36c62d46 Author: João Távora Date: Tue Oct 30 12:22:32 2018 +0000 Accept deprecated field in symbolinformation * eglot.el (xref-backend-identifier-completion-table) (eglot-imenu): Accept and ignore "deprecated" GitHub-reference: fix https://github.com/joaotavora/eglot/issues/138 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3d77a4faf4..297d6f4f0e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1348,7 +1348,7 @@ DUMMY is ignored." (setq eglot--xref-known-symbols (mapcar (jsonrpc-lambda - (&key name kind location containerName) + (&key name kind location containerName _deprecated) (propertize name :textDocumentPositionParams (list :textDocument text-id @@ -1656,7 +1656,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (let ((entries (mapcar (jsonrpc-lambda - (&key name kind location _containerName) + (&key name kind location _containerName _deprecated) (cons (propertize name :kind (cdr (assoc kind eglot--symbol-kind-names))) (eglot--lsp-position-to-point (plist-get (plist-get location :range) :start)))) commit eae904fc9cc1d51eee4b7f08cc4e83d130e60a7c Author: Dale Sedivec Date: Sun Oct 21 15:02:47 2018 -0500 Fix misspelling of "outstanding" () Copyright-paperwork-exempt: yes * eglot.el (eglot--mode-line-format): Fix a typo. GitHub-reference: https://github.com/joaotavora/eglot/issues/74 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 75d38573e9..3d77a4faf4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1000,7 +1000,7 @@ Uses THING, FACE, DEFS and PREPEND." 'compilation-mode-line-run '()))) ,@(when (cl-plusp pending) `("/" ,(eglot--mode-line-props - (format "%d oustanding requests" pending) 'warning + (format "%d outstanding requests" pending) 'warning '((mouse-3 eglot-forget-pending-continuations "fahgettaboudit")))))))))) commit f3c43b4cac387384359322d1e21e5410a16b3eae Author: Michal Krzywkowski Date: Tue Sep 11 23:13:56 2018 +0200 Handle case when project was not found in eclipse.jdt.ls contact diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a23e73df83..75d38573e9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1959,8 +1959,9 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." ((string= system-type "darwin") "config_mac") ((string= system-type "windows-nt") "config_win") (t "config_linux")))) + (project (or (project-current) `(transient . ,default-directory))) (workspace - (expand-file-name (md5 (car (project-roots (project-current)))) + (expand-file-name (md5 (car (project-roots project))) (concat user-emacs-directory "eglot-eclipse-jdt-cache")))) (unless jar commit c726fc7a9ccc857aefc97282b30eef94266c2f93 Author: Michal Krzywkowski Date: Thu Aug 9 15:25:58 2018 +0200 Override eglot-execute-command for eclipse.jdt.ls server * eglot.el (eglot-execute-command eglot-eclipse-jdt): New defmethod. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index daae454229..a23e73df83 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1984,6 +1984,11 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." "-configuration" config "-data" workspace))))) +(cl-defmethod eglot-execute-command + ((_server eglot-eclipse-jdt) (_cmd (eql java.apply.workspaceEdit)) arguments) + "Eclipse JDT breaks spec and replies with edits as arguments." + (mapc #'eglot--apply-workspace-edit arguments)) + ;; FIXME: A horrible hack of Flymake's insufficient API that must go ;; into Emacs master, or better, 26.2 commit 5423eed9e7160b363e55e24d910b51e98c1f2bd0 Author: Michal Krzywkowski Date: Thu Aug 9 12:21:11 2018 +0200 Add support for eclipse.jdt.ls server * eglot.el (eglot-server-programs): Add java-mode entry. (eglot-eclipse-jdt): New class. (eglot-initialization-options): Override for eglot-eclipse-jdt. (eglot--eclipse-jdt-contact): New function. GitHub-reference: per https://github.com/joaotavora/eglot/issues/63 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4376d3af7b..daae454229 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -91,7 +91,9 @@ language-server/bin/php-language-server.php")) (haskell-mode . ("hie-wrapper")) (kotlin-mode . ("kotlin-language-server")) - (go-mode . ("go-langserver" "-mode=stdio" "-gocodecompletion"))) + (go-mode . ("go-langserver" "-mode=stdio" + "-gocodecompletion")) + (java-mode . eglot--eclipse-jdt-contact)) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated @@ -1892,6 +1894,96 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (list :cacheDirectory (file-name-as-directory cache) :progressReportFrequencyMs -1))) + +;;; eclipse-jdt-specific +;;; +(defclass eglot-eclipse-jdt (eglot-lsp-server) () + :documentation "Eclipse's Java Development Tools Language Server.") + +(cl-defmethod eglot-initialization-options ((server eglot-eclipse-jdt)) + "Passes through required jdt initialization options" + `(:workspaceFolders + [,@(cl-delete-duplicates + (mapcar #'eglot--path-to-uri + (let* ((roots (project-roots (eglot--project server))) + (root (car roots))) + (append + roots + (mapcar + #'file-name-directory + (append + (file-expand-wildcards (concat root "*/pom.xml")) + (file-expand-wildcards (concat root "*/build.gradle")) + (file-expand-wildcards (concat root "*/.project"))))))) + :test #'string=)] + ,@(if-let ((home (or (getenv "JAVA_HOME") + (ignore-errors + (expand-file-name + ".." + (file-name-directory + (file-chase-links (executable-find "javac")))))))) + `(:settings (:java (:home ,home))) + (ignore (eglot--warn "JAVA_HOME env var not set"))))) + +(defun eglot--eclipse-jdt-contact (interactive) + "Return a contact for connecting to eclipse.jdt.ls server, as a cons cell." + (cl-labels + ((is-the-jar + (path) + (and (string-match-p + "org\\.eclipse\\.equinox\\.launcher_.*\\.jar$" + (file-name-nondirectory path)) + (file-exists-p path)))) + (let* ((classpath (or (getenv "CLASSPATH") ":")) + (cp-jar (cl-find-if #'is-the-jar (split-string classpath ":"))) + (jar cp-jar) + (dir + (cond + (jar (file-name-as-directory + (expand-file-name ".." (file-name-directory jar)))) + (interactive + (expand-file-name + (read-directory-name + (concat "Path to eclipse.jdt.ls directory (could not" + " find it in CLASSPATH): ") + nil nil t))) + (t (error "Could not find eclipse.jdt.ls jar in CLASSPATH")))) + (repodir + (concat dir + "org.eclipse.jdt.ls.product/target/repository/")) + (repodir (if (file-directory-p repodir) repodir dir)) + (config + (concat + repodir + (cond + ((string= system-type "darwin") "config_mac") + ((string= system-type "windows-nt") "config_win") + (t "config_linux")))) + (workspace + (expand-file-name (md5 (car (project-roots (project-current)))) + (concat user-emacs-directory + "eglot-eclipse-jdt-cache")))) + (unless jar + (setq jar + (cl-find-if #'is-the-jar + (directory-files (concat repodir "plugins") t)))) + (unless (and jar (file-exists-p jar) (file-directory-p config)) + (error "Could not find required eclipse.jdt.ls files (build required?)")) + (when (and interactive (not cp-jar) + (y-or-n-p (concat "Add path to the server program " + "to CLASSPATH environment variable?"))) + (setenv "CLASSPATH" (concat (getenv "CLASSPATH") ":" jar))) + (unless (file-directory-p workspace) + (make-directory workspace t)) + (cons 'eglot-eclipse-jdt + (list (executable-find "java") + "-Declipse.application=org.eclipse.jdt.ls.core.id1" + "-Dosgi.bundles.defaultStartLevel=4" + "-Declipse.product=org.eclipse.jdt.ls.core.product" + "-jar" jar + "-configuration" config + "-data" workspace))))) + ;; FIXME: A horrible hack of Flymake's insufficient API that must go ;; into Emacs master, or better, 26.2 commit 563011ec640f91799c22b9d9bb5b6eb6087dd5bd Author: Michal Krzywkowski Date: Thu Aug 9 12:20:36 2018 +0200 Allow function contacts to be interactive * eglot.el (eglot-server-programs): Mention that the function must accept one argument. (eglot--guess-contact): Pass to functional contacts the interactive value. GitHub-reference: per https://github.com/joaotavora/eglot/issues/63 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 57d19b33c1..4376d3af7b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -124,8 +124,14 @@ of those modes. CONTACT can be: `jsonrpc-process-connection', which you should see for the semantics of the mandatory :PROCESS argument. -* A function of no arguments producing any of the above values - for CONTACT.") +* A function of a single argument producing any of the above + values for CONTACT. The argument's value is non-nil if the + connection was requested interactively (e.g. from the `eglot' + command), and nil if it wasn't (e.g. from `eglot-ensure'). If + the call is interactive, the function can ask the user for + hints on finding the required programs, etc. Otherwise, it + should not ask the user for any input, and return nil or signal + an error if it can't produce a valid CONTACT.") (defface eglot-mode-line '((t (:inherit font-lock-constant-face :weight bold))) @@ -353,7 +359,9 @@ be guessed." (lambda (m1 m2) (or (eq m1 m2) (and (listp m1) (memq m2 m1))))))) - (guess (if (functionp guess) (funcall guess) guess)) + (guess (if (functionp guess) + (funcall guess interactive) + guess)) (class (or (and (consp guess) (symbolp (car guess)) (prog1 (car guess) (setq guess (cdr guess)))) 'eglot-lsp-server)) commit ebacb5f8614ff2db2371da98ede733502847e3e8 Author: Michal Krzywkowski Date: Sat Oct 6 17:08:04 2018 +0200 Improve signature help * eglot.el (eglot--sig-info): Don't lose existing information. Attempt to highlight the active parameter by searching for it's :label in signature's :label. Append to the result first sentence of signature's :documentation, if present. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7e79b165c6..57d19b33c1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1537,23 +1537,41 @@ is not active." (defun eglot--sig-info (sigs active-sig active-param) (cl-loop for (sig . moresigs) on (append sigs nil) for i from 0 - concat (cl-destructuring-bind (&key label _documentation parameters) sig - (let (active-doc) - (concat - (propertize (replace-regexp-in-string "(.*$" "(" label) - 'face 'font-lock-function-name-face) - (cl-loop - for (param . moreparams) on (append parameters nil) for j from 0 - concat (cl-destructuring-bind (&key label documentation) param - (when (and (eql j active-param) (eql i active-sig)) - (setq label (propertize - label - 'face 'eldoc-highlight-function-argument)) - (when documentation - (setq active-doc (concat label ": " documentation)))) - label) - if moreparams concat ", " else concat ")") - (when active-doc (concat "\n" active-doc))))) + concat (cl-destructuring-bind (&key label documentation parameters) sig + (with-temp-buffer + (save-excursion (insert label)) + (when (looking-at "\\([^(]+\\)(") + (add-face-text-property (match-beginning 1) (match-end 1) + 'font-lock-function-name-face)) + + (when (and (stringp documentation) (eql i active-sig) + (string-match "[[:space:]]*\\([^.\r\n]+[.]?\\)" + documentation)) + (setq documentation (match-string 1 documentation)) + (unless (string-prefix-p (string-trim documentation) label) + (goto-char (point-max)) + (insert ": " documentation))) + (when (and (eql i active-sig) active-param + (< -1 active-param (length parameters))) + (cl-destructuring-bind (&key label documentation) + (aref parameters active-param) + (goto-char (point-min)) + (let ((case-fold-search nil)) + (cl-loop for nmatches from 0 + while (and (not (string-empty-p label)) + (search-forward label nil t)) + finally do + (when (= 1 nmatches) + (add-face-text-property + (- (point) (length label)) (point) + 'eldoc-highlight-function-argument)))) + (when documentation + (goto-char (point-max)) + (insert "\n" + (propertize + label 'face 'eldoc-highlight-function-argument) + ": " documentation)))) + (buffer-string))) when moresigs concat "\n")) (defun eglot-help-at-point () commit 3a24bc0f4f65b3729b239776b2bdae00edd5884a Author: Michal Krzywkowski Date: Sat Sep 8 21:36:00 2018 +0200 Sort references and definitions by line number * eglot.el (eglot--sort-xrefs): New function. (xref-backend-definitions): (xref-backend-references): (xref-backend-apropos): Use it. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 41863cc213..7e79b165c6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1323,6 +1323,12 @@ DUMMY is ignored." ;; F!@(#*&#$)CKING OFF-BY-ONE again (1+ line) character)))) +(defun eglot--sort-xrefs (xrefs) + (sort xrefs + (lambda (a b) + (< (xref-location-line (xref-item-location a)) + (xref-location-line (xref-item-location b)))))) + (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) (when (eglot--server-capable :documentSymbolProvider) (let ((server (eglot--current-server-or-lose)) @@ -1363,9 +1369,10 @@ DUMMY is ignored." :textDocument/definition (get-text-property 0 :textDocumentPositionParams identifier))))) - (mapcar (jsonrpc-lambda (&key uri range) - (eglot--xref-make identifier uri (plist-get range :start))) - location-or-locations))) + (eglot--sort-xrefs + (mapcar (jsonrpc-lambda (&key uri range) + (eglot--xref-make identifier uri (plist-get range :start))) + location-or-locations)))) (cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) (unless (eglot--server-capable :referencesProvider) @@ -1376,25 +1383,27 @@ DUMMY is ignored." (and rich (get-text-property 0 :textDocumentPositionParams rich)))))) (unless params (eglot--error "Don' know where %s is in the workspace!" identifier)) - (mapcar - (jsonrpc-lambda (&key uri range) - (eglot--xref-make identifier uri (plist-get range :start))) - (jsonrpc-request (eglot--current-server-or-lose) - :textDocument/references - (append - params - (list :context - (list :includeDeclaration t))))))) + (eglot--sort-xrefs + (mapcar + (jsonrpc-lambda (&key uri range) + (eglot--xref-make identifier uri (plist-get range :start))) + (jsonrpc-request (eglot--current-server-or-lose) + :textDocument/references + (append + params + (list :context + (list :includeDeclaration t)))))))) (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) - (mapcar - (jsonrpc-lambda (&key name location &allow-other-keys) - (cl-destructuring-bind (&key uri range) location - (eglot--xref-make name uri (plist-get range :start)))) - (jsonrpc-request (eglot--current-server-or-lose) - :workspace/symbol - `(:query ,pattern))))) + (eglot--sort-xrefs + (mapcar + (jsonrpc-lambda (&key name location &allow-other-keys) + (cl-destructuring-bind (&key uri range) location + (eglot--xref-make name uri (plist-get range :start)))) + (jsonrpc-request (eglot--current-server-or-lose) + :workspace/symbol + `(:query ,pattern)))))) (defun eglot-format-buffer () "Format contents of current buffer." commit 119cb95f856b205aad7c753e02d2a07a6289c8c4 Merge: aa8653cc3f 3d9a6f1e79 Author: Michal Krzywkowski Date: Mon Oct 15 23:12:28 2018 +0200 Merge pull request from mkcms/fix-diagnostics-wrong-type-argument Handle case when diagnostic :character is out of range GitHub-reference: https://github.com/joaotavora/eglot/issues/104 commit 3d9a6f1e79e76bd69c3f3ada4d964233d16730fb Author: Michal Krzywkowski Date: Sun Sep 9 00:24:49 2018 +0200 Handle case when diagnostic :character is out of range * eglot.el (eglot-handle-notification): Don't error out when flymake-diag-region returns nil. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8014024ecd..b25d8b90df 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1080,14 +1080,21 @@ COMMAND is a symbol naming the command." ((`(,beg . ,end) (eglot--range-region range))) ;; Fallback to `flymake-diag-region' if server ;; botched the range - (if (= beg end) - (let* ((st (plist-get range :start)) - (diag-region - (flymake-diag-region - (current-buffer) (1+ (plist-get st :line)) - (plist-get st :character)))) - (setq beg (car diag-region) - end (cdr diag-region)))) + (when (= beg end) + (if-let* ((st (plist-get range :start)) + (diag-region + (flymake-diag-region + (current-buffer) (1+ (plist-get st :line)) + (plist-get st :character)))) + (setq beg (car diag-region) end (cdr diag-region)) + (eglot--widening + (goto-char (point-min)) + (setq beg + (point-at-bol + (1+ (plist-get (plist-get range :start) :line)))) + (setq end + (point-at-eol + (1+ (plist-get (plist-get range :end) :line))))))) (eglot--make-diag (current-buffer) beg end (cond ((<= sev 1) 'eglot-error) ((= sev 2) 'eglot-warning) commit aa8653cc3f0ecc0c0652fedc7e9d7835aabee8ed Author: Michal Krzywkowski Date: Mon Oct 15 22:58:14 2018 +0200 Eglot-ignored-server-capabilites: prefer all choices over "other" Previously the "Other" choice matched every value, so it was always shown in the customize buffer. * eglot.el (eglot-ignored-server-capabilites): Make the "Other" choice the last possible option. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8014024ecd..b660683016 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -747,7 +747,6 @@ You could add, for instance, the symbol under cursor." :type '(repeat (choice - (symbol :tag "Other") (const :tag "Documentation on hover" :hoverProvider) (const :tag "Code completion" :completionProvider) (const :tag "Function signature help" :signatureHelpProvider) @@ -767,7 +766,8 @@ under cursor." (const :tag "Highlight links in document" :documentLinkProvider) (const :tag "Decorate color references" :colorProvider) (const :tag "Fold regions of buffer" :foldingRangeProvider) - (const :tag "Execute custom commands" :executeCommandProvider)))) + (const :tag "Execute custom commands" :executeCommandProvider) + (symbol :tag "Other")))) (defun eglot--server-capable (&rest feats) "Determine if current server is capable of FEATS." commit d294a3e010a955edb9e26537b144f8aabe4b5f99 Author: Michal Krzywkowski Date: Wed Oct 3 21:08:38 2018 +0200 Make eglot-ignored-server-capabilites more user-friendly () * eglot.el (eglot-ignored-server-capabilites): Add list of possible choices to :type, along with a user-friendly description. GitHub-reference: https://github.com/joaotavora/eglot/issues/126 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d2ec9291b0..8014024ecd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -745,7 +745,29 @@ Doubles as an indicator of snippet support." You could add, for instance, the symbol `:documentHighlightProvider' to prevent automatic highlighting under cursor." - :type '(repeat symbol)) + :type '(repeat + (choice + (symbol :tag "Other") + (const :tag "Documentation on hover" :hoverProvider) + (const :tag "Code completion" :completionProvider) + (const :tag "Function signature help" :signatureHelpProvider) + (const :tag "Go to definition" :definitionProvider) + (const :tag "Go to type definition" :typeDefinitionProvider) + (const :tag "Go to implementation" :implementationProvider) + (const :tag "Find references" :referencesProvider) + (const :tag "Highlight symbols automatically" :documentHighlightProvider) + (const :tag "List symbols in buffer" :documentSymbolProvider) + (const :tag "List symbols in workspace" :workspaceSymbolProvider) + (const :tag "Execute code actions" :codeActionProvider) + (const :tag "Code lens" :codeLensProvider) + (const :tag "Format buffer" :documentFormattingProvider) + (const :tag "Format portion of buffer" :documentRangeFormattingProvider) + (const :tag "On-type formatting" :documentOnTypeFormattingProvider) + (const :tag "Rename symbol" :renameProvider) + (const :tag "Highlight links in document" :documentLinkProvider) + (const :tag "Decorate color references" :colorProvider) + (const :tag "Fold regions of buffer" :foldingRangeProvider) + (const :tag "Execute custom commands" :executeCommandProvider)))) (defun eglot--server-capable (&rest feats) "Determine if current server is capable of FEATS." commit 72eae8b7de1d7c7257748f48ec960481f6223eef Author: Michal Krzywkowski Date: Mon Sep 24 14:24:19 2018 +0200 Correctly map documentsymbol's :kind to its name () Previously we were mapping :kind in DocumentSymbol with names from the CompletionItemKind enum, whereas we should have used the SymbolKind enum. * eglot.el (eglot--symbol-kind-names): New variable. (eglot-imenu): Use it instead of eglot--kind-names. GitHub-reference: https://github.com/joaotavora/eglot/issues/121 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8e4be897b7..d2ec9291b0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -718,6 +718,17 @@ Doubles as an indicator of snippet support." (13 . "Enum") (14 . "Keyword") (15 . "Snippet") (16 . "Color") (17 . "File") (18 . "Reference"))) +(defconst eglot--symbol-kind-names + `((1 . "File") (2 . "Module") + (3 . "Namespace") (4 . "Package") (5 . "Class") + (6 . "Method") (7 . "Property") (8 . "Field") + (9 . "Constructor") (10 . "Enum") (11 . "Interface") + (12 . "Function") (13 . "Variable") (14 . "Constant") + (15 . "String") (16 . "Number") (17 . "Boolean") + (18 . "Array") (19 . "Object") (20 . "Key") + (21 . "Null") (22 . "EnumMember") (23 . "Struct") + (24 . "Event") (25 . "Operator") (26 . "TypeParameter"))) + (defun eglot--format-markup (markup) "Format MARKUP according to LSP's spec." (pcase-let ((`(,string ,mode) @@ -1580,7 +1591,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (mapcar (jsonrpc-lambda (&key name kind location _containerName) - (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) + (cons (propertize name :kind (cdr (assoc kind eglot--symbol-kind-names))) (eglot--lsp-position-to-point (plist-get (plist-get location :range) :start)))) (jsonrpc-request (eglot--current-server-or-lose) commit 169360c2570c31bd3dcf8d32b754cd15ca51d3e4 Author: whatacold Date: Mon Sep 24 20:21:47 2018 +0800 Autoload eglot-ensure () Copyright-paperwork-exempt: yes * eglot.el (eglot-ensure): Add autoload cookie GitHub-reference: https://github.com/joaotavora/eglot/issues/120 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 959cc5d1b8..8e4be897b7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -446,6 +446,7 @@ INTERACTIVE is t if called interactively." (defvar eglot--managed-mode) ; forward decl +;;;###autoload (defun eglot-ensure () "Start Eglot session for current buffer if there isn't one." (let ((buffer (current-buffer))) commit 4771f2f6850cc5c2402e0f88ae85a29ab1d76709 Author: João Távora Date: Mon Sep 17 10:00:16 2018 +0100 Don't block kill-buffer-hook if server somehow hangs * eglot.el (eglot--signal-textDocument/didClose): Use with-demoted-errors. GitHub-reference: close https://github.com/joaotavora/eglot/issues/115 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1a702543b9..959cc5d1b8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1223,9 +1223,11 @@ When called interactively, use the currently active server" (defun eglot--signal-textDocument/didClose () "Send textDocument/didClose to server." - (jsonrpc-notify - (eglot--current-server-or-lose) - :textDocument/didClose `(:textDocument ,(eglot--TextDocumentIdentifier)))) + (with-demoted-errors + "[eglot] error sending textDocument/didClose: %s" + (jsonrpc-notify + (eglot--current-server-or-lose) + :textDocument/didClose `(:textDocument ,(eglot--TextDocumentIdentifier))))) (defun eglot--signal-textDocument/willSave () "Send textDocument/willSave to server." commit 5c9fb5c3982f266c55a3ab472b296496389c95f0 Author: Aleksey Kladov Date: Sat Sep 8 23:19:29 2018 +0100 Don't send other notifications before initialized Copyright-paperwork-exempt: yes * eglot.el (eglot--connect): send initialized before activating minor mode. GitHub-reference: close https://github.com/joaotavora/eglot/issues/100 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 49cfce28ce..1a702543b9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -559,10 +559,10 @@ This docstring appeases checkdoc, that's all." (push server (gethash project eglot--servers-by-project)) (setf (eglot--capabilities server) capabilities) + (jsonrpc-notify server :initialized `(:__dummy__ t)) (dolist (buffer (buffer-list)) (with-current-buffer buffer (eglot--maybe-activate-editing-mode server))) - (jsonrpc-notify server :initialized `(:__dummy__ t)) (setf (eglot--inhibit-autoreconnect server) (cond ((booleanp eglot-autoreconnect) commit 2d6b24bfa67190e0364ecaba0298dac891992f40 Author: João Távora Date: Fri Sep 7 12:46:46 2018 +0100 Prefer ccls over cquery for c/c++ * README.md (Installation and usage): Prefer ccls to cquery. Mention clangd. * eglot.el (eglot-server-programs): Suggest ccls for c/c++ by default. GitHub-reference: close https://github.com/joaotavora/eglot/issues/94 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 14c32db9ee..49cfce28ce 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -83,8 +83,7 @@ js2-mode rjsx-mode) . ("javascript-typescript-stdio")) (sh-mode . ("bash-language-server" "start")) - ((c++-mode - c-mode) . (eglot-cquery "cquery")) + ((c++-mode c-mode) . ("ccls")) (ruby-mode . ("solargraph" "socket" "--port" :autoport)) commit ef5e1235f657f224468ea9ba32cfff2d46475fc0 Author: João Távora Date: Fri Sep 7 12:22:14 2018 +0100 Fix serious breakage introduced by string-prefix-p doesn't work on symbols * eglot.el (eglot-handle-notification): Coerce method name to string. GitHub-reference: https://github.com/joaotavora/eglot/issues/93 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2ef22593e0..14c32db9ee 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -987,7 +987,7 @@ Uses THING, FACE, DEFS and PREPEND." (cl-defmethod eglot-handle-notification (_server method &key &allow-other-keys) "Handle unknown notification" - (unless (string-prefix-p "$" method) + (unless (string-prefix-p "$" (format "%s" method)) (eglot--warn "Server sent unknown notification method `%s'" method))) (cl-defmethod eglot-handle-request commit acda0eda5a8599d8a843eb3c9dc214d80593853f Author: Fangrui Song Date: Fri Sep 7 02:00:18 2018 -0700 Don't warn on implementation-specific notifications () Only warn when method name doesn't start with '$'. Per the spec: "if a server or client receives notifications or requests starting with ‘$/’ it is free to ignore them if they are unknown." * eglot.el (eglot-handle-notification t t): Check method name for $. GitHub-reference: https://github.com/joaotavora/eglot/issues/93 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4ff7337d11..2ef22593e0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -987,7 +987,8 @@ Uses THING, FACE, DEFS and PREPEND." (cl-defmethod eglot-handle-notification (_server method &key &allow-other-keys) "Handle unknown notification" - (eglot--warn "Server sent unknown notification method `%s'" method)) + (unless (string-prefix-p "$" method) + (eglot--warn "Server sent unknown notification method `%s'" method))) (cl-defmethod eglot-handle-request (_server method &key &allow-other-keys) commit 6499223125ed680ab5721c5aad55561fe4c37121 Author: Evgeni Kolev Date: Mon Aug 27 16:17:09 2018 +0300 When exiting emacs, don't ask the user to confirm killing processes () Copyright-paperwork-exempt: yes * eglot.el (eglot--connect, eglot--inferior-bootstrap): pass noquery t to make-process. GitHub-reference: https://github.com/joaotavora/eglot/issues/83 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 31cce816a1..4ff7337d11 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -513,6 +513,7 @@ This docstring appeases checkdoc, that's all." :command contact :connection-type 'pipe :coding 'utf-8-emacs-unix + :noquery t :stderr (get-buffer-create (format "*%s stderr*" readable-name)))))))) (spread @@ -627,6 +628,7 @@ CONNECT-ARGS are passed as additional arguments to (make-process :name (format "autostart-inferior-%s" name) :stderr (format "*%s stderr*" name) + :noquery t :command (cl-subst (format "%s" port-number) :autoport contact))) (setq connection commit d01b5110dc6d0cf713760dbaf26ce0158478ad11 Author: João Távora Date: Sat Aug 25 22:57:22 2018 +0100 Handle case when :textdocumentsync isn't a number Also closes https://github.com/joaotavora/eglot/issues/87. * eglot.el (eglot--signal-textDocument/didChange): Grab :change from :textDocumentSync server capability. GitHub-reference: close https://github.com/joaotavora/eglot/issues/86 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7587164068..31cce816a1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1191,7 +1191,9 @@ When called interactively, use the currently active server" "Send textDocument/didChange to server." (when eglot--recent-changes (let* ((server (eglot--current-server-or-lose)) - (sync-kind (eglot--server-capable :textDocumentSync)) + (sync-capability (eglot--server-capable :textDocumentSync)) + (sync-kind (if (numberp sync-capability) sync-capability + (plist-get sync-capability :change))) (full-sync-p (or (eq sync-kind 1) (eq :emacs-messup eglot--recent-changes)))) (jsonrpc-notify commit 93ca152da7dbef8a58a00277a4f7a23222bf0055 Author: João Távora Date: Mon Aug 20 23:51:27 2018 +0100 Correctly delete text before expanding snippet completions Suggested by Amol Mandhane. * eglot.el (eglot-completion-at-point): Use length of obj in :exit-function GitHub-reference: close https://github.com/joaotavora/eglot/issues/82 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index eba1b1d1cf..7587164068 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1464,7 +1464,7 @@ is not active." (text-properties-at 0 obj) (when-let ((fn (and (eql insertTextFormat 2) (eglot--snippet-expansion-fn)))) - (delete-region (car bounds) (point)) + (delete-region (- (point) (length obj)) (point)) (funcall fn insertText)) (eglot--signal-textDocument/didChange) (eglot-eldoc-function))))))) commit 78102bc38ad796b92e03305af7510074b2ba1476 Author: Phillip Dixon Date: Mon Aug 20 21:21:51 2018 +1200 Ignore extra keys in textdocument/publishdiagnostics () Accoding to the "discussion" in https://reviews.llvm.org/D50571, it was deemed sufficient that VSCode is fine with the non-standard extension -- jt Copyright-paperwork-exempt: yes * eglot.el (eglot-handle-notification): Add &allow-other-keys GitHub-reference: https://github.com/joaotavora/eglot/issues/81 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4f726440e2..eba1b1d1cf 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1036,7 +1036,8 @@ COMMAND is a symbol naming the command." (cl-loop for diag-spec across diagnostics collect (cl-destructuring-bind (&key range ((:severity sev)) _group - _code source message) + _code source message + &allow-other-keys) diag-spec (setq message (concat source ": " message)) (pcase-let commit 9fa0dd072aa3277686ec581ae3075475e02dada6 Author: João Távora Date: Mon Aug 20 01:10:11 2018 +0100 Consider :triggercharacters in company completion * eglot.el (eglot-completion-at-point): Take advantage of :company-prefix-length. GitHub-reference: close https://github.com/joaotavora/eglot/issues/80 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3bd342dde6..4f726440e2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1384,8 +1384,9 @@ is not active." (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." (let ((bounds (bounds-of-thing-at-point 'symbol)) - (server (eglot--current-server-or-lose))) - (when (eglot--server-capable :completionProvider) + (server (eglot--current-server-or-lose)) + (completion-capability (eglot--server-capable :completionProvider))) + (when completion-capability (list (or (car bounds) (point)) (or (cdr bounds) (point)) @@ -1451,6 +1452,10 @@ is not active." (erase-buffer) (insert (eglot--format-markup documentation)) (current-buffer))))) + :company-prefix-length + (cl-some #'looking-back + (mapcar #'regexp-quote + (plist-get completion-capability :triggerCharacters))) :exit-function (lambda (obj _status) (cl-destructuring-bind (&key insertTextFormat insertText commit 667821d2963eb7123bb9f76909247985d3435ef2 Author: João Távora Date: Sun Aug 19 02:11:09 2018 +0100 Improve snippet support * eglot.el (eglot-client-capabilities): Don't always declare snippet support. (eglot--snippet-expansion-fn): New helper. (eglot-completion-at-point): Better annotations when snippets are supported. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d38e250328..3bd342dde6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -204,7 +204,11 @@ let the buffer grow forever." :dynamicRegistration :json-false :willSave t :willSaveWaitUntil t :didSave t) :completion (list :dynamicRegistration :json-false - :completionItem `(:snippetSupport t)) + :completionItem + `(:snippetSupport + ,(if (eglot--snippet-expansion-fn) + t + :json-false))) :hover `(:dynamicRegistration :json-false) :signatureHelp `(:dynamicRegistration :json-false) :references `(:dynamicRegistration :json-false) @@ -698,6 +702,13 @@ If optional MARKER, return a marker instead" (let ((retval (url-filename (url-generic-parse-url (url-unhex-string uri))))) (if (eq system-type 'windows-nt) (substring retval 1) retval))) +(defun eglot--snippet-expansion-fn () + "Compute a function to expand snippets. +Doubles as an indicator of snippet support." + (and (boundp 'yas-minor-mode) + (symbol-value 'yas-minor-mode) + 'yas-expand-snippet)) + (defconst eglot--kind-names `((1 . "Text") (2 . "Method") (3 . "Function") (4 . "Constructor") (5 . "Field") (6 . "Variable") (7 . "Class") (8 . "Interface") @@ -1387,25 +1398,36 @@ is not active." :cancel-on-input t)) (items (if (vectorp resp) resp (plist-get resp :items)))) (mapcar - (jsonrpc-lambda (&rest all &key label insertText &allow-other-keys) - (let ((insert (or insertText (string-trim-left label)))) - (add-text-properties 0 1 all insert) - (put-text-property 0 1 'eglot--lsp-completion all insert) - insert)) + (jsonrpc-lambda (&rest all &key label insertText insertTextFormat + &allow-other-keys) + (let ((completion + (cond ((and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)) + (string-trim-left label)) + (t + (or insertText (string-trim-left label)))))) + (add-text-properties 0 1 all completion) + (put-text-property 0 1 'eglot--lsp-completion all completion) + completion)) items)))) :annotation-function (lambda (obj) - (cl-destructuring-bind (&key detail documentation kind &allow-other-keys) + (cl-destructuring-bind (&key detail kind insertTextFormat + &allow-other-keys) (text-properties-at 0 obj) - (let ((annotation - (or (and documentation - (replace-regexp-in-string - "\n.*" "" (eglot--format-markup documentation))) - detail - (cdr (assoc kind eglot--kind-names))))) + (let* ((detail (and (stringp detail) + (not (string= detail "")) + detail)) + (annotation + (or detail + (cdr (assoc kind eglot--kind-names))))) (when annotation - (concat " " (propertize annotation - 'face 'font-lock-function-name-face)))))) + (concat " " + (propertize annotation + 'face 'font-lock-function-name-face) + (and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn) + " (snippet)")))))) :display-sort-function (lambda (items) (sort items (lambda (a b) @@ -1434,10 +1456,10 @@ is not active." insertText &allow-other-keys) (text-properties-at 0 obj) - (when (and (eql insertTextFormat 2) - (fboundp 'yas-expand-snippet)) - (delete-region (- (point) (length obj)) (point)) - (funcall 'yas-expand-snippet insertText)) + (when-let ((fn (and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)))) + (delete-region (car bounds) (point)) + (funcall fn insertText)) (eglot--signal-textDocument/didChange) (eglot-eldoc-function))))))) commit 2190da46201ec01b58f9524e0e07172fbf44e3de Author: Evgeni Kolev Date: Sat Aug 18 14:33:13 2018 +0300 Add go-langserver () Copyright-paperwork-exempt: yes * README.md (Installation and usage): Add go-langserver. * eglot.el (eglot-server-programs): Add go-langserver. GitHub-reference: https://github.com/joaotavora/eglot/issues/74 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3fd98c7d80..d38e250328 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -91,7 +91,8 @@ (php-mode . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php")) (haskell-mode . ("hie-wrapper")) - (kotlin-mode . ("kotlin-language-server"))) + (kotlin-mode . ("kotlin-language-server")) + (go-mode . ("go-langserver" "-mode=stdio" "-gocodecompletion"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated commit 88e9d97119dbb8879b74634b88d1c8e7da2bb5a4 Author: João Távora Date: Sat Aug 18 12:26:55 2018 +0100 Don't error if server replies with empty hover message * eglot.el (eglot-eldoc-function): Check non-nil contents. GitHub-reference: per https://github.com/joaotavora/eglot/issues/74 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6f3676117a..3fd98c7d80 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1512,7 +1512,9 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." :success-fn (jsonrpc-lambda (&key contents range) (unless sig-showing (when-buffer-window - (when-let (info (eglot--hover-info contents range)) + (when-let (info (and contents + (eglot--hover-info contents + range))) (eldoc-message info))))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) commit 7704fbac0be3bc40ec99467cbe5ed04f80698335 Author: Michal Krzywkowski Date: Sat Aug 18 01:56:01 2018 +0200 Fix textdocument/hover responses where markedstring is a plist () * eglot.el (eglot--hover-info): Forward all non-vector content to eglot--format-markup. GitHub-reference: https://github.com/joaotavora/eglot/issues/72 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13c72abdc4..6f3676117a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1446,8 +1446,7 @@ is not active." (let ((heading (and range (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (concat (buffer-substring beg end) ": ")))) (body (mapconcat #'eglot--format-markup - (append (cond ((vectorp contents) contents) - ((stringp contents) (list contents)))) "\n"))) + (if (vectorp contents) contents (list contents)) "\n"))) (when (or heading (cl-plusp (length body))) (concat heading body)))) (defun eglot--sig-info (sigs active-sig active-param) commit 0f33ef3e67cc16b04c7308688788d2c59b144a40 Author: João Távora Date: Fri Aug 10 02:29:26 2018 +0100 Support snippet completions * eglot.el (eglot-client-capabilities): Declare support for snippet-based completions. (eglot-completion-at-point): Expand snippet completions with YASnippet if that is found. (eglot-note, eglot-warning, eglot-error): Diagnostic overlay priorities have to be slightly lower than yasnippet's, which must be reasonably high. GitHub-reference: close https://github.com/joaotavora/eglot/issues/50 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 63fbce205d..13c72abdc4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -202,7 +202,8 @@ let the buffer grow forever." :synchronization (list :dynamicRegistration :json-false :willSave t :willSaveWaitUntil t :didSave t) - :completion `(:dynamicRegistration :json-false) + :completion (list :dynamicRegistration :json-false + :completionItem `(:snippetSupport t)) :hover `(:dynamicRegistration :json-false) :signatureHelp `(:dynamicRegistration :json-false) :references `(:dynamicRegistration :json-false) @@ -956,13 +957,15 @@ Uses THING, FACE, DEFS and PREPEND." (defalias 'eglot--make-diag 'flymake-make-diagnostic) (defalias 'eglot--diag-data 'flymake-diagnostic-data) -(dolist (type '(eglot-error eglot-warning eglot-note)) - (put type 'flymake-overlay-control - `((mouse-face . highlight) - (keymap . ,(let ((map (make-sparse-keymap))) - (define-key map [mouse-1] - (eglot--mouse-call 'eglot-code-actions)) - map))))) +(cl-loop for i from 1 + for type in '(eglot-note eglot-warning eglot-error ) + do (put type 'flymake-overlay-control + `((mouse-face . highlight) + (priority . ,(+ 50 i)) + (keymap . ,(let ((map (make-sparse-keymap))) + (define-key map [mouse-1] + (eglot--mouse-call 'eglot-code-actions)) + map))))) ;;; Protocol implementation (Requests, notifications, etc) @@ -1384,7 +1387,7 @@ is not active." (items (if (vectorp resp) resp (plist-get resp :items)))) (mapcar (jsonrpc-lambda (&rest all &key label insertText &allow-other-keys) - (let ((insert (or insertText label))) + (let ((insert (or insertText (string-trim-left label)))) (add-text-properties 0 1 all insert) (put-text-property 0 1 'eglot--lsp-completion all insert) insert)) @@ -1425,9 +1428,17 @@ is not active." (erase-buffer) (insert (eglot--format-markup documentation)) (current-buffer))))) - :exit-function (lambda (_string _status) - (eglot--signal-textDocument/didChange) - (eglot-eldoc-function)))))) + :exit-function (lambda (obj _status) + (cl-destructuring-bind (&key insertTextFormat + insertText + &allow-other-keys) + (text-properties-at 0 obj) + (when (and (eql insertTextFormat 2) + (fboundp 'yas-expand-snippet)) + (delete-region (- (point) (length obj)) (point)) + (funcall 'yas-expand-snippet insertText)) + (eglot--signal-textDocument/didChange) + (eglot-eldoc-function))))))) (defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") commit 25a7b3d4a1ab26d96144155b06cde1052cebb4d3 Author: João Távora Date: Fri Aug 17 23:23:31 2018 +0100 Fix eglot-capabilities when querying for multiple features * eglot-tests.el (eglot-capabilities): New test. * eglot.el (eglot--server-capable): Fix problems with queries for multiple capabilities. GitHub-reference: per https://github.com/joaotavora/eglot/issues/74 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 88c6b45141..63fbce205d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -728,12 +728,12 @@ under cursor." feats) (cl-loop for caps = (eglot--capabilities (eglot--current-server-or-lose)) then (cadr probe) - for feat in feats + for (feat . more) on feats for probe = (plist-member caps feat) if (not probe) do (cl-return nil) if (eq (cadr probe) :json-false) do (cl-return nil) - if (not (listp (cadr probe))) do (cl-return (cadr probe)) - finally (cl-return (or probe t))))) + if (not (listp (cadr probe))) do (cl-return (if more nil (cadr probe))) + finally (cl-return (or (cadr probe) t))))) (defun eglot--range-region (range &optional markers) "Return region (BEG . END) that represents LSP RANGE. commit b72a4e4e2e241703b74a91ff3157c366bd07bbdc Author: João Távora Date: Mon Aug 13 20:02:48 2018 +0100 Prompt for server in interactive eglot-shutdown * eglot.el (eglot--read-server): New helper. (eglot-shutdown): Use it. GitHub-reference: close https://github.com/joaotavora/eglot/issues/73 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 43334adffa..88c6b45141 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -261,12 +261,18 @@ let the buffer grow forever." (defun eglot-shutdown (server &optional _interactive timeout preserve-buffers) "Politely ask SERVER to quit. +Interactively, read SERVER from the minibuffer unless there is +only one and it's managing the current buffer. + Forcefully quit it if it doesn't respond within TIMEOUT seconds. +Don't leave this function with the server still running. + If PRESERVE-BUFFERS is non-nil (interactively, when called with a prefix argument), do not kill events and output buffers of -SERVER. Don't leave this function with the server still -running." - (interactive (list (eglot--current-server-or-lose) t nil current-prefix-arg)) +SERVER. ." + (interactive (list (eglot--read-server "Shutdown which server" + (eglot--current-server)) + t nil current-prefix-arg)) (eglot--message "Asking %s politely to terminate" (jsonrpc-name server)) (unwind-protect (progn @@ -737,6 +743,32 @@ If optional MARKERS, make markers." (end (eglot--lsp-position-to-point (plist-get range :end) markers))) (cons beg end))) +(defun eglot--read-server (prompt &optional dont-if-just-the-one) + "Read a running Eglot server from minibuffer using PROMPT. +If DONT-IF-JUST-THE-ONE and there's only one server, don't prompt +and just return it. PROMPT shouldn't end with a question mark." + (let ((servers (cl-loop for servers + being hash-values of eglot--servers-by-project + append servers)) + (name (lambda (srv) + (format "%s/%s" (eglot--project-nickname srv) + (eglot--major-mode srv))))) + (cond ((null servers) + (eglot--error "No servers!")) + ((or (cdr servers) (not dont-if-just-the-one)) + (let* ((default (when-let ((current (eglot--current-server))) + (funcall name current))) + (read (completing-read + (if default + (format "%s (default %s)? " prompt default) + (concat prompt "? ")) + (mapcar name servers) + nil t + nil nil + default))) + (cl-find read servers :key name :test #'equal))) + (t (car servers))))) + ;;; Minor modes ;;; commit 04415fa0665e097d4dd4eeabc23e19047986ff91 Author: James Nguyen Date: Sun Aug 12 17:50:17 2018 -0700 Add kotlin-language-server () https://github.com/fwcd/KotlinLanguageServer copyright-paperwork-exempt: yes * README.md (Installation and Usage): declare Kotlin support. GitHub-reference: https://github.com/joaotavora/eglot/issues/70 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 427fa293b6..43334adffa 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -90,7 +90,8 @@ :autoport)) (php-mode . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php")) - (haskell-mode . ("hie-wrapper"))) + (haskell-mode . ("hie-wrapper")) + (kotlin-mode . ("kotlin-language-server"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated commit b5e28c2ea6434d6216d927bac169bbdb089da969 Author: João Távora Date: Mon Aug 13 01:45:40 2018 +0100 Handle edits to same position in the correct order In eglot--apply-text-edits, the markers returned by eglot--lsp-position-to-point are of the "stay" type, i.e. have an insertion-type of nil. This causes multiple insertion edits to the same location to happen in the reverse order in which they appear in the LSP message, which is a violation of the spec and a bug. There are more ways to solve this (see related discuttion in https://github.com/joaotavora/eglot/pull/64), but the easiest way is to revert the order in which the edits are processed. This is because the spec tells us that the order is only relevant in precisely this "same position" case. So if we reverse the order we fix this bug and don't break anything else. * eglot.el (eglot--apply-text-edits): Apply edits in reverse.. GitHub-reference: close https://github.com/joaotavora/eglot/issues/64 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index eeea104e04..427fa293b6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1555,7 +1555,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (progress-reporter-update reporter (cl-incf done))))))) (mapcar (jsonrpc-lambda (&key range newText) (cons newText (eglot--range-region range 'markers))) - edits)) + (reverse edits))) (undo-amalgamate-change-group change-group) (progress-reporter-done reporter)))) commit cc2044834e2e15d76db630446af88ffe45fd50bf Author: João Távora Date: Mon Aug 13 01:29:41 2018 +0100 Control the size of the events buffer * eglot.el (eglot-events-buffer-size): New defcustom. (eglot--connect): Use it. GitHub-reference: close https://github.com/joaotavora/eglot/issues/41 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ac529dc8d2..eeea104e04 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -154,6 +154,14 @@ as 0, i.e. don't block at all." :type '(choice (boolean :tag "Whether to inhibit autoreconnection") (integer :tag "Number of seconds"))) +(defcustom eglot-events-buffer-size 2000000 + "Control the size of the Eglot events buffer. +If a number, don't let the buffer grow larger than that many +characters. If 0, don't use an event's buffer at all. If nil, +let the buffer grow forever." + :type '(choice (const :tag "No limit" nil) + (integer :tag "Number of characters"))) + ;;; API (WORK-IN-PROGRESS!) ;;; (cl-defmacro eglot--with-live-buffer (buf &rest body) @@ -502,6 +510,7 @@ This docstring appeases checkdoc, that's all." (apply #'make-instance class :name readable-name + :events-buffer-scrollback-size eglot-events-buffer-size :notification-dispatcher (funcall spread #'eglot-handle-notification) :request-dispatcher (funcall spread #'eglot-handle-request) :on-shutdown #'eglot--on-shutdown commit d164ece5cf0fefec4c36c1b80f9f43c35e00f4f9 Author: João Távora Date: Sat Aug 11 14:52:33 2018 +0100 Implement asynchronous server connection A new defcustom eglot-sync-connect controls this feature. If it is t, eglot should behave like previously, waiting synchronously for a connection to be established, with the exception that there is now a non-nil timeout set to eglot-connect-timeout, which defaults to 30 seconds. eglot-connect is now considerably more complicated as it replicates most of the work that jsonrpc-request does vis-a-vis handling errors, timeouts and user quits.. * eglot-tests.el (eglot--call-with-dirs-and-files): Simplify cleanup logic. (slow-sync-connection-wait) (slow-sync-connection-intime, slow-async-connection) (slow-sync-error): New tests. * eglot.el (eglot-sync-connect): New defcustom. (eglot-ensure, eglot): Simplify. (eglot--connect): Honour eglot-sync-connect. Complicate considerably. (eglot-connect-timeout): New defcustom. (Package-requires): Require jsonrpc 1.0.6 GitHub-reference: close https://github.com/joaotavora/eglot/issues/68 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 70a725c22f..ac529dc8d2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -7,7 +7,7 @@ ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.5")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.6")) ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by @@ -140,6 +140,19 @@ lasted more than that many seconds." :type '(choice (boolean :tag "Whether to inhibit autoreconnection") (integer :tag "Number of seconds"))) +(defcustom eglot-connect-timeout 30 + "Number of seconds before timing out LSP connection attempts. +If nil, never time out." + :type 'number) + +(defcustom eglot-sync-connect 3 + "Control blocking of LSP connection attempts. +If t, block for `eglot-connect-timeout' seconds. A positive +integer number means block for that many seconds, and then wait +for the connection in the background. nil has the same meaning +as 0, i.e. don't block at all." + :type '(choice (boolean :tag "Whether to inhibit autoreconnection") + (integer :tag "Number of seconds"))) ;;; API (WORK-IN-PROGRESS!) ;;; @@ -259,9 +272,7 @@ running." ;; Now ask jsonrpc.el to shut down the server (which under normal ;; conditions should return immediately). (jsonrpc-shutdown server (not preserve-buffers)) - (unless preserve-buffers - (mapc #'kill-buffer - `(,(jsonrpc-events-buffer server) ,(jsonrpc-stderr-buffer server)))))) + (unless preserve-buffers (kill-buffer (jsonrpc-events-buffer server))))) (defun eglot--on-shutdown (server) "Called by jsonrpc.el when SERVER is already dead." @@ -399,15 +410,7 @@ INTERACTIVE is t if called interactively." (y-or-n-p "[eglot] Live process found, reconnect instead? ")) (eglot-reconnect current-server interactive) (when live-p (ignore-errors (eglot-shutdown current-server))) - (let ((server (eglot--connect managed-major-mode - project - class - contact))) - (eglot--message "Connected! Process `%s' now \ -managing `%s' buffers in project `%s'." - (jsonrpc-name server) managed-major-mode - (eglot--project-nickname server)) - server)))) + (eglot--connect managed-major-mode project class contact)))) (defun eglot-reconnect (server &optional interactive) "Reconnect to SERVER. @@ -432,12 +435,7 @@ INTERACTIVE is t if called interactively." (remove-hook 'post-command-hook #'maybe-connect nil) (eglot--with-live-buffer buffer (unless eglot--managed-mode - (let ((server (apply #'eglot--connect (eglot--guess-contact)))) - (eglot--message - "Automatically started `%s' to manage `%s' buffers in project `%s'" - (jsonrpc-name server) - major-mode - (eglot--project-nickname server))))))) + (apply #'eglot--connect (eglot--guess-contact)))))) (when buffer-file-name (add-hook 'post-command-hook #'maybe-connect 'append nil))))) @@ -508,42 +506,84 @@ This docstring appeases checkdoc, that's all." :request-dispatcher (funcall spread #'eglot-handle-request) :on-shutdown #'eglot--on-shutdown initargs)) - success) + (cancelled nil) + (tag (make-symbol "connected-catch-tag"))) (setf (eglot--saved-initargs server) initargs) (setf (eglot--project server) project) (setf (eglot--project-nickname server) nickname) (setf (eglot--major-mode server) managed-major-mode) (setf (eglot--inferior-process server) autostart-inferior-process) - (push server (gethash project eglot--servers-by-project)) - (run-hook-with-args 'eglot-connect-hook server) + ;; Now start the handshake. To honour `eglot-sync-connect' + ;; maybe-sync-maybe-async semantics we use `jsonrpc-async-request' + ;; and mimic most of `jsonrpc-request'. (unwind-protect - (cl-destructuring-bind (&key capabilities) - (jsonrpc-request - server - :initialize - (list :processId (unless (eq (jsonrpc-process-type server) 'network) - (emacs-pid)) - :rootPath (expand-file-name default-directory) - :rootUri (eglot--path-to-uri default-directory) - :initializationOptions (eglot-initialization-options server) - :capabilities (eglot-client-capabilities server))) - (setf (eglot--capabilities server) capabilities) - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (eglot--maybe-activate-editing-mode server))) - (jsonrpc-notify server :initialized `(:__dummy__ t)) - (run-hook-with-args 'eglot-server-initialized-hook server) - (setf (eglot--inhibit-autoreconnect server) - (cond - ((booleanp eglot-autoreconnect) (not eglot-autoreconnect)) - ((cl-plusp eglot-autoreconnect) - (run-with-timer eglot-autoreconnect nil - (lambda () - (setf (eglot--inhibit-autoreconnect server) - (null eglot-autoreconnect))))))) - (setq success server)) - (when (and (not success) (jsonrpc-running-p server)) - (eglot-shutdown server))))) + (condition-case _quit + (let ((retval + (catch tag + (jsonrpc-async-request + server + :initialize + (list :processId (unless (eq (jsonrpc-process-type server) + 'network) + (emacs-pid)) + :rootPath (expand-file-name default-directory) + :rootUri (eglot--path-to-uri default-directory) + :initializationOptions (eglot-initialization-options + server) + :capabilities (eglot-client-capabilities server)) + :success-fn + (jsonrpc-lambda (&key capabilities) + (unless cancelled + (push server + (gethash project eglot--servers-by-project)) + (setf (eglot--capabilities server) capabilities) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (eglot--maybe-activate-editing-mode server))) + (jsonrpc-notify server :initialized `(:__dummy__ t)) + (setf (eglot--inhibit-autoreconnect server) + (cond + ((booleanp eglot-autoreconnect) + (not eglot-autoreconnect)) + ((cl-plusp eglot-autoreconnect) + (run-with-timer + eglot-autoreconnect nil + (lambda () + (setf (eglot--inhibit-autoreconnect server) + (null eglot-autoreconnect))))))) + (run-hook-with-args 'eglot-connect-hook server) + (run-hook-with-args 'eglot-server-initialized-hook server) + (eglot--message + "Connected! Server `%s' now managing `%s' buffers \ +in project `%s'." + (jsonrpc-name server) managed-major-mode + (eglot--project-nickname server)) + (when tag (throw tag t)))) + :timeout eglot-connect-timeout + :error-fn (jsonrpc-lambda (&key code message _data) + (unless cancelled + (jsonrpc-shutdown server) + (let ((msg (format "%s: %s" code message))) + (if tag (throw tag `(error . ,msg)) + (eglot--error msg))))) + :timeout-fn (lambda () + (unless cancelled + (jsonrpc-shutdown server) + (let ((msg (format "Timed out"))) + (if tag (throw tag `(error . ,msg)) + (eglot--error msg)))))) + (cond ((numberp eglot-sync-connect) + (accept-process-output nil eglot-sync-connect)) + (eglot-sync-connect + (while t (accept-process-output nil 30))))))) + (pcase retval + (`(error . ,msg) (eglot--error msg)) + (`nil (eglot--message "Waiting in background for server `%s'" + (jsonrpc-name server)) + nil) + (_ server))) + (quit (jsonrpc-shutdown server) (setq cancelled 'quit))) + (setq tag nil)))) (defun eglot--inferior-bootstrap (name contact &optional connect-args) "Use CONTACT to start a server, then connect to it. commit ef5266397a0447a356b401df5572becdb30e7c61 Author: Michal Krzywkowski Date: Sun Aug 12 01:22:26 2018 +0200 Kill server's output and events buffers from eglot-shutdown () * eglot.el (Package-Requires): Require jsonrpc 1.0.5 (eglot-shutdown): Kill events and stderr buffers of the server, unless new PRESERVE-BUFFERS argument is non-nil. eglot-reconnect): Preserve buffers on shutdown. * eglot-tests.el (eglot--call-with-dirs-and-files): Call eglot-shutdown with non-nil PRESERVE-BUFFERS arg. GitHub-reference: https://github.com/joaotavora/eglot/issues/66 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e37ec94ab0..70a725c22f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -7,7 +7,7 @@ ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.2")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.5")) ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by @@ -237,11 +237,14 @@ lasted more than that many seconds." (defvar eglot--servers-by-project (make-hash-table :test #'equal) "Keys are projects. Values are lists of processes.") -(defun eglot-shutdown (server &optional _interactive timeout) +(defun eglot-shutdown (server &optional _interactive timeout preserve-buffers) "Politely ask SERVER to quit. Forcefully quit it if it doesn't respond within TIMEOUT seconds. -Don't leave this function with the server still running." - (interactive (list (eglot--current-server-or-lose) t)) +If PRESERVE-BUFFERS is non-nil (interactively, when called with a +prefix argument), do not kill events and output buffers of +SERVER. Don't leave this function with the server still +running." + (interactive (list (eglot--current-server-or-lose) t nil current-prefix-arg)) (eglot--message "Asking %s politely to terminate" (jsonrpc-name server)) (unwind-protect (progn @@ -253,9 +256,12 @@ Don't leave this function with the server still running." ;; Turn off `eglot--managed-mode' where appropriate. (dolist (buffer (eglot--managed-buffers server)) (eglot--with-live-buffer buffer (eglot--managed-mode-onoff server nil))) - ;; Now ask jsonrpc.el to shutdown server (which in normal + ;; Now ask jsonrpc.el to shut down the server (which under normal ;; conditions should return immediately). - (jsonrpc-shutdown server))) + (jsonrpc-shutdown server (not preserve-buffers)) + (unless preserve-buffers + (mapc #'kill-buffer + `(,(jsonrpc-events-buffer server) ,(jsonrpc-stderr-buffer server)))))) (defun eglot--on-shutdown (server) "Called by jsonrpc.el when SERVER is already dead." @@ -408,7 +414,7 @@ managing `%s' buffers in project `%s'." INTERACTIVE is t if called interactively." (interactive (list (eglot--current-server-or-lose) t)) (when (jsonrpc-running-p server) - (ignore-errors (eglot-shutdown server interactive))) + (ignore-errors (eglot-shutdown server interactive nil 'preserve-buffers))) (eglot--connect (eglot--major-mode server) (eglot--project server) (eieio-object-class-name server) commit 4dc3c8d0dce1ebf0d26d9092593bddb4119c26d0 Author: João Távora Date: Sat Aug 11 17:28:59 2018 +0100 Improve eglot-execute-command api to ease overriding by servers * eglot.el (eglot-execute-command): COMMAND can be a symbol. (eglot-code-actions): Pass symbols to eglot-command. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e250ce0924..e37ec94ab0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -159,7 +159,7 @@ lasted more than that many seconds." "Handle SERVER's METHOD notification with PARAMS.") (cl-defgeneric eglot-execute-command (server command arguments) - "Execute on SERVER COMMAND with ARGUMENTS.") + "Ask SERVER to execute COMMAND with ARGUMENTS.") (cl-defgeneric eglot-initialization-options (server) "JSON object to send under `initializationOptions'" @@ -891,11 +891,10 @@ Uses THING, FACE, DEFS and PREPEND." (cl-defmethod eglot-execute-command (server command arguments) - "Execute command by making a :workspace/executeCommand request." - (jsonrpc-request - server - :workspace/executeCommand - `(:command ,command :arguments ,arguments))) + "Execute COMMAND on SERVER with `:workspace/executeCommand'. +COMMAND is a symbol naming the command." + (jsonrpc-request server :workspace/executeCommand + `(:command ,(format "%s" command) :arguments ,arguments))) (cl-defmethod eglot-handle-notification (_server (_method (eql window/showMessage)) &key type message) @@ -1586,10 +1585,10 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (if (eq (setq retval (tmm-prompt menu)) never-mind) (keyboard-quit) retval)))))) - (if command-and-args - (eglot-execute-command server (plist-get command-and-args :command) - (plist-get command-and-args :arguments)) - (eglot--message "No code actions here")))) + (cl-destructuring-bind (&key _title command arguments) command-and-args + (if command + (eglot-execute-command server (intern command) arguments) + (eglot--message "No code actions here"))))) commit 2ebf34f1e12af56c4c2ff9bbaa0b01f4827b5005 Author: Michal Krzywkowski Date: Thu Aug 9 15:24:29 2018 +0200 Add a generic eglot-execute-command api * eglot.el (eglot-execute-command): New defgeneric and method. Use it to request :workspace/executeCommand. (eglot-code-actions): Use it. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9e16d4316e..e250ce0924 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -158,6 +158,9 @@ lasted more than that many seconds." (cl-defgeneric eglot-handle-notification (server method id &rest params) "Handle SERVER's METHOD notification with PARAMS.") +(cl-defgeneric eglot-execute-command (server command arguments) + "Execute on SERVER COMMAND with ARGUMENTS.") + (cl-defgeneric eglot-initialization-options (server) "JSON object to send under `initializationOptions'" (:method (_s) nil)) ; blank default @@ -886,6 +889,14 @@ Uses THING, FACE, DEFS and PREPEND." "Handle unknown request" (jsonrpc-error "Unknown request method `%s'" method)) +(cl-defmethod eglot-execute-command + (server command arguments) + "Execute command by making a :workspace/executeCommand request." + (jsonrpc-request + server + :workspace/executeCommand + `(:command ,command :arguments ,arguments))) + (cl-defmethod eglot-handle-notification (_server (_method (eql window/showMessage)) &key type message) "Handle notification window/showMessage" @@ -1576,7 +1587,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (keyboard-quit) retval)))))) (if command-and-args - (jsonrpc-request server :workspace/executeCommand command-and-args) + (eglot-execute-command server (plist-get command-and-args :command) + (plist-get command-and-args :arguments)) (eglot--message "No code actions here")))) commit c76c0240dcb4cb1b11f58852c598986f3328d2d7 Author: Michal Krzywkowski Date: Fri Aug 10 19:13:59 2018 +0200 * eglot.el (eglot-cquery): capitalize docstring. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7f0e91dbbc..9e16d4316e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1661,7 +1661,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." ;;; cquery-specific ;;; (defclass eglot-cquery (eglot-lsp-server) () - :documentation "cquery's C/C++ langserver.") + :documentation "Cquery's C/C++ langserver.") (cl-defmethod eglot-initialization-options ((server eglot-cquery)) "Passes through required cquery initialization options" commit 4144d9adc5cea0fb8c2bb9db217cc8fd2d625e02 Author: João Távora Date: Fri Aug 10 01:42:01 2018 +0100 * eglot.el (advice-add jsonrpc-request): add &allow-other-keys diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3b1b364ac9..7f0e91dbbc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1048,7 +1048,8 @@ Records START, END and PRE-CHANGE-LENGTH locally." ;; bad idea, since that might lead to the request never having a ;; chance to run, because `jsonrpc-connection-ready-p'. (advice-add #'jsonrpc-request :before - (cl-function (lambda (_proc _method _params &key deferred _timeout) + (cl-function (lambda (_proc _method _params &key + deferred &allow-other-keys) (when (and eglot--managed-mode deferred) (eglot--signal-textDocument/didChange)))) '((name . eglot--signal-textDocument/didChange))) commit 0eddf00dd9953c680b739ce14ff1dc3bd9ad8e86 Author: João Távora Date: Fri Aug 10 01:28:41 2018 +0100 Require jsonrpc.el 1.0.2 (gnu elpa didn't build 1.0.1) * eglot.el (Package-Requires): Require jsonrpc 1.0.2 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9e714163a4..3b1b364ac9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -7,7 +7,7 @@ ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.1")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.2")) ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by commit c61b3624f5a20efaea3ff997d48100917c286ffa Author: João Távora Date: Fri Aug 10 01:03:59 2018 +0100 Snappier completions that don't hinder typing This should improve company-capf's performance. * eglot.el (Package-Requires): Require jsonrpc 1.0,1 (eglot-completion-at-point): Use completion-table-dynamic. Pass CANCEL-ON-INPUT to jsonrpc-request. GitHub-reference: close https://github.com/joaotavora/eglot/issues/61 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d29bef540c..9e714163a4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -7,7 +7,7 @@ ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.0")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.1")) ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by @@ -1275,12 +1275,13 @@ is not active." (list (or (car bounds) (point)) (or (cdr bounds) (point)) - (completion-table-with-cache + (completion-table-dynamic (lambda (_ignored) (let* ((resp (jsonrpc-request server :textDocument/completion (eglot--TextDocumentPositionParams) - :deferred :textDocument/completion)) + :deferred :textDocument/completion + :cancel-on-input t)) (items (if (vectorp resp) resp (plist-get resp :items)))) (mapcar (jsonrpc-lambda (&rest all &key label insertText &allow-other-keys) @@ -1317,7 +1318,8 @@ is not active." (plist-get (jsonrpc-request server :completionItem/resolve (get-text-property - 0 'eglot--lsp-completion obj)) + 0 'eglot--lsp-completion obj) + :cancel-on-input t) :documentation))))) (when documentation (with-current-buffer (get-buffer-create " *eglot doc*") commit da11bba15e8026cd177364d3be9d87554b3ab147 Author: Michal Krzywkowski Date: Thu Aug 9 21:21:32 2018 +0200 Notify server of recent changes before save notification * eglot.el (eglot--signal-textDocument/didSave): Call eglot--signal-textDocument/didChange. GitHub-reference: close https://github.com/joaotavora/eglot/issues/60 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 125c795bc3..d29bef540c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1121,6 +1121,7 @@ When called interactively, use the currently active server" (defun eglot--signal-textDocument/didSave () "Send textDocument/didSave to server." + (eglot--signal-textDocument/didChange) (jsonrpc-notify (eglot--current-server-or-lose) :textDocument/didSave commit c93150ebbe929eef79e023bce4463d24ae245a94 Author: João Távora Date: Tue Aug 7 22:13:28 2018 +0100 * eglot.el (eglot-initialization-options): fix spurious typo. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 33b478f4af..125c795bc3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1663,7 +1663,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." "Passes through required cquery initialization options" (let* ((root (car (project-roots (eglot--project server)))) (cache (expand-file-name ".cquery_cached_index/" root))) - (list :cacheDirectory (file-name-as-directory cache)Ini + (list :cacheDirectory (file-name-as-directory cache) :progressReportFrequencyMs -1))) commit 71a3fb813f8e4055bc4f9f68db73be5aa17d0867 Author: João Távora Date: Mon Aug 6 17:53:09 2018 +0100 Accept functions as entries in eglot-server-programs CONTACT in the (MAJOR-MODE . CONTACT) association in eglot-server-programs can now be a function of no arguments producing any value previously valid for contact. The function is called at time of `M-x eglot` or `eglot-ensure`. This is useful for servers requiring command-line invocations that depend on the specific momentary environment. * eglot.el (eglot-server-programs): CONTACT can be a fucntion of no arguments. (eglot--guess-contact, eglot--connect): Accept function CONTACTs. GitHub-reference: per https://github.com/joaotavora/eglot/issues/63 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c3a0d518d8..33b478f4af 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -121,7 +121,10 @@ of those modes. CONTACT can be: converted to produce a plist with a suitable :PROCESS initarg to CLASS-NAME. The class `eglot-lsp-server' descends `jsonrpc-process-connection', which you should see for the - semantics of the mandatory :PROCESS argument.") + semantics of the mandatory :PROCESS argument. + +* A function of no arguments producing any of the above values + for CONTACT.") (defface eglot-mode-line '((t (:inherit font-lock-constant-face :weight bold))) @@ -310,6 +313,7 @@ be guessed." (lambda (m1 m2) (or (eq m1 m2) (and (listp m1) (memq m2 m1))))))) + (guess (if (functionp guess) (funcall guess) guess)) (class (or (and (consp guess) (symbolp (car guess)) (prog1 (car guess) (setq guess (cdr guess)))) 'eglot-lsp-server)) @@ -457,6 +461,7 @@ This docstring appeases checkdoc, that's all." (nickname (file-name-base (directory-file-name default-directory))) (readable-name (format "EGLOT (%s/%s)" nickname managed-major-mode)) autostart-inferior-process + (contact (if (functionp contact) (funcall contact) contact)) (initargs (cond ((keywordp (car contact)) contact) ((integerp (cadr contact)) @@ -1658,7 +1663,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." "Passes through required cquery initialization options" (let* ((root (car (project-roots (eglot--project server)))) (cache (expand-file-name ".cquery_cached_index/" root))) - (list :cacheDirectory (file-name-as-directory cache) + (list :cacheDirectory (file-name-as-directory cache)Ini :progressReportFrequencyMs -1))) commit ea04e60ce5a9b8ca158668d7611331ab7713f0d3 Author: João Távora Date: Mon Aug 6 01:10:12 2018 +0100 Eglot-workspace-configuration's keys needn't be keywords * eglot.el (eglot-signal-didChangeConfiguration): Convert alist keys into a json-compatible plist. GitHub-reference: per https://github.com/joaotavora/eglot/issues/59 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7e825d6746..c3a0d518d8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1062,7 +1062,10 @@ When called interactively, use the currently active server" (list :settings (cl-loop for (k . v) in eglot-workspace-configuration - collect k collect v)))) + collect (if (keywordp k) + k + (intern (format ":%s" k))) + collect v)))) (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." commit af0e2e5b1fea4dbf21c556ca1245d7623cc92250 Author: João Távora Date: Fri Aug 3 09:14:42 2018 +0100 Default eglot-handle-notifictiona|request must &allow-other-keys * eglot.el (eglot-handle-notification, eglot-handle-request): Add &allow-other-keys diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 58efc16686..7e825d6746 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -872,12 +872,12 @@ Uses THING, FACE, DEFS and PREPEND." ;;; Protocol implementation (Requests, notifications, etc) ;;; (cl-defmethod eglot-handle-notification - (_server method &key) + (_server method &key &allow-other-keys) "Handle unknown notification" (eglot--warn "Server sent unknown notification method `%s'" method)) (cl-defmethod eglot-handle-request - (_server method &key) + (_server method &key &allow-other-keys) "Handle unknown request" (jsonrpc-error "Unknown request method `%s'" method)) commit 7b7312f8d66ca7a34fb509704f77009db7d7aaa7 Author: Michal Krzywkowski Date: Fri Aug 3 10:22:18 2018 +0200 Fix placement of diagnostics with same start and end positions Some servers such as cquery and clangd publish diagnostic with identical start and end positions. * eglot.el (eglot-handle-notification :textDocument/publishDiagnostics): Add 1 to :line since LSP lines are 0-based. Don't subtract 1 from :character, since both emacs and LSP have 0-based columns. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0bb162b07c..58efc16686 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -929,8 +929,8 @@ Uses THING, FACE, DEFS and PREPEND." (let* ((st (plist-get range :start)) (diag-region (flymake-diag-region - (current-buffer) (plist-get st :line) - (1- (plist-get st :character))))) + (current-buffer) (1+ (plist-get st :line)) + (plist-get st :character)))) (setq beg (car diag-region) end (cdr diag-region)))) (eglot--make-diag (current-buffer) beg end commit b14cba5cb73dfaa94b4f277362c938bf83a48b9d Author: João Távora Date: Thu Aug 2 11:02:20 2018 +0100 Erase company-doc buffer in between doc requests * eglot.el (eglot-completion-at-point): Erase temporary "*eglot-doc*" buffer for company's doc. GitHub-reference: close https://github.com/joaotavora/eglot/issues/58 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f590313810..0bb162b07c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1312,6 +1312,7 @@ is not active." :documentation))))) (when documentation (with-current-buffer (get-buffer-create " *eglot doc*") + (erase-buffer) (insert (eglot--format-markup documentation)) (current-buffer))))) :exit-function (lambda (_string _status) commit 308b1a9ee3bce89512d743e4a58116aa984310f0 Author: Michal Krzywkowski Date: Mon Jul 30 22:17:32 2018 +0200 * eglot.el (eglot-client-capabilities): fix a typo. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f0a4b7144d..f590313810 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -167,7 +167,7 @@ lasted more than that many seconds." :applyEdit t :executeCommand `(:dynamicRegistration :json-false) :workspaceEdit `(:documentChanges :json-false) - :didChangeWatchesFiles `(:dynamicRegistration t) + :didChangeWatchedFiles `(:dynamicRegistration t) :symbol `(:dynamicRegistration :json-false)) :textDocument (list commit 5fc7c9a9ef51349375a73e6640a16a75f3ffbf60 Author: Michal Krzywkowski Date: Tue Jul 10 15:10:25 2018 +0200 Implement textdocument/rangeformatting * eglot.el (eglot-format): New command. (eglot-format-buffer): Use it as implementation. (eglot-client-capabilities): Add :rangeFormatting. * eglot-tests.el (formatting): Also test range formatting. * README.md (Commands and keybindings): Mention eglot-format. (Language features): Tick textDocument/rangeFormatting. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fbc6a53db5..f0a4b7144d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -183,6 +183,7 @@ lasted more than that many seconds." :documentHighlight `(:dynamicRegistration :json-false) :codeAction `(:dynamicRegistration :json-false) :formatting `(:dynamicRegistration :json-false) + :rangeFormatting `(:dynamicRegistration :json-false) :rename `(:dynamicRegistration :json-false) :publishDiagnostics `(:relatedInformation :json-false)) :experimental (list)))) @@ -1227,17 +1228,35 @@ DUMMY is ignored." (defun eglot-format-buffer () "Format contents of current buffer." (interactive) - (unless (eglot--server-capable :documentFormattingProvider) - (eglot--error "Server can't format!")) - (eglot--apply-text-edits - (jsonrpc-request - (eglot--current-server-or-lose) - :textDocument/formatting - (list :textDocument (eglot--TextDocumentIdentifier) - :options (list :tabSize tab-width - :insertSpaces - (if indent-tabs-mode :json-false t))) - :deferred :textDocument/formatting))) + (eglot-format nil nil)) + +(defun eglot-format (&optional beg end) + "Format region BEG END. +If either BEG or END is nil, format entire buffer. +Interactively, format active region, or entire buffer if region +is not active." + (interactive (and (region-active-p) (list (region-beginning) (region-end)))) + (pcase-let ((`(,method ,cap ,args) + (cond + ((and beg end) + `(:textDocument/rangeFormatting + :documentRangeFormattingProvider + (:range ,(list :start (eglot--pos-to-lsp-position beg) + :end (eglot--pos-to-lsp-position end))))) + (t + '(:textDocument/formatting :documentFormattingProvider nil))))) + (unless (eglot--server-capable cap) + (eglot--error "Server can't format!")) + (eglot--apply-text-edits + (jsonrpc-request + (eglot--current-server-or-lose) + method + (cl-list* + :textDocument (eglot--TextDocumentIdentifier) + :options (list :tabSize tab-width + :insertSpaces (if indent-tabs-mode :json-false t)) + args) + :deferred method)))) (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." commit e6a801ccf6f745d149306d2afaaa4b3d313415e9 Author: Michal Krzywkowski Date: Sat Jul 28 19:14:02 2018 +0200 Correctly make lsp positions in narrowed buffers * eglot.el (eglot--pos-to-lsp-position): Fix return value when narrowing is in effect. GitHub-reference: close https://github.com/joaotavora/eglot/issues/54 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3c01657abb..fbc6a53db5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -597,10 +597,10 @@ CONNECT-ARGS are passed as additional arguments to (defun eglot--pos-to-lsp-position (&optional pos) "Convert point POS to LSP position." - (save-excursion - (list :line (1- (line-number-at-pos pos t)) ; F!@&#$CKING OFF-BY-ONE - :character (- (goto-char (or pos (point))) - (line-beginning-position))))) + (eglot--widening + (list :line (1- (line-number-at-pos pos t)) ; F!@&#$CKING OFF-BY-ONE + :character (- (goto-char (or pos (point))) + (line-beginning-position))))) (defun eglot--lsp-position-to-point (pos-plist &optional marker) "Convert LSP position POS-PLIST to Emacs point. commit 6ffe90229b0a1503bb312880ef139d9ea311692b Author: Väinö Järvelä Date: Sat Jul 28 20:03:05 2018 +0300 Fix typo in willsavewaituntil rpc request () Copyright-paperwork-exempt: yes * eglot.el (eglot--signal-textDocument/willSave): Fix typo. GitHub-reference: https://github.com/joaotavora/eglot/issues/51 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1ca7b6d65d..3c01657abb 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1107,7 +1107,7 @@ When called interactively, use the currently active server" (when (eglot--server-capable :textDocumentSync :willSaveWaitUntil) (ignore-errors (eglot--apply-text-edits - (jsonrpc-request server :textDocument/willSaveWaituntil params + (jsonrpc-request server :textDocument/willSaveWaitUntil params :timeout 0.5)))))) (defun eglot--signal-textDocument/didSave () commit eb279829cb5c11039abda16b92945a882cfcb39d Author: Michal Krzywkowski Date: Sat Jul 28 19:01:10 2018 +0200 Work around emacs bugs 32237, 32278 () See: https://debbugs.gnu.org/cgi/bugreport.cgi?bug=32237 https://debbugs.gnu.org/cgi/bugreport.cgi?bug=32278 * eglot.el (eglot--apply-text-edits): Inhibit modification hooks and call them manually for the changed region. GitHub-reference: https://github.com/joaotavora/eglot/issues/53 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 695db498ff..1ca7b6d65d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1438,7 +1438,23 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (save-excursion (save-restriction (narrow-to-region beg end) - (replace-buffer-contents temp))) + + ;; On emacs versions < 26.2, + ;; `replace-buffer-contents' is buggy - it calls + ;; change functions with invalid arguments - so we + ;; manually call the change functions here. + ;; + ;; See emacs bugs #32237, #32278: + ;; https://debbugs.gnu.org/cgi/bugreport.cgi?bug=32237 + ;; https://debbugs.gnu.org/cgi/bugreport.cgi?bug=32278 + (let ((inhibit-modification-hooks t) + (length (- end beg))) + (run-hook-with-args 'before-change-functions + beg end) + (replace-buffer-contents temp) + (run-hook-with-args 'after-change-functions + beg (+ beg (length newText)) + length)))) (progress-reporter-update reporter (cl-incf done))))))) (mapcar (jsonrpc-lambda (&key range newText) (cons newText (eglot--range-region range 'markers))) commit 9bc459df6ddbd79f18fd569b008b5f82640c0fce Author: João Távora Date: Fri Jul 27 10:06:21 2018 +0100 Be less verbose when using eglot-ensure * eglot.el (eglot-ensure): Don't message when a buffer is already managed. GitHub-reference: close https://github.com/joaotavora/eglot/issues/48 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 77f370e347..695db498ff 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -137,7 +137,7 @@ lasted more than that many seconds." :type '(choice (boolean :tag "Whether to inhibit autoreconnection") (integer :tag "Number of seconds"))) - + ;;; API (WORK-IN-PROGRESS!) ;;; (cl-defmacro eglot--with-live-buffer (buf &rest body) @@ -225,7 +225,7 @@ lasted more than that many seconds." :documentation "Represents a server. Wraps a process for LSP communication.") - + ;;; Process management (defvar eglot--servers-by-project (make-hash-table :test #'equal) "Keys are projects. Values are lists of processes.") @@ -417,9 +417,7 @@ INTERACTIVE is t if called interactively." () (remove-hook 'post-command-hook #'maybe-connect nil) (eglot--with-live-buffer buffer - (if eglot--managed-mode - (eglot--message "Buffer is already managed by existing `%s'" - (eglot--project-nickname (eglot--current-server))) + (unless eglot--managed-mode (let ((server (apply #'eglot--connect (eglot--guess-contact)))) (eglot--message "Automatically started `%s' to manage `%s' buffers in project `%s'" commit 273c5b62f3836a14e628eff0591b2601f81d736a Author: Alan Zimmerman Date: Thu Jul 26 12:32:24 2018 +0200 Add entry for haskell-ide-engine in eglot-server-programs () * eglot.el (eglot-server-programs): Add entry for haskell-mode GitHub-reference: https://github.com/joaotavora/eglot/issues/49 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6a6a1b7f2b..77f370e347 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -89,7 +89,8 @@ . ("solargraph" "socket" "--port" :autoport)) (php-mode . ("php" "vendor/felixfbecker/\ -language-server/bin/php-language-server.php"))) +language-server/bin/php-language-server.php")) + (haskell-mode . ("hie-wrapper"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated commit eb7702b61de9f5683c5dfe66f77723f92d630433 Author: João Távora Date: Wed Jul 25 18:19:13 2018 +0000 Fix messages of eglot-ensure * eglot.el (eglot-ensure): fix messages. GitHub-reference: per https://github.com/joaotavora/eglot/issues/48 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cb9a5ed051..6a6a1b7f2b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -417,13 +417,12 @@ INTERACTIVE is t if called interactively." (remove-hook 'post-command-hook #'maybe-connect nil) (eglot--with-live-buffer buffer (if eglot--managed-mode - (eglot--message "%s is already managed by existing `%s'" - buffer + (eglot--message "Buffer is already managed by existing `%s'" (eglot--project-nickname (eglot--current-server))) (let ((server (apply #'eglot--connect (eglot--guess-contact)))) (eglot--message "Automatically started `%s' to manage `%s' buffers in project `%s'" - (eglot--project-nickname server) + (jsonrpc-name server) major-mode (eglot--project-nickname server))))))) (when buffer-file-name commit 693e4282510f8ba8d70e6144707907ba9fbd56b0 Author: João Távora Date: Sun Jul 22 19:07:43 2018 +0100 Don't turn on flymake-mode any more than is needed If flymake-mode is in eglot--managed-mode-hook, it will be called even if eglot--managed-mode is being turned off, which could be problematic because it triggers a check if flymake-start-on-flymake-mode is t. * eglot.el (eglot--managed-mode): Turn on flymake-mode and eldoc-mode here. GitHub-reference: close https://github.com/joaotavora/eglot/issues/44 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 49a1f3d496..cb9a5ed051 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -700,7 +700,9 @@ If optional MARKERS, make markers." (add-hook 'change-major-mode-hook 'eglot--managed-mode-onoff nil t) (add-function :before-until (local 'eldoc-documentation-function) #'eglot-eldoc-function) - (add-function :around (local 'imenu-create-index-function) #'eglot-imenu)) + (add-function :around (local 'imenu-create-index-function) #'eglot-imenu) + (flymake-mode 1) + (eldoc-mode 1)) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) @@ -738,9 +740,6 @@ Reset in `eglot--managed-mode-onoff'.") (setf (eglot--managed-buffers server) (delq buf (eglot--managed-buffers server))))))))) -(add-hook 'eglot--managed-mode-hook 'flymake-mode) -(add-hook 'eglot--managed-mode-hook 'eldoc-mode) - (defun eglot--current-server () "Find the current logical EGLOT server." (or commit c959101180852aa2f46b2e51b21a36a76fcb8052 Author: João Távora Date: Fri Jul 20 17:45:47 2018 +0100 Robustify in the face of manual mode changes When manually changing the major-mode of a managed buffer, this sends a didClose and tears down Eglot-related stuff like if were killing the buffer. After changing the mode, we have to recheck that we are now not managed by another server (or by the same server, in case we changed the mode to be the same mode). * eglot.el (eglot-shutdown): Use eglot--with-live-buffer (eglot--on-shutdown): Use eglot--with-live-buffer (eglot--managed-mode): Use change-major-mode-hook. (eglot--managed-mode-onoff): Change protocol. Turn off when called with no arguments. (eglot--maybe-activate-editing-mode): Don't do anything if mode is already active. Suitable for calling from after-change-major-mode-hook. (after-change-major-mode-hook): Add eglot--maybe-activate-editing-mode. GitHub-reference: close https://github.com/joaotavora/eglot/issues/44 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8f9a43d93e..49a1f3d496 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -244,7 +244,7 @@ Don't leave this function with the server still running." (ignore-errors (jsonrpc-request server :exit nil :timeout 1))) ;; Turn off `eglot--managed-mode' where appropriate. (dolist (buffer (eglot--managed-buffers server)) - (with-current-buffer buffer (eglot--managed-mode-onoff server -1))) + (eglot--with-live-buffer buffer (eglot--managed-mode-onoff server nil))) ;; Now ask jsonrpc.el to shutdown server (which in normal ;; conditions should return immediately). (jsonrpc-shutdown server))) @@ -253,7 +253,7 @@ Don't leave this function with the server still running." "Called by jsonrpc.el when SERVER is already dead." ;; Turn off `eglot--managed-mode' where appropriate. (dolist (buffer (eglot--managed-buffers server)) - (with-current-buffer buffer (eglot--managed-mode-onoff server -1))) + (eglot--with-live-buffer buffer (eglot--managed-mode-onoff server nil))) ;; Kill any expensive watches (maphash (lambda (_id watches) (mapcar #'file-notify-rm-watch watches)) @@ -691,11 +691,13 @@ If optional MARKERS, make markers." (add-hook 'before-change-functions 'eglot--before-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) + (add-hook 'kill-buffer-hook 'eglot--managed-mode-onoff nil t) (add-hook 'before-revert-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'before-save-hook 'eglot--signal-textDocument/willSave nil t) (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) (add-hook 'xref-backend-functions 'eglot-xref-backend nil t) (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) + (add-hook 'change-major-mode-hook 'eglot--managed-mode-onoff nil t) (add-function :before-until (local 'eldoc-documentation-function) #'eglot-eldoc-function) (add-function :around (local 'imenu-create-index-function) #'eglot-imenu)) @@ -709,6 +711,7 @@ If optional MARKERS, make markers." (remove-hook 'after-save-hook 'eglot--signal-textDocument/didSave t) (remove-hook 'xref-backend-functions 'eglot-xref-backend t) (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) + (remove-hook 'change-major-mode-hook #'eglot--managed-mode-onoff t) (remove-function (local 'eldoc-documentation-function) #'eglot-eldoc-function) (remove-function (local 'imenu-create-index-function) #'eglot-imenu) @@ -718,17 +721,22 @@ If optional MARKERS, make markers." "A cached reference to the current EGLOT server. Reset in `eglot--managed-mode-onoff'.") -(defun eglot--managed-mode-onoff (server arg) - "Proxy for function `eglot--managed-mode' with ARG and SERVER." - (eglot--managed-mode arg) +(defun eglot--managed-mode-onoff (&optional server turn-on) + "Proxy for function `eglot--managed-mode' with TURN-ON and SERVER." (let ((buf (current-buffer))) - (cond (eglot--managed-mode + (cond ((and server turn-on) + (eglot--managed-mode 1) (setq eglot--cached-current-server server) (cl-pushnew buf (eglot--managed-buffers server))) (t - (setq eglot--cached-current-server nil) - (setf (eglot--managed-buffers server) - (delq buf (eglot--managed-buffers server))))))) + (eglot--managed-mode -1) + (let ((server + (or server + eglot--cached-current-server))) + (setq eglot--cached-current-server nil) + (when server + (setf (eglot--managed-buffers server) + (delq buf (eglot--managed-buffers server))))))))) (add-hook 'eglot--managed-mode-hook 'flymake-mode) (add-hook 'eglot--managed-mode-hook 'eldoc-mode) @@ -754,21 +762,24 @@ Reset in `eglot--managed-mode-onoff'.") "Maybe activate mode function `eglot--managed-mode'. If SERVER is supplied, do it only if BUFFER is managed by it. In that case, also signal textDocument/didOpen." - (unless server - (when eglot--cached-current-server - (display-warning - :eglot "`eglot--cached-current-server' is non-nil, but it should be!\n\ + (unless eglot--managed-mode + (unless server + (when eglot--cached-current-server + (display-warning + :eglot "`eglot--cached-current-server' is non-nil, but it shouldn't be!\n\ Please report this as a possible bug.") - (setq eglot--cached-current-server nil))) - ;; Called even when revert-buffer-in-progress-p - (let* ((cur (and buffer-file-name (eglot--current-server))) - (server (or (and (null server) cur) (and server (eq server cur) cur)))) - (when server - (setq eglot--unreported-diagnostics `(:just-opened . nil)) - (eglot--managed-mode-onoff server 1) - (eglot--signal-textDocument/didOpen)))) + (setq eglot--cached-current-server nil))) + ;; Called even when revert-buffer-in-progress-p + (let* ((cur (and buffer-file-name (eglot--current-server))) + (server (or (and (null server) cur) (and server (eq server cur) cur)))) + (when server + (setq eglot--unreported-diagnostics `(:just-opened . nil)) + (eglot--managed-mode-onoff server t) + (eglot--signal-textDocument/didOpen))))) + (add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) +(add-hook 'after-change-major-mode-hook 'eglot--maybe-activate-editing-mode) (defun eglot-clear-status (server) "Clear the last JSONRPC error for SERVER." commit 228ddf368de316667905448dca847bb760cf2d73 Author: João Távora Date: Thu Jul 12 00:30:32 2018 +0100 Implement workspace/didchangeconfiguration () * README.md (Supported Protocol Features, Commands and keybindings): mention workspace/didChangeConfiguration. * eglot.el (eglot-server-initialized-hook): New hook. (eglot--connect): Run it. (eglot-workspace-configuration): New variable. (eglot-signal-didChangeConfiguration): New command. GitHub-reference: close https://github.com/joaotavora/eglot/issues/29 GitHub-reference: close https://github.com/joaotavora/eglot/issues/40 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 77f8260b22..8f9a43d93e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -446,6 +446,11 @@ INTERACTIVE is t if called interactively." (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") +(defvar eglot-server-initialized-hook + '(eglot-signal-didChangeConfiguration) + "Hook run after server is successfully initialized. +Each function is passed the server as an argument") + (defun eglot--connect (managed-major-mode project class contact) "Connect to MANAGED-MAJOR-MODE, PROJECT, CLASS and CONTACT. This docstring appeases checkdoc, that's all." @@ -514,6 +519,7 @@ This docstring appeases checkdoc, that's all." (with-current-buffer buffer (eglot--maybe-activate-editing-mode server))) (jsonrpc-notify server :initialized `(:__dummy__ t)) + (run-hook-with-args 'eglot-server-initialized-hook server) (setf (eglot--inhibit-autoreconnect server) (cond ((booleanp eglot-autoreconnect) (not eglot-autoreconnect)) @@ -1033,6 +1039,22 @@ Records START, END and PRE-CHANGE-LENGTH locally." (eglot--signal-textDocument/didChange)))) '((name . eglot--signal-textDocument/didChange))) +(defvar-local eglot-workspace-configuration () + "Alist of (SETTING . VALUE) entries configuring the LSP server. +Setting should be a keyword, value can be any value that can be +converted to JSON.") + +(defun eglot-signal-didChangeConfiguration (server) + "Send a `:workspace/didChangeConfiguration' signal to SERVER. +When called interactively, use the currently active server" + (interactive (list (eglot--current-server-or-lose))) + (jsonrpc-notify + server :workspace/didChangeConfiguration + (list + :settings + (cl-loop for (k . v) in eglot-workspace-configuration + collect k collect v)))) + (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." (when eglot--recent-changes commit 46de6683a7e827b3fb3d273605b3461075038c6f Author: João Távora Date: Wed Jul 11 09:46:14 2018 +0100 Handle experimental/unknown server methods gracefully * eglot.el (eglot-handle-notification t t, eglot-handle-request t t): Add default handlers for unknown methods. (eglot-handle-notification $cquery/progress) (eglot-handle-notification $cquery/setInactiveRegions) (eglot-handle-notification $cquery/publishSemanticHighlighting): Remove these no-ops. GitHub-reference: close https://github.com/joaotavora/eglot/issues/39 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8d61bbbce3..77f8260b22 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -856,6 +856,16 @@ Uses THING, FACE, DEFS and PREPEND." ;;; Protocol implementation (Requests, notifications, etc) ;;; +(cl-defmethod eglot-handle-notification + (_server method &key) + "Handle unknown notification" + (eglot--warn "Server sent unknown notification method `%s'" method)) + +(cl-defmethod eglot-handle-request + (_server method &key) + "Handle unknown request" + (jsonrpc-error "Unknown request method `%s'" method)) + (cl-defmethod eglot-handle-notification (_server (_method (eql window/showMessage)) &key type message) "Handle notification window/showMessage" @@ -1582,21 +1592,6 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (list :cacheDirectory (file-name-as-directory cache) :progressReportFrequencyMs -1))) -(cl-defmethod eglot-handle-notification - ((_server eglot-cquery) (_method (eql $cquery/progress)) - &rest counts &key _activeThreads &allow-other-keys) - "No-op for noisy $cquery/progress extension") - -(cl-defmethod eglot-handle-notification - ((_server eglot-cquery) (_method (eql $cquery/setInactiveRegions)) - &key _uri _inactiveRegions &allow-other-keys) - "No-op for unsupported $cquery/setInactiveRegions extension") - -(cl-defmethod eglot-handle-notification - ((_server eglot-cquery) (_method (eql $cquery/publishSemanticHighlighting)) - &key _uri _symbols &allow-other-keys) - "No-op for unsupported $cquery/publishSemanticHighlighting extension") - ;; FIXME: A horrible hack of Flymake's insufficient API that must go ;; into Emacs master, or better, 26.2 commit f52846f56f143788b8e235ba24f050e32486fb3f Author: João Távora Date: Mon Jul 9 22:23:57 2018 +0100 Jsonrpc.el is now a gnu elpa depedency * Makefile (ELFILES): Don't include jsonrpc. (jsonrpc-check): Remove target. (check): Don't run jsonrpc-check * README.md (either): Mention "packaged in a single file" again * eglot.el (Package-Requires): Require jsonrpc 1.0.0 (Version): Bump to 1.1 * jsonrpc.el: Remove * jsonrpc-tests.el: Remove diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 26c52d31f3..8d61bbbce3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,12 +2,12 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 1.0 +;; Version: 1.1 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.0")) ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by commit 03caf3bb27603d2c75269ed6f2c5db85da115cc2 Author: João Távora Date: Mon Jul 9 00:33:50 2018 +0100 * eglot.el (eglot-completion-at-point): fix broken indentation diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 396590f6fd..26c52d31f3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1216,10 +1216,10 @@ DUMMY is ignored." (items (if (vectorp resp) resp (plist-get resp :items)))) (mapcar (jsonrpc-lambda (&rest all &key label insertText &allow-other-keys) - (let ((insert (or insertText label))) - (add-text-properties 0 1 all insert) - (put-text-property 0 1 'eglot--lsp-completion all insert) - insert)) + (let ((insert (or insertText label))) + (add-text-properties 0 1 all insert) + (put-text-property 0 1 'eglot--lsp-completion all insert) + insert)) items)))) :annotation-function (lambda (obj) commit 6aeaf37c9b54abfba72eeca853255f16194accad Author: Ricardo Martins Date: Sun Jul 8 17:02:32 2018 +0100 Format documentation in completion annotations Fixes an issue with the latest RLS, where the server returns a plist instead of a plain string as documentation for completion candidates, which broke the `annotation-function`. This change was introduced by https://github.com/rust-lang-nursery/rls/commit/206a9fb41e837333d0e67187a6a9fe24868b77a4 Copyright-paperwork-exempt: yes * eglot.el (eglot-completion-at-point): Use eglot--format-markup diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fd63338845..396590f6fd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1216,10 +1216,10 @@ DUMMY is ignored." (items (if (vectorp resp) resp (plist-get resp :items)))) (mapcar (jsonrpc-lambda (&rest all &key label insertText &allow-other-keys) - (let ((insert (or insertText label))) - (add-text-properties 0 1 all insert) - (put-text-property 0 1 'eglot--lsp-completion all insert) - insert)) + (let ((insert (or insertText label))) + (add-text-properties 0 1 all insert) + (put-text-property 0 1 'eglot--lsp-completion all insert) + insert)) items)))) :annotation-function (lambda (obj) @@ -1227,7 +1227,8 @@ DUMMY is ignored." (text-properties-at 0 obj) (let ((annotation (or (and documentation - (replace-regexp-in-string "\n.*" "" documentation)) + (replace-regexp-in-string + "\n.*" "" (eglot--format-markup documentation))) detail (cdr (assoc kind eglot--kind-names))))) (when annotation commit d8a14e9ea84a7636f230ed4a9870b064f48d98fc Author: João Távora Date: Fri Jul 6 18:39:20 2018 +0100 Unbreak completion when no possible annotation * eglot.el (eglot-completion-at-point): Handle case where no doc, detail or kind. GitHub-reference: close https://github.com/joaotavora/eglot/issues/37 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 509fe6c88d..fd63338845 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1225,11 +1225,14 @@ DUMMY is ignored." (lambda (obj) (cl-destructuring-bind (&key detail documentation kind &allow-other-keys) (text-properties-at 0 obj) - (concat " " (propertize - (or (and documentation - (replace-regexp-in-string "\n.*" "" documentation)) - detail (cdr (assoc kind eglot--kind-names))) - 'face 'font-lock-function-name-face)))) + (let ((annotation + (or (and documentation + (replace-regexp-in-string "\n.*" "" documentation)) + detail + (cdr (assoc kind eglot--kind-names))))) + (when annotation + (concat " " (propertize annotation + 'face 'font-lock-function-name-face)))))) :display-sort-function (lambda (items) (sort items (lambda (a b) commit d599dfd79f17eea5e4a9708dd29336149ac386ac Author: João Távora Date: Mon Jul 2 23:09:27 2018 +0100 Handle outrageously large and buggy line numbers * eglot.el (eglot--lsp-position-to-point): Truncate line number to most-positive-fixnum. GitHub-reference: close https://github.com/joaotavora/eglot/issues/34 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8cad3ad1b5..509fe6c88d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -602,7 +602,8 @@ CONNECT-ARGS are passed as additional arguments to "Convert LSP position POS-PLIST to Emacs point. If optional MARKER, return a marker instead" (save-excursion (goto-char (point-min)) - (forward-line (plist-get pos-plist :line)) + (forward-line (min most-positive-fixnum + (plist-get pos-plist :line))) (forward-char (min (plist-get pos-plist :character) (- (line-end-position) (line-beginning-position)))) commit 1a58481719f63c3e77832f7c8474ce745a05ad4c Author: João Távora Date: Sun Jul 1 22:49:40 2018 +0100 Inhibit auto-reconnect until connection is established Otherwise, a server that crashes on startup is enough to throw Eglot into a reconnection infloop. * eglot.el (eglot-lsp-server): Initialize "inhibit-autoreconnect" slot to t. GitHub-reference: close https://github.com/joaotavora/eglot/issues/36 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 50733f7458..8cad3ad1b5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -206,6 +206,7 @@ lasted more than that many seconds." :documentation "List (ID DOING-WHAT DONE-P) representing server progress." :initform `(nil nil t) :accessor eglot--spinner) (inhibit-autoreconnect + :initform t :documentation "Generalized boolean inhibiting auto-reconnection if true." :accessor eglot--inhibit-autoreconnect) (file-watches commit 2b071ccba5c4f88fd0a66c505110d5081d695631 Author: João Távora Date: Thu Jun 28 23:30:39 2018 +0100 Bind default-directory when launching servers Apparently, not doing so trips some servers, like Scala's. * eglot.el (eglot--connect): Bind default-directory. GitHub-reference: close https://github.com/joaotavora/eglot/issues/33 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 01658f9343..50733f7458 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -448,8 +448,8 @@ INTERACTIVE is t if called interactively." (defun eglot--connect (managed-major-mode project class contact) "Connect to MANAGED-MAJOR-MODE, PROJECT, CLASS and CONTACT. This docstring appeases checkdoc, that's all." - (let* ((nickname (file-name-base (directory-file-name - (car (project-roots project))))) + (let* ((default-directory (car (project-roots project))) + (nickname (file-name-base (directory-file-name default-directory))) (readable-name (format "EGLOT (%s/%s)" nickname managed-major-mode)) autostart-inferior-process (initargs @@ -504,10 +504,8 @@ This docstring appeases checkdoc, that's all." :initialize (list :processId (unless (eq (jsonrpc-process-type server) 'network) (emacs-pid)) - :rootPath (expand-file-name - (car (project-roots project))) - :rootUri (eglot--path-to-uri - (car (project-roots project))) + :rootPath (expand-file-name default-directory) + :rootUri (eglot--path-to-uri default-directory) :initializationOptions (eglot-initialization-options server) :capabilities (eglot-client-capabilities server))) (setf (eglot--capabilities server) capabilities) commit 973b0255229f0d5fa9aaf141743584c969293d05 Author: João Távora Date: Mon Jun 25 17:37:43 2018 +0100 Cache buffer's managing server * eglot.el (eglot--cached-current-server): New variable. (eglot--managed-mode-onoff): Set it. (eglot--current-server): Read it. (eglot--maybe-activate-editing-mode): Add assertion. GitHub-reference: close https://github.com/joaotavora/eglot/issues/32 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 88746a7282..01658f9343 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -708,23 +708,33 @@ If optional MARKERS, make markers." (remove-function (local 'imenu-create-index-function) #'eglot-imenu) (setq eglot--current-flymake-report-fn nil)))) +(defvar-local eglot--cached-current-server nil + "A cached reference to the current EGLOT server. +Reset in `eglot--managed-mode-onoff'.") + (defun eglot--managed-mode-onoff (server arg) "Proxy for function `eglot--managed-mode' with ARG and SERVER." (eglot--managed-mode arg) (let ((buf (current-buffer))) - (if eglot--managed-mode - (cl-pushnew buf (eglot--managed-buffers server)) - (setf (eglot--managed-buffers server) - (delq buf (eglot--managed-buffers server)))))) + (cond (eglot--managed-mode + (setq eglot--cached-current-server server) + (cl-pushnew buf (eglot--managed-buffers server))) + (t + (setq eglot--cached-current-server nil) + (setf (eglot--managed-buffers server) + (delq buf (eglot--managed-buffers server))))))) (add-hook 'eglot--managed-mode-hook 'flymake-mode) (add-hook 'eglot--managed-mode-hook 'eldoc-mode) (defun eglot--current-server () "Find the current logical EGLOT server." - (let* ((probe (or (project-current) `(transient . ,default-directory)))) - (cl-find major-mode (gethash probe eglot--servers-by-project) - :key #'eglot--major-mode))) + (or + eglot--cached-current-server + (let* ((probe (or (project-current) + `(transient . ,default-directory)))) + (cl-find major-mode (gethash probe eglot--servers-by-project) + :key #'eglot--major-mode)))) (defun eglot--current-server-or-lose () "Return current logical EGLOT server connection or error." @@ -738,6 +748,12 @@ If optional MARKERS, make markers." "Maybe activate mode function `eglot--managed-mode'. If SERVER is supplied, do it only if BUFFER is managed by it. In that case, also signal textDocument/didOpen." + (unless server + (when eglot--cached-current-server + (display-warning + :eglot "`eglot--cached-current-server' is non-nil, but it should be!\n\ +Please report this as a possible bug.") + (setq eglot--cached-current-server nil))) ;; Called even when revert-buffer-in-progress-p (let* ((cur (and buffer-file-name (eglot--current-server))) (server (or (and (null server) cur) (and server (eq server cur) cur)))) commit 9b3ef1315ce3d0cb3762a92158a7241af31dd6cc Author: João Távora Date: Mon Jun 25 13:12:55 2018 +0100 Unbreak imenu for cquery servers (and probably more) * eglot.el (eglot-imenu): Don't try to make a group for symbols without kind. GitHub-reference: close https://github.com/joaotavora/eglot/issues/31 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f8574e9428..88746a7282 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1349,8 +1349,10 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." :textDocument/documentSymbol `(:textDocument ,(eglot--TextDocumentIdentifier)))))) (append - (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) - entries) + (cl-remove nil + (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) + entries) + :key #'car) entries)) (funcall oldfun))) commit 5b66bec822823636e09dfef74919c733ded9f1e5 Author: João Távora Date: Mon Jun 25 13:02:03 2018 +0100 Unbreak basic imenu functionality * eglot.el (eglot--managed-mode): Add missing quote to imenu-create-index-function. GitHub-reference: per https://github.com/joaotavora/eglot/issues/31 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ce33543351..f8574e9428 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -692,7 +692,7 @@ If optional MARKERS, make markers." (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) (add-function :before-until (local 'eldoc-documentation-function) #'eglot-eldoc-function) - (add-function :around (local imenu-create-index-function) #'eglot-imenu)) + (add-function :around (local 'imenu-create-index-function) #'eglot-imenu)) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) @@ -705,7 +705,7 @@ If optional MARKERS, make markers." (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) (remove-function (local 'eldoc-documentation-function) #'eglot-eldoc-function) - (remove-function (local imenu-create-index-function) #'eglot-imenu) + (remove-function (local 'imenu-create-index-function) #'eglot-imenu) (setq eglot--current-flymake-report-fn nil)))) (defun eglot--managed-mode-onoff (server arg) commit d40cbb99a5f859474b54f7d8d2fed05acc5e3ef1 Author: Ricardo Martins <1706+meqif@users.noreply.github.com> Date: Mon Jun 25 12:51:10 2018 +0100 Fix typo in the solargraph server program Copyright-paperwork-exempt: yes * eglot.el (eglot-server-programs): Fix typo. GitHub-reference: close https://github.com/joaotavora/eglot/issues/30 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 430a016461..ce33543351 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -86,7 +86,7 @@ ((c++-mode c-mode) . (eglot-cquery "cquery")) (ruby-mode - . ("solagraph" "socket" "--port" + . ("solargraph" "socket" "--port" :autoport)) (php-mode . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php"))) commit 5556a341ed02cccf20742dce32d65b5e9a0a1e92 Author: João Távora Date: Sat Jun 23 17:43:23 2018 +0100 Fix some rather silly bugs in some interactive specs * eglot.el (eglot-events-buffer, eglot-stderr-buffer) (eglot-forget-pending-continuations): Fix interactive specs. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6d0541a1e6..430a016461 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -430,17 +430,17 @@ INTERACTIVE is t if called interactively." (defun eglot-events-buffer (server) "Display events buffer for SERVER." - (interactive (eglot--current-server-or-lose)) + (interactive (list (eglot--current-server-or-lose))) (display-buffer (jsonrpc-events-buffer server))) (defun eglot-stderr-buffer (server) "Display stderr buffer for SERVER." - (interactive (eglot--current-server-or-lose)) + (interactive (list (eglot--current-server-or-lose))) (display-buffer (jsonrpc-stderr-buffer server))) (defun eglot-forget-pending-continuations (server) "Forget pending requests for SERVER." - (interactive (eglot--current-server-or-lose)) + (interactive (list (eglot--current-server-or-lose))) (jsonrpc-forget-pending-continuations server)) (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") commit 0c61c1b4a9f8dc358209ed2b9844b813a36215a6 Author: João Távora Date: Sat Jun 23 17:00:57 2018 +0100 Implement tcp autostart/autoconnect (and support ruby's solargraph) * README.md (Installation and usage): Mention support for Solargraph (Connecting via TCP): New section (Connecting automatically): New section * eglot.el (eglot-server-programs): Add ruby-mode. Overhaul docstring. (eglot-lsp-server): Add inferior-process slot. (eglot--on-shutdown): Kill any autostarted inferior-process (eglot--guess-contact): Allow prompting with :autoport parameter. (eglot--connect): Consider :autoport case. (eglot--inferior-bootstrap): New helper. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7a4468d2c3..6d0541a1e6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -85,32 +85,42 @@ (sh-mode . ("bash-language-server" "start")) ((c++-mode c-mode) . (eglot-cquery "cquery")) + (ruby-mode + . ("solagraph" "socket" "--port" + :autoport)) (php-mode . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated -CONTACT specifies how to start a server for managing buffers of -those modes. CONTACT can be: +CONTACT specifies how to connect to a server for managing buffers +of those modes. CONTACT can be: * In the most common case, a list of strings (PROGRAM [ARGS...]). -PROGRAM is called with ARGS and is expected to serve LSP requests -over the standard input/output channels. + PROGRAM is called with ARGS and is expected to serve LSP requests + over the standard input/output channels. -* A list (HOST PORT [ARGS...]) where HOST is a string and PORT is -a positive integer number for connecting to a server via TCP. -Remaining ARGS are passed to `open-network-stream' for upgrading -the connection with encryption or other capabilities. +* A list (HOST PORT [TCP-ARGS...]) where HOST is a string and PORT is + na positive integer number for connecting to a server via TCP. + Remaining ARGS are passed to `open-network-stream' for + upgrading the connection with encryption or other capabilities. + +* A list (PROGRAM [ARGS...] :autoport [MOREARGS...]), whereby a + combination of the two previous options is used.. First, an + attempt is made to find an available server port, then PROGRAM + is launched with ARGS; the `:autoport' keyword substituted for + that number; and MOREARGS. Eglot then attempts to to establish + a TCP connection to that port number on the localhost. * A cons (CLASS-NAME . INITARGS) where CLASS-NAME is a symbol -designating a subclass of `eglot-lsp-server', for representing -experimental LSP servers. INITARGS is a keyword-value plist used -to initialize CLASS-NAME, or a plain list interpreted as the -previous descriptions of CONTACT, in which case it is converted -to produce a plist with a suitable :PROCESS initarg to -CLASS-NAME. The class `eglot-lsp-server' descends -`jsonrpc-process-connection', which you should see for semantics -of the mandatory :PROCESS argument.") + designating a subclass of `eglot-lsp-server', for representing + experimental LSP servers. INITARGS is a keyword-value plist + used to initialize CLASS-NAME, or a plain list interpreted as + the previous descriptions of CONTACT, in which case it is + converted to produce a plist with a suitable :PROCESS initarg + to CLASS-NAME. The class `eglot-lsp-server' descends + `jsonrpc-process-connection', which you should see for the + semantics of the mandatory :PROCESS argument.") (defface eglot-mode-line '((t (:inherit font-lock-constant-face :weight bold))) @@ -205,8 +215,11 @@ lasted more than that many seconds." :documentation "List of buffers managed by server." :accessor eglot--managed-buffers) (saved-initargs - :documentation "Saved initargs for reconnection purposes" - :accessor eglot--saved-initargs)) + :documentation "Saved initargs for reconnection purposes." + :accessor eglot--saved-initargs) + (inferior-process + :documentation "Server subprocess started automatically." + :accessor eglot--inferior-process)) :documentation "Represents a server. Wraps a process for LSP communication.") @@ -244,6 +257,9 @@ Don't leave this function with the server still running." (maphash (lambda (_id watches) (mapcar #'file-notify-rm-watch watches)) (eglot--file-watches server)) + ;; Kill any autostarted inferior processes + (when-let (proc (eglot--inferior-process server)) + (delete-process proc)) ;; Sever the project/server relationship for `server' (setf (gethash (eglot--project server) eglot--servers-by-project) (delq server @@ -297,7 +313,11 @@ be guessed." (program (and (listp guess) (stringp (car guess)) (car guess))) (base-prompt (and interactive - "[eglot] Enter program to execute (or :): ")) + "Enter program to execute (or :): ")) + (program-guess + (and program + (combine-and-quote-strings (cl-subst ":autoport:" + :autoport guess)))) (prompt (and base-prompt (cond (current-prefix-arg base-prompt) @@ -306,20 +326,22 @@ be guessed." managed-mode base-prompt)) ((and program (not (executable-find program))) (concat (format "[eglot] I guess you want to run `%s'" - (combine-and-quote-strings guess)) + program-guess) (format ", but I can't find `%s' in PATH!" program) "\n" base-prompt))))) (contact (or (and prompt (let ((s (read-shell-command prompt - (if program (combine-and-quote-strings guess)) + program-guess 'eglot-command-history))) (if (string-match "^\\([^\s\t]+\\):\\([[:digit:]]+\\)$" (string-trim s)) (list (match-string 1 s) (string-to-number (match-string 2 s))) - (split-string-and-unquote s)))) + (cl-subst + :autoport ":autoport:" (split-string-and-unquote s) + :test #'equal)))) guess (eglot--error "Couldn't guess for `%s'!" managed-mode)))) (list managed-mode project class contact))) @@ -429,6 +451,7 @@ This docstring appeases checkdoc, that's all." (let* ((nickname (file-name-base (directory-file-name (car (project-roots project))))) (readable-name (format "EGLOT (%s/%s)" nickname managed-major-mode)) + autostart-inferior-process (initargs (cond ((keywordp (car contact)) contact) ((integerp (cadr contact)) @@ -437,6 +460,14 @@ This docstring appeases checkdoc, that's all." readable-name nil (car contact) (cadr contact) (cddr contact))))) + ((and (stringp (car contact)) (memq :autoport contact)) + `(:process ,(lambda () + (pcase-let ((`(,connection . ,inferior) + (eglot--inferior-bootstrap + readable-name + contact))) + (setq autostart-inferior-process inferior) + connection)))) ((stringp (car contact)) `(:process ,(lambda () (make-process @@ -463,6 +494,7 @@ This docstring appeases checkdoc, that's all." (setf (eglot--project server) project) (setf (eglot--project-nickname server) nickname) (setf (eglot--major-mode server) managed-major-mode) + (setf (eglot--inferior-process server) autostart-inferior-process) (push server (gethash project eglot--servers-by-project)) (run-hook-with-args 'eglot-connect-hook server) (unwind-protect @@ -495,6 +527,54 @@ This docstring appeases checkdoc, that's all." (when (and (not success) (jsonrpc-running-p server)) (eglot-shutdown server))))) +(defun eglot--inferior-bootstrap (name contact &optional connect-args) + "Use CONTACT to start a server, then connect to it. +Return a cons of two process objects (CONNECTION . INFERIOR). +Name both based on NAME. +CONNECT-ARGS are passed as additional arguments to +`open-network-stream'." + (let* ((port-probe (make-network-process :name "eglot-port-probe-dummy" + :server t + :host "localhost" + :service 0)) + (port-number (unwind-protect + (process-contact port-probe :service) + (delete-process port-probe))) + inferior connection) + (unwind-protect + (progn + (setq inferior + (make-process + :name (format "autostart-inferior-%s" name) + :stderr (format "*%s stderr*" name) + :command (cl-subst + (format "%s" port-number) :autoport contact))) + (setq connection + (cl-loop + repeat 10 for i from 1 + do (accept-process-output nil 0.5) + while (process-live-p inferior) + do (eglot--message + "Trying to connect to localhost and port %s (attempt %s)" + port-number i) + thereis (ignore-errors + (apply #'open-network-stream + (format "autoconnect-%s" name) + nil + "localhost" port-number connect-args)))) + (cons connection inferior)) + (cond ((and (process-live-p connection) + (process-live-p inferior)) + (eglot--message "Done, connected to %s!" port-number)) + (t + (when inferior (delete-process inferior)) + (when connection (delete-process connection)) + (eglot--error "Could not start and connect to server%s" + (if inferior + (format " started with %s" + (process-command inferior)) + "!"))))))) + ;;; Helpers (move these to API?) ;;; commit 55ed673ae7054fb495688946f94abd9754d94a21 Merge: d370eeb128 c45e4a916a Author: João Távora Date: Fri Jun 22 16:53:53 2018 +0100 Merge branch 'jsonrpc-refactor', bump version to 1.0 * eglot.el (Version): Bump to 1.0 commit d370eeb128ab4f2a3bf0e17c9bd7a19be74fb11a Author: João Távora Date: Fri Jun 22 16:51:42 2018 +0100 * eglot.el (version): bump to 0.11 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f4a03da7e6..62116d3320 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 0.10 +;; Version: 0.11 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot commit c45e4a916a3978e0a6c614e3f5420e592ae86658 Merge: 97c17252e3 7826b265a0 Author: João Távora Date: Fri Jun 22 16:44:09 2018 +0100 Merge master into jsonrpc-refactor (using imerge) commit 7826b265a0ecd9357719b2fb9491c9bcb517d4cc Author: João Távora Date: Thu Jun 21 23:32:14 2018 +0100 Empty ranges are valid in lsp The previous hack in eglot--range-region, designed to appease cquery's occasional practice of publishing diagnostics with empty regions, was moved to the proper notification handler. Reported by mkcms . * eglot.el (eglot--range-region): Allow empty ranges, which are allowed in LSP. (eglot-handle-notification :textDocument/publishDiagnostics): Maybe fallback to flymake-diag-region here. GitHub-reference: close https://github.com/joaotavora/eglot/issues/27 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2fdf433c55..f4a03da7e6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -896,10 +896,7 @@ If optional MARKERS, make markers." (let* ((st (plist-get range :start)) (beg (eglot--lsp-position-to-point st markers)) (end (eglot--lsp-position-to-point (plist-get range :end) markers))) - ;; Fallback to `flymake-diag-region' if server botched the range - (if (/= beg end) (cons beg end) (flymake-diag-region - (current-buffer) (plist-get st :line) - (1- (plist-get st :character)))))) + (cons beg end))) ;;; Minor modes @@ -1125,7 +1122,18 @@ Don't leave this function with the server still running." _code source message) diag-spec (setq message (concat source ": " message)) - (pcase-let ((`(,beg . ,end) (eglot--range-region range))) + (pcase-let + ((`(,beg . ,end) (eglot--range-region range))) + ;; Fallback to `flymake-diag-region' if server + ;; botched the range + (if (= beg end) + (let* ((st (plist-get range :start)) + (diag-region + (flymake-diag-region + (current-buffer) (plist-get st :line) + (1- (plist-get st :character))))) + (setq beg (car diag-region) + end (cdr diag-region)))) (eglot--make-diag (current-buffer) beg end (cond ((<= sev 1) 'eglot-error) ((= sev 2) 'eglot-warning) commit 81b7c8e7d736d4118cb52afcaf6a46a55646fabf Author: João Távora Date: Thu Jun 21 17:48:10 2018 +0100 Apply text edits as a single undoable edit As suggested by mkcms , but do it in eglot--apply-text-edits, where it benefits all its users. Also, just using undo-boundary is not enough, one needs undo-amalgamate-change-group to mess with the boundaries already in buffer-undo-list. * eglot.el (eglot--apply-text-edits): Use undo-amalgamate-change-group. GitHub-reference: close https://github.com/joaotavora/eglot/issues/22 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9e686eece6..2fdf433c55 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1584,7 +1584,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (eglot--error "Edits on `%s' require version %d, you have %d" (current-buffer) version eglot--versioned-identifier)) (atomic-change-group - (let* ((howmany (length edits)) + (let* ((change-group (prepare-change-group)) + (howmany (length edits)) (reporter (make-progress-reporter (format "[eglot] applying %s edits to `%s'..." howmany (current-buffer)) @@ -1604,6 +1605,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (mapcar (eglot--lambda (&key range newText) (cons newText (eglot--range-region range 'markers))) edits)) + (undo-amalgamate-change-group change-group) (progress-reporter-done reporter)))) (defun eglot--apply-workspace-edit (wedit &optional confirm) commit 1e49d2f3b45be49278f13a4b9fbb5dab2707952a Author: João Távora Date: Thu Jun 21 17:20:20 2018 +0100 Report progress when applying edits Use make-progress-reporter in eglot--apply-text-edits As suggested by mkcms , but do it in eglot--apply-text-edits, where it benefits all its users. * eglot.el (eglot--apply-text-edits): Use a progress reporter. Fix marker point recovery. GitHub-reference: close https://github.com/joaotavora/eglot/issues/23 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 489e3658b1..9e686eece6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1584,18 +1584,27 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (eglot--error "Edits on `%s' require version %d, you have %d" (current-buffer) version eglot--versioned-identifier)) (atomic-change-group - (mapc (pcase-lambda (`(,newText ,beg . ,end)) - (save-restriction - (narrow-to-region beg end) + (let* ((howmany (length edits)) + (reporter (make-progress-reporter + (format "[eglot] applying %s edits to `%s'..." + howmany (current-buffer)) + 0 howmany)) + (done 0)) + (mapc (pcase-lambda (`(,newText ,beg . ,end)) (let ((source (current-buffer))) (with-temp-buffer (insert newText) (let ((temp (current-buffer))) - (with-current-buffer source (replace-buffer-contents temp))))))) - (mapcar (eglot--lambda (&key range newText) - (cons newText (eglot--range-region range 'markers))) - edits))) - (eglot--message "%s: Performed %s edits" (current-buffer) (length edits))) + (with-current-buffer source + (save-excursion + (save-restriction + (narrow-to-region beg end) + (replace-buffer-contents temp))) + (progress-reporter-update reporter (cl-incf done))))))) + (mapcar (eglot--lambda (&key range newText) + (cons newText (eglot--range-region range 'markers))) + edits)) + (progress-reporter-done reporter)))) (defun eglot--apply-workspace-edit (wedit &optional confirm) "Apply the workspace edit WEDIT. If CONFIRM, ask user first." commit 31b21e371da5676fbdac4a3f352eceb657a62180 Author: João Távora Date: Thu Jun 21 14:53:50 2018 +0100 Apply text edits atomically As suggested by mkcms , but do it in eglot--apply-text-edits, where it benefits all its users. * eglot.el (eglot--apply-text-edits): Use atomic-change-group. GitHub-reference: per https://github.com/joaotavora/eglot/issues/22 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e96a3e68e2..489e3658b1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1583,17 +1583,18 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (unless (or (not version) (equal version eglot--versioned-identifier)) (eglot--error "Edits on `%s' require version %d, you have %d" (current-buffer) version eglot--versioned-identifier)) - (mapc (pcase-lambda (`(,newText ,beg . ,end)) - (save-restriction - (narrow-to-region beg end) - (let ((source (current-buffer))) - (with-temp-buffer - (insert newText) - (let ((temp (current-buffer))) - (with-current-buffer source (replace-buffer-contents temp))))))) - (mapcar (eglot--lambda (&key range newText) - (cons newText (eglot--range-region range 'markers))) - edits)) + (atomic-change-group + (mapc (pcase-lambda (`(,newText ,beg . ,end)) + (save-restriction + (narrow-to-region beg end) + (let ((source (current-buffer))) + (with-temp-buffer + (insert newText) + (let ((temp (current-buffer))) + (with-current-buffer source (replace-buffer-contents temp))))))) + (mapcar (eglot--lambda (&key range newText) + (cons newText (eglot--range-region range 'markers))) + edits))) (eglot--message "%s: Performed %s edits" (current-buffer) (length edits))) (defun eglot--apply-workspace-edit (wedit &optional confirm) commit 6fc53b840d22c08ca444efd4bf693a25e2bb26e4 Author: João Távora Date: Thu Jun 21 13:58:48 2018 +0100 Defer textdocument/formatting requests * eglot.el (eglot-format-buffer): Pass DEFERRED to eglot--request. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0f366c41a7..e96a3e68e2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1410,7 +1410,8 @@ DUMMY is ignored." (list :textDocument (eglot--TextDocumentIdentifier) :options (list :tabSize tab-width :insertSpaces - (if indent-tabs-mode :json-false t)))))) + (if indent-tabs-mode :json-false t))) + :textDocument/formatting))) (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." commit 4f346ba25036ca5525ab298aa34a3bcb4f4807f1 Author: João Távora Date: Wed Jun 20 19:29:30 2018 +0100 Simplify eglot-format-buffer Use replace-buffer-contents, as suggested by mkcms , but do it in eglot--apply-text-edits, where it benefits all its users. * README.md (Commands and keybindings): Mention eglot-format-buffer. * eglot.el (eglot-format-buffer): Don't try to heuristically preserve point here. (eglot--apply-text-edits): Use replace-buffer-contents. * eglot-tests.el (formatting): adjust test to strictly check for point position. GitHub-reference: per https://github.com/joaotavora/eglot/issues/22 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 447e8c17f2..0f366c41a7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1403,27 +1403,14 @@ DUMMY is ignored." (interactive) (unless (eglot--server-capable :documentFormattingProvider) (eglot--error "Server can't format!")) - (let* ((server (eglot--current-server-or-lose)) - (resp - (eglot--request - server - :textDocument/formatting - (list :textDocument (eglot--TextDocumentIdentifier) - :options (list - :tabSize tab-width - :insertSpaces (not indent-tabs-mode))) - :textDocument/formatting)) - (after-point - (buffer-substring (point) (min (+ (point) 60) (point-max)))) - (regexp (and (not (bobp)) - (replace-regexp-in-string - "[\s\t\n\r]+" "[\s\t\n\r]+" - (concat "\\(" (regexp-quote after-point) "\\)"))))) - (when resp - (save-excursion - (eglot--apply-text-edits resp)) - (when (and (bobp) regexp (search-forward-regexp regexp nil t)) - (goto-char (match-beginning 1)))))) + (eglot--apply-text-edits + (eglot--request + (eglot--current-server-or-lose) + :textDocument/formatting + (list :textDocument (eglot--TextDocumentIdentifier) + :options (list :tabSize tab-width + :insertSpaces + (if indent-tabs-mode :json-false t)))))) (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." @@ -1595,12 +1582,17 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (unless (or (not version) (equal version eglot--versioned-identifier)) (eglot--error "Edits on `%s' require version %d, you have %d" (current-buffer) version eglot--versioned-identifier)) - (eglot--widening - (mapc (pcase-lambda (`(,newText ,beg . ,end)) - (goto-char beg) (delete-region beg end) (insert newText)) - (mapcar (eglot--lambda (&key range newText) - (cons newText (eglot--range-region range 'markers))) - edits))) + (mapc (pcase-lambda (`(,newText ,beg . ,end)) + (save-restriction + (narrow-to-region beg end) + (let ((source (current-buffer))) + (with-temp-buffer + (insert newText) + (let ((temp (current-buffer))) + (with-current-buffer source (replace-buffer-contents temp))))))) + (mapcar (eglot--lambda (&key range newText) + (cons newText (eglot--range-region range 'markers))) + edits)) (eglot--message "%s: Performed %s edits" (current-buffer) (length edits))) (defun eglot--apply-workspace-edit (wedit &optional confirm) commit 29b44f7ec1dc6a094e751ec0af6fb950c7858a71 Author: João Távora Date: Wed Jun 20 19:05:25 2018 +0100 Guess server for js2-mode and rjsx-mode * eglot.el (eglot-server-programs): Add entries for js2-mode and rjsx-mode. Coalesce entries for c++ and c-mode. Improve docstring. (eglot--guess-contact): Allow lists are keys in eglot-server-programs. GitHub-reference: close https://github.com/joaotavora/eglot/issues/26 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ecbd509b00..447e8c17f2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -78,15 +78,19 @@ (defvar eglot-server-programs '((rust-mode . (eglot-rls "rls")) (python-mode . ("pyls")) - (js-mode . ("javascript-typescript-stdio")) + ((js-mode + js2-mode + rjsx-mode) . ("javascript-typescript-stdio")) (sh-mode . ("bash-language-server" "start")) - (c++-mode . (eglot-cquery "cquery")) - (c-mode . (eglot-cquery "cquery")) + ((c++-mode + c-mode) . (eglot-cquery "cquery")) (php-mode . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . SPEC) pair. MAJOR-MODE is a -mode symbol. SPEC is +mode symbol, or a list of mode symbols. The associated SPEC +specifies how to start a server for managing buffers of those +modes. SPEC can be: * In the most common case, a list of strings (PROGRAM [ARGS...]). PROGRAM is called with ARGS and is expected to serve LSP requests @@ -359,7 +363,10 @@ be guessed." (eglot--error "Can't guess mode to manage for `%s'" (current-buffer))) (t guessed-mode))) (project (or (project-current) `(transient . ,default-directory))) - (guess (cdr (assoc managed-mode eglot-server-programs))) + (guess (cdr (assoc managed-mode eglot-server-programs + (lambda (m1 m2) + (or (eq m1 m2) + (and (listp m1) (memq m2 m1))))))) (class (or (and (consp guess) (symbolp (car guess)) (prog1 (car guess) (setq guess (cdr guess)))) 'eglot-lsp-server)) commit 30d3874723f7f56615a32d44ca2e19870f783bb5 Author: João Távora Date: Wed Jun 20 18:47:45 2018 +0100 Improve eglot-ensure and mention it in readme.md * README.md (Installation and Usage): Mention eglot-ensure. * eglot.el (eglot-ensure): No-op for non-file buffers. (eglot--connect): Don't fallback to 'eglot-lsp-server here. (eglot--guess-contact): Error if something can't be guessed. GitHub-reference: close https://github.com/joaotavora/eglot/issues/25 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1aa3661d4d..ecbd509b00 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -299,7 +299,7 @@ class SERVER-CLASS." server connect-success) (setq server (make-instance - (or server-class 'eglot-lsp-server) + server-class :process proc :major-mode managed-major-mode :project project :contact contact :name name :project-nickname nickname @@ -341,23 +341,28 @@ class SERVER-CLASS." (defun eglot--guess-contact (&optional interactive) "Helper for `eglot'. -Return (MANAGED-MODE PROJECT CONTACT CLASS). -If INTERACTIVE, maybe prompt user." +Return (MANAGED-MODE PROJECT CLASS CONTACT). If INTERACTIVE is +non-nil, maybe prompt user, else error as soon as something can't +be guessed." (let* ((guessed-mode (if buffer-file-name major-mode)) (managed-mode (cond - ((or (>= (prefix-numeric-value current-prefix-arg) 16) - (not guessed-mode)) + ((and interactive + (or (>= (prefix-numeric-value current-prefix-arg) 16) + (not guessed-mode))) (intern (completing-read "[eglot] Start a server to manage buffers of what major mode? " (mapcar #'symbol-name (eglot--all-major-modes)) nil t (symbol-name guessed-mode) nil (symbol-name guessed-mode) nil))) + ((not guessed-mode) + (eglot--error "Can't guess mode to manage for `%s'" (current-buffer))) (t guessed-mode))) (project (or (project-current) `(transient . ,default-directory))) (guess (cdr (assoc managed-mode eglot-server-programs))) - (class (and (consp guess) (symbolp (car guess)) - (prog1 (car guess) (setq guess (cdr guess))))) + (class (or (and (consp guess) (symbolp (car guess)) + (prog1 (car guess) (setq guess (cdr guess)))) + 'eglot-lsp-server)) (program (and (listp guess) (stringp (car guess)) (car guess))) (base-prompt (and interactive @@ -374,16 +379,18 @@ If INTERACTIVE, maybe prompt user." (format ", but I can't find `%s' in PATH!" program) "\n" base-prompt))))) (contact - (if prompt - (let ((s (read-shell-command - prompt - (if program (combine-and-quote-strings guess)) - 'eglot-command-history))) - (if (string-match "^\\([^\s\t]+\\):\\([[:digit:]]+\\)$" - (string-trim s)) - (list (match-string 1 s) (string-to-number (match-string 2 s))) - (split-string-and-unquote s))) - guess))) + (or (and prompt + (let ((s (read-shell-command + prompt + (if program (combine-and-quote-strings guess)) + 'eglot-command-history))) + (if (string-match "^\\([^\s\t]+\\):\\([[:digit:]]+\\)$" + (string-trim s)) + (list (match-string 1 s) + (string-to-number (match-string 2 s))) + (split-string-and-unquote s)))) + guess + (eglot--error "Couldn't guess for `%s'!" managed-mode)))) (list managed-mode project class contact))) ;;;###autoload @@ -470,7 +477,8 @@ INTERACTIVE is t if called interactively." (eglot--name server) major-mode (eglot--project-nickname server))))))) - (add-hook 'post-command-hook #'maybe-connect 'append nil)))) + (when buffer-file-name + (add-hook 'post-command-hook #'maybe-connect 'append nil))))) (defun eglot--process-sentinel (proc change) "Called when PROC undergoes CHANGE." commit 8bb92096ef7cd22b3651ae67b8e737a0d6e718a1 Author: Rami Chowdhury <460769+necaris@users.noreply.github.com> Date: Sat Jun 16 18:59:57 2018 -0400 Use gfm-mode for formatted strings () * eglot.el (eglot--format-markup): Use gfm-mode instead of markdown-mode. Copyright-paperwork-exempt: yes GitHub-reference: https://github.com/joaotavora/eglot/issues/20 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ac17fb895f..1aa3661d4d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -847,7 +847,7 @@ If optional MARKER, return a marker instead" "Format MARKUP according to LSP's spec." (pcase-let ((`(,string ,mode) (if (stringp markup) (list (string-trim markup) - (intern "markdown-mode")) + (intern "gfm-mode")) (list (plist-get markup :value) (intern (concat (plist-get markup :language) "-mode" )))))) (with-temp-buffer commit e10f933d613e7acefbfc39f590b14d856f901acd Author: João Távora Date: Fri Jun 15 23:56:29 2018 +0100 Minor cleanup to new textdocument/formatting feature * README.md (Language feature): Tick textDocument/formatting * eglot.el (eglot-client-capabilities): Add formatting capability. Also move codeAction capability to the correct section. (eglot-format-buffer): Remove unused lexical variable before-point. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 98802dea60..ac17fb895f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -163,7 +163,6 @@ deferred to the future.") :workspace (list :applyEdit t :executeCommand `(:dynamicRegistration :json-false) - :codeAction `(:dynamicRegistration :json-false) :workspaceEdit `(:documentChanges :json-false) :didChangeWatchesFiles `(:dynamicRegistration t) :symbol `(:dynamicRegistration :json-false)) @@ -179,6 +178,8 @@ deferred to the future.") :definition `(:dynamicRegistration :json-false) :documentSymbol `(:dynamicRegistration :json-false) :documentHighlight `(:dynamicRegistration :json-false) + :codeAction `(:dynamicRegistration :json-false) + :formatting `(:dynamicRegistration :json-false) :rename `(:dynamicRegistration :json-false) :publishDiagnostics `(:relatedInformation :json-false)) :experimental (list)))) @@ -1397,8 +1398,6 @@ DUMMY is ignored." :tabSize tab-width :insertSpaces (not indent-tabs-mode))) :textDocument/formatting)) - (before-point - (buffer-substring (max (- (point) 60) (point-min)) (point))) (after-point (buffer-substring (point) (min (+ (point) 60) (point-max)))) (regexp (and (not (bobp)) commit 504389181d15880179f668e4e11143be62487c9b Author: Michal Krzywkowski Date: Sat Jun 16 00:47:52 2018 +0200 Implement formatting () Implement textDocument/formatting * eglot.el (eglot-format-buffer): New command to format current buffer. * eglot-tests.el (formatting): New test. GitHub-reference: https://github.com/joaotavora/eglot/issues/19 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ecfde4a022..98802dea60 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1382,6 +1382,35 @@ DUMMY is ignored." :workspace/symbol (list :query pattern))))) +(defun eglot-format-buffer () + "Format contents of current buffer." + (interactive) + (unless (eglot--server-capable :documentFormattingProvider) + (eglot--error "Server can't format!")) + (let* ((server (eglot--current-server-or-lose)) + (resp + (eglot--request + server + :textDocument/formatting + (list :textDocument (eglot--TextDocumentIdentifier) + :options (list + :tabSize tab-width + :insertSpaces (not indent-tabs-mode))) + :textDocument/formatting)) + (before-point + (buffer-substring (max (- (point) 60) (point-min)) (point))) + (after-point + (buffer-substring (point) (min (+ (point) 60) (point-max)))) + (regexp (and (not (bobp)) + (replace-regexp-in-string + "[\s\t\n\r]+" "[\s\t\n\r]+" + (concat "\\(" (regexp-quote after-point) "\\)"))))) + (when resp + (save-excursion + (eglot--apply-text-edits resp)) + (when (and (bobp) regexp (search-forward-regexp regexp nil t)) + (goto-char (match-beginning 1)))))) + (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." (let ((bounds (bounds-of-thing-at-point 'symbol)) commit 249dd2bd0dcfa4049dc6e82ac81e802233bd1947 Author: João Távora Date: Wed Jun 13 18:48:41 2018 +0100 * eglot.el (version): bump to 0.10 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7bef2ec08e..ecfde4a022 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 0.9 +;; Version: 0.10 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot commit b21929955d9fe59ffc79a6759f1b8887bb7699cd Author: João Távora Date: Wed Jun 13 18:46:19 2018 +0100 Fix a bug when eglot--request times out * eglot.el (eglot--request): Better timeout message. (eglot--async-request): Must return the timer. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e438c492dd..7bef2ec08e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -737,7 +737,7 @@ happens, the original timer keeps counting). Return (ID TIMER)." (setf (eglot--status server) `(,message t)) server `(:message "error ignored, status set" :id ,id :error ,code))) - (or timer (funcall make-timer))) + (setq timer (or timer (funcall make-timer)))) (eglot--pending-continuations server)) (list id timer))) @@ -758,7 +758,10 @@ DEFERRED is passed to `eglot--async-request', which see." (eglot--async-request server method params :success-fn (lambda (result) (throw done `(done ,result))) - :timeout-fn (lambda () (throw done '(error "Timed out"))) + :timeout-fn (lambda () (throw done + `(error + ,(format "Request id=%s timed out" + (car id-and-timer))))) :error-fn (eglot--lambda (&key code message _data) (throw done `(error ,(format "Ooops: %s: %s" code message)))) commit 332657f444a14f5b7a7a1ad85c79e3fe27540723 Author: João Távora Date: Wed Jun 13 12:46:36 2018 +0100 * eglot.el (eglot-shutdown): accept timeout param. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d9fbd331c1..e438c492dd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1039,14 +1039,14 @@ Uses THING, FACE, DEFS and PREPEND." ;;; Protocol implementation (Requests, notifications, etc) ;;; -(defun eglot-shutdown (server &optional _interactive) +(defun eglot-shutdown (server &optional _interactive timeout) "Politely ask SERVER to quit. -Forcefully quit it if it doesn't respond. Don't leave this -function with the server still running." +Forcefully quit it if it doesn't respond within TIMEOUT seconds. +Don't leave this function with the server still running." (interactive (list (eglot--current-server-or-lose) t)) (eglot--message "Asking %s politely to terminate" (eglot--name server)) (unwind-protect - (let ((eglot-request-timeout 3)) + (let ((eglot-request-timeout (or timeout 1.5))) (setf (eglot--shutdown-requested server) t) (eglot--request server :shutdown nil) ;; this one is supposed to always fail, hence ignore-errors commit c86410efad4e638a2d9571d1ebd86bd83c1b8e49 Author: João Távora Date: Wed Jun 13 12:30:20 2018 +0100 Fix bug in querying server capabilities This lead to javascript-typescript-stdio being sent an incremental didChange notif, which it doesn't support. * eglot.el (eglot--server-capable): Fix bug. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 59475eb7fc..d9fbd331c1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -867,8 +867,8 @@ under cursor." for feat in feats for probe = (plist-member caps feat) if (not probe) do (cl-return nil) - if (eq (cadr probe) t) do (cl-return t) if (eq (cadr probe) :json-false) do (cl-return nil) + if (not (listp (cadr probe))) do (cl-return (cadr probe)) finally (cl-return (or probe t))))) (defun eglot--range-region (range &optional markers) commit 7bdc94f79c7b92611ad0fd1dfb948b65b299a0a2 Author: João Távora Date: Sun Jun 10 14:57:02 2018 +0100 * eglot.el (version): bump to 0.9 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 47352f7523..59475eb7fc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 0.8 +;; Version: 0.9 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot commit 97c17252e386ead858f5633e9d578052bf447c98 Merge: bbfc1fdcf6 1d61ff4043 Author: João Távora Date: Sun Jun 10 14:56:10 2018 +0100 Merge branch 'master' into jsonrpc-refactor commit 1d61ff404356f024c0911623f2f3640324caa527 Author: João Távora Date: Sun Jun 10 13:41:10 2018 +0100 New eglot-ensure to put in a major-mode's hook * eglot.el (Commentary): Mention eglo-ensure. (eglot--connect): Rearrange args. (eglot--guess-contact): Rename from eglot--interactive. (eglot): Use eglot--guess-contact. (eglot, eglot-reconnect): Rearrange call to eglot--connect. (eglot-ensure): New command to put in mode hook. * eglot-tests.el (eglot--tests-connect): New helper. (auto-detect-running-server, auto-reconnect, rls-watches-files) (rls-basic-diagnostics, rls-hover-after-edit, rls-rename) (basic-completions, hover-after-completions): Use it. GitHub-reference: close https://github.com/joaotavora/eglot/issues/17 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4d3a46970e..47352f7523 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -46,6 +46,13 @@ ;; To "unmanage" these buffers, shutdown the server with M-x ;; eglot-shutdown. ;; +;; You can also do: +;; +;; (add-hook 'foo-mode-hook 'eglot-ensure) +;; +;; To attempt to start an eglot session automatically everytime a +;; foo-mode buffer is visited. +;; ;;; Code: (require 'json) @@ -279,7 +286,7 @@ CONTACT is in `eglot'. Returns a process object." (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") -(defun eglot--connect (project managed-major-mode contact server-class) +(defun eglot--connect (managed-major-mode project server-class contact) "Connect for PROJECT, MANAGED-MAJOR-MODE and CONTACT. INTERACTIVE is t if inside interactive call. Return an object of class SERVER-CLASS." @@ -331,8 +338,10 @@ class SERVER-CLASS." (defvar eglot--command-history nil "History of COMMAND arguments to `eglot'.") -(defun eglot--interactive () - "Helper for `eglot'." +(defun eglot--guess-contact (&optional interactive) + "Helper for `eglot'. +Return (MANAGED-MODE PROJECT CONTACT CLASS). +If INTERACTIVE, maybe prompt user." (let* ((guessed-mode (if buffer-file-name major-mode)) (managed-mode (cond @@ -349,17 +358,20 @@ class SERVER-CLASS." (class (and (consp guess) (symbolp (car guess)) (prog1 (car guess) (setq guess (cdr guess))))) (program (and (listp guess) (stringp (car guess)) (car guess))) - (base-prompt "[eglot] Enter program to execute (or :): ") + (base-prompt + (and interactive + "[eglot] Enter program to execute (or :): ")) (prompt - (cond (current-prefix-arg base-prompt) - ((null guess) - (format "[eglot] Sorry, couldn't guess for `%s'\n%s!" - managed-mode base-prompt)) - ((and program (not (executable-find program))) - (concat (format "[eglot] I guess you want to run `%s'" - (combine-and-quote-strings guess)) - (format ", but I can't find `%s' in PATH!" program) - "\n" base-prompt)))) + (and base-prompt + (cond (current-prefix-arg base-prompt) + ((null guess) + (format "[eglot] Sorry, couldn't guess for `%s'!\n%s" + managed-mode base-prompt)) + ((and program (not (executable-find program))) + (concat (format "[eglot] I guess you want to run `%s'" + (combine-and-quote-strings guess)) + (format ", but I can't find `%s' in PATH!" program) + "\n" base-prompt))))) (contact (if prompt (let ((s (read-shell-command @@ -371,10 +383,10 @@ class SERVER-CLASS." (list (match-string 1 s) (string-to-number (match-string 2 s))) (split-string-and-unquote s))) guess))) - (list managed-mode project contact class t))) + (list managed-mode project class contact))) ;;;###autoload -(defun eglot (managed-major-mode project command server-class +(defun eglot (managed-major-mode project server-class command &optional interactive) "Manage a project with a Language Server Protocol (LSP) server. @@ -405,7 +417,7 @@ SERVER-CLASS is a symbol naming a class that must inherit from `eglot-server', or nil to use the default server class. INTERACTIVE is t if called interactively." - (interactive (eglot--interactive)) + (interactive (append (eglot--guess-contact t) '(t))) (let ((current-server (eglot--current-server))) (if (and current-server (process-live-p (eglot--process current-server)) @@ -415,10 +427,10 @@ INTERACTIVE is t if called interactively." (when (and current-server (process-live-p (eglot--process current-server))) (ignore-errors (eglot-shutdown current-server))) - (let ((server (eglot--connect project - managed-major-mode - command - server-class))) + (let ((server (eglot--connect managed-major-mode + project + server-class + command))) (eglot--message "Connected! Server `%s' now \ managing `%s' buffers in project `%s'." (eglot--name server) managed-major-mode @@ -431,12 +443,34 @@ INTERACTIVE is t if called interactively." (interactive (list (eglot--current-server-or-lose) t)) (when (process-live-p (eglot--process server)) (ignore-errors (eglot-shutdown server interactive))) - (eglot--connect (eglot--project server) - (eglot--major-mode server) - (eglot--contact server) - (eieio-object-class server)) + (eglot--connect (eglot--major-mode server) + (eglot--project server) + (eieio-object-class server) + (eglot--contact server)) (eglot--message "Reconnected!")) +(defvar eglot--managed-mode) ;forward decl + +(defun eglot-ensure () + "Start Eglot session for current buffer if there isn't one." + (let ((buffer (current-buffer))) + (cl-labels + ((maybe-connect + () + (remove-hook 'post-command-hook #'maybe-connect nil) + (eglot--with-live-buffer buffer + (if eglot--managed-mode + (eglot--message "%s is already managed by existing `%s'" + buffer + (eglot--name (eglot--current-server))) + (let ((server (apply #'eglot--connect (eglot--guess-contact)))) + (eglot--message + "Automatically started `%s' to manage `%s' buffers in project `%s'" + (eglot--name server) + major-mode + (eglot--project-nickname server))))))) + (add-hook 'post-command-hook #'maybe-connect 'append nil)))) + (defun eglot--process-sentinel (proc change) "Called when PROC undergoes CHANGE." (let ((server (process-get proc 'eglot-server))) commit bbfc1fdcf605b010960023e073ff0bf6da3886e2 Author: João Távora Date: Sun Jun 10 07:31:45 2018 +0100 Remove connection grabbing antics from jsonrpc.el * eglot.el (eglot--managed-mode): Don't touch jsonrpc-find-connection-functions. (eglot--current-server-or-lose, eglot--current-server): New functions (resuscitate). (eglot-shutdown, eglot, eglot-reconnect) (eglot--server-capable, eglot--maybe-activate-editing-mode) (eglot-clear-status, eglot--mode-line-format) (eglot--signal-textDocument/didChange) (eglot--signal-textDocument/didOpen) (eglot--signal-textDocument/didSave) (xref-backend-identifier-completion-table) (xref-backend-definitions, xref-backend-references) (xref-backend-apropos, eglot-completion-at-point) (eglot-help-at-point, eglot-eldoc-function, eglot-imenu) (eglot-rename, eglot-code-actions): Use eglot--current-server and eglot--current-server-or-lose. (eglot-events-buffer, eglot-stderr-buffer) (eglot-forget-pending-continuations): New commands. (eglot--mode-line-format): Use eglot-stderr-buffer. * jsonrpc.el (jsonrpc-find-connection-functions) (jsonrpc-current-connection, jsonrpc-current-connection-or-lose): Remove. (jsonrpc-stderr-buffer, jsonrpc-events-buffer): Simplify. (jsonrpc-forget-pending-continuations): No longer interactive. * eglot-tests.el (auto-detect-running-server, auto-reconnect): Use eglot--current-server. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f8bd32cd97..32879ec320 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -207,7 +207,7 @@ lasted more than that many seconds." "Politely ask SERVER to quit. Forcefully quit it if it doesn't respond. Don't leave this function with the server still running." - (interactive (list (jsonrpc-current-connection-or-lose) t)) + (interactive (list (eglot--current-server-or-lose) t)) (eglot--message "Asking %s politely to terminate" (jsonrpc-name server)) (unwind-protect (progn @@ -330,7 +330,7 @@ INTERACTIVE is t if called interactively." (interactive (eglot--interactive)) (let* ((nickname (file-name-base (directory-file-name (car (project-roots project))))) - (current-server (jsonrpc-current-connection)) + (current-server (eglot--current-server)) (live-p (and current-server (jsonrpc-running-p current-server)))) (if (and live-p interactive @@ -352,7 +352,7 @@ managing `%s' buffers in project `%s'." (defun eglot-reconnect (server &optional interactive) "Reconnect to SERVER. INTERACTIVE is t if called interactively." - (interactive (list (jsonrpc-current-connection-or-lose) t)) + (interactive (list (eglot--current-server-or-lose) t)) (when (jsonrpc-running-p server) (ignore-errors (eglot-shutdown server interactive))) (eglot--connect (eglot--project server) @@ -363,7 +363,20 @@ INTERACTIVE is t if called interactively." (eglot--saved-initargs server)) (eglot--message "Reconnected!")) -(defalias 'eglot-events-buffer 'jsonrpc-events-buffer) +(defun eglot-events-buffer (server) + "Display events buffer for SERVER." + (interactive (eglot--current-server-or-lose)) + (display-buffer (jsonrpc-events-buffer server))) + +(defun eglot-stderr-buffer (server) + "Display stderr buffer for SERVER." + (interactive (eglot--current-server-or-lose)) + (display-buffer (jsonrpc-stderr-buffer server))) + +(defun eglot-forget-pending-continuations (server) + "Forget pending requests for SERVER." + (interactive (eglot--current-server-or-lose)) + (jsonrpc-forget-pending-continuations server)) (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") @@ -515,7 +528,7 @@ under cursor." (unless (cl-some (lambda (feat) (memq feat eglot-ignored-server-capabilites)) feats) - (cl-loop for caps = (eglot--capabilities (jsonrpc-current-connection-or-lose)) + (cl-loop for caps = (eglot--capabilities (eglot--current-server-or-lose)) then (cadr probe) for feat in feats for probe = (plist-member caps feat) @@ -548,7 +561,6 @@ If optional MARKERS, make markers." nil nil eglot-mode-map (cond (eglot--managed-mode - (add-hook 'jsonrpc-find-connection-functions 'eglot--find-current-server nil t) (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'before-change-functions 'eglot--before-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) @@ -562,7 +574,6 @@ If optional MARKERS, make markers." #'eglot-eldoc-function) (add-function :around (local imenu-create-index-function) #'eglot-imenu)) (t - (remove-hook 'jsonrpc-find-connection-functions 'eglot--find-current-server t) (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) (remove-hook 'before-change-functions 'eglot--before-change t) @@ -589,12 +600,17 @@ If optional MARKERS, make markers." (add-hook 'eglot--managed-mode-hook 'flymake-mode) (add-hook 'eglot--managed-mode-hook 'eldoc-mode) -(defun eglot--find-current-server () +(defun eglot--current-server () "Find the current logical EGLOT server." (let* ((probe (or (project-current) `(transient . ,default-directory)))) (cl-find major-mode (gethash probe eglot--servers-by-project) :key #'eglot--major-mode))) +(defun eglot--current-server-or-lose () + "Return current logical EGLOT server connection or error." + (or (eglot--current-server) + (jsonrpc-error "No current JSON-RPC connection"))) + (defvar-local eglot--unreported-diagnostics nil "Unreported Flymake diagnostics for this buffer.") @@ -603,7 +619,7 @@ If optional MARKERS, make markers." If SERVER is supplied, do it only if BUFFER is managed by it. In that case, also signal textDocument/didOpen." ;; Called even when revert-buffer-in-progress-p - (let* ((cur (and buffer-file-name (eglot--find-current-server))) + (let* ((cur (and buffer-file-name (eglot--current-server))) (server (or (and (null server) cur) (and server (eq server cur) cur)))) (when server (setq eglot--unreported-diagnostics `(:just-opened . nil)) @@ -614,7 +630,7 @@ that case, also signal textDocument/didOpen." (defun eglot-clear-status (server) "Clear the last JSONRPC error for SERVER." - (interactive (list (jsonrpc-current-connection-or-lose))) + (interactive (list (eglot--current-server-or-lose))) (setf (jsonrpc-last-error server) nil)) @@ -651,7 +667,7 @@ Uses THING, FACE, DEFS and PREPEND." (defun eglot--mode-line-format () "Compose the EGLOT's mode-line." - (pcase-let* ((server (jsonrpc-current-connection)) + (pcase-let* ((server (eglot--current-server)) (nick (and server (eglot--project-nickname server))) (pending (and server (hash-table-count (jsonrpc--request-continuations server)))) @@ -662,7 +678,7 @@ Uses THING, FACE, DEFS and PREPEND." (when nick `(":" ,(eglot--mode-line-props nick 'eglot-mode-line - '((C-mouse-1 jsonrpc-stderr-buffer "go to stderr buffer") + '((C-mouse-1 eglot-stderr-buffer "go to stderr buffer") (mouse-1 eglot-events-buffer "go to events buffer") (mouse-2 eglot-shutdown "quit server") (mouse-3 eglot-reconnect "reconnect to server"))) @@ -680,7 +696,7 @@ Uses THING, FACE, DEFS and PREPEND." ,@(when (cl-plusp pending) `("/" ,(eglot--mode-line-props (format "%d oustanding requests" pending) 'warning - '((mouse-3 jsonrpc-forget-pending-continuations + '((mouse-3 eglot-forget-pending-continuations "fahgettaboudit")))))))))) (add-to-list 'mode-line-misc-info @@ -863,7 +879,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." (when eglot--recent-changes - (let* ((server (jsonrpc-current-connection-or-lose)) + (let* ((server (eglot--current-server-or-lose)) (sync-kind (eglot--server-capable :textDocumentSync)) (full-sync-p (or (eq sync-kind 1) (eq :emacs-messup eglot--recent-changes)))) @@ -887,18 +903,18 @@ Records START, END and PRE-CHANGE-LENGTH locally." "Send textDocument/didOpen to server." (setq eglot--recent-changes nil eglot--versioned-identifier 0) (jsonrpc-notify - (jsonrpc-current-connection-or-lose) + (eglot--current-server-or-lose) :textDocument/didOpen `(:textDocument ,(eglot--TextDocumentItem)))) (defun eglot--signal-textDocument/didClose () "Send textDocument/didClose to server." (jsonrpc-notify - (jsonrpc-current-connection-or-lose) + (eglot--current-server-or-lose) :textDocument/didClose `(:textDocument ,(eglot--TextDocumentIdentifier)))) (defun eglot--signal-textDocument/willSave () "Send textDocument/willSave to server." - (let ((server (jsonrpc-current-connection-or-lose)) + (let ((server (eglot--current-server-or-lose)) (params `(:reason 1 :textDocument ,(eglot--TextDocumentIdentifier)))) (jsonrpc-notify server :textDocument/willSave params) (when (eglot--server-capable :textDocumentSync :willSaveWaitUntil) @@ -910,7 +926,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." (defun eglot--signal-textDocument/didSave () "Send textDocument/didSave to server." (jsonrpc-notify - (jsonrpc-current-connection-or-lose) + (eglot--current-server-or-lose) :textDocument/didSave (list ;; TODO: Handle TextDocumentSaveRegistrationOptions to control this. @@ -950,7 +966,7 @@ DUMMY is ignored." (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) (when (eglot--server-capable :documentSymbolProvider) - (let ((server (jsonrpc-current-connection-or-lose)) + (let ((server (eglot--current-server-or-lose)) (text-id (eglot--TextDocumentIdentifier))) (completion-table-with-cache (lambda (string) @@ -984,7 +1000,7 @@ DUMMY is ignored." (location-or-locations (if rich-identifier (get-text-property 0 :locations rich-identifier) - (jsonrpc-request (jsonrpc-current-connection-or-lose) + (jsonrpc-request (eglot--current-server-or-lose) :textDocument/definition (get-text-property 0 :textDocumentPositionParams identifier))))) @@ -1004,7 +1020,7 @@ DUMMY is ignored." (mapcar (jsonrpc-lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) - (jsonrpc-request (jsonrpc-current-connection-or-lose) + (jsonrpc-request (eglot--current-server-or-lose) :textDocument/references (append params @@ -1017,14 +1033,14 @@ DUMMY is ignored." (jsonrpc-lambda (&key name location &allow-other-keys) (cl-destructuring-bind (&key uri range) location (eglot--xref-make name uri (plist-get range :start)))) - (jsonrpc-request (jsonrpc-current-connection-or-lose) + (jsonrpc-request (eglot--current-server-or-lose) :workspace/symbol `(:query ,pattern))))) (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." (let ((bounds (bounds-of-thing-at-point 'symbol)) - (server (jsonrpc-current-connection-or-lose))) + (server (eglot--current-server-or-lose))) (when (eglot--server-capable :completionProvider) (list (or (car bounds) (point)) @@ -1113,7 +1129,7 @@ DUMMY is ignored." "Request \"hover\" information for the thing at point." (interactive) (cl-destructuring-bind (&key contents range) - (jsonrpc-request (jsonrpc-current-connection-or-lose) :textDocument/hover + (jsonrpc-request (eglot--current-server-or-lose) :textDocument/hover (eglot--TextDocumentPositionParams)) (when (seq-empty-p contents) (eglot--error "No hover info here")) (let ((blurb (eglot--hover-info contents range))) @@ -1124,7 +1140,7 @@ DUMMY is ignored." "EGLOT's `eldoc-documentation-function' function. If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (let* ((buffer (current-buffer)) - (server (jsonrpc-current-connection-or-lose)) + (server (eglot--current-server-or-lose)) (position-params (eglot--TextDocumentPositionParams)) sig-showing) (cl-macrolet ((when-buffer-window @@ -1183,7 +1199,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) (eglot--lsp-position-to-point (plist-get (plist-get location :range) :start)))) - (jsonrpc-request (jsonrpc-current-connection-or-lose) + (jsonrpc-request (eglot--current-server-or-lose) :textDocument/documentSymbol `(:textDocument ,(eglot--TextDocumentIdentifier)))))) (append @@ -1242,7 +1258,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (unless (eglot--server-capable :renameProvider) (eglot--error "Server can't rename!")) (eglot--apply-workspace-edit - (jsonrpc-request (jsonrpc-current-connection-or-lose) + (jsonrpc-request (eglot--current-server-or-lose) :textDocument/rename `(,@(eglot--TextDocumentPositionParams) :newName ,newname)) current-prefix-arg)) @@ -1259,7 +1275,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (t (list (point-min) (point-max)))))) (unless (eglot--server-capable :codeActionProvider) (eglot--error "Server can't execute code actions!")) - (let* ((server (jsonrpc-current-connection-or-lose)) + (let* ((server (eglot--current-server-or-lose)) (actions (jsonrpc-request server :textDocument/codeAction commit 4d680281947ad05399c9b3fc9274ef33c07d3855 Author: João Távora Date: Sun Jun 10 07:16:41 2018 +0100 Simplify jsonrpc status setting * eglot.el (eglot--connect): Don't set jsonrpc-status. (eglot-clear-status): New interactive command. (eglot--mode-line-format): Simplify. * jsonrpc.el (jsonrpc--async-request-1): Simplify. (jsonrpc-connection): Replace status with last-error. (jsonrpc-clear-status): Delete. (jsonrpc--connection-receive): Set last-error. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13413ab07f..f8bd32cd97 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -423,7 +423,6 @@ appeases checkdoc, that's all." :initializationOptions (eglot-initialization-options server) :capabilities (eglot-client-capabilities server))) (setf (eglot--capabilities server) capabilities) - (setf (jsonrpc-status server) nil) (dolist (buffer (buffer-list)) (with-current-buffer buffer (eglot--maybe-activate-editing-mode server))) @@ -613,6 +612,11 @@ that case, also signal textDocument/didOpen." (add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) +(defun eglot-clear-status (server) + "Clear the last JSONRPC error for SERVER." + (interactive (list (jsonrpc-current-connection-or-lose))) + (setf (jsonrpc-last-error server) nil)) + ;;; Mode-line, menu and other sugar ;;; @@ -652,7 +656,7 @@ Uses THING, FACE, DEFS and PREPEND." (pending (and server (hash-table-count (jsonrpc--request-continuations server)))) (`(,_id ,doing ,done-p ,detail) (and server (eglot--spinner server))) - (`(,status ,serious-p) (and server (jsonrpc-status server)))) + (last-error (and server (jsonrpc-last-error server)))) (append `(,(eglot--mode-line-props "eglot" 'eglot-mode-line nil)) (when nick @@ -662,11 +666,12 @@ Uses THING, FACE, DEFS and PREPEND." (mouse-1 eglot-events-buffer "go to events buffer") (mouse-2 eglot-shutdown "quit server") (mouse-3 eglot-reconnect "reconnect to server"))) - ,@(when serious-p + ,@(when last-error `("/" ,(eglot--mode-line-props "error" 'compilation-mode-line-fail - '((mouse-3 jsonrpc-clear-status "clear this status")) - (format "An error occured: %s\n" status)))) + '((mouse-3 eglot-clear-status "clear this status")) + (format "An error occured: %s\n" (plist-get last-error + :message))))) ,@(when (and doing (not done-p)) `("/" ,(eglot--mode-line-props (format "%s%s" doing commit ce96614d8acd0c12b8d209678bbcc2af5b95d1c6 Author: João Távora Date: Sat Jun 9 21:19:13 2018 +0100 Request dispatcher's return value determines response No more jsonrpc-reply. * eglot.el (eglot-handle-request window/showMessageRequest): Simplify. (eglot--register-unregister): Simplify. (eglot-handle-request workspace/applyEdit): Simplify. (eglot--apply-text-edits): Signal a jsonrpc-error. (eglot--apply-workspace-edit): Simplify. * jsonrpc-tests.el (jsonrpc--with-emacsrpc-fixture): Don't jsonrpc--reply. * jsonrpc.el (jsonrpc-error, jsonrpc-connection, jsonrpc-request): Improve docstring. (jsonrpc-error): Polymorphic args. (jsonrpc--unanswered-request-id): Remove. (jsonrpc--connection-receive): Rework and simplify. (jsonrpc-reply): Simplify. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 60a03228dd..13413ab07f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -707,24 +707,18 @@ Uses THING, FACE, DEFS and PREPEND." type message)) (cl-defmethod eglot-handle-request - (server (_method (eql window/showMessageRequest)) &key type message actions) + (_server (_method (eql window/showMessageRequest)) &key type message actions) "Handle server request window/showMessageRequest" - (let (reply) - (unwind-protect - (setq reply - (completing-read - (concat - (format (propertize "[eglot] Server reports (type=%s): %s" - 'face (if (<= type 1) 'error)) - type message) - "\nChoose an option: ") - (or (mapcar (lambda (obj) (plist-get obj :title)) actions) - '("OK")) - nil t (plist-get (elt actions 0) :title))) - (if reply - (jsonrpc-reply server :result `(:title ,reply)) - (jsonrpc-reply server - :error `(:code -32800 :message "User cancelled")))))) + (or (completing-read + (concat + (format (propertize "[eglot] Server reports (type=%s): %s" + 'face (if (<= type 1) 'error)) + type message) + "\nChoose an option: ") + (or (mapcar (lambda (obj) (plist-get obj :title)) actions) + '("OK")) + nil t (plist-get (elt actions 0) :title)) + (jsonrpc-error :code -32800 :message "User cancelled"))) (cl-defmethod eglot-handle-notification (_server (_method (eql window/logMessage)) &key _type _message) @@ -762,18 +756,13 @@ Uses THING, FACE, DEFS and PREPEND." (cl-defun eglot--register-unregister (server things how) "Helper for `registerCapability'. THINGS are either registrations or unregisterations." - (dolist (thing (cl-coerce things 'list)) - (cl-destructuring-bind (&key id method registerOptions) thing - (let (retval) - (unwind-protect - (setq retval (apply (intern (format "eglot--%s-%s" how method)) - server :id id registerOptions)) - (unless (eq t (car retval)) - (cl-return-from eglot--register-unregister - (jsonrpc-reply - server - :error `(:code -32601 :message ,(or (cadr retval) "sorry"))))))))) - (jsonrpc-reply server :result `(:message "OK"))) + (cl-loop + for thing in (cl-coerce things 'list) + collect (cl-destructuring-bind (&key id method registerOptions) thing + (apply (intern (format "eglot--%s-%s" how method)) + server :id id registerOptions)) + into results + finally return `(:ok ,@results))) (cl-defmethod eglot-handle-request (server (_method (eql client/registerCapability)) &key registrations) @@ -787,14 +776,9 @@ THINGS are either registrations or unregisterations." (eglot--register-unregister server unregisterations 'unregister)) (cl-defmethod eglot-handle-request - (server (_method (eql workspace/applyEdit)) &key _label edit) + (_server (_method (eql workspace/applyEdit)) &key _label edit) "Handle server request workspace/applyEdit" - (condition-case err - (progn (eglot--apply-workspace-edit edit 'confirm) - (jsonrpc-reply server :result `(:applied ))) - (error (jsonrpc-reply server - :result `(:applied :json-false) - :error `(:code -32001 :message (format "%s" ,err)))))) + (eglot--apply-workspace-edit edit 'confirm)) (defun eglot--TextDocumentIdentifier () "Compute TextDocumentIdentifier object for current buffer." @@ -1206,8 +1190,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (defun eglot--apply-text-edits (edits &optional version) "Apply EDITS for current buffer if at VERSION, or if it's nil." (unless (or (not version) (equal version eglot--versioned-identifier)) - (eglot--error "Edits on `%s' require version %d, you have %d" - (current-buffer) version eglot--versioned-identifier)) + (jsonrpc-error "Edits on `%s' require version %d, we have %d" + (current-buffer) version eglot--versioned-identifier)) (eglot--widening (mapc (pcase-lambda (`(,newText ,beg . ,end)) (goto-char beg) (delete-region beg end) (insert newText)) @@ -1223,7 +1207,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (mapcar (jsonrpc-lambda (&key textDocument edits) (cl-destructuring-bind (&key uri version) textDocument (list (eglot--uri-to-path uri) edits version))) - documentChanges))) + documentChanges)) + edit) (cl-loop for (uri edits) on changes by #'cddr do (push (list (eglot--uri-to-path uri) edits) prepared)) (if (or confirm @@ -1233,16 +1218,17 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (format "[eglot] Server wants to edit:\n %s\n Proceed? " (mapconcat #'identity (mapcar #'car prepared) "\n "))) (eglot--error "User cancelled server edit"))) + (while (setq edit (car prepared)) + (cl-destructuring-bind (path edits &optional version) edit + (with-current-buffer (find-file-noselect path) + (eglot--apply-text-edits edits version)) + (pop prepared)) + t) (unwind-protect - (let (edit) (while (setq edit (car prepared)) - (cl-destructuring-bind (path edits &optional version) edit - (with-current-buffer (find-file-noselect path) - (eglot--apply-text-edits edits version)) - (pop prepared)))) - (if prepared (eglot--warn "Caution: edits of files %s failed." - (mapcar #'car prepared)) - (eglot-eldoc-function) - (eglot--message "Edit successful!")))))) + (if prepared (eglot--warn "Caution: edits of files %s failed." + (mapcar #'car prepared)) + (eglot-eldoc-function) + (eglot--message "Edit successful!")))))) (defun eglot-rename (newname) "Rename the current symbol to NEWNAME." @@ -1345,7 +1331,10 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (progn (dolist (dir (delete-dups (mapcar #'file-name-directory globs))) (push (file-notify-add-watch dir '(change) #'handle-event) (gethash id (eglot--file-watches server)))) - (setq success `(t "OK"))) + (setq + success + `(:message ,(format "OK, watching %s watchers" + (length watchers))))) (unless success (eglot--unregister-workspace/didChangeWatchedFiles server :id id)))))) commit dbf42e651aff5a1b5bd19faa7cde5e43e3967901 Merge: 75c6d5b9d3 4d07619584 Author: João Távora Date: Sat Jun 9 21:13:19 2018 +0100 Merge branch 'master' into jsonrpc-refactor commit 4d076195840812070153e2d23fe2dba9763a4913 Author: João Távora Date: Sat Jun 9 21:12:48 2018 +0100 Fix indentation f@#$%^ by previous commit Courtesy of aggressive-indent-mode... Agressive it is... diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9dd98f0b9b..4d3a46970e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -696,13 +696,13 @@ happens, the original timer keeps counting). Return (ID TIMER)." (puthash id (list (or success-fn (eglot--lambda (&rest _ignored) - (eglot--debug - server `(:message "success ignored" :id ,id)))) + (eglot--debug + server `(:message "success ignored" :id ,id)))) (or error-fn (eglot--lambda (&key code message &allow-other-keys) - (setf (eglot--status server) `(,message t)) - server `(:message "error ignored, status set" - :id ,id :error ,code))) + (setf (eglot--status server) `(,message t)) + server `(:message "error ignored, status set" + :id ,id :error ,code))) (or timer (funcall make-timer))) (eglot--pending-continuations server)) (list id timer))) @@ -726,8 +726,8 @@ DEFERRED is passed to `eglot--async-request', which see." :success-fn (lambda (result) (throw done `(done ,result))) :timeout-fn (lambda () (throw done '(error "Timed out"))) :error-fn (eglot--lambda (&key code message _data) - (throw done `(error - ,(format "Ooops: %s: %s" code message)))) + (throw done `(error + ,(format "Ooops: %s: %s" code message)))) :deferred deferred)) (while t (accept-process-output nil 30))) (pcase-let ((`(,id ,timer) id-and-timer)) @@ -1286,15 +1286,15 @@ DUMMY is ignored." (setq eglot--xref-known-symbols (mapcar (eglot--lambda (&key name kind location containerName) - (propertize name - :textDocumentPositionParams - (list :textDocument text-id - :position (plist-get - (plist-get location :range) - :start)) - :locations (list location) - :kind kind - :containerName containerName)) + (propertize name + :textDocumentPositionParams + (list :textDocument text-id + :position (plist-get + (plist-get location :range) + :start)) + :locations (list location) + :kind kind + :containerName containerName)) (eglot--request server :textDocument/documentSymbol `(:textDocument ,text-id)))) (all-completions string eglot--xref-known-symbols)))))) @@ -1316,7 +1316,7 @@ DUMMY is ignored." (get-text-property 0 :textDocumentPositionParams identifier))))) (mapcar (eglot--lambda (&key uri range) - (eglot--xref-make identifier uri (plist-get range :start))) + (eglot--xref-make identifier uri (plist-get range :start))) location-or-locations))) (cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) @@ -1329,7 +1329,7 @@ DUMMY is ignored." (unless params (eglot--error "Don' know where %s is in the workspace!" identifier)) (mapcar (eglot--lambda (&key uri range) - (eglot--xref-make identifier uri (plist-get range :start))) + (eglot--xref-make identifier uri (plist-get range :start))) (eglot--request (eglot--current-server-or-lose) :textDocument/references (append @@ -1339,8 +1339,8 @@ DUMMY is ignored." (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) (mapcar (eglot--lambda (&key name location &allow-other-keys) - (cl-destructuring-bind (&key uri range) location - (eglot--xref-make name uri (plist-get range :start)))) + (cl-destructuring-bind (&key uri range) location + (eglot--xref-make name uri (plist-get range :start)))) (eglot--request (eglot--current-server-or-lose) :workspace/symbol (list :query pattern))))) @@ -1362,10 +1362,10 @@ DUMMY is ignored." (items (if (vectorp resp) resp (plist-get resp :items)))) (mapcar (eglot--lambda (&rest all &key label insertText &allow-other-keys) - (let ((insert (or insertText label))) - (add-text-properties 0 1 all insert) - (put-text-property 0 1 'eglot--lsp-completion all insert) - insert)) + (let ((insert (or insertText label))) + (add-text-properties 0 1 all insert) + (put-text-property 0 1 'eglot--lsp-completion all insert) + insert)) items)))) :annotation-function (lambda (obj) @@ -1459,20 +1459,20 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." server :textDocument/signatureHelp position-params :success-fn (eglot--lambda (&key signatures activeSignature activeParameter) - (when-buffer-window - (when (cl-plusp (length signatures)) - (setq sig-showing t) - (eldoc-message (eglot--sig-info signatures - activeSignature - activeParameter))))) + (when-buffer-window + (when (cl-plusp (length signatures)) + (setq sig-showing t) + (eldoc-message (eglot--sig-info signatures + activeSignature + activeParameter))))) :deferred :textDocument/signatureHelp)) (when (eglot--server-capable :hoverProvider) (eglot--async-request server :textDocument/hover position-params :success-fn (eglot--lambda (&key contents range) - (unless sig-showing - (when-buffer-window - (eldoc-message (eglot--hover-info contents range))))) + (unless sig-showing + (when-buffer-window + (eldoc-message (eglot--hover-info contents range))))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) (eglot--async-request @@ -1482,12 +1482,12 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (setq eglot--highlights (when-buffer-window (mapcar (eglot--lambda (&key range _kind _role) - (pcase-let ((`(,beg . ,end) - (eglot--range-region range))) - (let ((ov (make-overlay beg end))) - (overlay-put ov 'face 'highlight) - (overlay-put ov 'evaporate t) - ov))) + (pcase-let ((`(,beg . ,end) + (eglot--range-region range))) + (let ((ov (make-overlay beg end))) + (overlay-put ov 'face 'highlight) + (overlay-put ov 'evaporate t) + ov))) highlights)))) :deferred :textDocument/documentHighlight)))) nil) @@ -1498,9 +1498,9 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (let ((entries (mapcar (eglot--lambda (&key name kind location _containerName) - (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) - (eglot--lsp-position-to-point - (plist-get (plist-get location :range) :start)))) + (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) + (eglot--lsp-position-to-point + (plist-get (plist-get location :range) :start)))) (eglot--request (eglot--current-server-or-lose) :textDocument/documentSymbol `(:textDocument ,(eglot--TextDocumentIdentifier)))))) @@ -1519,7 +1519,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (mapc (pcase-lambda (`(,newText ,beg . ,end)) (goto-char beg) (delete-region beg end) (insert newText)) (mapcar (eglot--lambda (&key range newText) - (cons newText (eglot--range-region range 'markers))) + (cons newText (eglot--range-region range 'markers))) edits))) (eglot--message "%s: Performed %s edits" (current-buffer) (length edits))) @@ -1528,8 +1528,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (cl-destructuring-bind (&key changes documentChanges) wedit (let ((prepared (mapcar (eglot--lambda (&key textDocument edits) - (cl-destructuring-bind (&key uri version) textDocument - (list (eglot--uri-to-path uri) edits version))) + (cl-destructuring-bind (&key uri version) textDocument + (list (eglot--uri-to-path uri) edits version))) documentChanges))) (cl-loop for (uri edits) on changes by #'cddr do (push (list (eglot--uri-to-path uri) edits) prepared)) @@ -1589,7 +1589,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (eglot--diag-data diag)))) (flymake-diagnostics beg end))])))) (menu-items (mapcar (eglot--lambda (&key title command arguments) - `(,title . (:command ,command :arguments ,arguments))) + `(,title . (:command ,command :arguments ,arguments))) actions)) (menu (and menu-items `("Eglot code actions:" ("dummy" ,@menu-items)))) (command-and-args commit af32ce29ef3f9f5d93ad4ce57b9cf1a59435455e Author: João Távora Date: Sat Jun 9 21:09:03 2018 +0100 Attempt to handle rls sophisticated globs for didchangewwatchedfiles * eglot.el (eglot--wildcard-to-regexp): New helper. (eglot--register-workspace/didChangeWatchedFiles): Use it. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 97db8fc3f7..9dd98f0b9b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1610,6 +1610,18 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." ;;; Dynamic registration ;;; +(defun eglot--wildcard-to-regexp (wildcard) + "(Very lame attempt to) convert WILDCARD to a Elisp regexp." + (cl-loop + with substs = '(("{" . "\\\\(") + ("}" . "\\\\)") + ("," . "\\\\|")) + with string = (wildcard-to-regexp wildcard) + for (pattern . rep) in substs + for target = string then result + for result = (replace-regexp-in-string pattern rep target) + finally return result)) + (cl-defun eglot--register-workspace/didChangeWatchedFiles (server &key id watchers) "Handle dynamic registration of workspace/didChangeWatchedFiles" (eglot--unregister-workspace/didChangeWatchedFiles server :id id) @@ -1623,7 +1635,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." ((and (memq action '(created changed deleted)) (cl-find file globs :test (lambda (f glob) - (string-match (wildcard-to-regexp + (string-match (eglot--wildcard-to-regexp (expand-file-name glob)) f)))) (eglot--notify commit 96edec8d36eaea04134ea9070746835b7f557bd0 Author: João Távora Date: Fri Jun 8 18:58:34 2018 +0100 Check flymake-mode before calling report-fn * eglot.el (eglot-handle-notification): Check flymake-mode. GitHub-reference: close https://github.com/joaotavora/eglot/issues/16 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 81a63fdbea..97db8fc3f7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -696,13 +696,13 @@ happens, the original timer keeps counting). Return (ID TIMER)." (puthash id (list (or success-fn (eglot--lambda (&rest _ignored) - (eglot--debug - server `(:message "success ignored" :id ,id)))) + (eglot--debug + server `(:message "success ignored" :id ,id)))) (or error-fn (eglot--lambda (&key code message &allow-other-keys) - (setf (eglot--status server) `(,message t)) - server `(:message "error ignored, status set" - :id ,id :error ,code))) + (setf (eglot--status server) `(,message t)) + server `(:message "error ignored, status set" + :id ,id :error ,code))) (or timer (funcall make-timer))) (eglot--pending-continuations server)) (list id timer))) @@ -726,8 +726,8 @@ DEFERRED is passed to `eglot--async-request', which see." :success-fn (lambda (result) (throw done `(done ,result))) :timeout-fn (lambda () (throw done '(error "Timed out"))) :error-fn (eglot--lambda (&key code message _data) - (throw done `(error - ,(format "Ooops: %s: %s" code message)))) + (throw done `(error + ,(format "Ooops: %s: %s" code message)))) :deferred deferred)) (while t (accept-process-output nil 30))) (pcase-let ((`(,id ,timer) id-and-timer)) @@ -1079,7 +1079,7 @@ function with the server still running." (t 'eglot-note)) message `((eglot-lsp-diag . ,diag-spec))))) into diags - finally (cond (eglot--current-flymake-report-fn + finally (cond ((and flymake-mode eglot--current-flymake-report-fn) (funcall eglot--current-flymake-report-fn diags) (setq eglot--unreported-diagnostics nil)) (t @@ -1286,15 +1286,15 @@ DUMMY is ignored." (setq eglot--xref-known-symbols (mapcar (eglot--lambda (&key name kind location containerName) - (propertize name - :textDocumentPositionParams - (list :textDocument text-id - :position (plist-get - (plist-get location :range) - :start)) - :locations (list location) - :kind kind - :containerName containerName)) + (propertize name + :textDocumentPositionParams + (list :textDocument text-id + :position (plist-get + (plist-get location :range) + :start)) + :locations (list location) + :kind kind + :containerName containerName)) (eglot--request server :textDocument/documentSymbol `(:textDocument ,text-id)))) (all-completions string eglot--xref-known-symbols)))))) @@ -1316,7 +1316,7 @@ DUMMY is ignored." (get-text-property 0 :textDocumentPositionParams identifier))))) (mapcar (eglot--lambda (&key uri range) - (eglot--xref-make identifier uri (plist-get range :start))) + (eglot--xref-make identifier uri (plist-get range :start))) location-or-locations))) (cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) @@ -1329,7 +1329,7 @@ DUMMY is ignored." (unless params (eglot--error "Don' know where %s is in the workspace!" identifier)) (mapcar (eglot--lambda (&key uri range) - (eglot--xref-make identifier uri (plist-get range :start))) + (eglot--xref-make identifier uri (plist-get range :start))) (eglot--request (eglot--current-server-or-lose) :textDocument/references (append @@ -1339,8 +1339,8 @@ DUMMY is ignored." (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) (mapcar (eglot--lambda (&key name location &allow-other-keys) - (cl-destructuring-bind (&key uri range) location - (eglot--xref-make name uri (plist-get range :start)))) + (cl-destructuring-bind (&key uri range) location + (eglot--xref-make name uri (plist-get range :start)))) (eglot--request (eglot--current-server-or-lose) :workspace/symbol (list :query pattern))))) @@ -1362,10 +1362,10 @@ DUMMY is ignored." (items (if (vectorp resp) resp (plist-get resp :items)))) (mapcar (eglot--lambda (&rest all &key label insertText &allow-other-keys) - (let ((insert (or insertText label))) - (add-text-properties 0 1 all insert) - (put-text-property 0 1 'eglot--lsp-completion all insert) - insert)) + (let ((insert (or insertText label))) + (add-text-properties 0 1 all insert) + (put-text-property 0 1 'eglot--lsp-completion all insert) + insert)) items)))) :annotation-function (lambda (obj) @@ -1459,20 +1459,20 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." server :textDocument/signatureHelp position-params :success-fn (eglot--lambda (&key signatures activeSignature activeParameter) - (when-buffer-window - (when (cl-plusp (length signatures)) - (setq sig-showing t) - (eldoc-message (eglot--sig-info signatures - activeSignature - activeParameter))))) + (when-buffer-window + (when (cl-plusp (length signatures)) + (setq sig-showing t) + (eldoc-message (eglot--sig-info signatures + activeSignature + activeParameter))))) :deferred :textDocument/signatureHelp)) (when (eglot--server-capable :hoverProvider) (eglot--async-request server :textDocument/hover position-params :success-fn (eglot--lambda (&key contents range) - (unless sig-showing - (when-buffer-window - (eldoc-message (eglot--hover-info contents range))))) + (unless sig-showing + (when-buffer-window + (eldoc-message (eglot--hover-info contents range))))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) (eglot--async-request @@ -1482,12 +1482,12 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (setq eglot--highlights (when-buffer-window (mapcar (eglot--lambda (&key range _kind _role) - (pcase-let ((`(,beg . ,end) - (eglot--range-region range))) - (let ((ov (make-overlay beg end))) - (overlay-put ov 'face 'highlight) - (overlay-put ov 'evaporate t) - ov))) + (pcase-let ((`(,beg . ,end) + (eglot--range-region range))) + (let ((ov (make-overlay beg end))) + (overlay-put ov 'face 'highlight) + (overlay-put ov 'evaporate t) + ov))) highlights)))) :deferred :textDocument/documentHighlight)))) nil) @@ -1498,9 +1498,9 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (let ((entries (mapcar (eglot--lambda (&key name kind location _containerName) - (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) - (eglot--lsp-position-to-point - (plist-get (plist-get location :range) :start)))) + (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) + (eglot--lsp-position-to-point + (plist-get (plist-get location :range) :start)))) (eglot--request (eglot--current-server-or-lose) :textDocument/documentSymbol `(:textDocument ,(eglot--TextDocumentIdentifier)))))) @@ -1519,7 +1519,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (mapc (pcase-lambda (`(,newText ,beg . ,end)) (goto-char beg) (delete-region beg end) (insert newText)) (mapcar (eglot--lambda (&key range newText) - (cons newText (eglot--range-region range 'markers))) + (cons newText (eglot--range-region range 'markers))) edits))) (eglot--message "%s: Performed %s edits" (current-buffer) (length edits))) @@ -1528,8 +1528,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (cl-destructuring-bind (&key changes documentChanges) wedit (let ((prepared (mapcar (eglot--lambda (&key textDocument edits) - (cl-destructuring-bind (&key uri version) textDocument - (list (eglot--uri-to-path uri) edits version))) + (cl-destructuring-bind (&key uri version) textDocument + (list (eglot--uri-to-path uri) edits version))) documentChanges))) (cl-loop for (uri edits) on changes by #'cddr do (push (list (eglot--uri-to-path uri) edits) prepared)) @@ -1589,7 +1589,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (eglot--diag-data diag)))) (flymake-diagnostics beg end))])))) (menu-items (mapcar (eglot--lambda (&key title command arguments) - `(,title . (:command ,command :arguments ,arguments))) + `(,title . (:command ,command :arguments ,arguments))) actions)) (menu (and menu-items `("Eglot code actions:" ("dummy" ,@menu-items)))) (command-and-args commit 75c6d5b9d32da048b6af7802f74b3895b89cd07e Merge: 8def9a6196 34f10965a9 Author: João Távora Date: Fri Jun 8 18:48:06 2018 +0100 Merge master into jsonrpc-refactor commit 8def9a619605708a1b8783093f031c054b3ad585 Author: João Távora Date: Fri Jun 8 18:37:54 2018 +0100 Simplify jsonrpc connection shutdown * eglot.el (eglot--process): Delete. (eglot-shutdown): Use jsonrpc-shutdown. (eglot--on-shutdown): Simplify. (eglot-reconnect): Simplify. (eglot--connect): Simplify. * jsonrpc-tests.el (jsonrpc--with-emacsrpc-fixture): Simplify. * jsonrpc.el (jsonrpc-process-type, jsonrpc-running-p) (jsonrpc-shutdown): New methods. * eglot-tests.el (auto-reconnect): Use jsonrpc--process. (eglot--call-with-dirs-and-files): Use jsonrpc-running-p. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e1592ab3b1..cdb1c5b097 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -203,11 +203,6 @@ lasted more than that many seconds." (defvar eglot--servers-by-project (make-hash-table :test #'equal) "Keys are projects. Values are lists of processes.") -;; HACK: Do something to fix this in the jsonrpc API or here, but in -;; the meantime concentrate the hack here. -(defalias 'eglot--process 'jsonrpc--process - "An abuse of `jsonrpc--process', a jsonrpc.el internal.") - (defun eglot-shutdown (server &optional _interactive) "Politely ask SERVER to quit. Forcefully quit it if it doesn't respond. Don't leave this @@ -218,16 +213,15 @@ function with the server still running." (progn (setf (eglot--shutdown-requested server) t) (jsonrpc-request server :shutdown nil :timeout 3) - ;; this one is supposed to always fail, hence ignore-errors + ;; this one is supposed to always fail, because it asks the + ;; server to exit itself. Hence ignore-errors. (ignore-errors (jsonrpc-request server :exit nil :timeout 1))) ;; Turn off `eglot--managed-mode' where appropriate. (dolist (buffer (eglot--managed-buffers server)) (with-current-buffer buffer (eglot--managed-mode-onoff server -1))) - (while (progn (accept-process-output nil 0.1) - (not (eq (eglot--shutdown-requested server) :sentinel-done))) - (eglot--warn "Sentinel for %s still hasn't run, brutally deleting it!" - (eglot--process server)) - (delete-process (eglot--process server))))) + ;; Now ask jsonrpc.el to shutdown server (which in normal + ;; conditions should return immediately). + (jsonrpc-shutdown server))) (defun eglot--on-shutdown (server) "Called by jsonrpc.el when SERVER is already dead." @@ -243,7 +237,7 @@ function with the server still running." (delq server (gethash (eglot--project server) eglot--servers-by-project))) (cond ((eglot--shutdown-requested server) - (setf (eglot--shutdown-requested server) :sentinel-done)) + t) ((not (eglot--inhibit-autoreconnect server)) (eglot--warn "Reconnecting after unexpected server exit.") (eglot-reconnect server)) @@ -337,8 +331,7 @@ INTERACTIVE is t if called interactively." (let* ((nickname (file-name-base (directory-file-name (car (project-roots project))))) (current-server (jsonrpc-current-connection)) - (live-p (and current-server - (process-live-p (eglot--process current-server))))) + (live-p (and current-server (jsonrpc-running-p current-server)))) (if (and live-p interactive (y-or-n-p "[eglot] Live process found, reconnect instead? ")) @@ -360,7 +353,7 @@ managing `%s' buffers in project `%s'." "Reconnect to SERVER. INTERACTIVE is t if called interactively." (interactive (list (jsonrpc-current-connection-or-lose) t)) - (when (process-live-p (eglot--process server)) + (when (jsonrpc-running-p server) (ignore-errors (eglot-shutdown server interactive))) (eglot--connect (eglot--project server) (eglot--major-mode server) @@ -421,9 +414,7 @@ appeases checkdoc, that's all." (jsonrpc-request server :initialize - (list :processId (unless (eq (process-type - (eglot--process server)) - 'network) + (list :processId (unless (eq (jsonrpc-process-type server) 'network) (emacs-pid)) :rootPath (expand-file-name (car (project-roots project))) @@ -446,7 +437,7 @@ appeases checkdoc, that's all." (setf (eglot--inhibit-autoreconnect server) (null eglot-autoreconnect))))))) (setq success server)) - (unless (or success (not (process-live-p (eglot--process server)))) + (when (and (not success) (jsonrpc-running-p server)) (eglot-shutdown server))))) commit 43d9c7b8653e5fb4c5b5652b9a1e6135b0d2781d Author: João Távora Date: Fri Jun 8 16:05:02 2018 +0100 Support json.c. api purely based on classes No more jsonrpc-connect. This is a big commit because of a data loss problem. It should be at least two separate commits (json.c-support and new API) * eglot.el (eglot-server-programs): Rework docstring. (eglot-handle-request): Don't take ID param (eglot-lsp-server): No more initargs. (eglot--interactive): Return 5 args. (eglot): Take 5 args. (eglot-reconnect): Pass 6 args to eglot--connect. (eglot--dispatch): Remove. (eglot--connect): Take 6 args. Rework. (eglot-handle-notification): Change all specializations to use a non-keyword symbol spec. (eglot-handle-request): Remove ID param from all specializations. Don't pass ID to jsonrpc-reply. (eglot--register-unregister): Don't take JSONRPC-ID arg. Don't pass ID to jsonrpc-reply. * jsonrpc-tests.el (returns-3, signals-an--32603-JSONRPC-error) (times-out, stretching-it-but-works) (json-el-cant-serialize-this, jsonrpc-connection-ready-p) (deferred-action-intime, deferred-action-toolate) (deferred-action-timeout): Pass JSON objects compatible with json.c (jsonrpc--test-client, jsonrpc--test-endpoint): New classes (jsonrpc--with-emacsrpc-fixture): Don't use jsonrpc-connect. (jsonrpc-connection-ready-p): Update signature. * jsonrpc.el: Rewrite commentary. (jsonrpc-connection): Rework class. (jsonrpc-process-connection): Rework class. (initialize-instance): New methods.. (jsonrpc--json-read, jsonrpc--json-encode): Reindent. (jsonrpc-connect): Delete. (jsonrpc--json-read, jsonrpc--json-encode): New functions for working with json.c (jsonrpc--process-filter): Call them. (jsonrpc--unanswered-request-id): New variable. (jsonrpc--connection-receive): Use jsonrpc--unanswered-request-id (jsonrpc-connection-send): Take keyword params to build message instead of message. (jsonrpc-notify, jsonrpc--async-request-1): Use new jsonrpc-connection-send. (jsonrpc-reply): Simplify. * eglot-tests.el (rls-watches-files, rls-basic-diagnostics) (rls-hover-after-edit): Correctly compare using string= and non-keyword symbols. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d5498f9533..e1592ab3b1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -79,8 +79,8 @@ (php-mode . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php"))) "How the command `eglot' guesses the server to start. -An association list of (MAJOR-MODE . SPEC) pair. MAJOR-MODE is a -mode symbol. SPEC is +An association list of (MAJOR-MODE . CONTACT) pair. MAJOR-MODE +is a mode symbol. CONTACT is: * In the most common case, a list of strings (PROGRAM [ARGS...]). PROGRAM is called with ARGS and is expected to serve LSP requests @@ -91,12 +91,15 @@ a positive integer number for connecting to a server via TCP. Remaining ARGS are passed to `open-network-stream' for upgrading the connection with encryption or other capabilities. -* A function of no arguments returning a connected process. - -* A cons (CLASS-NAME . SPEC) where CLASS-NAME is a symbol -designating a subclass of symbol `eglot-lsp-server', for -representing experimental LSP servers. In this case SPEC is -interpreted as described above this point.") +* A cons (CLASS-NAME . INITARGS) where CLASS-NAME is a symbol +designating a subclass of `eglot-lsp-server', for representing +experimental LSP servers. INITARGS is a keyword-value plist used +to initialize CLASS-NAME, or a plain list interpreted as the +previous descriptions of CONTACT, in which case it is converted +to produce a plist with a suitable :PROCESS initarg to +CLASS-NAME. The class `eglot-lsp-server' descends +`jsonrpc-process-connection', which you should see for semantics +of the mandatory :PROCESS argument.") (defface eglot-mode-line '((t (:inherit font-lock-constant-face :weight bold))) @@ -124,8 +127,8 @@ lasted more than that many seconds." "Save excursion and restriction. Widen. Then run BODY." (declare (debug t)) `(save-excursion (save-restriction (widen) ,@body))) -(cl-defgeneric eglot-handle-request (server method id &rest params) - "Handle SERVER's METHOD request with ID and PARAMS.") +(cl-defgeneric eglot-handle-request (server method &rest params) + "Handle SERVER's METHOD request with PARAMS.") (cl-defgeneric eglot-handle-notification (server method id &rest params) "Handle SERVER's METHOD notification with PARAMS.") @@ -164,10 +167,10 @@ lasted more than that many seconds." (defclass eglot-lsp-server (jsonrpc-process-connection) ((project-nickname :documentation "Short nickname for the associated project." - :initarg :project-nickname :accessor eglot--project-nickname) + :accessor eglot--project-nickname) (major-mode :documentation "Major mode symbol." - :initarg :major-mode :accessor eglot--major-mode) + :accessor eglot--major-mode) (capabilities :documentation "JSON object containing server capabilities." :accessor eglot--capabilities) @@ -176,19 +179,22 @@ lasted more than that many seconds." :accessor eglot--shutdown-requested) (project :documentation "Project associated with server." - :initarg :project :accessor eglot--project) + :accessor eglot--project) (spinner :documentation "List (ID DOING-WHAT DONE-P) representing server progress." :initform `(nil nil t) :accessor eglot--spinner) (inhibit-autoreconnect :documentation "Generalized boolean inhibiting auto-reconnection if true." - :initarg :inhibit-autoreconnect :accessor eglot--inhibit-autoreconnect) + :accessor eglot--inhibit-autoreconnect) (file-watches :documentation "Map ID to list of WATCHES for `didChangeWatchedFiles'." :initform (make-hash-table :test #'equal) :accessor eglot--file-watches) (managed-buffers :documentation "List of buffers managed by server." - :initarg :managed-buffers :accessor eglot--managed-buffers)) + :accessor eglot--managed-buffers) + (saved-initargs + :documentation "Saved initargs for reconnection purposes" + :accessor eglot--saved-initargs)) :documentation "Represents a server. Wraps a process for LSP communication.") @@ -296,47 +302,35 @@ function with the server still running." (list (match-string 1 s) (string-to-number (match-string 2 s))) (split-string-and-unquote s))) guess))) - (list managed-mode project (cons class contact) t))) + (list managed-mode project class contact t))) ;;;###autoload -(defun eglot (managed-major-mode project contact &optional interactive) +(defun eglot (managed-major-mode project class contact &optional interactive) "Manage a project with a Language Server Protocol (LSP) server. -The LSP server is started (or contacted) via CONTACT. If this -operation is successful, current *and future* file buffers of -MANAGED-MAJOR-MODE inside PROJECT automatically become +The LSP server of CLASS started (or contacted) via CONTACT. If +this operation is successful, current *and future* file buffers +of MANAGED-MAJOR-MODE inside PROJECT automatically become \"managed\" by the LSP server, meaning information about their contents is exchanged periodically to provide enhanced code-analysis via `xref-find-definitions', `flymake-mode', `eldoc-mode', `completion-at-point', among others. Interactively, the command attempts to guess MANAGED-MAJOR-MODE -from current buffer, CONTACT from `eglot-server-programs' and -PROJECT from `project-current'. If it can't guess, the user is -prompted. With a single \\[universal-argument] prefix arg, it -always prompt for COMMAND. With two \\[universal-argument] -prefix args, also prompts for MANAGED-MAJOR-MODE. +from current buffer, CLASS and CONTACT from +`eglot-server-programs' and PROJECT from `project-current'. If +it can't guess, the user is prompted. With a single +\\[universal-argument] prefix arg, it always prompt for COMMAND. +With two \\[universal-argument] prefix args, also prompts for +MANAGED-MAJOR-MODE. PROJECT is a project instance as returned by `project-current'. -CONTACT specifies how to contact the server. It can be: - -* a list of strings (COMMAND [ARGS...]) specifying how -to start a server subprocess to connect to. - -* A list with a string as the first element and an integer number -as the second list is interpreted as (HOST PORT [PARAMETERS...]) -and connects to an existing server via TCP, with the remaining -PARAMETERS being given as `open-network-stream's optional -arguments. +CLASS is a subclass of symbol `eglot-lsp-server'. -* A list (CLASS-SYM CONTACT...) where CLASS-SYM names the -subclass of `eglot-server' used to create the server object. The -remaining arguments are processed as described in the previous -paragraphs. - -* A function of arguments returning arguments compatible with the -previous description. +CONTACT specifies how to contact the server. It is a +keyword-value plist used to initialize CLASS or a plain list as +described in `eglot-server-programs', which see. INTERACTIVE is t if called interactively." (interactive (eglot--interactive)) @@ -354,6 +348,7 @@ INTERACTIVE is t if called interactively." managed-major-mode (format "%s/%s" nickname managed-major-mode) nickname + class contact))) (eglot--message "Connected! Process `%s' now \ managing `%s' buffers in project `%s'." @@ -371,29 +366,51 @@ INTERACTIVE is t if called interactively." (eglot--major-mode server) (jsonrpc-name server) (eglot--project-nickname server) - (jsonrpc-contact server)) + (eieio-object-class-name server) + (eglot--saved-initargs server)) (eglot--message "Reconnected!")) (defalias 'eglot-events-buffer 'jsonrpc-events-buffer) (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") -(defun eglot--dispatch (server method id params) - "Dispatcher passed to `jsonrpc-connect'. -Calls a function on SERVER, METHOD ID and PARAMS." - (let ((method (intern (format ":%s" method)))) - (if id - (apply #'eglot-handle-request server id method params) - (apply #'eglot-handle-notification server method params) - (force-mode-line-update t)))) - -(defun eglot--connect (project managed-major-mode name nickname contact) +(defun eglot--connect (project managed-major-mode name nickname + class contact) "Connect to PROJECT, MANAGED-MAJOR-MODE, NAME. -And NICKNAME and CONTACT." - (let* ((contact (if (functionp contact) (funcall contact) contact)) +And don't forget NICKNAME and CLASS, CONTACT. This docstring +appeases checkdoc, that's all." + (let* ((readable-name (format "EGLOT (%s/%s)" nickname managed-major-mode)) + (initargs + (cond ((keywordp (car contact)) contact) + ((integerp (cadr contact)) + `(:process ,(lambda () + (apply #'open-network-stream + readable-name nil + (car contact) (cadr contact) + (cddr contact))))) + ((stringp (car contact)) + `(:process ,(lambda () + (make-process + :name readable-name + :command contact + :connection-type 'pipe + :coding 'utf-8-emacs-unix + :stderr (get-buffer-create + (format "*%s stderr*" readable-name)))))))) + (spread + (lambda (fn) + (lambda (&rest args) + (apply fn (append (butlast args) (car (last args))))))) (server - (jsonrpc-connect name contact #'eglot--dispatch #'eglot--on-shutdown)) + (apply + #'make-instance class + :name name + :notification-dispatcher (funcall spread #'eglot-handle-notification) + :request-dispatcher (funcall spread #'eglot-handle-request) + :on-shutdown #'eglot--on-shutdown + initargs)) success) + (setf (eglot--saved-initargs server) initargs) (setf (eglot--project server) project) (setf (eglot--project-nickname server) nickname) (setf (eglot--major-mode server) managed-major-mode) @@ -688,14 +705,14 @@ Uses THING, FACE, DEFS and PREPEND." ;;; Protocol implementation (Requests, notifications, etc) ;;; (cl-defmethod eglot-handle-notification - (_server (_method (eql :window/showMessage)) &key type message) + (_server (_method (eql window/showMessage)) &key type message) "Handle notification window/showMessage" (eglot--message (propertize "Server reports (type=%s): %s" 'face (if (<= type 1) 'error)) type message)) (cl-defmethod eglot-handle-request - (server id (_method (eql :window/showMessageRequest)) &key type message actions) + (server (_method (eql window/showMessageRequest)) &key type message actions) "Handle server request window/showMessageRequest" (let (reply) (unwind-protect @@ -710,23 +727,23 @@ Uses THING, FACE, DEFS and PREPEND." '("OK")) nil t (plist-get (elt actions 0) :title))) (if reply - (jsonrpc-reply server id :result `(:title ,reply)) - (jsonrpc-reply server id + (jsonrpc-reply server :result `(:title ,reply)) + (jsonrpc-reply server :error `(:code -32800 :message "User cancelled")))))) (cl-defmethod eglot-handle-notification - (_server (_method (eql :window/logMessage)) &key _type _message) + (_server (_method (eql window/logMessage)) &key _type _message) "Handle notification window/logMessage") ;; noop, use events buffer (cl-defmethod eglot-handle-notification - (_server (_method (eql :telemetry/event)) &rest _any) + (_server (_method (eql telemetry/event)) &rest _any) "Handle notification telemetry/event") ;; noop, use events buffer (defvar-local eglot--unreported-diagnostics nil "Unreported diagnostics for this buffer.") (cl-defmethod eglot-handle-notification - (server (_method (eql :textDocument/publishDiagnostics)) &key uri diagnostics) + (server (_method (eql textDocument/publishDiagnostics)) &key uri diagnostics) "Handle notification publishDiagnostics" (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) (with-current-buffer buffer @@ -750,7 +767,7 @@ Uses THING, FACE, DEFS and PREPEND." (setq eglot--unreported-diagnostics (cons t diags)))))) (jsonrpc--debug server "Diagnostics received for unvisited %s" uri))) -(cl-defun eglot--register-unregister (server jsonrpc-id things how) +(cl-defun eglot--register-unregister (server things how) "Helper for `registerCapability'. THINGS are either registrations or unregisterations." (dolist (thing (cl-coerce things 'list)) @@ -762,28 +779,28 @@ THINGS are either registrations or unregisterations." (unless (eq t (car retval)) (cl-return-from eglot--register-unregister (jsonrpc-reply - server jsonrpc-id + server :error `(:code -32601 :message ,(or (cadr retval) "sorry"))))))))) - (jsonrpc-reply server jsonrpc-id :result `(:message "OK"))) + (jsonrpc-reply server :result `(:message "OK"))) (cl-defmethod eglot-handle-request - (server id (_method (eql :client/registerCapability)) &key registrations) + (server (_method (eql client/registerCapability)) &key registrations) "Handle server request client/registerCapability" - (eglot--register-unregister server id registrations 'register)) + (eglot--register-unregister server registrations 'register)) (cl-defmethod eglot-handle-request - (server id (_method (eql :client/unregisterCapability)) + (server (_method (eql client/unregisterCapability)) &key unregisterations) ;; XXX: "unregisterations" (sic) "Handle server request client/unregisterCapability" - (eglot--register-unregister server id unregisterations 'unregister)) + (eglot--register-unregister server unregisterations 'unregister)) (cl-defmethod eglot-handle-request - (server id (_method (eql :workspace/applyEdit)) &key _label edit) + (server (_method (eql workspace/applyEdit)) &key _label edit) "Handle server request workspace/applyEdit" (condition-case err (progn (eglot--apply-workspace-edit edit 'confirm) - (jsonrpc-reply server id :result `(:applied ))) - (error (jsonrpc-reply server id + (jsonrpc-reply server :result `(:applied ))) + (error (jsonrpc-reply server :result `(:applied :json-false) :error `(:code -32001 :message (format "%s" ,err)))))) @@ -1348,7 +1365,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (and (equal "Indexing" what) done))))) (cl-defmethod eglot-handle-notification - ((server eglot-rls) (_method (eql :window/progress)) + ((server eglot-rls) (_method (eql window/progress)) &key id done title message &allow-other-keys) "Handle notification window/progress" (setf (eglot--spinner server) (list id title done message))) @@ -1367,17 +1384,17 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." :progressReportFrequencyMs -1))) (cl-defmethod eglot-handle-notification - ((_server eglot-cquery) (_method (eql :$cquery/progress)) + ((_server eglot-cquery) (_method (eql $cquery/progress)) &rest counts &key _activeThreads &allow-other-keys) "No-op for noisy $cquery/progress extension") (cl-defmethod eglot-handle-notification - ((_server eglot-cquery) (_method (eql :$cquery/setInactiveRegions)) + ((_server eglot-cquery) (_method (eql $cquery/setInactiveRegions)) &key _uri _inactiveRegions &allow-other-keys) "No-op for unsupported $cquery/setInactiveRegions extension") (cl-defmethod eglot-handle-notification - ((_server eglot-cquery) (_method (eql :$cquery/publishSemanticHighlighting)) + ((_server eglot-cquery) (_method (eql $cquery/publishSemanticHighlighting)) &key _uri _symbols &allow-other-keys) "No-op for unsupported $cquery/publishSemanticHighlighting extension") commit 34f10965a9ec00adf56a3cba55f53ae199d388c7 Author: João Távora Date: Wed Jun 6 12:04:39 2018 +0100 Slightly polish the flymake integration For backends, like RLS, that don't textDocument/publishDiagnostics right away, assume that the file is clean. Since Flymake allows multiple reportings, it should be OK. * eglot.el (eglot--unreported-diagnostics): Move variable up here. (eglot--maybe-activate-editing-mode): Assume no diagnostics on open. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5ede791035..81a63fdbea 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -900,6 +900,9 @@ If optional MARKERS, make markers." (add-hook 'eglot--managed-mode-hook 'flymake-mode) (add-hook 'eglot--managed-mode-hook 'eldoc-mode) +(defvar-local eglot--unreported-diagnostics nil + "Unreported Flymake diagnostics for this buffer.") + (defun eglot--maybe-activate-editing-mode (&optional server) "Maybe activate mode function `eglot--managed-mode'. If SERVER is supplied, do it only if BUFFER is managed by it. In @@ -908,6 +911,7 @@ that case, also signal textDocument/didOpen." (let* ((cur (and buffer-file-name (eglot--current-server))) (server (or (and (null server) cur) (and server (eq server cur) cur)))) (when server + (setq eglot--unreported-diagnostics `(:just-opened . nil)) (eglot--managed-mode-onoff server 1) (eglot--signal-textDocument/didOpen)))) @@ -1057,9 +1061,6 @@ function with the server still running." (_server (_method (eql :telemetry/event)) &rest _any) "Handle notification telemetry/event") ;; noop, use events buffer -(defvar-local eglot--unreported-diagnostics nil - "Unreported diagnostics for this buffer.") - (cl-defmethod eglot-handle-notification (server (_method (eql :textDocument/publishDiagnostics)) &key uri diagnostics) "Handle notification publishDiagnostics" commit ae85cee296c20d7c58866fafcba8755b159d4fab Author: João Távora Date: Wed Jun 6 11:09:43 2018 +0100 * eglot.el (eglot--make-process): use 'utf-8-emacs-unix Attempt to improve the situation reported in https://github.com/joaotavora/eglot/issues/14. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 950cf6ada8..5ede791035 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -258,7 +258,7 @@ CONTACT is in `eglot'. Returns a process object." (apply #'open-network-stream name stdout contact)) (t (make-process :name name :command contact :buffer stdout - :coding 'no-conversion :connection-type 'pipe + :coding 'utf-8-emacs-unix :connection-type 'pipe :stderr (setq stderr (format "*%s stderr*" name))))))) (process-put proc 'eglot-stderr stderr) (set-process-buffer proc (get-buffer-create stdout)) commit 41d646a830cac5b346de3fc015bbd8509c02c90b Merge: 77a3060610 b03cf2115b Author: João Távora Date: Tue Jun 5 19:13:46 2018 +0100 Merge master into jsonrpc-refactor (using imerge) commit b03cf2115b4f69903be5b653884c4d60dbf812f2 Author: João Távora Date: Tue Jun 5 17:28:03 2018 +0100 Shoosh compiler * eglot.el (eglot-server-ready-p): Use cl-defmethod diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 66ba03a6bd..950cf6ada8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1153,7 +1153,7 @@ THINGS are either registrations or unregisterations." (defvar-local eglot--recent-changes nil "Recent buffer changes as collected by `eglot--before-change'.") -(defmethod eglot-server-ready-p (_s _what) +(cl-defmethod eglot-server-ready-p (_s _what) "Normally ready if no outstanding changes." (not eglot--recent-changes)) (defvar-local eglot--change-idle-timer nil "Idle timer for didChange signals.") commit 24898b7d6056b856a22892ed816bfd161e4ade5f Author: João Távora Date: Tue Jun 5 17:26:38 2018 +0100 Cleanup the flymake 26.1 hack slightly * eglot.el (eglot-handle-notification): Use proper flymake diagnostic types. (eglot-code-actions): Use eglot--diag-data. (eglot--make-diag, eglot--diag-data): New aliases to `flymake-diagnostic-data' and `flymake-make-diagnostic'. (eglot-error eglot-warning eglot-note) (dolist eglot-error eglot-warning eglot-note): put flymake-overlay-control in these. (eglot-error eglot-warning eglot-note): put corresponding flymake-category. (horrible hack at the end): Move the Flymake 26.1 hack here. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0869bd52cb..66ba03a6bd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -983,38 +983,20 @@ Uses THING, FACE, DEFS and PREPEND." (add-to-list 'mode-line-misc-info `(eglot--managed-mode (" [" eglot--mode-line-format "] "))) - -;; FIXME: A horrible hack of Flymake's insufficient API that must go -;; into Emacs master, or better, 26.2 -(cl-defstruct (eglot--diag (:include flymake--diag) - (:constructor eglot--make-diag - (buffer beg end type text props))) - props) - -(advice-add 'flymake--highlight-line :after - (lambda (diag) - (when (cl-typep diag 'eglot--diag) - (let ((ov (cl-find diag - (overlays-at (flymake-diagnostic-beg diag)) - :key (lambda (ov) - (overlay-get ov 'flymake-diagnostic))))) - (cl-loop for (key . value) in (eglot--diag-props diag) - do (overlay-put ov key value))))) - '((name . eglot-hacking-in-some-per-diag-overlay-properties))) - - -(defun eglot--overlay-diag-props () - `((mouse-face . highlight) - (help-echo . (lambda (window _ov pos) - (with-selected-window window - (mapconcat - #'flymake-diagnostic-text - (flymake-diagnostics pos) - "\n")))) - (keymap . ,(let ((map (make-sparse-keymap))) - (define-key map [mouse-1] - (eglot--mouse-call 'eglot-code-actions)) - map)))) +(put 'eglot-note 'flymake-category 'flymake-note) +(put 'eglot-warning 'flymake-category 'flymake-warning) +(put 'eglot-error 'flymake-category 'flymake-error) + +(defalias 'eglot--make-diag 'flymake-make-diagnostic) +(defalias 'eglot--diag-data 'flymake-diagnostic-data) + +(dolist (type '(eglot-error eglot-warning eglot-note)) + (put type 'flymake-overlay-control + `((mouse-face . highlight) + (keymap . ,(let ((map (make-sparse-keymap))) + (define-key map [mouse-1] + (eglot--mouse-call 'eglot-code-actions)) + map))))) ;;; Protocol implementation (Requests, notifications, etc) @@ -1091,12 +1073,10 @@ function with the server still running." (setq message (concat source ": " message)) (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (eglot--make-diag (current-buffer) beg end - (cond ((<= sev 1) ':error) - ((= sev 2) ':warning) - (t ':note)) - message (cons - `(eglot-lsp-diag . ,diag-spec) - (eglot--overlay-diag-props))))) + (cond ((<= sev 1) 'eglot-error) + ((= sev 2) 'eglot-warning) + (t 'eglot-note)) + message `((eglot-lsp-diag . ,diag-spec))))) into diags finally (cond (eglot--current-flymake-report-fn (funcall eglot--current-flymake-report-fn diags) @@ -1605,10 +1585,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." `(:diagnostics [,@(mapcar (lambda (diag) (cdr (assoc 'eglot-lsp-diag - (eglot--diag-props diag)))) - (cl-remove-if-not - (lambda (diag) (cl-typep diag 'eglot--diag)) - (flymake-diagnostics beg end)))])))) + (eglot--diag-data diag)))) + (flymake-diagnostics beg end))])))) (menu-items (mapcar (eglot--lambda (&key title command arguments) `(,title . (:command ,command :arguments ,arguments))) actions)) @@ -1718,6 +1696,40 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." &key _uri _symbols &allow-other-keys) "No-op for unsupported $cquery/publishSemanticHighlighting extension") + +;; FIXME: A horrible hack of Flymake's insufficient API that must go +;; into Emacs master, or better, 26.2 +(when (version< emacs-version "27.0") + (cl-defstruct (eglot--diag (:include flymake--diag) + (:constructor eglot--make-diag-1)) + data-1) + (defsubst eglot--make-diag (buffer beg end type text data) + (let ((sym (alist-get type eglot--diag-error-types-to-old-types))) + (eglot--make-diag-1 :buffer buffer :beg beg :end end :type sym + :text text :data-1 data))) + (defsubst eglot--diag-data (diag) + (and (eglot--diag-p diag) (eglot--diag-data-1 diag))) + (defvar eglot--diag-error-types-to-old-types + '((eglot-error . :error) + (eglot-warning . :warning) + (eglot-note . :note))) + (advice-add + 'flymake--highlight-line :after + (lambda (diag) + (when (eglot--diag-p diag) + (let ((ov (cl-find diag + (overlays-at (flymake-diagnostic-beg diag)) + :key (lambda (ov) + (overlay-get ov 'flymake-diagnostic)))) + (overlay-properties + (get (car (rassoc (flymake-diagnostic-type diag) + eglot--diag-error-types-to-old-types)) + 'flymake-overlay-control))) + (cl-loop for (k . v) in overlay-properties + do (overlay-put ov k v))))) + '((name . eglot-hacking-in-some-per-diag-overlay-properties)))) + + (provide 'eglot) ;;; eglot.el ends here commit 6bda52d1ac5b90a2cbf402bcc2565f00249467da Author: João Távora Date: Mon Jun 4 12:49:28 2018 +0100 Support purposedly ignoring a server capability * eglot.el (eglot-ignored-server-capabilites): New defcustom. (eglot--server-capable): Use it. GitHub-reference: close https://github.com/joaotavora/eglot/issues/12 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index de8d990f3a..0869bd52cb 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -816,16 +816,26 @@ If optional MARKER, return a marker instead" (ignore-errors (funcall mode)) (insert string) (font-lock-ensure) (buffer-string)))) +(defcustom eglot-ignored-server-capabilites (list) + "LSP server capabilities that Eglot could use, but won't. +You could add, for instance, the symbol +`:documentHighlightProvider' to prevent automatic highlighting +under cursor." + :type '(repeat symbol)) + (defun eglot--server-capable (&rest feats) "Determine if current server is capable of FEATS." - (cl-loop for caps = (eglot--capabilities (eglot--current-server-or-lose)) - then (cadr probe) - for feat in feats - for probe = (plist-member caps feat) - if (not probe) do (cl-return nil) - if (eq (cadr probe) t) do (cl-return t) - if (eq (cadr probe) :json-false) do (cl-return nil) - finally (cl-return (or probe t)))) + (unless (cl-some (lambda (feat) + (memq feat eglot-ignored-server-capabilites)) + feats) + (cl-loop for caps = (eglot--capabilities (eglot--current-server-or-lose)) + then (cadr probe) + for feat in feats + for probe = (plist-member caps feat) + if (not probe) do (cl-return nil) + if (eq (cadr probe) t) do (cl-return t) + if (eq (cadr probe) :json-false) do (cl-return nil) + finally (cl-return (or probe t))))) (defun eglot--range-region (range &optional markers) "Return region (BEG . END) that represents LSP RANGE. commit b1b6be0c3ac20d9dd6910603849c755767869b69 Author: brotzeit Date: Mon Jun 4 12:50:01 2018 +0200 Fix typos Close https://github.com/joaotavora/eglot/issues/13. * eglot.el (eglot--all-major-modes) (eglot--notify, eglot--xref-reset-known-symbols): Fix typos. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6193b93e6d..de8d990f3a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -270,7 +270,7 @@ CONTACT is in `eglot'. Returns a process object." proc)) (defun eglot--all-major-modes () - "Return all know major modes." + "Return all known major modes." (let ((retval)) (mapatoms (lambda (sym) (when (plist-member (symbol-plist sym) 'derived-mode-parent) @@ -737,7 +737,7 @@ DEFERRED is passed to `eglot--async-request', which see." (cadr res))) (cl-defun eglot--notify (server method params) - "Notify SERVER of something, don't expect a reply.e" + "Notify SERVER of something, don't expect a reply." (eglot--send server `(:jsonrpc "2.0" :method ,method :params ,params))) (cl-defun eglot--reply (server id &key result error) @@ -1272,7 +1272,7 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." (defun eglot--xref-reset-known-symbols (&rest _dummy) "Reset `eglot--xref-reset-known-symbols'. -DUMMY is ignored" +DUMMY is ignored." (setq eglot--xref-known-symbols nil)) (advice-add 'xref-find-definitions :after #'eglot--xref-reset-known-symbols) commit 77a306061053dac632a67f68ab2c0eb4daf28397 Author: João Távora Date: Sat Jun 2 01:06:07 2018 +0100 Fix another merge-related bug in eglot-eldoc-function * eglot.el (eglot-eldoc-function): Correctly destructure eglot--range-region. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 64d06a3aa6..3f82c893fd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1138,7 +1138,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (when-buffer-window (mapcar (jsonrpc-lambda (&key range _kind _role) - (pcase-let ((`(,beg ,end) + (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (let ((ov (make-overlay beg end))) (overlay-put ov 'face 'highlight) commit 172c58f304292fbf4801dc31a10ede9f8e3198bf Author: João Távora Date: Sat Jun 2 00:58:10 2018 +0100 * eglot.el (eglot-eldoc-function): remove spurious log message diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 31ef081bd0..64d06a3aa6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1126,7 +1126,6 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (unless sig-showing (when-buffer-window (when-let (info (eglot--hover-info contents range)) - (eglot--message "OK so info is %S and %S" info (null info)) (eldoc-message info))))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) commit 7a5eb10f35d1cb45b8ae0b467ca938108264250f Author: João Távora Date: Fri Jun 1 17:58:00 2018 +0100 * eglot.el (version): bump to 0.8 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3c84297634..6193b93e6d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 0.7 +;; Version: 0.8 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot commit 7b040d4d1866b2be31ca4809b1bbb341921c8f73 Author: João Távora Date: Fri Jun 1 17:52:00 2018 +0100 Fix completionitem/resolve Should fix interoperation with company-quickhelp. * eglot.el (eglot-completion-at-point): Correctly pass properties to completionItem/resolve. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b133ffbbb2..3c84297634 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1372,7 +1372,9 @@ DUMMY is ignored" (mapcar (eglot--lambda (&rest all &key label insertText &allow-other-keys) (let ((insert (or insertText label))) - (add-text-properties 0 1 all insert) insert)) + (add-text-properties 0 1 all insert) + (put-text-property 0 1 'eglot--lsp-completion all insert) + insert)) items)))) :annotation-function (lambda (obj) @@ -1391,13 +1393,15 @@ DUMMY is ignored" (or (get-text-property 0 :sortText b) ""))))) :company-doc-buffer (lambda (obj) - (let ((documentation - (or (get-text-property 0 :documentation obj) - (and (eglot--server-capable :completionProvider - :resolveProvider) - (plist-get (eglot--request server :completionItem/resolve - (text-properties-at 0 obj)) - :documentation))))) + (let* ((documentation + (or (get-text-property 0 :documentation obj) + (and (eglot--server-capable :completionProvider + :resolveProvider) + (plist-get + (eglot--request server :completionItem/resolve + (get-text-property + 0 'eglot--lsp-completion obj)) + :documentation))))) (when documentation (with-current-buffer (get-buffer-create " *eglot doc*") (insert (eglot--format-markup documentation)) commit 8429c2c2feadf517aae798fa561b5c357cb1415e Author: João Távora Date: Fri Jun 1 17:04:17 2018 +0100 Explicitly trigger eldoc after workspace edits It's usually a nice thing to do. * eglot.el (eglot--apply-workspace-edit): Call eglot-eldoc-function. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8437d8cc8a..b133ffbbb2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1216,7 +1216,6 @@ Records START, END and PRE-CHANGE-LENGTH locally." (cl-loop for (beg end len text) in (reverse eglot--recent-changes) vconcat `[,(list :range `(:start ,beg :end ,end) :rangeLength len :text text)])))) - (setq eglot--recent-changes nil) (setf (eglot--spinner server) (list nil :textDocument/didChange t)) (eglot--call-deferred server)))) @@ -1554,6 +1553,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (pop prepared)))) (if prepared (eglot--warn "Caution: edits of files %s failed." (mapcar #'car prepared)) + (eglot-eldoc-function) (eglot--message "Edit successful!")))))) (defun eglot-rename (newname) commit 0e3d15f51c0a06751441c9392db0676a25ddf968 Author: João Távora Date: Fri Jun 1 16:59:00 2018 +0100 New command m-x eglot-code-actions Also available when left-clicking diagnostics. * README.md: Mention eglot-code-actions. Slightly rewrite differences to lsp-mode. * eglot.el (eglot-code-actions): New command. (eglot-handle-notification :textDocument/publishDiagnostics): Use eglot--make-diag and eglot--overlay-diag-props. (eglot--mode-line-props): Use eglot--mouse-call. (eglot--mouse-call): Renamed from eglot--mode-line-call. (eglot-client-capabilities): List :executeCommand and :codeAction as capabilities. (eglot--diag, advice-add flymake--highlight-line): Horrible hack. (eglot--overlay-diag-props): Horrible hack. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a991cb1f78..8437d8cc8a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -155,6 +155,8 @@ deferred to the future.") (list :workspace (list :applyEdit t + :executeCommand `(:dynamicRegistration :json-false) + :codeAction `(:dynamicRegistration :json-false) :workspaceEdit `(:documentChanges :json-false) :didChangeWatchesFiles `(:dynamicRegistration t) :symbol `(:dynamicRegistration :json-false)) @@ -908,12 +910,15 @@ that case, also signal textDocument/didOpen." (put 'eglot--mode-line-format 'risky-local-variable t) -(defun eglot--mode-line-call (what) +(defun eglot--mouse-call (what) "Make an interactive lambda for calling WHAT from mode-line." (lambda (event) (interactive "e") - (with-selected-window (posn-window (event-start event)) - (call-interactively what)))) + (let ((start (event-start event))) (with-selected-window (posn-window start) + (save-excursion + (goto-char (or (posn-point start) + (point))) + (call-interactively what)))))) (defun eglot--mode-line-props (thing face defs &optional prepend) "Helper for function `eglot--mode-line-format'. @@ -921,7 +926,7 @@ Uses THING, FACE, DEFS and PREPEND." (cl-loop with map = (make-sparse-keymap) for (elem . rest) on defs for (key def help) = elem - do (define-key map `[mode-line ,key] (eglot--mode-line-call def)) + do (define-key map `[mode-line ,key] (eglot--mouse-call def)) concat (format "%s: %s" key help) into blurb when rest concat "\n" into blurb finally (return `(:propertize ,thing @@ -968,6 +973,39 @@ Uses THING, FACE, DEFS and PREPEND." (add-to-list 'mode-line-misc-info `(eglot--managed-mode (" [" eglot--mode-line-format "] "))) + +;; FIXME: A horrible hack of Flymake's insufficient API that must go +;; into Emacs master, or better, 26.2 +(cl-defstruct (eglot--diag (:include flymake--diag) + (:constructor eglot--make-diag + (buffer beg end type text props))) + props) + +(advice-add 'flymake--highlight-line :after + (lambda (diag) + (when (cl-typep diag 'eglot--diag) + (let ((ov (cl-find diag + (overlays-at (flymake-diagnostic-beg diag)) + :key (lambda (ov) + (overlay-get ov 'flymake-diagnostic))))) + (cl-loop for (key . value) in (eglot--diag-props diag) + do (overlay-put ov key value))))) + '((name . eglot-hacking-in-some-per-diag-overlay-properties))) + + +(defun eglot--overlay-diag-props () + `((mouse-face . highlight) + (help-echo . (lambda (window _ov pos) + (with-selected-window window + (mapconcat + #'flymake-diagnostic-text + (flymake-diagnostics pos) + "\n")))) + (keymap . ,(let ((map (make-sparse-keymap))) + (define-key map [mouse-1] + (eglot--mouse-call 'eglot-code-actions)) + map)))) + ;;; Protocol implementation (Requests, notifications, etc) ;;; @@ -1037,16 +1075,18 @@ function with the server still running." (with-current-buffer buffer (cl-loop for diag-spec across diagnostics - collect (cl-destructuring-bind (&key range severity _group + collect (cl-destructuring-bind (&key range ((:severity sev)) _group _code source message) diag-spec + (setq message (concat source ": " message)) (pcase-let ((`(,beg . ,end) (eglot--range-region range))) - (flymake-make-diagnostic (current-buffer) - beg end - (cond ((<= severity 1) :error) - ((= severity 2) :warning) - (t :note)) - (concat source ": " message)))) + (eglot--make-diag (current-buffer) beg end + (cond ((<= sev 1) ':error) + ((= sev 2) ':warning) + (t ':note)) + message (cons + `(eglot-lsp-diag . ,diag-spec) + (eglot--overlay-diag-props))))) into diags finally (cond (eglot--current-flymake-report-fn (funcall eglot--current-flymake-report-fn diags) @@ -1528,6 +1568,52 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." :newName ,newname)) current-prefix-arg)) + +(defun eglot-code-actions (&optional beg end) + "Get and offer to execute code actions between BEG and END." + (interactive + (let (diags) + (cond ((region-active-p) (list (region-beginning) (region-end))) + ((setq diags (flymake-diagnostics (point))) + (list (cl-reduce #'min (mapcar #'flymake-diagnostic-beg diags)) + (cl-reduce #'max (mapcar #'flymake-diagnostic-end diags)))) + (t (list (point-min) (point-max)))))) + (unless (eglot--server-capable :codeActionProvider) + (eglot--error "Server can't execute code actions!")) + (let* ((server (eglot--current-server-or-lose)) + (actions (eglot--request + server + :textDocument/codeAction + (list :textDocument (eglot--TextDocumentIdentifier) + :range (list :start (eglot--pos-to-lsp-position beg) + :end (eglot--pos-to-lsp-position end)) + :context + `(:diagnostics + [,@(mapcar (lambda (diag) + (cdr (assoc 'eglot-lsp-diag + (eglot--diag-props diag)))) + (cl-remove-if-not + (lambda (diag) (cl-typep diag 'eglot--diag)) + (flymake-diagnostics beg end)))])))) + (menu-items (mapcar (eglot--lambda (&key title command arguments) + `(,title . (:command ,command :arguments ,arguments))) + actions)) + (menu (and menu-items `("Eglot code actions:" ("dummy" ,@menu-items)))) + (command-and-args + (and menu + (if (listp last-nonmenu-event) + (x-popup-menu last-nonmenu-event menu) + (let ((never-mind (gensym)) retval) + (setcdr (cadr menu) + (cons `("never mind..." . ,never-mind) (cdadr menu))) + (if (eq (setq retval (tmm-prompt menu)) never-mind) + (keyboard-quit) + retval)))))) + (if command-and-args + (eglot--request server :workspace/executeCommand command-and-args) + (eglot--message "No code actions here")))) + + ;;; Dynamic registration ;;; commit 5a8d94c76834d510a86e35588113c38355204068 Author: João Távora Date: Fri Jun 1 16:09:19 2018 +0100 Revert an unfinished feature that made it to the last commit commit ee6ab89666704232f744d6262040979b0142c44b Author: João Távora Date: Fri Jun 1 14:43:30 2018 +0100 Add MELPA badge diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3dfe159762..a991cb1f78 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -908,15 +908,12 @@ that case, also signal textDocument/didOpen." (put 'eglot--mode-line-format 'risky-local-variable t) -(defun eglot--mouse-call (what) +(defun eglot--mode-line-call (what) "Make an interactive lambda for calling WHAT from mode-line." (lambda (event) (interactive "e") - (let ((start (event-start event))) (with-selected-window (posn-window start) - (save-excursion - (goto-char (or (posn-point start) - (point))) - (call-interactively what)))))) + (with-selected-window (posn-window (event-start event)) + (call-interactively what)))) (defun eglot--mode-line-props (thing face defs &optional prepend) "Helper for function `eglot--mode-line-format'. @@ -924,7 +921,7 @@ Uses THING, FACE, DEFS and PREPEND." (cl-loop with map = (make-sparse-keymap) for (elem . rest) on defs for (key def help) = elem - do (define-key map `[mode-line ,key] (eglot--mouse-call def)) + do (define-key map `[mode-line ,key] (eglot--mode-line-call def)) concat (format "%s: %s" key help) into blurb when rest concat "\n" into blurb finally (return `(:propertize ,thing @@ -971,41 +968,6 @@ Uses THING, FACE, DEFS and PREPEND." (add-to-list 'mode-line-misc-info `(eglot--managed-mode (" [" eglot--mode-line-format "] "))) - -;; A horrible hack of Flymake's insufficient API that must go into -;; Emacs master, or better, 26.2 -(cl-defstruct (eglot--diag (:include flymake--diag) - (:constructor eglot--make-diag - (buffer beg end type text props))) - props) -(advice-add 'flymake--highlight-line :after - (lambda (diag) - (when (cl-typep diag 'eglot--diag) - (let ((ov (cl-find diag - (overlays-at (flymake-diagnostic-beg diag)) - :key (lambda (ov) - (overlay-get ov 'flymake-diagnostic))))) - (cl-loop for (key . value) in (eglot--diag-props diag) - do (overlay-put ov key value))))) - '((name . eglot-hacking-in-some-per-diag-overlay-properties))) - - -(defun eglot--overlay-diag-props () - `((mouse-face . highlight) - (help-echo . (lambda (window _ov pos) - (with-selected-window window - (concat (mapconcat - #'flymake-diagnostic-text - (flymake-diagnostics pos) - "\n") - "\nmouse-1: Get LSP code actions")))) - (keymap . ,(let ((map (make-sparse-keymap))) - (define-key map [mouse-1] - (eglot--mouse-call 'eglot-get-code-actions)) - map)))) - - - ;;; Protocol implementation (Requests, notifications, etc) ;;; @@ -1075,18 +1037,16 @@ function with the server still running." (with-current-buffer buffer (cl-loop for diag-spec across diagnostics - collect (cl-destructuring-bind (&key range ((:severity sev)) _group + collect (cl-destructuring-bind (&key range severity _group _code source message) diag-spec - (setq message (concat source ": " message)) (pcase-let ((`(,beg . ,end) (eglot--range-region range))) - (eglot--make-diag (current-buffer) beg end - (cond ((<= sev 1) ':error) - ((= sev 2) ':warning) - (t ':note)) - message (cons - `(eglot-lsp-diag . ,diag-spec) - (eglot--overlay-diag-props))))) + (flymake-make-diagnostic (current-buffer) + beg end + (cond ((<= severity 1) :error) + ((= severity 2) :warning) + (t :note)) + (concat source ": " message)))) into diags finally (cond (eglot--current-flymake-report-fn (funcall eglot--current-flymake-report-fn diags) @@ -1568,44 +1528,6 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." :newName ,newname)) current-prefix-arg)) - -(defun eglot-get-code-actions (&optional beg end) - "Get code actions between BEG and END." - (interactive - (let (diags) - (cond ((region-active-p) (list (region-beginning) (region-end))) - ((setq diags (flymake-diagnostics (point))) - (list (cl-reduce #'min (mapcar #'flymake-diagnostic-beg diags)) - (cl-reduce #'max (mapcar #'flymake-diagnostic-end diags)))) - (t (list (point-min) (point-max)))))) - (let* ((actions (eglot--request - (eglot--current-server-or-lose) - :textDocument/codeAction - (list :textDocument (eglot--TextDocumentIdentifier) - :range (list :start (eglot--pos-to-lsp-position beg) - :end (eglot--pos-to-lsp-position end)) - :context - `(:diagnostics - [,@(mapcar (lambda (diag) - (cdr (assoc 'eglot-lsp-diag - (eglot--diag-props diag)))) - (cl-remove-if-not - (lambda (diag) (cl-typep diag 'eglot--diag)) - (flymake-diagnostics beg end)))])))) - (menu (let ((map (make-sparse-keymap))) - (mapc (eglot--lambda (&key title command _arguments) - (define-key map (vector (intern command)) - `(,title dummy))) - actions) - (setq map `(keymap "Code actions here:" ,@(cdr map))))) - (command-sym (car - (if (listp last-nonmenu-event) - (x-popup-menu last-nonmenu-event menu) - (tmm-prompt menu)))) - - ) - (message "would be applying %S" command-sym))) - ;;; Dynamic registration ;;; @@ -1679,9 +1601,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (let* ((root (car (project-roots (eglot--project server)))) (cache (expand-file-name ".cquery_cached_index/" root))) (list :cacheDirectory (file-name-as-directory cache) - :progressReportFrequencyMs -1 - :discoverSystemIncludes :json-false - :enableIndexOnDidChange t))) + :progressReportFrequencyMs -1))) (cl-defmethod eglot-handle-notification ((_server eglot-cquery) (_method (eql :$cquery/progress)) commit ee6ab89666704232f744d6262040979b0142c44b Author: João Távora Date: Fri Jun 1 14:43:30 2018 +0100 Add melpa badge * README.mdown: Now in MELPA too diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a991cb1f78..3dfe159762 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -908,12 +908,15 @@ that case, also signal textDocument/didOpen." (put 'eglot--mode-line-format 'risky-local-variable t) -(defun eglot--mode-line-call (what) +(defun eglot--mouse-call (what) "Make an interactive lambda for calling WHAT from mode-line." (lambda (event) (interactive "e") - (with-selected-window (posn-window (event-start event)) - (call-interactively what)))) + (let ((start (event-start event))) (with-selected-window (posn-window start) + (save-excursion + (goto-char (or (posn-point start) + (point))) + (call-interactively what)))))) (defun eglot--mode-line-props (thing face defs &optional prepend) "Helper for function `eglot--mode-line-format'. @@ -921,7 +924,7 @@ Uses THING, FACE, DEFS and PREPEND." (cl-loop with map = (make-sparse-keymap) for (elem . rest) on defs for (key def help) = elem - do (define-key map `[mode-line ,key] (eglot--mode-line-call def)) + do (define-key map `[mode-line ,key] (eglot--mouse-call def)) concat (format "%s: %s" key help) into blurb when rest concat "\n" into blurb finally (return `(:propertize ,thing @@ -968,6 +971,41 @@ Uses THING, FACE, DEFS and PREPEND." (add-to-list 'mode-line-misc-info `(eglot--managed-mode (" [" eglot--mode-line-format "] "))) + +;; A horrible hack of Flymake's insufficient API that must go into +;; Emacs master, or better, 26.2 +(cl-defstruct (eglot--diag (:include flymake--diag) + (:constructor eglot--make-diag + (buffer beg end type text props))) + props) +(advice-add 'flymake--highlight-line :after + (lambda (diag) + (when (cl-typep diag 'eglot--diag) + (let ((ov (cl-find diag + (overlays-at (flymake-diagnostic-beg diag)) + :key (lambda (ov) + (overlay-get ov 'flymake-diagnostic))))) + (cl-loop for (key . value) in (eglot--diag-props diag) + do (overlay-put ov key value))))) + '((name . eglot-hacking-in-some-per-diag-overlay-properties))) + + +(defun eglot--overlay-diag-props () + `((mouse-face . highlight) + (help-echo . (lambda (window _ov pos) + (with-selected-window window + (concat (mapconcat + #'flymake-diagnostic-text + (flymake-diagnostics pos) + "\n") + "\nmouse-1: Get LSP code actions")))) + (keymap . ,(let ((map (make-sparse-keymap))) + (define-key map [mouse-1] + (eglot--mouse-call 'eglot-get-code-actions)) + map)))) + + + ;;; Protocol implementation (Requests, notifications, etc) ;;; @@ -1037,16 +1075,18 @@ function with the server still running." (with-current-buffer buffer (cl-loop for diag-spec across diagnostics - collect (cl-destructuring-bind (&key range severity _group + collect (cl-destructuring-bind (&key range ((:severity sev)) _group _code source message) diag-spec + (setq message (concat source ": " message)) (pcase-let ((`(,beg . ,end) (eglot--range-region range))) - (flymake-make-diagnostic (current-buffer) - beg end - (cond ((<= severity 1) :error) - ((= severity 2) :warning) - (t :note)) - (concat source ": " message)))) + (eglot--make-diag (current-buffer) beg end + (cond ((<= sev 1) ':error) + ((= sev 2) ':warning) + (t ':note)) + message (cons + `(eglot-lsp-diag . ,diag-spec) + (eglot--overlay-diag-props))))) into diags finally (cond (eglot--current-flymake-report-fn (funcall eglot--current-flymake-report-fn diags) @@ -1528,6 +1568,44 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." :newName ,newname)) current-prefix-arg)) + +(defun eglot-get-code-actions (&optional beg end) + "Get code actions between BEG and END." + (interactive + (let (diags) + (cond ((region-active-p) (list (region-beginning) (region-end))) + ((setq diags (flymake-diagnostics (point))) + (list (cl-reduce #'min (mapcar #'flymake-diagnostic-beg diags)) + (cl-reduce #'max (mapcar #'flymake-diagnostic-end diags)))) + (t (list (point-min) (point-max)))))) + (let* ((actions (eglot--request + (eglot--current-server-or-lose) + :textDocument/codeAction + (list :textDocument (eglot--TextDocumentIdentifier) + :range (list :start (eglot--pos-to-lsp-position beg) + :end (eglot--pos-to-lsp-position end)) + :context + `(:diagnostics + [,@(mapcar (lambda (diag) + (cdr (assoc 'eglot-lsp-diag + (eglot--diag-props diag)))) + (cl-remove-if-not + (lambda (diag) (cl-typep diag 'eglot--diag)) + (flymake-diagnostics beg end)))])))) + (menu (let ((map (make-sparse-keymap))) + (mapc (eglot--lambda (&key title command _arguments) + (define-key map (vector (intern command)) + `(,title dummy))) + actions) + (setq map `(keymap "Code actions here:" ,@(cdr map))))) + (command-sym (car + (if (listp last-nonmenu-event) + (x-popup-menu last-nonmenu-event menu) + (tmm-prompt menu)))) + + ) + (message "would be applying %S" command-sym))) + ;;; Dynamic registration ;;; @@ -1601,7 +1679,9 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (let* ((root (car (project-roots (eglot--project server)))) (cache (expand-file-name ".cquery_cached_index/" root))) (list :cacheDirectory (file-name-as-directory cache) - :progressReportFrequencyMs -1))) + :progressReportFrequencyMs -1 + :discoverSystemIncludes :json-false + :enableIndexOnDidChange t))) (cl-defmethod eglot-handle-notification ((_server eglot-cquery) (_method (eql :$cquery/progress)) commit 07d71f4ec75cc60634d36fff25ffc362be3dad72 Author: João Távora Date: Fri Jun 1 14:12:54 2018 +0100 Prevent possible cquery choke on :initializationoptions Hopefully help debug https://github.com/joaotavora/eglot/issues/10. * eglot.el (eglot-initialization-options): Use `list' diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cc5649fa38..a991cb1f78 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1600,8 +1600,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." "Passes through required cquery initialization options" (let* ((root (car (project-roots (eglot--project server)))) (cache (expand-file-name ".cquery_cached_index/" root))) - (vector :cacheDirectory (file-name-as-directory cache) - :progressReportFrequencyMs -1))) + (list :cacheDirectory (file-name-as-directory cache) + :progressReportFrequencyMs -1))) (cl-defmethod eglot-handle-notification ((_server eglot-cquery) (_method (eql :$cquery/progress)) commit 97b07351560ce5c7416c3cbaec6924d12ac6c84a Merge: 8a7d17b5ea db5dc348a1 Author: João Távora Date: Wed May 30 16:27:30 2018 +0100 Merge master into jsonrpc-refactor (using imerge) commit db5dc348a1b12a124de05be05cbbbce412cf5c2b Author: João Távora Date: Wed May 30 03:20:54 2018 +0100 * eglot.el (version): bump to 0.7 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6a7ba6bad4..cc5649fa38 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 0.6 +;; Version: 0.7 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot commit 84189937d42a581a1bfa44252caf9b3cb1d6b4a4 Author: João Távora Date: Mon May 28 23:07:56 2018 +0100 More yak shaving * eglot.el (eglot--with-live-buffer, eglot--widening): New macros. (eglot--lambda): Move up here. (eglot--process-filter): Simplify with eglot--with-live-buffer. (eglot--async-request): Simplify with eglot--with-live-buffer. (eglot--TextDocumentItem): Simplify with eglot--widening. (eglot--signal-textDocument/didChange, eglot--apply-text-edits): Simplify with eglot--widening. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f3bf2b7b03..6a7ba6bad4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -118,6 +118,22 @@ lasted more than that many seconds." ;;; API (WORK-IN-PROGRESS!) ;;; +(cl-defmacro eglot--with-live-buffer (buf &rest body) + "Check BUF live, then do BODY in it." (declare (indent 1) (debug t)) + (let ((b (cl-gensym))) + `(let ((,b ,buf)) (if (buffer-live-p ,b) (with-current-buffer ,b ,@body))))) + +(cl-defmacro eglot--lambda (cl-lambda-list &body body) + "Make a unary function of ARG, a plist-like JSON object. +CL-LAMBDA-LIST destructures ARGS before running BODY." + (declare (indent 1) (debug (sexp &rest form))) + (let ((e (gensym "eglot--lambda-elem"))) + `(lambda (,e) (apply (cl-function (lambda ,cl-lambda-list ,@body)) ,e)))) + +(cl-defmacro eglot--widening (&rest body) + "Save excursion and restriction. Widen. Then run BODY." (declare (debug t)) + `(save-excursion (save-restriction (widen) ,@body))) + (cl-defgeneric eglot-server-ready-p (server what) ;; API "Tell if SERVER is ready for WHAT in current buffer. If it isn't, a deferrable `eglot--async-request' *will* be @@ -464,67 +480,64 @@ INTERACTIVE is t if called interactively." (defun eglot--process-filter (proc string) "Called when new data STRING has arrived for PROC." - (when (buffer-live-p (process-buffer proc)) - (with-current-buffer (process-buffer proc) - (let ((inhibit-read-only t) - (expected-bytes (process-get proc 'eglot-expected-bytes))) - ;; Insert the text, advancing the process marker. - ;; - (save-excursion - (goto-char (process-mark proc)) - (insert string) - (set-marker (process-mark proc) (point))) - ;; Loop (more than one message might have arrived) - ;; - (unwind-protect - (let (done) - (while (not done) - (cond - ((not expected-bytes) - ;; Starting a new message - ;; - (setq expected-bytes - (and (search-forward-regexp - "\\(?:.*: .*\r\n\\)*Content-Length: \ + (eglot--with-live-buffer (process-buffer proc) + (let ((expected-bytes (process-get proc 'eglot-expected-bytes)) + (inhibit-read-only t) done) + ;; Insert the text, advancing the process marker. + ;; + (save-excursion + (goto-char (process-mark proc)) + (insert string) + (set-marker (process-mark proc) (point))) + ;; Loop (more than one message might have arrived) + ;; + (unwind-protect + (while (not done) + (cond ((not expected-bytes) + ;; Starting a new message + ;; + (setq expected-bytes + (and (search-forward-regexp + "\\(?:.*: .*\r\n\\)*Content-Length: \ *\\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" - (+ (point) 100) - t) - (string-to-number (match-string 1)))) - (unless expected-bytes - (setq done :waiting-for-new-message))) - (t - ;; Attempt to complete a message body - ;; - (let ((available-bytes (- (position-bytes (process-mark proc)) - (position-bytes (point))))) - (cond - ((>= available-bytes - expected-bytes) - (let* ((message-end (byte-to-position - (+ (position-bytes (point)) - expected-bytes)))) - (unwind-protect - (save-restriction - (narrow-to-region (point) message-end) - (let* ((json-object-type 'plist) - (json-message (json-read))) - ;; Process content in another buffer, - ;; shielding buffer from tamper - ;; - (with-temp-buffer - (eglot--server-receive - (process-get proc 'eglot-server) - json-message)))) - (goto-char message-end) - (delete-region (point-min) (point)) - (setq expected-bytes nil)))) - (t - ;; Message is still incomplete - ;; - (setq done :waiting-for-more-bytes-in-this-message)))))))) - ;; Saved parsing state for next visit to this filter - ;; - (process-put proc 'eglot-expected-bytes expected-bytes)))))) + (+ (point) 100) + t) + (string-to-number (match-string 1)))) + (unless expected-bytes + (setq done :waiting-for-new-message))) + (t + ;; Attempt to complete a message body + ;; + (let ((available-bytes (- (position-bytes (process-mark proc)) + (position-bytes (point))))) + (cond + ((>= available-bytes + expected-bytes) + (let* ((message-end (byte-to-position + (+ (position-bytes (point)) + expected-bytes)))) + (unwind-protect + (save-restriction + (narrow-to-region (point) message-end) + (let* ((json-object-type 'plist) + (json-message (json-read))) + ;; Process content in another buffer, + ;; shielding buffer from tamper + ;; + (with-temp-buffer + (eglot--server-receive + (process-get proc 'eglot-server) + json-message)))) + (goto-char message-end) + (delete-region (point-min) (point)) + (setq expected-bytes nil)))) + (t + ;; Message is still incomplete + ;; + (setq done :waiting-for-more-bytes-in-this-message))))))) + ;; Saved parsing state for next visit to this filter + ;; + (process-put proc 'eglot-expected-bytes expected-bytes))))) (defun eglot-events-buffer (server &optional interactive) "Display events buffer for current LSP SERVER. @@ -631,11 +644,6 @@ originated." (eglot--debug server `(:maybe-run-deferred ,(mapcar #'caddr actions))) (mapc #'funcall (mapcar #'car actions)))) -(cl-defmacro eglot--lambda (cl-lambda-list &body body) - (declare (indent 1) (debug (sexp &rest form))) - (let ((e (gensym "eglot--lambda-elem"))) - `(lambda (,e) (apply (cl-function (lambda ,cl-lambda-list ,@body)) ,e)))) - (defvar-local eglot--next-request-id 0 "ID for next `eglot--async-request'.") (cl-defun eglot--async-request (server @@ -652,7 +660,7 @@ nullary TIMEOUT-FN. If DEFERRED, maybe defer request to the future, or to never at all, in case a new request with identical DEFERRED and for the same buffer overrides it (however, if that happens, the original timer keeps counting). Return (ID TIMER)." - (pcase-let* ( (buf (current-buffer)) (pos (point-marker)) + (pcase-let* ( (buf (current-buffer)) (`(,_ ,timer ,old-id) (and deferred (gethash (list deferred buf) (eglot--deferred-actions server)))) @@ -675,12 +683,9 @@ happens, the original timer keeps counting). Return (ID TIMER)." ;; Also, if it's the first deferring for this id, inform the log (eglot--debug server `(:deferring ,method :id ,id :params ,params))) (puthash (list deferred buf) - (list (lambda () (when (buffer-live-p buf) - (with-current-buffer buf - (save-excursion - (goto-char pos) - (apply #'eglot--async-request server - method params args))))) + (list (lambda () (eglot--with-live-buffer buf + (apply #'eglot--async-request server + method params args))) (or timer (funcall make-timer)) id) (eglot--deferred-actions server)) (cl-return-from eglot--async-request nil))) @@ -741,7 +746,7 @@ DEFERRED is passed to `eglot--async-request', which see." ,@(when error `(:error ,error))))) -;;; Helpers +;;; Helpers (move these to API?) ;;; (defun eglot--error (format &rest args) "Error out with FORMAT with ARGS." @@ -1107,9 +1112,8 @@ THINGS are either registrations or unregisterations." (match-string 1 (symbol-name major-mode)) "unknown") :text - (save-restriction - (widen) - (buffer-substring-no-properties (point-min) (point-max)))))) + (eglot--widening + (buffer-substring-no-properties (point-min) (point-max)))))) (defun eglot--TextDocumentPositionParams () "Compute TextDocumentPositionParams." @@ -1148,11 +1152,10 @@ Records START, END and PRE-CHANGE-LENGTH locally." (let ((buf (current-buffer))) (setq eglot--change-idle-timer (run-with-idle-timer - 0.5 nil (lambda () (when (buffer-live-p buf) - (with-current-buffer buf - (when eglot--managed-mode - (eglot--signal-textDocument/didChange) - (setq eglot--change-idle-timer nil))))))))) + 0.5 nil (lambda () (eglot--with-live-buffer buf + (when eglot--managed-mode + (eglot--signal-textDocument/didChange) + (setq eglot--change-idle-timer nil)))))))) (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." @@ -1161,19 +1164,19 @@ Records START, END and PRE-CHANGE-LENGTH locally." (sync-kind (eglot--server-capable :textDocumentSync)) (full-sync-p (or (eq sync-kind 1) (eq :emacs-messup eglot--recent-changes)))) - (save-restriction - (widen) - (eglot--notify - server :textDocument/didChange - (list - :textDocument (eglot--VersionedTextDocumentIdentifier) - :contentChanges - (if full-sync-p - (vector `(:text ,(buffer-substring-no-properties (point-min) - (point-max)))) - (cl-loop for (beg end len text) in (reverse eglot--recent-changes) - vconcat `[,(list :range `(:start ,beg :end ,end) - :rangeLength len :text text)]))))) + (eglot--notify + server :textDocument/didChange + (list + :textDocument (eglot--VersionedTextDocumentIdentifier) + :contentChanges + (if full-sync-p + (vector `(:text ,(eglot--widening + (buffer-substring-no-properties (point-min) + (point-max))))) + (cl-loop for (beg end len text) in (reverse eglot--recent-changes) + vconcat `[,(list :range `(:start ,beg :end ,end) + :rangeLength len :text text)])))) + (setq eglot--recent-changes nil) (setf (eglot--spinner server) (list nil :textDocument/didChange t)) (eglot--call-deferred server)))) @@ -1478,14 +1481,12 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (unless (or (not version) (equal version eglot--versioned-identifier)) (eglot--error "Edits on `%s' require version %d, you have %d" (current-buffer) version eglot--versioned-identifier)) - (save-restriction - (widen) - (save-excursion - (mapc (pcase-lambda (`(,newText ,beg . ,end)) - (goto-char beg) (delete-region beg end) (insert newText)) - (mapcar (eglot--lambda (&key range newText) - (cons newText (eglot--range-region range 'markers))) - edits)))) + (eglot--widening + (mapc (pcase-lambda (`(,newText ,beg . ,end)) + (goto-char beg) (delete-region beg end) (insert newText)) + (mapcar (eglot--lambda (&key range newText) + (cons newText (eglot--range-region range 'markers))) + edits))) (eglot--message "%s: Performed %s edits" (current-buffer) (length edits))) (defun eglot--apply-workspace-edit (wedit &optional confirm) commit 9ad9651c4f58b3fb1355cc91e30fae385dbc43e6 Author: João Távora Date: Mon May 28 22:30:01 2018 +0100 New m-x eglot-stderr-buffer useful for debugging * eglot.el (eglot--make-process): Save stderr buffer in process. (eglot-stderr-buffer): New interactive command. (eglot--mode-line-format): Bind C-mouse-1 to new eglot-stderr-buffer. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d75499348f..f3bf2b7b03 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -233,22 +233,21 @@ DEFERRED request from BUF, to be sent not later than TIMER as ID." "Make a process object from CONTACT. NAME is used to name the the started process or connection. CONTACT is in `eglot'. Returns a process object." - (let* ((buffer (get-buffer-create (format "*%s stdout*" name))) + (let* ((stdout (format "*%s stdout*" name)) stderr (proc (cond ((processp contact) contact) ((integerp (cadr contact)) - (apply #'open-network-stream name buffer contact)) + (apply #'open-network-stream name stdout contact)) (t (make-process - :name name - :command contact - :coding 'no-conversion - :connection-type 'pipe - :stderr (get-buffer-create (format "*%s stderr*" name))))))) - (set-process-buffer proc buffer) - (set-marker (process-mark proc) (with-current-buffer buffer (point-min))) + :name name :command contact :buffer stdout + :coding 'no-conversion :connection-type 'pipe + :stderr (setq stderr (format "*%s stderr*" name))))))) + (process-put proc 'eglot-stderr stderr) + (set-process-buffer proc (get-buffer-create stdout)) + (set-marker (process-mark proc) (with-current-buffer stdout (point-min))) (set-process-filter proc #'eglot--process-filter) (set-process-sentinel proc #'eglot--process-sentinel) - (with-current-buffer buffer + (with-current-buffer stdout (let ((inhibit-read-only t)) (erase-buffer) (read-only-mode t))) proc)) @@ -544,6 +543,12 @@ INTERACTIVE is t if called interactively." (when interactive (display-buffer buffer)) buffer)) +(defun eglot-stderr-buffer (server) + "Pop to stderr of SERVER, if it exists, else error." + (interactive (list (eglot--current-server-or-lose))) + (if-let ((b (process-get (eglot--process server) 'eglot-stderr))) + (pop-to-buffer b) (user-error "[eglot] No stderr buffer!"))) + (defun eglot--log-event (server message &optional type) "Log an eglot-related event. SERVER is the current server. MESSAGE is a JSON-like plist. @@ -934,26 +939,25 @@ Uses THING, FACE, DEFS and PREPEND." (when name `(":" ,(eglot--mode-line-props name 'eglot-mode-line - '((mouse-1 eglot-events-buffer "go to events buffer") + '((C-mouse-1 eglot-stderr-buffer "go to stderr buffer") + (mouse-1 eglot-events-buffer "go to events buffer") (mouse-2 eglot-shutdown "quit server") (mouse-3 eglot-reconnect "reconnect to server"))) ,@(when serious-p `("/" ,(eglot--mode-line-props "error" 'compilation-mode-line-fail - '((mouse-1 eglot-events-buffer "go to events buffer") - (mouse-3 eglot-clear-status "clear this status")) + '((mouse-3 eglot-clear-status "clear this status")) (format "An error occured: %s\n" status)))) ,@(when (and doing (not done-p)) `("/" ,(eglot--mode-line-props (format "%s%s" doing (if detail (format ":%s" detail) "")) - 'compilation-mode-line-run - '((mouse-1 eglot-events-buffer "go to events buffer"))))) + 'compilation-mode-line-run '()))) ,@(when (cl-plusp pending) `("/" ,(eglot--mode-line-props (format "%d" pending) 'warning - '((mouse-1 eglot-events-buffer "go to events buffer") - (mouse-3 eglot-clear-status "clear this status")) + '((mouse-3 eglot-forget-pending-continuations + "forget these continuations")) (format "%d pending requests\n" pending))))))))) (add-to-list 'mode-line-misc-info commit 1febf627146fdb294bd9e04ef9ce1782a1c1ad27 Author: João Távora Date: Sun May 27 19:45:52 2018 +0100 On reconnection, ignore errors of shutting down hung server * eglot.el (eglot, eglot-reconnect): Ignore any errors on shutdown. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8ab4b496e2..d75499348f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -397,7 +397,7 @@ INTERACTIVE is t if called interactively." (eglot-reconnect current-server interactive) (when (and current-server (process-live-p (eglot--process current-server))) - (eglot-shutdown current-server)) + (ignore-errors (eglot-shutdown current-server))) (let ((server (eglot--connect project managed-major-mode command @@ -413,7 +413,7 @@ managing `%s' buffers in project `%s'." INTERACTIVE is t if called interactively." (interactive (list (eglot--current-server-or-lose) t)) (when (process-live-p (eglot--process server)) - (eglot-shutdown server interactive)) + (ignore-errors (eglot-shutdown server interactive))) (eglot--connect (eglot--project server) (eglot--major-mode server) (eglot--contact server) commit 7652fd090993f523fcf249fa8f2ed70ccfe784b7 Author: João Távora Date: Sun May 27 16:51:15 2018 +0100 Be more criterious before running the idle timer * eglot.el (eglot--change-idle-timer): make a defvar-local (eglot--after-change): Only run timer if the buffer is live. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c48aa2bf83..8ab4b496e2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1118,7 +1118,7 @@ THINGS are either registrations or unregisterations." (defmethod eglot-server-ready-p (_s _what) "Normally ready if no outstanding changes." (not eglot--recent-changes)) -(defvar eglot--change-idle-timer nil "Idle timer for textDocument/didChange.") +(defvar-local eglot--change-idle-timer nil "Idle timer for didChange signals.") (defun eglot--before-change (start end) "Hook onto `before-change-functions'. @@ -1141,10 +1141,14 @@ Records START, END and PRE-CHANGE-LENGTH locally." `(,pre-change-length ,(buffer-substring-no-properties start end))) (setf eglot--recent-changes :emacs-messup)) (when eglot--change-idle-timer (cancel-timer eglot--change-idle-timer)) - (setq eglot--change-idle-timer - (run-with-idle-timer - 0.5 nil (lambda () (eglot--signal-textDocument/didChange) - (setq eglot--change-idle-timer nil))))) + (let ((buf (current-buffer))) + (setq eglot--change-idle-timer + (run-with-idle-timer + 0.5 nil (lambda () (when (buffer-live-p buf) + (with-current-buffer buf + (when eglot--managed-mode + (eglot--signal-textDocument/didChange) + (setq eglot--change-idle-timer nil))))))))) (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." commit 0c80d78b6d4815c8ca202fb6146e7f78721428a8 Author: João Távora Date: Sun May 27 15:34:50 2018 +0100 Make eglot--recent-changes a simpler list * eglot.el (eglot-server-ready-p): Don't add default method here. (eglot-server-ready-p): Do it here. (eglot--outstanding-edits-p): Remove. (eglot--before-change, eglot--after-change) (eglot--signal-textDocument/didChange): Use eglot--recent-changes as a list. Simplify. (eglot--signal-textDocument/didOpen): Use eglot--recent-changes as a list. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index dde0c22f83..c48aa2bf83 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -121,9 +121,7 @@ lasted more than that many seconds." (cl-defgeneric eglot-server-ready-p (server what) ;; API "Tell if SERVER is ready for WHAT in current buffer. If it isn't, a deferrable `eglot--async-request' *will* be -deferred to the future." - (:method (_s _what) "Normally ready if no outstanding changes." - (not (eglot--outstanding-edits-p)))) +deferred to the future.") (cl-defgeneric eglot-handle-request (server method id &rest params) "Handle SERVER's METHOD request with ID and PARAMS.") @@ -1117,10 +1115,8 @@ THINGS are either registrations or unregisterations." (defvar-local eglot--recent-changes nil "Recent buffer changes as collected by `eglot--before-change'.") -(defun eglot--outstanding-edits-p () - "Non-nil if there are outstanding edits." - (cl-plusp (+ (length (car eglot--recent-changes)) - (length (cdr eglot--recent-changes))))) +(defmethod eglot-server-ready-p (_s _what) + "Normally ready if no outstanding changes." (not eglot--recent-changes)) (defvar eglot--change-idle-timer nil "Idle timer for textDocument/didChange.") @@ -1130,19 +1126,20 @@ Records START and END, crucially convert them into LSP (line/char) positions before that information is lost (because the after-change thingy doesn't know if newlines were deleted/added)" - (setf (car eglot--recent-changes) - (vconcat (car eglot--recent-changes) - `[(,(eglot--pos-to-lsp-position start) - ,(eglot--pos-to-lsp-position end))]))) + (when (listp eglot--recent-changes) + (push `(,(eglot--pos-to-lsp-position start) + ,(eglot--pos-to-lsp-position end)) + eglot--recent-changes))) (defun eglot--after-change (start end pre-change-length) "Hook onto `after-change-functions'. Records START, END and PRE-CHANGE-LENGTH locally." (cl-incf eglot--versioned-identifier) - (setf (cdr eglot--recent-changes) - (vconcat (cdr eglot--recent-changes) - `[(,pre-change-length - ,(buffer-substring-no-properties start end))])) + (if (and (listp eglot--recent-changes) + (null (cddr (car eglot--recent-changes)))) + (setf (cddr (car eglot--recent-changes)) + `(,pre-change-length ,(buffer-substring-no-properties start end))) + (setf eglot--recent-changes :emacs-messup)) (when eglot--change-idle-timer (cancel-timer eglot--change-idle-timer)) (setq eglot--change-idle-timer (run-with-idle-timer @@ -1151,14 +1148,11 @@ Records START, END and PRE-CHANGE-LENGTH locally." (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." - (when (eglot--outstanding-edits-p) + (when eglot--recent-changes (let* ((server (eglot--current-server-or-lose)) (sync-kind (eglot--server-capable :textDocumentSync)) - (emacs-messup (/= (length (car eglot--recent-changes)) - (length (cdr eglot--recent-changes)))) - (full-sync-p (or (eq sync-kind 1) emacs-messup))) - (when emacs-messup - (eglot--warn "`eglot--recent-changes' messup: %s" eglot--recent-changes)) + (full-sync-p (or (eq sync-kind 1) + (eq :emacs-messup eglot--recent-changes)))) (save-restriction (widen) (eglot--notify @@ -1166,22 +1160,19 @@ Records START, END and PRE-CHANGE-LENGTH locally." (list :textDocument (eglot--VersionedTextDocumentIdentifier) :contentChanges - (if full-sync-p (vector - (list - :text (buffer-substring-no-properties (point-min) - (point-max)))) - (cl-loop for (start-pos end-pos) across (car eglot--recent-changes) - for (len after-text) across (cdr eglot--recent-changes) - vconcat `[,(list :range `(:start ,start-pos :end ,end-pos) - :rangeLength len - :text after-text)]))))) - (setq eglot--recent-changes (cons [] [])) + (if full-sync-p + (vector `(:text ,(buffer-substring-no-properties (point-min) + (point-max)))) + (cl-loop for (beg end len text) in (reverse eglot--recent-changes) + vconcat `[,(list :range `(:start ,beg :end ,end) + :rangeLength len :text text)]))))) + (setq eglot--recent-changes nil) (setf (eglot--spinner server) (list nil :textDocument/didChange t)) (eglot--call-deferred server)))) (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." - (setq eglot--recent-changes (cons [] []) eglot--versioned-identifier 0) + (setq eglot--recent-changes nil eglot--versioned-identifier 0) (eglot--notify (eglot--current-server-or-lose) :textDocument/didOpen `(:textDocument ,(eglot--TextDocumentItem)))) commit f7e5adc1b1727aa778759e44b4d9b991413c134f Author: João Távora Date: Sun May 27 14:53:53 2018 +0100 Set eglot--versioned-identifier to 0 on didopen Else cquery will rightfully complain about this. * eglot.el (eglot--signal-textDocument/didOpen): Also set eglot--versioned-identifier to 0. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fa12577c69..dde0c22f83 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1181,7 +1181,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." - (setq eglot--recent-changes (cons [] [])) + (setq eglot--recent-changes (cons [] []) eglot--versioned-identifier 0) (eglot--notify (eglot--current-server-or-lose) :textDocument/didOpen `(:textDocument ,(eglot--TextDocumentItem)))) commit 882b571693992b5f2780e97070aa30265eab2a08 Author: João Távora Date: Sun May 27 14:17:36 2018 +0100 Correctly apply workspace edits in documentchanges form This was breaking M-x eglot-rename for cquery * eglot.el (eglot--apply-workspace-edit): Fix and simplify. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cd0ee65477..fa12577c69 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1491,39 +1491,30 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (defun eglot--apply-workspace-edit (wedit &optional confirm) "Apply the workspace edit WEDIT. If CONFIRM, ask user first." - (let (prepared) - (cl-destructuring-bind (&key changes documentChanges) - wedit - (cl-loop - for change on documentChanges - do (push (cl-destructuring-bind (&key textDocument edits) change - (cl-destructuring-bind (&key uri version) textDocument - (list (eglot--uri-to-path uri) edits version))) - prepared)) + (cl-destructuring-bind (&key changes documentChanges) wedit + (let ((prepared + (mapcar (eglot--lambda (&key textDocument edits) + (cl-destructuring-bind (&key uri version) textDocument + (list (eglot--uri-to-path uri) edits version))) + documentChanges))) (cl-loop for (uri edits) on changes by #'cddr - do (push (list (eglot--uri-to-path uri) edits) prepared))) - (if (or confirm - (cl-notevery #'find-buffer-visiting - (mapcar #'car prepared))) - (unless (y-or-n-p - (format "[eglot] Server requests to edit %s files.\n %s\n\ -Proceed? " - (length prepared) - (mapconcat #'identity - (mapcar #'car prepared) - "\n "))) - (eglot--error "User cancelled server edit"))) - (unwind-protect - (let (edit) - (while (setq edit (car prepared)) - (cl-destructuring-bind (path edits &optional version) edit - (with-current-buffer (find-file-noselect path) - (eglot--apply-text-edits edits version)) - (pop prepared)))) - (if prepared - (eglot--warn "Caution: edits of files %s failed." - (mapcar #'car prepared)) - (eglot--message "Edit successful!"))))) + do (push (list (eglot--uri-to-path uri) edits) prepared)) + (if (or confirm + (cl-notevery #'find-buffer-visiting + (mapcar #'car prepared))) + (unless (y-or-n-p + (format "[eglot] Server wants to edit:\n %s\n Proceed? " + (mapconcat #'identity (mapcar #'car prepared) "\n "))) + (eglot--error "User cancelled server edit"))) + (unwind-protect + (let (edit) (while (setq edit (car prepared)) + (cl-destructuring-bind (path edits &optional version) edit + (with-current-buffer (find-file-noselect path) + (eglot--apply-text-edits edits version)) + (pop prepared)))) + (if prepared (eglot--warn "Caution: edits of files %s failed." + (mapcar #'car prepared)) + (eglot--message "Edit successful!")))))) (defun eglot-rename (newname) "Rename the current symbol to NEWNAME." commit d7864900a58fc4ce335811a69d15a5e954b06000 Author: João Távora Date: Sun May 27 13:17:07 2018 +0100 * eglot.el (version): bump to 0.6 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ce131958db..cd0ee65477 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 0.5 +;; Version: 0.6 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot commit 22294e04cd996c3bd337611cbf32a462909a6273 Author: João Távora Date: Sun May 27 12:58:49 2018 +0100 Hopefully fix the flymake bootstrap problem Immediately after M-x eglot, eglot's use of flymake was having trouble detecting the first diagnostics sent from the server, resulting in an annoying "Wait" in the mode-line. * eglot.el (eglot--current-flymake-report-fn): Move up here. (eglot--managed-mode): Set eglot--current-flymake-report-fn to nil on teardown. (eglot--maybe-activate-editing-mode): Simplify. (eglot-handle-notification textDocument/publishDiagnostics): Set unreported-diagnostics to a cons. (eglot-handle-notification eglot-rls window/progress): Simplify. * eglot-tests.el (rls-basic-diagnostics): Simplify test. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5fab5749c5..ce131958db 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -833,6 +833,9 @@ If optional MARKERS, make markers." ;;; (defvar eglot-mode-map (make-sparse-keymap)) +(defvar-local eglot--current-flymake-report-fn nil + "Current flymake report function for this buffer") + (define-minor-mode eglot--managed-mode "Mode for source buffers managed by some EGLOT project." nil nil eglot-mode-map @@ -862,7 +865,8 @@ If optional MARKERS, make markers." (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) (remove-function (local 'eldoc-documentation-function) #'eglot-eldoc-function) - (remove-function (local imenu-create-index-function) #'eglot-imenu)))) + (remove-function (local imenu-create-index-function) #'eglot-imenu) + (setq eglot--current-flymake-report-fn nil)))) (defun eglot--managed-mode-onoff (server arg) "Proxy for function `eglot--managed-mode' with ARG and SERVER." @@ -876,9 +880,6 @@ If optional MARKERS, make markers." (add-hook 'eglot--managed-mode-hook 'flymake-mode) (add-hook 'eglot--managed-mode-hook 'eldoc-mode) -(defvar-local eglot--current-flymake-report-fn nil - "Current flymake report function for this buffer") - (defun eglot--maybe-activate-editing-mode (&optional server) "Maybe activate mode function `eglot--managed-mode'. If SERVER is supplied, do it only if BUFFER is managed by it. In @@ -888,9 +889,7 @@ that case, also signal textDocument/didOpen." (server (or (and (null server) cur) (and server (eq server cur) cur)))) (when server (eglot--managed-mode-onoff server 1) - (eglot--signal-textDocument/didOpen) - (flymake-start) - (funcall (or eglot--current-flymake-report-fn #'ignore) nil)))) + (eglot--signal-textDocument/didOpen)))) (add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) @@ -1046,7 +1045,7 @@ function with the server still running." (funcall eglot--current-flymake-report-fn diags) (setq eglot--unreported-diagnostics nil)) (t - (setq eglot--unreported-diagnostics diags))))) + (setq eglot--unreported-diagnostics (cons t diags)))))) (eglot--debug server "Diagnostics received for unvisited %s" uri))) (cl-defun eglot--register-unregister (server jsonrpc-id things how) @@ -1221,7 +1220,7 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." (setq eglot--current-flymake-report-fn report-fn) ;; Report anything unreported (when eglot--unreported-diagnostics - (funcall report-fn eglot--unreported-diagnostics) + (funcall report-fn (cdr eglot--unreported-diagnostics)) (setq eglot--unreported-diagnostics nil))) (defun eglot-xref-backend () @@ -1598,12 +1597,7 @@ Proceed? " ((server eglot-rls) (_method (eql :window/progress)) &key id done title message &allow-other-keys) "Handle notification window/progress" - (setf (eglot--spinner server) (list id title done message)) - (when (and (equal "Indexing" title) done) - (dolist (buffer (eglot--managed-buffers server)) - (with-current-buffer buffer - (funcall (or eglot--current-flymake-report-fn #'ignore) - eglot--unreported-diagnostics))))) + (setf (eglot--spinner server) (list id title done message))) ;;; cquery-specific commit 92593bb4c13a80303866cdb6992adb2012e2b279 Author: João Távora Date: Sun May 27 11:58:41 2018 +0100 Set spinner in textdocument/didchange as it matters to rls Otherwise, the asynch eldoc action will immediately send the textDocument/documentHighlight requests, without understanding that they need to be deferred a bit more. * eglot.el (eglot--signal-textDocument/didChange): Set the spinner here. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b95757ccef..5fab5749c5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1177,6 +1177,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." :rangeLength len :text after-text)]))))) (setq eglot--recent-changes (cons [] [])) + (setf (eglot--spinner server) (list nil :textDocument/didChange t)) (eglot--call-deferred server)))) (defun eglot--signal-textDocument/didOpen () commit f20edc04ed67a086d608ed3af9702f09481e9c92 Author: João Távora Date: Sun May 27 11:41:24 2018 +0100 Fix assorted cquery-related bugs Some versions of cquery send a :role key as part of the response to textDocument/documentHighlight. Ignore it for now. Also cquery sometimes send 0-length ranges upon which we now fallback to flymake-diag-region. Finally, in eglot-eldoc-funciton, the previous hack of calling the eglot--hover-info outside of the when-buffer-window macrolet contained a bug. It must be called in the correct buffer. Revert the hack and do it by querying from eglot.el if ert is running tests. * eglot.el (eglot--range-region): Return a cons and fallback to flymake-diag-region if server returned a useless range. (eglot-handle-notification, eglot--hover-info): Update call to eglot--range-region. (eglot-help-at-point): Ensure `eglot--hover-info` runs in right buffer. (eglot-eldoc-function): Don't abuse eldoc-last-message like this. Also update call to eglot--range-region. Consider ert-running-test (eglot--apply-text-edits): Use pcase-lambda. (ert): require it. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 095b296f24..b95757ccef 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -60,6 +60,7 @@ (require 'xref) (require 'subr-x) (require 'filenotify) +(require 'ert) ;;; User tweakable stuff @@ -817,10 +818,15 @@ If optional MARKER, return a marker instead" finally (cl-return (or probe t)))) (defun eglot--range-region (range &optional markers) - "Return region (BEG END) that represents LSP RANGE. + "Return region (BEG . END) that represents LSP RANGE. If optional MARKERS, make markers." - (list (eglot--lsp-position-to-point (plist-get range :start) markers) - (eglot--lsp-position-to-point (plist-get range :end) markers))) + (let* ((st (plist-get range :start)) + (beg (eglot--lsp-position-to-point st markers)) + (end (eglot--lsp-position-to-point (plist-get range :end) markers))) + ;; Fallback to `flymake-diag-region' if server botched the range + (if (/= beg end) (cons beg end) (flymake-diag-region + (current-buffer) (plist-get st :line) + (1- (plist-get st :character)))))) ;;; Minor modes @@ -1028,7 +1034,7 @@ function with the server still running." collect (cl-destructuring-bind (&key range severity _group _code source message) diag-spec - (pcase-let ((`(,beg ,end) (eglot--range-region range))) + (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (flymake-make-diagnostic (current-buffer) beg end (cond ((<= severity 1) :error) @@ -1362,7 +1368,7 @@ DUMMY is ignored" (defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") (defun eglot--hover-info (contents &optional range) - (concat (and range (pcase-let ((`(,beg ,end) (eglot--range-region range))) + (concat (and range (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (concat (buffer-substring beg end) ": "))) (mapconcat #'eglot--format-markup (append (cond ((vectorp contents) contents) @@ -1397,9 +1403,9 @@ DUMMY is ignored" (eglot--request (eglot--current-server-or-lose) :textDocument/hover (eglot--TextDocumentPositionParams)) (when (seq-empty-p contents) (eglot--error "No hover info here")) - (with-help-window "*eglot help*" - (with-current-buffer standard-output - (insert (eglot--hover-info contents range)))))) + (let ((blurb (eglot--hover-info contents range))) + (with-help-window "*eglot help*" + (with-current-buffer standard-output (insert blurb)))))) (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function. @@ -1409,8 +1415,9 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (position-params (eglot--TextDocumentPositionParams)) sig-showing) (cl-macrolet ((when-buffer-window - (&body body) `(when (get-buffer-window buffer) - (with-current-buffer buffer ,@body)))) + (&body body) + `(when (or (get-buffer-window buffer) (ert-running-test)) + (with-current-buffer buffer ,@body)))) (when (eglot--server-capable :signatureHelpProvider) (eglot--async-request server :textDocument/signatureHelp position-params @@ -1428,8 +1435,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." server :textDocument/hover position-params :success-fn (eglot--lambda (&key contents range) (unless sig-showing - (setq eldoc-last-message (eglot--hover-info contents range)) - (when-buffer-window (eldoc-message eldoc-last-message)))) + (when-buffer-window + (eldoc-message (eglot--hover-info contents range))))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) (eglot--async-request @@ -1438,8 +1445,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (mapc #'delete-overlay eglot--highlights) (setq eglot--highlights (when-buffer-window - (mapcar (eglot--lambda (&key range _kind) - (pcase-let ((`(,beg ,end) + (mapcar (eglot--lambda (&key range _kind _role) + (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (let ((ov (make-overlay beg end))) (overlay-put ov 'face 'highlight) @@ -1475,7 +1482,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (save-restriction (widen) (save-excursion - (mapc (eglot--lambda (newText beg end) + (mapc (pcase-lambda (`(,newText ,beg . ,end)) (goto-char beg) (delete-region beg end) (insert newText)) (mapcar (eglot--lambda (&key range newText) (cons newText (eglot--range-region range 'markers))) commit 9b468c1cc19a75455bdeddb10f0405e0612ebaa7 Author: João Távora Date: Sat May 26 19:29:51 2018 +0100 * eglot.el (version): bump to 0.5 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8d34b1be05..095b296f24 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 0.4 +;; Version: 0.5 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot commit d366cc04b706d02e26016fba470240ba2fa7a8a2 Merge: 004702460d 9c14cfd179 Author: João Távora Date: Sat May 26 19:10:45 2018 +0100 Merge branch 'cquery-support' into master The conflicts in eglot.el where fixed by calling the new eglot--debug helper coming from 'cquery-support'. This helper was converted to allow a non-string format passed directly to eglot--log-event. Also fixed some compilation warnings. * eglot.el (eglot--debug): Allow non-string FORMAT to be a JSON object. (eglot-handle-notification :$cquery/progress) (eglot-handle-notification :$cquery/setInactiveRegions) (eglot-handle-notification :$cquery/publishSemanticHighlighting): Solve compilation warnings. commit 004702460d4516306d5a1bae640bce9a367715f1 Author: João Távora Date: Sat May 26 18:52:17 2018 +0100 Really ensure eglot--shutdown deletes a process completely * eglot.el (eglot-lsp-server): rename slot "moribund" to "shutdown-requested" (eglot--connect): Don't check if shutdown was requested here. (eglot--process-sentinel): Set shutdown-requested to :sentinel-done here. (eglot-shutdown): use eglot--shutdown-requested. Improve check for process liveness. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b83e772c49..cc3fae1f54 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -184,9 +184,9 @@ deferred to the future." (capabilities :documentation "JSON object containing server capabilities." :accessor eglot--capabilities) - (moribund + (shutdown-requested :documentation "Flag set when server is shutting down." - :accessor eglot--moribund) + :accessor eglot--shutdown-requested) (project :documentation "Project associated with server." :initarg :project :accessor eglot--project) @@ -307,7 +307,7 @@ class SERVER-CLASS." (run-hook-with-args 'eglot-connect-hook server) (setq connect-success server)) (unless (or connect-success - (not (process-live-p proc)) (eglot--moribund server)) + (not (process-live-p proc))) (eglot-shutdown server))))) (defvar eglot--command-history nil @@ -454,7 +454,8 @@ INTERACTIVE is t if called interactively." (eglot--process server))) (delete-process proc) ;; Consider autoreconnecting - (cond ((eglot--moribund server)) + (cond ((eglot--shutdown-requested server) + (setf (eglot--shutdown-requested server) :sentinel-done)) ((not (eglot--inhibit-autoreconnect server)) (eglot--warn "Reconnecting after unexpected server exit") (eglot-reconnect server)) @@ -959,15 +960,17 @@ function with the server still running." (eglot--message "Asking %s politely to terminate" (eglot--name server)) (unwind-protect (let ((eglot-request-timeout 3)) - (setf (eglot--moribund server) t) + (setf (eglot--shutdown-requested server) t) (eglot--request server :shutdown nil) ;; this one is supposed to always fail, hence ignore-errors (ignore-errors (eglot--request server :exit nil))) ;; Turn off `eglot--managed-mode' where appropriate. (dolist (buffer (eglot--managed-buffers server)) (with-current-buffer buffer (eglot--managed-mode-onoff server -1))) - (when (process-live-p (eglot--process server)) - (eglot--warn "Brutally deleting non-compliant server %s" (eglot--name server)) + (while (progn (accept-process-output nil 0.1) + (not (eq (eglot--shutdown-requested server) :sentinel-done))) + (eglot--warn "Sentinel for %s still hasn't run, brutally deleting it!" + (eglot--process server)) (delete-process (eglot--process server))))) (cl-defmethod eglot-handle-notification commit 94998753703afa546a96e460d96a4c58655e9927 Author: João Távora Date: Sat May 26 16:37:10 2018 +0100 Get rid of eglot--obj, an uninteresting abstraction * eglot.el (eglot--obj): Get rid of this. It wasn't widely used anyway. (eglot-client-capabilities) (eglot--connect, eglot--async-request, eglot--notify) (eglot--reply, eglot--pos-to-lsp-position, eglot-handle-request) (eglot--register-unregister, eglot-handle-request) (eglot--TextDocumentIdentifier) (eglot--VersionedTextDocumentIdentifier) (eglot--TextDocumentItem, eglot--TextDocumentPositionParams) (eglot--signal-textDocument/didChange) (eglot--signal-textDocument/didSave) (xref-backend-identifier-completion-table) (xref-backend-references, xref-backend-apropos, eglot-imenu) (eglot-rename): Use list instead of eglot--obj. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1a4e3c2677..b83e772c49 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -115,14 +115,6 @@ lasted more than that many seconds." ;;; API (WORK-IN-PROGRESS!) ;;; -(defmacro eglot--obj (&rest what) - "Make WHAT a JSON object suitable for `json-encode'." - (declare (debug (&rest form))) - ;; FIXME: not really API. Should it be? - ;; FIXME: maybe later actually do something, for now this just fixes - ;; the indenting of literal plists. - `(list ,@what)) - (cl-defgeneric eglot-server-ready-p (server what) ;; API "Tell if SERVER is ready for WHAT in current buffer. If it isn't, a deferrable `eglot--async-request' *will* be @@ -143,15 +135,15 @@ deferred to the future." (cl-defgeneric eglot-client-capabilities (server) "What the EGLOT LSP client supports for SERVER." (:method (_s) - (eglot--obj - :workspace (eglot--obj + (list + :workspace (list :applyEdit t :workspaceEdit `(:documentChanges :json-false) :didChangeWatchesFiles `(:dynamicRegistration t) :symbol `(:dynamicRegistration :json-false)) :textDocument - (eglot--obj - :synchronization (eglot--obj + (list + :synchronization (list :dynamicRegistration :json-false :willSave t :willSaveWaitUntil t :didSave t) :completion `(:dynamicRegistration :json-false) @@ -163,7 +155,7 @@ deferred to the future." :documentHighlight `(:dynamicRegistration :json-false) :rename `(:dynamicRegistration :json-false) :publishDiagnostics `(:relatedInformation :json-false)) - :experimental (eglot--obj)))) + :experimental (list)))) ;;; Process management @@ -300,7 +292,7 @@ class SERVER-CLASS." (eglot--request server :initialize - (eglot--obj + (list :processId (unless (eq (process-type proc) 'network) (emacs-pid)) :capabilities (eglot-client-capabilities server) :rootPath (expand-file-name (car (project-roots project))) @@ -311,7 +303,7 @@ class SERVER-CLASS." (dolist (buffer (buffer-list)) (with-current-buffer buffer (eglot--maybe-activate-editing-mode server))) - (eglot--notify server :initialized (eglot--obj :__dummy__ t)) + (eglot--notify server :initialized `(:__dummy__ t)) (run-hook-with-args 'eglot-connect-hook server) (setq connect-success server)) (unless (or connect-success @@ -691,12 +683,12 @@ happens, the original timer keeps counting). Return (ID TIMER)." (or success-fn (eglot--lambda (&rest _ignored) (eglot--log-event - server (eglot--obj :message "success ignored" :id id)))) + server `(:message "success ignored" :id ,id)))) (or error-fn (eglot--lambda (&key code message &allow-other-keys) (setf (eglot--status server) `(,message t)) - server (eglot--obj :message "error ignored, status set" - :id id :error code))) + server `(:message "error ignored, status set" + :id ,id :error ,code))) (or timer (funcall make-timer))) (eglot--pending-continuations server)) (list id timer))) @@ -732,16 +724,14 @@ DEFERRED is passed to `eglot--async-request', which see." (cl-defun eglot--notify (server method params) "Notify SERVER of something, don't expect a reply.e" - (eglot--send server (eglot--obj :jsonrpc "2.0" - :method method - :params params))) + (eglot--send server `(:jsonrpc "2.0" :method ,method :params ,params))) (cl-defun eglot--reply (server id &key result error) "Reply to PROCESS's request ID with MESSAGE." (eglot--send - server`(:jsonrpc "2.0" :id ,id - ,@(when result `(:result ,result)) - ,@(when error `(:error ,error))))) + server `(:jsonrpc "2.0" :id ,id + ,@(when result `(:result ,result)) + ,@(when error `(:error ,error))))) ;;; Helpers @@ -763,9 +753,9 @@ DEFERRED is passed to `eglot--async-request', which see." (defun eglot--pos-to-lsp-position (&optional pos) "Convert point POS to LSP position." (save-excursion - (eglot--obj :line (1- (line-number-at-pos pos t)) ; F!@&#$CKING OFF-BY-ONE - :character (- (goto-char (or pos (point))) - (line-beginning-position))))) + (list :line (1- (line-number-at-pos pos t)) ; F!@&#$CKING OFF-BY-ONE + :character (- (goto-char (or pos (point))) + (line-beginning-position))))) (defun eglot--lsp-position-to-point (pos-plist &optional marker) "Convert LSP position POS-PLIST to Emacs point. @@ -1003,10 +993,9 @@ function with the server still running." '("OK")) nil t (plist-get (elt actions 0) :title))) (if reply - (eglot--reply server id :result (eglot--obj :title reply)) + (eglot--reply server id :result `(:title ,reply)) (eglot--reply server id - :error (eglot--obj :code -32800 - :message "User cancelled")))))) + :error `(:code -32800 :message "User cancelled")))))) (cl-defmethod eglot-handle-notification (_server (_method (eql :window/logMessage)) &key _type _message) @@ -1058,7 +1047,7 @@ THINGS are either registrations or unregisterations." (eglot--reply server jsonrpc-id :error `(:code -32601 :message ,(or (cadr retval) "sorry"))))))))) - (eglot--reply server jsonrpc-id :result (eglot--obj :message "OK"))) + (eglot--reply server jsonrpc-id :result `(:message "OK"))) (cl-defmethod eglot-handle-request (server id (_method (eql :client/registerCapability)) &key registrations) @@ -1079,37 +1068,36 @@ THINGS are either registrations or unregisterations." (eglot--reply server id :result `(:applied ))) (error (eglot--reply server id :result `(:applied :json-false) - :error (eglot--obj :code -32001 - :message (format "%s" err)))))) + :error `(:code -32001 :message ,(format "%s" err)))))) (defun eglot--TextDocumentIdentifier () "Compute TextDocumentIdentifier object for current buffer." - (eglot--obj :uri (eglot--path-to-uri buffer-file-name))) + (list :uri (eglot--path-to-uri buffer-file-name))) (defvar-local eglot--versioned-identifier 0) (defun eglot--VersionedTextDocumentIdentifier () "Compute VersionedTextDocumentIdentifier object for current buffer." (append (eglot--TextDocumentIdentifier) - (eglot--obj :version eglot--versioned-identifier))) + `(:version ,eglot--versioned-identifier))) (defun eglot--TextDocumentItem () "Compute TextDocumentItem object for current buffer." (append (eglot--VersionedTextDocumentIdentifier) - (eglot--obj :languageId - (if (string-match "\\(.*\\)-mode" (symbol-name major-mode)) - (match-string 1 (symbol-name major-mode)) - "unknown") - :text - (save-restriction - (widen) - (buffer-substring-no-properties (point-min) (point-max)))))) + (list :languageId + (if (string-match "\\(.*\\)-mode" (symbol-name major-mode)) + (match-string 1 (symbol-name major-mode)) + "unknown") + :text + (save-restriction + (widen) + (buffer-substring-no-properties (point-min) (point-max)))))) (defun eglot--TextDocumentPositionParams () "Compute TextDocumentPositionParams." - (eglot--obj :textDocument (eglot--TextDocumentIdentifier) - :position (eglot--pos-to-lsp-position))) + (list :textDocument (eglot--TextDocumentIdentifier) + :position (eglot--pos-to-lsp-position))) (defvar-local eglot--recent-changes nil "Recent buffer changes as collected by `eglot--before-change'.") @@ -1160,19 +1148,18 @@ Records START, END and PRE-CHANGE-LENGTH locally." (widen) (eglot--notify server :textDocument/didChange - (eglot--obj + (list :textDocument (eglot--VersionedTextDocumentIdentifier) :contentChanges (if full-sync-p (vector - (eglot--obj + (list :text (buffer-substring-no-properties (point-min) (point-max)))) (cl-loop for (start-pos end-pos) across (car eglot--recent-changes) for (len after-text) across (cdr eglot--recent-changes) - vconcat `[,(eglot--obj :range (eglot--obj :start start-pos - :end end-pos) - :rangeLength len - :text after-text)]))))) + vconcat `[,(list :range `(:start ,start-pos :end ,end-pos) + :rangeLength len + :text after-text)]))))) (setq eglot--recent-changes (cons [] [])) (eglot--call-deferred server)))) @@ -1206,7 +1193,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." (eglot--notify (eglot--current-server-or-lose) :textDocument/didSave - (eglot--obj + (list ;; TODO: Handle TextDocumentSaveRegistrationOptions to control this. :text (buffer-substring-no-properties (point-min) (point-max)) :textDocument (eglot--TextDocumentIdentifier)))) @@ -1253,17 +1240,15 @@ DUMMY is ignored" (eglot--lambda (&key name kind location containerName) (propertize name :textDocumentPositionParams - (eglot--obj :textDocument text-id - :position (plist-get - (plist-get location :range) - :start)) + (list :textDocument text-id + :position (plist-get + (plist-get location :range) + :start)) :locations (list location) :kind kind :containerName containerName)) - (eglot--request server - :textDocument/documentSymbol - (eglot--obj - :textDocument text-id)))) + (eglot--request + server :textDocument/documentSymbol `(:textDocument ,text-id)))) (all-completions string eglot--xref-known-symbols)))))) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) @@ -1301,8 +1286,7 @@ DUMMY is ignored" :textDocument/references (append params - (eglot--obj :context - (eglot--obj :includeDeclaration t))))))) + `(:context (:includeDeclaration t))))))) (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) @@ -1311,7 +1295,7 @@ DUMMY is ignored" (eglot--xref-make name uri (plist-get range :start)))) (eglot--request (eglot--current-server-or-lose) :workspace/symbol - (eglot--obj :query pattern))))) + (list :query pattern))))) (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." @@ -1466,8 +1450,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (plist-get (plist-get location :range) :start)))) (eglot--request (eglot--current-server-or-lose) :textDocument/documentSymbol - (eglot--obj - :textDocument (eglot--TextDocumentIdentifier)))))) + `(:textDocument ,(eglot--TextDocumentIdentifier)))))) (append (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) entries) @@ -1534,7 +1517,7 @@ Proceed? " (eglot--apply-workspace-edit (eglot--request (eglot--current-server-or-lose) :textDocument/rename `(,@(eglot--TextDocumentPositionParams) - ,@(eglot--obj :newName newname))) + :newName ,newname)) current-prefix-arg)) commit f165670762c427d49446ad9002d8f84c176cd9d9 Author: João Távora Date: Sat May 26 16:22:46 2018 +0100 Cleanup deferred request mechanism with a readable log * eglot.el (eglot-lsp-server): Rework doc of deferred-actions slot. (defvar eglot--next-request-id): Move down, now buffer local. (defun eglot--next-request-id): Remove. (eglot--call-deferred): Be more informative. (eglot--async-request): Simplify. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 51bcc8bd2d..1a4e3c2677 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -211,8 +211,8 @@ deferred to the future." :documentation "How server was started and how it can be re-started." :initarg :contact :accessor eglot--contact) (deferred-actions - :documentation "Map (DEFERRED-ID BUF) to (FN TIMER). -DEFERRED request from BUF is FN. It's sent later, not later than TIMER." + :documentation "Map (DEFERRED BUF) to (FN TIMER ID). FN is a saved\ +DEFERRED request from BUF, to be sent not later than TIMER as ID." :initform (make-hash-table :test #'equal) :accessor eglot--deferred-actions) (file-watches :documentation "Map ID to list of WATCHES for `didChangeWatchedFiles'." @@ -615,12 +615,6 @@ originated." (string-bytes json) json)) (eglot--log-event server message 'client))) -(defvar eglot--next-request-id 0 "ID for next request.") - -(defun eglot--next-request-id () - "Compute the next id for a client request." - (setq eglot--next-request-id (1+ eglot--next-request-id))) - (defun eglot-forget-pending-continuations (server) "Stop waiting for responses from the current LSP SERVER." (interactive (list (eglot--current-server-or-lose))) @@ -635,7 +629,7 @@ originated." (defun eglot--call-deferred (server) "Call SERVER's deferred actions, who may again defer themselves." (when-let ((actions (hash-table-values (eglot--deferred-actions server)))) - (eglot--log-event server `(:running-deferred ,(length actions))) + (eglot--log-event server `(:maybe-run-deferred ,(mapcar #'caddr actions))) (mapc #'funcall (mapcar #'car actions)))) (cl-defmacro eglot--lambda (cl-lambda-list &body body) @@ -643,6 +637,8 @@ originated." (let ((e (gensym "eglot--lambda-elem"))) `(lambda (,e) (apply (cl-function (lambda ,cl-lambda-list ,@body)) ,e)))) +(defvar-local eglot--next-request-id 0 "ID for next `eglot--async-request'.") + (cl-defun eglot--async-request (server method params @@ -656,59 +652,52 @@ objects, respectively. Wait TIMEOUT seconds for response or call nullary TIMEOUT-FN. If DEFERRED, maybe defer request to the future, or to never at all, in case a new request with identical DEFERRED and for the same buffer overrides it (however, if that -happens, the original timeout keeps counting). Return a list (ID -TIMER)." - (let* ((id (eglot--next-request-id)) - (timer nil) - (make-timer - (lambda ( ) - (or timer - (run-with-timer - timeout nil - (lambda () - (remhash id (eglot--pending-continuations server)) - (funcall (or timeout-fn - (lambda () - (eglot--log-event - server `(:timed-out ,method :id ,id - :params ,params))))))))))) +happens, the original timer keeps counting). Return (ID TIMER)." + (pcase-let* ( (buf (current-buffer)) (pos (point-marker)) + (`(,_ ,timer ,old-id) + (and deferred (gethash (list deferred buf) + (eglot--deferred-actions server)))) + (id (or old-id (cl-incf eglot--next-request-id))) + (make-timer + (lambda ( ) + (run-with-timer + timeout nil + (lambda () + (remhash id (eglot--pending-continuations server)) + (if timeout-fn (funcall timeout-fn) + (eglot--log-event + server `(:timed-out ,method :id ,id :params ,params)))))))) (when deferred - (let* ((buf (current-buffer)) - (existing (gethash (list deferred buf) - (eglot--deferred-actions server)))) - (when existing (setq existing (cadr existing))) - (if (eglot-server-ready-p server deferred) - (remhash (list deferred buf) (eglot--deferred-actions server)) - (eglot--log-event server `(:deferring ,method :id ,id :params ,params)) - (let* ((buf (current-buffer)) (point (point)) - (later (lambda () - (when (buffer-live-p buf) - (with-current-buffer buf - (save-excursion - (goto-char point) - (apply #'eglot--async-request server - method params args))))))) - (puthash (list deferred buf) - (list later (setq timer (funcall make-timer))) - (eglot--deferred-actions server)) - (cl-return-from eglot--async-request nil))))) - ;; Really run it - ;; - (eglot--send server (eglot--obj :jsonrpc "2.0" - :id id - :method method - :params params)) - (puthash id - (list (or success-fn - (eglot--lambda (&rest _ignored) - (eglot--log-event - server (eglot--obj :message "success ignored" :id id)))) - (or error-fn - (eglot--lambda (&key code message &allow-other-keys) - (setf (eglot--status server) `(,message t)) - server (eglot--obj :message "error ignored, status set" - :id id :error code))) - (setq timer (funcall make-timer))) + (if (eglot-server-ready-p server deferred) + ;; Server is ready, we jump below and send it immediately. + (remhash (list deferred buf) (eglot--deferred-actions server)) + ;; Otherwise, save in `eglot--deferred-actions' and exit non-locally + (unless old-id + ;; Also, if it's the first deferring for this id, inform the log + (eglot--log-event server `(:deferring ,method :id ,id :params ,params))) + (puthash (list deferred buf) + (list (lambda () (when (buffer-live-p buf) + (with-current-buffer buf + (save-excursion + (goto-char pos) + (apply #'eglot--async-request server + method params args))))) + (or timer (funcall make-timer)) id) + (eglot--deferred-actions server)) + (cl-return-from eglot--async-request nil))) + ;; Really send the request + (eglot--send server `(:jsonrpc "2.0" :id ,id :method ,method :params ,params)) + (puthash id (list + (or success-fn + (eglot--lambda (&rest _ignored) + (eglot--log-event + server (eglot--obj :message "success ignored" :id id)))) + (or error-fn + (eglot--lambda (&key code message &allow-other-keys) + (setf (eglot--status server) `(,message t)) + server (eglot--obj :message "error ignored, status set" + :id id :error code))) + (or timer (funcall make-timer))) (eglot--pending-continuations server)) (list id timer))) commit 545e9c8a1314aa095f6e12e9764011441f182bed Author: João Távora Date: Sat May 26 16:13:53 2018 +0100 Simpify eglot--server-receive * eglot.el (eglot--obj): Cleanup whitespace. (eglot--server-receive): Simplify. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8fe24ed197..51bcc8bd2d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -115,7 +115,7 @@ lasted more than that many seconds." ;;; API (WORK-IN-PROGRESS!) ;;; -(defmacro eglot--obj (&rest what) +(defmacro eglot--obj (&rest what) "Make WHAT a JSON object suitable for `json-encode'." (declare (debug (&rest form))) ;; FIXME: not really API. Should it be? @@ -582,29 +582,28 @@ originated." (gethash id (eglot--pending-continuations server))))) (eglot--log-event server message 'server) (when error (setf (eglot--status server) `(,error t))) - (unless (or (null method) - (keywordp method)) + (unless (or (null method) (keywordp method)) (setq method (intern (format ":%s" method)))) - (cond ((and method id) - (condition-case-unless-debug _err - (apply #'eglot-handle-request server id method params) - (cl-no-applicable-method - (eglot--reply server id - :error `(:code -32601 :message "Method unimplemented"))))) - (method - (condition-case-unless-debug _err - (apply #'eglot-handle-notification server method params) - (cl-no-applicable-method - (eglot--log-event - server '(:error `(:message "Notification unimplemented")))))) - (continuations - (cancel-timer (cl-third continuations)) - (remhash id (eglot--pending-continuations server)) - (if error - (funcall (cl-second continuations) error) - (funcall (cl-first continuations) result))) - (id - (eglot--warn "Ooops no continuation for id %s" id))) + (cond + (method + (condition-case-unless-debug _err + (if id + (apply #'eglot-handle-request server id method params) + (apply #'eglot-handle-notification server method params)) + (cl-no-applicable-method + (if id + (eglot--reply + server id :error `(:code -32601 :message "Method unimplemented")) + (eglot--log-event + server '(:error `(:message "Notification unimplemented"))))))) + (continuations + (cancel-timer (cl-third continuations)) + (remhash id (eglot--pending-continuations server)) + (if error + (funcall (cl-second continuations) error) + (funcall (cl-first continuations) result))) + (id + (eglot--warn "Ooops no continuation for id %s" id))) (eglot--call-deferred server) (force-mode-line-update t)))) commit 1dcdc11127a21f1018d4a87b23295c1a113e4350 Author: João Távora Date: Sat May 26 16:11:11 2018 +0100 Don't rely on flymake's idle timer for textdocument/didchange * eglot.el (eglot--after-change): Set idle timer here. (eglot--change-idle-timer): New var. (eglot--signal-textDocument/didChange): No seed to set spinner here. (eglot-flymake-backend) Don't send didChange here. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3d36344440..8fe24ed197 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1131,6 +1131,8 @@ THINGS are either registrations or unregisterations." (cl-plusp (+ (length (car eglot--recent-changes)) (length (cdr eglot--recent-changes))))) +(defvar eglot--change-idle-timer nil "Idle timer for textDocument/didChange.") + (defun eglot--before-change (start end) "Hook onto `before-change-functions'. Records START and END, crucially convert them into @@ -1149,7 +1151,12 @@ Records START, END and PRE-CHANGE-LENGTH locally." (setf (cdr eglot--recent-changes) (vconcat (cdr eglot--recent-changes) `[(,pre-change-length - ,(buffer-substring-no-properties start end))]))) + ,(buffer-substring-no-properties start end))])) + (when eglot--change-idle-timer (cancel-timer eglot--change-idle-timer)) + (setq eglot--change-idle-timer + (run-with-idle-timer + 0.5 nil (lambda () (eglot--signal-textDocument/didChange) + (setq eglot--change-idle-timer nil))))) (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." @@ -1166,8 +1173,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." (eglot--notify server :textDocument/didChange (eglot--obj - :textDocument - (eglot--VersionedTextDocumentIdentifier) + :textDocument (eglot--VersionedTextDocumentIdentifier) :contentChanges (if full-sync-p (vector (eglot--obj @@ -1180,7 +1186,6 @@ Records START, END and PRE-CHANGE-LENGTH locally." :rangeLength len :text after-text)]))))) (setq eglot--recent-changes (cons [] [])) - (setf (eglot--spinner server) (list nil :textDocument/didChange t)) (eglot--call-deferred server)))) (defun eglot--signal-textDocument/didOpen () @@ -1225,9 +1230,7 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." ;; Report anything unreported (when eglot--unreported-diagnostics (funcall report-fn eglot--unreported-diagnostics) - (setq eglot--unreported-diagnostics nil)) - ;; Signal a didChange that might eventually bring new diagnotics - (eglot--signal-textDocument/didChange)) + (setq eglot--unreported-diagnostics nil))) (defun eglot-xref-backend () "EGLOT xref backend." commit 8a7d17b5ea378579a251dbd8409b46f158a86a8f Author: João Távora Date: Sat May 26 01:07:10 2018 +0100 Document current api breaches a bit * eglot-tests.el (eglot--call-with-dirs-and-files) (auto-reconnect): use eglot--process * eglot.el (eglot-shutdown, eglot, eglot-reconnect) (eglot--connect): Use eglot--process (eglot--process): Alias to concentrate the hack here. (eglot--signal-textDocument/didChange): Tweak comment. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 132833112d..ccb7b4908a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -191,6 +191,11 @@ lasted more than that many seconds." (defvar eglot--servers-by-project (make-hash-table :test #'equal) "Keys are projects. Values are lists of processes.") +;; HACK: Do something to fix this in the jsonrpc API or here, but in +;; the meantime concentrate the hack here. +(defalias 'eglot--process 'jsonrpc--process + "An abuse of `jsonrpc--process', a jsonrpc.el internal.") + (defun eglot-shutdown (server &optional _interactive) "Politely ask SERVER to quit. Forcefully quit it if it doesn't respond. Don't leave this @@ -206,9 +211,9 @@ function with the server still running." ;; Turn off `eglot--managed-mode' where appropriate. (dolist (buffer (eglot--managed-buffers server)) (with-current-buffer buffer (eglot--managed-mode-onoff server -1))) - (when (process-live-p (jsonrpc--process server)) + (when (process-live-p (eglot--process server)) (eglot--warn "Brutally deleting non-compliant server %s" (jsonrpc-name server)) - (delete-process (jsonrpc--process server))))) + (delete-process (eglot--process server))))) (defun eglot--on-shutdown (server) "Called by jsonrpc.el when SERVER is already dead." @@ -330,7 +335,7 @@ INTERACTIVE is t if called interactively." (car (project-roots project))))) (current-server (jsonrpc-current-connection)) (live-p (and current-server - (process-live-p (jsonrpc--process current-server))))) + (process-live-p (eglot--process current-server))))) (if (and live-p interactive (y-or-n-p "[eglot] Live process found, reconnect instead? ")) @@ -351,7 +356,7 @@ managing `%s' buffers in project `%s'." "Reconnect to SERVER. INTERACTIVE is t if called interactively." (interactive (list (jsonrpc-current-connection-or-lose) t)) - (when (process-live-p (jsonrpc--process server)) + (when (process-live-p (eglot--process server)) (eglot-shutdown server interactive)) (eglot--connect (eglot--project server) (eglot--major-mode server) @@ -391,7 +396,7 @@ And NICKNAME and CONTACT." server :initialize (jsonrpc-obj :processId (unless (eq (process-type - (jsonrpc--process server)) + (eglot--process server)) 'network) (emacs-pid)) :rootPath (expand-file-name @@ -415,7 +420,7 @@ And NICKNAME and CONTACT." (setf (eglot--inhibit-autoreconnect server) (null eglot-autoreconnect))))))) (setq success server)) - (unless (or success (not (process-live-p (jsonrpc--process server))) + (unless (or success (not (process-live-p (eglot--process server))) (eglot--moribund server)) (eglot-shutdown server))))) @@ -851,7 +856,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." :text after-text)]))))) (setq eglot--recent-changes (cons [] [])) (setf (eglot--spinner server) (list nil :textDocument/didChange t)) - ;; HACK! + ;; HACK! perhaps jsonrpc should just call this on every send (jsonrpc--call-deferred server)))) (defun eglot--signal-textDocument/didOpen () commit 5e767fb73c95643c3d6c5864e9ece0cb166f14b6 Merge: 3a127eed7b 0ebd4a816d Author: João Távora Date: Sat May 26 00:29:50 2018 +0100 Merge branch use-eieio-server-defclass into jsonrpc-refactor commit 9c14cfd179b43f1de87bbb0aca83ba0c34288eb4 Author: Josh Elsasser Date: Thu May 24 20:10:15 2018 -0700 Log debug messages through eglot--debug * eglot.el (eglot--async-request, eglot--process-sentinel): (eglot--call-deferred): Use eglot--debug to log messages to the server events buffer. (eglot--server-receive): Demote "Notification unimplemented" message on missing handlers to a pure debug message. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 467e524b07..55fa40649c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -432,7 +432,7 @@ INTERACTIVE is t if called interactively." (defun eglot--process-sentinel (proc change) "Called when PROC undergoes CHANGE." (let ((server (process-get proc 'eglot-server))) - (eglot--log-event server `(:message "Process state changed" :change ,change)) + (eglot--debug server "Process state changed: %s" change) (when (not (process-live-p proc)) (with-current-buffer (eglot-events-buffer server) (let ((inhibit-read-only t)) @@ -597,8 +597,7 @@ originated." (condition-case-unless-debug _err (apply #'eglot-handle-notification server method params) (cl-no-applicable-method - (eglot--log-event - server '(:error `(:message "Notification unimplemented")))))) + (eglot--debug server "Notification unimplemented: %s" method)))) (continuations (cancel-timer (cl-third continuations)) (remhash id (eglot--pending-continuations server)) @@ -638,7 +637,7 @@ originated." (defun eglot--call-deferred (server) "Call SERVER's deferred actions, who may again defer themselves." (when-let ((actions (hash-table-values (eglot--deferred-actions server)))) - (eglot--log-event server `(:running-deferred ,(length actions))) + (eglot--debug server "running %d deferred actions" (length actions)) (mapc #'funcall (mapcar #'car actions)))) (cl-defmacro eglot--lambda (cl-lambda-list &body body) @@ -682,7 +681,7 @@ TIMER)." (when existing (setq existing (cadr existing))) (if (eglot-server-ready-p server deferred) (remhash (list deferred buf) (eglot--deferred-actions server)) - (eglot--log-event server `(:deferring ,method :id ,id :params ,params)) + (eglot--debug server "deferring %s (id %s)" method id) (let* ((buf (current-buffer)) (point (point)) (later (lambda () (when (buffer-live-p buf) @@ -704,8 +703,7 @@ TIMER)." (puthash id (list (or success-fn (eglot--lambda (&rest _ignored) - (eglot--log-event - server (eglot--obj :message "success ignored" :id id)))) + (eglot--debug server "%s success ignored (id %s)" method id))) (or error-fn (eglot--lambda (&key code message &allow-other-keys) (setf (eglot--status server) `(,message t)) commit 45d4814a806ee611ae9ef2b2acc8927cf8903b90 Author: Josh Elsasser Date: Mon May 21 17:15:12 2018 -0700 Demote unvisited diagnostics logging to debug level The PublishDiagnostic spec (LSP Specification, 3.0) does not strictly forbid the server from publishing diagnostics before a file has been visited. * eglot.el (eglot--server-textDocument/publishDiagnostics): Log the "received diagnostics for unvisited file" warning as debug to avoid spamming users of compliant language servers. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 92f16c0d04..467e524b07 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1038,7 +1038,7 @@ function with the server still running." "Unreported diagnostics for this buffer.") (cl-defmethod eglot-handle-notification - (_server (_method (eql :textDocument/publishDiagnostics)) &key uri diagnostics) + (server (_method (eql :textDocument/publishDiagnostics)) &key uri diagnostics) "Handle notification publishDiagnostics" (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) (with-current-buffer buffer @@ -1060,7 +1060,7 @@ function with the server still running." (setq eglot--unreported-diagnostics nil)) (t (setq eglot--unreported-diagnostics diags))))) - (eglot--warn "Diagnostics received for unvisited %s" uri))) + (eglot--debug server "Diagnostics received for unvisited %s" uri))) (cl-defun eglot--register-unregister (server jsonrpc-id things how) "Helper for `registerCapability'. commit 3650efa65ee1e34c36464c425e8cbb06cb317a8b Author: Josh Elsasser Date: Mon May 21 17:03:55 2018 -0700 Introduce eglot--debug for unimportant messages * eglot.el (eglot--debug): New function to log noisy or trivial messages to the eglot events buffer. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 189ad1e5aa..92f16c0d04 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -774,6 +774,10 @@ DEFERRED is passed to `eglot--async-request', which see." (let ((warning-minimum-level :error)) (display-warning 'eglot (apply #'format format args) :warning))) +(defun eglot--debug (server format &rest args) + "Warning message with FORMAT and ARGS." + (eglot--log-event server `(:message ,(format format args)))) + (defun eglot--pos-to-lsp-position (&optional pos) "Convert point POS to LSP position." (save-excursion commit e4039bba6d27d0746175a7f787364d1a3dcf2f87 Author: Josh Elsasser Date: Mon May 21 12:27:05 2018 -0700 Add cquery support for c/c++ projects Implements minimal support for the core cquery language server. None of its extensions are implemented yet. * eglot.el (eglot-server-programs): Add cquery to list of guessed programs for c-mode and c++-mode. (eglot-initialization-options eglot-cquery): Specialize init options to pass cquery a cache directory and disable a flood of $cquery/progress messages. (eglot-handle-notification $cquery/publishSemanticHighlighting): (eglot-handle-notification $cquery/setInactiveRegions): (eglot-handle-notification $cquery/progress): New no-op functions to avoid filling logs with "unknown message" warnings. (eglot-cquery): New eglot-lsp-server subclass. * README.md: Mention cquery in the README. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3d36344440..189ad1e5aa 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -72,6 +72,8 @@ (python-mode . ("pyls")) (js-mode . ("javascript-typescript-stdio")) (sh-mode . ("bash-language-server" "start")) + (c++-mode . (eglot-cquery "cquery")) + (c-mode . (eglot-cquery "cquery")) (php-mode . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php"))) "How the command `eglot' guesses the server to start. @@ -1613,6 +1615,34 @@ Proceed? " (funcall (or eglot--current-flymake-report-fn #'ignore) eglot--unreported-diagnostics))))) + +;;; cquery-specific +;;; +(defclass eglot-cquery (eglot-lsp-server) () + :documentation "cquery's C/C++ langserver.") + +(cl-defmethod eglot-initialization-options ((server eglot-cquery)) + "Passes through required cquery initialization options" + (let* ((root (car (project-roots (eglot--project server)))) + (cache (expand-file-name ".cquery_cached_index/" root))) + (vector :cacheDirectory (file-name-as-directory cache) + :progressReportFrequencyMs -1))) + +(cl-defmethod eglot-handle-notification + ((server eglot-cquery) (_method (eql :$cquery/progress)) + &rest counts &key activeThreads &allow-other-keys) + "No-op for noisy $cquery/progress extension") + +(cl-defmethod eglot-handle-notification + ((server eglot-cquery) (_method (eql :$cquery/setInactiveRegions)) + &key uri inactiveRegions &allow-other-keys) + "No-op for unsupported $cquery/setInactiveRegions extension") + +(cl-defmethod eglot-handle-notification + ((server eglot-cquery) (_method (eql :$cquery/publishSemanticHighlighting)) + &key uri symbols &allow-other-keys) + "No-op for unsupported $cquery/publishSemanticHighlighting extension") + (provide 'eglot) ;;; eglot.el ends here commit 0ebd4a816dff264519407694ed7d8d39bbce9c5a Author: João Távora Date: Tue May 22 11:42:12 2018 +0100 Fix indentation broken by the defclass monster commit * eglot.el (for, eglot-handle-notification publishDiagnostics) (eglot-handle-request registerCapability, eglot-handle-request unregisterCapability, eglot-handle-request applyEdit): fix indentation. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5e9bdc3566..3d36344440 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -113,7 +113,7 @@ lasted more than that many seconds." (integer :tag "Number of seconds"))) -;;; API +;;; API (WORK-IN-PROGRESS!) ;;; (defmacro eglot--obj (&rest what) "Make WHAT a JSON object suitable for `json-encode'." @@ -302,7 +302,7 @@ class SERVER-CLASS." :initialize (eglot--obj :processId (unless (eq (process-type proc) 'network) (emacs-pid)) - :capabilities (eglot-client-capabilities) + :capabilities (eglot-client-capabilities server) :rootPath (expand-file-name (car (project-roots project))) :rootUri (eglot--path-to-uri (car (project-roots project))) :initializationOptions (eglot-initialization-options server))) @@ -820,15 +820,15 @@ If optional MARKER, return a marker instead" (insert string) (font-lock-ensure) (buffer-string)))) (defun eglot--server-capable (&rest feats) -"Determine if current server is capable of FEATS." -(cl-loop for caps = (eglot--capabilities (eglot--current-server-or-lose)) - then (cadr probe) - for feat in feats - for probe = (plist-member caps feat) - if (not probe) do (cl-return nil) - if (eq (cadr probe) t) do (cl-return t) - if (eq (cadr probe) :json-false) do (cl-return nil) - finally (cl-return (or probe t)))) + "Determine if current server is capable of FEATS." + (cl-loop for caps = (eglot--capabilities (eglot--current-server-or-lose)) + then (cadr probe) + for feat in feats + for probe = (plist-member caps feat) + if (not probe) do (cl-return nil) + if (eq (cadr probe) t) do (cl-return t) + if (eq (cadr probe) :json-false) do (cl-return nil) + finally (cl-return (or probe t)))) (defun eglot--range-region (range &optional markers) "Return region (BEG END) that represents LSP RANGE. @@ -1032,7 +1032,7 @@ function with the server still running." "Unreported diagnostics for this buffer.") (cl-defmethod eglot-handle-notification - (_server (_method (eql :textDocument/publishDiagnostics)) &key uri diagnostics) + (_server (_method (eql :textDocument/publishDiagnostics)) &key uri diagnostics) "Handle notification publishDiagnostics" (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) (with-current-buffer buffer @@ -1073,18 +1073,18 @@ THINGS are either registrations or unregisterations." (eglot--reply server jsonrpc-id :result (eglot--obj :message "OK"))) (cl-defmethod eglot-handle-request - (server id (_method (eql :client/registerCapability)) &key registrations) + (server id (_method (eql :client/registerCapability)) &key registrations) "Handle server request client/registerCapability" (eglot--register-unregister server id registrations 'register)) (cl-defmethod eglot-handle-request (server id (_method (eql :client/unregisterCapability)) - &key unregisterations) ;; XXX: "unregisterations" (sic) + &key unregisterations) ;; XXX: "unregisterations" (sic) "Handle server request client/unregisterCapability" (eglot--register-unregister server id unregisterations 'unregister)) (cl-defmethod eglot-handle-request - (server id (_method (eql :workspace/applyEdit)) &key _label edit) + (server id (_method (eql :workspace/applyEdit)) &key _label edit) "Handle server request workspace/applyEdit" (condition-case err (progn (eglot--apply-workspace-edit edit 'confirm) commit c765121f67df7f7cdc03e16b1130f4e508b39a98 Author: João Távora Date: Tue May 22 11:41:02 2018 +0100 Introduce new api methods for experimental clients to use Should help Josh Elsasser implement pull request https://github.com/joaotavora/eglot/issues/6. * eglot.el (eglot--obj): Move upwards in file. (eglot-server-ready-p): Tweak comment. (eglot-initialization-options): New API defgeneric.. (eglot-client-capabilities): New API defgeneric. (eglot--client-capabilities): Remove. (eglot--connect): Call new API methods here. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 43cda1b42e..5e9bdc3566 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -115,12 +115,19 @@ lasted more than that many seconds." ;;; API ;;; +(defmacro eglot--obj (&rest what) + "Make WHAT a JSON object suitable for `json-encode'." + (declare (debug (&rest form))) + ;; FIXME: not really API. Should it be? + ;; FIXME: maybe later actually do something, for now this just fixes + ;; the indenting of literal plists. + `(list ,@what)) + (cl-defgeneric eglot-server-ready-p (server what) ;; API "Tell if SERVER is ready for WHAT in current buffer. If it isn't, a deferrable `eglot--async-request' *will* be deferred to the future." - (:method (_s _what) - "Normally not ready if outstanding changes." + (:method (_s _what) "Normally ready if no outstanding changes." (not (eglot--outstanding-edits-p)))) (cl-defgeneric eglot-handle-request (server method id &rest params) @@ -129,6 +136,35 @@ deferred to the future." (cl-defgeneric eglot-handle-notification (server method id &rest params) "Handle SERVER's METHOD notification with PARAMS.") +(cl-defgeneric eglot-initialization-options (server) + "JSON object to send under `initializationOptions'" + (:method (_s) nil)) ; blank default + +(cl-defgeneric eglot-client-capabilities (server) + "What the EGLOT LSP client supports for SERVER." + (:method (_s) + (eglot--obj + :workspace (eglot--obj + :applyEdit t + :workspaceEdit `(:documentChanges :json-false) + :didChangeWatchesFiles `(:dynamicRegistration t) + :symbol `(:dynamicRegistration :json-false)) + :textDocument + (eglot--obj + :synchronization (eglot--obj + :dynamicRegistration :json-false + :willSave t :willSaveWaitUntil t :didSave t) + :completion `(:dynamicRegistration :json-false) + :hover `(:dynamicRegistration :json-false) + :signatureHelp `(:dynamicRegistration :json-false) + :references `(:dynamicRegistration :json-false) + :definition `(:dynamicRegistration :json-false) + :documentSymbol `(:dynamicRegistration :json-false) + :documentHighlight `(:dynamicRegistration :json-false) + :rename `(:dynamicRegistration :json-false) + :publishDiagnostics `(:relatedInformation :json-false)) + :experimental (eglot--obj)))) + ;;; Process management (defvar eglot--servers-by-project (make-hash-table :test #'equal) @@ -223,13 +259,6 @@ CONTACT is in `eglot'. Returns a process object." (let ((inhibit-read-only t)) (erase-buffer) (read-only-mode t))) proc)) -(defmacro eglot--obj (&rest what) - "Make WHAT a suitable argument for `json-encode'." - (declare (debug (&rest form))) - ;; FIXME: maybe later actually do something, for now this just fixes - ;; the indenting of literal plists. - `(list ,@what)) - (defun eglot--all-major-modes () "Return all know major modes." (let ((retval)) @@ -238,29 +267,6 @@ CONTACT is in `eglot'. Returns a process object." (push sym retval)))) retval)) -(defun eglot--client-capabilities () - "What the EGLOT LSP client supports." - (eglot--obj - :workspace (eglot--obj - :applyEdit t - :workspaceEdit `(:documentChanges :json-false) - :didChangeWatchesFiles `(:dynamicRegistration t) - :symbol `(:dynamicRegistration :json-false)) - :textDocument (eglot--obj - :synchronization (eglot--obj - :dynamicRegistration :json-false - :willSave t :willSaveWaitUntil t :didSave t) - :completion `(:dynamicRegistration :json-false) - :hover `(:dynamicRegistration :json-false) - :signatureHelp `(:dynamicRegistration :json-false) - :references `(:dynamicRegistration :json-false) - :definition `(:dynamicRegistration :json-false) - :documentSymbol `(:dynamicRegistration :json-false) - :documentHighlight `(:dynamicRegistration :json-false) - :rename `(:dynamicRegistration :json-false) - :publishDiagnostics `(:relatedInformation :json-false)) - :experimental (eglot--obj))) - (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") (defun eglot--connect (project managed-major-mode contact server-class) @@ -294,15 +300,12 @@ class SERVER-CLASS." (eglot--request server :initialize - (eglot--obj :processId (unless (eq (process-type proc) - 'network) - (emacs-pid)) - :capabilities(eglot--client-capabilities) - :rootPath (expand-file-name - (car (project-roots project))) - :rootUri (eglot--path-to-uri - (car (project-roots project))) - :initializationOptions [])) + (eglot--obj + :processId (unless (eq (process-type proc) 'network) (emacs-pid)) + :capabilities (eglot-client-capabilities) + :rootPath (expand-file-name (car (project-roots project))) + :rootUri (eglot--path-to-uri (car (project-roots project))) + :initializationOptions (eglot-initialization-options server))) (setf (eglot--capabilities server) capabilities) (setf (eglot--status server) nil) (dolist (buffer (buffer-list)) commit 40d7fc6f043ee45549c1a7334d853f9a2c5ad231 Author: João Távora Date: Tue May 22 11:14:08 2018 +0100 Do rust's rls hack properly with new class-based api * eglot.el (eglot-server-ready-p): New API method. (eglot-handle-request, eglot-handle-notification): New defgeneric's. (eglot--ready-predicates, eglot--server-ready-p): Remove. (eglot--async-request): Call eglot-server-ready-p. (eglot--request): Tweak comment. (eglot--rls-probably-ready-for-p): Remove. (eglot-server-ready-p eglot-rls): Adapts earlier eglot--rls-probably-ready-for-p. (eglot-handle-notification eglot-rls): Specialize to eglot-rls. (eglot-rls): New eglot-lsp-server subclass. * eglot-tests.el (auto-detect-running-server) (auto-reconnect, basic-completions) (hover-after-completions): Use eglot--interactive diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cb213e515c..43cda1b42e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -68,7 +68,7 @@ :prefix "eglot-" :group 'applications) -(defvar eglot-server-programs '((rust-mode . ("rls")) +(defvar eglot-server-programs '((rust-mode . (eglot-rls "rls")) (python-mode . ("pyls")) (js-mode . ("javascript-typescript-stdio")) (sh-mode . ("bash-language-server" "start")) @@ -112,6 +112,23 @@ lasted more than that many seconds." :type '(choice (boolean :tag "Whether to inhibit autoreconnection") (integer :tag "Number of seconds"))) + +;;; API +;;; +(cl-defgeneric eglot-server-ready-p (server what) ;; API + "Tell if SERVER is ready for WHAT in current buffer. +If it isn't, a deferrable `eglot--async-request' *will* be +deferred to the future." + (:method (_s _what) + "Normally not ready if outstanding changes." + (not (eglot--outstanding-edits-p)))) + +(cl-defgeneric eglot-handle-request (server method id &rest params) + "Handle SERVER's METHOD request with ID and PARAMS.") + +(cl-defgeneric eglot-handle-notification (server method id &rest params) + "Handle SERVER's METHOD notification with PARAMS.") + ;;; Process management (defvar eglot--servers-by-project (make-hash-table :test #'equal) @@ -619,16 +636,6 @@ originated." (eglot--log-event server `(:running-deferred ,(length actions))) (mapc #'funcall (mapcar #'car actions)))) -(defvar eglot--ready-predicates '(eglot--server-ready-p) - "Special hook of predicates controlling deferred actions. -If one of these returns nil, a deferrable `eglot--async-request' -will be deferred. Each predicate is passed the symbol for the -request request and a process object.") - -(defun eglot--server-ready-p (_what _server) - "Tell if SERVER is ready for processing deferred WHAT." - (not (eglot--outstanding-edits-p))) - (cl-defmacro eglot--lambda (cl-lambda-list &body body) (declare (indent 1) (debug (sexp &rest form))) (let ((e (gensym "eglot--lambda-elem"))) @@ -668,8 +675,7 @@ TIMER)." (existing (gethash (list deferred buf) (eglot--deferred-actions server)))) (when existing (setq existing (cadr existing))) - (if (run-hook-with-args-until-failure 'eglot--ready-predicates - deferred server) + (if (eglot-server-ready-p server deferred) (remhash (list deferred buf) (eglot--deferred-actions server)) (eglot--log-event server `(:deferring ,method :id ,id :params ,params)) (let* ((buf (current-buffer)) (point (point)) @@ -708,9 +714,9 @@ TIMER)." "Like `eglot--async-request' for SERVER, METHOD and PARAMS, but synchronous. Meaning only return locally if successful, otherwise exit non-locally. DEFERRED is passed to `eglot--async-request', which see." - ;; Launching a deferred sync request with outstanding changes is a - ;; bad idea, since that might lead to the request never having a - ;; chance to run, because `eglot--ready-predicates'. + ;; HACK: A deferred sync request with outstanding changes is a bad + ;; idea, since that might lead to the request never having a chance + ;; to run, because idle timers don't run in `accept-process-output'. (when deferred (eglot--signal-textDocument/didChange)) (let* ((done (make-symbol "eglot-catch")) id-and-timer (res @@ -1583,22 +1589,19 @@ Proceed? " ;;; Rust-specific ;;; -(defun eglot--rls-probably-ready-for-p (what server) - "Guess if the RLS running in SERVER is ready for WHAT." - (or (eq what :textDocument/completion) ; RLS normally ready for this - ; one, even if building ; - (pcase-let ((`(,_id ,what ,done ,_detail) (eglot--spinner server))) - (and (equal "Indexing" what) done)))) +(defclass eglot-rls (eglot-lsp-server) () :documentation "Rustlang's RLS.") -;;;###autoload -(progn - (add-hook 'rust-mode-hook 'eglot--setup-rls-idiosyncrasies) - (defun eglot--setup-rls-idiosyncrasies () - "Prepare `eglot' to deal with RLS's special treatment." - (add-hook 'eglot--ready-predicates 'eglot--rls-probably-ready-for-p t t))) +(cl-defmethod eglot-server-ready-p ((server eglot-rls) what) + "Except for :completion, RLS isn't ready until Indexing done." + (and (cl-call-next-method) + (or ;; RLS normally ready for this, even if building. + (eq :textDocument/completion what) + (pcase-let ((`(,_id ,what ,done ,_detail) (eglot--spinner server))) + (and (equal "Indexing" what) done))))) (cl-defmethod eglot-handle-notification - (server (_method (eql :window/progress)) &key id done title message &allow-other-keys) + ((server eglot-rls) (_method (eql :window/progress)) + &key id done title message &allow-other-keys) "Handle notification window/progress" (setf (eglot--spinner server) (list id title done message)) (when (and (equal "Indexing" title) done) commit 7e1b0cd1331e659b18b40e32af4f57fc089d5354 Author: João Távora Date: Tue May 22 02:00:49 2018 +0100 Use an eieio class to represent a server. Allow clients of eglot.el to use specific server classes to represent experimental servers. Wherever you used to read "proc" you now probably read "server", unless it's really the process properties that are sought after. Should help Josh Elsasser implement pull request https://github.com/joaotavora/eglot/issues/6. * eglot-tests.el (eglot--call-with-dirs-and-files) (auto-detect-running-server, auto-reconnect, basic-completions) (hover-after-completions): Adapt to server defclass instead of proc. * eglot.el (eglot-server-programs): Add docstring. (eglot--processes-by-project): Removed. (eglot--servers-by-project): New variable. (eglot--current-process): Removed. (eglot--current-server): New function. (eglot-server): New class. (cl-print-object eglot-server): New method. (eglot--current-process-or-lose): Removed. (eglot--current-server-or-lose): New function. (eglot--define-process-var): Remove. (eglot--make-process): Rework. (eglot--project-short-name): Remove. (eglot--connect): Rework. (eglot--interactive): Rework to allow custom classes. (eglot, eglot-reconnect, eglot--process-sentinel) (eglot--process-filter, eglot-events-buffer, eglot--log-event): Rework. (eglot--process-receive): Removed. (eglot--server-receive): New function. (eglot--send): Renamed from eglot--process-send. (eglot--process-send): Removed. (eglot-forget-pending-continuations) (eglot-clear-status, eglot--call-deferred) (eglot--server-ready-p, eglot--async-request, eglot--request) (eglot--notify, eglot--reply, eglot--managed-mode-onoff) (eglot--maybe-activate-editing-mode, eglot--mode-line-format): Rework. (eglot-shutdown): Rework. (eglot-handle-notification *, eglot-handle-request *) (eglot--register-unregister) (eglot--signal-textDocument/didOpen) (eglot--signal-textDocument/didClose) (eglot--signal-textDocument/willSave) (eglot--signal-textDocument/didSave) (xref-backend-identifier-completion-table) (xref-backend-definitions, xref-backend-references) (xref-backend-apropos, eglot-completion-at-point) (eglot-help-at-point, eglot-eldoc-function, eglot-imenu) (eglot-rename) (eglot--register-workspace/didChangeWatchedFiles) (eglot--unregister-workspace/didChangeWatchedFiles) (eglot--rls-probably-ready-for-p, eglot-handle-notification): Rework (proc->server) fixup * eglot-tests.el (eglot--call-with-dirs-and-files) (auto-detect-running-server, auto-reconnect, basic-completions) (hover-after-completions): * eglot.el (eglot--processes-by-project): Removed. (eglot--servers-by-project): New variable. (eglot--current-process): Removed. (eglot--current-server): New function. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 65beb35b2a..cb213e515c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -74,7 +74,25 @@ (sh-mode . ("bash-language-server" "start")) (php-mode . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php"))) - "Alist mapping major modes to server executables.") + "How the command `eglot' guesses the server to start. +An association list of (MAJOR-MODE . SPEC) pair. MAJOR-MODE is a +mode symbol. SPEC is + +* In the most common case, a list of strings (PROGRAM [ARGS...]). +PROGRAM is called with ARGS and is expected to serve LSP requests +over the standard input/output channels. + +* A list (HOST PORT [ARGS...]) where HOST is a string and PORT is a +positive integer number for connecting to a server via TCP. +Remaining ARGS are passed to `open-network-stream' for upgrading +the connection with encryption, etc... + +* A function of no arguments returning a connected process. + +* A cons (CLASS-NAME . SPEC) where CLASS-NAME is a symbol +designating a subclass of `eglot-lsp-server', for +representing experimental LSP servers. In this case SPEC is +interpreted as described above this point.") (defface eglot-mode-line '((t (:inherit font-lock-constant-face :weight bold))) @@ -96,93 +114,86 @@ lasted more than that many seconds." ;;; Process management -(defvar eglot--processes-by-project (make-hash-table :test #'equal) +(defvar eglot--servers-by-project (make-hash-table :test #'equal) "Keys are projects. Values are lists of processes.") -(defun eglot--current-process () +(defclass eglot-lsp-server () + ((process + :documentation "Wrapped process object." + :initarg :process :accessor eglot--process) + (name + :documentation "Readable name used for naming processes, buffers, etc..." + :initarg :name :accessor eglot--name) + (project-nickname + :documentation "Short nickname for the associated project." + :initarg :project-nickname :accessor eglot--project-nickname) + (major-mode + :documentation "Major mode symbol." + :initarg :major-mode :accessor eglot--major-mode) + (pending-continuations + :documentation "Map request ID's to (SUCCESS-FN ERROR-FN TIMEOUT-FN) triads." + :initform (make-hash-table) :accessor eglot--pending-continuations) + (events-buffer + :documentation "Buffer holding a log of server-related events." + :accessor eglot--events-buffer) + (capabilities + :documentation "JSON object containing server capabilities." + :accessor eglot--capabilities) + (moribund + :documentation "Flag set when server is shutting down." + :accessor eglot--moribund) + (project + :documentation "Project associated with server." + :initarg :project :accessor eglot--project) + (spinner + :documentation "List (ID DOING-WHAT DONE-P) representing server progress." + :initform `(nil nil t) :accessor eglot--spinner) + (status + :documentation "List (STATUS SERIOUS-P) representing server problems/status." + :initform `(:unknown nil) :accessor eglot--status) + (inhibit-autoreconnect + :documentation "Generalized boolean inhibiting auto-reconnection if true." + :initarg :inhibit-autoreconnect :accessor eglot--inhibit-autoreconnect) + (contact + :documentation "How server was started and how it can be re-started." + :initarg :contact :accessor eglot--contact) + (deferred-actions + :documentation "Map (DEFERRED-ID BUF) to (FN TIMER). +DEFERRED request from BUF is FN. It's sent later, not later than TIMER." + :initform (make-hash-table :test #'equal) :accessor eglot--deferred-actions) + (file-watches + :documentation "Map ID to list of WATCHES for `didChangeWatchedFiles'." + :initform (make-hash-table :test #'equal) :accessor eglot--file-watches) + (managed-buffers + :documentation "List of buffers managed by server." + :initarg :managed-buffers :accessor eglot--managed-buffers)) + :documentation + "Represents a server. Wraps a process for LSP communication.") + +(cl-defmethod cl-print-object ((obj eglot-lsp-server) stream) + (princ (format "#<%s: %s>" (eieio-object-class obj) (eglot--name obj)) stream)) + +(defun eglot--current-server () "The current logical EGLOT process." (let* ((probe (or (project-current) `(transient . ,default-directory)))) - (cl-find major-mode (gethash probe eglot--processes-by-project) + (cl-find major-mode (gethash probe eglot--servers-by-project) :key #'eglot--major-mode))) -(defun eglot--current-process-or-lose () +(defun eglot--current-server-or-lose () "Return the current EGLOT process or error." - (or (eglot--current-process) (eglot--error "No current EGLOT process"))) - -(defmacro eglot--define-process-var (var-sym initval &optional doc) - "Define VAR-SYM as a generalized process-local variable. -INITVAL is the default value. DOC is the documentation." - (declare (indent 2) (doc-string 3)) - `(progn - (defun ,var-sym (proc) - ,doc (let* ((plist (process-plist proc)) - (probe (plist-member plist ',var-sym))) - (if probe (cadr probe) - (let ((def ,initval)) (process-put proc ',var-sym def) def)))) - (gv-define-setter ,var-sym (to-store process) - `(let ((once ,to-store)) (process-put ,process ',',var-sym once) once)))) - -(eglot--define-process-var eglot--short-name nil - "A short name for the process") - -(eglot--define-process-var eglot--major-mode nil - "The major-mode this server is managing.") - -(eglot--define-process-var eglot--expected-bytes nil - "How many bytes declared by server") - -(eglot--define-process-var eglot--pending-continuations (make-hash-table) - "A hash table of request ID to continuation lambdas") - -(eglot--define-process-var eglot--events-buffer nil - "A buffer pretty-printing the EGLOT RPC events") - -(eglot--define-process-var eglot--capabilities :unreported - "Holds list of capabilities that server reported") - -(eglot--define-process-var eglot--moribund nil - "Non-nil if server is about to exit") - -(eglot--define-process-var eglot--project nil - "The project the server belongs to.") - -(eglot--define-process-var eglot--spinner `(nil nil t) - "\"Spinner\" used by some servers. -A list (ID WHAT DONE-P).") + (or (eglot--current-server) (eglot--error "No current EGLOT process"))) -(eglot--define-process-var eglot--status `(:unknown nil) - "Status as declared by the server. -A list (WHAT SERIOUS-P).") - -(eglot--define-process-var eglot--inhibit-autoreconnect eglot-autoreconnect - "If non-nil, don't autoreconnect on unexpected quit.") - -(eglot--define-process-var eglot--contact nil - "Method used to contact a server.") - -(eglot--define-process-var eglot--deferred-actions - (make-hash-table :test #'equal) - "Actions deferred to when server is thought to be ready.") - -(eglot--define-process-var eglot--file-watches (make-hash-table :test #'equal) - "File system watches for the didChangeWatchedfiles thingy.") - -(eglot--define-process-var eglot--managed-buffers nil - "Buffers managed by the server.") - -(defun eglot--make-process (name managed-major-mode contact) - "Make a process from CONTACT. +(defun eglot--make-process (name contact) + "Make a process object from CONTACT. NAME is used to name the the started process or connection. -MANAGED-MAJOR-MODE is a symbol naming a major mode. CONTACT is in `eglot'. Returns a process object." - (let* ((readable-name (format "EGLOT server (%s/%s)" name managed-major-mode)) - (buffer (get-buffer-create (format "*%s stdout*" readable-name))) + (let* ((buffer (get-buffer-create (format "*%s stdout*" name))) (proc (cond ((processp contact) contact) ((integerp (cadr contact)) - (apply #'open-network-stream readable-name buffer contact)) + (apply #'open-network-stream name buffer contact)) (t (make-process - :name readable-name + :name name :command contact :coding 'no-conversion :connection-type 'pipe @@ -191,6 +202,8 @@ CONTACT is in `eglot'. Returns a process object." (set-marker (process-mark proc) (with-current-buffer buffer (point-min))) (set-process-filter proc #'eglot--process-filter) (set-process-sentinel proc #'eglot--process-sentinel) + (with-current-buffer buffer + (let ((inhibit-read-only t)) (erase-buffer) (read-only-mode t))) proc)) (defmacro eglot--obj (&rest what) @@ -200,10 +213,6 @@ CONTACT is in `eglot'. Returns a process object." ;; the indenting of literal plists. `(list ,@what)) -(defun eglot--project-short-name (project) - "Give PROJECT a short name." - (file-name-base (directory-file-name (car (project-roots project))))) - (defun eglot--all-major-modes () "Return all know major modes." (let ((retval)) @@ -237,54 +246,57 @@ CONTACT is in `eglot'. Returns a process object." (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") -(defun eglot--connect (project managed-major-mode short-name contact _interactive) - "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT. -INTERACTIVE is t if inside interactive call." - (let* ((proc (eglot--make-process - short-name managed-major-mode (if (functionp contact) - (funcall contact) contact))) - (buffer (process-buffer proc))) - (setf (eglot--contact proc) contact - (eglot--project proc) project - (eglot--major-mode proc) managed-major-mode) - (with-current-buffer buffer - (let ((inhibit-read-only t) success) - (setf (eglot--inhibit-autoreconnect proc) - (cond - ((booleanp eglot-autoreconnect) (not eglot-autoreconnect)) - ((cl-plusp eglot-autoreconnect) - (run-with-timer eglot-autoreconnect nil - (lambda () - (setf (eglot--inhibit-autoreconnect proc) - (null eglot-autoreconnect))))))) - (setf (eglot--short-name proc) short-name) - (push proc (gethash project eglot--processes-by-project)) - (run-hook-with-args 'eglot-connect-hook proc) - (erase-buffer) - (read-only-mode t) - (unwind-protect - (cl-destructuring-bind (&key capabilities) - (eglot--request - proc - :initialize - (eglot--obj :processId (unless (eq (process-type proc) - 'network) - (emacs-pid)) - :capabilities(eglot--client-capabilities) - :rootPath (expand-file-name - (car (project-roots project))) - :rootUri (eglot--path-to-uri - (car (project-roots project))) - :initializationOptions [])) - (setf (eglot--capabilities proc) capabilities) - (setf (eglot--status proc) nil) - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (eglot--maybe-activate-editing-mode proc))) - (eglot--notify proc :initialized (eglot--obj :__dummy__ t)) - (setq success proc)) - (unless (or success (not (process-live-p proc)) (eglot--moribund proc)) - (eglot-shutdown proc))))))) +(defun eglot--connect (project managed-major-mode contact server-class) + "Connect for PROJECT, MANAGED-MAJOR-MODE and CONTACT. +INTERACTIVE is t if inside interactive call. Return an object of +class SERVER-CLASS." + (let* ((nickname (file-name-base (directory-file-name + (car (project-roots project))))) + (name (format "EGLOT (%s/%s)" nickname managed-major-mode)) + (proc (eglot--make-process + name (if (functionp contact) (funcall contact) contact))) + server connect-success) + (setq server + (make-instance + (or server-class 'eglot-lsp-server) + :process proc :major-mode managed-major-mode + :project project :contact contact + :name name :project-nickname nickname + :inhibit-autoreconnect + (cond + ((booleanp eglot-autoreconnect) (not eglot-autoreconnect)) + ((cl-plusp eglot-autoreconnect) + (run-with-timer eglot-autoreconnect nil + (lambda () + (setf (eglot--inhibit-autoreconnect server) + (null eglot-autoreconnect)))))))) + (push server (gethash project eglot--servers-by-project)) + (process-put proc 'eglot-server server) + (unwind-protect + (cl-destructuring-bind (&key capabilities) + (eglot--request + server + :initialize + (eglot--obj :processId (unless (eq (process-type proc) + 'network) + (emacs-pid)) + :capabilities(eglot--client-capabilities) + :rootPath (expand-file-name + (car (project-roots project))) + :rootUri (eglot--path-to-uri + (car (project-roots project))) + :initializationOptions [])) + (setf (eglot--capabilities server) capabilities) + (setf (eglot--status server) nil) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (eglot--maybe-activate-editing-mode server))) + (eglot--notify server :initialized (eglot--obj :__dummy__ t)) + (run-hook-with-args 'eglot-connect-hook server) + (setq connect-success server)) + (unless (or connect-success + (not (process-live-p proc)) (eglot--moribund server)) + (eglot-shutdown server))))) (defvar eglot--command-history nil "History of COMMAND arguments to `eglot'.") @@ -303,34 +315,37 @@ INTERACTIVE is t if inside interactive call." (symbol-name guessed-mode) nil (symbol-name guessed-mode) nil))) (t guessed-mode))) (project (or (project-current) `(transient . ,default-directory))) - (guessed (cdr (assoc managed-mode eglot-server-programs))) - (program (and (listp guessed) (stringp (car guessed)) (car guessed))) + (guess (cdr (assoc managed-mode eglot-server-programs))) + (class (and (consp guess) (symbolp (car guess)) + (prog1 (car guess) (setq guess (cdr guess))))) + (program (and (listp guess) (stringp (car guess)) (car guess))) (base-prompt "[eglot] Enter program to execute (or :): ") (prompt (cond (current-prefix-arg base-prompt) - ((null guessed) + ((null guess) (format "[eglot] Sorry, couldn't guess for `%s'\n%s!" managed-mode base-prompt)) ((and program (not (executable-find program))) (concat (format "[eglot] I guess you want to run `%s'" - (combine-and-quote-strings guessed)) + (combine-and-quote-strings guess)) (format ", but I can't find `%s' in PATH!" program) "\n" base-prompt)))) (contact (if prompt (let ((s (read-shell-command prompt - (if program (combine-and-quote-strings guessed)) + (if program (combine-and-quote-strings guess)) 'eglot-command-history))) (if (string-match "^\\([^\s\t]+\\):\\([[:digit:]]+\\)$" (string-trim s)) (list (match-string 1 s) (string-to-number (match-string 2 s))) (split-string-and-unquote s))) - guessed))) - (list managed-mode project contact t))) + guess))) + (list managed-mode project contact class t))) ;;;###autoload -(defun eglot (managed-major-mode project command &optional interactive) +(defun eglot (managed-major-mode project command server-class + &optional interactive) "Manage a project with a Language Server Protocol (LSP) server. The LSP server is started (or contacted) via COMMAND. If this @@ -356,86 +371,90 @@ list is of the form \":\" it is taken as an indication to connect to a server instead of starting one. This is also know as the server's \"contact\". -MANAGED-MAJOR-MODE is an Emacs major mode. +SERVER-CLASS is a symbol naming a class that must inherit from +`eglot-server', or nil to use the default server class. INTERACTIVE is t if called interactively." (interactive (eglot--interactive)) - (let* ((short-name (eglot--project-short-name project))) - (let ((current-process (eglot--current-process))) - (if (and (process-live-p current-process) - interactive - (y-or-n-p "[eglot] Live process found, reconnect instead? ")) - (eglot-reconnect current-process interactive) - (when (process-live-p current-process) - (eglot-shutdown current-process)) - (let ((proc (eglot--connect project + (let ((current-server (eglot--current-server))) + (if (and current-server + (process-live-p (eglot--process current-server)) + interactive + (y-or-n-p "[eglot] Live process found, reconnect instead? ")) + (eglot-reconnect current-server interactive) + (when (and current-server + (process-live-p (eglot--process current-server))) + (eglot-shutdown current-server)) + (let ((server (eglot--connect project managed-major-mode - short-name command - interactive))) - (eglot--message "Connected! Process `%s' now \ + server-class))) + (eglot--message "Connected! Server `%s' now \ managing `%s' buffers in project `%s'." - proc managed-major-mode short-name) - proc))))) + (eglot--name server) managed-major-mode + (eglot--project-nickname server)) + server)))) -(defun eglot-reconnect (process &optional interactive) - "Reconnect to PROCESS. +(defun eglot-reconnect (server &optional interactive) + "Reconnect to SERVER. INTERACTIVE is t if called interactively." - (interactive (list (eglot--current-process-or-lose) t)) - (when (process-live-p process) - (eglot-shutdown process interactive)) - (eglot--connect (eglot--project process) - (eglot--major-mode process) - (eglot--short-name process) - (eglot--contact process) - interactive) + (interactive (list (eglot--current-server-or-lose) t)) + (when (process-live-p (eglot--process server)) + (eglot-shutdown server interactive)) + (eglot--connect (eglot--project server) + (eglot--major-mode server) + (eglot--contact server) + (eieio-object-class server)) (eglot--message "Reconnected!")) (defun eglot--process-sentinel (proc change) "Called when PROC undergoes CHANGE." - (eglot--log-event proc `(:message "Process state changed" :change ,change)) - (when (not (process-live-p proc)) - (with-current-buffer (eglot-events-buffer proc) - (let ((inhibit-read-only t)) - (insert "\n----------b---y---e---b---y---e----------\n"))) - ;; Cancel outstanding timers and file system watches - (maphash (lambda (_id triplet) - (cl-destructuring-bind (_success _error timeout) triplet - (cancel-timer timeout))) - (eglot--pending-continuations proc)) - (maphash (lambda (_id watches) - (mapcar #'file-notify-rm-watch watches)) - (eglot--file-watches proc)) - (unwind-protect - ;; Call all outstanding error handlers - (maphash (lambda (_id triplet) - (cl-destructuring-bind (_success error _timeout) triplet - (funcall error `(:code -1 :message "Server died")))) - (eglot--pending-continuations proc)) - ;; Turn off `eglot--managed-mode' where appropriate. - (dolist (buffer (eglot--managed-buffers proc)) - (with-current-buffer buffer (eglot--managed-mode-onoff proc -1))) - ;; Forget about the process-project relationship - (setf (gethash (eglot--project proc) eglot--processes-by-project) - (delq proc - (gethash (eglot--project proc) eglot--processes-by-project))) - ;; Say last words - (eglot--message "%s exited with status %s" proc (process-exit-status proc)) - (delete-process proc) - ;; Consider autoreconnecting - (cond ((eglot--moribund proc)) - ((not (eglot--inhibit-autoreconnect proc)) - (eglot--warn "Reconnecting after unexpected server exit") - (eglot-reconnect proc)) - ((timerp (eglot--inhibit-autoreconnect proc)) - (eglot--warn "Not auto-reconnecting, last on didn't last long.")))))) + (let ((server (process-get proc 'eglot-server))) + (eglot--log-event server `(:message "Process state changed" :change ,change)) + (when (not (process-live-p proc)) + (with-current-buffer (eglot-events-buffer server) + (let ((inhibit-read-only t)) + (insert "\n----------b---y---e---b---y---e----------\n"))) + ;; Cancel outstanding timers and file system watches + (maphash (lambda (_id triplet) + (cl-destructuring-bind (_success _error timeout) triplet + (cancel-timer timeout))) + (eglot--pending-continuations server)) + (maphash (lambda (_id watches) + (mapcar #'file-notify-rm-watch watches)) + (eglot--file-watches server)) + (unwind-protect + ;; Call all outstanding error handlers + (maphash (lambda (_id triplet) + (cl-destructuring-bind (_success error _timeout) triplet + (funcall error `(:code -1 :message "Server died")))) + (eglot--pending-continuations server)) + ;; Turn off `eglot--managed-mode' where appropriate. + (dolist (buffer (eglot--managed-buffers server)) + (with-current-buffer buffer (eglot--managed-mode-onoff server -1))) + ;; Forget about the process-project relationship + (setf (gethash (eglot--project server) eglot--servers-by-project) + (delq server + (gethash (eglot--project server) eglot--servers-by-project))) + ;; Say last words + (eglot--message "%s exited with status %s" (eglot--name server) + (process-exit-status + (eglot--process server))) + (delete-process proc) + ;; Consider autoreconnecting + (cond ((eglot--moribund server)) + ((not (eglot--inhibit-autoreconnect server)) + (eglot--warn "Reconnecting after unexpected server exit") + (eglot-reconnect server)) + ((timerp (eglot--inhibit-autoreconnect server)) + (eglot--warn "Not auto-reconnecting, last on didn't last long."))))))) (defun eglot--process-filter (proc string) "Called when new data STRING has arrived for PROC." (when (buffer-live-p (process-buffer proc)) (with-current-buffer (process-buffer proc) (let ((inhibit-read-only t) - (expected-bytes (eglot--expected-bytes proc))) + (expected-bytes (process-get proc 'eglot-expected-bytes))) ;; Insert the text, advancing the process marker. ;; (save-excursion @@ -480,7 +499,9 @@ INTERACTIVE is t if called interactively." ;; shielding buffer from tamper ;; (with-temp-buffer - (eglot--process-receive proc json-message)))) + (eglot--server-receive + (process-get proc 'eglot-server) + json-message)))) (goto-char message-end) (delete-region (point-min) (point)) (setq expected-bytes nil)))) @@ -490,31 +511,31 @@ INTERACTIVE is t if called interactively." (setq done :waiting-for-more-bytes-in-this-message)))))))) ;; Saved parsing state for next visit to this filter ;; - (setf (eglot--expected-bytes proc) expected-bytes)))))) + (process-put proc 'eglot-expected-bytes expected-bytes)))))) -(defun eglot-events-buffer (process &optional interactive) - "Display events buffer for current LSP connection PROCESS. +(defun eglot-events-buffer (server &optional interactive) + "Display events buffer for current LSP SERVER. INTERACTIVE is t if called interactively." - (interactive (list (eglot--current-process-or-lose) t)) - (let* ((probe (eglot--events-buffer process)) - (buffer (or (and (buffer-live-p probe) - probe) + (interactive (list (eglot--current-server-or-lose) t)) + (let* ((probe (eglot--events-buffer server)) + (buffer (or (and (buffer-live-p probe) probe) (let ((buffer (get-buffer-create (format "*%s events*" - (process-name process))))) + (eglot--name server))))) (with-current-buffer buffer (buffer-disable-undo) (read-only-mode t) - (setf (eglot--events-buffer process) buffer)) + (setf (eglot--events-buffer server) buffer)) buffer)))) (when interactive (display-buffer buffer)) buffer)) -(defun eglot--log-event (proc message &optional type) +(defun eglot--log-event (server message &optional type) "Log an eglot-related event. -PROC is the current process. MESSAGE is a JSON-like plist. TYPE -is a symbol saying if this is a client or server originated." - (with-current-buffer (eglot-events-buffer proc) +SERVER is the current server. MESSAGE is a JSON-like plist. +TYPE is a symbol saying if this is a client or server +originated." + (with-current-buffer (eglot-events-buffer server) (cl-destructuring-bind (&key method id error &allow-other-keys) message (let* ((inhibit-read-only t) (subtype (cond ((and method id) 'request) @@ -533,47 +554,47 @@ is a symbol saying if this is a client or server originated." (setq msg (propertize msg 'face 'error))) (insert-before-markers msg)))))) -(defun eglot--process-receive (proc message) - "Process MESSAGE from PROC." +(defun eglot--server-receive (server message) + "Process MESSAGE from SERVER." (cl-destructuring-bind (&key method id params error result _jsonrpc) message (let* ((continuations (and id (not method) - (gethash id (eglot--pending-continuations proc))))) - (eglot--log-event proc message 'server) - (when error (setf (eglot--status proc) `(,error t))) + (gethash id (eglot--pending-continuations server))))) + (eglot--log-event server message 'server) + (when error (setf (eglot--status server) `(,error t))) (unless (or (null method) (keywordp method)) (setq method (intern (format ":%s" method)))) (cond ((and method id) (condition-case-unless-debug _err - (apply #'eglot-handle-request proc id method params) + (apply #'eglot-handle-request server id method params) (cl-no-applicable-method - (eglot--reply proc id - :error `(:code -32601 :message "Method unimplemented"))))) + (eglot--reply server id + :error `(:code -32601 :message "Method unimplemented"))))) (method (condition-case-unless-debug _err - (apply #'eglot-handle-notification proc method params) + (apply #'eglot-handle-notification server method params) (cl-no-applicable-method (eglot--log-event - proc '(:error `(:message "Notification unimplemented")))))) + server '(:error `(:message "Notification unimplemented")))))) (continuations (cancel-timer (cl-third continuations)) - (remhash id (eglot--pending-continuations proc)) + (remhash id (eglot--pending-continuations server)) (if error (funcall (cl-second continuations) error) (funcall (cl-first continuations) result))) (id (eglot--warn "Ooops no continuation for id %s" id))) - (eglot--call-deferred proc) + (eglot--call-deferred server) (force-mode-line-update t)))) -(defun eglot--process-send (proc message) - "Send MESSAGE to PROC (ID is optional)." +(defun eglot--send (server message) + "Send MESSAGE to SERVER (ID is optional)." (let ((json (json-encode message))) - (process-send-string proc (format "Content-Length: %d\r\n\r\n%s" - (string-bytes json) - json)) - (eglot--log-event proc message 'client))) + (process-send-string (eglot--process server) + (format "Content-Length: %d\r\n\r\n%s" + (string-bytes json) json)) + (eglot--log-event server message 'client))) (defvar eglot--next-request-id 0 "ID for next request.") @@ -581,21 +602,21 @@ is a symbol saying if this is a client or server originated." "Compute the next id for a client request." (setq eglot--next-request-id (1+ eglot--next-request-id))) -(defun eglot-forget-pending-continuations (process) - "Stop waiting for responses from the current LSP PROCESS." - (interactive (list (eglot--current-process-or-lose))) - (clrhash (eglot--pending-continuations process))) +(defun eglot-forget-pending-continuations (server) + "Stop waiting for responses from the current LSP SERVER." + (interactive (list (eglot--current-server-or-lose))) + (clrhash (eglot--pending-continuations server))) -(defun eglot-clear-status (process) - "Clear most recent error message from PROCESS." - (interactive (list (eglot--current-process-or-lose))) - (setf (eglot--status process) nil) +(defun eglot-clear-status (server) + "Clear most recent error message from SERVER." + (interactive (list (eglot--current-server-or-lose))) + (setf (eglot--status server) nil) (force-mode-line-update t)) -(defun eglot--call-deferred (proc) - "Call PROC's deferred actions, who may again defer themselves." - (when-let ((actions (hash-table-values (eglot--deferred-actions proc)))) - (eglot--log-event proc `(:running-deferred ,(length actions))) +(defun eglot--call-deferred (server) + "Call SERVER's deferred actions, who may again defer themselves." + (when-let ((actions (hash-table-values (eglot--deferred-actions server)))) + (eglot--log-event server `(:running-deferred ,(length actions))) (mapc #'funcall (mapcar #'car actions)))) (defvar eglot--ready-predicates '(eglot--server-ready-p) @@ -604,8 +625,8 @@ If one of these returns nil, a deferrable `eglot--async-request' will be deferred. Each predicate is passed the symbol for the request request and a process object.") -(defun eglot--server-ready-p (_what _proc) - "Tell if server of PROC ready for processing deferred WHAT." +(defun eglot--server-ready-p (_what _server) + "Tell if SERVER is ready for processing deferred WHAT." (not (eglot--outstanding-edits-p))) (cl-defmacro eglot--lambda (cl-lambda-list &body body) @@ -613,14 +634,14 @@ request request and a process object.") (let ((e (gensym "eglot--lambda-elem"))) `(lambda (,e) (apply (cl-function (lambda ,cl-lambda-list ,@body)) ,e)))) -(cl-defun eglot--async-request (proc +(cl-defun eglot--async-request (server method params &rest args &key success-fn error-fn timeout-fn (timeout eglot-request-timeout) (deferred nil)) - "Make a request to PROCESS, expecting a reply later on. + "Make a request to SERVER expecting a reply later on. SUCCESS-FN and ERROR-FN are passed `:result' and `:error' objects, respectively. Wait TIMEOUT seconds for response or call nullary TIMEOUT-FN. If DEFERRED, maybe defer request to the @@ -636,52 +657,55 @@ TIMER)." (run-with-timer timeout nil (lambda () - (remhash id (eglot--pending-continuations proc)) + (remhash id (eglot--pending-continuations server)) (funcall (or timeout-fn (lambda () (eglot--log-event - proc `(:timed-out ,method :id ,id - :params ,params))))))))))) + server `(:timed-out ,method :id ,id + :params ,params))))))))))) (when deferred (let* ((buf (current-buffer)) - (existing (gethash (list deferred buf) (eglot--deferred-actions proc)))) + (existing (gethash (list deferred buf) + (eglot--deferred-actions server)))) (when existing (setq existing (cadr existing))) (if (run-hook-with-args-until-failure 'eglot--ready-predicates - deferred proc) - (remhash (list deferred buf) (eglot--deferred-actions proc)) - (eglot--log-event proc `(:deferring ,method :id ,id :params ,params)) + deferred server) + (remhash (list deferred buf) (eglot--deferred-actions server)) + (eglot--log-event server `(:deferring ,method :id ,id :params ,params)) (let* ((buf (current-buffer)) (point (point)) (later (lambda () (when (buffer-live-p buf) (with-current-buffer buf - (save-excursion (goto-char point) - (apply #'eglot--async-request proc - method params args))))))) - (puthash (list deferred buf) (list later (setq timer (funcall make-timer))) - (eglot--deferred-actions proc)) + (save-excursion + (goto-char point) + (apply #'eglot--async-request server + method params args))))))) + (puthash (list deferred buf) + (list later (setq timer (funcall make-timer))) + (eglot--deferred-actions server)) (cl-return-from eglot--async-request nil))))) ;; Really run it ;; - (eglot--process-send proc (eglot--obj :jsonrpc "2.0" - :id id - :method method - :params params)) + (eglot--send server (eglot--obj :jsonrpc "2.0" + :id id + :method method + :params params)) (puthash id (list (or success-fn (eglot--lambda (&rest _ignored) (eglot--log-event - proc (eglot--obj :message "success ignored" :id id)))) + server (eglot--obj :message "success ignored" :id id)))) (or error-fn (eglot--lambda (&key code message &allow-other-keys) - (setf (eglot--status proc) `(,message t)) - proc (eglot--obj :message "error ignored, status set" - :id id :error code))) + (setf (eglot--status server) `(,message t)) + server (eglot--obj :message "error ignored, status set" + :id id :error code))) (setq timer (funcall make-timer))) - (eglot--pending-continuations proc)) + (eglot--pending-continuations server)) (list id timer))) -(defun eglot--request (proc method params &optional deferred) - "Like `eglot--async-request' for PROC, METHOD and PARAMS, but synchronous. +(defun eglot--request (server method params &optional deferred) + "Like `eglot--async-request' for SERVER, METHOD and PARAMS, but synchronous. Meaning only return locally if successful, otherwise exit non-locally. DEFERRED is passed to `eglot--async-request', which see." ;; Launching a deferred sync request with outstanding changes is a @@ -695,7 +719,7 @@ DEFERRED is passed to `eglot--async-request', which see." (setq id-and-timer (eglot--async-request - proc method params + server method params :success-fn (lambda (result) (throw done `(done ,result))) :timeout-fn (lambda () (throw done '(error "Timed out"))) :error-fn (eglot--lambda (&key code message _data) @@ -704,23 +728,23 @@ DEFERRED is passed to `eglot--async-request', which see." :deferred deferred)) (while t (accept-process-output nil 30))) (pcase-let ((`(,id ,timer) id-and-timer)) - (when id (remhash id (eglot--pending-continuations proc))) + (when id (remhash id (eglot--pending-continuations server))) (when timer (cancel-timer timer)))))) (when (eq 'error (car res)) (eglot--error (cadr res))) (cadr res))) -(cl-defun eglot--notify (process method params) - "Notify PROCESS of something, don't expect a reply.e" - (eglot--process-send process (eglot--obj :jsonrpc "2.0" - :method method - :params params))) +(cl-defun eglot--notify (server method params) + "Notify SERVER of something, don't expect a reply.e" + (eglot--send server (eglot--obj :jsonrpc "2.0" + :method method + :params params))) -(cl-defun eglot--reply (process id &key result error) +(cl-defun eglot--reply (server id &key result error) "Reply to PROCESS's request ID with MESSAGE." - (eglot--process-send - process `(:jsonrpc "2.0" :id ,id - ,@(when result `(:result ,result)) - ,@(when error `(:error ,error))))) + (eglot--send + server`(:jsonrpc "2.0" :id ,id + ,@(when result `(:result ,result)) + ,@(when error `(:error ,error))))) ;;; Helpers @@ -788,7 +812,7 @@ If optional MARKER, return a marker instead" (defun eglot--server-capable (&rest feats) "Determine if current server is capable of FEATS." -(cl-loop for caps = (eglot--capabilities (eglot--current-process-or-lose)) +(cl-loop for caps = (eglot--capabilities (eglot--current-server-or-lose)) then (cadr probe) for feat in feats for probe = (plist-member caps feat) @@ -839,14 +863,14 @@ If optional MARKERS, make markers." #'eglot-eldoc-function) (remove-function (local imenu-create-index-function) #'eglot-imenu)))) -(defun eglot--managed-mode-onoff (proc arg) - "Proxy for function `eglot--managed-mode' with ARG and PROC." +(defun eglot--managed-mode-onoff (server arg) + "Proxy for function `eglot--managed-mode' with ARG and SERVER." (eglot--managed-mode arg) (let ((buf (current-buffer))) (if eglot--managed-mode - (cl-pushnew buf (eglot--managed-buffers proc)) - (setf (eglot--managed-buffers proc) - (delq buf (eglot--managed-buffers proc)))))) + (cl-pushnew buf (eglot--managed-buffers server)) + (setf (eglot--managed-buffers server) + (delq buf (eglot--managed-buffers server)))))) (add-hook 'eglot--managed-mode-hook 'flymake-mode) (add-hook 'eglot--managed-mode-hook 'eldoc-mode) @@ -854,15 +878,15 @@ If optional MARKERS, make markers." (defvar-local eglot--current-flymake-report-fn nil "Current flymake report function for this buffer") -(defun eglot--maybe-activate-editing-mode (&optional proc) +(defun eglot--maybe-activate-editing-mode (&optional server) "Maybe activate mode function `eglot--managed-mode'. -If PROC is supplied, do it only if BUFFER is managed by it. In +If SERVER is supplied, do it only if BUFFER is managed by it. In that case, also signal textDocument/didOpen." ;; Called even when revert-buffer-in-progress-p - (let* ((cur (and buffer-file-name (eglot--current-process))) - (proc (or (and (null proc) cur) (and proc (eq proc cur) cur)))) - (when proc - (eglot--managed-mode-onoff proc 1) + (let* ((cur (and buffer-file-name (eglot--current-server))) + (server (or (and (null server) cur) (and server (eq server cur) cur)))) + (when server + (eglot--managed-mode-onoff server 1) (eglot--signal-textDocument/didOpen) (flymake-start) (funcall (or eglot--current-flymake-report-fn #'ignore) nil)))) @@ -899,12 +923,14 @@ Uses THING, FACE, DEFS and PREPEND." (defun eglot--mode-line-format () "Compose the EGLOT's mode-line." - (pcase-let* ((proc (eglot--current-process)) - (name (and (process-live-p proc) (eglot--short-name proc))) - (pending (and proc (hash-table-count - (eglot--pending-continuations proc)))) - (`(,_id ,doing ,done-p ,detail) (and proc (eglot--spinner proc))) - (`(,status ,serious-p) (and proc (eglot--status proc)))) + (pcase-let* ((server (eglot--current-server)) + (name (and + server + (eglot--project-nickname server))) + (pending (and server (hash-table-count + (eglot--pending-continuations server)))) + (`(,_id ,doing ,done-p ,detail) (and server (eglot--spinner server))) + (`(,status ,serious-p) (and server (eglot--status server)))) (append `(,(eglot--mode-line-props "eglot" 'eglot-mode-line nil)) (when name @@ -938,34 +964,34 @@ Uses THING, FACE, DEFS and PREPEND." ;;; Protocol implementation (Requests, notifications, etc) ;;; -(defun eglot-shutdown (proc &optional _interactive) - "Politely ask the server PROC to quit. +(defun eglot-shutdown (server &optional _interactive) + "Politely ask SERVER to quit. Forcefully quit it if it doesn't respond. Don't leave this function with the server still running." - (interactive (list (eglot--current-process-or-lose) t)) - (eglot--message "Asking %s politely to terminate" proc) + (interactive (list (eglot--current-server-or-lose) t)) + (eglot--message "Asking %s politely to terminate" (eglot--name server)) (unwind-protect (let ((eglot-request-timeout 3)) - (setf (eglot--moribund proc) t) - (eglot--request proc :shutdown nil) + (setf (eglot--moribund server) t) + (eglot--request server :shutdown nil) ;; this one is supposed to always fail, hence ignore-errors - (ignore-errors (eglot--request proc :exit nil))) + (ignore-errors (eglot--request server :exit nil))) ;; Turn off `eglot--managed-mode' where appropriate. - (dolist (buffer (eglot--managed-buffers proc)) - (with-current-buffer buffer (eglot--managed-mode-onoff proc -1))) - (when (process-live-p proc) - (eglot--warn "Brutally deleting non-compliant existing process %s" proc) - (delete-process proc)))) + (dolist (buffer (eglot--managed-buffers server)) + (with-current-buffer buffer (eglot--managed-mode-onoff server -1))) + (when (process-live-p (eglot--process server)) + (eglot--warn "Brutally deleting non-compliant server %s" (eglot--name server)) + (delete-process (eglot--process server))))) (cl-defmethod eglot-handle-notification - (_process (_method (eql :window/showMessage)) &key type message) + (_server (_method (eql :window/showMessage)) &key type message) "Handle notification window/showMessage" (eglot--message (propertize "Server reports (type=%s): %s" 'face (if (<= type 1) 'error)) type message)) (cl-defmethod eglot-handle-request - (process id (_method (eql :window/showMessageRequest)) &key type message actions) + (server id (_method (eql :window/showMessageRequest)) &key type message actions) "Handle server request window/showMessageRequest" (let (reply) (unwind-protect @@ -980,24 +1006,24 @@ function with the server still running." '("OK")) nil t (plist-get (elt actions 0) :title))) (if reply - (eglot--reply process id :result (eglot--obj :title reply)) - (eglot--reply process id + (eglot--reply server id :result (eglot--obj :title reply)) + (eglot--reply server id :error (eglot--obj :code -32800 :message "User cancelled")))))) (cl-defmethod eglot-handle-notification - (_proc (_method (eql :window/logMessage)) &key _type _message) + (_server (_method (eql :window/logMessage)) &key _type _message) "Handle notification window/logMessage") ;; noop, use events buffer (cl-defmethod eglot-handle-notification - (_proc (_method (eql :telemetry/event)) &rest _any) + (_server (_method (eql :telemetry/event)) &rest _any) "Handle notification telemetry/event") ;; noop, use events buffer (defvar-local eglot--unreported-diagnostics nil "Unreported diagnostics for this buffer.") (cl-defmethod eglot-handle-notification - (_proc (_method (eql :textDocument/publishDiagnostics)) &key uri diagnostics) + (_server (_method (eql :textDocument/publishDiagnostics)) &key uri diagnostics) "Handle notification publishDiagnostics" (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) (with-current-buffer buffer @@ -1021,7 +1047,7 @@ function with the server still running." (setq eglot--unreported-diagnostics diags))))) (eglot--warn "Diagnostics received for unvisited %s" uri))) -(cl-defun eglot--register-unregister (proc jsonrpc-id things how) +(cl-defun eglot--register-unregister (server jsonrpc-id things how) "Helper for `registerCapability'. THINGS are either registrations or unregisterations." (dolist (thing (cl-coerce things 'list)) @@ -1029,32 +1055,32 @@ THINGS are either registrations or unregisterations." (let (retval) (unwind-protect (setq retval (apply (intern (format "eglot--%s-%s" how method)) - proc :id id registerOptions)) + server :id id registerOptions)) (unless (eq t (car retval)) (cl-return-from eglot--register-unregister (eglot--reply - proc jsonrpc-id + server jsonrpc-id :error `(:code -32601 :message ,(or (cadr retval) "sorry"))))))))) - (eglot--reply proc jsonrpc-id :result (eglot--obj :message "OK"))) + (eglot--reply server jsonrpc-id :result (eglot--obj :message "OK"))) (cl-defmethod eglot-handle-request - (proc id (_method (eql :client/registerCapability)) &key registrations) + (server id (_method (eql :client/registerCapability)) &key registrations) "Handle server request client/registerCapability" - (eglot--register-unregister proc id registrations 'register)) + (eglot--register-unregister server id registrations 'register)) (cl-defmethod eglot-handle-request - (proc id (_method (eql :client/unregisterCapability)) + (server id (_method (eql :client/unregisterCapability)) &key unregisterations) ;; XXX: "unregisterations" (sic) "Handle server request client/unregisterCapability" - (eglot--register-unregister proc id unregisterations 'unregister)) + (eglot--register-unregister server id unregisterations 'unregister)) (cl-defmethod eglot-handle-request - (proc id (_method (eql :workspace/applyEdit)) &key _label edit) + (server id (_method (eql :workspace/applyEdit)) &key _label edit) "Handle server request workspace/applyEdit" (condition-case err (progn (eglot--apply-workspace-edit edit 'confirm) - (eglot--reply proc id :result `(:applied ))) - (error (eglot--reply proc id + (eglot--reply server id :result `(:applied ))) + (error (eglot--reply server id :result `(:applied :json-false) :error (eglot--obj :code -32001 :message (format "%s" err)))))) @@ -1119,7 +1145,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." (when (eglot--outstanding-edits-p) - (let* ((proc (eglot--current-process-or-lose)) + (let* ((server (eglot--current-server-or-lose)) (sync-kind (eglot--server-capable :textDocumentSync)) (emacs-messup (/= (length (car eglot--recent-changes)) (length (cdr eglot--recent-changes)))) @@ -1129,7 +1155,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." (save-restriction (widen) (eglot--notify - proc :textDocument/didChange + server :textDocument/didChange (eglot--obj :textDocument (eglot--VersionedTextDocumentIdentifier) @@ -1145,38 +1171,38 @@ Records START, END and PRE-CHANGE-LENGTH locally." :rangeLength len :text after-text)]))))) (setq eglot--recent-changes (cons [] [])) - (setf (eglot--spinner proc) (list nil :textDocument/didChange t)) - (eglot--call-deferred proc)))) + (setf (eglot--spinner server) (list nil :textDocument/didChange t)) + (eglot--call-deferred server)))) (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." (setq eglot--recent-changes (cons [] [])) (eglot--notify - (eglot--current-process-or-lose) + (eglot--current-server-or-lose) :textDocument/didOpen `(:textDocument ,(eglot--TextDocumentItem)))) (defun eglot--signal-textDocument/didClose () "Send textDocument/didClose to server." (eglot--notify - (eglot--current-process-or-lose) + (eglot--current-server-or-lose) :textDocument/didClose `(:textDocument ,(eglot--TextDocumentIdentifier)))) (defun eglot--signal-textDocument/willSave () "Send textDocument/willSave to server." - (let ((proc (eglot--current-process-or-lose)) + (let ((server (eglot--current-server-or-lose)) (params `(:reason 1 :textDocument ,(eglot--TextDocumentIdentifier)))) - (eglot--notify proc :textDocument/willSave params) + (eglot--notify server :textDocument/willSave params) (ignore-errors (let ((eglot-request-timeout 0.5)) (when (plist-get :willSaveWaitUntil (eglot--server-capable :textDocumentSync)) (eglot--apply-text-edits - (eglot--request proc :textDocument/willSaveWaituntil params))))))) + (eglot--request server :textDocument/willSaveWaituntil params))))))) (defun eglot--signal-textDocument/didSave () "Send textDocument/didSave to server." (eglot--notify - (eglot--current-process-or-lose) + (eglot--current-server-or-lose) :textDocument/didSave (eglot--obj ;; TODO: Handle TextDocumentSaveRegistrationOptions to control this. @@ -1218,7 +1244,7 @@ DUMMY is ignored" (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) (when (eglot--server-capable :documentSymbolProvider) - (let ((proc (eglot--current-process-or-lose)) + (let ((server (eglot--current-server-or-lose)) (text-id (eglot--TextDocumentIdentifier))) (completion-table-with-cache (lambda (string) @@ -1234,7 +1260,7 @@ DUMMY is ignored" :locations (list location) :kind kind :containerName containerName)) - (eglot--request proc + (eglot--request server :textDocument/documentSymbol (eglot--obj :textDocument text-id)))) @@ -1252,7 +1278,7 @@ DUMMY is ignored" (location-or-locations (if rich-identifier (get-text-property 0 :locations rich-identifier) - (eglot--request (eglot--current-process-or-lose) + (eglot--request (eglot--current-server-or-lose) :textDocument/definition (get-text-property 0 :textDocumentPositionParams identifier))))) @@ -1271,7 +1297,7 @@ DUMMY is ignored" (eglot--error "Don' know where %s is in the workspace!" identifier)) (mapcar (eglot--lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) - (eglot--request (eglot--current-process-or-lose) + (eglot--request (eglot--current-server-or-lose) :textDocument/references (append params @@ -1283,21 +1309,21 @@ DUMMY is ignored" (mapcar (eglot--lambda (&key name location &allow-other-keys) (cl-destructuring-bind (&key uri range) location (eglot--xref-make name uri (plist-get range :start)))) - (eglot--request (eglot--current-process-or-lose) + (eglot--request (eglot--current-server-or-lose) :workspace/symbol (eglot--obj :query pattern))))) (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." (let ((bounds (bounds-of-thing-at-point 'symbol)) - (proc (eglot--current-process-or-lose))) + (server (eglot--current-server-or-lose))) (when (eglot--server-capable :completionProvider) (list (or (car bounds) (point)) (or (cdr bounds) (point)) (completion-table-with-cache (lambda (_ignored) - (let* ((resp (eglot--request proc + (let* ((resp (eglot--request server :textDocument/completion (eglot--TextDocumentPositionParams) :textDocument/completion)) @@ -1328,7 +1354,7 @@ DUMMY is ignored" (or (get-text-property 0 :documentation obj) (and (eglot--server-capable :completionProvider :resolveProvider) - (plist-get (eglot--request proc :completionItem/resolve + (plist-get (eglot--request server :completionItem/resolve (text-properties-at 0 obj)) :documentation))))) (when documentation @@ -1374,7 +1400,7 @@ DUMMY is ignored" "Request \"hover\" information for the thing at point." (interactive) (cl-destructuring-bind (&key contents range) - (eglot--request (eglot--current-process-or-lose) :textDocument/hover + (eglot--request (eglot--current-server-or-lose) :textDocument/hover (eglot--TextDocumentPositionParams)) (when (seq-empty-p contents) (eglot--error "No hover info here")) (with-help-window "*eglot help*" @@ -1385,7 +1411,7 @@ DUMMY is ignored" "EGLOT's `eldoc-documentation-function' function. If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (let* ((buffer (current-buffer)) - (proc (eglot--current-process-or-lose)) + (server (eglot--current-server-or-lose)) (position-params (eglot--TextDocumentPositionParams)) sig-showing) (cl-macrolet ((when-buffer-window @@ -1393,7 +1419,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (with-current-buffer buffer ,@body)))) (when (eglot--server-capable :signatureHelpProvider) (eglot--async-request - proc :textDocument/signatureHelp position-params + server :textDocument/signatureHelp position-params :success-fn (eglot--lambda (&key signatures activeSignature activeParameter) (when-buffer-window @@ -1405,7 +1431,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." :deferred :textDocument/signatureHelp)) (when (eglot--server-capable :hoverProvider) (eglot--async-request - proc :textDocument/hover position-params + server :textDocument/hover position-params :success-fn (eglot--lambda (&key contents range) (unless sig-showing (setq eldoc-last-message (eglot--hover-info contents range)) @@ -1413,7 +1439,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) (eglot--async-request - proc :textDocument/documentHighlight position-params + server :textDocument/documentHighlight position-params :success-fn (lambda (highlights) (mapc #'delete-overlay eglot--highlights) (setq eglot--highlights @@ -1438,7 +1464,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) (eglot--lsp-position-to-point (plist-get (plist-get location :range) :start)))) - (eglot--request (eglot--current-process-or-lose) + (eglot--request (eglot--current-server-or-lose) :textDocument/documentSymbol (eglot--obj :textDocument (eglot--TextDocumentIdentifier)))))) @@ -1506,7 +1532,7 @@ Proceed? " (unless (eglot--server-capable :renameProvider) (eglot--error "Server can't rename!")) (eglot--apply-workspace-edit - (eglot--request (eglot--current-process-or-lose) + (eglot--request (eglot--current-server-or-lose) :textDocument/rename `(,@(eglot--TextDocumentPositionParams) ,@(eglot--obj :newName newname))) current-prefix-arg)) @@ -1514,9 +1540,9 @@ Proceed? " ;;; Dynamic registration ;;; -(cl-defun eglot--register-workspace/didChangeWatchedFiles (proc &key id watchers) +(cl-defun eglot--register-workspace/didChangeWatchedFiles (server &key id watchers) "Handle dynamic registration of workspace/didChangeWatchedFiles" - (eglot--unregister-workspace/didChangeWatchedFiles proc :id id) + (eglot--unregister-workspace/didChangeWatchedFiles server :id id) (let* (success (globs (mapcar (lambda (w) (plist-get w :globPattern)) watchers))) (cl-labels @@ -1531,7 +1557,7 @@ Proceed? " (expand-file-name glob)) f)))) (eglot--notify - proc :workspace/didChangeWatchedFiles + server :workspace/didChangeWatchedFiles `(:changes ,(vector `(:uri ,(eglot--path-to-uri file) :type ,(cl-case action (created 1) @@ -1543,25 +1569,25 @@ Proceed? " (unwind-protect (progn (dolist (dir (delete-dups (mapcar #'file-name-directory globs))) (push (file-notify-add-watch dir '(change) #'handle-event) - (gethash id (eglot--file-watches proc)))) + (gethash id (eglot--file-watches server)))) (setq success `(t "OK"))) (unless success - (eglot--unregister-workspace/didChangeWatchedFiles proc :id id)))))) + (eglot--unregister-workspace/didChangeWatchedFiles server :id id)))))) -(cl-defun eglot--unregister-workspace/didChangeWatchedFiles (proc &key id) +(cl-defun eglot--unregister-workspace/didChangeWatchedFiles (server &key id) "Handle dynamic unregistration of workspace/didChangeWatchedFiles" - (mapc #'file-notify-rm-watch (gethash id (eglot--file-watches proc))) - (remhash id (eglot--file-watches proc)) + (mapc #'file-notify-rm-watch (gethash id (eglot--file-watches server))) + (remhash id (eglot--file-watches server)) (list t "OK")) ;;; Rust-specific ;;; -(defun eglot--rls-probably-ready-for-p (what proc) - "Guess if the RLS running in PROC is ready for WHAT." +(defun eglot--rls-probably-ready-for-p (what server) + "Guess if the RLS running in SERVER is ready for WHAT." (or (eq what :textDocument/completion) ; RLS normally ready for this ; one, even if building ; - (pcase-let ((`(,_id ,what ,done ,_detail) (eglot--spinner proc))) + (pcase-let ((`(,_id ,what ,done ,_detail) (eglot--spinner server))) (and (equal "Indexing" what) done)))) ;;;###autoload @@ -1572,11 +1598,11 @@ Proceed? " (add-hook 'eglot--ready-predicates 'eglot--rls-probably-ready-for-p t t))) (cl-defmethod eglot-handle-notification - (proc (_method (eql :window/progress)) &key id done title message &allow-other-keys) + (server (_method (eql :window/progress)) &key id done title message &allow-other-keys) "Handle notification window/progress" - (setf (eglot--spinner proc) (list id title done message)) + (setf (eglot--spinner server) (list id title done message)) (when (and (equal "Indexing" title) done) - (dolist (buffer (eglot--managed-buffers proc)) + (dolist (buffer (eglot--managed-buffers server)) (with-current-buffer buffer (funcall (or eglot--current-flymake-report-fn #'ignore) eglot--unreported-diagnostics))))) commit 7938af0c4a4b084b9eb29505ddeea78ae39d208d Author: João Távora Date: Mon May 21 09:00:49 2018 +0100 Introduce eglot-handle-request and eglot-handle-notification as api * eglot.el (eglot--process-receive): Call eglot-handle-request and eglot-handle-notification. (eglot-handle-notification, eglot-handle-request): New generic functions. (eglot--server-window/showMessage) (eglot--server-window/progress) (eglot--server-telemetry/event, eglot--server-window/logMessage): Convert to eglot-handle-notification. (eglot-handle-request, eglot--server-client/registerCapability) (eglot--server-client/unregisterCapability) (eglot-handle-request): Convert to eglot-handle-request. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9d7253af26..65beb35b2a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -541,17 +541,21 @@ is a symbol saying if this is a client or server originated." (gethash id (eglot--pending-continuations proc))))) (eglot--log-event proc message 'server) (when error (setf (eglot--status proc) `(,error t))) - (cond (method - ;; a server notification or a server request - (let* ((handler-sym (intern (concat "eglot--server-" method)))) - (if (functionp handler-sym) - ;; FIXME: will fail if params is array instead of not an object - (apply handler-sym proc (append params (if id `(:id ,id)))) - (eglot--warn "No implementation of method %s yet" method) - (when id - (eglot--reply - proc id - :error `(:code -32601 :message "Method unimplemented")))))) + (unless (or (null method) + (keywordp method)) + (setq method (intern (format ":%s" method)))) + (cond ((and method id) + (condition-case-unless-debug _err + (apply #'eglot-handle-request proc id method params) + (cl-no-applicable-method + (eglot--reply proc id + :error `(:code -32601 :message "Method unimplemented"))))) + (method + (condition-case-unless-debug _err + (apply #'eglot-handle-notification proc method params) + (cl-no-applicable-method + (eglot--log-event + proc '(:error `(:message "Notification unimplemented")))))) (continuations (cancel-timer (cl-third continuations)) (remhash id (eglot--pending-continuations proc)) @@ -953,14 +957,15 @@ function with the server still running." (eglot--warn "Brutally deleting non-compliant existing process %s" proc) (delete-process proc)))) -(cl-defun eglot--server-window/showMessage (_process &key type message) +(cl-defmethod eglot-handle-notification + (_process (_method (eql :window/showMessage)) &key type message) "Handle notification window/showMessage" (eglot--message (propertize "Server reports (type=%s): %s" 'face (if (<= type 1) 'error)) type message)) -(cl-defun eglot--server-window/showMessageRequest - (process &key id type message actions) +(cl-defmethod eglot-handle-request + (process id (_method (eql :window/showMessageRequest)) &key type message actions) "Handle server request window/showMessageRequest" (let (reply) (unwind-protect @@ -980,17 +985,19 @@ function with the server still running." :error (eglot--obj :code -32800 :message "User cancelled")))))) -(cl-defun eglot--server-window/logMessage (_proc &key _type _message) +(cl-defmethod eglot-handle-notification + (_proc (_method (eql :window/logMessage)) &key _type _message) "Handle notification window/logMessage") ;; noop, use events buffer -(cl-defun eglot--server-telemetry/event (_proc &rest _any) +(cl-defmethod eglot-handle-notification + (_proc (_method (eql :telemetry/event)) &rest _any) "Handle notification telemetry/event") ;; noop, use events buffer (defvar-local eglot--unreported-diagnostics nil "Unreported diagnostics for this buffer.") -(cl-defun eglot--server-textDocument/publishDiagnostics - (_proc &key uri diagnostics) +(cl-defmethod eglot-handle-notification + (_proc (_method (eql :textDocument/publishDiagnostics)) &key uri diagnostics) "Handle notification publishDiagnostics" (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) (with-current-buffer buffer @@ -1015,7 +1022,7 @@ function with the server still running." (eglot--warn "Diagnostics received for unvisited %s" uri))) (cl-defun eglot--register-unregister (proc jsonrpc-id things how) - "Helper for `eglot--server-client/registerCapability'. + "Helper for `registerCapability'. THINGS are either registrations or unregisterations." (dolist (thing (cl-coerce things 'list)) (cl-destructuring-bind (&key id method registerOptions) thing @@ -1030,18 +1037,19 @@ THINGS are either registrations or unregisterations." :error `(:code -32601 :message ,(or (cadr retval) "sorry"))))))))) (eglot--reply proc jsonrpc-id :result (eglot--obj :message "OK"))) -(cl-defun eglot--server-client/registerCapability - (proc &key id registrations) +(cl-defmethod eglot-handle-request + (proc id (_method (eql :client/registerCapability)) &key registrations) "Handle server request client/registerCapability" (eglot--register-unregister proc id registrations 'register)) -(cl-defun eglot--server-client/unregisterCapability - (proc &key id unregisterations) ;; XXX: Yeah, typo and all.. See spec... +(cl-defmethod eglot-handle-request + (proc id (_method (eql :client/unregisterCapability)) + &key unregisterations) ;; XXX: "unregisterations" (sic) "Handle server request client/unregisterCapability" (eglot--register-unregister proc id unregisterations 'unregister)) -(cl-defun eglot--server-workspace/applyEdit - (proc &key id _label edit) +(cl-defmethod eglot-handle-request + (proc id (_method (eql :workspace/applyEdit)) &key _label edit) "Handle server request workspace/applyEdit" (condition-case err (progn (eglot--apply-workspace-edit edit 'confirm) @@ -1563,12 +1571,12 @@ Proceed? " "Prepare `eglot' to deal with RLS's special treatment." (add-hook 'eglot--ready-predicates 'eglot--rls-probably-ready-for-p t t))) -(cl-defun eglot--server-window/progress - (process &key id done title message &allow-other-keys) +(cl-defmethod eglot-handle-notification + (proc (_method (eql :window/progress)) &key id done title message &allow-other-keys) "Handle notification window/progress" - (setf (eglot--spinner process) (list id title done message)) + (setf (eglot--spinner proc) (list id title done message)) (when (and (equal "Indexing" title) done) - (dolist (buffer (eglot--managed-buffers process)) + (dolist (buffer (eglot--managed-buffers proc)) (with-current-buffer buffer (funcall (or eglot--current-flymake-report-fn #'ignore) eglot--unreported-diagnostics))))) commit 3a127eed7b76ec02f4f408cbb03ab46ed2ac48b8 Author: João Távora Date: Sun May 20 15:24:55 2018 +0100 Get rid of jsonrpc.el customization group and timeout * eglot.el (eglot-shutdown, eglot--signal-textDocument/willSave): Pass :timeout to jsonrpc-request. (defadvice jsonrpc-request): Add :timeout kwarg * jsonrpc.el (defgroup jsonrpc, jsonrpc-request-timeout): Remove. (jrpc-default-request-timeout): New constant. (jsonrpc-async-request): Use it. (jsonrpc-request): Accept timeout kwarg and pass it on. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 41e57b4bb5..020e352c44 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -148,9 +148,9 @@ called interactively." (interactive (list (jsonrpc-current-process-or-lose) t)) (eglot--message "Asking %s politely to terminate" proc) (unwind-protect - (let ((jsonrpc-request-timeout 3)) + (progn (setf (eglot--moribund proc) t) - (jsonrpc-request proc :shutdown nil) + (jsonrpc-request proc :shutdown nil :timeout 3) ;; this one should always fail, hence ignore-errors (ignore-errors (jsonrpc-request proc :exit nil))) ;; Turn off `eglot--managed-mode' where appropriate. @@ -749,7 +749,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." ;; bad idea, since that might lead to the request never having a ;; chance to run, because `jsonrpc-ready-predicates'. (advice-add #'jsonrpc-request :before - (cl-function (lambda (_proc _method _params &key deferred) + (cl-function (lambda (_proc _method _params &key deferred _timeout) (when (and eglot--managed-mode deferred) (eglot--signal-textDocument/didChange)))) '((name . eglot--signal-textDocument/didChange))) @@ -805,12 +805,11 @@ Records START, END and PRE-CHANGE-LENGTH locally." (let ((proc (jsonrpc-current-process-or-lose)) (params `(:reason 1 :textDocument ,(eglot--TextDocumentIdentifier)))) (jsonrpc-notify proc :textDocument/willSave params) - (ignore-errors - (let ((jsonrpc-request-timeout 0.5)) - (when (plist-get :willSaveWaitUntil - (eglot--server-capable :textDocumentSync)) - (eglot--apply-text-edits - (jsonrpc-request proc :textDocument/willSaveWaituntil params))))))) + (when (eglot--server-capable :textDocumentSync :willSaveWaitUntil) + (ignore-errors + (eglot--apply-text-edits + (jsonrpc-request proc :textDocument/willSaveWaituntil params + :timeout 0.5)))))) (defun eglot--signal-textDocument/didSave () "Send textDocument/didSave to server." commit 351eb7f4ce86dfca00074aad3db961e28bd6fda5 Merge: 1b62dfd97b 28b199c344 Author: João Távora Date: Sun May 20 15:07:23 2018 +0100 Heroically merge master into jsonrpc-refactor (using imerge) commit 1b62dfd97b0d7fbbf18c7c39ede9639daab66024 Author: João Távora Date: Sun May 20 13:21:12 2018 +0100 Rename jrpc.el to jsonrpc.el * eglot.el [everywhere]: jrpc -> jsonrpc everywhere. Reindent. * eglot-tests [everywhere]: jrpc -> jsonrpc everywhere. * jsonrpc.el: New file. * Makefile: jrpc.el -> jsonrpc.el diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 81229a5817..907c98b2c0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -59,7 +59,7 @@ (require 'flymake) (require 'xref) (require 'subr-x) -(require 'jrpc) +(require 'jsonrpc) (require 'filenotify) @@ -98,26 +98,26 @@ lasted more than that many seconds." (defvar eglot--processes-by-project (make-hash-table :test #'equal) "Keys are projects. Values are lists of processes.") -(jrpc-define-process-var eglot--major-mode nil +(jsonrpc-define-process-var eglot--major-mode nil "The major-mode this server is managing.") -(jrpc-define-process-var eglot--capabilities :unreported +(jsonrpc-define-process-var eglot--capabilities :unreported "Holds list of capabilities that server reported") -(jrpc-define-process-var eglot--project nil +(jsonrpc-define-process-var eglot--project nil "The project the server belongs to.") -(jrpc-define-process-var eglot--spinner `(nil nil t) +(jsonrpc-define-process-var eglot--spinner `(nil nil t) "\"Spinner\" used by some servers. A list (ID WHAT DONE-P).") -(jrpc-define-process-var eglot--moribund nil +(jsonrpc-define-process-var eglot--moribund nil "Non-nil if server is about to exit") -(jrpc-define-process-var eglot--inhibit-autoreconnect eglot-autoreconnect +(jsonrpc-define-process-var eglot--inhibit-autoreconnect eglot-autoreconnect "If non-nil, don't autoreconnect on unexpected quit.") -(jrpc-define-process-var eglot--file-watches (make-hash-table :test #'equal) +(jsonrpc-define-process-var eglot--file-watches (make-hash-table :test #'equal) "File system watches for the didChangeWatchedfiles thingy.") (defun eglot--on-shutdown (proc) @@ -146,14 +146,14 @@ A list (ID WHAT DONE-P).") Forcefully quit it if it doesn't respond. Don't leave this function with the server still running. INTERACTIVE is t if called interactively." - (interactive (list (jrpc-current-process-or-lose) t)) + (interactive (list (jsonrpc-current-process-or-lose) t)) (when interactive (eglot--message "Asking %s politely to terminate" proc)) (unwind-protect - (let ((jrpc-request-timeout 3)) + (let ((jsonrpc-request-timeout 3)) (setf (eglot--moribund proc) t) - (jrpc-request proc :shutdown nil) + (jsonrpc-request proc :shutdown nil) ;; this one should always fail under normal conditions - (ignore-errors (jrpc-request proc :exit nil))) + (ignore-errors (jsonrpc-request proc :exit nil))) (when (process-live-p proc) (eglot--warn "Brutally deleting existing process %s" proc) (delete-process proc)))) @@ -178,14 +178,14 @@ called interactively." (defun eglot--client-capabilities () "What the EGLOT LSP client supports." - (jrpc-obj - :workspace (jrpc-obj + (jsonrpc-obj + :workspace (jsonrpc-obj :applyEdit t :workspaceEdit `(:documentChanges :json-false) :didChangeWatchesFiles `(:dynamicRegistration t) :symbol `(:dynamicRegistration :json-false)) - :textDocument (jrpc-obj - :synchronization (jrpc-obj + :textDocument (jsonrpc-obj + :synchronization (jsonrpc-obj :dynamicRegistration :json-false :willSave t :willSaveWaitUntil t :didSave t) :completion `(:dynamicRegistration :json-false) @@ -197,7 +197,7 @@ called interactively." :documentHighlight `(:dynamicRegistration :json-false) :rename `(:dynamicRegistration :json-false) :publishDiagnostics `(:relatedInformation :json-false)) - :experimental (jrpc-obj))) + :experimental (jsonrpc-obj))) (defvar eglot--command-history nil "History of CONTACT arguments to `eglot'.") @@ -282,7 +282,7 @@ MANAGED-MAJOR-MODE is an Emacs major mode. INTERACTIVE is t if called interactively." (interactive (eglot--interactive)) (let* ((short-name (eglot--project-short-name project))) - (let ((current-process (jrpc-current-process))) + (let ((current-process (jsonrpc-current-process))) (if (and (process-live-p current-process) interactive (y-or-n-p "[eglot] Live process found, reconnect instead? ")) @@ -301,32 +301,33 @@ managing `%s' buffers in project `%s'." (defun eglot-reconnect (process &optional interactive) "Reconnect to PROCESS. INTERACTIVE is t if called interactively." - (interactive (list (jrpc-current-process-or-lose) t)) + (interactive (list (jsonrpc-current-process-or-lose) t)) (when (process-live-p process) (eglot-shutdown process interactive)) (eglot--connect (eglot--project process) (eglot--major-mode process) - (jrpc-name process) - (jrpc-contact process)) + (jsonrpc-name process) + (jsonrpc-contact process)) (eglot--message "Reconnected!")) -(defalias 'eglot-events-buffer 'jrpc-events-buffer) +(defalias 'eglot-events-buffer 'jsonrpc-events-buffer) (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") (defun eglot--dispatch (proc method id params) - "Dispatcher passed to `jrpc-connect'. + "Dispatcher passed to `jsonrpc-connect'. Builds a function from METHOD, passes it PROC, ID and PARAMS." (let* ((handler-sym (intern (format "eglot--server-%s" method)))) (if (functionp handler-sym) ;; FIXME: fails if params is array, not object (apply handler-sym proc (append params (if id `(:id ,id)))) - (jrpc-reply proc id - :error (jrpc-obj :code -32601 :message "Unimplemented"))) + (jsonrpc-reply proc id + :error (jsonrpc-obj :code -32601 :message "Unimplemented"))) (force-mode-line-update t))) (defun eglot--connect (project managed-major-mode name contact) (let* ((contact (if (functionp contact) (funcall contact) contact)) - (proc (jrpc-connect name contact #'eglot--dispatch #'eglot--on-shutdown)) + (proc + (jsonrpc-connect name contact #'eglot--dispatch #'eglot--on-shutdown)) success) (setf (eglot--project proc) project) (setf (eglot--major-mode proc)managed-major-mode) @@ -334,23 +335,23 @@ Builds a function from METHOD, passes it PROC, ID and PARAMS." (run-hook-with-args 'eglot-connect-hook proc) (unwind-protect (cl-destructuring-bind (&key capabilities) - (jrpc-request + (jsonrpc-request proc :initialize - (jrpc-obj :processId (unless (eq (process-type proc) - 'network) - (emacs-pid)) - :rootPath (car (project-roots project)) - :rootUri (eglot--path-to-uri - (car (project-roots project))) - :initializationOptions [] - :capabilities (eglot--client-capabilities))) + (jsonrpc-obj :processId (unless (eq (process-type proc) + 'network) + (emacs-pid)) + :rootPath (car (project-roots project)) + :rootUri (eglot--path-to-uri + (car (project-roots project))) + :initializationOptions [] + :capabilities (eglot--client-capabilities))) (setf (eglot--capabilities proc) capabilities) - (setf (jrpc-status proc) nil) + (setf (jsonrpc-status proc) nil) (dolist (buffer (buffer-list)) (with-current-buffer buffer (eglot--maybe-activate-editing-mode proc))) - (jrpc-notify proc :initialized (jrpc-obj :__dummy__ t)) + (jsonrpc-notify proc :initialized (jsonrpc-obj :__dummy__ t)) (setf (eglot--inhibit-autoreconnect proc) (cond ((booleanp eglot-autoreconnect) (not eglot-autoreconnect)) @@ -389,12 +390,12 @@ Builds a function from METHOD, passes it PROC, ID and PARAMS." (defun eglot--pos-to-lsp-position (&optional pos) "Convert point POS to LSP position." (save-excursion - (jrpc-obj :line - ;; F!@(#*&#$)CKING OFF-BY-ONE - (1- (line-number-at-pos pos t)) - :character - (- (goto-char (or pos (point))) - (line-beginning-position))))) + (jsonrpc-obj :line + ;; F!@(#*&#$)CKING OFF-BY-ONE + (1- (line-number-at-pos pos t)) + :character + (- (goto-char (or pos (point))) + (line-beginning-position))))) (defun eglot--lsp-position-to-point (pos-plist) "Convert LSP position POS-PLIST to Emacs point." @@ -444,7 +445,7 @@ Builds a function from METHOD, passes it PROC, ID and PARAMS." (defun eglot--server-capable (feat) "Determine if current server is capable of FEAT." - (plist-get (eglot--capabilities (jrpc-current-process-or-lose)) feat)) + (plist-get (eglot--capabilities (jsonrpc-current-process-or-lose)) feat)) (defun eglot--range-region (range) "Return region (BEG . END) that represents LSP RANGE." @@ -461,8 +462,8 @@ Builds a function from METHOD, passes it PROC, ID and PARAMS." nil nil eglot-mode-map (cond (eglot--managed-mode - (add-hook 'jrpc-find-process-functions 'eglot--find-current-process nil t) - (add-hook 'jrpc-ready-predicates 'eglot--server-ready-p nil t) + (add-hook 'jsonrpc-find-process-functions 'eglot--find-current-process nil t) + (add-hook 'jsonrpc-ready-predicates 'eglot--server-ready-p nil t) (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'before-change-functions 'eglot--before-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) @@ -476,8 +477,8 @@ Builds a function from METHOD, passes it PROC, ID and PARAMS." #'eglot-eldoc-function) (add-function :around (local imenu-create-index-function) #'eglot-imenu)) (t - (remove-hook 'jrpc-find-process-functions 'eglot--find-current-process t) - (remove-hook 'jrpc-ready-predicates 'eglot--server-ready-p t) + (remove-hook 'jsonrpc-find-process-functions 'eglot--find-current-process t) + (remove-hook 'jsonrpc-ready-predicates 'eglot--server-ready-p t) (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) (remove-hook 'before-change-functions 'eglot--before-change t) @@ -549,11 +550,11 @@ Uses THING, FACE, DEFS and PREPEND." (defun eglot--mode-line-format () "Compose the EGLOT's mode-line." - (pcase-let* ((proc (jrpc-current-process)) - (name (and (process-live-p proc) (jrpc-name proc))) - (pending (and proc (length (jrpc-outstanding-request-ids proc)))) + (pcase-let* ((proc (jsonrpc-current-process)) + (name (and (process-live-p proc) (jsonrpc-name proc))) + (pending (and proc (length (jsonrpc-outstanding-request-ids proc)))) (`(,_id ,doing ,done-p ,detail) (and proc (eglot--spinner proc))) - (`(,status ,serious-p) (and proc (jrpc-status proc)))) + (`(,status ,serious-p) (and proc (jsonrpc-status proc)))) (append `(,(eglot--mode-line-props "eglot" 'eglot-mode-line nil)) (when name @@ -609,10 +610,10 @@ Uses THING, FACE, DEFS and PREPEND." '("OK")) nil t (plist-get (elt actions 0) :title))) (if reply - (jrpc-reply process id :result (jrpc-obj :title reply)) - (jrpc-reply process id - :error (jrpc-obj :code -32800 - :message "User cancelled")))))) + (jsonrpc-reply process id :result (jsonrpc-obj :title reply)) + (jsonrpc-reply process id + :error (jsonrpc-obj :code -32800 + :message "User cancelled")))))) (cl-defun eglot--server-window/logMessage (_proc &key _type _message) "Handle notification window/logMessage") ;; noop, use events buffer @@ -659,10 +660,10 @@ THINGS are either registrations or unregisterations." proc :id id registerOptions)) (unless (eq t (car retval)) (cl-return-from eglot--register-unregister - (jrpc-reply + (jsonrpc-reply proc jsonrpc-id :error `(:code -32601 :message ,(or (cadr retval) "sorry"))))))))) - (jrpc-reply proc jsonrpc-id :result (jrpc-obj :message "OK"))) + (jsonrpc-reply proc jsonrpc-id :result (jsonrpc-obj :message "OK"))) (cl-defun eglot--server-client/registerCapability (proc &key id registrations) @@ -680,42 +681,42 @@ THINGS are either registrations or unregisterations." (condition-case err (progn (eglot--apply-workspace-edit edit 'confirm) - (jrpc-reply proc id :result `(:applied ))) + (jsonrpc-reply proc id :result `(:applied ))) (error - (jrpc-reply proc id - :result `(:applied :json-false) - :error - (jrpc-obj :code -32001 - :message (format "%s" err)))))) + (jsonrpc-reply proc id + :result `(:applied :json-false) + :error + (jsonrpc-obj :code -32001 + :message (format "%s" err)))))) (defun eglot--TextDocumentIdentifier () "Compute TextDocumentIdentifier object for current buffer." - (jrpc-obj :uri (eglot--path-to-uri buffer-file-name))) + (jsonrpc-obj :uri (eglot--path-to-uri buffer-file-name))) (defvar-local eglot--versioned-identifier 0) (defun eglot--VersionedTextDocumentIdentifier () "Compute VersionedTextDocumentIdentifier object for current buffer." (append (eglot--TextDocumentIdentifier) - (jrpc-obj :version eglot--versioned-identifier))) + (jsonrpc-obj :version eglot--versioned-identifier))) (defun eglot--TextDocumentItem () "Compute TextDocumentItem object for current buffer." (append (eglot--VersionedTextDocumentIdentifier) - (jrpc-obj :languageId - (if (string-match "\\(.*\\)-mode" (symbol-name major-mode)) - (match-string 1 (symbol-name major-mode)) - "unknown") - :text - (save-restriction - (widen) - (buffer-substring-no-properties (point-min) (point-max)))))) + (jsonrpc-obj :languageId + (if (string-match "\\(.*\\)-mode" (symbol-name major-mode)) + (match-string 1 (symbol-name major-mode)) + "unknown") + :text + (save-restriction + (widen) + (buffer-substring-no-properties (point-min) (point-max)))))) (defun eglot--TextDocumentPositionParams () "Compute TextDocumentPositionParams." - (jrpc-obj :textDocument (eglot--TextDocumentIdentifier) - :position (eglot--pos-to-lsp-position))) + (jsonrpc-obj :textDocument (eglot--TextDocumentIdentifier) + :position (eglot--pos-to-lsp-position))) (defvar-local eglot--recent-changes nil "Recent buffer changes as collected by `eglot--before-change'.") @@ -747,16 +748,16 @@ Records START, END and PRE-CHANGE-LENGTH locally." ;; HACK! Launching a deferred sync request with outstanding changes is a ;; bad idea, since that might lead to the request never having a -;; chance to run, because `jrpc-ready-predicates'. -(advice-add #'jrpc-request :before +;; chance to run, because `jsonrpc-ready-predicates'. +(advice-add #'jsonrpc-request :before (cl-function (lambda (_proc _method _params &key deferred) - (when (and eglot--managed-mode deferred) - (eglot--signal-textDocument/didChange))))) + (when (and eglot--managed-mode deferred) + (eglot--signal-textDocument/didChange))))) (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." (when (eglot--outstanding-edits-p) - (let* ((proc (jrpc-current-process-or-lose)) + (let* ((proc (jsonrpc-current-process-or-lose)) (sync-kind (eglot--server-capable :textDocumentSync)) (emacs-messup (/= (length (car eglot--recent-changes)) (length (cdr eglot--recent-changes)))) @@ -765,58 +766,58 @@ Records START, END and PRE-CHANGE-LENGTH locally." (eglot--warn "`eglot--recent-changes' messup: %s" eglot--recent-changes)) (save-restriction (widen) - (jrpc-notify + (jsonrpc-notify proc :textDocument/didChange - (jrpc-obj + (jsonrpc-obj :textDocument (eglot--VersionedTextDocumentIdentifier) :contentChanges (if full-sync-p (vector - (jrpc-obj + (jsonrpc-obj :text (buffer-substring-no-properties (point-min) (point-max)))) (cl-loop for (start-pos end-pos) across (car eglot--recent-changes) for (len after-text) across (cdr eglot--recent-changes) - vconcat `[,(jrpc-obj :range (jrpc-obj :start start-pos - :end end-pos) - :rangeLength len - :text after-text)]))))) + vconcat `[,(jsonrpc-obj :range (jsonrpc-obj :start start-pos + :end end-pos) + :rangeLength len + :text after-text)]))))) (setq eglot--recent-changes (cons [] [])) (setf (eglot--spinner proc) (list nil :textDocument/didChange t)) ;; HACK! - (jrpc--call-deferred proc)))) + (jsonrpc--call-deferred proc)))) (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." (setq eglot--recent-changes (cons [] [])) - (jrpc-notify - (jrpc-current-process-or-lose) + (jsonrpc-notify + (jsonrpc-current-process-or-lose) :textDocument/didOpen `(:textDocument ,(eglot--TextDocumentItem)))) (defun eglot--signal-textDocument/didClose () "Send textDocument/didClose to server." - (jrpc-notify - (jrpc-current-process-or-lose) + (jsonrpc-notify + (jsonrpc-current-process-or-lose) :textDocument/didClose `(:textDocument ,(eglot--TextDocumentIdentifier)))) (defun eglot--signal-textDocument/willSave () "Send textDocument/willSave to server." - (let ((proc (jrpc-current-process-or-lose)) + (let ((proc (jsonrpc-current-process-or-lose)) (params `(:reason 1 :textDocument ,(eglot--TextDocumentIdentifier)))) - (jrpc-notify proc :textDocument/willSave params) + (jsonrpc-notify proc :textDocument/willSave params) (ignore-errors - (let ((jrpc-request-timeout 0.5)) + (let ((jsonrpc-request-timeout 0.5)) (when (plist-get :willSaveWaitUntil (eglot--server-capable :textDocumentSync)) (eglot--apply-text-edits - (jrpc-request proc :textDocument/willSaveWaituntil params))))))) + (jsonrpc-request proc :textDocument/willSaveWaituntil params))))))) (defun eglot--signal-textDocument/didSave () "Send textDocument/didSave to server." - (jrpc-notify - (jrpc-current-process-or-lose) + (jsonrpc-notify + (jsonrpc-current-process-or-lose) :textDocument/didSave - (jrpc-obj + (jsonrpc-obj ;; TODO: Handle TextDocumentSaveRegistrationOptions to control this. :text (buffer-substring-no-properties (point-min) (point-max)) :textDocument (eglot--TextDocumentIdentifier)))) @@ -856,26 +857,27 @@ DUMMY is ignored" (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) (when (eglot--server-capable :documentSymbolProvider) - (let ((proc (jrpc-current-process-or-lose)) + (let ((proc (jsonrpc-current-process-or-lose)) (text-id (eglot--TextDocumentIdentifier))) (completion-table-with-cache (lambda (string) (setq eglot--xref-known-symbols (mapcar - (jrpc-lambda (&key name kind location containerName) + (jsonrpc-lambda + (&key name kind location containerName) (propertize name :textDocumentPositionParams - (jrpc-obj :textDocument text-id - :position (plist-get - (plist-get location :range) - :start)) + (jsonrpc-obj :textDocument text-id + :position (plist-get + (plist-get location :range) + :start)) :locations (list location) :kind kind :containerName containerName)) - (jrpc-request proc - :textDocument/documentSymbol - (jrpc-obj - :textDocument text-id)))) + (jsonrpc-request proc + :textDocument/documentSymbol + (jsonrpc-obj + :textDocument text-id)))) (all-completions string eglot--xref-known-symbols)))))) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) @@ -890,13 +892,13 @@ DUMMY is ignored" (location-or-locations (if rich-identifier (get-text-property 0 :locations rich-identifier) - (jrpc-request (jrpc-current-process-or-lose) - :textDocument/definition - (get-text-property - 0 :textDocumentPositionParams identifier))))) - (mapcar (jrpc-lambda (&key uri range) - (eglot--xref-make identifier uri (plist-get range :start))) - location-or-locations))) + (jsonrpc-request (jsonrpc-current-process-or-lose) + :textDocument/definition + (get-text-property + 0 :textDocumentPositionParams identifier))))) + (mapcar (jsonrpc-lambda (&key uri range) + (eglot--xref-make identifier uri (plist-get range :start))) + location-or-locations))) (cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) (unless (eglot--server-capable :referencesProvider) @@ -908,42 +910,42 @@ DUMMY is ignored" (unless params (eglot--error "Don' know where %s is in the workspace!" identifier)) (mapcar - (jrpc-lambda (&key uri range) + (jsonrpc-lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) - (jrpc-request (jrpc-current-process-or-lose) - :textDocument/references - (append - params - (jrpc-obj :context - (jrpc-obj :includeDeclaration t))))))) + (jsonrpc-request (jsonrpc-current-process-or-lose) + :textDocument/references + (append + params + (jsonrpc-obj :context + (jsonrpc-obj :includeDeclaration t))))))) (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) (mapcar - (jrpc-lambda (&key name location &allow-other-keys) + (jsonrpc-lambda (&key name location &allow-other-keys) (cl-destructuring-bind (&key uri range) location (eglot--xref-make name uri (plist-get range :start)))) - (jrpc-request (jrpc-current-process-or-lose) - :workspace/symbol - (jrpc-obj :query pattern))))) + (jsonrpc-request (jsonrpc-current-process-or-lose) + :workspace/symbol + (jsonrpc-obj :query pattern))))) (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." (let ((bounds (bounds-of-thing-at-point 'symbol)) - (proc (jrpc-current-process-or-lose))) + (proc (jsonrpc-current-process-or-lose))) (when (eglot--server-capable :completionProvider) (list (or (car bounds) (point)) (or (cdr bounds) (point)) (completion-table-with-cache (lambda (_ignored) - (let* ((resp (jrpc-request proc - :textDocument/completion - (eglot--TextDocumentPositionParams) - :deferred :textDocument/completion)) + (let* ((resp (jsonrpc-request proc + :textDocument/completion + (eglot--TextDocumentPositionParams) + :deferred :textDocument/completion)) (items (if (vectorp resp) resp (plist-get resp :items)))) (mapcar - (jrpc-lambda (&rest all &key label &allow-other-keys) + (jsonrpc-lambda (&rest all &key label &allow-other-keys) (add-text-properties 0 1 all label) label) items)))) :annotation-function @@ -962,8 +964,8 @@ DUMMY is ignored" (lambda (obj) (let ((documentation (or (get-text-property 0 :documentation obj) - (plist-get (jrpc-request proc :completionItem/resolve - (text-properties-at 0 obj)) + (plist-get (jsonrpc-request proc :completionItem/resolve + (text-properties-at 0 obj)) :documentation)))) (when documentation (with-current-buffer (get-buffer-create " *eglot doc*") @@ -1014,8 +1016,8 @@ DUMMY is ignored" "Request \"hover\" information for the thing at point." (interactive) (cl-destructuring-bind (&key contents range) - (jrpc-request (jrpc-current-process-or-lose) :textDocument/hover - (eglot--TextDocumentPositionParams)) + (jsonrpc-request (jsonrpc-current-process-or-lose) :textDocument/hover + (eglot--TextDocumentPositionParams)) (when (seq-empty-p contents) (eglot--error "No hover info here")) (with-help-window "*eglot help*" (with-current-buffer standard-output @@ -1025,48 +1027,51 @@ DUMMY is ignored" "EGLOT's `eldoc-documentation-function' function. If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (let* ((buffer (current-buffer)) - (proc (jrpc-current-process-or-lose)) + (proc (jsonrpc-current-process-or-lose)) (position-params (eglot--TextDocumentPositionParams)) sig-showing) (cl-macrolet ((when-buffer-window (&body body) `(when (get-buffer-window buffer) (with-current-buffer buffer ,@body)))) (when (eglot--server-capable :signatureHelpProvider) - (jrpc-async-request + (jsonrpc-async-request proc :textDocument/signatureHelp position-params - :success-fn (jrpc-lambda (&key signatures activeSignature - activeParameter) - (when-buffer-window - (when (cl-plusp (length signatures)) - (setq sig-showing t) - (eldoc-message (eglot--sig-info signatures - activeSignature - activeParameter))))) + :success-fn + (jsonrpc-lambda (&key signatures activeSignature + activeParameter) + (when-buffer-window + (when (cl-plusp (length signatures)) + (setq sig-showing t) + (eldoc-message (eglot--sig-info signatures + activeSignature + activeParameter))))) :deferred :textDocument/signatureHelp)) (when (eglot--server-capable :hoverProvider) - (jrpc-async-request + (jsonrpc-async-request proc :textDocument/hover position-params - :success-fn (jrpc-lambda (&key contents range) + :success-fn (jsonrpc-lambda (&key contents range) (unless sig-showing (when-buffer-window - (eldoc-message (eglot--hover-info contents range))))) + (eldoc-message + (eglot--hover-info contents range))))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) - (jrpc-async-request + (jsonrpc-async-request proc :textDocument/documentHighlight position-params - :success-fn (lambda (highlights) - (mapc #'delete-overlay eglot--highlights) - (setq eglot--highlights - (when-buffer-window - (mapcar - (jrpc-lambda (&key range _kind) - (pcase-let ((`(,beg . ,end) - (eglot--range-region range))) - (let ((ov (make-overlay beg end))) - (overlay-put ov 'face 'highlight) - (overlay-put ov 'evaporate t) - ov))) - highlights)))) + :success-fn + (lambda (highlights) + (mapc #'delete-overlay eglot--highlights) + (setq eglot--highlights + (when-buffer-window + (mapcar + (jsonrpc-lambda (&key range _kind) + (pcase-let ((`(,beg . ,end) + (eglot--range-region range))) + (let ((ov (make-overlay beg end))) + (overlay-put ov 'face 'highlight) + (overlay-put ov 'evaporate t) + ov))) + highlights)))) :deferred :textDocument/documentHighlight)))) nil) @@ -1075,14 +1080,15 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (if (eglot--server-capable :documentSymbolProvider) (let ((entries (mapcar - (jrpc-lambda (&key name kind location _containerName) + (jsonrpc-lambda + (&key name kind location _containerName) (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) (eglot--lsp-position-to-point (plist-get (plist-get location :range) :start)))) - (jrpc-request (jrpc-current-process-or-lose) - :textDocument/documentSymbol - (jrpc-obj - :textDocument (eglot--TextDocumentIdentifier)))))) + (jsonrpc-request (jsonrpc-current-process-or-lose) + :textDocument/documentSymbol + (jsonrpc-obj + :textDocument (eglot--TextDocumentIdentifier)))))) (append (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) entries) @@ -1094,7 +1100,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (unless (or (not version) (equal version eglot--versioned-identifier)) (eglot--error "Edits on `%s' require version %d, you have %d" (current-buffer) version eglot--versioned-identifier)) - (mapc (jrpc-lambda (&key range newText) + (mapc (jsonrpc-lambda + (&key range newText) (save-restriction (widen) (save-excursion @@ -1146,9 +1153,9 @@ Proceed? " (unless (eglot--server-capable :renameProvider) (eglot--error "Server can't rename!")) (eglot--apply-workspace-edit - (jrpc-request (jrpc-current-process-or-lose) - :textDocument/rename `(,@(eglot--TextDocumentPositionParams) - ,@(jrpc-obj :newName newname))) + (jsonrpc-request (jsonrpc-current-process-or-lose) + :textDocument/rename `(,@(eglot--TextDocumentPositionParams) + ,@(jsonrpc-obj :newName newname))) current-prefix-arg)) @@ -1170,7 +1177,7 @@ Proceed? " (string-match (wildcard-to-regexp (expand-file-name glob)) f)))) - (jrpc-notify + (jsonrpc-notify proc :workspace/didChangeWatchedFiles `(:changes ,(vector `(:uri ,(eglot--path-to-uri file) :type ,(cl-case action @@ -1209,7 +1216,7 @@ Proceed? " (add-hook 'rust-mode-hook 'eglot--setup-rls-idiosyncrasies) (defun eglot--setup-rls-idiosyncrasies () "Prepare `eglot' to deal with RLS's special treatment." - (add-hook 'jrpc-ready-predicates 'eglot--rls-probably-ready-for-p t t))) + (add-hook 'jsonrpc-ready-predicates 'eglot--rls-probably-ready-for-p t t))) (cl-defun eglot--server-window/progress (process &key id done title message &allow-other-keys) commit 28b199c3448104161aa93ea0ec0732c367421c54 Author: João Távora Date: Sun May 20 00:44:21 2018 +0100 Fix a bug introduced in the previous commit * eglot.el (eglot--format-markup): Ignore errors when calling possibly unknown functions. (eglot-completion-at-point): Use eglot--format-markup (eglot--hover-info): Yak shaving diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 72e5d31473..9d7253af26 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -779,7 +779,8 @@ If optional MARKER, return a marker instead" (list (plist-get markup :value) (intern (concat (plist-get markup :language) "-mode" )))))) (with-temp-buffer - (funcall mode) (insert string) (font-lock-ensure) (buffer-string)))) + (ignore-errors (funcall mode)) + (insert string) (font-lock-ensure) (buffer-string)))) (defun eglot--server-capable (&rest feats) "Determine if current server is capable of FEATS." @@ -1324,10 +1325,7 @@ DUMMY is ignored" :documentation))))) (when documentation (with-current-buffer (get-buffer-create " *eglot doc*") - (erase-buffer) - (ignore-errors (funcall (intern "markdown-mode"))) - (font-lock-ensure) - (insert documentation) + (insert (eglot--format-markup documentation)) (current-buffer))))) :exit-function (lambda (_string _status) (eglot--signal-textDocument/didChange) @@ -1339,10 +1337,8 @@ DUMMY is ignored" (concat (and range (pcase-let ((`(,beg ,end) (eglot--range-region range))) (concat (buffer-substring beg end) ": "))) (mapconcat #'eglot--format-markup - (append (cond ((vectorp contents) - contents) - (contents - (list contents)))) "\n"))) + (append (cond ((vectorp contents) contents) + (contents (list contents)))) "\n"))) (defun eglot--sig-info (sigs active-sig active-param) (cl-loop commit baf1b82eaa7aed01b47baee8a787852f642c2542 Author: João Távora Date: Sun May 20 00:35:11 2018 +0100 Rewrite a couple of defs and shave a yak * eglot.el (eglot--define-process-var): Simplify. (eglot--format-markup): Rewrite. (eglot--warn, eglot--pos-to-lsp-position) (eglot--lsp-position-to-point, eglot--server-capable) (eglot--maybe-activate-editing-mode) (eglot--server-textDocument/publishDiagnostics) (eglot--server-workspace/applyEdit, eglot--hover-info): Yak shaving. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7bfc8a91aa..72e5d31473 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -109,21 +109,16 @@ lasted more than that many seconds." "Return the current EGLOT process or error." (or (eglot--current-process) (eglot--error "No current EGLOT process"))) -(defmacro eglot--define-process-var - (var-sym initval &optional doc) +(defmacro eglot--define-process-var (var-sym initval &optional doc) "Define VAR-SYM as a generalized process-local variable. INITVAL is the default value. DOC is the documentation." - (declare (indent 2)) + (declare (indent 2) (doc-string 3)) `(progn - (put ',var-sym 'function-documentation ,doc) (defun ,var-sym (proc) - (let* ((plist (process-plist proc)) - (probe (plist-member plist ',var-sym))) - (if probe - (cadr probe) - (let ((def ,initval)) - (process-put proc ',var-sym def) - def)))) + ,doc (let* ((plist (process-plist proc)) + (probe (plist-member plist ',var-sym))) + (if probe (cadr probe) + (let ((def ,initval)) (process-put proc ',var-sym def) def)))) (gv-define-setter ,var-sym (to-store process) `(let ((once ,to-store)) (process-put ,process ',',var-sym once) once)))) @@ -738,29 +733,23 @@ DEFERRED is passed to `eglot--async-request', which see." "Warning message with FORMAT and ARGS." (apply #'eglot--message (concat "(warning) " format) args) (let ((warning-minimum-level :error)) - (display-warning 'eglot - (apply #'format format args) - :warning))) + (display-warning 'eglot (apply #'format format args) :warning))) (defun eglot--pos-to-lsp-position (&optional pos) "Convert point POS to LSP position." (save-excursion - (eglot--obj :line - ;; F!@(#*&#$)CKING OFF-BY-ONE - (1- (line-number-at-pos pos t)) - :character - (- (goto-char (or pos (point))) - (line-beginning-position))))) + (eglot--obj :line (1- (line-number-at-pos pos t)) ; F!@&#$CKING OFF-BY-ONE + :character (- (goto-char (or pos (point))) + (line-beginning-position))))) (defun eglot--lsp-position-to-point (pos-plist &optional marker) "Convert LSP position POS-PLIST to Emacs point. If optional MARKER, return a marker instead" (save-excursion (goto-char (point-min)) (forward-line (plist-get pos-plist :line)) - (forward-char - (min (plist-get pos-plist :character) - (- (line-end-position) - (line-beginning-position)))) + (forward-char (min (plist-get pos-plist :character) + (- (line-end-position) + (line-beginning-position)))) (if marker (copy-marker (point-marker)) (point)))) (defun eglot--path-to-uri (path) @@ -784,31 +773,24 @@ If optional MARKER, return a marker instead" (defun eglot--format-markup (markup) "Format MARKUP according to LSP's spec." - (cond ((stringp markup) - (with-temp-buffer - (ignore-errors (funcall (intern "markdown-mode"))) ;escape bytecomp - (font-lock-ensure) - (insert markup) - (string-trim (buffer-string)))) - (t - (with-temp-buffer - (ignore-errors (funcall (intern (concat - (plist-get markup :language) - "-mode" )))) - (insert (plist-get markup :value)) - (font-lock-ensure) - (buffer-string))))) + (pcase-let ((`(,string ,mode) + (if (stringp markup) (list (string-trim markup) + (intern "markdown-mode")) + (list (plist-get markup :value) + (intern (concat (plist-get markup :language) "-mode" )))))) + (with-temp-buffer + (funcall mode) (insert string) (font-lock-ensure) (buffer-string)))) (defun eglot--server-capable (&rest feats) - "Determine if current server is capable of FEATS." - (cl-loop for caps = (eglot--capabilities (eglot--current-process-or-lose)) - then (cadr probe) - for feat in feats - for probe = (plist-member caps feat) - if (not probe) do (cl-return nil) - if (eq (cadr probe) t) do (cl-return t) - if (eq (cadr probe) :json-false) do (cl-return nil) - finally (cl-return (or probe t)))) +"Determine if current server is capable of FEATS." +(cl-loop for caps = (eglot--capabilities (eglot--current-process-or-lose)) + then (cadr probe) + for feat in feats + for probe = (plist-member caps feat) + if (not probe) do (cl-return nil) + if (eq (cadr probe) t) do (cl-return t) + if (eq (cadr probe) :json-false) do (cl-return nil) + finally (cl-return (or probe t)))) (defun eglot--range-region (range &optional markers) "Return region (BEG END) that represents LSP RANGE. @@ -864,7 +846,6 @@ If optional MARKERS, make markers." (add-hook 'eglot--managed-mode-hook 'flymake-mode) (add-hook 'eglot--managed-mode-hook 'eldoc-mode) - (defvar-local eglot--current-flymake-report-fn nil "Current flymake report function for this buffer") @@ -874,8 +855,7 @@ If PROC is supplied, do it only if BUFFER is managed by it. In that case, also signal textDocument/didOpen." ;; Called even when revert-buffer-in-progress-p (let* ((cur (and buffer-file-name (eglot--current-process))) - (proc (or (and (null proc) cur) - (and proc (eq proc cur) cur)))) + (proc (or (and (null proc) cur) (and proc (eq proc cur) cur)))) (when proc (eglot--managed-mode-onoff proc 1) (eglot--signal-textDocument/didOpen) @@ -1009,7 +989,7 @@ function with the server still running." "Unreported diagnostics for this buffer.") (cl-defun eglot--server-textDocument/publishDiagnostics - (_process &key uri diagnostics) + (_proc &key uri diagnostics) "Handle notification publishDiagnostics" (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) (with-current-buffer buffer @@ -1063,15 +1043,12 @@ THINGS are either registrations or unregisterations." (proc &key id _label edit) "Handle server request workspace/applyEdit" (condition-case err - (progn - (eglot--apply-workspace-edit edit 'confirm) - (eglot--reply proc id :result `(:applied ))) - (error - (eglot--reply proc id - :result `(:applied :json-false) - :error - (eglot--obj :code -32001 - :message (format "%s" err)))))) + (progn (eglot--apply-workspace-edit edit 'confirm) + (eglot--reply proc id :result `(:applied ))) + (error (eglot--reply proc id + :result `(:applied :json-false) + :error (eglot--obj :code -32001 + :message (format "%s" err)))))) (defun eglot--TextDocumentIdentifier () "Compute TextDocumentIdentifier object for current buffer." @@ -1359,15 +1336,13 @@ DUMMY is ignored" (defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") (defun eglot--hover-info (contents &optional range) - (concat (and range - (pcase-let ((`(,beg ,end) (eglot--range-region range))) - (concat (buffer-substring beg end) ": "))) + (concat (and range (pcase-let ((`(,beg ,end) (eglot--range-region range))) + (concat (buffer-substring beg end) ": "))) (mapconcat #'eglot--format-markup - (append - (cond ((vectorp contents) - contents) - (contents - (list contents)))) "\n"))) + (append (cond ((vectorp contents) + contents) + (contents + (list contents)))) "\n"))) (defun eglot--sig-info (sigs active-sig active-param) (cl-loop commit 65ed542c78b3249b9548fa36d98debaa0ab9cfb5 Author: João Távora Date: Sat May 19 23:41:14 2018 +0100 * eglot.el (version): bump to 0.4 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e9da523169..7bfc8a91aa 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 0.3 +;; Version: 0.4 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot commit 749e83c57c6e51a8111c0a960e6ac56a69238db4 Author: João Távora Date: Sat May 19 17:04:46 2018 +0100 Add some completion tests for pyls * eglot-tests.el (edebug): Require it. (eglot--call-with-dirs-and-files): Simplify. (eglot--call-with-test-timeout): Don't timeout if edebug. (auto-detect-running-server, auto-reconnect): Skip unless rls is found. (basic-completions): New test. (hover-after-completions): New failing test. * eglot.el (eglot-eldoc-function): Force write eldoc-last-message, for tests sake. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5829aff59b..e9da523169 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1429,8 +1429,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." proc :textDocument/hover position-params :success-fn (eglot--lambda (&key contents range) (unless sig-showing - (when-buffer-window - (eldoc-message (eglot--hover-info contents range))))) + (setq eldoc-last-message (eglot--hover-info contents range)) + (when-buffer-window (eldoc-message eldoc-last-message)))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) (eglot--async-request commit 490ad2ce0b25cacf9e534af8d9c1ceadba6ddbe2 Author: João Távora Date: Sat May 19 16:50:09 2018 +0100 Handle managed buffers in own process var This should save some trouble when testing noninteractively. Because eglot--shutdown didn't turn off the minor mode, test code running immediately after it could still make didClose requests, for example. The sentinel was the previous responsible for turning off the minor mode and didn't get a chance to run in that case. Now eglot--shutdown is also responsible for turning off the minor mode. All this should be hidden behind eglot--managed-mode-onoff. * eglot.el (eglot--managed-buffers): New process-local variable. (eglot--process-sentinel): Turn off managed mode. (eglot--managed-mode-onoff): New function. (eglot--managed-mode): Don't offer to kill server here. (eglot--buffer-managed-p): Remove. (eglot--maybe-activate-editing-mode): Activate mode here. (eglot-shutdown): Turn off minor mode here. (eglot--server-window/progress): Simplify slightly. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13d008689c..5829aff59b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -172,6 +172,9 @@ A list (WHAT SERIOUS-P).") (eglot--define-process-var eglot--file-watches (make-hash-table :test #'equal) "File system watches for the didChangeWatchedfiles thingy.") +(eglot--define-process-var eglot--managed-buffers nil + "Buffers managed by the server.") + (defun eglot--make-process (name managed-major-mode contact) "Make a process from CONTACT. NAME is used to name the the started process or connection. @@ -415,10 +418,8 @@ INTERACTIVE is t if called interactively." (funcall error `(:code -1 :message "Server died")))) (eglot--pending-continuations proc)) ;; Turn off `eglot--managed-mode' where appropriate. - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (when (eglot--buffer-managed-p proc) - (eglot--managed-mode -1)))) + (dolist (buffer (eglot--managed-buffers proc)) + (with-current-buffer buffer (eglot--managed-mode-onoff proc -1))) ;; Forget about the process-project relationship (setf (gethash (eglot--project proc) eglot--processes-by-project) (delq proc @@ -849,19 +850,20 @@ If optional MARKERS, make markers." (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) (remove-function (local 'eldoc-documentation-function) #'eglot-eldoc-function) - (remove-function (local imenu-create-index-function) #'eglot-imenu) - (let ((proc (eglot--current-process))) - (when (and (process-live-p proc) (y-or-n-p "[eglot] Kill server too? ")) - (eglot-shutdown proc t)))))) + (remove-function (local imenu-create-index-function) #'eglot-imenu)))) + +(defun eglot--managed-mode-onoff (proc arg) + "Proxy for function `eglot--managed-mode' with ARG and PROC." + (eglot--managed-mode arg) + (let ((buf (current-buffer))) + (if eglot--managed-mode + (cl-pushnew buf (eglot--managed-buffers proc)) + (setf (eglot--managed-buffers proc) + (delq buf (eglot--managed-buffers proc)))))) (add-hook 'eglot--managed-mode-hook 'flymake-mode) (add-hook 'eglot--managed-mode-hook 'eldoc-mode) -(defun eglot--buffer-managed-p (&optional proc) - "Tell if current buffer is managed by PROC." - (and buffer-file-name (let ((cur (eglot--current-process))) - (or (and (null proc) cur) - (and proc (eq proc cur)))))) (defvar-local eglot--current-flymake-report-fn nil "Current flymake report function for this buffer") @@ -871,11 +873,14 @@ If optional MARKERS, make markers." If PROC is supplied, do it only if BUFFER is managed by it. In that case, also signal textDocument/didOpen." ;; Called even when revert-buffer-in-progress-p - (when (eglot--buffer-managed-p proc) - (eglot--managed-mode 1) - (eglot--signal-textDocument/didOpen) - (flymake-start) - (funcall (or eglot--current-flymake-report-fn #'ignore) nil))) + (let* ((cur (and buffer-file-name (eglot--current-process))) + (proc (or (and (null proc) cur) + (and proc (eq proc cur) cur)))) + (when proc + (eglot--managed-mode-onoff proc 1) + (eglot--signal-textDocument/didOpen) + (flymake-start) + (funcall (or eglot--current-flymake-report-fn #'ignore) nil)))) (add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) @@ -960,6 +965,9 @@ function with the server still running." (eglot--request proc :shutdown nil) ;; this one is supposed to always fail, hence ignore-errors (ignore-errors (eglot--request proc :exit nil))) + ;; Turn off `eglot--managed-mode' where appropriate. + (dolist (buffer (eglot--managed-buffers proc)) + (with-current-buffer buffer (eglot--managed-mode-onoff proc -1))) (when (process-live-p proc) (eglot--warn "Brutally deleting non-compliant existing process %s" proc) (delete-process proc)))) @@ -1589,11 +1597,10 @@ Proceed? " "Handle notification window/progress" (setf (eglot--spinner process) (list id title done message)) (when (and (equal "Indexing" title) done) - (dolist (buffer (buffer-list)) + (dolist (buffer (eglot--managed-buffers process)) (with-current-buffer buffer - (when (eglot--buffer-managed-p process) - (funcall (or eglot--current-flymake-report-fn #'ignore) - eglot--unreported-diagnostics)))))) + (funcall (or eglot--current-flymake-report-fn #'ignore) + eglot--unreported-diagnostics))))) (provide 'eglot) ;;; eglot.el ends here commit 5b8aa5c90851528bc8a04aaefc891fff6be6846b Author: João Távora Date: Sat May 19 14:26:46 2018 +0100 Robustness fixes for the request mechanism * eglot.el (eglot--async-request): Pass actual id to eglot--log-event (eglot--request): Also cancel any continuations. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a5c4696131..13d008689c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -640,7 +640,7 @@ TIMER)." (funcall (or timeout-fn (lambda () (eglot--log-event - proc `(:timed-out ,method :id id + proc `(:timed-out ,method :id ,id :params ,params))))))))))) (when deferred (let* ((buf (current-buffer)) @@ -703,7 +703,9 @@ DEFERRED is passed to `eglot--async-request', which see." ,(format "Ooops: %s: %s" code message)))) :deferred deferred)) (while t (accept-process-output nil 30))) - (when (cadr id-and-timer) (cancel-timer (cadr id-and-timer)))))) + (pcase-let ((`(,id ,timer) id-and-timer)) + (when id (remhash id (eglot--pending-continuations proc))) + (when timer (cancel-timer timer)))))) (when (eq 'error (car res)) (eglot--error (cadr res))) (cadr res))) commit 212db69280a825473b8539b1fcafc67587d05d05 Author: João Távora Date: Sat May 19 13:45:10 2018 +0100 Check capabilities before sending :completionitem/resolve * eglot.el (eglot--server-capable): Rewrite. (eglot-completion-at-point): Check caps before sending :completionItem/resolve diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e8af7ed80e..a5c4696131 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -796,9 +796,16 @@ If optional MARKER, return a marker instead" (font-lock-ensure) (buffer-string))))) -(defun eglot--server-capable (feat) - "Determine if current server is capable of FEAT." - (plist-get (eglot--capabilities (eglot--current-process-or-lose)) feat)) +(defun eglot--server-capable (&rest feats) + "Determine if current server is capable of FEATS." + (cl-loop for caps = (eglot--capabilities (eglot--current-process-or-lose)) + then (cadr probe) + for feat in feats + for probe = (plist-member caps feat) + if (not probe) do (cl-return nil) + if (eq (cadr probe) t) do (cl-return t) + if (eq (cadr probe) :json-false) do (cl-return nil) + finally (cl-return (or probe t)))) (defun eglot--range-region (range &optional markers) "Return region (BEG END) that represents LSP RANGE. @@ -1323,9 +1330,11 @@ DUMMY is ignored" (lambda (obj) (let ((documentation (or (get-text-property 0 :documentation obj) - (plist-get (eglot--request proc :completionItem/resolve - (text-properties-at 0 obj)) - :documentation)))) + (and (eglot--server-capable :completionProvider + :resolveProvider) + (plist-get (eglot--request proc :completionItem/resolve + (text-properties-at 0 obj)) + :documentation))))) (when documentation (with-current-buffer (get-buffer-create " *eglot doc*") (erase-buffer) commit 75ca40724b869991b191e02e5930db21c1527dd1 Author: João Távora Date: Sat May 19 13:16:36 2018 +0100 Better decide what text exactly to present as completions For inserting, :insertText takes precedence over :label. For annotating, first sentence of :documentation, then :detail, then :kind name. Also remember to send didChange in the :exit-function * eglot.el (eglot-completion-function): Rework main function and :annotation-function, and :exit-function diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9d5ef80535..e8af7ed80e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -702,7 +702,7 @@ DEFERRED is passed to `eglot--async-request', which see." (throw done `(error ,(format "Ooops: %s: %s" code message)))) :deferred deferred)) - (while t (accept-process-output nil 30))) + (while t (accept-process-output nil 30))) (when (cadr id-and-timer) (cancel-timer (cadr id-and-timer)))))) (when (eq 'error (car res)) (eglot--error (cadr res))) (cadr res))) @@ -1300,15 +1300,19 @@ DUMMY is ignored" :textDocument/completion)) (items (if (vectorp resp) resp (plist-get resp :items)))) (mapcar - (eglot--lambda (&rest all &key label &allow-other-keys) - (add-text-properties 0 1 all label) label) + (eglot--lambda (&rest all &key label insertText &allow-other-keys) + (let ((insert (or insertText label))) + (add-text-properties 0 1 all insert) insert)) items)))) :annotation-function (lambda (obj) - (propertize (concat " " (or (get-text-property 0 :detail obj) - (cdr (assoc (get-text-property 0 :kind obj) - eglot--kind-names)))) - 'face 'font-lock-function-name-face)) + (cl-destructuring-bind (&key detail documentation kind &allow-other-keys) + (text-properties-at 0 obj) + (concat " " (propertize + (or (and documentation + (replace-regexp-in-string "\n.*" "" documentation)) + detail (cdr (assoc kind eglot--kind-names))) + 'face 'font-lock-function-name-face)))) :display-sort-function (lambda (items) (sort items (lambda (a b) @@ -1329,8 +1333,9 @@ DUMMY is ignored" (font-lock-ensure) (insert documentation) (current-buffer))))) - :exit-function - (lambda (_string _status) (eglot-eldoc-function)))))) + :exit-function (lambda (_string _status) + (eglot--signal-textDocument/didChange) + (eglot-eldoc-function)))))) (defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") commit 9606e5950a9d6b466bfb5ce78875c247d6560b55 Author: João Távora Date: Sat May 19 13:12:08 2018 +0100 * eglot.el (eglot-clear-status): remember to update modeline diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3635e47b62..9d5ef80535 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -589,7 +589,8 @@ is a symbol saying if this is a client or server originated." (defun eglot-clear-status (process) "Clear most recent error message from PROCESS." (interactive (list (eglot--current-process-or-lose))) - (setf (eglot--status process) nil)) + (setf (eglot--status process) nil) + (force-mode-line-update t)) (defun eglot--call-deferred (proc) "Call PROC's deferred actions, who may again defer themselves." commit a570c09fade804c55296c30deb2df3bb7842a97e Author: João Távora Date: Sat May 19 11:12:41 2018 +0100 Collect regions to change as markers, then edit * eglot.el (eglot--lsp-position-to-point): Accept MARKER optional arg. (eglot--range-region): Accept MARKERS optional arg. Return a list. (eglot--server-textDocument/publishDiagnostics) (eglot--hover-info, eglot-eldoc-function): eglot--range-region returns a list, not a cons. (eglot--apply-text-edits): First collect regions as markers, then edit. GitHub-reference: close https://github.com/joaotavora/eglot/issues/4 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 981029071a..3635e47b62 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -748,15 +748,16 @@ DEFERRED is passed to `eglot--async-request', which see." (- (goto-char (or pos (point))) (line-beginning-position))))) -(defun eglot--lsp-position-to-point (pos-plist) - "Convert LSP position POS-PLIST to Emacs point." +(defun eglot--lsp-position-to-point (pos-plist &optional marker) + "Convert LSP position POS-PLIST to Emacs point. +If optional MARKER, return a marker instead" (save-excursion (goto-char (point-min)) (forward-line (plist-get pos-plist :line)) (forward-char (min (plist-get pos-plist :character) (- (line-end-position) (line-beginning-position)))) - (point))) + (if marker (copy-marker (point-marker)) (point)))) (defun eglot--path-to-uri (path) "URIfy PATH." @@ -798,10 +799,11 @@ DEFERRED is passed to `eglot--async-request', which see." "Determine if current server is capable of FEAT." (plist-get (eglot--capabilities (eglot--current-process-or-lose)) feat)) -(defun eglot--range-region (range) - "Return region (BEG . END) that represents LSP RANGE." - (cons (eglot--lsp-position-to-point (plist-get range :start)) - (eglot--lsp-position-to-point (plist-get range :end)))) +(defun eglot--range-region (range &optional markers) + "Return region (BEG END) that represents LSP RANGE. +If optional MARKERS, make markers." + (list (eglot--lsp-position-to-point (plist-get range :start) markers) + (eglot--lsp-position-to-point (plist-get range :end) markers))) ;;; Minor modes @@ -998,7 +1000,7 @@ function with the server still running." collect (cl-destructuring-bind (&key range severity _group _code source message) diag-spec - (pcase-let ((`(,beg . ,end) (eglot--range-region range))) + (pcase-let ((`(,beg ,end) (eglot--range-region range))) (flymake-make-diagnostic (current-buffer) beg end (cond ((<= severity 1) :error) @@ -1333,7 +1335,7 @@ DUMMY is ignored" (defun eglot--hover-info (contents &optional range) (concat (and range - (pcase-let ((`(,beg . ,end) (eglot--range-region range))) + (pcase-let ((`(,beg ,end) (eglot--range-region range))) (concat (buffer-substring beg end) ": "))) (mapconcat #'eglot--format-markup (append @@ -1413,7 +1415,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (setq eglot--highlights (when-buffer-window (mapcar (eglot--lambda (&key range _kind) - (pcase-let ((`(,beg . ,end) + (pcase-let ((`(,beg ,end) (eglot--range-region range))) (let ((ov (make-overlay beg end))) (overlay-put ov 'face 'highlight) @@ -1447,13 +1449,14 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (unless (or (not version) (equal version eglot--versioned-identifier)) (eglot--error "Edits on `%s' require version %d, you have %d" (current-buffer) version eglot--versioned-identifier)) - (mapc (eglot--lambda (&key range newText) - (save-restriction - (widen) - (save-excursion - (pcase-let ((`(,beg . ,end) (eglot--range-region range))) - (goto-char beg) (delete-region beg end) (insert newText))))) - edits) + (save-restriction + (widen) + (save-excursion + (mapc (eglot--lambda (newText beg end) + (goto-char beg) (delete-region beg end) (insert newText)) + (mapcar (eglot--lambda (&key range newText) + (cons newText (eglot--range-region range 'markers))) + edits)))) (eglot--message "%s: Performed %s edits" (current-buffer) (length edits))) (defun eglot--apply-workspace-edit (wedit &optional confirm) commit 937f999a13a5767d49eabc54d8baa3a355bf4ebf Author: João Távora Date: Sat May 19 10:24:18 2018 +0100 If we're going to send rootpath, better send an absolute one javascript-typescript-langserver complained. * eglot.el (eglot--connect): Use expand-file-name. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5542902f74..981029071a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -273,7 +273,8 @@ INTERACTIVE is t if inside interactive call." 'network) (emacs-pid)) :capabilities(eglot--client-capabilities) - :rootPath (car (project-roots project)) + :rootPath (expand-file-name + (car (project-roots project))) :rootUri (eglot--path-to-uri (car (project-roots project))) :initializationOptions [])) commit 6ee1deebf74ec41b14175ded65c3e9e92734b09e Author: João Távora Date: Sat May 19 10:06:12 2018 +0100 Robustify timer handling for eglot--async-request This basically cherry-picks an ealier commit for the jsonrpc-refactor branch: a2aa1ed..: João Távora 2018-05-18 Robustify timer handling for jrpc-async-request * jrpc.el (jrpc--async-request): Improve timeout handling. Return a list (ID TIMER) (jrpc--request): Protect against user-quits, cancelling timer diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0a2fcf5955..5542902f74 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -624,13 +624,13 @@ objects, respectively. Wait TIMEOUT seconds for response or call nullary TIMEOUT-FN. If DEFERRED, maybe defer request to the future, or to never at all, in case a new request with identical DEFERRED and for the same buffer overrides it (however, if that -happens, the original timeout keeps counting). Return the ID of -this request." +happens, the original timeout keeps counting). Return a list (ID +TIMER)." (let* ((id (eglot--next-request-id)) - (existing-timer nil) - (make-timeout + (timer nil) + (make-timer (lambda ( ) - (or existing-timer + (or timer (run-with-timer timeout nil (lambda () @@ -643,7 +643,7 @@ this request." (when deferred (let* ((buf (current-buffer)) (existing (gethash (list deferred buf) (eglot--deferred-actions proc)))) - (when existing (setq existing-timer (cadr existing))) + (when existing (setq existing (cadr existing))) (if (run-hook-with-args-until-failure 'eglot--ready-predicates deferred proc) (remhash (list deferred buf) (eglot--deferred-actions proc)) @@ -655,11 +655,15 @@ this request." (save-excursion (goto-char point) (apply #'eglot--async-request proc method params args))))))) - (puthash (list deferred buf) (list later (funcall make-timeout)) + (puthash (list deferred buf) (list later (setq timer (funcall make-timer))) (eglot--deferred-actions proc)) (cl-return-from eglot--async-request nil))))) ;; Really run it ;; + (eglot--process-send proc (eglot--obj :jsonrpc "2.0" + :id id + :method method + :params params)) (puthash id (list (or success-fn (eglot--lambda (&rest _ignored) @@ -670,12 +674,9 @@ this request." (setf (eglot--status proc) `(,message t)) proc (eglot--obj :message "error ignored, status set" :id id :error code))) - (funcall make-timeout)) + (setq timer (funcall make-timer))) (eglot--pending-continuations proc)) - (eglot--process-send proc (eglot--obj :jsonrpc "2.0" - :id id - :method method - :params params)))) + (list id timer))) (defun eglot--request (proc method params &optional deferred) "Like `eglot--async-request' for PROC, METHOD and PARAMS, but synchronous. @@ -685,18 +686,22 @@ DEFERRED is passed to `eglot--async-request', which see." ;; bad idea, since that might lead to the request never having a ;; chance to run, because `eglot--ready-predicates'. (when deferred (eglot--signal-textDocument/didChange)) - (let* ((done (make-symbol "eglot--request-catch-tag")) + (let* ((done (make-symbol "eglot-catch")) id-and-timer (res - (catch done - (eglot--async-request - proc method params - :success-fn (lambda (result) (throw done `(done ,result))) - :timeout-fn (lambda () (throw done '(error "Timed out"))) - :error-fn (eglot--lambda (&key code message _data) - (throw done `(error - ,(format "Ooops: %s: %s" code message)))) - :deferred deferred) - (while t (accept-process-output nil 30))))) + (unwind-protect + (catch done + (setq + id-and-timer + (eglot--async-request + proc method params + :success-fn (lambda (result) (throw done `(done ,result))) + :timeout-fn (lambda () (throw done '(error "Timed out"))) + :error-fn (eglot--lambda (&key code message _data) + (throw done `(error + ,(format "Ooops: %s: %s" code message)))) + :deferred deferred)) + (while t (accept-process-output nil 30))) + (when (cadr id-and-timer) (cancel-timer (cadr id-and-timer)))))) (when (eq 'error (car res)) (eglot--error (cadr res))) (cadr res))) commit 2290ce100fc56560c63cbd0d50816e725023315a Author: João Távora Date: Sat May 19 09:29:52 2018 +0100 Simplify some infrastructure fucntions * eglot.el (eglot--contact): Simplify docstring. (eglot--make-process): Simplify. (eglot--connect): Simplify. (eglot--interactive): Simplify and correct odd bug. (eglot--process-sentinel): Correct messages. Delete before attempting reconnection. (eglot-shutdown): Simplify. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4a847bb602..0a2fcf5955 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -163,9 +163,7 @@ A list (WHAT SERIOUS-P).") "If non-nil, don't autoreconnect on unexpected quit.") (eglot--define-process-var eglot--contact nil - "Method used to contact a server. -Either a list of strings (a shell command and arguments), or a -list of a single string of the form :") + "Method used to contact a server.") (eglot--define-process-var eglot--deferred-actions (make-hash-table :test #'equal) @@ -176,29 +174,23 @@ list of a single string of the form :") (defun eglot--make-process (name managed-major-mode contact) "Make a process from CONTACT. -NAME is a name to give the inferior process or connection. +NAME is used to name the the started process or connection. MANAGED-MAJOR-MODE is a symbol naming a major mode. -CONTACT is as `eglot--contact'. Returns a process object." +CONTACT is in `eglot'. Returns a process object." (let* ((readable-name (format "EGLOT server (%s/%s)" name managed-major-mode)) - (buffer (get-buffer-create - (format "*%s inferior*" readable-name))) - singleton - (proc - (if (and (setq singleton (and (null (cdr contact)) (car contact))) - (string-match "^[\s\t]*\\(.*\\):\\([[:digit:]]+\\)[\s\t]*$" - singleton)) - (open-network-stream readable-name - buffer - (match-string 1 singleton) - (string-to-number - (match-string 2 singleton))) - (make-process :name readable-name - :buffer buffer - :command contact - :coding 'no-conversion - :connection-type 'pipe - :stderr (get-buffer-create (format "*%s stderr*" - name)))))) + (buffer (get-buffer-create (format "*%s stdout*" readable-name))) + (proc (cond + ((processp contact) contact) + ((integerp (cadr contact)) + (apply #'open-network-stream readable-name buffer contact)) + (t (make-process + :name readable-name + :command contact + :coding 'no-conversion + :connection-type 'pipe + :stderr (get-buffer-create (format "*%s stderr*" name))))))) + (set-process-buffer proc buffer) + (set-marker (process-mark proc) (with-current-buffer buffer (point-min))) (set-process-filter proc #'eglot--process-filter) (set-process-sentinel proc #'eglot--process-sentinel) proc)) @@ -250,7 +242,9 @@ CONTACT is as `eglot--contact'. Returns a process object." (defun eglot--connect (project managed-major-mode short-name contact _interactive) "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT. INTERACTIVE is t if inside interactive call." - (let* ((proc (eglot--make-process short-name managed-major-mode contact)) + (let* ((proc (eglot--make-process + short-name managed-major-mode (if (functionp contact) + (funcall contact) contact))) (buffer (process-buffer proc))) (setf (eglot--contact proc) contact (eglot--project proc) project @@ -309,32 +303,32 @@ INTERACTIVE is t if inside interactive call." (mapcar #'symbol-name (eglot--all-major-modes)) nil t (symbol-name guessed-mode) nil (symbol-name guessed-mode) nil))) (t guessed-mode))) - (guessed-command (cdr (assoc managed-mode eglot-server-programs))) + (project (or (project-current) `(transient . ,default-directory))) + (guessed (cdr (assoc managed-mode eglot-server-programs))) + (program (and (listp guessed) (stringp (car guessed)) (car guessed))) (base-prompt "[eglot] Enter program to execute (or :): ") (prompt (cond (current-prefix-arg base-prompt) - ((null guessed-command) - (concat (format "[eglot] Sorry, couldn't guess for `%s'!" - managed-mode) - "\n" base-prompt)) - ((and (listp guessed-command) - (not (executable-find (car guessed-command)))) + ((null guessed) + (format "[eglot] Sorry, couldn't guess for `%s'\n%s!" + managed-mode base-prompt)) + ((and program (not (executable-find program))) (concat (format "[eglot] I guess you want to run `%s'" - (combine-and-quote-strings guessed-command)) - (format ", but I can't find `%s' in PATH!" - (car guessed-command)) - "\n" base-prompt))))) - (list - managed-mode - (or (project-current) `(transient . ,default-directory)) - (if prompt - (split-string-and-unquote - (read-shell-command prompt - (if (listp guessed-command) - (combine-and-quote-strings guessed-command)) - 'eglot-command-history)) - guessed-command) - t))) + (combine-and-quote-strings guessed)) + (format ", but I can't find `%s' in PATH!" program) + "\n" base-prompt)))) + (contact + (if prompt + (let ((s (read-shell-command + prompt + (if program (combine-and-quote-strings guessed)) + 'eglot-command-history))) + (if (string-match "^\\([^\s\t]+\\):\\([[:digit:]]+\\)$" + (string-trim s)) + (list (match-string 1 s) (string-to-number (match-string 2 s))) + (split-string-and-unquote s))) + guessed))) + (list managed-mode project contact t))) ;;;###autoload (defun eglot (managed-major-mode project command &optional interactive) @@ -417,7 +411,7 @@ INTERACTIVE is t if called interactively." ;; Call all outstanding error handlers (maphash (lambda (_id triplet) (cl-destructuring-bind (_success error _timeout) triplet - (funcall error :code -1 :message (format "Server died")))) + (funcall error `(:code -1 :message "Server died")))) (eglot--pending-continuations proc)) ;; Turn off `eglot--managed-mode' where appropriate. (dolist (buffer (buffer-list)) @@ -428,14 +422,16 @@ INTERACTIVE is t if called interactively." (setf (gethash (eglot--project proc) eglot--processes-by-project) (delq proc (gethash (eglot--project proc) eglot--processes-by-project))) - (eglot--message "Server exited with status %s" (process-exit-status proc)) + ;; Say last words + (eglot--message "%s exited with status %s" proc (process-exit-status proc)) + (delete-process proc) + ;; Consider autoreconnecting (cond ((eglot--moribund proc)) ((not (eglot--inhibit-autoreconnect proc)) (eglot--warn "Reconnecting after unexpected server exit") (eglot-reconnect proc)) ((timerp (eglot--inhibit-autoreconnect proc)) - (eglot--warn "Not auto-reconnecting, last on didn't last long."))) - (delete-process proc)))) + (eglot--warn "Not auto-reconnecting, last on didn't last long.")))))) (defun eglot--process-filter (proc string) "Called when new data STRING has arrived for PROC." @@ -934,23 +930,20 @@ Uses THING, FACE, DEFS and PREPEND." ;;; Protocol implementation (Requests, notifications, etc) ;;; -(defun eglot-shutdown (proc &optional interactive) +(defun eglot-shutdown (proc &optional _interactive) "Politely ask the server PROC to quit. Forcefully quit it if it doesn't respond. Don't leave this -function with the server still running. INTERACTIVE is t if -called interactively." +function with the server still running." (interactive (list (eglot--current-process-or-lose) t)) - (when interactive (eglot--message "Asking %s politely to terminate" proc)) + (eglot--message "Asking %s politely to terminate" proc) (unwind-protect (let ((eglot-request-timeout 3)) (setf (eglot--moribund proc) t) - (eglot--request proc - :shutdown - nil) - ;; this one should always fail + (eglot--request proc :shutdown nil) + ;; this one is supposed to always fail, hence ignore-errors (ignore-errors (eglot--request proc :exit nil))) (when (process-live-p proc) - (eglot--warn "Brutally deleting existing process %s" proc) + (eglot--warn "Brutally deleting non-compliant existing process %s" proc) (delete-process proc)))) (cl-defun eglot--server-window/showMessage (_process &key type message) commit 99cb423db699ec1ed359326b9b6f9892067de5bc Author: João Távora Date: Fri May 18 16:52:19 2018 +0100 Jrpc.el should know nothing of mode-line updates * eglot.el (eglot--dispatch): METHOD can be a symbol. Call force-mode-line-update here. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4e02c72e3e..81229a5817 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -317,11 +317,12 @@ INTERACTIVE is t if called interactively." (defun eglot--dispatch (proc method id params) "Dispatcher passed to `jrpc-connect'. Builds a function from METHOD, passes it PROC, ID and PARAMS." - (let* ((handler-sym (intern (concat "eglot--server-" method)))) + (let* ((handler-sym (intern (format "eglot--server-%s" method)))) (if (functionp handler-sym) ;; FIXME: fails if params is array, not object (apply handler-sym proc (append params (if id `(:id ,id)))) (jrpc-reply proc id - :error (jrpc-obj :code -32601 :message "Unimplemented"))))) + :error (jrpc-obj :code -32601 :message "Unimplemented"))) + (force-mode-line-update t))) (defun eglot--connect (project managed-major-mode name contact) (let* ((contact (if (functionp contact) (funcall contact) contact)) commit 868d531c9ee06cf2ebd7f4232c148f8e414f98ef Author: João Távora Date: Fri May 18 12:35:36 2018 +0100 Improve jrpc.el's doc (and change jrpc-request's protocol a tiny bit) * jrpc.el (jrpc-async-request) (jrpc-request,jrpc-notify,jrpc-reply): Improve docstring. (jrpc-connect): Improve docstring and add autoload cookie (jrpc-request): DEFERRED param is now &key (defgroup jrpc): Fix description. * eglot.el (advice-add jrpc-request): Use &key deferred. (eglot-completion-at-point): Pass :deferred to jrpc-request diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b820ddeae5..4e02c72e3e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -744,11 +744,13 @@ Records START, END and PRE-CHANGE-LENGTH locally." `[(,pre-change-length ,(buffer-substring-no-properties start end))]))) -;; HACK! +;; HACK! Launching a deferred sync request with outstanding changes is a +;; bad idea, since that might lead to the request never having a +;; chance to run, because `jrpc-ready-predicates'. (advice-add #'jrpc-request :before - (lambda (_proc _method _params &optional deferred) + (cl-function (lambda (_proc _method _params &key deferred) (when (and eglot--managed-mode deferred) - (eglot--signal-textDocument/didChange)))) + (eglot--signal-textDocument/didChange))))) (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." @@ -937,7 +939,7 @@ DUMMY is ignored" (let* ((resp (jrpc-request proc :textDocument/completion (eglot--TextDocumentPositionParams) - :textDocument/completion)) + :deferred :textDocument/completion)) (items (if (vectorp resp) resp (plist-get resp :items)))) (mapcar (jrpc-lambda (&rest all &key label &allow-other-keys) commit d29b695179719844a8d0cdb0996b250a716f9e23 Merge: a6046e9efd 3596240259 Author: João Távora Date: Fri May 18 11:57:22 2018 +0100 Merge branch 'master' into jsonrpc-refactor (using good ol' git merge) commit 35962402594c6aaf320878e1c5a6433f1d0553d7 Author: João Távora Date: Thu May 17 14:09:50 2018 +0100 * eglot.el (version): bump to 0.3 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 08c2f55dbe..4a847bb602 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 0.2 +;; Version: 0.3 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot commit 04da3b6abdcddebdaf899a1f2f0de511f4174146 Author: João Távora Date: Thu May 17 14:03:20 2018 +0100 Make it work on windows Apparently passing :coding 'no-conversion to make-process on windows is essential to receive any text at all in the process filter. Also needed to tweak uri-to-path and path-to-uri. Thanks to lsp-mode.el for these hints * eglot.el (eglot--make-process): Pass :coding 'no-conversion to make-process. (eglot--path-to-uri): Add a forward slash if windows-nt. (eglot--uri-to-path): Remove a forward slash if windows-nt. (eglot--server-textDocument/publishDiagnostics): Simplify and use eglot--uri-to-path. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 01a6b5d47f..08c2f55dbe 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -195,6 +195,7 @@ CONTACT is as `eglot--contact'. Returns a process object." (make-process :name readable-name :buffer buffer :command contact + :coding 'no-conversion :connection-type 'pipe :stderr (get-buffer-create (format "*%s stderr*" name)))))) @@ -756,14 +757,16 @@ DEFERRED is passed to `eglot--async-request', which see." (point))) (defun eglot--path-to-uri (path) - "Urify PATH." - (url-hexify-string (concat "file://" (file-truename path)) - url-path-allowed-chars)) + "URIfy PATH." + (url-hexify-string + (concat "file://" (if (eq system-type 'windows-nt) "/") (file-truename path)) + url-path-allowed-chars)) (defun eglot--uri-to-path (uri) "Convert URI to a file path." (when (keywordp uri) (setq uri (substring (symbol-name uri) 1))) - (url-filename (url-generic-parse-url (url-unhex-string uri)))) + (let ((retval (url-filename (url-generic-parse-url (url-unhex-string uri))))) + (if (eq system-type 'windows-nt) (substring retval 1) retval))) (defconst eglot--kind-names `((1 . "Text") (2 . "Method") (3 . "Function") (4 . "Constructor") @@ -989,11 +992,7 @@ called interactively." (cl-defun eglot--server-textDocument/publishDiagnostics (_process &key uri diagnostics) "Handle notification publishDiagnostics" - (let* ((obj (url-generic-parse-url uri)) - (filename (car (url-path-and-query obj))) - (buffer (find-buffer-visiting filename))) - (cond - (buffer + (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) (with-current-buffer buffer (cl-loop for diag-spec across diagnostics @@ -1012,9 +1011,8 @@ called interactively." (funcall eglot--current-flymake-report-fn diags) (setq eglot--unreported-diagnostics nil)) (t - (setq eglot--unreported-diagnostics diags)))))) - (t - (eglot--message "OK so %s isn't visited" filename))))) + (setq eglot--unreported-diagnostics diags))))) + (eglot--warn "Diagnostics received for unvisited %s" uri))) (cl-defun eglot--register-unregister (proc jsonrpc-id things how) "Helper for `eglot--server-client/registerCapability'. commit 1104060048f1e2fae7e5ed19a00bbe1d738a5861 Author: João Távora Date: Thu May 17 14:04:15 2018 +0100 Fix eglot--error and eglot--message helpers * eglot.el (eglot--error, eglot--message): Safely interpret %s. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 30d5b435bb..01a6b5d47f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -721,11 +721,11 @@ DEFERRED is passed to `eglot--async-request', which see." ;;; (defun eglot--error (format &rest args) "Error out with FORMAT with ARGS." - (error (apply #'format format args))) + (error "[eglot] %s" (apply #'format format args))) (defun eglot--message (format &rest args) "Message out with FORMAT with ARGS." - (message (concat "[eglot] " (apply #'format format args)))) + (message "[eglot] %s" (apply #'format format args))) (defun eglot--warn (format &rest args) "Warning message with FORMAT and ARGS." commit b7d0c91afc3d954d1d47e4dce48d607b87bdfcf6 Author: João Távora Date: Thu May 17 14:04:33 2018 +0100 * eglot.el (eglot--lambda): add missing indent spec. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e17e4f8766..30d5b435bb 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -610,7 +610,7 @@ request request and a process object.") (not (eglot--outstanding-edits-p))) (cl-defmacro eglot--lambda (cl-lambda-list &body body) - (declare (debug (sexp &rest form))) + (declare (indent 1) (debug (sexp &rest form))) (let ((e (gensym "eglot--lambda-elem"))) `(lambda (,e) (apply (cl-function (lambda ,cl-lambda-list ,@body)) ,e)))) commit 0eb1ef8d36124a63d211c7d7f7c16ba17cbed975 Author: João Távora Date: Thu May 17 00:30:53 2018 +0100 Simplify some function calling infrastructure eglot--mapply is a confusing abstraction. Hide some of that confusion behind eglot--lambda. More stably dispatch server notifications and requests without introspecting their contents. * eglot.el (eglot--process-receive): Simplify. (eglot--async-request): Improve doc. (eglot--request): Simplify. (eglot--mapply): Remove. (xref-backend-identifier-completion-table) (xref-backend-definitions, xref-backend-references) (xref-backend-apropos, eglot-completion-at-point) (eglot-eldoc-function, eglot-imenu, eglot--apply-text-edits): Don't use eglot--mapply, use normal mapcar/mapc. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0c11b96cbe..e17e4f8766 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -541,7 +541,7 @@ is a symbol saying if this is a client or server originated." (defun eglot--process-receive (proc message) "Process MESSAGE from PROC." - (cl-destructuring-bind (&key method id error &allow-other-keys) message + (cl-destructuring-bind (&key method id params error result _jsonrpc) message (let* ((continuations (and id (not method) (gethash id (eglot--pending-continuations proc))))) @@ -551,24 +551,19 @@ is a symbol saying if this is a client or server originated." ;; a server notification or a server request (let* ((handler-sym (intern (concat "eglot--server-" method)))) (if (functionp handler-sym) - (apply handler-sym proc (append - (plist-get message :params) - (if id `(:id ,id)))) + ;; FIXME: will fail if params is array instead of not an object + (apply handler-sym proc (append params (if id `(:id ,id)))) (eglot--warn "No implementation of method %s yet" method) (when id (eglot--reply proc id - :error (eglot--obj :code -32601 - :message "Method unimplemented")))))) + :error `(:code -32601 :message "Method unimplemented")))))) (continuations (cancel-timer (cl-third continuations)) (remhash id (eglot--pending-continuations proc)) (if error - (apply (cl-second continuations) error) - (let ((res (plist-get message :result))) - (if (listp res) - (apply (cl-first continuations) res) - (funcall (cl-first continuations) res))))) + (funcall (cl-second continuations) error) + (funcall (cl-first continuations) result))) (id (eglot--warn "Ooops no continuation for id %s" id))) (eglot--call-deferred proc) @@ -615,8 +610,9 @@ request request and a process object.") (not (eglot--outstanding-edits-p))) (cl-defmacro eglot--lambda (cl-lambda-list &body body) - (declare (indent 1) (debug (sexp &rest form))) - `(cl-function (lambda ,cl-lambda-list ,@body))) + (declare (debug (sexp &rest form))) + (let ((e (gensym "eglot--lambda-elem"))) + `(lambda (,e) (apply (cl-function (lambda ,cl-lambda-list ,@body)) ,e)))) (cl-defun eglot--async-request (proc method @@ -625,12 +621,14 @@ request request and a process object.") &key success-fn error-fn timeout-fn (timeout eglot-request-timeout) (deferred nil)) - "Make a request to PROCESS, expecting a reply. -Return the ID of this request. Wait TIMEOUT seconds for response. -If DEFERRED, maybe defer request to the future, or never at all, -in case a new request with identical DEFERRED and for the same -buffer overrides it. However, if that happens, the original -timeout keeps counting." + "Make a request to PROCESS, expecting a reply later on. +SUCCESS-FN and ERROR-FN are passed `:result' and `:error' +objects, respectively. Wait TIMEOUT seconds for response or call +nullary TIMEOUT-FN. If DEFERRED, maybe defer request to the +future, or to never at all, in case a new request with identical +DEFERRED and for the same buffer overrides it (however, if that +happens, the original timeout keeps counting). Return the ID of +this request." (let* ((id (eglot--next-request-id)) (existing-timer nil) (make-timeout @@ -692,23 +690,18 @@ DEFERRED is passed to `eglot--async-request', which see." (when deferred (eglot--signal-textDocument/didChange)) (let* ((done (make-symbol "eglot--request-catch-tag")) (res - (catch done (eglot--async-request - proc method params - :success-fn (lambda (&rest args) - (throw done (if (vectorp (car args)) - (car args) args))) - :error-fn (eglot--lambda - (&key code message &allow-other-keys) - (throw done - `(error ,(format "Oops: %s: %s" - code message)))) - :timeout-fn (lambda () - (throw done '(error "Timed out"))) - :deferred deferred) - ;; now spin, baby! - (while t (accept-process-output nil 0.01))))) - (when (and (listp res) (eq 'error (car res))) (eglot--error (cadr res))) - res)) + (catch done + (eglot--async-request + proc method params + :success-fn (lambda (result) (throw done `(done ,result))) + :timeout-fn (lambda () (throw done '(error "Timed out"))) + :error-fn (eglot--lambda (&key code message _data) + (throw done `(error + ,(format "Ooops: %s: %s" code message)))) + :deferred deferred) + (while t (accept-process-output nil 30))))) + (when (eq 'error (car res)) (eglot--error (cadr res))) + (cadr res))) (cl-defun eglot--notify (process method params) "Notify PROCESS of something, don't expect a reply.e" @@ -762,11 +755,6 @@ DEFERRED is passed to `eglot--async-request', which see." (line-beginning-position)))) (point))) - -(defun eglot--mapply (fun seq) - "Apply FUN to every element of SEQ." - (mapcar (lambda (e) (apply fun e)) seq)) - (defun eglot--path-to-uri (path) "Urify PATH." (url-hexify-string (concat "file://" (file-truename path)) @@ -1232,7 +1220,7 @@ DUMMY is ignored" (completion-table-with-cache (lambda (string) (setq eglot--xref-known-symbols - (eglot--mapply + (mapcar (eglot--lambda (&key name kind location containerName) (propertize name :textDocumentPositionParams @@ -1265,10 +1253,9 @@ DUMMY is ignored" :textDocument/definition (get-text-property 0 :textDocumentPositionParams identifier))))) - (eglot--mapply - (eglot--lambda (&key uri range) - (eglot--xref-make identifier uri (plist-get range :start))) - location-or-locations))) + (mapcar (eglot--lambda (&key uri range) + (eglot--xref-make identifier uri (plist-get range :start))) + location-or-locations))) (cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) (unless (eglot--server-capable :referencesProvider) @@ -1279,25 +1266,23 @@ DUMMY is ignored" (and rich (get-text-property 0 :textDocumentPositionParams rich)))))) (unless params (eglot--error "Don' know where %s is in the workspace!" identifier)) - (eglot--mapply - (eglot--lambda (&key uri range) - (eglot--xref-make identifier uri (plist-get range :start))) - (eglot--request (eglot--current-process-or-lose) - :textDocument/references - (append - params - (eglot--obj :context - (eglot--obj :includeDeclaration t))))))) + (mapcar (eglot--lambda (&key uri range) + (eglot--xref-make identifier uri (plist-get range :start))) + (eglot--request (eglot--current-process-or-lose) + :textDocument/references + (append + params + (eglot--obj :context + (eglot--obj :includeDeclaration t))))))) (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) - (eglot--mapply - (eglot--lambda (&key name location &allow-other-keys) - (cl-destructuring-bind (&key uri range) location - (eglot--xref-make name uri (plist-get range :start)))) - (eglot--request (eglot--current-process-or-lose) - :workspace/symbol - (eglot--obj :query pattern))))) + (mapcar (eglot--lambda (&key name location &allow-other-keys) + (cl-destructuring-bind (&key uri range) location + (eglot--xref-make name uri (plist-get range :start)))) + (eglot--request (eglot--current-process-or-lose) + :workspace/symbol + (eglot--obj :query pattern))))) (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." @@ -1314,7 +1299,7 @@ DUMMY is ignored" (eglot--TextDocumentPositionParams) :textDocument/completion)) (items (if (vectorp resp) resp (plist-get resp :items)))) - (eglot--mapply + (mapcar (eglot--lambda (&rest all &key label &allow-other-keys) (add-text-properties 0 1 all label) label) items)))) @@ -1430,15 +1415,14 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (mapc #'delete-overlay eglot--highlights) (setq eglot--highlights (when-buffer-window - (eglot--mapply - (eglot--lambda (&key range _kind) - (pcase-let ((`(,beg . ,end) - (eglot--range-region range))) - (let ((ov (make-overlay beg end))) - (overlay-put ov 'face 'highlight) - (overlay-put ov 'evaporate t) - ov))) - highlights)))) + (mapcar (eglot--lambda (&key range _kind) + (pcase-let ((`(,beg . ,end) + (eglot--range-region range))) + (let ((ov (make-overlay beg end))) + (overlay-put ov 'face 'highlight) + (overlay-put ov 'evaporate t) + ov))) + highlights)))) :deferred :textDocument/documentHighlight)))) nil) @@ -1446,7 +1430,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." "EGLOT's `imenu-create-index-function' overriding OLDFUN." (if (eglot--server-capable :documentSymbolProvider) (let ((entries - (eglot--mapply + (mapcar (eglot--lambda (&key name kind location _containerName) (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) (eglot--lsp-position-to-point @@ -1466,14 +1450,13 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (unless (or (not version) (equal version eglot--versioned-identifier)) (eglot--error "Edits on `%s' require version %d, you have %d" (current-buffer) version eglot--versioned-identifier)) - (eglot--mapply - (eglot--lambda (&key range newText) - (save-restriction - (widen) - (save-excursion - (pcase-let ((`(,beg . ,end) (eglot--range-region range))) - (goto-char beg) (delete-region beg end) (insert newText))))) - edits) + (mapc (eglot--lambda (&key range newText) + (save-restriction + (widen) + (save-excursion + (pcase-let ((`(,beg . ,end) (eglot--range-region range))) + (goto-char beg) (delete-region beg end) (insert newText))))) + edits) (eglot--message "%s: Performed %s edits" (current-buffer) (length edits))) (defun eglot--apply-workspace-edit (wedit &optional confirm) commit 5db50ddd47518aa82e0be1584e7019f6ae60adbb Author: João Távora Date: Wed May 16 23:45:35 2018 +0100 Replace eglot--with-lsp-range with a function and pcase-let * eglot.el (eglot--with-lsp-range): Remove. (eglot--range-region): New function. (eglot--server-textDocument/publishDiagnostics) (eglot--hover-info, eglot-eldoc-function) (eglot--apply-text-edits): Use it. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index da3a09f3b2..0c11b96cbe 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -805,14 +805,10 @@ DEFERRED is passed to `eglot--async-request', which see." "Determine if current server is capable of FEAT." (plist-get (eglot--capabilities (eglot--current-process-or-lose)) feat)) -(cl-defmacro eglot--with-lsp-range ((start end) range &body body - &aux (range-sym (cl-gensym))) - "Bind LSP RANGE to START and END. Evaluate BODY." - (declare (indent 2) (debug (sexp sexp &rest form))) - `(let* ((,range-sym ,range) - (,start (eglot--lsp-position-to-point (plist-get ,range-sym :start))) - (,end (eglot--lsp-position-to-point (plist-get ,range-sym :end)))) - ,@body)) +(defun eglot--range-region (range) + "Return region (BEG . END) that represents LSP RANGE." + (cons (eglot--lsp-position-to-point (plist-get range :start)) + (eglot--lsp-position-to-point (plist-get range :end)))) ;;; Minor modes @@ -1016,7 +1012,7 @@ called interactively." collect (cl-destructuring-bind (&key range severity _group _code source message) diag-spec - (eglot--with-lsp-range (beg end) range + (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (flymake-make-diagnostic (current-buffer) beg end (cond ((<= severity 1) :error) @@ -1355,7 +1351,7 @@ DUMMY is ignored" (defun eglot--hover-info (contents &optional range) (concat (and range - (eglot--with-lsp-range (beg end) range + (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (concat (buffer-substring beg end) ": "))) (mapconcat #'eglot--format-markup (append @@ -1436,7 +1432,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (when-buffer-window (eglot--mapply (eglot--lambda (&key range _kind) - (eglot--with-lsp-range (beg end) range + (pcase-let ((`(,beg . ,end) + (eglot--range-region range))) (let ((ov (make-overlay beg end))) (overlay-put ov 'face 'highlight) (overlay-put ov 'evaporate t) @@ -1474,7 +1471,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (save-restriction (widen) (save-excursion - (eglot--with-lsp-range (beg end) range + (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (goto-char beg) (delete-region beg end) (insert newText))))) edits) (eglot--message "%s: Performed %s edits" (current-buffer) (length edits))) commit a6046e9efd71e0efabe487686a4f8c08de76d78d Author: João Távora Date: Wed May 16 23:39:12 2018 +0100 Simpler callback protocol for jsonrpc parameters and results Instead of introspecting the :params or :result object to discover if an object is present, and changing the Elisp function call type (funcall vs apply) accordingly, alway funcall. It's up to the application to destructure if it wishes. jrpc-lambda can help with that and keep the application code simple. * eglot.el (eglot--on-shutdown): Fix indentation. (eglot--dispatch): Simplify. (xref-backend-identifier-completion-table) (xref-backend-definitions, xref-backend-references) (xref-backend-apropos, eglot-completion-at-point) (eglot-eldoc-function, eglot-imenu, eglot--apply-text-edits): Don't use jrpc-mapply. * jrpc.el (jrpc--process-receive): Allow only keys defined in JSONRPC2.0 (jrpc--process-receive): Don't overload function call type based on remote response. (jrpc-lambda): Return a unary lambda. (jrpc-request): Simplify. (jrpc-mapply): Remove. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4f2e25b289..caf2e8c82f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -128,7 +128,7 @@ A list (ID WHAT DONE-P).") (eglot--managed-mode -1)))) ;; Kill any expensive watches (maphash (lambda (_id watches) - (mapcar #'file-notify-rm-watch watches)) + (mapcar #'file-notify-rm-watch watches)) (eglot--file-watches proc)) ;; Sever the project/process relationship for proc (setf (gethash (eglot--project proc) eglot--processes-by-project) @@ -314,7 +314,7 @@ INTERACTIVE is t if called interactively." (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") -(defun eglot--dispatch (proc method id &rest params) +(defun eglot--dispatch (proc method id params) "Dispatcher passed to `jrpc-connect'. Builds a function from METHOD, passes it PROC, ID and PARAMS." (let* ((handler-sym (intern (concat "eglot--server-" method)))) @@ -865,7 +865,7 @@ DUMMY is ignored" (completion-table-with-cache (lambda (string) (setq eglot--xref-known-symbols - (jrpc-mapply + (mapcar (jrpc-lambda (&key name kind location containerName) (propertize name :textDocumentPositionParams @@ -898,7 +898,7 @@ DUMMY is ignored" :textDocument/definition (get-text-property 0 :textDocumentPositionParams identifier))))) - (jrpc-mapply + (mapcar (jrpc-lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) location-or-locations))) @@ -912,7 +912,7 @@ DUMMY is ignored" (and rich (get-text-property 0 :textDocumentPositionParams rich)))))) (unless params (eglot--error "Don' know where %s is in the workspace!" identifier)) - (jrpc-mapply + (mapcar (jrpc-lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) (jrpc-request (jrpc-current-process-or-lose) @@ -924,7 +924,7 @@ DUMMY is ignored" (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) - (jrpc-mapply + (mapcar (jrpc-lambda (&key name location &allow-other-keys) (cl-destructuring-bind (&key uri range) location (eglot--xref-make name uri (plist-get range :start)))) @@ -947,7 +947,7 @@ DUMMY is ignored" (eglot--TextDocumentPositionParams) :textDocument/completion)) (items (if (vectorp resp) resp (plist-get resp :items)))) - (jrpc-mapply + (mapcar (jrpc-lambda (&rest all &key label &allow-other-keys) (add-text-properties 0 1 all label) label) items)))) @@ -1040,7 +1040,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (jrpc-async-request proc :textDocument/signatureHelp position-params :success-fn (jrpc-lambda (&key signatures activeSignature - activeParameter) + activeParameter) (when-buffer-window (when (cl-plusp (length signatures)) (setq sig-showing t) @@ -1063,7 +1063,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (mapc #'delete-overlay eglot--highlights) (setq eglot--highlights (when-buffer-window - (jrpc-mapply + (mapcar (jrpc-lambda (&key range _kind) (eglot--with-lsp-range (beg end) range (let ((ov (make-overlay beg end))) @@ -1078,7 +1078,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." "EGLOT's `imenu-create-index-function' overriding OLDFUN." (if (eglot--server-capable :documentSymbolProvider) (let ((entries - (jrpc-mapply + (mapcar (jrpc-lambda (&key name kind location _containerName) (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) (eglot--lsp-position-to-point @@ -1098,14 +1098,13 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (unless (or (not version) (equal version eglot--versioned-identifier)) (eglot--error "Edits on `%s' require version %d, you have %d" (current-buffer) version eglot--versioned-identifier)) - (jrpc-mapply - (jrpc-lambda (&key range newText) - (save-restriction - (widen) - (save-excursion - (eglot--with-lsp-range (beg end) range - (goto-char beg) (delete-region beg end) (insert newText))))) - edits) + (mapc (jrpc-lambda (&key range newText) + (save-restriction + (widen) + (save-excursion + (eglot--with-lsp-range (beg end) range + (goto-char beg) (delete-region beg end) (insert newText))))) + edits) (eglot--message "%s: Performed %s edits" (current-buffer) (length edits))) (defun eglot--apply-workspace-edit (wedit &optional confirm) commit 8bbd2ba28d0774c3d2547c35e9d34d184d90bdc4 Author: João Távora Date: Wed May 16 14:21:27 2018 +0100 More flexible jrpc.el and improve eglot.el's doc Generalize and rework CONTACT arg to jrpc-connect * eglot.el (eglot--command-history): Tweak docstring. (eglot--interactive): Rework. (eglot): Rework docstring. COMMAND is now CONTACT. (eglot--connect): Use new jrpc-connect protocol. (eglot-server-programs): Reword doc. * jrpc.el (jrpc--make-process): Use new form of CONTACT. (jrpc-connect): Explain new semantics of CONTACT. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13c1b49c75..4f2e25b289 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -75,7 +75,9 @@ (sh-mode . ("bash-language-server" "start")) (php-mode . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php"))) - "Alist mapping major modes to server executables.") + "Alist of (MAJOR-MODE . CONTACT) mapping major modes to server executables. +CONTACT can be anything accepted by that parameter in the +function `eglot', which see.") (defface eglot-mode-line '((t (:inherit font-lock-constant-face :weight bold))) @@ -198,7 +200,7 @@ called interactively." :experimental (jrpc-obj))) (defvar eglot--command-history nil - "History of COMMAND arguments to `eglot'.") + "History of CONTACT arguments to `eglot'.") (defun eglot--interactive () "Helper for `eglot'." @@ -213,6 +215,7 @@ called interactively." (mapcar #'symbol-name (eglot--all-major-modes)) nil t (symbol-name guessed-mode) nil (symbol-name guessed-mode) nil))) (t guessed-mode))) + (project (or (project-current) `(transient . ,default-directory))) (guessed-command (cdr (assoc managed-mode eglot-server-programs))) (base-prompt "[eglot] Enter program to execute (or :): ") (prompt @@ -222,26 +225,30 @@ called interactively." managed-mode) "\n" base-prompt)) ((and (listp guessed-command) + (not (integerp (cadr guessed-command))) (not (executable-find (car guessed-command)))) (concat (format "[eglot] I guess you want to run `%s'" (combine-and-quote-strings guessed-command)) (format ", but I can't find `%s' in PATH!" (car guessed-command)) - "\n" base-prompt))))) - (list - managed-mode - (or (project-current) `(transient . ,default-directory)) - (if prompt - (split-string-and-unquote - (read-shell-command prompt - (if (listp guessed-command) - (combine-and-quote-strings guessed-command)) - 'eglot-command-history)) - guessed-command) - t))) + "\n" base-prompt)))) + (contact + (cond ((not prompt) guessed-command) + (t + (let ((string (read-shell-command + prompt + (if (listp guessed-command) + (combine-and-quote-strings guessed-command)) + 'eglot-command-history))) + (if (and string (string-match + "^\\([^\s\t]+\\):\\([[:digit:]]+\\)$" + (string-trim string))) + (list (match-string 1 string) (match-string 2 string)) + (split-string-and-unquote string))))))) + (list managed-mode project contact t))) ;;;###autoload -(defun eglot (managed-major-mode project command &optional interactive) +(defun eglot (managed-major-mode project contact &optional interactive) "Manage a project with a Language Server Protocol (LSP) server. The LSP server is started (or contacted) via COMMAND. If this @@ -253,7 +260,7 @@ code-analysis via `xref-find-definitions', `flymake-mode', `eldoc-mode', `completion-at-point', among others. Interactively, the command attempts to guess MANAGED-MAJOR-MODE -from current buffer, COMMAND from `eglot-server-programs' and +from current buffer, CONTACT from `eglot-server-programs' and PROJECT from `project-current'. If it can't guess, the user is prompted. With a single \\[universal-argument] prefix arg, it always prompt for COMMAND. With two \\[universal-argument] @@ -261,11 +268,14 @@ prefix args, also prompts for MANAGED-MAJOR-MODE. PROJECT is a project instance as returned by `project-current'. -COMMAND is a list of strings, an executable program and -optionally its arguments. If the first and only string in the -list is of the form \":\" it is taken as an -indication to connect to a server instead of starting one. This -is also know as the server's \"contact\". +CONTACT is a list of strings (COMMAND [ARGS...]) specifying how +to start a server subprocess to connect to. If the second +element in the list is an integer number instead of a string, the +list is interpreted as (HOST PORT [PARAMETERS...]) to connect to +an existing server via TCP, the remaining PARAMETERS being given +as `open-network-stream's optional arguments. CONTACT can also +be a function of no arguments returning a live connected process +object. MANAGED-MAJOR-MODE is an Emacs major mode. @@ -282,7 +292,7 @@ INTERACTIVE is t if called interactively." (let ((proc (eglot--connect project managed-major-mode (format "%s/%s" short-name managed-major-mode) - command))) + contact))) (eglot--message "Connected! Process `%s' now \ managing `%s' buffers in project `%s'." proc managed-major-mode short-name) @@ -310,13 +320,13 @@ Builds a function from METHOD, passes it PROC, ID and PARAMS." (let* ((handler-sym (intern (concat "eglot--server-" method)))) (if (functionp handler-sym) (apply handler-sym proc (append params (if id `(:id ,id)))) - (jrpc-reply - proc id + (jrpc-reply proc id :error (jrpc-obj :code -32601 :message "Unimplemented"))))) -(defun eglot--connect (project managed-major-mode name command) - (let ((proc (jrpc-connect name command #'eglot--dispatch #'eglot--on-shutdown)) - success) +(defun eglot--connect (project managed-major-mode name contact) + (let* ((contact (if (functionp contact) (funcall contact) contact)) + (proc (jrpc-connect name contact #'eglot--dispatch #'eglot--on-shutdown)) + success) (setf (eglot--project proc) project) (setf (eglot--major-mode proc)managed-major-mode) (push proc (gethash project eglot--processes-by-project)) @@ -350,7 +360,7 @@ Builds a function from METHOD, passes it PROC, ID and PARAMS." (null eglot-autoreconnect))))))) (setq success proc)) (unless (or success (not (process-live-p proc)) (eglot--moribund proc)) - (eglot-shutdown proc))))) + (eglot-shutdown proc))))) (defun eglot--server-ready-p (_what _proc) "Tell if server of PROC ready for processing deferred WHAT." commit d184bed317e57f11dc14359952218179cc0ffa5c Merge: 1deb7cf8ac 40e256a1bf Author: João Távora Date: Wed May 16 01:21:36 2018 +0100 Merge master into jsonrpc-refactor (using imerge) commit 40e256a1bf6d237eb5637fd726077332519338a6 Author: João Távora Date: Tue May 15 13:24:08 2018 +0100 Add php's php-language-server to built-in guessed servers Closes https://github.com/joaotavora/eglot/issues/1. The problem in that issue is that php-language-server has a bug when it's not passed it the deprecated ":rootPath" field. The bug doesn't happen if the ":processId" field is also absent. Eglot was triggering the bug, because it didn't pass ":rootPath", but there's nothing wrong in doing so. * README.md: Add php-language-server to the built-in list. * eglot.el (eglot-server-programs): Add php-language-server. (eglot--connect): Also pass (deprecated) rootPath. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4ff6204fc3..da3a09f3b2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -71,7 +71,9 @@ (defvar eglot-server-programs '((rust-mode . ("rls")) (python-mode . ("pyls")) (js-mode . ("javascript-typescript-stdio")) - (sh-mode . ("bash-language-server" "start"))) + (sh-mode . ("bash-language-server" "start")) + (php-mode . ("php" "vendor/felixfbecker/\ +language-server/bin/php-language-server.php"))) "Alist mapping major modes to server executables.") (defface eglot-mode-line @@ -276,6 +278,7 @@ INTERACTIVE is t if inside interactive call." 'network) (emacs-pid)) :capabilities(eglot--client-capabilities) + :rootPath (car (project-roots project)) :rootUri (eglot--path-to-uri (car (project-roots project))) :initializationOptions [])) commit 4ef2d1875cc2d1f8b7906ed0912176a4888436c4 Author: João Távora Date: Tue May 15 10:59:46 2018 +0100 Bump version and slightly improve doc * eglot.el: Bump version. Add nicer Commentary header. (eglot): Improve docstring. * README.md: Update diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 83f4efd143..4ff6204fc3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 0.1 +;; Version: 0.2 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot @@ -24,8 +24,28 @@ ;;; Commentary: -;; Simply M-x eglot should be enough to get you started, but see README.md. - +;; Simply M-x eglot should be enough to get you started, but here's a +;; little info (see the accompanying README.md or the URL for more). +;; +;; M-x eglot starts a server via a shell-command guessed from +;; `eglot-server-programs', using the current major-mode (for whatever +;; language you're programming in) as a hint. If it can't guess, it +;; prompts you in the mini-buffer for these things. Actually, the +;; server needen't be locally started: you can connect to a running +;; server via TCP by entering a syntax. +;; +;; Anyway, if the connection is successful, you should see an `eglot' +;; indicator pop up in your mode-line. More importantly, this means +;; current *and future* file buffers of that major mode *inside your +;; current project* automatically become \"managed\" by the LSP +;; server, i.e. information about their contents is exchanged +;; periodically to provide enhanced code analysis via +;; `xref-find-definitions', `flymake-mode', `eldoc-mode', +;; `completion-at-point', among others. +;; +;; To "unmanage" these buffers, shutdown the server with M-x +;; eglot-shutdown. +;; ;;; Code: (require 'json) @@ -314,9 +334,22 @@ INTERACTIVE is t if inside interactive call." ;;;###autoload (defun eglot (managed-major-mode project command &optional interactive) - "Start a Language Server Protocol server. -Server is started with COMMAND and manages buffers of -MANAGED-MAJOR-MODE for the current project. + "Manage a project with a Language Server Protocol (LSP) server. + +The LSP server is started (or contacted) via COMMAND. If this +operation is successful, current *and future* file buffers of +MANAGED-MAJOR-MODE inside PROJECT automatically become +\"managed\" by the LSP server, meaning information about their +contents is exchanged periodically to provide enhanced +code-analysis via `xref-find-definitions', `flymake-mode', +`eldoc-mode', `completion-at-point', among others. + +Interactively, the command attempts to guess MANAGED-MAJOR-MODE +from current buffer, COMMAND from `eglot-server-programs' and +PROJECT from `project-current'. If it can't guess, the user is +prompted. With a single \\[universal-argument] prefix arg, it +always prompt for COMMAND. With two \\[universal-argument] +prefix args, also prompts for MANAGED-MAJOR-MODE. PROJECT is a project instance as returned by `project-current'. @@ -328,12 +361,6 @@ is also know as the server's \"contact\". MANAGED-MAJOR-MODE is an Emacs major mode. -Interactively, guess MANAGED-MAJOR-MODE from current buffer and -COMMAND from `eglot-server-programs'. With a single -\\[universal-argument] prefix arg, prompt for COMMAND. With two -\\[universal-argument] prefix args, also prompt for -MANAGED-MAJOR-MODE. - INTERACTIVE is t if called interactively." (interactive (eglot--interactive)) (let* ((short-name (eglot--project-short-name project))) commit 11debd99a758647e0b04ba6f4417cdee4d31b631 Author: João Távora Date: Mon May 14 23:58:29 2018 +0100 More quietly report request timeouts as events * eglot.el (eglot--sync-request): Use eglot--log-event diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f87b4eaf12..83f4efd143 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -612,9 +612,9 @@ timeout keeps counting." (remhash id (eglot--pending-continuations proc)) (funcall (or timeout-fn (lambda () - (eglot--error - "Tired of waiting for reply to %s, id=%s" - method id)))))))))) + (eglot--log-event + proc `(:timed-out ,method :id id + :params ,params))))))))))) (when deferred (let* ((buf (current-buffer)) (existing (gethash (list deferred buf) (eglot--deferred-actions proc)))) commit 149bb814e642d367ffc2106ca1b1e88e90a38944 Author: João Távora Date: Mon May 14 23:09:27 2018 +0100 Shutdown server if connection initialization fails Also tweak autoreconnection logic * eglot.el (eglot--connect): Immediately `eglot-shutdown` if connection initialization failed. Don't treat interactive calls specially. (eglot--process-sentinel): Tweak messages. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index eb04c720b0..f87b4eaf12 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -224,7 +224,7 @@ CONTACT is as `eglot--contact'. Returns a process object." (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") -(defun eglot--connect (project managed-major-mode short-name contact interactive) +(defun eglot--connect (project managed-major-mode short-name contact _interactive) "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT. INTERACTIVE is t if inside interactive call." (let* ((proc (eglot--make-process short-name managed-major-mode contact)) @@ -233,11 +233,10 @@ INTERACTIVE is t if inside interactive call." (eglot--project proc) project (eglot--major-mode proc) managed-major-mode) (with-current-buffer buffer - (let ((inhibit-read-only t)) + (let ((inhibit-read-only t) success) (setf (eglot--inhibit-autoreconnect proc) (cond ((booleanp eglot-autoreconnect) (not eglot-autoreconnect)) - (interactive nil) ((cl-plusp eglot-autoreconnect) (run-with-timer eglot-autoreconnect nil (lambda () @@ -248,24 +247,27 @@ INTERACTIVE is t if inside interactive call." (run-hook-with-args 'eglot-connect-hook proc) (erase-buffer) (read-only-mode t) - (cl-destructuring-bind (&key capabilities) - (eglot--request - proc - :initialize - (eglot--obj :processId (unless (eq (process-type proc) - 'network) - (emacs-pid)) - :rootUri (eglot--path-to-uri - (car (project-roots project))) - :initializationOptions [] - :capabilities (eglot--client-capabilities))) - (setf (eglot--capabilities proc) capabilities) - (setf (eglot--status proc) nil) - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (eglot--maybe-activate-editing-mode proc))) - (eglot--notify proc :initialized (eglot--obj :__dummy__ t)) - proc))))) + (unwind-protect + (cl-destructuring-bind (&key capabilities) + (eglot--request + proc + :initialize + (eglot--obj :processId (unless (eq (process-type proc) + 'network) + (emacs-pid)) + :capabilities(eglot--client-capabilities) + :rootUri (eglot--path-to-uri + (car (project-roots project))) + :initializationOptions [])) + (setf (eglot--capabilities proc) capabilities) + (setf (eglot--status proc) nil) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (eglot--maybe-activate-editing-mode proc))) + (eglot--notify proc :initialized (eglot--obj :__dummy__ t)) + (setq success proc)) + (unless (or success (not (process-live-p proc)) (eglot--moribund proc)) + (eglot-shutdown proc))))))) (defvar eglot--command-history nil "History of COMMAND arguments to `eglot'.") @@ -398,10 +400,10 @@ INTERACTIVE is t if called interactively." (eglot--message "Server exited with status %s" (process-exit-status proc)) (cond ((eglot--moribund proc)) ((not (eglot--inhibit-autoreconnect proc)) - (eglot--warn "Reconnecting unexpected server exit.") + (eglot--warn "Reconnecting after unexpected server exit") (eglot-reconnect proc)) - (t - (eglot--warn "Not auto-reconnecting, last one didn't last long."))) + ((timerp (eglot--inhibit-autoreconnect proc)) + (eglot--warn "Not auto-reconnecting, last on didn't last long."))) (delete-process proc)))) (defun eglot--process-filter (proc string) commit 0d3e4ea1bdd9cf3e8d2368b897eb5ee6c07a3004 Author: João Távora Date: Mon May 14 22:19:23 2018 +0100 Fix a ridiculous bug when generating transient projects * eglot.el (eglot--find-current-process, eglot--interactive): Fix horrible bug. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 85b2d89a67..eb04c720b0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -79,7 +79,7 @@ lasted more than that many seconds." (defun eglot--current-process () "The current logical EGLOT process." - (let* ((probe (or (project-current) (cons 'transient default-directory)))) + (let* ((probe (or (project-current) `(transient . ,default-directory)))) (cl-find major-mode (gethash probe eglot--processes-by-project) :key #'eglot--major-mode))) @@ -300,7 +300,7 @@ INTERACTIVE is t if inside interactive call." "\n" base-prompt))))) (list managed-mode - (or (project-current) `(transient . default-directory)) + (or (project-current) `(transient . ,default-directory)) (if prompt (split-string-and-unquote (read-shell-command prompt commit 1deb7cf8acbe3280cb76578e2600d8e5de911aef Author: João Távora Date: Mon May 14 22:17:00 2018 +0100 Fix a ridiculous bug when generating transient projects * eglot.el (eglot--find-current-process, eglot--interactive): Fix horrible bug. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13aeff6956..11d048e03a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -136,7 +136,7 @@ called interactively." (defun eglot--find-current-process () "The current logical EGLOT process." - (let* ((probe (or (project-current) (cons 'transient default-directory)))) + (let* ((probe (or (project-current) `(transient . ,default-directory)))) (cl-find major-mode (gethash probe eglot--processes-by-project) :key #'eglot--major-mode))) @@ -208,7 +208,7 @@ called interactively." "\n" base-prompt))))) (list managed-mode - (or (project-current) `(transient . default-directory)) + (or (project-current) `(transient . ,default-directory)) (if prompt (split-string-and-unquote (read-shell-command prompt commit f529f554a38c1d6cfbf2ece60d18a4d0c0a21caa Author: João Távora Date: Mon May 14 21:49:58 2018 +0100 Jrpc-connect is now passed a generic dispatching function * eglot.el (eglot--dispatch): New helper. (eglot--connect): Use it. * jrpc.el (jrpc--dispatcher, jrpc--request-continuations) (jrpc--server-request-ids): New process-local var. (jrpc--pending-continuations, jrpc--method-prefix): Remove. (jrpc-connect): Take DISPATCHER instead of PREFIX. (jrpc--process-receive): Use proc's dispatcher. (jrpc--process-send): Make private. (jrpc-forget-pending-continuations, jrpc-async-request) (jrpc-reply, jrpc-notify): Use new function names. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 879972df1b..13aeff6956 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -277,9 +277,18 @@ INTERACTIVE is t if called interactively." (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") +(defun eglot--dispatch (proc method id &rest params) + ;; a server notification or a server request + (let* ((handler-sym (intern (concat "eglot--server-" method)))) + (if (functionp handler-sym) + (apply handler-sym proc (append params (if id `(:id ,id)))) + (jrpc-reply + proc id + :error (jrpc-obj :code -32601 :message "Unimplemented"))))) + (defun eglot--connect (project managed-major-mode name command dont-inhibit) - (let ((proc (jrpc-connect name command "eglot--server-" #'eglot--on-shutdown))) + (let ((proc (jrpc-connect name command #'eglot--dispatch #'eglot--on-shutdown))) (setf (eglot--project proc) project) (setf (eglot--major-mode proc)managed-major-mode) (push proc (gethash project eglot--processes-by-project)) commit 10a19cb11ba7a83d12626d7da2467bd2356982c3 Merge: 35dae7034b c511228cda Author: João Távora Date: Mon May 14 20:06:44 2018 +0100 Merge master into jsonrpc-refactor (using imerge) commit 35dae7034b96fe0f32bdd51377e9a2574adea374 Author: João Távora Date: Mon May 14 19:19:12 2018 +0100 Proper server shutdown when jrpc.el is used The shutdown hook can't be a buffer-local thing, it has to be a server property. Also, on shutdown in eglot.el, remember to first unmanage buffers and only then affect eglot--processes-by-project. * eglot.el (eglot--on-shutdown): reverse order of first two sexps. (eglot--connect): Pass a shutdown function to jrpc-connect (eglot--managed-mode): Don't use jrpc-server-moribund-hook (eglot--buffer-managed-p): Simplify. Use eglot--find-current-process. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 934270c43f..f33a851ece 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -95,13 +95,14 @@ A list (ID WHAT DONE-P).") (defun eglot--on-shutdown (proc) ;; Turn off `eglot--managed-mode' where appropriate. - (setf (gethash (eglot--project proc) eglot--processes-by-project) - (delq proc - (gethash (eglot--project proc) eglot--processes-by-project))) (dolist (buffer (buffer-list)) (with-current-buffer buffer (when (eglot--buffer-managed-p proc) (eglot--managed-mode -1)))) + ;; Sever the project/process relationship for proc + (setf (gethash (eglot--project proc) eglot--processes-by-project) + (delq proc + (gethash (eglot--project proc) eglot--processes-by-project))) (cond ((eglot--moribund proc)) ((not (eglot--inhibit-autoreconnect proc)) (eglot--warn "Reconnecting unexpected server exit.") @@ -267,7 +268,7 @@ INTERACTIVE is t if called interactively." (defun eglot--connect (project managed-major-mode name command dont-inhibit) - (let ((proc (jrpc-connect name command "eglot--server-"))) + (let ((proc (jrpc-connect name command "eglot--server-" #'eglot--on-shutdown))) (setf (eglot--project proc) project) (setf (eglot--major-mode proc)managed-major-mode) (push proc (gethash project eglot--processes-by-project)) @@ -326,11 +327,11 @@ INTERACTIVE is t if called interactively." "Convert point POS to LSP position." (save-excursion (jrpc-obj :line - ;; F!@(#*&#$)CKING OFF-BY-ONE - (1- (line-number-at-pos pos t)) - :character - (- (goto-char (or pos (point))) - (line-beginning-position))))) + ;; F!@(#*&#$)CKING OFF-BY-ONE + (1- (line-number-at-pos pos t)) + :character + (- (goto-char (or pos (point))) + (line-beginning-position))))) (defun eglot--lsp-position-to-point (pos-plist) "Convert LSP position POS-PLIST to Emacs point." @@ -401,7 +402,6 @@ INTERACTIVE is t if called interactively." (eglot--managed-mode (add-hook 'jrpc-find-process-functions 'eglot--find-current-process nil t) (add-hook 'jrpc-ready-predicates 'eglot--server-ready-p nil t) - (add-hook 'jrpc-server-moribund-hook 'eglot--on-shutdown nil t) (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'before-change-functions 'eglot--before-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) @@ -417,7 +417,6 @@ INTERACTIVE is t if called interactively." (t (remove-hook 'jrpc-find-process-functions 'eglot--find-current-process t) (remove-hook 'jrpc-ready-predicates 'eglot--server-ready-p t) - (remove-hook 'jrpc-server-moribund-hook 'eglot--on-shutdown t) (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) (remove-hook 'before-change-functions 'eglot--before-change t) @@ -439,11 +438,9 @@ INTERACTIVE is t if called interactively." (defun eglot--buffer-managed-p (&optional proc) "Tell if current buffer can be managed by PROC." - (and buffer-file-name - (cond ((null proc) (jrpc-current-process)) - (t (and (eq major-mode (eglot--major-mode proc)) - (let ((proj (project-current))) - (and proj (equal proj (eglot--project proc))))))))) + (and buffer-file-name (let ((cur (eglot--find-current-process))) + (or (and (null proc) cur) + (and proc (eq proc cur)))))) (defvar-local eglot--current-flymake-report-fn nil "Current flymake report function for this buffer") @@ -585,12 +582,12 @@ Uses THING, FACE, DEFS and PREPEND." _code source message) diag-spec (eglot--with-lsp-range (beg end) range - (flymake-make-diagnostic (current-buffer) - beg end - (cond ((<= severity 1) :error) - ((= severity 2) :warning) - (t :note)) - (concat source ": " message)))) + (flymake-make-diagnostic (current-buffer) + beg end + (cond ((<= severity 1) :error) + ((= severity 2) :warning) + (t :note)) + (concat source ": " message)))) into diags finally (cond (eglot--current-flymake-report-fn (funcall eglot--current-flymake-report-fn diags) @@ -657,18 +654,18 @@ Uses THING, FACE, DEFS and PREPEND." (append (eglot--VersionedTextDocumentIdentifier) (jrpc-obj :languageId - (if (string-match "\\(.*\\)-mode" (symbol-name major-mode)) - (match-string 1 (symbol-name major-mode)) - "unknown") - :text - (save-restriction - (widen) - (buffer-substring-no-properties (point-min) (point-max)))))) + (if (string-match "\\(.*\\)-mode" (symbol-name major-mode)) + (match-string 1 (symbol-name major-mode)) + "unknown") + :text + (save-restriction + (widen) + (buffer-substring-no-properties (point-min) (point-max)))))) (defun eglot--TextDocumentPositionParams () "Compute TextDocumentPositionParams." (jrpc-obj :textDocument (eglot--TextDocumentIdentifier) - :position (eglot--pos-to-lsp-position))) + :position (eglot--pos-to-lsp-position))) (defvar-local eglot--recent-changes nil "Recent buffer changes as collected by `eglot--before-change'.") @@ -931,7 +928,7 @@ DUMMY is ignored" (defun eglot--hover-info (contents &optional range) (concat (and range (eglot--with-lsp-range (beg end) range - (concat (buffer-substring beg end) ": "))) + (concat (buffer-substring beg end) ": "))) (mapconcat #'eglot--format-markup (append (cond ((vectorp contents) commit c511228cdaedbde8136847dbfe576ad1473d9aed Author: João Távora Date: Mon May 14 14:18:18 2018 +0100 Support didchangewatchedfiles with dynamic registration RLS uses this, presumaly for knowing about Cargo.toml changes and stuff. * README.md: Update protocol compliance. * eglot.el (filenotify): Require it. (eglot--file-watches): New process-local var. (eglot--process-sentinel): Kill all watches (eglot--register-unregister): New helper. (eglot--server-client/registerCapability): Simplify. (eglot--server-client/unregisterCapability): New method. (eglot--register-workspace/didChangeWatchedFiles) (eglot--unregister-workspace/didChangeWatchedFiles): New capability. (eglot--client-capabilities): Update. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9b0f290af5..85b2d89a67 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -39,6 +39,7 @@ (require 'flymake) (require 'xref) (require 'subr-x) +(require 'filenotify) ;;; User tweakable stuff @@ -148,6 +149,9 @@ list of a single string of the form :") (make-hash-table :test #'equal) "Actions deferred to when server is thought to be ready.") +(eglot--define-process-var eglot--file-watches (make-hash-table :test #'equal) + "File system watches for the didChangeWatchedfiles thingy.") + (defun eglot--make-process (name managed-major-mode contact) "Make a process from CONTACT. NAME is a name to give the inferior process or connection. @@ -199,6 +203,9 @@ CONTACT is as `eglot--contact'. Returns a process object." "What the EGLOT LSP client supports." (eglot--obj :workspace (eglot--obj + :applyEdit t + :workspaceEdit `(:documentChanges :json-false) + :didChangeWatchesFiles `(:dynamicRegistration t) :symbol `(:dynamicRegistration :json-false)) :textDocument (eglot--obj :synchronization (eglot--obj @@ -365,11 +372,14 @@ INTERACTIVE is t if called interactively." (with-current-buffer (eglot-events-buffer proc) (let ((inhibit-read-only t)) (insert "\n----------b---y---e---b---y---e----------\n"))) - ;; Cancel outstanding timers + ;; Cancel outstanding timers and file system watches (maphash (lambda (_id triplet) (cl-destructuring-bind (_success _error timeout) triplet (cancel-timer timeout))) (eglot--pending-continuations proc)) + (maphash (lambda (_id watches) + (mapcar #'file-notify-rm-watch watches)) + (eglot--file-watches proc)) (unwind-protect ;; Call all outstanding error handlers (maphash (lambda (_id triplet) @@ -990,32 +1000,31 @@ called interactively." (t (eglot--message "OK so %s isn't visited" filename))))) +(cl-defun eglot--register-unregister (proc jsonrpc-id things how) + "Helper for `eglot--server-client/registerCapability'. +THINGS are either registrations or unregisterations." + (dolist (thing (cl-coerce things 'list)) + (cl-destructuring-bind (&key id method registerOptions) thing + (let (retval) + (unwind-protect + (setq retval (apply (intern (format "eglot--%s-%s" how method)) + proc :id id registerOptions)) + (unless (eq t (car retval)) + (cl-return-from eglot--register-unregister + (eglot--reply + proc jsonrpc-id + :error `(:code -32601 :message ,(or (cadr retval) "sorry"))))))))) + (eglot--reply proc jsonrpc-id :result (eglot--obj :message "OK"))) + (cl-defun eglot--server-client/registerCapability (proc &key id registrations) - "Handle notification client/registerCapability" - (let ((jsonrpc-id id) - (done (make-symbol "done"))) - (catch done - (mapc - (lambda (reg) - (apply - (cl-function - (lambda (&key id method registerOptions) - (pcase-let* - ((handler-sym (intern (concat "eglot--register-" - method))) - (`(,ok ,message) - (and (functionp handler-sym) - (apply handler-sym proc :id id registerOptions)))) - (unless ok - (throw done - (eglot--reply proc jsonrpc-id - :error (eglot--obj - :code -32601 - :message (or message "sorry :-(")))))))) - reg)) - registrations) - (eglot--reply proc id :result (eglot--obj :message "OK"))))) + "Handle server request client/registerCapability" + (eglot--register-unregister proc id registrations 'register)) + +(cl-defun eglot--server-client/unregisterCapability + (proc &key id unregisterations) ;; XXX: Yeah, typo and all.. See spec... + "Handle server request client/unregisterCapability" + (eglot--register-unregister proc id unregisterations 'unregister)) (cl-defun eglot--server-workspace/applyEdit (proc &key id _label edit) @@ -1489,12 +1498,45 @@ Proceed? " ;;; Dynamic registration ;;; -(cl-defun eglot--register-workspace/didChangeWatchedFiles - (_proc &key _id _watchers) +(cl-defun eglot--register-workspace/didChangeWatchedFiles (proc &key id watchers) "Handle dynamic registration of workspace/didChangeWatchedFiles" - ;; TODO: file-notify-add-watch and - ;; file-notify-rm-watch can probably handle this - (list nil "Sorry, can't do this yet")) + (eglot--unregister-workspace/didChangeWatchedFiles proc :id id) + (let* (success + (globs (mapcar (lambda (w) (plist-get w :globPattern)) watchers))) + (cl-labels + ((handle-event + (event) + (cl-destructuring-bind (desc action file &optional file1) event + (cond + ((and (memq action '(created changed deleted)) + (cl-find file globs + :test (lambda (f glob) + (string-match (wildcard-to-regexp + (expand-file-name glob)) + f)))) + (eglot--notify + proc :workspace/didChangeWatchedFiles + `(:changes ,(vector `(:uri ,(eglot--path-to-uri file) + :type ,(cl-case action + (created 1) + (changed 2) + (deleted 3))))))) + ((eq action 'renamed) + (handle-event desc 'deleted file) + (handle-event desc 'created file1)))))) + (unwind-protect + (progn (dolist (dir (delete-dups (mapcar #'file-name-directory globs))) + (push (file-notify-add-watch dir '(change) #'handle-event) + (gethash id (eglot--file-watches proc)))) + (setq success `(t "OK"))) + (unless success + (eglot--unregister-workspace/didChangeWatchedFiles proc :id id)))))) + +(cl-defun eglot--unregister-workspace/didChangeWatchedFiles (proc &key id) + "Handle dynamic unregistration of workspace/didChangeWatchedFiles" + (mapc #'file-notify-rm-watch (gethash id (eglot--file-watches proc))) + (remhash id (eglot--file-watches proc)) + (list t "OK")) ;;; Rust-specific commit d0e32ae98cd3680cf0b91e3f37a53a8e4fdea843 Author: João Távora Date: Mon May 14 11:41:46 2018 +0100 Remove an unused variable * eglot.el (eglot--expect-carriage-return): Get rid of this. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index eab7ce7111..9b0f290af5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -532,8 +532,6 @@ is a symbol saying if this is a client or server originated." (eglot--call-deferred proc) (force-mode-line-update t)))) -(defvar eglot--expect-carriage-return nil) - (defun eglot--process-send (proc message) "Send MESSAGE to PROC (ID is optional)." (let ((json (json-encode message))) @@ -542,7 +540,7 @@ is a symbol saying if this is a client or server originated." json)) (eglot--log-event proc message 'client))) -(defvar eglot--next-request-id 0) +(defvar eglot--next-request-id 0 "ID for next request.") (defun eglot--next-request-id () "Compute the next id for a client request." commit 92efbb8dac5b4443445ecc16ea3455aa609ded82 Author: João Távora Date: Mon May 14 11:20:37 2018 +0100 Now send willsavewaituntil * eglot.el (eglot--client-capabilities): Report willSaveWaitUntil. (eglot--server-workspace/applyEdit): Fix docstring. (eglot--signal-textDocument/willSave): Send willSaveWaitUntil (eglot--signal-textDocument/didOpen) (eglot--signal-textDocument/didClose): Don't eglot--obj. (eglot--apply-text-edits): Simplify. Use current buffer. (eglot--apply-workspace-edit): Use new eglot--apply-text-edits. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6dc2213555..eab7ce7111 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -203,9 +203,7 @@ CONTACT is as `eglot--contact'. Returns a process object." :textDocument (eglot--obj :synchronization (eglot--obj :dynamicRegistration :json-false - :willSave t - :willSaveWaitUntil :json-false - :didSave t) + :willSave t :willSaveWaitUntil t :didSave t) :completion `(:dynamicRegistration :json-false) :hover `(:dynamicRegistration :json-false) :signatureHelp `(:dynamicRegistration :json-false) @@ -1023,7 +1021,7 @@ called interactively." (cl-defun eglot--server-workspace/applyEdit (proc &key id _label edit) - "Handle notification client/registerCapability" + "Handle server request workspace/applyEdit" (condition-case err (progn (eglot--apply-workspace-edit edit 'confirm) @@ -1127,26 +1125,27 @@ Records START, END and PRE-CHANGE-LENGTH locally." (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." (setq eglot--recent-changes (cons [] [])) - (eglot--notify (eglot--current-process-or-lose) - :textDocument/didOpen - (eglot--obj :textDocument - (eglot--TextDocumentItem)))) + (eglot--notify + (eglot--current-process-or-lose) + :textDocument/didOpen `(:textDocument ,(eglot--TextDocumentItem)))) (defun eglot--signal-textDocument/didClose () "Send textDocument/didClose to server." - (eglot--notify (eglot--current-process-or-lose) - :textDocument/didClose - (eglot--obj :textDocument - (eglot--TextDocumentIdentifier)))) + (eglot--notify + (eglot--current-process-or-lose) + :textDocument/didClose `(:textDocument ,(eglot--TextDocumentIdentifier)))) (defun eglot--signal-textDocument/willSave () "Send textDocument/willSave to server." - (eglot--notify - (eglot--current-process-or-lose) - :textDocument/willSave - (eglot--obj - :reason 1 ; Manual, emacs laughs in the face of auto-save muahahahaha - :textDocument (eglot--TextDocumentIdentifier)))) + (let ((proc (eglot--current-process-or-lose)) + (params `(:reason 1 :textDocument ,(eglot--TextDocumentIdentifier)))) + (eglot--notify proc :textDocument/willSave params) + (ignore-errors + (let ((eglot-request-timeout 0.5)) + (when (plist-get :willSaveWaitUntil + (eglot--server-capable :textDocumentSync)) + (eglot--apply-text-edits + (eglot--request proc :textDocument/willSaveWaituntil params))))))) (defun eglot--signal-textDocument/didSave () "Send textDocument/didSave to server." @@ -1426,22 +1425,20 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." entries)) (funcall oldfun))) -(defun eglot--apply-text-edits (buffer edits &optional version) - "Apply the EDITS for BUFFER." - (with-current-buffer buffer - (unless (or (not version) - (equal version eglot--versioned-identifier)) - (eglot--error "Edits on `%s' require version %d, you have %d" - buffer version eglot--versioned-identifier)) - (eglot--mapply - (eglot--lambda (&key range newText) - (save-restriction - (widen) - (save-excursion - (eglot--with-lsp-range (beg end) range - (goto-char beg) (delete-region beg end) (insert newText))))) - edits) - (eglot--message "%s: Performed %s edits" (current-buffer) (length edits)))) +(defun eglot--apply-text-edits (edits &optional version) + "Apply EDITS for current buffer if at VERSION, or if it's nil." + (unless (or (not version) (equal version eglot--versioned-identifier)) + (eglot--error "Edits on `%s' require version %d, you have %d" + (current-buffer) version eglot--versioned-identifier)) + (eglot--mapply + (eglot--lambda (&key range newText) + (save-restriction + (widen) + (save-excursion + (eglot--with-lsp-range (beg end) range + (goto-char beg) (delete-region beg end) (insert newText))))) + edits) + (eglot--message "%s: Performed %s edits" (current-buffer) (length edits))) (defun eglot--apply-workspace-edit (wedit &optional confirm) "Apply the workspace edit WEDIT. If CONFIRM, ask user first." @@ -1471,9 +1468,8 @@ Proceed? " (let (edit) (while (setq edit (car prepared)) (cl-destructuring-bind (path edits &optional version) edit - (eglot--apply-text-edits (find-file-noselect path) - edits - version) + (with-current-buffer (find-file-noselect path) + (eglot--apply-text-edits edits version)) (pop prepared)))) (if prepared (eglot--warn "Caution: edits of files %s failed." commit 188cd6da28f271144fb88b9280532f18aec0b3dd Author: João Távora Date: Mon May 14 10:50:49 2018 +0100 Don't define a menu if nothing to show there for now * eglot.el (eglot-menu): Remove it. (eglot--mode-line-format): Don't define a menu. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 447724283c..6dc2213555 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -843,10 +843,6 @@ that case, also signal textDocument/didOpen." ;;; Mode-line, menu and other sugar ;;; -(defvar eglot-menu) - -(easy-menu-define eglot-menu eglot-mode-map "EGLOT" `("EGLOT" )) - (defvar eglot--mode-line-format `(:eval (eglot--mode-line-format))) (put 'eglot--mode-line-format 'risky-local-variable t) @@ -881,8 +877,7 @@ Uses THING, FACE, DEFS and PREPEND." (`(,_id ,doing ,done-p ,detail) (and proc (eglot--spinner proc))) (`(,status ,serious-p) (and proc (eglot--status proc)))) (append - `(,(eglot--mode-line-props "eglot" 'eglot-mode-line - '((down-mouse-1 eglot-menu "pop up EGLOT menu")))) + `(,(eglot--mode-line-props "eglot" 'eglot-mode-line nil)) (when name `(":" ,(eglot--mode-line-props name 'eglot-mode-line commit 33583c642a3ff08d94c2226fa4df7b2204a1e3ae Author: João Távora Date: Sun May 13 23:25:15 2018 +0100 Use rls in travis ci and add actual tests Also run a hook when connected * eglot-tests.el (eglot--with-dirs-and-files) (eglot--make-file-or-dirs, eglot--call-with-dirs-and-files) (eglot--find-file-noselect): New helpers. (auto-detect-running-server, auto-reconnect): New actual tests. * eglot.el (eglot-connect): Run hook when connected (eglot-connect-hook): New variable * .travis.yml: Use rust stable and install rls * README.md: Update mention of automated tests diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3d5d4927ec..447724283c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -217,6 +217,8 @@ CONTACT is as `eglot--contact'. Returns a process object." :publishDiagnostics `(:relatedInformation :json-false)) :experimental (eglot--obj))) +(defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") + (defun eglot--connect (project managed-major-mode short-name contact interactive) "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT. INTERACTIVE is t if inside interactive call." @@ -238,6 +240,7 @@ INTERACTIVE is t if inside interactive call." (null eglot-autoreconnect))))))) (setf (eglot--short-name proc) short-name) (push proc (gethash project eglot--processes-by-project)) + (run-hook-with-args 'eglot-connect-hook proc) (erase-buffer) (read-only-mode t) (cl-destructuring-bind (&key capabilities) commit 9d0984c0cde87e71cea4729f6e6ca63bfbfa5814 Author: João Távora Date: Sun May 13 23:22:31 2018 +0100 Fix automatic project creation * eglot.el (eglot): Take PROJECT arg. Return process. (eglot--interactive): Returns a project. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c1b63ea6e3..3d5d4927ec 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -292,6 +292,7 @@ INTERACTIVE is t if inside interactive call." "\n" base-prompt))))) (list managed-mode + (or (project-current) `(transient . default-directory)) (if prompt (split-string-and-unquote (read-shell-command prompt @@ -302,11 +303,13 @@ INTERACTIVE is t if inside interactive call." t))) ;;;###autoload -(defun eglot (managed-major-mode command &optional interactive) +(defun eglot (managed-major-mode project command &optional interactive) "Start a Language Server Protocol server. Server is started with COMMAND and manages buffers of MANAGED-MAJOR-MODE for the current project. +PROJECT is a project instance as returned by `project-current'. + COMMAND is a list of strings, an executable program and optionally its arguments. If the first and only string in the list is of the form \":\" it is taken as an @@ -323,8 +326,7 @@ MANAGED-MAJOR-MODE. INTERACTIVE is t if called interactively." (interactive (eglot--interactive)) - (let* ((project (project-current 'maybe)) - (short-name (eglot--project-short-name project))) + (let* ((short-name (eglot--project-short-name project))) (let ((current-process (eglot--current-process))) (if (and (process-live-p current-process) interactive @@ -339,7 +341,8 @@ INTERACTIVE is t if called interactively." interactive))) (eglot--message "Connected! Process `%s' now \ managing `%s' buffers in project `%s'." - proc managed-major-mode short-name)))))) + proc managed-major-mode short-name) + proc))))) (defun eglot-reconnect (process &optional interactive) "Reconnect to PROCESS. commit bf1365c4f8fcb989bb6907c2cec57edab22cef60 Author: João Távora Date: Sun May 13 22:03:32 2018 +0100 Work with any old directory, no formal project needed Actually, uses a "transient project" which project-current returns if desperate. * README.md: Update * eglot.el (eglot--current-process) (eglot--current-process-or-lose): Simplify. (eglot): Maybe prompt user for project. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2e872dc523..c1b63ea6e3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -24,8 +24,7 @@ ;;; Commentary: -;; M-x eglot in some file under some .git controlled dir should get -;; you started, but see README.md. +;; Simply M-x eglot should be enough to get you started, but see README.md. ;;; Code: @@ -79,15 +78,13 @@ lasted more than that many seconds." (defun eglot--current-process () "The current logical EGLOT process." - (let* ((cur (project-current)) - (processes (and cur (gethash cur eglot--processes-by-project)))) - (cl-find major-mode processes :key #'eglot--major-mode))) + (let* ((probe (or (project-current) (cons 'transient default-directory)))) + (cl-find major-mode (gethash probe eglot--processes-by-project) + :key #'eglot--major-mode))) (defun eglot--current-process-or-lose () "Return the current EGLOT process or error." - (or (eglot--current-process) - (eglot--error "No current EGLOT process%s" - (if (project-current) "" " (Also no current project)")))) + (or (eglot--current-process) (eglot--error "No current EGLOT process"))) (defmacro eglot--define-process-var (var-sym initval &optional doc) @@ -326,11 +323,8 @@ MANAGED-MAJOR-MODE. INTERACTIVE is t if called interactively." (interactive (eglot--interactive)) - (let* ((project (project-current)) + (let* ((project (project-current 'maybe)) (short-name (eglot--project-short-name project))) - (unless project (eglot--error "Cannot work without a current project!")) - (unless command (eglot--error "Don't know how to start EGLOT for %s buffers" - major-mode)) (let ((current-process (eglot--current-process))) (if (and (process-live-p current-process) interactive commit c8bed8412292ac108c4a5f6ac5991e30bf643e4a Author: João Távora Date: Sun May 13 20:35:45 2018 +0100 Ask server for textdocument/signaturehelp if it supports it * eglot.el (eglot--client-capabilities): Capable of signature Help. (eglot--sig-info): Helper for eglot-eldoc-function. (eglot-eldoc-function): Send textDocument/signatureHelp * README.md: Update to mention textDocument/signatureHelp diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f6eabe1686..2e872dc523 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -211,6 +211,7 @@ CONTACT is as `eglot--contact'. Returns a process object." :didSave t) :completion `(:dynamicRegistration :json-false) :hover `(:dynamicRegistration :json-false) + :signatureHelp `(:dynamicRegistration :json-false) :references `(:dynamicRegistration :json-false) :definition `(:dynamicRegistration :json-false) :documentSymbol `(:dynamicRegistration :json-false) @@ -1330,6 +1331,28 @@ DUMMY is ignored" (contents (list contents)))) "\n"))) +(defun eglot--sig-info (sigs active-sig active-param) + (cl-loop + for (sig . moresigs) on (append sigs nil) for i from 0 + concat (cl-destructuring-bind (&key label _documentation parameters) sig + (let (active-doc) + (concat + (propertize (replace-regexp-in-string "(.*$" "(" label) + 'face 'font-lock-function-name-face) + (cl-loop + for (param . moreparams) on (append parameters nil) for j from 0 + concat (cl-destructuring-bind (&key label documentation) param + (when (and (eql j active-param) (eql i active-sig)) + (setq label (propertize + label + 'face 'eldoc-highlight-function-argument)) + (when documentation + (setq active-doc (concat label ": " documentation)))) + label) + if moreparams concat ", " else concat ")") + (when active-doc (concat "\n" active-doc))))) + when moresigs concat "\n")) + (defun eglot-help-at-point () "Request \"hover\" information for the thing at point." (interactive) @@ -1342,35 +1365,51 @@ DUMMY is ignored" (insert (eglot--hover-info contents range)))))) (defun eglot-eldoc-function () - "EGLOT's `eldoc-documentation-function' function." - (let ((buffer (current-buffer)) - (proc (eglot--current-process-or-lose)) - (position-params (eglot--TextDocumentPositionParams))) - (when (eglot--server-capable :hoverProvider) - (eglot--async-request - proc :textDocument/hover position-params - :success-fn (eglot--lambda (&key contents range) - (when (get-buffer-window buffer) - (with-current-buffer buffer - (eldoc-message (eglot--hover-info contents range))))) - :deferred :textDocument/hover)) - (when (eglot--server-capable :documentHighlightProvider) - (eglot--async-request - proc :textDocument/documentHighlight position-params - :success-fn (lambda (highlights) - (mapc #'delete-overlay eglot--highlights) - (setq eglot--highlights - (when (get-buffer-window buffer) - (with-current-buffer buffer - (eglot--mapply - (eglot--lambda (&key range _kind) - (eglot--with-lsp-range (beg end) range - (let ((ov (make-overlay beg end))) - (overlay-put ov 'face 'highlight) - (overlay-put ov 'evaporate t) - ov))) - highlights))))) - :deferred :textDocument/documentHighlight))) + "EGLOT's `eldoc-documentation-function' function. +If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." + (let* ((buffer (current-buffer)) + (proc (eglot--current-process-or-lose)) + (position-params (eglot--TextDocumentPositionParams)) + sig-showing) + (cl-macrolet ((when-buffer-window + (&body body) `(when (get-buffer-window buffer) + (with-current-buffer buffer ,@body)))) + (when (eglot--server-capable :signatureHelpProvider) + (eglot--async-request + proc :textDocument/signatureHelp position-params + :success-fn (eglot--lambda (&key signatures activeSignature + activeParameter) + (when-buffer-window + (when (cl-plusp (length signatures)) + (setq sig-showing t) + (eldoc-message (eglot--sig-info signatures + activeSignature + activeParameter))))) + :deferred :textDocument/signatureHelp)) + (when (eglot--server-capable :hoverProvider) + (eglot--async-request + proc :textDocument/hover position-params + :success-fn (eglot--lambda (&key contents range) + (unless sig-showing + (when-buffer-window + (eldoc-message (eglot--hover-info contents range))))) + :deferred :textDocument/hover)) + (when (eglot--server-capable :documentHighlightProvider) + (eglot--async-request + proc :textDocument/documentHighlight position-params + :success-fn (lambda (highlights) + (mapc #'delete-overlay eglot--highlights) + (setq eglot--highlights + (when-buffer-window + (eglot--mapply + (eglot--lambda (&key range _kind) + (eglot--with-lsp-range (beg end) range + (let ((ov (make-overlay beg end))) + (overlay-put ov 'face 'highlight) + (overlay-put ov 'evaporate t) + ov))) + highlights)))) + :deferred :textDocument/documentHighlight)))) nil) (defun eglot-imenu (oldfun) commit dd4d81696ebcc531c8f59242537993b36654501f Author: João Távora Date: Sat May 12 22:05:20 2018 +0100 Fix copyright header. obviously not since 2003 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6f9034942d..f6eabe1686 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1,6 +1,6 @@ ;;; eglot.el --- Client for Language Server Protocol (LSP) servers -*- lexical-binding: t; -*- -;; Copyright (C) 2003-2018 Free Software Foundation, Inc. +;; Copyright (C) 2018 Free Software Foundation, Inc. ;; Version: 0.1 ;; Author: João Távora commit 0804d7da83edc68809b001032480eac8a0cde9cb Author: João Távora Date: Sun May 13 03:09:03 2018 +0100 Refactor json-rpc lib jrpc.el from eglot.el * eglot.el [too many to mention]: Move lower level functions to jrpc.el. Hook onto jrpc's external interfaces. * jrpc.el: New file diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4cb3bec50e..934270c43f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -40,6 +40,7 @@ (require 'flymake) (require 'xref) (require 'subr-x) +(require 'jrpc) ;;; User tweakable stuff @@ -58,12 +59,8 @@ '((t (:inherit font-lock-constant-face :weight bold))) "Face for package-name in EGLOT's mode line.") -(defcustom eglot-request-timeout 10 - "How many seconds to wait for a reply from the server." - :type :integer) - (defcustom eglot-autoreconnect 3 - "Control EGLOT's ability to reconnect automatically. + "Control ability to reconnect automatically to the LSP server. If t, always reconnect automatically (not recommended). If nil, never reconnect automatically after unexpected server shutdowns, crashes or network failures. A positive integer number says to @@ -77,114 +74,63 @@ lasted more than that many seconds." (defvar eglot--processes-by-project (make-hash-table :test #'equal) "Keys are projects. Values are lists of processes.") -(defun eglot--current-process () - "The current logical EGLOT process." - (let* ((cur (project-current)) - (processes (and cur (gethash cur eglot--processes-by-project)))) - (cl-find major-mode processes :key #'eglot--major-mode))) - -(defun eglot--current-process-or-lose () - "Return the current EGLOT process or error." - (or (eglot--current-process) - (eglot--error "No current EGLOT process%s" - (if (project-current) "" " (Also no current project)")))) - -(defmacro eglot--define-process-var - (var-sym initval &optional doc) - "Define VAR-SYM as a generalized process-local variable. -INITVAL is the default value. DOC is the documentation." - (declare (indent 2)) - `(progn - (put ',var-sym 'function-documentation ,doc) - (defun ,var-sym (proc) - (let* ((plist (process-plist proc)) - (probe (plist-member plist ',var-sym))) - (if probe - (cadr probe) - (let ((def ,initval)) - (process-put proc ',var-sym def) - def)))) - (gv-define-setter ,var-sym (to-store process) - `(let ((once ,to-store)) (process-put ,process ',',var-sym once) once)))) - -(eglot--define-process-var eglot--short-name nil - "A short name for the process") - -(eglot--define-process-var eglot--major-mode nil +(jrpc-define-process-var eglot--major-mode nil "The major-mode this server is managing.") -(eglot--define-process-var eglot--expected-bytes nil - "How many bytes declared by server") - -(eglot--define-process-var eglot--pending-continuations (make-hash-table) - "A hash table of request ID to continuation lambdas") - -(eglot--define-process-var eglot--events-buffer nil - "A buffer pretty-printing the EGLOT RPC events") - -(eglot--define-process-var eglot--capabilities :unreported +(jrpc-define-process-var eglot--capabilities :unreported "Holds list of capabilities that server reported") -(eglot--define-process-var eglot--moribund nil - "Non-nil if server is about to exit") - -(eglot--define-process-var eglot--project nil +(jrpc-define-process-var eglot--project nil "The project the server belongs to.") -(eglot--define-process-var eglot--spinner `(nil nil t) +(jrpc-define-process-var eglot--spinner `(nil nil t) "\"Spinner\" used by some servers. A list (ID WHAT DONE-P).") -(eglot--define-process-var eglot--status `(:unknown nil) - "Status as declared by the server. -A list (WHAT SERIOUS-P).") +(jrpc-define-process-var eglot--moribund nil + "Non-nil if server is about to exit") -(eglot--define-process-var eglot--inhibit-autoreconnect eglot-autoreconnect +(jrpc-define-process-var eglot--inhibit-autoreconnect eglot-autoreconnect "If non-nil, don't autoreconnect on unexpected quit.") -(eglot--define-process-var eglot--contact nil - "Method used to contact a server. -Either a list of strings (a shell command and arguments), or a -list of a single string of the form :") - -(eglot--define-process-var eglot--deferred-actions - (make-hash-table :test #'equal) - "Actions deferred to when server is thought to be ready.") - -(defun eglot--make-process (name managed-major-mode contact) - "Make a process from CONTACT. -NAME is a name to give the inferior process or connection. -MANAGED-MAJOR-MODE is a symbol naming a major mode. -CONTACT is as `eglot--contact'. Returns a process object." - (let* ((readable-name (format "EGLOT server (%s/%s)" name managed-major-mode)) - (buffer (get-buffer-create - (format "*%s inferior*" readable-name))) - singleton - (proc - (if (and (setq singleton (and (null (cdr contact)) (car contact))) - (string-match "^[\s\t]*\\(.*\\):\\([[:digit:]]+\\)[\s\t]*$" - singleton)) - (open-network-stream readable-name - buffer - (match-string 1 singleton) - (string-to-number - (match-string 2 singleton))) - (make-process :name readable-name - :buffer buffer - :command contact - :connection-type 'pipe - :stderr (get-buffer-create (format "*%s stderr*" - name)))))) - (set-process-filter proc #'eglot--process-filter) - (set-process-sentinel proc #'eglot--process-sentinel) - proc)) - -(defmacro eglot--obj (&rest what) - "Make WHAT a suitable argument for `json-encode'." - (declare (debug (&rest form))) - ;; FIXME: maybe later actually do something, for now this just fixes - ;; the indenting of literal plists. - `(list ,@what)) +(defun eglot--on-shutdown (proc) + ;; Turn off `eglot--managed-mode' where appropriate. + (setf (gethash (eglot--project proc) eglot--processes-by-project) + (delq proc + (gethash (eglot--project proc) eglot--processes-by-project))) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (when (eglot--buffer-managed-p proc) + (eglot--managed-mode -1)))) + (cond ((eglot--moribund proc)) + ((not (eglot--inhibit-autoreconnect proc)) + (eglot--warn "Reconnecting unexpected server exit.") + (eglot-reconnect proc)) + (t + (eglot--warn "Not auto-reconnecting, last one didn't last long.")))) + +(defun eglot-shutdown (proc &optional interactive) + "Politely ask the server PROC to quit. +Forcefully quit it if it doesn't respond. Don't leave this +function with the server still running. INTERACTIVE is t if +called interactively." + (interactive (list (jrpc-current-process-or-lose) t)) + (when interactive (eglot--message "Asking %s politely to terminate" proc)) + (unwind-protect + (let ((jrpc-request-timeout 3)) + (setf (eglot--moribund proc) t) + (jrpc-request proc :shutdown nil) + ;; this one should always fail under normal conditions + (ignore-errors (jrpc-request proc :exit nil))) + (when (process-live-p proc) + (eglot--warn "Brutally deleting existing process %s" proc) + (delete-process proc)))) + +(defun eglot--find-current-process () + "The current logical EGLOT process." + (let* ((cur (project-current)) + (processes (and cur (gethash cur eglot--processes-by-project)))) + (cl-find major-mode processes :key #'eglot--major-mode))) (defun eglot--project-short-name (project) "Give PROJECT a short name." @@ -200,11 +146,11 @@ CONTACT is as `eglot--contact'. Returns a process object." (defun eglot--client-capabilities () "What the EGLOT LSP client supports." - (eglot--obj - :workspace (eglot--obj + (jrpc-obj + :workspace (jrpc-obj :symbol `(:dynamicRegistration :json-false)) - :textDocument (eglot--obj - :synchronization (eglot--obj + :textDocument (jrpc-obj + :synchronization (jrpc-obj :dynamicRegistration :json-false :willSave t :willSaveWaitUntil :json-false @@ -217,49 +163,7 @@ CONTACT is as `eglot--contact'. Returns a process object." :documentHighlight `(:dynamicRegistration :json-false) :rename `(:dynamicRegistration :json-false) :publishDiagnostics `(:relatedInformation :json-false)) - :experimental (eglot--obj))) - -(defun eglot--connect (project managed-major-mode short-name contact interactive) - "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT. -INTERACTIVE is t if inside interactive call." - (let* ((proc (eglot--make-process short-name managed-major-mode contact)) - (buffer (process-buffer proc))) - (setf (eglot--contact proc) contact - (eglot--project proc) project - (eglot--major-mode proc) managed-major-mode) - (with-current-buffer buffer - (let ((inhibit-read-only t)) - (setf (eglot--inhibit-autoreconnect proc) - (cond - ((booleanp eglot-autoreconnect) (not eglot-autoreconnect)) - (interactive nil) - ((cl-plusp eglot-autoreconnect) - (run-with-timer eglot-autoreconnect nil - (lambda () - (setf (eglot--inhibit-autoreconnect proc) - (null eglot-autoreconnect))))))) - (setf (eglot--short-name proc) short-name) - (push proc (gethash project eglot--processes-by-project)) - (erase-buffer) - (read-only-mode t) - (cl-destructuring-bind (&key capabilities) - (eglot--request - proc - :initialize - (eglot--obj :processId (unless (eq (process-type proc) - 'network) - (emacs-pid)) - :rootUri (eglot--path-to-uri - (car (project-roots project))) - :initializationOptions [] - :capabilities (eglot--client-capabilities))) - (setf (eglot--capabilities proc) capabilities) - (setf (eglot--status proc) nil) - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (eglot--maybe-activate-editing-mode proc))) - (eglot--notify proc :initialized (eglot--obj :__dummy__ t)) - proc))))) + :experimental (jrpc-obj))) (defvar eglot--command-history nil "History of COMMAND arguments to `eglot'.") @@ -330,7 +234,7 @@ INTERACTIVE is t if called interactively." (unless project (eglot--error "Cannot work without a current project!")) (unless command (eglot--error "Don't know how to start EGLOT for %s buffers" major-mode)) - (let ((current-process (eglot--current-process))) + (let ((current-process (jrpc-current-process))) (if (and (process-live-p current-process) interactive (y-or-n-p "[eglot] Live process found, reconnect instead? ")) @@ -339,7 +243,7 @@ INTERACTIVE is t if called interactively." (eglot-shutdown current-process)) (let ((proc (eglot--connect project managed-major-mode - short-name + (format "%s/%s" short-name managed-major-mode) command interactive))) (eglot--message "Connected! Process `%s' now \ @@ -349,336 +253,56 @@ managing `%s' buffers in project `%s'." (defun eglot-reconnect (process &optional interactive) "Reconnect to PROCESS. INTERACTIVE is t if called interactively." - (interactive (list (eglot--current-process-or-lose) t)) + (interactive (list (jrpc-current-process-or-lose) t)) (when (process-live-p process) (eglot-shutdown process interactive)) (eglot--connect (eglot--project process) (eglot--major-mode process) - (eglot--short-name process) - (eglot--contact process) + (jrpc-name process) + (jrpc-contact process) interactive) (eglot--message "Reconnected!")) -(defun eglot--process-sentinel (proc change) - "Called when PROC undergoes CHANGE." - (eglot--log-event proc `(:message "Process state changed" :change ,change)) - (when (not (process-live-p proc)) - (with-current-buffer (eglot-events-buffer proc) - (let ((inhibit-read-only t)) - (insert "\n----------b---y---e---b---y---e----------\n"))) - ;; Cancel outstanding timers - (maphash (lambda (_id triplet) - (cl-destructuring-bind (_success _error timeout) triplet - (cancel-timer timeout))) - (eglot--pending-continuations proc)) - (unwind-protect - ;; Call all outstanding error handlers - (maphash (lambda (_id triplet) - (cl-destructuring-bind (_success error _timeout) triplet - (funcall error :code -1 :message (format "Server died")))) - (eglot--pending-continuations proc)) - ;; Turn off `eglot--managed-mode' where appropriate. +(defalias 'eglot-events-buffer 'jrpc-events-buffer) + +(defun eglot--connect (project managed-major-mode name command + dont-inhibit) + (let ((proc (jrpc-connect name command "eglot--server-"))) + (setf (eglot--project proc) project) + (setf (eglot--major-mode proc)managed-major-mode) + (push proc (gethash project eglot--processes-by-project)) + (cl-destructuring-bind (&key capabilities) + (jrpc-request + proc + :initialize + (jrpc-obj :processId (unless (eq (process-type proc) + 'network) + (emacs-pid)) + :rootUri (eglot--path-to-uri + (car (project-roots project))) + :initializationOptions [] + :capabilities (eglot--client-capabilities))) + (setf (eglot--capabilities proc) capabilities) + (setf (jrpc-status proc) nil) (dolist (buffer (buffer-list)) (with-current-buffer buffer - (when (eglot--buffer-managed-p proc) - (eglot--managed-mode -1)))) - ;; Forget about the process-project relationship - (setf (gethash (eglot--project proc) eglot--processes-by-project) - (delq proc - (gethash (eglot--project proc) eglot--processes-by-project))) - (eglot--message "Server exited with status %s" (process-exit-status proc)) - (cond ((eglot--moribund proc)) - ((not (eglot--inhibit-autoreconnect proc)) - (eglot--warn "Reconnecting unexpected server exit.") - (eglot-reconnect proc)) - (t - (eglot--warn "Not auto-reconnecting, last one didn't last long."))) - (delete-process proc)))) - -(defun eglot--process-filter (proc string) - "Called when new data STRING has arrived for PROC." - (when (buffer-live-p (process-buffer proc)) - (with-current-buffer (process-buffer proc) - (let ((inhibit-read-only t) - (expected-bytes (eglot--expected-bytes proc))) - ;; Insert the text, advancing the process marker. - ;; - (save-excursion - (goto-char (process-mark proc)) - (insert string) - (set-marker (process-mark proc) (point))) - ;; Loop (more than one message might have arrived) - ;; - (unwind-protect - (let (done) - (while (not done) - (cond - ((not expected-bytes) - ;; Starting a new message - ;; - (setq expected-bytes - (and (search-forward-regexp - "\\(?:.*: .*\r\n\\)*Content-Length: \ -*\\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" - (+ (point) 100) - t) - (string-to-number (match-string 1)))) - (unless expected-bytes - (setq done :waiting-for-new-message))) - (t - ;; Attempt to complete a message body - ;; - (let ((available-bytes (- (position-bytes (process-mark proc)) - (position-bytes (point))))) - (cond - ((>= available-bytes - expected-bytes) - (let* ((message-end (byte-to-position - (+ (position-bytes (point)) - expected-bytes)))) - (unwind-protect - (save-restriction - (narrow-to-region (point) message-end) - (let* ((json-object-type 'plist) - (json-message (json-read))) - ;; Process content in another buffer, - ;; shielding buffer from tamper - ;; - (with-temp-buffer - (eglot--process-receive proc json-message)))) - (goto-char message-end) - (delete-region (point-min) (point)) - (setq expected-bytes nil)))) - (t - ;; Message is still incomplete - ;; - (setq done :waiting-for-more-bytes-in-this-message)))))))) - ;; Saved parsing state for next visit to this filter - ;; - (setf (eglot--expected-bytes proc) expected-bytes)))))) - -(defun eglot-events-buffer (process &optional interactive) - "Display events buffer for current LSP connection PROCESS. -INTERACTIVE is t if called interactively." - (interactive (list (eglot--current-process-or-lose) t)) - (let* ((probe (eglot--events-buffer process)) - (buffer (or (and (buffer-live-p probe) - probe) - (let ((buffer (get-buffer-create - (format "*%s events*" - (process-name process))))) - (with-current-buffer buffer - (buffer-disable-undo) - (read-only-mode t) - (setf (eglot--events-buffer process) buffer)) - buffer)))) - (when interactive (display-buffer buffer)) - buffer)) - -(defun eglot--log-event (proc message &optional type) - "Log an eglot-related event. -PROC is the current process. MESSAGE is a JSON-like plist. TYPE -is a symbol saying if this is a client or server originated." - (with-current-buffer (eglot-events-buffer proc) - (cl-destructuring-bind (&key method id error &allow-other-keys) message - (let* ((inhibit-read-only t) - (subtype (cond ((and method id) 'request) - (method 'notification) - (id 'reply) - (t 'message))) - (type - (format "%s-%s" (or type :internal) subtype))) - (goto-char (point-max)) - (let ((msg (format "%s%s%s:\n%s\n" - type - (if id (format " (id:%s)" id) "") - (if error " ERROR" "") - (pp-to-string message)))) - (when error - (setq msg (propertize msg 'face 'error))) - (insert-before-markers msg)))))) - -(defun eglot--process-receive (proc message) - "Process MESSAGE from PROC." - (cl-destructuring-bind (&key method id error &allow-other-keys) message - (let* ((continuations (and id - (not method) - (gethash id (eglot--pending-continuations proc))))) - (eglot--log-event proc message 'server) - (when error (setf (eglot--status proc) `(,error t))) - (cond (method - ;; a server notification or a server request - (let* ((handler-sym (intern (concat "eglot--server-" method)))) - (if (functionp handler-sym) - (apply handler-sym proc (append - (plist-get message :params) - (if id `(:id ,id)))) - (eglot--warn "No implementation of method %s yet" method) - (when id - (eglot--reply - proc id - :error (eglot--obj :code -32601 - :message "Method unimplemented")))))) - (continuations - (cancel-timer (cl-third continuations)) - (remhash id (eglot--pending-continuations proc)) - (if error - (apply (cl-second continuations) error) - (let ((res (plist-get message :result))) - (if (listp res) - (apply (cl-first continuations) res) - (funcall (cl-first continuations) res))))) - (id - (eglot--warn "Ooops no continuation for id %s" id))) - (eglot--call-deferred proc) - (force-mode-line-update t)))) - -(defvar eglot--expect-carriage-return nil) - -(defun eglot--process-send (proc message) - "Send MESSAGE to PROC (ID is optional)." - (let ((json (json-encode message))) - (process-send-string proc (format "Content-Length: %d\r\n\r\n%s" - (string-bytes json) - json)) - (eglot--log-event proc message 'client))) - -(defvar eglot--next-request-id 0) - -(defun eglot--next-request-id () - "Compute the next id for a client request." - (setq eglot--next-request-id (1+ eglot--next-request-id))) - -(defun eglot-forget-pending-continuations (process) - "Stop waiting for responses from the current LSP PROCESS." - (interactive (list (eglot--current-process-or-lose))) - (clrhash (eglot--pending-continuations process))) - -(defun eglot-clear-status (process) - "Clear most recent error message from PROCESS." - (interactive (list (eglot--current-process-or-lose))) - (setf (eglot--status process) nil)) - -(defun eglot--call-deferred (proc) - "Call PROC's deferred actions, who may again defer themselves." - (when-let ((actions (hash-table-values (eglot--deferred-actions proc)))) - (eglot--log-event proc `(:running-deferred ,(length actions))) - (mapc #'funcall (mapcar #'car actions)))) - -(defvar eglot--ready-predicates '(eglot--server-ready-p) - "Special hook of predicates controlling deferred actions. -If one of these returns nil, a deferrable `eglot--async-request' -will be deferred. Each predicate is passed the symbol for the -request request and a process object.") + (eglot--maybe-activate-editing-mode proc))) + (jrpc-notify proc :initialized (jrpc-obj :__dummy__ t)) + (setf (eglot--inhibit-autoreconnect proc) + (cond + ((booleanp eglot-autoreconnect) (not eglot-autoreconnect)) + (dont-inhibit nil) + ((cl-plusp eglot-autoreconnect) + (run-with-timer eglot-autoreconnect nil + (lambda () + (setf (eglot--inhibit-autoreconnect proc) + (null eglot-autoreconnect))))))) + proc))) (defun eglot--server-ready-p (_what _proc) "Tell if server of PROC ready for processing deferred WHAT." (not (eglot--outstanding-edits-p))) -(cl-defmacro eglot--lambda (cl-lambda-list &body body) - (declare (indent 1) (debug (sexp &rest form))) - `(cl-function (lambda ,cl-lambda-list ,@body))) - -(cl-defun eglot--async-request (proc - method - params - &rest args - &key success-fn error-fn timeout-fn - (timeout eglot-request-timeout) - (deferred nil)) - "Make a request to PROCESS, expecting a reply. -Return the ID of this request. Wait TIMEOUT seconds for response. -If DEFERRED, maybe defer request to the future, or never at all, -in case a new request with identical DEFERRED and for the same -buffer overrides it. However, if that happens, the original -timeout keeps counting." - (let* ((id (eglot--next-request-id)) - (existing-timer nil) - (make-timeout - (lambda ( ) - (or existing-timer - (run-with-timer - timeout nil - (lambda () - (remhash id (eglot--pending-continuations proc)) - (funcall (or timeout-fn - (lambda () - (eglot--error - "Tired of waiting for reply to %s, id=%s" - method id)))))))))) - (when deferred - (let* ((buf (current-buffer)) - (existing (gethash (list deferred buf) (eglot--deferred-actions proc)))) - (when existing (setq existing-timer (cadr existing))) - (if (run-hook-with-args-until-failure 'eglot--ready-predicates - deferred proc) - (remhash (list deferred buf) (eglot--deferred-actions proc)) - (eglot--log-event proc `(:deferring ,method :id ,id :params ,params)) - (let* ((buf (current-buffer)) (point (point)) - (later (lambda () - (when (buffer-live-p buf) - (with-current-buffer buf - (save-excursion (goto-char point) - (apply #'eglot--async-request proc - method params args))))))) - (puthash (list deferred buf) (list later (funcall make-timeout)) - (eglot--deferred-actions proc)) - (cl-return-from eglot--async-request nil))))) - ;; Really run it - ;; - (puthash id - (list (or success-fn - (eglot--lambda (&rest _ignored) - (eglot--log-event - proc (eglot--obj :message "success ignored" :id id)))) - (or error-fn - (eglot--lambda (&key code message &allow-other-keys) - (setf (eglot--status proc) `(,message t)) - proc (eglot--obj :message "error ignored, status set" - :id id :error code))) - (funcall make-timeout)) - (eglot--pending-continuations proc)) - (eglot--process-send proc (eglot--obj :jsonrpc "2.0" - :id id - :method method - :params params)))) - -(defun eglot--request (proc method params &optional deferred) - "Like `eglot--async-request' for PROC, METHOD and PARAMS, but synchronous. -Meaning only return locally if successful, otherwise exit non-locally. -DEFERRED is passed to `eglot--async-request', which see." - ;; Launching a deferred sync request with outstanding changes is a - ;; bad idea, since that might lead to the request never having a - ;; chance to run, because `eglot--ready-predicates'. - (when deferred (eglot--signal-textDocument/didChange)) - (let ((retval)) - (eglot--async-request - proc method params - :success-fn (lambda (&rest args) - (setq retval `(done ,(if (vectorp (car args)) - (car args) args)))) - :error-fn (eglot--lambda (&key code message &allow-other-keys) - (setq retval `(error ,(format "Oops: %s: %s" code message)))) - :timeout-fn (lambda () - (setq retval '(error "Timed out"))) - :deferred deferred) - (while (not retval) (accept-process-output nil 30)) - (when (eq 'error (car retval)) (eglot--error (cadr retval))) - (cadr retval))) - -(cl-defun eglot--notify (process method params) - "Notify PROCESS of something, don't expect a reply.e" - (eglot--process-send process (eglot--obj :jsonrpc "2.0" - :method method - :params params))) - -(cl-defun eglot--reply (process id &key result error) - "Reply to PROCESS's request ID with MESSAGE." - (eglot--process-send - process `(:jsonrpc "2.0" :id ,id - ,@(when result `(:result ,result)) - ,@(when error `(:error ,error))))) - ;;; Helpers ;;; @@ -701,7 +325,7 @@ DEFERRED is passed to `eglot--async-request', which see." (defun eglot--pos-to-lsp-position (&optional pos) "Convert point POS to LSP position." (save-excursion - (eglot--obj :line + (jrpc-obj :line ;; F!@(#*&#$)CKING OFF-BY-ONE (1- (line-number-at-pos pos t)) :character @@ -718,11 +342,6 @@ DEFERRED is passed to `eglot--async-request', which see." (line-beginning-position)))) (point))) - -(defun eglot--mapply (fun seq) - "Apply FUN to every element of SEQ." - (mapcar (lambda (e) (apply fun e)) seq)) - (defun eglot--path-to-uri (path) "Urify PATH." (url-hexify-string (concat "file://" (file-truename path)) @@ -759,7 +378,7 @@ DEFERRED is passed to `eglot--async-request', which see." (defun eglot--server-capable (feat) "Determine if current server is capable of FEAT." - (plist-get (eglot--capabilities (eglot--current-process-or-lose)) feat)) + (plist-get (eglot--capabilities (jrpc-current-process-or-lose)) feat)) (cl-defmacro eglot--with-lsp-range ((start end) range &body body &aux (range-sym (cl-gensym))) @@ -780,6 +399,9 @@ DEFERRED is passed to `eglot--async-request', which see." nil nil eglot-mode-map (cond (eglot--managed-mode + (add-hook 'jrpc-find-process-functions 'eglot--find-current-process nil t) + (add-hook 'jrpc-ready-predicates 'eglot--server-ready-p nil t) + (add-hook 'jrpc-server-moribund-hook 'eglot--on-shutdown nil t) (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'before-change-functions 'eglot--before-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) @@ -793,6 +415,9 @@ DEFERRED is passed to `eglot--async-request', which see." #'eglot-eldoc-function) (add-function :around (local imenu-create-index-function) #'eglot-imenu)) (t + (remove-hook 'jrpc-find-process-functions 'eglot--find-current-process t) + (remove-hook 'jrpc-ready-predicates 'eglot--server-ready-p t) + (remove-hook 'jrpc-server-moribund-hook 'eglot--on-shutdown t) (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) (remove-hook 'before-change-functions 'eglot--before-change t) @@ -805,7 +430,7 @@ DEFERRED is passed to `eglot--async-request', which see." (remove-function (local 'eldoc-documentation-function) #'eglot-eldoc-function) (remove-function (local imenu-create-index-function) #'eglot-imenu) - (let ((proc (eglot--current-process))) + (let ((proc (eglot--find-current-process))) (when (and (process-live-p proc) (y-or-n-p "[eglot] Kill server too? ")) (eglot-shutdown proc t)))))) @@ -813,10 +438,12 @@ DEFERRED is passed to `eglot--async-request', which see." (add-hook 'eglot--managed-mode-hook 'eldoc-mode) (defun eglot--buffer-managed-p (&optional proc) - "Tell if current buffer is managed by PROC." - (and buffer-file-name (let ((cur (eglot--current-process))) - (or (and (null proc) cur) - (and proc (eq proc cur)))))) + "Tell if current buffer can be managed by PROC." + (and buffer-file-name + (cond ((null proc) (jrpc-current-process)) + (t (and (eq major-mode (eglot--major-mode proc)) + (let ((proj (project-current))) + (and proj (equal proj (eglot--project proc))))))))) (defvar-local eglot--current-flymake-report-fn nil "Current flymake report function for this buffer") @@ -868,12 +495,11 @@ Uses THING, FACE, DEFS and PREPEND." (defun eglot--mode-line-format () "Compose the EGLOT's mode-line." - (pcase-let* ((proc (eglot--current-process)) - (name (and (process-live-p proc) (eglot--short-name proc))) - (pending (and proc (hash-table-count - (eglot--pending-continuations proc)))) + (pcase-let* ((proc (jrpc-current-process)) + (name (and (process-live-p proc) (jrpc-name proc))) + (pending (and proc (length (jrpc-outstanding-request-ids proc)))) (`(,_id ,doing ,done-p ,detail) (and proc (eglot--spinner proc))) - (`(,status ,serious-p) (and proc (eglot--status proc)))) + (`(,status ,serious-p) (and proc (jrpc-status proc)))) (append `(,(eglot--mode-line-props "eglot" 'eglot-mode-line '((down-mouse-1 eglot-menu "pop up EGLOT menu")))) @@ -908,25 +534,6 @@ Uses THING, FACE, DEFS and PREPEND." ;;; Protocol implementation (Requests, notifications, etc) ;;; -(defun eglot-shutdown (proc &optional interactive) - "Politely ask the server PROC to quit. -Forcefully quit it if it doesn't respond. Don't leave this -function with the server still running. INTERACTIVE is t if -called interactively." - (interactive (list (eglot--current-process-or-lose) t)) - (when interactive (eglot--message "Asking %s politely to terminate" proc)) - (unwind-protect - (let ((eglot-request-timeout 3)) - (setf (eglot--moribund proc) t) - (eglot--request proc - :shutdown - nil) - ;; this one should always fail - (ignore-errors (eglot--request proc :exit nil))) - (when (process-live-p proc) - (eglot--warn "Brutally deleting existing process %s" proc) - (delete-process proc)))) - (cl-defun eglot--server-window/showMessage (_process &key type message) "Handle notification window/showMessage" (eglot--message (propertize "Server reports (type=%s): %s" @@ -949,10 +556,10 @@ called interactively." '("OK")) nil t (plist-get (elt actions 0) :title))) (if reply - (eglot--reply process id :result (eglot--obj :title reply)) - (eglot--reply process id - :error (eglot--obj :code -32800 - :message "User cancelled")))))) + (jrpc-reply process id :result (jrpc-obj :title reply)) + (jrpc-reply process id + :error (jrpc-obj :code -32800 + :message "User cancelled")))))) (cl-defun eglot--server-window/logMessage (_proc &key _type _message) "Handle notification window/logMessage") ;; noop, use events buffer @@ -978,12 +585,12 @@ called interactively." _code source message) diag-spec (eglot--with-lsp-range (beg end) range - (flymake-make-diagnostic (current-buffer) - beg end - (cond ((<= severity 1) :error) - ((= severity 2) :warning) - (t :note)) - (concat source ": " message)))) + (flymake-make-diagnostic (current-buffer) + beg end + (cond ((<= severity 1) :error) + ((= severity 2) :warning) + (t :note)) + (concat source ": " message)))) into diags finally (cond (eglot--current-flymake-report-fn (funcall eglot--current-flymake-report-fn diags) @@ -996,7 +603,7 @@ called interactively." (cl-defun eglot--server-client/registerCapability (proc &key id registrations) "Handle notification client/registerCapability" - (let ((jsonrpc-id id) + (let ((jrpc-id id) (done (make-symbol "done"))) (catch done (mapc @@ -1012,13 +619,13 @@ called interactively." (apply handler-sym proc :id id registerOptions)))) (unless ok (throw done - (eglot--reply proc jsonrpc-id - :error (eglot--obj - :code -32601 - :message (or message "sorry :-(")))))))) + (jrpc-reply proc jrpc-id + :error (jrpc-obj + :code -32601 + :message (or message "sorry :-(")))))))) reg)) registrations) - (eglot--reply proc id :result (eglot--obj :message "OK"))))) + (jrpc-reply proc id :result (jrpc-obj :message "OK"))))) (cl-defun eglot--server-workspace/applyEdit (proc &key id _label edit) @@ -1026,30 +633,30 @@ called interactively." (condition-case err (progn (eglot--apply-workspace-edit edit 'confirm) - (eglot--reply proc id :result `(:applied ))) + (jrpc-reply proc id :result `(:applied ))) (error - (eglot--reply proc id - :result `(:applied :json-false) - :error - (eglot--obj :code -32001 - :message (format "%s" err)))))) + (jrpc-reply proc id + :result `(:applied :json-false) + :error + (jrpc-obj :code -32001 + :message (format "%s" err)))))) (defun eglot--TextDocumentIdentifier () "Compute TextDocumentIdentifier object for current buffer." - (eglot--obj :uri (eglot--path-to-uri buffer-file-name))) + (jrpc-obj :uri (eglot--path-to-uri buffer-file-name))) (defvar-local eglot--versioned-identifier 0) (defun eglot--VersionedTextDocumentIdentifier () "Compute VersionedTextDocumentIdentifier object for current buffer." (append (eglot--TextDocumentIdentifier) - (eglot--obj :version eglot--versioned-identifier))) + (jrpc-obj :version eglot--versioned-identifier))) (defun eglot--TextDocumentItem () "Compute TextDocumentItem object for current buffer." (append (eglot--VersionedTextDocumentIdentifier) - (eglot--obj :languageId + (jrpc-obj :languageId (if (string-match "\\(.*\\)-mode" (symbol-name major-mode)) (match-string 1 (symbol-name major-mode)) "unknown") @@ -1060,7 +667,7 @@ called interactively." (defun eglot--TextDocumentPositionParams () "Compute TextDocumentPositionParams." - (eglot--obj :textDocument (eglot--TextDocumentIdentifier) + (jrpc-obj :textDocument (eglot--TextDocumentIdentifier) :position (eglot--pos-to-lsp-position))) (defvar-local eglot--recent-changes nil @@ -1091,10 +698,16 @@ Records START, END and PRE-CHANGE-LENGTH locally." `[(,pre-change-length ,(buffer-substring-no-properties start end))]))) +;; HACK! +(advice-add #'jrpc-request :before + (lambda (_proc _method _params &optional deferred) + (when (and eglot--managed-mode deferred) + (eglot--signal-textDocument/didChange)))) + (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." (when (eglot--outstanding-edits-p) - (let* ((proc (eglot--current-process-or-lose)) + (let* ((proc (jrpc-current-process-or-lose)) (sync-kind (eglot--server-capable :textDocumentSync)) (emacs-messup (/= (length (car eglot--recent-changes)) (length (cdr eglot--recent-changes)))) @@ -1103,56 +716,57 @@ Records START, END and PRE-CHANGE-LENGTH locally." (eglot--warn "`eglot--recent-changes' messup: %s" eglot--recent-changes)) (save-restriction (widen) - (eglot--notify + (jrpc-notify proc :textDocument/didChange - (eglot--obj + (jrpc-obj :textDocument (eglot--VersionedTextDocumentIdentifier) :contentChanges (if full-sync-p (vector - (eglot--obj + (jrpc-obj :text (buffer-substring-no-properties (point-min) (point-max)))) (cl-loop for (start-pos end-pos) across (car eglot--recent-changes) for (len after-text) across (cdr eglot--recent-changes) - vconcat `[,(eglot--obj :range (eglot--obj :start start-pos - :end end-pos) - :rangeLength len - :text after-text)]))))) + vconcat `[,(jrpc-obj :range (jrpc-obj :start start-pos + :end end-pos) + :rangeLength len + :text after-text)]))))) (setq eglot--recent-changes (cons [] [])) (setf (eglot--spinner proc) (list nil :textDocument/didChange t)) - (eglot--call-deferred proc)))) + ;; HACK! + (jrpc--call-deferred proc)))) (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." (setq eglot--recent-changes (cons [] [])) - (eglot--notify (eglot--current-process-or-lose) - :textDocument/didOpen - (eglot--obj :textDocument - (eglot--TextDocumentItem)))) + (jrpc-notify (jrpc-current-process-or-lose) + :textDocument/didOpen + (jrpc-obj :textDocument + (eglot--TextDocumentItem)))) (defun eglot--signal-textDocument/didClose () "Send textDocument/didClose to server." - (eglot--notify (eglot--current-process-or-lose) - :textDocument/didClose - (eglot--obj :textDocument - (eglot--TextDocumentIdentifier)))) + (jrpc-notify (jrpc-current-process-or-lose) + :textDocument/didClose + (jrpc-obj :textDocument + (eglot--TextDocumentIdentifier)))) (defun eglot--signal-textDocument/willSave () "Send textDocument/willSave to server." - (eglot--notify - (eglot--current-process-or-lose) + (jrpc-notify + (jrpc-current-process-or-lose) :textDocument/willSave - (eglot--obj + (jrpc-obj :reason 1 ; Manual, emacs laughs in the face of auto-save muahahahaha :textDocument (eglot--TextDocumentIdentifier)))) (defun eglot--signal-textDocument/didSave () "Send textDocument/didSave to server." - (eglot--notify - (eglot--current-process-or-lose) + (jrpc-notify + (jrpc-current-process-or-lose) :textDocument/didSave - (eglot--obj + (jrpc-obj ;; TODO: Handle TextDocumentSaveRegistrationOptions to control this. :text (buffer-substring-no-properties (point-min) (point-max)) :textDocument (eglot--TextDocumentIdentifier)))) @@ -1192,26 +806,26 @@ DUMMY is ignored" (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) (when (eglot--server-capable :documentSymbolProvider) - (let ((proc (eglot--current-process-or-lose)) + (let ((proc (jrpc-current-process-or-lose)) (text-id (eglot--TextDocumentIdentifier))) (completion-table-with-cache (lambda (string) (setq eglot--xref-known-symbols - (eglot--mapply - (eglot--lambda (&key name kind location containerName) + (jrpc-mapply + (jrpc-lambda (&key name kind location containerName) (propertize name :textDocumentPositionParams - (eglot--obj :textDocument text-id - :position (plist-get - (plist-get location :range) - :start)) + (jrpc-obj :textDocument text-id + :position (plist-get + (plist-get location :range) + :start)) :locations (list location) :kind kind :containerName containerName)) - (eglot--request proc - :textDocument/documentSymbol - (eglot--obj - :textDocument text-id)))) + (jrpc-request proc + :textDocument/documentSymbol + (jrpc-obj + :textDocument text-id)))) (all-completions string eglot--xref-known-symbols)))))) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) @@ -1226,12 +840,12 @@ DUMMY is ignored" (location-or-locations (if rich-identifier (get-text-property 0 :locations rich-identifier) - (eglot--request (eglot--current-process-or-lose) - :textDocument/definition - (get-text-property - 0 :textDocumentPositionParams identifier))))) - (eglot--mapply - (eglot--lambda (&key uri range) + (jrpc-request (jrpc-current-process-or-lose) + :textDocument/definition + (get-text-property + 0 :textDocumentPositionParams identifier))))) + (jrpc-mapply + (jrpc-lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) location-or-locations))) @@ -1244,43 +858,43 @@ DUMMY is ignored" (and rich (get-text-property 0 :textDocumentPositionParams rich)))))) (unless params (eglot--error "Don' know where %s is in the workspace!" identifier)) - (eglot--mapply - (eglot--lambda (&key uri range) + (jrpc-mapply + (jrpc-lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) - (eglot--request (eglot--current-process-or-lose) - :textDocument/references - (append - params - (eglot--obj :context - (eglot--obj :includeDeclaration t))))))) + (jrpc-request (jrpc-current-process-or-lose) + :textDocument/references + (append + params + (jrpc-obj :context + (jrpc-obj :includeDeclaration t))))))) (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) - (eglot--mapply - (eglot--lambda (&key name location &allow-other-keys) + (jrpc-mapply + (jrpc-lambda (&key name location &allow-other-keys) (cl-destructuring-bind (&key uri range) location (eglot--xref-make name uri (plist-get range :start)))) - (eglot--request (eglot--current-process-or-lose) - :workspace/symbol - (eglot--obj :query pattern))))) + (jrpc-request (jrpc-current-process-or-lose) + :workspace/symbol + (jrpc-obj :query pattern))))) (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." (let ((bounds (bounds-of-thing-at-point 'symbol)) - (proc (eglot--current-process-or-lose))) + (proc (jrpc-current-process-or-lose))) (when (eglot--server-capable :completionProvider) (list (or (car bounds) (point)) (or (cdr bounds) (point)) (completion-table-with-cache (lambda (_ignored) - (let* ((resp (eglot--request proc - :textDocument/completion - (eglot--TextDocumentPositionParams) - :textDocument/completion)) + (let* ((resp (jrpc-request proc + :textDocument/completion + (eglot--TextDocumentPositionParams) + :textDocument/completion)) (items (if (vectorp resp) resp (plist-get resp :items)))) - (eglot--mapply - (eglot--lambda (&rest all &key label &allow-other-keys) + (jrpc-mapply + (jrpc-lambda (&rest all &key label &allow-other-keys) (add-text-properties 0 1 all label) label) items)))) :annotation-function @@ -1299,8 +913,8 @@ DUMMY is ignored" (lambda (obj) (let ((documentation (or (get-text-property 0 :documentation obj) - (plist-get (eglot--request proc :completionItem/resolve - (text-properties-at 0 obj)) + (plist-get (jrpc-request proc :completionItem/resolve + (text-properties-at 0 obj)) :documentation)))) (when documentation (with-current-buffer (get-buffer-create " *eglot doc*") @@ -1317,7 +931,7 @@ DUMMY is ignored" (defun eglot--hover-info (contents &optional range) (concat (and range (eglot--with-lsp-range (beg end) range - (concat (buffer-substring beg end) ": "))) + (concat (buffer-substring beg end) ": "))) (mapconcat #'eglot--format-markup (append (cond ((vectorp contents) @@ -1329,8 +943,8 @@ DUMMY is ignored" "Request \"hover\" information for the thing at point." (interactive) (cl-destructuring-bind (&key contents range) - (eglot--request (eglot--current-process-or-lose) :textDocument/hover - (eglot--TextDocumentPositionParams)) + (jrpc-request (jrpc-current-process-or-lose) :textDocument/hover + (eglot--TextDocumentPositionParams)) (when (seq-empty-p contents) (eglot--error "No hover info here")) (with-help-window "*eglot help*" (with-current-buffer standard-output @@ -1339,26 +953,26 @@ DUMMY is ignored" (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function." (let ((buffer (current-buffer)) - (proc (eglot--current-process-or-lose)) + (proc (jrpc-current-process-or-lose)) (position-params (eglot--TextDocumentPositionParams))) (when (eglot--server-capable :hoverProvider) - (eglot--async-request + (jrpc-async-request proc :textDocument/hover position-params - :success-fn (eglot--lambda (&key contents range) + :success-fn (jrpc-lambda (&key contents range) (when (get-buffer-window buffer) (with-current-buffer buffer (eldoc-message (eglot--hover-info contents range))))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) - (eglot--async-request + (jrpc-async-request proc :textDocument/documentHighlight position-params :success-fn (lambda (highlights) (mapc #'delete-overlay eglot--highlights) (setq eglot--highlights (when (get-buffer-window buffer) (with-current-buffer buffer - (eglot--mapply - (eglot--lambda (&key range _kind) + (jrpc-mapply + (jrpc-lambda (&key range _kind) (eglot--with-lsp-range (beg end) range (let ((ov (make-overlay beg end))) (overlay-put ov 'face 'highlight) @@ -1372,15 +986,15 @@ DUMMY is ignored" "EGLOT's `imenu-create-index-function' overriding OLDFUN." (if (eglot--server-capable :documentSymbolProvider) (let ((entries - (eglot--mapply - (eglot--lambda (&key name kind location _containerName) + (jrpc-mapply + (jrpc-lambda (&key name kind location _containerName) (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) (eglot--lsp-position-to-point (plist-get (plist-get location :range) :start)))) - (eglot--request (eglot--current-process-or-lose) - :textDocument/documentSymbol - (eglot--obj - :textDocument (eglot--TextDocumentIdentifier)))))) + (jrpc-request (jrpc-current-process-or-lose) + :textDocument/documentSymbol + (jrpc-obj + :textDocument (eglot--TextDocumentIdentifier)))))) (append (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) entries) @@ -1394,8 +1008,8 @@ DUMMY is ignored" (equal version eglot--versioned-identifier)) (eglot--error "Edits on `%s' require version %d, you have %d" buffer version eglot--versioned-identifier)) - (eglot--mapply - (eglot--lambda (&key range newText) + (jrpc-mapply + (jrpc-lambda (&key range newText) (save-restriction (widen) (save-excursion @@ -1448,9 +1062,9 @@ Proceed? " (unless (eglot--server-capable :renameProvider) (eglot--error "Server can't rename!")) (eglot--apply-workspace-edit - (eglot--request (eglot--current-process-or-lose) - :textDocument/rename `(,@(eglot--TextDocumentPositionParams) - ,@(eglot--obj :newName newname))) + (jrpc-request (jrpc-current-process-or-lose) + :textDocument/rename `(,@(eglot--TextDocumentPositionParams) + ,@(jrpc-obj :newName newname))) current-prefix-arg)) @@ -1478,7 +1092,7 @@ Proceed? " (add-hook 'rust-mode-hook 'eglot--setup-rls-idiosyncrasies) (defun eglot--setup-rls-idiosyncrasies () "Prepare `eglot' to deal with RLS's special treatment." - (add-hook 'eglot--ready-predicates 'eglot--rls-probably-ready-for-p t t))) + (add-hook 'jrpc-ready-predicates 'eglot--rls-probably-ready-for-p t t))) (cl-defun eglot--server-window/progress (process &key id done title message &allow-other-keys) commit bb08431bca7475dffdc3f37a97e4aea468a83641 Author: João Távora Date: Sun May 13 02:39:32 2018 +0100 Reinstate the catch/loop/throw idiom in eglot-request This reverts parts of commit fabee14ed5b32c30c6ac5cb1ce88f387522a6a1e. Unfortunately, this may cause problems when calling the error callbacks directly as in the process sentinel. In that particular scenario the accept-process-output won't have return, because no output has is being handled. Consequently, if we're unlucky, we have another 30 seconds to way before the flag is tested and the loop exits. * eglot.el (eglot-request): Use catch/loop/throw again diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13f6f61d58..6f9034942d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -651,20 +651,25 @@ DEFERRED is passed to `eglot--async-request', which see." ;; bad idea, since that might lead to the request never having a ;; chance to run, because `eglot--ready-predicates'. (when deferred (eglot--signal-textDocument/didChange)) - (let ((retval)) - (eglot--async-request - proc method params - :success-fn (lambda (&rest args) - (setq retval `(done ,(if (vectorp (car args)) - (car args) args)))) - :error-fn (eglot--lambda (&key code message &allow-other-keys) - (setq retval `(error ,(format "Oops: %s: %s" code message)))) - :timeout-fn (lambda () - (setq retval '(error "Timed out"))) - :deferred deferred) - (while (not retval) (accept-process-output nil 30)) - (when (eq 'error (car retval)) (eglot--error (cadr retval))) - (cadr retval))) + (let* ((done (make-symbol "eglot--request-catch-tag")) + (res + (catch done (eglot--async-request + proc method params + :success-fn (lambda (&rest args) + (throw done (if (vectorp (car args)) + (car args) args))) + :error-fn (eglot--lambda + (&key code message &allow-other-keys) + (throw done + `(error ,(format "Oops: %s: %s" + code message)))) + :timeout-fn (lambda () + (throw done '(error "Timed out"))) + :deferred deferred) + ;; now spin, baby! + (while t (accept-process-output nil 0.01))))) + (when (and (listp res) (eq 'error (car res))) (eglot--error (cadr res))) + res)) (cl-defun eglot--notify (process method params) "Notify PROCESS of something, don't expect a reply.e" commit 1aa3018c654adb383ac2beaa88df15302459fb57 Author: João Távora Date: Sat May 12 22:05:20 2018 +0100 Fix copyright header. obviously not since 2003 diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13f6f61d58..4cb3bec50e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1,6 +1,6 @@ ;;; eglot.el --- Client for Language Server Protocol (LSP) servers -*- lexical-binding: t; -*- -;; Copyright (C) 2003-2018 Free Software Foundation, Inc. +;; Copyright (C) 2018 Free Software Foundation, Inc. ;; Version: 0.1 ;; Author: João Távora commit 72b7487c55bfc694eec23391e116a6866c40c949 Author: João Távora Date: Sat May 12 15:59:53 2018 +0100 New command eglot-help-at-point and a readme update * README.md (Commands and keybindings): New section. * eglot.el (eglot-eldoc-function): Use eglot--hover-info. Don't care about kind in highlightSymbol (eglot--hover-info): New helper. (eglot-help-at-point): New command. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c8612cf140..13f6f61d58 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1314,6 +1314,28 @@ DUMMY is ignored" (defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") +(defun eglot--hover-info (contents &optional range) + (concat (and range + (eglot--with-lsp-range (beg end) range + (concat (buffer-substring beg end) ": "))) + (mapconcat #'eglot--format-markup + (append + (cond ((vectorp contents) + contents) + (contents + (list contents)))) "\n"))) + +(defun eglot-help-at-point () + "Request \"hover\" information for the thing at point." + (interactive) + (cl-destructuring-bind (&key contents range) + (eglot--request (eglot--current-process-or-lose) :textDocument/hover + (eglot--TextDocumentPositionParams)) + (when (seq-empty-p contents) (eglot--error "No hover info here")) + (with-help-window "*eglot help*" + (with-current-buffer standard-output + (insert (eglot--hover-info contents range)))))) + (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function." (let ((buffer (current-buffer)) @@ -1325,17 +1347,7 @@ DUMMY is ignored" :success-fn (eglot--lambda (&key contents range) (when (get-buffer-window buffer) (with-current-buffer buffer - (eldoc-message - (concat - (and range - (eglot--with-lsp-range (beg end) range - (concat (buffer-substring beg end) ": "))) - (mapconcat #'eglot--format-markup - (append - (cond ((vectorp contents) - contents) - (contents - (list contents)))) "\n")))))) + (eldoc-message (eglot--hover-info contents range))))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) (eglot--async-request @@ -1346,12 +1358,11 @@ DUMMY is ignored" (when (get-buffer-window buffer) (with-current-buffer buffer (eglot--mapply - (eglot--lambda (&key range kind) + (eglot--lambda (&key range _kind) (eglot--with-lsp-range (beg end) range (let ((ov (make-overlay beg end))) (overlay-put ov 'face 'highlight) (overlay-put ov 'evaporate t) - (overlay-put ov :kind kind) ov))) highlights))))) :deferred :textDocument/documentHighlight))) @@ -1482,3 +1493,7 @@ Proceed? " (provide 'eglot) ;;; eglot.el ends here + +;; Local Variables: +;; checkdoc-force-docstrings-flag: nil +;; End: commit fabee14ed5b32c30c6ac5cb1ce88f387522a6a1e Author: João Távora Date: Fri May 11 11:46:53 2018 +0100 Get rid of catch/loop/throw idiom (suggested by thien-thi nguyen) * eglot.el (eglot--process-filter) (eglot--request): Replace catch/loop/throw idiom with let/test/loop/set diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index aabaf5407f..c8612cf140 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -400,8 +400,7 @@ INTERACTIVE is t if called interactively." (when (buffer-live-p (process-buffer proc)) (with-current-buffer (process-buffer proc) (let ((inhibit-read-only t) - (expected-bytes (eglot--expected-bytes proc)) - (done (make-symbol "eglot--process-filter-done-tag"))) + (expected-bytes (eglot--expected-bytes proc))) ;; Insert the text, advancing the process marker. ;; (save-excursion @@ -411,8 +410,8 @@ INTERACTIVE is t if called interactively." ;; Loop (more than one message might have arrived) ;; (unwind-protect - (catch done - (while t + (let (done) + (while (not done) (cond ((not expected-bytes) ;; Starting a new message @@ -425,7 +424,7 @@ INTERACTIVE is t if called interactively." t) (string-to-number (match-string 1)))) (unless expected-bytes - (throw done :waiting-for-new-message))) + (setq done :waiting-for-new-message))) (t ;; Attempt to complete a message body ;; @@ -453,7 +452,7 @@ INTERACTIVE is t if called interactively." (t ;; Message is still incomplete ;; - (throw done :waiting-for-more-bytes-in-this-message)))))))) + (setq done :waiting-for-more-bytes-in-this-message)))))))) ;; Saved parsing state for next visit to this filter ;; (setf (eglot--expected-bytes proc) expected-bytes)))))) @@ -652,25 +651,20 @@ DEFERRED is passed to `eglot--async-request', which see." ;; bad idea, since that might lead to the request never having a ;; chance to run, because `eglot--ready-predicates'. (when deferred (eglot--signal-textDocument/didChange)) - (let* ((done (make-symbol "eglot--request-catch-tag")) - (res - (catch done (eglot--async-request - proc method params - :success-fn (lambda (&rest args) - (throw done (if (vectorp (car args)) - (car args) args))) - :error-fn (eglot--lambda - (&key code message &allow-other-keys) - (throw done - `(error ,(format "Oops: %s: %s" - code message)))) - :timeout-fn (lambda () - (throw done '(error "Timed out"))) - :deferred deferred) - ;; now spin, baby! - (while t (accept-process-output nil 0.01))))) - (when (and (listp res) (eq 'error (car res))) (eglot--error (cadr res))) - res)) + (let ((retval)) + (eglot--async-request + proc method params + :success-fn (lambda (&rest args) + (setq retval `(done ,(if (vectorp (car args)) + (car args) args)))) + :error-fn (eglot--lambda (&key code message &allow-other-keys) + (setq retval `(error ,(format "Oops: %s: %s" code message)))) + :timeout-fn (lambda () + (setq retval '(error "Timed out"))) + :deferred deferred) + (while (not retval) (accept-process-output nil 30)) + (when (eq 'error (car retval)) (eglot--error (cadr retval))) + (cadr retval))) (cl-defun eglot--notify (process method params) "Notify PROCESS of something, don't expect a reply.e" commit 4af0193fad1ca40b48db4fba5b27e8a6f6c8d11a Author: João Távora Date: Fri May 11 10:59:59 2018 +0100 Rework autoreconnection logic Can't be a global var, has to be a per process thing. * eglot.el (eglot-autoreconnect): New defcustom (eglot--inhibit-autoreconnect): Renamed from eglot--inhibit-autoreconnect (eglot--connect): Run autoreconnect timer here. (eglot--inhibit-auto-reconnect): Removed. (eglot--process-sentinel): Don't run timer here. Rework. (eglot, eglot-reconnect): Pass INTERACTIVE to eglot--connect. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3b3aada055..aabaf5407f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -59,9 +59,19 @@ "Face for package-name in EGLOT's mode line.") (defcustom eglot-request-timeout 10 - "How many seconds to way for a reply from the server." + "How many seconds to wait for a reply from the server." :type :integer) +(defcustom eglot-autoreconnect 3 + "Control EGLOT's ability to reconnect automatically. +If t, always reconnect automatically (not recommended). If nil, +never reconnect automatically after unexpected server shutdowns, +crashes or network failures. A positive integer number says to +only autoreconnect if the previous successful connection attempt +lasted more than that many seconds." + :type '(choice (boolean :tag "Whether to inhibit autoreconnection") + (integer :tag "Number of seconds"))) + ;;; Process management (defvar eglot--processes-by-project (make-hash-table :test #'equal) @@ -129,6 +139,9 @@ A list (ID WHAT DONE-P).") "Status as declared by the server. A list (WHAT SERIOUS-P).") +(eglot--define-process-var eglot--inhibit-autoreconnect eglot-autoreconnect + "If non-nil, don't autoreconnect on unexpected quit.") + (eglot--define-process-var eglot--contact nil "Method used to contact a server. Either a list of strings (a shell command and arguments), or a @@ -206,8 +219,9 @@ CONTACT is as `eglot--contact'. Returns a process object." :publishDiagnostics `(:relatedInformation :json-false)) :experimental (eglot--obj))) -(defun eglot--connect (project managed-major-mode short-name contact) - "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT." +(defun eglot--connect (project managed-major-mode short-name contact interactive) + "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT. +INTERACTIVE is t if inside interactive call." (let* ((proc (eglot--make-process short-name managed-major-mode contact)) (buffer (process-buffer proc))) (setf (eglot--contact proc) contact @@ -215,6 +229,15 @@ CONTACT is as `eglot--contact'. Returns a process object." (eglot--major-mode proc) managed-major-mode) (with-current-buffer buffer (let ((inhibit-read-only t)) + (setf (eglot--inhibit-autoreconnect proc) + (cond + ((booleanp eglot-autoreconnect) (not eglot-autoreconnect)) + (interactive nil) + ((cl-plusp eglot-autoreconnect) + (run-with-timer eglot-autoreconnect nil + (lambda () + (setf (eglot--inhibit-autoreconnect proc) + (null eglot-autoreconnect))))))) (setf (eglot--short-name proc) short-name) (push proc (gethash project eglot--processes-by-project)) (erase-buffer) @@ -317,7 +340,8 @@ INTERACTIVE is t if called interactively." (let ((proc (eglot--connect project managed-major-mode short-name - command))) + command + interactive))) (eglot--message "Connected! Process `%s' now \ managing `%s' buffers in project `%s'." proc managed-major-mode short-name)))))) @@ -331,12 +355,10 @@ INTERACTIVE is t if called interactively." (eglot--connect (eglot--project process) (eglot--major-mode process) (eglot--short-name process) - (eglot--contact process)) + (eglot--contact process) + interactive) (eglot--message "Reconnected!")) -(defvar eglot--inhibit-auto-reconnect nil - "If non-nil, don't autoreconnect on unexpected quit.") - (defun eglot--process-sentinel (proc change) "Called when PROC undergoes CHANGE." (eglot--log-event proc `(:message "Process state changed" :change ,change)) @@ -364,22 +386,13 @@ INTERACTIVE is t if called interactively." (setf (gethash (eglot--project proc) eglot--processes-by-project) (delq proc (gethash (eglot--project proc) eglot--processes-by-project))) - (cond ((eglot--moribund proc) - (eglot--message "(sentinel) Moribund process exited with status %s" - (process-exit-status proc))) - ((null eglot--inhibit-auto-reconnect) - (eglot--warn - "(sentinel) Reconnecting after process unexpectedly changed to `%s'." - change) - (setq eglot--inhibit-auto-reconnect - (run-with-timer 3 nil - (lambda () - (setq eglot--inhibit-auto-reconnect nil)))) + (eglot--message "Server exited with status %s" (process-exit-status proc)) + (cond ((eglot--moribund proc)) + ((not (eglot--inhibit-autoreconnect proc)) + (eglot--warn "Reconnecting unexpected server exit.") (eglot-reconnect proc)) (t - (eglot--warn - "(sentinel) Not auto-reconnecting, last one didn't last long." - change))) + (eglot--warn "Not auto-reconnecting, last one didn't last long."))) (delete-process proc)))) (defun eglot--process-filter (proc string) commit 1251bd1336077b9a3490929fd2aa2c70acada495 Author: João Távora Date: Fri May 11 02:03:10 2018 +0100 Duh, json.el is in emacs, and json-mode.el is useless here * eglot.el (Package-Requires): Don't require json-mode diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e33e54045c..3b3aada055 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -7,7 +7,7 @@ ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1") (json-mode "1.6.0")) +;; Package-Requires: ((emacs "26.1")) ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by commit abfe41cc2c2932525844d9d1cef83bf877145bf4 Author: João Távora Date: Thu May 10 22:40:32 2018 +0100 Prepare to sumbit to gnu elpa * eglot.el: Update headers. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 635ca26632..e33e54045c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1,12 +1,13 @@ ;;; eglot.el --- Client for Language Server Protocol (LSP) servers -*- lexical-binding: t; -*- -;; Copyright (C) 2017 João Távora +;; Copyright (C) 2003-2018 Free Software Foundation, Inc. ;; Version: 0.1 -;; Author: João Távora -;; Url: https://github.com/joaotavora/eglot +;; Author: João Távora +;; Maintainer: João Távora +;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1")) +;; Package-Requires: ((emacs "26.1") (json-mode "1.6.0")) ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by commit dfd5947b11edf20b9c95a272d195b8fd737a855c Author: João Távora Date: Thu May 10 22:56:11 2018 +0100 (eglot--xref-make): fix use of cl-destructuring-bind. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4c5d53e320..635ca26632 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1176,7 +1176,7 @@ DUMMY is ignored" (defun eglot--xref-make (name uri position) "Like `xref-make' but with LSP's NAME, URI and POSITION." - (cl-destructuring-bind (line character) position + (cl-destructuring-bind (&key line character) position (xref-make name (xref-make-file-location (eglot--uri-to-path uri) ;; F!@(#*&#$)CKING OFF-BY-ONE again commit 3d193f2f33bb5524fd840f9f82ecc5adeabffa01 Author: João Távora Date: Thu May 10 22:04:59 2018 +0100 Misc little adjustments for readability * eglot.el (eglot--log-event, eglot--process-receive) (eglot--xref-make, xref-backend-apropos): Use cl-destructuring-bind. (eglot--server-window/showMessageRequest): Compact. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 35573034bf..4c5d53e320 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -467,62 +467,58 @@ INTERACTIVE is t if called interactively." PROC is the current process. MESSAGE is a JSON-like plist. TYPE is a symbol saying if this is a client or server originated." (with-current-buffer (eglot-events-buffer proc) - (let* ((inhibit-read-only t) - (id (plist-get message :id)) - (error (plist-get message :error)) - (method (plist-get message :method)) - (subtype (cond ((and method id) 'request) - (method 'notification) - (id 'reply) - (t 'message))) - (type - (format "%s-%s" (or type :internal) subtype))) - (goto-char (point-max)) - (let ((msg (format "%s%s%s:\n%s\n" - type - (if id (format " (id:%s)" id) "") - (if error " ERROR" "") - (pp-to-string message)))) - (when error - (setq msg (propertize msg 'face 'error))) - (insert-before-markers msg))))) + (cl-destructuring-bind (&key method id error &allow-other-keys) message + (let* ((inhibit-read-only t) + (subtype (cond ((and method id) 'request) + (method 'notification) + (id 'reply) + (t 'message))) + (type + (format "%s-%s" (or type :internal) subtype))) + (goto-char (point-max)) + (let ((msg (format "%s%s%s:\n%s\n" + type + (if id (format " (id:%s)" id) "") + (if error " ERROR" "") + (pp-to-string message)))) + (when error + (setq msg (propertize msg 'face 'error))) + (insert-before-markers msg)))))) (defun eglot--process-receive (proc message) "Process MESSAGE from PROC." - (let* ((id (plist-get message :id)) - (method (plist-get message :method)) - (err (plist-get message :error)) - (continuations (and id - (not method) - (gethash id (eglot--pending-continuations proc))))) - (eglot--log-event proc message 'server) - (when err (setf (eglot--status proc) `(,err t))) - (cond (method - ;; a server notification or a server request - (let* ((handler-sym (intern (concat "eglot--server-" method)))) - (if (functionp handler-sym) - (apply handler-sym proc (append - (plist-get message :params) - (if id `(:id ,id)))) - (eglot--warn "No implementation of method %s yet" method) - (when id - (eglot--reply - proc id - :error (eglot--obj :code -32601 - :message "Method unimplemented")))))) - (continuations - (cancel-timer (cl-third continuations)) - (remhash id (eglot--pending-continuations proc)) - (if err - (apply (cl-second continuations) err) - (let ((res (plist-get message :result))) - (if (listp res) - (apply (cl-first continuations) res) - (funcall (cl-first continuations) res))))) - (id - (eglot--warn "Ooops no continuation for id %s" id))) - (eglot--call-deferred proc) - (force-mode-line-update t))) + (cl-destructuring-bind (&key method id error &allow-other-keys) message + (let* ((continuations (and id + (not method) + (gethash id (eglot--pending-continuations proc))))) + (eglot--log-event proc message 'server) + (when error (setf (eglot--status proc) `(,error t))) + (cond (method + ;; a server notification or a server request + (let* ((handler-sym (intern (concat "eglot--server-" method)))) + (if (functionp handler-sym) + (apply handler-sym proc (append + (plist-get message :params) + (if id `(:id ,id)))) + (eglot--warn "No implementation of method %s yet" method) + (when id + (eglot--reply + proc id + :error (eglot--obj :code -32601 + :message "Method unimplemented")))))) + (continuations + (cancel-timer (cl-third continuations)) + (remhash id (eglot--pending-continuations proc)) + (if error + (apply (cl-second continuations) error) + (let ((res (plist-get message :result))) + (if (listp res) + (apply (cl-first continuations) res) + (funcall (cl-first continuations) res))))) + (id + (eglot--warn "Ooops no continuation for id %s" id))) + (eglot--call-deferred proc) + (force-mode-line-update t)))) (defvar eglot--expect-carriage-return nil) @@ -941,10 +937,9 @@ called interactively." 'face (if (<= type 1) 'error)) type message) "\nChoose an option: ") - (mapcar (lambda (obj) (plist-get obj :title)) actions) - nil - t - (plist-get (elt actions 0) :title))) + (or (mapcar (lambda (obj) (plist-get obj :title)) actions) + '("OK")) + nil t (plist-get (elt actions 0) :title))) (if reply (eglot--reply process id :result (eglot--obj :title reply)) (eglot--reply process id @@ -1181,11 +1176,11 @@ DUMMY is ignored" (defun eglot--xref-make (name uri position) "Like `xref-make' but with LSP's NAME, URI and POSITION." - (xref-make name (xref-make-file-location - (eglot--uri-to-path uri) - ;; F!@(#*&#$)CKING OFF-BY-ONE again - (1+ (plist-get position :line)) - (plist-get position :character)))) + (cl-destructuring-bind (line character) position + (xref-make name (xref-make-file-location + (eglot--uri-to-path uri) + ;; F!@(#*&#$)CKING OFF-BY-ONE again + (1+ line) character)))) (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) (when (eglot--server-capable :documentSymbolProvider) @@ -1255,8 +1250,7 @@ DUMMY is ignored" (when (eglot--server-capable :workspaceSymbolProvider) (eglot--mapply (eglot--lambda (&key name location &allow-other-keys) - (let ((range (plist-get location :range)) - (uri (plist-get location :uri))) + (cl-destructuring-bind (&key uri range) location (eglot--xref-make name uri (plist-get range :start)))) (eglot--request (eglot--current-process-or-lose) :workspace/symbol commit 572bb298b26715d4451cacd6ac91685fb0d2ddec Author: João Távora Date: Thu May 10 21:56:54 2018 +0100 Support :completionitem/resolve This is quite handy with company and company-quickhelp * eglot.el (eglot-completion-at-point): Send :completionItem/resolve * README.md: Mention completionItem/resolve diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 58a2374bbf..35573034bf 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1278,23 +1278,35 @@ DUMMY is ignored" :textDocument/completion)) (items (if (vectorp resp) resp (plist-get resp :items)))) (eglot--mapply - (eglot--lambda (&key insertText label kind detail - documentation sortText &allow-other-keys) - (propertize (or insertText label) - :kind-name (cdr (assoc kind eglot--kind-names)) - :detail detail - :documentation documentation :sortText sortText)) + (eglot--lambda (&rest all &key label &allow-other-keys) + (add-text-properties 0 1 all label) label) items)))) :annotation-function - (lambda (what) - (propertize (concat " " (or (get-text-property 0 :detail what) - (get-text-property 0 :kind what))) + (lambda (obj) + (propertize (concat " " (or (get-text-property 0 :detail obj) + (cdr (assoc (get-text-property 0 :kind obj) + eglot--kind-names)))) 'face 'font-lock-function-name-face)) :display-sort-function - (lambda (items) (sort items (lambda (a b) - (string-lessp - (get-text-property 0 :sortText a) - (get-text-property 0 :sortText b))))) + (lambda (items) + (sort items (lambda (a b) + (string-lessp + (or (get-text-property 0 :sortText a) "") + (or (get-text-property 0 :sortText b) ""))))) + :company-doc-buffer + (lambda (obj) + (let ((documentation + (or (get-text-property 0 :documentation obj) + (plist-get (eglot--request proc :completionItem/resolve + (text-properties-at 0 obj)) + :documentation)))) + (when documentation + (with-current-buffer (get-buffer-create " *eglot doc*") + (erase-buffer) + (ignore-errors (funcall (intern "markdown-mode"))) + (font-lock-ensure) + (insert documentation) + (current-buffer))))) :exit-function (lambda (_string _status) (eglot-eldoc-function)))))) commit 0f73b0ef434329b59f9db5065a239afe49be2fdc Author: João Távora Date: Thu May 10 21:31:37 2018 +0100 Rename functions. eglot--request is now the synchronous one * eglot.el (eglot--connect, eglot-shutdown) (xref-backend-identifier-completion-table) (xref-backend-definitions, xref-backend-references) (xref-backend-apropos, eglot-completion-at-point, eglot-rename): Call eglot--request. (eglot--async-request): Renamed from eglot--request. (eglot--request): Renamed from eglot--sync-request. (eglot--TextDocumentIdentifier) (eglot--VersionedTextDocumentIdentifier) (eglot--TextDocumentPositionParams, eglot--TextDocumentItem): Renamed from the more verbose eglot--current-buffer-* variante. (eglot-rename, eglot-imenu, eglot-eldoc-function) (eglot-completion-at-point, xref-backend-definitions) (xref-backend-identifier-at-point) (eglot--signal-textDocument/didSave) (xref-backend-identifier-completion-table) (eglot--signal-textDocument/didClose) (eglot--signal-textDocument/didOpen) (eglot--signal-textDocument/didChange): Use new function names. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 22799cc299..58a2374bbf 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -219,7 +219,7 @@ CONTACT is as `eglot--contact'. Returns a process object." (erase-buffer) (read-only-mode t) (cl-destructuring-bind (&key capabilities) - (eglot--sync-request + (eglot--request proc :initialize (eglot--obj :processId (unless (eq (process-type proc) @@ -558,9 +558,9 @@ is a symbol saying if this is a client or server originated." (defvar eglot--ready-predicates '(eglot--server-ready-p) "Special hook of predicates controlling deferred actions. -When one of these functions returns nil, a deferrable -`eglot--request' will be deferred. Each predicate is passed the -an symbol for the request request and a process object.") +If one of these returns nil, a deferrable `eglot--async-request' +will be deferred. Each predicate is passed the symbol for the +request request and a process object.") (defun eglot--server-ready-p (_what _proc) "Tell if server of PROC ready for processing deferred WHAT." @@ -570,13 +570,13 @@ an symbol for the request request and a process object.") (declare (indent 1) (debug (sexp &rest form))) `(cl-function (lambda ,cl-lambda-list ,@body))) -(cl-defun eglot--request (proc - method - params - &rest args - &key success-fn error-fn timeout-fn - (timeout eglot-request-timeout) - (deferred nil)) +(cl-defun eglot--async-request (proc + method + params + &rest args + &key success-fn error-fn timeout-fn + (timeout eglot-request-timeout) + (deferred nil)) "Make a request to PROCESS, expecting a reply. Return the ID of this request. Wait TIMEOUT seconds for response. If DEFERRED, maybe defer request to the future, or never at all, @@ -610,11 +610,11 @@ timeout keeps counting." (when (buffer-live-p buf) (with-current-buffer buf (save-excursion (goto-char point) - (apply #'eglot--request proc + (apply #'eglot--async-request proc method params args))))))) (puthash (list deferred buf) (list later (funcall make-timeout)) (eglot--deferred-actions proc)) - (cl-return-from eglot--request nil))))) + (cl-return-from eglot--async-request nil))))) ;; Really run it ;; (puthash id @@ -634,17 +634,17 @@ timeout keeps counting." :method method :params params)))) -(defun eglot--sync-request (proc method params &optional deferred) - "Like `eglot--request' for PROC, METHOD and PARAMS, but synchronous. +(defun eglot--request (proc method params &optional deferred) + "Like `eglot--async-request' for PROC, METHOD and PARAMS, but synchronous. Meaning only return locally if successful, otherwise exit non-locally. -DEFERRED is passed to `eglot--request', which see." +DEFERRED is passed to `eglot--async-request', which see." ;; Launching a deferred sync request with outstanding changes is a ;; bad idea, since that might lead to the request never having a ;; chance to run, because `eglot--ready-predicates'. (when deferred (eglot--signal-textDocument/didChange)) - (let* ((done (make-symbol "eglot--sync-request-catch-tag")) + (let* ((done (make-symbol "eglot--request-catch-tag")) (res - (catch done (eglot--request + (catch done (eglot--async-request proc method params :success-fn (lambda (&rest args) (throw done (if (vectorp (car args)) @@ -914,11 +914,11 @@ called interactively." (unwind-protect (let ((eglot-request-timeout 3)) (setf (eglot--moribund proc) t) - (eglot--sync-request proc - :shutdown - nil) + (eglot--request proc + :shutdown + nil) ;; this one should always fail - (ignore-errors (eglot--sync-request proc :exit nil))) + (ignore-errors (eglot--request proc :exit nil))) (when (process-live-p proc) (eglot--warn "Brutally deleting existing process %s" proc) (delete-process proc)))) @@ -1031,21 +1031,21 @@ called interactively." (eglot--obj :code -32001 :message (format "%s" err)))))) -(defun eglot--current-buffer-TextDocumentIdentifier () +(defun eglot--TextDocumentIdentifier () "Compute TextDocumentIdentifier object for current buffer." (eglot--obj :uri (eglot--path-to-uri buffer-file-name))) (defvar-local eglot--versioned-identifier 0) -(defun eglot--current-buffer-VersionedTextDocumentIdentifier () +(defun eglot--VersionedTextDocumentIdentifier () "Compute VersionedTextDocumentIdentifier object for current buffer." - (append (eglot--current-buffer-TextDocumentIdentifier) + (append (eglot--TextDocumentIdentifier) (eglot--obj :version eglot--versioned-identifier))) -(defun eglot--current-buffer-TextDocumentItem () +(defun eglot--TextDocumentItem () "Compute TextDocumentItem object for current buffer." (append - (eglot--current-buffer-VersionedTextDocumentIdentifier) + (eglot--VersionedTextDocumentIdentifier) (eglot--obj :languageId (if (string-match "\\(.*\\)-mode" (symbol-name major-mode)) (match-string 1 (symbol-name major-mode)) @@ -1055,9 +1055,9 @@ called interactively." (widen) (buffer-substring-no-properties (point-min) (point-max)))))) -(defun eglot--current-buffer-TextDocumentPositionParams () +(defun eglot--TextDocumentPositionParams () "Compute TextDocumentPositionParams." - (eglot--obj :textDocument (eglot--current-buffer-TextDocumentIdentifier) + (eglot--obj :textDocument (eglot--TextDocumentIdentifier) :position (eglot--pos-to-lsp-position))) (defvar-local eglot--recent-changes nil @@ -1104,7 +1104,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." proc :textDocument/didChange (eglot--obj :textDocument - (eglot--current-buffer-VersionedTextDocumentIdentifier) + (eglot--VersionedTextDocumentIdentifier) :contentChanges (if full-sync-p (vector (eglot--obj @@ -1126,14 +1126,14 @@ Records START, END and PRE-CHANGE-LENGTH locally." (eglot--notify (eglot--current-process-or-lose) :textDocument/didOpen (eglot--obj :textDocument - (eglot--current-buffer-TextDocumentItem)))) + (eglot--TextDocumentItem)))) (defun eglot--signal-textDocument/didClose () "Send textDocument/didClose to server." (eglot--notify (eglot--current-process-or-lose) :textDocument/didClose (eglot--obj :textDocument - (eglot--current-buffer-TextDocumentIdentifier)))) + (eglot--TextDocumentIdentifier)))) (defun eglot--signal-textDocument/willSave () "Send textDocument/willSave to server." @@ -1142,7 +1142,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." :textDocument/willSave (eglot--obj :reason 1 ; Manual, emacs laughs in the face of auto-save muahahahaha - :textDocument (eglot--current-buffer-TextDocumentIdentifier)))) + :textDocument (eglot--TextDocumentIdentifier)))) (defun eglot--signal-textDocument/didSave () "Send textDocument/didSave to server." @@ -1152,7 +1152,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." (eglot--obj ;; TODO: Handle TextDocumentSaveRegistrationOptions to control this. :text (buffer-substring-no-properties (point-min) (point-max)) - :textDocument (eglot--current-buffer-TextDocumentIdentifier)))) + :textDocument (eglot--TextDocumentIdentifier)))) (defun eglot-flymake-backend (report-fn &rest _more) "An EGLOT Flymake backend. @@ -1190,7 +1190,7 @@ DUMMY is ignored" (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) (when (eglot--server-capable :documentSymbolProvider) (let ((proc (eglot--current-process-or-lose)) - (text-id (eglot--current-buffer-TextDocumentIdentifier))) + (text-id (eglot--TextDocumentIdentifier))) (completion-table-with-cache (lambda (string) (setq eglot--xref-known-symbols @@ -1205,17 +1205,17 @@ DUMMY is ignored" :locations (list location) :kind kind :containerName containerName)) - (eglot--sync-request proc - :textDocument/documentSymbol - (eglot--obj - :textDocument text-id)))) + (eglot--request proc + :textDocument/documentSymbol + (eglot--obj + :textDocument text-id)))) (all-completions string eglot--xref-known-symbols)))))) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) (when-let ((symatpt (symbol-at-point))) (propertize (symbol-name symatpt) :textDocumentPositionParams - (eglot--current-buffer-TextDocumentPositionParams)))) + (eglot--TextDocumentPositionParams)))) (cl-defmethod xref-backend-definitions ((_backend (eql eglot)) identifier) (let* ((rich-identifier @@ -1223,10 +1223,10 @@ DUMMY is ignored" (location-or-locations (if rich-identifier (get-text-property 0 :locations rich-identifier) - (eglot--sync-request (eglot--current-process-or-lose) - :textDocument/definition - (get-text-property - 0 :textDocumentPositionParams identifier))))) + (eglot--request (eglot--current-process-or-lose) + :textDocument/definition + (get-text-property + 0 :textDocumentPositionParams identifier))))) (eglot--mapply (eglot--lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) @@ -1244,12 +1244,12 @@ DUMMY is ignored" (eglot--mapply (eglot--lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) - (eglot--sync-request (eglot--current-process-or-lose) - :textDocument/references - (append - params - (eglot--obj :context - (eglot--obj :includeDeclaration t))))))) + (eglot--request (eglot--current-process-or-lose) + :textDocument/references + (append + params + (eglot--obj :context + (eglot--obj :includeDeclaration t))))))) (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) @@ -1258,9 +1258,9 @@ DUMMY is ignored" (let ((range (plist-get location :range)) (uri (plist-get location :uri))) (eglot--xref-make name uri (plist-get range :start)))) - (eglot--sync-request (eglot--current-process-or-lose) - :workspace/symbol - (eglot--obj :query pattern))))) + (eglot--request (eglot--current-process-or-lose) + :workspace/symbol + (eglot--obj :query pattern))))) (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." @@ -1272,11 +1272,10 @@ DUMMY is ignored" (or (cdr bounds) (point)) (completion-table-with-cache (lambda (_ignored) - (let* ((resp (eglot--sync-request - proc - :textDocument/completion - (eglot--current-buffer-TextDocumentPositionParams) - :textDocument/completion)) + (let* ((resp (eglot--request proc + :textDocument/completion + (eglot--TextDocumentPositionParams) + :textDocument/completion)) (items (if (vectorp resp) resp (plist-get resp :items)))) (eglot--mapply (eglot--lambda (&key insertText label kind detail @@ -1305,9 +1304,9 @@ DUMMY is ignored" "EGLOT's `eldoc-documentation-function' function." (let ((buffer (current-buffer)) (proc (eglot--current-process-or-lose)) - (position-params (eglot--current-buffer-TextDocumentPositionParams))) + (position-params (eglot--TextDocumentPositionParams))) (when (eglot--server-capable :hoverProvider) - (eglot--request + (eglot--async-request proc :textDocument/hover position-params :success-fn (eglot--lambda (&key contents range) (when (get-buffer-window buffer) @@ -1325,7 +1324,7 @@ DUMMY is ignored" (list contents)))) "\n")))))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) - (eglot--request + (eglot--async-request proc :textDocument/documentHighlight position-params :success-fn (lambda (highlights) (mapc #'delete-overlay eglot--highlights) @@ -1353,11 +1352,10 @@ DUMMY is ignored" (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) (eglot--lsp-position-to-point (plist-get (plist-get location :range) :start)))) - (eglot--sync-request - (eglot--current-process-or-lose) - :textDocument/documentSymbol - (eglot--obj - :textDocument (eglot--current-buffer-TextDocumentIdentifier)))))) + (eglot--request (eglot--current-process-or-lose) + :textDocument/documentSymbol + (eglot--obj + :textDocument (eglot--TextDocumentIdentifier)))))) (append (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) entries) @@ -1425,11 +1423,9 @@ Proceed? " (unless (eglot--server-capable :renameProvider) (eglot--error "Server can't rename!")) (eglot--apply-workspace-edit - (eglot--sync-request (eglot--current-process-or-lose) - :textDocument/rename - (append - (eglot--current-buffer-TextDocumentPositionParams) - (eglot--obj :newName newname))) + (eglot--request (eglot--current-process-or-lose) + :textDocument/rename `(,@(eglot--TextDocumentPositionParams) + ,@(eglot--obj :newName newname))) current-prefix-arg)) @@ -1448,7 +1444,7 @@ Proceed? " (defun eglot--rls-probably-ready-for-p (what proc) "Guess if the RLS running in PROC is ready for WHAT." (or (eq what :textDocument/completion) ; RLS normally ready for this - ; one, even if building + ; one, even if building ; (pcase-let ((`(,_id ,what ,done ,_detail) (eglot--spinner proc))) (and (equal "Indexing" what) done)))) commit e36892ef513e753bbef6787d15d56b6e669dbcfa Author: João Távora Date: Thu May 10 20:00:15 2018 +0100 Friendlier m-x eglot * eglot.el (eglot-server-programs): Renamed from eglot-executables (eglot--interactive): Redesign (eglot): Docstring. (eglot--connect): Now a synchronous gig. (eglot--interactive): Friendlier. (eglot): Improve docstring, rework a bit. (eglot-reconnect): Rework a bit. (eglot--process-sentinel): Insert "byebye" ruler here. * README.md: Update diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b2709a6ae4..22799cc299 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -47,9 +47,10 @@ :prefix "eglot-" :group 'applications) -(defvar eglot-executables '((rust-mode . ("rls")) - (python-mode . ("pyls")) - (js-mode . ("javascript-typescript-stdio"))) +(defvar eglot-server-programs '((rust-mode . ("rls")) + (python-mode . ("pyls")) + (js-mode . ("javascript-typescript-stdio")) + (sh-mode . ("bash-language-server" "start"))) "Alist mapping major modes to server executables.") (defface eglot-mode-line @@ -204,10 +205,8 @@ CONTACT is as `eglot--contact'. Returns a process object." :publishDiagnostics `(:relatedInformation :json-false)) :experimental (eglot--obj))) -(defun eglot--connect (project managed-major-mode - short-name contact &optional success-fn) - "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT. -SUCCESS-FN with no args if all goes well." +(defun eglot--connect (project managed-major-mode short-name contact) + "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT." (let* ((proc (eglot--make-process short-name managed-major-mode contact)) (buffer (process-buffer proc))) (setf (eglot--contact proc) contact @@ -216,70 +215,68 @@ SUCCESS-FN with no args if all goes well." (with-current-buffer buffer (let ((inhibit-read-only t)) (setf (eglot--short-name proc) short-name) - (push proc - (gethash (project-current) - eglot--processes-by-project)) + (push proc (gethash project eglot--processes-by-project)) (erase-buffer) (read-only-mode t) - (with-current-buffer (eglot-events-buffer proc) - (let ((inhibit-read-only t)) - (insert - (format "\n-----------------------------------\n")))) - (eglot--request - proc - :initialize - (eglot--obj :processId (unless (eq (process-type proc) - 'network) - (emacs-pid)) - :rootUri (eglot--path-to-uri - (car (project-roots (project-current)))) - :initializationOptions [] - :capabilities (eglot--client-capabilities)) - :success-fn - (cl-function - (lambda (&key capabilities) - (setf (eglot--capabilities proc) capabilities) - (setf (eglot--status proc) nil) - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (eglot--maybe-activate-editing-mode proc))) - (when success-fn (funcall success-fn proc)) - (eglot--notify proc :initialized (eglot--obj :__dummy__ t))))))))) + (cl-destructuring-bind (&key capabilities) + (eglot--sync-request + proc + :initialize + (eglot--obj :processId (unless (eq (process-type proc) + 'network) + (emacs-pid)) + :rootUri (eglot--path-to-uri + (car (project-roots project))) + :initializationOptions [] + :capabilities (eglot--client-capabilities))) + (setf (eglot--capabilities proc) capabilities) + (setf (eglot--status proc) nil) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (eglot--maybe-activate-editing-mode proc))) + (eglot--notify proc :initialized (eglot--obj :__dummy__ t)) + proc))))) (defvar eglot--command-history nil "History of COMMAND arguments to `eglot'.") (defun eglot--interactive () "Helper for `eglot'." - (let* ((managed-major-mode + (let* ((guessed-mode (if buffer-file-name major-mode)) + (managed-mode (cond - ((or current-prefix-arg - (not buffer-file-name)) + ((or (>= (prefix-numeric-value current-prefix-arg) 16) + (not guessed-mode)) (intern (completing-read "[eglot] Start a server to manage buffers of what major mode? " (mapcar #'symbol-name (eglot--all-major-modes)) nil t - (symbol-name major-mode) nil - (symbol-name major-mode) nil))) - (t major-mode))) - (guessed-command - (cdr (assoc managed-major-mode eglot-executables)))) + (symbol-name guessed-mode) nil (symbol-name guessed-mode) nil))) + (t guessed-mode))) + (guessed-command (cdr (assoc managed-mode eglot-server-programs))) + (base-prompt "[eglot] Enter program to execute (or :): ") + (prompt + (cond (current-prefix-arg base-prompt) + ((null guessed-command) + (concat (format "[eglot] Sorry, couldn't guess for `%s'!" + managed-mode) + "\n" base-prompt)) + ((and (listp guessed-command) + (not (executable-find (car guessed-command)))) + (concat (format "[eglot] I guess you want to run `%s'" + (combine-and-quote-strings guessed-command)) + (format ", but I can't find `%s' in PATH!" + (car guessed-command)) + "\n" base-prompt))))) (list - managed-major-mode - (let ((prompt - (cond (current-prefix-arg - "[eglot] Enter program to execute (or :): ") - ((null guessed-command) - (format "[eglot] Sorry, couldn't guess for `%s'!\n\ -Enter program to execute (or :): " - managed-major-mode))))) - (if prompt - (split-string-and-unquote - (read-shell-command prompt - (if (listp guessed-command) - (combine-and-quote-strings guessed-command)) - 'eglot-command-history)) - guessed-command)) + managed-mode + (if prompt + (split-string-and-unquote + (read-shell-command prompt + (if (listp guessed-command) + (combine-and-quote-strings guessed-command)) + 'eglot-command-history)) + guessed-command) t))) ;;;###autoload @@ -296,8 +293,11 @@ is also know as the server's \"contact\". MANAGED-MAJOR-MODE is an Emacs major mode. -With a prefix arg, prompt for MANAGED-MAJOR-MODE and COMMAND, -else guess them from current context and `eglot-executables'. +Interactively, guess MANAGED-MAJOR-MODE from current buffer and +COMMAND from `eglot-server-programs'. With a single +\\[universal-argument] prefix arg, prompt for COMMAND. With two +\\[universal-argument] prefix args, also prompt for +MANAGED-MAJOR-MODE. INTERACTIVE is t if called interactively." (interactive (eglot--interactive)) @@ -313,16 +313,13 @@ INTERACTIVE is t if called interactively." (eglot-reconnect current-process interactive) (when (process-live-p current-process) (eglot-shutdown current-process)) - (eglot--connect project - managed-major-mode - short-name - command - (lambda (proc) - (eglot--message "Connected! Process `%s' now \ + (let ((proc (eglot--connect project + managed-major-mode + short-name + command))) + (eglot--message "Connected! Process `%s' now \ managing `%s' buffers in project `%s'." - proc - managed-major-mode - short-name))))))) + proc managed-major-mode short-name)))))) (defun eglot-reconnect (process &optional interactive) "Reconnect to PROCESS. @@ -333,8 +330,8 @@ INTERACTIVE is t if called interactively." (eglot--connect (eglot--project process) (eglot--major-mode process) (eglot--short-name process) - (eglot--contact process) - (lambda (_proc) (eglot--message "Reconnected!")))) + (eglot--contact process)) + (eglot--message "Reconnected!")) (defvar eglot--inhibit-auto-reconnect nil "If non-nil, don't autoreconnect on unexpected quit.") @@ -343,6 +340,9 @@ INTERACTIVE is t if called interactively." "Called when PROC undergoes CHANGE." (eglot--log-event proc `(:message "Process state changed" :change ,change)) (when (not (process-live-p proc)) + (with-current-buffer (eglot-events-buffer proc) + (let ((inhibit-read-only t)) + (insert "\n----------b---y---e---b---y---e----------\n"))) ;; Cancel outstanding timers (maphash (lambda (_id triplet) (cl-destructuring-bind (_success _error timeout) triplet commit 522bcdf0e82546920a74239f2eeadae3af5921ed Author: João Távora Date: Thu May 10 19:23:56 2018 +0100 Improve eglot-eldoc-function Use the :range key if the server provided it. Also simplify code with a new eglot--with-lsp-range macro. * eglot.el (eglot--format-markup): Tweak comment. (eglot--with-lsp-range): New helper macro. (eglot--server-textDocument/publishDiagnostics) (eglot--apply-text-edits): Use it. (eglot-eldoc-function): Use range if server provides it. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d1e4d1548a..b2709a6ae4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -740,7 +740,7 @@ DEFERRED is passed to `eglot--request', which see." "Format MARKUP according to LSP's spec." (cond ((stringp markup) (with-temp-buffer - (ignore-errors (funcall (intern "markdown-mode"))) ;escape bytecompiler + (ignore-errors (funcall (intern "markdown-mode"))) ;escape bytecomp (font-lock-ensure) (insert markup) (string-trim (buffer-string)))) @@ -757,6 +757,15 @@ DEFERRED is passed to `eglot--request', which see." "Determine if current server is capable of FEAT." (plist-get (eglot--capabilities (eglot--current-process-or-lose)) feat)) +(cl-defmacro eglot--with-lsp-range ((start end) range &body body + &aux (range-sym (cl-gensym))) + "Bind LSP RANGE to START and END. Evaluate BODY." + (declare (indent 2) (debug (sexp sexp &rest form))) + `(let* ((,range-sym ,range) + (,start (eglot--lsp-position-to-point (plist-get ,range-sym :start))) + (,end (eglot--lsp-position-to-point (plist-get ,range-sym :end)))) + ,@body)) + ;;; Minor modes ;;; @@ -965,17 +974,13 @@ called interactively." collect (cl-destructuring-bind (&key range severity _group _code source message) diag-spec - (cl-destructuring-bind (&key start end) - range - (let* ((begin-pos (eglot--lsp-position-to-point start)) - (end-pos (eglot--lsp-position-to-point end))) - (flymake-make-diagnostic - (current-buffer) - begin-pos end-pos - (cond ((<= severity 1) :error) - ((= severity 2) :warning) - (t :note)) - (concat source ": " message))))) + (eglot--with-lsp-range (beg end) range + (flymake-make-diagnostic (current-buffer) + beg end + (cond ((<= severity 1) :error) + ((= severity 2) :warning) + (t :note)) + (concat source ": " message)))) into diags finally (cond (eglot--current-flymake-report-fn (funcall eglot--current-flymake-report-fn diags) @@ -1302,15 +1307,23 @@ DUMMY is ignored" (proc (eglot--current-process-or-lose)) (position-params (eglot--current-buffer-TextDocumentPositionParams))) (when (eglot--server-capable :hoverProvider) - (eglot--request proc :textDocument/hover position-params - :success-fn (eglot--lambda (&key contents _range) - (eldoc-message - (mapconcat #'eglot--format-markup - (if (vectorp contents) - contents - (list contents)) - "\n"))) - :deferred :textDocument/hover)) + (eglot--request + proc :textDocument/hover position-params + :success-fn (eglot--lambda (&key contents range) + (when (get-buffer-window buffer) + (with-current-buffer buffer + (eldoc-message + (concat + (and range + (eglot--with-lsp-range (beg end) range + (concat (buffer-substring beg end) ": "))) + (mapconcat #'eglot--format-markup + (append + (cond ((vectorp contents) + contents) + (contents + (list contents)))) "\n")))))) + :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) (eglot--request proc :textDocument/documentHighlight position-params @@ -1321,11 +1334,8 @@ DUMMY is ignored" (with-current-buffer buffer (eglot--mapply (eglot--lambda (&key range kind) - (cl-destructuring-bind (&key start end) range - (let ((ov (make-overlay - (eglot--lsp-position-to-point start) - (eglot--lsp-position-to-point end) - buffer))) + (eglot--with-lsp-range (beg end) range + (let ((ov (make-overlay beg end))) (overlay-put ov 'face 'highlight) (overlay-put ov 'evaporate t) (overlay-put ov :kind kind) @@ -1366,11 +1376,8 @@ DUMMY is ignored" (save-restriction (widen) (save-excursion - (let ((start (eglot--lsp-position-to-point (plist-get range :start)))) - (goto-char start) - (delete-region start - (eglot--lsp-position-to-point (plist-get range :end))) - (insert newText))))) + (eglot--with-lsp-range (beg end) range + (goto-char beg) (delete-region beg end) (insert newText))))) edits) (eglot--message "%s: Performed %s edits" (current-buffer) (length edits)))) commit 979a90456d89da9d838d79ff0b727e4f24e0305a Author: João Távora Date: Thu May 10 18:44:47 2018 +0100 Reduce log chatter * eglot.el (eglot--process-sentinel, eglot--request): Use eglot--log-event. (eglot--log-event): Print "message" if type unknown. (eglot--debug, eglot--log): Remove. (eglot--server-window/logMessage, eglot--server-telemetry/event): Make noops. (eglot--call-deferred): Also reduce chatter here. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8b8d712244..d1e4d1548a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -341,7 +341,7 @@ INTERACTIVE is t if called interactively." (defun eglot--process-sentinel (proc change) "Called when PROC undergoes CHANGE." - (eglot--debug "(sentinel) Process state changed to %s" change) + (eglot--log-event proc `(:message "Process state changed" :change ,change)) (when (not (process-live-p proc)) ;; Cancel outstanding timers (maphash (lambda (_id triplet) @@ -474,8 +474,7 @@ is a symbol saying if this is a client or server originated." (subtype (cond ((and method id) 'request) (method 'notification) (id 'reply) - ;; pyls keeps on sending these - (t 'unexpected-thingy))) + (t 'message))) (type (format "%s-%s" (or type :internal) subtype))) (goto-char (point-max)) @@ -553,7 +552,7 @@ is a symbol saying if this is a client or server originated." (defun eglot--call-deferred (proc) "Call PROC's deferred actions, who may again defer themselves." - (let ((actions (hash-table-values (eglot--deferred-actions proc)))) + (when-let ((actions (hash-table-values (eglot--deferred-actions proc)))) (eglot--log-event proc `(:running-deferred ,(length actions))) (mapc #'funcall (mapcar #'car actions)))) @@ -619,16 +618,15 @@ timeout keeps counting." ;; Really run it ;; (puthash id - (list (or success-fn (eglot--lambda (&rest result-body) - (eglot--debug - "Request %s, id=%s replied to with result=%s" - method id result-body))) - (or error-fn (eglot--lambda - (&key code message &allow-other-keys) - (setf (eglot--status proc) `(,message t)) - (eglot--warn - "Request %s, id=%s errored with code=%s: %s" - method id code message))) + (list (or success-fn + (eglot--lambda (&rest _ignored) + (eglot--log-event + proc (eglot--obj :message "success ignored" :id id)))) + (or error-fn + (eglot--lambda (&key code message &allow-other-keys) + (setf (eglot--status proc) `(,message t)) + proc (eglot--obj :message "error ignored, status set" + :id id :error code))) (funcall make-timeout)) (eglot--pending-continuations proc)) (eglot--process-send proc (eglot--obj :jsonrpc "2.0" @@ -680,12 +678,6 @@ DEFERRED is passed to `eglot--request', which see." ;;; Helpers ;;; -(defun eglot--debug (format &rest args) - "Debug message FORMAT with ARGS." - (display-warning 'eglot - (apply #'format format args) - :debug)) - (defun eglot--error (format &rest args) "Error out with FORMAT with ARGS." (error (apply #'format format args))) @@ -694,10 +686,6 @@ DEFERRED is passed to `eglot--request', which see." "Message out with FORMAT with ARGS." (message (concat "[eglot] " (apply #'format format args)))) -(defun eglot--log (format &rest args) - "Log out with FORMAT with ARGS." - (message (concat "[eglot-log] " (apply #'format format args)))) - (defun eglot--warn (format &rest args) "Warning message with FORMAT and ARGS." (apply #'eglot--message (concat "(warning) " format) args) @@ -954,15 +942,11 @@ called interactively." :error (eglot--obj :code -32800 :message "User cancelled")))))) -(cl-defun eglot--server-window/logMessage (_process &key type message) - "Handle notification window/logMessage" - (eglot--log (propertize "Server reports (type=%s): %s" - 'face (if (<= type 1) 'error)) - type message)) +(cl-defun eglot--server-window/logMessage (_proc &key _type _message) + "Handle notification window/logMessage") ;; noop, use events buffer -(cl-defun eglot--server-telemetry/event (_process &rest any) - "Handle notification telemetry/event" - (eglot--log "Server telemetry: %s" any)) +(cl-defun eglot--server-telemetry/event (_proc &rest _any) + "Handle notification telemetry/event") ;; noop, use events buffer (defvar-local eglot--unreported-diagnostics nil "Unreported diagnostics for this buffer.") commit 68892622c9da9f16dbd12b11486680b0e8038844 Author: João Távora Date: Thu May 10 13:52:21 2018 +0100 Only call deferred actions after a full message has been received Otherwise it can be quite wasteful. * eglot.el (eglot--process-filter): Don't eglot--call-deferred here. (eglot--process-receive): Do it here. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d49367b5fe..8b8d712244 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -380,6 +380,7 @@ INTERACTIVE is t if called interactively." "(sentinel) Not auto-reconnecting, last one didn't last long." change))) (delete-process proc)))) + (defun eglot--process-filter (proc string) "Called when new data STRING has arrived for PROC." (when (buffer-live-p (process-buffer proc)) @@ -441,8 +442,7 @@ INTERACTIVE is t if called interactively." (throw done :waiting-for-more-bytes-in-this-message)))))))) ;; Saved parsing state for next visit to this filter ;; - (setf (eglot--expected-bytes proc) expected-bytes)))) - (eglot--call-deferred proc))) + (setf (eglot--expected-bytes proc) expected-bytes)))))) (defun eglot-events-buffer (process &optional interactive) "Display events buffer for current LSP connection PROCESS. @@ -522,6 +522,7 @@ is a symbol saying if this is a client or server originated." (funcall (cl-first continuations) res))))) (id (eglot--warn "Ooops no continuation for id %s" id))) + (eglot--call-deferred proc) (force-mode-line-update t))) (defvar eglot--expect-carriage-return nil) commit 8fb14037db4849c0f06f4fda458f845826fa78ab Author: João Távora Date: Thu May 10 13:14:19 2018 +0100 Resist server failure during synchronous requests Calling the error handler unprotected could lead to the rest of the sentinel not running at all. This defeated the auto-reconnection in Rust, for example. * eglot.el (eglot--process-sentinel): Rework. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bf0b9e9271..d49367b5fe 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -339,50 +339,47 @@ INTERACTIVE is t if called interactively." (defvar eglot--inhibit-auto-reconnect nil "If non-nil, don't autoreconnect on unexpected quit.") -(defun eglot--process-sentinel (process change) - "Called with PROCESS undergoes CHANGE." +(defun eglot--process-sentinel (proc change) + "Called when PROC undergoes CHANGE." (eglot--debug "(sentinel) Process state changed to %s" change) - (when (not (process-live-p process)) - ;; Cancel timers and error any outstanding continuations - ;; + (when (not (process-live-p proc)) + ;; Cancel outstanding timers (maphash (lambda (_id triplet) - (cl-destructuring-bind (_success error timeout) triplet - (cancel-timer timeout) - (funcall error :code -1 :message (format "Server died")))) - (eglot--pending-continuations process)) - ;; Turn off `eglot--managed-mode' where appropriate. - ;; - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (when (eglot--buffer-managed-p process) - (eglot--managed-mode -1)))) - ;; Forget about the process-project relationship - ;; - (setf (gethash (eglot--project process) eglot--processes-by-project) - (delq process - (gethash (eglot--project process) eglot--processes-by-project))) - (cond ((eglot--moribund process) - (eglot--message "(sentinel) Moribund process exited with status %s" - (process-exit-status process))) - ((null eglot--inhibit-auto-reconnect) - (eglot--warn - "(sentinel) Reconnecting after process unexpectedly changed to `%s'." - change) - (condition-case-unless-debug err - (eglot-reconnect process) - (error (eglot--warn "Auto-reconnect failed: %s " err) )) - (setq eglot--inhibit-auto-reconnect - (run-with-timer - 3 nil - (lambda () - (setq eglot--inhibit-auto-reconnect nil))))) - (t - (eglot--warn - "(sentinel) Not auto-reconnecting, last one didn't last long." - change))) - (force-mode-line-update t) - (delete-process process))) - + (cl-destructuring-bind (_success _error timeout) triplet + (cancel-timer timeout))) + (eglot--pending-continuations proc)) + (unwind-protect + ;; Call all outstanding error handlers + (maphash (lambda (_id triplet) + (cl-destructuring-bind (_success error _timeout) triplet + (funcall error :code -1 :message (format "Server died")))) + (eglot--pending-continuations proc)) + ;; Turn off `eglot--managed-mode' where appropriate. + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (when (eglot--buffer-managed-p proc) + (eglot--managed-mode -1)))) + ;; Forget about the process-project relationship + (setf (gethash (eglot--project proc) eglot--processes-by-project) + (delq proc + (gethash (eglot--project proc) eglot--processes-by-project))) + (cond ((eglot--moribund proc) + (eglot--message "(sentinel) Moribund process exited with status %s" + (process-exit-status proc))) + ((null eglot--inhibit-auto-reconnect) + (eglot--warn + "(sentinel) Reconnecting after process unexpectedly changed to `%s'." + change) + (setq eglot--inhibit-auto-reconnect + (run-with-timer 3 nil + (lambda () + (setq eglot--inhibit-auto-reconnect nil)))) + (eglot-reconnect proc)) + (t + (eglot--warn + "(sentinel) Not auto-reconnecting, last one didn't last long." + change))) + (delete-process proc)))) (defun eglot--process-filter (proc string) "Called when new data STRING has arrived for PROC." (when (buffer-live-p (process-buffer proc)) commit f0d4e043b130d9dfaf125957194e4c75111f0773 Author: João Távora Date: Thu May 10 12:54:35 2018 +0100 Simplify mode-line updating logic * eglot.el (eglot--define-process-var): Simplify. (eglot--short-name, eglot--spinner, eglot--status): Don't auto-update mode-line. (eglot--process-receive): Update it here. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1ba5324de9..bf0b9e9271 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -78,11 +78,9 @@ (if (project-current) "" " (Also no current project)")))) (defmacro eglot--define-process-var - (var-sym initval &optional doc mode-line-update-p) + (var-sym initval &optional doc) "Define VAR-SYM as a generalized process-local variable. -INITVAL is the default value. DOC is the documentation. -MODE-LINE-UPDATE-P says to also force a mode line update -after setting it." +INITVAL is the default value. DOC is the documentation." (declare (indent 2)) `(progn (put ',var-sym 'function-documentation ,doc) @@ -94,16 +92,11 @@ after setting it." (let ((def ,initval)) (process-put proc ',var-sym def) def)))) - (gv-define-setter ,var-sym (to-store &optional process) - (let* ((prop ',var-sym)) - ,(let ((form '(let ((proc (or ,process (eglot--current-process-or-lose)))) - (process-put proc ',prop ,to-store)))) - (if mode-line-update-p - `(backquote (prog1 ,form (force-mode-line-update t))) - `(backquote ,form))))))) + (gv-define-setter ,var-sym (to-store process) + `(let ((once ,to-store)) (process-put ,process ',',var-sym once) once)))) (eglot--define-process-var eglot--short-name nil - "A short name for the process" t) + "A short name for the process") (eglot--define-process-var eglot--major-mode nil "The major-mode this server is managing.") @@ -128,11 +121,11 @@ after setting it." (eglot--define-process-var eglot--spinner `(nil nil t) "\"Spinner\" used by some servers. -A list (ID WHAT DONE-P)." t) +A list (ID WHAT DONE-P).") (eglot--define-process-var eglot--status `(:unknown nil) "Status as declared by the server. -A list (WHAT SERIOUS-P)." t) +A list (WHAT SERIOUS-P).") (eglot--define-process-var eglot--contact nil "Method used to contact a server. @@ -531,7 +524,8 @@ is a symbol saying if this is a client or server originated." (apply (cl-first continuations) res) (funcall (cl-first continuations) res))))) (id - (eglot--warn "Ooops no continuation for id %s" id))))) + (eglot--warn "Ooops no continuation for id %s" id))) + (force-mode-line-update t))) (defvar eglot--expect-carriage-return nil) commit 845063c090678d5888f69ec58baebe215b26f885 Author: João Távora Date: Thu May 10 12:51:21 2018 +0100 More rls-specifics: update flymake diags when indexing done RLS could/should report diagnostics for every opened file, even if there aren't any problems. Because it doesn't, loop for every buffer managed by the process and call eglot--current-flymake-report-fn * eglot.el (eglot--server-window/progress): Call eglot--current-flymake-report-fn diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f865c22589..1ba5324de9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -825,6 +825,9 @@ DEFERRED is passed to `eglot--request', which see." (or (and (null proc) cur) (and proc (eq proc cur)))))) +(defvar-local eglot--current-flymake-report-fn nil + "Current flymake report function for this buffer") + (defun eglot--maybe-activate-editing-mode (&optional proc) "Maybe activate mode function `eglot--managed-mode'. If PROC is supplied, do it only if BUFFER is managed by it. In @@ -969,9 +972,6 @@ called interactively." "Handle notification telemetry/event" (eglot--log "Server telemetry: %s" any)) -(defvar-local eglot--current-flymake-report-fn nil - "Current flymake report function for this buffer") - (defvar-local eglot--unreported-diagnostics nil "Unreported diagnostics for this buffer.") @@ -1479,7 +1479,13 @@ Proceed? " (cl-defun eglot--server-window/progress (process &key id done title message &allow-other-keys) "Handle notification window/progress" - (setf (eglot--spinner process) (list id title done message))) + (setf (eglot--spinner process) (list id title done message)) + (when (and (equal "Indexing" title) done) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (when (eglot--buffer-managed-p process) + (funcall (or eglot--current-flymake-report-fn #'ignore) + eglot--unreported-diagnostics)))))) (provide 'eglot) ;;; eglot.el ends here commit 4d5eff8c97a21d8bb245fe7f3b9d68efbd29d866 Author: João Távora Date: Thu May 10 12:21:29 2018 +0100 Adjust flymake integration When opening a new file (signalling textDocument/didOpen) it makes sense to call the flymake callback (if it exists) with no diagnostics, just to get rid of that "Wait", since we don't know if later in this callback cycle the server will ever report new diagnostics. * eglot.el (eglot--managed-mode): Don't call flymake-mode or eldoc-mode (eglot--managed-mode-hook): Add them here. (eglot--maybe-activate-editing-mode): Call flymake callback. (eglot--server-textDocument/publishDiagnostics): Set unreported diagnostics to nil if invoking callback. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 21ec247967..f865c22589 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -798,9 +798,7 @@ DEFERRED is passed to `eglot--request', which see." (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) (add-function :before-until (local 'eldoc-documentation-function) #'eglot-eldoc-function) - (add-function :around (local imenu-create-index-function) #'eglot-imenu) - (flymake-mode 1) - (eldoc-mode 1)) + (add-function :around (local imenu-create-index-function) #'eglot-imenu)) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) @@ -818,6 +816,9 @@ DEFERRED is passed to `eglot--request', which see." (when (and (process-live-p proc) (y-or-n-p "[eglot] Kill server too? ")) (eglot-shutdown proc t)))))) +(add-hook 'eglot--managed-mode-hook 'flymake-mode) +(add-hook 'eglot--managed-mode-hook 'eldoc-mode) + (defun eglot--buffer-managed-p (&optional proc) "Tell if current buffer is managed by PROC." (and buffer-file-name (let ((cur (eglot--current-process))) @@ -832,7 +833,8 @@ that case, also signal textDocument/didOpen." (when (eglot--buffer-managed-p proc) (eglot--managed-mode 1) (eglot--signal-textDocument/didOpen) - (flymake-start))) + (flymake-start) + (funcall (or eglot--current-flymake-report-fn #'ignore) nil))) (add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) @@ -999,11 +1001,11 @@ called interactively." (t :note)) (concat source ": " message))))) into diags - finally (if eglot--current-flymake-report-fn - (funcall eglot--current-flymake-report-fn - diags) - (setq eglot--unreported-diagnostics - diags))))) + finally (cond (eglot--current-flymake-report-fn + (funcall eglot--current-flymake-report-fn diags) + (setq eglot--unreported-diagnostics nil)) + (t + (setq eglot--unreported-diagnostics diags)))))) (t (eglot--message "OK so %s isn't visited" filename))))) @@ -1174,15 +1176,12 @@ Records START, END and PRE-CHANGE-LENGTH locally." (defun eglot-flymake-backend (report-fn &rest _more) "An EGLOT Flymake backend. Calls REPORT-FN maybe if server publishes diagnostics in time." - ;; Maybe call immediately if anything unreported (this will clear - ;; any pending diags) + (setq eglot--current-flymake-report-fn report-fn) + ;; Report anything unreported (when eglot--unreported-diagnostics (funcall report-fn eglot--unreported-diagnostics) (setq eglot--unreported-diagnostics nil)) - ;; Setup so maybe it's called later, too. - (setq eglot--current-flymake-report-fn report-fn) - ;; Take this opportunity to signal a didChange that might eventually - ;; make the server report new diagnostics. + ;; Signal a didChange that might eventually bring new diagnotics (eglot--signal-textDocument/didChange)) (defun eglot-xref-backend () commit dd4fada6b54b5f72d1a8a4852f386e57078a4b63 Author: João Távora Date: Thu May 10 12:07:31 2018 +0100 Shorten summary line to appease package-lint.el diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fbddf16c3f..21ec247967 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1,4 +1,4 @@ -;;; eglot.el --- A client for Language Server Protocol (LSP) servers -*- lexical-binding: t; -*- +;;; eglot.el --- Client for Language Server Protocol (LSP) servers -*- lexical-binding: t; -*- ;; Copyright (C) 2017 João Távora commit eac9d2917847f77431bdffefae751c99a15e1df8 Author: João Távora Date: Thu May 10 12:07:11 2018 +0100 More correctly setup rust-mode-related autoloads By autoloading the add-hook form and the eglot--setup-rls-idiosyncrasies definition, a user can start rust-mode without loading eglot.el along with it. * eglot.el (rust-mode-hook) (eglot--setup-rls-idiosyncrasies): Wrap in autoloaded progn. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index eba0e84f26..fbddf16c3f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1471,12 +1471,11 @@ Proceed? " (and (equal "Indexing" what) done)))) ;;;###autoload -(add-hook 'rust-mode-hook 'eglot--setup-rls-idiosyncrasies) - -;;;###autoload -(defun eglot--setup-rls-idiosyncrasies () - "RLS needs special treatment..." - (add-hook 'eglot--ready-predicates 'eglot--rls-probably-ready-for-p t t)) +(progn + (add-hook 'rust-mode-hook 'eglot--setup-rls-idiosyncrasies) + (defun eglot--setup-rls-idiosyncrasies () + "Prepare `eglot' to deal with RLS's special treatment." + (add-hook 'eglot--ready-predicates 'eglot--rls-probably-ready-for-p t t))) (cl-defun eglot--server-window/progress (process &key id done title message &allow-other-keys) commit d2fa8fea9aa71f9ef553b2a67946a33f208e5cb5 Author: João Távora Date: Wed May 9 23:24:15 2018 +0100 Add minimal headers, commentary and autoloads diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4739cea72f..eba0e84f26 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,8 +2,11 @@ ;; Copyright (C) 2017 João Távora +;; Version: 0.1 ;; Author: João Távora -;; Keywords: extensions +;; Url: https://github.com/joaotavora/eglot +;; Keywords: convenience, languages +;; Package-Requires: ((emacs "26.1")) ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by @@ -20,7 +23,8 @@ ;;; Commentary: -;; +;; M-x eglot in some file under some .git controlled dir should get +;; you started, but see README.md. ;;; Code: @@ -285,6 +289,7 @@ Enter program to execute (or :): " guessed-command)) t))) +;;;###autoload (defun eglot (managed-major-mode command &optional interactive) "Start a Language Server Protocol server. Server is started with COMMAND and manages buffers of @@ -1465,8 +1470,10 @@ Proceed? " (pcase-let ((`(,_id ,what ,done ,_detail) (eglot--spinner proc))) (and (equal "Indexing" what) done)))) +;;;###autoload (add-hook 'rust-mode-hook 'eglot--setup-rls-idiosyncrasies) +;;;###autoload (defun eglot--setup-rls-idiosyncrasies () "RLS needs special treatment..." (add-hook 'eglot--ready-predicates 'eglot--rls-probably-ready-for-p t t)) commit 040d3e78ea381c79829c031642c4cafdee6b1961 Author: João Távora Date: Wed May 9 23:12:19 2018 +0100 Fancier rls spinner * eglot.el (eglot--mode-line-format): Use (nth 3) of eglot--spinner. (eglot--server-window/progress): Save detail message in spinner. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9a0b8246c1..4739cea72f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -869,7 +869,7 @@ Uses THING, FACE, DEFS and PREPEND." (name (and (process-live-p proc) (eglot--short-name proc))) (pending (and proc (hash-table-count (eglot--pending-continuations proc)))) - (`(,_id ,doing ,done-p) (and proc (eglot--spinner proc))) + (`(,_id ,doing ,done-p ,detail) (and proc (eglot--spinner proc))) (`(,status ,serious-p) (and proc (eglot--status proc)))) (append `(,(eglot--mode-line-props "eglot" 'eglot-mode-line @@ -888,7 +888,9 @@ Uses THING, FACE, DEFS and PREPEND." (format "An error occured: %s\n" status)))) ,@(when (and doing (not done-p)) `("/" ,(eglot--mode-line-props - doing 'compilation-mode-line-run + (format "%s%s" doing + (if detail (format ":%s" detail) "")) + 'compilation-mode-line-run '((mouse-1 eglot-events-buffer "go to events buffer"))))) ,@(when (cl-plusp pending) `("/" ,(eglot--mode-line-props @@ -1460,7 +1462,7 @@ Proceed? " "Guess if the RLS running in PROC is ready for WHAT." (or (eq what :textDocument/completion) ; RLS normally ready for this ; one, even if building - (pcase-let ((`(,_id ,what ,done) (eglot--spinner proc))) + (pcase-let ((`(,_id ,what ,done ,_detail) (eglot--spinner proc))) (and (equal "Indexing" what) done)))) (add-hook 'rust-mode-hook 'eglot--setup-rls-idiosyncrasies) @@ -1470,9 +1472,9 @@ Proceed? " (add-hook 'eglot--ready-predicates 'eglot--rls-probably-ready-for-p t t)) (cl-defun eglot--server-window/progress - (process &key id done title &allow-other-keys) + (process &key id done title message &allow-other-keys) "Handle notification window/progress" - (setf (eglot--spinner process) (list id title done))) + (setf (eglot--spinner process) (list id title done message))) (provide 'eglot) ;;; eglot.el ends here commit d76cc9aea9c0e77bac28385050f14acff4b4e25f Author: João Távora Date: Wed May 9 22:41:37 2018 +0100 New "deferred requests" that wait until server is ready Calling textDocument/hover or textDocument/documentHighlight before the server has had a chance to process a textDocument/didChange is normally useless. The matter is worse for servers like RLS which only become ready much later and send a special notif for it (see https://github.com/rust-lang-nursery/rls/issues/725). So, keeping the same coding style add a DEFERRED arg to eglot--request that makes it maybe not run the function immediately. Add a bunch of logic for probing readiness of servers. * README.md: Update * eglot.el (eglot--deferred-actions): New process-local var. (eglot--process-filter): Call deferred actions. (eglot--request): Rewrite. (eglot--sync-request): Rewrite. (eglot--call-deferred, eglot--ready-predicates) (eglot--server-ready-p): New helpers. (eglot--signal-textDocument/didChange): Set spinner and call deferred actions. (eglot-completion-at-point): Pass DEFERRED to eglot-sync-request. (eglot-eldoc-function): Pass DEFERRED to eglot-request (eglot--rls-probably-ready-for-p): New helper. (rust-mode-hook): Add eglot--setup-rls-idiosyncrasies (eglot--setup-rls-idiosyncrasies): New helper. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d5eae031e0..9a0b8246c1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -135,6 +135,10 @@ A list (WHAT SERIOUS-P)." t) Either a list of strings (a shell command and arguments), or a list of a single string of the form :") +(eglot--define-process-var eglot--deferred-actions + (make-hash-table :test #'equal) + "Actions deferred to when server is thought to be ready.") + (defun eglot--make-process (name managed-major-mode contact) "Make a process from CONTACT. NAME is a name to give the inferior process or connection. @@ -442,7 +446,8 @@ INTERACTIVE is t if called interactively." (throw done :waiting-for-more-bytes-in-this-message)))))))) ;; Saved parsing state for next visit to this filter ;; - (setf (eglot--expected-bytes proc) expected-bytes)))))) + (setf (eglot--expected-bytes proc) expected-bytes)))) + (eglot--call-deferred proc))) (defun eglot-events-buffer (process &optional interactive) "Display events buffer for current LSP connection PROCESS. @@ -549,110 +554,118 @@ is a symbol saying if this is a client or server originated." (interactive (list (eglot--current-process-or-lose))) (setf (eglot--status process) nil)) -(cl-defun eglot--request (process - method - params - &key success-fn error-fn timeout-fn (async-p t) - (timeout eglot-request-timeout)) - "Make a request to PROCESS, expecting a reply. -Return the ID of this request, unless ASYNC-P is nil, in which -case never returns locally. Wait TIMEOUT seconds for a -response." - (let* ((id (eglot--next-request-id)) - (timeout-fn (or timeout-fn - (lambda () - (eglot--warn - "(request) Tired of waiting for reply to %s" id)))) - (error-fn (or error-fn - (cl-function - (lambda (&key code message &allow-other-keys) - (setf (eglot--status process) `(,message t)) - (eglot--warn - "(request) Request id=%s errored with code=%s: %s" - id code message))))) - (success-fn (or success-fn - (cl-function - (lambda (&rest result-body) - (eglot--debug - "(request) Request id=%s replied to with result=%s" - id result-body))))) - (catch-tag (cl-gensym (format "eglot--tag-%d-" id)))) - (eglot--process-send process - (eglot--obj :jsonrpc "2.0" - :id id - :method method - :params params)) - (catch catch-tag - (let ((timeout-timer - (run-with-timer - timeout nil - (if async-p - (lambda () - (remhash id (eglot--pending-continuations process)) - (funcall timeout-fn)) - (lambda () - (remhash id (eglot--pending-continuations process)) - (throw catch-tag (funcall timeout-fn))))))) - (puthash id - (list (if async-p - success-fn - (lambda (&rest args) - (throw catch-tag (apply success-fn args)))) - (if async-p - error-fn - (lambda (&rest args) - (throw catch-tag (apply error-fn args)))) - timeout-timer) - (eglot--pending-continuations process)) - (unless async-p - (unwind-protect - (while t - (unless (process-live-p process) - (cond ((eglot--moribund process) - (throw catch-tag (delete-process process))) - (t - (eglot--error - "(request) Proc %s died unexpectedly during request with code %s" - process - (process-exit-status process))))) - (accept-process-output nil 0.01)) - (when (memq timeout-timer timer-list) - (eglot--message - "(request) Last-change cancelling timer for continuation %s" id) - (cancel-timer timeout-timer)))))) - ;; Finally, return the id. - id)) +(defun eglot--call-deferred (proc) + "Call PROC's deferred actions, who may again defer themselves." + (let ((actions (hash-table-values (eglot--deferred-actions proc)))) + (eglot--log-event proc `(:running-deferred ,(length actions))) + (mapc #'funcall (mapcar #'car actions)))) + +(defvar eglot--ready-predicates '(eglot--server-ready-p) + "Special hook of predicates controlling deferred actions. +When one of these functions returns nil, a deferrable +`eglot--request' will be deferred. Each predicate is passed the +an symbol for the request request and a process object.") + +(defun eglot--server-ready-p (_what _proc) + "Tell if server of PROC ready for processing deferred WHAT." + (not (eglot--outstanding-edits-p))) (cl-defmacro eglot--lambda (cl-lambda-list &body body) (declare (indent 1) (debug (sexp &rest form))) `(cl-function (lambda ,cl-lambda-list ,@body))) -(defun eglot--sync-request (proc method params) +(cl-defun eglot--request (proc + method + params + &rest args + &key success-fn error-fn timeout-fn + (timeout eglot-request-timeout) + (deferred nil)) + "Make a request to PROCESS, expecting a reply. +Return the ID of this request. Wait TIMEOUT seconds for response. +If DEFERRED, maybe defer request to the future, or never at all, +in case a new request with identical DEFERRED and for the same +buffer overrides it. However, if that happens, the original +timeout keeps counting." + (let* ((id (eglot--next-request-id)) + (existing-timer nil) + (make-timeout + (lambda ( ) + (or existing-timer + (run-with-timer + timeout nil + (lambda () + (remhash id (eglot--pending-continuations proc)) + (funcall (or timeout-fn + (lambda () + (eglot--error + "Tired of waiting for reply to %s, id=%s" + method id)))))))))) + (when deferred + (let* ((buf (current-buffer)) + (existing (gethash (list deferred buf) (eglot--deferred-actions proc)))) + (when existing (setq existing-timer (cadr existing))) + (if (run-hook-with-args-until-failure 'eglot--ready-predicates + deferred proc) + (remhash (list deferred buf) (eglot--deferred-actions proc)) + (eglot--log-event proc `(:deferring ,method :id ,id :params ,params)) + (let* ((buf (current-buffer)) (point (point)) + (later (lambda () + (when (buffer-live-p buf) + (with-current-buffer buf + (save-excursion (goto-char point) + (apply #'eglot--request proc + method params args))))))) + (puthash (list deferred buf) (list later (funcall make-timeout)) + (eglot--deferred-actions proc)) + (cl-return-from eglot--request nil))))) + ;; Really run it + ;; + (puthash id + (list (or success-fn (eglot--lambda (&rest result-body) + (eglot--debug + "Request %s, id=%s replied to with result=%s" + method id result-body))) + (or error-fn (eglot--lambda + (&key code message &allow-other-keys) + (setf (eglot--status proc) `(,message t)) + (eglot--warn + "Request %s, id=%s errored with code=%s: %s" + method id code message))) + (funcall make-timeout)) + (eglot--pending-continuations proc)) + (eglot--process-send proc (eglot--obj :jsonrpc "2.0" + :id id + :method method + :params params)))) + +(defun eglot--sync-request (proc method params &optional deferred) "Like `eglot--request' for PROC, METHOD and PARAMS, but synchronous. -Meaning only return locally if successful, otherwise exit non-locally." - (let* ((timeout-error-sym (cl-gensym)) - (catch-tag (make-symbol "eglot--sync-request-catch-tag")) - (retval - (catch catch-tag - (eglot--request proc method params - :success-fn (lambda (&rest args) - (throw catch-tag (if (vectorp (car args)) - (car args) - args))) - :error-fn (eglot--lambda - (&key code message &allow-other-keys) - (eglot--error "Oops: %s: %s" code message)) - :timeout-fn (lambda () - (throw catch-tag timeout-error-sym)) - :async-p nil)))) - ;; FIXME: There's maybe an emacs bug here. Because timeout-fn runs - ;; in a timer, the better and obvious choice of throwing the erro - ;; in the lambda is not quitting the `accept-process-output' - ;; infinite loop up there. So use this contorted strategy with - ;; `cl-gensym'. - (if (eq retval timeout-error-sym) - (eglot--error "Tired of waiting for reply to sync request") - retval))) +Meaning only return locally if successful, otherwise exit non-locally. +DEFERRED is passed to `eglot--request', which see." + ;; Launching a deferred sync request with outstanding changes is a + ;; bad idea, since that might lead to the request never having a + ;; chance to run, because `eglot--ready-predicates'. + (when deferred (eglot--signal-textDocument/didChange)) + (let* ((done (make-symbol "eglot--sync-request-catch-tag")) + (res + (catch done (eglot--request + proc method params + :success-fn (lambda (&rest args) + (throw done (if (vectorp (car args)) + (car args) args))) + :error-fn (eglot--lambda + (&key code message &allow-other-keys) + (throw done + `(error ,(format "Oops: %s: %s" + code message)))) + :timeout-fn (lambda () + (throw done '(error "Timed out"))) + :deferred deferred) + ;; now spin, baby! + (while t (accept-process-output nil 0.01))))) + (when (and (listp res) (eq 'error (car res))) (eglot--error (cadr res))) + res)) (cl-defun eglot--notify (process method params) "Notify PROCESS of something, don't expect a reply.e" @@ -1113,7 +1126,9 @@ Records START, END and PRE-CHANGE-LENGTH locally." :end end-pos) :rangeLength len :text after-text)]))))) - (setq eglot--recent-changes (cons [] []))))) + (setq eglot--recent-changes (cons [] [])) + (setf (eglot--spinner proc) (list nil :textDocument/didChange t)) + (eglot--call-deferred proc)))) (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." @@ -1273,7 +1288,8 @@ DUMMY is ignored" (let* ((resp (eglot--sync-request proc :textDocument/completion - (eglot--current-buffer-TextDocumentPositionParams))) + (eglot--current-buffer-TextDocumentPositionParams) + :textDocument/completion)) (items (if (vectorp resp) resp (plist-get resp :items)))) (eglot--mapply (eglot--lambda (&key insertText label kind detail @@ -1311,7 +1327,8 @@ DUMMY is ignored" (if (vectorp contents) contents (list contents)) - "\n"))))) + "\n"))) + :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) (eglot--request proc :textDocument/documentHighlight position-params @@ -1331,7 +1348,8 @@ DUMMY is ignored" (overlay-put ov 'evaporate t) (overlay-put ov :kind kind) ov))) - highlights)))))))) + highlights))))) + :deferred :textDocument/documentHighlight))) nil) (defun eglot-imenu (oldfun) @@ -1438,6 +1456,19 @@ Proceed? " ;;; Rust-specific ;;; +(defun eglot--rls-probably-ready-for-p (what proc) + "Guess if the RLS running in PROC is ready for WHAT." + (or (eq what :textDocument/completion) ; RLS normally ready for this + ; one, even if building + (pcase-let ((`(,_id ,what ,done) (eglot--spinner proc))) + (and (equal "Indexing" what) done)))) + +(add-hook 'rust-mode-hook 'eglot--setup-rls-idiosyncrasies) + +(defun eglot--setup-rls-idiosyncrasies () + "RLS needs special treatment..." + (add-hook 'eglot--ready-predicates 'eglot--rls-probably-ready-for-p t t)) + (cl-defun eglot--server-window/progress (process &key id done title &allow-other-keys) "Handle notification window/progress" commit 0a9c14efad60e29ba7604e19f5822757e0c066d6 Author: João Távora Date: Wed May 9 22:28:29 2018 +0100 Call eglot-eldoc-function after completion finishes * eglot.el (eglot-completion-at-point): Call eglot-eldoc-function after completion finishes. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0795bc3e8c..d5eae031e0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1292,7 +1292,9 @@ DUMMY is ignored" (lambda (items) (sort items (lambda (a b) (string-lessp (get-text-property 0 :sortText a) - (get-text-property 0 :sortText b))))))))) + (get-text-property 0 :sortText b))))) + :exit-function + (lambda (_string _status) (eglot-eldoc-function)))))) (defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") commit 0aa29932a60ea837cf226da55ce3b984859af8d0 Author: João Távora Date: Wed May 9 22:26:02 2018 +0100 Simplify `eglot-shutdown` * eglot.el (eglot, eglot-reconnect) (eglot--managed-mode): Call new eglot-shutdown. (eglot-shutdown): Simplify. (eglot--process-sentinel): Also call error functions. (eglot--process-filter): Reindent. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bed98ed092..0795bc3e8c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -310,7 +310,7 @@ INTERACTIVE is t if called interactively." (y-or-n-p "[eglot] Live process found, reconnect instead? ")) (eglot-reconnect current-process interactive) (when (process-live-p current-process) - (eglot-shutdown current-process 'sync)) + (eglot-shutdown current-process)) (eglot--connect project managed-major-mode short-name @@ -327,7 +327,7 @@ managing `%s' buffers in project `%s'." INTERACTIVE is t if called interactively." (interactive (list (eglot--current-process-or-lose) t)) (when (process-live-p process) - (eglot-shutdown process 'sync interactive)) + (eglot-shutdown process interactive)) (eglot--connect (eglot--project process) (eglot--major-mode process) (eglot--short-name process) @@ -341,13 +341,12 @@ INTERACTIVE is t if called interactively." "Called with PROCESS undergoes CHANGE." (eglot--debug "(sentinel) Process state changed to %s" change) (when (not (process-live-p process)) - ;; Remember to cancel all timers + ;; Cancel timers and error any outstanding continuations ;; - (maphash (lambda (id triplet) - (cl-destructuring-bind (_success _error timeout) triplet - (eglot--message - "(sentinel) Cancelling timer for continuation %s" id) - (cancel-timer timeout))) + (maphash (lambda (_id triplet) + (cl-destructuring-bind (_success error timeout) triplet + (cancel-timer timeout) + (funcall error :code -1 :message (format "Server died")))) (eglot--pending-continuations process)) ;; Turn off `eglot--managed-mode' where appropriate. ;; @@ -400,46 +399,47 @@ INTERACTIVE is t if called interactively." (unwind-protect (catch done (while t - (cond ((not expected-bytes) - ;; Starting a new message - ;; - (setq expected-bytes - (and (search-forward-regexp - "\\(?:.*: .*\r\n\\)*Content-Length: \ + (cond + ((not expected-bytes) + ;; Starting a new message + ;; + (setq expected-bytes + (and (search-forward-regexp + "\\(?:.*: .*\r\n\\)*Content-Length: \ *\\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" - (+ (point) 100) - t) - (string-to-number (match-string 1)))) - (unless expected-bytes - (throw done :waiting-for-new-message))) - (t - ;; Attempt to complete a message body - ;; - (let ((available-bytes (- (position-bytes (process-mark proc)) - (position-bytes (point))))) - (cond - ((>= available-bytes - expected-bytes) - (let* ((message-end (byte-to-position - (+ (position-bytes (point)) - expected-bytes)))) - (unwind-protect - (save-restriction - (narrow-to-region (point) message-end) - (let* ((json-object-type 'plist) - (json-message (json-read))) - ;; Process content in another buffer, - ;; shielding buffer from tamper - ;; - (with-temp-buffer - (eglot--process-receive proc json-message)))) - (goto-char message-end) - (delete-region (point-min) (point)) - (setq expected-bytes nil)))) - (t - ;; Message is still incomplete - ;; - (throw done :waiting-for-more-bytes-in-this-message)))))))) + (+ (point) 100) + t) + (string-to-number (match-string 1)))) + (unless expected-bytes + (throw done :waiting-for-new-message))) + (t + ;; Attempt to complete a message body + ;; + (let ((available-bytes (- (position-bytes (process-mark proc)) + (position-bytes (point))))) + (cond + ((>= available-bytes + expected-bytes) + (let* ((message-end (byte-to-position + (+ (position-bytes (point)) + expected-bytes)))) + (unwind-protect + (save-restriction + (narrow-to-region (point) message-end) + (let* ((json-object-type 'plist) + (json-message (json-read))) + ;; Process content in another buffer, + ;; shielding buffer from tamper + ;; + (with-temp-buffer + (eglot--process-receive proc json-message)))) + (goto-char message-end) + (delete-region (point-min) (point)) + (setq expected-bytes nil)))) + (t + ;; Message is still incomplete + ;; + (throw done :waiting-for-more-bytes-in-this-message)))))))) ;; Saved parsing state for next visit to this filter ;; (setf (eglot--expected-bytes proc) expected-bytes)))))) @@ -798,7 +798,7 @@ Meaning only return locally if successful, otherwise exit non-locally." (remove-function (local imenu-create-index-function) #'eglot-imenu) (let ((proc (eglot--current-process))) (when (and (process-live-p proc) (y-or-n-p "[eglot] Kill server too? ")) - (eglot-shutdown proc nil t)))))) + (eglot-shutdown proc t)))))) (defun eglot--buffer-managed-p (&optional proc) "Tell if current buffer is managed by PROC." @@ -890,38 +890,24 @@ Uses THING, FACE, DEFS and PREPEND." ;;; Protocol implementation (Requests, notifications, etc) ;;; -(defun eglot-shutdown (process &optional sync interactive) - "Politely ask the server PROCESS to quit. -Forcefully quit it if it doesn't respond. -If SYNC, don't leave this function with the server still -running. INTERACTIVE is t if called interactively." - (interactive (list (eglot--current-process-or-lose) t t)) - (when interactive - (eglot--message "(eglot-shutdown) Asking %s politely to terminate" - process)) - (let ((brutal (lambda () - (eglot--warn "Brutally deleting existing process %s" - process) - (setf (eglot--moribund process) t) - (delete-process process)))) - (eglot--request - process :shutdown nil - :success-fn (lambda (&rest _anything) - (when interactive - (eglot--message "Now asking %s politely to exit" process)) - (setf (eglot--moribund process) t) - (eglot--request process - :exit - nil - :success-fn brutal - :async-p (not sync) - :error-fn brutal - :timeout-fn brutal - :timeout 3)) - :error-fn brutal - :async-p (not sync) - :timeout-fn brutal - :timeout 3))) +(defun eglot-shutdown (proc &optional interactive) + "Politely ask the server PROC to quit. +Forcefully quit it if it doesn't respond. Don't leave this +function with the server still running. INTERACTIVE is t if +called interactively." + (interactive (list (eglot--current-process-or-lose) t)) + (when interactive (eglot--message "Asking %s politely to terminate" proc)) + (unwind-protect + (let ((eglot-request-timeout 3)) + (setf (eglot--moribund proc) t) + (eglot--sync-request proc + :shutdown + nil) + ;; this one should always fail + (ignore-errors (eglot--sync-request proc :exit nil))) + (when (process-live-p proc) + (eglot--warn "Brutally deleting existing process %s" proc) + (delete-process proc)))) (cl-defun eglot--server-window/showMessage (_process &key type message) "Handle notification window/showMessage" commit dad1b764c05ccb4e558e551067bc996a7bebd274 Author: João Távora Date: Wed May 9 12:24:10 2018 +0100 Get rid of eglot-mode * eglot.el (eglot--managed-mode): Don't call eglot-mode. When shutting down, offer to kill server. (mode-line-misc-info): Update to use eglot--managed-mode diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3eb43cee96..bed98ed092 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -769,7 +769,6 @@ Meaning only return locally if successful, otherwise exit non-locally." nil nil eglot-mode-map (cond (eglot--managed-mode - (eglot-mode 1) (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'before-change-functions 'eglot--before-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) @@ -796,11 +795,10 @@ Meaning only return locally if successful, otherwise exit non-locally." (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) (remove-function (local 'eldoc-documentation-function) #'eglot-eldoc-function) - (remove-function (local imenu-create-index-function) #'eglot-imenu)))) - -(define-minor-mode eglot-mode - "Minor mode for all buffers managed by EGLOT in some way." nil - nil eglot-mode-map) + (remove-function (local imenu-create-index-function) #'eglot-imenu) + (let ((proc (eglot--current-process))) + (when (and (process-live-p proc) (y-or-n-p "[eglot] Kill server too? ")) + (eglot-shutdown proc nil t)))))) (defun eglot--buffer-managed-p (&optional proc) "Tell if current buffer is managed by PROC." @@ -886,7 +884,8 @@ Uses THING, FACE, DEFS and PREPEND." (mouse-3 eglot-clear-status "clear this status")) (format "%d pending requests\n" pending))))))))) -(add-to-list 'mode-line-misc-info `(eglot-mode (" [" eglot--mode-line-format "] "))) +(add-to-list 'mode-line-misc-info + `(eglot--managed-mode (" [" eglot--mode-line-format "] "))) ;;; Protocol implementation (Requests, notifications, etc) commit 54a59fc000f9a3040a4644b9f25a01d9fbfac3dc Author: João Távora Date: Wed May 9 10:58:28 2018 +0100 Simplify eglot--signal-textdocument/didchange * eglot.el (eglot--recent-before-changes) (eglot--recent-after-changes): Delete. (eglot--recent-changes): New var. (eglot--outstanding-edits-p, eglot--before-change) (eglot--after-change): Rewrite. (eglot--signal-textDocument/didChange): Rewrite. (eglot--signal-textDocument/didOpen): Initialize buffer-local eglot--recent-changes here. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 546671ee8b..3eb43cee96 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1043,17 +1043,12 @@ running. INTERACTIVE is t if called interactively." (eglot--obj :code -32001 :message (format "%s" err)))))) -(defvar eglot--recent-before-changes nil - "List of recent changes as collected by `eglot--before-change'.") -(defvar eglot--recent-after-changes nil - "List of recent changes as collected by `eglot--after-change'.") - -(defvar-local eglot--versioned-identifier 0) - (defun eglot--current-buffer-TextDocumentIdentifier () "Compute TextDocumentIdentifier object for current buffer." (eglot--obj :uri (eglot--path-to-uri buffer-file-name))) +(defvar-local eglot--versioned-identifier 0) + (defun eglot--current-buffer-VersionedTextDocumentIdentifier () "Compute VersionedTextDocumentIdentifier object for current buffer." (append (eglot--current-buffer-TextDocumentIdentifier) @@ -1077,78 +1072,67 @@ running. INTERACTIVE is t if called interactively." (eglot--obj :textDocument (eglot--current-buffer-TextDocumentIdentifier) :position (eglot--pos-to-lsp-position))) +(defvar-local eglot--recent-changes nil + "Recent buffer changes as collected by `eglot--before-change'.") + +(defun eglot--outstanding-edits-p () + "Non-nil if there are outstanding edits." + (cl-plusp (+ (length (car eglot--recent-changes)) + (length (cdr eglot--recent-changes))))) + (defun eglot--before-change (start end) "Hook onto `before-change-functions'. Records START and END, crucially convert them into LSP (line/char) positions before that information is lost (because the after-change thingy doesn't know if newlines were deleted/added)" - (push (list (eglot--pos-to-lsp-position start) - (eglot--pos-to-lsp-position end)) - eglot--recent-before-changes)) + (setf (car eglot--recent-changes) + (vconcat (car eglot--recent-changes) + `[(,(eglot--pos-to-lsp-position start) + ,(eglot--pos-to-lsp-position end))]))) (defun eglot--after-change (start end pre-change-length) "Hook onto `after-change-functions'. Records START, END and PRE-CHANGE-LENGTH locally." (cl-incf eglot--versioned-identifier) - (push (list start end pre-change-length - (buffer-substring-no-properties start end)) - eglot--recent-after-changes)) + (setf (cdr eglot--recent-changes) + (vconcat (cdr eglot--recent-changes) + `[(,pre-change-length + ,(buffer-substring-no-properties start end))]))) (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." - (unwind-protect - (when (or eglot--recent-before-changes - eglot--recent-after-changes) - (let* ((proc (eglot--current-process-or-lose)) - (sync-kind (plist-get (eglot--capabilities proc) - :textDocumentSync)) - (emacs-messup - (/= (length eglot--recent-before-changes) - (length eglot--recent-after-changes))) - (full-sync-p (or (eq sync-kind 1) emacs-messup))) - (when emacs-messup - (unless (eq sync-kind 1) - (eglot--warn "Using full sync because before: %s and after: %s" - eglot--recent-before-changes - eglot--recent-after-changes))) - (save-restriction - (widen) - (unless (or (not sync-kind) - (eq sync-kind 0)) - (eglot--notify - proc - :textDocument/didChange - (eglot--obj - :textDocument - (eglot--current-buffer-VersionedTextDocumentIdentifier) - :contentChanges - (if full-sync-p - (vector - (eglot--obj - :text (buffer-substring-no-properties (point-min) - (point-max)))) - (apply - #'vector - (mapcar - (pcase-lambda (`(,before-start-position - ,before-end-position - ,_after-start - ,_after-end - ,len - ,after-text)) - (eglot--obj :range (eglot--obj :start before-start-position - :end before-end-position) - :rangeLength len - :text after-text)) - (reverse (cl-mapcar 'append - eglot--recent-before-changes - eglot--recent-after-changes))))))))))) - (setq eglot--recent-before-changes nil - eglot--recent-after-changes nil))) + (when (eglot--outstanding-edits-p) + (let* ((proc (eglot--current-process-or-lose)) + (sync-kind (eglot--server-capable :textDocumentSync)) + (emacs-messup (/= (length (car eglot--recent-changes)) + (length (cdr eglot--recent-changes)))) + (full-sync-p (or (eq sync-kind 1) emacs-messup))) + (when emacs-messup + (eglot--warn "`eglot--recent-changes' messup: %s" eglot--recent-changes)) + (save-restriction + (widen) + (eglot--notify + proc :textDocument/didChange + (eglot--obj + :textDocument + (eglot--current-buffer-VersionedTextDocumentIdentifier) + :contentChanges + (if full-sync-p (vector + (eglot--obj + :text (buffer-substring-no-properties (point-min) + (point-max)))) + (cl-loop for (start-pos end-pos) across (car eglot--recent-changes) + for (len after-text) across (cdr eglot--recent-changes) + vconcat `[,(eglot--obj :range (eglot--obj :start start-pos + :end end-pos) + :rangeLength len + :text after-text)]))))) + (setq eglot--recent-changes (cons [] []))))) (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." + (setq eglot--recent-changes (cons [] [])) (eglot--notify (eglot--current-process-or-lose) :textDocument/didOpen (eglot--obj :textDocument commit 461d48a1d0414a2cc46ac565d75c791c117bd11d Author: João Távora Date: Wed May 9 02:43:47 2018 +0100 Fix odd bugs and tweak stuff * eglot.el (eglot--log-event): Insert before markers. (eglot--process-receive): Shave lines. (xref-backend-references): Use cl-return-from. (eglot--log-event): Simplify (eglot-completion-at-point): Saner annotation diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 37ad616a50..546671ee8b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -462,7 +462,7 @@ INTERACTIVE is t if called interactively." (when interactive (display-buffer buffer)) buffer)) -(defun eglot--log-event (proc message type) +(defun eglot--log-event (proc message &optional type) "Log an eglot-related event. PROC is the current process. MESSAGE is a JSON-like plist. TYPE is a symbol saying if this is a client or server originated." @@ -477,7 +477,7 @@ is a symbol saying if this is a client or server originated." ;; pyls keeps on sending these (t 'unexpected-thingy))) (type - (format "%s-%s" type subtype))) + (format "%s-%s" (or type :internal) subtype))) (goto-char (point-max)) (let ((msg (format "%s%s%s:\n%s\n" type @@ -486,7 +486,7 @@ is a symbol saying if this is a client or server originated." (pp-to-string message)))) (when error (setq msg (propertize msg 'face 'error))) - (insert msg))))) + (insert-before-markers msg))))) (defun eglot--process-receive (proc message) "Process MESSAGE from PROC." @@ -500,14 +500,12 @@ is a symbol saying if this is a client or server originated." (when err (setf (eglot--status proc) `(,err t))) (cond (method ;; a server notification or a server request - (let* ((handler-sym (intern (concat "eglot--server-" - method)))) + (let* ((handler-sym (intern (concat "eglot--server-" method)))) (if (functionp handler-sym) (apply handler-sym proc (append (plist-get message :params) (if id `(:id ,id)))) - (eglot--warn "No implementation of method %s yet" - method) + (eglot--warn "No implementation of method %s yet" method) (when id (eglot--reply proc id @@ -1264,7 +1262,8 @@ DUMMY is ignored" location-or-locations))) (cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) - (unless (eglot--server-capable :referencesProvider) (cl-return nil)) + (unless (eglot--server-capable :referencesProvider) + (cl-return-from xref-backend-references nil)) (let ((params (or (get-text-property 0 :textDocumentPositionParams identifier) (let ((rich (car (member identifier eglot--xref-known-symbols)))) @@ -1316,10 +1315,10 @@ DUMMY is ignored" :documentation documentation :sortText sortText)) items)))) :annotation-function - (lambda (what) (let ((detail (get-text-property 0 :detail what)) - (kind-name (get-text-property 0 :kind what))) - (concat (if detail (format " %s" detail) "") - (if kind-name (format " (%s)" kind-name) "")))) + (lambda (what) + (propertize (concat " " (or (get-text-property 0 :detail what) + (get-text-property 0 :kind what))) + 'face 'font-lock-function-name-face)) :display-sort-function (lambda (items) (sort items (lambda (a b) (string-lessp commit 038dd046bfde8be5f40976adf3856916afbcf5c7 Author: João Távora Date: Tue May 8 16:07:07 2018 +0100 Support workspace/applyedit * eglot.el (eglot--reply): Don't send result or error if not provided. (eglot--server-workspace/applyEdit): New server method. (eglot--apply-text-edits): Rework. (eglot--apply-workspace-edit): New helper. (eglot-rename): Simplify. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9ba87ef5bb..37ad616a50 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -664,10 +664,10 @@ Meaning only return locally if successful, otherwise exit non-locally." (cl-defun eglot--reply (process id &key result error) "Reply to PROCESS's request ID with MESSAGE." - (eglot--process-send process (eglot--obj :jsonrpc "2.0" - :id id - :result result - :error error))) + (eglot--process-send + process `(:jsonrpc "2.0" :id ,id + ,@(when result `(:result ,result)) + ,@(when error `(:error ,error))))) ;;; Helpers @@ -1031,6 +1031,20 @@ running. INTERACTIVE is t if called interactively." registrations) (eglot--reply proc id :result (eglot--obj :message "OK"))))) +(cl-defun eglot--server-workspace/applyEdit + (proc &key id _label edit) + "Handle notification client/registerCapability" + (condition-case err + (progn + (eglot--apply-workspace-edit edit 'confirm) + (eglot--reply proc id :result `(:applied ))) + (error + (eglot--reply proc id + :result `(:applied :json-false) + :error + (eglot--obj :code -32001 + :message (format "%s" err)))))) + (defvar eglot--recent-before-changes nil "List of recent changes as collected by `eglot--before-change'.") (defvar eglot--recent-after-changes nil @@ -1370,61 +1384,76 @@ DUMMY is ignored" entries)) (funcall oldfun))) -(defun eglot--apply-text-edits (uri edits proc &optional version) - "Apply the EDITS for buffer of URI and return it." - (let* ((path (eglot--uri-to-path uri)) - (buffer (and path - (find-file-noselect path)))) - (unless buffer - (eglot--error "Can't find `%s' to perform server edits")) - (with-current-buffer buffer - (unless (eq proc (eglot--current-process)) - (eglot--error "Buffer `%s' for `%s' isn't managed by %s" - (current-buffer) uri proc)) - (unless (or (not version) - (equal version eglot--versioned-identifier)) - (eglot--error "Edits on `%s' require version %d, you have %d" - uri version eglot--versioned-identifier)) - (eglot--mapply - (eglot--lambda (&key range newText) - (save-restriction - (widen) - (save-excursion - (let ((start (eglot--lsp-position-to-point (plist-get range :start)))) - (goto-char start) - (delete-region start - (eglot--lsp-position-to-point (plist-get range :end))) - (insert newText))))) - edits) - (eglot--message "%s: %s edits" (current-buffer) (length edits))) - buffer)) +(defun eglot--apply-text-edits (buffer edits &optional version) + "Apply the EDITS for BUFFER." + (with-current-buffer buffer + (unless (or (not version) + (equal version eglot--versioned-identifier)) + (eglot--error "Edits on `%s' require version %d, you have %d" + buffer version eglot--versioned-identifier)) + (eglot--mapply + (eglot--lambda (&key range newText) + (save-restriction + (widen) + (save-excursion + (let ((start (eglot--lsp-position-to-point (plist-get range :start)))) + (goto-char start) + (delete-region start + (eglot--lsp-position-to-point (plist-get range :end))) + (insert newText))))) + edits) + (eglot--message "%s: Performed %s edits" (current-buffer) (length edits)))) + +(defun eglot--apply-workspace-edit (wedit &optional confirm) + "Apply the workspace edit WEDIT. If CONFIRM, ask user first." + (let (prepared) + (cl-destructuring-bind (&key changes documentChanges) + wedit + (cl-loop + for change on documentChanges + do (push (cl-destructuring-bind (&key textDocument edits) change + (cl-destructuring-bind (&key uri version) textDocument + (list (eglot--uri-to-path uri) edits version))) + prepared)) + (cl-loop for (uri edits) on changes by #'cddr + do (push (list (eglot--uri-to-path uri) edits) prepared))) + (if (or confirm + (cl-notevery #'find-buffer-visiting + (mapcar #'car prepared))) + (unless (y-or-n-p + (format "[eglot] Server requests to edit %s files.\n %s\n\ +Proceed? " + (length prepared) + (mapconcat #'identity + (mapcar #'car prepared) + "\n "))) + (eglot--error "User cancelled server edit"))) + (unwind-protect + (let (edit) + (while (setq edit (car prepared)) + (cl-destructuring-bind (path edits &optional version) edit + (eglot--apply-text-edits (find-file-noselect path) + edits + version) + (pop prepared)))) + (if prepared + (eglot--warn "Caution: edits of files %s failed." + (mapcar #'car prepared)) + (eglot--message "Edit successful!"))))) (defun eglot-rename (newname) "Rename the current symbol to NEWNAME." (interactive - (list - (read-from-minibuffer (format "Rename `%s' to: " (symbol-at-point))))) + (list (read-from-minibuffer (format "Rename `%s' to: " (symbol-at-point))))) (unless (eglot--server-capable :renameProvider) (eglot--error "Server can't rename!")) - (let* ((proc (eglot--current-process-or-lose)) - (workspace-edit - (eglot--sync-request proc - :textDocument/rename - (append - (eglot--current-buffer-TextDocumentPositionParams) - (eglot--obj :newName newname)))) - performed) - (cl-destructuring-bind (&key changes documentChanges) - workspace-edit - (cl-loop for change on documentChanges - do (push - (cl-destructuring-bind (&key textDocument edits) change - (cl-destructuring-bind (&key uri version) textDocument - (eglot--apply-text-edits uri edits proc version))) - performed)) - (cl-loop for (uri edits) on changes by #'cddr - do (push (eglot--apply-text-edits uri edits proc) - performed))))) + (eglot--apply-workspace-edit + (eglot--sync-request (eglot--current-process-or-lose) + :textDocument/rename + (append + (eglot--current-buffer-TextDocumentPositionParams) + (eglot--obj :newName newname))) + current-prefix-arg)) ;;; Dynamic registration commit ca678a54c8f2634c6c68f26f25837777b1211e19 Author: João Távora Date: Tue May 8 13:37:35 2018 +0100 Support textdocument/rename * README.md: Mention rename support. * eglot.el (eglot--uri-to-path): Handle uri hidden in keywords. (eglot--apply-text-edits): New helper. (eglot-rename): New interactive command. (eglot--client-capabilities): Add rename capability. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c5c99f2e78..9ba87ef5bb 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -199,6 +199,7 @@ CONTACT is as `eglot--contact'. Returns a process object." :definition `(:dynamicRegistration :json-false) :documentSymbol `(:dynamicRegistration :json-false) :documentHighlight `(:dynamicRegistration :json-false) + :rename `(:dynamicRegistration :json-false) :publishDiagnostics `(:relatedInformation :json-false)) :experimental (eglot--obj))) @@ -729,6 +730,7 @@ Meaning only return locally if successful, otherwise exit non-locally." (defun eglot--uri-to-path (uri) "Convert URI to a file path." + (when (keywordp uri) (setq uri (substring (symbol-name uri) 1))) (url-filename (url-generic-parse-url (url-unhex-string uri)))) (defconst eglot--kind-names @@ -1368,6 +1370,62 @@ DUMMY is ignored" entries)) (funcall oldfun))) +(defun eglot--apply-text-edits (uri edits proc &optional version) + "Apply the EDITS for buffer of URI and return it." + (let* ((path (eglot--uri-to-path uri)) + (buffer (and path + (find-file-noselect path)))) + (unless buffer + (eglot--error "Can't find `%s' to perform server edits")) + (with-current-buffer buffer + (unless (eq proc (eglot--current-process)) + (eglot--error "Buffer `%s' for `%s' isn't managed by %s" + (current-buffer) uri proc)) + (unless (or (not version) + (equal version eglot--versioned-identifier)) + (eglot--error "Edits on `%s' require version %d, you have %d" + uri version eglot--versioned-identifier)) + (eglot--mapply + (eglot--lambda (&key range newText) + (save-restriction + (widen) + (save-excursion + (let ((start (eglot--lsp-position-to-point (plist-get range :start)))) + (goto-char start) + (delete-region start + (eglot--lsp-position-to-point (plist-get range :end))) + (insert newText))))) + edits) + (eglot--message "%s: %s edits" (current-buffer) (length edits))) + buffer)) + +(defun eglot-rename (newname) + "Rename the current symbol to NEWNAME." + (interactive + (list + (read-from-minibuffer (format "Rename `%s' to: " (symbol-at-point))))) + (unless (eglot--server-capable :renameProvider) + (eglot--error "Server can't rename!")) + (let* ((proc (eglot--current-process-or-lose)) + (workspace-edit + (eglot--sync-request proc + :textDocument/rename + (append + (eglot--current-buffer-TextDocumentPositionParams) + (eglot--obj :newName newname)))) + performed) + (cl-destructuring-bind (&key changes documentChanges) + workspace-edit + (cl-loop for change on documentChanges + do (push + (cl-destructuring-bind (&key textDocument edits) change + (cl-destructuring-bind (&key uri version) textDocument + (eglot--apply-text-edits uri edits proc version))) + performed)) + (cl-loop for (uri edits) on changes by #'cddr + do (push (eglot--apply-text-edits uri edits proc) + performed))))) + ;;; Dynamic registration ;;; commit 3d3c12faf230e63e1468a5283cfad28abef07649 Author: João Távora Date: Tue May 8 11:38:02 2018 +0100 Reasonable textdocument/documenthighlight support * README.md: Update. * eglot.el (eglot--current-buffer-TextDocumentPositionParams): New helper. (xref-backend-identifier-completion-table): Refactor a bit. (xref-backend-identifier-at-point): Use when-let and eglot--current-buffer-TextDocumentPositionParams (xref-backend-definitions, xref-backend-references): Refactor a bit. (eglot-completion-at-point): Use eglot--current-buffer-TextDocumentPositionParams (eglot-eldoc-function): Rewrite to handle textDocument/documentHighlight. (eglot--highlights): New variable. (eglot--client-capabilities): Update with support for documentHighlight. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 807df984af..c5c99f2e78 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -193,11 +193,12 @@ CONTACT is as `eglot--contact'. Returns a process object." :willSave t :willSaveWaitUntil :json-false :didSave t) - :completion `(:dynamicRegistration :json-false) - :hover `(:dynamicRegistration :json-false) - :references `(:dynamicRegistration :json-false) - :definition `(:dynamicRegistration :json-false) - :documentSymbol `(:dynamicRegistration :json-false) + :completion `(:dynamicRegistration :json-false) + :hover `(:dynamicRegistration :json-false) + :references `(:dynamicRegistration :json-false) + :definition `(:dynamicRegistration :json-false) + :documentSymbol `(:dynamicRegistration :json-false) + :documentHighlight `(:dynamicRegistration :json-false) :publishDiagnostics `(:relatedInformation :json-false)) :experimental (eglot--obj))) @@ -1057,6 +1058,11 @@ running. INTERACTIVE is t if called interactively." (widen) (buffer-substring-no-properties (point-min) (point-max)))))) +(defun eglot--current-buffer-TextDocumentPositionParams () + "Compute TextDocumentPositionParams." + (eglot--obj :textDocument (eglot--current-buffer-TextDocumentIdentifier) + :position (eglot--pos-to-lsp-position))) + (defun eglot--before-change (start end) "Hook onto `before-change-functions'. Records START and END, crucially convert them into @@ -1206,11 +1212,12 @@ DUMMY is ignored" (eglot--mapply (eglot--lambda (&key name kind location containerName) (propertize name - :position (plist-get - (plist-get location :range) - :start) + :textDocumentPositionParams + (eglot--obj :textDocument text-id + :position (plist-get + (plist-get location :range) + :start)) :locations (list location) - :textDocument text-id :kind kind :containerName containerName)) (eglot--sync-request proc @@ -1220,11 +1227,10 @@ DUMMY is ignored" (all-completions string eglot--xref-known-symbols)))))) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) - (let ((symatpt (symbol-at-point))) - (when symatpt - (propertize (symbol-name symatpt) - :textDocument (eglot--current-buffer-TextDocumentIdentifier) - :position (eglot--pos-to-lsp-position))))) + (when-let ((symatpt (symbol-at-point))) + (propertize (symbol-name symatpt) + :textDocumentPositionParams + (eglot--current-buffer-TextDocumentPositionParams)))) (cl-defmethod xref-backend-definitions ((_backend (eql eglot)) identifier) (let* ((rich-identifier @@ -1234,11 +1240,8 @@ DUMMY is ignored" (get-text-property 0 :locations rich-identifier) (eglot--sync-request (eglot--current-process-or-lose) :textDocument/definition - (eglot--obj - :textDocument - (get-text-property 0 :textDocument identifier) - :position - (get-text-property 0 :position identifier)))))) + (get-text-property + 0 :textDocumentPositionParams identifier))))) (eglot--mapply (eglot--lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) @@ -1246,26 +1249,21 @@ DUMMY is ignored" (cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) (unless (eglot--server-capable :referencesProvider) (cl-return nil)) - (let* ((identifier (if (get-text-property 0 :position identifier) - identifier - (car (member identifier eglot--xref-known-symbols)))) - (position - (and identifier (get-text-property 0 :position identifier))) - (textDocument - (and identifier (get-text-property 0 :textDocument identifier)))) - (unless (and position textDocument) - (eglot--error "Don't know where %s is in the workspace" identifier)) + (let ((params + (or (get-text-property 0 :textDocumentPositionParams identifier) + (let ((rich (car (member identifier eglot--xref-known-symbols)))) + (and rich (get-text-property 0 :textDocumentPositionParams rich)))))) + (unless params + (eglot--error "Don' know where %s is in the workspace!" identifier)) (eglot--mapply (eglot--lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) (eglot--sync-request (eglot--current-process-or-lose) :textDocument/references - (eglot--obj - :textDocument - textDocument - :position - position - :context (eglot--obj :includeDeclaration t)))))) + (append + params + (eglot--obj :context + (eglot--obj :includeDeclaration t))))))) (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) @@ -1291,9 +1289,7 @@ DUMMY is ignored" (let* ((resp (eglot--sync-request proc :textDocument/completion - (eglot--obj - :textDocument (eglot--current-buffer-TextDocumentIdentifier) - :position (eglot--pos-to-lsp-position)))) + (eglot--current-buffer-TextDocumentPositionParams))) (items (if (vectorp resp) resp (plist-get resp :items)))) (eglot--mapply (eglot--lambda (&key insertText label kind detail @@ -1314,21 +1310,42 @@ DUMMY is ignored" (get-text-property 0 :sortText a) (get-text-property 0 :sortText b))))))))) +(defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") + (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function." - (when (eglot--server-capable :hoverProvider) - (eglot--request (eglot--current-process-or-lose) - :textDocument/hover - (eglot--obj - :textDocument (eglot--current-buffer-TextDocumentIdentifier) - :position (eglot--pos-to-lsp-position)) - :success-fn (eglot--lambda (&key contents _range) - (eldoc-message - (mapconcat #'eglot--format-markup - (if (vectorp contents) - contents - (list contents)) - "\n"))))) + (let ((buffer (current-buffer)) + (proc (eglot--current-process-or-lose)) + (position-params (eglot--current-buffer-TextDocumentPositionParams))) + (when (eglot--server-capable :hoverProvider) + (eglot--request proc :textDocument/hover position-params + :success-fn (eglot--lambda (&key contents _range) + (eldoc-message + (mapconcat #'eglot--format-markup + (if (vectorp contents) + contents + (list contents)) + "\n"))))) + (when (eglot--server-capable :documentHighlightProvider) + (eglot--request + proc :textDocument/documentHighlight position-params + :success-fn (lambda (highlights) + (mapc #'delete-overlay eglot--highlights) + (setq eglot--highlights + (when (get-buffer-window buffer) + (with-current-buffer buffer + (eglot--mapply + (eglot--lambda (&key range kind) + (cl-destructuring-bind (&key start end) range + (let ((ov (make-overlay + (eglot--lsp-position-to-point start) + (eglot--lsp-position-to-point end) + buffer))) + (overlay-put ov 'face 'highlight) + (overlay-put ov 'evaporate t) + (overlay-put ov :kind kind) + ov))) + highlights)))))))) nil) (defun eglot-imenu (oldfun) commit dbe81138d6711aa7f6f02dda8ca644a92bdaea47 Author: João Távora Date: Tue May 8 02:05:03 2018 +0100 Fix odd bugs * eglot.el (eglot--process-receive, eglot--request): Set status to actual error message. (eglot--managed-mode): Manage imenu-create-index-function correctly. (eglot--mode-line-format): Print error status. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9ce7e261a3..807df984af 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -495,7 +495,7 @@ is a symbol saying if this is a client or server originated." (not method) (gethash id (eglot--pending-continuations proc))))) (eglot--log-event proc message 'server) - (when err (setf (eglot--status proc) '("error" t))) + (when err (setf (eglot--status proc) `(,err t))) (cond (method ;; a server notification or a server request (let* ((handler-sym (intern (concat "eglot--server-" @@ -566,7 +566,7 @@ response." (error-fn (or error-fn (cl-function (lambda (&key code message &allow-other-keys) - (setf (eglot--status process) '("error" t)) + (setf (eglot--status process) `(,message t)) (eglot--warn "(request) Request id=%s errored with code=%s: %s" id code message))))) @@ -780,7 +780,7 @@ Meaning only return locally if successful, otherwise exit non-locally." (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) (add-function :before-until (local 'eldoc-documentation-function) #'eglot-eldoc-function) - (advice-add imenu-create-index-function :around #'eglot-imenu) + (add-function :around (local imenu-create-index-function) #'eglot-imenu) (flymake-mode 1) (eldoc-mode 1)) (t @@ -795,7 +795,7 @@ Meaning only return locally if successful, otherwise exit non-locally." (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) (remove-function (local 'eldoc-documentation-function) #'eglot-eldoc-function) - (advice-remove imenu-create-index-function #'eglot-imenu)))) + (remove-function (local imenu-create-index-function) #'eglot-imenu)))) (define-minor-mode eglot-mode "Minor mode for all buffers managed by EGLOT in some way." nil @@ -870,9 +870,10 @@ Uses THING, FACE, DEFS and PREPEND." (mouse-3 eglot-reconnect "reconnect to server"))) ,@(when serious-p `("/" ,(eglot--mode-line-props - status 'compilation-mode-line-fail + "error" 'compilation-mode-line-fail '((mouse-1 eglot-events-buffer "go to events buffer") - (mouse-3 eglot-clear-status "clear this status"))))) + (mouse-3 eglot-clear-status "clear this status")) + (format "An error occured: %s\n" status)))) ,@(when (and doing (not done-p)) `("/" ,(eglot--mode-line-props doing 'compilation-mode-line-run @@ -881,7 +882,8 @@ Uses THING, FACE, DEFS and PREPEND." `("/" ,(eglot--mode-line-props (format "%d" pending) 'warning '((mouse-1 eglot-events-buffer "go to events buffer") - (mouse-3 eglot-clear-status "clear this status")))))))))) + (mouse-3 eglot-clear-status "clear this status")) + (format "%d pending requests\n" pending))))))))) (add-to-list 'mode-line-misc-info `(eglot-mode (" [" eglot--mode-line-format "] "))) commit 2c093aeb840f528270b3b3969e9506dd1f9a1ef2 Author: João Távora Date: Tue May 8 01:52:27 2018 +0100 When killing server, always wait 3 seconds * eglot.el (eglot--request): Accept TIMEOUT param. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2e409ef840..9ce7e261a3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -552,10 +552,12 @@ is a symbol saying if this is a client or server originated." (cl-defun eglot--request (process method params - &key success-fn error-fn timeout-fn (async-p t)) + &key success-fn error-fn timeout-fn (async-p t) + (timeout eglot-request-timeout)) "Make a request to PROCESS, expecting a reply. Return the ID of this request, unless ASYNC-P is nil, in which -case never returns locally." +case never returns locally. Wait TIMEOUT seconds for a +response." (let* ((id (eglot--next-request-id)) (timeout-fn (or timeout-fn (lambda () @@ -583,7 +585,7 @@ case never returns locally." (catch catch-tag (let ((timeout-timer (run-with-timer - eglot-request-timeout nil + timeout nil (if async-p (lambda () (remhash id (eglot--pending-continuations process)) @@ -912,10 +914,12 @@ running. INTERACTIVE is t if called interactively." :success-fn brutal :async-p (not sync) :error-fn brutal - :timeout-fn brutal)) + :timeout-fn brutal + :timeout 3)) :error-fn brutal :async-p (not sync) - :timeout-fn brutal))) + :timeout-fn brutal + :timeout 3))) (cl-defun eglot--server-window/showMessage (_process &key type message) "Handle notification window/showMessage" commit 378a8371d289696ddf614e8b37a76358b16b418d Author: João Távora Date: Tue May 8 01:46:30 2018 +0100 Try to fix some textdocument/completion bugs * eglot.el (eglot-completion-at-point): Rework slightly. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d7ea32977f..2e409ef840 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1274,13 +1274,13 @@ DUMMY is ignored" (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." - (let ((bounds (bounds-of-thing-at-point 'sexp)) + (let ((bounds (bounds-of-thing-at-point 'symbol)) (proc (eglot--current-process-or-lose))) (when (eglot--server-capable :completionProvider) (list (or (car bounds) (point)) (or (cdr bounds) (point)) - (completion-table-dynamic + (completion-table-with-cache (lambda (_ignored) (let* ((resp (eglot--sync-request proc @@ -1291,19 +1291,17 @@ DUMMY is ignored" (items (if (vectorp resp) resp (plist-get resp :items)))) (eglot--mapply (eglot--lambda (&key insertText label kind detail - documentation sortText) - (propertize insertText - :label label :kind kind :detail detail + documentation sortText &allow-other-keys) + (propertize (or insertText label) + :kind-name (cdr (assoc kind eglot--kind-names)) + :detail detail :documentation documentation :sortText sortText)) items)))) :annotation-function (lambda (what) (let ((detail (get-text-property 0 :detail what)) - (kind (get-text-property 0 :kind what))) - (format "%s%s" - detail - (if kind - (format " (%s)" (cdr (assoc kind eglot--kind-names))) - "")))) + (kind-name (get-text-property 0 :kind what))) + (concat (if detail (format " %s" detail) "") + (if kind-name (format " (%s)" kind-name) "")))) :display-sort-function (lambda (items) (sort items (lambda (a b) (string-lessp commit 6fd613042e32b14762e7dc2492e80603e51b90ae Author: João Távora Date: Mon May 7 23:43:03 2018 +0100 Half-decent imenu support via textdocument/documentsymbol * README.md: Update capability * eglot.el (eglot--lsp-position-to-point): New function. (eglot--managed-mode): Handle imenu-create-index-function. (eglot--server-textDocument/publishDiagnostics): Use eglot--lsp-position-to-point. (eglot-imenu): New function. (eglot--client-capabilities): Capable of documentSymbol. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7cc375667d..d7ea32977f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -193,10 +193,11 @@ CONTACT is as `eglot--contact'. Returns a process object." :willSave t :willSaveWaitUntil :json-false :didSave t) - :completion `(:dynamicRegistration :json-false) - :hover `(:dynamicRegistration :json-false) - :references `(:dynamicRegistration :json-false) - :definition `(:dynamicRegistration :json-false) + :completion `(:dynamicRegistration :json-false) + :hover `(:dynamicRegistration :json-false) + :references `(:dynamicRegistration :json-false) + :definition `(:dynamicRegistration :json-false) + :documentSymbol `(:dynamicRegistration :json-false) :publishDiagnostics `(:relatedInformation :json-false)) :experimental (eglot--obj))) @@ -703,6 +704,17 @@ Meaning only return locally if successful, otherwise exit non-locally." (- (goto-char (or pos (point))) (line-beginning-position))))) +(defun eglot--lsp-position-to-point (pos-plist) + "Convert LSP position POS-PLIST to Emacs point." + (save-excursion (goto-char (point-min)) + (forward-line (plist-get pos-plist :line)) + (forward-char + (min (plist-get pos-plist :character) + (- (line-end-position) + (line-beginning-position)))) + (point))) + + (defun eglot--mapply (fun seq) "Apply FUN to every element of SEQ." (mapcar (lambda (e) (apply fun e)) seq)) @@ -766,6 +778,7 @@ Meaning only return locally if successful, otherwise exit non-locally." (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) (add-function :before-until (local 'eldoc-documentation-function) #'eglot-eldoc-function) + (advice-add imenu-create-index-function :around #'eglot-imenu) (flymake-mode 1) (eldoc-mode 1)) (t @@ -779,7 +792,8 @@ Meaning only return locally if successful, otherwise exit non-locally." (remove-hook 'xref-backend-functions 'eglot-xref-backend t) (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) (remove-function (local 'eldoc-documentation-function) - #'eglot-eldoc-function)))) + #'eglot-eldoc-function) + (advice-remove imenu-create-index-function #'eglot-imenu)))) (define-minor-mode eglot-mode "Minor mode for all buffers managed by EGLOT in some way." nil @@ -956,36 +970,28 @@ running. INTERACTIVE is t if called interactively." (cond (buffer (with-current-buffer buffer - (cl-flet ((pos-at (pos-plist) - (save-excursion (goto-char (point-min)) - (forward-line (plist-get pos-plist :line)) - (forward-char - (min (plist-get pos-plist :character) - (- (line-end-position) - (line-beginning-position)))) - (point)))) - (cl-loop for diag-spec across diagnostics - collect (cl-destructuring-bind (&key range severity _group - _code source message) - diag-spec - (cl-destructuring-bind (&key start end) - range - (let* ((begin-pos (pos-at start)) - (end-pos (pos-at end))) - (flymake-make-diagnostic - (current-buffer) - begin-pos end-pos - (cond ((<= severity 1) :error) - ((= severity 2) :warning) - (t :note)) - (concat source ": " message))))) - into diags - finally - (if eglot--current-flymake-report-fn - (funcall eglot--current-flymake-report-fn - diags) - (setq eglot--unreported-diagnostics - diags)))))) + (cl-loop + for diag-spec across diagnostics + collect (cl-destructuring-bind (&key range severity _group + _code source message) + diag-spec + (cl-destructuring-bind (&key start end) + range + (let* ((begin-pos (eglot--lsp-position-to-point start)) + (end-pos (eglot--lsp-position-to-point end))) + (flymake-make-diagnostic + (current-buffer) + begin-pos end-pos + (cond ((<= severity 1) :error) + ((= severity 2) :warning) + (t :note)) + (concat source ": " message))))) + into diags + finally (if eglot--current-flymake-report-fn + (funcall eglot--current-flymake-report-fn + diags) + (setq eglot--unreported-diagnostics + diags))))) (t (eglot--message "OK so %s isn't visited" filename))))) @@ -1321,6 +1327,26 @@ DUMMY is ignored" "\n"))))) nil) +(defun eglot-imenu (oldfun) + "EGLOT's `imenu-create-index-function' overriding OLDFUN." + (if (eglot--server-capable :documentSymbolProvider) + (let ((entries + (eglot--mapply + (eglot--lambda (&key name kind location _containerName) + (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) + (eglot--lsp-position-to-point + (plist-get (plist-get location :range) :start)))) + (eglot--sync-request + (eglot--current-process-or-lose) + :textDocument/documentSymbol + (eglot--obj + :textDocument (eglot--current-buffer-TextDocumentIdentifier)))))) + (append + (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) + entries) + entries)) + (funcall oldfun))) + ;;; Dynamic registration ;;; commit fe01515f05c60b97f312b3b6b72c5540eb518107 Author: João Távora Date: Mon May 7 22:56:20 2018 +0100 Only request stuff that server says it's capable of * eglot.el (eglot--server-capable): New helper. (eglot-xref-backend) (xref-backend-identifier-completion-table) (xref-backend-references, xref-backend-apropos) (eglot-completion-at-point, eglot-eldoc-function): Use it. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 02439dfa04..7cc375667d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -740,6 +740,10 @@ Meaning only return locally if successful, otherwise exit non-locally." (font-lock-ensure) (buffer-string))))) +(defun eglot--server-capable (feat) + "Determine if current server is capable of FEAT." + (plist-get (eglot--capabilities (eglot--current-process-or-lose)) feat)) + ;;; Minor modes ;;; @@ -1158,7 +1162,9 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." ;; make the server report new diagnostics. (eglot--signal-textDocument/didChange)) -(defun eglot-xref-backend () "EGLOT xref backend." 'eglot) +(defun eglot-xref-backend () + "EGLOT xref backend." + (when (eglot--server-capable :definitionProvider) 'eglot)) (defvar eglot--xref-known-symbols nil) @@ -1179,26 +1185,27 @@ DUMMY is ignored" (plist-get position :character)))) (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) - (let ((proc (eglot--current-process-or-lose)) - (text-id (eglot--current-buffer-TextDocumentIdentifier))) - (completion-table-with-cache - (lambda (string) - (setq eglot--xref-known-symbols - (eglot--mapply - (eglot--lambda (&key name kind location containerName) - (propertize name - :position (plist-get - (plist-get location :range) - :start) - :locations (list location) - :textDocument text-id - :kind kind - :containerName containerName)) - (eglot--sync-request proc - :textDocument/documentSymbol - (eglot--obj - :textDocument text-id)))) - (all-completions string eglot--xref-known-symbols))))) + (when (eglot--server-capable :documentSymbolProvider) + (let ((proc (eglot--current-process-or-lose)) + (text-id (eglot--current-buffer-TextDocumentIdentifier))) + (completion-table-with-cache + (lambda (string) + (setq eglot--xref-known-symbols + (eglot--mapply + (eglot--lambda (&key name kind location containerName) + (propertize name + :position (plist-get + (plist-get location :range) + :start) + :locations (list location) + :textDocument text-id + :kind kind + :containerName containerName)) + (eglot--sync-request proc + :textDocument/documentSymbol + (eglot--obj + :textDocument text-id)))) + (all-completions string eglot--xref-known-symbols)))))) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) (let ((symatpt (symbol-at-point))) @@ -1226,6 +1233,7 @@ DUMMY is ignored" location-or-locations))) (cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) + (unless (eglot--server-capable :referencesProvider) (cl-return nil)) (let* ((identifier (if (get-text-property 0 :position identifier) identifier (car (member identifier eglot--xref-known-symbols)))) @@ -1234,8 +1242,7 @@ DUMMY is ignored" (textDocument (and identifier (get-text-property 0 :textDocument identifier)))) (unless (and position textDocument) - (eglot--error "Sorry, can't discover where %s is in the workspace" - identifier)) + (eglot--error "Don't know where %s is in the workspace" identifier)) (eglot--mapply (eglot--lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) @@ -1249,21 +1256,21 @@ DUMMY is ignored" :context (eglot--obj :includeDeclaration t)))))) (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) - (eglot--mapply - (eglot--lambda (&key name location &allow-other-keys) - (let ((range (plist-get location :range)) - (uri (plist-get location :uri))) - (eglot--xref-make name uri (plist-get range :start)))) - (eglot--sync-request (eglot--current-process-or-lose) - :workspace/symbol - (eglot--obj :query pattern)))) + (when (eglot--server-capable :workspaceSymbolProvider) + (eglot--mapply + (eglot--lambda (&key name location &allow-other-keys) + (let ((range (plist-get location :range)) + (uri (plist-get location :uri))) + (eglot--xref-make name uri (plist-get range :start)))) + (eglot--sync-request (eglot--current-process-or-lose) + :workspace/symbol + (eglot--obj :query pattern))))) (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." (let ((bounds (bounds-of-thing-at-point 'sexp)) (proc (eglot--current-process-or-lose))) - (when (plist-get (eglot--capabilities proc) - :completionProvider) + (when (eglot--server-capable :completionProvider) (list (or (car bounds) (point)) (or (cdr bounds) (point)) @@ -1299,18 +1306,19 @@ DUMMY is ignored" (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function." - (eglot--request (eglot--current-process-or-lose) - :textDocument/hover - (eglot--obj - :textDocument (eglot--current-buffer-TextDocumentIdentifier) - :position (eglot--pos-to-lsp-position)) - :success-fn (eglot--lambda (&key contents _range) - (eldoc-message - (mapconcat #'eglot--format-markup - (if (vectorp contents) - contents - (list contents)) - "\n")))) + (when (eglot--server-capable :hoverProvider) + (eglot--request (eglot--current-process-or-lose) + :textDocument/hover + (eglot--obj + :textDocument (eglot--current-buffer-TextDocumentIdentifier) + :position (eglot--pos-to-lsp-position)) + :success-fn (eglot--lambda (&key contents _range) + (eldoc-message + (mapconcat #'eglot--format-markup + (if (vectorp contents) + contents + (list contents)) + "\n"))))) nil) commit 3299a6a4b6d7f7c907fbd6a22f249bbdd3174810 Author: João Távora Date: Mon May 7 22:40:53 2018 +0100 Simplify mode-line code with a helper. * eglot.el (eglot--mdoe-line-props): New helper. (eglot--mode-line-format): Use it. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a600f4787b..02439dfa04 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -817,93 +817,51 @@ that case, also signal textDocument/didOpen." (with-selected-window (posn-window (event-start event)) (call-interactively what)))) +(defun eglot--mode-line-props (thing face defs &optional prepend) + "Helper for function `eglot--mode-line-format'. +Uses THING, FACE, DEFS and PREPEND." + (cl-loop with map = (make-sparse-keymap) + for (elem . rest) on defs + for (key def help) = elem + do (define-key map `[mode-line ,key] (eglot--mode-line-call def)) + concat (format "%s: %s" key help) into blurb + when rest concat "\n" into blurb + finally (return `(:propertize ,thing + face ,face + keymap ,map help-echo ,(concat prepend blurb) + mouse-face mode-line-highlight)))) + (defun eglot--mode-line-format () - "Compose the mode-line format spec." + "Compose the EGLOT's mode-line." (pcase-let* ((proc (eglot--current-process)) - (name (and proc - (process-live-p proc) - (eglot--short-name proc))) - (pending (and proc - (hash-table-count - (eglot--pending-continuations proc)))) - (`(,_id ,doing ,done-p) - (and proc - (eglot--spinner proc))) - (`(,status ,serious-p) - (and proc - (eglot--status proc)))) + (name (and (process-live-p proc) (eglot--short-name proc))) + (pending (and proc (hash-table-count + (eglot--pending-continuations proc)))) + (`(,_id ,doing ,done-p) (and proc (eglot--spinner proc))) + (`(,status ,serious-p) (and proc (eglot--status proc)))) (append - `((:propertize "eglot" - face eglot-mode-line - keymap ,(let ((map (make-sparse-keymap))) - (define-key map [mode-line down-mouse-1] - (eglot--mode-line-call 'eglot-menu)) - map) - mouse-face mode-line-highlight - help-echo "mouse-1: pop-up EGLOT menu" - )) + `(,(eglot--mode-line-props "eglot" 'eglot-mode-line + '((down-mouse-1 eglot-menu "pop up EGLOT menu")))) (when name - `(":" - (:propertize - ,name - face eglot-mode-line - keymap ,(let ((map (make-sparse-keymap))) - (define-key map [mode-line mouse-1] - (eglot--mode-line-call 'eglot-events-buffer)) - (define-key map [mode-line mouse-2] - (eglot--mode-line-call 'eglot-shutdown)) - (define-key map [mode-line mouse-3] - (eglot--mode-line-call 'eglot-reconnect)) - map) - mouse-face mode-line-highlight - help-echo ,(concat "mouse-1: go to events buffer\n" - "mouse-2: quit server\n" - "mouse-3: reconnect to server")) + `(":" ,(eglot--mode-line-props + name 'eglot-mode-line + '((mouse-1 eglot-events-buffer "go to events buffer") + (mouse-2 eglot-shutdown "quit server") + (mouse-3 eglot-reconnect "reconnect to server"))) ,@(when serious-p - `("/" - (:propertize - ,status - help-echo ,(concat "mouse-1: go to events buffer\n" - "mouse-3: clear this status") - mouse-face mode-line-highlight - face compilation-mode-line-fail - keymap ,(let ((map (make-sparse-keymap))) - (define-key map [mode-line mouse-1] - (eglot--mode-line-call 'eglot-events-buffer)) - (define-key map [mode-line mouse-3] - (eglot--mode-line-call 'eglot-clear-status)) - map)))) + `("/" ,(eglot--mode-line-props + status 'compilation-mode-line-fail + '((mouse-1 eglot-events-buffer "go to events buffer") + (mouse-3 eglot-clear-status "clear this status"))))) ,@(when (and doing (not done-p)) - `("/" - (:propertize - ,doing - help-echo ,(concat "mouse-1: go to events buffer") - mouse-face mode-line-highlight - face compilation-mode-line-run - keymap ,(let ((map (make-sparse-keymap))) - (define-key map [mode-line mouse-1] - (eglot--mode-line-call 'eglot-events-buffer)) - map)))) + `("/" ,(eglot--mode-line-props + doing 'compilation-mode-line-run + '((mouse-1 eglot-events-buffer "go to events buffer"))))) ,@(when (cl-plusp pending) - `("/" - (:propertize - (format "%d" pending) - help-echo ,(format - "%s unanswered requests\n%s" - pending - (concat "mouse-1: go to events buffer" - "mouse-3: forget pending continuations")) - mouse-face mode-line-highlight - face ,(cond ((and pending (cl-plusp pending)) - 'warning) - (t - 'eglot-mode-line)) - keymap ,(let ((map (make-sparse-keymap))) - (define-key map [mode-line mouse-1] - (eglot--mode-line-call 'eglot-events-buffer)) - (define-key map [mode-line mouse-3] - (eglot--mode-line-call 'eglot-forget-pending-continuations)) - map))))))))) + `("/" ,(eglot--mode-line-props + (format "%d" pending) 'warning + '((mouse-1 eglot-events-buffer "go to events buffer") + (mouse-3 eglot-clear-status "clear this status")))))))))) (add-to-list 'mode-line-misc-info `(eglot-mode (" [" eglot--mode-line-format "] "))) commit ea51ade3a22979c5edca80042578daaaf78ff1c6 Author: João Távora Date: Mon May 7 22:16:28 2018 +0100 Get rid of eglot--buffer-open-count Hasn't really proved useful yet. * eglot.el (eglot--buffer-open-count): Remove. (eglot--signal-textDocument/didOpen) (eglot--signal-textDocument/didClose): Simplify. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1318feee51..a600f4787b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -135,9 +135,6 @@ A list (WHAT SERIOUS-P)." t) Either a list of strings (a shell command and arguments), or a list of a single string of the form :") -(eglot--define-process-var eglot--buffer-open-count (make-hash-table) - "Keeps track of didOpen/didClose notifs for each buffer.") - (defun eglot--make-process (name managed-major-mode contact) "Make a process from CONTACT. NAME is a name to give the inferior process or connection. @@ -1158,33 +1155,17 @@ Records START, END and PRE-CHANGE-LENGTH locally." (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." - (let* ((proc (eglot--current-process-or-lose)) - (count (1+ (or (gethash (current-buffer) - (eglot--buffer-open-count proc)) - 0)))) - (when (> count 1) - (eglot--error "Too many textDocument/didOpen notifs for %s" (current-buffer))) - (setf (gethash (current-buffer) (eglot--buffer-open-count proc)) - count) - (eglot--notify proc - :textDocument/didOpen - (eglot--obj :textDocument - (eglot--current-buffer-TextDocumentItem))))) + (eglot--notify (eglot--current-process-or-lose) + :textDocument/didOpen + (eglot--obj :textDocument + (eglot--current-buffer-TextDocumentItem)))) (defun eglot--signal-textDocument/didClose () "Send textDocument/didClose to server." - (let* ((proc (eglot--current-process-or-lose)) - (count (1- (or (gethash (current-buffer) - (eglot--buffer-open-count proc)) - 0)))) - (when (< count 0) - (eglot--error "Too many textDocument/didClose notifs for %s" (current-buffer))) - (setf (gethash (current-buffer) (eglot--buffer-open-count proc)) - count) - (eglot--notify proc - :textDocument/didClose - (eglot--obj :textDocument - (eglot--current-buffer-TextDocumentIdentifier))))) + (eglot--notify (eglot--current-process-or-lose) + :textDocument/didClose + (eglot--obj :textDocument + (eglot--current-buffer-TextDocumentIdentifier)))) (defun eglot--signal-textDocument/willSave () "Send textDocument/willSave to server." commit 44cdd8062bd1d40caa76e91964a49e7acf3ae56c Author: João Távora Date: Mon May 7 22:15:21 2018 +0100 Get rid of eglot--special-buffer-process Hasn't really proved useful yet. * eglot.el (eglot--special-buffer-process): Delete. (eglot--current-process): Simplify. (eglot--events-buffer): Simplify. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ba3b5bee71..1318feee51 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -61,19 +61,11 @@ (defvar eglot--processes-by-project (make-hash-table :test #'equal) "Keys are projects. Values are lists of processes.") -(defvar-local eglot--special-buffer-process nil - "Current buffer's eglot process.") - (defun eglot--current-process () "The current logical EGLOT process." - (or eglot--special-buffer-process - (let* ((cur (project-current)) - (processes - (and cur - (gethash cur eglot--processes-by-project)))) - (cl-find major-mode - processes - :key #'eglot--major-mode)))) + (let* ((cur (project-current)) + (processes (and cur (gethash cur eglot--processes-by-project)))) + (cl-find major-mode processes :key #'eglot--major-mode))) (defun eglot--current-process-or-lose () "Return the current EGLOT process or error." @@ -465,9 +457,7 @@ INTERACTIVE is t if called interactively." (with-current-buffer buffer (buffer-disable-undo) (read-only-mode t) - (setf (eglot--events-buffer process) buffer - eglot--special-buffer-process process) - (eglot-mode)) + (setf (eglot--events-buffer process) buffer)) buffer)))) (when interactive (display-buffer buffer)) buffer)) commit f1b7d1d15c8c5a99267ef398d58eb6b862d76ae0 Author: João Távora Date: Mon May 7 22:14:06 2018 +0100 * eglot.el: reformat to shave off some lines. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9d7b16d39f..ba3b5bee71 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -43,10 +43,9 @@ :prefix "eglot-" :group 'applications) -(defvar eglot-executables - '((rust-mode . ("rls")) - (python-mode . ("pyls")) - (js-mode . ("javascript-typescript-stdio"))) +(defvar eglot-executables '((rust-mode . ("rls")) + (python-mode . ("pyls")) + (js-mode . ("javascript-typescript-stdio"))) "Alist mapping major modes to server executables.") (defface eglot-mode-line @@ -80,8 +79,7 @@ "Return the current EGLOT process or error." (or (eglot--current-process) (eglot--error "No current EGLOT process%s" - (if (project-current) "" - " (Also no current project)")))) + (if (project-current) "" " (Also no current project)")))) (defmacro eglot--define-process-var (var-sym initval &optional doc mode-line-update-p) @@ -166,13 +164,12 @@ CONTACT is as `eglot--contact'. Returns a process object." (match-string 1 singleton) (string-to-number (match-string 2 singleton))) - (make-process - :name readable-name - :buffer buffer - :command contact - :connection-type 'pipe - :stderr (get-buffer-create (format "*%s stderr*" - name)))))) + (make-process :name readable-name + :buffer buffer + :command contact + :connection-type 'pipe + :stderr (get-buffer-create (format "*%s stderr*" + name)))))) (set-process-filter proc #'eglot--process-filter) (set-process-sentinel proc #'eglot--process-sentinel) proc)) @@ -186,9 +183,7 @@ CONTACT is as `eglot--contact'. Returns a process object." (defun eglot--project-short-name (project) "Give PROJECT a short name." - (file-name-base - (directory-file-name - (car (project-roots project))))) + (file-name-base (directory-file-name (car (project-roots project))))) (defun eglot--all-major-modes () "Return all know major modes." @@ -202,14 +197,7 @@ CONTACT is as `eglot--contact'. Returns a process object." "What the EGLOT LSP client supports." (eglot--obj :workspace (eglot--obj - :applyEdit nil - :workspaceEdit nil - :didChangeConfiguration nil - :didChangeWatchedFiles nil - :symbol nil - :executeCommand nil - :workspaceFolders nil - :configuration nil) + :symbol `(:dynamicRegistration :json-false)) :textDocument (eglot--obj :synchronization (eglot--obj :dynamicRegistration :json-false @@ -277,8 +265,7 @@ SUCCESS-FN with no args if all goes well." (intern (completing-read "[eglot] Start a server to manage buffers of what major mode? " - (mapcar #'symbol-name - (eglot--all-major-modes)) nil t + (mapcar #'symbol-name (eglot--all-major-modes)) nil t (symbol-name major-mode) nil (symbol-name major-mode) nil))) (t major-mode))) @@ -332,17 +319,16 @@ INTERACTIVE is t if called interactively." (eglot-reconnect current-process interactive) (when (process-live-p current-process) (eglot-shutdown current-process 'sync)) - (eglot--connect - project - managed-major-mode - short-name - command - (lambda (proc) - (eglot--message "Connected! Process `%s' now managing `%s' \ -buffers in project `%s'." - proc - managed-major-mode - short-name))))))) + (eglot--connect project + managed-major-mode + short-name + command + (lambda (proc) + (eglot--message "Connected! Process `%s' now \ +managing `%s' buffers in project `%s'." + proc + managed-major-mode + short-name))))))) (defun eglot-reconnect (process &optional interactive) "Reconnect to PROCESS. @@ -350,12 +336,11 @@ INTERACTIVE is t if called interactively." (interactive (list (eglot--current-process-or-lose) t)) (when (process-live-p process) (eglot-shutdown process 'sync interactive)) - (eglot--connect - (eglot--project process) - (eglot--major-mode process) - (eglot--short-name process) - (eglot--contact process) - (lambda (_proc) (eglot--message "Reconnected!")))) + (eglot--connect (eglot--project process) + (eglot--major-mode process) + (eglot--short-name process) + (eglot--contact process) + (lambda (_proc) (eglot--message "Reconnected!")))) (defvar eglot--inhibit-auto-reconnect nil "If non-nil, don't autoreconnect on unexpected quit.") @@ -484,8 +469,7 @@ INTERACTIVE is t if called interactively." eglot--special-buffer-process process) (eglot-mode)) buffer)))) - (when interactive - (display-buffer buffer)) + (when interactive (display-buffer buffer)) buffer)) (defun eglot--log-event (proc message type) @@ -585,26 +569,23 @@ is a symbol saying if this is a client or server originated." Return the ID of this request, unless ASYNC-P is nil, in which case never returns locally." (let* ((id (eglot--next-request-id)) - (timeout-fn - (or timeout-fn - (lambda () - (eglot--warn - "(request) Tired of waiting for reply to %s" id)))) - (error-fn - (or error-fn - (cl-function - (lambda (&key code message &allow-other-keys) - (setf (eglot--status process) '("error" t)) - (eglot--warn - "(request) Request id=%s errored with code=%s: %s" - id code message))))) - (success-fn - (or success-fn - (cl-function - (lambda (&rest result-body) - (eglot--debug - "(request) Request id=%s replied to with result=%s: %s" - id result-body))))) + (timeout-fn (or timeout-fn + (lambda () + (eglot--warn + "(request) Tired of waiting for reply to %s" id)))) + (error-fn (or error-fn + (cl-function + (lambda (&key code message &allow-other-keys) + (setf (eglot--status process) '("error" t)) + (eglot--warn + "(request) Request id=%s errored with code=%s: %s" + id code message))))) + (success-fn (or success-fn + (cl-function + (lambda (&rest result-body) + (eglot--debug + "(request) Request id=%s replied to with result=%s" + id result-body))))) (catch-tag (cl-gensym (format "eglot--tag-%d-" id)))) (eglot--process-send process (eglot--obj :jsonrpc "2.0" @@ -654,9 +635,7 @@ case never returns locally." (cl-defmacro eglot--lambda (cl-lambda-list &body body) (declare (indent 1) (debug (sexp &rest form))) - `(cl-function - (lambda ,cl-lambda-list - ,@body))) + `(cl-function (lambda ,cl-lambda-list ,@body))) (defun eglot--sync-request (proc method params) "Like `eglot--request' for PROC, METHOD and PARAMS, but synchronous. @@ -687,18 +666,16 @@ Meaning only return locally if successful, otherwise exit non-locally." (cl-defun eglot--notify (process method params) "Notify PROCESS of something, don't expect a reply.e" - (eglot--process-send process - (eglot--obj :jsonrpc "2.0" - :method method - :params params))) + (eglot--process-send process (eglot--obj :jsonrpc "2.0" + :method method + :params params))) (cl-defun eglot--reply (process id &key result error) "Reply to PROCESS's request ID with MESSAGE." - (eglot--process-send process - (eglot--obj :jsonrpc "2.0" - :id id - :result result - :error error))) + (eglot--process-send process (eglot--obj :jsonrpc "2.0" + :id id + :result result + :error error))) ;;; Helpers @@ -745,9 +722,8 @@ Meaning only return locally if successful, otherwise exit non-locally." (defun eglot--path-to-uri (path) "Urify PATH." - (url-hexify-string - (concat "file://" (file-truename path)) - url-path-allowed-chars)) + (url-hexify-string (concat "file://" (file-truename path)) + url-path-allowed-chars)) (defun eglot--uri-to-path (uri) "Convert URI to a file path." @@ -782,13 +758,9 @@ Meaning only return locally if successful, otherwise exit non-locally." ;;; (defvar eglot-mode-map (make-sparse-keymap)) -(defvar eglot--managed-mode-map (make-sparse-keymap)) - (define-minor-mode eglot--managed-mode "Mode for source buffers managed by some EGLOT project." - nil - nil - eglot-mode-map + nil nil eglot-mode-map (cond (eglot--managed-mode (eglot-mode 1) @@ -824,10 +796,9 @@ Meaning only return locally if successful, otherwise exit non-locally." (defun eglot--buffer-managed-p (&optional proc) "Tell if current buffer is managed by PROC." - (and buffer-file-name - (let ((cur (eglot--current-process))) - (or (and (null proc) cur) - (and proc (eq proc cur)))))) + (and buffer-file-name (let ((cur (eglot--current-process))) + (or (and (null proc) cur) + (and proc (eq proc cur)))))) (defun eglot--maybe-activate-editing-mode (&optional proc) "Maybe activate mode function `eglot--managed-mode'. @@ -846,11 +817,9 @@ that case, also signal textDocument/didOpen." ;;; (defvar eglot-menu) -(easy-menu-define eglot-menu eglot-mode-map "EGLOT" - `("EGLOT" )) +(easy-menu-define eglot-menu eglot-mode-map "EGLOT" `("EGLOT" )) -(defvar eglot--mode-line-format - `(:eval (eglot--mode-line-format))) +(defvar eglot--mode-line-format `(:eval (eglot--mode-line-format))) (put 'eglot--mode-line-format 'risky-local-variable t) @@ -949,9 +918,7 @@ that case, also signal textDocument/didOpen." (eglot--mode-line-call 'eglot-forget-pending-continuations)) map))))))))) -(add-to-list 'mode-line-misc-info - `(eglot-mode - (" [" eglot--mode-line-format "] "))) +(add-to-list 'mode-line-misc-info `(eglot-mode (" [" eglot--mode-line-format "] "))) ;;; Protocol implementation (Requests, notifications, etc) @@ -971,9 +938,7 @@ running. INTERACTIVE is t if called interactively." (setf (eglot--moribund process) t) (delete-process process)))) (eglot--request - process - :shutdown - nil + process :shutdown nil :success-fn (lambda (&rest _anything) (when interactive (eglot--message "Now asking %s politely to exit" process)) @@ -989,8 +954,7 @@ running. INTERACTIVE is t if called interactively." :async-p (not sync) :timeout-fn brutal))) -(cl-defun eglot--server-window/showMessage - (_process &key type message) +(cl-defun eglot--server-window/showMessage (_process &key type message) "Handle notification window/showMessage" (eglot--message (propertize "Server reports (type=%s): %s" 'face (if (<= type 1) 'error)) @@ -1018,15 +982,13 @@ running. INTERACTIVE is t if called interactively." :error (eglot--obj :code -32800 :message "User cancelled")))))) -(cl-defun eglot--server-window/logMessage - (_process &key type message) +(cl-defun eglot--server-window/logMessage (_process &key type message) "Handle notification window/logMessage" (eglot--log (propertize "Server reports (type=%s): %s" 'face (if (<= type 1) 'error)) type message)) -(cl-defun eglot--server-telemetry/event - (_process &rest any) +(cl-defun eglot--server-telemetry/event (_process &rest any) "Handle notification telemetry/event" (eglot--log "Server telemetry: %s" any)) @@ -1045,16 +1007,14 @@ running. INTERACTIVE is t if called interactively." (cond (buffer (with-current-buffer buffer - (cl-flet ((pos-at - (pos-plist) - (save-excursion - (goto-char (point-min)) - (forward-line (plist-get pos-plist :line)) - (forward-char - (min (plist-get pos-plist :character) - (- (line-end-position) - (line-beginning-position)))) - (point)))) + (cl-flet ((pos-at (pos-plist) + (save-excursion (goto-char (point-min)) + (forward-line (plist-get pos-plist :line)) + (forward-char + (min (plist-get pos-plist :character) + (- (line-end-position) + (line-beginning-position)))) + (point)))) (cl-loop for diag-spec across diagnostics collect (cl-destructuring-bind (&key range severity _group _code source message) @@ -1066,12 +1026,9 @@ running. INTERACTIVE is t if called interactively." (flymake-make-diagnostic (current-buffer) begin-pos end-pos - (cond ((<= severity 1) - :error) - ((= severity 2) - :warning) - (t - :note)) + (cond ((<= severity 1) :error) + ((= severity 2) :warning) + (t :note)) (concat source ": " message))))) into diags finally @@ -1199,17 +1156,13 @@ Records START, END and PRE-CHANGE-LENGTH locally." ,_after-end ,len ,after-text)) - (eglot--obj - :range - (eglot--obj - :start before-start-position - :end before-end-position) - :rangeLength len - :text after-text)) - (reverse - (cl-mapcar 'append - eglot--recent-before-changes - eglot--recent-after-changes))))))))))) + (eglot--obj :range (eglot--obj :start before-start-position + :end before-end-position) + :rangeLength len + :text after-text)) + (reverse (cl-mapcar 'append + eglot--recent-before-changes + eglot--recent-after-changes))))))))))) (setq eglot--recent-before-changes nil eglot--recent-after-changes nil))) @@ -1290,12 +1243,11 @@ DUMMY is ignored" (defun eglot--xref-make (name uri position) "Like `xref-make' but with LSP's NAME, URI and POSITION." - (xref-make name - (xref-make-file-location - (eglot--uri-to-path uri) - ;; F!@(#*&#$)CKING OFF-BY-ONE again - (1+ (plist-get position :line)) - (plist-get position :character)))) + (xref-make name (xref-make-file-location + (eglot--uri-to-path uri) + ;; F!@(#*&#$)CKING OFF-BY-ONE again + (1+ (plist-get position :line)) + (plist-get position :character)))) (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) (let ((proc (eglot--current-process-or-lose)) @@ -1313,11 +1265,10 @@ DUMMY is ignored" :textDocument text-id :kind kind :containerName containerName)) - (eglot--sync-request - proc - :textDocument/documentSymbol - (eglot--obj - :textDocument text-id)))) + (eglot--sync-request proc + :textDocument/documentSymbol + (eglot--obj + :textDocument text-id)))) (all-completions string eglot--xref-known-symbols))))) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) @@ -1385,8 +1336,8 @@ DUMMY is ignored" (when (plist-get (eglot--capabilities proc) :completionProvider) (list - (if bounds (car bounds) (point)) - (if bounds (cdr bounds) (point)) + (or (car bounds) (point)) + (or (cdr bounds) (point)) (completion-table-dynamic (lambda (_ignored) (let* ((resp (eglot--sync-request @@ -1395,8 +1346,7 @@ DUMMY is ignored" (eglot--obj :textDocument (eglot--current-buffer-TextDocumentIdentifier) :position (eglot--pos-to-lsp-position)))) - (items (if (vectorp resp) resp - (plist-get resp :items)))) + (items (if (vectorp resp) resp (plist-get resp :items)))) (eglot--mapply (eglot--lambda (&key insertText label kind detail documentation sortText) @@ -1405,20 +1355,18 @@ DUMMY is ignored" :documentation documentation :sortText sortText)) items)))) :annotation-function - (lambda (what) - (let ((detail (get-text-property 0 :detail what)) - (kind (get-text-property 0 :kind what))) - (format "%s%s" - detail - (if kind - (format " (%s)" (cdr (assoc kind eglot--kind-names))) - "")))) + (lambda (what) (let ((detail (get-text-property 0 :detail what)) + (kind (get-text-property 0 :kind what))) + (format "%s%s" + detail + (if kind + (format " (%s)" (cdr (assoc kind eglot--kind-names))) + "")))) :display-sort-function - (lambda (items) - (sort items (lambda (a b) - (string-lessp - (get-text-property 0 :sortText a) - (get-text-property 0 :sortText b))))))))) + (lambda (items) (sort items (lambda (a b) + (string-lessp + (get-text-property 0 :sortText a) + (get-text-property 0 :sortText b))))))))) (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function." commit 283cfbcd4d53e22ecacdce33bbfe7a2a3b457a3f Author: João Távora Date: Mon May 7 22:06:49 2018 +0100 Fix bug in hover support * eldoc.el (eglot-eldoc-function): Use eglot--format-markup. (subr-x): Require it. (eglot--format-markup): Pacify byte-compiler. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8e927258be..9d7b16d39f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -34,6 +34,7 @@ (require 'warnings) (require 'flymake) (require 'xref) +(require 'subr-x) ;;; User tweakable stuff @@ -763,7 +764,7 @@ Meaning only return locally if successful, otherwise exit non-locally." "Format MARKUP according to LSP's spec." (cond ((stringp markup) (with-temp-buffer - (ignore-errors (funcall 'markdown-mode)) + (ignore-errors (funcall (intern "markdown-mode"))) ;escape bytecompiler (font-lock-ensure) (insert markup) (string-trim (buffer-string)))) @@ -1428,7 +1429,7 @@ DUMMY is ignored" :position (eglot--pos-to-lsp-position)) :success-fn (eglot--lambda (&key contents _range) (eldoc-message - (mapconcat #'eglot--format + (mapconcat #'eglot--format-markup (if (vectorp contents) contents (list contents)) commit a3fb899cb5bb6b88f2bf117578a273c81e4cb08e Author: João Távora Date: Mon May 7 18:46:28 2018 +0100 Clean up client capabilities * eglot.el (eglot--client-capabilities): Clean up client capabilities. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2c97f2ff48..8e927258be 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -215,23 +215,10 @@ CONTACT is as `eglot--contact'. Returns a process object." :willSave t :willSaveWaitUntil :json-false :didSave t) - :completion nil - :hover nil - :signatureHelp nil - :references nil - :documentHighlight nil - :documentSymbol nil - :formatting nil - :rangeFormatting nil - :onTypeFormatting nil - :definition nil - :typeDefinition nil - :implementation nil - :codeAction nil - :codeLens nil - :documentLink nil - :colorProvider nil - :rename nil + :completion `(:dynamicRegistration :json-false) + :hover `(:dynamicRegistration :json-false) + :references `(:dynamicRegistration :json-false) + :definition `(:dynamicRegistration :json-false) :publishDiagnostics `(:relatedInformation :json-false)) :experimental (eglot--obj))) commit 72712e5aed3716aae1e66930e8e899f12e85c5af Author: João Távora Date: Mon May 7 18:45:57 2018 +0100 Half-baked textdocument/hover support * eglot.el (eglot--format-markup): New helper. (eglot--managed-mode): Handle eldoc-documentation-function. (eglot-eldoc-function): New function. * README.md: update diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3bed8d9344..2c97f2ff48 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -772,6 +772,23 @@ Meaning only return locally if successful, otherwise exit non-locally." (13 . "Enum") (14 . "Keyword") (15 . "Snippet") (16 . "Color") (17 . "File") (18 . "Reference"))) +(defun eglot--format-markup (markup) + "Format MARKUP according to LSP's spec." + (cond ((stringp markup) + (with-temp-buffer + (ignore-errors (funcall 'markdown-mode)) + (font-lock-ensure) + (insert markup) + (string-trim (buffer-string)))) + (t + (with-temp-buffer + (ignore-errors (funcall (intern (concat + (plist-get markup :language) + "-mode" )))) + (insert (plist-get markup :value)) + (font-lock-ensure) + (buffer-string))))) + ;;; Minor modes ;;; @@ -796,7 +813,10 @@ Meaning only return locally if successful, otherwise exit non-locally." (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) (add-hook 'xref-backend-functions 'eglot-xref-backend nil t) (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) - (flymake-mode 1)) + (add-function :before-until (local 'eldoc-documentation-function) + #'eglot-eldoc-function) + (flymake-mode 1) + (eldoc-mode 1)) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) @@ -806,7 +826,9 @@ Meaning only return locally if successful, otherwise exit non-locally." (remove-hook 'before-save-hook 'eglot--signal-textDocument/willSave t) (remove-hook 'after-save-hook 'eglot--signal-textDocument/didSave t) (remove-hook 'xref-backend-functions 'eglot-xref-backend t) - (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t)))) + (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) + (remove-function (local 'eldoc-documentation-function) + #'eglot-eldoc-function)))) (define-minor-mode eglot-mode "Minor mode for all buffers managed by EGLOT in some way." nil @@ -1410,6 +1432,22 @@ DUMMY is ignored" (get-text-property 0 :sortText a) (get-text-property 0 :sortText b))))))))) +(defun eglot-eldoc-function () + "EGLOT's `eldoc-documentation-function' function." + (eglot--request (eglot--current-process-or-lose) + :textDocument/hover + (eglot--obj + :textDocument (eglot--current-buffer-TextDocumentIdentifier) + :position (eglot--pos-to-lsp-position)) + :success-fn (eglot--lambda (&key contents _range) + (eldoc-message + (mapconcat #'eglot--format + (if (vectorp contents) + contents + (list contents)) + "\n")))) + nil) + ;;; Dynamic registration ;;; commit 82c04790bd0d428b5f80bc32652f060498b1139f Author: João Távora Date: Mon May 7 18:43:30 2018 +0100 Tweak the async request engine. * eglot.el (eglot--request): Return the continuation id. (eglot--lambda): Move up in the file. (eglot--sync-request): Use a catch-tag. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 068e2ca5c5..3bed8d9344 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -593,7 +593,9 @@ is a symbol saying if this is a client or server originated." method params &key success-fn error-fn timeout-fn (async-p t)) - "Make a request to PROCESS, expecting a reply." + "Make a request to PROCESS, expecting a reply. +Return the ID of this request, unless ASYNC-P is nil, in which +case never returns locally." (let* ((id (eglot--next-request-id)) (timeout-fn (or timeout-fn @@ -658,22 +660,34 @@ is a symbol saying if this is a client or server originated." (when (memq timeout-timer timer-list) (eglot--message "(request) Last-change cancelling timer for continuation %s" id) - (cancel-timer timeout-timer)))))))) + (cancel-timer timeout-timer)))))) + ;; Finally, return the id. + id)) + +(cl-defmacro eglot--lambda (cl-lambda-list &body body) + (declare (indent 1) (debug (sexp &rest form))) + `(cl-function + (lambda ,cl-lambda-list + ,@body))) (defun eglot--sync-request (proc method params) "Like `eglot--request' for PROC, METHOD and PARAMS, but synchronous. Meaning only return locally if successful, otherwise exit non-locally." (let* ((timeout-error-sym (cl-gensym)) - (retval (eglot--request proc method params - :success-fn (lambda (&rest args) - (if (vectorp (car args)) - (car args) - args)) - :error-fn (cl-function - (lambda (&key code message &allow-other-keys) - (eglot--error "Oops: %s: %s" code message))) - :timeout-fn (lambda () timeout-error-sym) - :async-p nil))) + (catch-tag (make-symbol "eglot--sync-request-catch-tag")) + (retval + (catch catch-tag + (eglot--request proc method params + :success-fn (lambda (&rest args) + (throw catch-tag (if (vectorp (car args)) + (car args) + args))) + :error-fn (eglot--lambda + (&key code message &allow-other-keys) + (eglot--error "Oops: %s: %s" code message)) + :timeout-fn (lambda () + (throw catch-tag timeout-error-sym)) + :async-p nil)))) ;; FIXME: There's maybe an emacs bug here. Because timeout-fn runs ;; in a timer, the better and obvious choice of throwing the erro ;; in the lambda is not quitting the `accept-process-output' @@ -741,12 +755,6 @@ Meaning only return locally if successful, otherwise exit non-locally." "Apply FUN to every element of SEQ." (mapcar (lambda (e) (apply fun e)) seq)) -(cl-defmacro eglot--lambda (cl-lambda-list &body body) - (declare (indent 1) (debug (sexp &rest form))) - `(cl-function - (lambda ,cl-lambda-list - ,@body))) - (defun eglot--path-to-uri (path) "Urify PATH." (url-hexify-string commit 711d3a1d33d693eca9a1e21157518e1198b0ca9c Author: João Távora Date: Mon May 7 17:27:01 2018 +0100 Explain why didopen on after-revert-hook is a bad idea The reason is that the global find-file-hook is called again, and that already does the didOpen. Too many didOpen's would be bad. * eglot.el (eglot--managed-mode): Remove commented lines. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2ab47819f0..068e2ca5c5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -784,7 +784,6 @@ Meaning only return locally if successful, otherwise exit non-locally." (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'before-revert-hook 'eglot--signal-textDocument/didClose nil t) - ;;(add-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen nil t) (add-hook 'before-save-hook 'eglot--signal-textDocument/willSave nil t) (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) (add-hook 'xref-backend-functions 'eglot-xref-backend nil t) @@ -796,7 +795,6 @@ Meaning only return locally if successful, otherwise exit non-locally." (remove-hook 'before-change-functions 'eglot--before-change t) (remove-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose t) (remove-hook 'before-revert-hook 'eglot--signal-textDocument/didClose t) - ;; (remove-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen t) (remove-hook 'before-save-hook 'eglot--signal-textDocument/willSave t) (remove-hook 'after-save-hook 'eglot--signal-textDocument/didSave t) (remove-hook 'xref-backend-functions 'eglot-xref-backend t) commit 2b307d5a114e53934d2bdd473136304ec7bd4ab7 Author: João Távora Date: Mon May 7 17:23:27 2018 +0100 Half-decent completion support * README.md: Update. * eglot.el (eglot--kind-names): New variable. (eglot--managed-mode): Handle completion-at-point-functions. (eglot-completion-at-point): New function. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f6bf01073e..2ab47819f0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -757,6 +757,13 @@ Meaning only return locally if successful, otherwise exit non-locally." "Convert URI to a file path." (url-filename (url-generic-parse-url (url-unhex-string uri)))) +(defconst eglot--kind-names + `((1 . "Text") (2 . "Method") (3 . "Function") (4 . "Constructor") + (5 . "Field") (6 . "Variable") (7 . "Class") (8 . "Interface") + (9 . "Module") (10 . "Property") (11 . "Unit") (12 . "Value") + (13 . "Enum") (14 . "Keyword") (15 . "Snippet") (16 . "Color") + (17 . "File") (18 . "Reference"))) + ;;; Minor modes ;;; @@ -777,10 +784,11 @@ Meaning only return locally if successful, otherwise exit non-locally." (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'before-revert-hook 'eglot--signal-textDocument/didClose nil t) - ;; (add-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen nil t) + ;;(add-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen nil t) (add-hook 'before-save-hook 'eglot--signal-textDocument/willSave nil t) (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) (add-hook 'xref-backend-functions 'eglot-xref-backend nil t) + (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) (flymake-mode 1)) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) @@ -791,7 +799,8 @@ Meaning only return locally if successful, otherwise exit non-locally." ;; (remove-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen t) (remove-hook 'before-save-hook 'eglot--signal-textDocument/willSave t) (remove-hook 'after-save-hook 'eglot--signal-textDocument/didSave t) - (remove-hook 'xref-backend-functions 'eglot-xref-backend t)))) + (remove-hook 'xref-backend-functions 'eglot-xref-backend t) + (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t)))) (define-minor-mode eglot-mode "Minor mode for all buffers managed by EGLOT in some way." nil @@ -1353,6 +1362,48 @@ DUMMY is ignored" :workspace/symbol (eglot--obj :query pattern)))) +(defun eglot-completion-at-point () + "EGLOT's `completion-at-point' function." + (let ((bounds (bounds-of-thing-at-point 'sexp)) + (proc (eglot--current-process-or-lose))) + (when (plist-get (eglot--capabilities proc) + :completionProvider) + (list + (if bounds (car bounds) (point)) + (if bounds (cdr bounds) (point)) + (completion-table-dynamic + (lambda (_ignored) + (let* ((resp (eglot--sync-request + proc + :textDocument/completion + (eglot--obj + :textDocument (eglot--current-buffer-TextDocumentIdentifier) + :position (eglot--pos-to-lsp-position)))) + (items (if (vectorp resp) resp + (plist-get resp :items)))) + (eglot--mapply + (eglot--lambda (&key insertText label kind detail + documentation sortText) + (propertize insertText + :label label :kind kind :detail detail + :documentation documentation :sortText sortText)) + items)))) + :annotation-function + (lambda (what) + (let ((detail (get-text-property 0 :detail what)) + (kind (get-text-property 0 :kind what))) + (format "%s%s" + detail + (if kind + (format " (%s)" (cdr (assoc kind eglot--kind-names))) + "")))) + :display-sort-function + (lambda (items) + (sort items (lambda (a b) + (string-lessp + (get-text-property 0 :sortText a) + (get-text-property 0 :sortText b))))))))) + ;;; Dynamic registration ;;; commit b9a3366a428e011ac42c97cdfe891c739ab1b11e Author: João Távora Date: Mon May 7 16:32:23 2018 +0100 Solve another textdocument/didchange bug * eglot.el (eglot--signal-textDocument/didChange): Rework a bit. (eglot--after-change): Store the actual after-text in the eglot--recent-after-changes. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 045588de99..f6bf01073e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1128,52 +1128,65 @@ were deleted/added)" "Hook onto `after-change-functions'. Records START, END and PRE-CHANGE-LENGTH locally." (cl-incf eglot--versioned-identifier) - (push (list start end pre-change-length) eglot--recent-after-changes)) + (push (list start end pre-change-length + (buffer-substring-no-properties start end)) + eglot--recent-after-changes)) (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." - (when (and eglot--recent-before-changes - eglot--recent-after-changes) - (let* ((proc (eglot--current-process-or-lose)) - (sync-kind (plist-get (eglot--capabilities proc) :textDocumentSync))) - (save-restriction - (widen) - (unless (or (not sync-kind) - (eq sync-kind 0)) - (eglot--notify - proc - :textDocument/didChange - (eglot--obj - :textDocument - (eglot--current-buffer-VersionedTextDocumentIdentifier) - :contentChanges - (if (or (eq sync-kind 1) - (/= (length eglot--recent-before-changes) - (length eglot--recent-after-changes))) - (vector - (eglot--obj - :text (buffer-substring-no-properties (point-min) (point-max)))) - (apply - #'vector - (mapcar - (pcase-lambda (`(,before-start-position - ,before-end-position - ,after-start - ,after-end - ,len)) - (eglot--obj - :range - (eglot--obj - :start before-start-position - :end before-end-position) - :rangeLength len - :text (buffer-substring-no-properties after-start after-end))) - (reverse - (cl-mapcar 'append - eglot--recent-before-changes - eglot--recent-after-changes))))))))))) - (setq eglot--recent-before-changes nil - eglot--recent-after-changes nil)) + (unwind-protect + (when (or eglot--recent-before-changes + eglot--recent-after-changes) + (let* ((proc (eglot--current-process-or-lose)) + (sync-kind (plist-get (eglot--capabilities proc) + :textDocumentSync)) + (emacs-messup + (/= (length eglot--recent-before-changes) + (length eglot--recent-after-changes))) + (full-sync-p (or (eq sync-kind 1) emacs-messup))) + (when emacs-messup + (unless (eq sync-kind 1) + (eglot--warn "Using full sync because before: %s and after: %s" + eglot--recent-before-changes + eglot--recent-after-changes))) + (save-restriction + (widen) + (unless (or (not sync-kind) + (eq sync-kind 0)) + (eglot--notify + proc + :textDocument/didChange + (eglot--obj + :textDocument + (eglot--current-buffer-VersionedTextDocumentIdentifier) + :contentChanges + (if full-sync-p + (vector + (eglot--obj + :text (buffer-substring-no-properties (point-min) + (point-max)))) + (apply + #'vector + (mapcar + (pcase-lambda (`(,before-start-position + ,before-end-position + ,_after-start + ,_after-end + ,len + ,after-text)) + (eglot--obj + :range + (eglot--obj + :start before-start-position + :end before-end-position) + :rangeLength len + :text after-text)) + (reverse + (cl-mapcar 'append + eglot--recent-before-changes + eglot--recent-after-changes))))))))))) + (setq eglot--recent-before-changes nil + eglot--recent-after-changes nil))) (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." commit 083ed923a7517ca63d4f259f4bfee7b8986ea03f Author: João Távora Date: Mon May 7 15:04:02 2018 +0100 Support javascript's javascript-typescript-langserver * README.md: Improve a bit * eglot.el (eglot--make-process): Take MANAGED-MAJOR-MODE arg (eglot-executables): Add basic javascript support. (eglot--connect): Pass mode to eglot--make-process (eglot--interactive): Check that guessed command is a listp. (eglot): Minor improvement to message. (eglot--current-buffer-TextDocumentItem): Guess language from mode symbol. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cd485196b2..045588de99 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -42,8 +42,10 @@ :prefix "eglot-" :group 'applications) -(defvar eglot-executables '((rust-mode . ("rls")) - (python-mode . ("pyls"))) +(defvar eglot-executables + '((rust-mode . ("rls")) + (python-mode . ("pyls")) + (js-mode . ("javascript-typescript-stdio"))) "Alist mapping major modes to server executables.") (defface eglot-mode-line @@ -145,23 +147,24 @@ list of a single string of the form :") (eglot--define-process-var eglot--buffer-open-count (make-hash-table) "Keeps track of didOpen/didClose notifs for each buffer.") -(defun eglot--make-process (name contact) +(defun eglot--make-process (name managed-major-mode contact) "Make a process from CONTACT. NAME is a name to give the inferior process or connection. +MANAGED-MAJOR-MODE is a symbol naming a major mode. CONTACT is as `eglot--contact'. Returns a process object." - (let* ((readable-name (format "EGLOT server (%s)" name)) + (let* ((readable-name (format "EGLOT server (%s/%s)" name managed-major-mode)) (buffer (get-buffer-create (format "*%s inferior*" readable-name))) - (singleton (and (null (cdr contact)) (car contact))) + singleton (proc - (if (and - singleton - (string-match "^[\s\t]*\\(.*\\):\\([[:digit:]]+\\)[\s\t]*$" - singleton)) + (if (and (setq singleton (and (null (cdr contact)) (car contact))) + (string-match "^[\s\t]*\\(.*\\):\\([[:digit:]]+\\)[\s\t]*$" + singleton)) (open-network-stream readable-name buffer (match-string 1 singleton) - (string-to-number (match-string 2 singleton))) + (string-to-number + (match-string 2 singleton))) (make-process :name readable-name :buffer buffer @@ -236,7 +239,7 @@ CONTACT is as `eglot--contact'. Returns a process object." short-name contact &optional success-fn) "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT. SUCCESS-FN with no args if all goes well." - (let* ((proc (eglot--make-process short-name contact)) + (let* ((proc (eglot--make-process short-name managed-major-mode contact)) (buffer (process-buffer proc))) (setf (eglot--contact proc) contact (eglot--project proc) project @@ -297,15 +300,16 @@ SUCCESS-FN with no args if all goes well." managed-major-mode (let ((prompt (cond (current-prefix-arg - "[eglot] Execute program (or connect to :) ") + "[eglot] Enter program to execute (or :): ") ((null guessed-command) (format "[eglot] Sorry, couldn't guess for `%s'!\n\ -Execute program (or connect to :) " +Enter program to execute (or :): " managed-major-mode))))) (if prompt (split-string-and-unquote (read-shell-command prompt - (combine-and-quote-strings guessed-command) + (if (listp guessed-command) + (combine-and-quote-strings guessed-command)) 'eglot-command-history)) guessed-command)) t))) @@ -347,7 +351,7 @@ INTERACTIVE is t if called interactively." command (lambda (proc) (eglot--message "Connected! Process `%s' now managing `%s' \ -buffers in project %s." +buffers in project `%s'." proc managed-major-mode short-name))))))) @@ -1101,9 +1105,10 @@ running. INTERACTIVE is t if called interactively." "Compute TextDocumentItem object for current buffer." (append (eglot--current-buffer-VersionedTextDocumentIdentifier) - (eglot--obj :languageId (cdr (assoc major-mode - '((rust-mode . rust) - (emacs-lisp-mode . emacs-lisp)))) + (eglot--obj :languageId + (if (string-match "\\(.*\\)-mode" (symbol-name major-mode)) + (match-string 1 (symbol-name major-mode)) + "unknown") :text (save-restriction (widen) commit c5232c581e0ed714edb2cf30bd59f23511bd2fb2 Author: João Távora Date: Mon May 7 13:45:10 2018 +0100 Increase request timeout length to 10 seconds * eglot.el (eglot-request-timeout): New var. (eglot--request): Use it. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1d72b4c524..cd485196b2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -48,8 +48,11 @@ (defface eglot-mode-line '((t (:inherit font-lock-constant-face :weight bold))) - "Face for package-name in EGLOT's mode line." - :group 'eglot) + "Face for package-name in EGLOT's mode line.") + +(defcustom eglot-request-timeout 10 + "How many seconds to way for a reply from the server." + :type :integer) ;;; Process management @@ -617,7 +620,7 @@ is a symbol saying if this is a client or server originated." (catch catch-tag (let ((timeout-timer (run-with-timer - 5 nil + eglot-request-timeout nil (if async-p (lambda () (remhash id (eglot--pending-continuations process)) commit ab858c8ab1fa11aedac1499a3a3f47a066df8a93 Author: João Távora Date: Mon May 7 13:42:56 2018 +0100 Workaround two suspected emacs bugs * eglot.el (eglot--process-filter): Use a proper unique tag. Use unwind-protect. (eglot--sync-request): Rework. (eglot--server-client/registerCapability): Use a proper done tag. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 321307df29..1d72b4c524 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -415,7 +415,8 @@ INTERACTIVE is t if called interactively." (when (buffer-live-p (process-buffer proc)) (with-current-buffer (process-buffer proc) (let ((inhibit-read-only t) - (expected-bytes (eglot--expected-bytes proc))) + (expected-bytes (eglot--expected-bytes proc)) + (done (make-symbol "eglot--process-filter-done-tag"))) ;; Insert the text, advancing the process marker. ;; (save-excursion @@ -424,51 +425,52 @@ INTERACTIVE is t if called interactively." (set-marker (process-mark proc) (point))) ;; Loop (more than one message might have arrived) ;; - (catch 'done - (while t - (cond ((not expected-bytes) - ;; Starting a new message - ;; - (setq expected-bytes - (and (search-forward-regexp - "\\(?:.*: .*\r\n\\)*Content-Length: \ + (unwind-protect + (catch done + (while t + (cond ((not expected-bytes) + ;; Starting a new message + ;; + (setq expected-bytes + (and (search-forward-regexp + "\\(?:.*: .*\r\n\\)*Content-Length: \ *\\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" - (+ (point) 100) - t) - (string-to-number (match-string 1)))) - (unless expected-bytes - (throw 'done :waiting-for-new-message))) - (t - ;; Attempt to complete a message body - ;; - (let ((available-bytes (- (position-bytes (process-mark proc)) - (position-bytes (point))))) - (cond - ((>= available-bytes - expected-bytes) - (let* ((message-end (byte-to-position - (+ (position-bytes (point)) - expected-bytes)))) - (unwind-protect - (save-restriction - (narrow-to-region (point) message-end) - (let* ((json-object-type 'plist) - (json-message (json-read))) - ;; Process content in another buffer, - ;; shielding buffer from tamper - ;; - (with-temp-buffer - (eglot--process-receive proc json-message)))) - (goto-char message-end) - (delete-region (point-min) (point)) - (setq expected-bytes nil)))) + (+ (point) 100) + t) + (string-to-number (match-string 1)))) + (unless expected-bytes + (throw done :waiting-for-new-message))) (t - ;; Message is still incomplete + ;; Attempt to complete a message body ;; - (throw 'done :waiting-for-more-bytes-in-this-message)))))))) - ;; Saved parsing state for next visit to this filter - ;; - (setf (eglot--expected-bytes proc) expected-bytes))))) + (let ((available-bytes (- (position-bytes (process-mark proc)) + (position-bytes (point))))) + (cond + ((>= available-bytes + expected-bytes) + (let* ((message-end (byte-to-position + (+ (position-bytes (point)) + expected-bytes)))) + (unwind-protect + (save-restriction + (narrow-to-region (point) message-end) + (let* ((json-object-type 'plist) + (json-message (json-read))) + ;; Process content in another buffer, + ;; shielding buffer from tamper + ;; + (with-temp-buffer + (eglot--process-receive proc json-message)))) + (goto-char message-end) + (delete-region (point-min) (point)) + (setq expected-bytes nil)))) + (t + ;; Message is still incomplete + ;; + (throw done :waiting-for-more-bytes-in-this-message)))))))) + ;; Saved parsing state for next visit to this filter + ;; + (setf (eglot--expected-bytes proc) expected-bytes)))))) (defun eglot-events-buffer (process &optional interactive) "Display events buffer for current LSP connection PROCESS. @@ -654,18 +656,25 @@ is a symbol saying if this is a client or server originated." (defun eglot--sync-request (proc method params) "Like `eglot--request' for PROC, METHOD and PARAMS, but synchronous. Meaning only return locally if successful, otherwise exit non-locally." - (eglot--request proc method params - :success-fn (lambda (&rest args) - (if (vectorp (car args)) - (car args) - args)) - :error-fn (cl-function - (lambda (&key code message &allow-other-keys) - (eglot--error "Oops: %s: %s" code message))) - :timeout-fn (lambda () - (eglot--error - "Tired of waiting for reply to sync request")) - :async-p nil)) + (let* ((timeout-error-sym (cl-gensym)) + (retval (eglot--request proc method params + :success-fn (lambda (&rest args) + (if (vectorp (car args)) + (car args) + args)) + :error-fn (cl-function + (lambda (&key code message &allow-other-keys) + (eglot--error "Oops: %s: %s" code message))) + :timeout-fn (lambda () timeout-error-sym) + :async-p nil))) + ;; FIXME: There's maybe an emacs bug here. Because timeout-fn runs + ;; in a timer, the better and obvious choice of throwing the erro + ;; in the lambda is not quitting the `accept-process-output' + ;; infinite loop up there. So use this contorted strategy with + ;; `cl-gensym'. + (if (eq retval timeout-error-sym) + (eglot--error "Tired of waiting for reply to sync request") + retval))) (cl-defun eglot--notify (process method params) "Notify PROCESS of something, don't expect a reply.e" @@ -1045,8 +1054,9 @@ running. INTERACTIVE is t if called interactively." (cl-defun eglot--server-client/registerCapability (proc &key id registrations) "Handle notification client/registerCapability" - (let ((jsonrpc-id id)) - (catch 'done + (let ((jsonrpc-id id) + (done (make-symbol "done"))) + (catch done (mapc (lambda (reg) (apply @@ -1059,12 +1069,11 @@ running. INTERACTIVE is t if called interactively." (and (functionp handler-sym) (apply handler-sym proc :id id registerOptions)))) (unless ok - (throw - 'done - (eglot--reply proc jsonrpc-id - :error (eglot--obj - :code -32601 - :message (or message "sorry :-(")))))))) + (throw done + (eglot--reply proc jsonrpc-id + :error (eglot--obj + :code -32601 + :message (or message "sorry :-(")))))))) reg)) registrations) (eglot--reply proc id :result (eglot--obj :message "OK"))))) commit 963f9f4bf90e6a08ffe569155d4b7620545bc60f Author: João Távora Date: Mon May 7 12:18:08 2018 +0100 Etoomanylambdas * eglot.el (eglot--sync-request): Remove a lambda. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d6ecdfed18..321307df29 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -663,9 +663,8 @@ Meaning only return locally if successful, otherwise exit non-locally." (lambda (&key code message &allow-other-keys) (eglot--error "Oops: %s: %s" code message))) :timeout-fn (lambda () - (lambda () - (eglot--error - "Tired of waiting for reply to sync request"))) + (eglot--error + "Tired of waiting for reply to sync request")) :async-p nil)) (cl-defun eglot--notify (process method params) commit ace4b9150da111c721f46f29d2aab624b309d01e Author: João Távora Date: Mon May 7 11:53:44 2018 +0100 Fix the odd bug here and there * eglot.el (eglot--connect): Activate editing mode where applicable here. (eglot, eglot-reconnect): Not here or here. (eglot--process-sentinel): Catch auto-reconnect errors. (eglot--notify): Dont send 'id=null', it messes up js's lsp (eglot--reply): Do send id here. (eglot--log-event): Simplify protocol. Complexify implementation. (eglot--process-receive, eglot--process-send): Simplify eglot--log-event call. (eglot--request, eglot--notify, eglot--reply): Simplify eglot--process-send call (eglot--server-client/registerCapability): Fix bug when replying with wrong id. (eglot--xref-reset-known-symbols): Take DUMMY arg. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cb92361eec..d6ecdfed18 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -265,6 +265,9 @@ SUCCESS-FN with no args if all goes well." (lambda (&key capabilities) (setf (eglot--capabilities proc) capabilities) (setf (eglot--status proc) nil) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (eglot--maybe-activate-editing-mode proc))) (when success-fn (funcall success-fn proc)) (eglot--notify proc :initialized (eglot--obj :__dummy__ t))))))))) @@ -344,10 +347,7 @@ INTERACTIVE is t if called interactively." buffers in project %s." proc managed-major-mode - short-name) - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (eglot--maybe-activate-editing-mode proc))))))))) + short-name))))))) (defun eglot-reconnect (process &optional interactive) "Reconnect to PROCESS. @@ -360,11 +360,7 @@ INTERACTIVE is t if called interactively." (eglot--major-mode process) (eglot--short-name process) (eglot--contact process) - (lambda (proc) - (eglot--message "Reconnected!") - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (eglot--maybe-activate-editing-mode proc)))))) + (lambda (_proc) (eglot--message "Reconnected!")))) (defvar eglot--inhibit-auto-reconnect nil "If non-nil, don't autoreconnect on unexpected quit.") @@ -397,9 +393,11 @@ INTERACTIVE is t if called interactively." (process-exit-status process))) ((null eglot--inhibit-auto-reconnect) (eglot--warn - "(sentinel) Reconnecting after process unexpectedly changed to %s." + "(sentinel) Reconnecting after process unexpectedly changed to `%s'." change) - (eglot-reconnect process) + (condition-case-unless-debug err + (eglot-reconnect process) + (error (eglot--warn "Auto-reconnect failed: %s " err) )) (setq eglot--inhibit-auto-reconnect (run-with-timer 3 nil @@ -493,13 +491,22 @@ INTERACTIVE is t if called interactively." (display-buffer buffer)) buffer)) -(defun eglot--log-event (proc type message &optional id error) +(defun eglot--log-event (proc message type) "Log an eglot-related event. -PROC is the current process. TYPE is an identifier. MESSAGE is -a JSON-like plist or anything else. ID is a continuation -identifier. ERROR is non-nil if this is a JSON-RPC error." +PROC is the current process. MESSAGE is a JSON-like plist. TYPE +is a symbol saying if this is a client or server originated." (with-current-buffer (eglot-events-buffer proc) - (let ((inhibit-read-only t)) + (let* ((inhibit-read-only t) + (id (plist-get message :id)) + (error (plist-get message :error)) + (method (plist-get message :method)) + (subtype (cond ((and method id) 'request) + (method 'notification) + (id 'reply) + ;; pyls keeps on sending these + (t 'unexpected-thingy))) + (type + (format "%s-%s" type subtype))) (goto-char (point-max)) (let ((msg (format "%s%s%s:\n%s\n" type @@ -518,15 +525,7 @@ identifier. ERROR is non-nil if this is a JSON-RPC error." (continuations (and id (not method) (gethash id (eglot--pending-continuations proc))))) - (eglot--log-event proc - (cond ((and method id) 'server-request) - (method 'server-notification) - (continuations 'server-reply) - ;; pyls keeps on sending these - (t 'unexpected-server-thingy)) - message - id - err) + (eglot--log-event proc message 'server) (when err (setf (eglot--status proc) '("error" t))) (cond (method ;; a server notification or a server request @@ -557,19 +556,13 @@ identifier. ERROR is non-nil if this is a JSON-RPC error." (defvar eglot--expect-carriage-return nil) -(defun eglot--process-send (id proc message &optional reply) +(defun eglot--process-send (proc message) "Send MESSAGE to PROC (ID is optional)." - (let* ((json (json-encode message)) - (to-send (format "Content-Length: %d\r\n\r\n%s" - (string-bytes json) - json))) - (process-send-string proc to-send) - (eglot--log-event proc (if id - (if reply - 'client-reply - 'client-request) - 'client-notification) - message id (plist-get message :error)))) + (let ((json (json-encode message))) + (process-send-string proc (format "Content-Length: %d\r\n\r\n%s" + (string-bytes json) + json)) + (eglot--log-event proc message 'client))) (defvar eglot--next-request-id 0) @@ -614,8 +607,7 @@ identifier. ERROR is non-nil if this is a JSON-RPC error." "(request) Request id=%s replied to with result=%s: %s" id result-body))))) (catch-tag (cl-gensym (format "eglot--tag-%d-" id)))) - (eglot--process-send id - process + (eglot--process-send process (eglot--obj :jsonrpc "2.0" :id id :method method @@ -678,21 +670,18 @@ Meaning only return locally if successful, otherwise exit non-locally." (cl-defun eglot--notify (process method params) "Notify PROCESS of something, don't expect a reply.e" - (eglot--process-send nil - process + (eglot--process-send process (eglot--obj :jsonrpc "2.0" - :id nil :method method :params params))) (cl-defun eglot--reply (process id &key result error) "Reply to PROCESS's request ID with MESSAGE." - (eglot--process-send id process + (eglot--process-send process (eglot--obj :jsonrpc "2.0" - :id nil + :id id :result result - :error error) - t)) + :error error))) ;;; Helpers @@ -1057,28 +1046,29 @@ running. INTERACTIVE is t if called interactively." (cl-defun eglot--server-client/registerCapability (proc &key id registrations) "Handle notification client/registerCapability" - (catch 'done - (mapc - (lambda (reg) - (apply - (cl-function - (lambda (&key id method registerOptions) - (pcase-let* - ((handler-sym (intern (concat "eglot--register-" - method))) - (`(,ok ,message) - (and (functionp handler-sym) - (apply handler-sym proc :id id registerOptions)))) - (unless ok - (throw - 'done - (eglot--reply proc id - :error (eglot--obj - :code -32601 - :message (or message "sorry :-(")))))))) - reg)) - registrations) - (eglot--reply proc id :result (eglot--obj :message "OK")))) + (let ((jsonrpc-id id)) + (catch 'done + (mapc + (lambda (reg) + (apply + (cl-function + (lambda (&key id method registerOptions) + (pcase-let* + ((handler-sym (intern (concat "eglot--register-" + method))) + (`(,ok ,message) + (and (functionp handler-sym) + (apply handler-sym proc :id id registerOptions)))) + (unless ok + (throw + 'done + (eglot--reply proc jsonrpc-id + :error (eglot--obj + :code -32601 + :message (or message "sorry :-(")))))))) + reg)) + registrations) + (eglot--reply proc id :result (eglot--obj :message "OK"))))) (defvar eglot--recent-before-changes nil "List of recent changes as collected by `eglot--before-change'.") @@ -1236,8 +1226,9 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." (defvar eglot--xref-known-symbols nil) -(defun eglot--xref-reset-known-symbols () - "Reset `eglot--xref-reset-known-symbols'." +(defun eglot--xref-reset-known-symbols (&rest _dummy) + "Reset `eglot--xref-reset-known-symbols'. +DUMMY is ignored" (setq eglot--xref-known-symbols nil)) (advice-add 'xref-find-definitions :after #'eglot--xref-reset-known-symbols) commit f23a8e8486ad8bbe5f1743571106673fd9d25140 Author: João Távora Date: Sat May 5 11:26:12 2018 +0100 Half-decent xref support * eglot.el (eglot--xref-known-symbols): New hacky var. (eglot--xref-reset-known-symbols): New helper. (xref-find-definitions, xref-find-references): Advise after to call the new helper. (xref-backend-identifier-completion-table): Rework. (eglot--xref-make): New helper. (xref-backend-definitions): Use it. (xref-backend-references, xref-backend-apropos): Implement. (eglot--obj): Add a debug spec. (eglot--lambda): Add debug spec. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ad9ad52ba8..cb92361eec 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -33,6 +33,7 @@ (require 'compile) ; for some faces (require 'warnings) (require 'flymake) +(require 'xref) ;;; User tweakable stuff @@ -171,6 +172,7 @@ CONTACT is as `eglot--contact'. Returns a process object." (defmacro eglot--obj (&rest what) "Make WHAT a suitable argument for `json-encode'." + (declare (debug (&rest form))) ;; FIXME: maybe later actually do something, for now this just fixes ;; the indenting of literal plists. `(list ,@what)) @@ -736,7 +738,7 @@ Meaning only return locally if successful, otherwise exit non-locally." (mapcar (lambda (e) (apply fun e)) seq)) (cl-defmacro eglot--lambda (cl-lambda-list &body body) - (declare (indent 1)) + (declare (indent 1) (debug (sexp &rest form))) `(cl-function (lambda ,cl-lambda-list ,@body))) @@ -1232,16 +1234,46 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." (defun eglot-xref-backend () "EGLOT xref backend." 'eglot) +(defvar eglot--xref-known-symbols nil) + +(defun eglot--xref-reset-known-symbols () + "Reset `eglot--xref-reset-known-symbols'." + (setq eglot--xref-known-symbols nil)) + +(advice-add 'xref-find-definitions :after #'eglot--xref-reset-known-symbols) +(advice-add 'xref-find-references :after #'eglot--xref-reset-known-symbols) + +(defun eglot--xref-make (name uri position) + "Like `xref-make' but with LSP's NAME, URI and POSITION." + (xref-make name + (xref-make-file-location + (eglot--uri-to-path uri) + ;; F!@(#*&#$)CKING OFF-BY-ONE again + (1+ (plist-get position :line)) + (plist-get position :character)))) + (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) - (eglot--mapply - (eglot--lambda (&key name _kind _location _containerName) - ;; a shame we have to throw all that good stuff away - name) - (eglot--sync-request - (eglot--current-process-or-lose) - :textDocument/documentSymbol - (eglot--obj - :textDocument (eglot--current-buffer-TextDocumentIdentifier))))) + (let ((proc (eglot--current-process-or-lose)) + (text-id (eglot--current-buffer-TextDocumentIdentifier))) + (completion-table-with-cache + (lambda (string) + (setq eglot--xref-known-symbols + (eglot--mapply + (eglot--lambda (&key name kind location containerName) + (propertize name + :position (plist-get + (plist-get location :range) + :start) + :locations (list location) + :textDocument text-id + :kind kind + :containerName containerName)) + (eglot--sync-request + proc + :textDocument/documentSymbol + (eglot--obj + :textDocument text-id)))) + (all-completions string eglot--xref-known-symbols))))) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) (let ((symatpt (symbol-at-point))) @@ -1251,39 +1283,55 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." :position (eglot--pos-to-lsp-position))))) (cl-defmethod xref-backend-definitions ((_backend (eql eglot)) identifier) + (let* ((rich-identifier + (car (member identifier eglot--xref-known-symbols))) + (location-or-locations + (if rich-identifier + (get-text-property 0 :locations rich-identifier) + (eglot--sync-request (eglot--current-process-or-lose) + :textDocument/definition + (eglot--obj + :textDocument + (get-text-property 0 :textDocument identifier) + :position + (get-text-property 0 :position identifier)))))) + (eglot--mapply + (eglot--lambda (&key uri range) + (eglot--xref-make identifier uri (plist-get range :start))) + location-or-locations))) + +(cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) + (let* ((identifier (if (get-text-property 0 :position identifier) + identifier + (car (member identifier eglot--xref-known-symbols)))) + (position + (and identifier (get-text-property 0 :position identifier))) + (textDocument + (and identifier (get-text-property 0 :textDocument identifier)))) + (unless (and position textDocument) + (eglot--error "Sorry, can't discover where %s is in the workspace" + identifier)) + (eglot--mapply + (eglot--lambda (&key uri range) + (eglot--xref-make identifier uri (plist-get range :start))) + (eglot--sync-request (eglot--current-process-or-lose) + :textDocument/references + (eglot--obj + :textDocument + textDocument + :position + position + :context (eglot--obj :includeDeclaration t)))))) + +(cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (eglot--mapply - (eglot--lambda (&key uri range) - (xref-make identifier - (xref-make-file-location - (eglot--uri-to-path uri) - (plist-get (plist-get range :start) :line) - (plist-get (plist-get range :start) :character)))) - (or - ;; `identifier' already has `:locations' property if it was - ;; computed via `xref-backend-identifier-completion-table'... - ;; - (get-text-property 0 :locations identifier) - ;; otherwise, it came from - ;; `xref-backend-identifier-at-point', and we have to fetch - ;; manually - ;; - (let ((location-or-locations - (eglot--sync-request (eglot--current-process-or-lose) - :textDocument/definition - (eglot--obj - :textDocument - (get-text-property 0 :textDocument identifier) - :position - (get-text-property 0 :position identifier))))) - (if (vectorp (car location-or-locations)) - (car location-or-locations) - location-or-locations))))) - -(cl-defmethod xref-backend-references ((_backend (eql eglot)) _identifier) - (error "Not implemented")) - -(cl-defmethod xref-backend-apropos ((_backend (eql eglot)) _identifier) - (error "Not implemented")) + (eglot--lambda (&key name location &allow-other-keys) + (let ((range (plist-get location :range)) + (uri (plist-get location :uri))) + (eglot--xref-make name uri (plist-get range :start)))) + (eglot--sync-request (eglot--current-process-or-lose) + :workspace/symbol + (eglot--obj :query pattern)))) ;;; Dynamic registration commit bdd5f6961839c073713d6c2a184260acbe5696ce Author: João Távora Date: Sat May 5 02:29:06 2018 +0100 Very basic xref support * eglot.el (eglot--pos-to-lisp-position): Move up. (eglot--mapply, eglot--lambda): New helpers. (eglot--uri-to-path): New helper. (eglot--managed-mode): Manage xref-backend-functions. (eglot-xref-backend): New function. (xref-backend-identifier-completion-table) (xref-backend-identifier-at-point) (xref-backend-definitions): New methods. (xref-backend-references) (xref-backend-apropos): New methods, still unimplemented. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 34ce0f80f4..ad9ad52ba8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -721,12 +721,36 @@ Meaning only return locally if successful, otherwise exit non-locally." (apply #'format format args) :warning))) +(defun eglot--pos-to-lsp-position (&optional pos) + "Convert point POS to LSP position." + (save-excursion + (eglot--obj :line + ;; F!@(#*&#$)CKING OFF-BY-ONE + (1- (line-number-at-pos pos t)) + :character + (- (goto-char (or pos (point))) + (line-beginning-position))))) + +(defun eglot--mapply (fun seq) + "Apply FUN to every element of SEQ." + (mapcar (lambda (e) (apply fun e)) seq)) + +(cl-defmacro eglot--lambda (cl-lambda-list &body body) + (declare (indent 1)) + `(cl-function + (lambda ,cl-lambda-list + ,@body))) + (defun eglot--path-to-uri (path) "Urify PATH." (url-hexify-string (concat "file://" (file-truename path)) url-path-allowed-chars)) +(defun eglot--uri-to-path (uri) + "Convert URI to a file path." + (url-filename (url-generic-parse-url (url-unhex-string uri)))) + ;;; Minor modes ;;; @@ -750,6 +774,7 @@ Meaning only return locally if successful, otherwise exit non-locally." ;; (add-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen nil t) (add-hook 'before-save-hook 'eglot--signal-textDocument/willSave nil t) (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) + (add-hook 'xref-backend-functions 'eglot-xref-backend nil t) (flymake-mode 1)) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) @@ -759,7 +784,8 @@ Meaning only return locally if successful, otherwise exit non-locally." (remove-hook 'before-revert-hook 'eglot--signal-textDocument/didClose t) ;; (remove-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen t) (remove-hook 'before-save-hook 'eglot--signal-textDocument/willSave t) - (remove-hook 'after-save-hook 'eglot--signal-textDocument/didSave t)))) + (remove-hook 'after-save-hook 'eglot--signal-textDocument/didSave t) + (remove-hook 'xref-backend-functions 'eglot-xref-backend t)))) (define-minor-mode eglot-mode "Minor mode for all buffers managed by EGLOT in some way." nil @@ -1080,16 +1106,6 @@ running. INTERACTIVE is t if called interactively." (widen) (buffer-substring-no-properties (point-min) (point-max)))))) -(defun eglot--pos-to-lsp-position (pos) - "Convert point POS to LSP position." - (save-excursion - (eglot--obj :line - ;; F!@(#*&#$)CKING OFF-BY-ONE - (1- (line-number-at-pos pos t)) - :character - (- (goto-char pos) - (line-beginning-position))))) - (defun eglot--before-change (start end) "Hook onto `before-change-functions'. Records START and END, crucially convert them into @@ -1214,6 +1230,61 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." ;; make the server report new diagnostics. (eglot--signal-textDocument/didChange)) +(defun eglot-xref-backend () "EGLOT xref backend." 'eglot) + +(cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) + (eglot--mapply + (eglot--lambda (&key name _kind _location _containerName) + ;; a shame we have to throw all that good stuff away + name) + (eglot--sync-request + (eglot--current-process-or-lose) + :textDocument/documentSymbol + (eglot--obj + :textDocument (eglot--current-buffer-TextDocumentIdentifier))))) + +(cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) + (let ((symatpt (symbol-at-point))) + (when symatpt + (propertize (symbol-name symatpt) + :textDocument (eglot--current-buffer-TextDocumentIdentifier) + :position (eglot--pos-to-lsp-position))))) + +(cl-defmethod xref-backend-definitions ((_backend (eql eglot)) identifier) + (eglot--mapply + (eglot--lambda (&key uri range) + (xref-make identifier + (xref-make-file-location + (eglot--uri-to-path uri) + (plist-get (plist-get range :start) :line) + (plist-get (plist-get range :start) :character)))) + (or + ;; `identifier' already has `:locations' property if it was + ;; computed via `xref-backend-identifier-completion-table'... + ;; + (get-text-property 0 :locations identifier) + ;; otherwise, it came from + ;; `xref-backend-identifier-at-point', and we have to fetch + ;; manually + ;; + (let ((location-or-locations + (eglot--sync-request (eglot--current-process-or-lose) + :textDocument/definition + (eglot--obj + :textDocument + (get-text-property 0 :textDocument identifier) + :position + (get-text-property 0 :position identifier))))) + (if (vectorp (car location-or-locations)) + (car location-or-locations) + location-or-locations))))) + +(cl-defmethod xref-backend-references ((_backend (eql eglot)) _identifier) + (error "Not implemented")) + +(cl-defmethod xref-backend-apropos ((_backend (eql eglot)) _identifier) + (error "Not implemented")) + ;;; Dynamic registration ;;; commit a2c01431d8893bed71e88bc4d38bcaafc262ba13 Author: João Távora Date: Sat May 5 02:16:10 2018 +0100 New helper eglot--sync-request This should help with xref definitions * eglot.el (eglot--request): Rework a bit. Continuation is always cleared on timeout, regardless of user-supplied fn. (eglot--sync-request): New function. (eglot--process-receive): watch out for vector results. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2afb0e7556..34ce0f80f4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -546,7 +546,10 @@ identifier. ERROR is non-nil if this is a JSON-RPC error." (remhash id (eglot--pending-continuations proc)) (if err (apply (cl-second continuations) err) - (apply (cl-first continuations) (plist-get message :result)))) + (let ((res (plist-get message :result))) + (if (listp res) + (apply (cl-first continuations) res) + (funcall (cl-first continuations) res))))) (id (eglot--warn "Ooops no continuation for id %s" id))))) @@ -592,8 +595,7 @@ identifier. ERROR is non-nil if this is a JSON-RPC error." (or timeout-fn (lambda () (eglot--warn - "(request) Tired of waiting for reply to %s" id) - (remhash id (eglot--pending-continuations process))))) + "(request) Tired of waiting for reply to %s" id)))) (error-fn (or error-fn (cl-function @@ -618,11 +620,15 @@ identifier. ERROR is non-nil if this is a JSON-RPC error." :params params)) (catch catch-tag (let ((timeout-timer - (run-with-timer 5 nil - (if async-p - timeout-fn - (lambda () - (throw catch-tag (funcall timeout-fn))))))) + (run-with-timer + 5 nil + (if async-p + (lambda () + (remhash id (eglot--pending-continuations process)) + (funcall timeout-fn)) + (lambda () + (remhash id (eglot--pending-continuations process)) + (throw catch-tag (funcall timeout-fn))))))) (puthash id (list (if async-p success-fn @@ -651,6 +657,23 @@ identifier. ERROR is non-nil if this is a JSON-RPC error." "(request) Last-change cancelling timer for continuation %s" id) (cancel-timer timeout-timer)))))))) +(defun eglot--sync-request (proc method params) + "Like `eglot--request' for PROC, METHOD and PARAMS, but synchronous. +Meaning only return locally if successful, otherwise exit non-locally." + (eglot--request proc method params + :success-fn (lambda (&rest args) + (if (vectorp (car args)) + (car args) + args)) + :error-fn (cl-function + (lambda (&key code message &allow-other-keys) + (eglot--error "Oops: %s: %s" code message))) + :timeout-fn (lambda () + (lambda () + (eglot--error + "Tired of waiting for reply to sync request"))) + :async-p nil)) + (cl-defun eglot--notify (process method params) "Notify PROCESS of something, don't expect a reply.e" (eglot--process-send nil commit 632a39624d5fcb334a34cc64e374d6aa86e1c03c Author: João Távora Date: Sat May 5 02:24:37 2018 +0100 Cleanup mistake with textdocumentitem and textdocumentidentifier Also introduce eglot--path-to-uri * eglot.el (eglot--path-to-uri): Rename from eglot--uri and rework. (eglot--connect): Use it. (eglot--current-buffer-TextDocumentIdentifier): New function. (eglot--current-buffer-VersionedTextDocumentIdentifier) (eglot--signal-textDocument/didChange) (eglot--signal-textDocument/didClose) (eglot--signal-textDocument/willSave) (eglot--signal-textDocument/didSave): Use it. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2b3f270ef9..2afb0e7556 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -254,9 +254,8 @@ SUCCESS-FN with no args if all goes well." (eglot--obj :processId (unless (eq (process-type proc) 'network) (emacs-pid)) - :rootUri (eglot--uri - (expand-file-name (car (project-roots - (project-current))))) + :rootUri (eglot--path-to-uri + (car (project-roots (project-current)))) :initializationOptions [] :capabilities (eglot--client-capabilities)) :success-fn @@ -699,7 +698,11 @@ identifier. ERROR is non-nil if this is a JSON-RPC error." (apply #'format format args) :warning))) -(defun eglot--uri (path) "Add file:// to PATH." (concat "file://" path)) +(defun eglot--path-to-uri (path) + "Urify PATH." + (url-hexify-string + (concat "file://" (file-truename path)) + url-path-allowed-chars)) ;;; Minor modes @@ -1033,15 +1036,14 @@ running. INTERACTIVE is t if called interactively." (defvar-local eglot--versioned-identifier 0) +(defun eglot--current-buffer-TextDocumentIdentifier () + "Compute TextDocumentIdentifier object for current buffer." + (eglot--obj :uri (eglot--path-to-uri buffer-file-name))) + (defun eglot--current-buffer-VersionedTextDocumentIdentifier () "Compute VersionedTextDocumentIdentifier object for current buffer." - (eglot--obj :uri - (eglot--uri - (url-hexify-string - (file-truename buffer-file-name) - url-path-allowed-chars)) - ;; FIXME: later deal with workspaces - :version eglot--versioned-identifier)) + (append (eglot--current-buffer-TextDocumentIdentifier) + (eglot--obj :version eglot--versioned-identifier))) (defun eglot--current-buffer-TextDocumentItem () "Compute TextDocumentItem object for current buffer." @@ -1154,7 +1156,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." (eglot--notify proc :textDocument/didClose (eglot--obj :textDocument - (eglot--current-buffer-TextDocumentItem))))) + (eglot--current-buffer-TextDocumentIdentifier))))) (defun eglot--signal-textDocument/willSave () "Send textDocument/willSave to server." @@ -1163,7 +1165,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." :textDocument/willSave (eglot--obj :reason 1 ; Manual, emacs laughs in the face of auto-save muahahahaha - :textDocument (eglot--current-buffer-TextDocumentItem)))) + :textDocument (eglot--current-buffer-TextDocumentIdentifier)))) (defun eglot--signal-textDocument/didSave () "Send textDocument/didSave to server." @@ -1173,7 +1175,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." (eglot--obj ;; TODO: Handle TextDocumentSaveRegistrationOptions to control this. :text (buffer-substring-no-properties (point-min) (point-max)) - :textDocument (eglot--current-buffer-TextDocumentItem)))) + :textDocument (eglot--current-buffer-TextDocumentIdentifier)))) (defun eglot-flymake-backend (report-fn &rest _more) "An EGLOT Flymake backend. commit b83cd67751ecb8a1d08993f0751bc5d3dc480a24 Author: João Távora Date: Fri May 4 15:19:19 2018 +0100 Handle dynamic registration in general (but nothing specific yet) * eglot.el (eglot--server-client/registerCapability): Implement. (eglot--register-workspace/didChangeWatchedFiles): Dummy registrator. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 68fe3e287a..2b3f270ef9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1003,15 +1003,28 @@ running. INTERACTIVE is t if called interactively." (cl-defun eglot--server-client/registerCapability (proc &key id registrations) "Handle notification client/registerCapability" - (mapc (lambda (reg) - (apply (cl-function - (lambda (&key _id _method _registerOptions) - ;;; TODO: handle this - )) - reg)) - registrations) - (eglot--reply proc id :error (eglot--obj :code -32601 - :message "sorry :-("))) + (catch 'done + (mapc + (lambda (reg) + (apply + (cl-function + (lambda (&key id method registerOptions) + (pcase-let* + ((handler-sym (intern (concat "eglot--register-" + method))) + (`(,ok ,message) + (and (functionp handler-sym) + (apply handler-sym proc :id id registerOptions)))) + (unless ok + (throw + 'done + (eglot--reply proc id + :error (eglot--obj + :code -32601 + :message (or message "sorry :-(")))))))) + reg)) + registrations) + (eglot--reply proc id :result (eglot--obj :message "OK")))) (defvar eglot--recent-before-changes nil "List of recent changes as collected by `eglot--before-change'.") @@ -1176,6 +1189,16 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." ;; make the server report new diagnostics. (eglot--signal-textDocument/didChange)) + +;;; Dynamic registration +;;; +(cl-defun eglot--register-workspace/didChangeWatchedFiles + (_proc &key _id _watchers) + "Handle dynamic registration of workspace/didChangeWatchedFiles" + ;; TODO: file-notify-add-watch and + ;; file-notify-rm-watch can probably handle this + (list nil "Sorry, can't do this yet")) + ;;; Rust-specific ;;; commit 1e893ab726de8eea25655d714ea28cedd984de4d Author: João Távora Date: Fri May 4 15:02:03 2018 +0100 Honour textdocumentsync * eglot.el (eglot--signal-textDocument/didChange): Honour textDocumentSync diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5701422294..68fe3e287a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -251,7 +251,9 @@ SUCCESS-FN with no args if all goes well." (eglot--request proc :initialize - (eglot--obj :processId (emacs-pid) + (eglot--obj :processId (unless (eq (process-type proc) + 'network) + (emacs-pid)) :rootUri (eglot--uri (expand-file-name (car (project-roots (project-current))))) @@ -1070,44 +1072,44 @@ Records START, END and PRE-CHANGE-LENGTH locally." "Send textDocument/didChange to server." (when (and eglot--recent-before-changes eglot--recent-after-changes) - (save-excursion + (let* ((proc (eglot--current-process-or-lose)) + (sync-kind (plist-get (eglot--capabilities proc) :textDocumentSync))) (save-restriction (widen) - (if (/= (length eglot--recent-before-changes) - (length eglot--recent-after-changes)) - (eglot--notify - (eglot--current-process-or-lose) - :textDocument/didChange - (eglot--obj - :textDocument (eglot--current-buffer-VersionedTextDocumentIdentifier) - :contentChanges - (vector - (eglot--obj - :text (buffer-substring-no-properties (point-min) (point-max)))))) - (let ((combined (cl-mapcar 'append - eglot--recent-before-changes - eglot--recent-after-changes))) - (eglot--notify - (eglot--current-process-or-lose) - :textDocument/didChange - (eglot--obj - :textDocument (eglot--current-buffer-VersionedTextDocumentIdentifier) - :contentChanges + (unless (or (not sync-kind) + (eq sync-kind 0)) + (eglot--notify + proc + :textDocument/didChange + (eglot--obj + :textDocument + (eglot--current-buffer-VersionedTextDocumentIdentifier) + :contentChanges + (if (or (eq sync-kind 1) + (/= (length eglot--recent-before-changes) + (length eglot--recent-after-changes))) + (vector + (eglot--obj + :text (buffer-substring-no-properties (point-min) (point-max)))) (apply #'vector - (mapcar (pcase-lambda (`(,before-start-position - ,before-end-position - ,after-start - ,after-end - ,len)) - (eglot--obj - :range - (eglot--obj - :start before-start-position - :end before-end-position) - :rangeLength len - :text (buffer-substring-no-properties after-start after-end))) - (reverse combined)))))))))) + (mapcar + (pcase-lambda (`(,before-start-position + ,before-end-position + ,after-start + ,after-end + ,len)) + (eglot--obj + :range + (eglot--obj + :start before-start-position + :end before-end-position) + :rangeLength len + :text (buffer-substring-no-properties after-start after-end))) + (reverse + (cl-mapcar 'append + eglot--recent-before-changes + eglot--recent-after-changes))))))))))) (setq eglot--recent-before-changes nil eglot--recent-after-changes nil)) commit 8e4db752095664122ea1385ad64abd1dc7546fe2 Author: João Távora Date: Fri May 4 14:42:02 2018 +0100 Be quite explicit about our lack of capabilities right now * eglot.el (eglot--client-capabilities): Spread out. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8e39591a70..5701422294 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -192,9 +192,40 @@ CONTACT is as `eglot--contact'. Returns a process object." (defun eglot--client-capabilities () "What the EGLOT LSP client supports." (eglot--obj - :workspace (eglot--obj) + :workspace (eglot--obj + :applyEdit nil + :workspaceEdit nil + :didChangeConfiguration nil + :didChangeWatchedFiles nil + :symbol nil + :executeCommand nil + :workspaceFolders nil + :configuration nil) :textDocument (eglot--obj - :publishDiagnostics `(:relatedInformation nil)))) + :synchronization (eglot--obj + :dynamicRegistration :json-false + :willSave t + :willSaveWaitUntil :json-false + :didSave t) + :completion nil + :hover nil + :signatureHelp nil + :references nil + :documentHighlight nil + :documentSymbol nil + :formatting nil + :rangeFormatting nil + :onTypeFormatting nil + :definition nil + :typeDefinition nil + :implementation nil + :codeAction nil + :codeLens nil + :documentLink nil + :colorProvider nil + :rename nil + :publishDiagnostics `(:relatedInformation :json-false)) + :experimental (eglot--obj))) (defun eglot--connect (project managed-major-mode short-name contact &optional success-fn) commit 42733a1e9fd0501217857f98560ddbbe461aa0d0 Author: João Távora Date: Fri May 4 14:29:40 2018 +0100 Use rooturi instead of rootpath * eglot.el (eglot--connect) (eglot--current-buffer-VersionedTextDocumentIdentifier): Use eglot--uri. (eglot--uri): New function. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b305ca303c..8e39591a70 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -221,9 +221,9 @@ SUCCESS-FN with no args if all goes well." proc :initialize (eglot--obj :processId (emacs-pid) - :rootPath (concat - (expand-file-name (car (project-roots - (project-current))))) + :rootUri (eglot--uri + (expand-file-name (car (project-roots + (project-current))))) :initializationOptions [] :capabilities (eglot--client-capabilities)) :success-fn @@ -666,6 +666,8 @@ identifier. ERROR is non-nil if this is a JSON-RPC error." (apply #'format format args) :warning))) +(defun eglot--uri (path) "Add file:// to PATH." (concat "file://" path)) + ;;; Minor modes ;;; @@ -988,10 +990,10 @@ running. INTERACTIVE is t if called interactively." (defun eglot--current-buffer-VersionedTextDocumentIdentifier () "Compute VersionedTextDocumentIdentifier object for current buffer." (eglot--obj :uri - (concat "file://" - (url-hexify-string - (file-truename buffer-file-name) - url-path-allowed-chars)) + (eglot--uri + (url-hexify-string + (file-truename buffer-file-name) + url-path-allowed-chars)) ;; FIXME: later deal with workspaces :version eglot--versioned-identifier)) commit adbed0c21ae0e06c374a035c7f424d8e93b1e70e Author: João Távora Date: Fri May 4 14:18:12 2018 +0100 Make reported capabilities into its own function * eglot.el (eglot--client-capabilities): New function. (eglot--connect): Use it. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 10435a16f8..b305ca303c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -189,6 +189,13 @@ CONTACT is as `eglot--contact'. Returns a process object." (push sym retval)))) retval)) +(defun eglot--client-capabilities () + "What the EGLOT LSP client supports." + (eglot--obj + :workspace (eglot--obj) + :textDocument (eglot--obj + :publishDiagnostics `(:relatedInformation nil)))) + (defun eglot--connect (project managed-major-mode short-name contact &optional success-fn) "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT. @@ -218,11 +225,7 @@ SUCCESS-FN with no args if all goes well." (expand-file-name (car (project-roots (project-current))))) :initializationOptions [] - :capabilities - (eglot--obj - :workspace (eglot--obj) - :textDocument (eglot--obj - :publishDiagnostics `(:relatedInformation t)))) + :capabilities (eglot--client-capabilities)) :success-fn (cl-function (lambda (&key capabilities) commit 4b678a2fa9de46793a7ee802a19a7592db0a903c Author: João Távora Date: Fri May 4 14:15:41 2018 +0100 Include source info in diagnostics * eglot.el (eglot--server-textDocument/publishDiagnostics): Include source info. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d073be2603..10435a16f8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -936,7 +936,7 @@ running. INTERACTIVE is t if called interactively." (point)))) (cl-loop for diag-spec across diagnostics collect (cl-destructuring-bind (&key range severity _group - _code _source message) + _code source message) diag-spec (cl-destructuring-bind (&key start end) range @@ -951,7 +951,7 @@ running. INTERACTIVE is t if called interactively." :warning) (t :note)) - message)))) + (concat source ": " message))))) into diags finally (if eglot--current-flymake-report-fn commit 20e044b1a1fe1318110f5dec0f633397bd0f51cf Author: João Távora Date: Fri May 4 13:36:33 2018 +0100 Reply to client/registercapability (don't handle it yet) * eglot.el (eglot--server-client/registerCapability): New function. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 332f1fe849..d073be2603 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -962,6 +962,19 @@ running. INTERACTIVE is t if called interactively." (t (eglot--message "OK so %s isn't visited" filename))))) +(cl-defun eglot--server-client/registerCapability + (proc &key id registrations) + "Handle notification client/registerCapability" + (mapc (lambda (reg) + (apply (cl-function + (lambda (&key _id _method _registerOptions) + ;;; TODO: handle this + )) + reg)) + registrations) + (eglot--reply proc id :error (eglot--obj :code -32601 + :message "sorry :-("))) + (defvar eglot--recent-before-changes nil "List of recent changes as collected by `eglot--before-change'.") (defvar eglot--recent-after-changes nil commit 70fc9cc98d057cfc5e8bd35c60699edd2fb5107d Author: João Távora Date: Fri May 4 12:17:26 2018 +0100 Handle requests from server correctly * eglot.el (eglot--process-receive): Redesign. (eglot--process-send): Take REPLY arg. Discover if message is error. (eglot--reply): new function (eglot--log-event): Tweak docstring. (eglot--process-receive): Reply with -32601 if unimplemented. (eglot--server-window/showMessageRequest) (eglot--server-client/registerCapability): Use eglot--reply diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index df03d04478..332f1fe849 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -460,7 +460,7 @@ INTERACTIVE is t if called interactively." "Log an eglot-related event. PROC is the current process. TYPE is an identifier. MESSAGE is a JSON-like plist or anything else. ID is a continuation -identifier. ERROR is non-nil if this is an error." +identifier. ERROR is non-nil if this is a JSON-RPC error." (with-current-buffer (eglot-events-buffer proc) (let ((inhibit-read-only t)) (goto-char (point-max)) @@ -475,53 +475,49 @@ identifier. ERROR is non-nil if this is an error." (defun eglot--process-receive (proc message) "Process MESSAGE from PROC." - (let* ((response-id (plist-get message :id)) + (let* ((id (plist-get message :id)) + (method (plist-get message :method)) (err (plist-get message :error)) - (continuations (and response-id - (gethash response-id - (eglot--pending-continuations proc))))) + (continuations (and id + (not method) + (gethash id (eglot--pending-continuations proc))))) (eglot--log-event proc - (cond ((not response-id) - 'server-notification) - ((not continuations) - 'unexpected-server-reply) - (t - 'server-reply)) + (cond ((and method id) 'server-request) + (method 'server-notification) + (continuations 'server-reply) + ;; pyls keeps on sending these + (t 'unexpected-server-thingy)) message - response-id + id err) - (when err - (setf (eglot--status proc) '("error" t))) - (cond ((and response-id - (not continuations)) - (eglot--warn "Ooops no continuation for id %s" response-id)) - (continuations - (cancel-timer (cl-third continuations)) - (remhash response-id - (eglot--pending-continuations proc)) - (cond (err - (apply (cl-second continuations) err)) - (t - (apply (cl-first continuations) (plist-get message :result))))) - (t + (when err (setf (eglot--status proc) '("error" t))) + (cond (method ;; a server notification or a server request - (let* ((method (plist-get message :method)) - (handler-sym (intern (concat "eglot--server-" + (let* ((handler-sym (intern (concat "eglot--server-" method)))) (if (functionp handler-sym) (apply handler-sym proc (append (plist-get message :params) - (let ((id (plist-get message :id))) - (if id `(:id ,id))))) - ;; pyls keeps on sending nil notifs for each notif we - ;; send it, just ignore these. - (unless (null method) - (eglot--warn "No implemetation for notification %s yet" - method)))))))) + (if id `(:id ,id)))) + (eglot--warn "No implementation of method %s yet" + method) + (when id + (eglot--reply + proc id + :error (eglot--obj :code -32601 + :message "Method unimplemented")))))) + (continuations + (cancel-timer (cl-third continuations)) + (remhash id (eglot--pending-continuations proc)) + (if err + (apply (cl-second continuations) err) + (apply (cl-first continuations) (plist-get message :result)))) + (id + (eglot--warn "Ooops no continuation for id %s" id))))) (defvar eglot--expect-carriage-return nil) -(defun eglot--process-send (id proc message) +(defun eglot--process-send (id proc message &optional reply) "Send MESSAGE to PROC (ID is optional)." (let* ((json (json-encode message)) (to-send (format "Content-Length: %d\r\n\r\n%s" @@ -529,9 +525,11 @@ identifier. ERROR is non-nil if this is an error." json))) (process-send-string proc to-send) (eglot--log-event proc (if id - 'client-request + (if reply + 'client-reply + 'client-request) 'client-notification) - message id nil))) + message id (plist-get message :error)))) (defvar eglot--next-request-id 0) @@ -627,6 +625,15 @@ identifier. ERROR is non-nil if this is an error." :method method :params params))) +(cl-defun eglot--reply (process id &key result error) + "Reply to PROCESS's request ID with MESSAGE." + (eglot--process-send id process + (eglot--obj :jsonrpc "2.0" + :id nil + :result result + :error error) + t)) + ;;; Helpers ;;; @@ -884,13 +891,11 @@ running. INTERACTIVE is t if called interactively." nil t (plist-get (elt actions 0) :title))) - (eglot--process-send - id - process - (if reply - (eglot--obj :result (eglot--obj :title reply)) - ;; request cancelled - (eglot--obj :error -32800)))))) + (if reply + (eglot--reply process id :result (eglot--obj :title reply)) + (eglot--reply process id + :error (eglot--obj :code -32800 + :message "User cancelled")))))) (cl-defun eglot--server-window/logMessage (_process &key type message) commit 4f62e731d8e1b4c30bff9f24dd4020cb2a11b06e Author: João Távora Date: Fri May 4 12:03:51 2018 +0100 Don't auto-reconnect if last attempt lasted less than 3 seconds * eglot.el (eglot--inhibit-auto-reconnect): New var. (eglot--process-sentinel): Use it. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8f371d2011..df03d04478 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -329,6 +329,9 @@ INTERACTIVE is t if called interactively." (with-current-buffer buffer (eglot--maybe-activate-editing-mode proc)))))) +(defvar eglot--inhibit-auto-reconnect nil + "If non-nil, don't autoreconnect on unexpected quit.") + (defun eglot--process-sentinel (process change) "Called with PROCESS undergoes CHANGE." (eglot--debug "(sentinel) Process state changed to %s" change) @@ -355,11 +358,20 @@ INTERACTIVE is t if called interactively." (cond ((eglot--moribund process) (eglot--message "(sentinel) Moribund process exited with status %s" (process-exit-status process))) - (t + ((null eglot--inhibit-auto-reconnect) (eglot--warn "(sentinel) Reconnecting after process unexpectedly changed to %s." change) - (eglot-reconnect process))) + (eglot-reconnect process) + (setq eglot--inhibit-auto-reconnect + (run-with-timer + 3 nil + (lambda () + (setq eglot--inhibit-auto-reconnect nil))))) + (t + (eglot--warn + "(sentinel) Not auto-reconnecting, last one didn't last long." + change))) (force-mode-line-update t) (delete-process process))) commit fd9792fcc7896f150a851814b348ff30f64bce19 Author: João Távora Date: Fri May 4 11:54:06 2018 +0100 Workaround rls's regusal to treat nil as empty json object * eglot.el (eglot--connect): Use dummy params. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0300ad3da0..8f371d2011 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -229,7 +229,7 @@ SUCCESS-FN with no args if all goes well." (setf (eglot--capabilities proc) capabilities) (setf (eglot--status proc) nil) (when success-fn (funcall success-fn proc)) - (eglot--notify proc :initialized nil)))))))) + (eglot--notify proc :initialized (eglot--obj :__dummy__ t))))))))) (defvar eglot--command-history nil "History of COMMAND arguments to `eglot'.") commit 6b01d54a76518bc567c8f714c105347cd0f6449c Author: João Távora Date: Fri May 4 10:49:34 2018 +0100 Eglot-editing-mode becomes eglot--managed-mode * eglot.el (eglot--sentinel): Use eglot--managed-mode. (eglot--managed-mode-map): Renamed from eglot-editing-mode-map. (eglot--managed-mode): Renamed from eglot-editing-mode. (eglot-mode): Simplify. (eglot--buffer-managed-p): New function. (eglot--maybe-activate-editing-mode): Simplify. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ec7f3e6653..0300ad3da0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -341,12 +341,20 @@ INTERACTIVE is t if called interactively." "(sentinel) Cancelling timer for continuation %s" id) (cancel-timer timeout))) (eglot--pending-continuations process)) + ;; Turn off `eglot--managed-mode' where appropriate. + ;; + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (when (eglot--buffer-managed-p process) + (eglot--managed-mode -1)))) + ;; Forget about the process-project relationship + ;; + (setf (gethash (eglot--project process) eglot--processes-by-project) + (delq process + (gethash (eglot--project process) eglot--processes-by-project))) (cond ((eglot--moribund process) (eglot--message "(sentinel) Moribund process exited with status %s" - (process-exit-status process)) - (setf (gethash (eglot--project process) eglot--processes-by-project) - (delq process - (gethash (eglot--project process) eglot--processes-by-project)))) + (process-exit-status process))) (t (eglot--warn "(sentinel) Reconnecting after process unexpectedly changed to %s." @@ -641,60 +649,55 @@ identifier. ERROR is non-nil if this is an error." ;;; (defvar eglot-mode-map (make-sparse-keymap)) -(defvar eglot-editing-mode-map (make-sparse-keymap)) +(defvar eglot--managed-mode-map (make-sparse-keymap)) -(define-minor-mode eglot-editing-mode - "Minor mode for source buffers where EGLOT helps you edit." +(define-minor-mode eglot--managed-mode + "Mode for source buffers managed by some EGLOT project." nil nil eglot-mode-map (cond - (eglot-editing-mode + (eglot--managed-mode (eglot-mode 1) (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'before-change-functions 'eglot--before-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'before-revert-hook 'eglot--signal-textDocument/didClose nil t) - (add-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen nil t) + ;; (add-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen nil t) (add-hook 'before-save-hook 'eglot--signal-textDocument/willSave nil t) (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) - (flymake-mode 1) - (unless (eglot--current-process) - (eglot--warn "No process, start one with `M-x eglot'"))) + (flymake-mode 1)) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) (remove-hook 'before-change-functions 'eglot--before-change t) (remove-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose t) (remove-hook 'before-revert-hook 'eglot--signal-textDocument/didClose t) - (remove-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen t) + ;; (remove-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen t) (remove-hook 'before-save-hook 'eglot--signal-textDocument/willSave t) (remove-hook 'after-save-hook 'eglot--signal-textDocument/didSave t)))) (define-minor-mode eglot-mode "Minor mode for all buffers managed by EGLOT in some way." nil - nil eglot-mode-map - (cond (eglot-mode - (when (and buffer-file-name - (not eglot-editing-mode)) - (eglot-editing-mode 1))) - (t - (when eglot-editing-mode - (eglot-editing-mode -1))))) + nil eglot-mode-map) + +(defun eglot--buffer-managed-p (&optional proc) + "Tell if current buffer is managed by PROC." + (and buffer-file-name + (let ((cur (eglot--current-process))) + (or (and (null proc) cur) + (and proc (eq proc cur)))))) (defun eglot--maybe-activate-editing-mode (&optional proc) - "Maybe activate mode function `eglot-editing-mode'. + "Maybe activate mode function `eglot--managed-mode'. If PROC is supplied, do it only if BUFFER is managed by it. In that case, also signal textDocument/didOpen." - (when buffer-file-name - (let ((cur (eglot--current-process))) - (when (or (and (null proc) cur) - (and proc (eq proc cur))) - (unless eglot-editing-mode - (eglot-editing-mode 1)) - (eglot--signal-textDocument/didOpen) - (flymake-start))))) + ;; Called even when revert-buffer-in-progress-p + (when (eglot--buffer-managed-p proc) + (eglot--managed-mode 1) + (eglot--signal-textDocument/didOpen) + (flymake-start))) (add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) commit f26ff4e8162d1798cf3c877f162086b31a33e9f9 Author: João Távora Date: Fri May 4 10:47:17 2018 +0100 Make m-x eglot's interactive spec a separate function * eglot.el (eglot--interactive): New function. (eglot): Rework a little. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3d0c044834..ec7f3e6653 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -234,6 +234,39 @@ SUCCESS-FN with no args if all goes well." (defvar eglot--command-history nil "History of COMMAND arguments to `eglot'.") +(defun eglot--interactive () + "Helper for `eglot'." + (let* ((managed-major-mode + (cond + ((or current-prefix-arg + (not buffer-file-name)) + (intern + (completing-read + "[eglot] Start a server to manage buffers of what major mode? " + (mapcar #'symbol-name + (eglot--all-major-modes)) nil t + (symbol-name major-mode) nil + (symbol-name major-mode) nil))) + (t major-mode))) + (guessed-command + (cdr (assoc managed-major-mode eglot-executables)))) + (list + managed-major-mode + (let ((prompt + (cond (current-prefix-arg + "[eglot] Execute program (or connect to :) ") + ((null guessed-command) + (format "[eglot] Sorry, couldn't guess for `%s'!\n\ +Execute program (or connect to :) " + managed-major-mode))))) + (if prompt + (split-string-and-unquote + (read-shell-command prompt + (combine-and-quote-strings guessed-command) + 'eglot-command-history)) + guessed-command)) + t))) + (defun eglot (managed-major-mode command &optional interactive) "Start a Language Server Protocol server. Server is started with COMMAND and manages buffers of @@ -251,64 +284,33 @@ With a prefix arg, prompt for MANAGED-MAJOR-MODE and COMMAND, else guess them from current context and `eglot-executables'. INTERACTIVE is t if called interactively." - (interactive - (let* ((managed-major-mode - (cond - ((or current-prefix-arg - (not buffer-file-name)) - (intern - (completing-read - "[eglot] Start a server to manage buffers of what major mode? " - (mapcar #'symbol-name - (eglot--all-major-modes)) nil t - (symbol-name major-mode) nil - (symbol-name major-mode) nil))) - (t major-mode))) - (guessed-command - (cdr (assoc managed-major-mode eglot-executables)))) - (list - managed-major-mode - (let ((prompt - (cond (current-prefix-arg - "[eglot] Execute program (or connect to :) ") - ((null guessed-command) - (format "[eglot] Sorry, couldn't guess for `%s'!\n\ -Execute program (or connect to :) " - managed-major-mode))))) - (if prompt - (split-string-and-unquote - (read-shell-command prompt - (combine-and-quote-strings guessed-command) - 'eglot-command-history)) - guessed-command)) - t))) + (interactive (eglot--interactive)) (let* ((project (project-current)) (short-name (eglot--project-short-name project))) (unless project (eglot--error "Cannot work without a current project!")) (unless command (eglot--error "Don't know how to start EGLOT for %s buffers" major-mode)) (let ((current-process (eglot--current-process))) - (cond ((and (process-live-p current-process) - interactive - (y-or-n-p "[eglot] Live process found, reconnect instead? ")) - (eglot-reconnect current-process interactive)) - (t - (when (process-live-p current-process) - (eglot-shutdown current-process 'sync)) - (eglot--connect - project - managed-major-mode - short-name - command - (lambda (proc) - (eglot--message "Connected! Process `%s' now managing `%s' \ + (if (and (process-live-p current-process) + interactive + (y-or-n-p "[eglot] Live process found, reconnect instead? ")) + (eglot-reconnect current-process interactive) + (when (process-live-p current-process) + (eglot-shutdown current-process 'sync)) + (eglot--connect + project + managed-major-mode + short-name + command + (lambda (proc) + (eglot--message "Connected! Process `%s' now managing `%s' \ buffers in project %s." - proc - managed-major-mode - short-name) - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (eglot--maybe-activate-editing-mode proc)))))))))) + proc + managed-major-mode + short-name) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (eglot--maybe-activate-editing-mode proc))))))))) (defun eglot-reconnect (process &optional interactive) "Reconnect to PROCESS. commit c1e66cf87af0f2f26afbd55679bb68962fb76f91 Author: João Távora Date: Fri May 4 01:25:13 2018 +0100 When user declines to reconnect, first quit existing server * eglot.el (eglot): Rework reconnection logic. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c5a0c0f0d7..3d0c044834 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -293,13 +293,15 @@ Execute program (or connect to :) " (y-or-n-p "[eglot] Live process found, reconnect instead? ")) (eglot-reconnect current-process interactive)) (t + (when (process-live-p current-process) + (eglot-shutdown current-process 'sync)) (eglot--connect project managed-major-mode short-name command (lambda (proc) - (eglot--message "Connected! Process `%s' now managing `%s'\ + (eglot--message "Connected! Process `%s' now managing `%s' \ buffers in project %s." proc managed-major-mode commit 5a3d92cab383d385586fc3afb074b40469e81205 Author: João Távora Date: Fri May 4 11:21:11 2018 +0100 Connect to lsp server via tcp * eglot.el (eglot--make-process): Rename from eglot-make-local-process. (eglot): Fix docstring and rework. (eglot--bootstrap-fn): Remove (eglot--contact): New process-local var. (eglot--connect): Take CONTACT arg. (eglot--reconnect): Rework. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ac52b9adab..c5a0c0f0d7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -133,30 +133,40 @@ A list (ID WHAT DONE-P)." t) "Status as declared by the server. A list (WHAT SERIOUS-P)." t) -(eglot--define-process-var eglot--bootstrap-fn nil - "Function for returning processes/connetions to LSP servers. -Must be a function of one arg, a name, returning a process -object.") +(eglot--define-process-var eglot--contact nil + "Method used to contact a server. +Either a list of strings (a shell command and arguments), or a +list of a single string of the form :") (eglot--define-process-var eglot--buffer-open-count (make-hash-table) "Keeps track of didOpen/didClose notifs for each buffer.") -(defun eglot-make-local-process (name command) - "Make a local LSP process from COMMAND. +(defun eglot--make-process (name contact) + "Make a process from CONTACT. NAME is a name to give the inferior process or connection. -Returns a process object." +CONTACT is as `eglot--contact'. Returns a process object." (let* ((readable-name (format "EGLOT server (%s)" name)) + (buffer (get-buffer-create + (format "*%s inferior*" readable-name))) + (singleton (and (null (cdr contact)) (car contact))) (proc - (make-process - :name readable-name - :buffer (get-buffer-create - (format "*%s inferior*" readable-name)) - :command command - :connection-type 'pipe - :filter 'eglot--process-filter - :sentinel 'eglot--process-sentinel - :stderr (get-buffer-create (format "*%s stderr*" - name))))) + (if (and + singleton + (string-match "^[\s\t]*\\(.*\\):\\([[:digit:]]+\\)[\s\t]*$" + singleton)) + (open-network-stream readable-name + buffer + (match-string 1 singleton) + (string-to-number (match-string 2 singleton))) + (make-process + :name readable-name + :buffer buffer + :command contact + :connection-type 'pipe + :stderr (get-buffer-create (format "*%s stderr*" + name)))))) + (set-process-filter proc #'eglot--process-filter) + (set-process-sentinel proc #'eglot--process-sentinel) proc)) (defmacro eglot--obj (&rest what) @@ -180,13 +190,12 @@ Returns a process object." retval)) (defun eglot--connect (project managed-major-mode - short-name bootstrap-fn &optional success-fn) - "Make a connection for PROJECT, SHORT-NAME and MANAGED-MAJOR-MODE. -Use BOOTSTRAP-FN to make the actual process object. Call + short-name contact &optional success-fn) + "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT. SUCCESS-FN with no args if all goes well." - (let* ((proc (funcall bootstrap-fn short-name)) + (let* ((proc (eglot--make-process short-name contact)) (buffer (process-buffer proc))) - (setf (eglot--bootstrap-fn proc) bootstrap-fn + (setf (eglot--contact proc) contact (eglot--project proc) project (eglot--major-mode proc) managed-major-mode) (with-current-buffer buffer @@ -226,15 +235,17 @@ SUCCESS-FN with no args if all goes well." "History of COMMAND arguments to `eglot'.") (defun eglot (managed-major-mode command &optional interactive) - ;; FIXME: Later make this function also connect to TCP servers by - ;; overloading semantics on COMMAND. "Start a Language Server Protocol server. Server is started with COMMAND and manages buffers of MANAGED-MAJOR-MODE for the current project. COMMAND is a list of strings, an executable program and -optionally its arguments. MANAGED-MAJOR-MODE is an Emacs major -mode. +optionally its arguments. If the first and only string in the +list is of the form \":\" it is taken as an +indication to connect to a server instead of starting one. This +is also know as the server's \"contact\". + +MANAGED-MAJOR-MODE is an Emacs major mode. With a prefix arg, prompt for MANAGED-MAJOR-MODE and COMMAND, else guess them from current context and `eglot-executables'. @@ -257,47 +268,45 @@ INTERACTIVE is t if called interactively." (cdr (assoc managed-major-mode eglot-executables)))) (list managed-major-mode - (if current-prefix-arg - (split-string-and-unquote - (read-shell-command "[eglot] Run program: " - (combine-and-quote-strings guessed-command) - 'eglot-command-history)) - guessed-command) + (let ((prompt + (cond (current-prefix-arg + "[eglot] Execute program (or connect to :) ") + ((null guessed-command) + (format "[eglot] Sorry, couldn't guess for `%s'!\n\ +Execute program (or connect to :) " + managed-major-mode))))) + (if prompt + (split-string-and-unquote + (read-shell-command prompt + (combine-and-quote-strings guessed-command) + 'eglot-command-history)) + guessed-command)) t))) (let* ((project (project-current)) (short-name (eglot--project-short-name project))) - (unless project (eglot--error - "Cannot work without a current project!")) - (let ((current-process (eglot--current-process)) - (command - (or command - (eglot--error "Don't know how to start EGLOT for %s buffers" - major-mode)))) - (cond - ((and current-process - (process-live-p current-process)) - (when (and - interactive - (y-or-n-p "[eglot] Live process found, reconnect instead? ")) - (eglot-reconnect current-process interactive))) - (t - (eglot--connect - project - managed-major-mode - short-name - (lambda (name) - (eglot-make-local-process - name - command)) - (lambda (proc) - (eglot--message "Connected! Process `%s' now managing `%s'\ + (unless project (eglot--error "Cannot work without a current project!")) + (unless command (eglot--error "Don't know how to start EGLOT for %s buffers" + major-mode)) + (let ((current-process (eglot--current-process))) + (cond ((and (process-live-p current-process) + interactive + (y-or-n-p "[eglot] Live process found, reconnect instead? ")) + (eglot-reconnect current-process interactive)) + (t + (eglot--connect + project + managed-major-mode + short-name + command + (lambda (proc) + (eglot--message "Connected! Process `%s' now managing `%s'\ buffers in project %s." - proc - managed-major-mode - short-name) - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (eglot--maybe-activate-editing-mode proc)))))))))) + proc + managed-major-mode + short-name) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (eglot--maybe-activate-editing-mode proc)))))))))) (defun eglot-reconnect (process &optional interactive) "Reconnect to PROCESS. @@ -309,7 +318,7 @@ INTERACTIVE is t if called interactively." (eglot--project process) (eglot--major-mode process) (eglot--short-name process) - (eglot--bootstrap-fn process) + (eglot--contact process) (lambda (proc) (eglot--message "Reconnected!") (dolist (buffer (buffer-list)) commit 0d002553edc2f2189d0bf0d84fc981215192d96e Author: João Távora Date: Fri May 4 01:20:13 2018 +0100 More correctly keep track of didopen/didclose per buffer * eglot.el (eglot--buffer-open-count): Now a process-local var. (eglot--signal-textDocument/didOpen, eglot--signal-textDocument/didClose): Use it. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bdd339d971..ac52b9adab 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -138,6 +138,9 @@ A list (WHAT SERIOUS-P)." t) Must be a function of one arg, a name, returning a process object.") +(eglot--define-process-var eglot--buffer-open-count (make-hash-table) + "Keeps track of didOpen/didClose notifs for each buffer.") + (defun eglot-make-local-process (name command) "Make a local LSP process from COMMAND. NAME is a name to give the inferior process or connection. @@ -1026,26 +1029,35 @@ Records START, END and PRE-CHANGE-LENGTH locally." (setq eglot--recent-before-changes nil eglot--recent-after-changes nil)) -(defvar-local eglot--buffer-open-count 0) (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." - (cl-incf eglot--buffer-open-count) - (when (> eglot--buffer-open-count 1) - (error "Too many textDocument/didOpen notifs for %s" (current-buffer))) - (eglot--notify (eglot--current-process-or-lose) - :textDocument/didOpen - (eglot--obj :textDocument - (eglot--current-buffer-TextDocumentItem)))) + (let* ((proc (eglot--current-process-or-lose)) + (count (1+ (or (gethash (current-buffer) + (eglot--buffer-open-count proc)) + 0)))) + (when (> count 1) + (eglot--error "Too many textDocument/didOpen notifs for %s" (current-buffer))) + (setf (gethash (current-buffer) (eglot--buffer-open-count proc)) + count) + (eglot--notify proc + :textDocument/didOpen + (eglot--obj :textDocument + (eglot--current-buffer-TextDocumentItem))))) (defun eglot--signal-textDocument/didClose () "Send textDocument/didClose to server." - (cl-decf eglot--buffer-open-count) - (when (< eglot--buffer-open-count 0) - (error "Too many textDocument/didClose notifs for %s" (current-buffer))) - (eglot--notify (eglot--current-process-or-lose) - :textDocument/didClose - (eglot--obj :textDocument - (eglot--current-buffer-TextDocumentItem)))) + (let* ((proc (eglot--current-process-or-lose)) + (count (1- (or (gethash (current-buffer) + (eglot--buffer-open-count proc)) + 0)))) + (when (< count 0) + (eglot--error "Too many textDocument/didClose notifs for %s" (current-buffer))) + (setf (gethash (current-buffer) (eglot--buffer-open-count proc)) + count) + (eglot--notify proc + :textDocument/didClose + (eglot--obj :textDocument + (eglot--current-buffer-TextDocumentItem))))) (defun eglot--signal-textDocument/willSave () "Send textDocument/willSave to server." commit 465635456c581a7bc667fc1333ce463e19262a35 Author: João Távora Date: Fri May 4 00:25:03 2018 +0100 Fix mode-line mouse-clicks from outside selected window * eglot.el (eglot--mode-line-call): New helper. (eglot--mode-line-format): Use it. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 693535ac8b..bdd339d971 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -695,6 +695,13 @@ that case, also signal textDocument/didOpen." (put 'eglot--mode-line-format 'risky-local-variable t) +(defun eglot--mode-line-call (what) + "Make an interactive lambda for calling WHAT from mode-line." + (lambda (event) + (interactive "e") + (with-selected-window (posn-window (event-start event)) + (call-interactively what)))) + (defun eglot--mode-line-format () "Compose the mode-line format spec." (pcase-let* ((proc (eglot--current-process)) @@ -715,7 +722,7 @@ that case, also signal textDocument/didOpen." face eglot-mode-line keymap ,(let ((map (make-sparse-keymap))) (define-key map [mode-line down-mouse-1] - eglot-menu) + (eglot--mode-line-call 'eglot-menu)) map) mouse-face mode-line-highlight help-echo "mouse-1: pop-up EGLOT menu" @@ -726,9 +733,12 @@ that case, also signal textDocument/didOpen." ,name face eglot-mode-line keymap ,(let ((map (make-sparse-keymap))) - (define-key map [mode-line mouse-1] 'eglot-events-buffer) - (define-key map [mode-line mouse-2] 'eglot-shutdown) - (define-key map [mode-line mouse-3] 'eglot-reconnect) + (define-key map [mode-line mouse-1] + (eglot--mode-line-call 'eglot-events-buffer)) + (define-key map [mode-line mouse-2] + (eglot--mode-line-call 'eglot-shutdown)) + (define-key map [mode-line mouse-3] + (eglot--mode-line-call 'eglot-reconnect)) map) mouse-face mode-line-highlight help-echo ,(concat "mouse-1: go to events buffer\n" @@ -744,9 +754,9 @@ that case, also signal textDocument/didOpen." face compilation-mode-line-fail keymap ,(let ((map (make-sparse-keymap))) (define-key map [mode-line mouse-1] - 'eglot-events-buffer) + (eglot--mode-line-call 'eglot-events-buffer)) (define-key map [mode-line mouse-3] - 'eglot-clear-status) + (eglot--mode-line-call 'eglot-clear-status)) map)))) ,@(when (and doing (not done-p)) `("/" @@ -757,7 +767,7 @@ that case, also signal textDocument/didOpen." face compilation-mode-line-run keymap ,(let ((map (make-sparse-keymap))) (define-key map [mode-line mouse-1] - 'eglot-events-buffer) + (eglot--mode-line-call 'eglot-events-buffer)) map)))) ,@(when (cl-plusp pending) `("/" @@ -775,9 +785,9 @@ that case, also signal textDocument/didOpen." 'eglot-mode-line)) keymap ,(let ((map (make-sparse-keymap))) (define-key map [mode-line mouse-1] - 'eglot-events-buffer) + (eglot--mode-line-call 'eglot-events-buffer)) (define-key map [mode-line mouse-3] - 'eglot-forget-pending-continuations) + (eglot--mode-line-call 'eglot-forget-pending-continuations)) map))))))))) (add-to-list 'mode-line-misc-info commit 0a587a881afc76b479fc18d0f0c52e0b32998d80 Author: João Távora Date: Fri May 4 00:12:53 2018 +0100 * eglot.el (eglot--process-receive): skip null method notifs. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ebc07d2162..693535ac8b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -477,8 +477,11 @@ identifier. ERROR is non-nil if this is an error." (plist-get message :params) (let ((id (plist-get message :id))) (if id `(:id ,id))))) - (eglot--warn "No implemetation for notification %s yet" - method))))))) + ;; pyls keeps on sending nil notifs for each notif we + ;; send it, just ignore these. + (unless (null method) + (eglot--warn "No implemetation for notification %s yet" + method)))))))) (defvar eglot--expect-carriage-return nil) commit 009062feb70d5d9ea0c3c68f7c1a977bdb4c80bb Author: João Távora Date: Thu May 3 23:59:56 2018 +0100 Trim some edges and add a bunch of boring rpc methods * eglot.el (eglot--connect): Don't call eglot--protocol-initialize. (eglot--process-filter): Break long line. (eglot--process-receive): Also pass id to handler if a server request. (eglot--log): New helper. (eglot-editing-mode): Manage before-revert-hook, after-revert-hook, before-save-hook, after-save-hook. (eglot--protocol-initialize): Removed. (eglot--server-window/showMessage): Simplify. (eglot--server-window/showMessageRequest) (eglot--server-window/logMessage, eglot--server-telemetry/event): New handlers. (eglot--signal-textDocument/willSave) (eglot--signal-textDocument/didSave): New notifications. (eglot--signal-textDocument/didOpen) (eglot--signal-textDocument/didClose): Check eglot--buffer-open-count. (eglot--buffer-open-count): New var. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e7fe18ccf4..ebc07d2162 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -198,13 +198,26 @@ SUCCESS-FN with no args if all goes well." (let ((inhibit-read-only t)) (insert (format "\n-----------------------------------\n")))) - (eglot--protocol-initialize + (eglot--request proc + :initialize + (eglot--obj :processId (emacs-pid) + :rootPath (concat + (expand-file-name (car (project-roots + (project-current))))) + :initializationOptions [] + :capabilities + (eglot--obj + :workspace (eglot--obj) + :textDocument (eglot--obj + :publishDiagnostics `(:relatedInformation t)))) + :success-fn (cl-function (lambda (&key capabilities) (setf (eglot--capabilities proc) capabilities) (setf (eglot--status proc) nil) - (when success-fn (funcall success-fn proc))))))))) + (when success-fn (funcall success-fn proc)) + (eglot--notify proc :initialized nil)))))))) (defvar eglot--command-history nil "History of COMMAND arguments to `eglot'.") @@ -347,7 +360,8 @@ INTERACTIVE is t if called interactively." ;; (setq expected-bytes (and (search-forward-regexp - "\\(?:.*: .*\r\n\\)*Content-Length: *\\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" + "\\(?:.*: .*\r\n\\)*Content-Length: \ +*\\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" (+ (point) 100) t) (string-to-number (match-string 1)))) @@ -454,11 +468,15 @@ identifier. ERROR is non-nil if this is an error." (t (apply (cl-first continuations) (plist-get message :result))))) (t + ;; a server notification or a server request (let* ((method (plist-get message :method)) (handler-sym (intern (concat "eglot--server-" method)))) (if (functionp handler-sym) - (apply handler-sym proc (plist-get message :params)) + (apply handler-sym proc (append + (plist-get message :params) + (let ((id (plist-get message :id))) + (if id `(:id ,id))))) (eglot--warn "No implemetation for notification %s yet" method))))))) @@ -587,6 +605,10 @@ identifier. ERROR is non-nil if this is an error." "Message out with FORMAT with ARGS." (message (concat "[eglot] " (apply #'format format args)))) +(defun eglot--log (format &rest args) + "Log out with FORMAT with ARGS." + (message (concat "[eglot-log] " (apply #'format format args)))) + (defun eglot--warn (format &rest args) "Warning message with FORMAT and ARGS." (apply #'eglot--message (concat "(warning) " format) args) @@ -614,15 +636,22 @@ identifier. ERROR is non-nil if this is an error." (add-hook 'before-change-functions 'eglot--before-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) + (add-hook 'before-revert-hook 'eglot--signal-textDocument/didClose nil t) + (add-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen nil t) + (add-hook 'before-save-hook 'eglot--signal-textDocument/willSave nil t) + (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) (flymake-mode 1) - (if (eglot--current-process) - (eglot--signal-textDocument/didOpen) + (unless (eglot--current-process) (eglot--warn "No process, start one with `M-x eglot'"))) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) (remove-hook 'before-change-functions 'eglot--before-change t) - (remove-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose t)))) + (remove-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose t) + (remove-hook 'before-revert-hook 'eglot--signal-textDocument/didClose t) + (remove-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen t) + (remove-hook 'before-save-hook 'eglot--signal-textDocument/willSave t) + (remove-hook 'after-save-hook 'eglot--signal-textDocument/didSave t)))) (define-minor-mode eglot-mode "Minor mode for all buffers managed by EGLOT in some way." nil @@ -755,25 +784,6 @@ that case, also signal textDocument/didOpen." ;;; Protocol implementation (Requests, notifications, etc) ;;; -(defun eglot--protocol-initialize (process success-fn) - "Initialize LSP protocol. -PROCESS is a connected process (network or local). SUCCESS-FN is -called with capabilites after connection." - (eglot--request - process - :initialize - (eglot--obj :processId (emacs-pid) - :rootPath (concat - (expand-file-name (car (project-roots - (project-current))))) - :initializationOptions [] - :capabilities - (eglot--obj - :workspace (eglot--obj) - :textDocument (eglot--obj - :publishDiagnostics `(:relatedInformation t)))) - :success-fn success-fn)) - (defun eglot-shutdown (process &optional sync interactive) "Politely ask the server PROCESS to quit. Forcefully quit it if it doesn't respond. @@ -808,14 +818,47 @@ running. INTERACTIVE is t if called interactively." :timeout-fn brutal))) (cl-defun eglot--server-window/showMessage - (process &key type message) + (_process &key type message) "Handle notification window/showMessage" - (when (<= 1 type) - (setf (eglot--status process) '("error" t)) - (eglot--log-event process - (propertize "server-error" 'face 'error) - message)) - (eglot--message "Server reports (type=%s): %s" type message)) + (eglot--message (propertize "Server reports (type=%s): %s" + 'face (if (<= type 1) 'error)) + type message)) + +(cl-defun eglot--server-window/showMessageRequest + (process &key id type message actions) + "Handle server request window/showMessageRequest" + (let (reply) + (unwind-protect + (setq reply + (completing-read + (concat + (format (propertize "[eglot] Server reports (type=%s): %s" + 'face (if (<= type 1) 'error)) + type message) + "\nChoose an option: ") + (mapcar (lambda (obj) (plist-get obj :title)) actions) + nil + t + (plist-get (elt actions 0) :title))) + (eglot--process-send + id + process + (if reply + (eglot--obj :result (eglot--obj :title reply)) + ;; request cancelled + (eglot--obj :error -32800)))))) + +(cl-defun eglot--server-window/logMessage + (_process &key type message) + "Handle notification window/logMessage" + (eglot--log (propertize "Server reports (type=%s): %s" + 'face (if (<= type 1) 'error)) + type message)) + +(cl-defun eglot--server-telemetry/event + (_process &rest any) + "Handle notification telemetry/event" + (eglot--log "Server telemetry: %s" any)) (defvar-local eglot--current-flymake-report-fn nil "Current flymake report function for this buffer") @@ -970,8 +1013,12 @@ Records START, END and PRE-CHANGE-LENGTH locally." (setq eglot--recent-before-changes nil eglot--recent-after-changes nil)) +(defvar-local eglot--buffer-open-count 0) (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." + (cl-incf eglot--buffer-open-count) + (when (> eglot--buffer-open-count 1) + (error "Too many textDocument/didOpen notifs for %s" (current-buffer))) (eglot--notify (eglot--current-process-or-lose) :textDocument/didOpen (eglot--obj :textDocument @@ -979,11 +1026,33 @@ Records START, END and PRE-CHANGE-LENGTH locally." (defun eglot--signal-textDocument/didClose () "Send textDocument/didClose to server." + (cl-decf eglot--buffer-open-count) + (when (< eglot--buffer-open-count 0) + (error "Too many textDocument/didClose notifs for %s" (current-buffer))) (eglot--notify (eglot--current-process-or-lose) :textDocument/didClose (eglot--obj :textDocument (eglot--current-buffer-TextDocumentItem)))) +(defun eglot--signal-textDocument/willSave () + "Send textDocument/willSave to server." + (eglot--notify + (eglot--current-process-or-lose) + :textDocument/willSave + (eglot--obj + :reason 1 ; Manual, emacs laughs in the face of auto-save muahahahaha + :textDocument (eglot--current-buffer-TextDocumentItem)))) + +(defun eglot--signal-textDocument/didSave () + "Send textDocument/didSave to server." + (eglot--notify + (eglot--current-process-or-lose) + :textDocument/didSave + (eglot--obj + ;; TODO: Handle TextDocumentSaveRegistrationOptions to control this. + :text (buffer-substring-no-properties (point-min) (point-max)) + :textDocument (eglot--current-buffer-TextDocumentItem)))) + (defun eglot-flymake-backend (report-fn &rest _more) "An EGLOT Flymake backend. Calls REPORT-FN maybe if server publishes diagnostics in time." commit 6c84a2e8cbb1a1e9881710edc26c86f7947f2dd0 Author: João Távora Date: Thu May 3 22:44:13 2018 +0100 Fix a couple of rust-related edge cases * eglot.el (eglot--server-window/progress): Allow other keys. (eglot--server-textDocument/publishDiagnostics): Allow :group in diagnostic spec. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1bf0d561c3..e7fe18ccf4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -843,7 +843,7 @@ running. INTERACTIVE is t if called interactively." (line-beginning-position)))) (point)))) (cl-loop for diag-spec across diagnostics - collect (cl-destructuring-bind (&key range severity + collect (cl-destructuring-bind (&key range severity _group _code _source message) diag-spec (cl-destructuring-bind (&key start end) @@ -1002,7 +1002,7 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." ;;; Rust-specific ;;; (cl-defun eglot--server-window/progress - (process &key id done title ) + (process &key id done title &allow-other-keys) "Handle notification window/progress" (setf (eglot--spinner process) (list id title done))) commit 22965312e117a8f864a23e223f32c29cae0fdba4 Author: João Távora Date: Thu May 3 22:42:13 2018 +0100 Fix textdocument/didchange * eglot.el (eglot-editing-mode): Manage before-change-functions. (eglot--recent-changes): Deleted. (eglot--recent-before-changes): New var. (eglot--recent-after-changes): Renamed from eglot--recent-changes. (eglot--pos-to-lsp-position, eglot--before-change): New helpers. (eglot--after-change): Rework. (eglot--signal-textDocument/didChange): Rework. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 469d3f704a..1bf0d561c3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -611,6 +611,7 @@ identifier. ERROR is non-nil if this is an error." (eglot-editing-mode (eglot-mode 1) (add-hook 'after-change-functions 'eglot--after-change nil t) + (add-hook 'before-change-functions 'eglot--before-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) (flymake-mode 1) @@ -620,6 +621,7 @@ identifier. ERROR is non-nil if this is an error." (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) + (remove-hook 'before-change-functions 'eglot--before-change t) (remove-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose t)))) (define-minor-mode eglot-mode @@ -868,7 +870,9 @@ running. INTERACTIVE is t if called interactively." (t (eglot--message "OK so %s isn't visited" filename))))) -(defvar eglot--recent-changes nil +(defvar eglot--recent-before-changes nil + "List of recent changes as collected by `eglot--before-change'.") +(defvar eglot--recent-after-changes nil "List of recent changes as collected by `eglot--after-change'.") (defvar-local eglot--versioned-identifier 0) @@ -895,46 +899,76 @@ running. INTERACTIVE is t if called interactively." (widen) (buffer-substring-no-properties (point-min) (point-max)))))) -(defun eglot--after-change (start end length) +(defun eglot--pos-to-lsp-position (pos) + "Convert point POS to LSP position." + (save-excursion + (eglot--obj :line + ;; F!@(#*&#$)CKING OFF-BY-ONE + (1- (line-number-at-pos pos t)) + :character + (- (goto-char pos) + (line-beginning-position))))) + +(defun eglot--before-change (start end) + "Hook onto `before-change-functions'. +Records START and END, crucially convert them into +LSP (line/char) positions before that information is +lost (because the after-change thingy doesn't know if newlines +were deleted/added)" + (push (list (eglot--pos-to-lsp-position start) + (eglot--pos-to-lsp-position end)) + eglot--recent-before-changes)) + +(defun eglot--after-change (start end pre-change-length) "Hook onto `after-change-functions'. -Records START, END and LENGTH locally." +Records START, END and PRE-CHANGE-LENGTH locally." (cl-incf eglot--versioned-identifier) - (push (list start end length) eglot--recent-changes) - ;; (eglot--message "start is %s, end is %s, length is %s" start end length) - ) + (push (list start end pre-change-length) eglot--recent-after-changes)) (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." - (when eglot--recent-changes + (when (and eglot--recent-before-changes + eglot--recent-after-changes) (save-excursion (save-restriction (widen) - (let* ((start (cl-reduce #'min (mapcar #'car eglot--recent-changes))) - (end (cl-reduce #'max (mapcar #'cadr eglot--recent-changes)))) - (eglot--notify - (eglot--current-process-or-lose) - :textDocument/didChange - (eglot--obj - :textDocument (eglot--current-buffer-VersionedTextDocumentIdentifier) - :contentChanges - (vector + (if (/= (length eglot--recent-before-changes) + (length eglot--recent-after-changes)) + (eglot--notify + (eglot--current-process-or-lose) + :textDocument/didChange (eglot--obj - :range (eglot--obj - :start - (eglot--obj :line - (line-number-at-pos start t) - :character - (- (goto-char start) - (line-beginning-position))) - :end - (eglot--obj :line - (line-number-at-pos end t) - :character - (- (goto-char end) - (line-beginning-position)))) - :rangeLength (- end start) - :text (buffer-substring-no-properties start end)))))))) - (setq eglot--recent-changes nil))) + :textDocument (eglot--current-buffer-VersionedTextDocumentIdentifier) + :contentChanges + (vector + (eglot--obj + :text (buffer-substring-no-properties (point-min) (point-max)))))) + (let ((combined (cl-mapcar 'append + eglot--recent-before-changes + eglot--recent-after-changes))) + (eglot--notify + (eglot--current-process-or-lose) + :textDocument/didChange + (eglot--obj + :textDocument (eglot--current-buffer-VersionedTextDocumentIdentifier) + :contentChanges + (apply + #'vector + (mapcar (pcase-lambda (`(,before-start-position + ,before-end-position + ,after-start + ,after-end + ,len)) + (eglot--obj + :range + (eglot--obj + :start before-start-position + :end before-end-position) + :rangeLength len + :text (buffer-substring-no-properties after-start after-end))) + (reverse combined)))))))))) + (setq eglot--recent-before-changes nil + eglot--recent-after-changes nil)) (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." commit e5e9437882406c406ed401fc67faf97d523a0e8e Author: João Távora Date: Thu May 3 16:14:35 2018 +0100 Rename rpc methods for clarity * eglot.el (eglot--process-receive): Search for RPC server methods under `eglot--server-' (eglot-editing-mode, eglot--maybe-activate-editing-mode): Use new signal names. (eglot--server-window/showMessage): Rename from eglot--window/showMessage. (eglot--server-textDocument/publishDiagnostics): Renamed from eglot--textDocument/publishDiagnostics. (eglot--current-buffer-versioned-identifier): Remove. (eglot--current-buffer-VersionedTextDocumentIdentifier): Use eglot--versioned-identifier. (eglot--signal-textDocument/didChange): Renamed from eglot--maybe-signal-didChange. (eglot--signal-textDocument/didOpen): Renamed from eglot--signalDidOpen. (eglot--signal-textDocument/didClose): Rename from eglot--signalDidClose. (eglot-flymake-backend): Call eglot--signal-textDocument/didChange. (eglot--server-window/progress): Rename from eglot--window/progress. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7d44e6dd27..469d3f704a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -455,12 +455,12 @@ identifier. ERROR is non-nil if this is an error." (apply (cl-first continuations) (plist-get message :result))))) (t (let* ((method (plist-get message :method)) - (handler-sym (intern (concat "eglot--" + (handler-sym (intern (concat "eglot--server-" method)))) (if (functionp handler-sym) (apply handler-sym proc (plist-get message :params)) - (eglot--debug "No implemetation for notification %s yet" - method))))))) + (eglot--warn "No implemetation for notification %s yet" + method))))))) (defvar eglot--expect-carriage-return nil) @@ -612,15 +612,15 @@ identifier. ERROR is non-nil if this is an error." (eglot-mode 1) (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) - (add-hook 'kill-buffer-hook 'eglot--signalDidClose nil t) + (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) (flymake-mode 1) (if (eglot--current-process) - (eglot--signalDidOpen) + (eglot--signal-textDocument/didOpen) (eglot--warn "No process, start one with `M-x eglot'"))) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) - (remove-hook 'kill-buffer-hook 'eglot--signalDidClose t)))) + (remove-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose t)))) (define-minor-mode eglot-mode "Minor mode for all buffers managed by EGLOT in some way." nil @@ -643,7 +643,7 @@ that case, also signal textDocument/didOpen." (and proc (eq proc cur))) (unless eglot-editing-mode (eglot-editing-mode 1)) - (eglot--signalDidOpen) + (eglot--signal-textDocument/didOpen) (flymake-start))))) (add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) @@ -805,7 +805,7 @@ running. INTERACTIVE is t if called interactively." :async-p (not sync) :timeout-fn brutal))) -(cl-defun eglot--window/showMessage +(cl-defun eglot--server-window/showMessage (process &key type message) "Handle notification window/showMessage" (when (<= 1 type) @@ -821,7 +821,7 @@ running. INTERACTIVE is t if called interactively." (defvar-local eglot--unreported-diagnostics nil "Unreported diagnostics for this buffer.") -(cl-defun eglot--textDocument/publishDiagnostics +(cl-defun eglot--server-textDocument/publishDiagnostics (_process &key uri diagnostics) "Handle notification publishDiagnostics" (let* ((obj (url-generic-parse-url uri)) @@ -873,11 +873,6 @@ running. INTERACTIVE is t if called interactively." (defvar-local eglot--versioned-identifier 0) -(defun eglot--current-buffer-versioned-identifier () - "Return a VersionedTextDocumentIdentifier." - ;; FIXME: later deal with workspaces - eglot--versioned-identifier) - (defun eglot--current-buffer-VersionedTextDocumentIdentifier () "Compute VersionedTextDocumentIdentifier object for current buffer." (eglot--obj :uri @@ -885,7 +880,8 @@ running. INTERACTIVE is t if called interactively." (url-hexify-string (file-truename buffer-file-name) url-path-allowed-chars)) - :version (eglot--current-buffer-versioned-identifier))) + ;; FIXME: later deal with workspaces + :version eglot--versioned-identifier)) (defun eglot--current-buffer-TextDocumentItem () "Compute TextDocumentItem object for current buffer." @@ -907,7 +903,7 @@ Records START, END and LENGTH locally." ;; (eglot--message "start is %s, end is %s, length is %s" start end length) ) -(defun eglot--maybe-signal-didChange () +(defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." (when eglot--recent-changes (save-excursion @@ -940,14 +936,14 @@ Records START, END and LENGTH locally." :text (buffer-substring-no-properties start end)))))))) (setq eglot--recent-changes nil))) -(defun eglot--signalDidOpen () +(defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." (eglot--notify (eglot--current-process-or-lose) :textDocument/didOpen (eglot--obj :textDocument (eglot--current-buffer-TextDocumentItem)))) -(defun eglot--signalDidClose () +(defun eglot--signal-textDocument/didClose () "Send textDocument/didClose to server." (eglot--notify (eglot--current-process-or-lose) :textDocument/didClose @@ -966,12 +962,12 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." (setq eglot--current-flymake-report-fn report-fn) ;; Take this opportunity to signal a didChange that might eventually ;; make the server report new diagnostics. - (eglot--maybe-signal-didChange)) + (eglot--signal-textDocument/didChange)) ;;; Rust-specific ;;; -(cl-defun eglot--window/progress +(cl-defun eglot--server-window/progress (process &key id done title ) "Handle notification window/progress" (setf (eglot--spinner process) (list id title done))) commit 4d1c9b903d70283e88a3f35f2ebf6704aa127cd5 Author: João Távora Date: Thu May 3 16:04:03 2018 +0100 Reorganize file * eglot.el (eglot-mode-line): Move up. (eglot-make-local-process, eglot--all-major-modes, eglot--obj) (eglot--project-short-name, eglot--all-major-modes) (eglot-reconnect, eglot--maybe-activate-editing-mode) (eglot--protocol-initialize) (eglot--window/showMessage, eglot--current-flymake-report-fn) (eglot--unreported-diagnostics) (eglot--textDocument/publishDiagnostics, eglot--signalDidOpen) (eglot--signalDidClose): Move around. (eglot-quit-server): Renamed to eglot-shutdown. (eglot-shutdown): New function diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f25c7bd346..7d44e6dd27 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -34,6 +34,8 @@ (require 'warnings) (require 'flymake) + +;;; User tweakable stuff (defgroup eglot nil "Interaction with Language Server Protocol servers" :prefix "eglot-" @@ -43,6 +45,13 @@ (python-mode . ("pyls"))) "Alist mapping major modes to server executables.") +(defface eglot-mode-line + '((t (:inherit font-lock-constant-face :weight bold))) + "Face for package-name in EGLOT's mode line." + :group 'eglot) + + +;;; Process management (defvar eglot--processes-by-project (make-hash-table :test #'equal) "Keys are projects. Values are lists of processes.") @@ -129,20 +138,6 @@ A list (WHAT SERIOUS-P)." t) Must be a function of one arg, a name, returning a process object.") -(defun eglot--project-short-name (project) - "Give PROJECT a short name." - (file-name-base - (directory-file-name - (car (project-roots project))))) - -(defun eglot--all-major-modes () - "Return all know major modes." - (let ((retval)) - (mapatoms (lambda (sym) - (when (plist-member (symbol-plist sym) 'derived-mode-parent) - (push sym retval)))) - retval)) - (defun eglot-make-local-process (name command) "Make a local LSP process from COMMAND. NAME is a name to give the inferior process or connection. @@ -161,6 +156,26 @@ Returns a process object." name))))) proc)) +(defmacro eglot--obj (&rest what) + "Make WHAT a suitable argument for `json-encode'." + ;; FIXME: maybe later actually do something, for now this just fixes + ;; the indenting of literal plists. + `(list ,@what)) + +(defun eglot--project-short-name (project) + "Give PROJECT a short name." + (file-name-base + (directory-file-name + (car (project-roots project))))) + +(defun eglot--all-major-modes () + "Return all know major modes." + (let ((retval)) + (mapatoms (lambda (sym) + (when (plist-member (symbol-plist sym) 'derived-mode-parent) + (push sym retval)))) + retval)) + (defun eglot--connect (project managed-major-mode short-name bootstrap-fn &optional success-fn) "Make a connection for PROJECT, SHORT-NAME and MANAGED-MAJOR-MODE. @@ -191,23 +206,6 @@ SUCCESS-FN with no args if all goes well." (setf (eglot--status proc) nil) (when success-fn (funcall success-fn proc))))))))) -(defun eglot-reconnect (process &optional interactive) - "Reconnect to PROCESS. -INTERACTIVE is t if called interactively." - (interactive (list (eglot--current-process-or-lose) t)) - (when (process-live-p process) - (eglot-quit-server process 'sync interactive)) - (eglot--connect - (eglot--project process) - (eglot--major-mode process) - (eglot--short-name process) - (eglot--bootstrap-fn process) - (lambda (proc) - (eglot--message "Reconnected!") - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (eglot--maybe-activate-editing-mode proc)))))) - (defvar eglot--command-history nil "History of COMMAND arguments to `eglot'.") @@ -285,6 +283,23 @@ buffers in project %s." (with-current-buffer buffer (eglot--maybe-activate-editing-mode proc)))))))))) +(defun eglot-reconnect (process &optional interactive) + "Reconnect to PROCESS. +INTERACTIVE is t if called interactively." + (interactive (list (eglot--current-process-or-lose) t)) + (when (process-live-p process) + (eglot-shutdown process 'sync interactive)) + (eglot--connect + (eglot--project process) + (eglot--major-mode process) + (eglot--short-name process) + (eglot--bootstrap-fn process) + (lambda (proc) + (eglot--message "Reconnected!") + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (eglot--maybe-activate-editing-mode proc)))))) + (defun eglot--process-sentinel (process change) "Called with PROCESS undergoes CHANGE." (eglot--debug "(sentinel) Process state changed to %s" change) @@ -370,12 +385,6 @@ buffers in project %s." ;; (setf (eglot--expected-bytes proc) expected-bytes))))) -(defmacro eglot--obj (&rest what) - "Make WHAT a suitable argument for `json-encode'." - ;; FIXME: maybe later actually do something, for now this just fixes - ;; the indenting of literal plists. - `(list ,@what)) - (defun eglot-events-buffer (process &optional interactive) "Display events buffer for current LSP connection PROCESS. INTERACTIVE is t if called interactively." @@ -561,125 +570,6 @@ identifier. ERROR is non-nil if this is an error." :method method :params params))) - -;;; Requests -;;; -(defun eglot--protocol-initialize (process success-fn) - "Initialize LSP protocol. -PROCESS is a connected process (network or local). SUCCESS-FN is -called with capabilites after connection." - (eglot--request - process - :initialize - (eglot--obj :processId (emacs-pid) - :rootPath (concat - (expand-file-name (car (project-roots - (project-current))))) - :initializationOptions [] - :capabilities - (eglot--obj - :workspace (eglot--obj) - :textDocument (eglot--obj - :publishDiagnostics `(:relatedInformation t)))) - :success-fn success-fn)) - -(defun eglot-quit-server (process &optional sync interactive) - "Politely ask the server PROCESS to quit. -If SYNC, don't leave this function with the server still -running. INTERACTIVE is t if called interactively." - (interactive (list (eglot--current-process-or-lose) t t)) - (when interactive - (eglot--message "(eglot-quit-server) Asking %s politely to terminate" - process)) - (let ((brutal (lambda () - (eglot--warn "Brutally deleting existing process %s" - process) - (setf (eglot--moribund process) t) - (delete-process process)))) - (eglot--request - process - :shutdown - nil - :success-fn (lambda (&rest _anything) - (when interactive - (eglot--message "Now asking %s politely to exit" process)) - (setf (eglot--moribund process) t) - (eglot--request process - :exit - nil - :success-fn brutal - :async-p (not sync) - :error-fn brutal - :timeout-fn brutal)) - :error-fn brutal - :async-p (not sync) - :timeout-fn brutal))) - - -;;; Notifications -;;; -(defvar-local eglot--current-flymake-report-fn nil - "Current flymake report function for this buffer") -(defvar-local eglot--unreported-diagnostics nil - "Unreported diagnostics for this buffer.") - -(cl-defun eglot--textDocument/publishDiagnostics - (_process &key uri diagnostics) - "Handle notification publishDiagnostics" - (let* ((obj (url-generic-parse-url uri)) - (filename (car (url-path-and-query obj))) - (buffer (find-buffer-visiting filename))) - (cond - (buffer - (with-current-buffer buffer - (cl-flet ((pos-at - (pos-plist) - (save-excursion - (goto-char (point-min)) - (forward-line (plist-get pos-plist :line)) - (forward-char - (min (plist-get pos-plist :character) - (- (line-end-position) - (line-beginning-position)))) - (point)))) - (cl-loop for diag-spec across diagnostics - collect (cl-destructuring-bind (&key range severity - _code _source message) - diag-spec - (cl-destructuring-bind (&key start end) - range - (let* ((begin-pos (pos-at start)) - (end-pos (pos-at end))) - (flymake-make-diagnostic - (current-buffer) - begin-pos end-pos - (cond ((<= severity 1) - :error) - ((= severity 2) - :warning) - (t - :note)) - message)))) - into diags - finally - (if eglot--current-flymake-report-fn - (funcall eglot--current-flymake-report-fn - diags) - (setq eglot--unreported-diagnostics - diags)))))) - (t - (eglot--message "OK so %s isn't visited" filename))))) - -(cl-defun eglot--window/showMessage - (process &key type message) - "Handle notification window/showMessage" - (when (<= 1 type) - (setf (eglot--status process) '("error" t)) - (eglot--log-event process - (propertize "server-error" 'face 'error) - message)) - (eglot--message "Server reports (type=%s): %s" type message)) - ;;; Helpers ;;; @@ -705,34 +595,13 @@ running. INTERACTIVE is t if called interactively." (apply #'format format args) :warning))) - -;;; Minor modes and mode-line +;;; Minor modes ;;; -(defface eglot-mode-line - '((t (:inherit font-lock-constant-face :weight bold))) - "Face for package-name in EGLOT's mode line." - :group 'eglot) - (defvar eglot-mode-map (make-sparse-keymap)) (defvar eglot-editing-mode-map (make-sparse-keymap)) -(defun eglot--maybe-activate-editing-mode (&optional proc) - "Maybe activate mode function `eglot-editing-mode'. -If PROC is supplied, do it only if BUFFER is managed by it. In -that case, also signal textDocument/didOpen." - (when buffer-file-name - (let ((cur (eglot--current-process))) - (when (or (and (null proc) cur) - (and proc (eq proc cur))) - (unless eglot-editing-mode - (eglot-editing-mode 1)) - (eglot--signalDidOpen) - (flymake-start))))) - -(add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) - (define-minor-mode eglot-editing-mode "Minor mode for source buffers where EGLOT helps you edit." nil @@ -764,6 +633,24 @@ that case, also signal textDocument/didOpen." (when eglot-editing-mode (eglot-editing-mode -1))))) +(defun eglot--maybe-activate-editing-mode (&optional proc) + "Maybe activate mode function `eglot-editing-mode'. +If PROC is supplied, do it only if BUFFER is managed by it. In +that case, also signal textDocument/didOpen." + (when buffer-file-name + (let ((cur (eglot--current-process))) + (when (or (and (null proc) cur) + (and proc (eq proc cur))) + (unless eglot-editing-mode + (eglot-editing-mode 1)) + (eglot--signalDidOpen) + (flymake-start))))) + +(add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) + + +;;; Mode-line, menu and other sugar +;;; (defvar eglot-menu) (easy-menu-define eglot-menu eglot-mode-map "EGLOT" @@ -806,7 +693,7 @@ that case, also signal textDocument/didOpen." face eglot-mode-line keymap ,(let ((map (make-sparse-keymap))) (define-key map [mode-line mouse-1] 'eglot-events-buffer) - (define-key map [mode-line mouse-2] 'eglot-quit-server) + (define-key map [mode-line mouse-2] 'eglot-shutdown) (define-key map [mode-line mouse-3] 'eglot-reconnect) map) mouse-face mode-line-highlight @@ -863,6 +750,124 @@ that case, also signal textDocument/didOpen." `(eglot-mode (" [" eglot--mode-line-format "] "))) + +;;; Protocol implementation (Requests, notifications, etc) +;;; +(defun eglot--protocol-initialize (process success-fn) + "Initialize LSP protocol. +PROCESS is a connected process (network or local). SUCCESS-FN is +called with capabilites after connection." + (eglot--request + process + :initialize + (eglot--obj :processId (emacs-pid) + :rootPath (concat + (expand-file-name (car (project-roots + (project-current))))) + :initializationOptions [] + :capabilities + (eglot--obj + :workspace (eglot--obj) + :textDocument (eglot--obj + :publishDiagnostics `(:relatedInformation t)))) + :success-fn success-fn)) + +(defun eglot-shutdown (process &optional sync interactive) + "Politely ask the server PROCESS to quit. +Forcefully quit it if it doesn't respond. +If SYNC, don't leave this function with the server still +running. INTERACTIVE is t if called interactively." + (interactive (list (eglot--current-process-or-lose) t t)) + (when interactive + (eglot--message "(eglot-shutdown) Asking %s politely to terminate" + process)) + (let ((brutal (lambda () + (eglot--warn "Brutally deleting existing process %s" + process) + (setf (eglot--moribund process) t) + (delete-process process)))) + (eglot--request + process + :shutdown + nil + :success-fn (lambda (&rest _anything) + (when interactive + (eglot--message "Now asking %s politely to exit" process)) + (setf (eglot--moribund process) t) + (eglot--request process + :exit + nil + :success-fn brutal + :async-p (not sync) + :error-fn brutal + :timeout-fn brutal)) + :error-fn brutal + :async-p (not sync) + :timeout-fn brutal))) + +(cl-defun eglot--window/showMessage + (process &key type message) + "Handle notification window/showMessage" + (when (<= 1 type) + (setf (eglot--status process) '("error" t)) + (eglot--log-event process + (propertize "server-error" 'face 'error) + message)) + (eglot--message "Server reports (type=%s): %s" type message)) + +(defvar-local eglot--current-flymake-report-fn nil + "Current flymake report function for this buffer") + +(defvar-local eglot--unreported-diagnostics nil + "Unreported diagnostics for this buffer.") + +(cl-defun eglot--textDocument/publishDiagnostics + (_process &key uri diagnostics) + "Handle notification publishDiagnostics" + (let* ((obj (url-generic-parse-url uri)) + (filename (car (url-path-and-query obj))) + (buffer (find-buffer-visiting filename))) + (cond + (buffer + (with-current-buffer buffer + (cl-flet ((pos-at + (pos-plist) + (save-excursion + (goto-char (point-min)) + (forward-line (plist-get pos-plist :line)) + (forward-char + (min (plist-get pos-plist :character) + (- (line-end-position) + (line-beginning-position)))) + (point)))) + (cl-loop for diag-spec across diagnostics + collect (cl-destructuring-bind (&key range severity + _code _source message) + diag-spec + (cl-destructuring-bind (&key start end) + range + (let* ((begin-pos (pos-at start)) + (end-pos (pos-at end))) + (flymake-make-diagnostic + (current-buffer) + begin-pos end-pos + (cond ((<= severity 1) + :error) + ((= severity 2) + :warning) + (t + :note)) + message)))) + into diags + finally + (if eglot--current-flymake-report-fn + (funcall eglot--current-flymake-report-fn + diags) + (setq eglot--unreported-diagnostics + diags)))))) + (t + (eglot--message "OK so %s isn't visited" filename))))) + (defvar eglot--recent-changes nil "List of recent changes as collected by `eglot--after-change'.") @@ -902,20 +907,6 @@ Records START, END and LENGTH locally." ;; (eglot--message "start is %s, end is %s, length is %s" start end length) ) -(defun eglot--signalDidOpen () - "Send textDocument/didOpen to server." - (eglot--notify (eglot--current-process-or-lose) - :textDocument/didOpen - (eglot--obj :textDocument - (eglot--current-buffer-TextDocumentItem)))) - -(defun eglot--signalDidClose () - "Send textDocument/didClose to server." - (eglot--notify (eglot--current-process-or-lose) - :textDocument/didClose - (eglot--obj :textDocument - (eglot--current-buffer-TextDocumentItem)))) - (defun eglot--maybe-signal-didChange () "Send textDocument/didChange to server." (when eglot--recent-changes @@ -949,6 +940,20 @@ Records START, END and LENGTH locally." :text (buffer-substring-no-properties start end)))))))) (setq eglot--recent-changes nil))) +(defun eglot--signalDidOpen () + "Send textDocument/didOpen to server." + (eglot--notify (eglot--current-process-or-lose) + :textDocument/didOpen + (eglot--obj :textDocument + (eglot--current-buffer-TextDocumentItem)))) + +(defun eglot--signalDidClose () + "Send textDocument/didClose to server." + (eglot--notify (eglot--current-process-or-lose) + :textDocument/didClose + (eglot--obj :textDocument + (eglot--current-buffer-TextDocumentItem)))) + (defun eglot-flymake-backend (report-fn &rest _more) "An EGLOT Flymake backend. Calls REPORT-FN maybe if server publishes diagnostics in time." commit 58c19b7683263548b564696df61ea67b6fd96aa8 Author: João Távora Date: Thu May 3 15:47:39 2018 +0100 Delete two useless forward declarations * eglot.el (eglot-mode, eglot-editing-mode-map): Remove forward decls. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bb0427aca7..f25c7bd346 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -46,9 +46,6 @@ (defvar eglot--processes-by-project (make-hash-table :test #'equal) "Keys are projects. Values are lists of processes.") -(defvar eglot-editing-mode) ; forward decl -(defvar eglot-mode) ; forward decl - (defvar-local eglot--special-buffer-process nil "Current buffer's eglot process.") @@ -643,7 +640,8 @@ running. INTERACTIVE is t if called interactively." (forward-char (min (plist-get pos-plist :character) (- (line-end-position) - (line-beginning-position))))))) + (line-beginning-position)))) + (point)))) (cl-loop for diag-spec across diagnostics collect (cl-destructuring-bind (&key range severity _code _source message) commit f706000c89019e646cf55a417b3b87035ed32794 Author: João Távora Date: Thu May 3 15:46:27 2018 +0100 Fix flymake diagnostic positions It's better not to use flymake-diag-region here. * eglot.el (eglot--textDocument/publishDiagnostics): Calculate position by hand. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 86c0a4c05f..bb0427aca7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -637,10 +637,13 @@ running. INTERACTIVE is t if called interactively." (with-current-buffer buffer (cl-flet ((pos-at (pos-plist) - (car (flymake-diag-region - (current-buffer) - (plist-get pos-plist :line) - (plist-get pos-plist :character))))) + (save-excursion + (goto-char (point-min)) + (forward-line (plist-get pos-plist :line)) + (forward-char + (min (plist-get pos-plist :character) + (- (line-end-position) + (line-beginning-position))))))) (cl-loop for diag-spec across diagnostics collect (cl-destructuring-bind (&key range severity _code _source message) commit e366550f05aefd1eddbb363f43fdf6b567e9650f Author: João Távora Date: Thu May 3 15:45:30 2018 +0100 Must re-announce didopen after reconnect * eglot.el (eglot-reconnect): Also call eglot--maybe-activate-editing-mode for all buffers. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3561da2db4..86c0a4c05f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -205,8 +205,11 @@ INTERACTIVE is t if called interactively." (eglot--major-mode process) (eglot--short-name process) (eglot--bootstrap-fn process) - (lambda (_proc) - (eglot--message "Reconnected!")))) + (lambda (proc) + (eglot--message "Reconnected!") + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (eglot--maybe-activate-editing-mode proc)))))) (defvar eglot--command-history nil "History of COMMAND arguments to `eglot'.") commit 878922319e7223888033a1dec9b3cc032573346c Author: João Távora Date: Thu May 3 15:10:32 2018 +0100 Fix another flymake sync bug * eglot.el (eglot-flymake-backend): Only report unreported sometimes. (eglot--maybe-activate-editing-mode): Start flymake explicitly when didOpen. (eglot--textDocument/publishDiagnostics): No need to set unreported-diagnostics to nil. (flymake): Require it. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ccf4b723d3..3561da2db4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -32,6 +32,7 @@ (require 'pcase) (require 'compile) ; for some faces (require 'warnings) +(require 'flymake) (defgroup eglot nil "Interaction with Language Server Protocol servers" @@ -657,12 +658,11 @@ running. INTERACTIVE is t if called interactively." message)))) into diags finally - (if (null eglot--current-flymake-report-fn) - (setq eglot--unreported-diagnostics - diags) - (funcall eglot--current-flymake-report-fn - diags) - (setq eglot--unreported-diagnostics nil)))))) + (if eglot--current-flymake-report-fn + (funcall eglot--current-flymake-report-fn + diags) + (setq eglot--unreported-diagnostics + diags)))))) (t (eglot--message "OK so %s isn't visited" filename))))) @@ -724,7 +724,8 @@ that case, also signal textDocument/didOpen." (and proc (eq proc cur))) (unless eglot-editing-mode (eglot-editing-mode 1)) - (eglot--signalDidOpen))))) + (eglot--signalDidOpen) + (flymake-start))))) (add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) @@ -947,10 +948,11 @@ Records START, END and LENGTH locally." (defun eglot-flymake-backend (report-fn &rest _more) "An EGLOT Flymake backend. Calls REPORT-FN maybe if server publishes diagnostics in time." - ;; Call immediately with anything unreported (this will clear any - ;; pending diags) - (funcall report-fn eglot--unreported-diagnostics) - (setq eglot--unreported-diagnostics nil) + ;; Maybe call immediately if anything unreported (this will clear + ;; any pending diags) + (when eglot--unreported-diagnostics + (funcall report-fn eglot--unreported-diagnostics) + (setq eglot--unreported-diagnostics nil)) ;; Setup so maybe it's called later, too. (setq eglot--current-flymake-report-fn report-fn) ;; Take this opportunity to signal a didChange that might eventually commit dd7ce8988a1fe90b9fcb6a241382b9dcfebacabf Author: João Távora Date: Thu May 3 14:54:39 2018 +0100 Make m-x eglot the main entry point * eglot.el (eglot-new-process): Removed (eglot): Rename from eglot-new-process. (eglot-editing-mode): Mention M-x eglot * README.md: Use M-x eglot diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5c014f325b..ccf4b723d3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -208,9 +208,9 @@ INTERACTIVE is t if called interactively." (eglot--message "Reconnected!")))) (defvar eglot--command-history nil - "History of COMMAND arguments to `eglot-new-process'.") + "History of COMMAND arguments to `eglot'.") -(defun eglot-new-process (managed-major-mode command &optional interactive) +(defun eglot (managed-major-mode command &optional interactive) ;; FIXME: Later make this function also connect to TCP servers by ;; overloading semantics on COMMAND. "Start a Language Server Protocol server. @@ -252,7 +252,7 @@ INTERACTIVE is t if called interactively." (let* ((project (project-current)) (short-name (eglot--project-short-name project))) (unless project (eglot--error - "(new-process) Cannot work without a current project!")) + "Cannot work without a current project!")) (let ((current-process (eglot--current-process)) (command (or command @@ -742,7 +742,7 @@ that case, also signal textDocument/didOpen." (flymake-mode 1) (if (eglot--current-process) (eglot--signalDidOpen) - (eglot--warn "No process, start one with `M-x eglot-new-process'"))) + (eglot--warn "No process, start one with `M-x eglot'"))) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) commit 65f421f724dbe50218f3cfbda6d6d5451174cdc3 Author: João Távora Date: Thu May 3 14:49:43 2018 +0100 Fix assorted bugs * eglot.el (eglot--special-buffer-process): Must be buffer-local. (eglot--define-process-var): Fix disaster waiting to happen. (eglot--process-receive): Explicitly pass PROC to eglot--pending-continuations. (eglot--textDocument/publishDiagnostics): Clear unreported diagnostics diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f4ac858406..5c014f325b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -48,7 +48,7 @@ (defvar eglot-editing-mode) ; forward decl (defvar eglot-mode) ; forward decl -(defvar eglot--special-buffer-process nil +(defvar-local eglot--special-buffer-process nil "Current buffer's eglot process.") (defun eglot--current-process () @@ -78,13 +78,14 @@ after setting it." (declare (indent 2)) `(progn (put ',var-sym 'function-documentation ,doc) - (defun ,var-sym (&optional process) - (let* ((proc (or process (eglot--current-process-or-lose))) - (probe (process-get proc ',var-sym))) - (or probe - (let ((def ,initval)) - (process-put proc ',var-sym def) - def)))) + (defun ,var-sym (proc) + (let* ((plist (process-plist proc)) + (probe (plist-member plist ',var-sym))) + (if probe + (cadr probe) + (let ((def ,initval)) + (process-put proc ',var-sym def) + def)))) (gv-define-setter ,var-sym (to-store &optional process) (let* ((prop ',var-sym)) ,(let ((form '(let ((proc (or ,process (eglot--current-process-or-lose)))) @@ -417,7 +418,8 @@ identifier. ERROR is non-nil if this is an error." (let* ((response-id (plist-get message :id)) (err (plist-get message :error)) (continuations (and response-id - (gethash response-id (eglot--pending-continuations))))) + (gethash response-id + (eglot--pending-continuations proc))))) (eglot--log-event proc (cond ((not response-id) 'server-notification) @@ -436,7 +438,7 @@ identifier. ERROR is non-nil if this is an error." (continuations (cancel-timer (cl-third continuations)) (remhash response-id - (eglot--pending-continuations)) + (eglot--pending-continuations proc)) (cond (err (apply (cl-second continuations) err)) (t @@ -655,11 +657,12 @@ running. INTERACTIVE is t if called interactively." message)))) into diags finally - (if eglot--current-flymake-report-fn - (funcall eglot--current-flymake-report-fn - diags) - (setq eglot--unreported-diagnostics - diags)))))) + (if (null eglot--current-flymake-report-fn) + (setq eglot--unreported-diagnostics + diags) + (funcall eglot--current-flymake-report-fn + diags) + (setq eglot--unreported-diagnostics nil)))))) (t (eglot--message "OK so %s isn't visited" filename))))) commit 7ba401ce38fc3f68eccffc547948ed432de8143e Author: João Távora Date: Thu May 3 14:08:37 2018 +0100 Watch for files opened under umbrella of existing process * eglot.el (eglot--connect): Call success-fn with a proc. (eglot-reconnect): Adapt to new eglot--connect. (eglot-new-process): Call eglot--maybe-activate-editing-mode (eglot--maybe-activate-editing-mode): New function. (find-file-hook): Add it here. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 10af6c5876..f4ac858406 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -190,7 +190,7 @@ SUCCESS-FN with no args if all goes well." (lambda (&key capabilities) (setf (eglot--capabilities proc) capabilities) (setf (eglot--status proc) nil) - (when success-fn (funcall success-fn))))))))) + (when success-fn (funcall success-fn proc))))))))) (defun eglot-reconnect (process &optional interactive) "Reconnect to PROCESS. @@ -203,8 +203,8 @@ INTERACTIVE is t if called interactively." (eglot--major-mode process) (eglot--short-name process) (eglot--bootstrap-fn process) - (lambda () - (eglot--message "Reconnected")))) + (lambda (_proc) + (eglot--message "Reconnected!")))) (defvar eglot--command-history nil "History of COMMAND arguments to `eglot-new-process'.") @@ -273,21 +273,15 @@ INTERACTIVE is t if called interactively." (eglot-make-local-process name command)) - (lambda () - (eglot--message "Connected. Managing `%s' buffers in project %s." - managed-major-mode short-name) + (lambda (proc) + (eglot--message "Connected! Process `%s' now managing `%s'\ +buffers in project %s." + proc + managed-major-mode + short-name) (dolist (buffer (buffer-list)) (with-current-buffer buffer - (when(and buffer-file-name - (cl-some - (lambda (root) - (string-prefix-p - (expand-file-name root) - (expand-file-name buffer-file-name))) - (project-roots project))) - (unless eglot-editing-mode - (eglot-editing-mode 1)) - (eglot--signalDidOpen))))))))))) + (eglot--maybe-activate-editing-mode proc)))))))))) (defun eglot--process-sentinel (process change) "Called with PROCESS undergoes CHANGE." @@ -706,7 +700,7 @@ running. INTERACTIVE is t if called interactively." -;;; Mode line +;;; Minor modes and mode-line ;;; (defface eglot-mode-line '((t (:inherit font-lock-constant-face :weight bold))) @@ -717,6 +711,20 @@ running. INTERACTIVE is t if called interactively." (defvar eglot-editing-mode-map (make-sparse-keymap)) +(defun eglot--maybe-activate-editing-mode (&optional proc) + "Maybe activate mode function `eglot-editing-mode'. +If PROC is supplied, do it only if BUFFER is managed by it. In +that case, also signal textDocument/didOpen." + (when buffer-file-name + (let ((cur (eglot--current-process))) + (when (or (and (null proc) cur) + (and proc (eq proc cur))) + (unless eglot-editing-mode + (eglot-editing-mode 1)) + (eglot--signalDidOpen))))) + +(add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) + (define-minor-mode eglot-editing-mode "Minor mode for source buffers where EGLOT helps you edit." nil commit 508c8efe23b26e585deefb897586257ab9bc14ce Author: João Távora Date: Thu May 3 13:37:04 2018 +0100 Multiple servers per project are possible A server manages a specific major-mode within a project. * eglot.el (eglot--processes-by-project): Add docstring. (eglot--current-process): Search new eglot--processes-by-project format. (eglot--major-mode): New variable. (eglot--moribund, eglot--project): Update docstring. (eglot--project-short-name, eglot--all-major-modes): New helpers. (eglot--connect): Rework. (eglot-new-process): Rework severely. (eglot--command-history): New variable. (eglot--process-sentinel): Use new eglot--processes-by-project. Update mode line. (eglot-editing-mode): Don't start processes, just suggest it. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cd546a32d6..10af6c5876 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -42,7 +42,8 @@ (python-mode . ("pyls"))) "Alist mapping major modes to server executables.") -(defvar eglot--processes-by-project (make-hash-table :test #'equal)) +(defvar eglot--processes-by-project (make-hash-table :test #'equal) + "Keys are projects. Values are lists of processes.") (defvar eglot-editing-mode) ; forward decl (defvar eglot-mode) ; forward decl @@ -53,9 +54,13 @@ (defun eglot--current-process () "The current logical EGLOT process." (or eglot--special-buffer-process - (let ((cur (project-current))) - (and cur - (gethash cur eglot--processes-by-project))))) + (let* ((cur (project-current)) + (processes + (and cur + (gethash cur eglot--processes-by-project)))) + (cl-find major-mode + processes + :key #'eglot--major-mode)))) (defun eglot--current-process-or-lose () "Return the current EGLOT process or error." @@ -91,6 +96,9 @@ after setting it." (eglot--define-process-var eglot--short-name nil "A short name for the process" t) +(eglot--define-process-var eglot--major-mode nil + "The major-mode this server is managing.") + (eglot--define-process-var eglot--expected-bytes nil "How many bytes declared by server") @@ -104,10 +112,10 @@ after setting it." "Holds list of capabilities that server reported") (eglot--define-process-var eglot--moribund nil - "Non-nil if process is about to exit") + "Non-nil if server is about to exit") (eglot--define-process-var eglot--project nil - "The project the process belongs to.") + "The project the server belongs to.") (eglot--define-process-var eglot--spinner `(nil nil t) "\"Spinner\" used by some servers. @@ -122,6 +130,20 @@ A list (WHAT SERIOUS-P)." t) Must be a function of one arg, a name, returning a process object.") +(defun eglot--project-short-name (project) + "Give PROJECT a short name." + (file-name-base + (directory-file-name + (car (project-roots project))))) + +(defun eglot--all-major-modes () + "Return all know major modes." + (let ((retval)) + (mapatoms (lambda (sym) + (when (plist-member (symbol-plist sym) 'derived-mode-parent) + (push sym retval)))) + retval)) + (defun eglot-make-local-process (name command) "Make a local LSP process from COMMAND. NAME is a name to give the inferior process or connection. @@ -140,17 +162,22 @@ Returns a process object." name))))) proc)) -(defun eglot--connect (project short-name bootstrap-fn &optional success-fn) - "Make a connection with PROJECT, SHORT-NAME and BOOTSTRAP-FN. -Call SUCCESS-FN with no args if all goes well." +(defun eglot--connect (project managed-major-mode + short-name bootstrap-fn &optional success-fn) + "Make a connection for PROJECT, SHORT-NAME and MANAGED-MAJOR-MODE. +Use BOOTSTRAP-FN to make the actual process object. Call +SUCCESS-FN with no args if all goes well." (let* ((proc (funcall bootstrap-fn short-name)) (buffer (process-buffer proc))) (setf (eglot--bootstrap-fn proc) bootstrap-fn - (eglot--project proc) project) + (eglot--project proc) project + (eglot--major-mode proc) managed-major-mode) (with-current-buffer buffer (let ((inhibit-read-only t)) (setf (eglot--short-name proc) short-name) - (puthash (project-current) proc eglot--processes-by-project) + (push proc + (gethash (project-current) + eglot--processes-by-project)) (erase-buffer) (read-only-mode t) (with-current-buffer (eglot-events-buffer proc) @@ -173,24 +200,63 @@ INTERACTIVE is t if called interactively." (eglot-quit-server process 'sync interactive)) (eglot--connect (eglot--project process) + (eglot--major-mode process) (eglot--short-name process) (eglot--bootstrap-fn process) (lambda () (eglot--message "Reconnected")))) -(defun eglot-new-process (&optional interactive) - "Start a new EGLOT process and initialize it. +(defvar eglot--command-history nil + "History of COMMAND arguments to `eglot-new-process'.") + +(defun eglot-new-process (managed-major-mode command &optional interactive) + ;; FIXME: Later make this function also connect to TCP servers by + ;; overloading semantics on COMMAND. + "Start a Language Server Protocol server. +Server is started with COMMAND and manages buffers of +MANAGED-MAJOR-MODE for the current project. + +COMMAND is a list of strings, an executable program and +optionally its arguments. MANAGED-MAJOR-MODE is an Emacs major +mode. + +With a prefix arg, prompt for MANAGED-MAJOR-MODE and COMMAND, +else guess them from current context and `eglot-executables'. + INTERACTIVE is t if called interactively." - (interactive (list t)) - (let ((project (project-current))) + (interactive + (let* ((managed-major-mode + (cond + ((or current-prefix-arg + (not buffer-file-name)) + (intern + (completing-read + "[eglot] Start a server to manage buffers of what major mode? " + (mapcar #'symbol-name + (eglot--all-major-modes)) nil t + (symbol-name major-mode) nil + (symbol-name major-mode) nil))) + (t major-mode))) + (guessed-command + (cdr (assoc managed-major-mode eglot-executables)))) + (list + managed-major-mode + (if current-prefix-arg + (split-string-and-unquote + (read-shell-command "[eglot] Run program: " + (combine-and-quote-strings guessed-command) + 'eglot-command-history)) + guessed-command) + t))) + (let* ((project (project-current)) + (short-name (eglot--project-short-name project))) (unless project (eglot--error "(new-process) Cannot work without a current project!")) (let ((current-process (eglot--current-process)) - (command (let ((probe (cdr (assoc major-mode eglot-executables)))) - (unless probe - (eglot--error "Don't know how to start EGLOT for %s buffers" - major-mode)) - probe))) + (command + (or command + (eglot--error "Don't know how to start EGLOT for %s buffers" + major-mode)))) (cond ((and current-process (process-live-p current-process)) @@ -201,15 +267,15 @@ INTERACTIVE is t if called interactively." (t (eglot--connect project - (file-name-base - (directory-file-name - (car (project-roots (project-current))))) + managed-major-mode + short-name (lambda (name) (eglot-make-local-process name command)) (lambda () - (eglot--message "Connected") + (eglot--message "Connected. Managing `%s' buffers in project %s." + managed-major-mode short-name) (dolist (buffer (buffer-list)) (with-current-buffer buffer (when(and buffer-file-name @@ -238,12 +304,15 @@ INTERACTIVE is t if called interactively." (cond ((eglot--moribund process) (eglot--message "(sentinel) Moribund process exited with status %s" (process-exit-status process)) - (remhash (eglot--project process) eglot--processes-by-project)) + (setf (gethash (eglot--project process) eglot--processes-by-project) + (delq process + (gethash (eglot--project process) eglot--processes-by-project)))) (t (eglot--warn "(sentinel) Reconnecting after process unexpectedly changed to %s." change) (eglot-reconnect process))) + (force-mode-line-update t) (delete-process process))) (defun eglot--process-filter (proc string) @@ -662,9 +731,7 @@ running. INTERACTIVE is t if called interactively." (flymake-mode 1) (if (eglot--current-process) (eglot--signalDidOpen) - (if (y-or-n-p "No process, try to start one with `eglot-new-process'? ") - (eglot-new-process t) - (eglot--warn "No process")))) + (eglot--warn "No process, start one with `M-x eglot-new-process'"))) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) commit b950cb40b63b57a647cd29eb8c78481b848d77e2 Author: João Távora Date: Thu May 3 13:32:20 2018 +0100 Appease checkdoc.el * eglot.el (eglot--process-send, eglot--next-request-id) (eglot--current-buffer-VersionedTextDocumentIdentifier) (eglot--current-buffer-TextDocumentItem) (eglot--after-change, eglot--signalDidOpen) (eglot--signalDidClose, eglot--maybe-signal-didChange): Add docstring. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8bb0729d52..cd546a32d6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -390,6 +390,7 @@ identifier. ERROR is non-nil if this is an error." (defvar eglot--expect-carriage-return nil) (defun eglot--process-send (id proc message) + "Send MESSAGE to PROC (ID is optional)." (let* ((json (json-encode message)) (to-send (format "Content-Length: %d\r\n\r\n%s" (string-bytes json) @@ -403,6 +404,7 @@ identifier. ERROR is non-nil if this is an error." (defvar eglot--next-request-id 0) (defun eglot--next-request-id () + "Compute the next id for a client request." (setq eglot--next-request-id (1+ eglot--next-request-id))) (defun eglot-forget-pending-continuations (process) @@ -789,6 +791,7 @@ running. INTERACTIVE is t if called interactively." eglot--versioned-identifier) (defun eglot--current-buffer-VersionedTextDocumentIdentifier () + "Compute VersionedTextDocumentIdentifier object for current buffer." (eglot--obj :uri (concat "file://" (url-hexify-string @@ -797,6 +800,7 @@ running. INTERACTIVE is t if called interactively." :version (eglot--current-buffer-versioned-identifier))) (defun eglot--current-buffer-TextDocumentItem () + "Compute TextDocumentItem object for current buffer." (append (eglot--current-buffer-VersionedTextDocumentIdentifier) (eglot--obj :languageId (cdr (assoc major-mode @@ -808,24 +812,29 @@ running. INTERACTIVE is t if called interactively." (buffer-substring-no-properties (point-min) (point-max)))))) (defun eglot--after-change (start end length) + "Hook onto `after-change-functions'. +Records START, END and LENGTH locally." (cl-incf eglot--versioned-identifier) (push (list start end length) eglot--recent-changes) ;; (eglot--message "start is %s, end is %s, length is %s" start end length) ) (defun eglot--signalDidOpen () + "Send textDocument/didOpen to server." (eglot--notify (eglot--current-process-or-lose) :textDocument/didOpen (eglot--obj :textDocument (eglot--current-buffer-TextDocumentItem)))) (defun eglot--signalDidClose () + "Send textDocument/didClose to server." (eglot--notify (eglot--current-process-or-lose) :textDocument/didClose (eglot--obj :textDocument (eglot--current-buffer-TextDocumentItem)))) (defun eglot--maybe-signal-didChange () + "Send textDocument/didChange to server." (when eglot--recent-changes (save-excursion (save-restriction commit a371a8d2ad0457c22e77205af4cd034e0620bf68 Author: João Távora Date: Thu May 3 12:05:37 2018 +0100 Simplify flymake integration And get rid of the ridiculous environment thingy * eglot.el (eglot--process-sentinel): Continuations are triplets. (eglot--environment-vars, eglot--environment): Remove. (eglot--process-receive): Simplify. (eglot--unreported-diagnostics): New variable. (eglot--textDocument/publishDiagnostics): Simplify. (eglot-flymake-backend): Report unreported diagnostics. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 85844409f3..8bb0729d52 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -229,8 +229,8 @@ INTERACTIVE is t if called interactively." (when (not (process-live-p process)) ;; Remember to cancel all timers ;; - (maphash (lambda (id quad) - (cl-destructuring-bind (_success _error timeout _env) quad + (maphash (lambda (id triplet) + (cl-destructuring-bind (_success _error timeout) triplet (eglot--message "(sentinel) Cancelling timer for continuation %s" id) (cancel-timer timeout))) @@ -349,13 +349,6 @@ identifier. ERROR is non-nil if this is an error." (setq msg (propertize msg 'face 'error))) (insert msg))))) -(defvar eglot--environment-vars - '(eglot--current-flymake-report-fn) - "A list of variables with saved values on every request.") - -(defvar eglot--environment nil - "Dynamically bound alist of symbol and values.") - (defun eglot--process-receive (proc message) "Process MESSAGE from PROC." (let* ((response-id (plist-get message :id)) @@ -388,13 +381,9 @@ identifier. ERROR is non-nil if this is an error." (t (let* ((method (plist-get message :method)) (handler-sym (intern (concat "eglot--" - method))) - (eglot--environment (cl-fourth continuations))) + method)))) (if (functionp handler-sym) - (cl-progv - (mapcar #'car eglot--environment) - (mapcar #'cdr eglot--environment) - (apply handler-sym proc (plist-get message :params))) + (apply handler-sym proc (plist-get message :params)) (eglot--debug "No implemetation for notification %s yet" method))))))) @@ -476,9 +465,7 @@ identifier. ERROR is non-nil if this is an error." error-fn (lambda (&rest args) (throw catch-tag (apply error-fn args)))) - timeout-timer - (cl-loop for var in eglot--environment-vars - collect (cons var (symbol-value var)))) + timeout-timer) (eglot--pending-continuations process)) (unless async-p (unwind-protect @@ -563,23 +550,18 @@ running. INTERACTIVE is t if called interactively." ;;; Notifications ;;; -(defvar eglot--current-flymake-report-fn nil) +(defvar-local eglot--current-flymake-report-fn nil + "Current flymake report function for this buffer") +(defvar-local eglot--unreported-diagnostics nil + "Unreported diagnostics for this buffer.") (cl-defun eglot--textDocument/publishDiagnostics (_process &key uri diagnostics) "Handle notification publishDiagnostics" (let* ((obj (url-generic-parse-url uri)) (filename (car (url-path-and-query obj))) - (buffer (find-buffer-visiting filename)) - (report-fn (cdr (assoc 'eglot--current-flymake-report-fn - eglot--environment)))) + (buffer (find-buffer-visiting filename))) (cond - ((not eglot--current-flymake-report-fn) - (eglot--warn "publishDiagnostics called but no report-fn")) - ((and report-fn - (not (eq report-fn - eglot--current-flymake-report-fn))) - (eglot--warn "outdated publishDiagnostics report from server")) (buffer (with-current-buffer buffer (cl-flet ((pos-at @@ -607,9 +589,12 @@ running. INTERACTIVE is t if called interactively." :note)) message)))) into diags - finally (funcall - eglot--current-flymake-report-fn - diags))))) + finally + (if eglot--current-flymake-report-fn + (funcall eglot--current-flymake-report-fn + diags) + (setq eglot--unreported-diagnostics + diags)))))) (t (eglot--message "OK so %s isn't visited" filename))))) @@ -875,11 +860,14 @@ running. INTERACTIVE is t if called interactively." (defun eglot-flymake-backend (report-fn &rest _more) "An EGLOT Flymake backend. Calls REPORT-FN maybe if server publishes diagnostics in time." - ;; call immediately with no diagnostics, this just means we don't - ;; have them yet (and also clears any pending ones). - ;; - (funcall report-fn nil) + ;; Call immediately with anything unreported (this will clear any + ;; pending diags) + (funcall report-fn eglot--unreported-diagnostics) + (setq eglot--unreported-diagnostics nil) + ;; Setup so maybe it's called later, too. (setq eglot--current-flymake-report-fn report-fn) + ;; Take this opportunity to signal a didChange that might eventually + ;; make the server report new diagnostics. (eglot--maybe-signal-didChange)) commit 976896f2c34034a495c72096daeadeb2a9ab4846 Author: João Távora Date: Thu May 3 11:49:24 2018 +0100 Signal textdocument/didclose * eglot.el (eglot-editing-mode): Signal didClose. (eglot--signalDidClose): New. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 12e7fef08c..85844409f3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -671,6 +671,7 @@ running. INTERACTIVE is t if called interactively." (eglot-mode 1) (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) + (add-hook 'kill-buffer-hook 'eglot--signalDidClose nil t) (flymake-mode 1) (if (eglot--current-process) (eglot--signalDidOpen) @@ -679,7 +680,8 @@ running. INTERACTIVE is t if called interactively." (eglot--warn "No process")))) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) - (remove-hook 'after-change-functions 'eglot--after-change t)))) + (remove-hook 'after-change-functions 'eglot--after-change t) + (remove-hook 'kill-buffer-hook 'eglot--signalDidClose t)))) (define-minor-mode eglot-mode "Minor mode for all buffers managed by EGLOT in some way." nil @@ -832,6 +834,12 @@ running. INTERACTIVE is t if called interactively." (eglot--obj :textDocument (eglot--current-buffer-TextDocumentItem)))) +(defun eglot--signalDidClose () + (eglot--notify (eglot--current-process-or-lose) + :textDocument/didClose + (eglot--obj :textDocument + (eglot--current-buffer-TextDocumentItem)))) + (defun eglot--maybe-signal-didChange () (when eglot--recent-changes (save-excursion commit 8277231fa82e8a6cc92f9bac6a6d4d1e07ea7197 Author: João Távora Date: Thu May 3 11:48:35 2018 +0100 Rework connection restarting again Quitting a process removes it from the project. * eglot.el (eglot-editing-mode,eglot-mode): Forward declare. (eglot--project): New process-local var. (eglot--connect): Takes a project. (eglot-new-process): Rework. (eglot--sentinel): Remove proc from eglot--processes-by-project. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b9aa94c322..12e7fef08c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -44,6 +44,9 @@ (defvar eglot--processes-by-project (make-hash-table :test #'equal)) +(defvar eglot-editing-mode) ; forward decl +(defvar eglot-mode) ; forward decl + (defvar eglot--special-buffer-process nil "Current buffer's eglot process.") @@ -103,6 +106,9 @@ after setting it." (eglot--define-process-var eglot--moribund nil "Non-nil if process is about to exit") +(eglot--define-process-var eglot--project nil + "The project the process belongs to.") + (eglot--define-process-var eglot--spinner `(nil nil t) "\"Spinner\" used by some servers. A list (ID WHAT DONE-P)." t) @@ -134,12 +140,13 @@ Returns a process object." name))))) proc)) -(defun eglot--connect (short-name bootstrap-fn &optional success-fn) - "Make a connection with SHORT-NAME and BOOTSTRAP-FN. +(defun eglot--connect (project short-name bootstrap-fn &optional success-fn) + "Make a connection with PROJECT, SHORT-NAME and BOOTSTRAP-FN. Call SUCCESS-FN with no args if all goes well." (let* ((proc (funcall bootstrap-fn short-name)) (buffer (process-buffer proc))) - (setf (eglot--bootstrap-fn proc) bootstrap-fn) + (setf (eglot--bootstrap-fn proc) bootstrap-fn + (eglot--project proc) project) (with-current-buffer buffer (let ((inhibit-read-only t)) (setf (eglot--short-name proc) short-name) @@ -164,48 +171,57 @@ INTERACTIVE is t if called interactively." (interactive (list (eglot--current-process-or-lose) t)) (when (process-live-p process) (eglot-quit-server process 'sync interactive)) - (eglot--connect (eglot--short-name process) - (eglot--bootstrap-fn process) - (lambda () - (eglot--message "Reconnected")))) + (eglot--connect + (eglot--project process) + (eglot--short-name process) + (eglot--bootstrap-fn process) + (lambda () + (eglot--message "Reconnected")))) (defun eglot-new-process (&optional interactive) "Start a new EGLOT process and initialize it. INTERACTIVE is t if called interactively." (interactive (list t)) (let ((project (project-current))) - (unless project (eglot--error "(new-process) Cannot work without a current project!")) + (unless project (eglot--error + "(new-process) Cannot work without a current project!")) (let ((current-process (eglot--current-process)) (command (let ((probe (cdr (assoc major-mode eglot-executables)))) (unless probe (eglot--error "Don't know how to start EGLOT for %s buffers" major-mode)) probe))) - (cond ((and current-process - (process-live-p current-process)) - (eglot--message "(new-process) Reconnecting instead") - (eglot-reconnect current-process interactive)) - (t - (eglot--connect - (file-name-base - (directory-file-name - (car (project-roots (project-current))))) - (lambda (name) - (eglot-make-local-process - name - command)) - (lambda () - (eglot--message "Connected") - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (if (and buffer-file-name - (cl-some - (lambda (root) - (string-prefix-p - (expand-file-name root) - (expand-file-name buffer-file-name))) - (project-roots project))) - (eglot--signalDidOpen))))))))))) + (cond + ((and current-process + (process-live-p current-process)) + (when (and + interactive + (y-or-n-p "[eglot] Live process found, reconnect instead? ")) + (eglot-reconnect current-process interactive))) + (t + (eglot--connect + project + (file-name-base + (directory-file-name + (car (project-roots (project-current))))) + (lambda (name) + (eglot-make-local-process + name + command)) + (lambda () + (eglot--message "Connected") + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (when(and buffer-file-name + (cl-some + (lambda (root) + (string-prefix-p + (expand-file-name root) + (expand-file-name buffer-file-name))) + (project-roots project))) + (unless eglot-editing-mode + (eglot-editing-mode 1)) + (eglot--signalDidOpen))))))))))) (defun eglot--process-sentinel (process change) "Called with PROCESS undergoes CHANGE." @@ -221,7 +237,8 @@ INTERACTIVE is t if called interactively." (eglot--pending-continuations process)) (cond ((eglot--moribund process) (eglot--message "(sentinel) Moribund process exited with status %s" - (process-exit-status process))) + (process-exit-status process)) + (remhash (eglot--project process) eglot--processes-by-project)) (t (eglot--warn "(sentinel) Reconnecting after process unexpectedly changed to %s." commit b81dcb530f98dc04b734666c72cca53d8d1127d2 Author: João Távora Date: Thu May 3 11:39:49 2018 +0100 Redesign and simplify parser Fix horrible bugs. This is the correct way. * eglot.el (eglot--process-filter): Redesign. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 71dab80a41..b9aa94c322 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -234,71 +234,59 @@ INTERACTIVE is t if called interactively." (when (buffer-live-p (process-buffer proc)) (with-current-buffer (process-buffer proc) (let ((inhibit-read-only t) - (pre-insertion-mark (copy-marker (process-mark proc))) (expected-bytes (eglot--expected-bytes proc))) ;; Insert the text, advancing the process marker. - (goto-char (process-mark proc)) - (insert string) - (set-marker (process-mark proc) (point)) - - ;; goto point just before insertion ;; - (goto-char pre-insertion-mark) - - ;; loop for each message (more than one might have arrived) + (save-excursion + (goto-char (process-mark proc)) + (insert string) + (set-marker (process-mark proc) (point))) + ;; Loop (more than one message might have arrived) ;; (catch 'done (while t - (let* ((match (search-forward-regexp - "\\(?:.*: .*\r\n\\)*Content-Length: \\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" - (+ (point) 100) - t)) - (new-expected-bytes (and match - (string-to-number (match-string 1))))) - (when new-expected-bytes - (when expected-bytes - (eglot--warn - (concat "Unexpectedly starting new message but %s bytes " - "reportedly remaining from previous one") - expected-bytes)) - (setf (eglot--expected-bytes proc) new-expected-bytes) - (setq expected-bytes new-expected-bytes))) - - ;; check for message body - ;; - (let ((available-bytes (- (position-bytes (process-mark proc)) - (position-bytes (point))))) - (cond ((not expected-bytes) ; previous search didn't match - (eglot--warn - "Skipping %s bytes of unexpected garbage from process %s" - available-bytes - proc) - (goto-char (process-mark proc)) - (throw 'done :skipping-garbage)) - ((>= available-bytes - expected-bytes) - (let* ((message-end (byte-to-position - (+ (position-bytes (point)) - expected-bytes)))) - (unwind-protect - (save-restriction - (narrow-to-region (point) - message-end) - (let* ((json-object-type 'plist) - (json-message (json-read))) - ;; process in another buffer, shielding - ;; buffer from tamper - (with-temp-buffer - (eglot--process-receive proc json-message)))) - (goto-char message-end) - (setf (eglot--expected-bytes proc) nil - expected-bytes nil))) - (when (= (point) (process-mark proc)) - (throw 'done :clean-done))) - (t - ;; just adding some stuff to the end that doesn't yet - ;; complete the message - (throw 'done :waiting-for-more-bytes)))))))))) + (cond ((not expected-bytes) + ;; Starting a new message + ;; + (setq expected-bytes + (and (search-forward-regexp + "\\(?:.*: .*\r\n\\)*Content-Length: *\\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" + (+ (point) 100) + t) + (string-to-number (match-string 1)))) + (unless expected-bytes + (throw 'done :waiting-for-new-message))) + (t + ;; Attempt to complete a message body + ;; + (let ((available-bytes (- (position-bytes (process-mark proc)) + (position-bytes (point))))) + (cond + ((>= available-bytes + expected-bytes) + (let* ((message-end (byte-to-position + (+ (position-bytes (point)) + expected-bytes)))) + (unwind-protect + (save-restriction + (narrow-to-region (point) message-end) + (let* ((json-object-type 'plist) + (json-message (json-read))) + ;; Process content in another buffer, + ;; shielding buffer from tamper + ;; + (with-temp-buffer + (eglot--process-receive proc json-message)))) + (goto-char message-end) + (delete-region (point-min) (point)) + (setq expected-bytes nil)))) + (t + ;; Message is still incomplete + ;; + (throw 'done :waiting-for-more-bytes-in-this-message)))))))) + ;; Saved parsing state for next visit to this filter + ;; + (setf (eglot--expected-bytes proc) expected-bytes))))) (defmacro eglot--obj (&rest what) "Make WHAT a suitable argument for `json-encode'." commit dfe551f5776498c14691ef4e820ad2342a90e731 Author: João Távora Date: Wed May 2 17:00:46 2018 +0100 Auto-reconnect on unexpected connection loss * eglot.el (eglot-reconnect): Only quit if indeed not quit already. (eglot-new-process): Burn the command in the bootstrap fn. (eglot--process-sentinel): Automatically reconnect if closed unexpectedly. (eglot--warn): Also message to *Messages* diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index aa23252271..71dab80a41 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -162,7 +162,8 @@ Call SUCCESS-FN with no args if all goes well." "Reconnect to PROCESS. INTERACTIVE is t if called interactively." (interactive (list (eglot--current-process-or-lose) t)) - (eglot-quit-server process 'sync interactive) + (when (process-live-p process) + (eglot-quit-server process 'sync interactive)) (eglot--connect (eglot--short-name process) (eglot--bootstrap-fn process) (lambda () @@ -174,7 +175,12 @@ INTERACTIVE is t if called interactively." (interactive (list t)) (let ((project (project-current))) (unless project (eglot--error "(new-process) Cannot work without a current project!")) - (let ((current-process (eglot--current-process))) + (let ((current-process (eglot--current-process)) + (command (let ((probe (cdr (assoc major-mode eglot-executables)))) + (unless probe + (eglot--error "Don't know how to start EGLOT for %s buffers" + major-mode)) + probe))) (cond ((and current-process (process-live-p current-process)) (eglot--message "(new-process) Reconnecting instead") @@ -187,11 +193,7 @@ INTERACTIVE is t if called interactively." (lambda (name) (eglot-make-local-process name - (let ((probe (cdr (assoc major-mode eglot-executables)))) - (unless probe - (eglot--error "Don't know how to start EGLOT for %s buffers" - major-mode)) - probe))) + command)) (lambda () (eglot--message "Connected") (dolist (buffer (buffer-list)) @@ -221,8 +223,10 @@ INTERACTIVE is t if called interactively." (eglot--message "(sentinel) Moribund process exited with status %s" (process-exit-status process))) (t - (eglot--warn "(sentinel) Process unexpectedly changed to %s" - change))) + (eglot--warn + "(sentinel) Reconnecting after process unexpectedly changed to %s." + change) + (eglot-reconnect process))) (delete-process process))) (defun eglot--process-filter (proc string) @@ -633,6 +637,7 @@ running. INTERACTIVE is t if called interactively." (defun eglot--warn (format &rest args) "Warning message with FORMAT and ARGS." + (apply #'eglot--message (concat "(warning) " format) args) (let ((warning-minimum-level :error)) (display-warning 'eglot (apply #'format format args) commit 2b8142d285a0a545abf4cbbe108443b861c50959 Author: João Távora Date: Wed May 2 15:49:41 2018 +0100 Ready to start fixing flymake integration * eglot.el (eglot-editing-mode): Turn on flymake-mode. (eglot-flymake-backend): Always start by reporting no diagnostics. (eglot--textDocument/publishDiagnostics): No annoying message. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d3428a73a7..aa23252271 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -573,7 +573,6 @@ running. INTERACTIVE is t if called interactively." (eglot--warn "outdated publishDiagnostics report from server")) (buffer (with-current-buffer buffer - (eglot--message "OK so add some %s diags" (length diagnostics)) (cl-flet ((pos-at (pos-plist) (car (flymake-diag-region @@ -662,6 +661,7 @@ running. INTERACTIVE is t if called interactively." (eglot-mode 1) (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) + (flymake-mode 1) (if (eglot--current-process) (eglot--signalDidOpen) (if (y-or-n-p "No process, try to start one with `eglot-new-process'? ") @@ -857,7 +857,10 @@ running. INTERACTIVE is t if called interactively." (defun eglot-flymake-backend (report-fn &rest _more) "An EGLOT Flymake backend. Calls REPORT-FN maybe if server publishes diagnostics in time." - ;; FIXME: perhaps should call it immediately? + ;; call immediately with no diagnostics, this just means we don't + ;; have them yet (and also clears any pending ones). + ;; + (funcall report-fn nil) (setq eglot--current-flymake-report-fn report-fn) (eglot--maybe-signal-didChange)) commit bdfba7ed6290554e2908e1aaba2f5887bc073c37 Author: João Távora Date: Wed May 2 15:36:26 2018 +0100 Slightly more user friendly start * eglot.el (eglot-new-process): signal DidOpen for every file in project. (eglot-editing-mode): Offer to start process. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6e5954ca80..d3428a73a7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -193,7 +193,17 @@ INTERACTIVE is t if called interactively." major-mode)) probe))) (lambda () - (eglot--message "Connected")))))))) + (eglot--message "Connected") + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (if (and buffer-file-name + (cl-some + (lambda (root) + (string-prefix-p + (expand-file-name root) + (expand-file-name buffer-file-name))) + (project-roots project))) + (eglot--signalDidOpen))))))))))) (defun eglot--process-sentinel (process change) "Called with PROCESS undergoes CHANGE." @@ -647,16 +657,19 @@ running. INTERACTIVE is t if called interactively." nil nil eglot-mode-map - (cond (eglot-editing-mode - (eglot-mode 1) - (add-hook 'after-change-functions 'eglot--after-change nil t) - (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) - (if (eglot--current-process) - (eglot--signalDidOpen) - (eglot--warn "No process"))) - (t - (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) - (remove-hook 'after-change-functions 'eglot--after-change t)))) + (cond + (eglot-editing-mode + (eglot-mode 1) + (add-hook 'after-change-functions 'eglot--after-change nil t) + (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) + (if (eglot--current-process) + (eglot--signalDidOpen) + (if (y-or-n-p "No process, try to start one with `eglot-new-process'? ") + (eglot-new-process t) + (eglot--warn "No process")))) + (t + (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) + (remove-hook 'after-change-functions 'eglot--after-change t)))) (define-minor-mode eglot-mode "Minor mode for all buffers managed by EGLOT in some way." nil commit f35d1d51cd9bcf508607ea083c4c30c6818153dd Author: João Távora Date: Wed May 2 15:35:22 2018 +0100 Don't clutter ui with warnings * eglot.el (warnings): require it. (eglot--warn): set warning-minimum-level diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c7bb5d46f3..6e5954ca80 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -31,6 +31,7 @@ (require 'url-util) (require 'pcase) (require 'compile) ; for some faces +(require 'warnings) (defgroup eglot nil "Interaction with Language Server Protocol servers" @@ -623,9 +624,10 @@ running. INTERACTIVE is t if called interactively." (defun eglot--warn (format &rest args) "Warning message with FORMAT and ARGS." - (display-warning 'eglot - (apply #'format format args) - :warning)) + (let ((warning-minimum-level :error)) + (display-warning 'eglot + (apply #'format format args) + :warning))) commit dd467a4706c693dd8a66392d69e74100497ddb04 Author: João Távora Date: Wed May 2 15:06:50 2018 +0100 Rework commands for connecting and reconnecting * eglot.el (eglot--current-process-or-lose): Add doc. (eglot--command): Remove. (eglot--bootstrap-fn): New process-local variable. (eglot--connect): Redesign. (eglot-make-local-process): New function. (eglot-reconnect): New interactive command. (eglot-new-process): Redesign. (eglot--process-sentinel): Add doc. (eglot--protocol-initialize): Rework. (eglot--mode-line-format): Use eglot-reconnect. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fe63a29bc4..c7bb5d46f3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -54,6 +54,7 @@ (gethash cur eglot--processes-by-project))))) (defun eglot--current-process-or-lose () + "Return the current EGLOT process or error." (or (eglot--current-process) (eglot--error "No current EGLOT process%s" (if (project-current) "" @@ -109,29 +110,62 @@ A list (ID WHAT DONE-P)." t) "Status as declared by the server. A list (WHAT SERIOUS-P)." t) -(defun eglot--command (&optional errorp) - (let ((probe (cdr (assoc major-mode eglot-executables)))) - (unless (or (not errorp) - probe) - (eglot--error "Don't know how to start EGLOT for %s buffers" - major-mode)) - probe)) +(eglot--define-process-var eglot--bootstrap-fn nil + "Function for returning processes/connetions to LSP servers. +Must be a function of one arg, a name, returning a process +object.") -(defun eglot--connect (name filter sentinel) - "Helper for `eglot-new-process'. +(defun eglot-make-local-process (name command) + "Make a local LSP process from COMMAND. NAME is a name to give the inferior process or connection. -FILTER and SENTINEL are filter and sentinel. -Should return a list of (PROCESS BUFFER)." - (let ((proc (make-process :name name - :buffer (get-buffer-create - (format "*%s inferior*" name)) - :command (eglot--command 'error) - :connection-type 'pipe - :filter filter - :sentinel sentinel - :stderr (get-buffer-create (format "*%s stderr*" - name))))) - (list proc (process-buffer proc)))) +Returns a process object." + (let* ((readable-name (format "EGLOT server (%s)" name)) + (proc + (make-process + :name readable-name + :buffer (get-buffer-create + (format "*%s inferior*" readable-name)) + :command command + :connection-type 'pipe + :filter 'eglot--process-filter + :sentinel 'eglot--process-sentinel + :stderr (get-buffer-create (format "*%s stderr*" + name))))) + proc)) + +(defun eglot--connect (short-name bootstrap-fn &optional success-fn) + "Make a connection with SHORT-NAME and BOOTSTRAP-FN. +Call SUCCESS-FN with no args if all goes well." + (let* ((proc (funcall bootstrap-fn short-name)) + (buffer (process-buffer proc))) + (setf (eglot--bootstrap-fn proc) bootstrap-fn) + (with-current-buffer buffer + (let ((inhibit-read-only t)) + (setf (eglot--short-name proc) short-name) + (puthash (project-current) proc eglot--processes-by-project) + (erase-buffer) + (read-only-mode t) + (with-current-buffer (eglot-events-buffer proc) + (let ((inhibit-read-only t)) + (insert + (format "\n-----------------------------------\n")))) + (eglot--protocol-initialize + proc + (cl-function + (lambda (&key capabilities) + (setf (eglot--capabilities proc) capabilities) + (setf (eglot--status proc) nil) + (when success-fn (funcall success-fn))))))))) + +(defun eglot-reconnect (process &optional interactive) + "Reconnect to PROCESS. +INTERACTIVE is t if called interactively." + (interactive (list (eglot--current-process-or-lose) t)) + (eglot-quit-server process 'sync interactive) + (eglot--connect (eglot--short-name process) + (eglot--bootstrap-fn process) + (lambda () + (eglot--message "Reconnected")))) (defun eglot-new-process (&optional interactive) "Start a new EGLOT process and initialize it. @@ -140,32 +174,28 @@ INTERACTIVE is t if called interactively." (let ((project (project-current))) (unless project (eglot--error "(new-process) Cannot work without a current project!")) (let ((current-process (eglot--current-process))) - (when (and current-process - (process-live-p current-process)) - (eglot--message "(new-process) Asking current process to terminate first") - (eglot-quit-server current-process 'sync interactive))) - (let* ((short-name (file-name-base - (directory-file-name - (car (project-roots (project-current)))))) - (good-name - (format "EGLOT server (%s)" short-name))) - (pcase-let ((`(,proc ,buffer) - (eglot--connect good-name - 'eglot--process-filter - 'eglot--process-sentinel))) - (with-current-buffer buffer - (let ((inhibit-read-only t)) - (setf (eglot--short-name proc) short-name) - (puthash (project-current) proc eglot--processes-by-project) - (erase-buffer) - (read-only-mode t) - (with-current-buffer (eglot-events-buffer proc) - (let ((inhibit-read-only t)) - (insert - (format "\n-----------------------------------\n")))) - (eglot--protocol-initialize proc interactive))))))) + (cond ((and current-process + (process-live-p current-process)) + (eglot--message "(new-process) Reconnecting instead") + (eglot-reconnect current-process interactive)) + (t + (eglot--connect + (file-name-base + (directory-file-name + (car (project-roots (project-current))))) + (lambda (name) + (eglot-make-local-process + name + (let ((probe (cdr (assoc major-mode eglot-executables)))) + (unless probe + (eglot--error "Don't know how to start EGLOT for %s buffers" + major-mode)) + probe))) + (lambda () + (eglot--message "Connected")))))))) (defun eglot--process-sentinel (process change) + "Called with PROCESS undergoes CHANGE." (eglot--debug "(sentinel) Process state changed to %s" change) (when (not (process-live-p process)) ;; Remember to cancel all timers @@ -459,10 +489,10 @@ identifier. ERROR is non-nil if this is an error." ;;; Requests ;;; -(defun eglot--protocol-initialize (process interactive) +(defun eglot--protocol-initialize (process success-fn) "Initialize LSP protocol. -PROCESS is a connected process (network or local). -INTERACTIVE is t if caller was called interactively." +PROCESS is a connected process (network or local). SUCCESS-FN is +called with capabilites after connection." (eglot--request process :initialize @@ -476,14 +506,7 @@ INTERACTIVE is t if caller was called interactively." :workspace (eglot--obj) :textDocument (eglot--obj :publishDiagnostics `(:relatedInformation t)))) - :success-fn (cl-function - (lambda (&key capabilities) - (setf (eglot--capabilities process) capabilities) - (when interactive - (setf (eglot--status process) nil) - (eglot--message - "Server reports %d capabilities" - (length capabilities))))))) + :success-fn success-fn)) (defun eglot-quit-server (process &optional sync interactive) "Politely ask the server PROCESS to quit. @@ -687,12 +710,12 @@ running. INTERACTIVE is t if called interactively." keymap ,(let ((map (make-sparse-keymap))) (define-key map [mode-line mouse-1] 'eglot-events-buffer) (define-key map [mode-line mouse-2] 'eglot-quit-server) - (define-key map [mode-line mouse-3] 'eglot-new-process) + (define-key map [mode-line mouse-3] 'eglot-reconnect) map) mouse-face mode-line-highlight help-echo ,(concat "mouse-1: go to events buffer\n" "mouse-2: quit server\n" - "mouse-3: new process")) + "mouse-3: reconnect to server")) ,@(when serious-p `("/" (:propertize commit 4602fc02aefa84339b4be3a7fe23d44a4a4af00c Author: João Távora Date: Wed May 2 13:54:37 2018 +0100 Less obstrusive flymake stuff for now * eglot.el (eglot--after-change, eglot-flymake-backend): Fix. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9742f885d9..fe63a29bc4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -775,7 +775,8 @@ running. INTERACTIVE is t if called interactively." (defun eglot--after-change (start end length) (cl-incf eglot--versioned-identifier) (push (list start end length) eglot--recent-changes) - (eglot--message "start is %s, end is %s, length is %s" start end length)) + ;; (eglot--message "start is %s, end is %s, length is %s" start end length) + ) (defun eglot--signalDidOpen () (eglot--notify (eglot--current-process-or-lose) @@ -816,6 +817,9 @@ running. INTERACTIVE is t if called interactively." (setq eglot--recent-changes nil))) (defun eglot-flymake-backend (report-fn &rest _more) + "An EGLOT Flymake backend. +Calls REPORT-FN maybe if server publishes diagnostics in time." + ;; FIXME: perhaps should call it immediately? (setq eglot--current-flymake-report-fn report-fn) (eglot--maybe-signal-didChange)) commit 5ece72dc5cc272b60458f3166cfeb31bfa55a3f6 Author: João Távora Date: Wed May 2 13:37:35 2018 +0100 Events buffer uses eglot-mode, source buffers use eglot-editing-mode * eglot.el (eglot--special-buffer-process): New var. (eglot--current-process): Consider eglot--special-buffer-process. (eglot-events-buffer): Use eglot-mode (eglot-editing-mode): New minor mode. (eglot-mode): Turns on eglot-editing-mode maybe. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 457f316893..9742f885d9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -43,11 +43,15 @@ (defvar eglot--processes-by-project (make-hash-table :test #'equal)) +(defvar eglot--special-buffer-process nil + "Current buffer's eglot process.") + (defun eglot--current-process () "The current logical EGLOT process." - (let ((cur (project-current))) - (and cur - (gethash cur eglot--processes-by-project)))) + (or eglot--special-buffer-process + (let ((cur (project-current))) + (and cur + (gethash cur eglot--processes-by-project))))) (defun eglot--current-process-or-lose () (or (eglot--current-process) @@ -270,8 +274,9 @@ INTERACTIVE is t if called interactively." (with-current-buffer buffer (buffer-disable-undo) (read-only-mode t) - (setf (eglot--events-buffer process) - buffer)) + (setf (eglot--events-buffer process) buffer + eglot--special-buffer-process process) + (eglot-mode)) buffer)))) (when interactive (display-buffer buffer)) @@ -610,12 +615,15 @@ running. INTERACTIVE is t if called interactively." (defvar eglot-mode-map (make-sparse-keymap)) -(define-minor-mode eglot-mode - "Minor mode for buffers where EGLOT is possible" +(defvar eglot-editing-mode-map (make-sparse-keymap)) + +(define-minor-mode eglot-editing-mode + "Minor mode for source buffers where EGLOT helps you edit." nil nil eglot-mode-map - (cond (eglot-mode + (cond (eglot-editing-mode + (eglot-mode 1) (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) (if (eglot--current-process) @@ -625,6 +633,17 @@ running. INTERACTIVE is t if called interactively." (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t)))) +(define-minor-mode eglot-mode + "Minor mode for all buffers managed by EGLOT in some way." nil + nil eglot-mode-map + (cond (eglot-mode + (when (and buffer-file-name + (not eglot-editing-mode)) + (eglot-editing-mode 1))) + (t + (when eglot-editing-mode + (eglot-editing-mode -1))))) + (defvar eglot-menu) (easy-menu-define eglot-menu eglot-mode-map "EGLOT" commit 9bd7605d12c1a842dc6806e4199e64d8e2f89a6a Author: João Távora Date: Wed May 2 13:33:02 2018 +0100 Change status to error everytime an error is found * eglot.el (eglot--process-receive): Also set error status. (eglot--request): Fix a compilation warning. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1f2737effd..457f316893 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -317,6 +317,8 @@ identifier. ERROR is non-nil if this is an error." message response-id err) + (when err + (setf (eglot--status proc) '("error" t))) (cond ((and response-id (not continuations)) (eglot--warn "Ooops no continuation for id %s" response-id)) @@ -384,7 +386,7 @@ identifier. ERROR is non-nil if this is an error." (error-fn (or error-fn (cl-function - (lambda (&key data code message &allow-other-keys) + (lambda (&key code message &allow-other-keys) (setf (eglot--status process) '("error" t)) (eglot--warn "(request) Request id=%s errored with code=%s: %s" commit 4a87a536b97fcf00d28f6603132c6f960dd5f80f Author: João Távora Date: Wed May 2 13:32:13 2018 +0100 Correctly report what we currently are capable of Which is almost nothing. * eglot.el (eglot--protocol-initialize): Clean up. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index db594f4984..1f2737effd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -459,20 +459,23 @@ INTERACTIVE is t if caller was called interactively." (eglot--request process :initialize - `(:processId ,(emacs-pid) - :rootPath ,(concat - (expand-file-name (car (project-roots - (project-current))))) - :initializationOptions [] - :capabilities (:workspace (:executeCommand (:dynamicRegistration t)) - :textDocument (:synchronization (:didSave t)))) + (eglot--obj :processId (emacs-pid) + :rootPath (concat + (expand-file-name (car (project-roots + (project-current))))) + :initializationOptions [] + :capabilities + (eglot--obj + :workspace (eglot--obj) + :textDocument (eglot--obj + :publishDiagnostics `(:relatedInformation t)))) :success-fn (cl-function (lambda (&key capabilities) (setf (eglot--capabilities process) capabilities) (when interactive (setf (eglot--status process) nil) (eglot--message - "So yeah I got lots (%d) of capabilities" + "Server reports %d capabilities" (length capabilities))))))) (defun eglot-quit-server (process &optional sync interactive) commit ef1924c8e2f562a828e1f6a0ac17058b8a5b50fb Author: João Távora Date: Wed May 2 13:31:26 2018 +0100 Add eglot-clear-status interactive command * eglot.el (eglot-clear-status): New (eglot-forget-pending-continuations): Fix bug. (eglot--mode-line-format): Add link to eglot-clear-status. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1a82cc507d..db594f4984 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -361,9 +361,14 @@ identifier. ERROR is non-nil if this is an error." (defun eglot-forget-pending-continuations (process) "Stop waiting for responses from the current LSP PROCESS." - (interactive (eglot--current-process-or-lose)) + (interactive (list (eglot--current-process-or-lose))) (clrhash (eglot--pending-continuations process))) +(defun eglot-clear-status (process) + "Clear most recent error message from PROCESS." + (interactive (list (eglot--current-process-or-lose))) + (setf (eglot--status process) nil)) + (cl-defun eglot--request (process method params @@ -668,12 +673,15 @@ running. INTERACTIVE is t if called interactively." `("/" (:propertize ,status - help-echo ,(concat "mouse-1: go to events buffer") + help-echo ,(concat "mouse-1: go to events buffer\n" + "mouse-3: clear this status") mouse-face mode-line-highlight face compilation-mode-line-fail keymap ,(let ((map (make-sparse-keymap))) (define-key map [mode-line mouse-1] 'eglot-events-buffer) + (define-key map [mode-line mouse-3] + 'eglot-clear-status) map)))) ,@(when (and doing (not done-p)) `("/" commit dc5b0eb42afecfa23ec3e37114978487399159e7 Author: João Távora Date: Wed May 2 13:28:18 2018 +0100 Auto update mode-line after setting some process properties * eglot.el (eglot--define-process-var): Rework. (eglot--short-name, eglot--spinner, eglot--status): Update mode-line after setting it. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ff71a2f782..1a82cc507d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -55,7 +55,12 @@ (if (project-current) "" " (Also no current project)")))) -(defmacro eglot--define-process-var (var-sym initval &optional doc) +(defmacro eglot--define-process-var + (var-sym initval &optional doc mode-line-update-p) + "Define VAR-SYM as a generalized process-local variable. +INITVAL is the default value. DOC is the documentation. +MODE-LINE-UPDATE-P says to also force a mode line update +after setting it." (declare (indent 2)) `(progn (put ',var-sym 'function-documentation ,doc) @@ -67,12 +72,15 @@ (process-put proc ',var-sym def) def)))) (gv-define-setter ,var-sym (to-store &optional process) - (let ((prop ',var-sym)) - `(let ((proc (or ,process (eglot--current-process-or-lose)))) - (process-put proc ',prop ,to-store)))))) + (let* ((prop ',var-sym)) + ,(let ((form '(let ((proc (or ,process (eglot--current-process-or-lose)))) + (process-put proc ',prop ,to-store)))) + (if mode-line-update-p + `(backquote (prog1 ,form (force-mode-line-update t))) + `(backquote ,form))))))) (eglot--define-process-var eglot--short-name nil - "A short name for the process") + "A short name for the process" t) (eglot--define-process-var eglot--expected-bytes nil "How many bytes declared by server") @@ -91,11 +99,11 @@ (eglot--define-process-var eglot--spinner `(nil nil t) "\"Spinner\" used by some servers. -A list (ID WHAT DONE-P).") +A list (ID WHAT DONE-P)." t) (eglot--define-process-var eglot--status `(:unknown nil) "Status as declared by the server. -A list (WHAT SERIOUS-P).") +A list (WHAT SERIOUS-P)." t) (defun eglot--command (&optional errorp) (let ((probe (cdr (assoc major-mode eglot-executables)))) commit 7d6547dfa4c2a66597c0ac2950c4f93875bec884 Author: João Távora Date: Wed May 2 12:05:47 2018 +0100 Start experimenting with python * eglot.el (eglot-executables): Add pyls. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 22ff0318ca..ff71a2f782 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -37,7 +37,8 @@ :prefix "eglot-" :group 'applications) -(defvar eglot-executables '((rust-mode . ("rls"))) +(defvar eglot-executables '((rust-mode . ("rls")) + (python-mode . ("pyls"))) "Alist mapping major modes to server executables.") (defvar eglot--processes-by-project (make-hash-table :test #'equal)) commit 2c2aec71fb5079a2075627a09de748c4fd57fa9e Author: João Távora Date: Wed May 2 12:04:56 2018 +0100 Don't switch to possibly dead buffer in sentinel * eglot.el (eglot--process-sentinel): Don't with-current-buffer. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 44023b721d..22ff0318ca 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -153,24 +153,23 @@ INTERACTIVE is t if called interactively." (eglot--protocol-initialize proc interactive))))))) (defun eglot--process-sentinel (process change) - (with-current-buffer (process-buffer process) - (eglot--debug "(sentinel) Process state changed to %s" change) - (when (not (process-live-p process)) - ;; Remember to cancel all timers - ;; - (maphash (lambda (id quad) - (cl-destructuring-bind (_success _error timeout _env) quad - (eglot--message - "(sentinel) Cancelling timer for continuation %s" id) - (cancel-timer timeout))) - (eglot--pending-continuations process)) - (cond ((eglot--moribund process) - (eglot--message "(sentinel) Moribund process exited with status %s" - (process-exit-status process))) - (t - (eglot--warn "(sentinel) Process unexpectedly changed to %s" - change))) - (delete-process process)))) + (eglot--debug "(sentinel) Process state changed to %s" change) + (when (not (process-live-p process)) + ;; Remember to cancel all timers + ;; + (maphash (lambda (id quad) + (cl-destructuring-bind (_success _error timeout _env) quad + (eglot--message + "(sentinel) Cancelling timer for continuation %s" id) + (cancel-timer timeout))) + (eglot--pending-continuations process)) + (cond ((eglot--moribund process) + (eglot--message "(sentinel) Moribund process exited with status %s" + (process-exit-status process))) + (t + (eglot--warn "(sentinel) Process unexpectedly changed to %s" + change))) + (delete-process process))) (defun eglot--process-filter (proc string) "Called when new data STRING has arrived for PROC." @@ -371,7 +370,8 @@ identifier. ERROR is non-nil if this is an error." (error-fn (or error-fn (cl-function - (lambda (&key code message) + (lambda (&key data code message &allow-other-keys) + (setf (eglot--status process) '("error" t)) (eglot--warn "(request) Request id=%s errored with code=%s: %s" id code message))))) @@ -446,9 +446,9 @@ INTERACTIVE is t if caller was called interactively." process :initialize `(:processId ,(emacs-pid) - :rootPath ,(concat "file://" - (expand-file-name (car (project-roots - (project-current))))) + :rootPath ,(concat + (expand-file-name (car (project-roots + (project-current))))) :initializationOptions [] :capabilities (:workspace (:executeCommand (:dynamicRegistration t)) :textDocument (:synchronization (:didSave t)))) commit 235574be1574d9857c24aed97685dec49a2dbed5 Author: João Távora Date: Tue May 1 23:13:49 2018 +0100 Report server status in the mode-line * eglot.el (eglot--status): New var. (eglot--log-event): Try to be more useful for other stuff. (eglot--protocol-initialize): Set status to nil on successful connect. (eglot--window/showMessage): Set status to error if needed. (eglot--mode-line-format): Display status if serious. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e5d69379a4..44023b721d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -92,6 +92,10 @@ "\"Spinner\" used by some servers. A list (ID WHAT DONE-P).") +(eglot--define-process-var eglot--status `(:unknown nil) + "Status as declared by the server. +A list (WHAT SERIOUS-P).") + (defun eglot--command (&optional errorp) (let ((probe (cdr (assoc major-mode eglot-executables)))) (unless (or (not errorp) @@ -265,15 +269,22 @@ INTERACTIVE is t if called interactively." (display-buffer buffer)) buffer)) -(defun eglot--log-event (proc type message id error) +(defun eglot--log-event (proc type message &optional id error) + "Log an eglot-related event. +PROC is the current process. TYPE is an identifier. MESSAGE is +a JSON-like plist or anything else. ID is a continuation +identifier. ERROR is non-nil if this is an error." (with-current-buffer (eglot-events-buffer proc) (let ((inhibit-read-only t)) (goto-char (point-max)) - (insert (format "%s%s%s:\n%s\n" - type - (if id (format " (id:%s)" id) "") - (if error " ERROR" "") - (pp-to-string message)))))) + (let ((msg (format "%s%s%s:\n%s\n" + type + (if id (format " (id:%s)" id) "") + (if error " ERROR" "") + (pp-to-string message)))) + (when error + (setq msg (propertize msg 'face 'error))) + (insert msg))))) (defvar eglot--environment-vars '(eglot--current-flymake-report-fn) @@ -445,6 +456,7 @@ INTERACTIVE is t if caller was called interactively." (lambda (&key capabilities) (setf (eglot--capabilities process) capabilities) (when interactive + (setf (eglot--status process) nil) (eglot--message "So yeah I got lots (%d) of capabilities" (length capabilities))))))) @@ -535,6 +547,16 @@ running. INTERACTIVE is t if called interactively." (t (eglot--message "OK so %s isn't visited" filename))))) +(cl-defun eglot--window/showMessage + (process &key type message) + "Handle notification window/showMessage" + (when (<= 1 type) + (setf (eglot--status process) '("error" t)) + (eglot--log-event process + (propertize "server-error" 'face 'error) + message)) + (eglot--message "Server reports (type=%s): %s" type message)) + ;;; Helpers ;;; @@ -603,8 +625,12 @@ running. INTERACTIVE is t if called interactively." (pending (and proc (hash-table-count (eglot--pending-continuations proc)))) - (`(,_id ,what ,done-p) (and proc - (eglot--spinner)))) + (`(,_id ,doing ,done-p) + (and proc + (eglot--spinner proc))) + (`(,status ,serious-p) + (and proc + (eglot--status proc)))) (append `((:propertize "eglot" face eglot-mode-line @@ -629,10 +655,21 @@ running. INTERACTIVE is t if called interactively." help-echo ,(concat "mouse-1: go to events buffer\n" "mouse-2: quit server\n" "mouse-3: new process")) - ,@(when (and what (not done-p)) + ,@(when serious-p + `("/" + (:propertize + ,status + help-echo ,(concat "mouse-1: go to events buffer") + mouse-face mode-line-highlight + face compilation-mode-line-fail + keymap ,(let ((map (make-sparse-keymap))) + (define-key map [mode-line mouse-1] + 'eglot-events-buffer) + map)))) + ,@(when (and doing (not done-p)) `("/" (:propertize - ,what + ,doing help-echo ,(concat "mouse-1: go to events buffer") mouse-face mode-line-highlight face compilation-mode-line-run commit c170dbedf8147a0bc75b0f22c7a2ef52f8611872 Author: João Távora Date: Tue May 1 22:47:09 2018 +0100 Implement spinners and rls's window/progress * eglot.el (eglot--window/progress): New. (eglot--mode-line-format): Rework. (eglot--snpinner): New var. (compile): require it. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ab13b3a52a..e5d69379a4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -30,6 +30,7 @@ (require 'url-parse) (require 'url-util) (require 'pcase) +(require 'compile) ; for some faces (defgroup eglot nil "Interaction with Language Server Protocol servers" @@ -87,6 +88,10 @@ (eglot--define-process-var eglot--moribund nil "Non-nil if process is about to exit") +(eglot--define-process-var eglot--spinner `(nil nil t) + "\"Spinner\" used by some servers. +A list (ID WHAT DONE-P).") + (defun eglot--command (&optional errorp) (let ((probe (cdr (assoc major-mode eglot-executables)))) (unless (or (not errorp) @@ -591,17 +596,15 @@ running. INTERACTIVE is t if called interactively." (defun eglot--mode-line-format () "Compose the mode-line format spec." - (let* ((proc (eglot--current-process)) - (name (and proc - (process-live-p proc) - (eglot--short-name proc))) - (pending (and proc - (hash-table-count - (eglot--pending-continuations proc)))) - (format-number (lambda (n) (cond ((and n (not (zerop n))) - (format "%d" n)) - (n "-") - (t "*"))))) + (pcase-let* ((proc (eglot--current-process)) + (name (and proc + (process-live-p proc) + (eglot--short-name proc))) + (pending (and proc + (hash-table-count + (eglot--pending-continuations proc)))) + (`(,_id ,what ,done-p) (and proc + (eglot--spinner)))) (append `((:propertize "eglot" face eglot-mode-line @@ -612,41 +615,51 @@ running. INTERACTIVE is t if called interactively." mouse-face mode-line-highlight help-echo "mouse-1: pop-up EGLOT menu" )) - (if name - `(" " - (:propertize - ,name - face eglot-mode-line - keymap ,(let ((map (make-sparse-keymap))) - (define-key map [mode-line mouse-1] 'eglot-events-buffer) - (define-key map [mode-line mouse-2] 'eglot-quit-server) - (define-key map [mode-line mouse-3] 'eglot-new-process) - map) - mouse-face mode-line-highlight - help-echo ,(concat "mouse-1: events buffer\n" - "mouse-2: quit server\n" - "mouse-3: new process")) - "/" - (:propertize - ,(funcall format-number pending) - help-echo ,(if name - (format - "%s pending events outgoing\n%s" + (when name + `(":" + (:propertize + ,name + face eglot-mode-line + keymap ,(let ((map (make-sparse-keymap))) + (define-key map [mode-line mouse-1] 'eglot-events-buffer) + (define-key map [mode-line mouse-2] 'eglot-quit-server) + (define-key map [mode-line mouse-3] 'eglot-new-process) + map) + mouse-face mode-line-highlight + help-echo ,(concat "mouse-1: go to events buffer\n" + "mouse-2: quit server\n" + "mouse-3: new process")) + ,@(when (and what (not done-p)) + `("/" + (:propertize + ,what + help-echo ,(concat "mouse-1: go to events buffer") + mouse-face mode-line-highlight + face compilation-mode-line-run + keymap ,(let ((map (make-sparse-keymap))) + (define-key map [mode-line mouse-1] + 'eglot-events-buffer) + map)))) + ,@(when (cl-plusp pending) + `("/" + (:propertize + (format "%d" pending) + help-echo ,(format + "%s unanswered requests\n%s" pending (concat "mouse-1: go to events buffer" "mouse-3: forget pending continuations")) - "No current connection") - mouse-face mode-line-highlight - face ,(cond ((and pending (cl-plusp pending)) - 'warning) - (t - 'eglot-mode-line)) - keymap ,(let ((map (make-sparse-keymap))) - (define-key map [mode-line mouse-1] - 'eglot-events-buffer) - (define-key map [mode-line mouse-3] - 'eglot-forget-pending-continuations) - map))))))) + mouse-face mode-line-highlight + face ,(cond ((and pending (cl-plusp pending)) + 'warning) + (t + 'eglot-mode-line)) + keymap ,(let ((map (make-sparse-keymap))) + (define-key map [mode-line mouse-1] + 'eglot-events-buffer) + (define-key map [mode-line mouse-3] + 'eglot-forget-pending-continuations) + map))))))))) (add-to-list 'mode-line-misc-info `(eglot-mode @@ -728,5 +741,13 @@ running. INTERACTIVE is t if called interactively." (setq eglot--current-flymake-report-fn report-fn) (eglot--maybe-signal-didChange)) + +;;; Rust-specific +;;; +(cl-defun eglot--window/progress + (process &key id done title ) + "Handle notification window/progress" + (setf (eglot--spinner process) (list id title done))) + (provide 'eglot) ;;; eglot.el ends here commit 09dfb21d3e0511a4bf2935be53dfb1097d0f7831 Author: João Távora Date: Tue May 1 22:30:09 2018 +0100 Fix parser to accept multiple messages in one chunk * eglot.el (eglot--process-filter): Redesign slightly. (eglot--message-mark): Remove. don't need this. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cd213e72c9..ab13b3a52a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -69,9 +69,6 @@ `(let ((proc (or ,process (eglot--current-process-or-lose)))) (process-put proc ',prop ,to-store)))))) -(eglot--define-process-var eglot--message-mark nil - "Point where next unread message starts") - (eglot--define-process-var eglot--short-name nil "A short name for the process") @@ -139,9 +136,6 @@ INTERACTIVE is t if called interactively." (setf (eglot--short-name proc) short-name) (puthash (project-current) proc eglot--processes-by-project) (erase-buffer) - (let ((marker (point-marker))) - (set-marker-insertion-type marker nil) - (setf (eglot--message-mark proc) marker)) (read-only-mode t) (with-current-buffer (eglot-events-buffer proc) (let ((inhibit-read-only t)) @@ -170,71 +164,75 @@ INTERACTIVE is t if called interactively." (delete-process process)))) (defun eglot--process-filter (proc string) + "Called when new data STRING has arrived for PROC." (when (buffer-live-p (process-buffer proc)) (with-current-buffer (process-buffer proc) - (let ((moving (= (point) (process-mark proc))) - (inhibit-read-only t) + (let ((inhibit-read-only t) (pre-insertion-mark (copy-marker (process-mark proc))) - (expected-bytes (eglot--expected-bytes proc)) - (message-mark (eglot--message-mark proc))) - (save-excursion - ;; Insert the text, advancing the process marker. - (goto-char (process-mark proc)) - (insert string) - (set-marker (process-mark proc) (point))) - (if moving (goto-char (process-mark proc))) - - ;; check for new message header + (expected-bytes (eglot--expected-bytes proc))) + ;; Insert the text, advancing the process marker. + (goto-char (process-mark proc)) + (insert string) + (set-marker (process-mark proc) (point)) + + ;; goto point just before insertion ;; - (save-excursion - (goto-char pre-insertion-mark) - (let* ((match (search-forward-regexp - "\\(?:.*: .*\r\n\\)*Content-Length: \\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" - (+ (point) 100) - t)) - (new-expected-bytes (and match - (string-to-number (match-string 1))))) - (when new-expected-bytes - (when expected-bytes - (eglot--warn - (concat "Unexpectedly starting new message but %s bytes " - "reportedly remaining from previous one") - expected-bytes)) - (set-marker message-mark (point)) - (setf (eglot--expected-bytes proc) new-expected-bytes) - (setq expected-bytes new-expected-bytes)))) - - ;; check for message body + (goto-char pre-insertion-mark) + + ;; loop for each message (more than one might have arrived) ;; - (let ((available-bytes (- (position-bytes (process-mark proc)) - (position-bytes message-mark)))) - (cond ((not expected-bytes) - (eglot--warn - "Skipping %s bytes of unexpected garbage from process %s" - available-bytes - proc) - (set-marker message-mark (process-mark proc))) - ((>= available-bytes - expected-bytes) - (let* ((message-end (byte-to-position - (+ (position-bytes message-mark) - expected-bytes)))) - (unwind-protect - (save-excursion - (save-restriction - (goto-char message-mark) - (narrow-to-region message-mark - message-end) - (eglot--process-receive - proc - (let ((json-object-type 'plist)) - (json-read))))) - (set-marker message-mark message-end) - (setf (eglot--expected-bytes proc) nil)))) - (t - ;; just adding some stuff to the end that doesn't yet - ;; complete the message - ))))))) + (catch 'done + (while t + (let* ((match (search-forward-regexp + "\\(?:.*: .*\r\n\\)*Content-Length: \\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" + (+ (point) 100) + t)) + (new-expected-bytes (and match + (string-to-number (match-string 1))))) + (when new-expected-bytes + (when expected-bytes + (eglot--warn + (concat "Unexpectedly starting new message but %s bytes " + "reportedly remaining from previous one") + expected-bytes)) + (setf (eglot--expected-bytes proc) new-expected-bytes) + (setq expected-bytes new-expected-bytes))) + + ;; check for message body + ;; + (let ((available-bytes (- (position-bytes (process-mark proc)) + (position-bytes (point))))) + (cond ((not expected-bytes) ; previous search didn't match + (eglot--warn + "Skipping %s bytes of unexpected garbage from process %s" + available-bytes + proc) + (goto-char (process-mark proc)) + (throw 'done :skipping-garbage)) + ((>= available-bytes + expected-bytes) + (let* ((message-end (byte-to-position + (+ (position-bytes (point)) + expected-bytes)))) + (unwind-protect + (save-restriction + (narrow-to-region (point) + message-end) + (let* ((json-object-type 'plist) + (json-message (json-read))) + ;; process in another buffer, shielding + ;; buffer from tamper + (with-temp-buffer + (eglot--process-receive proc json-message)))) + (goto-char message-end) + (setf (eglot--expected-bytes proc) nil + expected-bytes nil))) + (when (= (point) (process-mark proc)) + (throw 'done :clean-done))) + (t + ;; just adding some stuff to the end that doesn't yet + ;; complete the message + (throw 'done :waiting-for-more-bytes)))))))))) (defmacro eglot--obj (&rest what) "Make WHAT a suitable argument for `json-encode'." commit 69a3abdd0f0b7118f8ebce60e339391c66f314fa Author: João Távora Date: Tue May 1 10:46:06 2018 +0100 Lay groundwork for uniform treatment of network connections * eglot.el (eglot--connect): New helper. (eglot-new-process): Use it. (pcase): Require it. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9e5f6c2818..cd213e72c9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -29,6 +29,7 @@ (require 'project) (require 'url-parse) (require 'url-util) +(require 'pcase) (defgroup eglot nil "Interaction with Language Server Protocol servers" @@ -97,12 +98,27 @@ major-mode)) probe)) +(defun eglot--connect (name filter sentinel) + "Helper for `eglot-new-process'. +NAME is a name to give the inferior process or connection. +FILTER and SENTINEL are filter and sentinel. +Should return a list of (PROCESS BUFFER)." + (let ((proc (make-process :name name + :buffer (get-buffer-create + (format "*%s inferior*" name)) + :command (eglot--command 'error) + :connection-type 'pipe + :filter filter + :sentinel sentinel + :stderr (get-buffer-create (format "*%s stderr*" + name))))) + (list proc (process-buffer proc)))) + (defun eglot-new-process (&optional interactive) "Start a new EGLOT process and initialize it. INTERACTIVE is t if called interactively." (interactive (list t)) - (let ((project (project-current)) - (command (eglot--command 'errorp))) + (let ((project (project-current))) (unless project (eglot--error "(new-process) Cannot work without a current project!")) (let ((current-process (eglot--current-process))) (when (and current-process @@ -114,30 +130,24 @@ INTERACTIVE is t if called interactively." (car (project-roots (project-current)))))) (good-name (format "EGLOT server (%s)" short-name))) - (with-current-buffer (get-buffer-create - (format "*%s inferior*" good-name)) - (let* ((proc - (make-process :name good-name - :buffer (current-buffer) - :command command - :connection-type 'pipe - :filter 'eglot--process-filter - :sentinel 'eglot--process-sentinel - :stderr (get-buffer-create (format "*%s stderr*" - good-name)))) - (inhibit-read-only t)) - (setf (eglot--short-name proc) short-name) - (puthash (project-current) proc eglot--processes-by-project) - (erase-buffer) - (let ((marker (point-marker))) - (set-marker-insertion-type marker nil) - (setf (eglot--message-mark proc) marker)) - (read-only-mode t) - (with-current-buffer (eglot-events-buffer proc) - (let ((inhibit-read-only t)) - (insert - (format "\n-----------------------------------\n")))) - (eglot--protocol-initialize proc interactive)))))) + (pcase-let ((`(,proc ,buffer) + (eglot--connect good-name + 'eglot--process-filter + 'eglot--process-sentinel))) + (with-current-buffer buffer + (let ((inhibit-read-only t)) + (setf (eglot--short-name proc) short-name) + (puthash (project-current) proc eglot--processes-by-project) + (erase-buffer) + (let ((marker (point-marker))) + (set-marker-insertion-type marker nil) + (setf (eglot--message-mark proc) marker)) + (read-only-mode t) + (with-current-buffer (eglot-events-buffer proc) + (let ((inhibit-read-only t)) + (insert + (format "\n-----------------------------------\n")))) + (eglot--protocol-initialize proc interactive))))))) (defun eglot--process-sentinel (process change) (with-current-buffer (process-buffer process) commit 76dd0850a71764d17bb72d243c5744ae4ef08df5 Author: João Távora Date: Tue May 1 10:18:31 2018 +0100 Doc fixes * eglot.el (eglot-mode-map): Move up before minor mode. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8356f8ef3f..9e5f6c2818 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -36,12 +36,12 @@ :group 'applications) (defvar eglot-executables '((rust-mode . ("rls"))) - "Alist mapping major modes to server executables") + "Alist mapping major modes to server executables.") (defvar eglot--processes-by-project (make-hash-table :test #'equal)) (defun eglot--current-process () - "The current logical EGLOT process" + "The current logical EGLOT process." (let ((cur (project-current))) (and cur (gethash cur eglot--processes-by-project)))) @@ -98,7 +98,8 @@ probe)) (defun eglot-new-process (&optional interactive) - "Starts a new EGLOT process and initializes it" + "Start a new EGLOT process and initialize it. +INTERACTIVE is t if called interactively." (interactive (list t)) (let ((project (project-current)) (command (eglot--command 'errorp))) @@ -226,12 +227,14 @@ ))))))) (defmacro eglot--obj (&rest what) - "Make an object suitable for `json-encode'" + "Make WHAT a suitable argument for `json-encode'." ;; FIXME: maybe later actually do something, for now this just fixes ;; the indenting of literal plists. `(list ,@what)) (defun eglot-events-buffer (process &optional interactive) + "Display events buffer for current LSP connection PROCESS. +INTERACTIVE is t if called interactively." (interactive (list (eglot--current-process-or-lose) t)) (let* ((probe (eglot--events-buffer process)) (buffer (or (and (buffer-live-p probe) @@ -264,7 +267,7 @@ "A list of variables with saved values on every request.") (defvar eglot--environment nil - "Dynamically bound alist of symbol and values") + "Dynamically bound alist of symbol and values.") (defun eglot--process-receive (proc message) "Process MESSAGE from PROC." @@ -325,6 +328,7 @@ (setq eglot--next-request-id (1+ eglot--next-request-id))) (defun eglot-forget-pending-continuations (process) + "Stop waiting for responses from the current LSP PROCESS." (interactive (eglot--current-process-or-lose)) (clrhash (eglot--pending-continuations process))) @@ -641,7 +645,7 @@ running. INTERACTIVE is t if called interactively." (" [" eglot--mode-line-format "] "))) (defvar eglot--recent-changes nil - "List of recent changes as collected by `eglot--after-change'") + "List of recent changes as collected by `eglot--after-change'.") (defvar-local eglot--versioned-identifier 0) commit 2ff4dff73de109615eb5697ac6effae53483e276 Author: João Távora Date: Tue May 1 10:14:53 2018 +0100 * eglot.el (eglot-mode-map): move up before minor mode. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8da6267123..8356f8ef3f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -550,6 +550,8 @@ running. INTERACTIVE is t if called interactively." "Face for package-name in EGLOT's mode line." :group 'eglot) +(defvar eglot-mode-map (make-sparse-keymap)) + (define-minor-mode eglot-mode "Minor mode for buffers where EGLOT is possible" nil @@ -567,9 +569,7 @@ running. INTERACTIVE is t if called interactively." (defvar eglot-menu) -(defvar eglot-mode-map (make-sparse-keymap)) - -(easy-menu-define eglot-menu eglot-mode-map "SLY" +(easy-menu-define eglot-menu eglot-mode-map "EGLOT" `("EGLOT" )) (defvar eglot--mode-line-format commit b84c05058984f17cfc3467b94b445781c763ca32 Author: João Távora Date: Mon Apr 30 18:54:54 2018 +0100 Start working on this again * eglot.el (url-util): Require it. (eglot--process-sentinel): pending continuations now are quads (added env). (eglot--process-filter): Unwind message markers correctly if handling fails. (eglot--obj): Simple macro. (eglot--log-event): Add some info to logged event. (eglot--environment-vars, eglot--environment): Helper vars. (eglot--process-receive): Improve. (eglot--process-send): Niver log. (eglot--request): Use eglot--obj. Add environment. (eglot--notify): New helper. (eglot--protocol-initialize): RLS must like file:// (eglot--current-flymake-report-fn): New var. (eglot--textDocument/publishDiagnostics): Use flymake from Emacs 26. (eglot-mode): Proper minor mode. (eglot--recent-changes, eglot--versioned-identifier): New stuff. (eglot--current-buffer-versioned-identifier) (eglot--current-buffer-VersionedTextDocumentIdentifier) (eglot--current-buffer-TextDocumentItem, eglot--after-change) (eglot--signalDidOpen, eglot--maybe-signal-didChange): New stuff. (eglot-flymake-backend): More or less a flymake backend function. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8946692c9e..8da6267123 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -28,6 +28,7 @@ (require 'cl-lib) (require 'project) (require 'url-parse) +(require 'url-util) (defgroup eglot nil "Interaction with Language Server Protocol servers" @@ -143,8 +144,8 @@ (when (not (process-live-p process)) ;; Remember to cancel all timers ;; - (maphash (lambda (id triplet) - (cl-destructuring-bind (_success _error timeout) triplet + (maphash (lambda (id quad) + (cl-destructuring-bind (_success _error timeout _env) quad (eglot--message "(sentinel) Cancelling timer for continuation %s" id) (cancel-timer timeout))) @@ -185,7 +186,7 @@ (when new-expected-bytes (when expected-bytes (eglot--warn - (concat "Unexpectedly starting new message but %s bytes" + (concat "Unexpectedly starting new message but %s bytes " "reportedly remaining from previous one") expected-bytes)) (set-marker message-mark (point)) @@ -207,22 +208,29 @@ (let* ((message-end (byte-to-position (+ (position-bytes message-mark) expected-bytes)))) - (save-excursion - (save-restriction - (goto-char message-mark) - (narrow-to-region message-mark - message-end) - (eglot--process-receive - proc - (let ((json-object-type 'plist)) - (json-read))))) - (set-marker message-mark message-end) - (setf (eglot--expected-bytes proc) nil))) + (unwind-protect + (save-excursion + (save-restriction + (goto-char message-mark) + (narrow-to-region message-mark + message-end) + (eglot--process-receive + proc + (let ((json-object-type 'plist)) + (json-read))))) + (set-marker message-mark message-end) + (setf (eglot--expected-bytes proc) nil)))) (t ;; just adding some stuff to the end that doesn't yet ;; complete the message ))))))) +(defmacro eglot--obj (&rest what) + "Make an object suitable for `json-encode'" + ;; FIXME: maybe later actually do something, for now this just fixes + ;; the indenting of literal plists. + `(list ,@what)) + (defun eglot-events-buffer (process &optional interactive) (interactive (list (eglot--current-process-or-lose) t)) (let* ((probe (eglot--events-buffer process)) @@ -241,22 +249,39 @@ (display-buffer buffer)) buffer)) -(defun eglot--log-event (proc type message) +(defun eglot--log-event (proc type message id error) (with-current-buffer (eglot-events-buffer proc) (let ((inhibit-read-only t)) (goto-char (point-max)) - (insert (format "%s: \n%s\n" type (pp-to-string message)))))) + (insert (format "%s%s%s:\n%s\n" + type + (if id (format " (id:%s)" id) "") + (if error " ERROR" "") + (pp-to-string message)))))) + +(defvar eglot--environment-vars + '(eglot--current-flymake-report-fn) + "A list of variables with saved values on every request.") + +(defvar eglot--environment nil + "Dynamically bound alist of symbol and values") (defun eglot--process-receive (proc message) - (let ((inhibit-read-only t)) - (insert (format "Server said:\n%s\n" message))) - (eglot--log-event proc 'server message) - ;; Maybe this is a responsee - ;; + "Process MESSAGE from PROC." (let* ((response-id (plist-get message :id)) (err (plist-get message :error)) (continuations (and response-id (gethash response-id (eglot--pending-continuations))))) + (eglot--log-event proc + (cond ((not response-id) + 'server-notification) + ((not continuations) + 'unexpected-server-reply) + (t + 'server-reply)) + message + response-id + err) (cond ((and response-id (not continuations)) (eglot--warn "Ooops no continuation for id %s" response-id)) @@ -271,21 +296,28 @@ (t (let* ((method (plist-get message :method)) (handler-sym (intern (concat "eglot--" - method)))) + method))) + (eglot--environment (cl-fourth continuations))) (if (functionp handler-sym) - (apply handler-sym proc (plist-get message :params)) + (cl-progv + (mapcar #'car eglot--environment) + (mapcar #'cdr eglot--environment) + (apply handler-sym proc (plist-get message :params))) (eglot--debug "No implemetation for notification %s yet" method))))))) (defvar eglot--expect-carriage-return nil) -(defun eglot--process-send (proc message) +(defun eglot--process-send (id proc message) (let* ((json (json-encode message)) (to-send (format "Content-Length: %d\r\n\r\n%s" (string-bytes json) json))) (process-send-string proc to-send) - (eglot--log-event proc 'client message))) + (eglot--log-event proc (if id + 'client-request + 'client-notification) + message id nil))) (defvar eglot--next-request-id 0) @@ -300,6 +332,7 @@ method params &key success-fn error-fn timeout-fn (async-p t)) + "Make a request to PROCESS, expecting a reply." (let* ((id (eglot--next-request-id)) (timeout-fn (or timeout-fn @@ -322,11 +355,12 @@ "(request) Request id=%s replied to with result=%s: %s" id result-body))))) (catch-tag (cl-gensym (format "eglot--tag-%d-" id)))) - (eglot--process-send process - `(:jsonrpc "2.0" - :id ,id - :method ,method - :params ,params)) + (eglot--process-send id + process + (eglot--obj :jsonrpc "2.0" + :id id + :method method + :params params)) (catch catch-tag (let ((timeout-timer (run-with-timer 5 nil @@ -343,7 +377,9 @@ error-fn (lambda (&rest args) (throw catch-tag (apply error-fn args)))) - timeout-timer) + timeout-timer + (cl-loop for var in eglot--environment-vars + collect (cons var (symbol-value var)))) (eglot--pending-continuations process)) (unless async-p (unwind-protect @@ -362,6 +398,15 @@ "(request) Last-change cancelling timer for continuation %s" id) (cancel-timer timeout-timer)))))))) +(cl-defun eglot--notify (process method params) + "Notify PROCESS of something, don't expect a reply.e" + (eglot--process-send nil + process + (eglot--obj :jsonrpc "2.0" + :id nil + :method method + :params params))) + ;;; Requests ;;; @@ -373,8 +418,7 @@ INTERACTIVE is t if caller was called interactively." process :initialize `(:processId ,(emacs-pid) - :rootPath ,(concat "" ;; FIXME RLS doesn't like "file://" - ;; "file://" + :rootPath ,(concat "file://" (expand-file-name (car (project-roots (project-current))))) :initializationOptions [] @@ -423,45 +467,54 @@ running. INTERACTIVE is t if called interactively." ;;; Notifications ;;; -(defvar-local eglot--diagnostic-overlays nil) +(defvar eglot--current-flymake-report-fn nil) (cl-defun eglot--textDocument/publishDiagnostics (_process &key uri diagnostics) "Handle notification publishDiagnostics" (let* ((obj (url-generic-parse-url uri)) (filename (car (url-path-and-query obj))) - (buffer (find-buffer-visiting filename))) + (buffer (find-buffer-visiting filename)) + (report-fn (cdr (assoc 'eglot--current-flymake-report-fn + eglot--environment)))) (cond + ((not eglot--current-flymake-report-fn) + (eglot--warn "publishDiagnostics called but no report-fn")) + ((and report-fn + (not (eq report-fn + eglot--current-flymake-report-fn))) + (eglot--warn "outdated publishDiagnostics report from server")) (buffer (with-current-buffer buffer (eglot--message "OK so add some %s diags" (length diagnostics)) - (mapc #'delete-overlay eglot--diagnostic-overlays) - (setq eglot--diagnostic-overlays nil) - (cl-flet ((pos-at (pos-plist) - (save-excursion - (goto-char (point-min)) - (forward-line (plist-get pos-plist :line)) - (forward-char (plist-get pos-plist :character)) - (point)))) - (cl-loop for diag across diagnostics - do (cl-destructuring-bind (&key range severity - _code _source message) - diag - (cl-destructuring-bind (&key start end) - range - (let* ((begin-pos (pos-at start)) - (end-pos (pos-at end)) - (ov (make-overlay begin-pos - end-pos - buffer))) - (push ov eglot--diagnostic-overlays) - (overlay-put ov 'face - (cl-case severity - (1 'flymake-errline) - (2 'flymake-warnline))) - (overlay-put ov 'help-echo - message) - (overlay-put ov 'eglot--diagnostic diag)))))))) + (cl-flet ((pos-at + (pos-plist) + (car (flymake-diag-region + (current-buffer) + (plist-get pos-plist :line) + (plist-get pos-plist :character))))) + (cl-loop for diag-spec across diagnostics + collect (cl-destructuring-bind (&key range severity + _code _source message) + diag-spec + (cl-destructuring-bind (&key start end) + range + (let* ((begin-pos (pos-at start)) + (end-pos (pos-at end))) + (flymake-make-diagnostic + (current-buffer) + begin-pos end-pos + (cond ((<= severity 1) + :error) + ((= severity 2) + :warning) + (t + :note)) + message)))) + into diags + finally (funcall + eglot--current-flymake-report-fn + diags))))) (t (eglot--message "OK so %s isn't visited" filename))))) @@ -498,7 +551,19 @@ running. INTERACTIVE is t if called interactively." :group 'eglot) (define-minor-mode eglot-mode - "Minor mode for buffers where EGLOT is possible") + "Minor mode for buffers where EGLOT is possible" + nil + nil + eglot-mode-map + (cond (eglot-mode + (add-hook 'after-change-functions 'eglot--after-change nil t) + (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) + (if (eglot--current-process) + (eglot--signalDidOpen) + (eglot--warn "No process"))) + (t + (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) + (remove-hook 'after-change-functions 'eglot--after-change t)))) (defvar eglot-menu) @@ -575,5 +640,81 @@ running. INTERACTIVE is t if called interactively." `(eglot-mode (" [" eglot--mode-line-format "] "))) +(defvar eglot--recent-changes nil + "List of recent changes as collected by `eglot--after-change'") + +(defvar-local eglot--versioned-identifier 0) + +(defun eglot--current-buffer-versioned-identifier () + "Return a VersionedTextDocumentIdentifier." + ;; FIXME: later deal with workspaces + eglot--versioned-identifier) + +(defun eglot--current-buffer-VersionedTextDocumentIdentifier () + (eglot--obj :uri + (concat "file://" + (url-hexify-string + (file-truename buffer-file-name) + url-path-allowed-chars)) + :version (eglot--current-buffer-versioned-identifier))) + +(defun eglot--current-buffer-TextDocumentItem () + (append + (eglot--current-buffer-VersionedTextDocumentIdentifier) + (eglot--obj :languageId (cdr (assoc major-mode + '((rust-mode . rust) + (emacs-lisp-mode . emacs-lisp)))) + :text + (save-restriction + (widen) + (buffer-substring-no-properties (point-min) (point-max)))))) + +(defun eglot--after-change (start end length) + (cl-incf eglot--versioned-identifier) + (push (list start end length) eglot--recent-changes) + (eglot--message "start is %s, end is %s, length is %s" start end length)) + +(defun eglot--signalDidOpen () + (eglot--notify (eglot--current-process-or-lose) + :textDocument/didOpen + (eglot--obj :textDocument + (eglot--current-buffer-TextDocumentItem)))) + +(defun eglot--maybe-signal-didChange () + (when eglot--recent-changes + (save-excursion + (save-restriction + (widen) + (let* ((start (cl-reduce #'min (mapcar #'car eglot--recent-changes))) + (end (cl-reduce #'max (mapcar #'cadr eglot--recent-changes)))) + (eglot--notify + (eglot--current-process-or-lose) + :textDocument/didChange + (eglot--obj + :textDocument (eglot--current-buffer-VersionedTextDocumentIdentifier) + :contentChanges + (vector + (eglot--obj + :range (eglot--obj + :start + (eglot--obj :line + (line-number-at-pos start t) + :character + (- (goto-char start) + (line-beginning-position))) + :end + (eglot--obj :line + (line-number-at-pos end t) + :character + (- (goto-char end) + (line-beginning-position)))) + :rangeLength (- end start) + :text (buffer-substring-no-properties start end)))))))) + (setq eglot--recent-changes nil))) + +(defun eglot-flymake-backend (report-fn &rest _more) + (setq eglot--current-flymake-report-fn report-fn) + (eglot--maybe-signal-didChange)) + (provide 'eglot) ;;; eglot.el ends here commit 2b972ba05be14b8a2ac81dc6af004c153437d0e8 Author: João Távora Date: Mon Apr 30 13:39:16 2018 +0100 Fix mode line * eglot.el (mode-line-misc-info): conditionalize to eglot-mode diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0a52da74fa..8946692c9e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -572,7 +572,7 @@ running. INTERACTIVE is t if called interactively." map))))))) (add-to-list 'mode-line-misc-info - `(t + `(eglot-mode (" [" eglot--mode-line-format "] "))) (provide 'eglot) commit 0391fdf06219bb4bce7d7f4c81f7ff3df9d0ea1c Author: João Távora Date: Mon Apr 30 13:38:37 2018 +0100 Fix some byte-compilation warnings diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index af9904a7b3..0a52da74fa 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -27,6 +27,7 @@ (require 'json) (require 'cl-lib) (require 'project) +(require 'url-parse) (defgroup eglot nil "Interaction with Language Server Protocol servers" @@ -211,8 +212,10 @@ (goto-char message-mark) (narrow-to-region message-mark message-end) - (eglot--process-receive proc (let ((json-object-type 'plist)) - (json-read))))) + (eglot--process-receive + proc + (let ((json-object-type 'plist)) + (json-read))))) (set-marker message-mark message-end) (setf (eglot--expected-bytes proc) nil))) (t @@ -258,21 +261,21 @@ (not continuations)) (eglot--warn "Ooops no continuation for id %s" response-id)) (continuations - (cancel-timer (third continuations)) + (cancel-timer (cl-third continuations)) (remhash response-id (eglot--pending-continuations)) (cond (err - (apply (second continuations) err)) + (apply (cl-second continuations) err)) (t - (apply (first continuations) (plist-get message :result))))) + (apply (cl-first continuations) (plist-get message :result))))) (t (let* ((method (plist-get message :method)) (handler-sym (intern (concat "eglot--" - method)))) + method)))) (if (functionp handler-sym) (apply handler-sym proc (plist-get message :params)) (eglot--debug "No implemetation for notification %s yet" - method))))))) + method))))))) (defvar eglot--expect-carriage-return nil) @@ -294,9 +297,9 @@ (clrhash (eglot--pending-continuations process))) (cl-defun eglot--request (process - method - params - &key success-fn error-fn timeout-fn (async-p t)) + method + params + &key success-fn error-fn timeout-fn (async-p t)) (let* ((id (eglot--next-request-id)) (timeout-fn (or timeout-fn @@ -363,6 +366,9 @@ ;;; Requests ;;; (defun eglot--protocol-initialize (process interactive) + "Initialize LSP protocol. +PROCESS is a connected process (network or local). +INTERACTIVE is t if caller was called interactively." (eglot--request process :initialize @@ -385,7 +391,7 @@ (defun eglot-quit-server (process &optional sync interactive) "Politely ask the server PROCESS to quit. If SYNC, don't leave this function with the server still -running." +running. INTERACTIVE is t if called interactively." (interactive (list (eglot--current-process-or-lose) t t)) (when interactive (eglot--message "(eglot-quit-server) Asking %s politely to terminate" @@ -437,47 +443,50 @@ running." (forward-line (plist-get pos-plist :line)) (forward-char (plist-get pos-plist :character)) (point)))) - (loop for diag across diagnostics - do (cl-destructuring-bind (&key range severity - _code _source message) - diag - (cl-destructuring-bind (&key start end) - range - (let* ((begin-pos (pos-at start)) - (end-pos (pos-at end)) - (ov (make-overlay begin-pos - end-pos - buffer))) - (push ov eglot--diagnostic-overlays) - (overlay-put ov 'face - (case severity - (1 'flymake-errline) - (2 'flymake-warnline))) - (overlay-put ov 'help-echo - message) - (overlay-put ov 'eglot--diagnostic diag)))))))) + (cl-loop for diag across diagnostics + do (cl-destructuring-bind (&key range severity + _code _source message) + diag + (cl-destructuring-bind (&key start end) + range + (let* ((begin-pos (pos-at start)) + (end-pos (pos-at end)) + (ov (make-overlay begin-pos + end-pos + buffer))) + (push ov eglot--diagnostic-overlays) + (overlay-put ov 'face + (cl-case severity + (1 'flymake-errline) + (2 'flymake-warnline))) + (overlay-put ov 'help-echo + message) + (overlay-put ov 'eglot--diagnostic diag)))))))) (t (eglot--message "OK so %s isn't visited" filename))))) ;;; Helpers ;;; -(defun - eglot--debug (format &rest args) +(defun eglot--debug (format &rest args) + "Debug message FORMAT with ARGS." (display-warning 'eglot - (apply #'format format args) - :debug)) + (apply #'format format args) + :debug)) (defun eglot--error (format &rest args) + "Error out with FORMAT with ARGS." (error (apply #'format format args))) (defun eglot--message (format &rest args) + "Message out with FORMAT with ARGS." (message (concat "[eglot] " (apply #'format format args)))) (defun eglot--warn (format &rest args) + "Warning message with FORMAT and ARGS." (display-warning 'eglot - (apply #'format format args) - :warning)) + (apply #'format format args) + :warning)) @@ -504,6 +513,7 @@ running." (put 'eglot--mode-line-format 'risky-local-variable t) (defun eglot--mode-line-format () + "Compose the mode-line format spec." (let* ((proc (eglot--current-process)) (name (and proc (process-live-p proc) commit 5f1839bf171423424d64960af176b1d3c7ad3f89 Author: João Távora Date: Wed Aug 16 16:53:40 2017 +0100 Overhaul async mechanism safety diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b59ee02ce3..af9904a7b3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -84,24 +84,8 @@ (eglot--define-process-var eglot--capabilities :unreported "Holds list of capabilities that server reported") -(cl-defmacro eglot--request (process - method - params - success-fn - &key - error-fn - timeout-fn - (async-p t)) - (append `(eglot--call-with-request - ,process - ,async-p - ,method - ,params - (cl-function ,success-fn)) - (and error-fn - `((cl-function ,error-fn))) - (and timeout-fn - `((cl-function ,timeout-fn))))) +(eglot--define-process-var eglot--moribund nil + "Non-nil if process is about to exit") (defun eglot--command (&optional errorp) (let ((probe (cdr (assoc major-mode eglot-executables)))) @@ -116,12 +100,12 @@ (interactive (list t)) (let ((project (project-current)) (command (eglot--command 'errorp))) - (unless project (eglot--error "Cannot work without a current project!")) + (unless project (eglot--error "(new-process) Cannot work without a current project!")) (let ((current-process (eglot--current-process))) (when (and current-process (process-live-p current-process)) - (eglot--message "Asking current process to terminate first") - (eglot-quit-server current-process 'sync))) + (eglot--message "(new-process) Asking current process to terminate first") + (eglot-quit-server current-process 'sync interactive))) (let* ((short-name (file-name-base (directory-file-name (car (project-roots (project-current)))))) @@ -154,20 +138,23 @@ (defun eglot--process-sentinel (process change) (with-current-buffer (process-buffer process) - (eglot--debug "Process state changed to %s" change) + (eglot--debug "(sentinel) Process state changed to %s" change) (when (not (process-live-p process)) ;; Remember to cancel all timers ;; - (maphash (lambda (id v) - (cl-destructuring-bind (_success _error timeout) v - (eglot--message "Cancelling timer for continuation %s" id) + (maphash (lambda (id triplet) + (cl-destructuring-bind (_success _error timeout) triplet + (eglot--message + "(sentinel) Cancelling timer for continuation %s" id) (cancel-timer timeout))) (eglot--pending-continuations process)) - (cond ((process-get process 'eglot--moribund) - (eglot--message "Process exited with status %s" + (cond ((eglot--moribund process) + (eglot--message "(sentinel) Moribund process exited with status %s" (process-exit-status process))) (t - (eglot--warn "Process unexpectedly changed to %s" change)))))) + (eglot--warn "(sentinel) Process unexpectedly changed to %s" + change))) + (delete-process process)))) (defun eglot--process-filter (proc string) (when (buffer-live-p (process-buffer proc)) @@ -306,35 +293,44 @@ (interactive (eglot--current-process-or-lose)) (clrhash (eglot--pending-continuations process))) -(defun eglot--call-with-request (process - async-p - method - params - success-fn - &optional error-fn timeout-fn) +(cl-defun eglot--request (process + method + params + &key success-fn error-fn timeout-fn (async-p t)) (let* ((id (eglot--next-request-id)) - (timeout-fn (or timeout-fn - (lambda () - (eglot--warn "Tired of waiting for reply to %s" id) - (remhash id (eglot--pending-continuations process))))) - (error-fn (or error-fn - (cl-function - (lambda (&key code message) - (eglot--warn "Request id=%s errored with code=%s: %s" - id code message))))) + (timeout-fn + (or timeout-fn + (lambda () + (eglot--warn + "(request) Tired of waiting for reply to %s" id) + (remhash id (eglot--pending-continuations process))))) + (error-fn + (or error-fn + (cl-function + (lambda (&key code message) + (eglot--warn + "(request) Request id=%s errored with code=%s: %s" + id code message))))) + (success-fn + (or success-fn + (cl-function + (lambda (&rest result-body) + (eglot--debug + "(request) Request id=%s replied to with result=%s: %s" + id result-body))))) (catch-tag (cl-gensym (format "eglot--tag-%d-" id)))) (eglot--process-send process - `(:jsonrpc "2.0" - :id ,id - :method ,method - :params ,params)) + `(:jsonrpc "2.0" + :id ,id + :method ,method + :params ,params)) (catch catch-tag (let ((timeout-timer (run-with-timer 5 nil (if async-p timeout-fn (lambda () - (throw catch-tag (apply timeout-fn))))))) + (throw catch-tag (funcall timeout-fn))))))) (puthash id (list (if async-p success-fn @@ -350,9 +346,18 @@ (unwind-protect (while t (unless (process-live-p process) - (eglot--error "Process %s died unexpectedly" process)) + (cond ((eglot--moribund process) + (throw catch-tag (delete-process process))) + (t + (eglot--error + "(request) Proc %s died unexpectedly during request with code %s" + process + (process-exit-status process))))) (accept-process-output nil 0.01)) - (cancel-timer timeout-timer))))))) + (when (memq timeout-timer timer-list) + (eglot--message + "(request) Last-change cancelling timer for continuation %s" id) + (cancel-timer timeout-timer)))))))) ;;; Requests @@ -363,38 +368,51 @@ :initialize `(:processId ,(emacs-pid) :rootPath ,(concat "" ;; FIXME RLS doesn't like "file://" - "file://" + ;; "file://" (expand-file-name (car (project-roots (project-current))))) :initializationOptions [] :capabilities (:workspace (:executeCommand (:dynamicRegistration t)) :textDocument (:synchronization (:didSave t)))) - (lambda (&key capabilities) - (setf (eglot--capabilities process) capabilities) - (when interactive - (eglot--message - "So yeah I got lots (%d) of capabilities" - (length capabilities)))))) - -(defun eglot-quit-server (process &optional sync) - (interactive (list (eglot--current-process-or-lose))) - (eglot--message "Asking server to terminate") - (eglot--request - process - :shutdown - nil - (lambda (&rest _anything) - (eglot--message "Now asking server to exit") - (process-put process 'eglot--moribund t) - (eglot--process-send process - `(:jsonrpc "2.0" - :method :exit))) - :async-p (not sync) - :timeout-fn (lambda () - (eglot--warn "Brutally deleting existing process %s" - process) - (process-put process 'eglot--moribund t) - (delete-process process)))) + :success-fn (cl-function + (lambda (&key capabilities) + (setf (eglot--capabilities process) capabilities) + (when interactive + (eglot--message + "So yeah I got lots (%d) of capabilities" + (length capabilities))))))) + +(defun eglot-quit-server (process &optional sync interactive) + "Politely ask the server PROCESS to quit. +If SYNC, don't leave this function with the server still +running." + (interactive (list (eglot--current-process-or-lose) t t)) + (when interactive + (eglot--message "(eglot-quit-server) Asking %s politely to terminate" + process)) + (let ((brutal (lambda () + (eglot--warn "Brutally deleting existing process %s" + process) + (setf (eglot--moribund process) t) + (delete-process process)))) + (eglot--request + process + :shutdown + nil + :success-fn (lambda (&rest _anything) + (when interactive + (eglot--message "Now asking %s politely to exit" process)) + (setf (eglot--moribund process) t) + (eglot--request process + :exit + nil + :success-fn brutal + :async-p (not sync) + :error-fn brutal + :timeout-fn brutal)) + :error-fn brutal + :async-p (not sync) + :timeout-fn brutal))) ;;; Notifications commit dc6c221a76b3051e0625c40a3bbef2297eec2308 Author: João Távora Date: Wed Aug 16 15:48:50 2017 +0100 Simplify `eglot--protocol-initialize` * eglot.el (eglot--protocol-initialize): Simplify diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cd91d7821c..b59ee02ce3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -70,7 +70,7 @@ "Point where next unread message starts") (eglot--define-process-var eglot--short-name nil - "A short name") + "A short name for the process") (eglot--define-process-var eglot--expected-bytes nil "How many bytes declared by server") @@ -81,6 +81,9 @@ (eglot--define-process-var eglot--events-buffer nil "A buffer pretty-printing the EGLOT RPC events") +(eglot--define-process-var eglot--capabilities :unreported + "Holds list of capabilities that server reported") + (cl-defmacro eglot--request (process method params @@ -367,29 +370,11 @@ :capabilities (:workspace (:executeCommand (:dynamicRegistration t)) :textDocument (:synchronization (:didSave t)))) (lambda (&key capabilities) - (cl-destructuring-bind - (&rest all - &key - ;; capabilities reported by server - _textDocumentSync - _hoverProvider - _completionProvider - _definitionProvider - _referencesProvider - _documentHighlightProvider - _documentSymbolProvider - _workspaceSymbolProvider - _codeActionProvider - _documentFormattingProvider - _documentRangeFormattingProvider - _renameProvider - _executeCommandProvider - ) - capabilities - (when interactive + (setf (eglot--capabilities process) capabilities) + (when interactive (eglot--message "So yeah I got lots (%d) of capabilities" - (length all))))))) + (length capabilities)))))) (defun eglot-quit-server (process &optional sync) (interactive (list (eglot--current-process-or-lose))) commit 584ae9e0b5cfa5f438bd4fa84d191963a33a38bf Author: João Távora Date: Wed Aug 16 13:49:31 2017 +0100 Experimental diagnostic overlays diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 479e87ae56..cd91d7821c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -414,11 +414,47 @@ ;;; Notifications ;;; +(defvar-local eglot--diagnostic-overlays nil) + (cl-defun eglot--textDocument/publishDiagnostics (_process &key uri diagnostics) "Handle notification publishDiagnostics" - (eglot--message "So yeah I got %s for %s" - diagnostics uri)) + (let* ((obj (url-generic-parse-url uri)) + (filename (car (url-path-and-query obj))) + (buffer (find-buffer-visiting filename))) + (cond + (buffer + (with-current-buffer buffer + (eglot--message "OK so add some %s diags" (length diagnostics)) + (mapc #'delete-overlay eglot--diagnostic-overlays) + (setq eglot--diagnostic-overlays nil) + (cl-flet ((pos-at (pos-plist) + (save-excursion + (goto-char (point-min)) + (forward-line (plist-get pos-plist :line)) + (forward-char (plist-get pos-plist :character)) + (point)))) + (loop for diag across diagnostics + do (cl-destructuring-bind (&key range severity + _code _source message) + diag + (cl-destructuring-bind (&key start end) + range + (let* ((begin-pos (pos-at start)) + (end-pos (pos-at end)) + (ov (make-overlay begin-pos + end-pos + buffer))) + (push ov eglot--diagnostic-overlays) + (overlay-put ov 'face + (case severity + (1 'flymake-errline) + (2 'flymake-warnline))) + (overlay-put ov 'help-echo + message) + (overlay-put ov 'eglot--diagnostic diag)))))))) + (t + (eglot--message "OK so %s isn't visited" filename))))) ;;; Helpers commit b864866dac26a959d3f7fabf7eefb7edc03a18ff Author: João Távora Date: Wed Aug 16 13:02:05 2017 +0100 Minor cleanup diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5699775a2b..479e87ae56 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -117,6 +117,7 @@ (let ((current-process (eglot--current-process))) (when (and current-process (process-live-p current-process)) + (eglot--message "Asking current process to terminate first") (eglot-quit-server current-process 'sync))) (let* ((short-name (file-name-base (directory-file-name @@ -148,7 +149,6 @@ (format "\n-----------------------------------\n")))) (eglot--protocol-initialize proc interactive)))))) - (defun eglot--process-sentinel (process change) (with-current-buffer (process-buffer process) (eglot--debug "Process state changed to %s" change) @@ -353,8 +353,7 @@ ;;; Requests -;;; - +;;; (defun eglot--protocol-initialize (process interactive) (eglot--request process @@ -445,8 +444,6 @@ ;;; Mode line ;;; - - (defface eglot-mode-line '((t (:inherit font-lock-constant-face :weight bold))) "Face for package-name in EGLOT's mode line." commit b2ea73ca9c80d1f20b4939eacf235dba05fc1aaf Author: João Távora Date: Wed Aug 16 12:58:20 2017 +0100 Cancel timeouts when process dies unexpectedly diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e3b288b038..5699775a2b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -153,9 +153,16 @@ (with-current-buffer (process-buffer process) (eglot--debug "Process state changed to %s" change) (when (not (process-live-p process)) + ;; Remember to cancel all timers + ;; + (maphash (lambda (id v) + (cl-destructuring-bind (_success _error timeout) v + (eglot--message "Cancelling timer for continuation %s" id) + (cancel-timer timeout))) + (eglot--pending-continuations process)) (cond ((process-get process 'eglot--moribund) (eglot--message "Process exited with status %s" - (process-exit-status process))) + (process-exit-status process))) (t (eglot--warn "Process unexpectedly changed to %s" change)))))) @@ -319,26 +326,30 @@ :method ,method :params ,params)) (catch catch-tag - (puthash id - (list (if async-p - success-fn - (lambda (&rest args) - (throw catch-tag (apply success-fn args)))) - (if async-p - error-fn - (lambda (&rest args) - (throw catch-tag (apply error-fn args)))) - (run-with-timer 5 nil - (if async-p - timeout-fn - (lambda () - (throw catch-tag (apply timeout-fn)))))) - (eglot--pending-continuations process)) - (unless async-p - (while t - (unless (eq (process-status process) 'open) - (eglot--error "Process %s died unexpectedly" process)) - (accept-process-output nil 0.01)))))) + (let ((timeout-timer + (run-with-timer 5 nil + (if async-p + timeout-fn + (lambda () + (throw catch-tag (apply timeout-fn))))))) + (puthash id + (list (if async-p + success-fn + (lambda (&rest args) + (throw catch-tag (apply success-fn args)))) + (if async-p + error-fn + (lambda (&rest args) + (throw catch-tag (apply error-fn args)))) + timeout-timer) + (eglot--pending-continuations process)) + (unless async-p + (unwind-protect + (while t + (unless (process-live-p process) + (eglot--error "Process %s died unexpectedly" process)) + (accept-process-output nil 0.01)) + (cancel-timer timeout-timer))))))) ;;; Requests commit 18ed39789ac832ba6e1db619930c3e958d8e1bb5 Author: João Távora Date: Wed Aug 16 12:50:53 2017 +0100 Organize a bit diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c7f8774d83..e3b288b038 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -148,25 +148,6 @@ (format "\n-----------------------------------\n")))) (eglot--protocol-initialize proc interactive)))))) -(defun eglot-quit-server (process &optional sync) - (interactive (list (eglot--current-process-or-lose))) - (eglot--message "Asking server to terminate") - (eglot--request - process - :shutdown - nil - (lambda (&rest _anything) - (eglot--message "Now asking server to exit") - (process-put process 'eglot--moribund t) - (eglot--process-send process - `(:jsonrpc "2.0" - :method :exit))) - :async-p (not sync) - :timeout-fn (lambda () - (eglot--warn "Brutally deleting existing process %s" - process) - (process-put process 'eglot--moribund t) - (delete-process process)))) (defun eglot--process-sentinel (process change) (with-current-buffer (process-buffer process) @@ -359,6 +340,9 @@ (eglot--error "Process %s died unexpectedly" process)) (accept-process-output nil 0.01)))))) + +;;; Requests +;;; (defun eglot--protocol-initialize (process interactive) (eglot--request @@ -366,6 +350,7 @@ :initialize `(:processId ,(emacs-pid) :rootPath ,(concat "" ;; FIXME RLS doesn't like "file://" + "file://" (expand-file-name (car (project-roots (project-current))))) :initializationOptions [] @@ -396,7 +381,26 @@ "So yeah I got lots (%d) of capabilities" (length all))))))) -(defun eglot--debug (format &rest args) +(defun eglot-quit-server (process &optional sync) + (interactive (list (eglot--current-process-or-lose))) + (eglot--message "Asking server to terminate") + (eglot--request + process + :shutdown + nil + (lambda (&rest _anything) + (eglot--message "Now asking server to exit") + (process-put process 'eglot--moribund t) + (eglot--process-send process + `(:jsonrpc "2.0" + :method :exit))) + :async-p (not sync) + :timeout-fn (lambda () + (eglot--warn "Brutally deleting existing process %s" + process) + (process-put process 'eglot--moribund t) + (delete-process process)))) + ;;; Notifications ;;; @@ -405,6 +409,12 @@ "Handle notification publishDiagnostics" (eglot--message "So yeah I got %s for %s" diagnostics uri)) + + +;;; Helpers +;;; +(defun + eglot--debug (format &rest args) (display-warning 'eglot (apply #'format format args) :debug)) commit a0003aa19a96b350e0468a2f9c43a83d935727db Author: João Távora Date: Wed Aug 16 12:50:20 2017 +0100 Improve `eglot--current-process' diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6e3a136596..c7f8774d83 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -41,13 +41,14 @@ (defun eglot--current-process () "The current logical EGLOT process" (let ((cur (project-current))) - (unless cur - (eglot--error "No current project, so no process")) - (gethash cur eglot--processes-by-project))) + (and cur + (gethash cur eglot--processes-by-project)))) (defun eglot--current-process-or-lose () (or (eglot--current-process) - (eglot--error "No current EGLOT process"))) + (eglot--error "No current EGLOT process%s" + (if (project-current) "" + " (Also no current project)")))) (defmacro eglot--define-process-var (var-sym initval &optional doc) (declare (indent 2)) commit 18b582dde2ef0275561a66fb292823f559ed7a0b Author: João Távora Date: Wed Aug 16 12:49:24 2017 +0100 Handle notifications diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5ca6833de7..6e3a136596 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -287,8 +287,13 @@ (t (apply (first continuations) (plist-get message :result))))) (t - (eglot--debug "No implemetation for notification %s yet" - (plist-get message :method)))))) + (let* ((method (plist-get message :method)) + (handler-sym (intern (concat "eglot--" + method)))) + (if (functionp handler-sym) + (apply handler-sym proc (plist-get message :params)) + (eglot--debug "No implemetation for notification %s yet" + method))))))) (defvar eglot--expect-carriage-return nil) @@ -391,6 +396,14 @@ (length all))))))) (defun eglot--debug (format &rest args) + +;;; Notifications +;;; +(cl-defun eglot--textDocument/publishDiagnostics + (_process &key uri diagnostics) + "Handle notification publishDiagnostics" + (eglot--message "So yeah I got %s for %s" + diagnostics uri)) (display-warning 'eglot (apply #'format format args) :debug)) commit 8baf2c7ac20e7d8327cfec5960a788e47d02be67 Author: João Távora Date: Wed Aug 16 12:13:52 2017 +0100 Introduce and use `eglot--current-process-or-lose' diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 92c12162f9..5ca6833de7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -45,12 +45,16 @@ (eglot--error "No current project, so no process")) (gethash cur eglot--processes-by-project))) +(defun eglot--current-process-or-lose () + (or (eglot--current-process) + (eglot--error "No current EGLOT process"))) + (defmacro eglot--define-process-var (var-sym initval &optional doc) (declare (indent 2)) `(progn (put ',var-sym 'function-documentation ,doc) (defun ,var-sym (&optional process) - (let* ((proc (or process (eglot--current-process))) + (let* ((proc (or process (eglot--current-process-or-lose))) (probe (process-get proc ',var-sym))) (or probe (let ((def ,initval)) @@ -58,7 +62,7 @@ def)))) (gv-define-setter ,var-sym (to-store &optional process) (let ((prop ',var-sym)) - `(let ((proc (or ,process (eglot--current-process)))) + `(let ((proc (or ,process (eglot--current-process-or-lose)))) (process-put proc ',prop ,to-store)))))) (eglot--define-process-var eglot--message-mark nil @@ -103,7 +107,7 @@ major-mode)) probe)) -(defun eglot-new-process (&optional _interactive) +(defun eglot-new-process (&optional interactive) "Starts a new EGLOT process and initializes it" (interactive (list t)) (let ((project (project-current)) @@ -141,10 +145,10 @@ (let ((inhibit-read-only t)) (insert (format "\n-----------------------------------\n")))) - (eglot--protocol-initialize proc)))))) + (eglot--protocol-initialize proc interactive)))))) (defun eglot-quit-server (process &optional sync) - (interactive (list (eglot--current-process))) + (interactive (list (eglot--current-process-or-lose))) (eglot--message "Asking server to terminate") (eglot--request process @@ -238,7 +242,7 @@ ))))))) (defun eglot-events-buffer (process &optional interactive) - (interactive (list (eglot--current-process) t)) + (interactive (list (eglot--current-process-or-lose) t)) (let* ((probe (eglot--events-buffer process)) (buffer (or (and (buffer-live-p probe) probe) @@ -302,7 +306,7 @@ (setq eglot--next-request-id (1+ eglot--next-request-id))) (defun eglot-forget-pending-continuations (process) - (interactive (eglot--current-process)) + (interactive (eglot--current-process-or-lose)) (clrhash (eglot--pending-continuations process))) (defun eglot--call-with-request (process @@ -350,38 +354,41 @@ (accept-process-output nil 0.01)))))) -(defun eglot--protocol-initialize (process) +(defun eglot--protocol-initialize (process interactive) (eglot--request - process - :initialize - `(:processId ,(emacs-pid) - :rootPath ,(concat "" ;; FIXME RLS doesn't like "file://" - (expand-file-name (car (project-roots - (project-current))))) - :initializationOptions [] - :capabilities (:workspace (:executeCommand (:dynamicRegistration t)) - :textDocument (:synchronization (:didSave t))) - ) - (lambda (&key capabilities) - (cl-destructuring-bind - (&rest all - &key - _textDocumentSync - _hoverProvider - _completionProvider - _definitionProvider - _referencesProvider - _documentHighlightProvider - _documentSymbolProvider - _workspaceSymbolProvider - _codeActionProvider - _documentFormattingProvider - _documentRangeFormattingProvider - _renameProvider - _executeCommandProvider - ) - capabilities - (message "so yeah I got lots (%d) of capabilities" (length all)))))) + process + :initialize + `(:processId ,(emacs-pid) + :rootPath ,(concat "" ;; FIXME RLS doesn't like "file://" + (expand-file-name (car (project-roots + (project-current))))) + :initializationOptions [] + :capabilities (:workspace (:executeCommand (:dynamicRegistration t)) + :textDocument (:synchronization (:didSave t)))) + (lambda (&key capabilities) + (cl-destructuring-bind + (&rest all + &key + ;; capabilities reported by server + _textDocumentSync + _hoverProvider + _completionProvider + _definitionProvider + _referencesProvider + _documentHighlightProvider + _documentSymbolProvider + _workspaceSymbolProvider + _codeActionProvider + _documentFormattingProvider + _documentRangeFormattingProvider + _renameProvider + _executeCommandProvider + ) + capabilities + (when interactive + (eglot--message + "So yeah I got lots (%d) of capabilities" + (length all))))))) (defun eglot--debug (format &rest args) (display-warning 'eglot commit d663b9282ddd49d7ad880a6fad6b2e07d16f59a1 Author: João Távora Date: Wed Aug 16 12:10:13 2017 +0100 Add a mode-line construct and some minor fanciness diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 166f23ccd8..92c12162f9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -64,6 +64,9 @@ (eglot--define-process-var eglot--message-mark nil "Point where next unread message starts") +(eglot--define-process-var eglot--short-name nil + "A short name") + (eglot--define-process-var eglot--expected-bytes nil "How many bytes declared by server") @@ -92,25 +95,29 @@ (and timeout-fn `((cl-function ,timeout-fn))))) -(defun eglot--command () - (cdr (assoc major-mode eglot-executables))) +(defun eglot--command (&optional errorp) + (let ((probe (cdr (assoc major-mode eglot-executables)))) + (unless (or (not errorp) + probe) + (eglot--error "Don't know how to start EGLOT for %s buffers" + major-mode)) + probe)) -(defun eglot-new-process (&optional interactive) +(defun eglot-new-process (&optional _interactive) "Starts a new EGLOT process and initializes it" (interactive (list t)) (let ((project (project-current)) - (command (eglot--command))) - (unless command (eglot--error "Cannot work without an LSP executable")) + (command (eglot--command 'errorp))) (unless project (eglot--error "Cannot work without a current project!")) (let ((current-process (eglot--current-process))) (when (and current-process (process-live-p current-process)) (eglot-quit-server current-process 'sync))) - (let ((good-name - (format "EGLOT server (%s)" - (file-name-base - (directory-file-name - (car (project-roots (project-current)))))))) + (let* ((short-name (file-name-base + (directory-file-name + (car (project-roots (project-current)))))) + (good-name + (format "EGLOT server (%s)" short-name))) (with-current-buffer (get-buffer-create (format "*%s inferior*" good-name)) (let* ((proc @@ -123,6 +130,7 @@ :stderr (get-buffer-create (format "*%s stderr*" good-name)))) (inhibit-read-only t)) + (setf (eglot--short-name proc) short-name) (puthash (project-current) proc eglot--processes-by-project) (erase-buffer) (let ((marker (point-marker))) @@ -133,9 +141,7 @@ (let ((inhibit-read-only t)) (insert (format "\n-----------------------------------\n")))) - (eglot--protocol-initialize proc) - (when interactive - (display-buffer (eglot-events-buffer proc)))))))) + (eglot--protocol-initialize proc)))))) (defun eglot-quit-server (process &optional sync) (interactive (list (eglot--current-process))) @@ -246,7 +252,7 @@ buffer)) buffer)))) (when interactive - (pop-to-buffer buffer)) + (display-buffer buffer)) buffer)) (defun eglot--log-event (proc type message) @@ -295,6 +301,10 @@ (defun eglot--next-request-id () (setq eglot--next-request-id (1+ eglot--next-request-id))) +(defun eglot-forget-pending-continuations (process) + (interactive (eglot--current-process)) + (clrhash (eglot--pending-continuations process))) + (defun eglot--call-with-request (process async-p method @@ -389,5 +399,93 @@ (apply #'format format args) :warning)) + + +;;; Mode line +;;; + + +(defface eglot-mode-line + '((t (:inherit font-lock-constant-face :weight bold))) + "Face for package-name in EGLOT's mode line." + :group 'eglot) + +(define-minor-mode eglot-mode + "Minor mode for buffers where EGLOT is possible") + +(defvar eglot-menu) + +(defvar eglot-mode-map (make-sparse-keymap)) + +(easy-menu-define eglot-menu eglot-mode-map "SLY" + `("EGLOT" )) + +(defvar eglot--mode-line-format + `(:eval (eglot--mode-line-format))) + +(put 'eglot--mode-line-format 'risky-local-variable t) + +(defun eglot--mode-line-format () + (let* ((proc (eglot--current-process)) + (name (and proc + (process-live-p proc) + (eglot--short-name proc))) + (pending (and proc + (hash-table-count + (eglot--pending-continuations proc)))) + (format-number (lambda (n) (cond ((and n (not (zerop n))) + (format "%d" n)) + (n "-") + (t "*"))))) + (append + `((:propertize "eglot" + face eglot-mode-line + keymap ,(let ((map (make-sparse-keymap))) + (define-key map [mode-line down-mouse-1] + eglot-menu) + map) + mouse-face mode-line-highlight + help-echo "mouse-1: pop-up EGLOT menu" + )) + (if name + `(" " + (:propertize + ,name + face eglot-mode-line + keymap ,(let ((map (make-sparse-keymap))) + (define-key map [mode-line mouse-1] 'eglot-events-buffer) + (define-key map [mode-line mouse-2] 'eglot-quit-server) + (define-key map [mode-line mouse-3] 'eglot-new-process) + map) + mouse-face mode-line-highlight + help-echo ,(concat "mouse-1: events buffer\n" + "mouse-2: quit server\n" + "mouse-3: new process")) + "/" + (:propertize + ,(funcall format-number pending) + help-echo ,(if name + (format + "%s pending events outgoing\n%s" + pending + (concat "mouse-1: go to events buffer" + "mouse-3: forget pending continuations")) + "No current connection") + mouse-face mode-line-highlight + face ,(cond ((and pending (cl-plusp pending)) + 'warning) + (t + 'eglot-mode-line)) + keymap ,(let ((map (make-sparse-keymap))) + (define-key map [mode-line mouse-1] + 'eglot-events-buffer) + (define-key map [mode-line mouse-3] + 'eglot-forget-pending-continuations) + map))))))) + +(add-to-list 'mode-line-misc-info + `(t + (" [" eglot--mode-line-format "] "))) + (provide 'eglot) ;;; eglot.el ends here commit 9f98c5a20d36bb20b2803b6a2ca749f4ad6e66c5 Author: João Távora Date: Wed Aug 16 11:36:44 2017 +0100 Rename eglot--continuations eglot--pending-continuations diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f2ef4184d6..166f23ccd8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -67,7 +67,7 @@ (eglot--define-process-var eglot--expected-bytes nil "How many bytes declared by server") -(eglot--define-process-var eglot--continuations (make-hash-table) +(eglot--define-process-var eglot--pending-continuations (make-hash-table) "A hash table of request ID to continuation lambdas") (eglot--define-process-var eglot--events-buffer nil @@ -264,12 +264,14 @@ (let* ((response-id (plist-get message :id)) (err (plist-get message :error)) (continuations (and response-id - (gethash response-id (eglot--continuations))))) + (gethash response-id (eglot--pending-continuations))))) (cond ((and response-id (not continuations)) (eglot--warn "Ooops no continuation for id %s" response-id)) (continuations (cancel-timer (third continuations)) + (remhash response-id + (eglot--pending-continuations)) (cond (err (apply (second continuations) err)) (t @@ -303,7 +305,7 @@ (timeout-fn (or timeout-fn (lambda () (eglot--warn "Tired of waiting for reply to %s" id) - (remhash id (eglot--continuations process))))) + (remhash id (eglot--pending-continuations process))))) (error-fn (or error-fn (cl-function (lambda (&key code message) @@ -330,7 +332,7 @@ timeout-fn (lambda () (throw catch-tag (apply timeout-fn)))))) - (eglot--continuations process)) + (eglot--pending-continuations process)) (unless async-p (while t (unless (eq (process-status process) 'open) commit 6beef2a347bb9f635194ae765d2e06b1c2c50b1e Author: João Távora Date: Wed Aug 16 09:24:46 2017 +0100 Remove a couple of comments diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f667eca867..f2ef4184d6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -36,7 +36,6 @@ (defvar eglot-executables '((rust-mode . ("rls"))) "Alist mapping major modes to server executables") -;;; TODO: Soon to be per-project (defvar eglot--processes-by-project (make-hash-table :test #'equal)) (defun eglot--current-process () @@ -279,7 +278,6 @@ (eglot--debug "No implemetation for notification %s yet" (plist-get message :method)))))) -;; (setq json-encoding-pretty-print nil) ; for debug (defvar eglot--expect-carriage-return nil) (defun eglot--process-send (proc message) commit 1e5b753bf46a4eb4fb32a062d6162063303f6cc7 Author: João Távora Date: Wed Aug 16 02:41:17 2017 +0100 Initial commit diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el new file mode 100644 index 0000000000..f667eca867 --- /dev/null +++ b/lisp/progmodes/eglot.el @@ -0,0 +1,393 @@ +;;; eglot.el --- A client for Language Server Protocol (LSP) servers -*- lexical-binding: t; -*- + +;; Copyright (C) 2017 João Távora + +;; Author: João Távora +;; Keywords: extensions + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; + +;;; Code: + +(require 'json) +(require 'cl-lib) +(require 'project) + +(defgroup eglot nil + "Interaction with Language Server Protocol servers" + :prefix "eglot-" + :group 'applications) + +(defvar eglot-executables '((rust-mode . ("rls"))) + "Alist mapping major modes to server executables") + +;;; TODO: Soon to be per-project +(defvar eglot--processes-by-project (make-hash-table :test #'equal)) + +(defun eglot--current-process () + "The current logical EGLOT process" + (let ((cur (project-current))) + (unless cur + (eglot--error "No current project, so no process")) + (gethash cur eglot--processes-by-project))) + +(defmacro eglot--define-process-var (var-sym initval &optional doc) + (declare (indent 2)) + `(progn + (put ',var-sym 'function-documentation ,doc) + (defun ,var-sym (&optional process) + (let* ((proc (or process (eglot--current-process))) + (probe (process-get proc ',var-sym))) + (or probe + (let ((def ,initval)) + (process-put proc ',var-sym def) + def)))) + (gv-define-setter ,var-sym (to-store &optional process) + (let ((prop ',var-sym)) + `(let ((proc (or ,process (eglot--current-process)))) + (process-put proc ',prop ,to-store)))))) + +(eglot--define-process-var eglot--message-mark nil + "Point where next unread message starts") + +(eglot--define-process-var eglot--expected-bytes nil + "How many bytes declared by server") + +(eglot--define-process-var eglot--continuations (make-hash-table) + "A hash table of request ID to continuation lambdas") + +(eglot--define-process-var eglot--events-buffer nil + "A buffer pretty-printing the EGLOT RPC events") + +(cl-defmacro eglot--request (process + method + params + success-fn + &key + error-fn + timeout-fn + (async-p t)) + (append `(eglot--call-with-request + ,process + ,async-p + ,method + ,params + (cl-function ,success-fn)) + (and error-fn + `((cl-function ,error-fn))) + (and timeout-fn + `((cl-function ,timeout-fn))))) + +(defun eglot--command () + (cdr (assoc major-mode eglot-executables))) + +(defun eglot-new-process (&optional interactive) + "Starts a new EGLOT process and initializes it" + (interactive (list t)) + (let ((project (project-current)) + (command (eglot--command))) + (unless command (eglot--error "Cannot work without an LSP executable")) + (unless project (eglot--error "Cannot work without a current project!")) + (let ((current-process (eglot--current-process))) + (when (and current-process + (process-live-p current-process)) + (eglot-quit-server current-process 'sync))) + (let ((good-name + (format "EGLOT server (%s)" + (file-name-base + (directory-file-name + (car (project-roots (project-current)))))))) + (with-current-buffer (get-buffer-create + (format "*%s inferior*" good-name)) + (let* ((proc + (make-process :name good-name + :buffer (current-buffer) + :command command + :connection-type 'pipe + :filter 'eglot--process-filter + :sentinel 'eglot--process-sentinel + :stderr (get-buffer-create (format "*%s stderr*" + good-name)))) + (inhibit-read-only t)) + (puthash (project-current) proc eglot--processes-by-project) + (erase-buffer) + (let ((marker (point-marker))) + (set-marker-insertion-type marker nil) + (setf (eglot--message-mark proc) marker)) + (read-only-mode t) + (with-current-buffer (eglot-events-buffer proc) + (let ((inhibit-read-only t)) + (insert + (format "\n-----------------------------------\n")))) + (eglot--protocol-initialize proc) + (when interactive + (display-buffer (eglot-events-buffer proc)))))))) + +(defun eglot-quit-server (process &optional sync) + (interactive (list (eglot--current-process))) + (eglot--message "Asking server to terminate") + (eglot--request + process + :shutdown + nil + (lambda (&rest _anything) + (eglot--message "Now asking server to exit") + (process-put process 'eglot--moribund t) + (eglot--process-send process + `(:jsonrpc "2.0" + :method :exit))) + :async-p (not sync) + :timeout-fn (lambda () + (eglot--warn "Brutally deleting existing process %s" + process) + (process-put process 'eglot--moribund t) + (delete-process process)))) + +(defun eglot--process-sentinel (process change) + (with-current-buffer (process-buffer process) + (eglot--debug "Process state changed to %s" change) + (when (not (process-live-p process)) + (cond ((process-get process 'eglot--moribund) + (eglot--message "Process exited with status %s" + (process-exit-status process))) + (t + (eglot--warn "Process unexpectedly changed to %s" change)))))) + +(defun eglot--process-filter (proc string) + (when (buffer-live-p (process-buffer proc)) + (with-current-buffer (process-buffer proc) + (let ((moving (= (point) (process-mark proc))) + (inhibit-read-only t) + (pre-insertion-mark (copy-marker (process-mark proc))) + (expected-bytes (eglot--expected-bytes proc)) + (message-mark (eglot--message-mark proc))) + (save-excursion + ;; Insert the text, advancing the process marker. + (goto-char (process-mark proc)) + (insert string) + (set-marker (process-mark proc) (point))) + (if moving (goto-char (process-mark proc))) + + ;; check for new message header + ;; + (save-excursion + (goto-char pre-insertion-mark) + (let* ((match (search-forward-regexp + "\\(?:.*: .*\r\n\\)*Content-Length: \\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" + (+ (point) 100) + t)) + (new-expected-bytes (and match + (string-to-number (match-string 1))))) + (when new-expected-bytes + (when expected-bytes + (eglot--warn + (concat "Unexpectedly starting new message but %s bytes" + "reportedly remaining from previous one") + expected-bytes)) + (set-marker message-mark (point)) + (setf (eglot--expected-bytes proc) new-expected-bytes) + (setq expected-bytes new-expected-bytes)))) + + ;; check for message body + ;; + (let ((available-bytes (- (position-bytes (process-mark proc)) + (position-bytes message-mark)))) + (cond ((not expected-bytes) + (eglot--warn + "Skipping %s bytes of unexpected garbage from process %s" + available-bytes + proc) + (set-marker message-mark (process-mark proc))) + ((>= available-bytes + expected-bytes) + (let* ((message-end (byte-to-position + (+ (position-bytes message-mark) + expected-bytes)))) + (save-excursion + (save-restriction + (goto-char message-mark) + (narrow-to-region message-mark + message-end) + (eglot--process-receive proc (let ((json-object-type 'plist)) + (json-read))))) + (set-marker message-mark message-end) + (setf (eglot--expected-bytes proc) nil))) + (t + ;; just adding some stuff to the end that doesn't yet + ;; complete the message + ))))))) + +(defun eglot-events-buffer (process &optional interactive) + (interactive (list (eglot--current-process) t)) + (let* ((probe (eglot--events-buffer process)) + (buffer (or (and (buffer-live-p probe) + probe) + (let ((buffer (get-buffer-create + (format "*%s events*" + (process-name process))))) + (with-current-buffer buffer + (buffer-disable-undo) + (read-only-mode t) + (setf (eglot--events-buffer process) + buffer)) + buffer)))) + (when interactive + (pop-to-buffer buffer)) + buffer)) + +(defun eglot--log-event (proc type message) + (with-current-buffer (eglot-events-buffer proc) + (let ((inhibit-read-only t)) + (goto-char (point-max)) + (insert (format "%s: \n%s\n" type (pp-to-string message)))))) + +(defun eglot--process-receive (proc message) + (let ((inhibit-read-only t)) + (insert (format "Server said:\n%s\n" message))) + (eglot--log-event proc 'server message) + ;; Maybe this is a responsee + ;; + (let* ((response-id (plist-get message :id)) + (err (plist-get message :error)) + (continuations (and response-id + (gethash response-id (eglot--continuations))))) + (cond ((and response-id + (not continuations)) + (eglot--warn "Ooops no continuation for id %s" response-id)) + (continuations + (cancel-timer (third continuations)) + (cond (err + (apply (second continuations) err)) + (t + (apply (first continuations) (plist-get message :result))))) + (t + (eglot--debug "No implemetation for notification %s yet" + (plist-get message :method)))))) + +;; (setq json-encoding-pretty-print nil) ; for debug +(defvar eglot--expect-carriage-return nil) + +(defun eglot--process-send (proc message) + (let* ((json (json-encode message)) + (to-send (format "Content-Length: %d\r\n\r\n%s" + (string-bytes json) + json))) + (process-send-string proc to-send) + (eglot--log-event proc 'client message))) + +(defvar eglot--next-request-id 0) + +(defun eglot--next-request-id () + (setq eglot--next-request-id (1+ eglot--next-request-id))) + +(defun eglot--call-with-request (process + async-p + method + params + success-fn + &optional error-fn timeout-fn) + (let* ((id (eglot--next-request-id)) + (timeout-fn (or timeout-fn + (lambda () + (eglot--warn "Tired of waiting for reply to %s" id) + (remhash id (eglot--continuations process))))) + (error-fn (or error-fn + (cl-function + (lambda (&key code message) + (eglot--warn "Request id=%s errored with code=%s: %s" + id code message))))) + (catch-tag (cl-gensym (format "eglot--tag-%d-" id)))) + (eglot--process-send process + `(:jsonrpc "2.0" + :id ,id + :method ,method + :params ,params)) + (catch catch-tag + (puthash id + (list (if async-p + success-fn + (lambda (&rest args) + (throw catch-tag (apply success-fn args)))) + (if async-p + error-fn + (lambda (&rest args) + (throw catch-tag (apply error-fn args)))) + (run-with-timer 5 nil + (if async-p + timeout-fn + (lambda () + (throw catch-tag (apply timeout-fn)))))) + (eglot--continuations process)) + (unless async-p + (while t + (unless (eq (process-status process) 'open) + (eglot--error "Process %s died unexpectedly" process)) + (accept-process-output nil 0.01)))))) + + +(defun eglot--protocol-initialize (process) + (eglot--request + process + :initialize + `(:processId ,(emacs-pid) + :rootPath ,(concat "" ;; FIXME RLS doesn't like "file://" + (expand-file-name (car (project-roots + (project-current))))) + :initializationOptions [] + :capabilities (:workspace (:executeCommand (:dynamicRegistration t)) + :textDocument (:synchronization (:didSave t))) + ) + (lambda (&key capabilities) + (cl-destructuring-bind + (&rest all + &key + _textDocumentSync + _hoverProvider + _completionProvider + _definitionProvider + _referencesProvider + _documentHighlightProvider + _documentSymbolProvider + _workspaceSymbolProvider + _codeActionProvider + _documentFormattingProvider + _documentRangeFormattingProvider + _renameProvider + _executeCommandProvider + ) + capabilities + (message "so yeah I got lots (%d) of capabilities" (length all)))))) + +(defun eglot--debug (format &rest args) + (display-warning 'eglot + (apply #'format format args) + :debug)) + +(defun eglot--error (format &rest args) + (error (apply #'format format args))) + +(defun eglot--message (format &rest args) + (message (concat "[eglot] " (apply #'format format args)))) + +(defun eglot--warn (format &rest args) + (display-warning 'eglot + (apply #'format format args) + :warning)) + +(provide 'eglot) +;;; eglot.el ends here