From 01aac931f90c1251ed6b3c30df672cf3073b16c5 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Sun, 4 Jan 2015 23:03:53 -0200 Subject: Implement sx-request-get-data for getting data from the data branch. --- sx-request.el | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/sx-request.el b/sx-request.el index ebc16d2..e91ac18 100644 --- a/sx-request.el +++ b/sx-request.el @@ -212,6 +212,35 @@ require authentication. Currently returns nil." '(())) + +;;; Our own generated data +(defvar sx-request--data-url-format + "https://raw.githubusercontent.com/vermiculus/sx.el/data/data/%s.el" + "Url of the \"data\" directory inside the SX `data' branch.") + +(defun sx-request-get-data (file) + "Fetch and return data stored online by SX. +FILE is a string or symbol, the name of the file which holds the +desired data, relative to `sx-request--data-url-format'. For +instance, `tags/emacs' returns the list of tags on Emacs.SE." + (let* ((url-automatic-caching t) + (url-inhibit-uncompression t) + (request-url (format sx-request--data-url-format file)) + (url-request-method "GET") + (url-request-extra-headers + '(("Content-Type" . "application/x-www-form-urlencoded"))) + (response-buffer (url-retrieve-synchronously request-url))) + (if (not response-buffer) + (error "Something went wrong in `url-retrieve-synchronously'") + (with-current-buffer response-buffer + (progn + (goto-char (point-min)) + (if (not (search-forward "\n\n" nil t)) + (error "Headers missing; response corrupt") + (when (looking-at-p "Not Found") (error "Page not found.")) + (prog1 (read (current-buffer)) + (kill-buffer (current-buffer))))))))) + ;;; Support Functions (defun sx-request--build-keyword-arguments (alist &optional kv-sep) -- cgit v1.2.3 From d26df2730db01a4904ad71eb3590b5d90c015767 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Sun, 4 Jan 2015 23:16:44 -0200 Subject: Implement sx-tag-list--get --- sx-tag.el | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/sx-tag.el b/sx-tag.el index 5e75890..0f726fd 100644 --- a/sx-tag.el +++ b/sx-tag.el @@ -62,6 +62,23 @@ Returns a list." (mapcar (lambda (x) (cdr (assoc 'name x))) (sx-tag--get-some-tags-containing site string))) + +;;; Getting tags from our data branch. Without the API. +;;;; @TODO: Once the cache is finished, this can probably be made into +;;;; a cache variasble with 1 day expiration time. +(defvar sx-tag-list-alist nil + "Alist where the tag list for each site is stored. +Elements are of the type (SITE . TAG-LIST).") + +(defun sx-tag-list--get (site) + "Retrieve all tags from SITE in a single request. +This does not access the API. Instead, it uses +`sx-request-get-data', which accesses SX's tag cache." + (or (cdr (assoc site sx-tag-list-alist)) + (let ((list (sx-request-get-data (concat "tags/" site)))) + (push (cons site list) sx-tag-list-alist) + list))) + ;;; Check tag validity (defun sx-tag--invalid-name-p (site tags) -- cgit v1.2.3 From 065ef960b3fcf80ea68dbcbbcca8c276a2d09b07 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Mon, 5 Jan 2015 00:53:01 -0200 Subject: Move sx--multiple-read to sx-tag-multiple-read --- sx-search.el | 13 ++++--------- sx-tag.el | 33 +++++++++++++++++++++++++++++++++ sx.el | 18 ------------------ 3 files changed, 37 insertions(+), 27 deletions(-) diff --git a/sx-search.el b/sx-search.el index d47905e..8614f49 100644 --- a/sx-search.el +++ b/sx-search.el @@ -32,13 +32,11 @@ (require 'sx) (require 'sx-question-list) +(require 'sx-tag) (defvar sx-search--query-history nil "Query history for interactive prompts.") -(defvar sx-search--tag-history nil - "Tags history for interactive prompts.") - ;;; Basic function (defun sx-search-get-questions (site page query &optional tags excluded-tags keywords) @@ -84,15 +82,12 @@ prefix argument, the user is asked for everything." (when (string= query "") (setq query nil)) (when current-prefix-arg - (setq tags (sx--multiple-read - (format "Tags (%s)" - (if query "optional" "mandatory")) - 'sx-search--tag-history)) + (setq tags (sx-tag-multiple-read + site (format "Tags%s" (if query " (optional)" "")))) (when (and (not query) (string= "" tags)) (sx-user-error "Must supply either QUERY or TAGS")) (setq excluded-tags - (sx--multiple-read - "Excluded tags (optional)" 'sx-search--tag-history))) + (sx-tag-multiple-read site "Excluded tags (optional)"))) (list site query tags excluded-tags))) ;; Here starts the actual function diff --git a/sx-tag.el b/sx-tag.el index 0f726fd..41ed9eb 100644 --- a/sx-tag.el +++ b/sx-tag.el @@ -99,6 +99,39 @@ Return the list of invalid tags in TAGS." :site site)))) (cl-remove-if (lambda (x) (member x result)) tags))) + +;;; Prompt the user for tags. +(defvar sx-tag-history nil + "Tags history for interactive prompts.") + +;;; @TODO: Make it so that hitting BACKSPACE with an empty input +;;; deletes a previously submitted tag. +(defun sx-tag-multiple-read (site prompt &optional initial-value) + "Interactively read a list of tags for SITE. +Call `sx-completing-read' multiple times, until input is empty. +Return a list of tags given by the user. + +PROMPT is a string displayed to the user and should not end with +a space nor a colon. INITIAL-VALUE is a list of already-selected +tags." + (let ((completion-list (sx-tag-list--get site)) + (list initial-value) + input) + (while (not (string= + "" + (setq input (sx-completing-read + (concat prompt " [" + (mapconcat #'identity list ",") + "]: ") + completion-list + (lambda (x) (not (member x list))) + nil + 'require-match + nil + 'sx-tag-history)))) + (push input list)) + list)) + (provide 'sx-tag) ;;; sx-tag.el ends here diff --git a/sx.el b/sx.el index 3271755..9924308 100644 --- a/sx.el +++ b/sx.el @@ -176,24 +176,6 @@ All ARGS are passed to `completing-read' or `ido-completing-read'." (apply (if ido-mode #'ido-completing-read #'completing-read) args)) -(defun sx--multiple-read (prompt hist-var) - "Interactively query the user for a list of strings. -Call `read-string' multiple times, until the input is empty. - -PROMPT is a string displayed to the user and should not end with -a space nor a colon. HIST-VAR is a quoted symbol, indicating a -list in which to store input history." - (let (list input) - (while (not (string= - "" - (setq input (read-string - (concat prompt " [" - (mapconcat #'identity list ",") - "]: ") - "" hist-var)))) - (push input list)) - list)) - (defmacro sx-sorted-insert-skip-first (newelt list &optional predicate) "Inserted NEWELT into LIST sorted by PREDICATE. This is designed for the (site id id ...) lists. So the first car -- cgit v1.2.3 From 359a48845905feab401b5dc0783733ff230f8956 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Mon, 5 Jan 2015 01:03:58 -0200 Subject: Refactor sx-compose--goto-tag-header --- sx-compose.el | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/sx-compose.el b/sx-compose.el index 8a6edc3..ac6700b 100644 --- a/sx-compose.el +++ b/sx-compose.el @@ -137,20 +137,25 @@ contents to the API, then calls `sx-compose-after-send-functions'." (with-current-buffer buffer (kill-new (buffer-string))))) +(defun sx-compose--goto-tag-header () + "Move to the \"Tags:\" header. +Match data is set so group 1 encompasses any already inserted +tags. Return a list of already inserted tags." + (goto-char (point-min)) + (unless (search-forward-regexp + "^Tags : *\\([^[:space:]].*\\) *$" + (next-single-property-change (point-min) 'sx-compose-separator) + 'noerror) + (error "No Tags header found")) + (split-string (match-string 1) "[[:space:],;]" + 'omit-nulls "[[:space:]]")) + (defun sx-compose--check-tags () "Check if tags in current compose buffer are valid." (save-excursion - (goto-char (point-min)) - (unless (search-forward-regexp - "^Tags : *\\([^[:space:]].*\\) *$" - (next-single-property-change (point-min) 'sx-compose-separator) - 'noerror) - (error "No Tags header found")) (let ((invalid-tags (sx-tag--invalid-name-p - (split-string (match-string 1) "[[:space:],;]" - 'omit-nulls "[[:space:]]") - sx-compose--site))) + sx-compose--site (sx-compose--goto-tag-header)))) (if invalid-tags ;; If the user doesn't want to create the tags, we return ;; nil and sending is aborted. -- cgit v1.2.3 From 60050b7b773a3bfd5d7ba0bfa53f46c6e1b4dca5 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Mon, 5 Jan 2015 01:04:11 -0200 Subject: Implement sx-compose-insert-tags --- sx-compose.el | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/sx-compose.el b/sx-compose.el index ac6700b..e1f6874 100644 --- a/sx-compose.el +++ b/sx-compose.el @@ -120,6 +120,21 @@ contents to the API, then calls `sx-compose-after-send-functions'." (run-hook-with-args 'sx-compose-after-send-functions (current-buffer) result))))) +(defun sx-compose-insert-tags () + "Prompt for a tag list for this draft and insert them." + (interactive) + (save-excursion + (let* ((old (sx-compose--goto-tag-header)) + (new + (save-match-data + (mapconcat + #'identity + (sx-tag-multiple-read sx-compose--site "Tags" old) + " ")))) + (if (match-string 1) + (replace-match new :fixedcase nil nil 1) + (insert new))))) + ;;; Functions for use in hooks (defun sx-compose-quit (buffer _) @@ -143,12 +158,13 @@ Match data is set so group 1 encompasses any already inserted tags. Return a list of already inserted tags." (goto-char (point-min)) (unless (search-forward-regexp - "^Tags : *\\([^[:space:]].*\\) *$" + (rx bol "Tags : " (group-n 1 (* not-newline)) eol) (next-single-property-change (point-min) 'sx-compose-separator) 'noerror) (error "No Tags header found")) - (split-string (match-string 1) "[[:space:],;]" - 'omit-nulls "[[:space:]]")) + (save-match-data + (split-string (match-string 1) (rx (any space ",;")) + 'omit-nulls (rx space)))) (defun sx-compose--check-tags () "Check if tags in current compose buffer are valid." -- cgit v1.2.3 From 34ea6829153a1b5b45104c0650a9d0a148491aef Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Mon, 5 Jan 2015 11:16:50 -0200 Subject: Fix sx-tag-completing-read --- sx-tag.el | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/sx-tag.el b/sx-tag.el index 41ed9eb..b2ad375 100644 --- a/sx-tag.el +++ b/sx-tag.el @@ -108,29 +108,30 @@ Return the list of invalid tags in TAGS." ;;; deletes a previously submitted tag. (defun sx-tag-multiple-read (site prompt &optional initial-value) "Interactively read a list of tags for SITE. -Call `sx-completing-read' multiple times, until input is empty. +Call `sx-completing-read' multiple times, until input is empty, +with completion options given by the tag list of SITE. Return a list of tags given by the user. PROMPT is a string displayed to the user and should not end with a space nor a colon. INITIAL-VALUE is a list of already-selected tags." (let ((completion-list (sx-tag-list--get site)) - (list initial-value) + (list (reverse initial-value)) + (empty-string + (propertize "--\x000-some-string-representing-empty-\x000--" + 'display "DONE")) input) (while (not (string= - "" + empty-string (setq input (sx-completing-read (concat prompt " [" - (mapconcat #'identity list ",") + (mapconcat #'identity (reverse list) ",") "]: ") completion-list - (lambda (x) (not (member x list))) - nil - 'require-match - nil - 'sx-tag-history)))) + nil 'require-match nil 'sx-tag-history + empty-string)))) (push input list)) - list)) + (reverse list))) (provide 'sx-tag) ;;; sx-tag.el ends here -- cgit v1.2.3 From f77495881d218a96a8b875e650a0d8acf0f36354 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Mon, 5 Jan 2015 11:23:30 -0200 Subject: Bind sx-compose-insert-tags to "\C-c\C-q" Fix #137 --- sx-compose.el | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sx-compose.el b/sx-compose.el index e1f6874..ee4f774 100644 --- a/sx-compose.el +++ b/sx-compose.el @@ -107,6 +107,9 @@ with the `sx-compose-create' function. (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) +(sx--define-conditional-key + sx-compose-mode-map "\C-c\C-q" #'sx-compose-insert-tags + (ignore-errors (string= "Title: " (substring (buffer-string) 0 7)))) (defun sx-compose-send () "Finish composing current buffer and send it. -- cgit v1.2.3 From 51b5f280119745074e9d16042b5608f25cbb6e06 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Mon, 5 Jan 2015 16:09:37 -0200 Subject: Turn sx-compose--question-headers into a defconst --- sx-compose.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sx-compose.el b/sx-compose.el index ee4f774..0e5e35f 100644 --- a/sx-compose.el +++ b/sx-compose.el @@ -67,7 +67,7 @@ succeeds.") Is invoked between `sx-compose-before-send-hook' and `sx-compose-after-send-functions'.") -(defvar sx-compose--question-headers +(defconst sx-compose--question-headers (concat #("Title: " 0 7 (intangible t read-only t rear-nonsticky t)) "%s" -- cgit v1.2.3 From e061fb947a02290314be7247d81963e7bfd0987b Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Mon, 5 Jan 2015 16:31:10 -0200 Subject: Move define-conditional-key to sx.el --- sx-switchto.el | 12 ------------ sx.el | 12 ++++++++++++ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/sx-switchto.el b/sx-switchto.el index 458586a..ed83360 100644 --- a/sx-switchto.el +++ b/sx-switchto.el @@ -54,18 +54,6 @@ ;;; These are keys which depend on context. ;;;; For instance, it makes no sense to have `switch-site' bound to a ;;;; key on a buffer with no `sx-question-list--site' variable. -(defmacro sx--define-conditional-key (keymap key def &rest body) - "In KEYMAP, define key sequence KEY as DEF conditionally. -This is like `define-key', except the definition \"disapears\" -whenever BODY evaluates to nil." - (declare (indent 3) - (debug (form form form &rest sexp))) - `(define-key ,keymap ,key - '(menu-item - ,(format "maybe-%s" (or (car (cdr-safe def)) def)) ignore - :filter (lambda (&optional _) - (when (progn ,@body) ,def))))) - (sx--define-conditional-key sx-switchto-map "s" #'sx-question-list-switch-site (and (boundp 'sx-question-list--site) sx-question-list--site)) diff --git a/sx.el b/sx.el index 9924308..bfc647d 100644 --- a/sx.el +++ b/sx.el @@ -247,6 +247,18 @@ Anything before the (sub)domain is removed." (rx string-start (or (and (0+ word) (optional ":") "//"))) "" url))) +(defmacro sx--define-conditional-key (keymap key def &rest body) + "In KEYMAP, define key sequence KEY as DEF conditionally. +This is like `define-key', except the definition \"disapears\" +whenever BODY evaluates to nil." + (declare (indent 3) + (debug (form form form &rest sexp))) + `(define-key ,keymap ,key + '(menu-item + ,(format "maybe-%s" (or (car (cdr-safe def)) def)) ignore + :filter (lambda (&optional _) + (when (progn ,@body) ,def))))) + ;;; Printing request data (defvar sx--overlays nil -- cgit v1.2.3 From cbd587f6fdcd9936132972e510d4cbd973d3a703 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Mon, 5 Jan 2015 16:31:48 -0200 Subject: Add sx-compose--is-question-p variable --- sx-compose.el | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/sx-compose.el b/sx-compose.el index 0e5e35f..db76531 100644 --- a/sx-compose.el +++ b/sx-compose.el @@ -82,6 +82,10 @@ Is invoked between `sx-compose-before-send-hook' and "Headers inserted when composing a new question. Used by `sx-compose-create'.") +(defvar sx-compose--is-question-p nil + "Non-nil if this `sx-compose-mode' buffer is a question.") +(make-variable-buffer-local 'sx-compose--is-question-p) + (defvar sx-compose--site nil "Site which the curent compose buffer belongs to.") (make-variable-buffer-local 'sx-compose--site) @@ -95,8 +99,11 @@ 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. +set. To make sure you set it correctly, you can create the +buffer with the `sx-compose-create' function. + +If creating a question draft, the `sx-compose--is-question-p' +variable should also be set to enable more functionality. \\ \\{sx-compose-mode}" @@ -109,7 +116,7 @@ with the `sx-compose-create' function. (define-key sx-compose-mode-map "\C-c\C-k" #'sx-compose-quit) (sx--define-conditional-key sx-compose-mode-map "\C-c\C-q" #'sx-compose-insert-tags - (ignore-errors (string= "Title: " (substring (buffer-string) 0 7)))) + sx-compose--is-question-p) (defun sx-compose-send () "Finish composing current buffer and send it. @@ -204,6 +211,7 @@ respectively added locally to `sx-compose-before-send-hook' and (with-current-buffer (sx-compose--get-buffer-create site parent) (sx-compose-mode) (setq sx-compose--site site) + (setq sx-compose--is-question-p is-question) (setq sx-compose--send-function (if (consp parent) (sx-assoc-let parent -- cgit v1.2.3 From 023b5511df15df4289783a8701fec2bed95462f1 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Mon, 5 Jan 2015 16:32:06 -0200 Subject: Add header-line to compose-buffer --- sx-compose.el | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/sx-compose.el b/sx-compose.el index db76531..67c476e 100644 --- a/sx-compose.el +++ b/sx-compose.el @@ -82,6 +82,19 @@ Is invoked between `sx-compose-before-send-hook' and "Headers inserted when composing a new question. Used by `sx-compose-create'.") +(defconst sx-compose--header-line + '(" " + (:propertize "C-c C-c" face mode-line-buffer-id) + ": Finish and Send" + (sx-compose--is-question-p + (" " + (:propertize "C-c C-q" face mode-line-buffer-id) + ": Insert tags")) + " " + (:propertize "C-c C-k" face mode-line-buffer-id) + ": Discard Draft") + "Header-line used on `sx-compose-mode' drafts.") + (defvar sx-compose--is-question-p nil "Non-nil if this `sx-compose-mode' buffer is a question.") (make-variable-buffer-local 'sx-compose--is-question-p) @@ -107,6 +120,7 @@ variable should also be set to enable more functionality. \\ \\{sx-compose-mode}" + (setq header-line-format sx-compose--header-line) (add-hook 'sx-compose-after-send-functions #'sx-compose-quit nil t) (add-hook 'sx-compose-after-send-functions -- cgit v1.2.3 From d49e720b6961ea005cdb26e4191b6f51384253ac Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Tue, 6 Jan 2015 19:28:15 -0200 Subject: sx-search: Use concat instead of format --- sx-search.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sx-search.el b/sx-search.el index 8614f49..fa08e56 100644 --- a/sx-search.el +++ b/sx-search.el @@ -83,7 +83,7 @@ prefix argument, the user is asked for everything." (setq query nil)) (when current-prefix-arg (setq tags (sx-tag-multiple-read - site (format "Tags%s" (if query " (optional)" "")))) + site (concat "Tags" (when query " (optional)")))) (when (and (not query) (string= "" tags)) (sx-user-error "Must supply either QUERY or TAGS")) (setq excluded-tags -- cgit v1.2.3 From 5b66d7865fb9f160586c7d579f8df7195804e927 Mon Sep 17 00:00:00 2001 From: Artur Malabarba Date: Tue, 6 Jan 2015 19:29:46 -0200 Subject: Typo --- sx.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sx.el b/sx.el index bfc647d..8680fc3 100644 --- a/sx.el +++ b/sx.el @@ -249,7 +249,7 @@ Anything before the (sub)domain is removed." (defmacro sx--define-conditional-key (keymap key def &rest body) "In KEYMAP, define key sequence KEY as DEF conditionally. -This is like `define-key', except the definition \"disapears\" +This is like `define-key', except the definition \"disappears\" whenever BODY evaluates to nil." (declare (indent 3) (debug (form form form &rest sexp))) -- cgit v1.2.3