New Scala 3 syntax in Emacs

Scala 3 has a new “quiet” syntax, which means it uses whitespace and indention to delineate blocks of code, instead of the parentheses. Currently emacs-scala-mode does not support this yet fully, and while it is in the works the quick solution is to turn of automatic indention for scala 3 projects. To achieve this two things are required, it needs to be detected that a scala file is part of a scala 3 project, and second the automatic indention and reformatting needs to be turned off.

A quick, and somewhat hacky, way to detect scala 3 projects is to check the build.sbt file for the version, using projectile to find the root of the project and searching for a scalaVersion to start with 3.

(defun is-scala3-project ()
  "Check if the current project is using scala3.

Loads the build.sbt file for the project and serach for the scalaVersion."
  (projectile-with-default-dir (projectile-project-root)
    (when (file-exists-p "build.sbt")
      (with-temp-buffer
        (insert-file-contents "build.sbt")
        (search-forward "scalaVersion := \"3" nil t)))))

Now using emacs advice-add the function adding the hooks to indent and format can be modified to no longer no longer insert the hooks for automatic formatting

(defun with-disable-for-scala3 (orig-scala-mode-map:add-self-insert-hooks &rest arguments)
    "When using scala3 skip adding indention hooks."
    (unless (is-scala3-project)
      (apply orig-scala-mode-map:add-self-insert-hooks arguments)))

(advice-add #'scala-mode-map:add-self-insert-hooks :around #'with-disable-for-scala3)

To still have some indention but not use the scala syntax specific one, the indent-line-function needs to be replaced with one purely using the previous line for reference ignoring syntax.

(defun disable-scala-indent ()
  "In scala 3 indent line does not work as expected due to whitespace grammar."
  (when (is-scala3-project)
    (setq indent-line-function 'indent-relative-maybe)))

(add-hook 'scala-mode-hook #'disable-scala-indent)

While it is more convenient to have indention supported, while the issue in emacs-scala-mode is being worked on this provides a quick fix to still have scala 3 code formatted. Also with metals and lsp supporting scala 3, the buffer can be reformatted in case formatting is an issue.

Attaching Metals LSP debugger to existing process in Emacs

Metals supports the DAP Protocol which allows to debug Scala code in Emacs. By default all the setup is done to use code lenses to start an instance of the program to debug it, set breakpoints inspect variables, all the good stuff to expect from a debugger. It is not as obvious how to debug a remote Scala process however as this requires a bit of setup.

Setting up the Scala process

Scala, as running on the JVM, allows to be run with the well know flags to be debugged namely

-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005

When using sbt this can be simplified to running as

sbt -jvm-debug 5005

which launches an sbt shell exposing a debug port 5005 on localhost. It is good practice to always use localhost and if truly remote to use SSH port forwarding, as the debugger has no authentication or any security in place.

Attaching the debugger

In dap-mode in Emacs uses debug-templates to determine what to run, in the case of scala and metals we need to provide the connection information as well as the lsp-project to use as the source for the code to set breakpoints and in general browse the code. The project name to use can normally be found in the build.sbt file this means for a project like the one associated with the FP Tower Foundations Course the setup looks like

(dap-register-debug-template
  "Scala Attach Foundations (localhost:5005)"
  '(:type "scala"
    :request "attach"
    :name "Scala Attach Foundations (localhost:5005)"
    :hostName "localhost"
    :port 5005
    :targets [(:uri "file:///Users/pfehre/source/foundations?id=foundation")]))

important is the targets uri in this case as the default template for scala attach sets the project name to root resulting in the invalid target file:///Users/pfehre/source/foundations?id=root. With this setup and evaluated the commad dap-debug will now contain a Scala Attach Foundations (localhost:5005) target to use to attach to the process.

As this is project specific setup it makes sense to add it to the .dir-locals.el file so it automatically gets registered when visiting the project folder.

((nil . ((eval . (dap-register-debug-template
                  "Scala Attach Foundations (localhost:5005)"
                  '(:type "scala"
                    :request "attach"
                    :name "Scala Attach Foundations (localhost:5005)"
                    :hostName "localhost"
                    :port 5005
                    :targets [(:uri "file:///Users/pfehre/source/foundations?id=foundation")]))))))

would make this work.

Bloop integration in Emacs

Some quick and hacky integration of Scala bloop with Emacs, using ammonite as the console and projectile for compilation and testing.

;;; bloop --- bloop minor mode
;; Author: Philipp Fehre <philipp@fehre.co.uk>
;; Keywords: scala, bloop, tools, convenience
;;; Commentary:
;; Helpers to integrate better with bloop, inspired by emacs-bloop
;; https://github.com/tues/emacs-bloop/blob/master/bloop.el
;; C-c M-j jack-in a bloop project running a new Ammonite REPL buffer
;; C-c M-z switch to an active Ammonite REPL
;; C-c b c Compile a bloop project backed by projectile-compile-project
;; C-c b t Test a bloop project backed by projectile-test-project
;; C-c b r Run a bloop project
;; Changelog:
;; - 25/8/2020 - Added run command mapping bloop-run (C-c b r)
;; - 1/5/2020 - initial working version
;;; Code:
(require 'projectile)
(require 'scala-mode)
(require 'ammonite-term-repl)
(require 's)
(defgroup bloop nil
"Bloop integration for emacs"
:group 'tools
:group 'convenience
:link '(url-link :tag "Gist" "https://gist.github.com/sideshowcoder/a9b2ceaca38cdf0ea95f29bf0130b171"))
(defcustom bloop-program-name "bloop"
"Program used to run bloop commands, default to whatever is in the path."
:type 'string
:group 'bloop)
(defcustom bloop-reporter "scalac"
"Either bloop or scalac.
The main difference is that bloop shows errors in reverse order.
Emacs generally assumes the first error in the output is the most
relevant so the scalac reporter will most likely be preferred.
This is used for test and compile."
:type 'string
:group 'bloop)
(defun bloop--command (&rest args)
"Build a bloop command for ARGS."
(s-join " " (cons bloop-program-name args)))
(defun bloop--available-projects ()
"Get a list of currently available projects from bloop."
(projectile-with-default-dir (projectile-project-root)
(let ((projects-string (shell-command-to-string (bloop--command "projects"))))
(split-string projects-string))))
(defun bloop-switch-to-ammonite ()
"Switch to the running Ammonite REPL."
(interactive)
(if-let ((ammonite-buffer (get-buffer ammonite-term-repl-buffer-name)))
(switch-to-buffer ammonite-buffer)
(message "Ammonite is not running try C-c M-j to start an Ammonite REPL for bloop.")))
(defun bloop-run-ammonite (project)
"Run Ammonite for a bloop PROJECT."
(interactive (list (completing-read "Run Ammonite REPL for project: " (bloop--available-projects))))
(projectile-with-default-dir (projectile-project-root)
(let ((ammonite-term-repl-program bloop-program-name)
(ammonite-term-repl-program-args (list "console" project)))
(run-ammonite))))
(defun bloop-compile (project)
"Compile a bloop PROJECT."
(interactive (list (completing-read "Compile bloop project: " (bloop--available-projects))))
(let ((command (bloop--command "compile" project)))
(projectile--run-project-cmd command projectile-compilation-cmd-map
:show-prompt 't
:prompt-prefix "Compile command: "
:save-buffers t)))
(defun bloop-test (project)
"Test a bloop PROJECT."
(interactive (list (completing-read "Test bloop project: " (bloop--available-projects))))
(let ((test-command (bloop--command "test" "--reporter" bloop-reporter project)))
(projectile--run-project-cmd test-command projectile-test-cmd-map
:show-prompt 't
:prompt-prefix "Test command: "
:save-buffers t)))
(defun bloop-run (project)
"Run a bloop PROJECT."
(interactive (list (completing-read "Run bloop project: " (bloop--available-projects))))
(let ((run-command (bloop--command "run" project)))
(projectile--run-project-cmd run-command projectile-run-cmd-map
:show-prompt 't
:prompt-prefix "Run command: "
:save-buffers t)))
;;;###autoload
(define-minor-mode bloop-mode
"Bloop integration for emacs."
:lighter " bloop"
:keymap (let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c M-j") #'bloop-run-ammonite)
(define-key map (kbd "C-c M-z") #'bloop-switch-to-ammonite)
(define-key map (kbd "C-c b c") #'bloop-compile)
(define-key map (kbd "C-c b t") #'bloop-test)
(define-key map (kbd "C-c b r") #'bloop-run)
map))
;;;###autoload
(add-hook 'scala-mode-hook 'bloop-mode)
(provide 'bloop)
;;; bloop.el ends here
view raw bloop.el hosted with ❤ by GitHub