branch: externals/eglot commit 258e3b068c9715aa20eb7d5ec714069fd70eee87 Merge: 5cda0ec 1f8c238 Author: João Távora <joaotav...@gmail.com> Commit: João Távora <joaotav...@gmail.com>
Merge master into jsonrpc-refactor (using imerge) --- README.md | 38 +++++++++++++---- eglot-tests.el | 2 +- eglot.el | 131 +++++++++++++++++++++++++++++++++++---------------------- jrpc.el | 16 +++---- 4 files changed, 120 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 7550a30..b87666c 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ Eglot *E*macs Poly*glot*. An Emacs client to [Language Server Protocol][lsp] servers. -``` -(add-to-list 'load-path "/path/to/eglot") -(require 'eglot) ; Requires emacs 26! +Eglot is [in ELPA][gnuelpa]. Installation is straightforward: +``` +(package-install 'eglot) ; Requires Emacs 26! ;; Now find some source file, any source file M-x eglot ``` @@ -15,14 +15,15 @@ M-x eglot *That's it*. If you're lucky, this guesses the LSP executable to start for the language of your choice. Otherwise, it prompts you to enter one: -`M-x eglot` currently guesses and works out-of-the-box with: +`M-x eglot` can guess and work out-of-the-box with these servers: * Javascript's [javascript-typescript-stdio][javascript-typescript-langserver] * Rust's [rls][rls] * Python's [pyls][pyls] * Bash's [bash-language-server][bash-language-server] +* PHP's [php-language-server][php-language-server] -I'll add more as I test more features. In the meantime you can +I'll add to this list as I test more servers. In the meantime you can customize `eglot-server-programs`: ```lisp @@ -41,7 +42,9 @@ Here's a summary of available commands: - `M-x eglot-reconnect` reconnects to the server; -- `M-x eglot-rename` asks the server to rename the symbol at point +- `M-x eglot-shutdown` says bye-bye to the server; + +- `M-x eglot-rename` asks the server to rename the symbol at point; - `M-x eglot-help-at-point` asks the server for help for symbol at point. Currently this is what `eldoc-mode` displays in the echo @@ -60,6 +63,23 @@ either: (define-key eglot-mode-map (kbd "<f6>") 'xref-find-definitions) ``` +# How does this work exactly? + +`M-x eglot` starts a server via a shell-command guessed from +`eglot-server-programs`, using the current major-mode (for whatever +language you're programming in) as a hint. + +If the connection is successful, you see an `[eglot:<server>]` +indicator pop up in your mode-line. More importantly, this means +current *and future* file buffers of that major mode *inside your +current project* automatically become \"managed\" by the LSP server, +i.e. information about their contents is exchanged periodically to +provide enhanced code analysis via `xref-find-definitions`, +`flymake-mode`, `eldoc-mode`, `completion-at-point`, among others. + +To "unmanage" these buffers, shutdown the server with `M-x +eglot-shutdown`. + # Supported Protocol features (3.6) ## General @@ -104,7 +124,7 @@ either: - [x] textDocument/completion - [x] completionItem/resolve (works quite well with [company-mode][company-mode]) - [x] textDocument/hover -- [x] textDocument/signatureHelp (fancy stuff with Python's [pyls[pyls]]) +- [x] textDocument/signatureHelp (fancy stuff with Python's [pyls][pyls]) - [x] textDocument/definition - [ ] textDocument/typeDefinition (3.6.0) - [ ] textDocument/implementation (3.6.0) @@ -152,7 +172,7 @@ User-visible differences: Under the hood: -- Message parser is much much simpler. +- Message parser is much simpler. - Defers signature requests like `textDocument/hover` until server is ready. Also sends `textDocument/didChange` for groups of edits, not one per each tiny change. @@ -169,10 +189,12 @@ Under the hood: [lsp]: https://microsoft.github.io/language-server-protocol/ [rls]: https://github.com/rust-lang-nursery/rls [pyls]: https://github.com/palantir/python-language-server +[gnuelpa]: https://elpa.gnu.org/packages/eglot.html [javascript-typescript-langserver]: https://github.com/sourcegraph/javascript-typescript-langserver [emacs-lsp]: https://github.com/emacs-lsp/lsp-mode [emacs-lsp-plugins]: https://github.com/emacs-lsp [bash-language-server]: https://github.com/mads-hartmann/bash-language-server +[php-language-server]: https://github.com/felixfbecker/php-language-server [company-mode]: https://github.com/company-mode/company-mode diff --git a/eglot-tests.el b/eglot-tests.el index e0ed324..7a832dd 100644 --- a/eglot-tests.el +++ b/eglot-tests.el @@ -1,6 +1,6 @@ ;;; eglot-tests.el --- Tests for eglot.el -*- lexical-binding: t; -*- -;; Copyright (C) 2018 João Távora +;; Copyright (C) 2018 Free Software Foundation, Inc. ;; Author: João Távora <joaotav...@gmail.com> ;; Keywords: tests diff --git a/eglot.el b/eglot.el index 11d048e..13c1b49 100644 --- a/eglot.el +++ b/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 0.1 +;; Version: 0.2 ;; Author: João Távora <joaotav...@gmail.com> ;; Maintainer: João Távora <joaotav...@gmail.com> ;; URL: https://github.com/joaotavora/eglot @@ -24,8 +24,28 @@ ;;; Commentary: -;; Simply M-x eglot should be enough to get you started, but see README.md. - +;; Simply M-x eglot should be enough to get you started, but here's a +;; little info (see the accompanying README.md or the URL for more). +;; +;; M-x eglot starts a server via a shell-command guessed from +;; `eglot-server-programs', using the current major-mode (for whatever +;; language you're programming in) as a hint. If it can't guess, it +;; prompts you in the mini-buffer for these things. Actually, the +;; server needen't be locally started: you can connect to a running +;; server via TCP by entering a <host:port> syntax. +;; +;; Anyway, if the connection is successful, you should see an `eglot' +;; indicator pop up in your mode-line. More importantly, this means +;; current *and future* file buffers of that major mode *inside your +;; current project* automatically become \"managed\" by the LSP +;; server, i.e. information about their contents is exchanged +;; periodically to provide enhanced code analysis via +;; `xref-find-definitions', `flymake-mode', `eldoc-mode', +;; `completion-at-point', among others. +;; +;; To "unmanage" these buffers, shutdown the server with M-x +;; eglot-shutdown. +;; ;;; Code: (require 'json) @@ -52,7 +72,9 @@ (defvar eglot-server-programs '((rust-mode . ("rls")) (python-mode . ("pyls")) (js-mode . ("javascript-typescript-stdio")) - (sh-mode . ("bash-language-server" "start"))) + (sh-mode . ("bash-language-server" "start")) + (php-mode . ("php" "vendor/felixfbecker/\ +language-server/bin/php-language-server.php"))) "Alist mapping major modes to server executables.") (defface eglot-mode-line @@ -112,9 +134,9 @@ A list (ID WHAT DONE-P).") (gethash (eglot--project proc) eglot--processes-by-project))) (cond ((eglot--moribund proc)) ((not (eglot--inhibit-autoreconnect proc)) - (eglot--warn "Reconnecting unexpected server exit.") + (eglot--warn "Reconnecting after unexpected server exit.") (eglot-reconnect proc)) - (t + ((timerp (eglot--inhibit-autoreconnect proc)) (eglot--warn "Not auto-reconnecting, last one didn't last long.")))) (defun eglot-shutdown (proc &optional interactive) @@ -220,9 +242,22 @@ called interactively." ;;;###autoload (defun eglot (managed-major-mode project command &optional interactive) - "Start a Language Server Protocol server. -Server is started with COMMAND and manages buffers of -MANAGED-MAJOR-MODE for the current project. + "Manage a project with a Language Server Protocol (LSP) server. + +The LSP server is started (or contacted) via COMMAND. If this +operation is successful, current *and future* file buffers of +MANAGED-MAJOR-MODE inside PROJECT automatically become +\"managed\" by the LSP server, meaning information about their +contents is exchanged periodically to provide enhanced +code-analysis via `xref-find-definitions', `flymake-mode', +`eldoc-mode', `completion-at-point', among others. + +Interactively, the command attempts to guess MANAGED-MAJOR-MODE +from current buffer, COMMAND from `eglot-server-programs' and +PROJECT from `project-current'. If it can't guess, the user is +prompted. With a single \\[universal-argument] prefix arg, it +always prompt for COMMAND. With two \\[universal-argument] +prefix args, also prompts for MANAGED-MAJOR-MODE. PROJECT is a project instance as returned by `project-current'. @@ -234,12 +269,6 @@ is also know as the server's \"contact\". MANAGED-MAJOR-MODE is an Emacs major mode. -Interactively, guess MANAGED-MAJOR-MODE from current buffer and -COMMAND from `eglot-server-programs'. With a single -\\[universal-argument] prefix arg, prompt for COMMAND. With two -\\[universal-argument] prefix args, also prompt for -MANAGED-MAJOR-MODE. - INTERACTIVE is t if called interactively." (interactive (eglot--interactive)) (let* ((short-name (eglot--project-short-name project))) @@ -253,8 +282,7 @@ INTERACTIVE is t if called interactively." (let ((proc (eglot--connect project managed-major-mode (format "%s/%s" short-name managed-major-mode) - command - interactive))) + command))) (eglot--message "Connected! Process `%s' now \ managing `%s' buffers in project `%s'." proc managed-major-mode short-name) @@ -269,8 +297,7 @@ INTERACTIVE is t if called interactively." (eglot--connect (eglot--project process) (eglot--major-mode process) (jrpc-name process) - (jrpc-contact process) - interactive) + (jrpc-contact process)) (eglot--message "Reconnected!")) (defalias 'eglot-events-buffer 'jrpc-events-buffer) @@ -278,7 +305,8 @@ INTERACTIVE is t if called interactively." (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") (defun eglot--dispatch (proc method id &rest params) - ;; a server notification or a server request + "Dispatcher passed to `jrpc-connect'. +Builds a function from METHOD, passes it PROC, ID and PARAMS." (let* ((handler-sym (intern (concat "eglot--server-" method)))) (if (functionp handler-sym) (apply handler-sym proc (append params (if id `(:id ,id)))) @@ -286,40 +314,43 @@ INTERACTIVE is t if called interactively." proc id :error (jrpc-obj :code -32601 :message "Unimplemented"))))) -(defun eglot--connect (project managed-major-mode name command - dont-inhibit) - (let ((proc (jrpc-connect name command #'eglot--dispatch #'eglot--on-shutdown))) +(defun eglot--connect (project managed-major-mode name command) + (let ((proc (jrpc-connect name command #'eglot--dispatch #'eglot--on-shutdown)) + success) (setf (eglot--project proc) project) (setf (eglot--major-mode proc)managed-major-mode) (push proc (gethash project eglot--processes-by-project)) (run-hook-with-args 'eglot-connect-hook proc) - (cl-destructuring-bind (&key capabilities) - (jrpc-request - proc - :initialize - (jrpc-obj :processId (unless (eq (process-type proc) - 'network) - (emacs-pid)) - :rootUri (eglot--path-to-uri - (car (project-roots project))) - :initializationOptions [] - :capabilities (eglot--client-capabilities))) - (setf (eglot--capabilities proc) capabilities) - (setf (jrpc-status proc) nil) - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (eglot--maybe-activate-editing-mode proc))) - (jrpc-notify proc :initialized (jrpc-obj :__dummy__ t)) - (setf (eglot--inhibit-autoreconnect proc) - (cond - ((booleanp eglot-autoreconnect) (not eglot-autoreconnect)) - (dont-inhibit nil) - ((cl-plusp eglot-autoreconnect) - (run-with-timer eglot-autoreconnect nil - (lambda () - (setf (eglot--inhibit-autoreconnect proc) - (null eglot-autoreconnect))))))) - proc))) + (unwind-protect + (cl-destructuring-bind (&key capabilities) + (jrpc-request + proc + :initialize + (jrpc-obj :processId (unless (eq (process-type proc) + 'network) + (emacs-pid)) + :rootPath (car (project-roots project)) + :rootUri (eglot--path-to-uri + (car (project-roots project))) + :initializationOptions [] + :capabilities (eglot--client-capabilities))) + (setf (eglot--capabilities proc) capabilities) + (setf (jrpc-status proc) nil) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (eglot--maybe-activate-editing-mode proc))) + (jrpc-notify proc :initialized (jrpc-obj :__dummy__ t)) + (setf (eglot--inhibit-autoreconnect proc) + (cond + ((booleanp eglot-autoreconnect) (not eglot-autoreconnect)) + ((cl-plusp eglot-autoreconnect) + (run-with-timer eglot-autoreconnect nil + (lambda () + (setf (eglot--inhibit-autoreconnect proc) + (null eglot-autoreconnect))))))) + (setq success proc)) + (unless (or success (not (process-live-p proc)) (eglot--moribund proc)) + (eglot-shutdown proc))))) (defun eglot--server-ready-p (_what _proc) "Tell if server of PROC ready for processing deferred WHAT." diff --git a/jrpc.el b/jrpc.el index ea0122b..6773e12 100644 --- a/jrpc.el +++ b/jrpc.el @@ -427,9 +427,9 @@ timeout keeps counting." (remhash id (jrpc--request-continuations proc)) (funcall (or timeout-fn (lambda () - (jrpc-error - "Tired of waiting for reply to %s, id=%s" - method id)))))))))) + (jrpc-log-event + proc `(:timed-out ,method :id id + :params ,params))))))))))) (when deferred (let* ((buf (current-buffer)) (existing (gethash (list deferred buf) (jrpc--deferred-actions proc)))) @@ -453,13 +453,13 @@ timeout keeps counting." (puthash id (list (or success-fn (jrpc-lambda (&rest _ignored) - (jrpc-log-event - proc (jrpc-obj :message "success ignored" :id id)))) + (jrpc-log-event + proc (jrpc-obj :message "success ignored" :id id)))) (or error-fn (jrpc-lambda (&key code message &allow-other-keys) - (setf (jrpc-status proc) `(,message t)) - proc (jrpc-obj :message "error ignored, status set" - :id id :error code))) + (setf (jrpc-status proc) `(,message t)) + proc (jrpc-obj :message "error ignored, status set" + :id id :error code))) (funcall make-timeout)) (jrpc--request-continuations proc)) (jrpc--process-send proc (jrpc-obj :jsonrpc "2.0"