Where parallels cross

Interesting bits of life

My Emacs setup for Scala development

Too long; didn't read

At the moment the best tools for your Scala development on Emacs are sbt-mode, metals, ob-amm, and scala-mode!

The problem

You do not use IntelliJ?!? I heard that question so many times that I enjoy the sound of it. Emacs for me is better. I started with Emacs by writing OCaml and I soon started working on Scala. At the time scala-mode was enough: we were just writing prototypes.

When I landed in a working environment where projects need multiple submodules, custom sbt executable loading certificates and other weird things... well I could feel the pressure.

A couple of years ago I was using the cool Ensime (I even developed my little scripts to use it for literate programming!), and I am really thankful for its development.

Now I would like to share the setup I built little by little for having a good Emacs environment.

A disclaimer: I do not use a debugger. My mind and printlns are enough (yours are too IMHO).

It is a problem indeed

Code navigation and completion is traumatic in Scala. There are moments where you have to complete to know what the variable at point has to offer. Sometimes having that auto-completion feature feels to me like I do not know what I am doing: ideally when I am writing a function I should know input and outputs clearly enough to know what I am operating on. Still is still a life changing feature. Same for easy compilation and test run.

So let's get to the configuration I use!

And there is a solution

First, syntax coloring is just too nice:

(use-package scala-mode
  :mode "\\.s\\(cala\\|bt\\)$"
  :config
    (load-file "~/.emacs.d/lisp/ob-scala.el"))

This use-package clause activates when the file ends for ".scala" or ".sbt".

Did you note the ob-scala load statement? Well I like literate programming so we need to be able to run scala Org Mode source blocks. It does not come by default (anymore) so I load the old ob-scala: you can find the code in the Appendix.

If you like literal programming with Org Mode too, add this as well:

(org-babel-do-load-languages
'org-babel-load-languages
'(; likely other languages here
  (scala . t)))

Now if you need to test libraries available online this is practically insufficient. Sometimes I want just to download a dependency and see how it works. A tool that sweetens this kind of exploration is Ammonite. And Emacs has some modes for it too:

(use-package ob-ammonite
  :ensure-system-package (amm . "sudo sh -c '(echo \"#!/usr/bin/env sh\" && curl -L https://github.com/lihaoyi/Ammonite/releases/download/2.0.4/2.13-2.0.4) > /usr/local/bin/amm && chmod +x /usr/local/bin/amm' && amm")
  :defer 1
  :config
  (use-package ammonite-term-repl)
  (setq ammonite-term-repl-auto-detect-predef-file nil)
  (setq ammonite-term-repl-program-args '("--no-remote-logging" "--no-default-predef" "--no-home-predef"))
  (defun my/substitute-sbt-deps-with-ammonite ()
    "Substitute sbt-style dependencies with ammonite ones."
    (interactive)
    (apply 'narrow-to-region (if (region-active-p) (my/cons-cell-to-list (region-bounds)) `(,(point-min) ,(point-max))))
    (goto-char (point-min))
    (let ((regex "\"\\(.+?\\)\"[ ]+%\\{1,2\\}[ ]+\"\\(.+?\\)\"[ ]+%\\{1,2\\}[ ]+\"\\(.+?\\)\"")
          (res))
      (while (re-search-forward regex nil t)
        (let* ((e (point))
               (b (search-backward "\"" nil nil 6))
               (s (buffer-substring-no-properties b e))
               (s-without-percent (apply 'concat (split-string s "%")))
               (s-without-quotes (remove-if (lambda (x) (eq x ?" ;"
                                                            ))
                                            s-without-percent))
               (s-as-list (split-string s-without-quotes)))
                    (delete-region b e)
          (goto-char b)
          (insert (format "import $ivy.`%s::%s:%s`" (first s-as-list) (second s-as-list) (third s-as-list)))
          )
        )
      res)
    (widen)))

Note that the :ensure-system-package clause will download Ammonite automatically for me if not available on the system.

Essentially ob-ammonite will make available amm Org Mode source blocks, in which you can easily download dependencies with the special import syntax import $ivy ....

Since a lot of dependencies I need to test come in the sbt format (the Scala major build tool), I also made a function that translates from sbt syntax to Ammonite syntax. Just copy the typical "org.scalaz" %% "scalaz-core" % "7.3.0-SNAPSHOT", move your pointer on it and do M-x my/substitute-sbt-deps-with-ammonite: you will have the dependency substituted and ready to be used in your Ammonite block or REPL.

Yes, because you can also just launch a Scala REPL with ammonite-term-repl! Very useful to test ideas on the fly.

Speaking of sbt, this is how I integrate the build tool for compiling, running and testing my project:

(use-package sbt-mode
  :commands sbt-start sbt-command
  :custom
   (sbt:default-command "testQuick")
  :config
  ;; WORKAROUND: https://github.com/ensime/emacs-sbt-mode/issues/31
  ;; allows using SPACE when in the minibuffer
  (substitute-key-definition
   'minibuffer-complete-word
   'self-insert-command
   minibuffer-local-completion-map))

Note that I use most often the sbt testQuick command which runs just the most relevant Scala tests, so I made that my default command.

The rest of my configuration is taken from https://scalameta.org/metals/docs/editors/emacs.html:

(use-package lsp-mode
  ;; Optional - enable lsp-mode automatically in scala files
  :hook (scala-mode . lsp)
  :config (setq lsp-prefer-flymake nil))

;; Add company-lsp backend for metals
(use-package company-lsp) ; you need company mode as well for this

(use-package lsp-metals)

(use-package lsp-ui) ;; this is necessary to get info about compilation

This makes Emacs feel like a Scala IDE with auto-completion (of imports!), type annotations, and various other nice features the scalameta community is working on (they are amazing people by the way, and I found it fun to contribute to Scalafix sometimes ago).

Conclusion

If you are a Scala developer, no more excuses: load all of this in your configuration and start coding! If not, Scala is a curious language worth a try (in particular if you are a Java developer :).

Happy hacking!

Appendix

This is the old mode (suggested by @hb9 on GitHub: https://github.com/hvesalai/emacs-scala-mode/issues/148#issuecomment-393521131):

;;; ob-scala.el --- org-babel functions for Scala evaluation

;; Copyright (C) 2012  Free Software Foundation, Inc.

;; Author: Andrzej Lichnerowicz
;; Keywords: literate programming, reproducible research
;; Homepage: http://orgmode.org

;; This file is part of GNU Emacs.

;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:
;; Currently only supports the external execution.  No session support yet.

;;; Requirements:
;; - Scala language :: http://www.scala-lang.org/
;; - Scala major mode :: Can be installed from Scala sources
;;  https://github.com/scala/scala-dist/blob/master/tool-support/src/emacs/scala-mode.el

;;; Code:
(require 'ob)
(require 'ob-ref)
(require 'ob-comint)
(require 'ob-eval)
(eval-when-compile (require 'cl))

(defvar org-babel-tangle-lang-exts) ;; Autoloaded
(add-to-list 'org-babel-tangle-lang-exts '("scala" . "scala"))
(defvar org-babel-default-header-args:scala '())
(defvar org-babel-scala-command "scala"
  "Name of the command to use for executing Scala code.")

(defun org-babel-execute:scala (body params)
  "Execute a block of Scala code with org-babel.  This function is
called by `org-babel-execute-src-block'"
  (message "executing Scala source code block")
  (let* ((processed-params (org-babel-process-params params))
         (session (org-babel-scala-initiate-session (nth 0 processed-params)))
         (vars (nth 1 processed-params))
         (result-params (nth 2 processed-params))
         (result-type (cdr (assoc :result-type params)))
         (full-body (org-babel-expand-body:generic
                     body params))
         (result (org-babel-scala-evaluate
                  session full-body result-type result-params)))

    (org-babel-reassemble-table
     result
     (org-babel-pick-name
      (cdr (assoc :colname-names params)) (cdr (assoc :colnames params)))
     (org-babel-pick-name
      (cdr (assoc :rowname-names params)) (cdr (assoc :rownames params))))))


(defun org-babel-scala-table-or-string (results)
  "Convert RESULTS into an appropriate elisp value.
If RESULTS look like a table, then convert them into an
Emacs-lisp table, otherwise return the results as a string."
  (org-babel-script-escape results))


(defvar org-babel-scala-wrapper-method

"var str_result :String = null;

Console.withOut(new java.io.OutputStream() {def write(b: Int){
}}) {
  str_result = {
%s
  }.toString
}

print(str_result)
")


(defun org-babel-scala-evaluate
  (session body &optional result-type result-params)
  "Evaluate BODY in external Scala process.
If RESULT-TYPE equals 'output then return standard output as a string.
If RESULT-TYPE equals 'value then return the value of the last statement
in BODY as elisp."
  (when session (error "Sessions are not (yet) supported for Scala"))
  (case result-type
    (output
     (let ((src-file (org-babel-temp-file "scala-")))
       (progn (with-temp-file src-file (insert body))
              (org-babel-eval
               (concat org-babel-scala-command " " src-file) ""))))
    (value
     (let* ((src-file (org-babel-temp-file "scala-"))
            (wrapper (format org-babel-scala-wrapper-method body)))
       (with-temp-file src-file (insert wrapper))
       ((lambda (raw)
          (if (member "code" result-params)
              raw
            (org-babel-scala-table-or-string raw)))
        (org-babel-eval
         (concat org-babel-scala-command " " src-file) ""))))))


(defun org-babel-prep-session:scala (session params)
  "Prepare SESSION according to the header arguments specified in PARAMS."
  (error "Sessions are not (yet) supported for Scala"))

(defun org-babel-scala-initiate-session (&optional session)
  "If there is not a current inferior-process-buffer in SESSION
then create.  Return the initialized session.  Sessions are not
supported in Scala."
  nil)

(provide 'ob-scala)



;;; ob-scala.el ends here

Comments