diff --git a/lsp-clients.el b/lsp-clients.el index a72d7c6dbea..793cbb2d933 100644 --- a/lsp-clients.el +++ b/lsp-clients.el @@ -153,19 +153,17 @@ This directory shoud contain a file matching groovy-language-server-*.jar" :server-id 'groovy-ls)) ;;; TypeScript/JavaScript + +(lsp-dependency javascript-typescript-langserver + (:system "javascript-typescript-stdio") + (:npm :package "javascript-typescript-langserver" + :path ".bin/javascript-typescript-stdio")) + (defgroup lsp-typescript-javascript nil "Support for TypeScript/JavaScript, using Sourcegraph's JavaScript/TypeScript language server." :group 'lsp-mode :link '(url-link "https://github.com/sourcegraph/javascript-typescript-langserver")) -(defcustom lsp-clients-javascript-typescript-server "javascript-typescript-stdio" - "The javascript-typescript-stdio executable to use. -Leave as just the executable name to use the default behavior of -finding the executable with variable `exec-path'." - :group 'lsp-typescript-javascript - :risky t - :type 'file) - (defcustom lsp-clients-typescript-javascript-server-args '() "Extra arguments for the typescript-language-server language server." :group 'lsp-typescript-javascript @@ -179,13 +177,17 @@ finding the executable with variable `exec-path'." (lsp-register-client (make-lsp-client :new-connection (lsp-stdio-connection (lambda () - (cons lsp-clients-javascript-typescript-server + (cons (lsp-package-path 'javascript-typescript-langserver) lsp-clients-typescript-javascript-server-args))) :activation-fn 'lsp-typescript-javascript-tsx-jsx-activate-p :priority -3 :completion-in-comments? t - :ignore-messages '("readFile .*? requested by TypeScript but content not available") - :server-id 'jsts-ls)) + :server-id 'jsts-ls + :download-server-fn (lambda (_client callback error-callback _update?) + (lsp-package-ensure + 'javascript-typescript-langserver + callback + error-callback)))) ;;; TypeScript @@ -194,14 +196,6 @@ finding the executable with variable `exec-path'." :group 'lsp-mode :link '(url-link "https://github.com/theia-ide/typescript-language-server")) -(defcustom lsp-clients-typescript-server "typescript-language-server" - "The typescript-language-server executable to use. -Leave as just the executable name to use the default behavior of -finding the executable with variable `exec-path'." - :group 'lsp-typescript - :risky t - :type 'file) - (defcustom lsp-clients-typescript-server-args '("--stdio") "Extra arguments for the typescript-language-server language server." :group 'lsp-typescript @@ -230,18 +224,37 @@ directory containing the package. Example: (and name location)) xs))))) +(lsp-dependency typescript-language-server + (:system "typescript-language-server") + (:npm :package "typescript-language-server" + :path ".bin/typescript-language-server")) + +(lsp-dependency typescript + (:system "tsserver") + (:npm :package "typescript" + :path ".bin/tsserver")) + (lsp-register-client (make-lsp-client :new-connection (lsp-stdio-connection (lambda () - (cons lsp-clients-typescript-server + (cons (lsp-package-path 'typescript-language-server) lsp-clients-typescript-server-args))) :activation-fn 'lsp-typescript-javascript-tsx-jsx-activate-p :priority -2 :completion-in-comments? t :initialization-options (lambda () (list :plugins lsp-clients-typescript-plugins - :logVerbosity lsp-clients-typescript-log-verbosity)) + :logVerbosity lsp-clients-typescript-log-verbosity + :tsServerPath (lsp-package-path 'typescript))) :ignore-messages '("readFile .*? requested by TypeScript but content not available") - :server-id 'ts-ls)) + :server-id 'ts-ls + :download-server-fn (lambda (_client callback error-callback _update?) + (lsp-package-ensure + 'typescript + (-partial #'lsp-package-ensure + 'typescript-language-server + callback + error-callback) + error-callback)))) @@ -338,19 +351,18 @@ particular FILE-NAME and MODE." (defun lsp-php--create-connection () "Create lsp connection." - (plist-put - (lsp-stdio-connection - (lambda () lsp-clients-php-server-command)) - :test? (lambda () - (if (and (cdr lsp-clients-php-server-command) - (eq (string-match-p "php[0-9.]*\\'" (car lsp-clients-php-server-command)) 0)) - ;; Start with the php command and the list has more elems. Test the existence of the PHP script. - (let ((php-file (nth 1 lsp-clients-php-server-command))) - (or (file-exists-p php-file) - (progn - (lsp-log "%s is not present." php-file) - nil))) - t)))) + (lsp-stdio-connection + (lambda () lsp-clients-php-server-command) + (lambda () + (if (and (cdr lsp-clients-php-server-command) + (eq (string-match-p "php[0-9.]*\\'" (car lsp-clients-php-server-command)) 0)) + ;; Start with the php command and the list has more elems. Test the existence of the PHP script. + (let ((php-file (nth 1 lsp-clients-php-server-command))) + (or (file-exists-p php-file) + (progn + (lsp-log "%s is not present." php-file) + nil))) + t)))) (lsp-register-client (make-lsp-client :new-connection (lsp-php--create-connection) @@ -390,7 +402,9 @@ particular FILE-NAME and MODE." "LSP support for OCaml, using ocaml-lsp-server." :group 'lsp-mode :link '(url-link "https://github.com/ocaml/ocaml-lsp")) + (define-obsolete-variable-alias 'lsp-merlin 'lsp-ocaml-lsp-server) +(define-obsolete-variable-alias 'lsp-merlin-command 'lsp-ocaml-lsp-server-command) (defcustom lsp-ocaml-lsp-server-command '("ocamllsp") @@ -400,7 +414,6 @@ particular FILE-NAME and MODE." (string :tag "Single string value") (repeat :tag "List of string values" string))) -(define-obsolete-variable-alias 'lsp-merlin-command 'lsp-ocaml-lsp-server-command) (lsp-register-client (make-lsp-client @@ -734,12 +747,11 @@ responsiveness at the cost of possibile stability issues." (defun lsp-clients-emmy-lua--create-connection () "Create connection to emmy lua language server." - (plist-put - (lsp-stdio-connection - (lambda () - (list lsp-clients-emmy-lua-java-path "-jar" lsp-clients-emmy-lua-jar-path))) - :test? (lambda () - (f-exists? lsp-clients-emmy-lua-jar-path)))) + (lsp-stdio-connection + (lambda () + (list lsp-clients-emmy-lua-java-path "-jar" lsp-clients-emmy-lua-jar-path)) + (lambda () + (f-exists? lsp-clients-emmy-lua-jar-path)))) (lsp-register-client (make-lsp-client :new-connection (lsp-clients-emmy-lua--create-connection) diff --git a/lsp-csharp.el b/lsp-csharp.el index 8a61778f661..6a337f6ce9a 100644 --- a/lsp-csharp.el +++ b/lsp-csharp.el @@ -35,7 +35,7 @@ Version 1.34.3 minimum is required." :link '(url-link "https://github.com/OmniSharp/omnisharp-roslyn")) (defcustom lsp-csharp-server-install-dir - (locate-user-emacs-file ".cache/omnisharp-roslyn/") + (f-join lsp-server-install-dir "omnisharp-roslyn/") "Installation directory for OmniSharp Roslyn server." :group 'lsp-csharp :type 'directory) @@ -77,7 +77,7 @@ Set this if you have the binary installed or have it built yourself." (defun lsp-csharp--server-dir (version) "The location of the installed OmniSharp server for VERSION." (when version - (f-join (expand-file-name lsp-csharp-server-install-dir) version))) + (f-join (expand-file-name lsp-csharp-server-install-dir) version))) (defun lsp-csharp--server-bin (version) "The location of OmniSharp executable/script to use to start the server." @@ -101,8 +101,8 @@ of Emacs. See https://lists.nongnu.org/archive/html/bug-gnu-emacs/2017-06/msg008 ((eq system-type 'darwin) "omnisharp-osx.tar.gz") ((and (eq system-type 'gnu/linux) - (or (eq (string-match "^x86_64" system-configuration) 0) - (eq (string-match "^i[3-6]86" system-configuration) 0))) + (or (eq (string-match "^x86_64" system-configuration) 0) + (eq (string-match "^i[3-6]86" system-configuration) 0))) "omnisharp-linux-x64.tar.gz") (t "omnisharp-mono.tar.gz"))) @@ -159,18 +159,16 @@ available on github and if so, downloads and installs a newer version." target-version) (if (or (not ask-confirmation) (yes-or-no-p (format "OmniSharp Roslyn Server %s. Do you want to download and install %s now?" - (if installed-version - (format "can be updated, currently installed version is %s" installed-version) - "is not installed") - target-version))) - (let ((cache-dir (expand-file-name (locate-user-emacs-file ".cache/"))) - (o-r-dir (expand-file-name (locate-user-emacs-file ".cache/omnisharp-roslyn/"))) - (new-server-dir (lsp-csharp--server-dir target-version)) + (if installed-version + (format "can be updated, currently installed version is %s" installed-version) + "is not installed") + target-version))) + (let ((new-server-dir (lsp-csharp--server-dir target-version)) (new-server-bin (lsp-csharp--server-bin target-version)) (package-filename (lsp-csharp--server-package-filename)) (package-url (lsp-csharp--server-package-url target-version))) - (f-mkdir cache-dir o-r-dir new-server-dir) + (mkdir new-server-dir t) (lsp-csharp--extract-server package-url (f-join new-server-dir package-filename) @@ -201,12 +199,12 @@ the file exist already." (unless (f-exists-p filename) (message (format "lsp-csharp: downloading from \"%s\"..." url)) (let ((gnutls-algorithm-priority - (if (and (not gnutls-algorithm-priority) - (boundp 'libgnutls-version) - (>= libgnutls-version 30603) - (version<= emacs-version "26.2")) - "NORMAL:-VERS-TLS1.3" - gnutls-algorithm-priority))) + (if (and (not gnutls-algorithm-priority) + (boundp 'libgnutls-version) + (>= libgnutls-version 30603) + (version<= emacs-version "26.2")) + "NORMAL:-VERS-TLS1.3" + gnutls-algorithm-priority))) (url-copy-file url filename nil)))) (defun lsp-csharp--extract (filename target-dir) @@ -242,13 +240,24 @@ Will attempt to install the server if it is not installed already for the current platform." (if lsp-csharp-server-path (list lsp-csharp-server-path "-lsp") - (list (lsp-csharp--get-or-install-server) "-lsp"))) + (list (lsp-csharp--server-bin (lsp-csharp--latest-installed-version)) "-lsp"))) (lsp-register-client (make-lsp-client :new-connection (lsp-stdio-connection - #'lsp-csharp--language-server-command) + #'lsp-csharp--language-server-command + (lambda () + (when-let (binary (lsp-csharp--server-bin (lsp-csharp--latest-installed-version))) + (f-exists? binary)))) + :major-modes '(csharp-mode) - :server-id 'csharp)) + :server-id 'csharp + :download-server-fn + (lambda (_client callback error-callback _update?) + (condition-case err + (progn + (lsp-csharp--install-server nil nil) + (funcall callback)) + (error (funcall error-callback (error-message-string err))))))) (provide 'lsp-csharp) ;;; lsp-csharp.el ends here diff --git a/lsp-eslint.el b/lsp-eslint.el index 7c540c871eb..2049ec448ac 100644 --- a/lsp-eslint.el +++ b/lsp-eslint.el @@ -198,16 +198,14 @@ (interactive) (lsp-send-execute-command "eslint.applyAllFixes" (vector (lsp--versioned-text-document-identifier)))) - - (lsp-register-client (make-lsp-client :new-connection - (plist-put (lsp-stdio-connection - (lambda () lsp-eslint-server-command)) - :test? (lambda () - (and (cl-second lsp-eslint-server-command) - (file-exists-p (cl-second lsp-eslint-server-command))))) + (lsp-stdio-connection + (lambda () lsp-eslint-server-command) + (lambda () + (and (cl-second lsp-eslint-server-command) + (file-exists-p (cl-second lsp-eslint-server-command))))) :activation-fn (lambda (filename &optional _) (or (string-match-p (rx (one-or-more anything) "." (or "ts" "js" "jsx" "tsx" "html" "vue")) diff --git a/lsp-fsharp.el b/lsp-fsharp.el index 7f67eaf22bf..eebd4d2e3bf 100644 --- a/lsp-fsharp.el +++ b/lsp-fsharp.el @@ -40,7 +40,7 @@ (const :tag "Use .Net Framework" net-framework)) :package-version '(lsp-mode . "6.1")) -(defcustom lsp-fsharp-server-install-dir (locate-user-emacs-file "fsautocomplete/") +(defcustom lsp-fsharp-server-install-dir (f-join lsp-server-install-dir "fsautocomplete/") "Install directory for fsautocomplete server. The slash is expected at the end." :group 'lsp-fsharp @@ -179,16 +179,7 @@ disable if `--backgorund-service-enabled' is not used" ".exe"))) (expand-file-name (concat "fsautocomplete" file-ext) lsp-fsharp-server-install-dir))) -(defun lsp-fsharp--fsac-locate () - "Return the location of the fsautocomplete langauge server." - (let ((fsac (lsp-fsharp--fsac-cmd))) - (unless (file-exists-p fsac) - (if (yes-or-no-p "Server is not installed. Do you want to install it?") - (lsp-fsharp--fsac-install) - (error "LSP F# cannot be started without FsAutoComplete Server"))) - fsac)) - -(defun lsp-fsharp--fsac-install () +(defun lsp-fsharp--fsac-install (_client callback _error-callback _update?) "Download the latest version of fsautocomplete and extract it to `lsp-fsharp-server-install-dir'." (let* ((temp-file (make-temp-file "fsautocomplete" nil ".zip")) (install-dir-full (expand-file-name lsp-fsharp-server-install-dir)) @@ -197,18 +188,19 @@ disable if `--backgorund-service-enabled' is not used" (t (user-error (format "Unable to unzip server - file %s cannot be extracted, please extract it manually" temp-file)))))) (url-copy-file lsp-fsharp-server-download-url temp-file t) (shell-command unzip-script) - (shell-command (format "%s %s --version" (lsp-fsharp--fsac-runtime-cmd) (lsp-fsharp--fsac-cmd))))) + (shell-command (format "%s %s --version" (lsp-fsharp--fsac-runtime-cmd) (lsp-fsharp--fsac-cmd))) + (funcall callback))) (defun lsp-fsharp-update-fsac () "Update fsautocomplete to the latest version." (interactive) (-let [install-dir (f-expand lsp-fsharp-server-install-dir)] (f-delete install-dir t) - (lsp-fsharp--fsac-install))) + (lsp-fsharp--fsac-install nil #'ignore #'lsp--error t))) (defun lsp-fsharp--make-launch-cmd () "Build the command required to launch fsautocomplete." - (append (list (lsp-fsharp--fsac-runtime-cmd) (lsp-fsharp--fsac-locate) "--background-service-enabled") + (append (list (lsp-fsharp--fsac-runtime-cmd) (lsp-fsharp--fsac-cmd) "--background-service-enabled") lsp-fsharp-server-args)) (defun lsp-fsharp--project-list () @@ -260,7 +252,9 @@ disable if `--backgorund-service-enabled' is not used" ("FSharp.EnableReferenceCodeLens" lsp-fsharp-enable-reference-code-lens t))) (lsp-register-client - (make-lsp-client :new-connection (lsp-stdio-connection 'lsp-fsharp--make-launch-cmd) + (make-lsp-client :new-connection (lsp-stdio-connection + #'lsp-fsharp--make-launch-cmd + (lambda () (f-exists? (lsp-fsharp--fsac-cmd)))) :major-modes '(fsharp-mode) :notification-handlers (ht ("fsharp/notifyCancel" #'ignore) ("fsharp/notifyWorkspace" #'ignore) @@ -275,7 +269,8 @@ disable if `--backgorund-service-enabled' is not used" (lsp-configuration-section "fsharp")) (lsp-fsharp--workspace-load (lsp-fsharp--project-list))))) - :server-id 'fsac)) + :server-id 'fsac + :download-server-fn #'lsp-fsharp--fsac-install)) (provide 'lsp-fsharp) ;;; lsp-fsharp.el ends here diff --git a/lsp-mode.el b/lsp-mode.el index ad2609f306f..1610fbcfea8 100644 --- a/lsp-mode.el +++ b/lsp-mode.el @@ -31,13 +31,14 @@ (require 'project) (require 'flymake)) -(require 'rx) (require 'bindat) +(require 'cl-generic) (require 'cl-lib) (require 'compile) (require 'dash) (require 'dash-functional) (require 'em-glob) +(require 'ewoc) (require 'f) (require 'filenotify) (require 'files) @@ -46,19 +47,19 @@ (require 'inline) (require 'json) (require 'lv) +(require 'markdown-mode) (require 'network-stream) (require 'pcase) +(require 'rx) (require 's) (require 'seq) (require 'spinner) (require 'subr-x) +(require 'tree-widget) (require 'url-parse) (require 'url-util) (require 'widget) (require 'xref) -(require 'tree-widget) -(require 'markdown-mode) -(require 'ewoc) (require 'yasnippet nil t) (declare-function company-mode "ext:company") @@ -458,15 +459,15 @@ diagnostics have changed." :type 'hook :group 'lsp-mode) +(define-obsolete-variable-alias 'lsp-workspace-folders-changed-hook + 'lsp-workspace-folders-changed-functions "lsp-mode 6.3") + (defcustom lsp-workspace-folders-changed-functions nil "Hooks to run after the folders has changed. The hook will receive two parameters list of added and removed folders." :type 'hook :group 'lsp-mode) -(define-obsolete-variable-alias 'lsp-workspace-folders-changed-hook - 'lsp-workspace-folders-changed-functions "lsp-mode 6.3") - (defcustom lsp-eldoc-hook '(lsp-hover) "Hooks to run for eldoc." :type 'hook @@ -1091,7 +1092,10 @@ INHERIT-INPUT-METHOD will be proxied to `completing-read' without changes." ;; associated handler function passing three arguments, the ‘lsp--workspace’ ;; object, the deserialized request parameters and the callback which accept ;; result as its parameter. - (async-request-handlers (make-hash-table :test 'equal) :read-only t)) + (async-request-handlers (make-hash-table :test 'equal) :read-only t) + (download-server-fn) + (download-in-progress?) + (buffers)) ;; from http://emacs.stackexchange.com/questions/8082/how-to-get-buffer-position-given-line-number-and-column-number (defun lsp--line-character-to-point (line character) @@ -2204,8 +2208,6 @@ CALLBACK - callback for the lenses." If WORKSPACE is not specified defaults to lsp--cur-workspace." (setf (lsp--workspace-status-string (or workspace lsp--cur-workspace)) status-string)) -(add-to-list 'global-mode-string '(t (:eval (-keep #'lsp--workspace-status-string (lsp-workspaces))))) - (defun lsp-session-set-metadata (key value &optional _workspace) "Associate KEY with VALUE in the WORKSPACE metadata. If WORKSPACE is not provided current workspace will be used." @@ -2883,7 +2885,8 @@ in that particular folder." (and lsp-signature-auto-activate (lsp--capability "signatureHelpProvider"))) (lambda () - (lsp--maybe-enable-signature-help trigger-characters))))) + (lsp--maybe-enable-signature-help trigger-characters)))) + (status '(t (:eval (-keep #'lsp--workspace-status-string (lsp-workspaces)))))) (cond (lsp-managed-mode (add-function :before-until (local 'eldoc-documentation-function) #'lsp-eldoc-function) @@ -2906,7 +2909,11 @@ in that particular folder." (when lsp-enable-xref (add-hook 'xref-backend-functions #'lsp--xref-backend nil t)) (when (and lsp-enable-text-document-color (lsp--capability "colorProvider")) - (add-hook 'lsp-on-change-hook #'lsp--document-color nil t))) + (add-hook 'lsp-on-change-hook #'lsp--document-color nil t)) + + (setq-local global-mode-string (if (-contains? global-mode-string status) + global-mode-string + (cons status global-mode-string)))) (t (setq-local indent-region-function nil) (remove-function (local 'eldoc-documentation-function) #'lsp-eldoc-function) @@ -2937,7 +2944,8 @@ in that particular folder." (lsp-lens-mode -1) (remove-hook 'xref-backend-functions #'lsp--xref-backend t) - (remove-hook 'lsp-on-change-hook #'lsp--document-color t))))) + (remove-hook 'lsp-on-change-hook #'lsp--document-color t) + (setq-local global-mode-string (remove status global-mode-string)))))) (defun lsp--text-document-did-open () "'document/didOpen event." @@ -3395,40 +3403,40 @@ Added to `after-change-functions'." (lambda (it) (with-lsp-workspace it (with-demoted-errors "Error in ‘lsp-on-change’: %S" - (save-match-data - ;; A (revert-buffer) call with the 'preserve-modes parameter (eg, as done - ;; by auto-revert-mode) will cause this handler to get called with a nil - ;; buffer-file-name. We need the buffer-file-name to send notifications; - ;; so we skip handling revert-buffer-caused changes and instead handle - ;; reverts separately in lsp-on-revert - (let ((sync-kind (or lsp-document-sync-method - (lsp--workspace-sync-method lsp--cur-workspace)))) - (cond - ((eq lsp--sync-incremental sync-kind) - (lsp-notify - "textDocument/didChange" - `(:textDocument - ,(lsp--versioned-text-document-identifier) - :contentChanges ,(vector (lsp--text-document-content-change-event - start end length))))) - ((eq lsp--sync-full sync-kind) - (if lsp-debounce-full-sync-notifications - (progn - (-some-> lsp--delay-timer cancel-timer) - (cl-pushnew (cons lsp--cur-workspace (current-buffer)) - lsp--delayed-requests - :test 'equal) - (setq lsp--delay-timer (run-with-idle-timer - lsp-debounce-full-sync-notifications-interval - nil - (lambda () - (setq lsp--delay-timer nil) - (lsp--flush-delayed-changes))))) - (lsp-notify - "textDocument/didChange" - `(:textDocument - ,(lsp--versioned-text-document-identifier) - :contentChanges ,(vector (lsp--full-change-event)))))))))))) + (save-match-data + ;; A (revert-buffer) call with the 'preserve-modes parameter (eg, as done + ;; by auto-revert-mode) will cause this handler to get called with a nil + ;; buffer-file-name. We need the buffer-file-name to send notifications; + ;; so we skip handling revert-buffer-caused changes and instead handle + ;; reverts separately in lsp-on-revert + (let ((sync-kind (or lsp-document-sync-method + (lsp--workspace-sync-method lsp--cur-workspace)))) + (cond + ((eq lsp--sync-incremental sync-kind) + (lsp-notify + "textDocument/didChange" + `(:textDocument + ,(lsp--versioned-text-document-identifier) + :contentChanges ,(vector (lsp--text-document-content-change-event + start end length))))) + ((eq lsp--sync-full sync-kind) + (if lsp-debounce-full-sync-notifications + (progn + (-some-> lsp--delay-timer cancel-timer) + (cl-pushnew (cons lsp--cur-workspace (current-buffer)) + lsp--delayed-requests + :test 'equal) + (setq lsp--delay-timer (run-with-idle-timer + lsp-debounce-full-sync-notifications-interval + nil + (lambda () + (setq lsp--delay-timer nil) + (lsp--flush-delayed-changes))))) + (lsp-notify + "textDocument/didChange" + `(:textDocument + ,(lsp--versioned-text-document-identifier) + :contentChanges ,(vector (lsp--full-change-event)))))))))))) (lsp-workspaces)) ;; force cleanup overlays after each change (lsp--remove-overlays 'lsp-highlight) @@ -3888,6 +3896,9 @@ If INCLUDE-DECLARATION is non-nil, request the server to include declarations." (defun dash-expand:&lsp-wks (key source) `(,(intern-soft (format "lsp--workspace-%s" (eval key) )) ,source)) +(defun dash-expand:&lsp-cln (key source) + `(,(intern-soft (format "lsp--client-%s" (eval key) )) ,source)) + (defun lsp--point-on-highlight? () (-some? (lambda (overlay) (overlay-get overlay 'lsp-highlight)) @@ -4936,7 +4947,6 @@ REFERENCES? t when METHOD returns references." (defalias 'lsp-on-open #'lsp--text-document-did-open) (defalias 'lsp-on-save #'lsp--text-document-did-save) - (defun lsp--set-configuration (settings) "Set the SETTINGS for the lsp server." (lsp-notify "workspace/didChangeConfiguration" `(:settings , settings))) @@ -5464,14 +5474,17 @@ Ignore non-boolean keys whose value is nil." (eval value)))) process-environment)))) -(defun lsp-stdio-connection (command) +(defun lsp-stdio-connection (command &optional test-command) "Returns a connection property list using COMMAND. -COMMAND can be: -A string, denoting the command to launch the language server. -A list of strings, denoting an executable with its command line arguments. -A function, that either returns a string or a list of strings. -In all cases, the launched language server should send and receive messages on -standard I/O." +COMMAND can be: A string, denoting the command to launch the +language server. A list of strings, denoting an executable with +its command line arguments. A function, that either returns a +string or a list of strings. In all cases, the launched language +server should send and receive messages on standard I/O. +TEST-COMMAND is a function with no arguments which returns +whether the command is present or not. When not specified +`lsp-mode' will check whether the first element of the list +returned by COMMAND is available via `executable-find'" (cl-check-type command (or string function (and list @@ -5498,7 +5511,9 @@ standard I/O." (set-process-query-on-exit-flag proc nil) (set-process-query-on-exit-flag (get-buffer-process stderr-buf) nil) (cons proc proc)))) - :test? (lambda () (-> command lsp-resolve-final-function lsp-server-present?)))) + :test? (or + test-command + (lambda () (-> command lsp-resolve-final-function lsp-server-present?))))) (defun lsp--open-network-stream (host port name) "Open network stream to HOST:PORT. @@ -5832,28 +5847,187 @@ SESSION is the active session." (eq client client-or-list)))))) lsp-disabled-clients)) + +;; download server + +(defcustom lsp-server-install-dir (expand-file-name + (locate-user-emacs-file (f-join ".cache" "lsp"))) + "Directory in which the servers will be installed." + :risky t + :type 'directory + :package-version '(lsp-mode . "6.3")) + +(defvar lsp--dependencies (ht)) + +(defmacro lsp-dependency (name &rest definitions) + (declare (debug (form body)) + (indent 1)) + `(puthash (quote ,name) (quote ,definitions) lsp--dependencies)) + +(defun lsp--server-binary-present? (client) + (unless (equal (lsp--client-server-id client) 'lsp-pwsh) + (condition-case () + (-some-> client lsp--client-new-connection (plist-get :test?) funcall) + (error nil) + (args-out-of-range nil)))) + +(defun lsp--download-status () + (-some--> #'lsp--client-download-in-progress? + (lsp--filter-clients it) + (-map (-compose #'symbol-name #'lsp--client-server-id) it) + (format "%s" it) + (propertize it 'face 'success) + (format "Installing following servers: %s" it) + (propertize it + 'local-map (make-mode-line-mouse-map + 'mouse-1 (lambda () + (interactive) + (switch-to-buffer (get-buffer-create " *lsp-install*")))) + 'mouse-face 'highlight))) + +(defun lsp--install-server-internal (client) + (setf (lsp--client-download-in-progress? client) t) + (add-to-list 'global-mode-string '(t (:eval (lsp--download-status)))) + (cl-flet ((done + (success? &optional error-message) + (-let [(&lsp-cln 'server-id 'buffers) client] + (setf (lsp--client-download-in-progress? client) nil + (lsp--client-buffers client) nil) + (if success? + (lsp--info "Server %s downloaded, auto-starting in %s buffers." server-id + (length buffers)) + (lsp--error "Server %s install process failed with the following error message: %s. Check *lsp-install* buffer." + server-id + error-message)) + (seq-do + (lambda (buffer) + (when (buffer-live-p buffer) + (with-current-buffer buffer + (setq global-mode-string (-remove-item '(t (:eval (lsp--download-status))) + global-mode-string)) + (lsp)))) + buffers) + (unless (lsp--filter-clients #'lsp--client-download-in-progress?) + (setq global-mode-string (-remove-item '(t (:eval (lsp--download-status))) + global-mode-string)))))) + (funcall + (lsp--client-download-server-fn client) + client + (lambda () (done t)) + (lambda (msg) (done nil msg)) + t)) + + (lsp--info "Download %s started." (lsp--client-server-id client))) + +(defun lsp-install-server () + (interactive) + (lsp--install-server-internal + (lsp--completing-read + "Select server to install: " + (or (->> lsp-clients + (ht-values) + (-filter (-andfn + (-not #'lsp--server-binary-present?) + (-not #'lsp--client-download-in-progress?) + #'lsp--client-download-server-fn))) + (user-error "There are no servers with automatic installation.")) + (-compose #'symbol-name #'lsp--client-server-id) + nil + t))) + +(defun lsp-async-start-process (callback error-callback &rest command) + (make-process + :name (cl-first command) + :command command + :sentinel (lambda (proc _) + (when (eq 'exit (process-status proc)) + (if (zerop (process-exit-status proc)) + (funcall callback) + (display-buffer " *lsp-install*") + (funcall error-callback + (format "Async process '%s' failed with exit code %d" + (process-name proc) (process-exit-status proc)))))) + :stdout " *lsp-install*" + :buffer " *lsp-install*" + :noquery t)) + +(defvar lsp-deps-providers + (list :npm (list :path #'lsp--npm-dependency-path + :install #'lsp--npm-dependency-download) + :system (list :path #'executable-find))) + +(defun lsp-package-path (dependency) + "Path to the DEPENDENCY each of the registered providers." + (let (path) + (-first (-lambda ((provider . rest)) + (setq path (-some-> lsp-deps-providers + (plist-get provider) + (plist-get :path) + (apply rest)))) + (gethash dependency lsp--dependencies)) + path)) + +(defun lsp-package-ensure (dependency callback error-callback) + "Asynchronously ensure a package." + (-first (-lambda ((provider . rest)) + (-some-> lsp-deps-providers + (plist-get provider) + (plist-get :install) + (apply (cl-list* callback error-callback rest)))) + (gethash dependency lsp--dependencies))) + + +;; npm handling + +(cl-defun lsp--npm-dependency-path (&key package path &allow-other-keys) + (let ((path (f-join lsp-server-install-dir "npm" package "node_modules" path))) + (unless (f-exists? path) + (error "The package %s is not installed. Unable to find %s." package path)) + path)) + +(cl-defun lsp--npm-dependency-download (callback error-callback &key package &allow-other-keys) + (if-let (npm-binary (executable-find "npm")) + (lsp-async-start-process + callback + error-callback + npm-binary + "--prefix" + (f-join lsp-server-install-dir "npm" package) + "install" + package) + (funcall callback nil "Make sure you have npm installed and on the path."))) + + (defun lsp--matching-clients? (client) - (and (and ;; both file and client remote or both local - (eq (---truthy? (file-remote-p buffer-file-name)) - (---truthy? (lsp--client-remote? client))) - (if-let (activation-fn (lsp--client-activation-fn client)) - (funcall activation-fn buffer-file-name major-mode) - (-contains? (lsp--client-major-modes client) major-mode)) - (-some-> client lsp--client-new-connection (plist-get :test?) funcall)) - (or (null lsp-enabled-clients) - (or (member (lsp--client-server-id client) lsp-enabled-clients) - (ignore (lsp--info "Client %s is not in lsp-enabled-clients" - (lsp--client-server-id client))))) - (not (lsp--client-disabled-p major-mode (lsp--client-server-id client))))) + (and + ;; both file and client remote or both local + (eq (---truthy? (file-remote-p buffer-file-name)) + (---truthy? (lsp--client-remote? client))) + + ;; activation function or major-mode match. + (if-let (activation-fn (lsp--client-activation-fn client)) + (funcall activation-fn buffer-file-name major-mode) + (-contains? (lsp--client-major-modes client) major-mode)) + + ;; check whether it is enabled if `lsp-enabled-clients' is not null + (or (null lsp-enabled-clients) + (or (member (lsp--client-server-id client) lsp-enabled-clients) + (ignore (lsp--info "Client %s is not in lsp-enabled-clients" + (lsp--client-server-id client))))) + + ;; check whether it is not disabled. + (not (lsp--client-disabled-p major-mode (lsp--client-server-id client))))) + +(defun lsp--filter-clients (pred) + (->> lsp-clients hash-table-values (-filter pred))) (defun lsp--find-clients () "Find clients which can handle BUFFER-MAJOR-MODE. SESSION is the currently active session. The function will also pick only remote enabled clients in case the FILE-NAME is on remote machine and vice versa." - (-when-let (matching-clients (->> lsp-clients - hash-table-values - (-filter #'lsp--matching-clients?))) + (-when-let (matching-clients (lsp--filter-clients (-andfn #'lsp--matching-clients? + #'lsp--server-binary-present?))) (lsp-log "Found the following clients for %s: %s" buffer-file-name (s-join ", " @@ -5862,7 +6036,7 @@ remote machine and vice versa." (lsp--client-server-id client) (lsp--client-priority client))) matching-clients))) - (-let* (((add-on-clients main-clients) (-separate 'lsp--client-add-on? matching-clients)) + (-let* (((add-on-clients main-clients) (-separate #'lsp--client-add-on? matching-clients)) (selected-clients (if-let (main-client (and main-clients (--max-by (> (lsp--client-priority it) (lsp--client-priority other)) @@ -6389,19 +6563,59 @@ argument ask the user to select which language server to start. " (when (and lsp-auto-configure lsp-auto-require-clients) (require 'lsp-clients)) - (when (and (buffer-file-name) - (setq-local lsp--buffer-workspaces - (or (lsp--try-open-in-library-workspace) - (lsp--try-project-root-workspaces (equal arg '(4)) - (and arg (not (equal arg 1))))))) - (lsp-mode 1) - (when lsp-auto-configure (lsp--auto-configure)) - - (setq-local lsp-buffer-uri (lsp--buffer-uri)) - - (lsp--info "Connected to %s." - (apply 'concat (--map (format "[%s]" (lsp--workspace-print it)) - lsp--buffer-workspaces))))) + (when (buffer-file-name) + (let (clients + (matching-clients (lsp--filter-clients (-andfn #'lsp--matching-clients? + #'lsp--server-binary-present?)))) + (cond + (matching-clients + (when (setq-local lsp--buffer-workspaces + (or (lsp--try-open-in-library-workspace) + (lsp--try-project-root-workspaces (equal arg '(4)) + (and arg (not (equal arg 1)))))) + (lsp-mode 1) + (when lsp-auto-configure (lsp--auto-configure)) + (setq-local lsp-buffer-uri (lsp--buffer-uri)) + (lsp--info "Connected to %s." + (apply 'concat (--map (format "[%s]" (lsp--workspace-print it)) + lsp--buffer-workspaces))))) + ;; look for servers which are currently being downloaded. + ((setq clients (lsp--filter-clients (-andfn #'lsp--matching-clients? + #'lsp--client-download-in-progress?))) + (lsp--info "There are language server(%s) installation in progress. +The server(s) will be started in the buffer when it has finished." + (-map #'lsp--client-server-id clients)) + (seq-do (lambda (client) + (cl-pushnew (current-buffer) (lsp--client-buffers client))) + clients)) + ;; look for servers to install + ((setq clients (lsp--filter-clients (-andfn #'lsp--matching-clients? + #'lsp--client-download-server-fn + (-not #'lsp--client-download-in-progress?)))) + (let ((client (lsp--completing-read + (concat "Unable to find installed server supporting this file. " + "The following servers could be installed automatically: ") + clients + (-compose #'symbol-name #'lsp--client-server-id) + nil + t))) + (cl-pushnew (current-buffer) (lsp--client-buffers client)) + (lsp--install-server-internal client))) + ;; no clients present + ((setq clients (unless matching-clients + (lsp--filter-clients (-andfn #'lsp--matching-clients? + (-not #'lsp--server-binary-present?))))) + (when (y-or-n-p (format "The following servers support current file but do not have automatic installation configuration: %s +You may find the installation instructions at https://github.com/emacs-lsp/lsp-mode/#supported-languages. Do you want open it?" + (mapconcat (lambda (client) + (symbol-name (lsp--client-server-id client))) + clients + " "))) + (browse-url "https://github.com/emacs-lsp/lsp-mode/#supported-languages"))) + ;; no matches + ((-> #'lsp--matching-clients? lsp--filter-clients not) + (lsp--error "There are no language servers supporting current mode %s registered with `lsp-mode'." + major-mode)))))) (defun lsp--init-if-visible () "Run `lsp' for the current buffer if the buffer is visible. diff --git a/lsp-pwsh.el b/lsp-pwsh.el index 4f773fcfb38..b2ef2b8d0f5 100755 --- a/lsp-pwsh.el +++ b/lsp-pwsh.el @@ -209,8 +209,7 @@ Valid values are 'Diagnostic', 'Verbose', 'Normal', 'Warning', and 'Error'" ("powershell.helpCompletion" lsp-pwsh-help-completion))) ;; lsp-pwsh custom variables -(defcustom lsp-pwsh-ext-path (expand-file-name ".extension/pwsh" - user-emacs-directory) +(defcustom lsp-pwsh-ext-path (f-join lsp-server-install-dir "pwsh") "The path to powershell vscode extension." :type 'string :group 'lsp-pwsh @@ -236,11 +235,7 @@ Must not nil.") (defun lsp-pwsh--command () "Return the command to start server." - (unless (and lsp-pwsh-exe (file-executable-p lsp-pwsh-exe)) - (user-error "Use `lsp-pwsh-exe' with the value of `%s' is not a valid powershell binary" - lsp-pwsh-exe)) - ;; Download extension - (lsp-pwsh-setup) + `(,lsp-pwsh-exe "-NoProfile" "-NonInteractive" "-NoLogo" ,@(if (eq system-type 'windows-nt) '("-ExecutionPolicy" "Bypass")) "-OutputFormat" "Text" @@ -256,8 +251,7 @@ Must not nil.") ;; "-AdditionalModules" "@('PowerShellEditorServices.VSCode')" "-Stdio" "-BundledModulesPath" ,lsp-pwsh-dir - "-FeatureFlags" "@()" - )) + "-FeatureFlags" "@()")) (defun lsp-pwsh--extra-init-params () "Return form describing parameters for language server.") @@ -292,18 +286,20 @@ Must not nil.") (lsp-register-client (make-lsp-client - :new-connection (lsp-stdio-connection #'lsp-pwsh--command) + :new-connection (lsp-stdio-connection #'lsp-pwsh--command + (lambda () + (f-exists? lsp-pwsh-dir))) :major-modes lsp-pwsh--major-modes :server-id 'pwsh-ls :priority -1 :multi-root t :initialization-options #'lsp-pwsh--extra-init-params - :notification-handlers (lsp-ht ("powerShell/executionStatusChanged" #'ignore) - ("output" #'ignore)) - :action-handlers (lsp-ht ("PowerShell.ApplyCodeActionEdits" - #'lsp-pwsh--apply-code-action-edits) - ("PowerShell.ShowCodeActionDocumentation" - #'lsp-pwsh--show-code-action-document)) + :notification-handlers (ht ("powerShell/executionStatusChanged" #'ignore) + ("output" #'ignore)) + :action-handlers (ht ("PowerShell.ApplyCodeActionEdits" + #'lsp-pwsh--apply-code-action-edits) + ("PowerShell.ShowCodeActionDocumentation" + #'lsp-pwsh--show-code-action-document)) :initialized-fn (lambda (w) (with-lsp-workspace w (lsp--set-configuration @@ -311,7 +307,7 @@ Must not nil.") (let ((caps (lsp--workspace-server-capabilities w))) (ht-set caps "documentRangeFormattingProvider" t) (ht-set caps "documentFormattingProvider" t))) - )) + :download-server-fn #'lsp-pwsh-setup)) ;; Compatibility (with-eval-after-load 'company-lsp @@ -321,13 +317,6 @@ Must not nil.") (not (memq major-mode lsp-pwsh--major-modes))) '((name . --force-post-completion-for-pwsh)))) -;;; Utils -(defconst lsp-pwsh-unzip-script "\"%s\" -noprofile -noninteractive -nologo -ex bypass -command Expand-Archive -Path '%s' -DestinationPath '%s'" - "Powershell script to unzip vscode extension package file.") - -(defconst lsp-pwsh-editor-svcs-dl-script "\"%s\" -noprofile -noninteractive -nologo -ex bypass -command Invoke-WebRequest -UseBasicParsing -uri '%s' -outfile '%s'" - "Command executed via `shell-command' to download the latest PowerShellEditorServices release.") - (defcustom lsp-pwsh-github-asset-url "https://github.com/%s/%s/releases/latest/download/%s" "GitHub latest asset template url." @@ -335,26 +324,34 @@ Must not nil.") :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) -(defun lsp-pwsh--get-extension (url dest) - "Get extension from URL and extract to DEST." - (let ((temp-file (make-temp-file "ext" nil ".zip"))) - ;; since we know it's installed, use powershell to download the file - ;; (and avoid url.el bugginess or additional libraries) - (shell-command (format lsp-pwsh-editor-svcs-dl-script lsp-pwsh-exe url temp-file)) - (message "lsp-pwsh: Downloading done!") - (if (file-exists-p dest) (delete-directory dest 'recursive)) - (shell-command (format lsp-pwsh-unzip-script lsp-pwsh-exe temp-file dest)) - (message "lsp-pwsh: Finish unzip!"))) - -(defun lsp-pwsh-setup (&optional forced) +(defun lsp-pwsh-setup (_client callback error-callback update) "Downloading PowerShellEditorServices to `lsp-pwsh-dir'. FORCED if specified with prefix argument." - (interactive "P") - (let ((parent-dir (file-name-directory lsp-pwsh-dir))) - (unless (and (not forced) (file-exists-p parent-dir)) - (lsp-pwsh--get-extension - (format lsp-pwsh-github-asset-url "PowerShell" "PowerShellEditorServices" "PowerShellEditorServices.zip") - parent-dir)))) + + (unless (and lsp-pwsh-exe (file-executable-p lsp-pwsh-exe)) + (user-error "Use `lsp-pwsh-exe' with the value of `%s' is not a valid powershell binary" + lsp-pwsh-exe)) + + (let ((url (format lsp-pwsh-github-asset-url "PowerShell" + "PowerShellEditorServices" "PowerShellEditorServices.zip")) + (temp-file (make-temp-file "ext" nil ".zip"))) + (unless (and (not update) (file-exists-p lsp-pwsh-dir)) + ;; since we know it's installed, use powershell to download the file + ;; (and avoid url.el bugginess or additional libraries) + (lsp-async-start-process + (lambda () + (lsp--info "lsp-pwsh: Downloading done!") + (when (file-exists-p lsp-pwsh-dir) (delete-directory lsp-pwsh-dir 'recursive)) + (lsp-async-start-process + callback + error-callback + lsp-pwsh-exe "-noprofile" "-noninteractive" "-nologo" + "-ex" "bypass" "-command" "Expand-Archive" + "-Path" temp-file "-DestinationPath" (file-name-directory lsp-pwsh-dir))) + error-callback + lsp-pwsh-exe + "-noprofile" "-noninteractive" "-nologo" "-ex" "bypass" "-command" + "Invoke-WebRequest" "-UseBasicParsing" "-uri" url "-outfile" temp-file)))) (provide 'lsp-pwsh) ;;; lsp-pwsh.el ends here diff --git a/lsp-vhdl.el b/lsp-vhdl.el index b805c0694a8..d6b4c5909c3 100644 --- a/lsp-vhdl.el +++ b/lsp-vhdl.el @@ -75,9 +75,9 @@ VHDL LS: A complete VHDL language server protocol implementation with diagnostic "Returns lsp-stdio-connection based on the selected server" (lsp-vhdl--set-server-path) (lsp-vhdl--set-server-args) - (plist-put - (lsp-stdio-connection (lambda () (list (plist-get lsp-vhdl--params 'server-path) (plist-get lsp-vhdl--params 'server-args)))) - :test? (lambda () (f-executable? (plist-get lsp-vhdl--params 'server-path))))) + (lsp-stdio-connection + (lambda () (list (plist-get lsp-vhdl--params 'server-path) (plist-get lsp-vhdl--params 'server-args))) + (lambda () (f-executable? (plist-get lsp-vhdl--params 'server-path))))) (defun lsp-vhdl--set-server-path() "Set path to server binary based on selection in lsp-vhdl-server." diff --git a/lsp-xml.el b/lsp-xml.el index d5adb493baf..2344e9353bb 100644 --- a/lsp-xml.el +++ b/lsp-xml.el @@ -197,11 +197,9 @@ Newlines and excess whitespace are removed." :package-version '(lsp-mode . "6.1")) (defun lsp-xml--create-connection () - (plist-put - (lsp-stdio-connection - (lambda () lsp-xml-server-command)) - :test? (lambda () - (f-exists? lsp-xml-jar-file)))) + (lsp-stdio-connection + (lambda () lsp-xml-server-command) + (lambda () (f-exists? lsp-xml-jar-file)))) (lsp-register-client (make-lsp-client :new-connection (lsp-xml--create-connection)