~ruther/emacs.d

d49a0c6246c2c7b7b03ca02e8ed8021530c16dfc — Rutherther 6 days ago c611c34
feat: add support for xml method documentation in C#
2 files changed, 237 insertions(+), 4 deletions(-)

M init.el
M lisp/csharp-yasnippet.el
M init.el => init.el +8 -3
@@ 1367,6 1367,8 @@
  :ensure nil
  :custom
  (treesit-font-lock-level 4)
  (treesit-load-name-override-list
   '((c-sharp "libtree-sitter-c_sharp" "tree_sitter_c_sharp")))
  :config
  (which-function-mode 1)



@@ 1440,14 1442,17 @@ minibuffer, even without explicitly focusing it."

(my-use-package csharp-mode
  :ensure nil
  :after eglot
  :mode (("\\.cs\\'" . csharp-mode))
  :after lsp-mode
  :mode (("\\.cs\\'" . csharp-ts-mode))
  :general
  (my-local-leader csharp-mode-map
    "d" '(csharp-document-at-point :wk "Document thing at point"))
  :config
  (my/indent-variable-mode-alist-add csharp-mode c-basic-offset)
  (my/indent-variable-mode-alist-add csharp-ts-mode csharp-ts-mode-indent-offset))

(my-use-package csharp-yasnippet
  :commands (csharp-file-path-to-namespace))
  :commands (csharp-file-path-to-namespace csharp-document-at-point))

  ;; Build solution
  ;;  --debug / --release

M lisp/csharp-yasnippet.el => lisp/csharp-yasnippet.el +229 -1
@@ 1,6 1,8 @@
;;; csharp-yasnippet.el --- C# utility functions for yasnippet

(provide 'csharp-yasnippet)
(require 'treesit)
(require 's)

(defun csharp--get-csproj-in-directory (dir)
  (car (directory-files dir


@@ 33,4 35,230 @@
         ""
       (concat "." (substring subdirectory 0 -1))))))

;;; csharp-yasnippet.el ends here
\ No newline at end of file
(defun ruther--treesit-current-node ()
  "Get the node at current point."
  (treesit-node-at (point)))

(defun ruther--treesit-search-upwards (node predicate)
  "Find node following NODE that matches PREDICATE."
  (if node
      (if (apply predicate (list node))
          node
        (ruther--treesit-search-upwards (treesit-node-parent node) predicate))
    nil))

(defvar csharp-documentable-nodes
  '((method_declaration . csharp--document-method-declaration)))

(defun csharp--next-documentable-node ()
  "Get the next node that can be documented"
  (let* ((current-node (ruther--treesit-current-node))
         (predicate (lambda (node)
                      (seq-contains-p
                       (mapcar 'car csharp-documentable-nodes)
                       (intern (treesit-node-type node))))))
    (or
     (ruther--treesit-search-upwards current-node predicate)
     (treesit-search-forward current-node predicate))))

(defun csharp--get-doc-comment-nodes-above (node)
  "Get all comments above NODE."
  (let ((prev-node (treesit-node-prev-sibling node)))
    (if (and
         (string-match-p "///.*" (treesit-node-text prev-node))
         (equal "comment" (treesit-node-type prev-node)))
        (cons
         prev-node
         (csharp--get-doc-comment-nodes-above prev-node))
      nil)))

(defun csharp--get-doc-comments-above-region (node)
  "Get region with all the documentation comments"
  (let ((nodes (csharp--get-doc-comment-nodes-above node)))
    (if nodes
        (cons
         (seq-min (mapcar 'treesit-node-start nodes))
         (seq-max (mapcar 'treesit-node-end nodes)))
      nil)))

(defun csharp--get-doc-comments-above (node)
  "Get documentation comments above the given NODE as a string."
  (let* ((doc-comment-nodes (reverse (csharp--get-doc-comment-nodes-above node)))
         (doc-comments (mapcar 'treesit-node-text doc-comment-nodes)))
    (s-trim
     (substring-no-properties
      (apply 'concat
             (mapcar
              (lambda (comment)
                (concat
                 (s-trim (substring comment 3))
                 "\n"))
              doc-comments))))))

(defun csharp--doc-comments-xml (node)
  "Get parsed documentation comments above the given NODE."
  (list
   (seq-filter
    (lambda (node) (not (stringp node)))
    (car
     (with-temp-buffer
       (insert "<doc>")
       (insert (csharp--get-doc-comments-above node))
       (insert "</doc>")
       (xml-parse-region))))))

(defun csharp--xml-search-children (xml predicate)
  (let ((str (apply
              'concat
              (mapcar
               (lambda (child)
                 (if (stringp child)
                     child
                   (with-temp-buffer
                     (xml-print (list child))
                     (buffer-string))))
               (xml-node-children
                (seq-first
                 (seq-filter
                  predicate
                  (xml-node-children (car xml)))))))))
    (if (string-empty-p str)
        nil
      (list str))))

(defun csharp--xml-param-doc (xml param-name)
  (csharp--xml-search-children
   xml
   (lambda (node)
     (and
      (equal (car node) 'param)
      (equal (xml-get-attribute node 'name) param-name)))))

(defun csharp--xml-type-param-doc (xml param-name)
  (csharp--xml-search-children
   xml
   (lambda (node)
     (and
      (equal (car node) 'typeparam)
      (equal (xml-get-attribute node 'name) param-name)))))

(defun csharp--xml-node-doc (xml node-name)
  (csharp--xml-search-children
   xml
   (lambda (node)
     (and
      (equal (car node) node-name)))))

(defun csharp--parameter-names (parameters name)
  "Get names of parameters of a C# method located at METHOD-NODE."
  (mapcar
   (lambda (parameter)
     (substring-no-properties (treesit-node-text (treesit-node-child-by-field-name (car parameter) "name"))))
   (when parameters
       (cdr (treesit-induce-sparse-tree
             parameters
             (lambda (node)
               (equal name (treesit-node-type node))))))))

(defun csharp--method-parameters (method-node)
  "Get names of parameters of a C# method located at METHOD-NODE."
  (csharp--parameter-names (treesit-node-child-by-field-name method-node "parameters") "parameter"))

(defun csharp--method-type-parameters (method-node)
  "Get names of parameters of a C# method located at METHOD-NODE."
  (csharp--parameter-names (treesit-node-child-by-field-name method-node "type_parameters") "type_parameter"))

(defun csharp--method-return-type (method-node)
  "Get names of parameters of a C# method located at METHOD-NODE."
  (substring-no-properties (treesit-node-text (treesit-node-child-by-field-name method-node "returns"))))

(defun csharp--method-void-p (method-node)
  "Get names of parameters of a C# method located at METHOD-NODE."
  (equal
   "void"
   (csharp--method-return-type method-node)))

(defun csharp--method-declaration-documentation (method-node)
  (let ((xml (csharp--doc-comments-xml method-node)))
    `((doc nil
           (summary nil ,@(or (csharp--xml-node-doc xml 'summary) '("\n\n")))
           ,@(if (csharp--xml-node-doc xml 'remarks)
                 `((remarks nil ,@(csharp--xml-node-doc xml 'remarks)))
               '())
           ,@(mapcar
              (lambda (parameter)
                `(param ((name . ,parameter)) ,@(or (csharp--xml-param-doc xml parameter) '(""))))
              (csharp--method-parameters method-node))
           ,@(mapcar
              (lambda (parameter)
                `(typeparam ((name . ,parameter)) ,@(or (csharp--xml-type-param-doc xml parameter) '(""))))
              (csharp--method-type-parameters method-node))
           ,@(if (csharp--method-void-p method-node)
                 '()
               `((returns nil ,@(or (csharp--xml-node-doc xml 'returns) '("")))))))))

(defun csharp--method-declaration-documentation-string (method-node)
  (let ((docs (csharp--method-declaration-documentation method-node)))
    (with-temp-buffer
      (xml-print docs)
      ;; delete <doc>
      (beginning-of-buffer)
      (kill-whole-line)
      ;; delete </doc>
      (end-of-buffer)
      (kill-whole-line)
      (goto-char (point-max))
      ;; Remove the trailing newline
      (delete-char -1)

      ;; (xml-mode)
      ;; (evil-indent (point-min) (point-max))

      (goto-char (point-min))
      (replace-regexp "^  <" "<")
      (replace-regexp ">$" ">")

      (goto-char (point-min))
      (replace-regexp "&lt;" "<")
      (goto-char (point-min))
      (replace-regexp "&gt;" ">")
      (goto-char (point-min))
      (replace-regexp "&quot;" "\"")

      (goto-char (point-min))
      (replace-regexp "^" "/// ")

      (substring-no-properties (buffer-string)))))

(defun csharp--document-method-declaration (method-node)
  (let ((current-docs (csharp--get-doc-comments-above-region method-node))
        (new-docs (csharp--method-declaration-documentation-string method-node)))
    (save-excursion
      (when current-docs
        (kill-region (car current-docs) (cdr current-docs))
        (goto-char (car current-docs)))
      (unless current-docs
        (goto-char (treesit-node-start method-node))
        (beginning-of-line)
        (newline)
        (forward-line -1)
        (setq-local current-docs (cons (point) 0)))
      (insert new-docs)
      (evil-indent (car current-docs)
                   (point)))))

(defun csharp--document-node (node)
  (apply (alist-get (intern (treesit-node-type node)) csharp-documentable-nodes)
         (list node)))

(defun csharp-document-at-point ()
  (interactive)
  (let* ((node (csharp--next-documentable-node)))
    (if node
        (csharp--document-node node)
      nil)))

(provide 'test)
;;; test.el ends here

;;; csharp-yasnippet.el ends here