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