Trying to get Emacs and Merlin working using just reason-cli (no opam)


#1

I am currently trying to get my setup working with ReasonML and Emacs. I had it working with opam… but apparently I need to stay up to date with ReasonML and that means depending on reason-cli to install everything.

Now I am worried because it seems like ReasonML doesn’t seem to install anything along the lines of a “share/emacs/site-lisp/”, instead the closest thing there is a folder called: ~/.nvm/versions/node/v9.6.1/lib/node_modules/reason-cli/3___________________/i/opam__slash__merlin_extend-0.3.0-bcf19216/ which got my hopes up… but turned out to to be pretty empty:

├── bin
├── doc
├── _esy
│ └── storePrefix
├── etc
├── lib
│ └── merlin_extend
│ ├── extend_driver.cmi
│ ├── extend_driver.mli
│ ├── extend_helper.cmi
│ ├── extend_helper.mli
│ ├── extend_main.cmi
│ ├── extend_main.mli
│ ├── extend_protocol.cmi
│ ├── extend_protocol.ml
│ ├── merlin_extend.a
│ ├── merlin_extend.cma
│ ├── merlin_extend.cmxa
│ └── META
├── man
├── sbin
└── share

Has anyone gotten Emacs and Merlin from reason-cli to work together?

edit: I just got my hopes up a lot finding this directory:
/home/kuwze/.nvm/versions/node/v9.6.1/lib/node_modules/bs-platform/vendor/ocaml/emacs
but it turns out to be for the outdated caml-mode… why is that being installed instead the Emacs lisp source for merlin?!? I tried looking more into caml-mode and it is so outdated that the last current reference to it is through an archive.org link.


#2

So… I got it working. Basically just git cloned the merlin and utop projects from github.

Here is my setup to help anyone else in the future:

;; company instead of ac-complete
;; I specifically do not use autocomplete so no (setq merlin-ac-setup 'easy)
(use-package company
  :ensure t
  :config
  (add-hook 'after-init-hook 'global-company-mode)
  (setq company-dabbrev-downcase 0)
  (setq company-idle-delay 0))

;; reason setup
(defun shell-cmd (cmd)
  "Returns the stdout output of a shell command or nil if the command returned
   an error"
  (car (ignore-errors (apply 'process-lines (split-string cmd)))))

(let* ((refmt-bin (shell-cmd "which refmt"))
       (merlin-bin (shell-cmd "which ocamlmerlin"))
       (merlin-base-dir (when merlin-bin
			  (replace-regexp-in-string "bin/ocamlmerlin$" "" merlin-bin))))

  (when refmt-bin
    (setq refmt-command refmt-bin))
  
  (add-hook
   'reason-mode-hook
   (lambda ()
     (add-hook 'before-save-hook 'refmt-before-save nil t)
     (merlin-mode))))

(use-package merlin
  ;; note: git clone https://github.com/ocaml/merlin to ~/.emacs.d/bootleg
  :load-path "~/.emacs.d/bootleg/merlin/emacs/"
  :config
  (setq merlin-command (shell-cmd "which ocamlmerlin"))
  (setq merlin-completion-with-doc t)
  
  ;; Make company aware of merlin
  (with-eval-after-load 'company
    (add-hook 'merlin-mode-hook 'company-mode)
    (add-to-list 'company-backends 'merlin-company-backend))

  :bind (:map merlin-mode-map
	      ("M-." . merlin-locate)
	      ("M-," . merlin-pop-stack)
	      ("M-m" . merlin-error-next)
	      ("M-n" . merlin-error-prev)
	      ("C-c C-o" . merlin-occurrences)
	      ("C-c C-j" . merlin-jump)
	      ("C-c i" . merlin-locate-ident)
	      ("C-c C-e" . merlin-iedit-occurrences))
  :hook
  ;; Start merlin on ml files
  (reason-mode . merlin-mode)
  (tuareg-mode . merlin-mode)
  (caml-mode-hook . merlin-mode))

(quelpa '(reason-mode :repo "reasonml-editor/reason-mode" :fetcher github :stable t))
(use-package reason-mode
  :config
  (with-eval-after-load 'utop
    (setq utop-command "rtop -emacs")))

(use-package utop
  ;; note: git clone https://github.com/diml/utop to ~/.emacs.d/bootleg
  :load-path "~/.emacs.d/bootleg/utop/src/top"
  :hook
  (tuareg-mode . utop-minor-mode)
  (reason-mode . utop-minor-mode))

#3

Thank you for providing these steps!

I got such question: did you try to use this with reason-cli 3.1.0 (bsb 2.2.2)? I installed 3.1.0 globally, tried your config, but Emacs still reports crazy errors. Some files without React are parsed ok, but ReasonReact files always show last line as a syntax error.


#4

Would you mind posting a simple ReasonReact test project I could test against? Also I’ll post a more complete Emacs config… I don’t want to post mine entirely because it’s a mixture of terrible coding and horrible practices.


#5

@kuwze, could you please use https://github.com/reasonml-community/reason-react-example .

In Emacs I opened src/todomvc/TodoFooter.re and get such errors:

However, VS Code does not report any errors and ‘npm run start’ also has 0 errors.


#6

So here is some minimal working code. It is smart enough to use reason-cli’s Merlin for ReasonML files and opam’s Merlin for OCaml files. I am very interested in any feedback as what other sensible defaults people should have starting out with Emacs for ReasonML/OCaml code. I also included Helm because I love it and want to introduce it to other people. Also, this depends on opam’s library being included before nvm’s. Here is what I mean (from my .zshrc):

# OPAM configuration
. /home/kuwze/.opam/opam-init/init.zsh > /dev/null 2> /dev/null || true

# nvm stuff
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion

Anyway, here is the GitHub repository.

And here is the init.el to copy from:

;; note:
;; which ocamlmerlin should spit out:
;; /home/`user_name`/.nvm/versions/node/`some-version`/bin/ocamlmerlin
;; and in `M-x ielm` evaluating the following:
;; (expand-file-name "emacs/site-lisp" (car (process-lines "opam" "config" "var" "share")))
;; should spit out:
;; "/home/`user_name`/.opam/`some-version`/share/emacs/site-lisp"


;; main>------------------------------------------------------------------------
(eval-when-compile
  (require 'cl))

(if (fboundp 'paren-set-mode)
    (paren-set-mode 'sexp)
  (defvar show-paren-style)
  (setq show-paren-style 'expression)
  (show-paren-mode t))
;; <main------------------------------------------------------------------------

;; packages>--------------------------------------------------------------------
(require 'package)
(setq package-enable-at-startup nil)
(setq package-archives '(("gnu" . "https://elpa.gnu.org/packages/")
                         ("marmalade" . "https://marmalade-repo.org/packages/")
                         ("MELPA" . "https://melpa.org/packages/")
                         ("MELPA Stable" . "http://stable.melpa.org/packages/")))
(package-initialize)

;; Bootstrap `use-package'
(unless (package-installed-p 'use-package)
  (package-refresh-contents)
  (package-install 'use-package))

(if (require 'quelpa nil t)
    (quelpa-self-upgrade)
  (with-temp-buffer
    (url-insert-file-contents "https://raw.github.com/quelpa/quelpa/master/bootstrap.el")
    (eval-buffer)))
;; <packages--------------------------------------------------------------------

;; ;; path>------------------------------------------------------------------------
(if (string-equal system-type "windows-nt")
    (progn
      (setenv "PATH" (concat
		      "C:\\Program Files\\Git\\usr\\bin" ";" ;; Unix tools
		      (getenv "PATH"))))
  (progn
    (use-package exec-path-from-shell
      :ensure t
      :config
      (when (memq window-system '(mac ns x))
	(exec-path-from-shell-initialize)))))
;; ;; <path------------------------------------------------------------------------

;; helm>------------------------------------------------------------------------
(use-package helm
  :ensure t
  :config
  (helm-mode 1)
  (helm-popup-tip-mode 1)
  (helm-autoresize-mode t)
  (setq helm-autoresize-min-height 40)

  (setq helm-M-x-fuzzy-match t)
  (setq helm-buffers-fuzzy-matching t)
  (setq helm-recentf-fuzzy-match t)
  (setq helm-lisp-fuzzy-completion t)
  
  (require 'helm-eshell)
  (add-hook 'eshell-mode-hook
	    #'(lambda ()
		(define-key eshell-mode-map (kbd "M-l")  'helm-eshell-history)))
  

  ;; (global-set-key (kbd "C-s") #'helm-occur) ; using helm-swoop now
  (global-set-key (kbd "C-c b") #'helm-filtered-bookmarks)
  (global-set-key (kbd "C-c C-b") #'helm-filtered-bookmarks) ; because I am an idiot
  (global-set-key (kbd "C-x C-f") #'helm-find-files)
  (global-set-key (kbd "C-x b") #'helm-mini)
  (global-set-key (kbd "C-x C-b") 'helm-buffers-list)
  (global-set-key (kbd "C-h f") 'helm-apropos)
  (global-set-key (kbd "C-h r") 'helm-info-emacs)
  (global-set-key (kbd "C-h C-l") 'helm-locate-library)
  (global-set-key (kbd "C-c f") 'helm-recentf)
  (global-set-key (kbd "C-h SPC") 'helm-all-mark-rings)
  (global-set-key (kbd "C-c h x") 'helm-register)
  
  (global-set-key (kbd "M-y") 'helm-show-kill-ring)
  (global-set-key (kbd "M-x") #'helm-M-x)

  (define-key minibuffer-local-map (kbd "C-c C-l") 'helm-minibuffer-history)
  
  (define-key helm-map [backspace] #'backward-kill-word))

(use-package helm-swoop
  :ensure t
  :config
  (global-set-key (kbd "C-s") 'helm-swoop-without-pre-input)
  (define-key helm-swoop-map (kbd "C-r") 'helm-previous-line)
  (define-key helm-swoop-map (kbd "C-s") 'helm-next-line))
;; <helm------------------------------------------------------------------------

;; ocaml>-----------------------------------------------------------------------
(let ((opam-share (ignore-errors (car (process-lines "opam" "config" "var" "share")))))
  (when (and opam-share (file-directory-p opam-share))
    (add-to-list 'load-path (expand-file-name "emacs/site-lisp" opam-share))))

(use-package ocp-indent)

(use-package tuareg
  :ensure t
  :config
  (add-hook 'before-save-hook 'ocp-indent-buffer nil t)
  (setq auto-mode-alist 
	(append '(("\\.ml[ily]?$" . tuareg-mode)
		  ("\\.topml$" . tuareg-mode))
		auto-mode-alist)))
;; <ocaml-----------------------------------------------------------------------

;; reasonml>--------------------------------------------------------------------
(defun shell-cmd (cmd)
  "Returns the stdout output of a shell command or nil if the command returned
   an error"
  (car (ignore-errors (apply 'process-lines (split-string cmd)))))

(quelpa '(reason-mode :repo "reasonml-editor/reason-mode" :fetcher github :stable t))
(use-package reason-mode
  :config
  (let* ((refmt-bin (shell-cmd "which refmt")))
    (when refmt-bin
      (setq refmt-command refmt-bin)))
  (add-hook
   'reason-mode-hook
   (lambda ()
     (add-hook 'before-save-hook 'refmt-before-save nil t)
     (setq-local merlin-command (shell-cmd "which ocamlmerlin"))
     (merlin-mode))))
;; <reasonml--------------------------------------------------------------------

;; merlin>----------------------------------------------------------------------
(use-package merlin
  :custom
  (merlin-command 'opam)
  (merlin-completion-with-doc t)
  (company-quickhelp-mode t)
  :config
  (autoload 'merlin-mode "merlin" nil t nil)
  :bind (:map merlin-mode-map
              ("M-." . merlin-locate)
              ("M-," . merlin-pop-stack)
              ("C-c C-o" . merlin-occurrences)
              ("C-c C-j" . merlin-jump)
              ("C-c i" . merlin-locate-ident)
              ("C-c C-e" . merlin-iedit-occurrences))
  :hook
  ;; Start merlin on ml files
  (reason-mode . merlin-mode)
  (tuareg-mode . merlin-mode)
  (caml-mode-hook . merlin-mode))

;; <merlin----------------------------------------------------------------------

;; utop>------------------------------------------------------------------------


(defun reason/rtop-prompt ()
  "The rtop prompt function."
  (let ((prompt (format "rtop[%d]> " utop-command-number)))
    (add-text-properties 0 (length prompt) '(face utop-prompt) prompt)
    prompt))

(use-package utop
  :config
  (defun utop-opam-utop () (progn
			     (setq-local utop-command "opam config exec -- utop -emacs")
			     'utop-minor-mode))
  (defun utop-reason-cli-rtop () (progn
				     (setq-local utop-command (concat (shell-cmd "which rtop") " -emacs"))
				     (setq-local utop-prompt 'reason/rtop-prompt)
				     'utop-minor-mode))
  :hook
  (tuareg-mode . utop-opam-utop)
  (reason-mode . utop-reason-cli-rtop))

;; <utop------------------------------------------------------------------------

;; company>---------------------------------------------------------------------

(use-package company
  :ensure t
  :config
  (add-hook 'after-init-hook 'global-company-mode)
  (setq company-dabbrev-downcase 0)
  (setq company-idle-delay 0))

(use-package company-quickhelp
  :ensure t
  :config
  (company-quickhelp-mode 1)
  (define-key company-active-map (kbd "C-c h") #'company-quickhelp-manual-begin))

;; <company---------------------------------------------------------------------

;; flycheck>--------------------------------------------------------------------

;; someday these will play nicely with both reasonml and ocaml...

(use-package flycheck
  :ensure t
  :config
  (global-flycheck-mode))

(use-package flycheck-popup-tip
  :ensure t
  :config
  (flycheck-popup-tip-mode))

(use-package flycheck-ocaml
  :ensure t
  :config
  (add-hook 'tuareg-mode-hook
	    (lambda ()
	      ;; disable Merlin's own error checking
	      (setq-local merlin-error-after-save nil)    
	      ;; enable Flycheck checker
	      (flycheck-ocaml-setup))))


;; <flycheck--------------------------------------------------------------------

#7

@gladimdim sorry, forgot to ping you. I hope this works for your project.