Fix tiny spelling mistake.
[org-mode.git] / contrib / lisp / org-interactive-query.el
blob1051e7ce113ee6a9959d52c8efa59e95844ea36c
1 ;;; org-interactive-query.el --- Interactive modification of agenda query
2 ;;
3 ;; Copyright 2007 Free Software Foundation, Inc.
4 ;;
5 ;; Author: Christopher League <league at contrapunctus dot net>
6 ;; Version: 1.0
7 ;; Keywords: org, wp
8 ;;
9 ;; This program is free software; you can redistribute it and/or modify
10 ;; it under the terms of the GNU General Public License as published by
11 ;; the Free Software Foundation; either version 3, or (at your option)
12 ;; any later version.
14 ;; This program is distributed in the hope that it will be useful,
15 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 ;; GNU General Public License for more details.
19 ;; You should have received a copy of the GNU General Public License
20 ;; along with this program; if not, write to the Free Software
21 ;; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
23 ;;; Commentary:
26 ;; This library implements interactive modification of a tags/todo query
27 ;; in the org-agenda. It adds 4 keys to the agenda
29 ;; / add a keyword as a positive selection criterion
30 ;; \ add a keyword as a newgative selection criterion
31 ;; = clear a keyword from the selection string
32 ;; ;
34 (require 'org)
36 (org-defkey org-agenda-mode-map "=" 'org-agenda-query-clear-cmd)
37 (org-defkey org-agenda-mode-map "/" 'org-agenda-query-and-cmd)
38 (org-defkey org-agenda-mode-map ";" 'org-agenda-query-or-cmd)
39 (org-defkey org-agenda-mode-map "\\" 'org-agenda-query-not-cmd)
41 ;;; Agenda interactive query manipulation
43 (defcustom org-agenda-query-selection-single-key t
44 "Non-nil means query manipulation exits after first change.
45 When nil, you have to press RET to exit it.
46 During query selection, you can toggle this flag with `C-c'.
47 This variable can also have the value `expert'. In this case, the window
48 displaying the tags menu is not even shown, until you press C-c again."
49 :group 'org-agenda
50 :type '(choice
51 (const :tag "No" nil)
52 (const :tag "Yes" t)
53 (const :tag "Expert" expert)))
55 (defun org-agenda-query-selection (current op table &optional todo-table)
56 "Fast query manipulation with single keys.
57 CURRENT is the current query string, OP is the initial
58 operator (one of \"+|-=\"), TABLE is an alist of tags and
59 corresponding keys, possibly with grouping information.
60 TODO-TABLE is a similar table with TODO keywords, should these
61 have keys assigned to them. If the keys are nil, a-z are
62 automatically assigned. Returns the new query string, or nil to
63 not change the current one."
64 (let* ((fulltable (append table todo-table))
65 (maxlen (apply 'max (mapcar
66 (lambda (x)
67 (if (stringp (car x)) (string-width (car x)) 0))
68 fulltable)))
69 (fwidth (+ maxlen 3 1 3))
70 (ncol (/ (- (window-width) 4) fwidth))
71 (expert (eq org-agenda-query-selection-single-key 'expert))
72 (exit-after-next org-agenda-query-selection-single-key)
73 (done-keywords org-done-keywords)
74 tbl char cnt e groups ingroup
75 tg c2 c c1 ntable rtn)
76 (save-window-excursion
77 (if expert
78 (set-buffer (get-buffer-create " *Org tags*"))
79 (delete-other-windows)
80 (split-window-vertically)
81 (org-switch-to-buffer-other-window (get-buffer-create " *Org tags*")))
82 (erase-buffer)
83 (org-set-local 'org-done-keywords done-keywords)
84 (insert "Query: " current "\n")
85 (org-agenda-query-op-line op)
86 (insert "\n\n")
87 (org-fast-tag-show-exit exit-after-next)
88 (setq tbl fulltable char ?a cnt 0)
89 (while (setq e (pop tbl))
90 (cond
91 ((equal e '(:startgroup))
92 (push '() groups) (setq ingroup t)
93 (when (not (= cnt 0))
94 (setq cnt 0)
95 (insert "\n"))
96 (insert "{ "))
97 ((equal e '(:endgroup))
98 (setq ingroup nil cnt 0)
99 (insert "}\n"))
101 (setq tg (car e) c2 nil)
102 (if (cdr e)
103 (setq c (cdr e))
104 ;; automatically assign a character.
105 (setq c1 (string-to-char
106 (downcase (substring
107 tg (if (= (string-to-char tg) ?@) 1 0)))))
108 (if (or (rassoc c1 ntable) (rassoc c1 table))
109 (while (or (rassoc char ntable) (rassoc char table))
110 (setq char (1+ char)))
111 (setq c2 c1))
112 (setq c (or c2 char)))
113 (if ingroup (push tg (car groups)))
114 (setq tg (org-add-props tg nil 'face
115 (cond
116 ((not (assoc tg table))
117 (org-get-todo-face tg))
118 (t nil))))
119 (if (and (= cnt 0) (not ingroup)) (insert " "))
120 (insert "[" c "] " tg (make-string
121 (- fwidth 4 (length tg)) ?\ ))
122 (push (cons tg c) ntable)
123 (when (= (setq cnt (1+ cnt)) ncol)
124 (insert "\n")
125 (if ingroup (insert " "))
126 (setq cnt 0)))))
127 (setq ntable (nreverse ntable))
128 (insert "\n")
129 (goto-char (point-min))
130 (if (and (not expert) (fboundp 'fit-window-to-buffer))
131 (fit-window-to-buffer))
132 (setq rtn
133 (catch 'exit
134 (while t
135 (message "[a-z..]:Toggle [SPC]:clear [RET]:accept [TAB]:free%s%s"
136 (if groups " [!] no groups" " [!]groups")
137 (if expert " [C-c]:window" (if exit-after-next " [C-c]:single" " [C-c]:multi")))
138 (setq c (let ((inhibit-quit t)) (read-char-exclusive)))
139 (cond
140 ((= c ?\r) (throw 'exit t))
141 ((= c ?!)
142 (setq groups (not groups))
143 (goto-char (point-min))
144 (while (re-search-forward "[{}]" nil t) (replace-match " ")))
145 ((= c ?\C-c)
146 (if (not expert)
147 (org-fast-tag-show-exit
148 (setq exit-after-next (not exit-after-next)))
149 (setq expert nil)
150 (delete-other-windows)
151 (split-window-vertically)
152 (org-switch-to-buffer-other-window " *Org tags*")
153 (and (fboundp 'fit-window-to-buffer)
154 (fit-window-to-buffer))))
155 ((or (= c ?\C-g)
156 (and (= c ?q) (not (rassoc c ntable))))
157 (setq quit-flag t))
158 ((= c ?\ )
159 (setq current "")
160 (if exit-after-next (setq exit-after-next 'now)))
161 ((= c ?\[) ; clear left
162 (org-agenda-query-decompose current)
163 (setq current (concat "/" (match-string 2 current)))
164 (if exit-after-next (setq exit-after-next 'now)))
165 ((= c ?\]) ; clear right
166 (org-agenda-query-decompose current)
167 (setq current (match-string 1 current))
168 (if exit-after-next (setq exit-after-next 'now)))
169 ((= c ?\t)
170 (condition-case nil
171 (setq current (read-string "Query: " current))
172 (quit))
173 (if exit-after-next (setq exit-after-next 'now)))
174 ;; operators
175 ((or (= c ?/) (= c ?+)) (setq op "+"))
176 ((or (= c ?\;) (= c ?|)) (setq op "|"))
177 ((or (= c ?\\) (= c ?-)) (setq op "-"))
178 ((= c ?=) (setq op "="))
179 ;; todos
180 ((setq e (rassoc c todo-table) tg (car e))
181 (setq current (org-agenda-query-manip
182 current op groups 'todo tg))
183 (if exit-after-next (setq exit-after-next 'now)))
184 ;; tags
185 ((setq e (rassoc c ntable) tg (car e))
186 (setq current (org-agenda-query-manip
187 current op groups 'tag tg))
188 (if exit-after-next (setq exit-after-next 'now))))
189 (if (eq exit-after-next 'now) (throw 'exit t))
190 (goto-char (point-min))
191 (beginning-of-line 1)
192 (delete-region (point) (point-at-eol))
193 (insert "Query: " current)
194 (beginning-of-line 2)
195 (delete-region (point) (point-at-eol))
196 (org-agenda-query-op-line op)
197 (goto-char (point-min)))))
198 (if rtn current nil))))
200 (defun org-agenda-query-op-line (op)
201 (insert "Operator: "
202 (org-agenda-query-op-entry (equal op "+") "/+" "and")
203 (org-agenda-query-op-entry (equal op "|") ";|" "or")
204 (org-agenda-query-op-entry (equal op "-") "\\-" "not")
205 (org-agenda-query-op-entry (equal op "=") "=" "clear")))
207 (defun org-agenda-query-op-entry (matchp chars str)
208 (if matchp
209 (org-add-props (format "[%s %s] " chars (upcase str))
210 nil 'face 'org-todo)
211 (format "[%s]%s " chars str)))
213 (defun org-agenda-query-decompose (current)
214 (string-match "\\([^/]*\\)/?\\(.*\\)" current))
216 (defun org-agenda-query-clear (current prefix tag)
217 (if (string-match (concat prefix "\\b" (regexp-quote tag) "\\b") current)
218 (replace-match "" t t current)
219 current))
221 (defun org-agenda-query-manip (current op groups kind tag)
222 "Apply an operator to a query string and a tag.
223 CURRENT is the current query string, OP is the operator, GROUPS is a
224 list of lists of tags that are mutually exclusive. KIND is 'tag for a
225 regular tag, or 'todo for a TODO keyword, and TAG is the tag or
226 keyword string."
227 ;; If this tag is already in query string, remove it.
228 (setq current (org-agenda-query-clear current "[-\\+&|]?" tag))
229 (if (equal op "=") current
230 ;; When using AND, also remove mutually exclusive tags.
231 (if (equal op "+")
232 (loop for g in groups do
233 (if (member tag g)
234 (mapc (lambda (x)
235 (setq current
236 (org-agenda-query-clear current "\\+" x)))
237 g))))
238 ;; Decompose current query into q1 (tags) and q2 (TODOs).
239 (org-agenda-query-decompose current)
240 (let* ((q1 (match-string 1 current))
241 (q2 (match-string 2 current)))
242 (cond
243 ((eq kind 'tag)
244 (concat q1 op tag "/" q2))
245 ;; It's a TODO; when using AND, drop all other TODOs.
246 ((equal op "+")
247 (concat q1 "/+" tag))
249 (concat q1 "/" q2 op tag))))))
251 (defun org-agenda-query-global-todo-keys (&optional files)
252 "Return alist of all TODO keywords and their fast keys, in all FILES."
253 (let (alist)
254 (unless (and files (car files))
255 (setq files (org-agenda-files)))
256 (save-excursion
257 (loop for f in files do
258 (set-buffer (find-file-noselect f))
259 (loop for k in org-todo-key-alist do
260 (setq alist (org-agenda-query-merge-todo-key
261 alist k)))))
262 alist))
264 (defun org-agenda-query-merge-todo-key (alist entry)
265 (let (e)
266 (cond
267 ;; if this is not a keyword (:startgroup, etc), ignore it
268 ((not (stringp (car entry))))
269 ;; if keyword already exists, replace char if it's null
270 ((setq e (assoc (car entry) alist))
271 (when (null (cdr e)) (setcdr e (cdr entry))))
272 ;; if char already exists, prepend keyword but drop char
273 ((rassoc (cdr entry) alist)
274 (message "TRACE POSITION 2")
275 (setq alist (cons (cons (car entry) nil) alist)))
276 ;; else, prepend COPY of entry
278 (setq alist (cons (cons (car entry) (cdr entry)) alist)))))
279 alist)
281 (defun org-agenda-query-generic-cmd (op)
282 "Activate query manipulation with OP as initial operator."
283 (let ((q (org-agenda-query-selection org-agenda-query-string op
284 org-tag-alist
285 (org-agenda-query-global-todo-keys))))
286 (when q
287 (setq org-agenda-query-string q)
288 (org-agenda-redo))))
290 (defun org-agenda-query-clear-cmd ()
291 "Activate query manipulation, to clear a tag from the string."
292 (interactive)
293 (org-agenda-query-generic-cmd "="))
295 (defun org-agenda-query-and-cmd ()
296 "Activate query manipulation, initially using the AND (+) operator."
297 (interactive)
298 (org-agenda-query-generic-cmd "+"))
300 (defun org-agenda-query-or-cmd ()
301 "Activate query manipulation, initially using the OR (|) operator."
302 (interactive)
303 (org-agenda-query-generic-cmd "|"))
305 (defun org-agenda-query-not-cmd ()
306 "Activate query manipulation, initially using the NOT (-) operator."
307 (interactive)
308 (org-agenda-query-generic-cmd "-"))
310 (provide 'org-interactive-query)