config

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

lsp-roslyn.el (15650B)


      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/LanguageServer")
     32   :group 'lsp-mode
     33   :package-version '(lsp-mode . "8.0.0"))
     34 
     35 (defconst lsp-roslyn--stdpipe-path (expand-file-name
     36                                     "lsp-roslyn-stdpipe.ps1"
     37                                     (file-name-directory (locate-library "lsp-roslyn")))
     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.13.0-2.24564.12"
     85   "Version of the Roslyn package to install.
     86 Gotten from https://dev.azure.com/azure-public/vside/_artifacts/feed/vs-impl/NuGet/Microsoft.CodeAnalysis.LanguageServer.win-x64"
     87   :type 'string
     88   :package-version '(lsp-mode . "8.0.0")
     89   :group 'lsp-roslyn)
     90 
     91 (defvar lsp-roslyn--pipe-name nil)
     92 
     93 (defun lsp-roslyn--parse-pipe-name (pipe)
     94   (if (eq system-type 'windows-nt)
     95       (progn
     96         (string-match "\\([a-z0-9]+\\)$" pipe)
     97         (match-string 1 pipe))
     98     pipe))
     99 
    100 (defun lsp-roslyn--parent-process-filter (_process output)
    101   "Parses the named pipe's name that the Roslyn server process prints on stdout."
    102   (let* ((data (json-parse-string output :object-type 'plist))
    103          (pipe (plist-get data :pipeName)))
    104     (when pipe
    105       (setq lsp-roslyn--pipe-name (lsp-roslyn--parse-pipe-name pipe)))))
    106 
    107 (defun lsp-roslyn--make-named-pipe-process (filter sentinel environment-fn process-name stderr-buf)
    108   "Creates the process that will handle the JSON-RPC communication."
    109   (let* ((process-environment
    110           (lsp--compute-process-environment environment-fn))
    111          (default-directory (lsp--default-directory-for-connection)))
    112     (cond
    113      ((eq system-type 'windows-nt)
    114       (make-process
    115        :name process-name
    116        :connection-type 'pipe
    117        :buffer (format "*%s*" process-name)
    118        :coding 'no-conversion
    119        :filter filter
    120        :sentinel sentinel
    121        :stderr stderr-buf
    122        :noquery t
    123        :command (lsp-resolve-final-command
    124                  `("PowerShell" "-NoProfile" "-ExecutionPolicy" "Bypass" "-Command"
    125                    ,lsp-roslyn--stdpipe-path "."
    126                    ,lsp-roslyn--pipe-name))))
    127      (t (make-network-process
    128          :service "roslyn"
    129          :name process-name
    130          :remote lsp-roslyn--pipe-name
    131          :sentinel sentinel
    132          :filter filter
    133          :noquery t)))))
    134 
    135 (defun lsp-roslyn--connect (filter sentinel name environment-fn _workspace)
    136   "Creates a connection to the Roslyn language server's named pipe.
    137 
    138 First creates an instance of the language server process, then
    139 creates another process connecting to the named pipe it specifies."
    140   (setq lsp-roslyn--pipe-name nil)
    141   (let* ((parent-process-name name)
    142          (parent-stderr-buf (format "*%s::stderr*" parent-process-name))
    143          (command-process (make-process
    144                            :name parent-process-name
    145                            :buffer (generate-new-buffer-name parent-process-name)
    146                            :coding 'no-conversion
    147                            :filter 'lsp-roslyn--parent-process-filter
    148                            :sentinel sentinel
    149                            :stderr parent-stderr-buf
    150                            :command `(,lsp-roslyn-dotnet-executable
    151                                       ,(lsp-roslyn--get-server-dll-path)
    152                                       ,(format "--logLevel=%s" lsp-roslyn-server-log-level)
    153                                       ,(format "--extensionLogDirectory=%s" lsp-roslyn-server-log-directory)
    154                                       ,@lsp-roslyn-server-extra-args)
    155                            :noquery t)))
    156     (accept-process-output command-process lsp-roslyn-server-timeout-seconds) ; wait for JSON with pipe name to print on stdout, like {"pipeName":"\\\\.\\pipe\\d1b72351"}
    157     (when (not lsp-roslyn--pipe-name)
    158       (error "Failed to receieve pipe name from Roslyn server process"))
    159     (let* ((process-name (generate-new-buffer-name (format "%s-pipe" name)))
    160            (stderr-buf (format "*%s::stderr*" process-name))
    161            (communication-process
    162             (lsp-roslyn--make-named-pipe-process filter sentinel environment-fn process-name stderr-buf)))
    163       (with-current-buffer (get-buffer parent-stderr-buf)
    164         (special-mode))
    165       (when-let* ((stderr-buffer (get-buffer stderr-buf)))
    166         (with-current-buffer stderr-buffer
    167           ;; Make the *NAME::stderr* buffer buffer-read-only, q to bury, etc.
    168           (special-mode))
    169         (set-process-query-on-exit-flag (get-buffer-process stderr-buffer) nil))
    170       (set-process-query-on-exit-flag command-process nil)
    171       (set-process-query-on-exit-flag communication-process nil)
    172       (cons communication-process communication-process))))
    173 
    174 (defun lsp-roslyn--uri-to-path (uri)
    175   "Convert a URI to a file path, without unhexifying."
    176   (let* ((url (url-generic-parse-url uri))
    177          (type (url-type url))
    178          (target (url-target url))
    179          (file
    180           (concat (decode-coding-string (url-filename url)
    181                                         (or locale-coding-system 'utf-8))
    182                   (when (and target
    183                              (not (s-match
    184                                    (rx "#" (group (1+ num)) (or "," "#")
    185                                        (group (1+ num))
    186                                        string-end)
    187                                    uri)))
    188                     (concat "#" target))))
    189          (file-name (if (and type (not (string= type "file")))
    190                         (if-let* ((handler (lsp--get-uri-handler type)))
    191                             (funcall handler uri)
    192                           uri)
    193                       ;; `url-generic-parse-url' is buggy on windows:
    194                       ;; https://github.com/emacs-lsp/lsp-mode/pull/265
    195                       (or (and (eq system-type 'windows-nt)
    196                                (eq (elt file 0) ?\/)
    197                                (substring file 1))
    198                           file))))
    199     (->> file-name
    200          (concat (-some #'lsp--workspace-host-root (lsp-workspaces)))
    201          (lsp-remap-path-if-needed))))
    202 
    203 (defun lsp-roslyn--path-to-uri (path)
    204   "Convert PATH to a URI, without hexifying."
    205   (url-unhex-string (lsp--path-to-uri-1 path)))
    206 
    207 (lsp-defun lsp-roslyn--on-project-initialization-complete (workspace _params)
    208   (lsp--info "%s: Project initialized successfully."
    209              (lsp--workspace-print workspace)))
    210 
    211 (defun lsp-roslyn--find-files-in-parent-directories (directory regex &optional result)
    212   "Search DIRECTORY for files matching REGEX and return their full paths if found."
    213   (let* ((parent-dir (file-truename (concat (file-name-directory directory) "../")))
    214          (found (directory-files directory 't regex))
    215          (result (append (or result '()) found)))
    216     (if (and (not (string= (file-truename directory) parent-dir))
    217              (< (length parent-dir) (length (file-truename directory))))
    218         (lsp-roslyn--find-files-in-parent-directories parent-dir regex result)
    219       result)))
    220 
    221 (defun lsp-roslyn--pick-solution-file-interactively (solution-files)
    222   (completing-read "Solution file for this workspace: " solution-files nil t))
    223 
    224 (defun lsp-roslyn--find-solution-file ()
    225   (let ((solutions (lsp-roslyn--find-files-in-parent-directories
    226                     (file-name-directory (buffer-file-name))
    227                     (rx (* any) ".sln" eos))))
    228     (cond
    229      ((not solutions) nil)
    230      ((eq (length solutions) 1) (cl-first solutions))
    231      (t (lsp-roslyn--pick-solution-file-interactively solutions)))))
    232 
    233 (defun lsp-roslyn-open-solution-file ()
    234   "Chooses the solution file to associate with the Roslyn language server."
    235   (interactive)
    236   (let ((solution-file (lsp-roslyn--find-solution-file)))
    237     (if solution-file
    238         (lsp-notify "solution/open" (list :solution (lsp--path-to-uri solution-file)))
    239       (lsp--error "No solution file was found for this workspace."))))
    240 
    241 (defun lsp-roslyn--on-initialized (_workspace)
    242   "Handler for Roslyn server initialization."
    243   (lsp-roslyn-open-solution-file))
    244 
    245 (defun lsp-roslyn--get-package-name ()
    246   "Gets the package name of the Roslyn language server."
    247   (format "microsoft.codeanalysis.languageserver.%s" (lsp-roslyn--get-rid)))
    248 
    249 (defun lsp-roslyn--get-server-dll-path ()
    250   "Gets the path to the language server DLL.
    251 Assumes it was installed with the server install function."
    252   (if lsp-roslyn-server-dll-override-path
    253       lsp-roslyn-server-dll-override-path
    254     (f-join lsp-roslyn-install-path "out"
    255             (lsp-roslyn--get-package-name)
    256             lsp-roslyn-package-version
    257             "content" "LanguageServer"
    258             (lsp-roslyn--get-rid)
    259             "Microsoft.CodeAnalysis.LanguageServer.dll")))
    260 
    261 (defun lsp-roslyn--get-rid ()
    262   "Retrieves the .NET Runtime Identifier (RID) for the current system."
    263   (let* ((is-x64 (string-match-p "x86_64" system-configuration))
    264          (is-arm64 (string-match-p "aarch64" system-configuration))
    265          (is-x86 (and (string-match-p "x86" system-configuration) (not is-x64))))
    266     (if-let* ((platform-name (cond
    267                              ((eq system-type 'gnu/linux) "linux")
    268                              ((eq system-type 'darwin) "osx")
    269                              ((eq system-type 'windows-nt) "win")))
    270              (arch-name (cond
    271                          (is-x64 "x64")
    272                          (is-arm64 "arm64")
    273                          (is-x86 "x86"))))
    274         (format "%s-%s" platform-name arch-name)
    275       (error "Unsupported platform: %s (%s)" system-type system-configuration))))
    276 
    277 ;; Adapted from roslyn.nvim's version
    278 (defconst lsp-roslyn--temp-project-nuget-config
    279   "<?xml version=\"1.0\" encoding=\"utf-8\"?>
    280 <configuration>
    281   <packageSources>
    282     <add key=\"vs-impl\" value=\"https://pkgs.dev.azure.com/azure-public/vside/_packaging/vs-impl/nuget/v3/index.json\" />
    283   </packageSources>
    284 </configuration>"
    285   "The nuget.config to use when downloading Roslyn.")
    286 
    287 ;; Adapted from roslyn.nvim's version
    288 (defun lsp-roslyn--temp-project-csproj (pkg-name pkg-version)
    289   "Generates a temporary .csproj to use for downloading the language server."
    290   (format
    291    "<Project Sdk=\"Microsoft.Build.NoTargets/1.0.80\">
    292     <PropertyGroup>
    293         <!-- Changes the global packages folder -->
    294         <RestorePackagesPath>out</RestorePackagesPath>
    295         <!-- This is not super relevant, as long as your SDK version supports it. -->
    296         <TargetFramework>net7.0</TargetFramework>
    297         <!-- If a package is resolved to a fallback folder, it may not be downloaded -->
    298         <DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
    299         <!-- We don't want to build this project, so we do not need the reference assemblies for the framework we chose -->
    300         <AutomaticallyUseReferenceAssemblyPackages>false</AutomaticallyUseReferenceAssemblyPackages>
    301     </PropertyGroup>
    302     <ItemGroup>
    303         <PackageDownload Include=\"%s\" version=\"[%s]\" />
    304     </ItemGroup>
    305 </Project>"
    306    pkg-name pkg-version))
    307 
    308 (defun lsp-roslyn--download-server (_client callback error-callback update?)
    309   "Downloads the Roslyn language server to `lsp-roslyn-install-path'.
    310 CALLBACK is called when the download finish successfully otherwise
    311 ERROR-CALLBACK is called.
    312 UPDATE is non-nil if it is already downloaded.
    313 FORCED if specified with prefix argument."
    314 
    315   (let ((pkg-name (lsp-roslyn--get-package-name)))
    316     (when update?
    317       (ignore-errors (delete-directory lsp-roslyn-install-path t)))
    318     (unless (f-exists? lsp-roslyn-install-path)
    319       (mkdir lsp-roslyn-install-path 'create-parent))
    320     (f-write-text lsp-roslyn--temp-project-nuget-config
    321                   'utf-8 (expand-file-name "nuget.config" lsp-roslyn-install-path))
    322     (f-write-text (lsp-roslyn--temp-project-csproj pkg-name lsp-roslyn-package-version)
    323                   'utf-8 (expand-file-name "DownloadRoslyn.csproj" lsp-roslyn-install-path))
    324     (lsp-async-start-process
    325      callback
    326      error-callback
    327      lsp-roslyn-dotnet-executable "restore" "--interactive" lsp-roslyn-install-path
    328      (format "/p:PackageName=%s" pkg-name)
    329      (format "/p:PackageVersion=%s" lsp-roslyn-package-version))))
    330 
    331 (defun lsp-roslyn--make-connection ()
    332   (list :connect (lambda (f s n e w) (lsp-roslyn--connect f s n e w))
    333         :test? (lambda () (f-exists? (lsp-roslyn--get-server-dll-path)))))
    334 
    335 (lsp-register-client
    336  (make-lsp-client :new-connection (lsp-roslyn--make-connection)
    337                   :priority 0
    338                   :server-id 'csharp-roslyn
    339                   :activation-fn (lsp-activate-on "csharp")
    340                   :notification-handlers (ht ("workspace/projectInitializationComplete" 'lsp-roslyn--on-project-initialization-complete))
    341 
    342                   ;; These two functions are the same as lsp-mode's except they do not
    343                   ;; (un)hexify URIs.
    344                   :path->uri-fn 'lsp-roslyn--path-to-uri
    345                   :uri->path-fn 'lsp-roslyn--uri-to-path
    346 
    347                   :initialized-fn #'lsp-roslyn--on-initialized
    348                   :download-server-fn #'lsp-roslyn--download-server))
    349 
    350 (provide 'lsp-roslyn)
    351 ;;; lsp-roslyn.el ends here