Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

module: add :lang flix #7914

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/modules.org
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ Modules that bring support for a language or group of languages to Emacs.
+ [[file:../modules/lang/ess/README.org][ess]] =+lsp= - TODO
+ [[file:../modules/lang/factor/README.org][factor]] - TODO
+ [[file:../modules/lang/faust/README.org][faust]] - TODO
+ [[file:../modules/lang/flix/README.org][flix]] - TODO
+ [[file:../modules/lang/fsharp/README.org][fsharp]] =+lsp= - TODO
+ [[file:../modules/lang/fstar/README.org][fstar]] - F* support
+ [[file:../modules/lang/gdscript/README.org][gdscript]] =+lsp= - TODO
Expand Down
43 changes: 43 additions & 0 deletions modules/lang/flix/README.org
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#+title: :lang flix
#+subtitle: scala + ocaml + haskell
#+created: Jul 1, 2024
#+since: 29.03.0

* Description :unfold:
This module adds [[https://flix.dev/][flix programming language]] support to Doom Emacs. It uses a slighly customized version of [[https://github.com/jhckragh/flix-mode][flix-mode.el]] for syntax highlighting. The module allows for flix commands to be run via the *flix.jar* that flix projects use to run code. It also adds REPL support for flix, allowing the user to create and interact with the REPL.

** Maintainers
*This module needs a maintainer.* [[doom-contrib-maintainer:][Become a maintainer?]]

** Module flags
/This module has no flags./

** Packages
/This module has no packages./

** Hacks
/No hacks documented for this module./

* TODO Installation
[[id:01cffea4-3329-45e2-a892-95a384ab2338][Enable this module in your ~doom!~ block.]]

* TODO Usage
#+begin_quote
󱌣 This module has no usage documentation yet. [[doom-contrib-module:][Write some?]]
#+end_quote

* TODO Configuration
#+begin_quote
󱌣 This module has no configuration documentation yet. [[doom-contrib-module:][Write some?]]
#+end_quote

* Troubleshooting
/There are no known problems with this module./ [[doom-report:][Report one?]]

* Frequently asked questions
/This module has no FAQs yet./ [[doom-suggest-faq:][Ask one?]]

* TODO Appendix
#+begin_quote
󱌣 This module has no appendix yet. [[doom-contrib-module:][Write one?]]
#+end_quote
119 changes: 119 additions & 0 deletions modules/lang/flix/autoload.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
;;; lang/flix/autoload.el -*- lexical-binding: t; -*-

;;;###autoload
(defun +flix/install-jar ()
"Install flix.jar in a selected directory if it is not already present."
(interactive)
(let* ((default-directory (or (projectile-project-root) default-directory))
(selected-dir (read-directory-name "Install into directory: " default-directory))
(jar-path (concat (file-name-as-directory selected-dir) "flix.jar"))
(download-url "https://github.com/flix/flix/releases/latest/download/flix.jar"))
(if (file-exists-p jar-path)
(message "flix.jar is already installed.")
(url-copy-file download-url jar-path t)
(message "flix.jar has been downloaded and installed.")
(setq flix-jar-directory selected-dir))))

;;;###autoload
(defun +flix/flix-command ()
"Run a command with the flix.jar in the specified directory."
(interactive)
(let* ((default-directory (or (projectile-project-root) flix-jar-directory default-directory))
(selected-dir (read-directory-name "init project in: " default-directory))
(command (read-string "Command: "))
(jar-path (concat (file-name-as-directory selected-dir) "flix.jar")))
(if (file-exists-p jar-path)
(let ((default-directory selected-dir))
(message (concat "Running flix " command "..."))
(setq flix-jar-directory selected-dir)
(async-shell-command (concat "java -jar flix.jar " command)))
(message "flix.jar not found in the selected directory."))))

;;;###autoload
(defun +flix/init-project ()
"Initialize a flix project in the selected directory if flix.jar is present."
(interactive)
(let* ((default-directory (or (projectile-project-root) flix-jar-directory default-directory))
(selected-dir (read-directory-name "init project in: " default-directory))
(jar-path (concat (file-name-as-directory selected-dir) "flix.jar")))
(if (file-exists-p jar-path)
(let ((default-directory selected-dir))
(message "initializing flix project...")
(setq flix-jar-directory selected-dir)
(shell-command "java -jar flix.jar init"))
(message "flix.jar not found in the selected directory."))))

;;;###autoload
(defun +flix/run-project ()
"Initialize a Flix project in the selected directory if flix.jar is present."
(interactive)
(let* ((default-directory (or (projectile-project-root) flix-jar-directory default-directory))
(selected-dir (read-directory-name "Flix exe: " default-directory))
(jar-path (concat (file-name-as-directory selected-dir) "flix.jar")))
(if (file-exists-p jar-path)
(let ((default-directory selected-dir))
(message "Running Flix project...")
(setq flix-jar-directory selected-dir)
(async-shell-command "java -jar flix.jar run"))
(message "flix.jar not found in the selected directory."))))

;;;###autoload
(defun +flix/build-project ()
"Initialize a Flix project in the selected directory if flix.jar is present."
(interactive)
(let* ((default-directory (or (projectile-project-root) flix-jar-directory default-directory))
(selected-dir (read-directory-name "Flix exe: " default-directory))
(jar-path (concat (file-name-as-directory selected-dir) "flix.jar")))
(if (file-exists-p jar-path)
(let ((default-directory selected-dir))
(message "Building Flix project...")
(setq flix-jar-directory selected-dir)
(async-shell-command "java -jar flix.jar build"))
(message "flix.jar not found in the selected directory."))))

;;;###autoload
(defun +flix/repl ()
"Run an inferior instance of `flix` inside Emacs."
(interactive)
(setq flix-last-buffer (current-buffer))
(+flix--repl-start-repl))

;;;###autoload
(defun +flix/goto-flix-buffer ()
"Switch to active Flix REPL"
(interactive)
(if (buffer-live-p (get-buffer flix-last-buffer))
(switch-to-buffer-other-window flix-last-buffer)
(message "Flix buffer deleted")))

;;;###autoload
(defun +flix/goto-repl ()
"Switch to active Flix REPL"
(interactive)
(setq flix-last-buffer (current-buffer))
(if (get-buffer "*Flix REPL*")
(switch-to-buffer-other-window "*Flix REPL*")
(+flix/repl)))

;;;###autoload
(defun +flix/repl-restart ()
"Restart the Flix REPL process."
(interactive)
(+flix--repl-stop-repl)
(+flix--repl-start-repl))

;;;###autoload
(defun +flix/set-jar-directory ()
"Set the directory where flix.jar is located."
(interactive)
(let* ((default-directory (or (projectile-project-root) flix-jar-directory default-directory))
(selected-dir (read-directory-name "Flix exe: " default-directory)))
(setq flix-jar-directory selected-dir)))

;;;###autoload
(defun +flix/send-line-to-repl ()
"Send the current line to the Flix REPL."
(interactive)
(let ((line (thing-at-point 'line t)))
(comint-send-string (get-buffer-process "*Flix REPL*") line)
(comint-send-string (get-buffer-process "*Flix REPL*") "\n")))
173 changes: 173 additions & 0 deletions modules/lang/flix/config.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
;;; lang/flix/config.el -*- lexical-binding: t; -*-

(defconst flix-mode-keywords
'("alias" "and" "as" "case" "catch" "chan" "choose" "class" "def"
"default" "deref" "else" "enum" "exists" "false" "forall" "force"
"from" "get" "if" "import" "inline" "instance" "into" "lat" "law"
"lawless" "lazy" "let" "match" "matchEff" "mut" "namespace" "new"
"not" "opaque" "or" "override" "project" "pub" "query" "ref"
"reifyBool" "reifyEff" "reifyType" "rel" "scoped" "sealed" "select"
"set" "solve" "spawn" "true" "try" "type" "unlawful" "use" "where"
"with")
"Keywords recognized by `flix-mode'.")

(defvar flix-mode-font-lock-keywords
`(("\\_<Impure\\|null\\_>\\|\\?\\?\\?\\|\\?[_[:lower:]][_[:alnum:]]*" (0 font-lock-warning-face))
("\\_<Pure\\_>" (0 font-lock-function-name-face))
("\\_<\\(true\\|false\\)\\_>" (0 font-lock-builtin-face))
("\\<\\(\\sw+\\)(" (1 font-lock-function-name-face))
("let\\*?[ \t]+\\([_[:lower:]][_[:alnum:]]*\\)" (1 font-lock-variable-name-face))
("\\_<\\([_[:lower:]][_[:alnum:]]*\\)[ \t]*:[ \t_[:upper:]]" (1 font-lock-variable-name-face))
("\\_<\\([_[:upper:]][_[:alnum:]]*\\)\\_>" (0 font-lock-type-face))
(,(concat "\\_<" (regexp-opt flix-mode-keywords) "\\_>") (0 font-lock-keyword-face)))
"Keyword highlighting for `flix-mode'.")

(defvar flix-mode-syntax-table
(let ((st (make-syntax-table)))
(modify-syntax-entry ?\; "." st)
(modify-syntax-entry ?: "." st)
(modify-syntax-entry ?& "." st)
(modify-syntax-entry ?# "." st)
(modify-syntax-entry ?= "." st)
(modify-syntax-entry ?\" "\"" st)
(modify-syntax-entry ?\' "\"" st)
(modify-syntax-entry ?_ "w" st)
(modify-syntax-entry ?/ ". 124b" st)
(modify-syntax-entry ?* ". 23" st)
(modify-syntax-entry ?\n "> b" st)
st)
"Syntax table for `flix-mode'.")

(defcustom flix-mode-tab-width 4
"The tab width to use for indentation.")

(defun flix-mode--indent-further ()
(interactive)
(let ((new-indent (+ (current-indentation) flix-mode-tab-width)))
(if (> (current-column) (current-indentation))
(save-excursion (indent-line-to new-indent))
(indent-line-to new-indent))))

(defun flix-mode--indent-less ()
(interactive)
(save-excursion
(indent-line-to (max 0 (- (current-indentation) flix-mode-tab-width)))))

(defun flix-mode--newline-and-maybe-indent ()
(interactive)
(let ((indent-further (and (eolp) (looking-back "[{(=]")))
(prev-indent (current-indentation)))
(newline)
(if indent-further
(indent-line-to (+ prev-indent flix-mode-tab-width))
(indent-line-to prev-indent))))

(defvar flix-mode-map
(let ((map (make-sparse-keymap)))
(define-key map "\C-m" 'flix-mode--newline-and-maybe-indent)
(define-key map [return] 'flix-mode--newline-and-maybe-indent)
(define-key map "\C-j" 'flix-mode--newline-and-maybe-indent)
(define-key map [tab] 'flix-mode--indent-further)
(define-key map [backtab] 'flix-mode--indent-less)
map)
"Keymap for `flix-mode'.")

;;;###autoload
(define-derived-mode flix-mode prog-mode "Flix"
"A major mode for editing Flix files."
:syntax-table flix-mode-syntax-table
(setq-local indent-tabs-mode nil)
(setq-local comment-start "//")
(setq-local comment-start-skip "\\(//+\\|/\\*+\\) *")
(setq-local comment-end "")
(setq-local font-lock-defaults '(flix-mode-font-lock-keywords))
(add-to-list 'electric-indent-chars ?\}))

;;;###autoload
(add-to-list 'auto-mode-alist '("\\.flix\\'" . flix-mode))

(provide 'flix-mode)

;;; flix-mode.el ends here


;;; Flix Repl
(require 'comint)
(require 'ansi-color)

(defvar flix-jar-directory nil
"The directory from which to start the Flix REPL.")

(defvar flix-last-buffer nil
"The last flix buffer to call the repl")

(defun +flix--repl-apply-ansi-color (string)
"Apply ANSI color codes to STRING."
(ansi-color-apply string))

(defun +flix--repl-stop-repl ()
"Stop the current Flix REPL process, if any."
(let ((buffer (get-buffer "*Flix REPL*")))
(when (and buffer (get-buffer-process buffer))
(kill-process (get-buffer-process buffer))
(kill-buffer buffer))))

(defun +flix--repl-start-repl ()
"Start a new Flix REPL process."
(let* ((default-directory (or flix-jar-directory (read-directory-name "Select directory with flix.jar: ")))
(buffer (get-buffer-create "*Flix REPL*")))
(unless (comint-check-proc buffer)
(with-current-buffer buffer
(setq flix-jar-directory default-directory)
(apply 'make-comint-in-buffer "Flix REPL" buffer "java" nil '("-jar" "flix.jar" "repl"))
(flix-repl-mode)))
(pop-to-buffer buffer)))

(defvar flix-repl-mode-map
(let ((map (nconc (make-sparse-keymap) comint-mode-map)))
(define-key map "\t" 'completion-at-point)
map)
"Basic mode map for `flix-repl-mode`.")

(defun +flix--repl-initialize ()
"Helper function to initialize `flix-repl`."
(setq comint-process-echoes t)
(setq comint-use-prompt-regexp t)
(add-hook 'comint-preoutput-filter-functions '+flix--repl-apply-ansi-color nil t))

(define-derived-mode flix-repl-mode comint-mode "Flix REPL"
"Major mode for `flix-repl`."
nil "Flix REPL"
(setq comint-prompt-regexp "flix> ")
(setq mode-line-process '(":%s"))
(setq comint-get-old-input (lambda () ""))
(setq comint-process-echoes t)
(setq comint-use-prompt-regexp t)
(set (make-local-variable 'paragraph-separate) "\\'")
(set (make-local-variable 'paragraph-start) comint-prompt-regexp)
(set (make-local-variable 'font-lock-defaults) '(nil t))
(set (make-local-variable 'comint-input-filter)
(lambda (str) (not (string-match "\\`\\s-*\\'" str))))
(set (make-local-variable 'comint-output-filter-functions)
(list 'comint-postoutput-scroll-to-bottom)))

(add-hook 'flix-repl-mode-hook '+flix--repl-initialize)

(provide 'flix-repl)


;; Keymaps for flix-mode.
(map! :map 'flix-mode-map
:desc "restart flix repl" "C-c C-c C-r" #'+flix/repl-restart
:desc "set jar directory" "C-c C-d" #'+flix/set-jar-directory
:desc "run flix project" "C-c C-r" #'+flix/run-project
:desc "build flix project" "C-c C-b" #'+flix/build-project
:desc "flix install jar" "C-c C-j" #'+flix/install-jar
:desc "flix init project" "C-c C-i" #'+flix/init-project
:desc "flix command" "C-c C-SPC" #'+flix/flix-command
:desc "goto flix repl" "C-c C-z" #'+flix/goto-repl)

;; Keymaps for flix-repl-mode.
(map! :map 'flix-repl-mode-map
:desc "restart flix repl" "C-c C-c C-r" #'+flix/repl-restart
:desc "goto flix buffer" "C-c C-z" #'+flix/goto-flix-buffer)
1 change: 1 addition & 0 deletions templates/init.example.el
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
;;ess ; emacs speaks statistics
;;factor
;;faust ; dsp, but you get to keep your soul
;;flix ; scala + ocaml + haskell
;;fortran ; in FORTRAN, GOD is REAL (unless declared INTEGER)
;;fsharp ; ML stands for Microsoft's Language
;;fstar ; (dependent) types and (monadic) effects and Z3
Expand Down