A document string minor mode using tree-sitter
Table of Contents
(use-package ts-docstr
:straight (ts-docstr :type git :host github :repo "emacs-vs/ts-docstr"
:files (:defaults "langs/*.el")))
git clone https://github.com/emacs-vs/ts-docstr /path/to/lib
then in Emacs:
(add-to-list 'load-path "/path/to/lib")
(require ts-docstr)
or
(use-package ts-docstr
:load-path "/path/to/lib")
Commands | Description |
---|---|
ts-docstr-at-point |
Add document string at current point. |
ts-docstr-mode |
Use nature key bindings. |
ts-docstr-ask |
Ask and update document string style (locally). |
The style can be customized by setting the variable with this pattern
ts-docstr-[lang]-style
. Here is an example to set Java
docstring style
to Javadoc
.
(setq ts-docstr-java-style 'javadoc)
Better editing experience,
(setq ts-docstr-key-support t)
Then do M-x ts-docstr-mode
.
(setq ts-docstr-ask-on-enable t)
Hook patterns are:
ts-docstr-[module]-before-[activate/parse/insert]-hook
ts-docstr-[module]-after-[activate/parse/insert]-hook
For example,
ts-docstr-c++-before-activate-hook
or just a general one, (without the language name)
ts-docstr-before-activate-hook
⚠️ Please sort these two lists alphabetically!
These languages are fairly complete:
- C / C++ / C#
- Go
- Java / JavaScript
- Lua
- PHP / Python
- Ruby / Rust
- Scala
- Swift
- TypeScript
These languages are in development:
- Elixir
- Haskell
- Shellscript
If you would like to contribute to this project, you may either clone and make pull requests to this repository. Or you can clone the project and establish your own branch of this tool. Any methods are welcome!
⚠ Before you start, make sure tree-sitter-langs supports the language you want to add!
⚠ The best way to learn how the entire process works is to look into other files in the
/langs
folder from the project root. Find a similar language and see through code, all languages' implementation are very similar to one another.
All parsers are defined in the /langs
folder from the project root. The file
is named with the prefix ts-docstr-
followed by the language name
. For
example, if you want to create a parser for the C
programming languge; the
file should be named ts-docstr-c.el
.
The parser file is consist in three part:
- ts-docstr-[lang]-activate
()
- ts-docstr-[lang]-parse
(node)
- ts-docstr-[lang]-insert
(node data)
The activate
function is used to search for a node and confirm weather it
should insert a document string. This function will eventually return a captured
node, or return nil
if we shouldn't insert a document string here.
Here is the simplified version of Java
activate function:
;;;###autoload
(defun ts-docstr-java-activate ()
"..."
;; Narrow region to next line, this defines the valid region to insert a
;; document string.
(ts-docstr-c-like-narrow-region
;; We grab a list of node from the narrowed region, here we try to capture
;; a `class' or `method' declaration. If this returns 2 or more nodes,
;; report an error since we don't expect these declarations happened on the
;; same line (region).
(nth 0 (ts-docstr-grab-nodes-in-range '(class_declaration
method_declaration)))))
Use ts-docstr-activatable-p
function to check to see if you are able to insert
a document string at point, this function returns a node.
(defun print-activate-node ()
(interactive) ; make interactive, so you could M-x
(message "node: %s" (ts-docstr-activatable-p)))
Evaluate, then M-x print-activate-node
to see if it return something or nil
.
The parse
function takes one argument node
from the activate
function.
In this stage, we collect all necessary data (paramters
, class
/enum
name,
etc) and put into a property list.
Here is a simplest parse
function for example:
;;;###autoload
(defun ts-docstr-java-parse (node)
"..."
(if (equal (tsc-node-type node) 'method_declaration)
;; `types' and `variables' are lists. Each store typenames and variables
;; name. We simply parse the tree/node in this steps.
(list :type types :variable variables
:return (ts-docstr-java--parse-return params) ; return `t' or `nil'
:name (ts-docstr-java--get-name node)) ; return `function' name
;; For `class', we don't need to parse parameters.
(list :name (ts-docstr-java--get-name node)))) ; return `class' name
The insert
function takes two arguments node
and data
from previous
stages. We will need to define a config variable and function.
The config variable:
(defcustom ts-docstr-java-style 'javadoc
"..."
:type '(choice (const :tag "No specify" nil)
(const :tag "Javadoc Style" javadoc)))
The config function:
;; This is later called and exposed in the insertion function.
(defun ts-docstr-java-config ()
"..."
(cl-case ts-docstr-java-style ; define the style by each style guide
(javadoc (list :start "/**" ; this plist is later used in insertion function
:prefix "* "
:end "*/"
:summary "{d}"
:param "@param {v} {d}"
:return "@return {d}"))
(t ...)))
Lastly, you can create the insert
function. In this stage you will write data
to the current file.
(defun ts-docstr-java-insert (node data)
"Insert document string upon NODE and DATA."
(ts-docstr-inserting
(ts-docstr-insert c-start "\n") ; /**
(ts-docstr-insert c-prefix " " (plist-get data :name) "\n") ; * NAME
(ts-docstr-insert c-end))) ; */
/**
* Example
*/
class Example {}
Find the language file in the /langs
folder from the project root, if you
couldn't find it, see ❓ How to support my favorite language?.
First look into the variable with the name similar to ts-docstr-[lang]-style
.
See the following example,
;; langs/ts-docstr-java.el
(defcustom ts-docstr-java-style 'javadoc
"..."
:type '(choice (const :tag "No specify" nil)
(const :tag "Javadoc Style" javadoc)))
See if the style is already supported. Continue reading if it does NOT, there are two parts you need to change in order to support your favorite style!
First, you need to add an option to the style variable so other people would notice the changes.
...
:type '(choice (const :tag "No specify" nil)
(const :tag "New Doc Style" new-doc-style)
...
Then next (last) step is to do the implementation in the insertion function. The implementation is isolated and complex depends on the style you targeted. Here we show the core logic of the implementation, but we prefer to look right into the code:
...
(ts-docstr-with-style-case
(new-doc-style ; listen to new-doc-style
(insert "My new docstring style! :)"))
...
To insert document string with key is to use ts-docstr-mode
. The entire key
related logic is placed in ts-docstr-key.el
file from the project root.
To implement one, you first need to design the method to insert the entire
document string (I called it triggeration function, or trigger). For example,
in C#
, you can trigger the document string insertion function by hittting
the third slash in a line.
Once you have an idea, write the function to do the action. Here is the example for C# slash triggeration function.
⚠ Check before you do, may be the triggeration is already there, and make sure your design doesn't conflict with other keys!
(defun ts-docstr-key-csharp-/ (&rest _)
"..."
(ts-docstr-key--with-env '(csharp-mode) ; Make sure it doesn't pollute other file
(when (and (ts-docstr--line-is "///") ; Make sure the line consist of ///
(ts-docstr--looking-back "///" 3) ; Make sure cursor is at the end of ///
(ts-docstr-activatable-p)) ; Check valid docstring insertion point
(backward-delete-char 3) ; Delete all slashes, ready to do docstrnig insertion
(ts-docstr-at-point)))) ; Do docstring insertion
Then you would just need to register the function to variable `ts-docstr-key-alist.
(defcustom ts-docstr-key-alist
`(("RET" . ts-docstr-key-doxygen-like-return)
("/" . ts-docstr-key-csharp-/) ; Add this line!
("/" . ts-docstr-key-go-/)
...