guix-devel/emacs/guix-pcomplete.el

394 lines
14 KiB
EmacsLisp
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; guix-pcomplete.el --- Functions for completing guix commands -*- lexical-binding: t -*-
;; Copyright © 2015 Alex Kost <alezost@gmail.com>
;; This file is part of GNU Guix.
;; GNU Guix is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Guix is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; This file provides completions for "guix" command that may be used in
;; `shell', `eshell' and wherever `pcomplete' works.
;;; Code:
(require 'pcomplete)
(require 'pcmpl-unix)
(require 'cl-lib)
(require 'guix-utils)
;;; Regexps for parsing various "guix ..." outputs
(defvar guix-pcomplete-parse-package-regexp
(rx bol (group (one-or-more (not blank))))
"Regexp used to find names of the packages.")
(defvar guix-pcomplete-parse-command-regexp
(rx bol " "
(group wordchar (one-or-more (or wordchar "-"))))
"Regexp used to find guix commands.
'Command' means any option not prefixed with '-'. For example,
guix subcommand, system action, importer, etc.")
(defvar guix-pcomplete-parse-long-option-regexp
(rx (or " " ", ")
(group "--" (one-or-more (or wordchar "-"))
(zero-or-one "=")))
"Regexp used to find long options.")
(defvar guix-pcomplete-parse-short-option-regexp
(rx bol (one-or-more blank)
"-" (group (not (any "- "))))
"Regexp used to find short options.")
(defvar guix-pcomplete-parse-linter-regexp
(rx bol "- " (group (one-or-more (or wordchar "-"))))
"Regexp used to find 'lint' checkers.")
(defvar guix-pcomplete-parse-regexp-group 1
"Parenthesized expression of regexps used to find commands and
options.")
;;; Non-receivable completions
(defvar guix-pcomplete-systems
'("x86_64-linux" "i686-linux" "armhf-linux" "mips64el-linux")
"List of supported systems.")
(defvar guix-pcomplete-hash-formats
'("nix-base32" "base32" "base16" "hex" "hexadecimal")
"List of supported hash formats.")
(defvar guix-pcomplete-refresh-subsets
'("core" "non-core")
"List of supported 'refresh' subsets.")
(defvar guix-pcomplete-key-policies
'("interactive" "always" "never")
"List of supported key download policies.")
;;; Interacting with guix
(defcustom guix-pcomplete-guix-program (executable-find "guix")
"Name of the 'guix' program.
It is used to find guix commands, options, packages, etc."
:type 'file
:group 'pcomplete
:group 'guix)
(defun guix-pcomplete-run-guix (&rest args)
"Run `guix-pcomplete-guix-program' with ARGS.
Insert the output to the current buffer."
(apply #'call-process
guix-pcomplete-guix-program nil t nil args))
(defun guix-pcomplete-run-guix-and-search (regexp &optional group
&rest args)
"Run `guix-pcomplete-guix-program' with ARGS and search for matches.
Return a list of strings matching REGEXP.
GROUP specifies a parenthesized expression used in REGEXP."
(with-temp-buffer
(apply #'guix-pcomplete-run-guix args)
(goto-char (point-min))
(let (result)
(while (re-search-forward regexp nil t)
(push (match-string-no-properties group) result))
(nreverse result))))
(defmacro guix-pcomplete-define-options-finder (name docstring regexp
&optional filter)
"Define function NAME to receive guix options and commands.
The defined function takes an optional COMMAND argument. This
function will run 'guix COMMAND --help' (or 'guix --help' if
COMMAND is nil) using `guix-pcomplete-run-guix-and-search' and
return its result.
If FILTER is specified, it should be a function. The result is
passed to this FILTER as argument and the result value of this
function call is returned."
(declare (doc-string 2) (indent 1))
`(guix-memoized-defun ,name (&optional command)
,docstring
(let* ((args '("--help"))
(args (if command (cons command args) args))
(res (apply #'guix-pcomplete-run-guix-and-search
,regexp guix-pcomplete-parse-regexp-group args)))
,(if filter
`(funcall ,filter res)
'res))))
(guix-pcomplete-define-options-finder guix-pcomplete-commands
"If COMMAND is nil, return a list of available guix commands.
If COMMAND is non-nil (it should be a string), return available
subcommands, actions, etc. for this guix COMMAND."
guix-pcomplete-parse-command-regexp)
(guix-pcomplete-define-options-finder guix-pcomplete-long-options
"Return a list of available long options for guix COMMAND."
guix-pcomplete-parse-long-option-regexp)
(guix-pcomplete-define-options-finder guix-pcomplete-short-options
"Return a string with available short options for guix COMMAND."
guix-pcomplete-parse-short-option-regexp
(lambda (list)
(mapconcat #'identity list "")))
(guix-memoized-defun guix-pcomplete-all-packages ()
"Return a list of all available Guix packages."
(guix-pcomplete-run-guix-and-search
guix-pcomplete-parse-package-regexp
guix-pcomplete-parse-regexp-group
"package" "--list-available"))
(guix-memoized-defun guix-pcomplete-installed-packages (&optional profile)
"Return a list of Guix packages installed in PROFILE."
(let* ((args (and profile
(list (concat "--profile=" profile))))
(args (append '("package" "--list-installed") args)))
(apply #'guix-pcomplete-run-guix-and-search
guix-pcomplete-parse-package-regexp
guix-pcomplete-parse-regexp-group
args)))
(guix-memoized-defun guix-pcomplete-lint-checkers ()
"Return a list of all available lint checkers."
(guix-pcomplete-run-guix-and-search
guix-pcomplete-parse-linter-regexp
guix-pcomplete-parse-regexp-group
"lint" "--list-checkers"))
;;; Completing
(defvar guix-pcomplete-option-regexp (rx string-start "-")
"Regexp to match an option.")
(defvar guix-pcomplete-long-option-regexp (rx string-start "--")
"Regexp to match a long option.")
(defvar guix-pcomplete-long-option-with-arg-regexp
(rx string-start
(group "--" (one-or-more any)) "="
(group (zero-or-more any)))
"Regexp to match a long option with its argument.
The first parenthesized group defines the option and the second
group - the argument.")
(defvar guix-pcomplete-short-option-with-arg-regexp
(rx string-start
(group "-" (not (any "-")))
(group (zero-or-more any)))
"Regexp to match a short option with its argument.
The first parenthesized group defines the option and the second
group - the argument.")
(defun guix-pcomplete-match-option ()
"Return non-nil, if the current argument is an option."
(pcomplete-match guix-pcomplete-option-regexp 0))
(defun guix-pcomplete-match-long-option ()
"Return non-nil, if the current argument is a long option."
(pcomplete-match guix-pcomplete-long-option-regexp 0))
(defun guix-pcomplete-match-long-option-with-arg ()
"Return non-nil, if the current argument is a long option with value."
(pcomplete-match guix-pcomplete-long-option-with-arg-regexp 0))
(defun guix-pcomplete-match-short-option-with-arg ()
"Return non-nil, if the current argument is a short option with value."
(pcomplete-match guix-pcomplete-short-option-with-arg-regexp 0))
(defun guix-pcomplete-long-option-arg (option args)
"Return a long OPTION's argument from a list of arguments ARGS."
(let* ((re (concat "\\`" option "=\\(.*\\)"))
(args (cl-member-if (lambda (arg)
(string-match re arg))
args))
(cur (car args)))
(when cur
(match-string-no-properties 1 cur))))
(defun guix-pcomplete-short-option-arg (option args)
"Return a short OPTION's argument from a list of arguments ARGS."
(let* ((re (concat "\\`" option "\\(.*\\)"))
(args (cl-member-if (lambda (arg)
(string-match re arg))
args))
(cur (car args)))
(when cur
(let ((arg (match-string-no-properties 1 cur)))
(if (string= "" arg)
(cadr args) ; take the next arg
arg)))))
(defun guix-pcomplete-complete-comma-args (entries)
"Complete comma separated arguments using ENTRIES."
(let ((index pcomplete-index))
(while (= index pcomplete-index)
(let* ((args (if (or (guix-pcomplete-match-long-option-with-arg)
(guix-pcomplete-match-short-option-with-arg))
(pcomplete-match-string 2 0)
(pcomplete-arg 0)))
(input (if (string-match ".*,\\(.*\\)" args)
(match-string-no-properties 1 args)
args)))
(pcomplete-here* entries input)))))
(defun guix-pcomplete-complete-command-arg (command)
"Complete argument for guix COMMAND."
(cond
((member command
'("archive" "build" "edit" "environment" "lint" "refresh"
"size"))
(while t
(pcomplete-here (guix-pcomplete-all-packages))))
(t (pcomplete-here* (pcomplete-entries)))))
(defun guix-pcomplete-complete-option-arg (command option &optional input)
"Complete argument for COMMAND's OPTION.
INPUT is the current partially completed string."
(cl-flet ((option? (short long)
(or (string= option short)
(string= option long)))
(command? (&rest commands)
(member command commands))
(complete (entries)
(pcomplete-here entries input nil t))
(complete* (entries)
(pcomplete-here* entries input t)))
(cond
((option? "-L" "--load-path")
(complete* (pcomplete-dirs)))
((string= "--key-download" option)
(complete* guix-pcomplete-key-policies))
((command? "package")
(cond
;; For '--install[=]' and '--remove[=]', try to complete a package
;; name (INPUT) after the "=" sign, and then the rest packages
;; separated with spaces.
((option? "-i" "--install")
(complete (guix-pcomplete-all-packages))
(while (not (guix-pcomplete-match-option))
(pcomplete-here (guix-pcomplete-all-packages))))
((option? "-r" "--remove")
(let* ((profile (or (guix-pcomplete-short-option-arg
"-p" pcomplete-args)
(guix-pcomplete-long-option-arg
"--profile" pcomplete-args)))
(profile (and profile (expand-file-name profile))))
(complete (guix-pcomplete-installed-packages profile))
(while (not (guix-pcomplete-match-option))
(pcomplete-here (guix-pcomplete-installed-packages profile)))))
((string= "--show" option)
(complete (guix-pcomplete-all-packages)))
((option? "-p" "--profile")
(complete* (pcomplete-dirs)))
((option? "-m" "--manifest")
(complete* (pcomplete-entries)))))
((and (command? "archive" "build" "size")
(option? "-s" "--system"))
(complete* guix-pcomplete-systems))
((and (command? "build")
(option? "-r" "--root"))
(complete* (pcomplete-entries)))
((and (command? "environment")
(option? "-l" "--load"))
(complete* (pcomplete-entries)))
((and (command? "hash" "download")
(option? "-f" "--format"))
(complete* guix-pcomplete-hash-formats))
((and (command? "lint")
(option? "-c" "--checkers"))
(guix-pcomplete-complete-comma-args
(guix-pcomplete-lint-checkers)))
((and (command? "publish")
(option? "-u" "--user"))
(complete* (pcmpl-unix-user-names)))
((and (command? "refresh")
(option? "-s" "--select"))
(complete* guix-pcomplete-refresh-subsets)))))
(defun guix-pcomplete-complete-options (command)
"Complete options (with their arguments) for guix COMMAND."
(while (guix-pcomplete-match-option)
(let ((index pcomplete-index))
(if (guix-pcomplete-match-long-option)
;; Long options.
(if (guix-pcomplete-match-long-option-with-arg)
(let ((option (pcomplete-match-string 1 0))
(arg (pcomplete-match-string 2 0)))
(guix-pcomplete-complete-option-arg
command option arg))
(pcomplete-here* (guix-pcomplete-long-options command))
;; We support '--opt arg' style (along with '--opt=arg'),
;; because 'guix package --install/--remove' may be used this
;; way. So try to complete an argument after the option has
;; been completed.
(unless (guix-pcomplete-match-option)
(guix-pcomplete-complete-option-arg
command (pcomplete-arg 0 -1))))
;; Short options.
(let ((arg (pcomplete-arg 0)))
(if (> (length arg) 2)
;; Support specifying an argument after a short option without
;; spaces (for example, '-L/tmp/foo').
(guix-pcomplete-complete-option-arg
command
(substring-no-properties arg 0 2)
(substring-no-properties arg 2))
(pcomplete-opt (guix-pcomplete-short-options command))
(guix-pcomplete-complete-option-arg
command (pcomplete-arg 0 -1)))))
;; If there were no completions, move to the next argument and get
;; out if the last argument is achieved.
(when (= index pcomplete-index)
(if (= pcomplete-index pcomplete-last)
(throw 'pcompleted nil)
(pcomplete-next-arg))))))
;;;###autoload
(defun pcomplete/guix ()
"Completion for `guix'."
(let ((commands (guix-pcomplete-commands)))
(pcomplete-here* (cons "--help" commands))
(let ((command (pcomplete-arg 'first 1)))
(when (member command commands)
(guix-pcomplete-complete-options command)
(let ((subcommands (guix-pcomplete-commands command)))
(when subcommands
(pcomplete-here* subcommands)))
(guix-pcomplete-complete-options command)
(guix-pcomplete-complete-command-arg command)))))
(provide 'guix-pcomplete)
;;; guix-pcomplete.el ends here