From d0bc7cba5a66fa05dfc589265221108ea8e32d5f Mon Sep 17 00:00:00 2001 From: Ivan Date: Wed, 27 Nov 2019 10:41:54 +0200 Subject: [PATCH] Implement automatic downloading facilities - Fixes #506 - Implemented base facilities for downloading and installing language servers - added new field :download-server-fn in lsp--client structure which will be called with (client callback update?). - Implemented automatic installation via npm for jsts-ls and ts-ls. - All servers(when feasible) should be installed under `lsp-server-install-dir`. @akirak - check the cl-defgenerics in lsp-mode - let me know if they are sufficient to implement the nix variands. @razzmatazz - I have ported CSharp implementation but I havent converted it into async. @seagle0128 - lsp-python-ms is not ported. In general, it will work as it is but it will ask to install the server even if you are in different file. @TOTBWF - F# is ported and working. --- lsp-clients.el | 80 ++++++++-------- lsp-csharp.el | 24 +++-- lsp-eslint.el | 12 +-- lsp-fsharp.el | 25 ++--- lsp-mode.el | 256 ++++++++++++++++++++++++++++++++++++++++--------- lsp-pwsh.el | 82 ++++++++-------- lsp-vhdl.el | 6 +- lsp-xml.el | 8 +- 8 files changed, 326 insertions(+), 167 deletions(-) diff --git a/lsp-clients.el b/lsp-clients.el index 26b2e63cd0c..2190b6248c2 100644 --- a/lsp-clients.el +++ b/lsp-clients.el @@ -158,14 +158,6 @@ This directory shoud contain a file matching groovy-language-server-*.jar" :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 +171,18 @@ 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 :npm "javascript-typescript-langserver" ".bin/javascript-typescript-stdio") 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 update?) + (lsp-package-ensure + :npm + "javascript-typescript-langserver" + (lambda (success? msg) + (funcall callback success? msg)))))) ;;; TypeScript @@ -194,14 +191,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 @@ -232,16 +221,27 @@ directory containing the package. Example: (lsp-register-client (make-lsp-client :new-connection (lsp-stdio-connection (lambda () - (cons lsp-clients-typescript-server + (cons (lsp-package-path :npm "typescript-language-server" ".bin/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 :npm "typescript" ".bin/tsserver"))) :ignore-messages '("readFile .*? requested by TypeScript but content not available") - :server-id 'ts-ls)) + :server-id 'ts-ls + :download-server-fn (lambda (_client callback update?) + (lsp-package-ensure + :npm + "typescript" + (lambda (success? msg) + (lsp-package-ensure + :npm + "typescript-language-server" + (lambda (success? msg) + (funcall callback success? msg)))))))) @@ -338,19 +338,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) @@ -734,12 +733,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..77a032ab5a4 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) @@ -163,14 +163,12 @@ available on github and if so, downloads and installs a newer 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)) + (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) @@ -242,13 +240,23 @@ 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 update?) + (condition-case err + (progn + (lsp-csharp--install-server nil nil) + (funcall callback t nil)) + (error (funcall callback nil (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..3af02a3badd 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 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,7 +188,8 @@ 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))) + (run-with-idle-timer 0 nil (lambda () (funcall callback t nil))))) (defun lsp-fsharp-update-fsac () "Update fsautocomplete to the latest version." @@ -208,7 +200,7 @@ disable if `--backgorund-service-enabled' is not used" (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 e826dbf1677..611818e58b5 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") @@ -1087,7 +1088,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) @@ -2200,8 +2204,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." @@ -2879,7 +2881,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 (lsp--workspace-status))))) (cond (lsp-managed-mode (add-function :before-until (local 'eldoc-documentation-function) #'lsp-eldoc-function) @@ -2902,7 +2905,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) @@ -2933,7 +2940,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." @@ -3883,6 +3891,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)) @@ -4931,7 +4942,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))) @@ -5455,14 +5465,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 @@ -5489,7 +5502,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. @@ -5823,19 +5838,124 @@ 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 "lsp-servers")) + "Directory in which the servers will be installed." + :risky t + :type 'directory + :package-version '(lsp-mode . "6.3")) + +(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--install-server-internal (client) + (setf (lsp--client-download-in-progress? client) t) + (funcall (lsp--client-download-server-fn client) + client + (lambda (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? + (progn + (lsp--info "Server %s downloaded, auto-starting in %s buffers." server-id + (length buffers)) + (seq-do + (lambda (buffer) + (when (buffer-live-p buffer) + (with-current-buffer buffer (lsp)))) + buffers)) + (lsp--error "Server %s install process failed with the following error message: %s. Check *lsp-install* buffer." + server-id + error-message)))) + 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 &rest command) + (make-process + :name (cl-first command) + :command command + :sentinel (lambda (proc status) + (when (eq 'exit (process-status proc)) + (if (zerop (process-exit-status proc)) + (funcall callback t nil) + (display-buffer " *lsp-install*") + (funcall callback + nil + (format "Async process '%s' failed with exit code %d" + (process-name proc) (process-exit-status proc)))))) + :stdout " *lsp-install*" + :buffer " *lsp-install*" + :noquery t)) + +(cl-defgeneric lsp-package-path (type package relative-path) + "Return package path for PACKAGE with TYPE. +RELATIVE-PATH is the path to the binary in the package.") + +(cl-defgeneric lsp-package-ensure (type package callback) + "Installs PACKAGE of TYPE and calls CALLBACK with (success? error-msg) params.") + + +;; npm +(cl-defmethod lsp-package-path ((_type (eql :npm)) package path) + (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-defmethod lsp-package-ensure ((_type (eql :npm)) package callback) + (if-let (npm-binary (executable-find "npm")) + (lsp-async-start-process + 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--find-clients () "Find clients which can handle BUFFER-MAJOR-MODE. @@ -5844,7 +5964,8 @@ 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?))) + (-filter (-andfn #'lsp--server-binary-present? + #'lsp--matching-clients?)))) (lsp-log "Found the following clients for %s: %s" buffer-file-name (s-join ", " @@ -6380,19 +6501,64 @@ 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) + (cond + ((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 to install + ((setq clients (->> lsp-clients + hash-table-values + (-filter (-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))) + ;; look for servers which are currently being downloaded. + ((setq clients (->> lsp-clients + hash-table-values + (-filter (-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)) + ((setq clients (->> lsp-clients + hash-table-values + (-filter (-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"))) + ((->> lsp-clients + hash-table-values + (-filter #'lsp--matching-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..0269ba863e2 100755 --- a/lsp-pwsh.el +++ b/lsp-pwsh.el @@ -32,7 +32,7 @@ (defgroup lsp-pwsh nil "LSP support for PowerShell, using the PowerShellEditorServices." - :group 'lsp-mode + :group 'lsp :package-version '(lsp-mode . "6.2")) ;; PowerShell vscode flags @@ -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 () + (-> lsp-pwsh-dir file-name-directory f-exists?))) :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,33 @@ 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 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 ((parent-dir (file-name-directory lsp-pwsh-dir)) + (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 parent-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 (process) + (lsp--info "lsp-pwsh: Downloading done!") + (when (file-exists-p parent-dir) (delete-directory parent-dir 'recursive)) + (lsp-async-start-process + (lambda (process) + (funcall callback t)) + lsp-pwsh-exe "-noprofile" "-noninteractive" "-nologo" + "-ex" "bypass" "-command" "Expand-Archive" + "-Path" temp-file "-DestinationPath" parent-dir)) + 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 cc5c99efacf..a54d61921f7 100644 --- a/lsp-vhdl.el +++ b/lsp-vhdl.el @@ -76,9 +76,9 @@ HDL Checker: A wrapper for third party tools such as GHDL, ModelSim, Vivado Simu "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)