A tree-sitter-based Emacs major mode for the Janet programming language
Author uses as primary means of editing Janet code.
Currently has more-or-less usable:
- Highlighting
- Indentation
- Imenu
- Navigation
- Which-Func
See the "Things to try" section below for additional details.
For a recent Emacs (>= 30.x) with the integrated tree-sitter support,
janet-emacs-trial-kit
may be a way of trying out janet-ts-mode along with some other
janet-supporting packages without having to change any existing
settings or manually download anything additional.
Emacs with tree-sitter support (master branch or >= emacs-30)
Note: janet-ts-mode was known to work with Emacs 29.x at one point,
but the author currently uses 30.x and doesn't exercise the code with
29.x, YMMV.
-
If Emacs >= 30.x with tree-sitter support is in place [1], something like the following in your .emacs equivalent should arrange for the tree-sitter-janet-simple grammar to be available:
(setq treesit-language-source-alist (if (eq 'windows-nt system-type) '((janet-simple . ("https://github.com/sogaiu/tree-sitter-janet-simple" nil nil "gcc.exe"))) '((janet-simple . ("https://github.com/sogaiu/tree-sitter-janet-simple"))))) (when (not (treesit-language-available-p 'janet-simple)) (treesit-install-language-grammar 'janet-simple))
-
janet-ts-mode prep
-
Manual
cd ~/src # or some place appropriategit clone https://github.com/sogaiu/janet-ts-mode- edit your .emacs equivalent so that:
load-pathknows about janet-ts-mode(require 'janet-ts-mode)is included
-
use-package
(use-package janet-ts-mode :vc (:url "https://github.com/sogaiu/janet-ts-mode" :rev :newest))
-
Elpaca - add something like the following to your .emacs equivalent:
(use-package janet-ts-mode :elpaca (:host github :repo "sogaiu/janet-ts-mode" :files ("*.el")))
-
Straight.el - add something like the following to your .emacs equivalent:
(straight-use-package '(janet-ts-mode :host github :repo "sogaiu/janet-ts-mode" :files ("*.el"))) (use-package janet-ts-mode :straight t)
-
First, open a .janet file and check that the current major mode is
janet-ts-mode (e.g. via C-h m). Try M-x janet-ts-mode if it
isn't.
-
Syntax highlighting - if things are working appropriately, non-empty buffer content should be appropriately highlighted.
-
Indentation - type in some code and press the
Tabkey to indent the current line, or select a region and press theTabkey to indent the region [2].Note that by default, within multi-line long-strings, indentation might feel inconvenient or otherwise not to your liking [3]. See the Indentation within Multi-Line Long-Strings section for more details.
-
Imenu - to end up at a particular top-level definition,
M-x imenu, and follow the prompts. Alternatively, look for a menu namedIndexand interact with that appropriately :) -
Navigation - use
M-x treesit-end-of-defun(C-M-e) to navigate to the end of the currentdefun(kind of tricky to explain, but something like "top-level" form for the case of a lispy language in Emacs), orM-x treesit-beginning-of-defun(C-M-a) to navigate to the beginning of the currentdefun, or if not within adefun, conveniently navigate through the buffer by repeatedly invokingM-x treesit-end-of-defunto go in the forward direction (toward the end of the buffer), orM-x treesit-beginning-of-defunto go in the reverse direction (toward the beginning of the buffer). -
Which-Func - when within a top-level definition, to see a guessed name for the definition,
M-x which-function-mode, and observe the mode line. If things went well, probably toward the right of the mode line there should be some text like:[nice-function-name]if point is within a definition with namenice-function-name.
-
Tree-sitter's view of the source -
M-x treesit-explore-modeand choosejanet-simpleto open a second buffer (named*tree-sitter explorer for ...*) that shows a view of what the tree-sitter grammar thinks the code in the buffer parses as. Likely there will be a fair number of instances of things likesym_lit,par_tup_lit,num_lit, etc. These are node names associated with the tree-sitter-janet-simple grammar. Clicking on node names in the newly opened buffer should cause point in the corresponding buffer with Janet source code in it to move accordingly. -
Tree-sitter queries -
M-: (treesit-query-validate ...)andM-: (treesit-query-string ...)... it's kind of complicated...please see comments injanet-ts-mode.el(^^;
There's a file named janet-ts-experiment.el that contains a variety
of convenience commands. As the file name suggests, the content is
experimental. Not sure what if anything I'll keep from it, but
currently it has:
-
Selection
- Select something relevant around point
- Expand current selection
-
Formatting Helpers
- Split across lines, content of selection at opening parens
- Format pairs within selection to be on own lines
-
Wrapping
- Wrap something at point in
tracevcall - Unwrap a
tracevcall that contains point
- Wrap something at point in
-
Comment and Long-String Folding
- Fold aforementioned forms so they don't take much space
- Unfold folded forms so the content can be seen
- Toggle the folding of certain forms
-
Delimiters
- Cycle delimiters:
(...)->[...]->{...}and back to parens - Move right delimiter at point over the next thing to the right - if you're not a structural editor user and have found auto-balanced delimiters (particularly parens) to get in your way, this function might be appealing.
- Cycle delimiters:
If the file is required, it should add various things to the
Janet-TS menu. Alternatively, using the "Enable Experimental
Features" menu item under the Janet-TS menu may work too.
There's another file named janet-ts-helpers.el that provides limited
integration with a number of Janet-related command line utilities:
Again, if the file is required, it should add various things to the
Janet-TS menu. Alternatively, using the "Enable Helpers Features"
menu item under the Janet-TS menu may work too.
As mentioned elsewhere, by default janet-ts-mode does not modify
whitespace within long-strings in response to the Tab key being
pressed [3].
One user reported that customization similar to the following yielded behavior more to their tastes:
(add-hook
'janet-ts-mode-hook
(lambda ()
(bind-key "C-<return>" (lambda ()
(interactive)
(electric-indent-just-newline nil)
(indent-relative t))
janet-ts-mode-map)
(bind-key "C-<tab>" (lambda ()
(interactive)
(indent-relative t))
janet-ts-mode-map)))- ahungry - theme and other discussions
- AlbertoGP - PR 12 to janet-mode
- alternateved - report and suggestion
- amano.kenji - testing and installation instruction suggestions
- bakpakin and Janet contributors
- bbatsov - clojure-ts-mode discussions
- casouri and other Emacs contributors - Emacs tree-sitter integration
- CosmicToast - use-package installation info
- dannyfreeman - clojure-ts-mode and discussions
- dgutov - ruby-ts--syntax-propertize work
- Fanael - rainbow-delimiters and syntax table discussion
- maxbrunsfeld and tree-sitter contributors - tree-sitter
- ml-2 - suggestions and discussions
- sbocq - discussion about
treesit-*-of-defun - thechampagne - autoloads
- ubolonton and elisp-tree-sitter contributors
- yrns - PR 11 to janet-mode
[1] If building Emacs >= 30.x from source (typically to get it with tree-sitter support), links below might be of interest:
- Debian / Ubuntu -- this should become easier once Emacs-tree-sitter integration ships.
- Void -- the easiest way I've tried so far :)
- These steps give instructions that are a bit less distribution-specific (though there is a trade-off regarding setup of things so that Emacs knows where to find the tree-sitter library).
- How to Get Started with Tree-Sitter might be worth looking at, though I haven't examined the details.
[2] Please note that although the description uses the phrase "press
the Tab key", what is actually meant is invoking the
indent-for-tab-command function in Emacs (which is what Tab is
bound to by default in many non-C programming-related major modes).
[3] The TLDR version of why the indentation works the way it does by default is:
-
Most other major modes for lisp-like languages behave this way.
-
An effort has been made to preserve the alleged intent of the author of the code.
See below for a more detailed explanation.
As mentioned above, most major modes for lisp-like languages (apart
from Clojure) in Emacs, behave in the same way by default with respect
to indentation, e.g. pressing Tab by itself does not result in any
change to the text. (This can be easily experienced by interacting
appropriately with docstrings for Emacs Lisp constructs.) So in that
sense janet-ts-mode's current arrangement is not particularly
divergent from most other similar efforts.
There is at least one other reason for the current behavior but it requires a bit of detailed explanation. It turns out that long-strings in Janet become ordinary strings after parsing and as is documented on the website:
Janet will automatically remove indentation (so-called "dedenting") for whitespace that appears before the column in which the long-string began.
That description seems to leave out a little bit, but the following examples should help with clarifying what happens:
(def a
``
First line
Second line
``)
# =>
" First line\n Second line\n "
(def b
``
First line
Second line
``)
# =>
"First line\nSecond line"Evaluation has not resulted in the string associated with a to be
different than what is visible in the code. For example, there are
two leading spaces and there is a single space after the first
newline. Further, the string has a second newline and ends with two
spaces.
In contrast, the ordinary string that is associated with b after
evaluation does not have any leading or trailing whitespace and there
is only a single newline. The second case is the type of situation
the quoted text above regarding "dedenting" refers to.
When parsed, multi-line long-string content is only modified if the first non-whitespace character on every line (after the first line) occurs at or after the column of the leftmost character of the opening delimiter of the long-string.
As can be seen in the example above for a, the "S" in "Second"
occurs before the leftmost character of the opening delimiter of the
long-string and as such, dedenting does not happen for any portion of
the long-string.
Editor indentation features can lead to changes in whitespace and as such, depending on how that's implemented, can in some cases lead to undesirable / unintended (and undetected) changes in the intent reflected in the formatting of a multi-line long-string due to the "dedenting" feature described above.
Two situations in which this might happen are if large sections of one's buffer or the entire buffer is selected and indentation is requested. In such cases there may be long-strings that are not visible being changed and if so the user will not see them and/or be aware of them and thus fail to detect whether the existing intent has been modified in ways contrary to their desires. Note that there may be more situations, the ones described above are presented to give one some general sense of the situation.
A safe way to preserve an author's alleged intent is to not modify whitespace within a long-string. This is the default behavior in janet-ts-mode.