Creating an Emacs Function to Create Yardoc Stubs

When One Pathway Fails, Try Another

This afternoon, I was exploring some Tree Sitter 📖 functions in Emacs 📖. I wanted to take a Ruby 📖’s method’s signature and create Yardoc stubs from the method parameters.

Requirements

Let’s say I have the following Ruby method definition:

def call(foo, bar:, baz: :default, **kwargs)
  # do stuff
end

I wanted to call a function and update the buffer as follows:

##
# @param foo [Object]
# @param bar [Object]
# @param baz [Object]
# @param kwargs [Object]
def call(foo, bar:, baz: :default, **kwargs)
  # do stuff
end

update

I received an email pointing out that I had mixed the treesit (e.g. treesit-inspect-node-at-point) and tree-sitter (e.g. tsc-get-child-by-field) functions.

Gah! I had that in my kill-ring. I also tried the following to no avail:

(let ((func-node (tree-sitter-node-at-point 'method))
        (params (tsc-get-child-by-field func-node ':method_parameters)))
    (message "%s" params))

The email also pointed out that my “Reply by Email” link was broken; so I fixed that.

Thank you dear reader!

The Interlude and Solution

I was encountering problems with tree-sitter functionality. The tree-sitter package is an external package. The treesit is a built-in package in Emacs 29. I prefer tree-sitter as it’s more performant in my use case. The following emacs-lisp writes a nil message:

(let ((func-node (treesit-inspect-node-at-point 'method))
      (params (tsc-get-child-by-field func-node ':method_parameters)))
  (message "%s" params))

The above, in my reading, should’ve found the node that had the method parameters.

Running into those problems, I took a different path. String parsing and regular expressions. Below is that solution:

(defun jf/ruby-mode/yardoc-ify ()
  "Add parameter yarddoc stubs for the current method."
  (interactive)
  ;; Remember where we started.
  (save-excursion
    ;; Goto the beginning of the function
    (ruby-beginning-of-defun)
    ;; Move to just after the first (
    (search-forward "(")
    ;; Move back to just before the (
    (backward-char)
    ;; Select parameters declaration
    (mark-sexp)
    ;; Copy that
    (copy-region-as-kill (point) (mark))
    ;; Split apart the parameters into their identifiers
    (let ((identifiers (mapcar (lambda (token)
                            (replace-regexp-in-string
                             "[^a-z|_]" ""
                             (car (s-split " "
                                           (s-trim token)))))
                          (s-split "," (substring-no-properties
                                        (car kill-ring))))))
      ;; Go to the beginning of the function again
      (ruby-beginning-of-defun)
      ;; Now insert the identifiers as yardoc
      (insert "##\n"
              (s-join "\n" (mapcar
                            (lambda (param)
                              (concat "# @param "
                                      param
                                      " [Object]"))
                            identifiers))
              "\n"))))