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