branch: externals/corfu
commit 59949d3db2b6f4923f0817b9712c02b61d80fc41
Author: Daniel Mendler <m...@daniel-mendler.de>
Commit: Daniel Mendler <m...@daniel-mendler.de>

    Use child frames instead of overlays
    
    Fix #10, #13 and #15.
---
 README.org |  11 ++-
 corfu.el   | 270 ++++++++++++++++++++++++++++++++++---------------------------
 2 files changed, 158 insertions(+), 123 deletions(-)

diff --git a/README.org b/README.org
index a49e053..1218f53 100644
--- a/README.org
+++ b/README.org
@@ -9,9 +9,9 @@
 * Introduction
 
 Corfu enhances the default completion in region function with a completion
-overlay. The current candidates are shown in a popup overlay below or above the
-point. Corfu can be considered the minimalistic completion-in-region 
counterpart
-of the [[https://github.com/minad/vertico][Vertico]] minibuffer UI.
+overlay. The current candidates are shown in a popup below or above the point.
+Corfu can be considered the minimalistic completion-in-region counterpart of 
the
+[[https://github.com/minad/vertico][Vertico]] minibuffer UI.
 
 Icomplete implements both completion-in-region and minibuffer completion in a
 single package. While Corfu and Vertico are technically similar to Icomplete,
@@ -135,14 +135,13 @@ counterpart of Corfu.
 This package is experimental and new. I am not yet claiming that this package
 works correctly. There are a few known technical caveats.
 
-- The overlay popup is brittle (Alternatives to consider: Posframe, Postip)
-- The thin popup borders are only drawn if =line-spacing=nil=.
+- The package does not work in text mode since it uses child frames.
 - The abort handling could be improved, for example the input could be undone.
 - The ~completion-in-region-mode-predicate~ is ignored in order to
   give the completion style full control. The predicate asks the backend if
   the starting point of the completion has changed.
 - Company kind icons and metadata are not supported (~company-kind~, 
~company-docsig~)
-- No support for multi-backends like Company (Implement a multi-capf?)
+- No support for multi-backends like Company (Implement a multi-capf?).
 - No sorting by history, since ~completion-at-point~ does not
   maintain a history (See branch =history= for a possible solution).
 
diff --git a/corfu.el b/corfu.el
index 164bd7b..e828207 100644
--- a/corfu.el
+++ b/corfu.el
@@ -28,8 +28,8 @@
 
 ;; Corfu enhances the default completion in region function with a
 ;; completion overlay. The current candidates are shown in a popup
-;; overlay below or above the point. Corfu can be considered the
-;; minimalistic completion-in-region counterpart of Vertico.
+;; below or above the point. Corfu can be considered the minimalistic
+;; completion-in-region counterpart of Vertico.
 
 ;;; Code:
 
@@ -88,21 +88,19 @@ Set to nil in order to disable confirmation."
 
 (defface corfu-bar
   '((((class color) (min-colors 88) (background dark))
-     :foreground "#444" :background "#bbb")
+     :foreground "#444")
     (((class color) (min-colors 88) (background light))
-     :foreground "#bbb" :background "#444")
-    (t :foreground "gray" :background "black"))
-  "The foreground color is used for the scrollbar indicator.
-If `line-spacing/=nil' or in text-mode, the background color is used instead.")
+     :foreground "#bbb")
+    (t :foreground "gray"))
+  "The foreground color is used for the scrollbar indicator.")
 
 (defface corfu-border
   '((((class color) (min-colors 88) (background dark))
-     :foreground "#444" :background "#444" )
+     :foreground "#444")
     (((class color) (min-colors 88) (background light))
-     :foreground "#bbb" :background "#ddd")
-    (t :foreground "gray" :background "gray"))
-  "The foreground color used for the thin border.
-If `line-spacing/=nil' or in text-mode, the background color is used instead.")
+     :foreground "#bbb")
+    (t :foreground "gray"))
+  "The foreground color used for the thin border.")
 
 (defvar corfu-map
   (let ((map (make-sparse-keymap)))
@@ -144,8 +142,14 @@ If `line-spacing/=nil' or in text-mode, the background 
color is used instead.")
 (defvar-local corfu--input nil
   "Cons of last prompt contents and point or t.")
 
-(defvar-local corfu--overlays nil
-  "Overlay showing the candidates.")
+(defvar-local corfu--popup-frame nil
+  "Popup frame.")
+
+(defvar-local corfu--popup-buffer nil
+  "Popup buffer.")
+
+(defvar-local corfu--overlay nil
+  "Current candidate overlay.")
 
 (defvar-local corfu--extra-properties nil
   "Extra completion properties.")
@@ -162,15 +166,12 @@ If `line-spacing/=nil' or in text-mode, the background 
color is used instead.")
     corfu--index
     corfu--input
     corfu--total
-    corfu--overlays
+    corfu--popup-frame
+    corfu--popup-buffer
+    corfu--overlay
     corfu--extra-properties)
   "Buffer-local state variables used by Corfu.")
 
-(defun corfu--char-size ()
-  "Return character size in pixels."
-  (let ((lh (line-pixel-height)))
-    (cons (round (* lh (frame-char-width)) (frame-char-height)) lh)))
-
 ;; XXX Is there a better way to generate an image? Bitmap vector?
 (defun corfu--border (w h width fg bg)
   "Generate border with FG and BG colors, WIDTH and image size W*H."
@@ -185,81 +186,106 @@ If `line-spacing/=nil' or in text-mode, the background 
color is used instead.")
              :background ,(face-attribute bg :background)
              :foreground ,(face-attribute fg :foreground)))))
 
-;; XXX The popup code is unreliable. There are many problematic
-;; scenarios where the popup may fail:
-;; 1. display-line-numbers-mode
-;; 2. Overhanging lines (popup at the end of buffer)
-;; 3. Overhanging columns (popup at the end of line)
-;; 4. Outline folding
-;; 5. Wide characters like TAB
-;; 6. Lines with invisible/display properties (Org folding)
-;; 7. Scrolled windows
+;; Function adapted from posframe.el by tumashu
+(defun corfu--child-frame (x y width height content)
+  "Show child frame at X/Y with WIDTH/HEIGHT and CONTENT."
+  (let* ((window-min-height 1)
+         (window-min-width 1)
+         (after-make-frame-functions)
+         (frame-resize-pixelwise t)
+         (window-resize-pixelwise t)
+         (edge (window-inside-pixel-edges))
+         (lh (line-pixel-height))
+         (x (max 0 (min (+ (car edge) x) (- (frame-pixel-width) width))))
+         (y (+ (cadr edge) y))
+        (y (if (> (+ y lh height) (frame-pixel-height))
+               (- y height)
+              (+ y lh))))
+    (setq corfu--popup-buffer (or corfu--popup-buffer (generate-new-buffer " 
*corfu*")))
+    (with-current-buffer corfu--popup-buffer
+      (setq-local mode-line-format nil
+                  header-line-format nil
+                  frame-title-format ""
+                  truncate-lines nil
+                  cursor-type nil
+                  cursor-in-non-selected-windows nil
+                  show-trailing-whitespace nil
+                  display-line-numbers nil
+                  left-fringe-width nil
+                  right-fringe-width nil
+                  left-margin-width nil
+                  right-margin-width nil
+                  fringes-outside-margins 0)
+      (let (inhibit-modification-hooks)
+        (erase-buffer)
+        (insert content)))
+    (setq corfu--popup-frame
+          (or corfu--popup-frame
+              (make-frame
+               `((parent-frame . ,(window-frame))
+                 (background-color . ,(face-attribute 'corfu-background 
:background))
+                 (no-accept-focus . t)
+                 (min-width . t)
+                 (min-height . t)
+                 (width . 0)
+                 (height . 0)
+                 (line-spacing . 0)
+                 (border-width . 0)
+                 (internal-border-width . 0)
+                 (left-fringe . 0)
+                 (right-fringe . 0)
+                 (vertical-scroll-bars . nil)
+                 (horizontal-scroll-bars . nil)
+                 (menu-bar-lines . 0)
+                 (tool-bar-lines . 0)
+                 (tab-bar-lines . 0)
+                 (no-other-frame . t)
+                 (unsplittable . t)
+                 (undecorated . t)
+                 (visibility . nil)
+                 (cursor-type . nil)
+                 (minibuffer . nil)
+                 (no-special-glyphs . t)))))
+    (set-window-buffer (frame-root-window corfu--popup-frame) 
corfu--popup-buffer)
+    (set-frame-size corfu--popup-frame width height t)
+    (set-frame-position corfu--popup-frame x y)
+    (make-frame-visible corfu--popup-frame)))
+
 (defun corfu--popup (pos lines &optional curr lo bar)
   "Show LINES as popup at POS, with CURR highlighted and scrollbar from LO to 
LO+BAR."
-  (save-excursion
-    (goto-char pos)
-    (let* ((inhibit-field-text-motion t) ;; ignore field boundaries 
(shell-mode!)
-           (size (corfu--char-size))
-           ;; XXX Deactivate fancy border on terminal or if line-spacing is 
used
-           (fancy (and (not line-spacing) (display-graphic-p)))
-           (lborder-curr (corfu--border (car size) (cdr size) 1 'corfu-border 
'corfu-current))
-           (rborder-curr (corfu--border (car size) (cdr size) -1 'corfu-border 
'corfu-current))
-           (rbar-curr (corfu--border (car size) (cdr size) (- (ceiling (car 
size) 3))
-                                     'corfu-bar 'corfu-current))
-           (lborder (corfu--border (car size) (cdr size) 1 'corfu-border 
'corfu-background))
-           (rborder (corfu--border (car size) (cdr size) -1 'corfu-border 
'corfu-background))
-           (rbar (corfu--border (car size) (cdr size) (- (ceiling (car size) 
3))
-                                'corfu-bar 'corfu-background))
-           (max-width (min (cdr corfu-width-limits) (/ (window-total-width) 
2)))
-           (col (- pos (line-beginning-position)))
-           (rest-width (- (window-total-width) col 4))
-           (ypos (count-screen-lines (save-excursion (move-to-window-line 0) 
(point))
-                                     (point)))
-           (count (length lines))
-           (row 0) (width) (formatted) (beg))
-      (if (< rest-width (car corfu-width-limits))
-          (setq lines (mapcar (lambda (x) (truncate-string-to-width x 
max-width)) lines)
-                width (apply #'max (car corfu-width-limits) (mapcar 
#'string-width lines))
-                col (max 0 (- col width 2)))
-        (setq max-width (min rest-width max-width)
-              lines (mapcar (lambda (x) (truncate-string-to-width x 
max-width)) lines)
-              width (apply #'max (car corfu-width-limits) (mapcar 
#'string-width lines))))
-      (beginning-of-line)
-      (vertical-motion (if (and (< count ypos)
-                              (>= count (- (floor (window-pixel-height) (cdr 
size)) ypos 1)))
-                         (- count) 1))
-      (setq beg (point))
-      (when (save-excursion
-              (vertical-motion 1)
-              (/= (point) (line-beginning-position)))
-        (push #(" \n" 0 1 (cursor t)) formatted))
-      (dolist (line lines)
-        (let ((bufline (buffer-substring (point) (line-end-position)))
-              (str (concat
-                    (if fancy (if (eq row curr) lborder-curr lborder) " ")
-                    line
-                    (make-string (- width (string-width line)) 32)
-                    (cond
-                     (fancy (if (and lo (<= lo row (+ lo bar)))
-                                (if (eq row curr) rbar-curr rbar)
-                              (if (eq row curr) rborder-curr rborder)))
-                     (lo (propertize " " 'face (if (<= lo row (+ lo bar))
-                                                   'corfu-bar 'corfu-border)))
-                     (t " ")))))
-          (add-face-text-property 0 (length str) (if (eq row curr) 
'corfu-current 'corfu-background) 'append str)
-          (push (concat
-                 (truncate-string-to-width bufline col 0 32) str
-                 (substring bufline (length (truncate-string-to-width bufline 
(+ col width 2))))
-                 "\n")
-                formatted)
-          (setq row (1+ row))
-          (vertical-motion 1)))
-      (let ((ov (make-overlay beg (point))))
-        (overlay-put ov 'priority 900)
-        (overlay-put ov 'window (selected-window))
-        (overlay-put ov 'invisible t)
-        (overlay-put ov 'before-string (string-join (nreverse formatted)))
-        (push ov corfu--overlays)))))
+  (let* ((cw (frame-char-width))
+         (ch (frame-char-height))
+         (bw (* 3 (ceiling cw 5)))
+         (lborder (corfu--border bw ch 1 'corfu-border 'corfu-background))
+         (lborder-curr (corfu--border bw ch 1 'corfu-border 'corfu-current))
+         (rborder (corfu--border bw ch -1 'corfu-border 'corfu-background))
+         (rborder-curr (corfu--border bw ch -1 'corfu-border 'corfu-current))
+         (rbar-curr (corfu--border bw ch (- (ceiling cw 4))
+                                   'corfu-bar 'corfu-current))
+         (rbar (corfu--border bw ch (- (ceiling cw 4))
+                              'corfu-bar 'corfu-background))
+         (width (min (cdr corfu-width-limits)
+                     (/ (frame-width) 2)
+                     (apply #'max (car corfu-width-limits)
+                            (mapcar #'string-width lines))))
+         (row 0)
+         (pos (posn-x-y (posn-at-point pos))))
+    (corfu--child-frame
+     (- (or (car pos) 0) bw) (or (cdr pos) 0)
+     (+ (* width cw) bw bw) (* (length lines) ch)
+     (mapconcat (lambda (line)
+                  (let ((str (concat
+                              (if (eq row curr) lborder-curr lborder)
+                              (truncate-string-to-width line width 0 32)
+                              (if (and lo (<= lo row (+ lo bar)))
+                                  (if (eq row curr) rbar-curr rbar)
+                                (if (eq row curr) rborder-curr rborder)))))
+                    (when (eq row curr)
+                      (add-face-text-property
+                       0 (length str) 'corfu-current 'append str))
+                    (setq row (1+ row))
+                    str))
+                lines "\n"))))
 
 (defun corfu--move-to-front (elem list)
   "Move ELEM to front of LIST."
@@ -365,13 +391,6 @@ If `line-spacing/=nil' or in text-mode, the background 
color is used instead.")
   (and (symbolp this-command)
        (string-match-p corfu--keep-alive (symbol-name this-command))))
 
-(defun corfu--pre-command-hook ()
-  "Delete overlays."
-  (mapc #'delete-overlay corfu--overlays)
-  (setq corfu--overlays nil)
-  (unless (or (< corfu--index 0) (corfu--keep-alive-p))
-    (corfu--insert 'exact)))
-
 (defun corfu-abort ()
   "Abort Corfu completion."
   (interactive)
@@ -413,11 +432,11 @@ If `line-spacing/=nil' or in text-mode, the background 
color is used instead.")
          (cands (funcall corfu--highlight (seq-subseq corfu--candidates start 
last)))
          (ann-cands (mapcar #'corfu--format-candidate (corfu--annotate 
metadata cands))))
     (when (>= curr 0)
-      (let ((ov (make-overlay beg end nil t t)))
-        (overlay-put ov 'priority 1000)
-        (overlay-put ov 'window (selected-window))
-        (overlay-put ov 'display (concat (substring str 0 corfu--base) (nth 
curr cands)))
-        (push ov corfu--overlays)))
+      (when corfu--overlay (delete-overlay corfu--overlay))
+      (setq corfu--overlay (make-overlay beg end nil t t))
+      (overlay-put corfu--overlay 'priority 1000)
+      (overlay-put corfu--overlay 'window (selected-window))
+      (overlay-put corfu--overlay 'display (concat (substring str 0 
corfu--base) (nth curr cands))))
     ;; Nonlinearity at the end and the beginning
     (when (/= start 0)
       (setq lo (max 1 lo)))
@@ -440,32 +459,45 @@ If `line-spacing/=nil' or in text-mode, the background 
color is used instead.")
      ((condition-case err
           (unless (equal corfu--input (cons str pt))
             (and (corfu--update-candidates str metadata pt table pred)) nil)
-        (t (message "%s" (error-message-string err)))))
+        (t (message "%s" (error-message-string err))
+           nil)))
      ((and (not corfu--candidates)                    ;; 1) There are no 
candidates
            initializing)                              ;; &  Initializing, 
first retrieval of candidates.
-      (minibuffer-message "No match"))                ;; => Show error message
+      (minibuffer-message "No match")                 ;; => Show error message
+      nil)
      ((and (not corfu--candidates)                    ;; 2) There are no 
candidates
            corfu-confirm)                             ;; &  Confirmation is 
enabled
-      (corfu--popup beg (list corfu-confirm)))        ;; => Show confirmation 
popup
+      (corfu--popup beg (list corfu-confirm))         ;; => Show confirmation 
popup
+      t)
      ((and corfu--candidates                          ;; 3) There exist 
candidates
            (not (equal corfu--candidates (list str))) ;; &  Not a sole exactly 
matching candidate
            (or (/= beg end) (corfu--keep-alive-p)))   ;; &  Input is non-empty 
or keep-alive command
-      (corfu--show-candidates beg end str metadata))  ;; => Show candidates 
popup
+      (corfu--show-candidates beg end str metadata)   ;; => Show candidates 
popup
+      t)
      ;; When after `completion-at-point/corfu-complete', no further completion 
is possible and the
      ;; current string is a valid match, exit with status 'finished.
      ((and (memq this-command '(corfu-complete completion-at-point))
            (not (stringp (try-completion str table pred)))
            (test-completion str table pred))
-      (corfu--done str 'finished)))))
+      (corfu--done str 'finished)
+      nil))))
+
+(defun corfu--pre-command-hook ()
+  "Insert selected candidate unless keep alive command."
+  (unless (or (< corfu--index 0) (corfu--keep-alive-p))
+    (corfu--insert 'exact)))
+
+(defun corfu--window-configuration-change-hook ()
+  "Terminate completion when window configuration changed."
+  (completion-in-region-mode -1))
 
 (defun corfu--post-command-hook ()
   "Refresh Corfu after last command."
-  (pcase completion-in-region--data
-    (`(,beg ,end ,_table ,_pred)
-     (when (and (eq (marker-buffer beg) (current-buffer)) (<= beg (point) end))
-       (corfu--update))))
-  (unless corfu--overlays
-    (completion-in-region-mode -1)))
+  (or (pcase completion-in-region--data
+        (`(,beg ,end ,_table ,_pred)
+         (when (and (eq (marker-buffer beg) (current-buffer)) (<= beg (point) 
end))
+           (corfu--update))))
+      (completion-in-region-mode -1)))
 
 (defun corfu--goto (index)
   "Go to candidate with INDEX."
@@ -608,14 +640,18 @@ If `line-spacing/=nil' or in text-mode, the background 
color is used instead.")
   "Setup Corfu completion state."
   (setq corfu--extra-properties completion-extra-properties)
   (setcdr (assq #'completion-in-region-mode minor-mode-overriding-map-alist) 
corfu-map)
+  (add-hook 'window-configuration-change-hook 
#'corfu--window-configuration-change-hook nil 'local)
   (add-hook 'pre-command-hook #'corfu--pre-command-hook nil 'local)
   (add-hook 'post-command-hook #'corfu--post-command-hook nil 'local))
 
 (defun corfu--teardown ()
   "Teardown Corfu."
+  (remove-hook 'window-configuration-change-hook 
#'corfu--window-configuration-change-hook 'local)
   (remove-hook 'pre-command-hook #'corfu--pre-command-hook 'local)
   (remove-hook 'post-command-hook #'corfu--post-command-hook 'local)
-  (mapc #'delete-overlay corfu--overlays)
+  (when corfu--overlay (delete-overlay corfu--overlay))
+  (when corfu--popup-frame (delete-frame corfu--popup-frame))
+  (when corfu--popup-buffer (kill-buffer corfu--popup-buffer))
   (mapc #'kill-local-variable corfu--state-vars))
 
 (defun corfu--mode-hook ()

Reply via email to