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)
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.
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.
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.
59 (defgroup planner-id nil
64 (defcustom planner-id-add-task-id-flag t
65 "Non-nil means add task IDs to newly-created tasks."
69 (defcustom planner-id-tracking-file
"~/.planner-id"
70 "File that stores an alist with the current planner ids."
74 (defcustom planner-id-update-automatically t
75 "Non-nil means update linked files automatically when file is saved."
79 (defface planner-id-face
80 '((((class color
) (background light
))
81 (:foreground
"lightgray"))
82 (t (:foreground
"darkgray")))
83 "Face for planner ID links."
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."
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
) "}}"))
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
118 (goto-char (or point
(point-min)))
123 (planner-id-get-id-from-string
124 (planner-task-description task-info
)))
127 (cons "Tasks" (number-to-string task-info
)))
129 ((stringp task-info
) (cons "Tasks" task-info
))))
131 (when (re-search-forward
132 (concat planner-task-regexp
".*"
135 (planner-id-format-as-string task-id
)
136 (planner-task-description task-info
))))
138 (goto-char (planner-line-beginning-position)))))
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."
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
)
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)."
165 (let ((elem (assoc identifier planner-id-values
)))
167 (setcdr elem
(1+ (cdr elem
)))
168 (add-to-list 'planner-id-values
(cons identifier
0))
173 (defun planner-id-load ()
174 "Read the data from `planner-id-tracking-file'."
175 (setq planner-id-values nil
)
179 (insert-file-contents-literally planner-id-tracking-file
)
180 (goto-char (point-min))
181 (setq planner-id-values
(read (current-buffer))))
183 (message "Could not read planner-id-values from %s. Setting it to nil."
184 planner-id-tracking-file
)))))
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."
196 (save-window-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
219 planner-live-task-regexp
)
220 ".*?{{Tasks:[0-9]+}}")
222 (planner-update-task)
223 ;; Force the next line to be considered even if
224 ;; planner-multi-update-task kicked in.
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."
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."
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."
251 (and (char-after pos
)
252 (not (eq (char-syntax (char-after pos
)) ?
))))
253 (let ((case-fold-search nil
)
254 (here (or pos
(point))))
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)))))))
265 (unless (boundp 'grep-command
)
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
)
288 (set-buffer (window-buffer (event-window event
)))
289 (and (event-point event
) (goto-char (event-point event
))))
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."
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)
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))
317 ;; "Local keymap used by planner when on an ID.")
320 (defun planner-id-markup (beg end
&optional verbose
)
321 "Highlight IDs as unobtrusive, clickable text from BEG to END.
324 (while (re-search-forward "{{[^}\n]+}}" end t
)
325 (planner-highlight-region
330 'face
'planner-id-face
332 ;;'keymap planner-id-keymap
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)))
342 (defun planner-id-setup ()
343 "Hook into `planner-mode'."
344 (add-hook 'muse-colors-buffer-hook
345 'planner-id-markup t t
)
347 (if (and (boundp 'write-file-functions
)
348 (not (featurep 'xemacs
)))
349 'write-file-functions
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