(require 'patch-sly) (defun pure-env (&rest preserve-vars) "Return a pure `env' command as a list of string." (append '("env" "-i") (mapcar (lambda (var) (concat var "=" (getenv var))) (append '("DISPLAY" "HOME" "LOGNAME" "TERM" "USER") preserve-vars)))) (defun ambrevar/delete-lisp-image-dump () "Delete SLY current connection image dump. This dump is expected to be generated by `lisp-repl-core-dumper'." (interactive) (let* ((pid (process-id (sly-inferior-process (sly-current-connection)))) (args (alist-get 'args (process-attributes pid))) (command (car (split-string (alist-get 'args (process-attributes pid)))))) (if (and (string= "image" (file-name-extension command)) (string= (file-name-directory command) (expand-file-name "~/.cache/lisp-repl-core-directory/"))) (when (yes-or-no-p (format "Delete %s?" command)) (let ((delete-by-moving-to-trash nil)) (delete-file command) (message "%S deleted." command))) (message "%S is not a Lisp image dump." command)))) (defun ambrevar/sbcl-for-nyxt (&optional refresh) ;; TODO: Run it in a container? "Fast, pure Guix environment for Nyxt development. If REFRESH is non-nil, rebuild the environment." (cl-flet ((mtime (file) (when (file-exists-p file) (file-attribute-modification-time (file-attributes file))))) (let* ((root (expand-file-name "~/.guix-temp-profiles/nyxt/nyxt")) (guix-def (expand-file-name "~/common-lisp/nyxt/build-scripts/guix.scm")) (guix-sbcl-def (expand-file-name "~/common-lisp/nyxt/build-scripts/guix-sbcl-for-nyxt.scm")) (guix-checkout (format "/var/guix/profiles/per-user/%s/current-guix" (user-login-name)))) (when (or refresh (not (file-exists-p root)) (time-less-p (mtime root) (mtime guix-def)) (time-less-p (mtime root) (mtime guix-sbcl-def)) (time-less-p (mtime root) (mtime guix-checkout))) (make-directory (file-name-directory root) :parents) (call-process "guix" nil nil nil "package" ;; May be needed to avoid having to rebuild some packages ;; after the REPL has started. "--no-grafts" (concat "--profile=" root) (concat "--install-from-file=" guix-sbcl-def)) ;; Update root mtime in case it's not changed. ;; Can't use `set-file-times' on links. (call-process "touch" nil nil nil "-h" root)) `((,@(pure-env "PERSONAL") ,(executable-find "bash") "--norc" "--noprofile" "-c" ,(concat (format "source '%s/etc/profile'" root) " && sbcl")))))) ;; Warning: ,restart-lisp does not take changes into account, the buffer must be re-created. (setq sly-lisp-implementations (let ((maybe-core-dumper (when-let ((exec (executable-find "lisp-repl-core-dumper"))) (list exec)))) `((sbcl-ambrevar ("lisp-repl-core-dumper" "-p" "ambrevar" "sbcl" "--eval" "(in-package :ambrevar/all)" "--eval" "(named-readtables:in-readtable clesh:syntax)")) (sbcl (,@maybe-core-dumper "sbcl")) (sbcl-failsafe ("sbcl")) (sbcl-nyxt (lambda () (ambrevar/sbcl-for-nyxt))) (sbcl-nyxt-refresh (lambda () (ambrevar/sbcl-for-nyxt :refresh))) ;; Simple REPL environment for Nyxt, in case sbcl-nyxt does not work. (sbcl-nyxt-failsafe ("guix" "environment" "--pure" "--preserve=^PERSONAL$" ; To find personal config, like engines and bookmarks. "-l" ,(expand-file-name "~/common-lisp/nyxt/build-scripts/guix.scm") ;; glib is needed to export GIO_EXTRA_MODULES. "--ad-hoc" "glib" "glib-networking" "gsettings-desktop-schemas" "gnupg" "--" "sbcl")) (sbcl-nyxt-site ("guix" "environment" "--pure" "-m" ,(expand-file-name "~/common-lisp/nyxt-site/guix-manifest.scm") "--" "sbcl")) (ccl (,@maybe-core-dumper "ccl")) (clisp (,@maybe-core-dumper "clisp")) (ecl ("ecl"))))) (setq sly-connection-update-interval 0.1) (when (require 'helm nil :noerror) (with-eval-after-load 'sly-mrepl (define-key sly-mrepl-mode-map (kbd "M-p") 'helm-comint-input-ring) (define-key sly-mrepl-mode-map (kbd "M-s f") 'helm-comint-prompts-all) (define-key sly-autodoc-mode-map (kbd "C-c C-d C-a") 'helm-sly-apropos) (define-key sly-mrepl-mode-map (kbd "C-c C-x c") 'helm-sly-list-connections))) (when (require 'helm-sly nil :noerror) (when (require 'evil-collection nil :noerror) (evil-define-key '(normal insert) helm-sly-connections-map (kbd "S-") 'helm-sly-run-go-to-repl-other-window)) (defun ambrevar/helm-sly-format-connection (connection buffer) (let ((fstring "%s%2s %s")) (format fstring (if (eq sly-default-connection connection) "*" " ") (helm-sly-connection-number connection) (replace-regexp-in-string "*$" "" (replace-regexp-in-string "*sly-mrepl for " "" (buffer-name buffer)))))) (setq helm-sly-connection-formatter #'ambrevar/helm-sly-format-connection) (global-helm-sly-mode) (add-to-list 'helm-source-names-using-follow "Lisp xrefs")) (defun ambrevar/sly-insert-double-quotes () "Convenient to write list of string, e.g. when writing a shell command line." (interactive) (while (sly-inside-string-p) (forward-char)) (cycle-spacing) (insert "\"\"") (backward-char) (when (and (boundp 'evil-state) (not (eq evil-state 'insert))) (call-interactively #'evil-insert))) (defun ambrevar/sly-insert-pipe () "Convenient to write a `:<>' pipe." (interactive) (while (sly-inside-string-p) (forward-char)) (newline-and-indent) (insert ":<>") (ambrevar/sly-insert-double-quotes)) (with-eval-after-load 'sly-mrepl (set-face-attribute 'sly-mrepl-output-face nil :inherit 'default :foreground) (setq sly-mrepl-history-file-name (expand-file-name "sly-mrepl-history" user-emacs-directory)) ;; While `,i RET` is short enough, it's one more key away in Evil insert state: (define-key sly-mrepl-mode-map (kbd "C-c M-p") 'sly-mrepl-set-package) (define-key sly-mrepl-mode-map (kbd "C-c M-o") 'sly-mrepl-clear-repl) (define-key sly-mrepl-mode-map (kbd "S-SPC") 'ambrevar/sly-insert-double-quotes) (define-key sly-mrepl-mode-map (kbd "C-S-SPC") 'ambrevar/sly-insert-pipe)) (defun ambrevar/sly-load-reload-system () (interactive) (funcall (if (sly-eval `(slynk-asdf:asdf-system-loaded-p ,(intern (sly-current-package)))) #'sly-asdf-reload-system #'sly-asdf-load-system) (intern (sly-current-package)))) (define-key lisp-mode-map (kbd "") 'ambrevar/sly-load-reload-system) (defun ambrevar/sly-colorize-buffer (str) "Useful for colorized output like the tests of Prove." (ansi-color-apply str)) (add-hook 'sly-mrepl-output-filter-functions 'ambrevar/sly-colorize-buffer) (defun ambrevar/indent-and-helm-company (arg) "Indent then call `helm-company'. Good substitute for `sly-mrepl-indent-and-complete-symbol'." (interactive "P") (indent-for-tab-command arg) (helm-company)) (setq sly-command-switch-to-existing-lisp 'always) (add-hook 'sly-mrepl-hook #'ambrevar/init-lispy) (add-hook 'sly-mrepl-hook #'rainbow-delimiters-mode) ;; REVIEW: With Emacs 27 we can: ;; (customize-set-variable 'helm-completion-style 'emacs) ;; (add-to-list 'completion-styles 'backend) (when (require 'helm-sly nil 'noerror) ;; (add-hook 'sly-mrepl-hook #'helm-sly-disable-internal-completion) ;; REVIEW: Company completion has the benefit of having annotations. (when (require 'helm-company nil :noerror) (add-hook 'lisp-mode-hook #'company-mode) (define-key lisp-mode-map (kbd "") 'ambrevar/indent-and-helm-company) (define-key lisp-mode-map (kbd "M-") 'ambrevar/indent-and-helm-company) (defun ambrevar/sly-set-keys () (define-key sly-mrepl-mode-map (kbd "") 'ambrevar/indent-and-helm-company) (define-key sly-mrepl-mode-map (kbd "M-") 'ambrevar/indent-and-helm-company)) (add-hook 'sly-mrepl-hook #'ambrevar/sly-set-keys) (add-hook 'sly-mrepl-hook #'company-mode))) (when (require 'helm-selector nil :noerror) (require 'init-sly-selector)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Prompt customization ;; TODO: Upstream! (defvar ambrevar/sly-status--last-command-time nil) (make-variable-buffer-local 'ambrevar/sly-status--last-command-time) (defun ambrevar/sly-status--record () (setq ambrevar/sly-status--last-command-time (current-time))) (defun ambrevar/sly-status-formatter (timestamp duration) "Return the status display for `ambrevar/sly-status'. TIMESTAMP is the value returned by `current-time' and DURATION is the floating time the command took to complete in seconds." (format "#[STATUS] End time %s, duration %.3fs\n" (format-time-string "%F %T" timestamp) duration)) (defcustom ambrevar/sly-status-min-duration 1 "If a command takes more time than this, display its status with `ambrevar/sly-status'." :group 'sly :type 'number) (defun ambrevar/sly-status (&optional formatter min-duration) "Termination timestamp and duration of command. Status is only returned if command duration was longer than MIN-DURATION \(defaults to `ambrevar/sly-status-min-duration'). FORMATTER is a function of two arguments, TIMESTAMP and DURATION, that returns a string." (if ambrevar/sly-status--last-command-time (let ((duration (time-to-seconds (time-subtract (current-time) ambrevar/sly-status--last-command-time)))) (setq ambrevar/sly-status--last-command-time nil) (if (> duration (or min-duration ambrevar/sly-status-min-duration)) (funcall (or formatter #'ambrevar/sly-status-formatter) (current-time) duration) "")) (progn (advice-add 'sly-mrepl--send-input-sexp :after #'ambrevar/sly-status--record) ""))) (defun ambrevar/sly-prepare-prompt (old-func &rest args) ; TODO: Remove when upstream have merged `sly-mrepl-prompt-formatter'. (let ((package (nth 0 args)) (new-prompt (format "%s%s\n%s" (ambrevar/sly-status) (abbreviate-file-name default-directory) (nth 1 args))) (error-level (nth 2 args)) (condition (nth 3 args))) (funcall old-func package new-prompt error-level condition))) (cl-defun ambrevar/sly-new-prompt (_package package-nickname &key entry-idx error-level &allow-other-keys) (concat (propertize (ambrevar/sly-status) 'font-lock-face 'font-lock-comment-face) "(" (propertize (abbreviate-file-name default-directory) 'font-lock-face 'diff-added) ")\n" (propertize "<" 'font-lock-face 'sly-mrepl-prompt-face) (propertize (number-to-string entry-idx) 'font-lock-face 'sly-mode-line) (propertize ":" 'font-lock-face 'sly-mrepl-prompt-face) (propertize package-nickname 'font-lock-face 'sly-mode-line) (when (cl-plusp error-level) (concat (sly-make-action-button (format "[%d]" error-level) #'sly-db-pop-to-debugger-maybe) " ")) (propertize "> " 'font-lock-face 'sly-mrepl-prompt-face))) (with-eval-after-load 'sly-mrepl (if (boundp 'sly-mrepl-prompt-formatter) (setq sly-mrepl-prompt-formatter #'ambrevar/sly-new-prompt) (advice-add 'sly-mrepl--insert-prompt :around #'ambrevar/sly-prepare-prompt))) (defun ambrevar/sly-mrepl-previous-prompt () "Go to the beginning of the previous REPL prompt." (interactive) ;; This has two wrinkles around the first prompt: (1) when going to ;; the first prompt it leaves point at column 0 (1) when called from ;; frist prompt goes to beginning of buffer. The correct fix is to ;; patch comint.el's comint-next-prompt and comint-previous-prompt ;; anyway... (let* ((inhibit-field-text-motion t) (pos (previous-single-char-property-change (previous-single-char-property-change (point) 'sly-mrepl--prompt) 'sly-mrepl--prompt))) (goto-char pos) (goto-char (line-beginning-position))) (end-of-line)) (defun ambrevar/sly-mrepl-next-prompt () "Go to the beginning of the next REPL prompt." (interactive) (let ((pos (next-single-char-property-change (line-beginning-position 2) 'sly-mrepl--prompt))) (goto-char pos) (if (get-text-property (point) 'sly-mrepl--prompt) (goto-char (next-single-char-property-change (point) 'sly-mrepl--prompt)) (point)))) (advice-add 'sly-mrepl-next-prompt :override #'ambrevar/sly-mrepl-next-prompt) (advice-add 'sly-mrepl-previous-prompt :override #'ambrevar/sly-mrepl-previous-prompt) (provide 'init-sly)