[comp.emacs] Partial completion update, version 1.04

daveg@cit-vax.Caltech.Edu (David Gillespie) (06/30/90)

Here's (yet) another version of my partial completion system.  Known bugs
have been fixed; behavior is even more compatible with standard Emacs
completion (let's hope RMS doesn't sue me for "look and feel". :-)
Handling of word-completion and file name completion is improved.

								-- Dave


;; Partial completion mechanism for GNU Emacs.  Version 1.04.
;; Copyright (C) 1990 Dave Gillespie, daveg@csvax.caltech.edu.
;; Special thanks to Hallvard Furuseth for his many ideas and contributions.

;; This file is part of GNU Emacs.

;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY.  No author or distributor
;; accepts responsibility to anyone for the consequences of using it
;; or for whether it serves any particular purpose or works at all,
;; unless he says so in writing.  Refer to the GNU Emacs General Public
;; License for full details.

;; Everyone is granted permission to copy, modify and redistribute
;; GNU Emacs, but only under the conditions described in the
;; GNU Emacs General Public License.   A copy of this license is
;; supposed to have been given to you along with GNU Emacs so you
;; can know your rights and responsibilities.  It should be in a
;; file named COPYING.  Among other things, the copyright notice
;; and this notice must be preserved on all copies.


;; Extended completion for the Emacs minibuffer.
;;
;; The basic idea is that the command name or other completable text is
;; divided into words and each word is completed separately, so that
;; "M-x p-b" expands to "M-x print-buffer".  If the entry is ambiguous
;; each word is completed as much as possible and then the cursor is
;; left at the first position where typing another letter will resolve
;; the ambiguity.
;;
;; Word separators for this purpose are hyphen, space, and period.
;; These would most likely occur in command names, Info menu items,
;; and file names, respectively.  But all word separators are treated
;; alike at all times.
;;
;; This completion package installs itself on Meta- key sequences by
;; default, but many people prefer to replace the old-style completer
;; outright.  You can do this by setting PC-meta-flag as described below.


;; Usage:  Load this file.  Now, during completable minibuffer entry,
;;
;;     M-TAB    means to do a partial completion;
;;     M-SPC    means to do a partial complete-word;
;;     M-RET    means to do a partial complete-and-exit;
;;     M-?      means to do a partial completion-help.
;;
;; If you set PC-meta-flag non-nil, then TAB, SPC, RET, and ? perform
;; these functions, and M-TAB etc. perform original Emacs completion.
;; To do this, put the command,
;;
;;       (setq PC-meta-flag t)
;;
;; in your .emacs file.  To load partial completion automatically, put
;;
;;       (load "complete")
;;
;; in your .emacs file, too.  Things will be faster if you byte-compile
;; this file when you install it.
;;
;; As an extra feature, in cases where RET would not normally
;; complete (such as `C-x b'), the M-RET key will always do a partial
;; complete-and-exit.  Thus `C-x b f.c RET' will select or create a
;; buffer called "f.c", but `C-x b f.c M-RET' will select the existing
;; buffer whose name matches that pattern (perhaps "filing.c").
;; (PC-meta-flag does not affect this behavior; M-RET used to be
;; undefined in this situation.)


(defvar PC-meta-flag nil
  "*If nil, TAB does normal Emacs completion and M-TAB does Partial Completion.
If t, TAB does Partial Completion and M-TAB does normal completion.")


(defvar PC-default-bindings t
  "Set this to nil to suppress the default partial completion key bindings.")

(if PC-default-bindings (progn
(define-key minibuffer-local-completion-map "\t" 'PC-complete)
(define-key minibuffer-local-completion-map " "  'PC-complete-word)
(define-key minibuffer-local-completion-map "?"  'PC-completion-help)

(define-key minibuffer-local-completion-map "\e\t" 'PC-complete)
(define-key minibuffer-local-completion-map "\e "  'PC-complete-word)
(define-key minibuffer-local-completion-map "\e\r" 'PC-force-complete-and-exit)
(define-key minibuffer-local-completion-map "\e\n" 'PC-force-complete-and-exit)
(define-key minibuffer-local-completion-map "\e?"  'PC-completion-help)

(define-key minibuffer-local-must-match-map "\t" 'PC-complete)
(define-key minibuffer-local-must-match-map " "  'PC-complete-word)
(define-key minibuffer-local-must-match-map "\r" 'PC-complete-and-exit)
(define-key minibuffer-local-must-match-map "\n" 'PC-complete-and-exit)
(define-key minibuffer-local-must-match-map "?"  'PC-completion-help)

(define-key minibuffer-local-must-match-map "\e\t" 'PC-complete)
(define-key minibuffer-local-must-match-map "\e "  'PC-complete-word)
(define-key minibuffer-local-must-match-map "\e\r" 'PC-complete-and-exit)
(define-key minibuffer-local-must-match-map "\e\n" 'PC-complete-and-exit)
(define-key minibuffer-local-must-match-map "\e?"  'PC-completion-help)
))


(defun PC-complete ()
  "Like minibuffer-complete, but allows \"b--di\"-style abbreviations.
For example, \"M-x b--di\" would match \"byte-recompile-directory\", or any
name which consists of three or more words, the first beginning with \"b\"
and the third beginning with \"di\".

The pattern \"b--d\" is ambiguous for \"byte-recompile-directory\" and
\"beginning-of-defun\", so this would produce a list of completions
just like when normal Emacs completions are ambiguous.

Word-delimiters for the purposes of Partial Completion are \"-\", \".\", and SPC."
  (interactive)
  (if (eq (or (> (length (this-command-keys)) 1)
	      (> (aref (this-command-keys) 0) 128)) PC-meta-flag)
      (minibuffer-complete)
    (PC-do-completion nil))
)


(defun PC-complete-word ()
  "Like minibuffer-complete-word, but allows \"b--di\"-style abbreviations.
See PC-complete for details."
  (interactive)
  (if (eq (or (> (length (this-command-keys)) 1)
	      (> (aref (this-command-keys) 0) 128)) PC-meta-flag)
      (if (= last-command-char ? )
	  (minibuffer-complete-word)
	(self-insert-command 1))
    (self-insert-command 1)
    (if (eobp)
	(PC-do-completion 'word)))
)


(defun PC-complete-and-exit ()
  "Like minibuffer-complete-and-exit, but allows \"b--di\"-style abbreviations.
See PC-complete for details."
  (interactive)
  (if (eq (or (> (length (this-command-keys)) 1)
	      (> (aref (this-command-keys) 0) 128)) PC-meta-flag)
      (minibuffer-complete-and-exit)
    (PC-do-complete-and-exit))
)

(defun PC-force-complete-and-exit ()
  "Like minibuffer-complete-and-exit, but allows \"b--di\"-style abbreviations.
See PC-complete for details."
  (interactive)
  (let ((minibuffer-completion-confirm nil))
    (PC-do-complete-and-exit))
)

(defun PC-do-complete-and-exit ()
  (if (= (buffer-size) 0)  ; Duplicate the "bug" that Info-menu relies on...
      (exit-minibuffer)
    (let ((flag (PC-do-completion 'exit)))
      (and flag
	   (if (or (eq flag 'complete)
		   (not minibuffer-completion-confirm))
	       (exit-minibuffer)
	     (temp-minibuffer-message " (Confirm)")))))
)


(defun PC-completion-help ()
  "Like minibuffer-completion-help, but allows \"b--di\"-style abbreviations.
See PC-complete for details."
  (interactive)
  (if (eq (or (> (length (this-command-keys)) 1)
	      (> (aref (this-command-keys) 0) 128)) PC-meta-flag)
      (minibuffer-completion-help)
    (PC-do-completion 'help))
)


(defun PC-do-completion (&optional mode)
  (let* ((table minibuffer-completion-table)
	 (pred minibuffer-completion-predicate)
	 (filename (eq table 'read-file-name-internal))
	 (dirname nil)
	 (str (buffer-string))
	 basestr
	 regex
	 (p 0)
	 (poss nil)
	 helpposs
	 (case-fold-search completion-ignore-case))

    ;; Check if buffer contents can already be considered complete
    (if (and (eq mode 'exit)
	     (PC-is-complete-p str table pred))
	'complete

      ;; Strip directory name if appropriate
      (if filename
	  (setq basestr (file-name-nondirectory str)
		dirname (file-name-directory str))
	(setq basestr str))

      ;; Convert search pattern to a standard regular expression
      (setq regex (regexp-quote basestr))
      (while (setq p (string-match "[-. ]" regex p))
	(if (eq (aref regex p) ? )
	    (setq regex (concat (substring regex 0 p)
				"[^-. ]*[-. ]"
				(substring regex (1+ p)))
		  p (+ p 12))
	  (let ((bump (if (eq (aref regex p) ?-) 0 -1)))
	    (setq regex (concat (substring regex 0 (+ p bump))
				"[^-. ]*"
				(substring regex (+ p bump)))
		  p (+ p 8)))))
      (setq regex (concat "\\`" regex))

      ;; Find an initial list of possible completions
      (if (not (setq p (string-match "[-. ]" str (length dirname))))

	  ;; Minibuffer contains no hyphens -- simple case!
	  (setq poss (all-completions str
				      table
				      pred))

	;; Use all-completions to do an initial cull.  This is a big win,
	;; since all-completions is written in C!
	(let ((compl (all-completions (substring str 0 p)
				      table
				      pred)))
	  (setq p compl)
	  (while p
	    (and (string-match regex (car p))
		 (setq poss (cons (car p) poss)))
	    (setq p (cdr p)))))

      ;; Now we have a list of possible completions
      (cond

       ;; No valid completions found
       ((null poss)
	(if (and (eq mode 'word)
		 (not PC-word-failed-flag))
	    (let ((PC-word-failed-flag t))
	      (delete-backward-char 1)
	      (PC-do-completion 'word))
	  (beep)
	  (temp-minibuffer-message (if (eq mode 'help)
				       " (No completions)"
				     " (No match)"))
	  nil))

       ;; More than one valid completion found
       ((or (cdr (setq helpposs poss))
	    (memq mode '(help word)))

	;; Handle completion-ignored-extensions
	(and filename
	     (not (eq mode 'help))
	     (let ((p2 poss))

	       ;; Build a regular expression representing the extensions list
	       (or (equal completion-ignored-extensions PC-ignored-extensions)
		   (setq PC-ignored-regexp
			 (concat "\\("
				 (mapconcat
				  'regexp-quote
				  (setq PC-ignored-extensions
					completion-ignored-extensions)
				  "\\|")
				 "\\)\\'")))

	       ;; Check if there are any without an ignored extension
	       (setq p nil)
	       (while p2
		 (or (string-match PC-ignored-regexp (car p2))
		     (setq p (cons (car p2) p)))
		 (setq p2 (cdr p2)))

	       ;; If there are "good" names, use them
	       (and p (setq poss p))))

	;; Is the actual string one of the possible completions?
	(setq p (and (not (eq mode 'help)) poss))
	(while (and p
		    (not (equal (car p) basestr)))
	  (setq p (cdr p)))
	(if p

	    (progn
	      (if (null mode)
		  (temp-minibuffer-message " (Complete, but not unique)"))
	      t)

	  ;; If ambiguous, try for a partial completion
	  (let ((improved nil)
		prefix
		(pt nil)
		(skip "\\`"))

	    ;; Check if next few letters are the same in all cases
	    (if (and (not (eq mode 'help))
		     (setq prefix (try-completion "" (mapcar 'list poss))))
		(let (i)
		  (if (eq mode 'word)
		      (setq prefix (PC-chop-word prefix basestr)))
		  (goto-char (+ (point-min) (length dirname)))
		  (while (and (progn
				(setq i 0)
				(while (< i (length prefix))
				  (if (eq (aref prefix i) (following-char))
				      (forward-char 1)
				    (if (or (and (looking-at " ")
						 (memq (aref prefix i)
						       '(?- ?. ? )))
					    (eq (downcase (aref prefix i))
						(downcase (following-char))))
					(delete-char 1)
				      (setq improved t))
				    (insert (substring prefix i (1+ i))))
				  (setq i (1+ i)))
				(or pt (setq pt (point)))
				(looking-at "[-. ]"))
			      (not (eq mode 'word))
			      (setq skip (concat skip
						 (regexp-quote prefix)
						 "[^-. ]*")
				    prefix (try-completion
					    ""
					    (mapcar
					     (function
					      (lambda (x)
						(list
						 (and (string-match skip x)
						      (substring
						       x
						       (match-end 0))))))
					     poss)))
			      (or (> i 0) (> (length prefix) 0))))
		  (goto-char (if (eq mode 'word) (point-max) pt))))

	    (if (and (eq mode 'word)
		     (not PC-word-failed-flag))

		(if improved

		    ;; We changed it... would it be complete without the space?
		    (if (PC-is-complete-p (buffer-substring 1 (1- (point-max)))
					  table pred)
			(delete-region (1- (point-max)) (point-max))))

	      (if improved

		  ;; We changed it... enough to be complete?
		  (and (eq mode 'exit)
		       (PC-is-complete-p (buffer-string) table pred))

		;; If totally ambiguous, display a list of completions
		(if (or completion-auto-help
			(eq mode 'help))
		    (with-output-to-temp-buffer " *Completions*"
		      (display-completion-list (sort helpposs 'string-lessp)))
		  (temp-minibuffer-message " (Next char not unique)"))
		nil)))))

       ;; Only one possible completion
       (t
	(if (equal basestr (car poss))
	    (if (null mode)
		(temp-minibuffer-message " (Sole completion)"))
	  (erase-buffer)
	  (insert (if filename
		      (substitute-in-file-name (concat dirname (car poss)))
		    (car poss))))
	t))))
)
(setq PC-ignored-extensions 'empty-cache)
(setq PC-word-failed-flag nil)


(defun PC-is-complete-p (str table pred)
  (let ((res (if (listp table)
		 (assoc str table)
	       (if (vectorp table)
		   (or (equal str "nil")   ; heh, heh, heh
		       (intern-soft str table))
		 (funcall table str pred 'lambda)))))
    (and (or (not pred)
	     (and (not (listp table)) (not (vectorp table)))
	     (funcall pred res))
	 res))
)

(defun PC-chop-word (new old)
  (let ((i -1)
	(j -1))
    (while (and (setq i (string-match "[-. ]" old (1+ i)))
		(setq j (string-match "[-. ]" new (1+ j)))))
    (if (and j
	     (or (not PC-word-failed-flag)
		 (setq j (string-match "[-. ]" new (1+ j)))))
	(substring new 0 (1+ j))
      new))
)

(defun temp-minibuffer-message (m)
  "A Lisp version of temp_minibuffer_message from minibuf.c."
  (let ((savemax (point-max)))
    (save-excursion
      (goto-char (point-max))
      (insert m))
    (let ((inhibit-quit t))
      (sit-for 2)
      (delete-region savemax (point-max))
      (if quit-flag
	  (setq quit-flag nil
		unread-command-char 7))))
)