From c05c57537aff8e258c8448ecc5492bc3e047a757 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Mon, 1 Dec 2014 15:58:59 +0000 Subject: Initial compose-mode implementation. --- sx-compose.el | 163 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 sx-compose.el diff --git a/sx-compose.el b/sx-compose.el new file mode 100644 index 0000000..edce659 --- /dev/null +++ b/sx-compose.el @@ -0,0 +1,163 @@ +;;; sx-compose.el --- Major-mode for coposing questions and answers. -*- 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: + + +;;; Code: +(require 'markdown-mode) + +(require 'sx) + +(defgroup sx-compose-mode nil + "Customization group for sx-compose-mode." + :prefix "sx-compose-mode-" + :tag "SX compose Mode" + :group 'sx) + + +;;; Faces and Variables +(defvar sx-compose-before-send-hook nil + "Hook run before POSTing to the API. +Functions are called without arguments and should return non-nil. + +Returning nil indicates something went wrong and the sending will +be aborted. In this case, the function is responsible for +notifying the user. + +Current buffer is the compose-mode buffer whose content is about +to be POSTed.") + +(defvar sx-compose-after-send-functions nil + "Hook run after POSTing to the API. +Functions on this hook should take one argument, the data +returned by `sx-compose--send-function' (usually the object +created by the API). They are only called if the transaction +succeeds.") + +(defvar sx-compose--send-function nil + "Function used by `sx-compose-send' to send the data. +Is invoked between `sx-compose-before-send-hook' and +`sx-compose-after-send-functions'.") + + +;;; Major-mode +(define-derived-mode sx-compose-mode markdown-mode "Compose" + "Major mode for coposing questions and answers. +Most of the functionality comes from `markdown-mode'. This mode +just implements some extra features related to posting to the +API. + +This mode won't function if `sx-compose--send-function' isn't +set. To make sure you set it correctly, you can create the buffer +with the `sx-compose--create' function. + +\\ +\\{sx-compose-mode}") + +(define-key sx-compose-mode-map "\C-c\C-c" #'sx-compose-send) + + +;;; Functions to help preparing buffers +(defun sx-compose--create (site parent &optional before-hooks after-functions) + "Create a `sx-compose-mode' buffer. +SITE is the site where it will be posted. + +If composing questions (not yet supported), PARENT is nil. +If composing answers, it is the `question_id'. +If editing answers or questions, it should be the alist data +related to that object. + +Each element of BEFORE-HOOKS and AFTER-FUNCTIONS are respectively +added locally to `sx-compose-before-send-hook' and +`sx-compose-after-send-functions'." + (or (integerp parent) (listp parent) + (error "Invalid PARENT")) + (with-current-buffer (sx-compose--get-buffer-create site parent) + (sx-compose-mode) + (setq sx-compose--send-function + (if (consp parent) + (sx-assoc-let parent + (lambda () (sx-method-call (if .title 'questions 'answers) + :site site + :keywords (sx-compose--generate-keywords .title) + :id (or .answer_id .question_id) + :submethod 'edit))) + (lambda () (sx-method-call 'questions + :site site + :keywords (sx-compose--generate-keywords (null parent)) + :id parent + :submethod (if parent 'answers/add 'add))))) + ;; Reverse so they're left in the same order. + (dolist (it (reverse before-hooks)) + (add-hook 'sx-compose-before-send-hook it nil t)) + (dolist (it (reverse after-functions)) + (add-hook 'sx-compose-after-send-functions it nil t)) + ;; Return the buffer + (current-buffer))) + +(defun sx-compose--generate-keywords (is-question) + "Reading current buffer, generate a keywords alist. +Keywords meant to be used in `sx-method-call'. + +`body_markdown' is read as the `buffer-string'. If IS-QUESTION is +non-nil, other keywords are read from the header " + (if (null is-question) + `((body_markdown . ,(buffer-string))) + ;; Question code will go here. + )) + +(defun sx-compose--get-buffer-create (site data) + "Get or create a buffer for use with `sx-compose-mode'. +SITE is the site for which composing is aimed (just used to +uniquely identify the buffers). + +If DATA is nil, get a fresh compose buffer. +If DATA is an integer, try to find an existing buffer +corresponding to that integer, otherwise create one. +If DATA is an alist (question or answer data), like above but use +the id property." + (cond + ((null data) + (generate-new-buffer + (format "*sx draft question %s*" site))) + ((integerp data) + (get-buffer-create + (format "*sx draft answer %s %s" + site data))) + (t + (get-buffer-create + (format "*sx draft edit %s %s" + site (sx-assoc-let data (or .answer_id .question_id))))))) + + +;;; Functions +(defun sx-compose-send () + "Finish composing current buffer and send it. +Calls `sx-compose-before-send-hook', POSTs the the current buffer +contents to the API, then calls `sx-compose-after-send-functions'." + (interactive) + (unless (run-hook-with-args-until-failure + sx-compose-before-send-hook) + (let ((result (funcall sx-compose--send-function))) + (run-hook-with-args sx-compose-after-send-functions + result)))) + +(provide 'sx-compose) +;;; sx-compose.el ends here -- cgit v1.2.3 From 5bba68bbbd76ae4db723354ba9112034cc0a724d Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Mon, 1 Dec 2014 16:36:07 +0000 Subject: Some after-send-hooks to improve experience --- sx-compose.el | 63 ++++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/sx-compose.el b/sx-compose.el index edce659..1eb4aa6 100644 --- a/sx-compose.el +++ b/sx-compose.el @@ -46,7 +46,8 @@ to be POSTed.") (defvar sx-compose-after-send-functions nil "Hook run after POSTing to the API. -Functions on this hook should take one argument, the data +Functions on this hook should take two arguments, the +`sx-compose-mode' buffer (which not be live) and the data returned by `sx-compose--send-function' (usually the object created by the API). They are only called if the transaction succeeds.") @@ -69,9 +70,38 @@ set. To make sure you set it correctly, you can create the buffer with the `sx-compose--create' function. \\ -\\{sx-compose-mode}") +\\{sx-compose-mode}" + (add-hook 'sx-compose-after-send-functions + #'sx-compose-quit nil t) + (add-hook 'sx-compose-after-send-functions + #'sx-compose--copy-as-kill nil t)) (define-key sx-compose-mode-map "\C-c\C-c" #'sx-compose-send) +(define-key sx-compose-mode-map "\C-c\C-k" #'sx-compose-quit) + +(defun sx-compose-send () + "Finish composing current buffer and send it. +Calls `sx-compose-before-send-hook', POSTs the the current buffer +contents to the API, then calls `sx-compose-after-send-functions'." + (interactive) + (when (run-hook-with-args-until-failure + sx-compose-before-send-hook) + (let ((result (funcall sx-compose--send-function))) + (with-demoted-errors + (run-hook-with-args sx-compose-after-send-functions + (current-buffer) result))))) + +(defun sx-compose-quit (buffer _) + "Kill BUFFER." + (interactive (list (current-buffer) nil)) + (when (buffer-live-p buffer) + (kill-buffer buffer))) + +(defun sx-compose--copy-as-kill (buffer _) + "Copy BUFFER contents to the kill-ring." + (when (buffer-live-p buffer) + (with-current-buffer buffer + (kill-new (buffer-string))))) ;;; Functions to help preparing buffers @@ -95,11 +125,17 @@ added locally to `sx-compose-before-send-hook' and (if (consp parent) (sx-assoc-let parent (lambda () (sx-method-call (if .title 'questions 'answers) + :auth 'warn + :url-method "POST" + :filter sx-browse-filter :site site :keywords (sx-compose--generate-keywords .title) :id (or .answer_id .question_id) :submethod 'edit))) (lambda () (sx-method-call 'questions + :auth 'warn + :url-method "POST" + :filter sx-browse-filter :site site :keywords (sx-compose--generate-keywords (null parent)) :id parent @@ -116,10 +152,10 @@ added locally to `sx-compose-before-send-hook' and "Reading current buffer, generate a keywords alist. Keywords meant to be used in `sx-method-call'. -`body_markdown' is read as the `buffer-string'. If IS-QUESTION is -non-nil, other keywords are read from the header " +`body' is read as the `buffer-string'. If IS-QUESTION is non-nil, +other keywords are read from the header " (if (null is-question) - `((body_markdown . ,(buffer-string))) + `((body . ,(buffer-string))) ;; Question code will go here. )) @@ -139,25 +175,12 @@ the id property." (format "*sx draft question %s*" site))) ((integerp data) (get-buffer-create - (format "*sx draft answer %s %s" + (format "*sx draft answer %s %s*" site data))) (t (get-buffer-create - (format "*sx draft edit %s %s" + (format "*sx draft edit %s %s*" site (sx-assoc-let data (or .answer_id .question_id))))))) - -;;; Functions -(defun sx-compose-send () - "Finish composing current buffer and send it. -Calls `sx-compose-before-send-hook', POSTs the the current buffer -contents to the API, then calls `sx-compose-after-send-functions'." - (interactive) - (unless (run-hook-with-args-until-failure - sx-compose-before-send-hook) - (let ((result (funcall sx-compose--send-function))) - (run-hook-with-args sx-compose-after-send-functions - result)))) - (provide 'sx-compose) ;;; sx-compose.el ends here -- cgit v1.2.3 From a803e5855553e7ce039d540a3550317e6805730d Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Mon, 1 Dec 2014 23:09:39 +0000 Subject: Rename before-functions --- sx-compose.el | 80 ++++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/sx-compose.el b/sx-compose.el index 1eb4aa6..45b2288 100644 --- a/sx-compose.el +++ b/sx-compose.el @@ -105,7 +105,17 @@ contents to the API, then calls `sx-compose-after-send-functions'." ;;; Functions to help preparing buffers -(defun sx-compose--create (site parent &optional before-hooks after-functions) +(defvar sx-compose--question-headers + (insert (concat + (propertize "Title: " 'rear-nonsticky t + 'read-only t + 'field 'sx-compose-header-title) + (propertize "\nTags: " 'rear-nonsticky t + 'field 'sx-compose-header-tags + 'read-only t ))) + "") + +(defun sx-compose--create (site parent &optional before-functions after-functions) "Create a `sx-compose-mode' buffer. SITE is the site where it will be posted. @@ -114,39 +124,47 @@ If composing answers, it is the `question_id'. If editing answers or questions, it should be the alist data related to that object. -Each element of BEFORE-HOOKS and AFTER-FUNCTIONS are respectively -added locally to `sx-compose-before-send-hook' and +Each element of BEFORE-FUNCTIONS and AFTER-FUNCTIONS are +respectively added locally to `sx-compose-before-send-hook' and `sx-compose-after-send-functions'." (or (integerp parent) (listp parent) (error "Invalid PARENT")) - (with-current-buffer (sx-compose--get-buffer-create site parent) - (sx-compose-mode) - (setq sx-compose--send-function - (if (consp parent) - (sx-assoc-let parent - (lambda () (sx-method-call (if .title 'questions 'answers) - :auth 'warn - :url-method "POST" - :filter sx-browse-filter - :site site - :keywords (sx-compose--generate-keywords .title) - :id (or .answer_id .question_id) - :submethod 'edit))) - (lambda () (sx-method-call 'questions - :auth 'warn - :url-method "POST" - :filter sx-browse-filter - :site site - :keywords (sx-compose--generate-keywords (null parent)) - :id parent - :submethod (if parent 'answers/add 'add))))) - ;; Reverse so they're left in the same order. - (dolist (it (reverse before-hooks)) - (add-hook 'sx-compose-before-send-hook it nil t)) - (dolist (it (reverse after-functions)) - (add-hook 'sx-compose-after-send-functions it nil t)) - ;; Return the buffer - (current-buffer))) + (let ((is-question + (and (listp parent) + (null (cdr (assoc 'answer_id parent)))))) + (with-current-buffer (sx-compose--get-buffer-create site parent) + (sx-compose-mode) + (setq sx-compose--send-function + (if (consp parent) + (sx-assoc-let parent + (lambda () (sx-method-call (if .title 'questions 'answers) + :auth 'warn + :url-method "POST" + :filter sx-browse-filter + :site site + :keywords (sx-compose--generate-keywords is-question) + :id (or .answer_id .question_id) + :submethod 'edit))) + (lambda () (sx-method-call 'questions + :auth 'warn + :url-method "POST" + :filter sx-browse-filter + :site site + :keywords (sx-compose--generate-keywords is-question) + :id parent + :submethod (if parent 'answers/add 'add))))) + ;; Reverse so they're left in the same order. + (dolist (it (reverse before-functions)) + (add-hook 'sx-compose-before-send-hook it nil t)) + (dolist (it (reverse after-functions)) + (add-hook 'sx-compose-after-send-functions it nil t)) + ;; If the buffer is empty, the draft didn't exist. So prepare the + ;; question. + (when (and is-question + (string= (buffer-string) "")) + (insert sx-compose--question-headers)) + ;; Return the buffer + (current-buffer)))) (defun sx-compose--generate-keywords (is-question) "Reading current buffer, generate a keywords alist. -- cgit v1.2.3 From aeb4303d3f0b0917f30fc9d9d66f1a4ca3d541b9 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Tue, 2 Dec 2014 00:13:41 +0000 Subject: Implement asking --- sx-compose.el | 63 ++++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/sx-compose.el b/sx-compose.el index 45b2288..e09240f 100644 --- a/sx-compose.el +++ b/sx-compose.el @@ -57,6 +57,16 @@ succeeds.") Is invoked between `sx-compose-before-send-hook' and `sx-compose-after-send-functions'.") +(defvar sx-compose--question-headers + (concat + #("Title: " 0 7 (intangible t read-only t rear-nonsticky t)) + #("\n" 0 1 (read-only t)) + #("Tags: " 0 7 (read-only t intangible t rear-nonsticky t)) + #("\n----------------------------------------\n" + 0 42 (read-only t))) + "Headers inserted when composing a new question. +Used by `sx-compose--create'.") + ;;; Major-mode (define-derived-mode sx-compose-mode markdown-mode "Compose" @@ -85,10 +95,10 @@ Calls `sx-compose-before-send-hook', POSTs the the current buffer contents to the API, then calls `sx-compose-after-send-functions'." (interactive) (when (run-hook-with-args-until-failure - sx-compose-before-send-hook) + 'sx-compose-before-send-hook) (let ((result (funcall sx-compose--send-function))) (with-demoted-errors - (run-hook-with-args sx-compose-after-send-functions + (run-hook-with-args 'sx-compose-after-send-functions (current-buffer) result))))) (defun sx-compose-quit (buffer _) @@ -105,21 +115,11 @@ contents to the API, then calls `sx-compose-after-send-functions'." ;;; Functions to help preparing buffers -(defvar sx-compose--question-headers - (insert (concat - (propertize "Title: " 'rear-nonsticky t - 'read-only t - 'field 'sx-compose-header-title) - (propertize "\nTags: " 'rear-nonsticky t - 'field 'sx-compose-header-tags - 'read-only t ))) - "") - (defun sx-compose--create (site parent &optional before-functions after-functions) "Create a `sx-compose-mode' buffer. SITE is the site where it will be posted. -If composing questions (not yet supported), PARENT is nil. +If composing questions, PARENT is nil. If composing answers, it is the `question_id'. If editing answers or questions, it should be the alist data related to that object. @@ -160,9 +160,11 @@ respectively added locally to `sx-compose-before-send-hook' and (add-hook 'sx-compose-after-send-functions it nil t)) ;; If the buffer is empty, the draft didn't exist. So prepare the ;; question. - (when (and is-question - (string= (buffer-string) "")) - (insert sx-compose--question-headers)) + (when (and is-question (string= (buffer-string) "")) + (let ((inhibit-point-motion-hooks)) + (insert sx-compose--question-headers) + (goto-char (point-min)) + (goto-char (line-end-position)))) ;; Return the buffer (current-buffer)))) @@ -172,10 +174,31 @@ Keywords meant to be used in `sx-method-call'. `body' is read as the `buffer-string'. If IS-QUESTION is non-nil, other keywords are read from the header " - (if (null is-question) - `((body . ,(buffer-string))) - ;; Question code will go here. - )) + `(,@(when is-question + (let ((inhibit-point-motion-hooks t) + (inhibit-read-only t) + (header-end + (next-single-property-change + (point-min) 'sx-compose-separator)) + keywords) + ;; Read the Title. + (goto-char (point-min)) + (when (search-forward-regexp "^Title: *\\(.*\\) *$" header-end 'noerror) + (error "No Title header found")) + (push (cons 'title (match-string 1)) keywords) + ;; And the tags + (goto-char (point-min)) + (unless (search-forward-regexp "^Tags: *\\([^[:space:]].*\\) *$" header-end 'noerror) + (error "No Tags header found")) + (push (cons 'tags (replace-regexp-in-string + "[[:space:],]" ";" (match-string 1))) + keywords) + ;; And erase the header so it doesn't get sent. + (delete-region + (point-min) + (next-single-property-change + header-end 'sx-compose-separator)))) + (body . ,(buffer-string)))) (defun sx-compose--get-buffer-create (site data) "Get or create a buffer for use with `sx-compose-mode'. -- cgit v1.2.3 From c6d0ee75c520041e9117422f7b8d5e86ca6cd5d4 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Tue, 2 Dec 2014 00:32:03 +0000 Subject: sx-answer to start composing from the question buffer --- sx-interaction.el | 43 +++++++++++++++++++++++++++++++++++++++---- sx-question-mode.el | 1 + 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/sx-interaction.el b/sx-interaction.el index 305e61c..3f8242c 100644 --- a/sx-interaction.el +++ b/sx-interaction.el @@ -189,13 +189,48 @@ OBJECT can be a question or an answer." (setcdr com-cell (apply #'vector - (append - (cl-map 'list #'identity - (cdr com-cell)) - (list comment))))) + (append + (cl-map 'list #'identity + (cdr com-cell)) + (list comment))))) ;; No previous comments, add it manually. (setcdr object (cons (car object) (cdr object))) (setcar object `(comments . [,comment]))))) + +;;; Answering +(defun sx-answer (data) + "Start composing an answer for question given by DATA. +DATA is a question alist. Interactively, it is guessed from +context at point. + +TEXT is a string. Interactively, it is read from the minibufer." + ;; Answering doesn't really make sense from anywhere other than + ;; inside a question. So we don't need `sx--data-here' here. + (interactive (list sx-question-mode--data)) + ;; When clicking the "Write an Answer" button, first arg is a marker. + (when (markerp data) (setq data (sx--data-here))) + (let ((buffer (current-buffer))) + (sx-assoc-let data + (pop-to-buffer + (sx-compose--create + .site .question_id nil + ;; After change functions + (lambda (_ res) + (sx--add-answer-to-question-object res sx-question-mode--data) + (when (buffer-live-p buffer) + (with-current-buffer buffer + (sx-question-mode-refresh 'no-update))))))))) + +(defun sx--add-answer-to-question-object (answer question) + "Add alist ANSWER to alist QUESTION in the right place." + (let ((cell (assoc 'answers question))) + (if cell + (setcdr cell (apply #'vector + (append (cdr cell) (list answer)))) + ;; No previous comments, add it manually. + (setcdr question (cons (car question) (cdr question))) + (setcar question `(answers . [,answer]))))) + (provide 'sx-interaction) ;;; sx-interaction.el ends here diff --git a/sx-question-mode.el b/sx-question-mode.el index c44519c..24b2cfb 100644 --- a/sx-question-mode.el +++ b/sx-question-mode.el @@ -188,6 +188,7 @@ Letters do not insert themselves; instead, they are commands. ("d" sx-toggle-downvote) ("q" quit-window) (" " scroll-up-command) + ("a" sx-answer) (,(kbd "S-SPC") scroll-down-command) ([backspace] scroll-down-command) ([tab] forward-button) -- cgit v1.2.3 From 142916ca3639995e7a06277ded627b3b01ee4931 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Tue, 2 Dec 2014 00:43:37 +0000 Subject: Add an answer button --- sx-button.el | 8 ++++++++ sx-question-print.el | 3 +++ 2 files changed, 11 insertions(+) diff --git a/sx-button.el b/sx-button.el index c1abf90..cd01598 100644 --- a/sx-button.el +++ b/sx-button.el @@ -121,6 +121,14 @@ code-block." 'action #'sx-comment :supertype 'sx-button) +(define-button-type 'sx-button-answer + 'help-echo (concat "mouse-1, RET" + (propertize ": write an answer" + 'face 'minibuffer-prompt)) + 'face 'custom-button + 'action #'sx-answer + :supertype 'sx-button) + (provide 'sx-button) ;;; sx-button.el ends here diff --git a/sx-question-print.el b/sx-question-print.el index 307742a..f206f56 100644 --- a/sx-question-print.el +++ b/sx-question-print.el @@ -177,6 +177,9 @@ 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)) + (insert "\n\n ") + (insert-text-button "Write an Answer" :type 'sx-button-answer) + ;; Reposition (goto-char (point-min)) (sx-question-mode-next-section)) -- cgit v1.2.3 From 34cdd0f1f21c99914d58b0acea26c848ff8aa67c Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Tue, 2 Dec 2014 00:45:55 +0000 Subject: Prettier comment button --- sx-button.el | 1 + 1 file changed, 1 insertion(+) diff --git a/sx-button.el b/sx-button.el index cd01598..8f0b6b9 100644 --- a/sx-button.el +++ b/sx-button.el @@ -118,6 +118,7 @@ code-block." 'help-echo (concat "mouse-1, RET" (propertize ": write a comment" 'face 'minibuffer-prompt)) + 'face 'custom-button 'action #'sx-comment :supertype 'sx-button) -- cgit v1.2.3 From 5fbfe0929d2d9f3e0747679db8add0d61a2184a4 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Tue, 2 Dec 2014 00:52:25 +0000 Subject: Fix doc --- sx-interaction.el | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sx-interaction.el b/sx-interaction.el index 3f8242c..1a773f4 100644 --- a/sx-interaction.el +++ b/sx-interaction.el @@ -202,9 +202,7 @@ OBJECT can be a question or an answer." (defun sx-answer (data) "Start composing an answer for question given by DATA. DATA is a question alist. Interactively, it is guessed from -context at point. - -TEXT is a string. Interactively, it is read from the minibufer." +context at point. " ;; Answering doesn't really make sense from anywhere other than ;; inside a question. So we don't need `sx--data-here' here. (interactive (list sx-question-mode--data)) -- cgit v1.2.3 From b8d3e61e0d02796fdd4d0c973dc4fbc9e856baa6 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Tue, 2 Dec 2014 00:57:44 +0000 Subject: Simplify update-display code --- sx-interaction.el | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/sx-interaction.el b/sx-interaction.el index 1a773f4..8673400 100644 --- a/sx-interaction.el +++ b/sx-interaction.el @@ -37,13 +37,15 @@ (and (derived-mode-p 'sx-question-mode) sx-question-mode--data))) -(defun sx--maybe-update-display () - "Refresh the question list if we're inside it." - (cond - ((derived-mode-p 'sx-question-list-mode) - (sx-question-list-refresh 'redisplay 'no-update)) - ((derived-mode-p 'sx-question-mode) - (sx-question-mode-refresh 'no-update)))) +(defun sx--maybe-update-display (&optional buffer) + "Refresh whatever is displayed in BUFFER or the current buffer. +If BUFFER is not live, nothing is done." + (setq buffer (or buffer (current-buffer))) + (when (buffer-live-p buffer) + (cond ((derived-mode-p 'sx-question-list-mode) + (sx-question-list-refresh 'redisplay 'no-update)) + ((derived-mode-p 'sx-question-mode) + (sx-question-mode-refresh 'no-update))))) (defun sx--copy-data (from to) "Copy all fields of alist FORM onto TO. @@ -216,9 +218,7 @@ context at point. " ;; After change functions (lambda (_ res) (sx--add-answer-to-question-object res sx-question-mode--data) - (when (buffer-live-p buffer) - (with-current-buffer buffer - (sx-question-mode-refresh 'no-update))))))))) + (sx--maybe-update-display buffer))))))) (defun sx--add-answer-to-question-object (answer question) "Add alist ANSWER to alist QUESTION in the right place." -- cgit v1.2.3 From 59c49ce60fcfa1fac4827beb11a04c0ef8585b9a Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Tue, 2 Dec 2014 01:20:09 +0000 Subject: Editing implemented Fix #11 --- sx-compose.el | 5 +++++ sx-interaction.el | 31 +++++++++++++++++++++++++++---- sx-question-mode.el | 1 + 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/sx-compose.el b/sx-compose.el index e09240f..dcb1bb0 100644 --- a/sx-compose.el +++ b/sx-compose.el @@ -165,6 +165,11 @@ respectively added locally to `sx-compose-before-send-hook' and (insert sx-compose--question-headers) (goto-char (point-min)) (goto-char (line-end-position)))) + (when (consp parent) + (when (or (string= (buffer-string) "") + (y-or-n-p "Draft buffer exists. Reset it? ")) + (erase-buffer) + (insert (cdr (assoc 'body_markdown parent))))) ;; Return the buffer (current-buffer)))) diff --git a/sx-interaction.el b/sx-interaction.el index 8673400..df871dd 100644 --- a/sx-interaction.el +++ b/sx-interaction.el @@ -199,6 +199,28 @@ OBJECT can be a question or an answer." (setcdr object (cons (car object) (cdr object))) (setcar object `(comments . [,comment]))))) + +;;; Editing +(defun sx-edit (data) + "Start editing an answer or question given by DATA. +DATA is an answer or question alist. Interactively, it is guessed +from context at point." + ;; Answering doesn't really make sense from anywhere other than + ;; inside a question. So we don't need `sx--data-here' here. + (interactive (list (sx--data-here))) + ;; If we ever make an "Edit" button, first arg is a marker. + (when (markerp data) (setq data (sx--data-here))) + (sx-assoc-let data + (when .comment_id (user-error "Editing comments is not supported yet")) + (let ((buffer (current-buffer))) + (pop-to-buffer + (sx-compose--create + .site data nil + ;; After send functions + (list (lambda (_ res) + (sx--copy-data (elt res 0) data) + (sx--maybe-update-display buffer)))))))) + ;;; Answering (defun sx-answer (data) @@ -215,10 +237,11 @@ context at point. " (pop-to-buffer (sx-compose--create .site .question_id nil - ;; After change functions - (lambda (_ res) - (sx--add-answer-to-question-object res sx-question-mode--data) - (sx--maybe-update-display buffer))))))) + ;; After send functions + (list (lambda (_ res) + (sx--add-answer-to-question-object + (elt res 0) sx-question-mode--data) + (sx--maybe-update-display buffer)))))))) (defun sx--add-answer-to-question-object (answer question) "Add alist ANSWER to alist QUESTION in the right place." diff --git a/sx-question-mode.el b/sx-question-mode.el index 24b2cfb..cc4c082 100644 --- a/sx-question-mode.el +++ b/sx-question-mode.el @@ -189,6 +189,7 @@ Letters do not insert themselves; instead, they are commands. ("q" quit-window) (" " scroll-up-command) ("a" sx-answer) + ("e" sx-edit) (,(kbd "S-SPC") scroll-down-command) ([backspace] scroll-down-command) ([tab] forward-button) -- cgit v1.2.3 From f939c9d9fd7a2e7aabe23b2a8084652d12ca5751 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Tue, 2 Dec 2014 01:20:34 +0000 Subject: Fix bug introduced by new use of overlays. --- sx-question-mode.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sx-question-mode.el b/sx-question-mode.el index cc4c082..b685ea7 100644 --- a/sx-question-mode.el +++ b/sx-question-mode.el @@ -110,7 +110,7 @@ Prefix argument N moves N sections down or up." (cl-decf count))))) (when (equal (selected-window) (get-buffer-window)) (when sx-question-mode-recenter-line - (let ((ov (car-safe (sx-question-mode--section-overlays-at (line-end-position))))) + (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))) -- cgit v1.2.3 From 1174858c8f7e87546e671c7322850e2c9e22de4d Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Tue, 2 Dec 2014 01:33:59 +0000 Subject: Implement a command for asking. --- sx-interaction.el | 17 +++++++++++++++++ sx-question-list.el | 1 + sx-tab.el | 22 +++++++++++++++------- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/sx-interaction.el b/sx-interaction.el index df871dd..ce00889 100644 --- a/sx-interaction.el +++ b/sx-interaction.el @@ -26,6 +26,8 @@ (require 'sx-question) (require 'sx-question-mode) (require 'sx-question-list) +(require 'sx-compose) +(require 'sx-tab) ;;; Using data in buffer @@ -221,6 +223,21 @@ from context at point." (sx--copy-data (elt res 0) data) (sx--maybe-update-display buffer)))))))) + +;;; Asking +(defun sx-ask (site) + "Start composing a question for SITE. +SITE is a string, indicating where the question will be posted." + ;; Answering doesn't really make sense from anywhere other than + ;; inside a question. So we don't need `sx--data-here' here. + (interactive (list (sx-tab--interactive-site-prompt))) + (let ((buffer (current-buffer))) + (pop-to-buffer + (sx-compose--create + site nil nil + ;; After send functions + (list (lambda (_ res) (sx--maybe-update-display buffer))))))) + ;;; Answering (defun sx-answer (data) diff --git a/sx-question-list.el b/sx-question-list.el index 9709b99..2bfcce0 100644 --- a/sx-question-list.el +++ b/sx-question-list.el @@ -299,6 +299,7 @@ into consideration. ("K" sx-question-list-previous-far) ("g" sx-question-list-refresh) (":" sx-question-list-switch-site) + ("a" sx-ask) ("v" sx-visit) ("u" sx-toggle-upvote) ("d" sx-toggle-downvote) diff --git a/sx-tab.el b/sx-tab.el index 5026a73..873e213 100644 --- a/sx-tab.el +++ b/sx-tab.el @@ -26,13 +26,24 @@ (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) +(defun sx-tab--interactive-site-prompt () + "Query the user for a site." + (let ((default (or sx-question-list--site + (sx-assoc-let sx-question-mode--data + .site) + sx-tab-default-site))) + (funcall (if ido-mode #'ido-completing-read #'completing-read) + (format "Site (%s): " default) + (sx-site-get-api-tokens) nil t nil nil + default))) + +;;; The main macro (defmacro sx-tab--define (tab pager &optional printer refresher &rest body) "Define a StackExchange tab called TAB. @@ -56,7 +67,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) @@ -64,13 +75,10 @@ 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) - (format "Site (%s): " sx-tab-default-site) - (sx-site-get-api-tokens) nil t nil nil - sx-tab-default-site))) + (sx-tab--interactive-site-prompt))) (sx-initialize) (unless site (setq site sx-tab-default-site)) ;; Create the buffer -- cgit v1.2.3 From 89a45e4724089a8bfdecaa6f58ae5394c10d85e3 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Tue, 2 Dec 2014 01:48:55 +0000 Subject: Fix up asking logic --- sx-compose.el | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/sx-compose.el b/sx-compose.el index dcb1bb0..0ae1f93 100644 --- a/sx-compose.el +++ b/sx-compose.el @@ -61,9 +61,11 @@ Is invoked between `sx-compose-before-send-hook' and (concat #("Title: " 0 7 (intangible t read-only t rear-nonsticky t)) #("\n" 0 1 (read-only t)) - #("Tags: " 0 7 (read-only t intangible t rear-nonsticky t)) - #("\n----------------------------------------\n" - 0 42 (read-only t))) + #("Tags : " 0 7 (read-only t intangible t rear-nonsticky t)) + #("\n" 0 1 (read-only t rear-nonsticky t)) + #("________________________________________\n\n" + 0 42 (read-only t rear-nonsticky t intangible t + sx-compose-separator t))) "Headers inserted when composing a new question. Used by `sx-compose--create'.") @@ -188,21 +190,24 @@ other keywords are read from the header " keywords) ;; Read the Title. (goto-char (point-min)) - (when (search-forward-regexp "^Title: *\\(.*\\) *$" header-end 'noerror) + (unless (search-forward-regexp + "^Title: *\\(.*\\) *$" header-end 'noerror) (error "No Title header found")) (push (cons 'title (match-string 1)) keywords) ;; And the tags (goto-char (point-min)) - (unless (search-forward-regexp "^Tags: *\\([^[:space:]].*\\) *$" header-end 'noerror) + (unless (search-forward-regexp "^Tags : *\\([^[:space:]].*\\) *$" + header-end 'noerror) (error "No Tags header found")) - (push (cons 'tags (replace-regexp-in-string - "[[:space:],]" ";" (match-string 1))) + (push (cons 'tags (split-string (match-string 1) "[[:space:],;]" + 'omit-nulls "[[:space:]]")) keywords) ;; And erase the header so it doesn't get sent. (delete-region (point-min) (next-single-property-change - header-end 'sx-compose-separator)))) + header-end 'sx-compose-separator)) + keywords)) (body . ,(buffer-string)))) (defun sx-compose--get-buffer-create (site data) -- cgit v1.2.3 From f800936f58d13d8de97eab4b57d3965256482e38 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Tue, 2 Dec 2014 02:18:25 +0000 Subject: Refactor sx-compose-create --- sx-compose.el | 8 ++++---- sx-interaction.el | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sx-compose.el b/sx-compose.el index 0ae1f93..273e02d 100644 --- a/sx-compose.el +++ b/sx-compose.el @@ -67,7 +67,7 @@ Is invoked between `sx-compose-before-send-hook' and 0 42 (read-only t rear-nonsticky t intangible t sx-compose-separator t))) "Headers inserted when composing a new question. -Used by `sx-compose--create'.") +Used by `sx-compose-create'.") ;;; Major-mode @@ -79,7 +79,7 @@ API. This mode won't function if `sx-compose--send-function' isn't set. To make sure you set it correctly, you can create the buffer -with the `sx-compose--create' function. +with the `sx-compose-create' function. \\ \\{sx-compose-mode}" @@ -117,8 +117,8 @@ contents to the API, then calls `sx-compose-after-send-functions'." ;;; Functions to help preparing buffers -(defun sx-compose--create (site parent &optional before-functions after-functions) - "Create a `sx-compose-mode' buffer. +(defun sx-compose-create (site parent &optional before-functions after-functions) + "Create an `sx-compose-mode' buffer. SITE is the site where it will be posted. If composing questions, PARENT is nil. diff --git a/sx-interaction.el b/sx-interaction.el index ce00889..85d681c 100644 --- a/sx-interaction.el +++ b/sx-interaction.el @@ -216,7 +216,7 @@ from context at point." (when .comment_id (user-error "Editing comments is not supported yet")) (let ((buffer (current-buffer))) (pop-to-buffer - (sx-compose--create + (sx-compose-create .site data nil ;; After send functions (list (lambda (_ res) @@ -233,7 +233,7 @@ SITE is a string, indicating where the question will be posted." (interactive (list (sx-tab--interactive-site-prompt))) (let ((buffer (current-buffer))) (pop-to-buffer - (sx-compose--create + (sx-compose-create site nil nil ;; After send functions (list (lambda (_ res) (sx--maybe-update-display buffer))))))) @@ -252,7 +252,7 @@ context at point. " (let ((buffer (current-buffer))) (sx-assoc-let data (pop-to-buffer - (sx-compose--create + (sx-compose-create .site .question_id nil ;; After send functions (list (lambda (_ res) -- cgit v1.2.3 From e5ebab3f99537b2dab0a7264f7e280564fdd6e6b Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Tue, 2 Dec 2014 02:18:43 +0000 Subject: Header Commentary to sx-compose --- sx-compose.el | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/sx-compose.el b/sx-compose.el index 273e02d..f5aef79 100644 --- a/sx-compose.el +++ b/sx-compose.el @@ -19,6 +19,16 @@ ;;; Commentary: +;; This file defines `sx-compose-mode' and its auxiliary functions and +;; variables. In order to use `sx-compose-mode', it is adamant that +;; the variable `sx-compose--send-function' be set. Otherwise it's +;; just a regular markdown buffer. +;; +;; In order to help avoid mistakes, there is the function +;; `sx-compose-create'. This is the preferred way of activating the +;; mode. It creates a buffer, activates the major mode, and sets the +;; `send-function' variable according to the arguments it is given. + ;;; Code: (require 'markdown-mode) -- cgit v1.2.3 From ac7a451140e4a431c956092dd77d3df4e74bfbf4 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Tue, 2 Dec 2014 02:23:43 +0000 Subject: Header Commentary to sx-interaction --- sx-interaction.el | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/sx-interaction.el b/sx-interaction.el index 85d681c..d0c4c47 100644 --- a/sx-interaction.el +++ b/sx-interaction.el @@ -19,6 +19,21 @@ ;;; Commentary: +;; This file holds a series of functions for performing arbitrary +;; interactions with arbitrary objects (objects here always mean the +;; alist of a question, answer, or comment). All commands take at +;; least a DATA argument corresponding to the object which, when +;; called interactively, is always derived from the context at point +;; (usually using the `sx--data-here' function). +;; +;; Interactions represented here involve voting, commenting, asking, +;; answering, editing. +;; +;; These are commands are meant to be available throughout the +;; interface. So it didn't make sense to put them in a specific +;; module. They also rely on a lot of dependencies, so they couldn't +;; be put in sx.el. + ;;; Code: @@ -70,7 +85,7 @@ If DATA is a question, also mark it as read." (let ((link (when (stringp .link) (funcall (if copy-as-kill #'kill-new #'browse-url) - .link)))) + .link)))) (when (and (called-interactively-p 'any) copy-as-kill) (message "Copied: %S" link))) (when (and .title (not copy-as-kill)) -- cgit v1.2.3 From 6f2f7f6cc68a673c75ae0ec9059c299967b43975 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Wed, 3 Dec 2014 14:12:03 +0000 Subject: Header commentary on sx-button Affects #123 --- sx-button.el | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/sx-button.el b/sx-button.el index 8f0b6b9..f1d7e4d 100644 --- a/sx-button.el +++ b/sx-button.el @@ -1,4 +1,4 @@ -;;; sx-button.el --- Defining buttons used throughout SX. +;;; sx-button.el --- Defining buttons used throughout SX. -*- lexical-binding: t; -*- ;; Copyright (C) 2014 Artur Malabarba @@ -18,6 +18,25 @@ ;; along with this program. If not, see . ;;; Commentary: +;; +;; This file defines all buttons used by SX. For information on +;; buttons, see: +;; http://www.gnu.org/software/emacs/manual/html_node/elisp/Buttons.html +;; +;; Most interactible parts of the SX buffers are buttons. Wherever you +;; are, you can always cycle through all buttons by hitting `TAB', +;; that should help identify what's a button in each buffer. +;; +;; To define a new type of button follow the examples below using +;; `define-button-type' with :supertype `sx-button'. Required +;; properties are `action' and `help-echo'. You'll probably want to +;; give it a `face' as well, unless you want it to look like a link. +;; +;; Buttons can then be inserted in their respective files using +;; `insert-text-button'. Give it the string, the `:type' you defined, +;; and any aditional properties that can only be determined at +;; creation. Existing text can be transformed into a button with +;; `make-text-button' instead. ;;; Code: @@ -132,7 +151,3 @@ code-block." (provide 'sx-button) ;;; sx-button.el ends here - -;; Local Variables: -;; lexical-binding: t -;; End: -- cgit v1.2.3 From 46de3f0ddba5a7611f025b816d637136593b20b1 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Wed, 3 Dec 2014 14:13:29 +0000 Subject: Not adamant --- sx-compose.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sx-compose.el b/sx-compose.el index f5aef79..d7d3ff3 100644 --- a/sx-compose.el +++ b/sx-compose.el @@ -20,9 +20,9 @@ ;;; Commentary: ;; This file defines `sx-compose-mode' and its auxiliary functions and -;; variables. In order to use `sx-compose-mode', it is adamant that -;; the variable `sx-compose--send-function' be set. Otherwise it's -;; just a regular markdown buffer. +;; variables. In order to use `sx-compose-mode', it is vital that the +;; variable `sx-compose--send-function' be set. Otherwise it's just a +;; regular markdown buffer. ;; ;; In order to help avoid mistakes, there is the function ;; `sx-compose-create'. This is the preferred way of activating the -- cgit v1.2.3 From 7373bde63cbb066d9aa4f05d0ac04d7ec2781cba Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Wed, 3 Dec 2014 15:08:29 +0000 Subject: Extend sx--data-here --- sx-interaction.el | 73 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/sx-interaction.el b/sx-interaction.el index 9a5bbcb..1603ca7 100644 --- a/sx-interaction.el +++ b/sx-interaction.el @@ -36,6 +36,8 @@ ;;; Code: +(eval-when-compile + '(require 'cl-lib)) (require 'sx) (require 'sx-question) @@ -46,20 +48,46 @@ ;;; Using data in buffer -(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)) - (and (derived-mode-p 'sx-question-mode) - sx-question-mode--data) - (and (null noerror) - (error "No question data found here")))) +(cl-defun sx--data-here (&key (error t) + (type nil) + (question-read-p nil)) + "Get the alist regarding object under point. +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. + +Possible keyword arguments are: + + :error If explicit given as nil, no errors are thrown. + :type Symbol restricting the type of object desired. Possible + values are 'question, 'answer, 'comment. If nothing is found of + that type. + :question-read-p If non-nil, and if object found is a question, + throw a `user-error' if it isn't `sx-question--read-p'. If + object found is not a question, this argument is ignored." + (let ((result + (or (let ((data (get-char-property (point) 'sx--data-here))) + (if (null type) data + (sx-assoc-let type + ;; Is data of the right type? + (cl-case type + (question (when .title data)) + (answer (when .answer_id data)) + (comment (when .comment_id data)))))) + ;; The following two only ever return questions. + (when (or (null type) (eq type 'question)) + ;; @TODO: `sx-question-list-mode' may one day display answers. + (or (and (derived-mode-p 'sx-question-list-mode) + (tabulated-list-get-id)) + (and (derived-mode-p 'sx-question-mode) + sx-question-mode--data))) + ;; Nothing was found + (and error (error "No %s found here" (or type "data")))))) + ;; If we found a question, we may need to check if it's read. + (if (and question-read-p (assoc 'title result)) + (if (sx-question--read-p result) result + (user-error "Question still unread. View it before acting on it")) + result))) (defun sx--maybe-update-display (&optional buffer) "Refresh whatever is displayed in BUFFER or the current buffer. @@ -129,7 +157,7 @@ If WINDOW nil, the window is decided by "Apply or remove upvote from DATA. DATA can be a question, answer, or comment. Interactively, it is guessed from context at point." - (interactive (list (sx--data-here))) + (interactive (list (sx--data-here :question-read-p t))) (sx-assoc-let data (sx-set-vote data "upvote" (null (eq .upvoted t))))) @@ -137,7 +165,7 @@ guessed from context at point." "Apply or remove downvote from DATA. DATA can be a question or an answer. Interactively, it is guessed from context at point." - (interactive (list (sx--data-here))) + (interactive (list (sx--data-here :question-read-p t))) (sx-assoc-let data (sx-set-vote data "downvote" (null (eq .downvoted t))))) @@ -176,7 +204,7 @@ 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 :question-read-p t) 'query)) ;; When clicking the "Add a Comment" button, first arg is a marker. (when (markerp data) (setq data (sx--data-here)) @@ -255,8 +283,6 @@ OBJECT can be a question or an answer." "Start editing an answer or question given by DATA. DATA is an answer or question alist. Interactively, it is guessed from context at point." - ;; Answering doesn't really make sense from anywhere other than - ;; inside a question. So we don't need `sx--data-here' here. (interactive (list (sx--data-here))) ;; If we ever make an "Edit" button, first arg is a marker. (when (markerp data) (setq data (sx--data-here))) @@ -276,8 +302,6 @@ from context at point." (defun sx-ask (site) "Start composing a question for SITE. SITE is a string, indicating where the question will be posted." - ;; Answering doesn't really make sense from anywhere other than - ;; inside a question. So we don't need `sx--data-here' here. (interactive (list (sx-tab--interactive-site-prompt))) (let ((buffer (current-buffer))) (pop-to-buffer @@ -292,9 +316,10 @@ SITE is a string, indicating where the question will be posted." "Start composing an answer for question given by DATA. DATA is a question alist. Interactively, it is guessed from context at point. " - ;; Answering doesn't really make sense from anywhere other than - ;; inside a question. So we don't need `sx--data-here' here. - (interactive (list sx-question-mode--data)) + ;; If the user tries to answer a question that's not viewed, he + ;; probaby hit the button by accident. + (interactive + (list (sx--data-here :question-read-p t :type 'question))) ;; When clicking the "Write an Answer" button, first arg is a marker. (when (markerp data) (setq data (sx--data-here))) (let ((buffer (current-buffer))) -- cgit v1.2.3 From 7ecd6404ea465c547f3217016c1940f94682fb96 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Wed, 3 Dec 2014 15:24:15 +0000 Subject: Add comments to sx--unindent-text --- sx.el | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sx.el b/sx.el index f1d3634..3010bdc 100644 --- a/sx.el +++ b/sx.el @@ -189,24 +189,31 @@ Anything before the (sub)domain is removed." "" url))) (defun sx--unindent-text (text) - "Remove indentation from TEXT." + "Remove indentation from TEXT. +Primarily designed to extract the content of markdown code +blocks." (with-temp-buffer (insert text) (goto-char (point-min)) (let (result) + ;; Get indentation of each non-blank line (while (null (eobp)) (skip-chars-forward "[:blank:]") (unless (looking-at "$") (push (current-column) result)) (forward-line 1)) (when result + ;; Build a regexp with the smallest indentation (let ((rx (format "^ \\{0,%s\\}" (apply #'min result)))) (goto-char (point-min)) + ;; Use this regexp to remove that much indentation + ;; throughout the buffer. (while (and (null (eobp)) (search-forward-regexp rx nil 'noerror)) (replace-match "") (forward-line 1))))) + ;; Return the buffer (buffer-string))) -- cgit v1.2.3 From 034430657c658e712de5ce8b827cf63ff45ca6e3 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Wed, 3 Dec 2014 15:28:49 +0000 Subject: sx--site gracefully accepts nil argument --- sx.el | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/sx.el b/sx.el index 3010bdc..431643c 100644 --- a/sx.el +++ b/sx.el @@ -269,11 +269,10 @@ with a `link' property). DATA can also be the link itself." (let ((link (if (stringp data) data (cdr (assoc 'link data))))) - (unless (stringp link) - (error "Data has no link property")) - (replace-regexp-in-string - "^https?://\\(?:\\(?1:[^/]+\\)\\.stackexchange\\|\\(?2:[^/]+\\)\\)\\.[^.]+/.*$" - "\\1\\2" link))) + (when (stringp link) + (replace-regexp-in-string + "^https?://\\(?:\\(?1:[^/]+\\)\\.stackexchange\\|\\(?2:[^/]+\\)\\)\\.[^.]+/.*$" + "\\1\\2" link)))) (defun sx--deep-dot-search (data) "Find symbols somewhere inside DATA which start with a `.'. -- cgit v1.2.3 From 4b3fa6cf6171950d4ed48de736395f8e74c6709f Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Wed, 3 Dec 2014 15:39:35 +0000 Subject: Don't rely on cus-edit for the faces --- sx-button.el | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/sx-button.el b/sx-button.el index f1d7e4d..dbadc2e 100644 --- a/sx-button.el +++ b/sx-button.el @@ -45,6 +45,14 @@ (require 'sx) (require 'sx-question) + +;;; Face +(defface sx-custom-button + '((((type x w32 ns) (class color)) ; Like default mode line + :box (:line-width 2 :style released-button) + :background "lightgrey" :foreground "black")) + "Face used on buttons such as \"Write an Answer\".") + ;;; Command definitions ;; This extends `button-map', which already defines RET and mouse-1. @@ -137,7 +145,7 @@ code-block." 'help-echo (concat "mouse-1, RET" (propertize ": write a comment" 'face 'minibuffer-prompt)) - 'face 'custom-button + 'face 'sx-custom-button 'action #'sx-comment :supertype 'sx-button) @@ -145,7 +153,7 @@ code-block." 'help-echo (concat "mouse-1, RET" (propertize ": write an answer" 'face 'minibuffer-prompt)) - 'face 'custom-button + 'face 'sx-custom-button 'action #'sx-answer :supertype 'sx-button) -- cgit v1.2.3 From 62615786ec83d959ee4e6e9db7da7198dbebe04e Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Wed, 3 Dec 2014 15:39:51 +0000 Subject: Fix unicode display on question list --- sx-question-list.el | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/sx-question-list.el b/sx-question-list.el index 628de30..c5c32d9 100644 --- a/sx-question-list.el +++ b/sx-question-list.el @@ -263,9 +263,9 @@ into consideration. ;; it's not terribly intuitive. (setq tabulated-list-sort-key nil) (add-hook 'tabulated-list-revert-hook - #'sx-question-list-refresh nil t) + #'sx-question-list-refresh nil t) (add-hook 'tabulated-list-revert-hook - #'sx-question-list--update-mode-line nil t) + #'sx-question-list--update-mode-line nil t) (tabulated-list-init-header)) (defcustom sx-question-list-date-sort-method 'last_activity_date @@ -398,7 +398,11 @@ a new list before redisplaying." (setq tabulated-list-entries (mapcar sx-question-list--print-function (cl-remove-if #'sx-question--hidden-p question-list))) - (when redisplay (tabulated-list-print 'remember)) + (when redisplay + (tabulated-list-print 'remember) + ;; Display weird chars correctly + (set-buffer-multibyte nil) + (set-buffer-multibyte t)) (when window (set-window-start window old-start))) (sx-message "Done.")) -- cgit v1.2.3 From 665668a3fcf6122837bca8845c98cf77872e18b9 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Wed, 3 Dec 2014 19:50:27 +0000 Subject: Refactor :question-read-p in data-here as a separate function --- sx-interaction.el | 80 +++++++++++++++++++++++++++---------------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/sx-interaction.el b/sx-interaction.el index 1603ca7..64bcc40 100644 --- a/sx-interaction.el +++ b/sx-interaction.el @@ -48,46 +48,46 @@ ;;; Using data in buffer -(cl-defun sx--data-here (&key (error t) - (type nil) - (question-read-p nil)) - "Get the alist regarding object under point. +(defun sx--data-here (&optional type noerror) + "Get the alist regarding object under point of type TYPE. 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. -Possible keyword arguments are: - - :error If explicit given as nil, no errors are thrown. - :type Symbol restricting the type of object desired. Possible - values are 'question, 'answer, 'comment. If nothing is found of - that type. - :question-read-p If non-nil, and if object found is a question, - throw a `user-error' if it isn't `sx-question--read-p'. If - object found is not a question, this argument is ignored." - (let ((result - (or (let ((data (get-char-property (point) 'sx--data-here))) - (if (null type) data - (sx-assoc-let type - ;; Is data of the right type? - (cl-case type - (question (when .title data)) - (answer (when .answer_id data)) - (comment (when .comment_id data)))))) - ;; The following two only ever return questions. - (when (or (null type) (eq type 'question)) - ;; @TODO: `sx-question-list-mode' may one day display answers. - (or (and (derived-mode-p 'sx-question-list-mode) - (tabulated-list-get-id)) - (and (derived-mode-p 'sx-question-mode) - sx-question-mode--data))) - ;; Nothing was found - (and error (error "No %s found here" (or type "data")))))) - ;; If we found a question, we may need to check if it's read. - (if (and question-read-p (assoc 'title result)) - (if (sx-question--read-p result) result - (user-error "Question still unread. View it before acting on it")) - result))) +TYPE is a symbol restricting the type of object desired. Possible +values are 'question, 'answer, 'comment, or nil (for any type). + +If no object of the requested type could be returned, an error is +thrown unless NOERROR is non-nil." + (or (let ((data (get-char-property (point) 'sx--data-here))) + (if (null type) data + (sx-assoc-let type + ;; Is data of the right type? + (cl-case type + (question (when .title data)) + (answer (when .answer_id data)) + (comment (when .comment_id data)))))) + ;; The following two only ever return questions. + (when (or (null type) (eq type 'question)) + ;; @TODO: `sx-question-list-mode' may one day display answers. + ;; Ideally, it would use the `sx--data-here' (so no special + ;; handling would be necessary. + (or (and (derived-mode-p 'sx-question-list-mode) + (tabulated-list-get-id)) + (and (derived-mode-p 'sx-question-mode) + sx-question-mode--data))) + ;; Nothing was found + (and (null noerror) + (error "No %s found here" (or type "data"))))) + +(defun sx--error-if-unread (data) + "Throw a user-error if DATA is an unread question. +If it's not a question, or if it is read, return DATA." + ;; If we found a question, we may need to check if it's read. + (if (assoc 'title data) + (if (sx-question--read-p data) data + (user-error "Question not yet read. View it before acting on it")) + data)) (defun sx--maybe-update-display (&optional buffer) "Refresh whatever is displayed in BUFFER or the current buffer. @@ -157,7 +157,7 @@ If WINDOW nil, the window is decided by "Apply or remove upvote from DATA. DATA can be a question, answer, or comment. Interactively, it is guessed from context at point." - (interactive (list (sx--data-here :question-read-p t))) + (interactive (list (sx--error-if-unread (sx--data-here)))) (sx-assoc-let data (sx-set-vote data "upvote" (null (eq .upvoted t))))) @@ -165,7 +165,7 @@ guessed from context at point." "Apply or remove downvote from DATA. DATA can be a question or an answer. Interactively, it is guessed from context at point." - (interactive (list (sx--data-here :question-read-p t))) + (interactive (list (sx--error-if-unread (sx--data-here)))) (sx-assoc-let data (sx-set-vote data "downvote" (null (eq .downvoted t))))) @@ -204,7 +204,7 @@ 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 :question-read-p t) 'query)) + (interactive (list (sx--error-if-unread (sx--data-here)) 'query)) ;; When clicking the "Add a Comment" button, first arg is a marker. (when (markerp data) (setq data (sx--data-here)) @@ -319,7 +319,7 @@ context at point. " ;; If the user tries to answer a question that's not viewed, he ;; probaby hit the button by accident. (interactive - (list (sx--data-here :question-read-p t :type 'question))) + (list (sx--error-if-unread (sx--data-here 'question)))) ;; When clicking the "Write an Answer" button, first arg is a marker. (when (markerp data) (setq data (sx--data-here))) (let ((buffer (current-buffer))) -- cgit v1.2.3 From e549dce5556c9550eedf220d247764e84b4879b7 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Wed, 3 Dec 2014 19:52:34 +0000 Subject: Slight simplification --- sx-interaction.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sx-interaction.el b/sx-interaction.el index 64bcc40..89050c3 100644 --- a/sx-interaction.el +++ b/sx-interaction.el @@ -84,9 +84,9 @@ thrown unless NOERROR is non-nil." "Throw a user-error if DATA is an unread question. If it's not a question, or if it is read, return DATA." ;; If we found a question, we may need to check if it's read. - (if (assoc 'title data) - (if (sx-question--read-p data) data - (user-error "Question not yet read. View it before acting on it")) + (if (and (assoc 'title data) + (null (sx-question--read-p data))) + (user-error "Question not yet read. View it before acting on it") data)) (defun sx--maybe-update-display (&optional buffer) -- cgit v1.2.3