aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTING.org15
-rw-r--r--README.org7
-rw-r--r--resources/sx.svg232
-rw-r--r--sx-button.el23
-rw-r--r--sx-cache.el5
-rw-r--r--sx-compose.el6
-rw-r--r--sx-inbox.el6
-rw-r--r--sx-question-list.el72
-rw-r--r--sx-question-mode.el49
-rw-r--r--sx-question-print.el75
-rw-r--r--sx-question.el3
-rw-r--r--sx-request.el2
-rw-r--r--sx-search.el36
-rw-r--r--sx-tag.el26
-rw-r--r--sx.el34
-rw-r--r--sx.org2
-rw-r--r--test/test-printing.el24
-rw-r--r--test/tests.el22
18 files changed, 548 insertions, 91 deletions
diff --git a/CONTRIBUTING.org b/CONTRIBUTING.org
index 3fcf111..cc9f8ce 100644
--- a/CONTRIBUTING.org
+++ b/CONTRIBUTING.org
@@ -1,9 +1,24 @@
+** Found a Bug?
+Open an issue! Please include:
+- a description of what you are trying to do
+- the behavior you actually see
+- steps we can take to reproduce the bug
+If the bug actually causes Emacs to issue an error, please
+additionally include the backtrace that led up to the error. To do
+this, use ~M-x toggle-debug-on-error~ to make sure that the setting is
+/enabled/. Now, when the error occurs, a =*Backtrace*= window will
+open with most of the information that we typically need. Copy and
+paste this into your bug report. (To get out of the =*Backtrace*=
+window and Emacs' subsequent 'debug mode', press =q=.)
+
+** Don't Know Your Way Around?
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.
+** Want to Help?
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.
diff --git a/README.org b/README.org
index b9888a7..4071561 100644
--- a/README.org
+++ b/README.org
@@ -1,6 +1,8 @@
#+Title: SX -- Stack Exchange for Emacs
[[https://travis-ci.org/vermiculus/sx.el][https://travis-ci.org/vermiculus/sx.el.svg?branch=master]]
+[[http://melpa.org/#/sx][file:http://melpa.org/packages/sx-badge.svg]]
+[[http://stable.melpa.org/#/sx][file:http://stable.melpa.org/packages/sx-badge.svg]]
[[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]]
@@ -35,6 +37,9 @@ section. Add comments with =c=.
As always, =C-h m= is the definitive resource for the functions of this mode.
* Installation
+SX is now available on MELPA! Install it via the usual method or run =M-x
+package-install RET sx RET=.
+
To install the development version, follow the usual steps:
- Clone this repository
- Add this directory to your ~load-path~
@@ -46,8 +51,6 @@ 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:
- [[https://github.com/vermiculus/sx.el/issues/new][Open an issue]]
diff --git a/resources/sx.svg b/resources/sx.svg
new file mode 100644
index 0000000..56d0129
--- /dev/null
+++ b/resources/sx.svg
@@ -0,0 +1,232 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="35.405388mm"
+ height="38.84499mm"
+ viewBox="0 0 125.45216 137.63973"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="sx.svg"
+ inkscape:export-filename="/home/will/sx.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient4846">
+ <stop
+ id="stop4848"
+ offset="0"
+ style="stop-color:#65baf3;stop-opacity:1;" />
+ <stop
+ style="stop-color:#65baf3;stop-opacity:1;"
+ offset="0.75"
+ id="stop4850" />
+ <stop
+ id="stop4852"
+ offset="0.75"
+ style="stop-color:#36a5ef;stop-opacity:1;" />
+ <stop
+ style="stop-color:#36a5ef;stop-opacity:1;"
+ offset="1"
+ id="stop4854" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3686">
+ <stop
+ style="stop-color:#5479ae;stop-opacity:1;"
+ offset="0"
+ id="stop3688" />
+ <stop
+ id="stop3690"
+ offset="0.75"
+ style="stop-color:#5479ae;stop-opacity:1;" />
+ <stop
+ style="stop-color:#205196;stop-opacity:1;"
+ offset="0.75"
+ id="stop3692" />
+ <stop
+ id="stop3694"
+ offset="1"
+ style="stop-color:#205196;stop-opacity:1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3666">
+ <stop
+ id="stop3668"
+ offset="0"
+ style="stop-color:#518fd9;stop-opacity:1;" />
+ <stop
+ style="stop-color:#518fd9;stop-opacity:1;"
+ offset="0.75"
+ id="stop3670" />
+ <stop
+ id="stop3672"
+ offset="0.75"
+ style="stop-color:#1d6dcd;stop-opacity:1;" />
+ <stop
+ style="stop-color:#1d6dcd;stop-opacity:1;"
+ offset="1"
+ id="stop3674" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4846"
+ id="linearGradient4844"
+ x1="-79.081757"
+ y1="283.1138"
+ x2="-19.876425"
+ y2="344.00983"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(488.55313,131.15373)" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3666"
+ id="linearGradient4862"
+ x1="-122.40535"
+ y1="400.3822"
+ x2="-99.634369"
+ y2="423.06076"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.95292128,0,0,0.95292128,464.95389,150.57487)" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3686"
+ id="linearGradient4870"
+ x1="-212.95235"
+ y1="477.93634"
+ x2="-182.6236"
+ y2="507.84705"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(480.47191,124.08266)" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1"
+ inkscape:cx="465.89524"
+ inkscape:cy="179.68499"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ fit-margin-top="5"
+ fit-margin-left="5"
+ fit-margin-right="5"
+ fit-margin-bottom="5"
+ inkscape:window-width="2880"
+ inkscape:window-height="1498"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-194.57597,-617.50518)">
+ <rect
+ width="354.98172"
+ height="354.98172"
+ x="367.20773"
+ y="262.03415"
+ id="rect4772"
+ style="display:none;fill:none" />
+ <g
+ id="g4788"
+ style="display:none"
+ transform="matrix(0.6933237,0,0,0.6933237,367.08916,261.89478)">
+ <g
+ id="g4790"
+ style="display:inline" />
+ </g>
+ <g
+ id="g4806"
+ style="display:none"
+ transform="matrix(0.6933237,0,0,0.6933237,367.08916,261.89478)">
+ <g
+ id="g4808"
+ style="display:inline">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 349.098,256.651 c -0.265,-0.254 37.637,27.605 39.421,25.012 6.362,-9.252 82.046,-93.137 84.784,-116.236 0.242,-2.003 -0.516,-4.096 -0.516,-4.096 0,0 -1.19,-0.144 -6.325,-4.314 -2.692,-2.192 -5.483,-4.581 -5.483,-4.581 -16.054,0.998 -57.885,41.559 -111.062,103.568"
+ id="path4810"
+ style="display:none;fill:#050505" />
+ </g>
+ </g>
+ <g
+ id="g4796"
+ style="stroke:none"
+ transform="matrix(0.15739948,0,0,0.15739948,247.43469,629.47196)">
+ <g
+ id="g4800"
+ style="stroke:none" />
+ </g>
+ <g
+ id="g4774"
+ transform="matrix(0.15739948,0,0,0.15739948,247.43469,629.47196)" />
+ <g
+ id="g4952"
+ transform="matrix(0.22702163,0,0,0.22702163,164.09751,570.01618)">
+ <path
+ sodipodi:nodetypes="ccsccscsscsc"
+ inkscape:connector-curvature="0"
+ id="path4806"
+ d="m 403.41449,388.7755 c -2.6741,-12.03361 7.6962,-11.399 17.7242,-12.06753 32.3125,-2.22844 76.43569,1.55992 115.65634,9.35948 19.71448,3.92049 23.17582,3.34267 23.17582,3.34267 28.07841,1.33707 49.91718,-13.37067 48.80295,-44.34608 -0.22285,-31.42106 -31.63829,-56.61619 -66.40765,-57.71673 -32.74253,-1.03637 -113.42786,4.67974 -113.42786,4.67974 94.04039,19.38747 109.41387,24.33504 114.31922,35.65513 2.89698,6.68534 -4.8392,13.33227 -30.75254,12.0336 -28.21151,-1.41388 -86.01798,-8.24524 -86.01798,-8.24524 -55.0426,-8.4681 -93.3719,-16.49051 -110.0852,5.34826 -10.919,14.26755 0.1313,18.0725 5.0339,28.32333"
+ style="fill:#84d2e9;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="cscccc"
+ inkscape:connector-curvature="0"
+ id="path4818"
+ d="m 499.36625,501.60118 c -19.95659,-15.28447 -55.33856,-42.75992 -61.50206,-47.615 -12.1801,-9.59442 -31.8078,-24.3181 -33.9177,-37.47548 l -76.6113,-17.55183 c 23.2443,40.55893 56.2956,60.5283 81.8022,80.13911 37.7758,5.9885 54.2997,9.12772 90.22886,22.5032 z"
+ style="fill:url(#linearGradient4844);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="ccccccc"
+ inkscape:connector-curvature="0"
+ id="path4814"
+ d="m 433.15079,512.22758 c -86.7917,-23.73213 -143.5795,-16.71037 -187.4274,6.34368 -21.9905,13.01681 -34.8165,22.85342 -33.3116,41.70349 l 93.6882,11.05699 c 9.3276,-11.26036 34.5389,-12.58769 68.2631,-20.03347 32.2743,-7.8539 154.7509,-6.26654 154.7509,-6.26654 -19.18051,-15.22821 -95.9632,-32.80415 -95.9632,-32.80415 z"
+ style="fill:url(#linearGradient4862);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="cccccccc"
+ inkscape:connector-curvature="0"
+ id="path4839-6"
+ d="m 212.96599,587.14537 c 1.8429,23.08351 11.6545,74.9329 68.3141,73.11653 l 133.0515,1.40072 -12.339,75.76574 73.75297,-75.45038 36.4933,-0.62941 c 149.2534,-82.78703 -174.65397,-24.80201 -200.54897,-44.62823 -4.9981,-6.16606 -12.7038,-7.60189 -4.8341,-17.10232"
+ style="fill:url(#linearGradient4870);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ </g>
+ <flowRoot
+ xml:space="preserve"
+ id="flowRoot4936"
+ style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ transform="matrix(0.22702163,0,0,0.22702163,164.09751,570.01618)"><flowRegion
+ id="flowRegion4938"><rect
+ id="rect4940"
+ width="1457.1428"
+ height="1300"
+ x="-1531.4286"
+ y="-64.780655" /></flowRegion><flowPara
+ id="flowPara4942" /></flowRoot> </g>
+</svg>
diff --git a/sx-button.el b/sx-button.el
index 1d4eb4f..d32314d 100644
--- a/sx-button.el
+++ b/sx-button.el
@@ -77,30 +77,35 @@ This is usually a link's URL, or the content of a code block."
(point) 'sx-button-copy-type)
content)))))
-(defun sx-button-edit-this (text-or-marker &optional major-mode)
- "Open a temp buffer populated with the string TEXT-OR-MARKER using MAJOR-MODE.
+(defun sx-button-edit-this (text-or-marker &optional majormode)
+ "Open a temp buffer populated with the string TEXT-OR-MARKER using MAJORMODE.
When given a marker (or interactively), use the 'sx-button-copy
and the 'sx-mode text-properties under the marker. These are
usually part of a code-block."
(interactive (list (point-marker)))
;; Buttons receive markers.
(when (markerp text-or-marker)
- (setq major-mode (get-text-property text-or-marker 'sx-mode))
+ (setq majormode (get-text-property text-or-marker 'sx-mode))
(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)
- (when major-mode
- (funcall major-mode))))
+ (when majormode
+ (funcall majormode))))
(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)
- (sx-user-error "No url under point: %s" (or pos (point))))))
+ (let ((url (or (get-text-property (or pos (point)) 'sx-button-url)
+ (sx-user-error "No url under point: %s" (or pos (point))))))
+ ;; If we didn't recognize the link, this errors immediately. If
+ ;; we mistakenly recognize it, it will error when we try to fetch
+ ;; whatever we thought it was.
+ (condition-case nil (sx-open-link url)
+ ;; When it errors, don't blame the user, just visit externally.
+ (error (sx-visit-externally url)))))
;;; Help-echo definitions
@@ -164,7 +169,7 @@ usually part of a code-block."
'face 'sx-user-name
:supertype 'sx-button)
-(declare-function sx-search-tag-at-point "sx-tag")
+(declare-function sx-search-tag-at-point "sx-search")
(define-button-type 'sx-button-tag
'action #'sx-search-tag-at-point
'help-echo sx-button--tag-help-echo
diff --git a/sx-cache.el b/sx-cache.el
index 3e8e08f..b17149f 100644
--- a/sx-cache.el
+++ b/sx-cache.el
@@ -73,8 +73,9 @@ DATA will be written as returned by `prin1'.
CACHE is resolved to a file name by `sx-cache-get-file-name'."
(sx-cache--ensure-sx-cache-directory-exists)
- (write-region (prin1-to-string data) nil
- (sx-cache-get-file-name cache))
+ (let (print-length print-level)
+ (write-region (prin1-to-string data) nil
+ (sx-cache-get-file-name cache)))
data)
(defun sx-cache--invalidate (cache &optional vars init-method)
diff --git a/sx-compose.el b/sx-compose.el
index eb5e2eb..ae13fb6 100644
--- a/sx-compose.el
+++ b/sx-compose.el
@@ -193,8 +193,7 @@ tags. Return a list of already inserted tags."
'noerror)
(error "No Tags header found"))
(save-match-data
- (split-string (match-string 1) (rx (any space ",;"))
- 'omit-nulls (rx space))))
+ (sx--split-string (match-string 1) (rx (any space ",;")))))
(defun sx-compose--check-tags ()
"Check if tags in current compose buffer are valid."
@@ -313,8 +312,7 @@ other keywords are read from the header "
(unless (search-forward-regexp "^Tags : *\\([^[:space:]].*\\) *$"
header-end 'noerror)
(error "No Tags header found"))
- (push (cons 'tags (split-string (match-string 1)
- "[[:space:],;]" 'omit-nulls))
+ (push (cons 'tags (sx--split-string (match-string 1) "[[:space:],;]"))
keywords)
;; And erase the header so it doesn't get sent.
(delete-region
diff --git a/sx-inbox.el b/sx-inbox.el
index 21589fb..3048509 100644
--- a/sx-inbox.el
+++ b/sx-inbox.el
@@ -127,11 +127,7 @@ These are identified by their links.")
(setq tabulated-list-format
[("Type" 30 t nil t) ("Date" 10 t :right-align t) ("Title" 0)])
(setq mode-line-format sx-inbox--mode-line)
- (setq header-line-format sx-inbox--header-line)
- ;; @TODO: This will no longer be necessary once we properly
- ;; refactor sx-question-list-mode.
- (remove-hook 'tabulated-list-revert-hook
- #'sx-question-list--update-mode-line t))
+ (setq header-line-format sx-inbox--header-line))
;;; Keybinds
diff --git a/sx-question-list.el b/sx-question-list.el
index 63484e1..32bc140 100644
--- a/sx-question-list.el
+++ b/sx-question-list.el
@@ -27,6 +27,7 @@
(require 'sx)
(require 'sx-time)
+(require 'sx-tag)
(require 'sx-site)
(require 'sx-question)
(require 'sx-question-mode)
@@ -165,7 +166,7 @@ Also see `sx-question-list-refresh'."
" "
;; @TODO: Make this width customizable. (Or maybe just make
;; the whole thing customizable)
- (format "%-40s" (mapconcat #'sx-tag--format .tags " "))
+ (format "%-40s" (sx-tag--format-tags .tags sx-question-list--site))
" "
(sx-user--format "%15d %4r" .owner)
(propertize " " 'display "\n")))))))
@@ -224,6 +225,24 @@ This is ignored if `sx-question-list--refresh-function' is set.")
": Quit")
"Header-line used on the question list.")
+(defconst sx-question-list--order-methods
+ '(("Recent Activity" . activity)
+ ("Creation Date" . creation)
+ ("Most Voted" . votes)
+ ("Score" . votes)
+ ("Hot" . hot))
+ "Alist of possible values to be passed to the `sort' keyword.")
+(make-variable-buffer-local 'sx-question-list--order-methods)
+
+(defun sx-question-list--interactive-order-prompt (&optional prompt)
+ "Interactively prompt for a sorting order.
+PROMPT is displayed to the user. If it is omitted, a default one
+is used."
+ (let ((order (sx-completing-read
+ (or prompt "Order questions by: ")
+ (mapcar #'car sx-question-list--order-methods))))
+ (cdr-safe (assoc-string order sx-question-list--order-methods))))
+
;;; Mode Definition
(define-derived-mode sx-question-list-mode
@@ -253,6 +272,10 @@ The full list of variables which can be set is:
5. `sx-question-list--dataset'
This is only used if both 3 and 4 are nil. It can be used to
display a static list.
+ 6. `sx-question-list--order'
+ Set this to the `sort' method that should be used when
+ requesting the list, if that makes sense. If it doesn't
+ leave it as nil.
\\<sx-question-list-mode-map>
If none of these is configured, the behaviour is that of a
\"Frontpage\", for the site given by
@@ -276,11 +299,12 @@ Adding further questions to the bottom of the list is done by:
display; otherwise, decrement `sx-question-list--pages-so-far'.
If `sx-question-list--site' is given, items 3 and 4 should take it
-into consideration.
+into consideration. The same holds for `sx-question-list--order'.
\\{sx-question-list-mode-map}"
(hl-line-mode 1)
- (sx-question-list--update-mode-line)
+ (setq mode-line-format
+ sx-question-list--mode-line-format)
(setq sx-question-list--pages-so-far 0)
(setq tabulated-list-format
[(" V" 3 t :right-align t)
@@ -292,8 +316,6 @@ into consideration.
(setq tabulated-list-sort-key nil)
(add-hook 'tabulated-list-revert-hook
#'sx-question-list-refresh nil t)
- (add-hook 'tabulated-list-revert-hook
- #'sx-question-list--update-mode-line nil t)
(setq header-line-format sx-question-list--header-line))
(defcustom sx-question-list-date-sort-method 'last_activity_date
@@ -372,14 +394,12 @@ Non-interactively, DATA is a question alist."
;; "Unanswered", etc.
"Variable describing current tab being viewed.")
-(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)
-
(defconst sx-question-list--mode-line-format
- '(" "
- mode-name
- " "
+ '(" "
+ (:propertize
+ (:eval (sx--pretty-site-parameter sx-question-list--site))
+ face mode-line-buffer-id)
+ " " mode-name ": "
(:propertize sx-question-list--current-tab
face mode-line-buffer-id)
" ["
@@ -390,7 +410,7 @@ Non-interactively, DATA is a question alist."
", "
"Total: "
(:propertize
- (:eval (int-to-string sx-question-list--total-count))
+ (:eval (int-to-string (length tabulated-list-entries)))
face mode-line-buffer-id)
"] ")
"Mode-line construct to use in question-list buffers.")
@@ -401,19 +421,14 @@ Non-interactively, DATA is a question alist."
(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.
- (when (derived-mode-p 'sx-question-list-mode)
- (setq mode-line-format
- sx-question-list--mode-line-format)
- (setq sx-question-list--total-count
- (length tabulated-list-entries))))
-
(defvar sx-question-list--site nil
"Site being displayed in the *question-list* buffer.")
(make-variable-buffer-local 'sx-question-list--site)
+(defvar sx-question-list--order nil
+ "Order being displayed in the *question-list* buffer.")
+(make-variable-buffer-local 'sx-question-list--order)
+
(defun sx-question-list-refresh (&optional redisplay no-update)
"Update the list of questions.
If REDISPLAY is non-nil (or if interactive), also call `tabulated-list-print'.
@@ -586,6 +601,19 @@ Sets `sx-question-list--site' and then call
(setq sx-question-list--site site)
(sx-question-list-refresh 'redisplay)))
+(defun sx-question-list-order-by (sort)
+ "Order questions in the current list by the method SORT.
+Sets `sx-question-list--order' and then calls
+`sx-question-list-refresh' with `redisplay'."
+ (interactive
+ (list (when sx-question-list--order
+ (sx-question-list--interactive-order-prompt))))
+ (unless sx-question-list--order
+ (sx-user-error "This list can't be reordered"))
+ (when (and sort (symbolp sort))
+ (setq sx-question-list--order sort)
+ (sx-question-list-refresh 'redisplay)))
+
(provide 'sx-question-list)
;;; sx-question-list.el ends here
diff --git a/sx-question-mode.el b/sx-question-mode.el
index 6125416..820c5b4 100644
--- a/sx-question-mode.el
+++ b/sx-question-mode.el
@@ -48,6 +48,7 @@ Common values for this variable are `pop-to-buffer' and `switch-to-buffer'."
(defvar sx-question-mode--data nil
"The data of the question being displayed.")
+(make-variable-buffer-local 'sx-question-mode--data)
(defun sx-question-mode--get-window ()
"Return a window displaying a question, or nil."
@@ -69,6 +70,7 @@ Returns the question buffer."
(defun sx-question-mode--erase-and-print-question (data)
"Erase contents of buffer and print question given by DATA.
Also marks the question as read with `sx-question--mark-read'."
+ (sx--ensure-site data)
(sx-question--mark-read data)
(let ((inhibit-read-only t))
(erase-buffer)
@@ -183,13 +185,47 @@ property."
": Quit")
"Header-line used on the question list.")
+(defconst sx-question-mode--mode-line
+ '(" "
+ ;; `sx-question-mode--data' is guaranteed to have through
+ ;; `sx--ensure-site' already, so we use `let-alist' instead of
+ ;; `sx-assoc-let' to improve performance (since the mode-line is
+ ;; updated a lot).
+ (:propertize
+ (:eval (sx--pretty-site-parameter
+ (let-alist sx-question-mode--data .site_par)))
+ face mode-line-buffer-id)
+ " " mode-name
+ " ["
+ "Answers: "
+ (:propertize
+ (:eval (number-to-string (let-alist sx-question-mode--data .answer_count)))
+ face mode-line-buffer-id)
+ ", "
+ "Stars: "
+ (:propertize
+ (:eval (number-to-string (or (let-alist sx-question-mode--data .favorite_count) 0)))
+ face mode-line-buffer-id)
+ ", "
+ "Views: "
+ (:propertize
+ (:eval (number-to-string (let-alist sx-question-mode--data .view_count)))
+ face mode-line-buffer-id)
+ "] ")
+ "Mode-line construct to use in `sx-question-mode' buffers.")
+
(define-derived-mode sx-question-mode special-mode "Question"
"Major mode to display and navigate a question and its answers.
Letters do not insert themselves; instead, they are commands.
+Don't activate this mode directly. Instead, to print a question
+on the current buffer use
+`sx-question-mode--erase-and-print-question'.
+
\\<sx-question-mode>
\\{sx-question-mode}"
(setq header-line-format sx-question-mode--header-line)
+ (setq mode-line-format sx-question-mode--mode-line)
;; Determine how to close this window.
(unless (window-parameter nil 'quit-restore)
(set-window-parameter
@@ -214,6 +250,7 @@ Letters do not insert themselves; instead, they are commands.
("v" sx-visit-externally)
("u" sx-upvote)
("d" sx-downvote)
+ ("O" sx-question-mode-order-by)
("q" quit-window)
(" " scroll-up-command)
("a" sx-answer)
@@ -256,6 +293,18 @@ query the api."
(unless (derived-mode-p 'sx-question-mode)
(error "Not in `sx-question-mode'")))
+(defun sx-question-mode-order-by (sort)
+ "Order answers in the current buffer by the method SORT.
+Sets `sx-question-list--order' and then calls
+`sx-question-list-refresh' with `redisplay'."
+ (interactive
+ (list (let ((order (sx-completing-read "Order answers by: "
+ (mapcar #'car sx-question-mode--sort-methods))))
+ (cdr-safe (assoc-string order sx-question-mode--sort-methods)))))
+ (when (and sort (functionp sort))
+ (setq sx-question-mode-answer-sort-function sort)
+ (sx-question-mode-refresh 'no-update)))
+
(provide 'sx-question-mode)
;;; sx-question-mode.el ends here
diff --git a/sx-question-print.el b/sx-question-print.el
index 56481cf..a575407 100644
--- a/sx-question-print.el
+++ b/sx-question-print.el
@@ -63,7 +63,7 @@ Some faces of this mode might be defined in the `sx-user' group."
:type 'string
:group 'sx-question-mode)
-(defcustom sx-question-mode-header-author-format "\nAuthor: %d %r"
+(defcustom sx-question-mode-header-author-format "\nAuthor: %d %r"
"String used to display the question author at the header.
% constructs have special meaning here. See `sx-user--format'."
:type 'string
@@ -74,7 +74,7 @@ Some faces of this mode might be defined in the `sx-user' group."
"Face used on the question date in the question buffer."
:group 'sx-question-mode-faces)
-(defcustom sx-question-mode-header-date "\nAsked on: "
+(defcustom sx-question-mode-header-date "\nPosted on: "
"String used before the question date at the header."
:type 'string
:group 'sx-question-mode)
@@ -100,12 +100,12 @@ Some faces of this mode might be defined in the `sx-user' group."
"Face used on <sub> and <sup> tags."
:group 'sx-question-mode-faces)
-(defcustom sx-question-mode-header-tags "\nTags: "
+(defcustom sx-question-mode-header-tags "\nTags: "
"String used before the question tags at the header."
:type 'string
:group 'sx-question-mode)
-(defcustom sx-question-mode-header-score "\nScore: "
+(defcustom sx-question-mode-header-score "\nScore: "
"String used before the question score at the header."
:type 'string
:group 'sx-question-mode)
@@ -136,6 +136,16 @@ the editor's name."
:type 'string
:group 'sx-question-mode)
+(defface sx-question-mode-accepted
+ '((t :foreground "ForestGreen" :inherit sx-question-mode-title))
+ "Face used for accepted answers in the question buffer."
+ :group 'sx-question-mode-faces)
+
+(defcustom sx-question-mode-answer-accepted-title "Accepted Answer"
+ "Title used at the start of accepted \"Answer\" section."
+ :type 'string
+ :group 'sx-question-mode)
+
(defcustom sx-question-mode-comments-title " Comments"
"Title used at the start of \"Comments\" sections."
:type 'string
@@ -153,13 +163,24 @@ replaced with the comment."
:type 'boolean
:group 'sx-question-mode)
+(defconst sx-question-mode--sort-methods
+ (let ((methods
+ '(("Higher-scoring" . sx-answer-higher-score-p)
+ ("Newer" . sx-answer-newer-p)
+ ("More active" . sx-answer-more-active-p))))
+ (append (mapcar (lambda (x) (cons (concat (car x) " first") (cdr x)))
+ methods)
+ (mapcar (lambda (x) (cons (concat (car x) " last")
+ (sx--invert-predicate (cdr x))))
+ methods))))
+
(defcustom sx-question-mode-answer-sort-function
#'sx-answer-higher-score-p
"Function used to sort answers in the question buffer."
- :type '(choice
- (const :tag "Higher-scoring first" sx-answer-higher-score-p)
- (const :tag "Newer first" sx-answer-newer-p)
- (const :tag "More active first" sx-answer-more-active-p))
+ :type
+ (cons 'choice
+ (mapcar (lambda (x) `(const :tag ,(car x) ,(cdr x)))
+ sx-question-mode--sort-methods))
:group 'sx-question-mode)
(defcustom sx-question-mode-use-images
@@ -208,22 +229,29 @@ DATA can represent a question or an answer."
(insert sx-question-mode-header-title)
(insert-text-button
;; Questions have title, Answers don't
- (or .title sx-question-mode-answer-title)
+ (cond (.title)
+ ((eq .is_accepted t) sx-question-mode-answer-accepted-title)
+ (t sx-question-mode-answer-title))
;; Section level
'sx-question-mode--section (if .title 1 2)
'sx-button-copy .share_link
+ 'face (if (eq .is_accepted t) 'sx-question-mode-accepted
+ 'sx-question-mode-title)
:type 'sx-question-mode-title)
+
;; Sections can be hidden with overlays
(sx--wrap-in-overlay
'(sx-question-mode--section-content t)
+
;; Author
(insert
(sx-user--format
(propertize sx-question-mode-header-author-format
'face 'sx-question-mode-header)
.owner))
+
+ ;; Date
(sx-question-mode--insert-header
- ;; Date
sx-question-mode-header-date
(concat
(sx-time-seconds-to-date .creation_date)
@@ -232,18 +260,22 @@ DATA can represent a question or an answer."
(sx-time-since .last_edit_date)
(sx-user--format "%d" .last_editor))))
'sx-question-mode-date)
+
+ ;; Score and upvoted/downvoted status.
(sx-question-mode--insert-header
sx-question-mode-header-score
- (format "%s" .score)
- (cond
- ((eq .upvoted t) 'sx-question-mode-score-upvoted)
- ((eq .downvoted t) 'sx-question-mode-score-downvoted)
- (t 'sx-question-mode-score)))
+ (format "%s%s" .score
+ (cond ((eq .upvoted t) "↑") ((eq .downvoted t) "↓") (t "")))
+ (cond ((eq .upvoted t) 'sx-question-mode-score-upvoted)
+ ((eq .downvoted t) 'sx-question-mode-score-downvoted)
+ (t 'sx-question-mode-score)))
+
+ ;; Tags
(when .title
;; Tags
(sx-question-mode--insert-header
sx-question-mode-header-tags
- (mapconcat #'sx-tag--format .tags " ")
+ (sx-tag--format-tags .tags .site_par)
nil))
;; Body
(insert "\n"
@@ -301,10 +333,10 @@ The comment is indented, filled, and then printed according to
(format sx-question-mode-comments-format
(sx-user--format "%d" .owner)
(substring
- ;; We fill with three spaces at the start, so the comment is
- ;; slightly indented.
(sx-question-mode--fill-and-fontify
- (concat " " .body_markdown))
+ ;; We fill with three spaces at the start, so the comment is
+ ;; slightly indented.
+ (concat " " (sx--squash-whitespace .body_markdown)))
;; Then we remove the spaces from the first line, since we'll
;; add the username there anyway.
3))))))
@@ -340,8 +372,9 @@ E.g.:
(defconst sx-question-mode--link-regexp
;; Done at compile time.
- (rx (or (and "[tag:" (group-n 5 (+ (not (any " ]")))) "]")
- (and (opt "!") "[" (group-n 1 (1+ (not (any "[]")))) "]"
+ (rx (or (and "[" (optional (group-n 6 "meta-")) "tag:"
+ (group-n 5 (+ (not (any " ]")))) "]")
+ (and (opt "!") "[" (group-n 1 (1+ (not (any "]")))) "]"
(or (and "(" (group-n 2 (1+ (not (any ")")))) ")")
(and "[" (group-n 3 (1+ (not (any "]")))) "]")))
(group-n 4 (and (and "http" (opt "s") "://") ""
diff --git a/sx-question.el b/sx-question.el
index a579639..1162eb9 100644
--- a/sx-question.el
+++ b/sx-question.el
@@ -148,7 +148,8 @@ See `sx-question--user-read-list'."
;; Question wasn't present.
(t
(sx-sorted-insert-skip-first
- q-cell site-cell (lambda (x y) (> (car x) (car y))))))))
+ q-cell site-cell
+ (lambda (x y) (> (or (car x) -1) (or (car y) -1))))))))
;; 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?
diff --git a/sx-request.el b/sx-request.el
index 7f18a2b..d7fd058 100644
--- a/sx-request.el
+++ b/sx-request.el
@@ -160,7 +160,7 @@ the main content of the response is returned."
(url-inhibit-uncompression t)
(url-request-data (sx-request--build-keyword-arguments args nil))
(request-url (concat sx-request-api-root method))
- (url-request-method (and request-method (symbol-name request-method)))
+ (url-request-method (and request-method (upcase (symbol-name request-method))))
(url-request-extra-headers
'(("Content-Type" . "application/x-www-form-urlencoded")))
(response-buffer (url-retrieve-synchronously request-url)))
diff --git a/sx-search.el b/sx-search.el
index c22a554..b245cbe 100644
--- a/sx-search.el
+++ b/sx-search.el
@@ -40,7 +40,9 @@
;;; Basic function
-(defun sx-search-get-questions (site page query &optional tags excluded-tags keywords)
+(defun sx-search-get-questions (site page query
+ &optional tags excluded-tags
+ &rest keywords)
"Like `sx-question-get-questions', but restrict results by a search.
Perform search on SITE. PAGE is an integer indicating which page
@@ -53,7 +55,6 @@ fail. EXCLUDED-TAGS is only is used if TAGS is also provided.
KEYWORDS is passed to `sx-method-call'."
(sx-method-call 'search
:keywords `((page . ,page)
- (sort . activity)
(intitle . ,query)
(tagged . ,tags)
(nottagged . ,excluded-tags)
@@ -62,8 +63,18 @@ KEYWORDS is passed to `sx-method-call'."
:auth t
:filter sx-browse-filter))
+(defconst sx-search--order-methods
+ (cons '("Relevance" . relevance)
+ (cl-remove-if (lambda (x) (eq (cdr x) 'hot))
+ (default-value 'sx-question-list--order-methods)))
+ "Alist of possible values to be passed to the `sort' keyword.")
+
+(defvar sx-search-default-order 'activity
+ "Default ordering method used on new searches.
+Possible values are the cdrs of `sx-search--order-methods'.")
+
-;;; User command
+;;;###autoload
(defun sx-search (site query &optional tags excluded-tags)
"Display search on SITE for question titles containing QUERY.
When TAGS is given, it is a lists of tags, one of which must
@@ -85,7 +96,7 @@ prefix argument, the user is asked for everything."
(when current-prefix-arg
(setq tags (sx-tag-multiple-read
site (concat "Tags" (when query " (optional)"))))
- (when (and (not query) (string= "" tags))
+ (unless (or query tags)
(sx-user-error "Must supply either QUERY or TAGS"))
(setq excluded-tags
(sx-tag-multiple-read site "Excluded tags (optional)")))
@@ -99,8 +110,11 @@ prefix argument, the user is asked for everything."
(lambda (page)
(sx-search-get-questions
sx-question-list--site page
- query tags excluded-tags)))
+ query tags excluded-tags
+ (cons 'sort sx-question-list--order))))
(setq sx-question-list--site site)
+ (setq sx-question-list--order sx-search-default-order)
+ (setq sx-question-list--order-methods sx-search--order-methods)
(sx-question-list-refresh 'redisplay)
(switch-to-buffer (current-buffer))))
@@ -112,9 +126,15 @@ prefix argument, the user is asked for everything."
(let ((tag (save-excursion
(when pos (goto-char pos))
(or (get-text-property (point) 'sx-tag)
- (thing-at-point 'symbol)))))
- (sx-search (or sx-question-list--site
- (sx-assoc-let sx-question-mode--data .site_par))
+ (thing-at-point 'symbol))))
+ (meta (save-excursion
+ (when pos (goto-char pos))
+ (get-text-property (point) 'sx-tag-meta)))
+ (site (replace-regexp-in-string
+ (rx string-start "meta.") ""
+ (or sx-question-list--site
+ (sx-assoc-let sx-question-mode--data .site_par)))))
+ (sx-search (concat (when meta "meta.") site)
nil tag)))
(provide 'sx-search)
diff --git a/sx-tag.el b/sx-tag.el
index 316226b..3c00ae2 100644
--- a/sx-tag.el
+++ b/sx-tag.el
@@ -142,19 +142,35 @@ tags."
;;; Printing
-(defun sx-tag--format (tag)
- "Format and return TAG for display."
+(defun sx-tag--format (tag &optional meta)
+ "Format and return TAG for display.
+If META is non-nil, the tag is for the meta site."
(with-temp-buffer
- (sx-tag--insert tag)
+ (sx-tag--insert tag meta)
(buffer-string)))
-(defun sx-tag--insert (tag)
- "Insert TAG button."
+(defun sx-tag--insert (tag &optional meta)
+ "Insert TAG button.
+If META is non-nil, the tag is for the meta site."
(insert-text-button (concat "[" tag "]")
'sx-button-copy tag
'sx-tag tag
+ 'sx-tag-meta meta
:type 'sx-button-tag))
+(defun sx-tag--format-tags (tags &optional site)
+ "Format and concatenate a sequence of TAGS.
+Returns a string of all tags in TAGS, separated by a space.
+
+SITE is the site to which the tags refer, it is only used to
+decide whether they are main or meta tags. SITE can also be t or
+nil, which respectively indicate meta and main."
+ (let ((is-meta
+ (if (stringp site) (string-match (rx string-start "meta.") site)
+ site)))
+ (mapconcat (lambda (tag) (sx-tag--format tag is-meta))
+ tags " ")))
+
(provide 'sx-tag)
;;; sx-tag.el ends here
diff --git a/sx.el b/sx.el
index e0609a7..e5e9c3e 100644
--- a/sx.el
+++ b/sx.el
@@ -4,7 +4,7 @@
;; Author: Sean Allred <code@seanallred.com>
;; URL: https://github.com/vermiculus/sx.el/
-;; Version: 0.1
+;; Version: 0.2
;; Keywords: help, hypermedia, tools
;; Package-Requires: ((emacs "24.1") (cl-lib "0.5") (json "1.3") (markdown-mode "2.0") (let-alist "1.0.3"))
@@ -28,7 +28,7 @@
;;; Code:
(require 'tabulated-list)
-(defconst sx-version "0.1" "Version of the `sx' package.")
+(defconst sx-version "0.2" "Version of the `sx' package.")
(defgroup sx nil
"Customization group for the `sx' package."
@@ -148,12 +148,7 @@ with a `link' property)."
;; From URL
(string-match (rx "/questions/"
;; Question ID
- (group-n 1 (+ digit)) "/"
- ;; Optional question title
- (optional (+ (not (any "/"))) "/")
- ;; Garbage at the end
- (optional (and (any "?#") (* any)))
- string-end)
+ (group-n 1 (+ digit)) "/")
link))
(push '(type . question) result)))
(push (cons 'id (string-to-number (match-string-no-properties 1 link)))
@@ -192,8 +187,19 @@ If ALIST doesn't have a `site' property, one is created using the
,(macroexpand
`(let-alist ,alist ,@body))))
+(defun sx--pretty-site-parameter (site)
+ "Returned a pretty and capitalized version of string SITE."
+ (mapconcat #'capitalize
+ (split-string site "\\.")
+ " "))
+
;;; Utility Functions
+(defun sx--split-string (string &optional separators)
+ "Split STRING into substrings bounded by matches for SEPARATORS."
+ (mapcar (lambda (s) (replace-regexp-in-string "\\` +\\| +\\'" "" s))
+ (split-string string separators 'omit-nulls)))
+
(defun sx-completing-read (&rest args)
"Like `completing-read', but possibly use ido.
All ARGS are passed to `completing-read' or `ido-completing-read'."
@@ -209,7 +215,7 @@ is intentionally skipped."
(while (and ;; We're not at the end.
(cdr-safe tail)
;; We're not at the right place.
- (,(or predicate #'<) x (cadr tail)))
+ (funcall (or ,predicate #'<) x (cadr tail)))
(setq tail (cdr tail)))
(setcdr tail (cons x (cdr tail)))))
@@ -336,6 +342,16 @@ GET-FUNC and performs the actual comparison."
(funcall ,get-func a)
(funcall ,get-func b))))
+(defun sx--squash-whitespace (string)
+ "Return STRING with consecutive whitespace squashed together."
+ (replace-regexp-in-string "[ \r\n]+" " " string))
+
+(defun sx--invert-predicate (predicate)
+ "Return PREDICATE function with arguments inverted.
+For instance (sx--invert-predicate #'<) is the same as #'>.
+Note this is not the same as negating PREDICATE."
+ (lambda (&rest args) (apply predicate (reverse args))))
+
;;; Printing request data
(defvar sx--overlays nil
diff --git a/sx.org b/sx.org
index e206cc2..014b0de 100644
--- a/sx.org
+++ b/sx.org
@@ -1,4 +1,4 @@
-#+MACRO: version 0.1
+#+MACRO: version 0.2
#+MACRO: versiondate 16 November 2014
#+MACRO: updated last updated {{{versiondate}}}
diff --git a/test/test-printing.el b/test/test-printing.el
index a6815e2..8016444 100644
--- a/test/test-printing.el
+++ b/test/test-printing.el
@@ -26,6 +26,18 @@ after being run through `sx-tag--format'."
;;; Tests
+(ert-deftest time-since ()
+ (cl-letf (((symbol-function #'float-time)
+ (lambda () 1420148997.)))
+ (should
+ (string=
+ "67m"
+ (sx-time-since 1420145000.)))
+ (should
+ (string=
+ "12h"
+ (sx-time-since 1420105000.)))))
+
(ert-deftest question-list-tag ()
"Test `sx-tag--format'."
(should
@@ -140,6 +152,17 @@ after being run through `sx-tag--format'."
(should
(equal object '((answers . [something "answer"]))))))
+
+;;; question-mode
+(ert-deftest sx-display-question ()
+ ;; Check it doesn't error.
+ (sx-display-question (elt sx-test-data-questions 0))
+ ;; Check it does error.
+ (should-error
+ (sx-display-question sx-test-data-questions))
+ (should-error
+ (sx-display-question sx-test-data-questions nil 1)))
+
(ert-deftest sx-question-mode--fill-and-fontify ()
"Check complicated questions are filled correctly."
(should
@@ -196,3 +219,4 @@ if you used the Stack Exchange login method, you'd...
[1]: http://i.stack.imgur.com/ktFTs.png
[2]: http://i.stack.imgur.com/5l2AY.png
[3]: http://i.stack.imgur.com/22myl.png")))
+
diff --git a/test/tests.el b/test/tests.el
index ce42a9f..be1552b 100644
--- a/test/tests.el
+++ b/test/tests.el
@@ -59,4 +59,24 @@
(apply #'message message args)))
(mapc #'sx-load-test
- '(api macros printing util search))
+ '(api macros printing util search state))
+
+(ert-deftest user-entry-functions ()
+ "Ensures all entry functions are autoloaded."
+ (should
+ (cl-every
+ #'fboundp
+ '(sx-ask
+ sx-authenticate
+ sx-bug-report
+ sx-switchto-map
+ sx-tab-featured
+ sx-tab-frontpage
+ sx-tab-hot
+ sx-tab-month
+ sx-tab-newest
+ sx-tab-starred
+ sx-tab-topvoted
+ sx-tab-unanswered
+ sx-tab-week
+ sx-version))))