diff --git a/modules/lang/ocaml/README.org b/modules/lang/ocaml/README.org new file mode 100644 index 000000000..53fa6db12 --- /dev/null +++ b/modules/lang/ocaml/README.org @@ -0,0 +1,79 @@ +#+TITLE: :lang ocaml + +This module adds [[https://ocaml.org/][OCaml]] support, powered by [[https://github.com/ocaml/tuareg][tuareg-mode]]. + ++ Code completion, look up documentation, and code navigation ([[https://github.com/ocaml/merlin/wiki/emacs-from-scratch][merlin]]) ++ REPL ([[https://github.com/ocaml-community/utop][utop]]) ++ Syntax-checking (~merlin~ with [[https://github.com/flycheck/flycheck-ocaml][flycheck-ocaml]]) ++ Auto-indentation ([[https://github.com/OCamlPro/ocp-indent][ocp-indent]]) ++ Code formatting ([[https://github.com/ocaml-ppx/ocamlformat][ocamlformat]]) ++ Dune file format ([[http://dune.build/][dune]]) + +* Table of Contents :TOC: +- [[Module Flags][Module Flags]] +- [[Prerequisites][Prerequisites]] +- [[Features][Features]] +- [[Configuration][Configuration]] +- [[Appendix][Appendix]] + - [[Commands][Commands]] + - [[Hacks][Hacks]] + +* Module Flags +This module provides the ~+opam-site-lisp~ flag to compile editor support ~.el~ +files from opam's =site-lisp= directory. +By default all editor plugins are installed from MELPA/GitHub even if the +corresponding =opam= package isn't installed. +If this flag is enabled then you must have all of the packages listed in +=package.el= installed via =opam=. + +To enable this, use: +#+BEGIN_SRC emacs-lisp +(doom! :lang (ocaml +opam-site-lisp)) +#+END_SRC + +* Prerequisites +It is highly recommended to install [[http://opam.ocaml.org/][opam]]. +To get all the features you should also install these ~opam~ packages, however +they are not all required (features should be disabled gracefully when a tool is +missing): +#+BEGIN_SRC shell +opam install merlin utop ocp-indent dune ocamlformat +#+END_SRC + +* Features ++ the following files should have syntax highlighting support: +~.ml{i,p,y,}~, ~.eliom{i,}~, ~jbuild~, ~dune~, ~opam~ ++ =merlin-mode= is activated whenever a =.merlin= file is found (including in a + parent directory) and =ocamlmerlin= executable is present ++ line-based auto-indentation is provided by =ocp-indent= when installed + +* Configuration ++ if =:completion company= is enabled then autocomplete is provided by =merlin= ++ when =:feature syntax-checker= is enabled then =flycheck-ocaml= is activated + to do on-the-fly syntax/type checking via =merlin=, otherwise this is only + done when the file is saved. ++ spell checking is activated in comments if =:feature spellcheck= is actived ++ a REPL is provided if =utop= is installed and =:feature eval= is actived ++ if =:editor format= is enabled, the =ocamlformat= executable is available and + there is an =.ocamlformat= file present then =format-all-buffer= is bound to + =ocamlformat=, otherwise to =ocp-indent= + +Run =make install= to install all packages, and =make doctor= to diagnose missing tools. + +* Appendix +** Commands + | command | key / ex command | description | + |------------------------------+------------------+------------------------------------| + | =merlin-type-enclosing= | =SPC m t= | display type under point | + | =tuareg-find-alternate-file= | =SPC m a= | switch between =.ml= and =.mli= | + | =merlin-locate= | =gd= | lookup definition | + | =merlin-occurences= | =SPC c D= | lookup references | + | =merlin-document= | =K= | lookup documentation | + | =utop= | =SPC o r= | open =utop= as REPL | + | =utop-eval-region= | =SPC c e= | evaluate selected region in =utop= | + +** Hacks ++ =set-pretty-symbols!= is called with the full tuareg prettify symbol list, this + can cause columns to change as certain keywords are shortened (e.g. =fun= + becomes \lambda. ++ =tuareg-opam-update-env= is called the first time =tuareg= is loaded diff --git a/modules/lang/ocaml/config.el b/modules/lang/ocaml/config.el index 409e0dbef..5a7c5c66c 100644 --- a/modules/lang/ocaml/config.el +++ b/modules/lang/ocaml/config.el @@ -1,13 +1,83 @@ ;;; lang/ocaml/config.el -*- lexical-binding: t; -*- -(def-package! tuareg - :mode ("\\.ml[4ilpy]?\\'" . tuareg-mode)) +;; def-project-mode!/associate! doesn't work when a +;; package is lazy loaded, and everything is compiled +(def-package! tuareg + :defer t ;; modes set by autoload + :config + ;; tuareg-mode has the prettify symbols itself + (set-pretty-symbols! 'tuareg-mode :alist + (append tuareg-prettify-symbols-basic-alist + tuareg-prettify-symbols-extra-alist)) + (setq tuareg-prettify-symbols-full t) + ;; Use opam to set environment + (setq tuareg-opam-insinuate t) + (tuareg-opam-update-env (tuareg-opam-current-compiler)) + ;; Spell-check comments + (when (featurep! :feature spellcheck) + (add-hook 'tuareg-mode-hook #'flyspell-prog-mode))) (def-package! merlin :after tuareg - :hook (tuareg-mode . merlin-mode) + :init + (set-lookup-handlers! 'tuareg-mode + :definition #'merlin-locate + :references #'merlin-occurrences + :documentation #'merlin-document) + (defun +ocaml|init-merlin () + (when (and (projectile-locate-dominating-file default-directory ".merlin") + (executable-find "ocamlmerlin")) + (merlin-mode))) + (add-hook 'tuareg-mode-hook #'+ocaml|init-merlin) + :config + (map! :map tuareg-mode-map + :localleader + :n "t" #'merlin-type-enclosing + :n "a" #'tuareg-find-alternate-file) (set-company-backend! 'tuareg-mode 'merlin-company-backend) - (after! company - (remove-hook 'company-backends 'merlin-company-backend))) + (setq merlin-completion-with-doc t)) + +(def-package! flycheck-ocaml + :when (featurep! :feature syntax-checker) + :after merlin + :config + ;; Disable Merlin's own error checking + (setq merlin-error-after-save nil) + ;; Enable Flycheck checker + (flycheck-ocaml-setup)) + +(def-package! ocp-indent + ;; must be careful to always defer this, it has autoloads that adds hooks + ;; which we do not want if the executable can't be found + :defer t + :init + (defun +ocaml|init-ocp-indent () + (when (executable-find "ocp-indent") + (ocp-setup-indent))) + (add-hook 'tuareg-mode-hook #'+ocaml|init-ocp-indent)) + +(def-package! utop + :defer t ;; loaded by hook below + :when (featurep! :feature eval) + :init + (set-repl-handler! 'tuareg-mode #'utop) + (set-eval-handler! 'tuareg-mode #'utop-eval-region) + (defun +ocaml|init-utop () + (when (executable-find "utop") + (utop-minor-mode))) + (add-hook 'tuareg-mode-hook #'+ocaml|init-utop)) + +(def-package! ocamlformat + :after tuareg + :commands (ocamlformat) + :init + (set-formatter! 'ocamlformat #'ocamlformat + :modes '(caml-mode tuareg-mode)) + (defun +ocaml|init-ocamlformat () + (setq +format-with 'ocp-indent) + (when (and (executable-find "ocamlformat") + (locate-dominating-file default-directory ".ocamlformat")) + (setq +format-with 'ocamlformat))) + (add-hook 'tuareg-mode-hook #'+ocaml|init-ocamlformat)) diff --git a/modules/lang/ocaml/doctor.el b/modules/lang/ocaml/doctor.el new file mode 100644 index 000000000..0b3427f41 --- /dev/null +++ b/modules/lang/ocaml/doctor.el @@ -0,0 +1,18 @@ +;; -*- lexical-binding: t; no-byte-compile: t; -*- +;;; lang/ocaml/doctor.el + +(unless (executable-find "ocamlmerlin") + (warn! "Couldn't find ocamlmerlin. Lookup, completion and syntax checking won't work.")) + +;; Tuareg can still indent +(unless (executable-find "ocp-indent") + (warn! "Couldn't find ocp-indent. Auto-indentation will be less precise.")) + +(when (featurep! :feature eval) + (unless (executable-find "utop") + (warn! "Couldn't find utop. REPL won't be available"))) + +(unless (executable-find "dune") + (warn! "Couldn't find dune. Won't be able to highlight dune files")) + +;; ocamlformat is optional, don't warn about it diff --git a/modules/lang/ocaml/packages.el b/modules/lang/ocaml/packages.el index 3da213bfd..2d6931013 100644 --- a/modules/lang/ocaml/packages.el +++ b/modules/lang/ocaml/packages.el @@ -1,5 +1,30 @@ ;; -*- no-byte-compile: t; -*- ;;; lang/ocaml/packages.el +(when (featurep! +opam-site-lisp) + (defvar +ocaml-elisp-dir + (when (executable-find "opam") + (let ((opam-share (ignore-errors (car (process-lines "opam" "config" "var" "share" "--safe"))))) + (when (and opam-share (file-directory-p opam-share)) + (expand-file-name "emacs/site-lisp" opam-share))))) + + (defmacro localpackage! (name) + `(package! ,name :recipe (:fetcher file :path ,+ocaml-elisp-dir))) + + (localpackage! opam-site-lisp)) + (package! tuareg) -(package! merlin) + +(when (featurep! :feature syntax-checker) + (package! flycheck-ocaml)) + +(unless (featurep! +opam-site-lisp) + (package! merlin) + (package! ocp-indent) + (when (featurep! :feature eval) + (package! utop)) + (when (featurep! :editor format) + ;; by default quelpa generated a version 0pre0.20180929.192844, which got parsed into (0 -1 0 ...), which when compared with version nil (0) + ;; in package-installed-p always yielded false + (package! ocamlformat :recipe (:fetcher github :repo "ocaml-ppx/ocamlformat" :files ("emacs/*.el")))) + (package! dune :recipe (:fetcher github :repo "ocaml/dune" :files ("editor-integration/emacs/*.el"))))