branch: externals/orgalist commit 4c4326e2142dfbc8ed1b45745ebd16fece24a46c Author: Nicolas Goaziou <m...@nicolasgoaziou.fr> Commit: Stefan Monnier <monn...@iro.umontreal.ca>
Initial version, sent to emacs-orgmode list --- orgalist.el | 916 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 916 insertions(+) diff --git a/orgalist.el b/orgalist.el new file mode 100644 index 0000000..0263046 --- /dev/null +++ b/orgalist.el @@ -0,0 +1,916 @@ +;;; orgalist.el --- Manage Org lists in non-Org buffers -*- lexical-binding: t; -*- + +;; Copyright (C) 2017 Nicolas Goaziou + +;; Author: Nicolas Goaziou <m...@nicolasgoaziou.fr> +;; Keywords: convenience + +;; 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: + +;; This library provides Org mode's plain lists in non-Org buffers. + +;; More specifically, it supports syntax for numbered, unnumbered, +;; description items, checkboxes, and counter cookies. + +;; Besides, the following features are supported: + +;; - Navigation (M-<up>, M-<down>) +;; - Indentation (M-<left>, M-<right>, M-S-<left>, M-S-<right>, TAB) +;; - Re-ordering (M-S-<up>, M-S-<down>) +;; - Item insertion (M-RET) +;; - Toggling checkboxes (C-c C-c) +;; - Cycling bullets (C-c -) +;; - Sorting items (C-c ^) +;; - Filling items (M-q) +;; - Auto filling (when Auto Fill mode is enabled) + +;; The library also implements radio lists: + +;; Call the `orgalist-insert-radio-list' function to insert a radio list +;; template in HTML, LaTeX, and Texinfo mode documents. Sending and +;; receiving radio lists works is the same as for radio tables (see +;; Org manual for details) except for these differences: + +;; - Orgalist minor mode must be active; +;; - Use the "ORGLST" keyword instead of "ORGTBL"; +;; - `M-x orgalist-send-list' works only on the first list item. + +;; Built-in translator functions are: `org-list-to-latex', +;; `org-list-to-html' and `org-list-to-texinfo'. They use the +;; `org-list-to-generic' translator function. See its documentation for +;; parameters for accurate customizations of lists. Here is a LaTeX +;; example: + +;; % BEGIN RECEIVE ORGLST to-buy +;; % END RECEIVE ORGLST to-buy +;; \begin{comment} +;; +;; #+ORGLST: SEND to-buy org-list-to-latex +;; - a new house +;; - a new computer +;; + a new keyboard +;; + a new mouse +;; - a new life +;; \end{comment} + +;; `M-x orgalist-send-list' on "a new house" inserts the translated +;; LaTeX list in-between the "BEGIN RECEIVE" and "END RECEIVE" marker +;; lines. + + +;;; Code: + +(require 'easymenu) +(require 'org-list) + +(defvar mail-header-separator) +(defvar message-signature-separator) + + +;;; Configuration variables + +(defgroup orgalist nil + "Manage plain lists in non-Org buffers." + :tag "Orgalist" + :group 'org) + +(defcustom orgalist-context-function + '((message-mode . orgalist-message-mode-context)) + "Alist between major modes and list context functions. +A list context function determines the boundaries of the buffer +that can contain an Org list. When no major mode is found, or the +context function returns nil, consider the whole buffer." + :group 'orgalist + :type '(repeat + (list (symbol :tag "Major mode") + (function :tag "Function")))) + +(defcustom orgalist-ordered-checkboxes nil + "When non-nil, only tick checkboxes in order. +In this case, a checkbox can only be checked when every checkbox +before it is checked too." + :group 'orgalist + :type 'boolean + :safe #'booleanp) + +(defcustom orgalist-radio-list-templates + '((latex-mode "% BEGIN RECEIVE ORGLST %n +% END RECEIVE ORGLST %n +\\begin{comment} +#+ORGLST: SEND %n org-list-to-latex +- +\\end{comment}\n") + (texinfo-mode "@c BEGIN RECEIVE ORGLST %n +@c END RECEIVE ORGLST %n +@ignore +#+ORGLST: SEND %n org-list-to-texinfo +- +@end ignore\n") + (html-mode "<!-- BEGIN RECEIVE ORGLST %n --> +<!-- END RECEIVE ORGLST %n --> +<!-- +#+ORGLST: SEND %n org-list-to-html +- +-->\n")) + "Templates for radio lists in different major modes. +All occurrences of %n in a template will be replaced with the name of the +list, obtained by prompting the user." + :group 'orgalist + :type '(repeat + (list (symbol :tag "Major mode") + (string :tag "Format")))) + + +;;; Mode specific context functions + +(defun orgalist-message-mode-context () + "Return boundaries of message body if point is in body. +Otherwise, return nil." + (save-excursion + (cond ((re-search-backward message-signature-separator nil t) + ;; After signature. + (cons (line-beginning-position 2) (point-max))) + ((re-search-backward + (concat "^" (regexp-quote mail-header-separator) "\n") nil t) + (goto-char (match-end 0)) + (cons (point) + (if (re-search-forward message-signature-separator nil t) + (match-beginning 0) + (point-max)))) + ((re-search-forward + (concat "^" (regexp-quote mail-header-separator) "\n") nil t) + ;; In header. + nil) + (t nil)))) + + +;;; Internal variables + +(defvar orgalist--menu nil + "The Orgalist menu.") + +(defconst orgalist--item-re + (concat + "^[ \t]*\\(\\(?:[-+]\\|\\(?:[0-9]+\\|[A-Za-z]\\)\\.\\)\\(?:[ \t]+\\|$\\)\\)" + "\\(?:\\[@\\([0-9]+\\|[A-Za-z]\\)\\][ \t]*\\)?" + "\\(?:\\(\\[[- xX]\\]\\)\\(?:[ \t]+\\|$\\)\\)?" + "\\(?:\\(.*\\)[ \t]+::\\(?:[ \t]+\\|$\\)\\)?") + "Match a list item and puts everything into groups: +group 1: bullet +group 2: counter +group 3: checkbox +group 4: description tag") + +(defvar-local orgalist--cycling-state nil + "Current cycling state when cycling indentation.") + + +;;; Internal functions + +(defun orgalist--call-in-item (fun pos) + "Call function FUN with buffer narrowed to item starting at POS." + (let* ((struct (save-excursion (goto-char pos) (orgalist--struct))) + (next (or (org-list-has-child-p pos struct) + (org-list-get-item-end pos struct))) + (fill-prefix + (make-string (length (org-list-get-bullet pos struct)) + ?\s))) + (save-restriction + (narrow-to-region pos next) + (funcall fun)))) + +(defun orgalist--boundaries () + "Return buffer boundaries, as a cons cell, where lists are acceptable. +Return nil if Orgalist mode is not active." + (and orgalist-mode + (let ((f (cdr (assq major-mode orgalist-context-function)))) + (or (and (functionp f) (funcall f)) + (cons (point-min) (point-max)))))) + +(defun orgalist--indentation () + "Return current line's indentation." + (save-excursion + (beginning-of-line) + (skip-chars-forward "[ \t]") + (current-column))) + +(defun orgalist--at-item-p () + "Non-nil if point is at an item." + (and (orgalist--boundaries) ;check context + (save-excursion + (beginning-of-line) + (looking-at-p + "[ \t]*\\(?:[-+]\\|\\(?:[a-zA-Z]\\|[0-9]+\\)\\.\\)\\([ \t]\\|$\\)")))) + +(defun orgalist--in-item-p () + "Return item beginning position when in a plain list, nil otherwise." + (let ((boundaries (orgalist--boundaries))) + (when boundaries + (save-excursion + (beginning-of-line) + (let ((lim-up (car (orgalist--boundaries))) + ;; Indentation isn't meaningful when point starts at an + ;; empty line . + (ind-ref (if (looking-at-p "^[ \t]*$") 10000 + (orgalist--indentation)))) + (if (looking-at orgalist--item-re) (point) + ;; Detect if cursor in the middle of blank lines after a list. + (let ((end-bounds nil)) + (when (and (setq end-bounds (org-in-regexp "^[ \t]*\n[ \t]*\n" 2)) + (>= (point) (car end-bounds)) + (< (point) (cdr end-bounds))) + (goto-char (car end-bounds)) + (forward-line -1))) + ;; Look for an item, less indented that reference line. + (catch 'exit + (while t + (let ((ind (orgalist--indentation))) + (cond + ;; This is exactly what we want. + ((and (looking-at orgalist--item-re) (< ind ind-ref)) + (throw 'exit (point))) + ;; At upper bound of search or looking at the end + ;; of a previous list: search is over. + ((<= (point) lim-up) (throw 'exit nil)) + ((looking-at "^[ \t]*\n[ \t]*\n") (throw 'exit nil)) + ;; Skip blank lines. + ((looking-at "^[ \t]*$") (forward-line -1)) + ;; Text at column 0 cannot belong to a list: stop. + ((= 0 ind) (throw 'exit nil)) + ;; Normal text less indented than reference line, + ;; take it as new reference. + ((< ind ind-ref) + (setq ind-ref ind) + (forward-line -1)) + (t (forward-line -1)))))))))))) + +(defun orgalist--struct () + "Return structure of list at point. + +A list structure is an alist where key is point at item, and +values are: + +1. indentation, +2. bullet with trailing white space, +3. bullet counter, if any, +4. checkbox, if any, +5. description tag, if any, +6. position at item end. + +Thus the following list, where numbers in parens are +line beginning positions: + +- [X] first item (1) + 1. sub-item 1 (18) + 5. [@5] sub-item 2 (34) + some other text belonging to first item (55) +- last item (97) + + tag :: description (109) + (131) +gets the following structure: + + ((1 0 \"- \" nil \"[X]\" nil 97) + (18 2 \"1. \" nil nil nil 34) + (34 2 \"5. \" \"5\" nil nil 55) + (97 0 \"- \" nil nil nil 131) + (109 2 \"+ \" nil nil \"tag\" 131)) + +Assume point is at an item." + (save-excursion + (beginning-of-line) + (pcase-let* ((`(,lim-up . ,lim-down) (orgalist--boundaries)) + (text-min-ind 10000) + (beg-cell (cons (point) (orgalist--indentation))) + (itm-lst nil) + (itm-lst-2 nil) + (end-lst nil) + (end-lst-2 nil) + (struct nil) + (assoc-at-point + ;; Return association at point. + (lambda (ind) + (looking-at orgalist--item-re) + (let ((bullet (match-string-no-properties 1))) + (list (point) + ind + bullet + (match-string-no-properties 2) ;counter + (match-string-no-properties 3) ;checkbox + ;; Description tag. + (and (string-match-p "[-+*]" bullet) + (match-string-no-properties 4)))))) + (end-before-blank + (lambda () + ;; Ensure list ends at the first blank line. + (skip-chars-backward " \r\t\n") + (min (line-beginning-position 2) lim-down)))) + ;; Read list from starting item to its beginning, and save top + ;; item position and indentation in BEG-CELL. Also store ending + ;; position of items in END-LST. + (save-excursion + (catch 'exit + (while t + (let ((ind (orgalist--indentation))) + (cond + ((<= (point) lim-up) + ;; At upward limit: if we ended at an item, store it, + ;; else dismiss useless data recorded above BEG-CELL. + ;; Jump to part 2. + (throw 'exit + (setq itm-lst + (if (not (looking-at orgalist--item-re)) + (memq (assq (car beg-cell) itm-lst) itm-lst) + (setq beg-cell (cons (point) ind)) + (cons (funcall assoc-at-point ind) itm-lst))))) + ;; Looking at a list ending regexp. Dismiss useless + ;; data recorded above BEG-CELL. Jump to part 2. + ((looking-at "^[ \t]*\n[ \t]*\n") + (throw 'exit + (setq itm-lst (memq (assq (car beg-cell) itm-lst) + itm-lst)))) + ;; Point is at an item. Add data to ITM-LST. It may + ;; also end a previous item: save it in END-LST. If ind + ;; is less or equal than BEG-CELL and there is no end + ;; at this ind or lesser, this item becomes the new + ;; BEG-CELL. + ((looking-at orgalist--item-re) + (push (funcall assoc-at-point ind) itm-lst) + (push (cons ind (point)) end-lst) + (when (< ind text-min-ind) (setq beg-cell (cons (point) ind))) + (forward-line -1)) + ;; Skip blank lines. + ((looking-at "^[ \t]*$") + (forward-line -1)) + ;; From there, point is not at an item. Interpret + ;; line's indentation: + ;; - text at column 0 is necessarily out of any list. + ;; Dismiss data recorded above BEG-CELL. Jump to + ;; part 2. + ;; - any other case may be an ending position for an + ;; hypothetical item above. Store it and proceed. + ((= ind 0) + (throw 'exit + (setq itm-lst + (memq (assq (car beg-cell) itm-lst) itm-lst)))) + (t + (when (< ind text-min-ind) (setq text-min-ind ind)) + (push (cons ind (point)) end-lst) + (forward-line -1))))))) + ;; Read list from starting point to its end, that is until we + ;; get out of context, or that a non-item line is less or + ;; equally indented than BEG-CELL's cdr. Also, store ending + ;; position of items in END-LST-2. + (catch 'exit + (while t + (let ((ind (orgalist--indentation))) + (cond + ((>= (point) lim-down) + ;; At downward limit: this is de facto the end of the + ;; list. Save point as an ending position, and jump to + ;; part 3. + (throw 'exit + (push (cons 0 (funcall end-before-blank)) end-lst-2))) + ;; Looking at a list ending regexp. Save point as an + ;; ending position and jump to part 3. + ((looking-at org-list-end-re) + (throw 'exit (push (cons 0 (point)) end-lst-2))) + ((looking-at orgalist--item-re) + ;; Point is at an item. Add data to ITM-LST-2. It may + ;; also end a previous item, so save it in END-LST-2. + (push (funcall assoc-at-point ind) itm-lst-2) + (push (cons ind (point)) end-lst-2) + (forward-line 1)) + ;; Skip blank lines along the way. + ((looking-at "^[ \t]*$") + (forward-line 1)) + ;; Ind is lesser or equal than BEG-CELL's. The list is + ;; over: store point as an ending position and jump to + ;; part 3. + ((<= ind (cdr beg-cell)) + (throw 'exit + (push (cons 0 (funcall end-before-blank)) end-lst-2))) + ;; Else, if ind is lesser or equal than previous item's, + ;; this is an ending position: store it. In any case, + ;; skip block or drawer at point, and move to next line. + (t + (when (<= ind (nth 1 (car itm-lst-2))) + (push (cons ind (point)) end-lst-2)) + (forward-line 1)))))) + (setq struct (nconc itm-lst (cdr (nreverse itm-lst-2)))) + (setq end-lst (nconc end-lst (cdr (nreverse end-lst-2)))) + ;; Associate each item to its end position. + (dolist (item struct) + (pcase-let ((`(,pos ,ind . ,_) item)) + ;; Remove end candidates behind current item. + (while (<= (cdar end-lst) pos) (pop end-lst)) + ;; Add end position to item assoc. + (let ((old-end (nthcdr 6 item)) + (new-end (assoc-default ind end-lst '<=))) + (if old-end + (setcar old-end new-end) + (setcdr item (append (cdr item) (list new-end))))))) + ;; Return STRUCT. + struct))) + +(defun orgalist--goto-following-item (previous?) + (let* ((struct (orgalist--struct)) + (prevs (org-list-prevs-alist struct)) + (next (funcall (if previous? #'org-list-get-prev-item + #'org-list-get-next-item) + (line-beginning-position) struct prevs))) + (unless next + (error (if previous? "On first item" "On last item"))) + (goto-char next))) + +(defun orgalist--move-item (up?) + (let* ((col (current-column)) + (item (line-beginning-position)) + (struct (orgalist--struct)) + (next-item (funcall (if up? #'org-list-get-prev-item + #'org-list-get-next-item) + item struct (org-list-prevs-alist struct)))) + (unless next-item + (user-error (if up? "Cannot move this item further up" + "Cannot move this item further down"))) + (setq struct + (org-list-swap-items (if up? next-item item) + (if up? item next-item) + struct)) + (unless up? + (let ((prevs (org-list-prevs-alist struct))) + (goto-char (org-list-get-next-item item struct prevs)))) + (org-list-write-struct struct (org-list-parents-alist struct)) + (move-to-column col))) + +(defun orgalist--auto-fill () + "Auto fill function." + (unless (org-match-line "^[ \t]*$") + (let ((item? (orgalist--in-item-p))) + (when item? + (orgalist--call-in-item normal-auto-fill-function item?))))) + +(defun orgalist--while-at-item (cmd) + "Return CMD when point is at a list item." + (when (orgalist--at-item-p) cmd)) + +(defun orgalist--while-in-item (cmd) + "Return CMD when point is in a list item." + (when (orgalist--in-item-p) cmd)) + + +;;; Bindings and menu + +(defconst orgalist--maybe-fill + '(menu-item "" orgalist-fill-item :filter orgalist--while-in-item)) + +(defconst orgalist--maybe-previous + '(menu-item "" orgalist-previous-item :filter orgalist--while-in-item)) + +(defconst orgalist--maybe-next + '(menu-item "" orgalist-next-item :filter orgalist--while-in-item)) + +(defconst orgalist--maybe-insert + '(menu-item "" orgalist-insert-item :filter orgalist--while-in-item)) + +(defconst orgalist--maybe-move-up + '(menu-item "" orgalist-move-item-up :filter orgalist--while-at-item)) + +(defconst orgalist--maybe-move-down + '(menu-item "" orgalist-move-item-down :filter orgalist--while-at-item)) + +(defconst orgalist--maybe-outdent + '(menu-item "" orgalist-outdent-item :filter orgalist--while-at-item)) + +(defconst orgalist--maybe-indent + '(menu-item "" orgalist-indent-item :filter orgalist--while-at-item)) + +(defconst orgalist--maybe-outdent-tree + '(menu-item "" orgalist-outdent-item-tree :filter orgalist--while-at-item)) + +(defconst orgalist--maybe-indent-tree + '(menu-item "" orgalist-indent-item-tree :filter orgalist--while-at-item)) + +(defconst orgalist--maybe-cycle-bullet + '(menu-item "" orgalist-cycle-bullet :filter orgalist--while-at-item)) + +(defconst orgalist--maybe-cycle-indentation + '(menu-item "" orgalist-cycle-indentation :filter orgalist--while-at-item)) + +(defconst orgalist--maybe-check + '(menu-item "" orgalist-check-item :filter orgalist--while-at-item)) + +(defconst orgalist--maybe-sort + '(menu-item "" orgalist-sort-items :filter orgalist--while-at-item)) + +(defconst orgalist-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "M-q") orgalist--maybe-fill) + (define-key map (kbd "M-<up>") orgalist--maybe-previous) + (define-key map (kbd "M-<down>") orgalist--maybe-next) + (define-key map (kbd "M-RET") orgalist--maybe-insert) + (define-key map (kbd "M-S-<up>") orgalist--maybe-move-up) + (define-key map (kbd "M-S-<down>") orgalist--maybe-move-down) + (define-key map (kbd "M-<left>") orgalist--maybe-outdent) + (define-key map (kbd "M-<right>") orgalist--maybe-indent) + (define-key map (kbd "M-S-<left>") orgalist--maybe-outdent-tree) + (define-key map (kbd "M-S-<right>") orgalist--maybe-indent-tree) + (define-key map (kbd "C-c -") orgalist--maybe-cycle-bullet) + (define-key map (kbd "C-c C-c") orgalist--maybe-check) + (define-key map (kbd "C-c ^") orgalist--maybe-sort) + (define-key map (kbd "TAB") orgalist--maybe-cycle-indentation) + map)) + +(easy-menu-define orgalist--menu + orgalist-mode-map + "Menu used when Orgalist mode is active." + '("Orgalist" :visible orgalist-mode + "----" + ["Insert item" orgalist-insert-item :active (orgalist--in-item-p)] + "----" + ["Check item" orgalist-check-item :active (orgalist--at-item-p)] + ["Cycle item" orgalist-cycle-bullet :active (orgalist--at-item-p)] + "----" + ["Previous item" orgalist-previous-item :active (orgalist--in-item-p)] + ["Next item" orgalist-next-item :active (orgalist--in-item-p)] + ["Move item up" orgalist-move-item-up :active (orgalist--at-item-p)] + ["Move item down" orgalist-move-item-gdown :active (orgalist--at-item-p)] + "---" + ["Indent item" orgalist-indent-item :active (orgalist--at-item-p)] + ["Indent item tree" orgalist-indent-item-tree :active (orgalist--at-item-p)] + ["Outdent item" orgalist-outdent-item :active (orgalist--at-item-p)] + ["Outdent item tree" orgalist-outdent-item-tree :active (orgalist--at-item-p)] + "---" + ["Sort items" orgalist-sort-items :active (orgalist--at-item-p)])) + + +;;; Minor mode definition + +(define-minor-mode orgalist-mode + "Toggle the minor mode `orgalist-mode'. + +This mode is for using Org mode plain lists commands in other +major modes. + +key binding +--- ------- +M-q `orgalist-fill-item' +M-RET `orgalist-insert-item' +M-<up> `orgalist-previous-item' +M-<down> `orgalist-next-item' +M-S-<up> `orgalist-move-item-up' +M-S-<down> `orgalist-move-item-down' +M-<left> `orgalist-outdent-item' +M-<right> `orgalist-indent-item' +M-S-<left> `orgalist-outdent-item-tree' +M-S-<right> `orgalist-indent-item-tree' +C-c - `orgalist-cycle-bullet' +C-c ^ `orgalist-sort-items' +C-c C-c `orgalist-check-item' +TAB `orgalist-cycle-indentation'" + :lighter " olst" + (cond + (orgalist-mode + (when (derived-mode-p 'org-mode) + (user-error "Cannot activate Orgalist mode in an Org buffer")) + (setq-local org-list-allow-alphabetical t) + (setq-local org-list-automatic-rules nil) + (setq-local org-list-demote-modify-bullet nil) + (setq-local org-list-two-spaces-after-bullet-regexp nil) + (add-function :before-until normal-auto-fill-function #'orgalist--auto-fill)) + (t + (remove-function normal-auto-fill-function #'orgalist--auto-fill)))) + + +;;; Public functions + +;;;###autoload +(defun orgalist-fill-item () + "Fill item as a paragraph." + (interactive) + (let ((item (orgalist--in-item-p))) + (unless item (user-error "Not in a list")) + (orgalist--call-in-item #'fill-paragraph item))) + +;;;###autoload +(defun orgalist-previous-item () + "Move to the beginning of the previous item. +Throw an error when at first item." + (interactive) + (let ((item (orgalist--in-item-p))) + (unless item (user-error "Not in a list")) + (goto-char item) + (orgalist--goto-following-item t))) + +;;;###autoload +(defun orgalist-next-item () + "Move to the beginning of the next item. +Throw an error when at last item." + (interactive) + (let ((item (orgalist--in-item-p))) + (unless item (user-error "Not in a list")) + (goto-char item) + (orgalist--goto-following-item nil))) + +;;;###autoload +(defun orgalist-insert-item (&optional checkbox) + "Insert a new item at the current level. + +If cursor is before first character after bullet of the item, the +new item will be created before the current one. + +If CHECKBOX is non-nil, add a checkbox next to the bullet." + (interactive "P") + (let ((item? (orgalist--in-item-p))) + (unless item? (user-error "Not in a list")) + (let* ((struct (save-excursion (goto-char item?) (orgalist--struct))) + (prevs (org-list-prevs-alist struct)) + ;; If we're in a description list, ask for the new term. + (desc + (and (eq 'descriptive (org-list-get-list-type item? struct prevs)) + " :: "))) + (setq struct (org-list-insert-item (point) struct prevs checkbox desc)) + (org-list-write-struct struct (org-list-parents-alist struct)) + (looking-at orgalist--item-re) + (goto-char (if (and (match-beginning 4) + (save-match-data + (string-match "\\." (match-string 1)))) + (match-beginning 4) + (match-end 0))) + (when desc (backward-char 1))))) + +;;;###autoload +(defun orgalist-move-item-down () + "Move the item at point down, i.e. swap with following item. +Sub-items (items with larger indentation) are considered part of +the item, so this really moves item trees." + (interactive) + (unless (orgalist--at-item-p) (user-error "Not in a list")) + (orgalist--move-item nil)) + +;;;###autoload +(defun orgalist-move-item-up () + "Move the item at point up, i.e. swap with previous item. +Sub-items (items with larger indentation) are considered part of +the item, so this really moves item trees." + (interactive) + (unless (orgalist--at-item-p) (user-error "Not in a list")) + (orgalist--move-item t)) + +;;;###autoload +(defun orgalist-cycle-indentation () + "Cycle levels of indentation of an empty item. +The first run indents the item, if applicable. Subsequent runs +outdent it at meaningful levels in the list. When done, item is +put back at its original position with its original bullet." + (interactive) + (unless (orgalist--at-item-p) (user-error "Not in a list")) + (let* ((struct (orgalist--struct)) + (ind (org-list-get-ind (line-beginning-position) struct)) + (bullet (org-trim (buffer-substring (line-beginning-position) + (line-end-position))))) + ;; Accept empty items or if cycle has already started. + (if (and (not (eq last-command 'orgalist-cycle-indentation)) + (save-excursion + (beginning-of-line) + (looking-at orgalist--item-re)) + (< (match-end 0) + (save-excursion + (goto-char (org-list-get-item-end + (line-beginning-position) struct)) + (skip-chars-backward " \r\t\n") + (point)))) + (progn + (setq this-command 'identity) + (error "Cannot move item")) + (setq this-command 'orgalist-cycle-indentation) + ;; When in the middle of the cycle, try to outdent first. If + ;; it fails, and point is still at initial position, indent. + ;; Else, re-create it at its original position. + (if (eq last-command 'orgalist-cycle-indentation) + (cond + ((ignore-errors (org-list-indent-item-generic -1 t struct))) + ((and (= ind (car orgalist--cycling-state)) + (ignore-errors (org-list-indent-item-generic 1 t struct)))) + (t (delete-region (line-beginning-position) (line-end-position)) + (indent-to-column (car orgalist--cycling-state)) + (insert (cdr orgalist--cycling-state) " ") + ;; Break cycle + (setq this-command 'identity))) + ;; If a cycle is starting, remember indentation and bullet, + ;; then try to indent. If it fails, try to outdent. + (setq orgalist--cycling-state (cons ind bullet)) + (cond + ((ignore-errors (org-list-indent-item-generic 1 t struct))) + ((ignore-errors (org-list-indent-item-generic -1 t struct))) + (t (user-error "Cannot move item"))))))) + +;;;###autoload +(defun orgalist-cycle-bullet () + "Cycle through the different itemize/enumerate bullets. +This cycle the entire list level through the sequence: + + `-' -> `+' -> `1.' -> `a.'" + (interactive) + (unless (orgalist--at-item-p) (user-error "Not in a list")) + (save-excursion + (beginning-of-line) + (let* ((struct (orgalist--struct)) + (parents (org-list-parents-alist struct)) + (prevs (org-list-prevs-alist struct)) + (list-beg (org-list-get-first-item (point) struct prevs)) + (bullet (org-list-get-bullet list-beg struct)) + (current (cond ((string-match "[A-Za-z]\\." bullet) "a.") + ((string-match "\\." bullet) "1.") + (t (org-trim bullet)))) + (bullet-list + (append '("-" "+") + ;; Description items cannot be numbered. + (unless (org-at-item-description-p) '("1.")) + (unless (org-at-item-description-p) '("a.")))) + (new (or (cadr (member current bullet-list)) + (car bullet-list)))) + ;; Use a short variation of `org-list-write-struct' as there's + ;; no need to go through all the steps. + (let ((old-struct (copy-tree struct))) + (org-list-set-bullet list-beg struct (org-list-bullet-string new)) + (org-list-struct-fix-bul struct prevs) + (org-list-struct-fix-ind struct parents) + (org-list-struct-apply-struct struct old-struct))))) + +;;;###autoload +(defun orgalist-outdent-item () + "Outdent a local list item, but not its children." + (interactive) + (unless (orgalist--at-item-p) (user-error "Not in a list")) + (org-list-indent-item-generic -1 t (orgalist--struct))) + +;;;###autoload +(defun orgalist-indent-item () + "Indent a local list item, but not its children." + (interactive) + (unless (orgalist--at-item-p) (user-error "Not in a list")) + (org-list-indent-item-generic 1 t (orgalist--struct))) + +;;;###autoload +(defun orgalist-outdent-item-tree () + "Outdent a local list item including its children." + (interactive) + (unless (orgalist--at-item-p) (user-error "Not in a list")) + (org-list-indent-item-generic -1 nil (orgalist--struct))) + +;;;###autoload +(defun orgalist-indent-item-tree () + "Indent a local list item including its children." + (interactive) + (unless (orgalist--at-item-p) (user-error "Not in a list")) + (org-list-indent-item-generic 1 nil (orgalist--struct))) + +;;;###autoload +(defun orgalist-check-item (&optional arg) + "Toggle the checkbox in the current line. + +With prefix ARG, add or remove checkboxes. With double prefix, +set checkbox to [-]. + +In any case, fix indentation, bullets and checkboxes in the list +at point." + (interactive "P") + (unless (orgalist--at-item-p) (user-error "Not in a list")) + (save-excursion + (beginning-of-line) + (catch :repair-only + (let* ((regexp (concat "[ \t]*\\(?:[-+]\\|\\(?:[a-zA-Z]\\|[0-9]+\\)\\.\\)" + "\\(?:[ \t]+\\[@\\(?:[a-zA-Z]\\|[0-9]+\\)\\]\\)?" + "[ \t]+\\(\\[[- xX]\\]\\)")) + (struct (orgalist--struct)) + (struct-copy (copy-tree struct)) + (parents (org-list-parents-alist struct)) + (prevs (org-list-prevs-alist struct)) + (current (and (looking-at regexp) (match-string 1))) + (new + (cond + ((equal arg '(16)) "[-]") + ((equal arg '(4)) (if current nil "[ ]")) + ((not current) + (throw :repair-only (org-list-write-struct struct parents))) + ((member current '("[X]" "[x]")) "[ ]") + (t "[X]")))) + (org-list-set-checkbox (point) struct new) + (when (org-list-struct-fix-box + struct parents prevs orgalist-ordered-checkboxes) + (org-list-write-struct struct-copy parents) + (error "Checkbox blocked because of unchecked box")) + (org-list-struct-apply-struct struct struct-copy))))) + +;;;###autoload +(defun orgalist-sort-items (with-case sorting-type) + "Sort list items. + +The cursor may be at any item of the list that should be sorted. +Sublists are not sorted. + +Comparing entries ignores case by default. However, with an +optional argument WITH-CASE, the sorting considers case as well. + +The command prompts for the SORTING-TYPE, which needs to be +a character among ?n ?N ?a ?A ?x ?X. Here is the detailed meaning +of each character: + +n Numerically, by converting the beginning of the item to a number. +a Alphabetically. Only the first line of item is checked. +x By \"checked\" status of a check list. + +Capital letters reverse the sort order." + (interactive + (list current-prefix-arg + (progn + (message "Sort plain list: [a/A]lpha [n/N]umeric [x/X]checked:") + (read-char-exclusive)))) + (unless (memq sorting-type '(?a ?A ?n ?N ?x ?X)) + (user-error "Invalid sorting type")) + (let ((org-after-sorting-entries-or-items-hook nil)) + (org-sort-list with-case sorting-type))) + +;;;###autoload +(defun orgalist-insert-radio-list () + "Insert a radio list template appropriate for current major mode." + (interactive) + (let* ((e (or (cl-assoc-if #'derived-mode-p orgalist-radio-list-templates) + (error "No radio list setup defined for %S" major-mode))) + (name (read-string "List name: ")) + (txt (replace-regexp-in-string "%n" name (nth 1 e) t t))) + (unless (bolp) (insert "\n")) + (save-excursion (insert txt)))) + +;;;###autoload +(defun orgalist-send-list (&optional maybe) + "Send a transformed version of this list to the receiver position. +With argument MAYBE, fail quietly if no transformation is defined +for this list." + (interactive) + (catch 'exit + (unless (orgalist--at-item-p) (error "Not at a list item")) + (save-excursion + (let ((case-fold-search t)) + (re-search-backward "^[ \t]*#\\+ORGLST:" nil t) + (unless (looking-at + "[ \t]*#\\+ORGLST:[ \t]+SEND[ \t]+\\(\\S-+\\)[ \t]+\\([^ \t\n]+\\)") + (if maybe (throw 'exit nil) + (error "Don't know how to transform this list"))))) + (let* ((name (regexp-quote (match-string 1))) + (transform (intern (match-string 2))) + (bottom-point + (save-excursion + (re-search-forward + "\\(\\\\end{comment}\\|@end ignore\\|-->\\)" nil t) + (match-beginning 0))) + (top-point + (progn + (re-search-backward "#\\+ORGLST" nil t) + (re-search-forward (org-item-beginning-re) bottom-point t) + (match-beginning 0))) + (plain-list (save-excursion + (goto-char top-point) + (org-list-to-lisp)))) + (unless (fboundp transform) + (error "No such transformation function %s" transform)) + (let ((txt (funcall transform plain-list))) + ;; Find the insertion(s) place(s). + (save-excursion + (goto-char (point-min)) + (let ((receiver-count 0) + (begin-re (format "BEGIN +RECEIVE +ORGLST +%s\\([ \t]\\|$\\)" + name)) + (end-re (format "END +RECEIVE +ORGLST +%s\\([ \t]\\|$\\)" + name))) + (while (re-search-forward begin-re nil t) + (cl-incf receiver-count) + (let ((beg (line-beginning-position 2))) + (unless (re-search-forward end-re nil t) + (user-error "Cannot find end of receiver location at %d" beg)) + (beginning-of-line) + (delete-region beg (point)) + (insert txt "\n"))) + (cond + ((> receiver-count 1) + (message "List converted and installed at receiver locations")) + ((= receiver-count 1) + (message "List converted and installed at receiver location")) + (t (user-error "No valid receiver location found"))))))))) + + +(provide 'orgalist) +;;; orgalist.el ends here