;;; nix-repl.el --- Nix repl -*- lexical-binding: t -*- ;; This file is NOT part of GNU Emacs. ;; Homepage: https://github.com/NixOS/nix-mode ;; Package-Requires: ((emacs "24.4")) ;; This file is NOT part of GNU Emacs. ;;; Commentary: ;;; Code: (defvar nix-prompt-regexp "nix-repl> ") (require 'comint) (require 'nix) (defgroup nix-repl nil "Nix-repl customizations." :group 'nix) (defcustom nix-repl-executable-args '("repl") "Arguments to provide to nix-repl." :type '(repeat string)) (defvar nix-repl-completion-redirect-buffer " *nix-repl completions redirect*" "Buffer to be used to redirect output of readline commands.") (defcustom nix-repl-completion-output-timeout 1.0 "Time in seconds to wait for completion output before giving up." :type 'float) (defvar nix-repl-mode-map (let ((map (make-sparse-keymap))) (define-key map "\t" 'completion-at-point) map)) (defun nix-repl-save-all-histories () "Call `comint-write-input-ring' for all `nix-repl-mode' buffers." (dolist (buf (buffer-list)) (with-current-buffer buf (when (eq major-mode 'nix-repl-mode) (comint-write-input-ring))))) (define-derived-mode nix-repl-mode comint-mode "Nix-REPL" "Interactive prompt for Nix." :interactive nil (setq-local comint-prompt-regexp nix-prompt-regexp) (setq-local comint-prompt-read-only t) (let* ((is-remote (file-remote-p default-directory)) (maybe-xdg-data-home (if is-remote (shell-command-to-string "echo -n $XDG_DATA_HOME") (or (getenv "XDG_DATA_HOME") ""))) (path-prefix (if (string-empty-p maybe-xdg-data-home) "~/.local/share" maybe-xdg-data-home)) (history-path (concat is-remote path-prefix "/nix/repl-history"))) (setq-local comint-input-ring-file-name history-path)) (comint-read-input-ring t) (add-hook 'kill-buffer-hook #'comint-write-input-ring nil 'local) (add-hook 'kill-emacs-hook #'nix-repl-save-all-histories nil 'local) (add-hook 'completion-at-point-functions #'nix-repl-completion-at-point nil 'local)) (defmacro nix--with-temp-process-filter (proc &rest body) "Use temp process PROC filter on BODY." (declare (indent defun)) `(let* ((buf (generate-new-buffer " *temp-process-output*")) (proc-filter-saved (process-filter ,proc)) (proc-marker (with-current-buffer buf (point-marker)))) (set-process-filter ,proc (nix--process-filter buf proc-marker)) (unwind-protect (with-current-buffer buf ,@body) (set-process-filter ,proc proc-filter-saved) (kill-buffer buf)))) ;;;###autoload (defun nix-repl () "Load the Nix-REPL." (interactive) (pop-to-buffer-same-window (get-buffer-create "*Nix-REPL*")) (unless (comint-check-proc (current-buffer)) (nix--make-repl-in-buffer (current-buffer)) (nix-repl-mode))) (defalias 'nix-repl-show 'nix-repl) (defun nix--make-repl-in-buffer (buffer) "Make Nix Repl in BUFFER." (apply 'make-comint-in-buffer (append `("Nix-REPL" ,buffer ,nix-executable nil) nix-repl-executable-args))) (defun nix-get-completions (process input) "Get completions for INPUT using native readline for PROCESS." (with-current-buffer (process-buffer process) (let* ((original-filter-fn (process-filter process)) (redirect-buffer (get-buffer-create nix-repl-completion-redirect-buffer)) (trigger "\t") (new-input (concat input trigger)) (input-length (save-excursion (+ (- (point-max) (comint-bol)) (length new-input)))) (delete-line-command (make-string input-length ?\b)) (input-to-send (concat new-input delete-line-command))) ;; Ensure restoring the process filter, even if the user quits ;; or there's some other error. (unwind-protect (with-current-buffer redirect-buffer ;; Cleanup the redirect buffer (erase-buffer) ;; Mimic `comint-redirect-send-command', unfortunately it ;; can't be used here because it expects a newline in the ;; command and that's exactly what we are trying to avoid. (let ((comint-redirect-echo-input nil) (comint-redirect-completed nil) (comint-redirect-perform-sanity-check nil) (comint-redirect-insert-matching-regexp t) (comint-redirect-finished-regexp nix-prompt-regexp) (comint-redirect-output-buffer redirect-buffer)) (set-process-filter process (apply-partially #'comint-redirect-filter original-filter-fn)) (process-send-string process input-to-send) ;; Grab output until our dummy completion used as ;; output end marker is found. (when (nix--accept-process-output process nix-repl-completion-output-timeout comint-redirect-finished-regexp) (beginning-of-line) (if (eq (char-after) ?\r) (cdr (split-string (buffer-substring-no-properties (line-beginning-position) (point-min)) "[ \f\t\n\r\v]+" t)) (search-forward "" nil t) (backward-char) (if (eq (char-before) ?\a) nil (list (buffer-substring-no-properties (line-beginning-position) (point)))))))) (set-process-filter process original-filter-fn))))) (defun nix--accept-process-output (process &optional timeout regexp) "Accept PROCESS output with TIMEOUT until REGEXP is found. Optional argument TIMEOUT is the timeout argument to `accept-process-output' calls. Optional argument REGEXP overrides the regexp to match the end of output, defaults to `comint-prompt-regexp'. Returns non-nil when output was properly captured. This utility is useful in situations where the output may be received in chunks, since `accept-process-output' gives no guarantees they will be grabbed in a single call." (let ((regexp (or regexp comint-prompt-regexp))) (catch 'found (while t (when (not (accept-process-output process timeout)) (throw 'found nil)) (when (progn (re-search-backward regexp nil t)) (throw 'found t)))))) ;;;###autoload (defun nix-repl-completion-at-point () "Completion at point function for Nix using \"nix-repl\". See `completion-at-point-functions'." (save-excursion (let* ((proc (get-buffer-process (current-buffer))) (prefix (and (derived-mode-p 'nix-repl-mode) proc (executable-find nix-executable) (nix--prefix-bounds)))) (pcase prefix (`(,beg . ,end) (list beg end (nix-get-completions proc (buffer-substring beg end)) :exclusive 'no)))))) (defun nix--prefix-bounds () "Get bounds of Nix attribute path at point as a (BEG . END) pair, or nil." (save-excursion (when (< (skip-chars-backward "a-zA-Z0-9'\\-_\\.") 0) (cons (point) (+ (point) (skip-chars-forward "a-zA-Z0-9'\\-_\\.")))))) (defun nix--send-repl (input &optional process mute) "Send INPUT to PROCESS. MUTE if true then don’t alert user." (let ((proc (or process (get-buffer-process (current-buffer))))) (if mute (nix--with-temp-process-filter proc (process-send-string proc input)) (process-send-string proc input)))) (defun nix--char-with-ctrl (char) "Generate control character CHAR." (char-to-string (logand #b10011111 char))) (defun nix--process-filter (buf marker) "Process filter for Nix-rel buffer BUF at MARKER." (lambda (_proc string) (when (buffer-live-p buf) (with-current-buffer buf (save-excursion (goto-char marker) (insert string) (set-marker marker (point))))))) (provide 'nix-repl) ;;; nix-repl.el ends here