lsp-csharp.el (22062B)
1 ;;; lsp-csharp.el --- description -*- lexical-binding: t; -*- 2 3 ;; Copyright (C) 2019 Jostein Kjønigsen, Saulius Menkevicius 4 5 ;; Author: Saulius Menkevicius <saulius.menkevicius@fastmail.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 ;; lsp-csharp client 24 25 ;;; Code: 26 27 (require 'lsp-mode) 28 (require 'gnutls) 29 (require 'f) 30 31 (defgroup lsp-csharp nil 32 "LSP support for C#, using the Omnisharp Language Server. 33 Version 1.34.3 minimum is required." 34 :group 'lsp-mode 35 :link '(url-link "https://github.com/OmniSharp/omnisharp-roslyn")) 36 37 (defgroup lsp-csharp-omnisharp nil 38 "LSP support for C#, using the Omnisharp Language Server. 39 Version 1.34.3 minimum is required." 40 :group 'lsp-mode 41 :link '(url-link "https://github.com/OmniSharp/omnisharp-roslyn") 42 :package-version '(lsp-mode . "9.0.0")) 43 44 (defcustom lsp-csharp-server-install-dir 45 (f-join lsp-server-install-dir "omnisharp-roslyn/") 46 "Installation directory for OmniSharp Roslyn server." 47 :group 'lsp-csharp-omnisharp 48 :type 'directory) 49 50 (defcustom lsp-csharp-server-path 51 nil 52 "The path to the OmniSharp Roslyn language-server binary. 53 Set this if you have the binary installed or have it built yourself." 54 :group 'lsp-csharp-omnisharp 55 :type '(string :tag "Single string value or nil")) 56 57 (defcustom lsp-csharp-test-run-buffer-name 58 "*lsp-csharp test run*" 59 "The name of buffer used for outputting lsp-csharp test run results." 60 :group 'lsp-csharp-omnisharp 61 :type 'string) 62 63 (defcustom lsp-csharp-solution-file 64 nil 65 "Solution to load when starting the server. 66 Usually this is to be set in your .dir-locals.el on the project root directory." 67 :group 'lsp-csharp-omnisharp 68 :type 'string) 69 70 (defcustom lsp-csharp-omnisharp-roslyn-download-url 71 (concat "https://github.com/omnisharp/omnisharp-roslyn/releases/latest/download/" 72 (cond ((eq system-type 'windows-nt) 73 ; On Windows we're trying to avoid a crash starting 64bit .NET PE binaries in 74 ; Emacs by using x86 version of omnisharp-roslyn on older (<= 26.4) versions 75 ; of Emacs. See https://lists.nongnu.org/archive/html/bug-gnu-emacs/2017-06/msg00893.html" 76 (if (and (string-match "^x86_64-.*" system-configuration) 77 (version<= "26.4" emacs-version)) 78 "omnisharp-win-x64.zip" 79 "omnisharp-win-x86.zip")) 80 81 ((eq system-type 'darwin) 82 (if (string-match "aarch64-.*" system-configuration) 83 "omnisharp-osx-arm64-net6.0.zip" 84 "omnisharp-osx-x64-net6.0.zip")) 85 86 ((and (eq system-type 'gnu/linux) 87 (or (eq (string-match "^x86_64" system-configuration) 0) 88 (eq (string-match "^i[3-6]86" system-configuration) 0))) 89 "omnisharp-linux-x64-net6.0.zip") 90 91 (t "omnisharp-mono.zip"))) 92 "Automatic download url for omnisharp-roslyn." 93 :group 'lsp-csharp-omnisharp 94 :type 'string) 95 96 (defcustom lsp-csharp-omnisharp-roslyn-store-path 97 (f-join lsp-csharp-server-install-dir "latest" "omnisharp-roslyn.zip") 98 "The path where omnisharp-roslyn .zip archive will be stored." 99 :group 'lsp-csharp-omnisharp 100 :type 'file) 101 102 (defcustom lsp-csharp-omnisharp-roslyn-binary-path 103 (f-join lsp-csharp-server-install-dir "latest" (if (eq system-type 'windows-nt) 104 "OmniSharp.exe" 105 "OmniSharp")) 106 "The path where omnisharp-roslyn binary after will be stored." 107 :group 'lsp-csharp-omnisharp 108 :type 'file) 109 110 (defcustom lsp-csharp-omnisharp-roslyn-server-dir 111 (f-join lsp-csharp-server-install-dir "latest" "omnisharp-roslyn") 112 "The path where omnisharp-roslyn .zip archive will be extracted." 113 :group 'lsp-csharp-omnisharp 114 :type 'file) 115 116 (lsp-dependency 117 'omnisharp-roslyn 118 `(:download :url lsp-csharp-omnisharp-roslyn-download-url 119 :decompress :zip 120 :store-path lsp-csharp-omnisharp-roslyn-store-path 121 :binary-path lsp-csharp-omnisharp-roslyn-binary-path 122 :set-executable? t) 123 '(:system "OmniSharp")) 124 125 (defun lsp-csharp--omnisharp-download-server (_client callback error-callback _update?) 126 "Download zip package for omnisharp-roslyn and install it. 127 Will invoke CALLBACK on success, ERROR-CALLBACK on error." 128 (lsp-package-ensure 'omnisharp-roslyn callback error-callback)) 129 130 (defun lsp-csharp--language-server-path () 131 "Resolve path to use to start the server." 132 (let ((executable-name (if (eq system-type 'windows-nt) 133 "OmniSharp.exe" 134 "OmniSharp"))) 135 (or (and lsp-csharp-server-path 136 (executable-find lsp-csharp-server-path)) 137 (executable-find executable-name) 138 (lsp-package-path 'omnisharp-roslyn)))) 139 140 (defun lsp-csharp-open-project-file () 141 "Open corresponding project file (.csproj) for the current file." 142 (interactive) 143 (-let* ((project-info-req (lsp-make-omnisharp-project-information-request :file-name (buffer-file-name))) 144 (project-info (lsp-request "o#/project" project-info-req)) 145 ((&omnisharp:ProjectInformation :ms-build-project) project-info) 146 ((&omnisharp:MsBuildProject :path) ms-build-project)) 147 (find-file path))) 148 149 (defun lsp-csharp--get-buffer-code-elements () 150 "Retrieve code structure by calling into the /v2/codestructure endpoint. 151 Returns :elements from omnisharp:CodeStructureResponse." 152 (-let* ((code-structure (lsp-request "o#/v2/codestructure" 153 (lsp-make-omnisharp-code-structure-request :file-name (buffer-file-name)))) 154 ((&omnisharp:CodeStructureResponse :elements) code-structure)) 155 elements)) 156 157 (defun lsp-csharp--inspect-code-elements-recursively (fn elements) 158 "Invoke FN for every omnisharp:CodeElement found recursively in ELEMENTS." 159 (seq-each 160 (lambda (el) 161 (funcall fn el) 162 (-let (((&omnisharp:CodeElement :children) el)) 163 (lsp-csharp--inspect-code-elements-recursively fn children))) 164 elements)) 165 166 (defun lsp-csharp--collect-code-elements-recursively (predicate elements) 167 "Flatten the omnisharp:CodeElement tree in ELEMENTS matching PREDICATE." 168 (let ((results nil)) 169 (lsp-csharp--inspect-code-elements-recursively (lambda (el) 170 (when (funcall predicate el) 171 (setq results (cons el results)))) 172 elements) 173 results)) 174 175 (lsp-defun lsp-csharp--l-c-within-range (l c (&omnisharp:Range :start :end)) 176 "Determine if L (line) and C (column) are within RANGE." 177 (-let* (((&omnisharp:Point :line start-l :column start-c) start) 178 ((&omnisharp:Point :line end-l :column end-c) end)) 179 (or (and (= l start-l) (>= c start-c) (or (> end-l start-l) (<= c end-c))) 180 (and (> l start-l) (< l end-l)) 181 (and (= l end-l) (<= c end-c))))) 182 183 (defun lsp-csharp--code-element-stack-on-l-c (l c elements) 184 "Return omnisharp:CodeElement stack at L (line) and C (column) in ELEMENTS tree." 185 (when-let ((matching-element (seq-find (lambda (el) 186 (-when-let* (((&omnisharp:CodeElement :ranges) el) 187 ((&omnisharp:RangeList :full?) ranges)) 188 (lsp-csharp--l-c-within-range l c full?))) 189 elements))) 190 (-let (((&omnisharp:CodeElement :children) matching-element)) 191 (cons matching-element (lsp-csharp--code-element-stack-on-l-c l c children))))) 192 193 (defun lsp-csharp--code-element-stack-at-point () 194 "Return omnisharp:CodeElement stack at point as a list." 195 (let ((pos-line (plist-get (lsp--cur-position) :line)) 196 (pos-col (plist-get (lsp--cur-position) :character))) 197 (lsp-csharp--code-element-stack-on-l-c pos-line 198 pos-col 199 (lsp-csharp--get-buffer-code-elements)))) 200 201 (lsp-defun lsp-csharp--code-element-test-method-p (element) 202 "Return test method name and test framework for a given ELEMENT." 203 (when element 204 (-when-let* (((&omnisharp:CodeElement :properties) element) 205 ((&omnisharp:CodeElementProperties :test-method-name? :test-framework?) properties)) 206 (list test-method-name? test-framework?)))) 207 208 (defun lsp-csharp--reset-test-buffer (present-buffer) 209 "Create new or reuse an existing test result output buffer. 210 PRESENT-BUFFER will make the buffer be presented to the user." 211 (with-current-buffer (get-buffer-create lsp-csharp-test-run-buffer-name) 212 (compilation-mode) 213 (read-only-mode) 214 (let ((inhibit-read-only t)) 215 (erase-buffer))) 216 217 (when present-buffer 218 (display-buffer lsp-csharp-test-run-buffer-name))) 219 220 (defun lsp-csharp--start-tests (test-method-framework test-method-names) 221 "Run test(s) identified by TEST-METHOD-NAMES using TEST-METHOD-FRAMEWORK." 222 (if (and test-method-framework test-method-names) 223 (let ((request-message (lsp-make-omnisharp-run-tests-in-class-request 224 :file-name (buffer-file-name) 225 :test-frameworkname test-method-framework 226 :method-names (vconcat test-method-names)))) 227 (lsp-csharp--reset-test-buffer t) 228 (lsp-session-set-metadata "last-test-method-framework" test-method-framework) 229 (lsp-session-set-metadata "last-test-method-names" test-method-names) 230 (lsp-request-async "o#/v2/runtestsinclass" 231 request-message 232 (-lambda ((&omnisharp:RunTestResponse)) 233 (message "lsp-csharp: Test run has started")))) 234 (message "lsp-csharp: No test methods to run"))) 235 236 (defun lsp-csharp--test-message (message) 237 "Emit a MESSAGE to lsp-csharp test run buffer." 238 (when-let ((existing-buffer (get-buffer lsp-csharp-test-run-buffer-name)) 239 (inhibit-read-only t)) 240 (with-current-buffer existing-buffer 241 (save-excursion 242 (goto-char (point-max)) 243 (insert message "\n"))))) 244 245 (defun lsp-csharp-run-test-at-point () 246 "Start test run at current point (if any)." 247 (interactive) 248 (let* ((stack (lsp-csharp--code-element-stack-at-point)) 249 (element-on-point (car (last stack))) 250 (test-method (lsp-csharp--code-element-test-method-p element-on-point)) 251 (test-method-name (car test-method)) 252 (test-method-framework (car (cdr test-method)))) 253 (lsp-csharp--start-tests test-method-framework (list test-method-name)))) 254 255 (defun lsp-csharp-run-all-tests-in-buffer () 256 "Run all test methods in the current buffer." 257 (interactive) 258 (let* ((elements (lsp-csharp--get-buffer-code-elements)) 259 (test-methods (lsp-csharp--collect-code-elements-recursively 'lsp-csharp--code-element-test-method-p elements)) 260 (test-method-framework (car (cdr (lsp-csharp--code-element-test-method-p (car test-methods))))) 261 (test-method-names (mapcar (lambda (method) 262 (car (lsp-csharp--code-element-test-method-p method))) 263 test-methods))) 264 (lsp-csharp--start-tests test-method-framework test-method-names))) 265 266 (defun lsp-csharp-run-test-in-buffer () 267 "Run selected test in current buffer." 268 (interactive) 269 (when-let* ((elements (lsp-csharp--get-buffer-code-elements)) 270 (test-methods (lsp-csharp--collect-code-elements-recursively 'lsp-csharp--code-element-test-method-p elements)) 271 (test-method-framework (car (cdr (lsp-csharp--code-element-test-method-p (car test-methods))))) 272 (test-method-names (mapcar (lambda (method) 273 (car (lsp-csharp--code-element-test-method-p method))) 274 test-methods)) 275 (selected-test-method-name (lsp--completing-read "Select test:" test-method-names 'identity))) 276 (lsp-csharp--start-tests test-method-framework (list selected-test-method-name)))) 277 278 (defun lsp-csharp-run-last-tests () 279 "Re-run test(s) that were run last time." 280 (interactive) 281 (if-let ((last-test-method-framework (lsp-session-get-metadata "last-test-method-framework")) 282 (last-test-method-names (lsp-session-get-metadata "last-test-method-names"))) 283 (lsp-csharp--start-tests last-test-method-framework last-test-method-names) 284 (message "lsp-csharp: No test method(s) found to be ran previously on this workspace"))) 285 286 (lsp-defun lsp-csharp--handle-os-error (_workspace (&omnisharp:ErrorMessage :file-name :text)) 287 "Handle the `o#/error' (interop) notification displaying a message." 288 (lsp-warn "%s: %s" file-name text)) 289 290 (lsp-defun lsp-csharp--handle-os-testmessage (_workspace (&omnisharp:TestMessageEvent :message)) 291 "Handle the `o#/testmessage and display test message on test output buffer." 292 (lsp-csharp--test-message message)) 293 294 (lsp-defun lsp-csharp--handle-os-testcompleted (_workspace (&omnisharp:DotNetTestResult 295 :method-name 296 :outcome 297 :error-message 298 :error-stack-trace 299 :standard-output 300 :standard-error)) 301 "Handle the `o#/testcompleted' message from the server. 302 303 Will display the results of the test on the lsp-csharp test output buffer." 304 (let ((passed (string-equal "passed" outcome))) 305 (lsp-csharp--test-message 306 (format "[%s] %s " 307 (propertize (upcase outcome) 'font-lock-face (if passed 'success 'error)) 308 method-name)) 309 310 (unless passed 311 (lsp-csharp--test-message error-message) 312 313 (when error-stack-trace 314 (lsp-csharp--test-message error-stack-trace)) 315 316 (unless (seq-empty-p standard-output) 317 (lsp-csharp--test-message "STANDARD OUTPUT:") 318 (seq-doseq (stdout-line standard-output) 319 (lsp-csharp--test-message stdout-line))) 320 321 (unless (seq-empty-p standard-error) 322 (lsp-csharp--test-message "STANDARD ERROR:") 323 (seq-doseq (stderr-line standard-error) 324 (lsp-csharp--test-message stderr-line)))))) 325 326 (lsp-defun lsp-csharp--action-client-find-references ((&Command :arguments?)) 327 "Read first argument from ACTION as Location and display xrefs for that location 328 using the `textDocument/references' request." 329 (-if-let* (((&Location :uri :range) (lsp-seq-first arguments?)) 330 ((&Range :start range-start) range) 331 (find-refs-params (append (lsp--text-document-position-params (list :uri uri) range-start) 332 (list :context (list :includeDeclaration json-false)))) 333 (locations-found (lsp-request "textDocument/references" find-refs-params))) 334 (lsp-show-xrefs (lsp--locations-to-xref-items locations-found) nil t) 335 (message "No references found"))) 336 337 (lsp-register-client 338 (make-lsp-client :new-connection 339 (lsp-stdio-connection 340 #'(lambda () 341 (append 342 (list (lsp-csharp--language-server-path) "-lsp") 343 (when lsp-csharp-solution-file 344 (list "-s" (expand-file-name lsp-csharp-solution-file))))) 345 #'(lambda () 346 (when-let ((binary (lsp-csharp--language-server-path))) 347 (f-exists? binary)))) 348 :activation-fn (lsp-activate-on "csharp") 349 :server-id 'omnisharp 350 :priority -1 351 :action-handlers (ht ("omnisharp/client/findReferences" 'lsp-csharp--action-client-find-references)) 352 :notification-handlers (ht ("o#/projectadded" 'ignore) 353 ("o#/projectchanged" 'ignore) 354 ("o#/projectremoved" 'ignore) 355 ("o#/packagerestorestarted" 'ignore) 356 ("o#/msbuildprojectdiagnostics" 'ignore) 357 ("o#/packagerestorefinished" 'ignore) 358 ("o#/unresolveddependencies" 'ignore) 359 ("o#/error" 'lsp-csharp--handle-os-error) 360 ("o#/testmessage" 'lsp-csharp--handle-os-testmessage) 361 ("o#/testcompleted" 'lsp-csharp--handle-os-testcompleted) 362 ("o#/projectconfiguration" 'ignore) 363 ("o#/projectdiagnosticstatus" 'ignore) 364 ("o#/backgrounddiagnosticstatus" 'ignore)) 365 :download-server-fn #'lsp-csharp--omnisharp-download-server)) 366 367 ;; 368 ;; Alternative "csharp-ls" language server support 369 ;; see https://github.com/razzmatazz/csharp-language-server 370 ;; 371 (lsp-defun lsp-csharp--cls-metadata-uri-handler (uri) 372 "Handle `csharp:/(metadata)' uri from csharp-ls server. 373 374 `csharp/metadata' request is issued to retrieve metadata from the server. 375 A cache file is created on project root dir that stores this metadata and 376 filename is returned so lsp-mode can display this file." 377 378 (-when-let* ((metadata-req (lsp-make-csharp-ls-c-sharp-metadata 379 :text-document (lsp-make-text-document-identifier :uri uri))) 380 (metadata (lsp-request "csharp/metadata" metadata-req)) 381 ((&csharp-ls:CSharpMetadataResponse :project-name 382 :assembly-name 383 :symbol-name 384 :source) metadata) 385 (filename (f-join ".cache" 386 "lsp-csharp" 387 "metadata" 388 "projects" project-name 389 "assemblies" assembly-name 390 (concat symbol-name ".cs"))) 391 (file-location (expand-file-name filename (lsp-workspace-root))) 392 (metadata-file-location (concat file-location ".metadata-uri")) 393 (path (f-dirname file-location))) 394 395 (unless (file-exists-p file-location) 396 (unless (file-directory-p path) 397 (make-directory path t)) 398 399 (with-temp-file metadata-file-location 400 (insert uri)) 401 402 (with-temp-file file-location 403 (insert source))) 404 405 file-location)) 406 407 (defun lsp-csharp--cls-before-file-open (_workspace) 408 "Set `lsp-buffer-uri' variable after C# file is open from *.metadata-uri file." 409 410 (let ((metadata-file-name (concat buffer-file-name ".metadata-uri"))) 411 (setq-local lsp-buffer-uri 412 (when (file-exists-p metadata-file-name) 413 (with-temp-buffer (insert-file-contents metadata-file-name) 414 (buffer-string)))))) 415 416 (defun lsp-csharp--cls-make-launch-cmd () 417 "Return command line to invoke csharp-ls." 418 419 ;; emacs-28.1 on macOS has an issue 420 ;; that it launches processes using posix_spawn but does not reset sigmask properly 421 ;; thus causing dotnet runtime to lockup awaiting a SIGCHLD signal that never comes 422 ;; from subprocesses that quit 423 ;; 424 ;; as a workaround we will wrap csharp-ls invocation in "/bin/ksh -c" on macos 425 ;; so it launches with proper sigmask 426 ;; 427 ;; see https://lists.gnu.org/archive/html/emacs-devel/2022-02/msg00461.html 428 429 (let ((startup-wrapper (cond ((and (eq 'darwin system-type) 430 (version= "28.1" emacs-version)) 431 (list "/bin/ksh" "-c")) 432 433 (t nil))) 434 435 (csharp-ls-exec (or (executable-find "csharp-ls") 436 (f-join (or (getenv "USERPROFILE") (getenv "HOME")) 437 ".dotnet" "tools" "csharp-ls"))) 438 439 (solution-file-params (when lsp-csharp-solution-file 440 (list "-s" lsp-csharp-solution-file)))) 441 (append startup-wrapper 442 (list csharp-ls-exec) 443 solution-file-params))) 444 445 (defun lsp-csharp--cls-download-server (_client callback error-callback update?) 446 "Install/update csharp-ls language server using `dotnet tool'. 447 448 Will invoke CALLBACK or ERROR-CALLBACK based on result. 449 Will update if UPDATE? is t" 450 (lsp-async-start-process 451 callback 452 error-callback 453 "dotnet" "tool" (if update? "update" "install") "-g" "csharp-ls")) 454 455 (lsp-register-client 456 (make-lsp-client :new-connection (lsp-stdio-connection #'lsp-csharp--cls-make-launch-cmd) 457 :priority -2 458 :server-id 'csharp-ls 459 :activation-fn (lsp-activate-on "csharp") 460 :before-file-open-fn #'lsp-csharp--cls-before-file-open 461 :uri-handlers (ht ("csharp" #'lsp-csharp--cls-metadata-uri-handler)) 462 :download-server-fn #'lsp-csharp--cls-download-server)) 463 464 (lsp-consistency-check lsp-csharp) 465 466 (provide 'lsp-csharp) 467 ;;; lsp-csharp.el ends here