Merged from mwolson@gnu.org--2006 (patch 43-47)
[planner-el.git] / planner-id.el
blob61178d0a628182857ef00c0948c5d4217cb6b49a
1 ;;; planner-id.el --- planner.el extension for global task IDs
3 ;; Copyright (C) 2003, 2004, 2005 Free Software Foundation, Inc.
5 ;; Author: Sacha Chua <sacha@free.net.ph>
6 ;; URL: http://www.plannerlove.com/
8 ;; This file is part of Planner. It is not part of GNU Emacs.
10 ;; Planner is free software; you can redistribute it and/or modify it
11 ;; under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation; either version 2, or (at your option)
13 ;; any later version.
15 ;; Planner is distributed in the hope that it will be useful, but
16 ;; WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 ;; General Public License for more details.
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with Planner; see the file COPYING. If not, write to the
22 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
23 ;; Boston, MA 02110-1301, USA.
25 ;;; Commentary:
27 ;; After loading planner.el, place planner-id.el in your load path
28 ;; and add this to your .emacs
30 ;; (require 'planner-id)
32 ;; This module modifies the behavior of planner.el, adding global task
33 ;; IDs so that tasks can be edited and updated.
35 ;; To automatically update linked tasks whenever you save a planner
36 ;; file, set planner-id-update-automatically to a non-nil value. This
37 ;; does not update completed or cancelled tasks. See documentation for
38 ;; planner-id-update-tasks-on-page to find out how to force updates.
40 ;; Planner IDs are of the form {{Identifier:Number}}
42 ;; Alternatives: If you don't mind using a function to change your
43 ;; task descriptions, you may find M-x planner-edit-task-description
44 ;; easier to use. Other changes (A/B/C, status) can be applied with
45 ;; M-x planner-update-task after you edit the buffer.
47 ;;; Contributors:
49 ;; Oliver Krause <oik@gmx.net>: Main idea, testing.
51 ;; Jim Ottaway provided a fix for interaction of task IDs with
52 ;; multiple links, as well as a regexp bug.
54 ;; Yann Hodique helped port this to Muse.
56 (require 'planner)
58 ;;; Code:
59 (defgroup planner-id nil
60 "Planner ID options."
61 :prefix "planner-id-"
62 :group 'planner)
64 (defcustom planner-id-add-task-id-flag t
65 "Non-nil means add task IDs to newly-created tasks."
66 :type 'boolean
67 :group 'planner-id)
69 (defcustom planner-id-tracking-file "~/.planner-id"
70 "File that stores an alist with the current planner ids."
71 :type 'file
72 :group 'planner-id)
74 (defcustom planner-id-update-automatically t
75 "Non-nil means update linked files automatically when file is saved."
76 :type 'boolean
77 :group 'planner-id)
79 (defface planner-id-face
80 '((((class color) (background light))
81 (:foreground "lightgray"))
82 (t (:foreground "darkgray")))
83 "Face for planner ID links."
84 :group 'planner-id)
86 (defvar planner-id-values nil
87 "Alist with (key nextvalue) pairs.")
89 (defvar planner-id-regexp "{{\\([^:\n]+\\):\\([0-9]+\\)}}"
90 "Regexp matching planner IDs.")
92 (defun planner-id-get-id-from-string (string &optional key)
93 "Return the planner ID in STRING as (identifier number).
94 If KEY is specified, match against that."
95 (when (string-match
96 (concat "{{\\("
97 (or key "[^:\n]+")
98 "\\):\\([0-9]+\\)}}") string)
99 (cons (planner-match-string-no-properties 1 string)
100 (planner-match-string-no-properties 2 string))))
102 (defun planner-id-get-current-id ()
103 "Return the planner ID on the current line as (identifier number)."
104 (planner-id-get-id-from-string
105 (buffer-substring (planner-line-beginning-position)
106 (planner-line-end-position))))
108 (defun planner-id-format-as-string (id)
109 "Return the planner ID as a string of the form {{identifier:number}}."
110 (concat "{{" (car id) ":" (cdr id) "}}"))
112 ;;;###autoload
113 (defun planner-id-find-task (task-info &optional point)
114 "Find task described by TASK-INFO. If POINT is non-nil, start from there.
115 If task is found, move point to line beginning and return non-nil.
116 If task is not found, leave point at POINT or the start of the buffer
117 and return nil."
118 (goto-char (or point (point-min)))
119 (let ((task-id
120 (cond
121 ;; Task ID
122 ((listp task-info)
123 (planner-id-get-id-from-string
124 (planner-task-description task-info)))
125 ;; Just the ID
126 ((numberp task-info)
127 (cons "Tasks" (number-to-string task-info)))
128 ;; ID as string
129 ((stringp task-info) (cons "Tasks" task-info))))
130 (found nil))
131 (when (re-search-forward
132 (concat planner-task-regexp ".*"
133 (regexp-quote
134 (if task-id
135 (planner-id-format-as-string task-id)
136 (planner-task-description task-info))))
137 nil t)
138 (goto-char (planner-line-beginning-position)))))
140 ;;; Redeclaration
141 ;;;###autoload
142 (defun planner-id-jump-to-linked-task (&optional info)
143 "Display the linked task page.
144 If INFO is specified, follow that task instead."
145 (interactive)
146 (let* ((task-info (or info (planner-current-task-info)))
147 (link (and task-info (planner-task-link task-info))))
148 (when (planner-local-page-p link)
149 (planner-find-file link)
150 (widen)
151 (planner-id-find-task task-info))))
153 (defun planner-id-save ()
154 "Save `planner-id-values' in `planner-id-tracking-file'."
155 (with-temp-file planner-id-tracking-file
156 (print planner-id-values (current-buffer))))
158 (defun planner-id-make-global-id (identifier)
159 "Return a globally unique ID as (IDENTIFIER number)."
160 (planner-id-load)
161 (let ((result
162 (cons
163 identifier
164 (number-to-string
165 (let ((elem (assoc identifier planner-id-values)))
166 (if elem
167 (setcdr elem (1+ (cdr elem)))
168 (add-to-list 'planner-id-values (cons identifier 0))
169 0))))))
170 (planner-id-save)
171 result))
173 (defun planner-id-load ()
174 "Read the data from `planner-id-tracking-file'."
175 (setq planner-id-values nil)
176 (with-temp-buffer
177 (condition-case nil
178 (progn
179 (insert-file-contents-literally planner-id-tracking-file)
180 (goto-char (point-min))
181 (setq planner-id-values (read (current-buffer))))
182 (error
183 (message "Could not read planner-id-values from %s. Setting it to nil."
184 planner-id-tracking-file)))))
186 ;;;###autoload
187 (defun planner-id-add-task-id-maybe ()
188 "Add task ID if `planner-id-add-task-id-flag' is non-nil."
189 (when planner-id-add-task-id-flag
190 (planner-id-add-task-id)))
192 (defun planner-id-add-task-id ()
193 "Add a task ID for the current task if it does not have one yet.
194 Update the linked task page, if any."
195 (interactive)
196 (save-window-excursion
197 (save-excursion
198 (let* ((task-info (planner-current-task-info)))
199 (unless (or (not task-info) (planner-id-get-current-id))
200 (planner-edit-task-description
201 (concat (planner-task-description task-info) " "
202 (planner-id-format-as-string
203 (planner-id-make-global-id "Tasks")))))))))
205 (defun planner-id-update-tasks-on-page (&optional force)
206 "Update all tasks on this page.
207 Completed or cancelled tasks are not updated. This can be added
208 to `write-file-functions' (CVS Emacs) or `write-file-hooks'.
209 If FORCE is non-nil, completed and cancelled tasks are also updated."
210 (interactive (list current-prefix-arg))
211 ;; Prevent planner-id updates from cascading
212 (let ((planner-id-update-automatically nil))
213 (with-planner-update-setup
214 (goto-char (point-min))
215 (while (re-search-forward
216 (concat
217 (if force
218 planner-task-regexp
219 planner-live-task-regexp)
220 ".*?{{Tasks:[0-9]+}}")
221 nil t)
222 (planner-update-task)
223 ;; Force the next line to be considered even if
224 ;; planner-multi-update-task kicked in.
225 (forward-line 1))))
226 nil)
228 (defun planner-id-remove-tasks-on-page ()
229 "Remove the task IDs from all tasks on this page.
230 This function does _not_ update tasks on linked pages."
231 (save-excursion
232 (goto-char (point-min))
233 (while (re-search-forward
234 (concat planner-task-regexp
235 "\\(.*?\\)\\(\\s-+{{Tasks:[0-9]+}}\\)") nil t)
236 (replace-match "" t t nil 1))))
238 (defun planner-id-add-task-id-to-all ()
239 "Add a task ID for all the tasks on the page.
240 Update the linked page, if any."
241 (interactive)
242 (save-excursion
243 (goto-char (point-min))
244 (while (re-search-forward planner-task-regexp nil t)
245 (planner-id-add-task-id))
246 (font-lock-fontify-buffer)))
248 (defun planner-id-at-point (&optional pos)
249 "Return non-nil if a URL or Wiki link name is at POS."
250 (if (or (null pos)
251 (and (char-after pos)
252 (not (eq (char-syntax (char-after pos)) ? ))))
253 (let ((case-fold-search nil)
254 (here (or pos (point))))
255 (save-excursion
256 (goto-char here)
257 (skip-chars-backward " \t\n")
258 (or (looking-at "{{Tasks:[^}\n]+}}")
259 (and (search-backward "{{" (planner-line-beginning-position) t)
260 (looking-at "{{Tasks:[^}\n]+}}"))
261 (<= here (match-end 0)))))))
263 (eval-and-compile
264 (require 'compile)
265 (unless (boundp 'grep-command)
266 ;; Emacs 21 CVS
267 (require 'grep)))
269 (defun planner-id-search-id (id)
270 "Search for all occurrences of ID."
271 (interactive "MID: ")
272 (grep (concat (or grep-command "grep") " "
273 (shell-quote-argument id) " "
274 (shell-quote-argument
275 (expand-file-name (planner-directory))) "/*")))
277 (defun planner-id-follow-id-at-point ()
278 "Display a list of all pages containing the ID at point."
279 (interactive current-prefix-arg)
280 (if (planner-id-at-point)
281 (planner-id-search-id (match-string 0))
282 (error "There is no valid link at point")))
284 ;; Very ugly compatibility hack.
285 (defmacro planner-follow-event (event)
286 (if (featurep 'xemacs)
287 `(progn
288 (set-buffer (window-buffer (event-window event)))
289 (and (event-point event) (goto-char (event-point event))))
290 `(progn
291 (set-buffer (window-buffer (posn-window (event-start event))))
292 (goto-char (posn-point (event-start event))))))
294 (defun planner-id-follow-id-at-mouse (event)
295 "Display a list of all pages containing the ID at mouse.
296 EVENT is the mouse event."
297 (interactive "eN")
298 (save-excursion
299 (planner-follow-event event))
300 (when (planner-id-at-point)
301 (planner-id-search-id (match-string 0))))
303 ;; (defvar planner-id-keymap
304 ;; (let ((map (make-sparse-keymap)))
305 ;; (define-key map [return] 'planner-id-follow-id-at-point)
306 ;; (define-key map [(control ?m)] 'planner-id-follow-id-at-point)
307 ;; (define-key map [(shift return)] 'planner-id-follow-id-at-point)
308 ;; (if (featurep 'xemacs)
309 ;; (progn
310 ;; (define-key map [(button2)] 'planner-id-follow-id-at-mouse)
311 ;; (define-key map [(shift button2)] 'planner-id-follow-id-at-mouse))
312 ;; (define-key map [(mouse-2)] 'planner-id-follow-id-at-mouse)
313 ;; (define-key map [(shift mouse-2)] 'planner-id-follow-id-at-mouse))
314 ;; (unless (eq emacs-major-version 21)
315 ;; (set-keymap-parent map planner-mode-map))
316 ;; map)
317 ;; "Local keymap used by planner when on an ID.")
319 ;;;###autoload
320 (defun planner-id-markup (beg end &optional verbose)
321 "Highlight IDs as unobtrusive, clickable text from BEG to END.
322 VERBOSE is ignored."
323 (goto-char beg)
324 (while (re-search-forward "{{[^}\n]+}}" end t)
325 (planner-highlight-region
326 (match-beginning 0)
327 (match-end 0)
328 'planner-id 60
329 (list
330 'face 'planner-id-face
331 'intangible nil
332 ;;'keymap planner-id-keymap
333 ))))
335 ;;;###autoload
336 (defun planner-id-update-tasks-maybe ()
337 "Update tasks depending on the value of `planner-id-update-automatically'."
338 (when planner-id-update-automatically
339 (planner-id-update-tasks-on-page)))
341 ;;;###autoload
342 (defun planner-id-setup ()
343 "Hook into `planner-mode'."
344 (add-hook 'muse-colors-buffer-hook
345 'planner-id-markup t t)
346 (add-hook
347 (if (and (boundp 'write-file-functions)
348 (not (featurep 'xemacs)))
349 'write-file-functions
350 'write-file-hooks)
351 'planner-id-update-tasks-maybe nil t))
353 (add-hook 'planner-mode-hook 'planner-id-setup)
354 (add-hook 'planner-create-task-hook 'planner-id-add-task-id-maybe)
355 (setq planner-jump-to-linked-task-function 'planner-id-jump-to-linked-task)
356 (setq planner-find-task-function 'planner-id-find-task)
358 (eval-after-load "planner-publish"
359 '(add-to-list 'planner-publish-markup-regexps
360 '(1270 planner-id-regexp 0 "")))
362 (provide 'planner-id)
364 ;;; planner-id.el ends here