;;; 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