201 lines
8.8 KiB
EmacsLisp

;;; lsp-mode.el --- Minor mode for interacting with Language Servers -*- lexical-binding: t -*-
;; Copyright (C) 2016 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") (flycheck "30"))
;; Version: 3.1
;;; Commentary:
;;; Code:
(require 'lsp-methods)
(require 'lsp-receive)
(require 'lsp-send)
(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")
(defun lsp--make-stdio-connection (name command command-fn)
(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))))
(make-process
:name name
:connection-type 'pipe
:coding 'no-conversion
:command final-command
:filter filter
:sentinel sentinel
:stderr (generate-new-buffer-name (concat "*" name " stderr*"))))))
(defun lsp--make-tcp-connection (name command command-fn host port)
(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
:coding 'no-conversion
:command final-command
:sentinel sentinel
:stderr (generate-new-buffer-name (concat "*" name " stderr*")))
tcp-proc (open-network-stream (concat name " TCP connection")
nil host port
:type 'plain))
(set-process-filter tcp-proc filter)
(cons proc tcp-proc))))
(defun lsp--verify-regexp-list (l)
(cl-assert (cl-typep l 'list) nil
"lsp-define-client: :ignore-regexps is not a list")
(dolist (e l l)
(cl-assert (cl-typep e 'string)
nil
(format
"lsp-define-client: :ignore-regexps element %s is not a string"
e))))
(defmacro lsp-define-stdio-client (name language-id get-root command &rest args)
"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:
`:ignore-regexps' is a list of regexps which when matched will be ignored by the output parser.
`: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.
`:initialize' is a function called when the client is intiailized. It takes a single argument, the newly created client.
"
(let ((enable (intern (format "%s-enable" name))))
`(defun ,enable ()
,(plist-get args :docstring)
(interactive)
(let ((client (make-lsp--client
:language-id ,(lsp--assert-type language-id #'stringp)
:send-sync #'lsp--stdio-send-sync
:send-async #'lsp--stdio-send-async
:new-connection (lsp--make-stdio-connection ,(symbol-name name) ,command
,(plist-get args :command-fn))
:get-root ,get-root
:ignore-regexps ,(plist-get args :ignore-regexps))))
(unless lsp-mode
,(when (plist-get args :initialize)
`(funcall ,(plist-get args :initialize) client))
(let ((root (funcall (lsp--client-get-root client))))
(if (lsp--should-start-p root)
(progn
(lsp-mode 1)
(lsp--start client))
(message "Not initializing project %s" root))))))))
(defmacro lsp-define-tcp-client (name language-id get-root command host port &rest args)
"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 which when matched will be ignored by the output parser.
`: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.
`:initialize' is a function called when the client is intiailized. It takes a single argument, the newly created client."
(let ((enable (intern (format "%s-enable" name))))
`(defun ,enable ()
,(plist-get args :docstring)
(interactive)
(let ((client (make-lsp--client
:language-id ,(lsp--assert-type language-id #'stringp)
:send-sync #'lsp--stdio-send-sync
:send-async #'lsp--stdio-send-async
:new-connection (lsp--make-tcp-connection ,(symbol-name name) ,command ,(plist-get args :command-fn) ,host ,port)
:get-root ,get-root
:ignore-regexps ,(plist-get args :ignore-regexps))))
(unless lsp-mode
,(when (plist-get args :initialize)
`(funcall ,(plist-get args :initialize) client))
(let ((root (funcall (lsp--client-get-root client))))
(if (lsp--should-start-p root)
(progn
(lsp-mode 1)
(lsp--start client))
(message "Not initializing project %s" root))))))))
;;;###autoload
(define-minor-mode lsp-mode ""
nil nil nil
:lighter " LSP"
:group 'lsp-mode)
(defconst lsp--sync-type
`((0 . "None")
(1 . "Full Document")
(2 . "Incremental Changes")))
(defconst lsp--capabilities
`(("textDocumentSync" . ("Document sync method" .
((1 . "None")
(2 . "Send full contents")
(3 . "Send incremental changes."))))
("hoverProvider" . ("The server provides hover support" . boolean))
("completionProvider" . ("The server provides completion support" . boolean))
("definitionProvider" . ("The server provides goto definition 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))
(("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