branch: externals/ivy commit cf418815b3f7848af9cd3887c2231a21216a32c3 Author: Basil L. Contovounesios <ba...@contovou.net> Commit: Basil L. Contovounesios <ba...@contovou.net>
Fix counsel-search decoding and docs Prefer 'plz' from GNU ELPA as an alternative to 'request' from NonGNU ELPA, and json.c over json.el. Sadly, neither HTTP library handles Content-Type appropriately (and Google returns Latin-1 where json.c expects UTF-8), so decode manually. * counsel.el (counsel-search-engine): Add option descriptions. (counsel-search-engines-alist): Consistently prefer HTTPS URLs. List default engine first. Document engine parameters. (counsel--search-request-data-google): Document and simplify. (counsel--search-request-data-ddg): Add docstring. (counsel--native-json, counsel--search-backend) (counsel--search-backends): New variables. (counsel--search-update, counsel--search-plz) (counsel--search-request): New functions. (counsel-search-function): Use them for supporting different JSON and HTTP libraries. (counsel-search-action): Improve docstring. (counsel-search): Load JSON and HTTP libraries as needed. Improve docstring. --- counsel.el | 154 ++++++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 122 insertions(+), 32 deletions(-) diff --git a/counsel.el b/counsel.el index 0cf96fd513..1b79af178a 100644 --- a/counsel.el +++ b/counsel.el @@ -6965,64 +6965,154 @@ Additional actions:\\<ivy-minibuffer-map> :caller 'counsel-major)) ;;** `counsel-search' -(declare-function request "ext:request") (defcustom counsel-search-engine 'ddg "The search engine choice in `counsel-search-engines-alist'." :type '(choice - (const ddg) - (const google))) + (const :tag "DuckDuckGo" ddg) + (const :tag "Google" google))) (defcustom counsel-search-engines-alist - '((google - "http://suggestqueries.google.com/complete/search" - "https://www.google.com/search?q=" - counsel--search-request-data-google) - (ddg + '((ddg "https://duckduckgo.com/ac/" "https://duckduckgo.com/html/?q=" - counsel--search-request-data-ddg)) - "Search engine parameters for `counsel-search'." + counsel--search-request-data-ddg) + (google + "https://suggestqueries.google.com/complete/search" + "https://www.google.com/search?q=" + counsel--search-request-data-google)) + "List of search engine parameters for `counsel-search'. +Each element is of the form (SYMBOL SUGGEST BROWSE EXTRACT), where: +SYMBOL identifies the search engine, as per `counsel-search-engine'. +SUGGEST is the URL to query for suggestions. +BROWSE is the URL prefix for visiting the selected result. +EXTRACT is a function that takes the object parsed from the SUGGEST + endpoint and transforms it into a set of Ivy candidates." + :package-version '(counsel . "0.16.0") :type '(alist :key-type symbol :value-type (list string string function))) (defun counsel--search-request-data-google (data) - (mapcar #'identity (aref data 1))) + "Extract Google suggestions from parsed JSON DATA. +Expects input of the form [\"a\" [\"ab\" \"ac\"] ...]." + (append (aref data 1) ())) (defun counsel--search-request-data-ddg (data) + "Extract DuckDuckGo suggestions from parsed JSON DATA. +Expects input of the form [((phrase . \"ab\")) ...]." (mapcar #'cdar data)) +(defvar counsel--native-json) +(put 'counsel--native-json 'variable-documentation + "Non-nil if Emacs supports JSON natively, or void.") + +(defun counsel--search-update (extract str type) + "Call EXTRACT on JSON STR of Content-TYPE." + (unless (fboundp 'mail-header-parse-content-type) + (require 'mail-parse)) + (declare-function json-parse-string "json.c") + (declare-function json-read-from-string "json") + (declare-function mail-content-type-get "mail-parse") + (declare-function mail-header-parse-content-type "mail-parse") + (let* ((ct (and type (mail-header-parse-content-type type))) + (coding (coding-system-from-name (mail-content-type-get ct 'charset)))) + (when coding + (setq str (decode-coding-string str coding t)))) + (let ((obj (if counsel--native-json + (json-parse-string str :object-type 'alist) + (defvar json-array-type) + (defvar json-object-type) + (let ((json-array-type 'vector) + (json-object-type 'alist)) + (json-read-from-string str))))) + (ivy-update-candidates (funcall extract obj)))) + +(defun counsel--search-plz (url extract) + "Fetch URL with `plz' and EXTRACT its JSON payload." + (declare-function plz "ext:plz") + (declare-function plz-response-body "ext:plz") + (declare-function plz-response-headers "ext:plz") + ;; Doesn't handle Content-Type, so defer decoding+parsing until :then. + ;; (See URL `https://github.com/alphapapa/plz.el/pull/66'.) + ;; Ask for a `plz-response' object because it already contains the parsed + ;; headers (though just widening the response buffer could be quicker). + (plz 'get url :as 'response :decode nil :noquery t + :then (lambda (response) + (let* ((heads (plz-response-headers response)) + (body (plz-response-body response)) + (ct (cdr (assq 'content-type heads)))) + (counsel--search-update extract body ct))))) + +(defun counsel--search-request (url extract) + "Fetch URL with `request' and EXTRACT its JSON payload." + (declare-function request "ext:request") + (declare-function request-response-header "ext:request") + ;; Doesn't handle Content-Type (expects coding system a priori), + ;; so defer decoding+parsing until :success. + (request url :type "GET" + :success (cl-function + (lambda (&key data response &allow-other-keys) + (let ((ct (request-response-header response "content-type"))) + (counsel--search-update extract data ct)))))) + +(defvar counsel--search-backend) +(put 'counsel--search-backend 'variable-documentation + "Feature symbol indicating available HTTP library, or void. +Valid values are the keys of `counsel--search-backends'.") + +(defvar counsel--search-backends + `((plz ,#'counsel--search-plz) + (request ,#'counsel--search-request)) + "List of (BACKEND GETTER) for `counsel-search'. +BACKEND is a feature symbol like `counsel--search-backend'. +GETTER is a function taking a URL and an EXTRACT function as in + `counsel-search-engines-alist'.") + (defun counsel-search-function (input) "Create a request to a search engine with INPUT. Return 0 tells `ivy--exhibit' not to update the minibuffer. We update it in the callback with `ivy-update-candidates'." (or (ivy-more-chars) - (let ((engine (cdr (assoc counsel-search-engine counsel-search-engines-alist)))) - (request - (nth 0 engine) - :type "GET" - :params (list - (cons "client" "firefox") - (cons "q" input)) - :parser 'json-read - :success (cl-function - (lambda (&key data &allow-other-keys) - (ivy-update-candidates - (funcall (nth 2 engine) data))))) + (let* ((backend (assq counsel--search-backend counsel--search-backends)) + (engine (assq counsel-search-engine counsel-search-engines-alist)) + (suggest (nth 1 engine)) + (extract (nth 3 engine)) + (url (concat suggest (if (ivy--string-search "?" suggest) "&" "?") + ;; FIXME: `client' needed only for `google'? + (url-build-query-string `(("client" "firefox") + ("q" ,input)))))) + ;; Do we need to cancel requests already in flight? + (funcall (nth 1 backend) url extract) 0))) -(defun counsel-search-action (x) - "Search for X." - (browse-url - (concat - (nth 2 (assoc counsel-search-engine counsel-search-engines-alist)) - (url-hexify-string x)))) +(defun counsel-search-action (candidate) + "Browse the search results for `counsel-search' CANDIDATE." + (let ((engine (assq counsel-search-engine counsel-search-engines-alist))) + (browse-url (concat (nth 2 engine) (url-hexify-string candidate))))) (defun counsel-search () - "Ivy interface for dynamically querying a search engine." + "Ivy interface for querying a search engine. +Dynamically displays search suggestions for the current input. +The user options `counsel-search-engine' and +`counsel-search-engines-alist' determine the engine." (interactive) - (require 'request) - (require 'json) + (unless (boundp 'counsel--search-backend) + (setq counsel--search-backend + ;; `plz' is on GNU ELPA; `request' on NonGNU ELPA. + (or (require 'plz nil t) + (require 'request nil t) + (user-error + "Required package `plz' (or `request') not installed")))) + ;; - Emacs 27: optional native JSON support. + ;; - Emacs 28: `json-available-p'. + ;; - Emacs 30: unconditional native JSON support. + ;; That means the following sets `counsel--native-json' to nil even for + ;; Emacs 27 with native JSON support, in the interest of simplicity. + (or (boundp 'counsel--native-json) + (setq counsel--native-json + (and (fboundp 'json-available-p) + (json-available-p))) + (require 'json)) (ivy-read "search: " #'counsel-search-function :action #'counsel-search-action :dynamic-collection t