402 lines
18 KiB
EmacsLisp
402 lines
18 KiB
EmacsLisp
|
;;; lsp-mode.el --- Minor mode for interacting with Language Servers -*- lexical-binding: t -*-
|
|||
|
|
|||
|
;; Copyright (C) 2016-2018 Vibhav Pant <vibhavp@gmail.com>
|
|||
|
|
|||
|
;; This program is free software: you can redistribute it and/or modify
|
|||
|
;; it under the terms of the GNU General Public License as published by
|
|||
|
;; the Free Software Foundation, either version 3 of the License, or
|
|||
|
;; (at your option) any later version.
|
|||
|
|
|||
|
;; This program is distributed in the hope that it will be useful,
|
|||
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|||
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|||
|
;; GNU General Public License for more details.
|
|||
|
|
|||
|
;; You should have received a copy of the GNU General Public License
|
|||
|
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
|
|||
|
;; Author: Vibhav Pant <vibhavp@gmail.com>
|
|||
|
;; URL: https://github.com/emacs-lsp/lsp-mode
|
|||
|
;; Package-Requires: ((emacs "25.1"))
|
|||
|
;; Version: 4.2
|
|||
|
|
|||
|
;;; Commentary:
|
|||
|
|
|||
|
;;; Code:
|
|||
|
|
|||
|
(require 'lsp-methods)
|
|||
|
(require 'lsp-io)
|
|||
|
(require 'cl-lib)
|
|||
|
(require 'network-stream)
|
|||
|
|
|||
|
(defvar lsp-version-support "3.0"
|
|||
|
"This is the version of the Language Server Protocol currently supported by ‘lsp-mode’.")
|
|||
|
|
|||
|
;;;###autoload
|
|||
|
(define-minor-mode lsp-mode ""
|
|||
|
nil nil nil
|
|||
|
:lighter (:eval (lsp-mode-line))
|
|||
|
:group 'lsp-mode)
|
|||
|
|
|||
|
(defun lsp--make-stdio-connection (name command command-fn stderr)
|
|||
|
(lambda (filter sentinel)
|
|||
|
(let* ((command (if command-fn (funcall command-fn) command))
|
|||
|
(final-command (if (consp command) command (list command))))
|
|||
|
(unless (executable-find (nth 0 final-command))
|
|||
|
(error (format "Couldn't find executable %s" (nth 0 final-command))))
|
|||
|
(let ((proc (make-process
|
|||
|
:name name
|
|||
|
:connection-type 'pipe
|
|||
|
:coding 'no-conversion
|
|||
|
:command final-command
|
|||
|
:filter filter
|
|||
|
:sentinel sentinel
|
|||
|
:stderr stderr
|
|||
|
:noquery t)))
|
|||
|
;; TODO: This is redundant with :noquery above, but due to a
|
|||
|
;; bug pre-Emacs 26 it is still needed
|
|||
|
;; https://debbugs.gnu.org/cgi/bugreport.cgi?bug=30031
|
|||
|
(set-process-query-on-exit-flag (get-buffer-process (get-buffer stderr)) nil)
|
|||
|
proc))))
|
|||
|
|
|||
|
(defun lsp--make-tcp-connection (name command command-fn host port stderr)
|
|||
|
(lambda (filter sentinel)
|
|||
|
(let* ((command (if command-fn (funcall command-fn) command))
|
|||
|
(final-command (if (consp command) command (list command)))
|
|||
|
proc tcp-proc)
|
|||
|
(unless (executable-find (nth 0 final-command))
|
|||
|
(error (format "Couldn't find executable %s" (nth 0 final-command))))
|
|||
|
(setq proc (make-process
|
|||
|
:name name
|
|||
|
:connection-type 'pipe
|
|||
|
:coding 'no-conversion
|
|||
|
:command final-command
|
|||
|
:sentinel sentinel
|
|||
|
:stderr stderr
|
|||
|
:noquery t)
|
|||
|
tcp-proc (open-network-stream (concat name " TCP connection")
|
|||
|
nil host port
|
|||
|
:type 'plain))
|
|||
|
;; TODO: Same :noquery issue (see above)
|
|||
|
(set-process-query-on-exit-flag (get-buffer-process (get-buffer stderr)) nil)
|
|||
|
(set-process-query-on-exit-flag tcp-proc nil)
|
|||
|
(set-process-filter tcp-proc filter)
|
|||
|
(cons proc tcp-proc))))
|
|||
|
|
|||
|
(cl-defmacro lsp-define-whitelist-add (name get-root
|
|||
|
&key docstring)
|
|||
|
"Define a function to add the project root for the current buffer to the whitleist.
|
|||
|
NAME is the base name for the command.
|
|||
|
GET-ROOT is the language-specific function to determine the project root for the current buffer."
|
|||
|
(let ((whitelist-add (intern (format "%s-whitelist-add" name)))
|
|||
|
(enable-interactive (intern (format "%s-enable" name))))
|
|||
|
`(defun ,whitelist-add ()
|
|||
|
,docstring
|
|||
|
(interactive)
|
|||
|
(let ((root (funcall ,get-root)))
|
|||
|
(customize-save-variable 'lsp-project-whitelist
|
|||
|
(add-to-list 'lsp-project-whitelist (lsp--as-regex root)))
|
|||
|
(,enable-interactive)))))
|
|||
|
|
|||
|
(cl-defmacro lsp-define-whitelist-remove (name get-root
|
|||
|
&key docstring)
|
|||
|
"Define a function to remove the project root for the current buffer from the whitleist.
|
|||
|
NAME is the base name for the command.
|
|||
|
GET-ROOT is the language-specific function to determine the project root for the current buffer."
|
|||
|
(let ((whitelist-remove (intern (format "%s-whitelist-remove" name))))
|
|||
|
`(defun ,whitelist-remove ()
|
|||
|
,docstring
|
|||
|
(interactive)
|
|||
|
(let ((root (funcall ,get-root)))
|
|||
|
(customize-save-variable 'lsp-project-whitelist
|
|||
|
(remove (lsp--as-regex root) lsp-project-whitelist))))))
|
|||
|
|
|||
|
(defun lsp--as-regex (root)
|
|||
|
"Convert the directory path in ROOT to an equivalent regex."
|
|||
|
(concat "^" (regexp-quote root) "$"))
|
|||
|
|
|||
|
(cl-defmacro lsp-define-stdio-client (name language-id get-root command
|
|||
|
&key docstring
|
|||
|
language-id-fn
|
|||
|
command-fn
|
|||
|
ignore-regexps
|
|||
|
ignore-messages
|
|||
|
extra-init-params
|
|||
|
initialize
|
|||
|
prefix-function)
|
|||
|
"Define a LSP client using stdio.
|
|||
|
NAME is the symbol to use for the name of the client.
|
|||
|
LANGUAGE-ID is the language id to be used when communication with
|
|||
|
the Language Server. COMMAND is the command to run.
|
|||
|
|
|||
|
Optional arguments:
|
|||
|
`:docstring' is an optional docstring used for the entrypoint function created by
|
|||
|
`lsp-define-stdio-client'.
|
|||
|
|
|||
|
`:ignore-regexps' is a list of regexps. When a data packet from the LSP server
|
|||
|
matches any of these regexps, it will be ignored. This is intended for dealing
|
|||
|
with LSP servers that output non-protocol data.
|
|||
|
|
|||
|
`:ignore-messages' is a list of regexps. When a message from the LSP server
|
|||
|
matches any of these regexps, it will be ignored. This is useful for filtering
|
|||
|
out unwanted messages; such as servers that send nonstandard message types, or
|
|||
|
extraneous `logMessage's.
|
|||
|
|
|||
|
`:command-fn' is a function that returns the command string/list to be used to
|
|||
|
launch the language server. If non-nil, COMMAND is ignored.
|
|||
|
|
|||
|
`:language-id-fn' is a function that returns the language-id string to be used
|
|||
|
while opening a new file. If non-nil, LANGUAGE-ID is ignored.
|
|||
|
|
|||
|
`:extra-init-params' is a plist that specifies any (optional)
|
|||
|
initializeOptions parameters required by the LSP server. A function taking
|
|||
|
a single argument (LSP workspace) and returning a plist is also accepted.
|
|||
|
|
|||
|
`:initialize' is a function called when the client is initialized. It takes a
|
|||
|
single argument, the newly created client.
|
|||
|
|
|||
|
`:prefix-function' is a function called for getting the prefix for completion.
|
|||
|
The function takes no parameter and returns a cons (start . end) representing
|
|||
|
the start and end bounds of the prefix. If it's not set, the client uses a
|
|||
|
default prefix function."
|
|||
|
(cl-check-type name symbol)
|
|||
|
(let ((enable-name (intern (format "%s-enable" name))))
|
|||
|
`(progn
|
|||
|
(lsp-define-whitelist-add ,name ,get-root)
|
|||
|
(lsp-define-whitelist-remove ,name ,get-root)
|
|||
|
(defun ,enable-name ()
|
|||
|
,docstring
|
|||
|
(interactive)
|
|||
|
(lsp--enable-stdio-client ',name
|
|||
|
:language-id ,language-id
|
|||
|
:language-id-fn ,language-id-fn
|
|||
|
:root-directory-fn ,get-root
|
|||
|
:command ,command
|
|||
|
:command-fn ,command-fn
|
|||
|
:ignore-regexps ,ignore-regexps
|
|||
|
:ignore-messages ,ignore-messages
|
|||
|
:extra-init-params ,extra-init-params
|
|||
|
:initialize-fn ,initialize
|
|||
|
:enable-function (function ,enable-name)
|
|||
|
:prefix-function ,prefix-function)))))
|
|||
|
|
|||
|
(cl-defun lsp--enable-stdio-client (name &key language-id language-id-fn
|
|||
|
root-directory-fn command command-fn
|
|||
|
ignore-regexps ignore-messages
|
|||
|
extra-init-params initialize-fn
|
|||
|
enable-function
|
|||
|
prefix-function)
|
|||
|
(cl-check-type name symbol)
|
|||
|
(cl-check-type language-id (or null string))
|
|||
|
(cl-check-type language-id-fn (or null function))
|
|||
|
(cl-check-type root-directory-fn (or null function))
|
|||
|
(cl-check-type command list)
|
|||
|
(cl-check-type command-fn (or null function))
|
|||
|
(cl-check-type ignore-regexps list)
|
|||
|
(cl-check-type ignore-messages list)
|
|||
|
(cl-check-type extra-init-params (or list function))
|
|||
|
(cl-check-type initialize-fn (or null function))
|
|||
|
;; (cl-check-type enable-function function)
|
|||
|
(cl-check-type prefix-function (or null function))
|
|||
|
(when (and (not lsp-mode) (buffer-file-name))
|
|||
|
(let* ((stderr (generate-new-buffer-name
|
|||
|
(concat "*" (symbol-name name) " stderr*")))
|
|||
|
(client (make-lsp--client
|
|||
|
:language-id (or language-id-fn (lambda (_) language-id))
|
|||
|
:new-connection (lsp--make-stdio-connection
|
|||
|
(symbol-name name)
|
|||
|
command
|
|||
|
command-fn
|
|||
|
stderr)
|
|||
|
:stderr stderr
|
|||
|
:get-root root-directory-fn
|
|||
|
:ignore-regexps ignore-regexps
|
|||
|
:ignore-messages ignore-messages
|
|||
|
:enable-function enable-function
|
|||
|
:prefix-function prefix-function)))
|
|||
|
(when initialize-fn
|
|||
|
(funcall initialize-fn client))
|
|||
|
(let ((root (funcall (lsp--client-get-root client))))
|
|||
|
(if (lsp--should-start-p root)
|
|||
|
(lsp--start client extra-init-params)
|
|||
|
(message "Not initializing project %s" root))))))
|
|||
|
|
|||
|
(cl-defmacro lsp-define-tcp-client (name language-id get-root command host port
|
|||
|
&key docstring
|
|||
|
language-id-fn
|
|||
|
command-fn
|
|||
|
ignore-regexps
|
|||
|
ignore-messages
|
|||
|
extra-init-params
|
|||
|
initialize
|
|||
|
prefix-function)
|
|||
|
"Define a LSP client using TCP.
|
|||
|
NAME is the symbol to use for the name of the client.
|
|||
|
LANGUAGE-ID is the language id to be used when communication with
|
|||
|
the Language Server. COMMAND is the command to run. HOST is the
|
|||
|
host address. PORT is the port number.
|
|||
|
|
|||
|
Optional arguments:
|
|||
|
`:ignore-regexps' is a list of regexps. When a data packet from the LSP server
|
|||
|
matches any of these regexps, it will be ignored. This is intended for dealing
|
|||
|
with LSP servers that output non-protocol data.
|
|||
|
|
|||
|
`:ignore-messages' is a list of regexps. When a message from the LSP server
|
|||
|
matches any of these regexps, it will be ignored. This is useful for filtering
|
|||
|
out unwanted messages; such as servers that send nonstandard message types, or
|
|||
|
extraneous `logMessage's.
|
|||
|
|
|||
|
`:command-fn' is a function that returns the command string/list to be used to
|
|||
|
launch the language server. If non-nil, COMMAND is ignored.
|
|||
|
|
|||
|
`:language-id-fn' is a function that returns the language-id string to be used
|
|||
|
while opening a new file. If non-nil, LANGUAGE-ID is ignored.
|
|||
|
|
|||
|
`:extra-init-params' is a plist that specifies any (optional)
|
|||
|
initializeOptions parameters required by the LSP server. A function taking
|
|||
|
a single argument (LSP workspace) and returning a plist is also accepted.
|
|||
|
|
|||
|
`:initialize' is a function called when the client is initialized. It takes a
|
|||
|
single argument, the newly created client.
|
|||
|
|
|||
|
`:prefix-function' is a function called for getting the prefix for completion.
|
|||
|
The function takes no parameter and returns a cons (start . end) representing
|
|||
|
the start and end bounds of the prefix. If it's not set, the client uses a
|
|||
|
default prefix function."
|
|||
|
(cl-check-type name symbol)
|
|||
|
(let ((enable-name (intern (format "%s-enable" name))))
|
|||
|
`(progn
|
|||
|
(lsp-define-whitelist-add ,name ,get-root)
|
|||
|
(lsp-define-whitelist-remove ,name ,get-root)
|
|||
|
(defun ,enable-name ()
|
|||
|
,docstring
|
|||
|
(interactive)
|
|||
|
(lsp--enable-tcp-client ',name
|
|||
|
:language-id ,language-id
|
|||
|
:language-id-fn ,language-id-fn
|
|||
|
:root-directory-fn ,get-root
|
|||
|
:command ,command
|
|||
|
:command-fn ,command-fn
|
|||
|
:host ,host
|
|||
|
:port ,port
|
|||
|
:ignore-regexps ,ignore-regexps
|
|||
|
:ignore-messages ,ignore-messages
|
|||
|
:extra-init-params ,extra-init-params
|
|||
|
:initialize-fn ,initialize
|
|||
|
:enable-function (function ,enable-name)
|
|||
|
:prefix-function ,prefix-function)))))
|
|||
|
|
|||
|
(cl-defun lsp--enable-tcp-client (name &key language-id language-id-fn
|
|||
|
root-directory-fn command command-fn
|
|||
|
host port
|
|||
|
ignore-regexps ignore-messages
|
|||
|
extra-init-params initialize-fn
|
|||
|
enable-function
|
|||
|
prefix-function)
|
|||
|
(cl-check-type name symbol)
|
|||
|
(cl-check-type language-id (or null string))
|
|||
|
(cl-check-type language-id-fn (or null function))
|
|||
|
(cl-check-type root-directory-fn (or null function))
|
|||
|
(cl-check-type command list)
|
|||
|
(cl-check-type command-fn (or null function))
|
|||
|
(cl-check-type host string)
|
|||
|
(cl-check-type port (integer 1 #xFFFF))
|
|||
|
(cl-check-type ignore-regexps list)
|
|||
|
(cl-check-type ignore-messages list)
|
|||
|
(cl-check-type extra-init-params (or list function))
|
|||
|
(cl-check-type initialize-fn (or null function))
|
|||
|
(cl-check-type prefix-function (or null function))
|
|||
|
(when (and (not lsp-mode) (buffer-file-name))
|
|||
|
(let* ((stderr (generate-new-buffer-name
|
|||
|
(concat "*" (symbol-name name) " stderr*")))
|
|||
|
(client (make-lsp--client
|
|||
|
:language-id (or language-id-fn (lambda (_) language-id))
|
|||
|
:new-connection (lsp--make-tcp-connection
|
|||
|
(symbol-name name)
|
|||
|
command
|
|||
|
command-fn
|
|||
|
host port
|
|||
|
stderr)
|
|||
|
:stderr stderr
|
|||
|
:get-root root-directory-fn
|
|||
|
:ignore-regexps ignore-regexps
|
|||
|
:ignore-messages ignore-messages
|
|||
|
:enable-function enable-function
|
|||
|
:prefix-function prefix-function)))
|
|||
|
(when initialize-fn
|
|||
|
(funcall initialize-fn client))
|
|||
|
(let ((root (funcall (lsp--client-get-root client))))
|
|||
|
(if (lsp--should-start-p root)
|
|||
|
(lsp--start client extra-init-params)
|
|||
|
(message "Not initializing project %s" root))))))
|
|||
|
|
|||
|
(defvar-local lsp-status nil
|
|||
|
"The current status of the LSP server.")
|
|||
|
|
|||
|
(defun lsp-workspace-status (status-string &optional workspace)
|
|||
|
"Set current workspace status to STATUS-STRING.
|
|||
|
If WORKSPACE is not specified defaults to lsp--cur-workspace."
|
|||
|
(setf (lsp--workspace-status (or workspace lsp--cur-workspace)) status-string))
|
|||
|
|
|||
|
(defun lsp-mode-line ()
|
|||
|
"Construct the mode line text."
|
|||
|
(concat " LSP" lsp-status (lsp--workspace-status lsp--cur-workspace)))
|
|||
|
|
|||
|
(defconst lsp--sync-type
|
|||
|
`((0 . "None")
|
|||
|
(1 . "Full Document")
|
|||
|
(2 . "Incremental Changes")))
|
|||
|
|
|||
|
(defconst lsp--capabilities
|
|||
|
`(("textDocumentSync" . ("Document sync method" .
|
|||
|
((0 . "None")
|
|||
|
(1 . "Send full contents")
|
|||
|
(2 . "Send incremental changes."))))
|
|||
|
("hoverProvider" . ("The server provides hover support" . boolean))
|
|||
|
("completionProvider" . ("The server provides completion support" . boolean))
|
|||
|
("signatureHelpProvider" . ("The server provides signature help support" . boolean))
|
|||
|
("definitionProvider" . ("The server provides goto definition support" . boolean))
|
|||
|
("typeDefinitionProvider" . ("The server provides goto type definition support" . boolean))
|
|||
|
("implementationProvider" . ("The server provides goto implementation support" . boolean))
|
|||
|
("referencesProvider" . ("The server provides references support" . boolean))
|
|||
|
(("documentHighlightProvider" . ("The server provides document highlight support." . boolean)))
|
|||
|
("documentSymbolProvider" . ("The server provides file symbol support" . boolean))
|
|||
|
("workspaceSymbolProvider" . ("The server provides project symbol support" . boolean))
|
|||
|
("codeActionProvider" . ("The server provides code actions" . boolean))
|
|||
|
("codeLensProvider" . ("The server provides code lens" . boolean))
|
|||
|
("documentFormattingProvider" . ("The server provides file formatting" . boolean))
|
|||
|
("documentOnTypeFormattingProvider" . ("The server provides on-type formatting" . boolean))
|
|||
|
("documentLinkProvider" . ("The server provides document link support" . boolean))
|
|||
|
("executeCommandProvider" . ("The server provides command execution support" . boolean))
|
|||
|
(("documentRangeFormattingProvider" . ("The server provides region formatting" . boolean)))
|
|||
|
(("renameProvider" . ("The server provides rename support" . boolean)))))
|
|||
|
|
|||
|
(defun lsp--cap-str (cap)
|
|||
|
(let* ((elem (assoc cap lsp--capabilities))
|
|||
|
(desc (cadr elem))
|
|||
|
(type (cddr elem))
|
|||
|
(value (gethash cap (lsp--server-capabilities))))
|
|||
|
(when (and elem desc type value)
|
|||
|
(concat desc (cond
|
|||
|
((listp type) (concat ": " (cdr (assoc value type))))) "\n"))))
|
|||
|
|
|||
|
(defun lsp-capabilities ()
|
|||
|
"View all capabilities for the language server associated with this buffer."
|
|||
|
(interactive)
|
|||
|
(unless lsp--cur-workspace
|
|||
|
(user-error "No language server is associated with this buffer"))
|
|||
|
(let ((str (mapconcat #'lsp--cap-str (reverse (hash-table-keys
|
|||
|
(lsp--server-capabilities))) ""))
|
|||
|
(buffer-name (generate-new-buffer-name "lsp-capabilities"))
|
|||
|
)
|
|||
|
(get-buffer-create buffer-name)
|
|||
|
(with-current-buffer buffer-name
|
|||
|
(view-mode -1)
|
|||
|
(erase-buffer)
|
|||
|
(insert str)
|
|||
|
(view-mode 1))
|
|||
|
(switch-to-buffer buffer-name)))
|
|||
|
|
|||
|
(provide 'lsp-mode)
|
|||
|
;;; lsp-mode.el ends here
|