Emacs as a F# IDE (part1)


tags: fsharp, emacs

Why Emacs

Emacs need years to get familiar with. But I think it's worth it. When you are familiar with it, you can customize it as you want, you can write whatever you want in your mind in a few of elisp codes. And there are also tons of libaries available.

F# is a great language. The fsharp-mode is full featured, and here is an introduction and some enhancement.

How

Install

We use fsharp-mode for code highlight, completion, flycheck and mainly everything. And we use ob-fsharp for code evaluating in org-mode.

(require-package 'fsharp-mode 'ob-fsharp)

The libraries are avalible at melpa. Here is the definition of require-package.

(defun require-package (&rest packages)
  (dolist (p packages)
    (unless (package-installed-p p)
      (condition-case nil (package-install p)
        (error
         (package-refresh-contents)
         (package-install p))))))

Completion, Code Check, Type Signature

Code completion, code checking and displaying type signature will work when you setup your project correctly and download all the F# dependencies you need for the project.

/img/completion.gif

/img/flycheck.gif

/img/eldoc.png

Jump to definition

/img/jump.gif

They are bound to M-. and M-,.

Indentation

Fontomas is a F# source code formatter. But there isn't a elisp library for it. However we can write it.

(defun fsharp-fantomas-format-region (start end)
  (interactive "r")
  (let ((source (shell-quote-argument (buffer-substring-no-properties start end)))
        (ok-buffer "*fantomas*")
        (error-buffer "*fantomas-errors*"))
    (save-window-excursion
      (shell-command-on-region
       start end (format "fantomas --indent 2 --pageWidth 99 --stdin %s --stdout" source)
       ok-buffer nil error-buffer)
      (if (get-buffer error-buffer)
          (progn
            (kill-buffer error-buffer)
            (message "Can't format region."))
        (delete-region start end)
        (insert (with-current-buffer ok-buffer
                  (s-chomp (buffer-string))))
        (delete-trailing-whitespace)
        (message "Region formatted.")))))

/img/indent-region.gif

(defun fsharp-fantomas-format-defun ()
  (interactive)
  (let ((origin (point))
        (start) (end))
    (fsharp-beginning-of-block)
    (setq start (point))
    (fsharp-end-of-block)
    ;; skip whitespace, empty lines, comments
    (while (and (not (= (line-number-at-pos) 1))
                (s-matches? "^\n$\\|^//\\|^(\\*" (thing-at-point 'line)))
      (forward-line -1))
    (move-end-of-line 1)
    (setq end (point))
    (fsharp-fantomas-format-region start end)
    (goto-char origin)))

/img/indent-defun.gif

(defun fsharp-fantomas-format-buffer ()
  (interactive)
  (let ((origin (point)))
    (fsharp-fantomas-format-region (point-min) (point-max))
    (goto-char origin)))

Load file to inferior buffer

fsharp-mode provide fsharp-load-buffer-file, which load the current buffer to the inferior fsharp process. However when you need to load some other files as dependencies, you don't have a function like fsharp-load-file. Here is one:

(defun fsharp-load-file (file-name)
  (interactive (comint-get-source "Load F# file: " nil '(fsharp-mode) t))
  (let ((command (concat "#load \"" file-name "\"")))
    (comint-check-source file-name)
    (fsharp-simple-send inferior-fsharp-buffer-name command)))

/img/load-file.gif

Add file to fsproj

When you edit a new F# file, you need to write it to the fsproj file. And when you delete a F# file, you need to remove it from the fsproj file, too.

(defun fsharp-add-this-file-to-proj ()
  (interactive)
  (when-let* ((file-long (f-this-file))
              (project (fsharp-mode/find-fsproj file-long))
              (file (f-filename file-long)))
    (with-current-buffer (find-file-noselect project)
      (goto-char (point-min))
      (unless (re-search-forward file nil t)
        (when (and (re-search-forward "<Compile Include=" nil t)
                   (re-search-backward "<" nil t))
          (insert (format "<Compile Include=\"%s\" />\n    " file))
          (save-buffer))))))
(defun fsharp-remove-this-file-from-proj ()
  (interactive)
  (when-let* ((file-long (f-this-file))
              (project (fsharp-mode/find-fsproj file-long))
              (file (f-filename file-long)))
    (with-current-buffer (find-file-noselect project)
      (goto-char (point-min))
      (when (re-search-forward (format "<Compile Include=\"%s\" />" file) nil t)
        (move-beginning-of-line 1)
        (kill-line)
        (kill-line)
        (save-buffer)))))

/img/add-to-proj.gif

Compile the project

This definition will change the compile directory based on the project type(fake, dotnet), and call compile interactively.

(defun fsharp-compile-project ()
  "Compile project using fake or dotnet."
  (interactive)
  (let ((fake-dir (locate-dominating-file default-directory "build.fsx"))
        (proj (fsharp-mode/find-fsproj (or (f-this-file) ""))))
    (cond (fake-dir (let ((default-directory fake-dir)
                          (compile-command "fake build"))
                      (call-interactively 'compile)))
          (proj (let ((compile-command (format "dotnet build \"%s\"" proj)))
                  (call-interactively 'compile)))
          (t (call-interactively 'compile)))))

/img/fsharp-compile-project.gif

Prettify symbols

You can change the symbols using prettify-symbols-mode.

(defun fsharp-enable-prettify-symbols ()
  (let ((alist '(("->" . ?→)
                 ("<-" . ?←)
                 ("|>" . ?⊳)
                 ("<|" . ?⊲))))
    (setq-local prettify-symbols-alist alist)))

(add-hook 'fsharp-mode-hook
          (lambda ()
            (fsharp-enable-prettify-symbols)))

/img/prettify.png

Manage project

I use eshell to init, install, test, run and compile a F# project. I will introduce how to use that and how to write completion for eshell in the next post.

/img/eshell-dotnet-demo.gif

Conclusion

You can get all the code here.