aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSean Allred <code@seanallred.com>2014-12-03 17:59:02 -0500
committerSean Allred <code@seanallred.com>2014-12-03 17:59:02 -0500
commit6b2ecadd89e31feb994883987c38f6988a140b8c (patch)
treec77890b4800813287f5d2fdf38effd389c219164
parent114ca09da984738df2510bc72753a3443d16857c (diff)
parent67b60ea558f0386a1ea3dadcf3a9c4d22d398620 (diff)
Merge branch 'master' into issue-130
Conflicts: sx-request.el Conflict was trivial.
-rw-r--r--CONTRIBUTING.org23
-rw-r--r--README.org73
-rw-r--r--sx-button.el37
-rw-r--r--sx-compose.el6
-rw-r--r--sx-interaction.el92
-rw-r--r--sx-method.el3
-rw-r--r--sx-question-list.el115
-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
-rw-r--r--sx.el18
13 files changed, 424 insertions, 175 deletions
diff --git a/CONTRIBUTING.org b/CONTRIBUTING.org
new file mode 100644
index 0000000..3fcf111
--- /dev/null
+++ b/CONTRIBUTING.org
@@ -0,0 +1,23 @@
+If you need help, search the issue tracker to see if anyone has asked
+your question before. If it hasn't, a good place to ask first is our
+chat room on [[https://gitter.im/vermiculus/sx.el][Gitter]]. Opening an issue is welcome of course, but chat
+will likely be faster for you. If a code change needs to be made, an
+issue can be written up as necessary.
+
+Have a great idea for SX? Again, discuss it on [[https://gitter.im/vermiculus/sx.el][Gitter]] first! Don't
+limit ideas to mimicking the official website, either -- this is
+Emacs; we should take advantage of its abilities.
+
+To see what we're working on /right now/, check out our [[http://www.waffle.io/vermiculus/sx.el][Waffle board]].
+If you would like to contribute, feel free to take on anything in the
+=ready= Waffle column (or the [[https://github.com/vermiculus/sx.el/issues?q=is%3Aopen+is%3Aissue+label%3Aready+-label%3A%22in+progress%22][=ready= GitHub label]]). These are issues
+which have been discussed enough to provide a good idea of what should
+be done. Issues in =backlog= are either still under discussion or
+simply are in the backlog.
+
+Of course, the greatest gift you can give to SX is a good word. Star
+the project on GitHub, mention it to others who would use it in your
+StackExchange chat rooms (as always, be courteous and respectful), and
+use it yourself.
+
+Enjoy!
diff --git a/README.org b/README.org
index df8d907..460ba34 100644
--- a/README.org
+++ b/README.org
@@ -4,58 +4,61 @@
[[https://gitter.im/vermiculus/sx.el?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge][https://badges.gitter.im/Join Chat.svg]]
[[https://www.waffle.io/vermiculus/sx.el][https://badge.waffle.io/vermiculus/sx.el.svg]]
-SX will be a full featured Stack Exchange mode for GNU Emacs 24+.
-Using the official API, we aim to create a more versatile experience
-for the Stack Exchange network within Emacs itself.
+SX will be a full featured Stack Exchange mode for GNU Emacs 24+. Using the
+official API, we aim to create a more versatile experience for the Stack
+Exchange network within Emacs itself.
* Features
-- ~sx-tab-frontpage~ ::
- List questions on a StackExchange site.
-- Viewing Posts ::
- - Use =jknp= to open questions from within ~list-questions~; use
- =RET= to move focus.
- - Use =v= to open the object at point in your browser.
- - Use =TAB= to fold questions and answers.
- - Use =RET= to open a link at point.
- - Use =:= to switch sites.
- - Vote up and down with =u= and =d=.
+** Viewing Questions
+View questions with one of the ~sx-tab-~ commands. These translate to the
+different 'tabs' that you can view on the official site. Implemented tabs
+include:
+- =frontpage= :: The default front page of questions.
+- =newest= :: Newest questions first.
+- =topvoted= :: Highest-voted questions first.
+- =hot= :: Questions with the most views, answers, and votes over the last few
+ days.
+- =week= :: Questions with the most views, answers, and votes this week.
+- =month= :: Questions with the most views, answers, and votes this month.
+The meaning of these tabs hopefully needs no explanation, but the official
+behavior is given as a tooltip on any site in the StackExchange network.
-** Planned
-- Archiving questions for offline access
-- Browsing and favoriting networks
-- Advanced searching
-- Writing questions, answers, and comments (with source code in its
- native major mode)
-- Notifications
-- Reputation reporting
-- ...
+Each of these opens up a list of questions. Switch sites with =:=. Navigate
+this list of questions with =jk= or =np=. =jk= will also view the question in a
+separate buffer. =v= will visit the question in your browser where =w= will
+simply copy a link. Upvote and downvote with =u= and =d=. =RET= will take you
+to the question buffer, where =RET= on headlines will expand and collapse each
+section. Add comments with =c=.
-Have a feature in mind that isn't on the list? Submit a pull request
-to add it to the list! If you want to discuss it first, pop in our
-Gitter chatroom (badge above) -- someone will be around shortly to
-talk about it.
+As always, =C-h m= is the definitive resource for the functions of this mode.
* Installation
To install the development version, follow the usual steps:
- Clone this repository
- Add this directory to your ~load-path~
- Issue ~(require 'sx)~
-This should give you access to the only entry point function at the
-moment, ~sx-tab-frontpage~.
+- Issue ~(require 'sx-tab)~
+This should give you access to the ~sx-tab-~ functions (the main entry points at
+this time).
+
+If you are going to be doing any asking / answering / commenting / upvoting /
+downvoting / /etc./, you must use ~sx-authenticate~ to provide SX with an
+authentication token to act on your behalf.
Eventually, this package will be available on MELPA.
* Contributing
-Please help contribute! Doing any of the following will help us immensely:
+Please help contribute! Doing any of the following will help us immensely:
- [[https://github.com/vermiculus/sx.el/issues/new][Open an issue]]
- [[https://github.com/vermiculus/sx.el/pulls][Submit a pull request]]
- [[https://gitter.im/vermiculus/sx.el][Suggest a package or library in our Chat on Gitter]] (or just hang out =:)=)
- Spread the word!
-For a better view of all of the open issues, take a look at our lovely
-[[http://www.waffle.io/vermiculus/sx.el][Waffle board]]. Feel free to take the torch on anything in =backlog= or
-=ready=. If you have thoughts on any other issues, don't hesitate to
-chime in!
+For a better view of all of the open issues, take a look at our lovely [[http://www.waffle.io/vermiculus/sx.el][Waffle
+board]]. Feel free to take the torch on anything in =backlog= or =ready=. If you
+have thoughts on any other issues, don't hesitate to chime in!
+
+See also =CONTRIBUTING.org=.
* Resources
- [[http://www.gnu.org/software/emacs/][GNU Emacs]]
@@ -68,3 +71,7 @@ it.
- [[file:resources/emacs.svg][Emacs icon]]
- [[file:resources/stackexchange.svg][Stack Exchange icon]]
+* COMMENT Local Variables
+# Local Variables:
+# fill-column: 80
+# End:
diff --git a/sx-button.el b/sx-button.el
index 8f0b6b9..dbadc2e 100644
--- a/sx-button.el
+++ b/sx-button.el
@@ -1,4 +1,4 @@
-;;; sx-button.el --- Defining buttons used throughout SX.
+;;; sx-button.el --- Defining buttons used throughout SX. -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Artur Malabarba
@@ -18,6 +18,25 @@
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
+;;
+;; This file defines all buttons used by SX. For information on
+;; buttons, see:
+;; http://www.gnu.org/software/emacs/manual/html_node/elisp/Buttons.html
+;;
+;; Most interactible parts of the SX buffers are buttons. Wherever you
+;; are, you can always cycle through all buttons by hitting `TAB',
+;; that should help identify what's a button in each buffer.
+;;
+;; To define a new type of button follow the examples below using
+;; `define-button-type' with :supertype `sx-button'. Required
+;; properties are `action' and `help-echo'. You'll probably want to
+;; give it a `face' as well, unless you want it to look like a link.
+;;
+;; Buttons can then be inserted in their respective files using
+;; `insert-text-button'. Give it the string, the `:type' you defined,
+;; and any aditional properties that can only be determined at
+;; creation. Existing text can be transformed into a button with
+;; `make-text-button' instead.
;;; Code:
@@ -27,6 +46,14 @@
(require 'sx-question)
+;;; Face
+(defface sx-custom-button
+ '((((type x w32 ns) (class color)) ; Like default mode line
+ :box (:line-width 2 :style released-button)
+ :background "lightgrey" :foreground "black"))
+ "Face used on buttons such as \"Write an Answer\".")
+
+
;;; Command definitions
;; This extends `button-map', which already defines RET and mouse-1.
(defvar sx-button-map
@@ -118,7 +145,7 @@ code-block."
'help-echo (concat "mouse-1, RET"
(propertize ": write a comment"
'face 'minibuffer-prompt))
- 'face 'custom-button
+ 'face 'sx-custom-button
'action #'sx-comment
:supertype 'sx-button)
@@ -126,13 +153,9 @@ code-block."
'help-echo (concat "mouse-1, RET"
(propertize ": write an answer"
'face 'minibuffer-prompt))
- 'face 'custom-button
+ 'face 'sx-custom-button
'action #'sx-answer
:supertype 'sx-button)
(provide 'sx-button)
;;; sx-button.el ends here
-
-;; Local Variables:
-;; lexical-binding: t
-;; End:
diff --git a/sx-compose.el b/sx-compose.el
index f5aef79..d7d3ff3 100644
--- a/sx-compose.el
+++ b/sx-compose.el
@@ -20,9 +20,9 @@
;;; Commentary:
;; This file defines `sx-compose-mode' and its auxiliary functions and
-;; variables. In order to use `sx-compose-mode', it is adamant that
-;; the variable `sx-compose--send-function' be set. Otherwise it's
-;; just a regular markdown buffer.
+;; variables. In order to use `sx-compose-mode', it is vital that the
+;; variable `sx-compose--send-function' be set. Otherwise it's just a
+;; regular markdown buffer.
;;
;; In order to help avoid mistakes, there is the function
;; `sx-compose-create'. This is the preferred way of activating the
diff --git a/sx-interaction.el b/sx-interaction.el
index 7f851e7..9b63e0a 100644
--- a/sx-interaction.el
+++ b/sx-interaction.el
@@ -36,6 +36,8 @@
;;; Code:
+(eval-when-compile
+ '(require 'cl-lib))
(require 'sx)
(require 'sx-question)
@@ -46,13 +48,46 @@
;;; Using data in buffer
-(defun sx--data-here ()
- "Get the text property `sx--data-here'."
- (or (get-char-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)))
+(defun sx--data-here (&optional type noerror)
+ "Get the alist regarding object under point of type TYPE.
+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.
+
+TYPE is a symbol restricting the type of object desired. Possible
+values are 'question, 'answer, 'comment, or nil (for any type).
+
+If no object of the requested type could be returned, an error is
+thrown unless NOERROR is non-nil."
+ (or (let ((data (get-char-property (point) 'sx--data-here)))
+ (if (null type) data
+ (sx-assoc-let type
+ ;; Is data of the right type?
+ (cl-case type
+ (question (when .title data))
+ (answer (when .answer_id data))
+ (comment (when .comment_id data))))))
+ ;; The following two only ever return questions.
+ (when (or (null type) (eq type 'question))
+ ;; @TODO: `sx-question-list-mode' may one day display answers.
+ ;; Ideally, it would use the `sx--data-here' (so no special
+ ;; handling would be necessary.
+ (or (and (derived-mode-p 'sx-question-list-mode)
+ (tabulated-list-get-id))
+ (and (derived-mode-p 'sx-question-mode)
+ sx-question-mode--data)))
+ ;; Nothing was found
+ (and (null noerror)
+ (error "No %s found here" (or type "data")))))
+
+(defun sx--error-if-unread (data)
+ "Throw a user-error if DATA is an unread question.
+If it's not a question, or if it is read, return DATA."
+ ;; If we found a question, we may need to check if it's read.
+ (if (and (assoc 'title data)
+ (null (sx-question--read-p data)))
+ (user-error "Question not yet read. View it before acting on it")
+ data))
(defun sx--maybe-update-display (&optional buffer)
"Refresh whatever is displayed in BUFFER or the current buffer.
@@ -70,6 +105,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,11 +129,35 @@ 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
guessed from context at point."
- (interactive (list (sx--data-here)))
+ (interactive (list (sx--error-if-unread (sx--data-here))))
(sx-assoc-let data
(sx-set-vote data "upvote" (null (eq .upvoted t)))))
@@ -104,7 +165,7 @@ guessed from context at point."
"Apply or remove downvote from DATA.
DATA can be a question or an answer. Interactively, it is guessed
from context at point."
- (interactive (list (sx--data-here)))
+ (interactive (list (sx--error-if-unread (sx--data-here))))
(sx-assoc-let data
(sx-set-vote data "downvote" (null (eq .downvoted t)))))
@@ -143,7 +204,7 @@ it is guessed from context at point.
If DATA is a comment, the comment is posted as a reply to it.
TEXT is a string. Interactively, it is read from the minibufer."
- (interactive (list (sx--data-here) 'query))
+ (interactive (list (sx--error-if-unread (sx--data-here)) 'query))
;; When clicking the "Add a Comment" button, first arg is a marker.
(when (markerp data)
(setq data (sx--data-here))
@@ -222,8 +283,6 @@ OBJECT can be a question or an answer."
"Start editing an answer or question given by DATA.
DATA is an answer or question alist. Interactively, it is guessed
from context at point."
- ;; Answering doesn't really make sense from anywhere other than
- ;; inside a question. So we don't need `sx--data-here' here.
(interactive (list (sx--data-here)))
;; If we ever make an "Edit" button, first arg is a marker.
(when (markerp data) (setq data (sx--data-here)))
@@ -243,8 +302,6 @@ from context at point."
(defun sx-ask (site)
"Start composing a question for SITE.
SITE is a string, indicating where the question will be posted."
- ;; Answering doesn't really make sense from anywhere other than
- ;; inside a question. So we don't need `sx--data-here' here.
(interactive (list (sx-tab--interactive-site-prompt)))
(let ((buffer (current-buffer)))
(pop-to-buffer
@@ -259,9 +316,10 @@ SITE is a string, indicating where the question will be posted."
"Start composing an answer for question given by DATA.
DATA is a question alist. Interactively, it is guessed from
context at point. "
- ;; Answering doesn't really make sense from anywhere other than
- ;; inside a question. So we don't need `sx--data-here' here.
- (interactive (list sx-question-mode--data))
+ ;; If the user tries to answer a question that's not viewed, he
+ ;; probaby hit the button by accident.
+ (interactive
+ (list (sx--error-if-unread (sx--data-here 'question))))
;; When clicking the "Write an Answer" button, first arg is a marker.
(when (markerp data) (setq data (sx--data-here)))
(let ((buffer (current-buffer)))
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..c5c32d9 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)
@@ -266,9 +263,9 @@ into consideration.
;; it's not terribly intuitive.
(setq tabulated-list-sort-key nil)
(add-hook 'tabulated-list-revert-hook
- #'sx-question-list-refresh nil t)
+ #'sx-question-list-refresh nil t)
(add-hook 'tabulated-list-revert-hook
- #'sx-question-list--update-mode-line nil t)
+ #'sx-question-list--update-mode-line nil t)
(tabulated-list-init-header))
(defcustom sx-question-list-date-sort-method 'last_activity_date
@@ -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
@@ -399,7 +398,11 @@ a new list before redisplaying."
(setq tabulated-list-entries
(mapcar sx-question-list--print-function
(cl-remove-if #'sx-question--hidden-p question-list)))
- (when redisplay (tabulated-list-print 'remember))
+ (when redisplay
+ (tabulated-list-print 'remember)
+ ;; Display weird chars correctly
+ (set-buffer-multibyte nil)
+ (set-buffer-multibyte t))
(when window
(set-window-start window old-start)))
(sx-message "Done."))
@@ -424,7 +427,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 +469,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 +533,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 c8c6eb6..0994fbd 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)
+(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
diff --git a/sx.el b/sx.el
index 411c9e2..8e3e5d3 100644
--- a/sx.el
+++ b/sx.el
@@ -202,24 +202,31 @@ Anything before the (sub)domain is removed."
"" url)))
(defun sx--unindent-text (text)
- "Remove indentation from TEXT."
+ "Remove indentation from TEXT.
+Primarily designed to extract the content of markdown code
+blocks."
(with-temp-buffer
(insert text)
(goto-char (point-min))
(let (result)
+ ;; Get indentation of each non-blank line
(while (null (eobp))
(skip-chars-forward "[:blank:]")
(unless (looking-at "$")
(push (current-column) result))
(forward-line 1))
(when result
+ ;; Build a regexp with the smallest indentation
(let ((rx (format "^ \\{0,%s\\}"
(apply #'min result))))
(goto-char (point-min))
+ ;; Use this regexp to remove that much indentation
+ ;; throughout the buffer.
(while (and (null (eobp))
(search-forward-regexp rx nil 'noerror))
(replace-match "")
(forward-line 1)))))
+ ;; Return the buffer
(buffer-string)))
@@ -275,11 +282,10 @@ with a `link' property).
DATA can also be the link itself."
(let ((link (if (stringp data) data
(cdr (assoc 'link data)))))
- (unless (stringp link)
- (error "Data has no link property"))
- (replace-regexp-in-string
- "^https?://\\(?:\\(?1:[^/]+\\)\\.stackexchange\\|\\(?2:[^/]+\\)\\)\\.[^.]+/.*$"
- "\\1\\2" link)))
+ (when (stringp link)
+ (replace-regexp-in-string
+ "^https?://\\(?:\\(?1:[^/]+\\)\\.stackexchange\\|\\(?2:[^/]+\\)\\)\\.[^.]+/.*$"
+ "\\1\\2" link))))
(defun sx--deep-dot-search (data)
"Find symbols somewhere inside DATA which start with a `.'.