config

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

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