diff --git a/admin/automerge b/admin/automerge index c7c17dfb5ec..d2c92948e17 100755 --- a/admin/automerge +++ b/admin/automerge @@ -35,18 +35,7 @@ ## it with the -d option in the repository directory, in case a pull ## updates this script while it is working. -set -o nounset - -die () # write error to stderr and exit -{ - [ $# -gt 0 ] && echo "$PN: $*" >&2 - exit 1 -} - -PN=${0##*/} # basename of script -PD=${0%/*} - -[ "$PD" = "$0" ] && PD=. # if PATH includes PWD +source "${0%/*}/emacs-shell-lib" usage () { @@ -129,13 +118,7 @@ OPTIND=1 [ "$test" ] && build=1 -if [ -x "$(command -v mktemp)" ]; then - tempfile=$(mktemp "/tmp/$PN.XXXXXXXXXX") -else - tempfile=/tmp/$PN.$$ -fi - -trap 'rm -f $tempfile 2> /dev/null' EXIT +tempfile="$(emacs_mktemp)" [ -e Makefile ] && [ "$build" ] && { @@ -263,5 +246,3 @@ git push || die "push error" exit 0 - -### automerge ends here diff --git a/admin/diff-tar-files b/admin/diff-tar-files index 6ab39eab2f5..869c9421502 100755 --- a/admin/diff-tar-files +++ b/admin/diff-tar-files @@ -1,4 +1,4 @@ -#! /bin/sh +#!/bin/bash # Copyright (C) 2001-2022 Free Software Foundation, Inc. @@ -17,6 +17,7 @@ # You should have received a copy of the GNU General Public License # along with GNU Emacs. If not, see . +source "${0%/*}/emacs-shell-lib" if [ $# != 2 ]; then cat < $old_tmp tar tf "$new_tar" | sed -e 's,^[^/]*,,' | sort > $new_tmp diff --git a/admin/emacs-shell-lib b/admin/emacs-shell-lib new file mode 100644 index 00000000000..750f81e0577 --- /dev/null +++ b/admin/emacs-shell-lib @@ -0,0 +1,87 @@ +#!/bin/bash +### emacs-shell-lib - shared code for Emacs shell scripts + +## Copyright (C) 2022 Free Software Foundation, Inc. + +## Author: Stefan Kangas + +## 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. + +## 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 GNU Emacs. If not, see . + +### Code: + +# Set an explicit umask. +umask 077 + +# Treat unset variables as an error. +set -o nounset + +# Exit immediately on error. +set -o errexit + +# Avoid non-standard command output from non-C locales. +unset LANG LC_ALL LC_MESSAGES + +PN=${0##*/} # basename of script +PD=${0%/*} # script directory + +[ "$PD" = "$0" ] && PD=. # if PATH includes PWD + +die () # write error to stderr and exit +{ + [ $# -gt 0 ] && echo "$PN: $@" >&2 + exit 1 +} + +emacs_tempfiles=() + +emacs_tempfiles_cleanup () +{ + for file in ${emacs_tempfiles[@]}; do + rm -f "${file}" 2> /dev/null + done +} + +trap ' + ret=$? + emacs_tempfiles_cleanup + exit $ret +' EXIT + +emacs_mktemp () +{ + local readonly file="${1-}" + local tempfile + local prefix + + if [ -z "$file" ]; then + prefix="$PN" + else + prefix="$1" + fi + + if [ -x "$(command -v mktemp)" ]; then + tempfile=$(mktemp "${TMPDIR-/tmp}/${prefix}.XXXXXXXXXX") + else + tempfile="${TMPDIR-/tmp}/${prefix}.$RANDOM$$" + (umask 077 && touch "$tempfile") + fi + + [ -z "${tempfile}" ] && die "Creating temporary file failed" + + emacs_tempfiles+=("${tempfile}") + + echo "$tempfile" +} diff --git a/admin/make-manuals b/admin/make-manuals index cb0c00a423f..a252bf20f1e 100755 --- a/admin/make-manuals +++ b/admin/make-manuals @@ -33,15 +33,7 @@ ### Code: -set -o nounset - -die () # write error to stderr and exit -{ - [ $# -gt 0 ] && echo "$PN: $@" >&2 - exit 1 -} - -PN=${0##*/} # basename of script +source "${0%/*}/emacs-shell-lib" usage () { @@ -96,8 +88,7 @@ OPTIND=1 [ -e admin/admin.el ] || die "admin/admin.el not found" -tempfile=/tmp/$PN.$$ -trap "rm -f $tempfile 2> /dev/null" EXIT +tempfile="$(emacs_mktemp)" [ "$continue" ] || rm -rf $outdir diff --git a/admin/update_autogen b/admin/update_autogen index d1f49d9f25e..55e11be95c7 100755 --- a/admin/update_autogen +++ b/admin/update_autogen @@ -32,18 +32,7 @@ ### Code: -set -o nounset - -die () # write error to stderr and exit -{ - [ $# -gt 0 ] && echo "$PN: $@" >&2 - exit 1 -} - -PN=${0##*/} # basename of script -PD=${0%/*} - -[ "$PD" = "$0" ] && PD=. # if PATH includes PWD +source "${0%/*}/emacs-shell-lib" ## This should be the admin directory. cd $PD || exit @@ -102,10 +91,7 @@ done [ "$basegen" ] || die "internal error" -tempfile=/tmp/$PN.$$ - -trap 'rm -f $tempfile 2> /dev/null' EXIT - +tempfile="$(emacs_mktemp)" while getopts ":hcfqA:CL" option ; do case $option in @@ -312,5 +298,3 @@ commit "loaddefs" $modified || die "commit error" exit 0 - -### update_autogen ends here diff --git a/admin/upload-manuals b/admin/upload-manuals index 50336ee64c0..04f7c3acc72 100755 --- a/admin/upload-manuals +++ b/admin/upload-manuals @@ -36,15 +36,7 @@ ### Code: -set -o nounset - -die () # write error to stderr and exit -{ - [ $# -gt 0 ] && echo "$PN: $@" >&2 - exit 1 -} - -PN=${0##*/} # basename of script +source "${0%/*}/emacs-shell-lib" usage () { diff --git a/doc/emacs/maintaining.texi b/doc/emacs/maintaining.texi index ad4a3ea3506..94171b3a089 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 818deb39415..b5e577d96a4 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. diff --git a/doc/lispref/functions.texi b/doc/lispref/functions.texi index 8b858e0aa01..7ffde7d43d1 100644 --- a/doc/lispref/functions.texi +++ b/doc/lispref/functions.texi @@ -533,6 +533,44 @@ Instead, use the @code{advertised-calling-convention} declaration compiler emit a warning message when it compiles Lisp programs which use the deprecated calling convention. +@cindex computed documentation string +@kindex :documentation +Documentation strings are usually static, but occasionally it can be +necessary to generate them dynamically. In some cases you can do so +by writing a macro which generates at compile time the code of the +function, including the desired documentation string. But you can +also generate the docstring dynamically by writing +@code{(:documentation @var{form})} instead of the documentation +string. This will evaluate @var{form} at run-time when the function +is defined and use it as the documentation string@footnote{This only +works in code using @code{lexical-binding}.}. You can also compute +the documentation string on the fly when it is requested, by setting +the @code{function-documentation} property of the function's symbol to +a Lisp form that evaluates to a string. + +For example: +@example +@group +(defun adder (x) + (lambda (y) + (:documentation (format "Add %S to the argument Y." x)) + (+ x y))) +(defalias 'adder5 (adder 5)) +(documentation 'adder5) + @result{} "Add 5 to the argument Y." +@end group + +@group +(put 'adder5 'function-documentation + '(concat (documentation (symbol-function 'adder5) 'raw) + " Consulted at " (format-time-string "%H:%M:%S"))) +(documentation 'adder5) + @result{} "Add 5 to the argument Y. Consulted at 15:52:13" +(documentation 'adder5) + @result{} "Add 5 to the argument Y. Consulted at 15:52:18" +@end group +@end example + @node Function Names @section Naming a Function @cindex function definition diff --git a/doc/lispref/modes.texi b/doc/lispref/modes.texi index 73c5fcd44d7..434538dcbf8 100644 --- a/doc/lispref/modes.texi +++ b/doc/lispref/modes.texi @@ -1851,7 +1851,9 @@ to enable or disable the buffer-local minor mode @var{mode} in all (or some; see below) buffers. It also executes the @var{body} forms. To turn on the minor mode in a buffer, it uses the function @var{turn-on}; to turn off the minor mode, it calls @var{mode} with -@minus{}1 as argument. +@minus{}1 as argument. (The function @var{turn-on} is a separate +function so it could determine whether to enable the minor mode or not +when it is not a priori clear that it should always be enabled.) Globally enabling the mode also affects buffers subsequently created by visiting files, and buffers that use a major mode other than diff --git a/doc/misc/Makefile.in b/doc/misc/Makefile.in index 1d881a5fc7f..b6eef7ea799 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 \ diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi new file mode 100644 index 00000000000..033464f9909 --- /dev/null +++ b/doc/misc/eglot.texi @@ -0,0 +1,1138 @@ +\input texinfo @c -*-texinfo-*- +@c %**start of header +@setfilename ../../eglot.info +@settitle Eglot: The Emacs Client for the 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 the @dfn{Language Server Protocol} +(@acronym{LSP}). The name ``Eglot'' is an acronym that stands for +@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 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 +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 +Servers}. + +@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 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 Servers +@section Setting Up LSP Servers +@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 variable @code{eglot-server-programs}. + +@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. 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})}}. + +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 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: + +@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. 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{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}. 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 Servers +@section Shutting Down LSP Servers +@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 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 Eglot +management of a buffer entails. + +@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's the list of the main features that 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 a completion package such as @code{company-mode}, a popular +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 +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 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 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, +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, +e.g.@: 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{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}, 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 +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 +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 +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 +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. +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 +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 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 +(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 +language server times out. The value of @code{nil} means never time +out. The default is 30 seconds. + +@item eglot-sync-connect +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 +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{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. + +@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 Servers}. +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 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 +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 + +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: + +@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, 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 Servers}, 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, 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 @code{eglot-@{@}}, 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 diff --git a/etc/NEWS b/etc/NEWS index 4b89e6d52a3..19c90141167 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -1357,6 +1357,16 @@ The default input method for the Tamil language environment is now change the input method's translation rules, customize the user option 'tamil-translation-rules'. +--- +*** New tamil99 input method for the Tamil language. +This supports the keyboard layout specifically designed for the Tamil +language. + +--- +*** New input method 'slovak-qwerty'. +This is a variant of the 'slovak' input method, which corresponds to +the QWERTY Slovak keyboards. + * Changes in Specialized Modes and Packages in Emacs 29.1 diff --git a/etc/PROBLEMS b/etc/PROBLEMS index ed2bc1ae051..aaecc41f6e8 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 diff --git a/lib-src/rcs2log b/lib-src/rcs2log index bc7875cfdd2..2a72404d9e5 100755 --- a/lib-src/rcs2log +++ b/lib-src/rcs2log @@ -209,7 +209,7 @@ month_data=' if type mktemp >/dev/null 2>&1; then logdir=`mktemp -d` else - logdir=$TMPDIR/rcs2log$$ + logdir="${TMPDIR-/tmp}/rcs2log$$" (umask 077 && mkdir "$logdir") fi || exit case $logdir in diff --git a/lisp/emacs-lisp/comp.el b/lisp/emacs-lisp/comp.el index 2c9b79334ba..5a05fe4854b 100644 --- a/lisp/emacs-lisp/comp.el +++ b/lisp/emacs-lisp/comp.el @@ -3928,6 +3928,7 @@ processes from `comp-async-compilations'" "Start compiling files from `comp-files-queue' asynchronously. When compilation is finished, run `native-comp-async-all-done-hook' and display a message." + (cl-assert (null comp-no-spawn)) (if (or comp-files-queue (> (comp-async-runnings) 0)) (unless (>= (comp-async-runnings) (comp-effective-async-max-jobs)) @@ -4048,7 +4049,7 @@ the deferred compilation mechanism." (stringp function-or-file)) (signal 'native-compiler-error (list "Not a function symbol or file" function-or-file))) - (unless comp-no-spawn + (when (or (null comp-no-spawn) comp-async-compilation) (catch 'no-native-compile (let* ((print-symbols-bare t) (data function-or-file) diff --git a/lisp/eshell/esh-util.el b/lisp/eshell/esh-util.el index 9b464a0a137..f47373c115f 100644 --- a/lisp/eshell/esh-util.el +++ b/lisp/eshell/esh-util.el @@ -455,7 +455,7 @@ list." ;; runs while point is in the minibuffer and the users attempt ;; to use completion. Don't ask me. (condition-case nil - (sit-for 0 0) + (sit-for 0) (error nil))) (defun eshell-read-passwd-file (file) diff --git a/lisp/info-look.el b/lisp/info-look.el index ce0a08dcbe6..2eec6f49f5c 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") diff --git a/lisp/leim/quail/indian.el b/lisp/leim/quail/indian.el index 048e16e8d80..31a34bc1de2 100644 --- a/lisp/leim/quail/indian.el +++ b/lisp/leim/quail/indian.el @@ -30,6 +30,8 @@ ;;; Code: +(require 'pcase) +(require 'seq) (require 'quail) (require 'ind-util) @@ -699,6 +701,165 @@ is." "tamil-inscript-digits" "Tamil" "TmlISD" "Tamil keyboard Inscript with Tamil digits support.") +;; Tamil99 input method +;; +;; Tamil99 is a keyboard layout and input method that is specifically +;; designed for the Tamil language. Vowels and vowel modifiers are +;; input with your left hand, and consonants are input with your right +;; hand. See https://en.wikipedia.org/wiki/Tamil_99 +;; +;; தமிழ்99 உள்ளீட்டு முறை +;; +;; தமிழ்99 தமிழுக்கென்றே உருவாக்கப்பட்ட விசைப்பலகை அமைப்பும் உள்ளீட்டு முறையும் +;; ஆகும். உயிர்களை இடக்கையுடனும் மெய்களை வலக்கையுடனும் தட்டச்சிடும்படி +;; அமைக்கப்பட்டது. https://ta.wikipedia.org/wiki/%E0%AE%A4%E0%AE%AE%E0%AE%BF%E0%AE%B4%E0%AF%8D_99 +;; காண்க. + +(quail-define-package + "tamil99" "Tamil" "தமிழ்99" + t "Tamil99 input method" + nil t t t t nil nil nil nil nil t) + +(defconst tamil99-vowels + '(("q" "ஆ") + ("w" "ஈ") + ("e" "ஊ") + ("r" "ஐ") + ("t" "ஏ") + ("a" "அ") + ("s" "இ") + ("d" "உ") + ("g" "எ") + ("z" "ஔ") + ("x" "ஓ") + ("c" "ஒ")) + "Mapping for vowels.") + +(defconst tamil99-vowel-modifiers + '(("q" "ா") + ("w" "ீ") + ("e" "ூ") + ("r" "ை") + ("t" "ே") + ("a" "") + ("s" "ி") + ("d" "ு") + ("g" "ெ") + ("z" "ௌ") + ("x" "ோ") + ("c" "ொ") + ("f" "்")) + "Mapping for vowel modifiers.") + +(defconst tamil99-hard-consonants + '(("h" "க") + ("[" "ச") + ("o" "ட") + ("l" "த") + ("j" "ப") + ("u" "ற")) + "Mapping for hard consonants (வல்லினம்).") + +(defconst tamil99-soft-consonants + '(("b" "ங") + ("]" "ஞ") + ("p" "ண") + (";" "ந") + ("k" "ம") + ("i" "ன")) + "Mapping for soft consonants (மெல்லினம்).") + +(defconst tamil99-medium-consonants + '(("'" "ய") + ("m" "ர") + ("n" "ல") + ("v" "வ") + ("/" "ழ") + ("y" "ள")) + "Mapping for medium consonants (இடையினம்).") + +(defconst tamil99-grantham-consonants + '(("Q" "ஸ") + ("W" "ஷ") + ("E" "ஜ") + ("R" "ஹ")) + "Mapping for grantham consonants (கிரந்தம்).") + +(defconst tamil99-consonants + (append tamil99-hard-consonants + tamil99-soft-consonants + tamil99-medium-consonants + tamil99-grantham-consonants) + "Mapping for all consonants.") + +(defconst tamil99-other + `(("T" ,(vector "க்ஷ")) + ("Y" ,(vector "ஶஂரீ")) + ("O" "[") + ("P" "]") + ("A" "௹") + ("S" "௺") + ("D" "௸") + ("F" "ஃ") + ("K" "\"") + ("L" ":") + (":" ";") + ("\"" "'") + ("Z" "௳") + ("X" "௴") + ("C" "௵") + ("V" "௶") + ("B" "௷") + ("M" "/")) + "Mapping for miscellaneous characters.") + +;; உயிர் +;; vowel +(mapc (pcase-lambda (`(,vowel-key ,vowel)) + (quail-defrule vowel-key vowel)) + tamil99-vowels) + +(mapc (pcase-lambda (`(,consonant-key ,consonant)) + ;; அகர உயிர்மெய் + ;; consonant symbol (consonant combined with the first vowel அ) + (quail-defrule consonant-key consonant) + ;; மெய்யொற்று பின் அகர உயிர்மெய் + ;; pulli on double consonant + (quail-defrule (concat consonant-key consonant-key) + (vector (concat consonant "்" consonant))) + (mapc (pcase-lambda (`(,vowel-key ,vowel-modifier)) + ;; உயிர்மெய் + ;; vowelised consonant + (quail-defrule (concat consonant-key vowel-key) + (vector (concat consonant vowel-modifier))) + ;; மெய்யொற்று பின் பிற உயிர்மெய் + ;; vowelised consonant after double consonant + (quail-defrule (concat consonant-key consonant-key vowel-key) + (vector (concat consonant "்" consonant vowel-modifier)))) + tamil99-vowel-modifiers)) + tamil99-consonants) + +(seq-mapn (pcase-lambda (`(,soft-consonant-key ,soft-consonant) + `(,hard-consonant-key ,hard-consonant)) + ;; மெல்லினம் பின் வல்லினம் + ;; hard consonant after soft consonant + (quail-defrule (concat soft-consonant-key hard-consonant-key) + (vector (concat soft-consonant "்" hard-consonant))) + (mapc (pcase-lambda (`(,vowel-key ,vowel-modifier)) + ;; மெல்லின ஒற்றொட்டிய வல்லினம் பின் உயிர்மெய் + ;; vowelised consonant after soft-hard consonant pair + (quail-defrule (concat soft-consonant-key hard-consonant-key vowel-key) + (vector (concat soft-consonant "்" hard-consonant vowel-modifier)))) + tamil99-vowel-modifiers)) + tamil99-soft-consonants + tamil99-hard-consonants) + +;; பிற வரியுருக்கள் +;; other characters +(mapc (pcase-lambda (`(,key ,translation)) + (quail-defrule key translation)) + tamil99-other) + ;; Probhat Input Method (quail-define-package "bengali-probhat" "Bengali" "BngPB" t diff --git a/lisp/leim/quail/slovak.el b/lisp/leim/quail/slovak.el index acde11d02a7..8ddd92d5b4d 100644 --- a/lisp/leim/quail/slovak.el +++ b/lisp/leim/quail/slovak.el @@ -3,7 +3,8 @@ ;; Copyright (C) 1998, 2001-2022 Free Software Foundation, Inc. ;; Authors: Tibor Šimko -;; Milan Zamazal +;; Milan Zamazal +;; Rudolf Adamkovič ;; Maintainer: Pavel Janík ;; Keywords: i18n, multilingual, input method, Slovak @@ -25,8 +26,9 @@ ;;; Commentary: ;; This file defines the following Slovak keyboards: -;; - standard Slovak keyboard +;; - standard Slovak keyboards, QWERTZ and QWERTY variants ;; - three Slovak keyboards for programmers +;; LocalWords: QWERTZ ;;; Code: @@ -35,7 +37,7 @@ (quail-define-package "slovak" "Slovak" "SK" t - "Standard Slovak keyboard." + "Standard Slovak QWERTZ keyboard." nil t nil nil t nil nil nil nil nil t) (quail-define-rules @@ -154,6 +156,123 @@ ("+0" ?\))) +(quail-define-package + "slovak-qwerty" "Slovak" "SK" t + "Standard Slovak QWERTY keyboard." + nil t nil nil t nil nil nil nil nil t) + +(quail-define-rules + ("1" ?+) + ("2" ?ľ) + ("3" ?š) + ("4" ?č) + ("5" ?ť) + ("6" ?ž) + ("7" ?ý) + ("8" ?á) + ("9" ?í) + ("0" ?é) + ("!" ?1) + ("@" ?2) + ("#" ?3) + ("$" ?4) + ("%" ?5) + ("^" ?6) + ("&" ?7) + ("*" ?8) + ("(" ?9) + (")" ?0) + ("-" ?=) + ("_" ?%) + ("=" ?') + ("[" ?ú) + ("{" ?/) + ("]" ?ä) + ("}" ?\() + ("\\" ?ň) + ("|" ?\)) + (";" ?ô) + (":" ?\") + ("'" ?§) + ("\"" ?!) + ("<" ??) + (">" ?:) + ("/" ?-) + ("?" ?_) + ("`" ?\;) + ("~" ?^) + ("=a" ?á) + ("+a" ?ä) + ("+=a" ?ä) + ("+c" ?č) + ("+d" ?ď) + ("=e" ?é) + ("+e" ?ě) + ("=i" ?í) + ("=l" ?ĺ) + ("+l" ?ľ) + ("+n" ?ň) + ("=o" ?ó) + ("+o" ?ô) + ("~o" ?ô) + ("+=o" ?ö) + ("=r" ?ŕ) + ("+r" ?ř) + ("=s" ?ß) + ("+s" ?š) + ("+t" ?ť) + ("=u" ?ú) + ("+u" ?ů) + ("+=u" ?ü) + ("=y" ?ý) + ("+z" ?ž) + ("=A" ?Á) + ("+A" ?Ä) + ("+=A" ?Ä) + ("+C" ?Č) + ("+D" ?Ď) + ("=E" ?É) + ("+E" ?Ě) + ("=I" ?Í) + ("=L" ?Ĺ) + ("+L" ?Ľ) + ("+N" ?Ň) + ("=O" ?Ó) + ("+O" ?Ô) + ("~O" ?Ô) + ("+=O" ?Ö) + ("=R" ?Ŕ) + ("+R" ?Ř) + ("=S" ?ß) + ("+S" ?Š) + ("+T" ?Ť) + ("=U" ?Ú) + ("+U" ?Ů) + ("+=U" ?Ü) + ("=Y" ?Ý) + ("+Z" ?Ž) + ("=q" ?`) + ("=2" ?@) + ("=3" ?#) + ("=4" ?$) + ("=5" ?%) + ("=6" ?^) + ("=7" ?&) + ("=8" ?*) + ("=9" ?\() + ("=0" ?\)) + ("+1" ?!) + ("+2" ?@) + ("+3" ?#) + ("+4" ?$) + ("+5" ?%) + ("+6" ?^) + ("+7" ?&) + ("+8" ?*) + ("+9" ?\() + ("+0" ?\))) + + (quail-define-package "slovak-prog-1" "Slovak" "SK" t "Slovak (non-standard) keyboard for programmers #1. diff --git a/lisp/menu-bar.el b/lisp/menu-bar.el index 526bccbbac9..849e0f77236 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/net/ldap.el b/lisp/net/ldap.el index 062ff05d69c..ccad8c4edb1 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)) diff --git a/lisp/play/zone.el b/lisp/play/zone.el index 5ea5bbc9267..e3a9507f1cc 100644 --- a/lisp/play/zone.el +++ b/lisp/play/zone.el @@ -139,7 +139,7 @@ run a specific program. The program must be a member of (untabify (point-min) (point-max)) (set-window-start (selected-window) (point-min)) (set-window-point (selected-window) wp) - (sit-for 0 500) + (sit-for 0.500) (let ((ct (and f (frame-parameter f 'cursor-type))) (show-trailing-whitespace nil) restore) @@ -249,7 +249,7 @@ run a specific program. The program must be a member of (while (not (input-pending-p)) (funcall (elt ops (random (length ops)))) (goto-char (point-min)) - (sit-for 0 10)))) + (sit-for 0.01)))) ;;;; whacking chars @@ -262,7 +262,7 @@ run a specific program. The program must be a member of (aset tbl i (+ 48 (random (- 123 48)))) (setq i (1+ i))) (translate-region (point-min) (point-max) tbl) - (sit-for 0 2))))) + (sit-for 0.002))))) (put 'zone-pgm-whack-chars 'wc-tbl (let ((tbl (make-string 128 ?x)) @@ -290,7 +290,7 @@ run a specific program. The program must be a member of (delete-char 1) (insert " "))) (forward-char 1)))) - (sit-for 0 2)))) + (sit-for 0.002)))) (defun zone-pgm-dissolve () (zone-remove-text) diff --git a/lisp/progmodes/cc-engine.el b/lisp/progmodes/cc-engine.el index 596cccdf48e..e71560fa25f 100644 --- a/lisp/progmodes/cc-engine.el +++ b/lisp/progmodes/cc-engine.el @@ -9106,7 +9106,9 @@ multi-line strings (but not C++, for example)." (when (save-excursion (goto-char post-prefix-pos) (looking-at c-self-contained-typename-key)) - (c-add-type pos (point))) + (c-add-type pos (save-excursion + (c-backward-syntactic-ws) + (point)))) (when (and c-record-type-identifiers c-last-identifier-range) (c-record-type-id c-last-identifier-range))) @@ -9191,7 +9193,10 @@ multi-line strings (but not C++, for example)." (goto-char id-end) (if (or res c-promote-possible-types) (progn - (c-add-type id-start id-end) + (c-add-type id-start (save-excursion + (goto-char id-end) + (c-backward-syntactic-ws) + (point))) (when (and c-record-type-identifiers id-range) (c-record-type-id id-range)) (unless res @@ -10762,8 +10767,16 @@ This function might do hidden buffer changes." (setq backup-if-not-cast t) (throw 'at-decl-or-cast t))) - (setq backup-if-not-cast t) - (throw 'at-decl-or-cast t))) + ;; If we're in declaration or template delimiters, or one + ;; of a certain set of characters follows, we've got a + ;; type and variable. + (if (or (memq context '(decl <>)) + (memq (char-after) '(?\; ?, ?= ?\( ?{ ?:))) + (progn + (setq backup-if-not-cast t) + (throw 'at-decl-or-cast t)) + ;; We're probably just typing a statement. + (throw 'at-decl-or-cast nil)))) ;; CASE 4 (when (and got-suffix @@ -10879,8 +10892,13 @@ This function might do hidden buffer changes." ;; CASE 10 (when at-decl-or-cast - ;; By now we've located the type in the declaration that we know - ;; we're in. + ;; By now we've located the type in the declaration that we think + ;; we're in. Do we have enough evidence to promote the putative + ;; type to a found type? The user may be halfway through typing + ;; a statement beginning with an identifier. + (when (and (eq at-type 'maybe) + (not (eq context 'top))) + (setq c-record-type-identifiers nil)) (throw 'at-decl-or-cast t)) ;; CASE 11 @@ -11123,7 +11141,10 @@ This function might do hidden buffer changes." (not (c-on-identifier))))))))) ;; Handle the cast. - (when (and c-record-type-identifiers at-type (not (eq at-type t))) + (when (and c-record-type-identifiers + at-type + (not (memq at-type '(t maybe)))) ; 'maybe isn't strong enough + ; evidence to promote the type. (let ((c-promote-possible-types t)) (goto-char type-start) (c-forward-type))) diff --git a/lisp/progmodes/cc-fonts.el b/lisp/progmodes/cc-fonts.el index b4ff32b9070..aa16da70703 100644 --- a/lisp/progmodes/cc-fonts.el +++ b/lisp/progmodes/cc-fonts.el @@ -1197,8 +1197,21 @@ casts and declarations are fontified. Used on level 2 and higher." ;; arguments lists (i.e. lists enclosed by <...>) is more strict about what ;; characters it allows within the list. (let ((type (and (> match-pos (point-min)) - (c-get-char-property (1- match-pos) 'c-type)))) - (cond ((not (memq (char-before match-pos) '(?\( ?, ?\[ ?< ?{))) + (c-get-char-property (1- match-pos) 'c-type))) + id-pos) + (cond + ;; Are we just after something like "(foo((bar))" ? + ((and (eq (char-before match-pos) ?\)) + (c-go-list-backward match-pos) + (progn + (c-backward-syntactic-ws) + (and (setq id-pos (c-on-identifier)) + (goto-char id-pos) + (progn + (c-backward-syntactic-ws) + (eq (char-before) ?\())))) + (c-get-fontification-context (point) not-front-decl toplev)) + ((not (memq (char-before match-pos) '(?\( ?, ?\[ ?< ?{))) (cons (and toplev 'top) nil)) ;; A control flow expression or a decltype ((and (eq (char-before match-pos) ?\() diff --git a/lisp/progmodes/cc-mode.el b/lisp/progmodes/cc-mode.el index dce300f33c9..2aa6b90dea3 100644 --- a/lisp/progmodes/cc-mode.el +++ b/lisp/progmodes/cc-mode.el @@ -2080,13 +2080,14 @@ with // and /*, not more generic line and block comments." (defun c-update-new-id (end) ;; Note the bounds of any identifier that END is in or just after, in ;; `c-new-id-start' and `c-new-id-end'. Otherwise set these variables to - ;; nil. + ;; nil. Set `c-new-id-is-type' unconditionally to nil. (save-excursion (goto-char end) (let ((id-beg (c-on-identifier))) (setq c-new-id-start id-beg c-new-id-end (and id-beg - (progn (c-end-of-current-token) (point))))))) + (progn (c-end-of-current-token) (point))) + c-new-id-is-type nil)))) (defun c-post-command () ;; If point was inside of a new identifier and no longer is, record that diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el new file mode 100644 index 00000000000..ada8b01fec2 --- /dev/null +++ b/lisp/progmodes/eglot.el @@ -0,0 +1,3461 @@ +;;; eglot.el --- The Emacs Client for LSP servers -*- lexical-binding: t; -*- + +;; Copyright (C) 2018-2022 Free Software Foundation, Inc. + +;; Version: 1.9 +;; Author: João Távora +;; Maintainer: João Távora +;; URL: https://github.com/joaotavora/eglot +;; Keywords: convenience, languages +;; 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 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. + +;; 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. + +;; 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 GNU Emacs. If not, see . + +;;; Commentary: + +;; Eglot ("Emacs Polyglot") is an Emacs LSP client that stays out of +;; your way. +;; +;; 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. +;; +;; 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. +;; +;; * 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. +;; +;; * 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. +;; +;; * 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: + +(require 'imenu) +(require 'cl-lib) +(require 'project) +(require 'url-parse) +(require 'url-util) +(require 'pcase) +(require 'compile) ; for some faces +(require 'warnings) +(require 'flymake) +(require 'xref) +(eval-when-compile + (require 'subr-x)) +(require 'jsonrpc) +(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)) + +;; 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) +(defvar company-tooltip-align-annotations) + + + +;;; User tweakable stuff +(defgroup eglot nil + "Interaction with Language Server Protocol servers." + :prefix "eglot-" + :group 'applications) + +(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. +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)))) + (err (lambda () + (error "None of '%s' are valid executables" + (mapconcat #'car listified ", "))))) + (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 + ;; 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) + when probe return (cons probe args) + finally (funcall err))))))) + +(defvar eglot-server-programs `((rust-mode . ,(eglot-alternatives '("rust-analyzer" "rls"))) + (cmake-mode . ("cmake-language-server")) + (vimrc-mode . ("vim-language-server" "--stdio")) + (python-mode + . ,(eglot-alternatives + '("pylsp" "pyls" ("pyright-langserver" "--stdio") "jedi-language-server"))) + ((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) . ,(eglot-alternatives + '("clangd" "ccls"))) + (((caml-mode :language-id "ocaml") + (tuareg-mode :language-id "ocaml") reason-mode) + . ("ocamllsp")) + (ruby-mode + . ("solargraph" "socket" "--port" :autoport)) + (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" + "languageserver::run()")) + (java-mode . ("jdtls")) + (dart-mode . ("dart" "language-server" + "--client-id" "emacs.eglot-dart")) + (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")) + (yaml-mode . ("yaml-language-server" "--stdio")) + (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")))) + (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 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")) + (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 +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. This association is such that the same resulting + server process will manage buffers of different major 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. + +* 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 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 + experimental LSP servers. INITARGS is a keyword-value plist + 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 + 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-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.") + +(defface eglot-diagnostic-tag-unnecessary-face + '((t (:inherit shadow))) + "Face used to render unused or unnecessary code.") + +(defface eglot-diagnostic-tag-deprecated-face + '((t . (:inherit shadow :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, +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"))) + +(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"))) + +(defcustom eglot-autoshutdown nil + "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 +characters. If 0, don't use an event's buffer at all. If nil, +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"))) + +(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))) + +(defcustom eglot-extend-to-xref nil + "If non-nil, activate Eglot in cross-referenced non-project files." + :type 'boolean) + +(defcustom eglot-menu-string "eglot" + "String displayed in 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.") + +;; Customizable via `completion-category-overrides'. +(when (assoc 'flex completion-styles-alist) + (add-to-list 'completion-category-defaults '(eglot (styles flex basic)))) + + +;;; 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") (19 . "Folder") (20 . "EnumMember") + (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))) + +(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." + (if (>= emacs-major-version 27) (executable-find command remote) + (executable-find command))) + + +;;; Message verification helpers +;;; +(eval-and-compile + (defvar eglot--lsp-interface-alist + `( + (CodeAction (:title) (:kind :diagnostics :edit :command :isPreferred)) + (ConfigurationItem () (:scopeUri :section)) + (Command ((:title . string) (:command . string)) (:arguments)) + (CompletionItem (:label) + (:kind :detail :documentation :deprecated :preselect + :sortText :filterText :insertText :insertTextFormat + :textEdit :additionalTextEdits :commitCharacters + :command :data :tags)) + (Diagnostic (:range :message) (:severity :code :source :relatedInformation :codeDescription :tags)) + (DocumentHighlight (:range) (:kind)) + (FileSystemWatcher (:globPattern) (:kind)) + (Hover (:contents) (:range)) + (InitializeResult (:capabilities) (:serverInfo)) + (Location (:uri :range)) + (LocationLink (:targetUri :targetRange :targetSelectionRange) (:originSelectionRange)) + (LogMessageParams (:type :message)) + (MarkupContent (:kind :value)) + (ParameterInformation (:label) (:documentation)) + (Position (:line :character)) + (Range (:start :end)) + (Registration (:id :method) (:registerOptions)) + (ResponseError (:code :message) (:data)) + (ShowMessageParams (:type :message)) + (ShowMessageRequestParams (:type :message) (:actions)) + (SignatureHelp (:signatures) (:activeSignature :activeParameter)) + (SignatureInformation (:label) (:documentation :parameters :activeParameter)) + (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) ()) + (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 +\"interface\". INTERFACE is a list (REQUIRED OPTIONAL) where +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: + + (Command ((:title . string) (:command . string)) (:arguments))")) + +(eval-and-compile + (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 +performed). + +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 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 symbol `disallow-unknown-methods' is present, Eglot warns +on unknown notifications and errors on unknown requests.")) + +(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--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 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) + (let ((too-many + (and + (memq 'disallow-non-standard-keys eglot-strict-mode) + (cl-set-difference + (eglot--keywordize-vars dspec) + (append required-keys optional-keys)))) + (ignored-required + (and + (memq 'enforce-required-keys eglot-strict-mode) + (cl-set-difference + required-keys (eglot--keywordize-vars dspec)))) + (missing-out + (and + (memq 'enforce-optional-keys eglot-strict-mode) + (cl-set-difference + optional-keys (eglot--keywordize-vars dspec))))) + (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, binding VARS in BODY. +VARS is ([(INTERFACE)] SYMS...) +Honour `eglot-strict-mode'." + (declare (indent 2) (debug (sexp sexp &rest form))) + (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 + (eglot--check-dspec interface-name vars) + `(let ((,object-once ,object)) + (cl-destructuring-bind (&key ,@vars &allow-other-keys) ,object-once + (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))) + (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 1) (debug (sexp &rest form))) + (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'." + (declare (indent 1) (debug (sexp &rest (sexp &rest form)))) + (let ((obj-once (make-symbol "obj-once"))) + `(let ((,obj-once ,obj)) + (cond + ,@(cl-loop + for (vars . body) in clauses + for vars-as-keywords = (eglot--keywordize-vars vars) + for interface-name = (if (consp (car vars)) + (car (pop vars))) + for condition = + (cond (interface-name + (eglot--check-dspec interface-name vars) + ;; In this mode, in runtime, we assume + ;; `eglot-strict-mode' is partially on, otherwise we + ;; can't disambiguate between certain types. + `(ignore-errors + (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 + ;; 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 + ,@body))) + (t + (eglot--error "%S didn't match any of %S" + ,obj-once + ',(mapcar #'car clauses))))))) + + +;;; API (WORK-IN-PROGRESS!) +;;; +(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))) + +(cl-defgeneric eglot-handle-request (server method &rest params) + "Handle SERVER's METHOD request with 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) + "Ask SERVER to execute COMMAND with ARGUMENTS.") + +(cl-defgeneric eglot-initialization-options (server) + "JSON object to send under `initializationOptions'." + (: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." + (: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) + (list + :workspace (list + :applyEdit t + :executeCommand `(:dynamicRegistration :json-false) + :workspaceEdit `(:documentChanges t) + :didChangeWatchedFiles + `(:dynamicRegistration + ,(if (eglot--trampish-p s) :json-false t)) + :symbol `(:dynamicRegistration :json-false) + :configuration t + :workspaceFolders t) + :textDocument + (list + :synchronization (list + :dynamicRegistration :json-false + :willSave t :willSaveWaitUntil t :didSave t) + :completion (list :dynamicRegistration :json-false + :completionItem + `(:snippetSupport + ,(if (eglot--snippet-expansion-fn) + t + :json-false) + :deprecatedSupport t + :tagSupport (:valueSet [1])) + :contextSupport t) + :hover (list :dynamicRegistration :json-false + :contentFormat + (if (fboundp 'gfm-view-mode) + ["markdown" "plaintext"] + ["plaintext"])) + :signatureHelp (list :dynamicRegistration :json-false + :signatureInformation + `(:parameterInformation + (:labelOffsetSupport t) + :activeParameterSupport t)) + :references `(:dynamicRegistration :json-false) + :definition (list :dynamicRegistration :json-false + :linkSupport t) + :declaration (list :dynamicRegistration :json-false + :linkSupport t) + :implementation (list :dynamicRegistration :json-false + :linkSupport t) + :typeDefinition (list :dynamicRegistration :json-false + :linkSupport t) + :documentSymbol (list + :dynamicRegistration :json-false + :hierarchicalDocumentSymbolSupport t + :symbolKind `(:valueSet + [,@(mapcar + #'car eglot--symbol-kind-names)])) + :documentHighlight `(:dynamicRegistration :json-false) + :codeAction (list + :dynamicRegistration :json-false + :codeActionLiteralSupport + '(:codeActionKind + (:valueSet + ["quickfix" + "refactor" "refactor.extract" + "refactor.inline" "refactor.rewrite" + "source" "source.organizeImports"])) + :isPreferredSupport t) + :formatting `(:dynamicRegistration :json-false) + :rangeFormatting `(:dynamicRegistration :json-false) + :rename `(:dynamicRegistration :json-false) + :publishDiagnostics (list :relatedInformation :json-false + ;; TODO: We can support :codeDescription after + ;; adding an appropriate UI to + ;; Flymake. + :codeDescriptionSupport :json-false + :tagSupport + `(:valueSet + [,@(mapcar + #'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." + :accessor eglot--project-nickname + :reader eglot-project-nickname) + (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) + (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) + (project + :documentation "Project associated with server." + :accessor eglot--project) + (spinner + :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 + :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." + :accessor eglot--managed-buffers) + (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.") + +(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) + "Keys are projects. Values are lists of processes.") + +(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. +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." + (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 + (setf (eglot--shutdown-requested server) t) + (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))))) + +(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. + (dolist (buffer (eglot--managed-buffers server)) + (let (;; Avoid duplicate shutdowns (github#389) + (eglot-autoshutdown nil)) + (eglot--when-live-buffer buffer (eglot--managed-mode-off)))) + ;; Kill any expensive watches + (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 + (gethash (eglot--project server) eglot--servers-by-project))) + (cond ((eglot--shutdown-requested server) + t) + ((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 one didn't last long.")))) + +(defun eglot--all-major-modes () + "Return all known major modes." + (let ((retval)) + (mapatoms (lambda (sym) + (when (plist-member (symbol-plist sym) 'derived-mode-parent) + (push sym retval)))) + retval)) + +(defvar eglot--command-history nil + "History of CONTACT arguments to `eglot'.") + +(defun eglot--lookup-mode (mode) + "Lookup `eglot-server-programs' for 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) + (eglot--ensure-list spec) + (and (provided-mode-derived-p mode probe) + (list + mode-symbols + (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 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)) + (main-mode + (cond + ((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))) + (triplet (eglot--lookup-mode main-mode)) + (managed-modes (car triplet)) + (language-id (or (cadr triplet) + (string-remove-suffix "-mode" (symbol-name guessed-mode)))) + (guess (caddr triplet)) + (guess (if (functionp guess) + (funcall guess interactive) + 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)) + ;; 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 :): ")) + (full-program-invocation + (and program + (cl-every #'stringp guess) + (combine-and-quote-strings guess))) + (prompt + (and base-prompt + (cond (current-prefix-arg base-prompt) + ((null guess) + (format "[eglot] Sorry, couldn't guess for `%s'!\n%s" + main-mode base-prompt)) + ((and program + (not (file-name-absolute-p program)) + (not (eglot--executable-find program t))) + (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 + (split-string-and-unquote + (read-shell-command + prompt + full-program-invocation + 'eglot-command-history))) + guess))) + (list managed-modes (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.") + +(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 +`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 + &optional interactive) + "Start LSP server in support of PROJECT's buffers under MANAGED-MAJOR-MODE. + +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 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, 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'. + +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 +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)) + (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? ")) + (eglot-reconnect current-server interactive) + (when live-p (ignore-errors (eglot-shutdown current-server))) + (eglot--connect managed-major-mode project class contact language-id)))) + +(defun eglot-reconnect (server &optional interactive) + "Reconnect to SERVER. +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-modes server) + (eglot--project server) + (eieio-object-class-name server) + (eglot--saved-initargs server) + (eglot--language-id server)) + (eglot--message "Reconnected!")) + +(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))) + (cl-labels + ((maybe-connect + () + (remove-hook 'post-command-hook #'maybe-connect nil) + (eglot--when-live-buffer buffer + (unless eglot--managed-mode + (apply #'eglot--connect (eglot--guess-contact)))))) + (when buffer-file-name + (add-hook 'post-command-hook #'maybe-connect 'append nil))))) + +(defun eglot-events-buffer (server) + "Display events buffer for SERVER. +Use current server's or first available Eglot events buffer." + (interactive (list (eglot-current-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." + (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 (list (eglot--current-server-or-lose))) + (jsonrpc-forget-pending-continuations server)) + +(defvar eglot-connect-hook + '(eglot-signal-didChangeConfiguration) + "Hook run after connecting in `eglot--connect'.") + +(defvar eglot-server-initialized-hook + '() + "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--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)) + +(defvar-local eglot--cached-server nil + "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. +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-modes)) + 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 + readable-name + contact))) + (setq autostart-inferior-process inferior) + connection)))) + ((stringp (car contact)) + (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)))))) + (server + (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 + 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) + (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) + ;; 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 + (condition-case _quit + (let ((retval + (catch tag + (jsonrpc-async-request + server + :initialize + (list :processId + (unless (or eglot-withhold-process-id + (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 (file-local-name + (expand-file-name default-directory)) + :rootUri (eglot--path-to-uri default-directory) + :initializationOptions (eglot-initialization-options + server) + :capabilities (eglot-client-capabilities server) + :workspaceFolders (eglot-workspace-folders server)) + :success-fn + (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 eglot--{}) + (dolist (buffer (buffer-list)) + (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 found (and cached) from + ;; `eglot--maybe-activate-editing-mode' in any + ;; managed buffer. + (eglot--maybe-activate-editing-mode))) + (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))))))) + (let ((default-directory (project-root project)) + (major-mode (car managed-modes))) + (hack-dir-local-variables-non-file-buffer) + (run-hook-with-args 'eglot-connect-hook server)) + (eglot--message + "Connected! Server `%s' now managing `%s' buffers \ +in project `%s'." + (or (plist-get serverInfo :name) + (jsonrpc-name server)) + managed-modes + (eglot-project-nickname server)) + (when tag (throw tag t)))) + :timeout eglot-connect-timeout + :error-fn (eglot--lambda ((ResponseError) code message) + (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 after %s seconds" + eglot-connect-timeout))) + (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 eglot-connect-timeout))))))) + (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. +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) + :noquery t + :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?) +;;; +(defun eglot--error (format &rest args) + "Error out with FORMAT with ARGS." + (error "[eglot] %s" (apply #'format format args))) + +(defun eglot--message (format &rest args) + "Message out with FORMAT with ARGS." + (message "[eglot] %s" (apply #'format format args))) + +(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) :warning))) + +(defun eglot-current-column () (- (point) (line-beginning-position))) + +(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' (the default), and +`eglot-current-column' for all others.") + +(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)) + ;; Fix github#860 + (min (point) (point-max)) '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 (progn (when pos (goto-char pos)) + (funcall eglot-current-column-function))))) + +(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 +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'. 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.") + +(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." + (save-restriction + (cl-loop + with lbp = (line-beginning-position) + initially + (narrow-to-region lbp (line-end-position)) + (move-to-column column) + for diff = (- column + (eglot-lsp-abiding-column lbp)) + 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. +If optional MARKER, return a marker instead" + (save-excursion + (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))))) + +(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." + (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* ((server (eglot-current-server)) + (remote-prefix (and server (eglot--trampish-p server))) + (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) + (cl-plusp (length retval))) + (substring retval 1) + retval))) + (concat remote-prefix normalized))) + +(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)) + +(defun eglot--format-markup (markup) + "Format MARKUP according to LSP's spec." + (pcase-let ((`(,string ,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 + (setq-local markdown-fontify-code-blocks-natively t) + (insert string) + (let ((inhibit-message t) + (message-log-max nil)) + (ignore-errors (delay-mode-hooks (funcall mode)))) + (font-lock-ensure) + (string-trim (buffer-string))))) + +(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 +under cursor." + :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." + (unless (cl-some (lambda (feat) + (memq feat eglot-ignored-server-capabilities)) + feats) + (cl-loop for caps = (eglot--capabilities (eglot--current-server-or-lose)) + then (cadr probe) + 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 (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. +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))) + (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-modes 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))))) + +(defun eglot--trampish-p (server) + "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)) + +(defun eglot--ensure-list (x) (if (listp x) x (list x))) + + +;;; Minor modes +;;; +(defvar eglot-mode-map + (let ((map (make-sparse-keymap))) + (define-key map [remap display-local-help] #'eldoc-doc-buffer) + map)) + +(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'.") + +(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 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 +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 an element to +this list and Eglot will refrain from setting it. + +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." + (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) + `(unless (or (not (boundp ',symbol)) (eglot--stay-out-of-p ',symbol)) + (push (cons ',symbol (symbol-value ',symbol)) eglot--saved-bindings) + (setq-local ,symbol ,binding))) + +(defun eglot-managed-p () + "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. +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." + :init-value nil :lighter nil :keymap eglot-mode-map + (cond + (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-off nil t) + ;; 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) + (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) + (add-hook 'pre-command-hook 'eglot--pre-command-hook nil t) + (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)) + (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 (local 'imenu-create-index-function) + #'eglot-imenu)) + (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) + (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) + (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 '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)) + (remove-function (local 'imenu-create-index-function) #'eglot-imenu) + (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 + (setf (eglot--managed-buffers server) + (delq (current-buffer) (eglot--managed-buffers server))) + (when (and eglot-autoshutdown + (null (eglot--managed-buffers 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." + (eglot--managed-mode -1)) + +(defun eglot-current-server () + "Return logical Eglot server for current buffer, nil if none." + (setq eglot--cached-server + (or eglot--cached-server + (cl-find major-mode + (gethash (eglot--current-project) eglot--servers-by-project) + :key #'eglot--major-modes + :test #'memq) + (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-current-server) + (jsonrpc-error "No current JSON-RPC connection"))) + +(defvar-local eglot--diagnostics nil + "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 () + "Maybe activate `eglot--managed-mode'. + +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)) + (setq eglot--diagnostics 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) + +(defun eglot-clear-status (server) + "Clear the last JSONRPC error for SERVER." + (interactive (list (eglot--current-server-or-lose))) + (setf (jsonrpc-last-error server) nil)) + + +;;; Mode-line, menu and other sugar +;;; +(defvar eglot--mode-line-format `(:eval (eglot--mode-line-format))) + +(put 'eglot--mode-line-format 'risky-local-variable t) + +(defun eglot--mouse-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) + (force-mode-line-update t)))))) + +(defun eglot-manual () "Open on-line documentation." + (interactive) (browse-url "https://github.com/joaotavora/eglot#readme")) + +(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" + ["Reconnect to server" eglot-reconnect] + ["Quit server" eglot-shutdown] + "--" + ["LSP events buffer" eglot-events-buffer] + ["Server stderr buffer" eglot-stderr-buffer] + ["Customize event buffer size" + (lambda () + (interactive) + (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." + (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)) + 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 Eglot's mode-line." + (pcase-let* ((server (eglot-current-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))) + (last-error (and server (jsonrpc-last-error server)))) + (append + `(,(propertize + eglot-menu-string + 'face 'eglot-mode-line + 'mouse-face 'mode-line-highlight + '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))) + (when nick + `(":" + ,(propertize + nick + 'face 'eglot-mode-line + 'mouse-face 'mode-line-highlight + '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-server-menu) + map)) + ,@(when last-error + `("/" ,(eglot--mode-line-props + "error" 'compilation-mode-line-fail + '((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)) + `("/" ,(eglot--mode-line-props doing + 'compilation-mode-line-run '()))) + ,@(when (cl-plusp pending) + `("/" ,(eglot--mode-line-props + (format "%d" pending) 'warning + '((mouse-3 eglot-forget-pending-continuations + "Forget pending continuations")) + "Number of outgoing, \ +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) + +(defalias 'eglot--make-diag 'flymake-make-diagnostic) +(defalias 'eglot--diag-data 'flymake-diagnostic-data) + +(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) +;;; +(cl-defmethod eglot-handle-notification + (_server method &key &allow-other-keys) + "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." + (when (memq 'disallow-unknown-methods eglot-strict-mode) + (jsonrpc-error "Unknown request method `%s'" method))) + +(cl-defmethod eglot-execute-command + (server command 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) + "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." + (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 + (_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) + "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." + (cl-flet ((eglot--diag-type (sev) + (cond ((null sev) 'eglot-error) + ((<= sev 1) 'eglot-error) + ((= sev 2) 'eglot-warning) + (t 'eglot-note))) + (mess (source 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 + for diag-spec across diagnostics + collect (eglot--dbind ((Diagnostic) range code message severity source tags) + diag-spec + (setq message (mess source code 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 + (line-beginning-position + (1+ (plist-get (plist-get range :start) :line)))) + (setq end + (line-end-position + (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)) + (when-let ((faces + (cl-loop for tag across tags + when (alist-get tag eglot--tag-faces) + collect it))) + `((face . ,faces)))))) + into diags + 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))))) + (cl-loop + with path = (expand-file-name (eglot--uri-to-path uri)) + for diag-spec across diagnostics + 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)))) + (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'. +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 (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) + "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." + (eglot--register-unregister server unregisterations 'unregister)) + +(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) + `(:applied t)) + +(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 + (ignore-errors + (buffer-file-name + (buffer-base-buffer))))))) + +(defvar-local eglot--versioned-identifier 0) + +(defun eglot--VersionedTextDocumentIdentifier () + "Compute VersionedTextDocumentIdentifier object for current buffer." + (append (eglot--TextDocumentIdentifier) + `(:version ,eglot--versioned-identifier))) + +(defun eglot--TextDocumentItem () + "Compute TextDocumentItem object for current buffer." + (append + (eglot--VersionedTextDocumentIdentifier) + (list :languageId + (eglot--language-id (eglot--current-server-or-lose)) + :text + (eglot--widening + (buffer-substring-no-properties (point-min) (point-max)))))) + +(defun eglot--TextDocumentPositionParams () + "Compute TextDocumentPositionParams." + (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', maybe call on-type-formatting." + (setq eglot--last-inserted-char last-input-event) + (let ((ot-provider (eglot--server-capable :documentOnTypeFormattingProvider))) + (when (and ot-provider + (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)))) + +(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 () + (append + (eglot--TextDocumentPositionParams) + `(:context + ,(if-let (trigger (and (characterp eglot--last-inserted-char) + (cl-find eglot--last-inserted-char + (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'.") + +(cl-defmethod jsonrpc-connection-ready-p ((_server eglot-lsp-server) _what) + "Tell if SERVER is ready for WHAT in current buffer." + (and (cl-call-next-method) (not eglot--recent-changes))) + +(defvar-local eglot--change-idle-timer nil "Idle timer for didChange signals.") + +(defun eglot--before-change (beg end) + "Hook onto `before-change-functions' with BEG and END." + (when (listp eglot--recent-changes) + ;; 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 nil)) + (,end . ,(copy-marker end t))) + eglot--recent-changes))) + +(defun eglot--after-change (beg end pre-change-length) + "Hook onto `after-change-functions'. +Records BEG, END and PRE-CHANGE-LENGTH locally." + (cl-incf eglot--versioned-identifier) + (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 and github#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. + (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))) + (setq eglot--change-idle-timer + (run-with-idle-timer + eglot-send-changes-idle-time + nil (lambda () (eglot--when-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 +;; chance to run, because `jsonrpc-connection-ready-p'. +(advice-add #'jsonrpc-request :before + (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))) + +(defvar-local eglot-workspace-configuration () + "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 object of the +format described above.") + +;;;###autoload +(put 'eglot-workspace-configuration 'safe-local-variable 'listp) + +(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)) + (with-no-warnings + (require 'json) + (when (require 'json-mode nil t) (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) + eglot-workspace-configuration)) + +(defun eglot--workspace-configuration-plist (server) + "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))) + +(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 + (or (eglot--workspace-configuration-plist server) + eglot--{})))) + +(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) + (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)) + (file-name-as-directory uri-path) + (project-root (eglot--project 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) + by #'cddr + when (string= + (if (keywordp wsection) + (substring (symbol-name wsection) 1) + wsection) + section) + return o)))) + items))) + +(defun eglot--signal-textDocument/didChange () + "Send textDocument/didChange to server." + (when eglot--recent-changes + (let* ((server (eglot--current-server-or-lose)) + (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 + 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) + ;; 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) + (setf (eglot--spinner server) (list nil :textDocument/didChange t)) + (jsonrpc--call-deferred server)))) + +(defun eglot--signal-textDocument/didOpen () + "Send textDocument/didOpen to server." + (setq eglot--recent-changes nil eglot--versioned-identifier 0) + (jsonrpc-notify + (eglot--current-server-or-lose) + :textDocument/didOpen `(:textDocument ,(eglot--TextDocumentItem)))) + +(defun eglot--signal-textDocument/didClose () + "Send textDocument/didClose to server." + (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." + (let ((server (eglot--current-server-or-lose)) + (params `(:reason 1 :textDocument ,(eglot--TextDocumentIdentifier)))) + (when (eglot--server-capable :textDocumentSync :willSave) + (jsonrpc-notify server :textDocument/willSave params)) + (when (eglot--server-capable :textDocumentSync :willSaveWaitUntil) + (ignore-errors + (eglot--apply-text-edits + (jsonrpc-request server :textDocument/willSaveWaitUntil params + :timeout 0.5)))))) + +(defun eglot--signal-textDocument/didSave () + "Send textDocument/didSave to server." + (eglot--signal-textDocument/didChange) + (jsonrpc-notify + (eglot--current-server-or-lose) + :textDocument/didSave + (list + ;; TODO: Handle TextDocumentSaveRegistrationOptions to control this. + :text (buffer-substring-no-properties (point-min) (point-max)) + :textDocument (eglot--TextDocumentIdentifier)))) + +(defun eglot-flymake-backend (report-fn &rest _more) + "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')." + (cond (eglot--managed-mode + (setq eglot--current-flymake-report-fn report-fn) + (eglot--report-to-flymake eglot--diagnostics)) + (t + (funcall report-fn nil)))) + +(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--diagnostics diags)) + +(defun eglot-xref-backend () "Eglot xref backend." 'eglot) + +(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.") + +(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) + (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)))) + +(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)) + (visiting (or (find-buffer-visiting file) + (gethash uri eglot--temp-location-buffers))) + (collect (lambda () + (eglot--widening + (pcase-let* ((`(,beg . ,end) (eglot--range-region range)) + (bol (progn (goto-char beg) (line-beginning-position))) + (substring (buffer-substring bol (line-end-position))) + (hi-beg (- beg bol)) + (hi-end (- (min (line-end-position) end) bol))) + (add-face-text-property hi-beg hi-end 'xref-match + t substring) + (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))) + ((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)) + (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))))))) + (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))) + +(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))) + "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." + (catch 'found + (maphash (lambda (_k v) + (while (consp v) + ;; Like mess? Ask minibuffer.el about improper lists. + (when (equal (car v) string) (throw 'found (car v))) + (setq 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))) + (let ((attempt + (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 + "`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)) + (let ((response + (jsonrpc-request + (eglot--current-server-or-lose) + method (append (eglot--TextDocumentPositionParams) extra-params)))) + (eglot--collecting-xrefs (collect) + (mapc + (lambda (loc-or-loc-link) + (let ((sym-name (symbol-name (symbol-at-point)))) + (eglot--dcase loc-or-loc-link + (((LocationLink) targetUri targetSelectionRange) + (collect (eglot--xref-make-match sym-name + targetUri targetSelectionRange))) + (((Location) uri range) + (collect (eglot--xref-make-match sym-name + uri range)))))) + (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." + (let ((eglot--lsp-xref-refs (eglot--lsp-xrefs-for-method + method + :extra-params extra-params + :capability capability))) + (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." + (interactive) + (eglot--lsp-xref-helper :textDocument/declaration)) + +(defun eglot-find-implementation () + "Find implementation for SYM, the identifier at point." + (interactive) + (eglot--lsp-xref-helper :textDocument/implementation)) + +(defun eglot-find-typeDefinition () + "Find type definition for SYM, the identifier at point." + (interactive) + (eglot--lsp-xref-helper :textDocument/typeDefinition)) + +(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 + 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) + (eglot--collecting-xrefs (collect) + (mapc + (eglot--lambda ((SymbolInformation) name location) + (eglot--dbind ((Location) uri range) location + (collect (eglot--xref-make-match name uri range)))) + (jsonrpc-request (eglot--current-server-or-lose) + :workspace/symbol + `(:query ,pattern)))))) + +(defun eglot-format-buffer () + "Format contents of current buffer." + (interactive) + (eglot-format nil nil)) + +(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. + +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 + ((and beg on-type-format) + `(:textDocument/onTypeFormatting + :documentOnTypeFormattingProvider + ,`(:position ,(eglot--pos-to-lsp-position beg) + :ch ,(string on-type-format)))) + ((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) + :insertFinalNewline (if require-final-newline t :json-false) + :trimFinalNewlines (if delete-trailing-lines t :json-false)) + args) + :deferred method)))) + +(defun eglot-completion-at-point () + "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)) + (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 (category . eglot) + (display-sort-function . ,sort-completions))) + resp items (cached-proxies :none) + (proxies + (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)) + ((and insertText + (not (string-empty-p insertText))) + insertText) + (t + (string-trim-left label))))) + (unless (zerop (length proxy)) + (put-text-property 0 1 'eglot--lsp-item item proxy)) + proxy)) + items))))) + (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) + (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)) + (or (cdr bounds) (point)) + (lambda (probe pred action) + (cond + ((eq action 'metadata) metadata) ; metadata + ((eq action 'lambda) ; test-completion + (test-completion probe (funcall proxies))) + ((eq (car-safe action) 'boundaries) nil) ; boundaries + ((null action) ; try-completion + (try-completion probe (funcall proxies))) + ((eq action t) ; all-completions + (all-completions + "" + (funcall proxies) + (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)))))))) + :annotation-function + (lambda (proxy) + (eglot--dbind ((CompletionItem) detail kind) + (get-text-property 0 'eglot--lsp-item proxy) + (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)))))) + :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-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) + (eq t (plist-get lsp-item :deprecated))))) + :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 + (let ((lsp-comp (get-text-property 0 'eglot--lsp-item proxy))) + (plist-get (funcall resolve-maybe lsp-comp) :documentation))) + (formatted (and documentation + (eglot--format-markup documentation)))) + (when formatted + (with-current-buffer (get-buffer-create " *eglot doc*") + (erase-buffer) + (insert formatted) + (current-buffer))))) + :company-require-match 'never + :company-prefix-length + (save-excursion + (when (car bounds) (goto-char (car bounds))) + (when (listp completion-capability) + (looking-back + (regexp-opt + (cl-coerce (cl-getf completion-capability :triggerCharacters) 'list)) + (line-beginning-position)))) + :exit-function + (lambda (proxy status) + (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 + ;; 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)))) + (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)))) + (when (cl-plusp (length additionalTextEdits)) + (eglot--apply-text-edits additionalTextEdits))) + (eglot--signal-textDocument/didChange) + (eldoc))))))))) + +(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 + concat + (eglot--dbind ((SignatureInformation) label documentation parameters activeParameter) sig + (with-temp-buffer + (save-excursion (insert label)) + (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)) + (add-face-text-property (match-beginning 1) (match-end 1) + 'font-lock-function-name-face)) + (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 ": " (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) + (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))))) + (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)) + (insert "\n" + (propertize + (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")) + +(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-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)) + +(defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") + +(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 'eglot-highlight-symbol-face) + (overlay-put ov 'modification-hooks + `(,(lambda (o &rest _) (delete-overlay o)))) + ov))) + highlights)))) + :deferred :textDocument/documentHighlight) + nil))) + +(defun eglot-imenu () + "Eglot's `imenu-create-index-function'. +Returns a list as described in docstring of `imenu--index-alist'." + (cl-labels + ((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) + (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)))) + (seq-group-by + (lambda (obj) (plist-get obj :kind)) + (mapcan #'unfurl + (jsonrpc-request (eglot--current-server-or-lose) + :textDocument/documentSymbol + `(:textDocument + ,(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." + (unless (or (not version) (equal version eglot--versioned-identifier)) + (jsonrpc-error "Edits on `%s' require version %d, you have %d" + (current-buffer) version eglot--versioned-identifier)) + (atomic-change-group + (let* ((change-group (prepare-change-group)) + (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 + (save-excursion + (save-restriction + (narrow-to-region beg end) + + ;; 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)) + (beg (marker-position beg)) + (end (marker-position end))) + (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 (eglot--lambda ((TextEdit) range newText) + (cons newText (eglot--range-region range 'markers))) + (reverse edits))) + (undo-amalgamate-change-group change-group) + (progress-reporter-done reporter)))) + +(defun eglot--apply-workspace-edit (wedit &optional confirm) + "Apply the workspace edit WEDIT. If CONFIRM, ask user first." + (eglot--dbind ((WorkspaceEdit) changes documentChanges) wedit + (let ((prepared + (mapcar (eglot--lambda ((TextDocumentEdit) textDocument edits) + (eglot--dbind ((VersionedTextDocumentIdentifier) uri version) + textDocument + (list (eglot--uri-to-path uri) edits version))) + documentChanges))) + (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))) + (unless (y-or-n-p + (format "[eglot] Server wants to edit:\n %s\n Proceed? " + (mapconcat #'identity (mapcar #'car prepared) "\n "))) + (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) + (eglot--apply-text-edits edits version)) + finally (eldoc) (eglot--message "Edit successful!"))))) + +(defun eglot-rename (newname) + "Rename the current symbol to NEWNAME." + (interactive + (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 + (jsonrpc-request (eglot--current-server-or-lose) + :textDocument/rename `(,@(eglot--TextDocumentPositionParams) + :newName ,newname)) + current-prefix-arg)) + +(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 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 +at point. With prefix argument, prompt for ACTION-KIND." + (interactive + `(,@(eglot--region-bounds) + ,(and current-prefix-arg + (completing-read "[eglot] Action kind: " + '("quickfix" "refactor.extract" "refactor.inline" + "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 + (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 + [,@(cl-loop for diag in (flymake-diagnostics beg end) + when (cdr (assoc 'eglot-lsp-diag + (eglot--diag-data diag))) + collect it)] + ,@(when action-kind `(:only [,action-kind])))) + :deferred t)) + ;; 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"))))) + (preferred-action (cl-find-if + (lambda (menu-item) + (plist-get (cdr menu-item) :isPreferred)) + menu-items)) + (default-action (car (or preferred-action (car 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:" + ("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 chosen + (((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))))))) + +(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 +;;; +(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) + (eglot--glob-compile globPattern t t)) + watchers)) + (dirs-to-watch + (delete-dups (mapcar #'file-name-directory + (project-files + (eglot--project server)))))) + (cl-labels + ((handle-event + (event) + (pcase-let ((`(,desc ,action ,file ,file1) event)) + (cond + ((and (memq action '(created changed deleted)) + (cl-find file globs :test (lambda (f g) (funcall g f)))) + (jsonrpc-notify + server :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 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 dirs-to-watch) (length watchers))))) + (unless success + (eglot-unregister-capability server method 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)) + (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-{}) + (: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-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 + 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 (re-search-forward ,(concat "\\=" (regexp-opt alternatives)) 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))) + + +;;; List connections mode + +(define-derived-mode eglot-list-connections-mode tabulated-list-mode + "" "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)) + +(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 +;;; +;; 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 +;;; + +(make-obsolete-variable 'eglot--managed-mode-hook + '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" +;; checkdoc-force-docstrings-flag: nil +;; End: + +;;; eglot.el ends here diff --git a/lisp/progmodes/modula2.el b/lisp/progmodes/modula2.el index e668570ba17..09cb848fd52 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." diff --git a/lisp/progmodes/perl-mode.el b/lisp/progmodes/perl-mode.el index 7b7a2cdf019..c5d5d703fc9 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 <display); #endif int dx, dy; + + /* Avoid warnings when SAFE_ALLOCA is not actually used. */ +#if defined HAVE_XINPUT2 || defined HAVE_XKB || defined HAVE_X_I18N USE_SAFE_ALLOCA; +#endif /* This function is not reentrant, so input should be blocked before it is called. */ @@ -24220,7 +24224,10 @@ handle_one_xevent (struct x_display_info *dpyinfo, count++; } +#if defined HAVE_XINPUT2 || defined HAVE_XKB || defined HAVE_X_I18N SAFE_FREE (); +#endif + return count; } @@ -25691,6 +25698,14 @@ x_new_font (struct frame *f, Lisp_Object font_object, int fontset) #ifdef HAVE_X11R6 +/* HAVE_X11R6 means Xlib conforms to the R6 specification or later. + HAVE_X11R6_XIM, OTOH, means that Emacs should try to use R6-style + callback driven input method initialization. They are separate + because Sun apparently ships buggy Xlib with some versions of + Solaris... */ + +#ifdef HAVE_X11R6_XIM + /* If preedit text is set on F, cancel preedit, free the text, and generate the appropriate events to cancel the preedit display. @@ -25756,6 +25771,8 @@ xim_destroy_callback (XIM xim, XPointer client_data, XPointer call_data) unblock_input (); } +#endif + #endif /* HAVE_X11R6 */ /* Open the connection to the XIM server on display DPYINFO. @@ -27117,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 @@ -27124,6 +27199,7 @@ x_ewmh_activate_frame (struct frame *f) { XEvent msg; struct x_display_info *dpyinfo; + Time time; dpyinfo = FRAME_DISPLAY_INFO (f); @@ -27144,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); @@ -30281,7 +30394,7 @@ mark_xterm (void) { Lisp_Object val; #if defined HAVE_XINPUT2 || defined USE_TOOLKIT_SCROLL_BARS \ - || defined HAVE_XRANDR || defined USE_GTK + || defined HAVE_XRANDR || defined USE_GTK || defined HAVE_X_I18N struct x_display_info *dpyinfo; #if defined HAVE_XINPUT2 || defined USE_TOOLKIT_SCROLL_BARS int i; @@ -30505,8 +30618,14 @@ x_get_keyboard_modifiers (struct x_display_info *dpyinfo) /* This sometimes happens when the function is called during display initialization, which can happen while obtaining vendor specific keysyms. */ + +#ifdef HAVE_XKB if (!dpyinfo->xkb_desc && !dpyinfo->modmap) x_find_modifier_meanings (dpyinfo); +#else + if (!dpyinfo->modmap) + x_find_modifier_meanings (dpyinfo); +#endif return list5 (make_uint (dpyinfo->hyper_mod_mask), make_uint (dpyinfo->super_mod_mask), @@ -30626,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. @@ -30879,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; } diff --git a/test/lisp/image/wallpaper-tests.el b/test/lisp/image/wallpaper-tests.el index cb6818f8c1b..a5d3343bd4d 100644 --- a/test/lisp/image/wallpaper-tests.el +++ b/test/lisp/image/wallpaper-tests.el @@ -99,9 +99,12 @@ ("touch" "touch" fil :init-action (lambda () (setq called t))))) (wallpaper-command (wallpaper--find-command)) - (wallpaper-command-args (wallpaper--find-command-args))) + (wallpaper-command-args (wallpaper--find-command-args)) + process) (should (functionp (wallpaper-setter-init-action wallpaper--current-setter))) - (wallpaper-set fil-jpg) + (setq process (wallpaper-set fil-jpg)) + ;; Wait for "touch" process to exit so temp file is removed. + (accept-process-output process 3) (should called))))) (ert-deftest wallpaper-set/calls-wallpaper-set-function () diff --git a/test/lisp/progmodes/cperl-mode-resources/here-docs.pl b/test/lisp/progmodes/cperl-mode-resources/here-docs.pl index bb3d4871a91..13d879bf762 100644 --- a/test/lisp/progmodes/cperl-mode-resources/here-docs.pl +++ b/test/lisp/progmodes/cperl-mode-resources/here-docs.pl @@ -140,4 +140,70 @@ HERE . 'indent-level'; # Continuation, should be indented +=head2 Test case 7 + +An indented HERE document using a bare identifier. + +=cut + +## test case + +$text = <<~HERE; +look-here +HERE + +$noindent = "New statement in this line"; + +=head2 Test case 8 + +A HERE document as an argument to print when printing to a filehandle. + +=cut + +## test case + +print $fh <<~HERE; +look-here +HERE + +$noindent = "New statement in this line"; + +=head2 Test case 9 + +A HERE document as a hash value. + +=cut + +my %foo = ( + text => <<~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 <