config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

lsp-roslyn.el (15962B)


      1 ;;; lsp-roslyn.el --- description -*- lexical-binding: t; -*-
      2 
      3 ;; Copyright (C) 2023 Ruin0x11
      4 
      5 ;; Author: Ruin0x11 <ipickering2@gmail.com>
      6 ;; Keywords:
      7 
      8 ;; This program is free software; you can redistribute it and/or modify
      9 ;; it under the terms of the GNU General Public License as published by
     10 ;; the Free Software Foundation, either version 3 of the License, or
     11 ;; (at your option) any later version.
     12 
     13 ;; This program is distributed in the hope that it will be useful,
     14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     16 ;; GNU General Public License for more details.
     17 
     18 ;; You should have received a copy of the GNU General Public License
     19 ;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
     20 
     21 ;;; Commentary:
     22 
     23 ;; C# client using the Roslyn language server
     24 
     25 ;;; Code:
     26 
     27 (require 'lsp-mode)
     28 
     29 (defgroup lsp-roslyn nil
     30   "LSP support for the C# programming language, using the Roslyn language server."
     31   :link '(url-link "https://github.com/dotnet/roslyn/tree/main/src/Features/LanguageServer")
     32   :group 'lsp-mode
     33   :package-version '(lsp-mode . "8.0.0"))
     34 
     35 (defvar lsp-roslyn--stdpipe-path (expand-file-name
     36                                   "lsp-roslyn-stdpipe.ps1"
     37                                   (file-name-directory (file-truename load-file-name)))
     38   "Path to the `stdpipe' script.
     39 On Windows, this script is used as a proxy for the language server's named pipe.
     40 Unused on other platforms.")
     41 
     42 (defcustom lsp-roslyn-install-path (expand-file-name "roslyn" lsp-server-install-dir)
     43   "The path to install the Roslyn server to."
     44   :type 'string
     45   :package-version '(lsp-mode . "8.0.0")
     46   :group 'lsp-roslyn)
     47 
     48 (defcustom lsp-roslyn-server-dll-override-path nil
     49   "Custom path to Microsoft.CodeAnalysis.LanguageServer.dll."
     50   :type '(choice (const nil) string)
     51   :package-version '(lsp-mode . "8.0.0")
     52   :group 'lsp-roslyn)
     53 
     54 (defcustom lsp-roslyn-server-timeout-seconds 60
     55   "Amount of time to wait for Roslyn server startup, in seconds."
     56   :type 'integer
     57   :package-version '(lsp-mode . "8.0.0")
     58   :group 'lsp-roslyn)
     59 
     60 (defcustom lsp-roslyn-server-log-level "Information"
     61   "Log level for the Roslyn language server."
     62   :type '(choice (:tag "None" "Trace" "Debug" "Information" "Warning" "Error" "Critical"))
     63   :package-version '(lsp-mode . "8.0.0")
     64   :group 'lsp-roslyn)
     65 
     66 (defcustom lsp-roslyn-server-log-directory (concat (temporary-file-directory) (file-name-as-directory "lsp-roslyn"))
     67   "Log directory for the Roslyn language server."
     68   :type 'string
     69   :package-version '(lsp-mode . "8.0.0")
     70   :group 'lsp-roslyn)
     71 
     72 (defcustom lsp-roslyn-server-extra-args '()
     73   "Extra arguments for the Roslyn language server."
     74   :type '(repeat string)
     75   :package-version '(lsp-mode . "8.0.0")
     76   :group 'lsp-roslyn)
     77 
     78 (defcustom lsp-roslyn-dotnet-executable "dotnet"
     79   "Dotnet executable to use with the Roslyn language server."
     80   :type 'string
     81   :package-version '(lsp-mode . "8.0.0")
     82   :group 'lsp-roslyn)
     83 
     84 (defcustom lsp-roslyn-package-version "4.9.0-3.23604.10"
     85   "Version of the Roslyn package to install."
     86   :type 'string
     87   :package-version '(lsp-mode . "8.0.0")
     88   :group 'lsp-roslyn)
     89 
     90 (defvar lsp-roslyn--pipe-name nil)
     91 
     92 (defun lsp-roslyn--parse-pipe-name (pipe)
     93   (if (eq system-type 'windows-nt)
     94       (progn
     95         (string-match "\\([a-z0-9]+\\)$" pipe)
     96         (match-string 1 pipe))
     97     pipe))
     98 
     99 (defun lsp-roslyn--parent-process-filter (_process output)
    100   "Parses the named pipe's name that the Roslyn server process prints on stdout."
    101   (let* ((data (json-parse-string output :object-type 'plist))
    102          (pipe (plist-get data :pipeName)))
    103     (when pipe
    104       (setq lsp-roslyn--pipe-name (lsp-roslyn--parse-pipe-name pipe)))))
    105 
    106 (defun lsp-roslyn--make-named-pipe-process (filter sentinel environment-fn process-name stderr-buf)
    107   "Creates the process that will handle the JSON-RPC communication."
    108   (let* ((process-environment
    109           (lsp--compute-process-environment environment-fn))
    110          (default-directory (lsp--default-directory-for-connection)))
    111     (cond
    112      ((eq system-type 'windows-nt)
    113       (make-process
    114        :name process-name
    115        :connection-type 'pipe
    116        :buffer (format "*%s*" process-name)
    117        :coding 'no-conversion
    118        :filter filter
    119        :sentinel sentinel
    120        :stderr stderr-buf
    121        :noquery t
    122        :command (list "PowerShell" "-NoProfile" "-ExecutionPolicy" "Bypass" "-Command" lsp-roslyn--stdpipe-path "." lsp-roslyn--pipe-name)))
    123      (t (make-network-process
    124          :name process-name
    125          :remote lsp-roslyn--pipe-name
    126          :sentinel sentinel
    127          :filter filter
    128          :noquery t)))))
    129 
    130 (defun lsp-roslyn--connect (filter sentinel name environment-fn _workspace)
    131   "Creates a connection to the Roslyn language server's named pipe.
    132 
    133 First creates an instance of the language server process, then
    134 creates another process connecting to the named pipe it specifies."
    135   (setq lsp-roslyn--pipe-name nil)
    136   (let* ((parent-process-name name)
    137          (parent-stderr-buf (format "*%s::stderr*" parent-process-name))
    138          (command-process (make-process
    139                            :name parent-process-name
    140                            :buffer (generate-new-buffer-name parent-process-name)
    141                            :coding 'no-conversion
    142                            :filter 'lsp-roslyn--parent-process-filter
    143                            :sentinel sentinel
    144                            :stderr parent-stderr-buf
    145                            :command (append
    146                                      (list lsp-roslyn-dotnet-executable
    147                                            (lsp-roslyn--get-server-dll-path)
    148                                            (format "--logLevel=%s" lsp-roslyn-server-log-level)
    149                                            (format "--extensionLogDirectory=%s" lsp-roslyn-server-log-directory))
    150                                      lsp-roslyn-server-extra-args)
    151                            :noquery t)))
    152     (accept-process-output command-process lsp-roslyn-server-timeout-seconds) ; wait for JSON with pipe name to print on stdout, like {"pipeName":"\\\\.\\pipe\\d1b72351"}
    153     (when (not lsp-roslyn--pipe-name)
    154       (error "Failed to receieve pipe name from Roslyn server process"))
    155     (let* ((process-name (generate-new-buffer-name (format "%s-pipe" name)))
    156            (stderr-buf (format "*%s::stderr*" process-name))
    157            (communication-process
    158             (lsp-roslyn--make-named-pipe-process filter sentinel environment-fn process-name stderr-buf)))
    159       (with-current-buffer (get-buffer parent-stderr-buf)
    160         (special-mode))
    161       (when-let ((stderr-buffer (get-buffer stderr-buf)))
    162         (with-current-buffer stderr-buffer
    163           ;; Make the *NAME::stderr* buffer buffer-read-only, q to bury, etc.
    164           (special-mode))
    165         (set-process-query-on-exit-flag (get-buffer-process stderr-buffer) nil))
    166       (set-process-query-on-exit-flag command-process nil)
    167       (set-process-query-on-exit-flag communication-process nil)
    168       (cons communication-process communication-process))))
    169 
    170 (defun lsp-roslyn--uri-to-path (uri)
    171   "Convert a URI to a file path, without unhexifying."
    172   (let* ((url (url-generic-parse-url uri))
    173          (type (url-type url))
    174          (target (url-target url))
    175          (file
    176           (concat (decode-coding-string (url-filename url)
    177                                         (or locale-coding-system 'utf-8))
    178                   (when (and target
    179                              (not (s-match
    180                                    (rx "#" (group (1+ num)) (or "," "#")
    181                                        (group (1+ num))
    182                                        string-end)
    183                                    uri)))
    184                     (concat "#" target))))
    185          (file-name (if (and type (not (string= type "file")))
    186                         (if-let ((handler (lsp--get-uri-handler type)))
    187                             (funcall handler uri)
    188                           uri)
    189                       ;; `url-generic-parse-url' is buggy on windows:
    190                       ;; https://github.com/emacs-lsp/lsp-mode/pull/265
    191                       (or (and (eq system-type 'windows-nt)
    192                                (eq (elt file 0) ?\/)
    193                                (substring file 1))
    194                           file))))
    195     (->> file-name
    196          (concat (-some #'lsp--workspace-host-root (lsp-workspaces)))
    197          (lsp-remap-path-if-needed))))
    198 
    199 (defun lsp-roslyn--path-to-uri (path)
    200   "Convert PATH to a URI, without hexifying."
    201   (url-unhex-string (lsp--path-to-uri-1 path)))
    202 
    203 (lsp-defun lsp-roslyn--log-message (_workspace params)
    204   (let ((type (gethash "type" params))
    205         (mes (gethash "message" params)))
    206     (cl-case type
    207       (1 (lsp--error "%s" mes))   ; Error
    208       (2 (lsp--warn "%s" mes))    ; Warning
    209       (3 (lsp--info "%s" mes))    ; Info
    210       (t (lsp--info "%s" mes))))) ; Log
    211 
    212 (lsp-defun lsp-roslyn--on-project-initialization-complete (workspace _params)
    213   (lsp--info "%s: Project initialized successfully."
    214              (lsp--workspace-print workspace)))
    215 
    216 (defun lsp-roslyn--find-files-in-parent-directories (directory regex &optional result)
    217   "Search DIRECTORY for files matching REGEX and return their full paths if found."
    218   (let* ((parent-dir (file-truename (concat (file-name-directory directory) "../")))
    219          (found (directory-files directory 't regex))
    220          (result (append (or result '()) found)))
    221     (if (and (not (string= (file-truename directory) parent-dir))
    222              (< (length parent-dir) (length (file-truename directory))))
    223         (lsp-roslyn--find-files-in-parent-directories parent-dir regex result)
    224       result)))
    225 
    226 (defun lsp-roslyn--pick-solution-file-interactively (solution-files)
    227   (completing-read "Solution file for this workspace: " solution-files nil t))
    228 
    229 (defun lsp-roslyn--find-solution-file ()
    230   (let ((solutions (lsp-roslyn--find-files-in-parent-directories
    231                     (file-name-directory (buffer-file-name))
    232                     (rx (* any) ".sln" eos))))
    233     (cond
    234      ((not solutions) nil)
    235      ((eq (length solutions) 1) (cl-first solutions))
    236      (t (lsp-roslyn--pick-solution-file-interactively solutions)))))
    237 
    238 (defun lsp-roslyn-open-solution-file ()
    239   "Chooses the solution file to associate with the Roslyn language server."
    240   (interactive)
    241   (let ((solution-file (lsp-roslyn--find-solution-file)))
    242     (if solution-file
    243         (lsp-notify "solution/open" (list :solution (lsp--path-to-uri solution-file)))
    244       (lsp--error "No solution file was found for this workspace."))))
    245 
    246 (defun lsp-roslyn--on-initialized (_workspace)
    247   "Handler for Roslyn server initialization."
    248   (lsp-roslyn-open-solution-file))
    249 
    250 (defun lsp-roslyn--get-package-name ()
    251   "Gets the package name of the Roslyn language server."
    252   (format "microsoft.codeanalysis.languageserver.%s" (lsp-roslyn--get-rid)))
    253 
    254 (defun lsp-roslyn--get-server-dll-path ()
    255   "Gets the path to the language server DLL.
    256 Assumes it was installed with the server install function."
    257   (if lsp-roslyn-server-dll-override-path
    258       lsp-roslyn-server-dll-override-path
    259     (f-join lsp-roslyn-install-path "out"
    260             (lsp-roslyn--get-package-name)
    261             lsp-roslyn-package-version
    262             "content" "LanguageServer"
    263             (lsp-roslyn--get-rid)
    264             "Microsoft.CodeAnalysis.LanguageServer.dll")))
    265 
    266 (defun lsp-roslyn--get-rid ()
    267   "Retrieves the .NET Runtime Identifier (RID) for the current system."
    268   (let* ((is-x64 (string-match-p "x86_64" system-configuration))
    269          (is-x86 (and (string-match-p "x86" system-configuration) (not is-x64)))
    270          (is-arm (string-match-p "arm" system-configuration)))
    271     (if-let ((platform-name (cond
    272                              ((eq system-type 'gnu/linux) "linux")
    273                              ((eq system-type 'darwin) "osx")
    274                              ((eq system-type 'windows-nt) "win")))
    275              (arch-name (cond
    276                          (is-x64 "x64")
    277                          (is-x86 "x86")
    278                          (is-arm "arm64"))))
    279         (format "%s-%s" platform-name arch-name)
    280       (error "Unsupported platform: %s (%s)" system-type system-configuration))))
    281 
    282 ;; Adapted from roslyn.nvim's version
    283 (defconst lsp-roslyn--temp-project-nuget-config
    284   "<?xml version=\"1.0\" encoding=\"utf-8\"?>
    285 <configuration>
    286   <packageSources>
    287     <clear />
    288     <add key=\"msft_consumption\" value=\"https://pkgs.dev.azure.com/azure-public/vside/_packaging/msft_consumption/nuget/v3/index.json\" />
    289   </packageSources>
    290   <disabledPackageSources>
    291     <clear />
    292   </disabledPackageSources>
    293 </configuration>"
    294   "The nuget.config to use when downloading Roslyn.")
    295 
    296 ;; Adapted from roslyn.nvim's version
    297 (defun lsp-roslyn--temp-project-csproj (pkg-name pkg-version)
    298   "Generates a temporary .csproj to use for downloading the language server."
    299   (format
    300    "<Project Sdk=\"Microsoft.Build.NoTargets/1.0.80\">
    301     <PropertyGroup>
    302         <!-- Changes the global packages folder -->
    303         <RestorePackagesPath>out</RestorePackagesPath>
    304         <!-- This is not super relevant, as long as your SDK version supports it. -->
    305         <TargetFramework>net7.0</TargetFramework>
    306         <!-- If a package is resolved to a fallback folder, it may not be downloaded -->
    307         <DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
    308         <!-- We don't want to build this project, so we do not need the reference assemblies for the framework we chose -->
    309         <AutomaticallyUseReferenceAssemblyPackages>false</AutomaticallyUseReferenceAssemblyPackages>
    310     </PropertyGroup>
    311     <ItemGroup>
    312         <PackageDownload Include=\"%s\" version=\"[%s]\" />
    313     </ItemGroup>
    314 </Project>"
    315    pkg-name pkg-version))
    316 
    317 (defun lsp-roslyn--download-server (_client callback error-callback update?)
    318   "Downloads the Roslyn language server to `lsp-roslyn-install-path'.
    319 CALLBACK is called when the download finish successfully otherwise
    320 ERROR-CALLBACK is called.
    321 UPDATE is non-nil if it is already downloaded.
    322 FORCED if specified with prefix argument."
    323 
    324   (let ((pkg-name (lsp-roslyn--get-package-name)))
    325     (when update?
    326       (ignore-errors (delete-directory lsp-roslyn-install-path t)))
    327     (unless (f-exists? lsp-roslyn-install-path)
    328       (mkdir lsp-roslyn-install-path 'create-parent))
    329     (f-write-text lsp-roslyn--temp-project-nuget-config
    330                   'utf-8 (expand-file-name "nuget.config" lsp-roslyn-install-path))
    331     (f-write-text (lsp-roslyn--temp-project-csproj pkg-name lsp-roslyn-package-version)
    332                   'utf-8 (expand-file-name "DownloadRoslyn.csproj" lsp-roslyn-install-path))
    333     (lsp-async-start-process
    334      callback
    335      error-callback
    336      lsp-roslyn-dotnet-executable "restore" lsp-roslyn-install-path
    337      (format "/p:PackageName=%s" pkg-name)
    338      (format "/p:PackageVersion=%s" lsp-roslyn-package-version))))
    339 
    340 (defun lsp-roslyn--make-connection ()
    341   (list :connect (lambda (f s n e w) (lsp-roslyn--connect f s n e w))
    342         :test? (lambda () (f-exists? (lsp-roslyn--get-server-dll-path)))))
    343 
    344 (lsp-register-client
    345  (make-lsp-client :new-connection (lsp-roslyn--make-connection)
    346                   :priority 0
    347                   :server-id 'csharp-roslyn
    348                   :activation-fn (lsp-activate-on "csharp")
    349                   :notification-handlers (ht ("window/logMessage" 'lsp-roslyn--log-message)
    350                                              ("workspace/projectInitializationComplete" 'lsp-roslyn--on-project-initialization-complete))
    351 
    352                   ;; These two functions are the same as lsp-mode's except they do not
    353                   ;; (un)hexify URIs.
    354                   :path->uri-fn 'lsp-roslyn--path-to-uri
    355                   :uri->path-fn 'lsp-roslyn--uri-to-path
    356 
    357                   :initialized-fn #'lsp-roslyn--on-initialized
    358                   :download-server-fn #'lsp-roslyn--download-server))
    359 
    360 (provide 'lsp-roslyn)
    361 ;;; lsp-roslyn.el ends here