Posts tagged "cabal":
Nix, cabal, and tests
At work I decided to attempt to change the setup of one of our projects from using
to the triplet I tend to prefer
During this I ran into two small issues relating to tests.
hspec-discover
both is, and isn't, available in the shell
I found mentions of this mentioned in an open cabal ticket and someone even made a git repo to explore it. I posted a question on the Nix discorse.
Basically, when running cabal test
in a dev shell, started with nix develop
,
the tool hspec-discover
wasn't found. At the same time the packages was
installed
(ins)$ ghc-pkg list | rg hspec hspec-2.9.7 hspec-core-2.9.7 (hspec-discover-2.9.7) hspec-expectations-0.8.2
and it was on the $PATH
(ins)$ whereis hspec-discover hspec-discover: /nix/store/vaq3gvak92whk5l169r06xrbkx6c0lqp-ghc-9.2.8-with-packages/bin/hspec-discover /nix/store/986bnyyhmi042kg4v6d918hli32lh9dw-hspec-discover-2.9.7/bin/hspec-discover
The solution, as the user julm pointed out, is to simply do what cabal
tells
you and run cabal update
first.
Dealing with tests that won't run during build
The project's tests were set up in such a way that standalone tests and
integration tests are mixed into the same test executable. As the integration
tests need the just built service to be running they can't be run during nix
build
. However, the only way of preventing that, without making code changes,
is to pass an argument to the test executable, --skip=<prefix>
, and I believe
that's not possible when using developPackage
. It's not a big deal though,
it's perfectly fine to run the tests separately using nix develop . command
...
. However, it turns out developPackage
and the underlying machinery is
smart enough to skip installing package required for testing when it's turned
off (using dontCheck
). This is the case also when returnShellEnv
is true
.
Luckily it's not too difficult to deal with it. I already had a variable
isDevShell
so I could simply reuse it and add the following expression to
modifier
(if isDevShell then hl.doCheck else hl.dontCheck)
Cabal, tree-sitter, and consult
After my last post I thought I'd move on to implement the rest of the functions
in haskell-mode's major mode for Cabal, functions like
haskell-cabal-goto-library-section
and
haskell-cabal-goto-executable-section
. Then I realised that what I really
want is a way to quickly jump to any section, that is, I want consult-cabal
!
What follows is very much a work-in-progress, but hopefully it'll show enough promise.
Listing the sections
As I have a tree-sitter
parse tree to hand it is fairly easy to fetch all the
nodes corresponding to sections. Since the last post I've made some
improvements to the parser and now the parse tree looks like this (I can
recommend the function treesit-explore-mode
to expore the parse tree, I've
found it invaluable ever since I realised it existed)
(cabal ... (properties ...) (sections (common common (section_name) ...) (library library ...) (executable executable (section_name) ...) ...))
That is, all the sections are children of the node called sections
.
The function to use for fetching all the nodes is treesit-query-capture
, it
needs a node to start on, which this case should be the full parse tree,
i.e. (treesit-buffer-root-node 'cabal)
and a query string. Given the
structure of the parse tree, and that I want to capture all children of
sections
, a query string like this one works
"(cabal (sections (_)* @section))"
Finally, by default treesit-query-capture
returns a list of tuples of the form
(<capture> . <node>)
, but in this case I only want the list of nodes, so the
full call will look like this
(treesit-query-capture (treesit-buffer-root-node 'cabal) "(cabal (sections (_)* @section))" nil nil t)
Hooking it up to consult
As I envision adding more things to jump to in the future, I decided to make use
of consult--multi
. That in turn means I need to define a "source" for the
sections. After a bit of digging and rummaging in the consult source I put
together this
(defvar consult-cabal--source-section `(:name "Sections" :category location :action ,#'consult-cabal--section-action :items ,#'consult-cabal--section-items) "Definition of source for Cabal sections.")
which means I need two functions, consult-cabal--section-action
and
consult-cabal--section-items
. I started with the latter.
Getting section nodes as items for consult
It took me a while to work understand how this would ever be able to work. The
function that :items
point to must return a list of strings, but how would I
ever be able to use just a string to jump to the correct location?
The solution is in a comment in the documentation of consult--multi
:
:items - List of strings to select from or function returning list of strings. Note that the strings can use text properties to carry metadata, which is then available to the :annotate, :action and :state functions.
I'd never come across text properties in Emacs before, so at first I
completely missed those two words. Once I'd looked up the concept in the
documentation everything fell into place. The function
consult-cabal--section-items
would simply attach the relevant node as a text
property to the strings in the list.
My current version, obviously a work-in-progress, takes a list of nodes and turns them naïvely into a string and attaches the node. I split it into two functions, like this
(defun consult-cabal--section-to-string (section) "Convert a single SECTION node to a string." (propertize (format "%S" section) :treesit-node section)) (defun consult-cabal--section-items () "Fetch all sections as a list of strings ." (let ((section-nodes (treesit-query-capture (treesit-buffer-root-node 'cabal) "(cabal (sections (_)* @section))" nil nil t))) (mapcar #'consult-cabal--section-to-string section-nodes)))
Implementing the action
The action function is called with the selected item, i.e. with the string and
its properties. That means, to jump to the selected section the function needs
to extract the node property, :treesit-node
, and jump to the start of it. the
function to use is get-text-property
, and as all characters in the string will
have to property I just picked the first one. The jumping itself I copied from
the navigation functions I'd written before.
(defun consult-cabal--section-action (item) "Go to the section referenced by ITEM." (when-let* ((node (get-text-property 0 :treesit-node item)) (new-pos (treesit-node-start node))) (goto-char new-pos)))
Tying it together with consult--multi
The final function, consult-cabal
, looks like this
(defun consult-cabal () "Choose a Cabal construct and jump to it." (interactive) (consult--multi '(consult-cabal--source-section) :sort nil))
Conclusions and where to find the code
The end result works as intended, but it's very rough. I'll try to improve it a bit more. In particular I want
- better strings -
(format "%S" node)
is all right to start with, but in the long run I want strings that describe the sections, and - preview as I navigate between items - AFAIU this is what the
:state
field is for, but I still haven't looked into how it works.
The source can be found here.
Making an Emacs major mode for Cabal using tree-sitter
A few days ago I posted on r/haskell that I'm attempting to put together a Cabal grammar for tree-sitter. Some things are still missing, but it covers enough to start doing what I initially intended: experiment with writing an alternative Emacs major mode for Cabal.
The documentation for the tree-sitter integration is very nice, and several of
the major modes already have tree-sitter variants, called X-ts-mode
where X
is e.g. python
, so putting together the beginning of a major mode wasn't too
much work.
Configuring Emacs
First off I had to make sure the parser for Cabal was installed. The snippet for that looks like this1
(use-package treesit :straight nil :ensure nil :commands (treesit-install-language-grammar) :init (setq treesit-language-source-alist '((cabal . ("https://gitlab.com/magus/tree-sitter-cabal.git")))))
With that in place the parser is installed using M-x
treesit-install-language-grammar
and choosing cabal
.
After that I removed my configuration for haskell-mode
and added the following
snippet to get my own major mode into my setup.
(use-package my-cabal-mode :straight (:type git :repo "git@gitlab.com:magus/my-emacs-pkgs.git" :branch "main" :files (:defaults "my-cabal-mode/*el")))
The major mode and font-locking
The built-in elisp documentation actually has a section on writing a major mode with tree-sitter, so it was easy to get started. Setting up the font-locking took a bit of trial-and-error, but once I had comments looking the way I wanted it was easy to add to the setup. Oh, and yes, there's a section on font-locking with tree-sitter in the documentation too. At the moment it looks like this
(defvar cabal--treesit-font-lock-setting (treesit-font-lock-rules :feature 'comment :language 'cabal '((comment) @font-lock-comment-face) :feature 'cabal-version :language 'cabal '((cabal_version _) @font-lock-constant-face) :feature 'field-name :language 'cabal '((field_name) @font-lock-keyword-face) :feature 'section-name :language 'cabal '((section_name) @font-lock-variable-name-face)) "Tree-sitter font-lock settings.") ;;;###autoload (define-derived-mode my-cabal-mode fundamental-mode "My Cabal" "My mode for Cabal files" (when (treesit-ready-p 'cabal) (treesit-parser-create 'cabal) ;; set up treesit (setq-local treesit-font-lock-feature-list '((comment field-name section-name) (cabal-version) () ())) (setq-local treesit-font-lock-settings cabal--treesit-font-lock-setting) (treesit-major-mode-setup))) ;;;###autoload (add-to-list 'auto-mode-alist '("\\.cabal\\'" . my-cabal-mode))
Navigation
One of the reasons I want to experiment with tree-sitter is to use it for code
navigation. My first attempt is to translate haskell-cabal-section-beginning
(in haskell-mode
, the source) to using tree-sitter. First a convenience
function to recognise if a node is a section or not
(defun cabal--node-is-section-p (n) "Predicate to check if treesit node N is a Cabal section." (member (treesit-node-type n) '("benchmark" "common" "executable" "flag" "library" "test_suite")))
That makes it possible to use treesit-parent-until
to traverse the nodes until
hitting a section node
(defun cabal-goto-beginning-of-section () "Go to the beginning of the current section." (interactive) (when-let* ((node-at-point (treesit-node-at (point))) (section-node (treesit-parent-until node-at-point #'cabal--node-is-section-p)) (start-pos (treesit-node-start section-node))) (goto-char start-pos)))
And the companion function, to go to the end of a section is very similar
(defun cabal-goto-end-of-section () "Go to the end of the current section." (interactive) (when-let* ((node-at-point (treesit-node-at (point))) (section-node (treesit-parent-until node-at-point #'cabal--node-is-section-p)) (end-pos (treesit-node-end section-node))) (goto-char end-pos)))
Footnotes:
I'm using straight.el and use-package
in my setup, but hopefully the
snippets can easily be converted to other ways of configuring Emacs.