From: Eshel Yaron Date: Thu, 25 Jul 2024 06:32:52 +0000 (+0200) Subject: Move command line completion code from kubed.el to new file cobra.el X-Git-Url: http://git.eshelyaron.com/gitweb/?a=commitdiff_plain;h=6be21cd28d7aa95d84e7cf296e76014b7c22c28f;p=emacs.git Move command line completion code from kubed.el to new file cobra.el --- diff --git a/lisp/minibuffer.el b/lisp/minibuffer.el index ad8d0f12ea9..59d1c96015b 100644 --- a/lisp/minibuffer.el +++ b/lisp/minibuffer.el @@ -1171,7 +1171,7 @@ styles for specific categories, such as files, buffers, etc." '((buffer (styles basic substring) (export-function . minibuffer-export-list-buffers)) (file (export-function . minibuffer-export-dired)) - (kubed-command-line (styles basic)) + (cobra-command-line (styles basic)) (unicode-name (styles basic substring)) ;; A new style that combines substring and pcm might be better, ;; e.g. one that does not anchor to bos. diff --git a/lisp/net/cobra.el b/lisp/net/cobra.el new file mode 100644 index 00000000000..1c5d5ec94d9 --- /dev/null +++ b/lisp/net/cobra.el @@ -0,0 +1,149 @@ +;;; cobra.el --- Complete Cobra command lines -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 Eshel Yaron + +;; Author: Eshel Yaron +;; Keywords: tools + +;; This program 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. + +;; This program 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 . + +;;; Commentary: + +;; Cobra is a popular Golang framework for CLI programs. This library +;; defines function `cobra-read-command-line', which helps you read a +;; command line for a program that uses Cobra, with completion. +;; Prominent examples of Cobra programs are `kubectl' and `docker'. + +;;; Code: + +(defvar cobra--cache nil) + +(defun cobra-completion-table (executable s p a) + "Completion table for command lines that invoke EXECUTABLE. + +Perform completion action A on string S with predicate P." + (let ((start 0)) + (while (string-match "[[:space:]=]" s start) + (setq start (match-end 0))) + (if (eq a 'metadata) + `(metadata + (category . cobra-command-line) + (affixation-function + . ,(lambda (cands) + (let ((max (seq-max + (cons 0 (mapcar #'string-width cands))))) + (mapcar + (lambda (cand) + (list + cand "" + (if-let + ((desc (get-text-property + 0 'cobra-argument-description + cand))) + (concat + (make-string (1+ (- max (string-width cand))) ?\s) + (propertize desc 'face 'completions-annotations)) + ""))) + cands))))) + (let* ((lines + (cdr + (if (string= s (car cobra--cache)) + ;; Cache hit. + cobra--cache + (setq + cobra--cache + (cons s + (apply #'process-lines-ignore-status + executable "__complete" + (let ((args (cdr (split-string-and-unquote s)))) + (if (string-suffix-p " " s) + ;; Restore omitted empty argument. + (nconc args '("")) + args)))))))) + (code nil) + (comps (seq-take-while + (lambda (line) + (not (and (string-match "^:\\([0-9]+\\)$" line) + (setq code (string-to-number + (match-string 1 line)))))) + lines))) + ;; `code' encodes "completion directives", as follows: + ;; #b000001: An error occurred, ignore completions. + ;; #b000010: Don't add space after completion. + ;; #b000100: Don't fall back to file completion. + ;; #b001000: Completions are really file extension filters. + ;; #b010000: Complete directory names. + ;; #b100000: Preserve completions order. + (when (and code (zerop (logand 1 code))) + ;; Error bit in unset, proceed. + (if (= #b100 (logand #b100 code)) + ;; No file name completion. + (if (eq (car-safe a) 'boundaries) + `(boundaries + ,start . ,(and (string-match "[[:space:]=]" (cdr a)) + (match-beginning 0))) + (let ((table + (mapcar + ;; Annotate completion candidates. + (lambda (comp) + (pcase (split-string comp "\t" t) + (`(,c ,d . ,_) + (propertize + c 'cobra-argument-description + ;; Only keep first sentence. + (car (split-string d "\\." t)))) + (`(,c . ,_) c))) + comps))) + (if a (complete-with-action a table (substring s start) p) + ;; `try-completion'. + (let ((comp (complete-with-action a table (substring s start) p))) + (if (stringp comp) (concat (substring s 0 start) comp) comp))))) + ;; File name completion. + (setq p + (cond + ((= #b1000 (logand #b1000 code)) + ;; `comps' are valid extensions. + (lambda (f) + (or (file-directory-p f) + (when (string-match "\\.[^.]*\\'" f) + (member (substring f (1+ (match-beginning 0))) + comps))))) + ((= #b10000 (logand #b10000 code)) + ;; Directory name completion. + #'file-directory-p))) + (if (eq (car-safe a) 'boundaries) + ;; Find nested boundaries. + (let* ((suf (cdr a)) + (bounds (completion-boundaries + (substring s start) #'completion-file-name-table p + (substring suf 0 (string-match "[[:space:]=]" suf))))) + `(boundaries ,(+ (car bounds) start) . ,(cdr bounds))) + (if a (complete-with-action a #'completion-file-name-table + (substring s start) p) + (let ((comp (complete-with-action a #'completion-file-name-table + (substring s start) p))) + (if (stringp comp) (concat (substring s 0 start) comp) comp)))))))))) + +;;;###autoload +(defun cobra-read-command-line (prompt initial &optional hist) + "Prompt with PROMPT for a command line starting with INITIAL. + +Optional argument HIST is the name of the history list variable to use, +if it is nil or omitted, it defaults to `shell-command-history'." + (let ((exec (car (split-string-and-unquote initial)))) + (completing-read prompt (apply-partially #'cobra-completion-table exec) + nil nil initial (or hist 'shell-command-history)))) + +(provide 'cobra) +;;; cobra.el ends here diff --git a/lisp/net/kubed.el b/lisp/net/kubed.el index f90c481c1fc..1e5aef557c0 100644 --- a/lisp/net/kubed.el +++ b/lisp/net/kubed.el @@ -1272,112 +1272,6 @@ Optional argument DEFAULT is the minibuffer default argument." (format-prompt prompt default) nil nil nil nil 'kubed-container-images-history default)) -(defvar kubed-command-line-completion-cache nil) - -(defun kubed-command-line-completion-table (s p a) - "Completion table for `kubectl' command lines. - -Perform completion action A on string S with predicate P." - (let ((start 0)) - (while (string-match "[[:space:]=]" s start) - (setq start (match-end 0))) - (if (eq a 'metadata) - `(metadata - (category . kubed-command-line) - (affixation-function - . ,(lambda (cands) - (let ((max (seq-max - (cons 0 (mapcar #'string-width cands))))) - (mapcar - (lambda (cand) - (list - cand "" - (if-let - ((desc (get-text-property - 0 'kubed-command-line-argument-description - cand))) - (concat - (make-string (1+ (- max (string-width cand))) ?\s) - (propertize desc 'face 'completions-annotations)) - ""))) - cands))))) - (let* ((lines - (cdr - (if (string= s (car kubed-command-line-completion-cache)) - kubed-command-line-completion-cache - (setq - kubed-command-line-completion-cache - (cons s - (apply #'process-lines-ignore-status - kubed-kubectl-executable "__complete" - (let ((args (cdr (split-string-and-unquote s)))) - (if (string-suffix-p " " s) - (nconc args '("")) - args)))))))) - (code nil) - (comps (seq-take-while - (lambda (line) - (not (and (string-match "^:\\([0-9]+\\)$" line) - (setq code (string-to-number - (match-string 1 line)))))) - lines))) - ;; `code' encodes "completion directives", as follows: - ;; #b000001: An error occurred, ignore completions. - ;; #b000010: Don't add space after completion. - ;; #b000100: Don't fall back to file completion. - ;; #b001000: Completions are really file extension filters. - ;; #b010000: Complete directory names. - ;; #b100000: Preserve completions order. - (when (and code (zerop (logand 1 code))) - ;; Error bit in unset, proceed. - (if (= #b100 (logand #b100 code)) - ;; No file name completion. - (if (eq (car-safe a) 'boundaries) - `(boundaries - ,start . ,(and (string-match "[[:space:]=]" (cdr a)) - (match-beginning 0))) - (let ((table - (mapcar - ;; Annotate completion candidates. - (lambda (comp) - (pcase (split-string comp "\t" t) - (`(,c ,d . ,_) - (propertize - c 'kubed-command-line-argument-description - ;; Only keep first sentence. - (car (split-string d "\\." t)))) - (`(,c . ,_) c))) - comps))) - (if a (complete-with-action a table (substring s start) p) - ;; `try-completion'. - (let ((comp (complete-with-action a table (substring s start) p))) - (if (stringp comp) (concat (substring s 0 start) comp) comp))))) - ;; File name completion. - (setq p - (cond - ((= #b1000 (logand #b1000 code)) - ;; `comps' are valid extensions. - (lambda (f) - (or (file-directory-p f) - (when (string-match "\\.[^.]*\\'" f) - (member (substring f (1+ (match-beginning 0))) - comps))))) - ((= #b10000 (logand #b10000 code)) - ;; Directory name completion. - #'file-directory-p))) - (if (eq (car-safe a) 'boundaries) - ;; Find nested boundaries. - (let* ((suf (cdr a)) - (bounds (completion-boundaries - (substring s start) #'completion-file-name-table p - (substring suf 0 (string-match "[[:space:]=]" suf))))) - `(boundaries ,(+ (car bounds) start) . ,(cdr bounds))) - (if a (complete-with-action a #'completion-file-name-table - (substring s start) p) - (let ((comp (complete-with-action a #'completion-file-name-table - (substring s start) p))) - (if (stringp comp) (concat (substring s 0 start) comp) comp)))))))))) - (defvar kubed-kubectl-command-history nil "Minibuffer history for `kubed-kubectl-command'.") @@ -1389,9 +1283,8 @@ This function calls `shell-command' (which see) to do the work. Interactively, prompt for COMMAND with completion for `kubectl' arguments." (interactive - (list (completing-read "Command: " #'kubed-command-line-completion-table - nil nil (concat kubed-kubectl-executable " ") - 'kubed-kubectl-command-history))) + (list (cobra-read-command-line "Command: " "kubectl " + 'kubed-kubectl-command-history))) (shell-command command)) ;;;###autoload (autoload 'kubed-prefix-map "kubed" nil t 'keymap)