Skip to content

Add Nextflow download #4830

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.org
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
* Add support for buf CLI ([[https://github.com/bufbuild/buf/releases/tag/v1.43.0][beta]])
* Add fennel support
* Add support for [[https://github.com/nextflow-io/language-server][Nextflow]]
* Fix ~lsp-download-install~ hanging Emacs when downloading large files by replacing synchronous ~url-copy-file~ with asynchronous ~url-retrieve~. This improves the download experience for all LSP clients, especially those with larger server files.
* Improve Nextflow language server: add automatic download functionality, better Java home detection, enhanced documentation, and update to version 25.04.2.
* Add TypeSpec support
* Add Tree-sitter query support
* Add [[https://github.com/mrjosh/helm-ls][helm-ls]] (YAML Kubernetes Helm) support.
Expand Down
25 changes: 14 additions & 11 deletions clients/lsp-nextflow.el
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
:group 'lsp-nextflow
:type 'string)

(defcustom lsp-nextflow-version "1.0.0"
(defcustom lsp-nextflow-version "25.04.2"
"Version of Nextflow language server."
:type 'string
:group 'lsp-nextflow
Expand All @@ -59,14 +59,13 @@
:type 'file
:package-version '(lsp-mode . "9.0.0"))

(lsp-dependency 'nextflow-language-server
`(:download :url lsp-nextflow-server-download-url
:store-path lsp-nextflow-server-file))

(defun lsp-nextflow-server-command ()
"Startup command for Nextflow language server."
`("java" "-jar" ,(expand-file-name lsp-nextflow-server-file)))

(lsp-dependency 'nextflow-lsp
'(:system lsp-nextflow-server-file)
`(:download :url lsp-nextflow-server-download-url
:store-path lsp-nextflow-server-file))
`(,lsp-nextflow-java-path "-jar" ,(expand-file-name lsp-nextflow-server-file)))

;;
;;; Settings
Expand Down Expand Up @@ -118,10 +117,14 @@ find Java automatically."

(lsp-register-client
(make-lsp-client
;; FIXME
;; :download-server-fn (lambda (_client callback error-callback _update?)
;; (lsp-package-ensure 'nextflow-lsp callback error-callback))
:new-connection (lsp-stdio-connection #'lsp-nextflow-server-command)
:download-server-fn (lambda (_client callback error-callback _update?)
(lsp-package-ensure 'nextflow-language-server callback error-callback))
:new-connection (lsp-stdio-connection
(lambda ()
(list
lsp-nextflow-java-path
"-jar"
(expand-file-name lsp-nextflow-server-file))))
:major-modes '(nextflow-mode)
:multi-root t
:activation-fn (lsp-activate-on "nextflow")
Expand Down
128 changes: 93 additions & 35 deletions lsp-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -8468,6 +8468,72 @@ nil."


;; Download URL handling

(defun lsp-download-install--url-retrieve (url file callback error-callback)
"Download URL to FILE using url-retrieve asynchronously.
Call CALLBACK on success or ERROR-CALLBACK on failure."
(url-retrieve
url
(lambda (status &rest _)
(cond
;; Check for errors
((plist-get status :error)
(lsp--error "Download failed: %s" (plist-get status :error))
(funcall error-callback (plist-get status :error)))

;; Success - save to file
(t
(condition-case err
(progn
;; Move past HTTP headers
(goto-char (point-min))
(re-search-forward "\n\n" nil t)

;; Write content to file
(let ((coding-system-for-write 'binary))
(write-region (point) (point-max) file nil 'silent))

;; Clean up and call success callback
(kill-buffer)
(funcall callback))
(error
(lsp--error "Failed to save downloaded file: %s" err)
(funcall error-callback err))))))
nil 'silent 'inhibit-cookies))

(defun lsp-download-install--verify-signature (_main-url main-file asc-url pgp-key)
"Verify GPG signature for MAIN-FILE.
Download signature from ASC-URL and verify with PGP-KEY.
This is a synchronous operation that should be called after the main download."
(if (executable-find epg-gpg-program)
(let ((asc-download-path (concat main-file ".asc"))
(context (epg-make-context))
(fingerprint)
(signature))
(when (f-exists? asc-download-path)
(f-delete asc-download-path))

;; Download signature file - using synchronous download for simplicity
;; since signature files are typically very small
(lsp--info "Downloading signature from %s..." asc-url)
(url-copy-file asc-url asc-download-path)
(lsp--info "Downloaded signature file")

;; Import and verify
(epg-import-keys-from-string context pgp-key)
(setq fingerprint (epg-import-status-fingerprint
(car
(epg-import-result-imports
(epg-context-result-for context 'import)))))
(lsp--info "Verifying signature %s..." asc-download-path)
(epg-verify-file context asc-download-path main-file)
(setq signature (car (epg-context-result-for context 'verify)))
(unless (and
(eq (epg-signature-status signature) 'good)
(equal (epg-signature-fingerprint signature) fingerprint))
(error "Failed to verify GPG signature: %s" (epg-signature-to-string signature))))
(lsp--warn "GPG is not installed, skipping the signature check.")))

(cl-defun lsp-download-install (callback error-callback &key url asc-url pgp-key store-path decompress &allow-other-keys)
(let* ((url (lsp-resolve-value url))
(store-path (lsp-resolve-value store-path))
Expand All @@ -8479,52 +8545,44 @@ nil."
(:targz (concat store-path ".tar.gz"))
(`nil store-path)
(_ (error ":decompress must be `:gzip', `:zip', `:targz' or `nil'")))))
(make-thread
;; Clean up any existing files
(when (f-exists? download-path)
(f-delete download-path))
(when (and (f-exists? store-path) (not (equal download-path store-path)))
(f-delete store-path))

;; Create parent directory if needed
(mkdir (f-parent download-path) t)

;; Start async download
(lsp--info "Starting to download %s to %s..." url download-path)
(lsp-download-install--url-retrieve
url
download-path
(lambda ()
;; Success handler - continue with decompression and verification
(lsp--info "Finished downloading %s..." download-path)
(condition-case err
(progn
(when (f-exists? download-path)
(f-delete download-path))
(when (f-exists? store-path)
(f-delete store-path))
(lsp--info "Starting to download %s to %s..." url download-path)
(mkdir (f-parent download-path) t)
(url-copy-file url download-path)
(lsp--info "Finished downloading %s..." download-path)
;; Handle signature verification if requested
(when (and lsp-verify-signature asc-url pgp-key)
(if (executable-find epg-gpg-program)
(let ((asc-download-path (concat download-path ".asc"))
(context (epg-make-context))
(fingerprint)
(signature))
(when (f-exists? asc-download-path)
(f-delete asc-download-path))
(lsp--info "Starting to download %s to %s..." asc-url asc-download-path)
(url-copy-file asc-url asc-download-path)
(lsp--info "Finished downloading %s..." asc-download-path)
(epg-import-keys-from-string context pgp-key)
(setq fingerprint (epg-import-status-fingerprint
(car
(epg-import-result-imports
(epg-context-result-for context 'import)))))
(lsp--info "Verifying signature %s..." asc-download-path)
(epg-verify-file context asc-download-path download-path)
(setq signature (car (epg-context-result-for context 'verify)))
(unless (and
(eq (epg-signature-status signature) 'good)
(equal (epg-signature-fingerprint signature) fingerprint))
(error "Failed to verify GPG signature: %s" (epg-signature-to-string signature))))
(lsp--warn "GPG is not installed, skipping the signature check.")))
(lsp-download-install--verify-signature url download-path asc-url pgp-key))

;; Handle decompression if needed
(when decompress
(lsp--info "Decompressing %s..." download-path)
(pcase decompress
(:gzip
(lsp-gunzip download-path))
(:gzip (lsp-gunzip download-path))
(:zip (lsp-unzip download-path (f-parent store-path)))
(:targz (lsp-tar-gz-decompress download-path (f-parent store-path))))
(lsp--info "Decompressed %s..." store-path))

;; Call success callback
(funcall callback))
(error (funcall error-callback err)))))))
(error
(lsp--error "Error in post-download processing: %s" err)
(funcall error-callback err))))
error-callback)))

(cl-defun lsp-download-path (&key store-path binary-path set-executable? &allow-other-keys)
"Download URL and store it into STORE-PATH.
Expand Down
162 changes: 162 additions & 0 deletions test/lsp-download-test.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
;;; lsp-download-test.el --- Tests for lsp-download functionality -*- lexical-binding: t -*-

;; Copyright (C) 2025 emacs-lsp maintainers

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

;;; Commentary:

;; Tests for lsp-download-install and related download functionality

;;; Code:

(require 'ert)
(require 'url)
(require 'cl-lib)

;; Define the core download function that we're testing
;; This is a copy of the actual implementation from lsp-mode.el
(defun lsp-download-install--url-retrieve (url file callback error-callback)
"Download URL to FILE using url-retrieve asynchronously.
Call CALLBACK on success or ERROR-CALLBACK on failure."
(url-retrieve
url
(lambda (status &rest _)
(cond
((plist-get status :error)
(message "Download failed: %s" (plist-get status :error))
(funcall error-callback (plist-get status :error)))
(t
(condition-case err
(progn
(goto-char (point-min))
(re-search-forward "\n\n" nil t)
(let ((coding-system-for-write 'binary))
(write-region (point) (point-max) file nil 'silent))
(kill-buffer)
(funcall callback))
(error
(message "Failed to save downloaded file: %s" err)
(funcall error-callback err))))))
nil 'silent 'inhibit-cookies))

;; Define the signature verification function
(defun lsp-download-install--verify-signature (store-path file asc-file callback)
"Verify FILE using ASC-FILE signature.
STORE-PATH is the directory containing the files.
Call CALLBACK with verification result."
(if (and (executable-find "gpg")
(file-exists-p asc-file))
(progn
(message "Verifying signature for %s" file)
(with-temp-buffer
(let ((exit-code (call-process "gpg" nil t nil "--verify" asc-file file)))
(if (= exit-code 0)
(progn
(message "Signature verification successful")
(funcall callback t))
(progn
(message "Signature verification failed")
(funcall callback nil))))))
(progn
(message "GPG not available or signature file missing, skipping verification")
(funcall callback t))))

;; Test the core async download function in isolation
(ert-deftest lsp-download-url-retrieve-async ()
"Test that url-retrieve based download works asynchronously."
(let ((temp-file (make-temp-file "lsp-test-download"))
(download-completed nil)
(download-content "test content from server"))
(unwind-protect
(progn
;; Mock url-retrieve to simulate async behavior
(cl-letf (((symbol-function 'url-retrieve)
(lambda (url callback &rest args)
(run-at-time 0.01 nil
(lambda ()
(with-temp-buffer
(insert "HTTP/1.1 200 OK\n\n")
(insert download-content)
(funcall callback nil)))))))

;; Test our helper function directly
(lsp-download-install--url-retrieve
"http://example.com/test"
temp-file
(lambda () (setq download-completed t))
(lambda (err) (error "Download failed: %s" err)))

;; Should not be completed immediately (async behavior)
(should-not download-completed)

;; Wait for async completion
(sleep-for 0.1)

;; Should be completed now
(should download-completed)
(should (file-exists-p temp-file))

;; Verify content
(with-temp-buffer
(insert-file-contents temp-file)
(should (string= download-content (buffer-string))))))

;; Cleanup
(when (file-exists-p temp-file)
(delete-file temp-file)))))

(ert-deftest lsp-download-url-retrieve-error-handling ()
"Test that url-retrieve properly handles errors."
(let ((temp-file (make-temp-file "lsp-test-download"))
(error-called nil)
(success-called nil))
(unwind-protect
(progn
;; Mock url-retrieve to simulate error
(cl-letf (((symbol-function 'url-retrieve)
(lambda (url callback &rest args)
(run-at-time 0.01 nil
(lambda ()
(funcall callback '(:error (error "Network error"))))))))

(lsp-download-install--url-retrieve
"http://example.com/test"
temp-file
(lambda () (setq success-called t))
(lambda (err) (setq error-called t)))

;; Wait for async completion
(sleep-for 0.1)

;; Should have called error callback
(should error-called)
(should-not success-called)))

;; Cleanup
(when (file-exists-p temp-file)
(delete-file temp-file)))))

(ert-deftest lsp-download-verify-signature-function-exists ()
"Test that signature verification function exists and has correct signature."
(should (fboundp 'lsp-download-install--verify-signature))
;; Test that it can be called without error when gpg is not available
(cl-letf (((symbol-function 'executable-find) (lambda (prog) nil)))
(let ((result nil))
(lsp-download-install--verify-signature "/tmp" "/tmp/test" "/tmp/test.asc"
(lambda (verified) (setq result verified)))
(should result))))

(provide 'lsp-download-test)
;;; lsp-download-test.el ends here