aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSean Allred <code@seanallred.com>2014-11-14 17:25:27 -0500
committerSean Allred <code@seanallred.com>2014-11-14 17:30:21 -0500
commit1e859ea6d9b5365f69f8dea5d690e6bee8350e7a (patch)
tree7f7da64a833f8c1c783b6d4977fc79f06d227d1d
parent403c021d92bb036be5d95735bc1403056db3780b (diff)
parent22cc1b1b959761cda2ff2048dbaeba99cc094930 (diff)
Merge branch 'master' into network-list
Conflicts: sx-filter.el sx-question.el
-rw-r--r--Cask1
-rw-r--r--README.org43
-rw-r--r--sx-auth.el2
-rw-r--r--sx-encoding.el35
-rw-r--r--sx-filter.el2
-rw-r--r--sx-question-list.el66
-rw-r--r--sx-question-mode.el436
-rw-r--r--sx-question.el76
-rw-r--r--sx-request.el10
-rw-r--r--sx-site.el8
-rw-r--r--sx-time.el22
-rw-r--r--sx.el88
-rw-r--r--test/tests.el32
13 files changed, 666 insertions, 155 deletions
diff --git a/Cask b/Cask
index 3e939ef..f0c70fb 100644
--- a/Cask
+++ b/Cask
@@ -8,6 +8,7 @@
(depends-on "json" "1.4")
(depends-on "url")
(depends-on "cl-lib")
+(depends-on "markdown-mode")
(development
(depends-on "ert"))
diff --git a/README.org b/README.org
index 9063399..ea115dd 100644
--- a/README.org
+++ b/README.org
@@ -1,17 +1,46 @@
#+Title: Stack-Mode
-#+Author: Sean Allred
-#+Date: [2014-10-30 Thu]
[[https://travis-ci.org/vermiculus/stack-mode][https://travis-ci.org/vermiculus/stack-mode.svg?branch=master]]
[[https://gitter.im/vermiculus/stack-mode?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/stack-mode][https://badge.waffle.io/vermiculus/stack-mode.svg]]
-=Stack-mode= hopes to be a full featured Stack Exchange mode for
-GNU Emacs 24 and up. Using version 2.2, and subsequent versions as available, of
-the Stack Exchange API we aim to create a more versatile experience of Stack Exchange inside of Emacs.
+StackMode will be a full featured Stack Exchange mode for GNU Emacs 24
+and up. Using the official API, we aim to create a more versatile
+experience for the Stack Exchange network within Emacs itself.
-This README will eventually hold a high-level feature list and details
-on installation and configuration.
+* Features
+- ~list-questions~ ::
+ 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 site in your browser.
+ - Use =TAB= to fold questions and answers.
+** 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
+- ...
+
+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.
+* Installation
+To install the development version, follow the usual steps:
+- Clone this repository
+- Add this directory to your ~load-path~
+- Issue ~(require 'sx-question-list)~
+This should give you access to the only entry point function at the
+moment, ~list-questions~.
+
+Eventually, this package will at least be available on MELPA.
+Depending on community involvement, it may even be submitted to the
+official GNU ELPA.
* Contributing
Please help contribute! Doing any of the following will help us immensely:
- [[https://github.com/vermiculus/stack-mode/issues/new][Open an issue]]
diff --git a/sx-auth.el b/sx-auth.el
index 7912508..f32e7aa 100644
--- a/sx-auth.el
+++ b/sx-auth.el
@@ -64,7 +64,7 @@ questions)."
(if (string-equal "" sx-auth-access-token)
(progn (setq sx-auth-access-token nil)
(error "You must enter this code to use this client fully"))
- (sx-cache-set 'auth `((access-token . ,sx-auth-access-token)))))
+ (sx-cache-set 'auth `((access_token . ,sx-auth-access-token)))))
(provide 'sx-auth)
;;; sx-auth.el ends here
diff --git a/sx-encoding.el b/sx-encoding.el
index 0b72365..9d48e60 100644
--- a/sx-encoding.el
+++ b/sx-encoding.el
@@ -23,6 +23,8 @@
;;; Code:
+(require 'cl-lib)
+
(defcustom sx-encoding-html-entities-plist
'(Aacute "Á" aacute "á" Acirc "Â" acirc "â" acute "´" AElig "Æ" aelig "æ"
Agrave "À" agrave "à" alefsym "ℵ" Alpha "Α" alpha "α" amp "&" and "∧"
@@ -74,6 +76,39 @@
(substring ss 1))))))))
(replace-regexp-in-string "&[^; ]*;" get-function string)))
+(defun sx-encoding-normalize-line-endings (string)
+ "Normalize the line endings for STRING"
+ (delete ?\r string))
+
+(defun sx-encoding-clean-content (string)
+ "Cleans STRING for display.
+Applies `sx-encoding-normalize-line-endings' and
+`sx-encoding-decode-entities'."
+ (sx-encoding-decode-entities
+ (sx-encoding-normalize-line-endings
+ string)))
+
+(defun sx-encoding-clean-content-deep (data)
+ "Clean DATA recursively where necessary.
+
+See `sx-encoding-clean-content'."
+ (if (consp data)
+ ;; If we're looking at a cons cell, test to see if is a list. If
+ ;; it is, map ourselves over the entire list. If it is not,
+ ;; reconstruct the cons cell using a cleaned cdr.
+ (if (listp (cdr data))
+ (cl-map #'list #'sx-encoding-clean-content-deep data)
+ (cons (car data) (sx-encoding-clean-content-deep (cdr data))))
+ ;; If we're looking at an atom, clean and return if we're looking
+ ;; at a string, map if we're looking at a vector, and just return
+ ;; if we aren't looking at either.
+ (cond
+ ((stringp data)
+ (sx-encoding-clean-content data))
+ ((vectorp data)
+ (cl-map #'vector #'sx-encoding-clean-content-deep data))
+ (t data))))
+
(defun sx-encoding-gzipped-p (data)
"Checks for magic bytes in DATA.
diff --git a/sx-filter.el b/sx-filter.el
index c053070..90681e8 100644
--- a/sx-filter.el
+++ b/sx-filter.el
@@ -54,7 +54,7 @@ or string."
"filter/create"
keyword-arguments)))
(sx-assoc-let (elt response 0)
- filter))))
+ .filter))))
;;; Storage and Retrieval
diff --git a/sx-question-list.el b/sx-question-list.el
index a164706..c6d298a 100644
--- a/sx-question-list.el
+++ b/sx-question-list.el
@@ -25,7 +25,9 @@
(require 'sx)
(require 'sx-time)
+(require 'sx-site)
(require 'sx-question)
+(require 'sx-question-mode)
;;; Customization
@@ -69,7 +71,7 @@
:group 'sx-question-list-faces)
(defface sx-question-list-tags
- '((t :inherit font-lock-function-name-face))
+ '((t :inherit sx-question-mode-tags))
""
:group 'sx-question-list-faces)
@@ -132,6 +134,8 @@ Letters do not insert themselves; instead, they are commands.
("j" sx-question-list-view-next)
("k" sx-question-list-view-previous)
("g" sx-question-list-refresh)
+ (":" sx-question-list-switch-site)
+ ("v" sx-question-list-visit)
([?\r] sx-question-list-display-question)))
(defvar sx-question-list--current-page "Latest"
@@ -178,6 +182,9 @@ Letters do not insert themselves; instead, they are commands.
(defvar sx-question-list--current-site "emacs"
"Site being displayed in the *question-list* buffer.")
+(defvar sx-question-list--current-dataset nil
+ "")
+
(defun sx-question-list-refresh (&optional redisplay no-update)
"Update the list of questions.
If REDISPLAY is non-nil, also call `tabulated-list-print'.
@@ -186,13 +193,25 @@ a new list before redisplaying."
(interactive "pP")
;; Reset the mode-line unread count (we rebuild it here).
(setq sx-question-list--unread-count 0)
- (let ((question-list (sx-question-get-questions
- sx-question-list--current-site)))
+ (let ((question-list
+ (if (and no-update sx-question-list--current-dataset)
+ sx-question-list--current-dataset
+ (sx-question-get-questions
+ sx-question-list--current-site))))
+ (setq sx-question-list--current-dataset question-list)
;; Print the result.
(setq tabulated-list-entries
(mapcar #'sx-question-list--print-info question-list)))
(when redisplay (tabulated-list-print 'remember)))
+(defun sx-question-list-visit (&optional data)
+ "Visits question under point (or from DATA) using `browse-url'."
+ (interactive)
+ (unless data (setq data (tabulated-list-get-id)))
+ (unless data (error "No question here!"))
+ (sx-assoc-let data
+ (browse-url .link)))
+
(defcustom sx-question-list-ago-string " ago"
"String appended to descriptions of the time since something happened.
Used in the questions list to indicate a question was updated \"4d ago\"."
@@ -205,26 +224,27 @@ Used in the questions list to indicate a question was updated \"4d ago\"."
(list
data
(vector
- (list (int-to-string score)
- 'face (if upvoted 'sx-question-list-score-upvoted
+ (list (int-to-string .score)
+ 'face (if .upvoted 'sx-question-list-score-upvoted
'sx-question-list-score))
- (list (int-to-string answer_count)
- 'face (if (sx-question--accepted-answer data)
+ (list (int-to-string .answer_count)
+ 'face (if (sx-question--accepted-answer .data)
'sx-question-list-answers-accepted
'sx-question-list-answers))
(concat
(propertize
- title
- 'face (if (sx-question--read-p data)
+ .title
+ 'face (if (sx-question--read-p .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 (concat (sx-time-since last_activity_date)
+ (propertize (concat (sx-time-since .last_activity_date)
sx-question-list-ago-string)
'face 'sx-question-list-date)
- (propertize (concat " [" (mapconcat #'identity tags "] [") "]")
+ " "
+ (propertize (mapconcat #'sx-question--tag-format .tags " ")
'face 'sx-question-list-tags)
(propertize " " 'display "\n"))))))
@@ -261,8 +281,9 @@ focus the relevant window."
(when (sx-question--read-p data)
(cl-decf sx-question-list--unread-count)
(sx-question--mark-read data))
- (unless (window-live-p sx-question--window)
- (setq sx-question--window
+ (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-below sx-question-list-height)
(error
@@ -272,11 +293,22 @@ focus the relevant window."
(car (cdr-safe er)))
nil
(error (cdr er)))))))
- (sx-question--display data sx-question--window)
+ (sx-question-mode--display data sx-question-mode--window)
(when focus
- (if sx-question--window
- (select-window sx-question--window)
- (switch-to-buffer sx-question--buffer))))
+ (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"
+ (interactive
+ (list (funcall (if ido-mode #'ido-completing-read #'completing-read)
+ "Switch to site: " (sx-site-get-api-tokens)
+ (lambda (site)
+ (not (equal site sx-question-list--current-site)))
+ t)))
+ (setq sx-question-list--current-site site)
+ (sx-question-list-refresh 'redisplay))
(defvar sx-question-list--buffer nil
"Buffer where the list of questions is displayed.")
diff --git a/sx-question-mode.el b/sx-question-mode.el
new file mode 100644
index 0000000..20d3035
--- /dev/null
+++ b/sx-question-mode.el
@@ -0,0 +1,436 @@
+;;; sx-question-mode.el --- Creating the buffer that displays 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:
+
+;;
+
+
+;;; Code:
+(require 'markdown-mode)
+
+(require 'sx)
+(require 'sx-question)
+
+(defgroup sx-question-mode nil
+ "Customization group for sx-question-mode."
+ :prefix "sx-question-mode-"
+ :group 'sx)
+
+(defgroup sx-question-mode-faces nil
+ "Customization group for the faces of `sx-question-mode'."
+ :prefix "sx-question-mode-"
+ :group 'sx-question-mode)
+
+
+;;; Displaying a question
+(defvar sx-question-mode--window nil
+ "Window where the content of questions is displayed.")
+
+(defvar sx-question-mode--buffer nil
+ "Buffer being used to display questions.")
+
+(defvar sx-question-mode--data nil
+ "The data of the question being displayed.")
+
+(defun sx-question-mode--display (data &optional window)
+ "Display question given by DATA on WINDOW.
+If WINDOW is nil, use selected one.
+Returns the question buffer."
+ (let ((inhibit-read-only t))
+ (with-current-buffer
+ (sx-question-mode--display-buffer window)
+ (erase-buffer)
+ (sx-question-mode)
+ (sx-question-mode--print-question data)
+ (current-buffer))))
+
+(defun sx-question-mode--display-buffer (window)
+ "Display and return the buffer used for displaying a question.
+Create the buffer if necessary.
+If WINDOW is given, use that to display the buffer."
+ ;; Create the buffer if necessary.
+ (unless (buffer-live-p sx-question-mode--buffer)
+ (setq sx-question-mode--buffer
+ (generate-new-buffer "*stack-question*")))
+ (cond
+ ;; Window was given, use it.
+ ((window-live-p window)
+ (set-window-buffer window sx-question-mode--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)))
+ sx-question-mode--buffer)
+
+
+;;; Printing a question's content
+;;;; Faces and Variables
+(defvar sx-question-mode--overlays nil
+ "")
+(make-variable-buffer-local 'sx-question-mode--overlays)
+
+(defface sx-question-mode-header
+ '((t :inherit font-lock-variable-name-face))
+ "Face used on the question headers in the question buffer."
+ :group 'sx-question-mode-faces)
+
+(defface sx-question-mode-title
+ '((t :height 1.3 :weight bold :inherit default))
+ "Face used on the question title in the question buffer."
+ :group 'sx-question-mode-faces)
+
+(defface sx-question-mode-title-comments
+ '((t :height 1.1 :inherit sx-question-mode-title))
+ "Face used on the question title in the question buffer."
+ :group 'sx-question-mode-faces)
+
+(defcustom sx-question-mode-header-title "\n"
+ "String used before the question title at the header."
+ :type 'string
+ :group 'sx-question-mode)
+
+(defface sx-question-mode-author
+ '((t :inherit font-lock-string-face))
+ "Face used on the question author in the question buffer."
+ :group 'sx-question-mode-faces)
+
+(defcustom sx-question-mode-header-author "\nAuthor: "
+ "String used before the question author at the header."
+ :type 'string
+ :group 'sx-question-mode)
+
+(defface sx-question-mode-date
+ '((t :inherit font-lock-string-face))
+ "Face used on the question date in the question buffer."
+ :group 'sx-question-mode-faces)
+
+(defcustom sx-question-mode-header-date "\nAsked on: "
+ "String used before the question date at the header."
+ :type 'string
+ :group 'sx-question-mode)
+
+(defface sx-question-mode-tags
+ '((t :underline nil :inherit font-lock-function-name-face))
+ "Face used on the question tags in the question buffer."
+ :group 'sx-question-mode-faces)
+
+(defface sx-question-mode-author
+ '((t :inherit font-lock-variable-name-face))
+ "Face used for author names in the question buffer."
+ :group 'sx-question-mode-faces)
+
+(defcustom sx-question-mode-header-tags "\nTags: "
+ "String used before the question tags at the header."
+ :type 'string
+ :group 'sx-question-mode)
+
+(defface sx-question-mode-content-face
+ '((((background dark)) :background "#090909")
+ (((background light)) :background "#f4f4f4"))
+ "Face used on the question body in the question buffer.
+Shouldn't have a foreground, or this will interfere with
+font-locking."
+ :group 'sx-question-mode-faces)
+
+(defcustom sx-question-mode-last-edit-format " (edited %s ago by %s)"
+ "Format used to describe last edit date in the header.
+First %s is replaced with the date, and the second %s with the
+editor's name."
+ :type 'string
+ :group 'sx-question-mode)
+
+(defcustom sx-question-mode-separator
+ (concat "\n" (make-string 80 ?_) "\n")
+ "Separator used between header and body."
+ :type 'string
+ :group 'sx-question-mode)
+
+(defcustom sx-question-mode-answer-title "Answer"
+ "Title used at the start of \"Answer\" sections."
+ :type 'string
+ :group 'sx-question-mode)
+
+(defcustom sx-question-mode-comments-title " Comments"
+ "Title used at the start of \"Comments\" sections."
+ :type 'string
+ :group 'sx-question-mode)
+
+(defcustom sx-question-mode-comments-format "%s: %s\n"
+ "Format used to display comments.
+First \"%s\" is replaced with user name.
+Second \"%s\" is replaced with the comment."
+ :type 'string
+ :group 'sx-question-mode)
+
+
+;;; Printing a question's content
+;;;; Functions
+;; This is where most of the work is still left to be done! Need to
+;; insert more data from QUESTION.
+(defun sx-question-mode--print-question (question)
+ "Print a buffer describing QUESTION.
+QUESTION must be a data structure returned by `json-read'."
+ ;; Clear the overlays
+ (mapc #'delete-overlay sx-question-mode--overlays)
+ (setq sx-question-mode--overlays nil)
+ ;; Print everything
+ (sx-question-mode--print-section question)
+ (sx-assoc-let question
+ (mapc #'sx-question-mode--print-section .answers))
+ (goto-char (point-min))
+ (with-selected-window sx-question-mode--window
+ (sx-question-mode-next-section)))
+
+(defun sx-question-mode--print-section (data)
+ "Print a section corresponding to DATA.
+DATA can represent a question or an answer."
+ (sx-assoc-let data
+ (insert sx-question-mode-header-title
+ (if .title
+ ;; Questions have title
+ (propertize
+ .title
+ 'font-lock-face 'sx-question-mode-title
+ 'sx-question-mode--section 1)
+ ;; Answers don't
+ (propertize
+ sx-question-mode-answer-title
+ 'font-lock-face 'sx-question-mode-title
+ 'sx-question-mode--section 2)))
+ ;; Sections can be hidden with overlays
+ (sx-question-mode--wrap-in-overlay
+ '(sx-question-mode--section-content t)
+ (sx-question-mode--insert-header
+ ;; Author
+ sx-question-mode-header-author
+ (sx-question-mode--propertized-display-name .owner)
+ 'sx-question-mode-author
+ ;; Date
+ sx-question-mode-header-date
+ (concat
+ (sx-time-seconds-to-date .creation_date)
+ (when .last_edit_date
+ (format sx-question-mode-last-edit-format
+ (sx-time-since .last_edit_date)
+ (sx-question-mode--propertized-display-name .last_editor))))
+ 'sx-question-mode-date)
+ (when .title
+ ;; Tags
+ (sx-question-mode--insert-header
+ sx-question-mode-header-tags
+ (mapconcat #'sx-question--tag-format .tags " ")
+ 'sx-question-mode-tags))
+ ;; Body
+ (insert (propertize sx-question-mode-separator
+ 'face 'sx-question-mode-header))
+ (sx-question-mode--wrap-in-overlay
+ '(face sx-question-mode-content-face)
+ (insert "\n"
+ (sx-question-mode--fill-string
+ .body_markdown)
+ (propertize sx-question-mode-separator
+ 'face 'sx-question-mode-header))))
+ ;; Comments
+ (when .comments
+ (insert
+ "\n"
+ (propertize
+ sx-question-mode-comments-title
+ 'font-lock-face 'sx-question-mode-title-comments
+ 'sx-question-mode--section 3))
+ (sx-question-mode--wrap-in-overlay
+ '(sx-question-mode--section-content t)
+ (insert "\n")
+ (sx-question-mode--wrap-in-overlay
+ '(face sx-question-mode-content-face)
+ (mapc #'sx-question-mode--print-comment .comments))))))
+
+(defun sx-question-mode--fill-string (text)
+ "Fill TEXT according to `markdown-mode' and return it."
+ (with-temp-buffer
+ (insert text)
+ (markdown-mode)
+ (goto-char (point-min))
+ ;; ;; Do something here
+ ;; (while (null (eobp))
+ ;; (skip-chars-forward "\r\n[:blank:]")
+ ;; (markdown-pre-region))
+ (buffer-string)))
+
+(defun sx-question-mode--propertized-display-name (author)
+ "Return display_name of AUTHOR with `sx-question-mode-author' face."
+ (sx-assoc-let author
+ (propertize .display_name
+ 'font-lock-face 'sx-question-mode-author)))
+
+(defun sx-question-mode--print-comment (data)
+ "Print the comment described by alist DATA."
+ (sx-assoc-let data
+ (insert
+ (format
+ sx-question-mode-comments-format
+ (sx-question-mode--propertized-display-name .owner)
+ (substring
+ ;; We fill with three spaces at the start, so the comment is
+ ;; slightly indented.
+ (sx-question-mode--fill-string
+ (concat " " .body_markdown))
+ ;; Then we remove the spaces from the first line, since we'll
+ ;; add the username there anyway.
+ 3)))))
+
+(defmacro sx-question-mode--wrap-in-overlay (properties &rest body)
+ "Execute BODY and wrap any inserted text in an overlay.
+Overlay is pushed on `sx-question-mode--overlays' and given PROPERTIES.
+Return the result of BODY."
+ (declare (indent 1)
+ (debug t))
+ `(let ((p (point-marker))
+ (result (progn ,@body)))
+ (let ((ov (make-overlay p (point)))
+ (props ,properties))
+ (while props
+ (overlay-put ov (pop props) (pop props)))
+ (push ov sx-question-mode--overlays))
+ result))
+
+(defun sx-question-mode--insert-header (&rest args)
+ "Insert HEADER and VALUE.
+HEADER is given `sx-question-mode-header' face, and value is given FACE.
+\(fn header value face [header value face] [header value face] ...)"
+ (while args
+ (insert
+ (propertize (pop args) 'font-lock-face 'sx-question-mode-header)
+ (propertize (pop args) 'font-lock-face (pop args)))))
+
+
+;;; Movement commands
+;; Sections are headers placed above a question's content or an
+;; answer's content, or above the list of comments. They are
+;; identified with the `sx-question-mode--section' text property.
+;; To move between sections, just search for the property. The value
+;; of the text-property is the depth of the section (1 for contents, 2
+;; for comments).
+(defcustom sx-question-mode-recenter-line 1
+ "Screen line to which we recenter after moving between sections.
+This is used as an argument to `recenter', only used if the end
+of section is outside the window.
+If nil, no recentering is performed."
+ :type '(choice (const :tag "Don't recenter" nil)
+ integer)
+ :group 'sx-question-mode)
+
+(defun sx-question-mode-next-section (&optional n)
+ "Move down to next section (question or answer) of this buffer.
+Prefix argument N moves N sections down or up."
+ (interactive "p")
+ (unless n (setq n 1))
+ (dotimes (_ (abs n))
+ ;; This will either move us to the next section, or move out of
+ ;; the current one.
+ (unless (sx-question-mode--goto-propety-change 'section n)
+ ;; If all we did was move out the current one, then move again
+ ;; and we're guaranteed to reach the next section.
+ (sx-question-mode--goto-propety-change 'section n)))
+ (when sx-question-mode-recenter-line
+ (let ((ov (car-safe (sx-question-mode--section-overlays-at (line-end-position)))))
+ (when (and (overlayp ov) (> (overlay-end ov) (window-end)))
+ (recenter sx-question-mode-recenter-line)))))
+
+(defun sx-question-mode-previous-section (&optional n)
+ "Move down to previous section (question or answer) of this buffer.
+Prefix argument N moves N sections up or down."
+ (interactive "p")
+ (sx-question-mode-next-section (- (or n 1))))
+
+(defun sx-question-mode--goto-propety-change (prop &optional direction)
+ "Move forward until the value of text-property `sx-question-mode--PROP' changes.
+Return the new value of PROP at point.
+If DIRECTION is negative, move backwards instead."
+ (let ((prop (intern (format "sx-question-mode--%s" prop)))
+ (func (if (and (numberp direction)
+ (< direction 0))
+ #'previous-single-property-change
+ #'next-single-property-change))
+ (limit (if (and (numberp direction)
+ (< direction 0))
+ (point-min) (point-max))))
+ (goto-char (funcall func (point) prop nil limit))
+ (get-text-property (point) prop)))
+
+
+(defun sx-question-mode-hide-show-section ()
+ "Hide or show section under point."
+ (interactive)
+ (let ((ov (car (or (sx-question-mode--section-overlays-at (point))
+ (sx-question-mode--section-overlays-at
+ (line-end-position))))))
+ (goto-char (overlay-start ov))
+ (forward-line 0)
+ (overlay-put
+ ov 'invisible
+ (null (overlay-get ov 'invisible)))))
+
+(defun sx-question-mode--section-overlays-at (pos)
+ "Return a list of `sx-question-mode--section-content' overlays at POS."
+ (cl-remove-if (lambda (x) (null (overlay-get x 'sx-question-mode--section-content)))
+ (overlays-at pos)))
+
+
+;;; Major-mode
+(define-derived-mode sx-question-mode markdown-mode "Question"
+ "Major mode for a question and its answers.
+Letters do not insert themselves; instead, they are commands.
+\\<sx-question-mode>
+\\{sx-question-mode}"
+ (remove-hook 'after-change-functions 'markdown-check-change-for-wiki-link t)
+ (remove-hook 'window-configuration-change-hook
+ 'markdown-fontify-buffer-wiki-links t)
+ (read-only-mode))
+
+(mapc
+ (lambda (x) (define-key sx-question-mode-map
+ (car x) (cadr x)))
+ `(("n" sx-question-mode-next-section)
+ ("p" sx-question-mode-previous-section)
+ ("j" sx-question-mode-next-section)
+ ("k" sx-question-mode-previous-section)
+ ("g" sx-question-mode-refresh)
+ ("q" quit-window)
+ (" " scroll-up-command)
+ (,(kbd "S-SPC") scroll-down-command)
+ ([backspace] scroll-down-command)
+ ([tab] sx-question-mode-hide-show-section)))
+
+(defun sx-question-mode-refresh ()
+ "Refresh currently displayed question.
+Queries the API for any changes to the question or its answers or
+comments, and redisplays it."
+ (interactive)
+ (unless (derived-mode-p 'sx-question-mode)
+ (error "Not in `sx-question-mode'"))
+ (sx-assoc-let sx-question-mode--data
+ (sx-question-mode--display
+ (sx-question-get-question
+ sx-question-list--current-site .question_id)
+ (selected-window))))
+
+(provide 'sx-question-mode)
+;;; sx-question-mode.el ends here
diff --git a/sx-question.el b/sx-question.el
index 601875f..2fa9d2b 100644
--- a/sx-question.el
+++ b/sx-question.el
@@ -26,11 +26,22 @@
(require 'sx)
(require 'sx-filter)
-(require 'sx-lto)
(require 'sx-method)
(defvar sx-question-browse-filter
- '(nil (user.profile_image shallow_user.profile_image)))
+ '((question.body_markdown
+ question.comments
+ question.answers
+ question.last_editor
+ user.display_name
+ comment.owner
+ comment.body_markdown
+ comment.body
+ answer.last_editor
+ answer.owner
+ answer.body_markdown
+ answer.comments)
+ (user.profile_image shallow_user.profile_image)))
(defun sx-question-get-questions (site &optional page)
"Get the page PAGE of questions from SITE."
@@ -40,6 +51,16 @@
(page . ,page))
sx-question-browse-filter))
+(defun sx-question-get-question (site id)
+ "Get the question ID from SITE."
+ (let ((res (sx-method-call
+ (format "questions/%s" id)
+ `((site . ,site))
+ sx-question-browse-filter)))
+ (if (vectorp res)
+ (elt res 0)
+ (error "Couldn't find question %S in %S" id site))))
+
;;; Question Properties
(defun sx-question--read-p (question)
@@ -56,54 +77,9 @@
"Mark QUESTION as being read, until it is updated again."
nil)
-
-;;; Displaying a question
-(defvar sx-question--window nil
- "Window where the content of questions is displayed.")
-
-(defvar sx-question--buffer nil
- "Buffer being used to display questions.")
-
-(defcustom sx-question-use-html t
- "If nil, markdown is used for the body."
- :type 'boolean
- :group 'sx-question)
-
-(defun sx-question--display (data &optional window)
- "Display question given by DATA on WINDOW.
-If WINDOW is nil, use selected one."
- (let ((sx-lto--body-src-block
- (if sx-question-use-html nil
- sx-lto--body-src-block))
- (inhibit-read-only t))
- (with-current-buffer
- (sx-question--display-buffer window)
- (erase-buffer)
- (insert
- (org-element-interpret-data
- (sx-lto--question data)))
- (org-mode)
- (show-all)
- (view-mode)
- (current-buffer))))
-
-(defun sx-question--display-buffer (window)
- "Display and return the buffer used for displaying a question.
-Create the buffer if necessary.
-If WINDOW is given, use that to display the buffer."
- ;; Create the buffer if necessary.
- (unless (buffer-live-p sx-question--buffer)
- (setq sx-question--buffer
- (generate-new-buffer "*sx-question*")))
- (cond
- ;; Window was given, use it.
- ((window-live-p window)
- (set-window-buffer window sx-question--buffer))
- ;; No window, but the buffer is already being displayed somewhere.
- ((get-buffer-window sx-question--buffer 'visible))
- ;; Neither, so we create the window.
- (t (switch-to-buffer sx-question--buffer)))
- sx-question--buffer)
+(defun sx-question--tag-format (tag)
+ "Formats TAG for display"
+ (concat "[" tag "]"))
(provide 'sx-question)
;;; sx-question.el ends here
diff --git a/sx-request.el b/sx-request.el
index 56362fc..6dc54e7 100644
--- a/sx-request.el
+++ b/sx-request.el
@@ -41,7 +41,7 @@
"The current version of the API.")
(defconst sx-request-api-root
- (format "http://api.stackexchange.com/%s/" sx-request-api-version)
+ (format "https://api.stackexchange.com/%s/" sx-request-api-version)
"The base URL to make requests from.")
(defcustom sx-request-silent-p
@@ -107,15 +107,15 @@ number of requests left every time it finishes a call.")
(error "Response could not be read by `json-read-from-string'"))
;; If we get here, the response is a valid data structure
(sx-assoc-let response
- (when error_id
+ (when .error_id
(error "Request failed: (%s) [%i %s] %S"
- method error_id error_name error_message))
+ .method .error_id .error_name .error_message))
(when (< (setq sx-request-remaining-api-requests
- quota_remaining)
+ .quota_remaining)
sx-request-remaining-api-requests-message-threshold)
(sx-message "%d API requests reamining"
sx-request-remaining-api-requests))
- items)))))))
+ (sx-encoding-clean-content-deep .items))))))))
;;; Support Functions
diff --git a/sx-site.el b/sx-site.el
index c8de938..2243fa8 100644
--- a/sx-site.el
+++ b/sx-site.el
@@ -45,8 +45,6 @@
nil
none))
-(defun sx-site-get-sites ())
-
(defun sx-site--get-site-list ()
(sx-cache-get
'site-list
@@ -59,6 +57,12 @@
"Favorite sites."
:group 'sx-site)
+(defun sx-site-get-api-tokens ()
+ "Return a list of all known site tokens."
+ (mapcar
+ (lambda (site) (cdr (assoc 'api_site_parameter site)))
+ (sx-site--get-site-list)))
+
(provide 'sx-site)
;;; stack-site.el ends here
diff --git a/sx-time.el b/sx-time.el
index 1c8e353..9c4dfaa 100644
--- a/sx-time.el
+++ b/sx-time.el
@@ -49,5 +49,27 @@
(concat (format "%.0f" (/ delay (car (cddr here))))
(cadr here)))))))
+(defcustom sx-time-date-format-year "%H:%M %e %b %Y"
+ "Format used for dates on a past year.
+See also `sx-time-date-format'."
+ :type 'string
+ :group 'sx-time)
+
+(defcustom sx-time-date-format "%H:%M - %d %b"
+ "Format used for dates on this year.
+See also `sx-time-date-format-year'."
+ :type 'string
+ :group 'sx-time)
+
+(defun sx-time-seconds-to-date (seconds)
+ "Return the integer SECONDS as a date string."
+ (let ((time (seconds-to-time seconds)))
+ (format-time-string
+ (if (string= (format-time-string "%Y")
+ (format-time-string "%Y" time))
+ sx-time-date-format
+ sx-time-date-format-year)
+ time)))
+
(provide 'sx-time)
;;; sx-time.el ends here
diff --git a/sx.el b/sx.el
index 0a1b046..6d802ce 100644
--- a/sx.el
+++ b/sx.el
@@ -71,82 +71,42 @@ a string, just return it."
;;; Interpreting request data
-(defvar sx--api-symbols
- '(
- accept_rate
- answer_count
- answer_id
- answers
- body
- body_markdown
- close_vote_count
- comment_count
- comment_id
- creation_date
- delete_vote_count
- display_name
- downvoted
- edited
- error_id
- error_name
- error_message
- favorite_count
- filter
- items
- is_accepted
- is_answered
- last_activity_date
- last_edit_date
- last_editor
- link
- owner
- profile_image
- question_id
- quota_remaining
- reopen_vote_count
- reputation
- score
- tags
- title
- upvoted
- user_id
- user_type
- view_count
- )
- "")
-
-(defun sx--deep-search (symbol list)
- "Non-nil if SYMBOL is contained somewhere inside LIST."
+(defun sx--deep-dot-search (data)
+ "Find symbols somewhere inside DATA which start with a `.'.
+Returns a list where each element is a cons cell. The car is the
+symbol, the cdr is the symbol without the `.'."
(cond
- ((symbolp list)
- (eq symbol list))
- ((not (listp list))
- nil)
- (t
- (remove nil (mapcar (lambda (x) (sx--deep-search symbol x)) list)))))
+ ((symbolp data)
+ (let ((name (symbol-name data)))
+ (when (string-match "\\`\\." name)
+ ;; Return the cons cell inside a list, so it can be appended
+ ;; with other results in the clause below.
+ (list (cons data (intern (replace-match "" nil nil name)))))))
+ ((not (listp data)) nil)
+ (t (apply
+ #'append
+ (remove nil (mapcar #'sx--deep-dot-search data))))))
(defmacro sx-assoc-let (alist &rest body)
- "Execute BODY while let-binding api symbols to their values in ALIST.
-Any api symbol is any symbol listed in `sx--api-symbols'. Only
-those present in BODY are letbound, which leads to optimal
-performance.
+ "Execute BODY while let-binding dotted symbols to their values in ALIST.
+Dotted symbol is any symbol starting with a `.'. Only those
+present in BODY are letbound, which leads to optimal performance.
For instance the following code
(stack-core-with-data alist
- (list title body))
+ (list .title .body))
is equivalent to
- (let ((title (cdr (assoc 'title alist)))
- (body (cdr (assoc 'body alist))))
- (list title body))"
+ (let ((.title (cdr (assoc 'title alist)))
+ (.body (cdr (assoc 'body alist))))
+ (list .title .body))"
(declare (indent 1)
(debug t))
- (let ((symbols (cl-member-if
- (lambda (x) (sx--deep-search x body))
- sx--api-symbols)))
- `(let ,(mapcar (lambda (x) `(,x (cdr (assoc ',x ,alist)))) symbols)
+ (let ((symbol-alist (sx--deep-dot-search body)))
+ `(let ,(mapcar (lambda (x) `(,(car x) (cdr (assoc ',(cdr x) ,alist))))
+ symbol-alist)
,@body)))
(defcustom sx-init-hook nil
diff --git a/test/tests.el b/test/tests.el
index a66394c..6a48257 100644
--- a/test/tests.el
+++ b/test/tests.el
@@ -20,6 +20,14 @@
(insert-file-contents file)
(read (buffer-string))))))
+(defmacro line-should-match (regexp)
+ ""
+ `(let ((line (buffer-substring-no-properties
+ (line-beginning-position)
+ (line-end-position))))
+ (message "Line here is: %S" line)
+ (should (string-match ,regexp line))))
+
(setq
sx-request-remaining-api-requests-message-threshold 50000
debug-on-error t
@@ -89,14 +97,6 @@
((1 . alpha) (2 . beta))]
'(1 2 3)))))
-(defmacro line-should-match (regexp)
- ""
- `(let ((line (buffer-substring-no-properties
- (line-beginning-position)
- (line-end-position))))
- (message "Line here is: %S" line)
- (should (string-match ,regexp line))))
-
(ert-deftest question-list-display ()
(cl-letf (((symbol-function #'sx-request-make)
(lambda (&rest _) sx-test-data-questions)))
@@ -116,3 +116,19 @@
(sx-question-list-previous 4)
(line-should-match
"^\\s-+2\\s-+1\\s-+&quot;Making tag completion table&quot; Freezes/Blocks -- how to disable [ 0-9]+[ydhms] ago\\s-+\\[autocomplete\\]")))
+
+(ert-deftest macro-test--sx-assoc-let ()
+ "Tests macro expansion for `sx-assoc-let'"
+ (should
+ (equal '(let ((.test (cdr (assoc 'test data))))
+ .test)
+ (macroexpand
+ '(sx-assoc-let data
+ .test))))
+ (should
+ (equal '(let ((.test-one (cdr (assoc 'test-one data)))
+ (.test-two (cdr (assoc 'test-two data))))
+ (cons .test-one .test-two))
+ (macroexpand
+ '(sx-assoc-let data
+ (cons .test-one .test-two))))))