;;;; Command-line completion hook

(in-package "SYSTEM")


(defun ext:longest-common-prefix (vectors &key (test #'eql))
  "Return the longest common prefix of all vectors in the list."
  (do ((imax (reduce #'min vectors :key #'length))
       (i 0 (1+ i)))
      ((or (= i imax)
           (let ((e (aref (first vectors) i)))
             (dolist (v (rest vectors) nil)
               (unless (funcall test (aref v i) e)
                 (return t)))))
       (subseq (first vectors) 0 i))))

;;-----------------------------------------------------------------------------
;; Completing routine for the GNU Readline library.
;; Input: string (the input line), and the boundaries of the text to be
;; completed:  (subseq string start end)
;; Output: a list of simple-strings. empty, when no meaningful completions.
;; otherwise, CDR = list of all meaningful completions,
;; CAR = the immediate replacement
(let ((state nil))
(defun completion (string start end)
  (let* ((quotedp (and (>= start 1) ; quoted completion?
                       (member (char string (- start 1)) '(#\" #\|))))
         (start1 (if quotedp (1- start) start))
         (functionalp1 (and (>= start1 1)
                            (equal (subseq string (- start1 1) start1) "(")))
         (functionalp2 (and (>= start1 2)
                            (equal (subseq string (- start1 2) start1) "#'")))
         ;; completion of a function or of any symbol?
         (functionalp (or (= start end) functionalp1 functionalp2))
         (search-package-names nil) ; complete among package names
         ;; test for special case: nothing was entered to be completed,
         ;; so we try to DESCRIBE the last function symbol entered
         (void-completion
           (and (= start end)
                (or (>= start (length string))
                    (whitespacep (schar string start))))))
    ;; If nothing useful was entered (just whitespace), print help.
    (when void-completion
      (do ((pos (min end (1- (length string))) (1- pos))
           (depth 0)
           (white end))
          ((or (minusp pos) (plusp depth))
           (setq start (+ pos 2) end white))
        (cond ((char= #\( (schar string pos)) (incf depth))
              ((char= #\) (schar string pos)) (decf depth))
              ((whitespacep (schar string pos)) (setq white pos))))
      (when (< end start)       ; nothing useful was entered - just whitespace
        (sys::help) (terpri)    ; print help
        (return-from completion 0))) ; redraw the prompt
    ;; FIXME: If quotedp is true due to #\", we should better collect matching
    ;;        filenames, not symbols, I think.
    ;; Collect matching symbols.
    (let ((new-state (list* string start end))
          (package *package*)
          (mapfun #'sys::map-symbols)
          (prefix nil))
      ;; Extract the package name:
      (unless quotedp
        (let ((colon (position #\: string :start start :end end)))
          (if colon
            (let ((packname (subseq string start colon))) ; fresh!
              (case (readtable-case *readtable*)
                (:upcase (nstring-upcase packname))
                (:downcase (nstring-downcase packname))
                (:invert (nstring-invertcase packname)))
              (when (equal packname "") (setq packname "KEYWORD"))
              (setq package (find-package packname))
              (unless package
                (return-from completion nil))
              (incf colon)
              (if (and (< colon end) (eql (char string colon) #\:))
                (incf colon)
                (setq mapfun #'sys::map-external-symbols))
              (setq prefix (subseq string start colon))
              (setq start colon))
            (setq search-package-names t))))
      (let* ((case-sensitive-p
               (or quotedp
                   (package-case-sensitive-p package)
                   (case (readtable-case *readtable*)
                     ((:UPCASE :DOWNCASE) nil)
                     ((:PRESERVE :INVERT) t))))
             ;; FIXME: Handling of (readtable-case *readtable*) = :INVERT is
             ;;        incomplete.
             (case-inverted-p (package-case-inverted-p package))
             (known-part (subseq string start end))
             (known-len (length known-part))
             (char-cmp (if case-sensitive-p #'char= #'char-equal))
             (string-cmp (if case-sensitive-p #'string= #'string-equal))
             (return-list '())
             (match-and-collect
              (lambda (name)
                (when (>= (length name) known-len)
                  (when case-inverted-p
                    (setq name (string-invertcase name)))
                  (when (funcall string-cmp name known-part :end1 known-len)
                    (push name return-list)))))
             (gatherer
               (if functionalp
                 (lambda (sym)
                   (when (fboundp sym)
                     (funcall match-and-collect (symbol-name sym))))
                 (lambda (sym) (funcall match-and-collect (symbol-name sym))))))
        (funcall mapfun gatherer package)
        (when (and search-package-names (null return-list))
          (dolist (pack (list-all-packages))
            (funcall match-and-collect (package-name pack))
            (dolist (nick (package-nicknames pack))
              (funcall match-and-collect nick)))
          (when return-list
            (setq return-list
                  (mapcan (lambda (pack)
                            (ext:with-collect (c)
                              (do-external-symbols (s pack)
                                (let ((ret (ext:string-concat
                                            (package-name pack) ":"
                                            (symbol-name s))))
                                  (when case-inverted-p
                                    (setq ret (nstring-invertcase ret)))
                                  (c ret)))))
                          (delete-duplicates
                           (map-into return-list #'find-package
                                     return-list))))))
        ;; Now react depending on the list of matching symbols.
        (when (null return-list)
          (return-from completion nil))
        (when (and void-completion
                   (< end (length string)) (whitespacep (schar string end)))
          (let ((first-matching-name
                  (find known-part return-list :test string-cmp)))
            (when case-inverted-p
              (setq first-matching-name (string-invertcase first-matching-name)))
            (let ((first-matching-sym (find-symbol first-matching-name package)))
              (return-from completion
                (when (and first-matching-sym (fboundp first-matching-sym))
                      ;; FIXME: why not test (null (cdr return-list)) ?
                  (cond ((equalp state new-state)
                         (describe first-matching-sym) (terpri) (terpri))
                        (t (setq state new-state)))
                  0)))))               ; redraw the prompt
        ;; For a function without arguments, append a closing paren.
        (when (and functionalp1
                   (not quotedp)    ; readline will close the quote after #\) !
                   (null (cdr return-list))
                   (let ((name (car return-list)))
                     (when case-inverted-p
                       (setq name (string-invertcase name)))
                     (let ((sym (find-symbol name package)))
                       (and sym (fboundp sym) (functionp (symbol-function sym))
                            (multiple-value-bind (name req-num opt-num rest-p key-p)
                                (function-signature (symbol-function sym))
                              (declare (ignore name))
                              (and (eql req-num 0) (eql opt-num 0)
                                   (not rest-p) (not key-p)))))))
          (setf (car return-list) (string-concat (car return-list) ")")))
        ;; Downcase a function name.
        (when (and (not quotedp) (not case-sensitive-p))
          (map-into return-list #'string-downcase return-list))
        ;; Sort the return-list.
        (setq return-list (sort return-list #'string<))
        ;; Look for the largest common initial piece.
        (push (longest-common-prefix return-list :test char-cmp) return-list)
        ;; Reattach prefix consisting of package name and colons.
        (when prefix
          (mapl #'(lambda (l) (setf (car l) (string-concat prefix (car l))))
                return-list))
        return-list))))
)

(setq custom::*completion* #'completion)

(defun ext:make-completion (list)
  "Return a function suitable for `CUSTOM::*COMPLETION*'."
  (lambda (string start end)
    (let ((return-list
           ;; REMOVE-IF may return its list argument,
           ;; and SORT modifies its argument,
           ;; so we have to use DELETE-IF+COPY-LIST
           ;; to ensure that we do not modify the list argument.
           (delete-if (lambda (s)
                        (let ((s (string s)))
                          (string/= s string
                                    :start1 0 :end1 (min (length s) (- end start))
                                    :start2 start :end2 end)))
                      (copy-list list))))
      (and return-list
           (cons (longest-common-prefix return-list :test #'char=)
                 (sort return-list #'string<))))))
