Browse Source

Implement multiple backend support.

Damien Merenne 5 years ago
parent
commit
d1ac4c688e

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+/.eldev
+/Eldev-local
+/.cask

+ 8 - 0
test/data/links.html

@@ -0,0 +1,8 @@
+<html>
+  <head>
+  </head>
+  <body>
+    <div><a id="test-1" href="test-1.html">test 1</a></div>
+    <div><a id="test-2" href="test-2.html">test 2</a></div>
+  </body>
+</html>

+ 8 - 0
test/data/test-1.html

@@ -0,0 +1,8 @@
+<html>
+  <head>
+    <title>test 1</title>
+  </head>
+  <body>
+    <h1>test 1<h1>
+  </body>
+</html>

+ 8 - 0
test/data/test-2.html

@@ -0,0 +1,8 @@
+<html>
+  <head>
+    <title>test 2</title>
+  </head>
+  <body>
+    <h1>test 2<h1>
+  </body>
+</html>

+ 72 - 0
test/test-helper.el

@@ -0,0 +1,72 @@
+;;; test-helper.el -- Test helpers for xwidget-webkit-plus
+
+;; Copyright (C) 2020 Damien Merenne <dam@cosinux.org>
+
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;
+
+;;; Code:
+
+
+(when (> emacs-major-version 26)
+  (defalias 'ert--print-backtrace 'backtrace-to-string))
+
+
+(defconst xwidget-plus-test-path (file-name-as-directory
+                             (file-name-directory (or load-file-name buffer-file-name)))
+  "The test directory.")
+
+(defconst xwidget-plus-test-data-path (file-name-as-directory
+                                       (concat xwidget-plus-test-path "data"))
+  "The test data directory.")
+
+(defconst xwidget-plus-root-path (file-name-as-directory
+                                         (file-name-directory
+                                          (directory-file-name xwidget-plus-test-path)))
+  "The package root path.")
+
+(add-to-list 'load-path xwidget-plus-root-path)
+
+(defun xwidget-plus-event-dispatch (&optional seconds)
+  (save-excursion
+    (with-current-buffer (xwidget-buffer (xwidget-webkit-last-session))
+      (let ((event (read-event nil nil seconds)))
+        (message "event:%s " event)
+        (xwidget-event-handler)
+        event))))
+
+(defun xwidget-plus-event-loop ()
+  (save-excursion
+    (with-current-buffer (xwidget-buffer (xwidget-webkit-last-session))
+      (while (xwidget-plus-event-dispatch 0.05)))))
+
+(defmacro with-browse (file &rest body)
+  (declare (indent 1))
+  (let ((url (format "file://%s%s" (expand-file-name xwidget-plus-test-data-path) file)))
+    `(progn
+       (xwidget-webkit-browse-url ,url)
+       ;; this will trigger a loading event
+       (xwidget-plus-event-dispatch)
+       (let ((xwidget (xwidget-webkit-last-session)))
+         (with-current-buffer (xwidget-buffer xwidget)
+           ,@body)))))
+
+
+(defun xwidget-plus-wait-for (script)
+  "Wait until scripts evaluate to true")
+(provide 'test-helper)
+;;; test-helper.el ends here

+ 164 - 0
test/xwidget-plus-follow-link-test.el

@@ -0,0 +1,164 @@
+;;; xwidget-plus-follow-link-test.el -- xwwp follow link test suite -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020 Damien Merenne <dam@cosinux.org>
+
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Test suite for xwidget-webkit-plus-follow-link feature
+
+;;; Code:
+
+(require 'test-helper)
+(require 'with-simulated-input)
+(require 'ivy)
+(require 'helm)
+(require 'xwidget-plus-follow-link)
+
+;; Disable helm and ivy, otherwise they hijack completing-read and break the
+;; default completion backend test.
+(setq completing-read-function #'completing-read-default)
+
+
+;; A mocked backend class that doesn't interactively read anything.
+(defclass xwidget-plus-completion-backend-test (xwidget-plus-completion-backend)
+  ((candidates-mock :initarg :candidates-mock)
+   (selected-mock :initarg :selected-mock)
+   (action-fn)
+   (classes)))
+
+(defun xwidget-plus-update-fn-callback (result)
+  "Called after updating candidates with the css classes in RESULT."
+  (let ((backend xwidget-plus-completion-backend-instance))
+    ;; Store the results.
+    (oset backend classes result)
+    ;; Trigger the action function with the mocked selected link
+    (funcall (oref backend action-fn) (oref backend selected-mock))
+    (xwidget-plus-event-dispatch)))
+
+(cl-defmethod xwidget-plus-follow-link-candidates ((backend xwidget-plus-completion-backend-test))
+  "Return the list of BACKEND mocked candidates."
+  (oref backend candidates-mock))
+
+(cl-defmethod xwidget-plus-follow-link-read ((backend xwidget-plus-completion-backend-test) _ _ action-fn update-fn)
+  "Store ACTION-FN in BACKEND, call the UPDATE-FN, and fetch the link element classes."
+  ;; Stoire action so that we can call it after having fetch the css classes.
+  (oset backend action-fn action-fn)
+  ;; Trigger the javascript update.
+  (funcall update-fn)
+  ;; Fetch css classes.
+  (xwidget-webkit-execute-script (xwidget-webkit-current-session) (--js "
+map = Array.prototype.map;
+r = {};
+document.querySelectorAll('a').forEach(l => {
+    r[l.id] = map.call(l.classList, t => {
+        return t.toString();
+     });
+});
+r
+" js--) #'xwidget-plus-update-fn-callback)
+  (xwidget-plus-event-dispatch))
+
+(cl-defmethod backend-test-link-classes ((backend xwidget-plus-completion-backend-test) link-id)
+  "Return test BACKEND css class names for LINK-ID."
+  (cdr (assoc link-id (oref backend classes))))
+
+(defmacro with-backend (backend &rest body)
+  "Run BODY with the specified BACKEND."
+  (declare (indent 1))
+  `(let* ((backend (,(intern (concat "xwidget-plus-completion-backend-" (symbol-name backend)))))
+          (xwidget-plus-completion-backend-instance backend))
+     ,@body))
+
+(defmacro with-test-backend-browse (candidates selected url &rest body)
+  "Run BODY with the specified BACKEND mocking CANDIDATES and SELECTED while browsing URL."
+  (declare (indent 3))
+  `(let* ((backend (xwidget-plus-completion-backend-test :candidates-mock ,candidates
+                                                         :selected-mock ,selected))
+          (xwidget-plus-completion-backend-instance backend))
+     (with-browse ,url
+       ,@body)))
+
+(ert-deftest test-xwidget-plus-follow-link-prepare-links ()
+  (let ((links '(("3" . "Functions")
+                 ("1" . "Function Cells")
+                 ("12" . "Structures")
+                 ("2" . "Anonymous Functions")
+                 ("9" . "Declare Form"))))
+    (should (equal '(("Function Cells" . 1)
+	             ("Anonymous Functions" . 2)
+	             ("Functions" . 3)
+	             ("Declare Form" . 9)
+	             ("Structures" . 12))
+            (xwidget-plus-follow-link-prepare-links links)))))
+
+(ert-deftest test-xwidget-plus-follow-link-highlight ()
+  (with-test-backend-browse '(0 0 1) 0 "links.html"
+    (xwidget-plus-follow-link)
+      (xwidget-plus-event-dispatch)
+      (should (string= "test-1.html" (file-name-nondirectory (xwidget-webkit-current-url))))
+      (should (equal (backend-test-link-classes backend "test-1") '["xwidget-plus-follow-link-selected"]))
+      (should (equal (backend-test-link-classes backend "test-2") '["xwidget-plus-follow-link-candidate"])))
+  (with-test-backend-browse '(1 0 1) 1 "links.html"
+    (split-window-vertically)
+    (save-excursion (switch-to-buffer-other-window "*Messages*"))
+    (xwidget-plus-follow-link)
+    (xwidget-plus-event-dispatch)
+    (should (string= "test-2.html" (file-name-nondirectory (xwidget-webkit-current-url))))
+    (should (equal (backend-test-link-classes backend "test-1") '["xwidget-plus-follow-link-candidate"]))
+    (should (equal (backend-test-link-classes backend "test-2") '["xwidget-plus-follow-link-selected"]))))
+
+(defmacro with-read-fixtures (backend &rest body)
+  (declare (indent 1))
+  `(let* ((links '(("test 1" . 0) ("test 2" . 1)))
+          link
+          (action (lambda (l) (setq link l)))
+          (update (lambda ())))
+     (with-backend ,backend
+       (with-browse "links.html"
+         ,@body))))
+
+(ert-deftest test-xwidget-plus-follow-link-read-default ()
+  (with-read-fixtures default
+    (with-simulated-input "test SPC 2 RET"
+      (xwidget-plus-follow-link-read backend "Test: " links action update)
+      (should (= 1 link)))))
+
+(ert-deftest test-xwidget-plus-follow-link-read-ido ()
+  (with-read-fixtures ido
+    (with-simulated-input "2 RET"
+      (xwidget-plus-follow-link-read backend "Test: " links action update)
+      (should (= 1 link)))))
+
+(ert-deftest test-xwidget-plus-follow-link-read-ivy ()
+  (with-read-fixtures ivy
+    (with-simulated-input "2 RET"
+      (xwidget-plus-follow-link-read backend "Test: " links action update)
+      (should (= 1 link)))))
+
+(ert-deftest test-xwidget-plus-follow-link-read-helm ()
+  (with-read-fixtures helm
+    (with-simulated-input '("2" (wsi-simulate-idle-time 0.1) "RET")
+      (xwidget-plus-follow-link-read backend "Test: " links action update)
+      (should (= 1 link)))))
+
+;; Local Variables:
+;; eval: (mmm-mode)
+;; eval: (mmm-add-classes '((elisp-js :submode js-mode :face mmm-code-submode-face :delimiter-mode nil :front "--js \"" :back "\" js--")))
+;; mmm-classes: elisp-js
+;; End:
+
+(provide 'xwidget-plus-follow-link-test)
+;;; xwidget-plus-follow-link-test.el ends here

+ 9 - 16
xwidget-plus-common.el

@@ -21,24 +21,17 @@
 
 ;;; Code:
 
+(defgroup xwidget-plus nil
+  "Augment the xwidget webkit browser."
+  :group 'convenience)
+
+
 (require 'xwidget)
 
 (defun xwidget-plus-make-class (class style)
   "Generate a css CLASS definition from the STYLE alist."
   (format ".%s { %s }\\n" class (mapconcat (lambda (v) (format "%s: %s;" (car v) (cdr v))) style " ")))
 
-(defun xwidget-plus-follow-link-style-definition ()
-  "Return the css definitions for the follow link feature."
-  (concat (xwidget-plus-make-class "xwidget-plus-follow-link-candidate" xwidget-plus-follow-link-candidate-style)
-          (xwidget-plus-make-class "xwidget-plus-follow-link-selected" xwidget-plus-follow-link-selected-style)))
-
-(defun xwidget-plus-follow-link-format-link (str)
-  "Format link title STR."
-  (setq str (replace-regexp-in-string "^[[:space:][:cntrl:]]+" "" str))
-  (setq str (replace-regexp-in-string "[[:space:][:cntrl:]]+$" "" str))
-  (setq str (replace-regexp-in-string "[[:cntrl:]]+" "/" str))
-  (replace-regexp-in-string "[[:space:]]+" " " str))
-
 (defmacro --js (js _ &rest replacements)
   "Apply `format' on JS with REPLACEMENTS  providing MMM mode delimiters.
 
@@ -66,6 +59,7 @@ if (!document.getElementById(__xwidget_id)) {
     e.innerHTML = '%s';
     document.getElementsByTagName('head')[0].appendChild(e);
 };
+null;
 " js-- id tag type content)))
     (xwidget-webkit-execute-script xwidget script)))
 
@@ -77,12 +71,11 @@ if (!document.getElementById(__xwidget_id)) {
   "Inject css STYLE in XWIDGET session using a style element with ID."
   (xwidget-plus-inject-head-element xwidget "style" id "text/css" style))
 
-
-(provide 'xwidget-plus-common)
-
-;;; xwidget-plus-common.el ends here
 ;; Local Variables:
 ;; eval: (mmm-mode)
 ;; eval: (mmm-add-classes '((elisp-js :submode js-mode :face mmm-code-submode-face :delimiter-mode nil :front "--js \"" :back "\" js--")))
 ;; mmm-classes: elisp-js
 ;; End:
+
+(provide 'xwidget-plus-common)
+;;; xwidget-plus-common.el ends here

+ 169 - 32
xwidget-plus-follow-link.el

@@ -21,7 +21,28 @@
 
 ;;; Code:
 
+(require 'xwidget)
 (require 'xwidget-plus-common)
+(require 'ivy)
+(require 'eieio)
+
+(defcustom xwidget-plus-follow-link-candidate-style '(("border" . "1px dashed blue")
+                                                      ("background" . "#0000ff20"))
+  "Style to apply to candidate links."
+  :type '(list (cons string string))
+  :group 'xwidget-plus)
+
+(defcustom xwidget-plus-follow-link-selected-style '(("border" . "1px dashed red")
+                                                  ("background" . "#ff000020"))
+  "Style to apply to currently selected link."
+  :type '(list (cons string string))
+  :group 'xwidget-plus)
+
+(defun xwidget-plus-follow-link-style-definition ()
+  "Return the css definitions for the follow link feature."
+  (concat (xwidget-plus-make-class "xwidget-plus-follow-link-candidate" xwidget-plus-follow-link-candidate-style)
+          (xwidget-plus-make-class "xwidget-plus-follow-link-selected" xwidget-plus-follow-link-selected-style)))
+
 
 (defconst xwidget-plus-follow-link-script (--js "
 function __xwidget_plus_follow_link_cleanup() {
@@ -36,7 +57,7 @@ function __xwidget_plus_follow_link_highlight(json, selected) {
         if (selected == id) {
             a.classList.add('xwidget-plus-follow-link-selected');
             a.scrollIntoView({behavior: 'smooth', block: 'center'});
-        } else if (ids[id]) {
+        } else if (ids.includes(id)) {
             a.classList.add('xwidget-plus-follow-link-candidate');
         }
     });
@@ -49,60 +70,177 @@ function __xwidget_plus_follow_link_action(id) {
 function __xwidget_plus_follow_link_links() {
     var r = {};
     document.querySelectorAll('a').forEach((a, i) => {
-        if (a.offsetWidth || a.offsetHeight || a.getClientRects().length)
-            r[i] = a.innerText;
+        if (a.offsetWidth || a.offsetHeight || a.getClientRects().length) {
+            if (a.innerText.match(/\\\\S/))
+                r[i] = a.innerText;
+        }
     });
     return r;
 }
 " js--))
 
-(defun xwidget-plus-follow-link-candidates ()
-  "Return the currently selected link and the candidates.
 
-The return value is a list whose first element is the selected
-link and the rest are the candidates."
+;; Completion backend class
+(defclass xwidget-plus-completion-backend () ((collection) (text)))
+
+(cl-defmethod xwidget-plus-follow-link-candidates ((backend xwidget-plus-completion-backend))
+    "Return the BACKEND selected link and the candidates.
+
+The return value is a list whose first element is the selected id
+link and the rest are the candidates ids.
+
+Return nil if the backend does not support narrowing selection list.")
+
+(cl-defmethod xwidget-plus-follow-link-read ((backend xwidget-plus-completion-backend) prompt collection action update-fn)
+  "use BACKEND to PROMPT the user for a link in COLLECTION.
+
+ACTION should be called with the resulting link.
+
+UPDATE-FN is a function that can be called when the candidates
+list is narrowed. It will highlight the link list in the
+browser.")
+
+
+;; Default backend using completing-read
+(defclass xwidget-plus-completion-backend-default (xwidget-plus-completion-backend) ())
+
+(cl-defmethod xwidget-plus-follow-link-candidates ((backend xwidget-plus-completion-backend-default))
+    "Return the BACKEND selected link and the candidates.
+
+The return value is a list whose first element is the selected id
+link and the rest are the candidates ids.
+
+Return nil if the backend does not support narrowing selection list."
+    (let* ((collection (oref backend collection))
+           (text (oref backend text))
+           (matches (seq-filter (lambda (i) (string-match-p (concat "^" (regexp-quote text)) (car i))) collection))
+           (matches (seq-map #'cdr matches)))
+      (if (= 1 (length matches))
+          matches
+        (cons nil matches))))
+
+(cl-defmethod xwidget-plus-follow-link-read ((backend xwidget-plus-completion-backend-default) prompt collection action update-fn)
+  "use BACKEND to PROMPT the user for a link in COLLECTION.
+
+ACTION should be called with the resulting link.
+
+UPDATE-FN is a function that can be called when the candidates
+list is narrowed. It will highlight the link list in the
+browser."
+  (funcall action (cdr (assoc (completing-read prompt (lambda (str pred _)
+                                                        (oset backend text str)
+                                                        (funcall update-fn)
+                                                        (try-completion str collection pred))
+                                               nil t)
+                              collection))))
+
+
+;; Ido backend using ido-completing-read
+(defclass xwidget-plus-completion-backend-ido (xwidget-plus-completion-backend) ())
+
+(cl-defmethod xwidget-plus-follow-link-candidates ((backend xwidget-plus-completion-backend-ido))
+  (let ((collection (oref backend collection)))
+    (when collection
+      (seq-map (lambda (i) (cdr (assoc i collection))) ido-matches))))
+
+(cl-defmethod xwidget-plus-follow-link-read ((backend xwidget-plus-completion-backend-ido) prompt collection action update-fn)
+  (let ((choices (seq-map #'car collection)))
+    (advice-add #'ido-set-matches :after update-fn)
+    (let ((link (cdr (assoc (ido-completing-read prompt choices nil t) collection))))
+      (oset backend collection nil)
+      (advice-remove #'ido-set-matches #'update-fn)
+      (funcall action link))))
+
+
+;; Ivy backend using completing read
+(defclass xwidget-plus-completion-backend-ivy (xwidget-plus-completion-backend) ())
+
+(cl-defmethod xwidget-plus-follow-link-candidates ((backend xwidget-plus-completion-backend-ivy))
   (with-current-buffer (ivy-state-buffer ivy-last)
-    (cons (ivy-state-current ivy-last) (ivy--filter ivy-text ivy--all-candidates))))
+    (let* ((collection (ivy-state-collection ivy-last))
+           (current (ivy-state-current ivy-last))
+           (candidates (ivy--filter ivy-text ivy--all-candidates))
+           (result (cons current candidates)))
+      (seq-map (lambda (c) (cdr (nth (get-text-property 0 'idx c) collection))) result))))
+
+(cl-defmethod xwidget-plus-follow-link-read ((backend xwidget-plus-completion-backend-ivy) prompt collection action update-fn)
+  (ivy-read "Link: " collection :require-match t :action (lambda (v) (funcall action (cdr v))) :update-fn update-fn))
+
+
+;; Helm backend
+(defclass xwidget-plus-completion-backend-helm (xwidget-plus-completion-backend) ((candidates)))
+
+(cl-defmethod xwidget-plus-follow-link-candidates ((backend xwidget-plus-completion-backend-helm))
+  (let* ((candidates (oref backend candidates))
+         (selection (helm-get-selection))
+         (selected (when selection (cdr (elt (oref backend collection) selection))))
+         (result (seq-map #'cdr candidates)))
+    (cons selected result)))
+
+(cl-defmethod xwidget-plus-follow-link-read ((backend xwidget-plus-completion-backend-helm) prompt collection action update-fn)
+  (add-hook 'helm-after-initialize-hook (lambda ()
+                                          (with-current-buffer "*helm-xwidget-plus*"
+                                            (add-hook 'helm-move-selection-after-hook update-fn nil t)))
+            nil t)
+  (helm :sources
+        (helm-make-source "Xwidget Plus" 'helm-source-sync
+          :candidates collection
+          :action action
+          :filtered-candidate-transformer (lambda (candidates _)
+                                            (oset backend candidates candidates)
+                                            (funcall update-fn)
+                                            candidates))
+        :prompt prompt
+        :buffer "*helm-xwidget-plus*"))
 
-(defun xwidget-plus-follow-link-highlight (xwidget links)
+(defvar xwidget-plus-completion-backend-instance (xwidget-plus-completion-backend))
+
+(defun xwidget-plus-follow-link-highlight (xwidget)
   "Highligh LINKS in XWIDGET buffer when updating candidates."
-  (let* ((candidates (xwidget-plus-follow-link-candidates))
-         (selected (car candidates))
-         (cands (cdr candidates))
-         (cands-id (seq-filter (lambda (v) (seq-contains-p cands (cdr v))) links))
-         (selected-id (car (rassoc selected links)))
-         (script (--js "__xwidget_plus_follow_link_highlight('%s', '%s');" js-- (json-serialize cands-id) selected-id)))
-    (xwidget-webkit-execute-script xwidget script)))
+  (let ((links (xwidget-plus-follow-link-candidates xwidget-plus-completion-backend-instance)))
+    (when links
+      (let* ((selected (car links))
+             (candidates (cdr links))
+             (script (--js "__xwidget_plus_follow_link_highlight('%s', %s);" js-- (json-serialize (vconcat candidates)) (or selected "null"))))
+        (xwidget-webkit-execute-script xwidget script)))))
 
 (defun xwidget-plus-follow-link-exit (xwidget)
   "Exit follow link mode in XWIDGET."
   (let ((script "__xwidget_plus_follow_link_cleanup();"))
     (xwidget-webkit-execute-script xwidget script)))
 
-(defun xwidget-plus-follow-link-action (xwidget links selected)
+(defun xwidget-plus-follow-link-action (xwidget selected)
   "Activate link matching SELECTED in XWIDGET LINKS."
-  (let* ((selected-id (car (rassoc selected links)))
-         (script (--js "__xwidget_plus_follow_link_action('%s');" js-- selected-id)))
+  (let ((script (--js "__xwidget_plus_follow_link_action(%s);" js-- selected)))
     (xwidget-webkit-execute-script xwidget script)))
 
+(defun xwidget-plus-follow-link-format-link (str)
+  "Format link title STR."
+  (setq str (replace-regexp-in-string "^[[:space:][:cntrl:]]+" "" str))
+  (setq str (replace-regexp-in-string "[[:space:][:cntrl:]]+$" "" str))
+  (setq str (replace-regexp-in-string "[[:cntrl:]]+" "/" str))
+  (replace-regexp-in-string "[[:space:]]+" " " str))
+
 (defun xwidget-plus-follow-link-prepare-links (links)
   "Prepare the alist of LINKS."
-  (setq links (seq-sort-by (lambda (v) (string-to-number (car v))) #'< links))
-  (setq links (seq-map (lambda (v) (cons (intern (car v)) (xwidget-plus-follow-link-format-link (cdr v))))
-                       links))
-  (seq-filter #'identity links))
+  (seq-sort-by (lambda (v) (cdr v)) #'<
+               (seq-map (lambda (v) (cons (xwidget-plus-follow-link-format-link (cdr v)) (string-to-number (car v))))
+                        links)))
 
-(defun xwidget-plus-follow-link-iv)
 (defun xwidget-plus-follow-link-callback (links)
   "Ask for a link belonging to the alist LINKS."
   (let* ((xwidget (xwidget-webkit-current-session))
          (links (xwidget-plus-follow-link-prepare-links links))
-         (choice (seq-map #'cdr links))
          link)
+    (oset xwidget-plus-completion-backend-instance collection links)
     (unwind-protect
-        (xwidget-plus-follow-link-action xwidget links
-                                         (ivy-read "Link: " choice :update-fn (apply-partially #'xwidget-plus-follow-link-highlight xwidget links)))
-      (xwidget-plus-follow-link-exit xwidget))))
+        (condition-case nil
+            (xwidget-plus-follow-link-read xwidget-plus-completion-backend-instance
+                                           "Link: " links
+                                           (apply-partially #'xwidget-plus-follow-link-action xwidget)
+                                           (apply-partially #'xwidget-plus-follow-link-highlight xwidget))
+          (quit (xwidget-plus-follow-link-exit xwidget))))
+    (oset xwidget-plus-completion-backend-instance collection nil)))
 
 ;;;###autoload
 (defun xwidget-plus-follow-link (&optional xwidget)
@@ -114,12 +252,11 @@ link and the rest are the candidates."
     (xwidget-plus-inject-script xwidget "__xwidget_plus_follow_link_script" xwidget-plus-follow-link-script)
     (xwidget-webkit-execute-script xwidget script #'xwidget-plus-follow-link-callback)))
 
-(provide 'xwidget-plus-follow-link)
-
-;;; xwidget-plus-follow-link.el ends here
-
 ;; Local Variables:
 ;; eval: (mmm-mode)
 ;; eval: (mmm-add-classes '((elisp-js :submode js-mode :face mmm-code-submode-face :delimiter-mode nil :front "--js \"" :back "\" js--")))
 ;; mmm-classes: elisp-js
 ;; End:
+
+(provide 'xwidget-plus-follow-link)
+;;; xwidget-plus-follow-link.el ends here

+ 6 - 24
xwidget-plus.el

@@ -5,7 +5,7 @@
 ;; Created: 2020-03-11
 ;; Keywords: convenience
 ;; Version: 0.1
-;; Package-Requires: ((emacs "25.3") (ivy "0.13.0"))
+;; Package-Requires: ((emacs "25.3"))
 
 ;; This file is NOT part of GNU Emacs.
 
@@ -37,26 +37,10 @@
 
 ;;
 
-
 ;;; Code:
-(require 'ivy)
-(require 'eieio)
-
-(defgroup xwidget-plus nil
-  "Augment the xwidget webkit browser."
-  :group 'convenience)
 
-(defcustom xwidget-plus-follow-link-candidate-style '(("border" . "1px dashed blue")
-                                                      ("background" . "#0000ff20"))
-  "Style to apply to candidate links."
-  :type '(list (cons string string))
-  :group 'xwidget-plus)
-
-(defcustom xwidget-plus-follow-link-selected-style '(("border" . "1px dashed red")
-                                                  ("background" . "#ff000020"))
-  "Style to apply to currently selected link."
-  :type '(list (cons string string))
-  :group 'xwidget-plus)
+(require 'xwidget-plus-common)
+(require 'xwidget-plus-follow-link)
 
 ;; Bring the window to front when summoning browse
 (defun xwidget-plus-webkit-browse-url-advise (&rest _)
@@ -64,13 +48,11 @@
     (switch-to-buffer-other-window (xwidget-buffer (xwidget-webkit-current-session))))
 (advice-add #'xwidget-webkit-browse-url :after #'xwidget-plus-webkit-browse-url-advise)
 
-
-(provide 'xwidget-link)
-
-;;; xwidget-link.el ends here
-
 ;; Local Variables:
 ;; eval: (mmm-mode)
 ;; eval: (mmm-add-classes '((elisp-js :submode js-mode :face mmm-code-submode-face :delimiter-mode nil :front "--js \"" :back "\" js--")))
 ;; mmm-classes: elisp-js
 ;; End:
+
+(provide 'xwidget-plus)
+;;; xwidget-plus.el ends here