config

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

lsp-roslyn.el (16064B)


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