diff options
Diffstat (limited to 'lisp/calendar/todo-mode.el')
-rw-r--r-- | lisp/calendar/todo-mode.el | 677 |
1 files changed, 345 insertions, 332 deletions
diff --git a/lisp/calendar/todo-mode.el b/lisp/calendar/todo-mode.el index c1c8e196eaf..08da75dbd60 100644 --- a/lisp/calendar/todo-mode.el +++ b/lisp/calendar/todo-mode.el @@ -188,25 +188,17 @@ The final element is \"*\", indicating an unspecified month.") "Array of abbreviated month names, in order. The final element is \"*\", indicating an unspecified month.") -(with-no-warnings - ;; FIXME: These vars lack a prefix, but this is out of our control, because - ;; they're defined by Calendar, e.g. for calendar-date-display-form. - (defvar dayname) - (defvar monthname) - (defvar day) - (defvar month) - (defvar year)) - (defconst todo-date-pattern (let ((dayname (diary-name-pattern calendar-day-name-array nil t))) (concat "\\(?4:\\(?5:" dayname "\\)\\|" - (let ((dayname) - (monthname (format "\\(?6:%s\\)" (diary-name-pattern - todo-month-name-array - todo-month-abbrev-array))) - (month "\\(?7:[0-9]+\\|\\*\\)") - (day "\\(?8:[0-9]+\\|\\*\\)") - (year "-?\\(?9:[0-9]+\\|\\*\\)")) + (calendar-dlet* + ((dayname) + (monthname (format "\\(?6:%s\\)" (diary-name-pattern + todo-month-name-array + todo-month-abbrev-array))) + (month "\\(?7:[0-9]+\\|\\*\\)") + (day "\\(?8:[0-9]+\\|\\*\\)") + (year "-?\\(?9:[0-9]+\\|\\*\\)")) (mapconcat #'eval calendar-date-display-form "")) "\\)")) "Regular expression matching a todo item date header.") @@ -861,17 +853,18 @@ category. With non-nil argument BACK, visit the numerically previous category (the highest numbered one, if the current category is the first)." (interactive) - (setq todo-category-number - (1+ (mod (- todo-category-number (if back 2 0)) - (length todo-categories)))) - (when todo-skip-archived-categories - (while (and (zerop (todo-get-count 'todo)) - (zerop (todo-get-count 'done)) - (not (zerop (todo-get-count 'archived)))) - (setq todo-category-number - (funcall (if back #'1- #'1+) todo-category-number)))) - (todo-category-select) - (goto-char (point-min))) + (let ((setcatnum (lambda () (1+ (mod (- todo-category-number + (if back 2 0)) + (length todo-categories)))))) + (setq todo-category-number (funcall setcatnum)) + (when todo-skip-archived-categories + (while (and (zerop (todo-get-count 'todo)) + (zerop (todo-get-count 'done)) + (not (zerop (todo-get-count 'archived)))) + (setq todo-category-number (funcall setcatnum)))) + (todo-category-select) + (if transient-mark-mode (deactivate-mark)) + (goto-char (point-min)))) (defun todo-backward-category () "Visit the numerically previous category in this todo file. @@ -936,11 +929,13 @@ Categories mode." (when goto-archive (todo-archive-mode)) (set-window-buffer (selected-window) (set-buffer (find-buffer-visiting file0))) + (if transient-mark-mode (deactivate-mark)) (unless todo-global-current-todo-file (setq todo-global-current-todo-file todo-current-todo-file)) (todo-category-number category) (todo-category-select) (goto-char (point-min)) + (if (bound-and-true-p hl-line-mode) (hl-line-highlight)) (when add-item (todo-insert-item--basic)))))) (defun todo-next-item (&optional count) @@ -1026,15 +1021,17 @@ empty line above the done items separator." (setq shown (progn (goto-char (point-min)) (re-search-forward todo-done-string-start nil t))) - (if (not (pos-visible-in-window-p shown)) - (recenter) - (goto-char opoint))))))) + (if (pos-visible-in-window-p shown) + (goto-char opoint) + (recenter) + (if transient-mark-mode (deactivate-mark)))))))) (defun todo-toggle-view-done-only () "Switch between displaying only done or only todo items." (interactive) (setq todo-show-done-only (not todo-show-done-only)) - (todo-category-select)) + (todo-category-select) + (if transient-mark-mode (deactivate-mark))) (defun todo-toggle-item-highlighting () "Highlight or unhighlight the todo item the cursor is on." @@ -1245,9 +1242,10 @@ this command should be used with caution." (widen) (todo-edit-mode) (remove-overlays) - (display-warning 'todo (format "\ + (display-warning + 'todo (format "\ -Type %s to return to Todo mode. +Type %s to return to Todo%s mode. This also runs a file format check and signals an error if the format has become invalid. However, this check cannot @@ -1257,7 +1255,12 @@ You can repair this inconsistency by invoking the command `todo-repair-categories-sexp', but this will revert any renumbering of the categories you have made, so you will have to renumber them again (see `(todo-mode) Reordering -Categories')." (substitute-command-keys "\\[todo-edit-quit]")))) +Categories'). +" + (substitute-command-keys "\\[todo-edit-quit]") + (if (equal "toda" (file-name-extension + (buffer-file-name))) + " Archive" "")))) (defun todo-add-category (&optional file cat) "Add a new category to a todo file. @@ -1833,7 +1836,6 @@ consist of the last todo items and the first done items." (defvar todo-date-from-calendar nil "Helper variable for setting item date from the Emacs Calendar.") -(defvar todo-insert-item--keys-so-far) (defvar todo-insert-item--parameters) (defun todo-insert-item (&optional arg) @@ -1855,8 +1857,7 @@ already been entered and which remain available. See `(todo-mode) Inserting New Items' for details of the parameters, their associated keys and their effects." (interactive "P") - (setq todo-insert-item--keys-so-far "i") - (todo-insert-item--next-param nil (list arg) todo-insert-item--parameters)) + (todo-insert-item--next-param (list arg) todo-insert-item--parameters nil "i")) (defun todo-insert-item--basic (&optional arg diary-type date-type time where) "Function implementing the core of `todo-insert-item'." @@ -1868,15 +1869,18 @@ their associated keys and their effects." (region (eq where 'region)) (here (eq where 'here)) diary-item) - (when copy - (cond - ((not (eq major-mode 'todo-mode)) - (user-error "You must be in Todo mode to copy a todo item")) - ((todo-done-item-p) - (user-error "You cannot copy a done item as a new todo item")) - ((looking-at "^$") - (user-error "Point must be on a todo item to copy it"))) - (setq diary-item (todo-diary-item-p))) + (when (and arg here) + (user-error "Here insertion only valid in current category")) + (when (and (or copy here) + (or (not (eq major-mode 'todo-mode)) (todo-done-item-p) + (when copy (looking-at "^$")) + (save-excursion + (beginning-of-line) + ;; Point is on done items separator. + (looking-at todo-category-done)))) + (user-error (concat "Item " (if copy "copying" "insertion") + " is not valid here"))) + (when copy (setq diary-item (todo-diary-item-p))) (when region (let (use-empty-active-region) (unless (and todo-use-only-highlighted-region (use-region-p)) @@ -1884,7 +1888,6 @@ their associated keys and their effects." (let* ((obuf (current-buffer)) (ocat (todo-current-category)) (opoint (point)) - (todo-mm (eq major-mode 'todo-mode)) (cat+file (cond ((equal arg '(4)) (todo-read-category "Insert in category: ")) ((equal arg '(16)) @@ -1902,7 +1905,10 @@ their associated keys and their effects." (new-item (cond (copy (todo-item-string)) (region (buffer-substring-no-properties (region-beginning) (region-end))) - (t (read-from-minibuffer "Todo item: ")))) + (t (if (eq major-mode 'todo-archive-mode) + (user-error (concat "Cannot insert a new Todo" + " item in an archive")) + (read-from-minibuffer "Todo item: "))))) (date-string (cond ((eq date-type 'date) (todo-read-date)) @@ -1939,7 +1945,6 @@ their associated keys and their effects." (unless todo-global-current-todo-file (setq todo-global-current-todo-file todo-current-todo-file)) (let ((buffer-read-only nil) - (called-from-outside (not (and todo-mm (equal cat ocat)))) done-only item-added) (unless copy (setq new-item @@ -1963,14 +1968,8 @@ their associated keys and their effects." "\n\t" new-item nil nil 1))) (unwind-protect (progn - ;; Make sure the correct category is selected. There - ;; are two cases: (i) we just visited the file, so no - ;; category is selected yet, or (ii) we invoked - ;; insertion "here" from outside the category we want - ;; to insert in (with priority insertion, category - ;; selection is done by todo-set-item-priority). - (when (or (= (- (point-max) (point-min)) (buffer-size)) - (and here called-from-outside)) + ;; If we just visited the file, no category is selected yet. + (when (= (- (point-max) (point-min)) (buffer-size)) (todo-category-number cat) (todo-category-select)) ;; If only done items are displayed in category, @@ -1981,16 +1980,7 @@ their associated keys and their effects." (setq done-only t) (todo-toggle-view-done-only)) (if here - (progn - ;; If command was invoked with point in done - ;; items section or outside of the current - ;; category, can't insert "here", so to be - ;; useful give new item top priority. - (when (or (todo-done-item-section-p) - called-from-outside - done-only) - (goto-char (point-min))) - (todo-insert-with-overlays new-item)) + (todo-insert-with-overlays new-item) (todo-set-item-priority new-item cat t)) (setq item-added t)) ;; If user cancels before setting priority, restore @@ -2105,20 +2095,24 @@ the item at point." (setq todo-categories-with-marks (assq-delete-all cat todo-categories-with-marks))) (todo-update-categories-sexp) - (todo-prefix-overlays))) + (todo-prefix-overlays) + (when (and (zerop (todo-get-count 'diary)) + (save-excursion + (goto-char (point-min)) + (re-search-forward + (concat "^" (regexp-quote todo-category-done)) + nil t))) + (let (todo-show-with-done) (todo-category-select))))) (if ov (delete-overlay ov))))) -(defvar todo-edit-item--param-key-alist) -(defvar todo-edit-done-item--param-key-alist) - (defun todo-edit-item (&optional arg) "Choose an editing operation for the current item and carry it out." (interactive "P") (let ((marked (assoc (todo-current-category) todo-categories-with-marks))) (cond ((and (todo-done-item-p) (not marked)) - (todo-edit-item--next-key todo-edit-done-item--param-key-alist)) + (todo-edit-item--next-key 'done arg)) ((or marked (todo-item-string)) - (todo-edit-item--next-key todo-edit-item--param-key-alist arg))))) + (todo-edit-item--next-key 'todo arg))))) (defun todo-edit-item--text (&optional arg) "Function providing the text editing facilities of `todo-edit-item'." @@ -2241,7 +2235,8 @@ made in the number or names of categories." (insert item)) (kill-buffer) (unless (eq (current-buffer) buf) - (set-window-buffer (selected-window) (set-buffer buf)))) + (set-window-buffer (selected-window) (set-buffer buf))) + (if transient-mark-mode (deactivate-mark))) ;; We got here via `F e'. (when (todo-check-format) ;; FIXME: separate out sexp check? @@ -2251,7 +2246,9 @@ made in the number or names of categories." ;; (todo-repair-categories-sexp) ;; Compare (todo-make-categories-list t) with sexp and if ;; different ask (todo-update-categories-sexp) ? - (todo-mode) + (if (equal (file-name-extension (buffer-file-name)) "toda") + (todo-archive-mode) + (todo-mode)) (let* ((cat-beg (concat "^" (regexp-quote todo-category-beg) "\\(.*\\)$")) (curline (buffer-substring-no-properties @@ -2274,8 +2271,8 @@ made in the number or names of categories." ;; `todo-edit-item' as e.g. `-' or `C-u'. (inc (prefix-numeric-value inc)) (buffer-read-only nil) - ndate ntime year monthname month day - dayname) ; Needed by calendar-date-display-form. + ndate ntime + year monthname month day dayname) (when marked (todo--user-error-if-marked-done-item)) (save-excursion (or (and marked (goto-char (point-min))) (todo-item-start)) @@ -2348,7 +2345,7 @@ made in the number or names of categories." ((or (string= omonth "*") (= mm 13)) (user-error "Cannot increment *")) (t - (let ((mminc (+ mm inc))) + (let ((mminc (+ mm inc (if (< inc 0) 12 0)))) ;; Increment or decrement month by INC ;; modulo 12. (setq mm (% mminc 12)) @@ -2416,7 +2413,15 @@ made in the number or names of categories." ;; If year, month or day date string components were ;; changed, rebuild the date string. (when (memq what '(year month day)) - (setq ndate (mapconcat #'eval calendar-date-display-form "")))) + (setq ndate + (calendar-dlet* + ;; Needed by calendar-date-display-form. + ((year year) + (monthname monthname) + (month month) + (day day) + (dayname dayname)) + (mapconcat #'eval calendar-date-display-form ""))))) (when ndate (replace-match ndate nil nil nil 1)) ;; Add new time string to the header, if it was supplied. (when ntime @@ -2549,7 +2554,11 @@ whose value can be either of the symbols `raise' or `lower', meaning to raise or lower the item's priority by one." (interactive) (unless (and (or (called-interactively-p 'any) (memq arg '(raise lower))) - (or (todo-done-item-p) (looking-at "^$"))) + ;; Noop if point is not on a todo (i.e. not done) item. + (or (todo-done-item-p) (looking-at "^$") + ;; On done items separator. + (save-excursion (beginning-of-line) + (looking-at todo-category-done)))) (let* ((item (or item (todo-item-string))) (marked (todo-marked-item-p)) (cat (or cat (cond ((eq major-mode 'todo-mode) @@ -2697,9 +2706,13 @@ section in the category moved to." (interactive "P") (let* ((cat1 (todo-current-category)) (marked (assoc cat1 todo-categories-with-marks))) - ;; Noop if point is not on an item and there are no marked items. - (unless (and (looking-at "^$") - (not marked)) + (unless + ;; Noop if point is not on an item and there are no marked items. + (and (or (looking-at "^$") + ;; On done items separator. + (save-excursion (beginning-of-line) + (looking-at todo-category-done))) + (not marked)) (let* ((buffer-read-only) (file1 todo-current-todo-file) (item (todo-item-string)) @@ -2856,10 +2869,14 @@ visible." (let* ((cat (todo-current-category)) (marked (assoc cat todo-categories-with-marks))) (when marked (todo--user-error-if-marked-done-item)) - (unless (and (not marked) - (or (todo-done-item-p) - ;; Point is between todo and done items. - (looking-at "^$"))) + (unless + ;; Noop if point is not on a todo (i.e. not done) item and + ;; there are no marked items. + (and (or (todo-done-item-p) (looking-at "^$") + ;; On done items separator. + (save-excursion (beginning-of-line) + (looking-at todo-category-done))) + (not marked)) (let* ((date-string (calendar-date-string (calendar-current-date) t t)) (time-string (if todo-always-add-time-string (concat " " (substring (current-time-string) @@ -3830,6 +3847,7 @@ face." (goto-char (point-min)) (while (not (eobp)) (setq match (re-search-forward regex nil t)) + (if (and match transient-mark-mode) (deactivate-mark)) (goto-char (line-beginning-position)) (unless (or (equal (point) 1) (looking-at (concat "^" (regexp-quote todo-category-beg)))) @@ -4028,19 +4046,22 @@ regexp items." (interactive "P") (todo-filter-items 'regexp arg t)) +(defvar todo--fifiles-history nil + "List of short file names used by todo-find-filtered-items-file.") + (defun todo-find-filtered-items-file () "Choose a filtered items file and visit it." (interactive) (let ((files (directory-files todo-directory t "\\.tod[rty]$" t)) falist file) (dolist (f files) - (let ((type (cond ((equal (file-name-extension f) "todr") "regexp") + (let ((sf-name (todo-short-file-name f)) + (type (cond ((equal (file-name-extension f) "todr") "regexp") ((equal (file-name-extension f) "todt") "top") ((equal (file-name-extension f) "tody") "diary")))) - (push (cons (concat (todo-short-file-name f) " (" type ")") f) - falist))) - (setq file (completing-read "Choose a filtered items file: " - falist nil t nil nil (car falist))) + (push (cons (concat sf-name " (" type ")") f) falist))) + (setq file (completing-read "Choose a filtered items file: " falist nil t nil + 'todo--fifiles-history (caar falist))) (setq file (cdr (assoc-string file falist))) (find-file file) (unless (derived-mode-p 'todo-filtered-items-mode) @@ -4050,25 +4071,27 @@ regexp items." (defun todo-go-to-source-item () "Display the file and category of the filtered item at point." (interactive) - (let* ((str (todo-item-string)) - (buf (current-buffer)) - (res (todo-find-item str)) - (found (nth 0 res)) - (file (nth 1 res)) - (cat (nth 2 res))) - (if (not found) - (message "Category %s does not contain this item." cat) - (kill-buffer buf) - (set-window-buffer (selected-window) - (set-buffer (find-buffer-visiting file))) - (setq todo-current-todo-file file) - (setq todo-category-number (todo-category-number cat)) - (let ((todo-show-with-done (if (or todo-filter-done-items - (eq (cdr found) 'done)) - t - todo-show-with-done))) - (todo-category-select)) - (goto-char (car found))))) + (unless (looking-at "^$") ; Empty line at EOB. + (let* ((str (todo-item-string)) + (buf (current-buffer)) + (res (todo-find-item str)) + (found (nth 0 res)) + (file (nth 1 res)) + (cat (nth 2 res))) + (if (not found) + (message "Category %s does not contain this item." cat) + (kill-buffer buf) + (set-window-buffer (selected-window) + (set-buffer (find-buffer-visiting file))) + (setq todo-current-todo-file file) + (setq todo-category-number (todo-category-number cat)) + (let ((todo-show-with-done (if (or todo-filter-done-items + (eq (cdr found) 'done)) + t + todo-show-with-done))) + (todo-category-select)) + (if transient-mark-mode (deactivate-mark)) + (goto-char (car found)))))) (defvar todo-multiple-filter-files nil "List of files selected from `todo-multiple-filter-files' widget.") @@ -4520,8 +4543,11 @@ its priority has changed, and `same' otherwise." (defun todo-save-filtered-items-buffer () "Save current Filtered Items buffer to a file. If the file already exists, overwrite it only on confirmation." - (let ((filename (or (buffer-file-name) (todo-filter-items-filename)))) - (write-file filename t))) + (let ((filename (or (buffer-file-name) (todo-filter-items-filename))) + (bufname (buffer-name))) + (write-file filename t) + (setq buffer-read-only t) + (rename-buffer bufname))) ;; ----------------------------------------------------------------------------- ;;; Printing Todo mode buffers @@ -4613,12 +4639,13 @@ strings built using the default value of (defun todo-convert-legacy-date-time () "Return converted date-time string. Helper function for `todo-convert-legacy-files'." - (let* ((year (match-string 1)) - (month (match-string 2)) - (monthname (calendar-month-name (string-to-number month) t)) - (day (match-string 3)) - (time (match-string 4)) - dayname) + (calendar-dlet* + ((year (match-string 1)) + (month (match-string 2)) + (monthname (calendar-month-name (string-to-number month) t)) + (day (match-string 3)) + (time (match-string 4)) + dayname) (replace-match "") (insert (mapconcat #'eval calendar-date-display-form "") (when time (concat " " time))))) @@ -5075,7 +5102,7 @@ again." (defun todo-check-format () "Signal an error if the current todo file is ill-formatted. -Otherwise return t. Display a message if the file is well-formed +Otherwise return t. Display a warning if the file is well-formed but the categories sexp differs from the current value of `todo-categories'." (save-excursion @@ -5109,12 +5136,14 @@ but the categories sexp differs from the current value of (forward-line))) ;; Warn user if categories sexp has changed. (unless (string= ssexp cats) - (message (concat "The sexp at the beginning of the file differs " - "from the value of `todo-categories'.\n" - "If the sexp is wrong, you can fix it with " - "M-x todo-repair-categories-sexp,\n" - "but note this reverts any changes you have " - "made in the order of the categories.")))))) + (display-warning 'todo "\ + +The sexp at the beginning of the file differs from the value of +`todo-categories'. If the sexp is wrong, you can fix it with +M-x todo-repair-categories-sexp, but note this reverts any +changes you have made in the order of the categories. +" + ))))) t) (defun todo-item-start () @@ -5131,6 +5160,8 @@ but the categories sexp differs from the current value of (forward-line) (looking-at (concat "^" (regexp-quote todo-category-done)))))) + ;; Point is on done items separator. + (save-excursion (beginning-of-line) (looking-at todo-category-done)) ;; Buffer is widened. (looking-at (regexp-quote todo-category-beg))) (goto-char (line-beginning-position)) @@ -5140,8 +5171,11 @@ but the categories sexp differs from the current value of (defun todo-item-end () "Move to end of current todo item and return its position." - ;; Items cannot end with a blank line. - (unless (looking-at "^$") + (unless (or + ;; Items cannot end with a blank line. + (looking-at "^$") + ;; Point is on done items separator. + (save-excursion (beginning-of-line) (looking-at todo-category-done))) (let* ((done (todo-done-item-p)) (to-lim nil) ;; For todo items, end is before the done items section, for done @@ -5292,6 +5326,7 @@ Overrides `diary-goto-entry'." nil t) (todo-category-number (match-string 1)) (todo-category-select) + (if transient-mark-mode (deactivate-mark)) (goto-char opoint)))))) (add-function :override diary-goto-entry-function #'todo-diary-goto-entry) @@ -5493,12 +5528,14 @@ of each other." ;;; Generating and applying item insertion and editing key sequences ;; ----------------------------------------------------------------------------- -;; Thanks to Stefan Monnier for suggesting dynamically generating item -;; insertion commands and their key bindings, and offering an elegant -;; implementation, which, however, relies on lexical scoping and so -;; cannot be used here until the Calendar code used by todo-mode.el is -;; converted to lexical binding. Hence, the following implementation -;; uses dynamic binding. +;; Thanks to Stefan Monnier for (i) not only suggesting dynamically +;; generating item insertion commands and their key bindings but also +;; offering an elegant implementation which, however, since it used +;; lexical binding, was at the time incompatible with the Calendar and +;; Diary code in todo-mode.el; and (ii) later making that code +;; compatible with lexical binding, so that his implementation, of +;; which the following is a somewhat expanded version, could be +;; realized in todo-mode.el. (defconst todo-insert-item--parameters '((default copy) (diary nonmarking) (calendar date dayname) time (here region)) @@ -5506,91 +5543,33 @@ of each other." Passed by `todo-insert-item' to `todo-insert-item--next-param' to dynamically create item insertion commands.") -(defconst todo-insert-item--param-key-alist - '((default . "i") - (copy . "p") - (diary . "y") - (nonmarking . "k") - (calendar . "c") - (date . "d") - (dayname . "n") - (time . "t") - (here . "h") - (region . "r")) - "List pairing item insertion parameters with their completion keys.") - -(defsubst todo-insert-item--keyof (param) - "Return key paired with item insertion PARAM." - (cdr (assoc param todo-insert-item--param-key-alist))) - -(defun todo-insert-item--argsleft (key list) - "Return sublist of LIST whose first member corresponds to KEY." - (let (l sym) - (mapc (lambda (m) - (when (consp m) - (catch 'found1 - (dolist (s m) - (when (equal key (todo-insert-item--keyof s)) - (throw 'found1 (setq sym s)))))) - (if sym - (progn - (push sym l) - (setq sym nil)) - (push m l))) - list) - (setq list (reverse l))) - (memq (catch 'found2 - (dolist (e todo-insert-item--param-key-alist) - (when (equal key (cdr e)) - (throw 'found2 (car e))))) - list)) - -(defsubst todo-insert-item--this-key () (char-to-string last-command-event)) - -(defvar todo-insert-item--keys-so-far "" - "String of item insertion keys so far entered for this command.") - -(defvar todo-insert-item--args nil) -(defvar todo-insert-item--argleft nil) -(defvar todo-insert-item--argsleft nil) -(defvar todo-insert-item--newargsleft nil) - -(defun todo-insert-item--apply-args () - "Build list of arguments for item insertion and apply them. -The list consists of item insertion parameters that can be passed -as insertion command arguments in fixed positions. If a position -in the list is not occupied by the corresponding parameter, it is -occupied by nil." - (let* ((arg (list (car todo-insert-item--args))) - (args (nconc (cdr todo-insert-item--args) - (list (car (todo-insert-item--argsleft - (todo-insert-item--this-key) - todo-insert-item--argsleft))))) - (arglist (if (= 4 (length args)) - args - (let ((v (make-vector 4 nil)) elt) - (while args - (setq elt (pop args)) - (cond ((memq elt '(diary nonmarking)) - (aset v 0 elt)) - ((memq elt '(calendar date dayname)) - (aset v 1 elt)) - ((eq elt 'time) - (aset v 2 elt)) - ((memq elt '(copy here region)) - (aset v 3 elt)))) - (append v nil))))) - (apply #'todo-insert-item--basic (nconc arg arglist)))) - -(defun todo-insert-item--next-param (last args argsleft) - "Build item insertion command from LAST, ARGS and ARGSLEFT and call it. -Dynamically generate key bindings, prompting with the keys -already entered and those still available." - (cl-assert argsleft) +(defun todo-insert-item--next-param (args params last keys-so-far) + "Generate and invoke an item insertion command. +Dynamically generate the command, its arguments ARGS and its key +binding by recursing through the list of parameters PARAMS, +taking the LAST from a sublist and prompting with KEYS-SO-FAR +keys already entered and those still available." + (cl-assert params) (let* ((map (make-sparse-keymap)) + (param-key-alist '((default . "i") + (copy . "p") + (diary . "y") + (nonmarking . "k") + (calendar . "c") + (date . "d") + (dayname . "n") + (time . "t") + (here . "h") + (region . "r"))) + ;; Return key paired with given item insertion parameter. + (key-of (lambda (param) (cdr (assoc param param-key-alist)))) + ;; The key just typed. + (this-key (lambda () (char-to-string last-command-event))) (prompt nil) - (addprompt - (lambda (k name) + ;; Add successively entered keys to the prompt and show what + ;; possibilities remain. + (add-to-prompt + (lambda (key name) (setq prompt (concat prompt (format @@ -5600,80 +5579,119 @@ already entered and those still available." "%s=>%s" (when (memq name '(copy nonmarking dayname region)) " }")) - (propertize k 'face 'todo-key-prompt) - name)))))) - (setq todo-insert-item--args args) - (setq todo-insert-item--argsleft argsleft) + (propertize key 'face 'todo-key-prompt) + name))))) + ;; Return the sublist of the given list of parameters whose + ;; first member is paired with the given key. + (get-params + (lambda (key lst) + (setq lst (if (consp lst) lst (list lst))) + (let (l sym) + (mapc (lambda (m) + (when (consp m) + (catch 'found1 + (dolist (s m) + (when (equal key (funcall key-of s)) + (throw 'found1 (setq sym s)))))) + (if sym + (progn + (push sym l) + (setq sym nil)) + (push m l))) + lst) + (setq lst (reverse l))) + (memq (catch 'found2 + (dolist (e param-key-alist) + (when (equal key (cdr e)) + (throw 'found2 (car e))))) + lst))) + ;; Build list of arguments for item insertion and then + ;; execute the basic insertion function. The list consists of + ;; item insertion parameters that can be passed as insertion + ;; command arguments in fixed positions. If a position in + ;; the list is not occupied by the corresponding parameter, + ;; it is occupied by nil. + (gen-and-exec + (lambda () + (let* ((arg (list (car args))) ; Possible prefix argument. + (rest (nconc (cdr args) + (list (car (funcall get-params + (funcall this-key) + params))))) + (parlist (if (= 4 (length rest)) + rest + (let ((v (make-vector 4 nil)) elt) + (while rest + (setq elt (pop rest)) + (cond ((memq elt '(diary nonmarking)) + (aset v 0 elt)) + ((memq elt '(calendar date dayname)) + (aset v 1 elt)) + ((eq elt 'time) + (aset v 2 elt)) + ((memq elt '(copy here region)) + (aset v 3 elt)))) + (append v nil))))) + (apply #'todo-insert-item--basic (nconc arg parlist))))) + ;; Operate on a copy of the parameter list so the original is + ;; not consumed, thus available for the next key typed. + (params0 params)) (when last (if (memq last '(default copy)) (progn - (setq todo-insert-item--argsleft nil) - (todo-insert-item--apply-args)) - (let ((k (todo-insert-item--keyof last))) - (funcall addprompt k (make-symbol (concat (symbol-name last) ":GO!"))) - (define-key map (todo-insert-item--keyof last) + (setq params0 nil) + (funcall gen-and-exec)) + (let ((key (funcall key-of last))) + (funcall add-to-prompt key (make-symbol + (concat (symbol-name last) ":GO!"))) + (define-key map (funcall key-of last) (lambda () (interactive) - (todo-insert-item--apply-args)))))) - (while todo-insert-item--argsleft - (let ((x (car todo-insert-item--argsleft))) - (setq todo-insert-item--newargsleft (cdr todo-insert-item--argsleft)) - (dolist (argleft (if (consp x) x (list x))) - (let ((k (todo-insert-item--keyof argleft))) - (funcall addprompt k argleft) - (define-key map k - (if (null todo-insert-item--newargsleft) - (lambda () (interactive) - (todo-insert-item--apply-args)) - (lambda () (interactive) - (setq todo-insert-item--keys-so-far - (concat todo-insert-item--keys-so-far " " - (todo-insert-item--this-key))) - (todo-insert-item--next-param - (car (todo-insert-item--argsleft - (todo-insert-item--this-key) - todo-insert-item--argsleft)) - (nconc todo-insert-item--args - (list (car (todo-insert-item--argsleft - (todo-insert-item--this-key) - todo-insert-item--argsleft)))) - (cdr (todo-insert-item--argsleft - (todo-insert-item--this-key) - todo-insert-item--argsleft))))))))) - (setq todo-insert-item--argsleft todo-insert-item--newargsleft)) - (when prompt (message "Press a key (so far `%s'): %s" - todo-insert-item--keys-so-far prompt)) + (funcall gen-and-exec)))))) + (while params0 + (let* ((x (car params0)) + (restparams (cdr params0))) + (dolist (param (if (consp x) x (list x))) + (let ((key (funcall key-of param))) + (funcall add-to-prompt key param) + (define-key map key + (if (null restparams) + (lambda () (interactive) + (funcall gen-and-exec)) + (lambda () (interactive) + (setq keys-so-far (concat keys-so-far " " (funcall this-key))) + (todo-insert-item--next-param + (nconc args (list (car (funcall get-params + (funcall this-key) param)))) + (cdr (funcall get-params (funcall this-key) params)) + (car (funcall get-params (funcall this-key) param)) + keys-so-far)))))) + (setq params0 restparams))) (set-transient-map map) - (setq todo-insert-item--argsleft argsleft))) - -(defconst todo-edit-item--param-key-alist - '((edit . "e") - (header . "h") - (multiline . "m") - (diary . "y") - (nonmarking . "k") - (date . "d") - (time . "t")) - "Alist of item editing parameters and their keys.") - -(defconst todo-edit-item--date-param-key-alist - '((full . "f") - (calendar . "c") - (today . "a") - (dayname . "n") - (year . "y") - (month . "m") - (daynum . "d")) - "Alist of item date editing parameters and their keys.") - -(defconst todo-edit-done-item--param-key-alist - '((add/edit . "c") - (delete . "d")) - "Alist of done item comment editing parameters and their keys.") - -(defvar todo-edit-item--prompt "Press a key (so far `e'): ") - -(defun todo-edit-item--next-key (params &optional arg) - (let* ((p->k (mapconcat (lambda (elt) + (when prompt (message "Press a key (so far `%s'): %s" keys-so-far prompt)) + (setq params0 params))) + +(defun todo-edit-item--next-key (type &optional arg) + (let* ((todo-param-key-alist '((edit . "e") + (header . "h") + (multiline . "m") + (diary . "y") + (nonmarking . "k") + (date . "d") + (time . "t"))) + (done-param-key-alist '((add/edit . "c") + (delete . "d"))) + (date-param-key-alist '((full . "f") + (calendar . "c") + (today . "a") + (dayname . "n") + (year . "y") + (month . "m") + (daynum . "d"))) + (params (pcase type + ('todo todo-param-key-alist) + ('done done-param-key-alist) + ('date date-param-key-alist))) + (p->k (mapconcat (lambda (elt) (format "%s=>%s" (propertize (cdr elt) 'face 'todo-key-prompt) @@ -5682,31 +5700,32 @@ already entered and those still available." '(add/edit delete)) " comment")))) params " ")) - (key-prompt (substitute-command-keys todo-edit-item--prompt)) + (key-prompt (substitute-command-keys + (concat "Press a key (so far `e" + (if (eq type 'date) " d" "") + "'): "))) (this-key (let ((key (read-key (concat key-prompt p->k)))) (and (characterp key) (char-to-string key)))) (this-param (car (rassoc this-key params)))) (pcase this-param - (`edit (todo-edit-item--text)) - (`header (todo-edit-item--text 'include-header)) - (`multiline (todo-edit-item--text 'multiline)) - (`add/edit (todo-edit-item--text 'comment-edit)) - (`delete (todo-edit-item--text 'comment-delete)) - (`diary (todo-edit-item--diary-inclusion)) - (`nonmarking (todo-edit-item--diary-inclusion 'nonmarking)) - (`date (let ((todo-edit-item--prompt "Press a key (so far `e d'): ")) - (todo-edit-item--next-key - todo-edit-item--date-param-key-alist arg))) - (`full (progn (todo-edit-item--header 'date) + ('edit (todo-edit-item--text)) + ('header (todo-edit-item--text 'include-header)) + ('multiline (todo-edit-item--text 'multiline)) + ('add/edit (todo-edit-item--text 'comment-edit)) + ('delete (todo-edit-item--text 'comment-delete)) + ('diary (todo-edit-item--diary-inclusion)) + ('nonmarking (todo-edit-item--diary-inclusion 'nonmarking)) + ('date (todo-edit-item--next-key 'date arg)) + ('full (progn (todo-edit-item--header 'date) (when todo-always-add-time-string (todo-edit-item--header 'time)))) - (`calendar (todo-edit-item--header 'calendar)) - (`today (todo-edit-item--header 'today)) - (`dayname (todo-edit-item--header 'dayname)) - (`year (todo-edit-item--header 'year arg)) - (`month (todo-edit-item--header 'month arg)) - (`daynum (todo-edit-item--header 'day arg)) - (`time (todo-edit-item--header 'time))))) + ('calendar (todo-edit-item--header 'calendar)) + ('today (todo-edit-item--header 'today)) + ('dayname (todo-edit-item--header 'dayname)) + ('year (todo-edit-item--header 'year arg)) + ('month (todo-edit-item--header 'month arg)) + ('daynum (todo-edit-item--header 'day arg)) + ('time (todo-edit-item--header 'time))))) ;; ----------------------------------------------------------------------------- ;;; Todo minibuffer utilities @@ -5990,8 +6009,8 @@ indicating an unspecified month, day, or year. When ARG is `day', non-nil arguments MO and YR determine the number of the last the day of the month." - (let (year monthname month day - dayname) ; Needed by calendar-date-display-form. + (calendar-dlet* + (year monthname month day dayname) ;Needed by calendar-date-display-form. (when (or (not arg) (eq arg 'year)) (while (if (natnump year) (< year 1) (not (eq year '*))) (setq year (read-from-minibuffer @@ -6418,9 +6437,6 @@ Filtered Items mode following todo (not done) items." ("N" todo-toggle-prefix-numbers) ("PB" todo-print-buffer) ("PF" todo-print-buffer-to-file) - ("b" todo-backward-category) - ("d" todo-item-done) - ("f" todo-forward-category) ("j" todo-jump-to-category) ("n" todo-next-item) ("p" todo-previous-item) @@ -6435,6 +6451,8 @@ Filtered Items mode following todo (not done) items." ("Fc" todo-show-categories-table) ("S" todo-search) ("X" todo-clear-matches) + ("b" todo-backward-category) + ("f" todo-forward-category) ("*" todo-toggle-mark-item) ) "List of key bindings for Todo and Todo Archive modes.") @@ -6703,32 +6721,19 @@ Added to `window-configuration-change-hook' in Todo mode." (setq-local todo-current-todo-file (file-truename (buffer-file-name))) (setq-local todo-show-done-only t)) -(defun todo-mode-external-set () - "Set `todo-categories' externally to `todo-current-todo-file'." - (setq-local todo-current-todo-file todo-global-current-todo-file) - (let ((cats (with-current-buffer - ;; Can't use find-buffer-visiting when - ;; `todo-show-categories-table' is called on first - ;; invocation of `todo-show', since there is then - ;; no buffer visiting the current file. - (find-file-noselect todo-current-todo-file 'nowarn) - (or todo-categories - ;; In Todo Edit mode todo-categories is now nil - ;; since it uses same buffer as Todo mode but - ;; doesn't have the latter's local variables. - (save-excursion - (goto-char (point-min)) - (read (buffer-substring-no-properties - (line-beginning-position) - (line-end-position)))))))) - (setq-local todo-categories cats))) - (define-derived-mode todo-edit-mode text-mode "Todo-Ed" "Major mode for editing multiline todo items. \\{todo-edit-mode-map}" (todo-modes-set-1) - (todo-mode-external-set) + (if (> (buffer-size) (- (point-max) (point-min))) + ;; Editing one item in an indirect buffer, so buffer-file-name is nil. + (setq-local todo-current-todo-file todo-global-current-todo-file) + ;; When editing archive file, make sure it is current todo file. + (setq-local todo-current-todo-file (file-truename (buffer-file-name))) + ;; Need this when editing the whole file to return to the category + ;; editing was invoked from. + (setq-local todo-categories (todo-set-categories))) (setq buffer-read-only nil)) (put 'todo-categories-mode 'mode-class 'special) @@ -6737,7 +6742,15 @@ Added to `window-configuration-change-hook' in Todo mode." "Major mode for displaying and editing todo categories. \\{todo-categories-mode-map}" - (todo-mode-external-set)) + (setq-local todo-current-todo-file todo-global-current-todo-file) + (setq-local todo-categories + ;; Can't use find-buffer-visiting when + ;; `todo-show-categories-table' is called on first + ;; invocation of `todo-show', since there is then no + ;; buffer visiting the current file. + (with-current-buffer (find-file-noselect + todo-current-todo-file 'nowarn) + todo-categories))) (put 'todo-filtered-items-mode 'mode-class 'special) |