aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--sx-interaction.el12
-rw-r--r--sx-load.el1
-rw-r--r--sx-question-list.el1
-rw-r--r--sx-question-mode.el1
-rw-r--r--sx-search.el112
-rw-r--r--sx.el25
-rw-r--r--test/test-search.el53
-rw-r--r--test/tests.el2
8 files changed, 203 insertions, 4 deletions
diff --git a/sx-interaction.el b/sx-interaction.el
index b2673c8..3877035 100644
--- a/sx-interaction.el
+++ b/sx-interaction.el
@@ -364,6 +364,18 @@ from context at point."
(sx-site-get-api-tokens) nil t nil nil
default)))
+(defun sx--maybe-site-prompt (arg)
+ "Get a site token conditionally in an interactive context.
+If ARG is non-nil, use `sx--interactive-site-prompt'.
+Otherwise, use `sx-question-list--site' if non-nil.
+If nil, use `sx--interactive-site-prompt' anyway."
+ ;; This could eventually be generalized into (sx--maybe-prompt
+ ;; prefix-arg value-if-non-nil #'prompt-function).
+ (if arg
+ (sx--interactive-site-prompt)
+ (or sx-question-list--site
+ (sx--interactive-site-prompt))))
+
;;;###autoload
(defun sx-ask (site)
"Start composing a question for SITE.
diff --git a/sx-load.el b/sx-load.el
index 481dba3..e7cb6b0 100644
--- a/sx-load.el
+++ b/sx-load.el
@@ -41,6 +41,7 @@
sx-question-mode
sx-question-print
sx-request
+ sx-search
sx-site
sx-tab
))
diff --git a/sx-question-list.el b/sx-question-list.el
index a5cd005..cf849db 100644
--- a/sx-question-list.el
+++ b/sx-question-list.el
@@ -317,6 +317,7 @@ into consideration.
(":" sx-question-list-switch-site)
("t" sx-tab-switch)
("a" sx-ask)
+ ("s" sx-search)
("v" sx-visit-externally)
("u" sx-toggle-upvote)
("d" sx-toggle-downvote)
diff --git a/sx-question-mode.el b/sx-question-mode.el
index 7d61167..721f935 100644
--- a/sx-question-mode.el
+++ b/sx-question-mode.el
@@ -231,6 +231,7 @@ Letters do not insert themselves; instead, they are commands.
(" " scroll-up-command)
("a" sx-answer)
("e" sx-edit)
+ ("s" sx-search)
(,(kbd "S-SPC") scroll-down-command)
([backspace] scroll-down-command)
([tab] forward-button)
diff --git a/sx-search.el b/sx-search.el
new file mode 100644
index 0000000..2633da9
--- /dev/null
+++ b/sx-search.el
@@ -0,0 +1,112 @@
+;;; sx-search.el --- Searching for questions. -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2014 Artur Malabarba
+
+;; Author: Artur Malabarba <bruce.connor.am@gmail.com>
+
+;; 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:
+
+;; Implements sarch functionality. The basic function is
+;; `sx-search-get-questions', which returns an array of questions
+;; according to a search term.
+;;
+;; This also defines a user-level command, `sx-search', which is an
+;; interactive wrapper around `sx-search-get-questions' and
+;; `sx-question-list-mode'.
+
+
+;;; Code:
+
+(require 'sx)
+(require 'sx-question-list)
+
+(defvar sx-search--query-history nil
+ "Query history for interactive prompts.")
+
+(defvar sx-search--tag-history nil
+ "Tags history for interactive prompts.")
+
+
+;;; Basic function
+(defun sx-search-get-questions (site page query &optional tags excluded-tags keywords)
+ "Like `sx-question-get-questions', but restrict results by a search.
+
+Perform search on SITE. PAGE is an integer indicating which page
+of results to return. QUERY, TAGS, and EXCLUDED-TAGS restrict the
+possible returned questions as per `sx-search'.
+
+Either QUERY or TAGS must be non-nil, or the search will
+fail. EXCLUDED-TAGS is only is used if TAGS is also provided.
+
+KEYWORDS is passed to `sx-method-call'."
+ (sx-method-call 'search
+ :keywords `((page . ,page)
+ (sort . activity)
+ (intitle . ,query)
+ (tagged . ,tags)
+ (nottagged . ,excluded-tags)
+ ,@keywords)
+ :site site
+ :auth t
+ :filter sx-browse-filter))
+
+
+;;; User command
+(defun sx-search (site query &optional tags excluded-tags)
+ "Display search on SITE for question titles containing QUERY.
+When TAGS is given, it is a lists of tags, one of which must
+match. When EXCLUDED-TAGS is given, it is a list of tags, none
+of which is allowed to match.
+
+Interactively, the user is asked for SITE and QUERY. With a
+prefix argument, the user is asked for everything."
+ (interactive
+ (let ((site (sx--maybe-site-prompt current-prefix-arg))
+ (query (read-string
+ (format "Query (%s): "
+ (if current-prefix-arg "optional" "mandatory"))
+ ""
+ 'sx-search--query-history))
+ tags excluded-tags)
+ (when (string= query "")
+ (setq query nil))
+ (when current-prefix-arg
+ (setq tags (sx--multiple-read
+ (format "Tags (%s)"
+ (if query "optional" "mandatory"))
+ 'sx-search--tag-history))
+ (when (and (not query) (string= "" tags))
+ (sx-user-error "Must supply either QUERY or TAGS"))
+ (setq excluded-tags
+ (sx--multiple-read
+ "Excluded tags (optional)" 'sx-search--tag-history)))
+ (list site query tags excluded-tags)))
+
+ ;; Here starts the actual function
+ (sx-initialize)
+ (with-current-buffer (get-buffer-create "*sx-search-result*")
+ (sx-question-list-mode)
+ (setq sx-question-list--next-page-function
+ (lambda (page)
+ (sx-search-get-questions
+ sx-question-list--site page
+ query tags excluded-tags)))
+ (setq sx-question-list--site site)
+ (sx-question-list-refresh 'redisplay)
+ (switch-to-buffer (current-buffer))))
+
+(provide 'sx-search)
+;;; sx-search.el ends here
diff --git a/sx.el b/sx.el
index e2ea914..62484b7 100644
--- a/sx.el
+++ b/sx.el
@@ -74,7 +74,7 @@ DATA can also be the link itself."
"\\1\\2" link))))
(defun sx--ensure-site (data)
- "Add a `site' property to DATA if it doesn't have one. Return DATA.
+ "Add a `site' property to DATA if it doesn't have one. Return DATA.
DATA can be a question, answer, comment, or user (or any object
with a `link' property)."
(when data
@@ -137,7 +137,8 @@ with a `link' property)."
result))
(defmacro sx-assoc-let (alist &rest body)
- "Identical to `let-alist', except `.site' has a special meaning.
+ "Use ALIST with `let-alist' to execute BODY.
+`.site_par' has a special meaning, thanks to `sx--ensure-site'.
If ALIST doesn't have a `site' property, one is created using the
`link' property."
(declare (indent 1) (debug t))
@@ -195,6 +196,24 @@ All ARGS are passed to `completing-read' or `ido-completing-read'."
(apply (if ido-mode #'ido-completing-read #'completing-read)
args))
+(defun sx--multiple-read (prompt hist-var)
+ "Interactively query the user for a list of strings.
+Call `read-string' multiple times, until the input is empty.
+
+PROMPT is a string displayed to the user and should not end with
+a space nor a colon. HIST-VAR is a quoted symbol, indicating a
+list in which to store input history."
+ (let (list input)
+ (while (not (string=
+ ""
+ (setq input (read-string
+ (concat prompt " ["
+ (mapconcat #'identity list ",")
+ "]: ")
+ "" hist-var))))
+ (push input list))
+ list))
+
(defmacro sx-sorted-insert-skip-first (newelt list &optional predicate)
"Inserted NEWELT into LIST sorted by PREDICATE.
This is designed for the (site id id ...) lists. So the first car
@@ -322,7 +341,7 @@ Return the result of BODY."
("ĥ" . "h")
("ĵ" . "j")
("^[:ascii:]" . ""))
- "List of replacements to use for non-ascii characters
+ "List of replacements to use for non-ascii characters.
Used to convert user names into @mentions.")
(defun sx--user-@name (user)
diff --git a/test/test-search.el b/test/test-search.el
new file mode 100644
index 0000000..72dbcdc
--- /dev/null
+++ b/test/test-search.el
@@ -0,0 +1,53 @@
+(defmacro test-with-bogus-string (cell &rest body)
+ "Let-bind a bogus string to CELL and execute BODY."
+ (declare (indent 1))
+ `(let ((,cell "E7631BCF-A94B-4507-8F0C-02CFB3207F55"))
+ ,@body))
+
+
+(ert-deftest test-search-basic ()
+ "Test basic search functionality"
+ (should
+ (sx-search-get-questions
+ "emacs" 1 "emacs")))
+
+(ert-deftest test-search-empty ()
+ "Test bogus search returns empty vector"
+ (test-with-bogus-string query
+ (should
+ (equal
+ []
+ (sx-search-get-questions "emacs" 1 query)))))
+
+(ert-deftest test-search-invalid ()
+ "Test invalid search"
+ (should-error
+ ;; @todo: test the interactive call
+ (sx-search
+ "emacs" nil nil ["emacs"])))
+
+(ert-deftest test-search-full-page ()
+ "Test retrieval of the full search page"
+ (should
+ (= 30 (length (sx-search-get-questions
+ "stackoverflow" 1 "jquery")))))
+
+(ert-deftest test-search-exclude-tags ()
+ "Test excluding tags from a search"
+ (should
+ (cl-every
+ (lambda (p)
+ (sx-assoc-let p
+ (not (member "org-export" .tags))))
+ (sx-search-get-questions
+ "emacs" 1 nil "org-mode" "org-export")))
+ (should
+ (cl-every
+ (lambda (p)
+ (sx-assoc-let p
+ (not (or (member "org-export" .tags)
+ (member "org-agenda" .tags)))))
+ (sx-search-get-questions
+ "emacs" 1 nil "org-mode"
+ ["org-export" "org-agenda"]))))
+
diff --git a/test/tests.el b/test/tests.el
index 53e053f..d06c0ff 100644
--- a/test/tests.el
+++ b/test/tests.el
@@ -58,4 +58,4 @@
(apply #'message message args)))
(mapc #'sx-load-test
- '(api macros printing util))
+ '(api macros printing util search))