;;; sx-question-mode.el --- major-mode for displaying questions -*- lexical-binding: t; -*- ;; Copyright (C) 2014 Artur Malabarba ;; Author: Artur Malabarba ;; 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 . ;;; Commentary: ;; This file provides a means to print questions with their answers ;; and all comments. See the customizable group `sx-question-mode'. ;;; Code: (eval-when-compile (require 'rx)) (require 'sx) (require 'sx-switchto) (require 'sx-question) (require 'sx-question-print) ;;; Displaying a question (defcustom sx-question-mode-display-buffer-function #'pop-to-buffer "Function used to display the question buffer. Called, for instance, when hitting \\`\\[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]. Common values for this variable are `pop-to-buffer' and `switch-to-buffer'." :type 'function :group 'sx-question-mode) (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.") (make-variable-buffer-local 'sx-question-mode--data) (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. Returns the question buffer." (with-current-buffer (sx-question-mode--display-buffer window) (sx-question-mode--erase-and-print-question data))) (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) (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 `sx-question-mode--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 "*sx-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 (funcall sx-question-mode-display-buffer-function sx-question-mode--buffer))) sx-question-mode--buffer) ;;; 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 0 "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") (let ((count (if n (abs n) 1))) (while (> count 0) ;; This will either move us to the next section, or move out of ;; the current one. (unless (sx--goto-property-change 'sx-question-mode--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--goto-property-change 'sx-question-mode--section n)) (unless (get-char-property (point) 'invisible) (cl-decf count)))) (when (equal (selected-window) (get-buffer-window)) (when sx-question-mode-recenter-line (let ((ov (sx-question-mode--section-overlays-at (line-end-position)))) (when (and (overlayp ov) (> (overlay-end ov) (window-end))) (recenter sx-question-mode-recenter-line)))) (sx-message-help-echo))) (defun sx-question-mode-previous-section (&optional n) "Move down to previous section (question or answer) of this buffer. Prefix argument moves N sections up or down." (interactive "p") (sx-question-mode-next-section (- (or n 1)))) (defun sx-question-mode-hide-show-section (&optional _) "Hide or show section under point. Optional argument _ is for `push-button'." (interactive) (let ((ov (or (sx-question-mode--section-overlays-at (line-end-position)) (sx-question-mode--section-overlays-at (point))))) (unless (overlayp ov) (sx-user-error "Not inside a question or answer")) (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 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 constants (defconst sx-question-mode--key-definitions '( ("" sx-question-mode-next-section) ("" sx-question-mode-previous-section) ("n" sx-question-mode-next-section "Navigate") ("p" sx-question-mode-previous-section "Navigate") ("g" sx-question-mode-refresh) ("v" sx-visit-externally) ("u" sx-upvote "upvote") ("d" sx-downvote "downvote") ("q" quit-window) ("SPC" scroll-up-command) ("e" sx-edit "edit") ("S" sx-search) ("*" sx-favorite "star") ("K" sx-delete "Delete") ("s" sx-switchto-map "switch-to") ("O" sx-question-mode-order-by "Order") ("c" sx-comment "comment") ("a" sx-answer "answer") ("TAB" forward-button "Navigate") ("" backward-button) ("" backward-button) ("" backward-button)) "List of key definitions for `sx-question-mode'. This list must follow the form described in `sx--key-definitions-to-header-line'.") (defconst sx-question-mode--header-line (sx--key-definitions-to-header-line sx-question-mode--key-definitions) "Header-line used on the question list.") ;;; Major-mode definition (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}" (setq header-line-format sx-question-mode--header-line) (setq mode-line-format sx-question-mode--mode-line) (buffer-disable-undo (current-buffer)) (set (make-local-variable 'nobreak-char-display) nil) ;; Determine how to close this window. (unless (window-parameter nil 'quit-restore) (set-window-parameter nil 'quit-restore `(other window nil ,(current-buffer)))) ;; We call font-lock-region manually. See `sx-question-mode--insert-markdown'. (font-lock-mode -1) (remove-hook 'after-change-functions 'markdown-check-change-for-wiki-link t) (remove-hook 'window-configuration-change-hook 'markdown-fontify-buffer-wiki-links t)) ;; We need this quote+eval combo because `kbd' was a macro in 24.2. (mapc (lambda (x) (eval `(define-key sx-question-mode-map (kbd ,(car x)) #',(cadr x)))) sx-question-mode--key-definitions) (defun sx-question-mode-refresh (&optional no-update) "Refresh currently displayed question. Queries the API for any changes to the question or its answers or comments, and redisplays it. With non-nil prefix argument NO-UPDATE, just redisplay, don't query the api." (interactive "P") (sx-question-mode--ensure-mode) (let ((point (point)) (line (count-screen-lines (window-start) (point)))) (sx-question-mode--erase-and-print-question (if no-update sx-question-mode--data (sx-assoc-let sx-question-mode--data (sx-question-get-question .site_par .question_id)))) (goto-char point) (when (equal (selected-window) (get-buffer-window (current-buffer))) (recenter line))) (sx-message "Done.")) (defun sx-question-mode--ensure-mode () "Ensures we are in question mode, erroring otherwise." (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 ;; Local Variables: ;; indent-tabs-mode: nil ;; End: