diff options
author | Artur Malabarba <bruce.connor.am@gmail.com> | 2014-12-03 14:39:53 +0000 |
---|---|---|
committer | Artur Malabarba <bruce.connor.am@gmail.com> | 2014-12-03 14:39:53 +0000 |
commit | 21e526919594fcb89b298366c210fb54676e7e9a (patch) | |
tree | fe76d354fcaaa16c649261735512666051e1c4db | |
parent | 46de3f0ddba5a7611f025b816d637136593b20b1 (diff) | |
parent | 07dc04e1e5111fff7f2905a155271edde3ce754c (diff) |
Merge branch 'master' into answering
Conflicts:
sx-question-list.el
sx-question-print.el
sx-tab.el
-rw-r--r-- | sx-interaction.el | 41 | ||||
-rw-r--r-- | sx-method.el | 3 | ||||
-rw-r--r-- | sx-question-list.el | 105 | ||||
-rw-r--r-- | sx-question-mode.el | 20 | ||||
-rw-r--r-- | sx-question-print.el | 13 | ||||
-rw-r--r-- | sx-question.el | 80 | ||||
-rw-r--r-- | sx-request.el | 16 | ||||
-rw-r--r-- | sx-tab.el | 103 |
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)) @@ -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 |