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