aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--sx-button.el129
-rw-r--r--sx-interaction.el50
-rw-r--r--sx-question-list.el105
-rw-r--r--sx-question-mode.el36
-rw-r--r--sx-question-print.el133
-rw-r--r--sx-question.el51
-rw-r--r--sx-tab.el98
-rw-r--r--sx.el62
8 files changed, 502 insertions, 162 deletions
diff --git a/sx-button.el b/sx-button.el
new file mode 100644
index 0000000..c1abf90
--- /dev/null
+++ b/sx-button.el
@@ -0,0 +1,129 @@
+;;; sx-button.el --- Defining buttons used throughout SX.
+
+;; 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 'button)
+
+(require 'sx)
+(require 'sx-question)
+
+
+;;; Command definitions
+;; This extends `button-map', which already defines RET and mouse-1.
+(defvar sx-button-map
+ (let ((map (copy-keymap button-map)))
+ (define-key map "w" #'sx-button-copy)
+ map)
+ "Keymap used on buttons.")
+
+(defun sx-button-copy ()
+ "Copy the content of thing at point.
+This is usually a link's URL, or the content of a code block."
+ (interactive)
+ (let ((content
+ (get-text-property (point) 'sx-button-copy)))
+ (if (null content)
+ (sx-message "Nothing to copy here.")
+ (kill-new content)
+ (sx-message "Copied %s to kill ring."
+ (or (get-text-property
+ (point) 'sx-button-copy-type)
+ content)))))
+
+(defun sx-button-edit-this (text-or-marker)
+ "Open a temp buffer populated with the string TEXT-OR-MARKER.
+When given a marker (or interactively), use the 'sx-button-copy
+text-property under the marker. This is usually the content of a
+code-block."
+ (interactive (list (point-marker)))
+ ;; Buttons receive markers.
+ (when (markerp text-or-marker)
+ (unless (setq text-or-marker
+ (get-text-property text-or-marker 'sx-button-copy))
+ (sx-message "Nothing of interest here.")))
+ (with-current-buffer (pop-to-buffer (generate-new-buffer
+ "*sx temp buffer*"))
+ (insert text-or-marker)))
+
+(defun sx-button-follow-link (&optional pos)
+ "Follow link at POS. If POS is nil, use `point'."
+ (interactive)
+ (browse-url
+ (or (get-text-property (or pos (point)) 'sx-button-url)
+ (user-error "No url under point: %s" (or pos (point))))))
+
+
+;;; Help-echo definitions
+(defvar sx-button--help-echo
+ (concat "mouse-1, RET"
+ (propertize ": %s -- " 'face 'minibuffer-prompt)
+ "w"
+ (propertize ": copy %s" 'face 'minibuffer-prompt))
+ "Base help-echo on which others can be written.")
+
+(defvar sx-button--question-title-help-echo
+ (format sx-button--help-echo
+ (propertize "hide content" 'face 'minibuffer-prompt)
+ (propertize "link" 'face 'minibuffer-prompt))
+ "Help echoed in the minibuffer when point is on a section.")
+
+(defvar sx-button--link-help-echo
+ (format sx-button--help-echo
+ (propertize "visit %s" 'face 'minibuffer-prompt)
+ (propertize "URL" 'face 'minibuffer-prompt))
+ "Help echoed in the minibuffer when point is on a section.")
+
+
+;;; Type definitions
+(define-button-type 'sx-button
+ 'follow-link t
+ 'keymap sx-button-map)
+
+(define-button-type 'sx-question-mode-title
+ 'face 'sx-question-mode-title
+ 'action #'sx-question-mode-hide-show-section
+ 'help-echo sx-button--question-title-help-echo
+ 'sx-button-copy-type "Share Link"
+ :supertype 'sx-button)
+
+(define-button-type 'sx-question-mode-code-block
+ 'action #'sx-button-edit-this
+ 'face nil
+ :supertype 'sx-button)
+
+(define-button-type 'sx-button-link
+ 'action #'sx-button-follow-link
+ :supertype 'sx-button)
+
+(define-button-type 'sx-button-comment
+ 'help-echo (concat "mouse-1, RET"
+ (propertize ": write a comment"
+ 'face 'minibuffer-prompt))
+ 'action #'sx-comment
+ :supertype 'sx-button)
+
+(provide 'sx-button)
+;;; sx-button.el ends here
+
+;; Local Variables:
+;; lexical-binding: t
+;; End:
diff --git a/sx-interaction.el b/sx-interaction.el
index aeb6781..598a113 100644
--- a/sx-interaction.el
+++ b/sx-interaction.el
@@ -33,13 +33,20 @@
;;; Using data in buffer
-(defun sx--data-here ()
- "Get the text property `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))
- (or (derived-mode-p 'sx-question-mode)
- sx-question-mode--data)))
+ (and (derived-mode-p 'sx-question-mode)
+ sx-question-mode--data)
+ (and (null noerror)
+ (error "No question data found here"))))
(defun sx--maybe-update-display ()
"Refresh the question list if we're inside it."
@@ -55,6 +62,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
@@ -77,6 +86,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
@@ -121,15 +154,18 @@ changes."
;;; Commenting
-(defun sx-comment (data text)
+(defun sx-comment (data &optional text)
"Post a comment on DATA given by TEXT.
DATA can be a question, an answer, or a comment. Interactively,
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--data-here) 'query))
+ ;; When clicking the "Add a Comment" button, first arg is a marker.
+ (when (markerp data)
+ (setq data (sx--data-here))
+ (setq text 'query))
(sx-assoc-let data
;; Get the comment text
(when (eq text 'query)
diff --git a/sx-question-list.el b/sx-question-list.el
index 58c233b..bed432f 100644
--- a/sx-question-list.el
+++ b/sx-question-list.el
@@ -147,9 +147,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)
@@ -301,12 +298,13 @@ into consideration.
("K" sx-question-list-previous-far)
("g" sx-question-list-refresh)
(":" sx-question-list-switch-site)
+ ("t" sx-question-list-switch-tab)
("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.
@@ -336,10 +334,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)
@@ -353,7 +347,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: "
@@ -363,6 +357,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.
@@ -382,7 +382,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
@@ -425,7 +424,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.
@@ -437,7 +466,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."
@@ -487,44 +530,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 6293ad2..9be02ce 100644
--- a/sx-question-mode.el
+++ b/sx-question-mode.el
@@ -33,8 +33,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.")
@@ -42,6 +47,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.
@@ -74,7 +87,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)
@@ -85,7 +99,7 @@ If WINDOW is given, use that to display the buffer."
;; 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
+(defcustom sx-question-mode-recenter-line 2
"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.
@@ -144,9 +158,9 @@ If DIRECTION is negative, move backwards instead."
"Hide or show section under point.
Optional argument _ is for `push-button'."
(interactive)
- (let ((ov (car (or (sx-question-mode--section-overlays-at (point))
- (sx-question-mode--section-overlays-at
- (line-end-position))))))
+ (let ((ov (or (sx-question-mode--section-overlays-at
+ (line-end-position))
+ (sx-question-mode--section-overlays-at (point)))))
(goto-char (overlay-start ov))
(forward-line 0)
(overlay-put
@@ -154,9 +168,11 @@ Optional argument _ is for `push-button'."
(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)))
+ "Return the highest priority section overlay at POS.
+A section overlay has a `sx-question-mode--section-content'
+property."
+ (cdr-safe (get-char-property-and-overlay
+ pos 'sx-question-mode--section-content nil)))
;;; Major-mode
diff --git a/sx-question-print.el b/sx-question-print.el
index 6b1c96e..c35da16 100644
--- a/sx-question-print.el
+++ b/sx-question-print.el
@@ -22,7 +22,7 @@
;;; Code:
(require 'markdown-mode)
-(require 'button)
+(require 'sx-button)
(eval-when-compile
(require 'rx))
@@ -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))
@@ -164,26 +169,8 @@ replaced with the comment."
:group 'sx-question-mode)
-;;; Buttons
-(define-button-type 'sx-question-mode-title
- 'face 'sx-question-mode-title
- 'action #'sx-question-mode-hide-show-section
- 'help-echo 'sx-question-mode--section-help-echo
- 'follow-link t)
-
-(define-button-type 'sx-question-mode-link
- 'follow-link t
- 'action #'sx-question-mode-follow-link)
-
-
;;; Functions
;;;; Printing the general structure
-(defvar sx-question-mode--section-help-echo
- (format
- (propertize "%s to hide/display content" 'face 'minibuffer-prompt)
- (propertize "RET" 'face 'font-lock-function-name-face))
- "Help echoed in the minibuffer when point is on a section.")
-
(defun sx-question-mode--print-question (question)
"Print a buffer describing QUESTION.
QUESTION must be a data structure returned by `json-read'."
@@ -195,6 +182,10 @@ QUESTION must be a data structure returned by `json-read'."
(sx-question-mode--print-section question)
(sx-assoc-let question
(mapc #'sx-question-mode--print-section .answers))
+ ;; Display weird chars correctly
+ (set-buffer-multibyte nil)
+ (set-buffer-multibyte t)
+ ;; Go up
(goto-char (point-min))
(sx-question-mode-next-section))
@@ -203,7 +194,7 @@ QUESTION must be a data structure returned by `json-read'."
DATA can represent a question or an answer."
;; This makes `data' accessible through `sx--data-here'.
(sx-assoc-let data
- (sx--wrap-in-text-property
+ (sx--wrap-in-overlay
(list 'sx--data-here data)
(insert sx-question-mode-header-title)
(insert-text-button
@@ -211,6 +202,7 @@ DATA can represent a question or an answer."
(or .title sx-question-mode-answer-title)
;; Section level
'sx-question-mode--section (if .title 1 2)
+ 'sx-button-copy .share_link
:type 'sx-question-mode-title)
;; Sections can be hidden with overlays
(sx--wrap-in-overlay
@@ -227,7 +219,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
@@ -254,22 +247,35 @@ DATA can represent a question or an answer."
.body_markdown)
"\n"
(propertize sx-question-mode-separator
- 'face 'sx-question-mode-header)))))
- ;; Comments have their own `sx--data-here' property (so they can
- ;; be upvoted too).
- (when .comments
- (insert "\n")
- (insert-text-button
- sx-question-mode-comments-title
- 'face 'sx-question-mode-title-comments
- 'sx-question-mode--section 3
- :type 'sx-question-mode-title)
- (sx--wrap-in-overlay
- '(sx-question-mode--section-content t)
- (insert "\n")
- (sx--wrap-in-overlay
- '(face sx-question-mode-content-face)
- (mapc #'sx-question-mode--print-comment .comments))))))
+ 'face 'sx-question-mode-header)))
+ ;; Comments have their own `sx--data-here' property (so they can
+ ;; be upvoted too).
+ (when .comments
+ (insert "\n")
+ (insert-text-button
+ sx-question-mode-comments-title
+ 'face 'sx-question-mode-title-comments
+ 'sx-question-mode--section 3
+ 'sx-button-copy .share_link
+ :type 'sx-question-mode-title)
+ (sx--wrap-in-overlay
+ '(sx-question-mode--section-content t)
+ (insert "\n")
+ (sx--wrap-in-overlay
+ '(face sx-question-mode-content-face)
+ (mapc #'sx-question-mode--print-comment .comments))
+ ;; If there are comments, we want part of this margin to go
+ ;; inside them, so the button get's placed beside the
+ ;; "Comments" header when you hide them.
+ (insert " ")))
+ ;; If there are no comments, we have to add this margin here.
+ (unless .comments
+ (insert " "))
+ (insert " ")
+ ;; This is where the "add a comment" button is printed.
+ (insert-text-button "Add a Comment"
+ :type 'sx-button-comment)
+ (insert "\n")))))
(defun sx-question-mode--propertize-display-name (author)
"Return display_name of AUTHOR with `sx-question-mode-author' face."
@@ -281,7 +287,7 @@ DATA can represent a question or an answer."
"Print the comment described by alist COMMENT-DATA.
The comment is indented, filled, and then printed according to
`sx-question-mode-comments-format'."
- (sx--wrap-in-text-property
+ (sx--wrap-in-overlay
(list 'sx--data-here comment-data)
(sx-assoc-let comment-data
(insert
@@ -363,7 +369,7 @@ E.g.:
(defun sx-question-mode--dont-fill-here ()
"If text shouldn't be filled here, return t and skip over it."
- (or (sx-question-mode--move-over-pre)
+ (or (sx-question-mode--skip-and-fontify-pre)
;; Skip headers and references
(let ((pos (point)))
(skip-chars-forward "\r\n[:blank:]")
@@ -399,19 +405,13 @@ URL is used as 'help-echo and 'url properties."
text
;; Mouse-over
'help-echo
- (format (propertize "URL: %s, %s to visit" 'face 'minibuffer-prompt)
- (propertize url 'face 'default)
- (propertize "RET" 'face 'font-lock-function-name-face))
+ (format sx-button--link-help-echo
+ (propertize (sx--shorten-url url)
+ 'face 'font-lock-function-name-face))
;; For visiting and stuff.
- 'url url
- :type 'sx-question-mode-link))
-
-(defun sx-question-mode-follow-link (&optional pos)
- "Follow link at POS. If POS is nil, use `point'."
- (interactive)
- (browse-url
- (or (get-text-property (or pos (point)) 'url)
- (user-error "No url under point: %s" (or pos (point))))))
+ 'sx-button-url url
+ 'sx-button-copy url
+ :type 'sx-button-link))
(defun sx-question-mode-find-reference (id &optional fallback-id)
"Find url identified by reference ID in current buffer.
@@ -421,16 +421,33 @@ If ID is nil, use FALLBACK-ID instead."
(goto-char (point-min))
(when (search-forward-regexp
(format sx-question-mode--reference-regexp
- (or id fallback-id))
+ (or id fallback-id))
nil t)
(match-string-no-properties 1)))))
-(defun sx-question-mode--move-over-pre ()
- "Non-nil if paragraph at point can be filled."
- (markdown-match-pre-blocks
- (save-excursion
- (skip-chars-forward "\r\n[:blank:]")
- (point))))
+(defun sx-question-mode--skip-and-fontify-pre ()
+ "If there's a pre block ahead, handle it, skip it and return t.
+Handling means to turn it into a button and remove erroneous
+font-locking."
+ (let (beg end text)
+ (when (markdown-match-pre-blocks
+ (save-excursion
+ (skip-chars-forward "\r\n[:blank:]")
+ (setq beg (point))))
+ (setq end (point))
+ (setq text
+ (sx--unindent-text
+ (buffer-substring
+ (save-excursion
+ (goto-char beg)
+ (line-beginning-position))
+ end)))
+ (put-text-property beg end 'display nil)
+ (make-text-button
+ beg end
+ 'face 'markdown-pre-face
+ 'sx-button-copy text
+ :type 'sx-question-mode-code-block))))
(provide 'sx-question-print)
;;; sx-question-print.el ends here
diff --git a/sx-question.el b/sx-question.el
index a9ff2b1..b04c180 100644
--- a/sx-question.el
+++ b/sx-question.el
@@ -29,15 +29,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))
@@ -89,27 +91,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
diff --git a/sx-tab.el b/sx-tab.el
index 3706780..86aaf3c 100644
--- a/sx-tab.el
+++ b/sx-tab.el
@@ -31,12 +31,25 @@
(require 'sx)
(require 'sx-question-list)
+(require 'sx-interaction)
(defcustom sx-tab-default-site "emacs"
"Name of the site to use by default when listing questions."
: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)))))
+
(defmacro sx-tab--define (tab pager &optional printer refresher
&rest body)
"Define a StackExchange tab called TAB.
@@ -60,7 +73,7 @@ variables, but before refreshing the display."
`(progn
(defvar ,buffer-variable nil
,(format "Buffer where the %s questions are displayed."
- tab))
+ tab))
(defun
,(intern (concat "sx-tab-" name))
(&optional no-update site)
@@ -68,7 +81,7 @@ variables, but before refreshing the display."
NO-UPDATE (the prefix arg) is passed to `sx-question-list-refresh'.
If SITE is nil, use `sx-tab-default-site'."
- tab)
+ tab)
(interactive
(list current-prefix-arg
(funcall (if ido-mode #'ido-completing-read #'completing-read)
@@ -94,14 +107,17 @@ 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
@@ -110,6 +126,80 @@ If SITE is nil, use `sx-tab-default-site'."
(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 899e9ff..719b536 100644
--- a/sx.el
+++ b/sx.el
@@ -63,6 +63,7 @@
question.upvoted
question.downvoted
question.question_id
+ question.share_link
user.display_name
comment.owner
comment.body_markdown
@@ -78,6 +79,7 @@
answer.answer_id
answer.last_editor
answer.link
+ answer.share_link
answer.owner
answer.body_markdown
answer.upvoted
@@ -171,12 +173,53 @@ would yield
cons-cell))))
data))))
+(defun sx--shorten-url (url)
+ "Shorten URL hiding anything other than the domain.
+Paths after the domain are replaced with \"...\".
+Anything before the (sub)domain is removed."
+ (replace-regexp-in-string
+ ;; Remove anything after domain.
+ (rx (group-n 1 (and (1+ (any word ".")) "/"))
+ (1+ anything) string-end)
+ (eval-when-compile
+ (concat "\\1" (if (char-displayable-p ?…) "…" "...")))
+ ;; Remove anything before subdomain.
+ (replace-regexp-in-string
+ (rx string-start (or (and (0+ word) (optional ":") "//")))
+ "" url)))
+
+(defun sx--unindent-text (text)
+ "Remove indentation from TEXT."
+ (with-temp-buffer
+ (insert text)
+ (goto-char (point-min))
+ (let (result)
+ (while (null (eobp))
+ (skip-chars-forward "[:blank:]")
+ (unless (looking-at "$")
+ (push (current-column) result))
+ (forward-line 1))
+ (when result
+ (let ((rx (format "^ \\{0,%s\\}"
+ (apply #'min result))))
+ (goto-char (point-min))
+ (while (and (null (eobp))
+ (search-forward-regexp rx nil 'noerror))
+ (replace-match "")
+ (forward-line 1)))))
+ (buffer-string)))
+
;;; Printing request data
(defvar sx--overlays nil
"Overlays created by sx on this buffer.")
(make-variable-buffer-local 'sx--overlays)
+(defvar sx--overlay-printing-depth 0
+ "Track how many overlays we're printing on top of each other.
+Used for assigning higher priority to inner overlays.")
+(make-variable-buffer-local 'sx--overlay-printing-depth)
+
(defmacro sx--wrap-in-overlay (properties &rest body)
"Start a scope with overlay PROPERTIES and execute BODY.
Overlay is pushed on the buffer-local variable `sx--overlays' and
@@ -186,24 +229,21 @@ Return the result of BODY."
(declare (indent 1)
(debug t))
`(let ((p (point-marker))
- (result (progn ,@body)))
+ (result (progn ,@body))
+ ;; The first overlay is the shallowest. Any overlays created
+ ;; while the first one is still being created go deeper and
+ ;; deeper.
+ (sx--overlay-printing-depth (1+ sx--overlay-printing-depth)))
(let ((ov (make-overlay p (point)))
(props ,properties))
(while props
(overlay-put ov (pop props) (pop props)))
+ ;; Let's multiply by 10 just in case we ever want to put
+ ;; something in the middle.
+ (overlay-put ov 'priority (* 10 sx--overlay-printing-depth))
(push ov sx--overlays))
result))
-(defmacro sx--wrap-in-text-property (properties &rest body)
- "Start a scope with PROPERTIES and execute BODY.
-Return the result of BODY."
- (declare (indent 1)
- (debug t))
- `(let ((p (point-marker))
- (result (progn ,@body)))
- (add-text-properties p (point) ,properties)
- result))
-
(defun sx--user-@name (user)
"Get the `display_name' of USER prepended with @.
In order to correctly @mention the user, all whitespace is