From 3dbaa864d41f0db971b40e9b65d08bb24346aabb Mon Sep 17 00:00:00 2001 From: Madeleine Sydney Date: Sun, 2 Mar 2025 20:50:53 -0700 Subject: [PATCH] feat(emacs): Haskell --- .../emacs/modules/lang/syd-lang-clojure.el | 2 + .../emacs/modules/lang/syd-lang-haskell.el | 69 +++++++++++++++++++ .../crumb/programs/emacs/modules/syd-lang.el | 1 + .../programs/emacs/modules/syd-projects.el | 5 +- .../programs/emacs/modules/syd-tooling.el | 20 +++++- .../project-skeletons/haskell-flake/.envrc | 1 + .../haskell-flake/__PROJECT-NAME__.cabal | 63 +++++++++++++++++ .../project-skeletons/haskell-flake/flake.nix | 36 ++++++++++ users/crumb/programs/git.nix | 1 + users/crumb/programs/haskell.nix | 46 +++++++++++++ 10 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 users/crumb/programs/emacs/modules/lang/syd-lang-haskell.el create mode 100644 users/crumb/programs/emacs/project-skeletons/haskell-flake/.envrc create mode 100644 users/crumb/programs/emacs/project-skeletons/haskell-flake/__PROJECT-NAME__.cabal create mode 100644 users/crumb/programs/emacs/project-skeletons/haskell-flake/flake.nix create mode 100644 users/crumb/programs/haskell.nix diff --git a/users/crumb/programs/emacs/modules/lang/syd-lang-clojure.el b/users/crumb/programs/emacs/modules/lang/syd-lang-clojure.el index 300d963..dc6c1cb 100644 --- a/users/crumb/programs/emacs/modules/lang/syd-lang-clojure.el +++ b/users/crumb/programs/emacs/modules/lang/syd-lang-clojure.el @@ -51,11 +51,13 @@ #'cider-eval-region)) :custom ((cider-show-error-buffer nil)) :general + ;; DEPRECATED: Remove once a `map!' equivalent is implemented. (:keymaps 'cider-repl-mode-map :states '(normal insert) "C-k" #'cider-repl-backward-input "C-j" #'cider-repl-forward-input "C-s" #'cider-repl-previous-matching-input) + ;; DEPRECATED: Remove once a `map!' equivalent is implemented. (:keymaps 'clojure-mode-map :states '(normal visual motion emacs insert) :major-modes t diff --git a/users/crumb/programs/emacs/modules/lang/syd-lang-haskell.el b/users/crumb/programs/emacs/modules/lang/syd-lang-haskell.el new file mode 100644 index 0000000..523a29d --- /dev/null +++ b/users/crumb/programs/emacs/modules/lang/syd-lang-haskell.el @@ -0,0 +1,69 @@ +;;; syd-lang-haskell.el -*- lexical-binding: t; -*- + +(require 'syd-handle-repl) +(require 'syd-handle-lookup) + +(defun syd-haskell-open-repl () + "Open a Haskell REPL." + (interactive) + (require 'inf-haskell) + (run-haskell)) + +(defun syd-haskell-evil-open-above () + "Opens a line above the current, following Haskell-mode's indentation" + (interactive) + (evil-beginning-of-line) + (haskell-indentation-newline-and-indent) + (evil-previous-line) + (haskell-indentation-indent-line) + (evil-append-line nil)) + +(defun syd-haskell-evil-open-below () + "Opens a line below the current, following Haskell-mode's indentation" + (interactive) + (evil-append-line nil) + (haskell-indentation-newline-and-indent)) + +(use-package haskell-mode + :mode (("\\.l?hs'" . haskell-literate-mode) + ("\\.hs'" . haskell-mode)) + :custom (; Show errors in REPL, not popup buffers. + (haskell-interactive-popup-errors nil) + (haskell-process-suggest-remove-import-line t) + (haskell-process-auto-import-loaded-modules t)) + :general + ;; DEPRECATED: Remove once a `map!' equivalent is implemented. + (:keymaps 'haskell-mode-map + :states '(normal visual motion emacs insert) + :major-modes t + :prefix syd-localleader-key + :non-normal-prefix syd-alt-localleader-key + "c" #'haskell-cabal-visit-file + "h s" #'haskell-hoogle-start-server + "h q" #'haskell-hoogle-kill-server) + (general-def :keymaps 'interactive-haskell-mode-map + :states '(normal insert) + "C-j" #'haskell-interactive-mode-history-next + "C-k" #'haskell-interactive-mode-history-previous) + :config + (set-repl-handler! '(haskell-mode haskell-cabal-mode literate-haskell-mode) + #'syd-haskell-open-repl + ;; Haskell-mode provides IDE features by communicating with a persistent + ;; REPL process à la Lisp. + :persist t) + (add-to-list 'completion-ignored-extensions ".hi") + + ;; Don't kill REPL popup on ESC/C-g + (set-popup-rule! "^\\*haskell\\*" :quit nil) + (syd-add-hook 'haskell-mode-local-vars-hook + ;; Folding of Haskell sections. + #'haskell-collapse-mode + #'interactive-haskell-mode)) + +(use-package lsp-haskell + :defer t + :init + (add-hook 'haskell-mode-local-vars-hook #'lsp 'append) + (add-hook 'haskell-literate-mode-local-vars-hook #'lsp 'append)) + +(provide 'syd-lang-haskell) diff --git a/users/crumb/programs/emacs/modules/syd-lang.el b/users/crumb/programs/emacs/modules/syd-lang.el index 8272299..c1d334d 100644 --- a/users/crumb/programs/emacs/modules/syd-lang.el +++ b/users/crumb/programs/emacs/modules/syd-lang.el @@ -4,5 +4,6 @@ (require 'syd-lang-emacs-lisp) (require 'syd-lang-clojure) (require 'syd-lang-nix) +(require 'syd-lang-haskell) (provide 'syd-lang) diff --git a/users/crumb/programs/emacs/modules/syd-projects.el b/users/crumb/programs/emacs/modules/syd-projects.el index 865fa95..91649b5 100755 --- a/users/crumb/programs/emacs/modules/syd-projects.el +++ b/users/crumb/programs/emacs/modules/syd-projects.el @@ -79,6 +79,9 @@ ;; REVIEW: Is it safe to make this be async? We require that the command ;; has finished before Git initialises. (skeletor-shell-command "nix run github:jlesquembre/clj-nix#deps-lock" - dir)))) + dir))) + (skeletor-define-template "haskell-flake" + :title "Haskell (Flake)" + :license-file-name "LICENSE")) (provide 'syd-projects) diff --git a/users/crumb/programs/emacs/modules/syd-tooling.el b/users/crumb/programs/emacs/modules/syd-tooling.el index 2312da9..2a3dbf9 100644 --- a/users/crumb/programs/emacs/modules/syd-tooling.el +++ b/users/crumb/programs/emacs/modules/syd-tooling.el @@ -1,5 +1,17 @@ ;;; syd-tooling.el -*- lexical-binding: t; -*- +(defun syd-lsp-lookup-documentation () + (interactive) + (when-let* ((buf (get-buffer "*lsp-help*"))) + (kill-buffer buf)) + (call-interactively #'lsp-describe-thing-at-point) + (let ((buf (get-buffer "*lsp-help*"))) + (when (get-buffer-window-list buf) + ;; Bury the buffer so the popup system has full control over how it's + ;; selected. + (bury-buffer buf) + buf))) + (use-package lsp-mode :init ;; We'll bind things ourselves. @@ -25,7 +37,13 @@ (user-error (concat "Ignoring a call to `lsp-install-server'" " — tell the caller to use Nix!"))) (set-popup-rule! (rx line-start "*lsp-" (or "help" "install")) - :size 0.35 :quit t :select nil)) + :size 13 :quit t :select nil) + + ;; DEPRECATED: Remove once syd-strategies is working. + (syd-add-hook 'lsp-mode + (defun syd-lsp-set-handlers-h () + (setq-local syd-lookup-documentation-handlers + (list #'syd-lsp-lookup-documentation))))) (use-package envrc ;; REVIEW: Can we load this any later/better? diff --git a/users/crumb/programs/emacs/project-skeletons/haskell-flake/.envrc b/users/crumb/programs/emacs/project-skeletons/haskell-flake/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/users/crumb/programs/emacs/project-skeletons/haskell-flake/.envrc @@ -0,0 +1 @@ +use flake diff --git a/users/crumb/programs/emacs/project-skeletons/haskell-flake/__PROJECT-NAME__.cabal b/users/crumb/programs/emacs/project-skeletons/haskell-flake/__PROJECT-NAME__.cabal new file mode 100644 index 0000000..3df7610 --- /dev/null +++ b/users/crumb/programs/emacs/project-skeletons/haskell-flake/__PROJECT-NAME__.cabal @@ -0,0 +1,63 @@ +cabal-version: 3.0 +name: __PROJECT-NAME__ +version: 0.1.0.0 +synopsis: __DESCRIPTION__ +description: __DESCRIPTION__ +license: GPL-3.0-only +license-file: LICENSE +author: __USER-NAME__ +maintainer: __USER-MAIL-ADDRESS__ + +-- copyright: +category: Language +build-type: Simple +extra-doc-files: + +common common + ghc-options: -Wno-typed-holes -fdefer-typed-holes + + default-extensions: + BlockArguments + DataKinds + DeriveDataTypeable + DeriveGeneric + DeriveTraversable + DerivingVia + FlexibleContexts + GADTs + GeneralisedNewtypeDeriving + LambdaCase + MultiWayIf + NoFieldSelectors + OverloadedLabels + OverloadedRecordDot + OverloadedStrings + PartialTypeSignatures + PatternSynonyms + StandaloneDeriving + TypeApplications + TypeFamilies + + default-language: GHC2021 + +library + import: common + + -- cabal-fmt: expand sydml/src/ -Main + exposed-modules: + + default-language: GHC2021 + + build-depends: + , base ^>=4.19.1.0 + , containers + , hashable + , mtl + , lens + , pretty-simple + , text >=2.0 && <2.2 + , transformers + , unordered-containers + + hs-source-dirs: src + diff --git a/users/crumb/programs/emacs/project-skeletons/haskell-flake/flake.nix b/users/crumb/programs/emacs/project-skeletons/haskell-flake/flake.nix new file mode 100644 index 0000000..4117f73 --- /dev/null +++ b/users/crumb/programs/emacs/project-skeletons/haskell-flake/flake.nix @@ -0,0 +1,36 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, ... }@inputs: + inputs.flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + hlib = pkgs.haskell.lib.compose; + hpkgs = pkgs.haskell.packages.ghc98.extend (final: prev: { + __PROJECT-NAME__ = + hlib.dontCheck (final.callCabal2nix "__PROJECT-NAME__" ./. {}); + }); + in { + packages = rec { + __PROJECT-NAME__ = hpkgs.__PROJECT-NAME__; + default = __PROJECT-NAME__; + }; + + devShells.default = hpkgs.shellFor { + packages = p: [ + p.__PROJECT-NAME__ + ]; + nativeBuildInputs = [ + hpkgs.cabal-fmt + hpkgs.fourmolu + hpkgs.haskell-language-server + hpkgs.cabal-install + hpkgs.hasktags + ]; + withHoogle = true; + }; + }); +} diff --git a/users/crumb/programs/git.nix b/users/crumb/programs/git.nix index 66b30a9..c24a1c7 100755 --- a/users/crumb/programs/git.nix +++ b/users/crumb/programs/git.nix @@ -1,6 +1,7 @@ { config, lib, pkgs, ... }: let + # TODO: Move somewhere else. my-email = "lomiskiam@gmail.com"; my-name = "Madeleine Sydney"; in lib.mkMerge [ diff --git a/users/crumb/programs/haskell.nix b/users/crumb/programs/haskell.nix new file mode 100644 index 0000000..198ebcd --- /dev/null +++ b/users/crumb/programs/haskell.nix @@ -0,0 +1,46 @@ +{ config, lib, pkgs, ... }: + +{ + # Convenient shorthand for quickly opening Haskell REPLs. + programs.bash.profileExtra = '' + # Start a GHCi REPL with the given packages made available. + ghci-with-packages () { + nix-shell -p "haskellPackages.ghcWithPackages (p: with p; [ $@ ])" \ + --run ghci + } + + # Run GHC with the given packages made available. + ghc-with-packages () { + getopt -o "p" -- "$@" + while true; do + case "$1" in + -p) + packages="$1" + shift 2 + ;; + --) + shift + break + ;; + esac + done + + if [ $? -ne 0 ]; then + echo "Invalid options provided" + exit 1 + fi + + eval set -- "$options" + + nix-shell -p "haskellPackages.ghcWithPackages (p: with p; [ $packages ])" \ + --run "ghc $@" + } + ''; + + # Some global Cabal configuration. + xdg.configFile.".cabal/config".text = '' + -- Globally-enable Nix integration. See + -- https://cabal.readthedocs.io/en/3.4/nix-integration.html + nix: True + ''; +}