From facb404e541ab9dca3f0833930f17842193b2da5 Mon Sep 17 00:00:00 2001 From: Jen-Chieh Shen Date: Sun, 17 Mar 2024 23:53:20 -0700 Subject: [PATCH] feat(lsp-cobol): Add automatic install language service capability (#4378) --- clients/lsp-cobol.el | 153 +++++++++++++++++++++++++++++++++++++++---- lsp-mode.el | 17 ++--- 2 files changed, 150 insertions(+), 20 deletions(-) diff --git a/clients/lsp-cobol.el b/clients/lsp-cobol.el index 682f36ef40..7378722cc2 100644 --- a/clients/lsp-cobol.el +++ b/clients/lsp-cobol.el @@ -32,29 +32,158 @@ :link '(url-link "https://github.com/eclipse-che4z/che-che4z-lsp-for-cobol") :package-version `(lsp-mode . "8.0.1")) +(defcustom lsp-cobol-server-path nil + "Path points for COBOL language service. + +This is only for development use." + :type 'string + :group 'lsp-cobol) + (defcustom lsp-cobol-port 1044 "Port to connect server to." :type 'integer :group 'lsp-cobol) +;; +;;; Util + +(defmacro lsp-cobol--mute-apply (&rest body) + "Execute BODY without message." + (declare (indent 0) (debug t)) + `(let (message-log-max) + (with-temp-message (or (current-message) nil) + (let ((inhibit-message t)) ,@body)))) + +(defun lsp-cobol--execute (cmd &rest args) + "Return non-nil if CMD executed succesfully with ARGS." + (save-window-excursion + (lsp-cobol--mute-apply + (= 0 (shell-command (concat cmd " " + (mapconcat #'shell-quote-argument + (cl-remove-if #'null args) + " "))))))) + +;; +;;; Installation + +(defcustom lsp-cobol-server-store-path + (expand-file-name "cobol/" lsp-server-install-dir) + "The path to the file in which COBOL language service will be stored." + :type 'file + :group 'lsp-cobol) + +(defcustom lsp-cobol-server-version "2.1.1" + "The COBOL language service version to install." + :type 'file + :group 'lsp-cobol) + +(defconst lsp-cobol-download-url-format + "https://github.com/eclipse-che4z/che-che4z-lsp-for-cobol/releases/download/%s/cobol-language-support-%s-%s-%s%s.vsix" + "Format to the download url link.") + +(defun lsp-cobol--server-url () + "Return Url points to the cobol language service's zip/tar file." + (let* ((x86 (string-prefix-p "x86_64" system-configuration)) + (arch (if x86 "x64" "arm64")) + (version lsp-cobol-server-version)) + (cl-case system-type + ((cygwin windows-nt ms-dos) + (format lsp-cobol-download-url-format + version "win32" arch version "-signed")) + (darwin + (format lsp-cobol-download-url-format + version "darwin" arch version "")) + (gnu/linux + (format lsp-cobol-download-url-format + version "linux" arch version ""))))) + +(defvar lsp-cobol--server-download-url (lsp-cobol--server-url) + "The actual url used to download language server.") + +(defvar lsp-cobol--downloaded-file (f-join lsp-cobol-server-store-path "temp.tar") + "The full file path after downloading the server zipped file.") + +(defun lsp-cobol--extract-compressed-file (callback) + "Install COBOL language service." + (cond ((file-exists-p lsp-cobol--downloaded-file) + ;; Suprisingly, you can just use `tar' to unzip a zip file on Windows. + ;; Therefore, just use the same command. + (lsp-cobol--execute "tar" "-xvzf" lsp-cobol--downloaded-file "-C" lsp-cobol-server-store-path) + ;; Delete the zip file. + (ignore-errors (delete-file lsp-cobol--downloaded-file))) + (t + (error "Can't extract the downloaded file: %s" lsp-cobol--downloaded-file))) + (funcall callback)) + +(defun lsp-cobol--stored-executable () + "Return the stored COBOL language service executable." + (executable-find + (f-join lsp-cobol-server-store-path + (concat "extension/server/native/" + (cl-case system-type + ((cygwin windows-nt ms-dos) "engine.exe") + (darwin "server-mac") + (gnu/linux "server-linux")))))) + +(lsp-dependency + 'cobol-ls + '(:system "cobol-ls") + `(:download :url ,lsp-cobol--server-download-url + :store-path ,lsp-cobol--downloaded-file)) + +;; +;;; Server + +;;;###autoload +(add-hook 'cobol-mode-hook #'lsp-cobol-start-ls) + +;;;###autoload +(defun lsp-cobol-start-ls () + "Start the COBOL language service." + (interactive) + (when-let ((exe (lsp-cobol--executable)) + ((lsp--port-available "localhost" lsp-cobol-port))) + (lsp-async-start-process #'ignore #'ignore exe))) + +;; +;;; Core + +(defun lsp-cobol--executable () + "Return the COBOL language service executable." + (or lsp-cobol-server-path + (lsp-cobol--stored-executable))) + +(defun lsp-cobol-server-start-fn (&rest _) + "Define COOBL language service start function." + `(,(lsp-cobol--executable))) + (defun lsp-cobol--tcp-connect-to-port () "Define a TCP connection to language server." (list - :connect (lambda (filter sentinel name _environment-fn _workspace) - (let* ((host "localhost") - (port lsp-cobol-port) - (tcp-proc (lsp--open-network-stream host port (concat name "::tcp")))) + :connect + (lambda (filter sentinel name _environment-fn _workspace) + (let* ((host "localhost") + (port lsp-cobol-port) + (tcp-proc (lsp--open-network-stream host port (concat name "::tcp")))) - (set-process-query-on-exit-flag tcp-proc nil) - (set-process-filter tcp-proc filter) - (set-process-sentinel tcp-proc sentinel) - (cons tcp-proc tcp-proc))) - :test? (lambda () t))) + ;; TODO: Same :noquery issue (see above) + (set-process-query-on-exit-flag tcp-proc nil) + (set-process-filter tcp-proc filter) + (set-process-sentinel tcp-proc sentinel) + (cons tcp-proc tcp-proc))) + :test? (lambda () (file-executable-p (lsp-cobol--executable))))) (lsp-register-client - (make-lsp-client :new-connection (lsp-cobol--tcp-connect-to-port) - :activation-fn (lsp-activate-on "cobol") - :server-id 'cobol)) + (make-lsp-client + :new-connection (lsp-cobol--tcp-connect-to-port) + :activation-fn (lsp-activate-on "cobol") + :priority -1 + :server-id 'cobol-ls + :download-server-fn + (lambda (_client callback error-callback _update?) + (lsp-package-ensure 'cobol-ls + (lambda () (lsp-cobol--extract-compressed-file callback)) + error-callback)))) (lsp-consistency-check lsp-cobol) diff --git a/lsp-mode.el b/lsp-mode.el index 34588dfd8e..dc1d001ec8 100644 --- a/lsp-mode.el +++ b/lsp-mode.el @@ -7495,16 +7495,17 @@ returned by COMMAND is available via `executable-find'" (cl-incf retries))))) (or connection (error "Port %s was never taken. Consider increasing `lsp-tcp-connection-timeout'." port)))) +(defun lsp--port-available (host port) + "Return non-nil if HOST and PORT are available." + (condition-case _err + (delete-process (open-network-stream "*connection-test*" nil host port :type 'plain)) + (file-error t))) + (defun lsp--find-available-port (host starting-port) "Find available port on HOST starting from STARTING-PORT." - (let ((success nil) - (port starting-port)) - (while (and (not success)) - (condition-case _err - (progn - (delete-process (open-network-stream "*connection-test*" nil host port :type 'plain)) - (cl-incf port)) - (file-error (setq success t)))) + (let ((port starting-port)) + (while (not (lsp--port-available host port)) + (cl-incf port)) port)) (defun lsp-tcp-connection (command-fn)