;;; lsp-csharp.el --- description -*- lexical-binding: t; -*- ;; Copyright (C) 2019 Jostein Kjønigsen, Saulius Menkevicius ;; Author: Saulius Menkevicius ;; Keywords: ;; 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 . ;;; Commentary: ;; lsp-csharp client ;;; Code: (require 'lsp-mode) (require 'gnutls) (require 'f) (defgroup lsp-csharp nil "LSP support for C#, using the Omnisharp Language Server. Version 1.34.3 minimum is required." :group 'lsp-mode :link '(url-link "https://github.com/OmniSharp/omnisharp-roslyn")) (defcustom lsp-csharp-server-install-dir (f-join lsp-server-install-dir "omnisharp-roslyn/") "Installation directory for OmniSharp Roslyn server." :group 'lsp-csharp :type 'directory) (defcustom lsp-csharp-server-path nil "The path to the OmniSharp Roslyn language-server binary. Set this if you have the binary installed or have it built yourself." :group 'lsp-csharp :type '(string :tag "Single string value or nil")) (defun lsp-csharp--version-list-latest (lst) (->> lst (-sort (lambda (a b) (not (version<= (substring a 1) (substring b 1))))) cl-first)) (defun lsp-csharp--latest-installed-version () "Returns latest version of the server installed on the machine (if any)." (lsp-csharp--version-list-latest (when (f-dir? lsp-csharp-server-install-dir) (seq-filter (lambda (f) (s-starts-with-p "v" f)) (seq-map 'f-filename (f-entries lsp-csharp-server-install-dir)))))) (defun lsp-csharp--fetch-json (url) "Retrieves and parses JSON from URL." (with-temp-buffer (url-insert-file-contents url) (let ((json-false :false)) (json-read)))) (defun lsp-csharp--latest-available-version () "Returns latest version of the server available from github." (lsp-csharp--version-list-latest (seq-map (lambda (elt) (s-trim (cdr (assq 'name elt)))) (lsp-csharp--fetch-json "https://api.github.com/repos/OmniSharp/omnisharp-roslyn/releases")))) (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))) (defun lsp-csharp--server-bin (version) "The location of OmniSharp executable/script to use to start the server." (let ((server-dir (lsp-csharp--server-dir version))) (when server-dir (f-join server-dir (cond ((eq system-type 'windows-nt) "OmniSharp.exe") (t "run")))))) (defun lsp-csharp--server-package-filename () "Returns name of tgz/zip file to be used for downloading the server for auto installation. On Windows we're trying to avoid a crash starting 64bit .NET PE binaries in Emacs by using x86 version of omnisharp-roslyn on older (<= 26.4) versions of Emacs. See https://lists.nongnu.org/archive/html/bug-gnu-emacs/2017-06/msg00893.html" (cond ((eq system-type 'windows-nt) (if (and (string-match "^x86_64-.*" system-configuration) (version<= "26.4" emacs-version)) "omnisharp-win-x64.zip" "omnisharp-win-x86.zip")) ((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))) "omnisharp-linux-x64.tar.gz") (t "omnisharp-mono.tar.gz"))) (defun lsp-csharp--server-package-url (version) "Returns URL to tgz/zip file to be used for downloading the server VERSION for installation." (concat "https://github.com/OmniSharp/omnisharp-roslyn/releases/download" "/" version "/" (lsp-csharp--server-package-filename))) (defun lsp-csharp--extract-server (url filename reinstall) "Downloads and extracts a tgz/zip into the same directory." ;; remove the file if reinstall is set (if (and reinstall (f-exists-p filename)) (f-delete filename)) (lsp-csharp--download url filename) (let ((target-dir (f-dirname filename))) (message (format "lsp-csharp: extracting \"%s\" to \"%s\"" (f-filename filename) target-dir)) (lsp-csharp--extract filename target-dir))) (defun lsp-csharp-update-server () "Checks if the currently installed version (if any) is lower than then one available on github and if so, downloads and installs a newer version." (interactive) (let ((latest-version (lsp-csharp--latest-available-version)) (installed-version (lsp-csharp--latest-installed-version))) (if latest-version (progn (if (and latest-version (or (not installed-version) (version< (substring installed-version 1) (substring latest-version 1)))) (lsp-csharp--install-server latest-version nil)) (message "lsp-csharp-update-server: latest installed version is %s; latest available is %s" (lsp-csharp--latest-installed-version) latest-version)) (message "lsp-csharp-update-server: cannot retrieve latest version info")))) (defun lsp-csharp--install-server (update-version ask-confirmation) "Installs (or updates to UPDATE-VERSION) server binary unless it is already installed." (let ((installed-version (lsp-csharp--latest-installed-version)) (target-version (or update-version (lsp-csharp--latest-available-version)))) (if (and target-version (not (string-equal installed-version target-version))) (progn (message "lsp-csharp-update-server: current version is %s; installing %s.." (or installed-version "(none)") 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 ((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))) (mkdir new-server-dir t) (lsp-csharp--extract-server package-url (f-join new-server-dir package-filename) nil) (unless (and new-server-bin (file-exists-p new-server-bin)) (error "Failed to auto-install the server %s; file \"%s\" was not found" target-version new-server-bin)))))))) (defun lsp-csharp--get-or-install-server () "Resolves path to server binary installed, otherwise, if not found will ask the user if we can download and install it. Returns location of script or a binary to use to start the server." (let ((installed-bin (lsp-csharp--server-bin (lsp-csharp--latest-installed-version)))) (if (and installed-bin (file-exists-p installed-bin)) installed-bin (progn (lsp-csharp--install-server nil t) (let ((installed-bin (lsp-csharp--server-bin (lsp-csharp--latest-installed-version)))) (unless installed-bin (error "Server binary is required for LSP C# to work.")) installed-bin))))) (defun lsp-csharp--download (url filename) "Downloads file from URL as FILENAME. Will not do anything should 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))) (url-copy-file url filename nil)))) (defun lsp-csharp--extract (filename target-dir) "Extracts FILENAME which is a downloaded omnisharp-roslyn server tarball or a zip file (based on a current platform) to TARGET-DIR." (cond ((eq system-type 'windows-nt) ;; on windows, we attempt to use powershell v5+, available on Windows 10+ (let ((powershell-version (substring (shell-command-to-string "powershell -command \"(Get-Host).Version.Major\"") 0 -1))) (if (>= (string-to-number powershell-version) 5) (call-process "powershell" nil nil nil "-command" (concat "add-type -assembly system.io.compression.filesystem;" "[io.compression.zipfile]::ExtractToDirectory(\"" filename "\", \"" target-dir "\")")) (message (concat "lsp-csharp: for automatic server installation procedure" " to work on Windows you need to have powershell v5+ installed"))))) ((or (eq system-type 'gnu/linux) (eq system-type 'darwin)) (call-process "tar" nil nil t "xf" filename "-C" target-dir)) (t (error "lsp-csharp cannot extract \"%s\" on platform %s (yet)" filename system-type)))) (defun lsp-csharp--language-server-path () "Resolves path to use to start the server." (if lsp-csharp-server-path lsp-csharp-server-path (lsp-csharp--server-bin (lsp-csharp--latest-installed-version)))) (defun lsp-csharp--language-server-command () "Resolves path and arguments to use to start the server." (list (lsp-csharp--language-server-path) "-lsp")) (lsp-register-client (make-lsp-client :new-connection (lsp-stdio-connection #'lsp-csharp--language-server-command (lambda () (when-let ((binary (lsp-csharp--language-server-path))) (f-exists? binary)))) :major-modes '(csharp-mode) :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