From d49a0c6246c2c7b7b03ca02e8ed8021530c16dfc Mon Sep 17 00:00:00 2001 From: Rutherther Date: Sat, 27 Sep 2025 21:35:16 +0200 Subject: [PATCH] feat: add support for xml method documentation in C# --- init.el | 11 +- lisp/csharp-yasnippet.el | 230 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 237 insertions(+), 4 deletions(-) diff --git a/init.el b/init.el index c01e9ace3e2a162fc96b613d0f6b1ec7cd53b706..070185dd309e09aabc116bc0a143970c158313ff 100644 --- a/init.el +++ b/init.el @@ -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 diff --git a/lisp/csharp-yasnippet.el b/lisp/csharp-yasnippet.el index 7ee1d454268f309bc894ef2b66a84db74e433d66..9de6f4bae967c91d7d277089326bbdeea043cb46 100644 --- a/lisp/csharp-yasnippet.el +++ b/lisp/csharp-yasnippet.el @@ -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 "") + (insert (csharp--get-doc-comments-above node)) + (insert "") + (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 + (beginning-of-buffer) + (kill-whole-line) + ;; delete + (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 "<" "<") + (goto-char (point-min)) + (replace-regexp ">" ">") + (goto-char (point-min)) + (replace-regexp """ "\"") + + (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