aboutsummaryrefslogtreecommitdiff
path: root/sx-question-mode.el
diff options
context:
space:
mode:
Diffstat (limited to 'sx-question-mode.el')
-rw-r--r--sx-question-mode.el436
1 files changed, 436 insertions, 0 deletions
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