aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--sx-interaction.el41
-rw-r--r--sx-method.el3
-rw-r--r--sx-question-list.el105
-rw-r--r--sx-question-mode.el20
-rw-r--r--sx-question-print.el13
-rw-r--r--sx-question.el80
-rw-r--r--sx-request.el16
-rw-r--r--sx-tab.el103
8 files changed, 271 insertions, 110 deletions
diff --git a/sx-interaction.el b/sx-interaction.el
index d0c4c47..9a5bbcb 100644
--- a/sx-interaction.el
+++ b/sx-interaction.el
@@ -46,13 +46,20 @@
;;; Using data in buffer
-(defun sx--data-here ()
- "Get the text property `sx--data-here'."
- (or (get-char-property (point) 'sx--data-here)
+(defun sx--data-here (&optional noerror)
+ "Get data for the question or other object under point.
+If NOERROR is non-nil, don't throw an error on failure.
+
+This looks at the text property `sx--data-here'. If it's not set,
+it looks at a few other reasonable variables. If those fail too,
+it throws an error."
+ (or (get-text-property (point) 'sx--data-here)
(and (derived-mode-p 'sx-question-list-mode)
(tabulated-list-get-id))
(and (derived-mode-p 'sx-question-mode)
- sx-question-mode--data)))
+ sx-question-mode--data)
+ (and (null noerror)
+ (error "No question data found here"))))
(defun sx--maybe-update-display (&optional buffer)
"Refresh whatever is displayed in BUFFER or the current buffer.
@@ -70,6 +77,8 @@ Only fields contained in TO are copied."
(setcar to (car from))
(setcdr to (cdr from)))
+
+;;; Visiting
(defun sx-visit (data &optional copy-as-kill)
"Visit DATA in a web browser.
DATA can be a question, answer, or comment. Interactively, it is
@@ -92,6 +101,30 @@ If DATA is a question, also mark it as read."
(sx-question--mark-read data)
(sx--maybe-update-display))))
+
+;;; Displaying
+(defun sx-display-question (&optional data focus window)
+ "Display question given by DATA, on WINDOW.
+When DATA is nil, display question under point. When FOCUS is
+non-nil (the default when called interactively), also focus the
+relevant window.
+
+If WINDOW nil, the window is decided by
+`sx-question-mode-display-buffer-function'."
+ (interactive (list (sx--data-here) t))
+ (when (sx-question--mark-read data)
+ (sx--maybe-update-display))
+ ;; Display the question.
+ (setq window
+ (get-buffer-window
+ (sx-question-mode--display data window)))
+ (when focus
+ (if (window-live-p window)
+ (select-window window)
+ (switch-to-buffer sx-question-mode--buffer))))
+
+
+;;; Voting
(defun sx-toggle-upvote (data)
"Apply or remove upvote from DATA.
DATA can be a question, answer, or comment. Interactively, it is
diff --git a/sx-method.el b/sx-method.el
index 1b20cbf..83455b8 100644
--- a/sx-method.el
+++ b/sx-method.el
@@ -82,7 +82,8 @@ Return the entire response as a complex alist."
(prog1
(format "?site=%s" site)
(setq site nil)))))
- (call #'sx-request-make))
+ (call #'sx-request-make)
+ parameters)
(lwarn "sx-call-method" :debug "A: %S T: %S. M: %S,%s. F: %S" (equal 'warn auth)
access-token method-auth full-method filter-auth)
(unless access-token
diff --git a/sx-question-list.el b/sx-question-list.el
index 2bfcce0..628de30 100644
--- a/sx-question-list.el
+++ b/sx-question-list.el
@@ -145,9 +145,6 @@ Also see `sx-question-list-refresh'."
.title
'face (if (sx-question--read-p question-data)
'sx-question-list-read-question
- ;; Increment `sx-question-list--unread-count' for
- ;; the mode-line.
- (cl-incf sx-question-list--unread-count)
'sx-question-list-unread-question))
(propertize " " 'display "\n ")
(propertize favorite 'face 'sx-question-list-favorite)
@@ -299,13 +296,14 @@ into consideration.
("K" sx-question-list-previous-far)
("g" sx-question-list-refresh)
(":" sx-question-list-switch-site)
+ ("t" sx-question-list-switch-tab)
("a" sx-ask)
("v" sx-visit)
("u" sx-toggle-upvote)
("d" sx-toggle-downvote)
("h" sx-question-list-hide)
("m" sx-question-list-mark-read)
- ([?\r] sx-question-list-display-question)))
+ ([?\r] sx-display-question)))
(defun sx-question-list-hide (data)
"Hide question under point.
@@ -335,10 +333,6 @@ Non-interactively, DATA is a question alist."
;; "Unanswered", etc.
"Variable describing current tab being viewed.")
-(defvar sx-question-list--unread-count 0
- "Holds the number of unread questions in the current buffer.")
-(make-variable-buffer-local 'sx-question-list--unread-count)
-
(defvar sx-question-list--total-count 0
"Holds the total number of questions in the current buffer.")
(make-variable-buffer-local 'sx-question-list--total-count)
@@ -352,7 +346,7 @@ Non-interactively, DATA is a question alist."
" ["
"Unread: "
(:propertize
- (:eval (int-to-string sx-question-list--unread-count))
+ (:eval (sx-question-list--unread-count))
face mode-line-buffer-id)
", "
"Total: "
@@ -362,6 +356,12 @@ Non-interactively, DATA is a question alist."
"] ")
"Mode-line construct to use in question-list buffers.")
+(defun sx-question-list--unread-count ()
+ "Number of unread questions in current dataset, as a string."
+ (int-to-string
+ (cl-count-if-not
+ #'sx-question--read-p sx-question-list--dataset)))
+
(defun sx-question-list--update-mode-line ()
"Fill the mode-line with useful information."
;; All the data we need is right in the buffer.
@@ -381,7 +381,6 @@ If the prefix argument NO-UPDATE is nil, query StackExchange for
a new list before redisplaying."
(interactive "p\nP")
;; Reset the mode-line unread count (we rebuild it here).
- (setq sx-question-list--unread-count 0)
(unless no-update
(setq sx-question-list--pages-so-far 1))
(let* ((question-list
@@ -424,7 +423,37 @@ Displayed in `sx-question-mode--window', replacing any question
that may currently be there."
(interactive "p")
(sx-question-list-next n)
- (sx-question-list-display-question))
+ (sx-display-question
+ (tabulated-list-get-id)
+ nil
+ (sx-question-list--create-question-window)))
+
+(defun sx-question-list--create-question-window ()
+ "Create or find a window where a question can be displayed.
+
+If any current window displays a question, that window is
+returned. If none do, a new one is created such that the
+question-list window remains `sx-question-list-height' lines
+high (if possible)."
+ (or (sx-question-mode--get-window)
+ ;; Create a proper window.
+ (let ((window
+ (condition-case er
+ (split-window (selected-window) sx-question-list-height 'below)
+ (error
+ ;; If the window is too small to split, use any one.
+ (if (string-match
+ "Window #<window .*> too small for splitting"
+ (car (cdr-safe er)))
+ (next-window)
+ (error (cdr er)))))))
+ ;; Configure the window to be closed on `q'.
+ (set-window-prev-buffers window nil)
+ (set-window-parameter
+ window 'quit-restore
+ ;; See (info "(elisp) Window Parameters")
+ `(window window ,(selected-window) ,sx-question-mode--buffer))
+ window)))
(defun sx-question-list-next (n)
"Move cursor down N questions.
@@ -436,7 +465,21 @@ This does not update `sx-question-mode--window'."
;; If we were trying to move forward, but we hit the end.
(when (eobp)
;; Try to get more questions.
- (sx-question-list-next-page))))
+ (sx-question-list-next-page))
+ (sx-question-list--ensure-line-good-line-position)))
+
+(defun sx-question-list--ensure-line-good-line-position ()
+ "Scroll window such that current line is a good place.
+Check if we're at least 6 lines from the bottom. Scroll up if
+we're not. Do the same for 3 lines from the top."
+ ;; At least one entry below us.
+ (let ((lines-to-bottom (count-screen-lines (point) (window-end))))
+ (unless (>= lines-to-bottom 6)
+ (recenter (- 6))))
+ ;; At least one entry above us.
+ (let ((lines-to-top (count-screen-lines (point) (window-start))))
+ (unless (>= lines-to-top 3)
+ (recenter 3))))
(defun sx-question-list-next-page ()
"Fetch and display the next page of questions."
@@ -486,44 +529,6 @@ This does not update `sx-question-mode--window'."
(interactive "p")
(sx-question-list-next-far (- n)))
-(defun sx-question-list-display-question (&optional data focus)
- "Display question given by DATA.
-When DATA is nil, display question under point. When FOCUS is
-non-nil (the default when called interactively), also focus the
-relevant window."
- (interactive '(nil t))
- (unless data (setq data (tabulated-list-get-id)))
- (unless data (error "No question here!"))
- (unless (sx-question--read-p data)
- (cl-decf sx-question-list--unread-count)
- (sx-question--mark-read data)
- (sx-question-list-refresh 'redisplay 'no-update))
- (unless (and (window-live-p sx-question-mode--window)
- (null (equal sx-question-mode--window (selected-window))))
- (setq sx-question-mode--window
- (condition-case er
- (split-window (selected-window) sx-question-list-height 'below)
- (error
- ;; If the window is too small to split, use current one.
- (if (string-match
- "Window #<window .*> too small for splitting"
- (car (cdr-safe er)))
- nil
- (error (cdr er)))))))
- ;; Display the question.
- (sx-question-mode--display data sx-question-mode--window)
- ;; Configure the window to be closed on `q'.
- (set-window-prev-buffers sx-question-mode--window nil)
- (set-window-parameter
- sx-question-mode--window
- 'quit-restore
- ;; See (info "(elisp) Window Parameters")
- `(window window ,(selected-window) ,sx-question-mode--buffer))
- (when focus
- (if sx-question-mode--window
- (select-window sx-question-mode--window)
- (switch-to-buffer sx-question-mode--buffer))))
-
(defun sx-question-list-switch-site (site)
"Switch the current site to SITE and display its questions.
Use `ido-completing-read' if variable `ido-mode' is active.
diff --git a/sx-question-mode.el b/sx-question-mode.el
index b685ea7..bccb658 100644
--- a/sx-question-mode.el
+++ b/sx-question-mode.el
@@ -30,8 +30,13 @@
;;; Displaying a question
-(defvar sx-question-mode--window nil
- "Window where the content of questions is displayed.")
+(defcustom sx-question-mode-display-buffer-function #'switch-to-buffer
+ "Function used to display the question buffer.
+Called, for instance, when hitting \\<sx-question-list-mode-map>`\\[sx-question-list-display-question]' on an entry in the
+question list.
+This is not used when navigating the question list with `\\[sx-question-list-view-next]."
+ :type 'function
+ :group 'sx-question-mode)
(defvar sx-question-mode--buffer nil
"Buffer being used to display questions.")
@@ -39,6 +44,14 @@
(defvar sx-question-mode--data nil
"The data of the question being displayed.")
+(defun sx-question-mode--get-window ()
+ "Return a window displaying a question, or nil."
+ (car-safe
+ (cl-member-if
+ (lambda (x) (with-selected-window x
+ (derived-mode-p 'sx-question-mode)))
+ (window-list nil 'never nil))))
+
(defun sx-question-mode--display (data &optional window)
"Display question given by DATA on WINDOW.
If WINDOW is nil, use selected one.
@@ -71,7 +84,8 @@ If WINDOW is given, use that to display the buffer."
;; No window, but the buffer is already being displayed somewhere.
((get-buffer-window sx-question-mode--buffer 'visible))
;; Neither, so we create the window.
- (t (switch-to-buffer sx-question-mode--buffer)))
+ (t (funcall sx-question-mode-display-buffer-function
+ sx-question-mode--buffer)))
sx-question-mode--buffer)
diff --git a/sx-question-print.el b/sx-question-print.el
index f206f56..eb79a7a 100644
--- a/sx-question-print.el
+++ b/sx-question-print.el
@@ -43,6 +43,11 @@
;;; Faces and Variables
+(defcustom sx-question-mode-deleted-user
+ '((display_name . "(deleted user)"))
+ "The structure used to represent a deleted account."
+ :type '(alist :options ((display_name string)))
+ :group 'sx-question-mode)
(defface sx-question-mode-header
'((t :inherit font-lock-variable-name-face))
@@ -179,7 +184,10 @@ QUESTION must be a data structure returned by `json-read'."
(mapc #'sx-question-mode--print-section .answers))
(insert "\n\n ")
(insert-text-button "Write an Answer" :type 'sx-button-answer)
- ;; Reposition
+ ;; Display weird chars correctly
+ (set-buffer-multibyte nil)
+ (set-buffer-multibyte t)
+ ;; Go up
(goto-char (point-min))
(sx-question-mode-next-section))
@@ -213,7 +221,8 @@ DATA can represent a question or an answer."
(when .last_edit_date
(format sx-question-mode-last-edit-format
(sx-time-since .last_edit_date)
- (sx-question-mode--propertize-display-name .last_editor))))
+ (sx-question-mode--propertize-display-name
+ (or .last_editor sx-question-mode-deleted-user)))))
'sx-question-mode-date)
(sx-question-mode--insert-header
sx-question-mode-header-score
diff --git a/sx-question.el b/sx-question.el
index 01ba030..c4b2445 100644
--- a/sx-question.el
+++ b/sx-question.el
@@ -26,15 +26,17 @@
(require 'sx-filter)
(require 'sx-method)
-(defun sx-question-get-questions (site &optional page)
+(defun sx-question-get-questions (site &optional page keywords)
"Get SITE questions. Return page PAGE (the first if nil).
Return a list of question. Each question is an alist of
properties returned by the API with an added (site SITE)
property.
+KEYWORDS are added to the method call along with PAGE.
+
`sx-method-call' is used with `sx-browse-filter'."
(sx-method-call 'questions
- :keywords `((page . ,page))
+ :keywords `((page . ,page) ,@keywords)
:site site
:auth t
:filter sx-browse-filter))
@@ -86,27 +88,32 @@ See `sx-question--user-read-list'."
(defun sx-question--mark-read (question)
"Mark QUESTION as being read until it is updated again.
+Returns nil if question (in its current state) was already marked
+read, i.e., if it was `sx-question--read-p'.
See `sx-question--user-read-list'."
- (sx-assoc-let question
- (sx-question--ensure-read-list .site)
- (let ((site-cell (assoc .site sx-question--user-read-list))
- (q-cell (cons .question_id .last_activity_date))
- cell)
- (cond
- ;; First question from this site.
- ((null site-cell)
- (push (list .site q-cell) sx-question--user-read-list))
- ;; Question already has an older time.
- ((setq cell (assoc .question_id site-cell))
- (setcdr cell .last_activity_date))
- ;; Question wasn't present.
- (t
- (sx-sorted-insert-skip-first
- q-cell site-cell (lambda (x y) (> (car x) (car y))))))))
- ;; Save the results.
- ;; @TODO This causes a small lag on `j' and `k' as the list gets
- ;; large. Should we do this on a timer?
- (sx-cache-set 'read-questions sx-question--user-read-list))
+ (prog1
+ (sx-assoc-let question
+ (sx-question--ensure-read-list .site)
+ (let ((site-cell (assoc .site sx-question--user-read-list))
+ (q-cell (cons .question_id .last_activity_date))
+ cell)
+ (cond
+ ;; First question from this site.
+ ((null site-cell)
+ (push (list .site q-cell) sx-question--user-read-list))
+ ;; Question already present.
+ ((setq cell (assoc .question_id site-cell))
+ ;; Current version is newer than cached version.
+ (when (> .last_activity_date (cdr cell))
+ (setcdr cell .last_activity_date)))
+ ;; Question wasn't present.
+ (t
+ (sx-sorted-insert-skip-first
+ q-cell site-cell (lambda (x y) (> (car x) (car y))))))))
+ ;; Save the results.
+ ;; @TODO This causes a small lag on `j' and `k' as the list gets
+ ;; large. Should we do this on a timer?
+ (sx-cache-set 'read-questions sx-question--user-read-list)))
;;;; Hidden
@@ -134,20 +141,21 @@ If no cache exists for it, initialize one with SITE."
(defun sx-question--mark-hidden (question)
"Mark QUESTION as being hidden."
- (let ((site-cell (assoc .site sx-question--user-hidden-list))
- cell)
- ;; If question already hidden, do nothing.
- (unless (memq .question_id site-cell)
- ;; First question from this site.
- (push (list .site .question_id) sx-question--user-hidden-list)
- ;; Question wasn't present.
- ;; Add it in, but make sure it's sorted (just in case we need
- ;; it later).
- (sx-sorted-insert-skip-first .question_id site-cell >)
- ;; This causes a small lag on `j' and `k' as the list gets large.
- ;; Should we do this on a timer?
- ;; Save the results.
- (sx-cache-set 'hidden-questions sx-question--user-hidden-list))))
+ (sx-assoc-let question
+ (let ((site-cell (assoc .site sx-question--user-hidden-list))
+ cell)
+ ;; If question already hidden, do nothing.
+ (unless (memq .question_id site-cell)
+ ;; First question from this site.
+ (push (list .site .question_id) sx-question--user-hidden-list)
+ ;; Question wasn't present.
+ ;; Add it in, but make sure it's sorted (just in case we need
+ ;; it later).
+ (sx-sorted-insert-skip-first .question_id site-cell >)
+ ;; This causes a small lag on `j' and `k' as the list gets large.
+ ;; Should we do this on a timer?
+ ;; Save the results.
+ (sx-cache-set 'hidden-questions sx-question--user-hidden-list)))))
;;;; Other data
diff --git a/sx-request.el b/sx-request.el
index a98af5a..6be363d 100644
--- a/sx-request.el
+++ b/sx-request.el
@@ -91,9 +91,8 @@ number of requests left every time it finishes a call."
;;; Making Requests
-(defun sx-request-make
- (method &optional args request-method)
- "Make a request to the API, executing METHOD with ARGS.
+(defun sx-request-make (method &optional args request-method)
+ "Make a request to the API, executing METHOD with ARGS.
You should almost certainly be using `sx-method-call' instead of
this function. REQUEST-METHOD is one of `GET' (default) or `POST'.
@@ -116,8 +115,7 @@ then read with `json-read-from-string'.
the main content of the response is returned."
(let* ((url-automatic-caching t)
(url-inhibit-uncompression t)
- (url-request-data (sx-request--build-keyword-arguments args
- nil))
+ (url-request-data (sx-request--build-keyword-arguments args nil))
(request-url (concat sx-request-api-root method))
(url-request-method request-method)
(url-request-extra-headers
@@ -168,15 +166,11 @@ Currently returns nil."
;;; Support Functions
-
-(defun sx-request--build-keyword-arguments (alist &optional
- kv-sep need-auth)
+(defun sx-request--build-keyword-arguments (alist &optional kv-sep)
"Format ALIST as a key-value list joined with KV-SEP.
If authentication is needed, include it also or error if it is
not available.
-If NEED-AUTH is non-nil, authentication is required.
-
Build a \"key=value&key=value&...\"-style string with the elements
of ALIST. If any value in the alist is nil, that pair will not
be included in the return. If you wish to pass a notion of
@@ -185,7 +179,7 @@ false, use the symbol `false'. Each element is processed with
;; Add API key to list of arguments, this allows for increased quota
;; automatically.
(let ((api-key (cons "key" sx-request-api-key))
- (auth (car (sx-cache-get 'auth))))
+ (auth (car (sx-cache-get 'auth))))
(push api-key alist)
(when auth
(push auth alist))
diff --git a/sx-tab.el b/sx-tab.el
index 873e213..f36d10f 100644
--- a/sx-tab.el
+++ b/sx-tab.el
@@ -29,9 +29,21 @@
(defcustom sx-tab-default-site "emacs"
"Name of the site to use by default when listing questions."
- :type 'string
+ :type 'string
:group 'sx)
+(defvar sx-tab--list nil
+ "List of the names of all defined tabs.")
+
+(defun sx-tab-switch (tab)
+ "Switch to another question-list tab."
+ (interactive
+ (list (funcall (if ido-mode #'ido-completing-read #'completing-read)
+ "Switch to tab: " sx-tab--list
+ (lambda (tab) (not (equal tab sx-question-list--current-tab)))
+ t)))
+ (funcall (intern (format "sx-tab-%s" (downcase tab)))))
+
(defun sx-tab--interactive-site-prompt ()
"Query the user for a site."
(let ((default (or sx-question-list--site
@@ -42,6 +54,7 @@
(format "Site (%s): " default)
(sx-site-get-api-tokens) nil t nil nil
default)))
+
;;; The main macro
(defmacro sx-tab--define (tab pager &optional printer refresher
@@ -98,14 +111,98 @@ If SITE is nil, use `sx-tab-default-site'."
(setq sx-question-list--current-tab ,tab)
,@body
(sx-question-list-refresh 'redisplay no-update))
- (switch-to-buffer ,buffer-variable)))))
+ (switch-to-buffer ,buffer-variable))
+ ;; Add this tab to the list of existing tabs. So we can prompt
+ ;; the user with completion and stuff.
+ (add-to-list 'sx-tab--list ,tab))))
;;; FrontPage
(sx-tab--define "FrontPage"
(lambda (page)
(sx-question-get-questions
- sx-question-list--site page)))
+ sx-question-list--site page '((sort . activity)))))
+;;;###autoload
+(autoload 'sx-tab-frontpage
+ (expand-file-name
+ "sx-tab"
+ (when load-file-name
+ (file-name-directory load-file-name)))
+ nil t)
+
+
+;;; Newest
+(sx-tab--define "Newest"
+ (lambda (page)
+ (sx-question-get-questions
+ sx-question-list--site page '((sort . creation)))))
+;;;###autoload
+(autoload 'sx-tab-newest
+ (expand-file-name
+ "sx-tab"
+ (when load-file-name
+ (file-name-directory load-file-name)))
+ nil t)
+
+
+
+;;; TopVoted
+(sx-tab--define "TopVoted"
+ (lambda (page)
+ (sx-question-get-questions
+ sx-question-list--site page '((sort . votes)))))
+;;;###autoload
+(autoload 'sx-tab-topvoted
+ (expand-file-name
+ "sx-tab"
+ (when load-file-name
+ (file-name-directory load-file-name)))
+ nil t)
+
+
+
+;;; Hot
+(sx-tab--define "Hot"
+ (lambda (page)
+ (sx-question-get-questions
+ sx-question-list--site page '((sort . hot)))))
+;;;###autoload
+(autoload 'sx-tab-hot
+ (expand-file-name
+ "sx-tab"
+ (when load-file-name
+ (file-name-directory load-file-name)))
+ nil t)
+
+
+
+;;; Week
+(sx-tab--define "Week"
+ (lambda (page)
+ (sx-question-get-questions
+ sx-question-list--site page '((sort . week)))))
+;;;###autoload
+(autoload 'sx-tab-week
+ (expand-file-name
+ "sx-tab"
+ (when load-file-name
+ (file-name-directory load-file-name)))
+ nil t)
+
+
+
+;;; Month
+(sx-tab--define "Month"
+ (lambda (page)
+ (sx-question-get-questions
+ sx-question-list--site page '((sort . month)))))
+;;;###autoload
+(autoload 'sx-tab-month
+ (expand-file-name
+ "sx-tab"
+ (when load-file-name
+ (file-name-directory load-file-name)))
+ nil t)
(provide 'sx-tab)
;;; sx-tab.el ends here