2008-12-07 Carsten Dominik <carsten.dominik@gmail.com>
[bpt/emacs.git] / lisp / org / org-id.el
CommitLineData
ff4be292 1;;; org-id.el --- Global identifiers for Org-mode entries
d4d1a4ac
CD
2;; Copyright (C) 2008 Free Software Foundation, Inc.
3;;
4;; Author: Carsten Dominik <carsten at orgmode dot org>
5;; Keywords: outlines, hypermedia, calendar, wp
6;; Homepage: http://orgmode.org
ff4be292 7;; Version: 6.14
d4d1a4ac
CD
8;;
9;; This file is part of GNU Emacs.
10;;
11;; GNU Emacs is free software: you can redistribute it and/or modify
12;; it under the terms of the GNU General Public License as published by
13;; the Free Software Foundation, either version 3 of the License, or
14;; (at your option) any later version.
15
16;; GNU Emacs is distributed in the hope that it will be useful,
17;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19;; GNU General Public License for more details.
20
21;; You should have received a copy of the GNU General Public License
22;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
23;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
24;;
25;;; Commentary:
26
27;; This file implements globally unique identifiers for Org-mode entries.
28;; Identifiers are stored in the entry as an :ID: property. Functions
29;; are provided that create and retrieve such identifiers, and that find
30;; entries based on the identifier.
31
621f83e4
CD
32;; Identifiers consist of a prefix (default "Org" given by the variable
33;; `org-id-prefix') and a unique part that can be created by a number
34;; of different methods, see the variable `org-id-method'.
35;; Org has a builtin method that uses a compact encoding of the creation
36;; time of the ID, with microsecond accuracy. This virtually
d4d1a4ac
CD
37;; guarantees globally unique identifiers, even if several people are
38;; creating ID's at the same time in files that will eventually be used
621f83e4
CD
39;; together. As an exernal method `uuidgen' is supported, if installed
40;; on the system.
d4d1a4ac
CD
41;;
42;; This file defines the following API:
43;;
44;; org-id-get-create
45;; Create an ID for the entry at point if it does not yet have one.
46;; Returns the ID (old or new). This function can be used
47;; interactively, with prefix argument the creation of a new ID is
48;; forced, even if there was an old one.
49;;
50;; org-id-get
51;; Get the ID property of an entry. Using appropriate arguments
52;; to the function, it can also create the ID for this entry.
53;;
54;; org-id-goto
55;; Command to go to a specific ID, this command can be used
56;; interactively.
57;;
58;; org-id-get-with-outline-path-completion
59;; Retrieve the ID of an entry, using outline path completion.
60;; This function can work for multiple files.
61;;
62;; org-id-get-with-outline-drilling
63;; Retrieve the ID of an entry, using outline path completion.
64;; This function only works for the current file.
65;;
66;; org-id-find
67;; Find the location of an entry with specific id.
68;;
69
70(require 'org)
71
72(declare-function message-make-fqdn "message" ())
73
74;;; Customization
75
76(defgroup org-id nil
77 "Options concerning global entry identifiers in Org-mode."
78 :tag "Org ID"
79 :group 'org)
80
621f83e4
CD
81(defcustom org-id-method 'org
82 "The method that should be used to create new ID's.
83
84An ID will consist of the prefix specified in `org-id-prefix', and a unique
85part created by the method this variable specifies.
86
87Allowed values are:
88
89org Org's own internal method, using an encoding of the current time,
90 and the current domain of the computer. This method will
91 honor the variable `org-id-include-domain'.
92
93uuidgen Call the external command uuidgen."
94 :group 'org-id
95 :type '(choice
96 (const :tag "Org's internal method" org)
97 (const :tag "external: uuidgen" uuidgen)))
98
99(defcustom org-id-prefix nil
d4d1a4ac
CD
100 "The prefix for IDs.
101
102This may be a string, or it can be nil to indicate that no prefix is required.
103When a string, the string should have no space characters as IDs are expected
104to have no space characters in them."
105 :group 'org-id
106 :type '(choice
107 (const :tag "No prefix")
108 (string :tag "Prefix")))
109
110(defcustom org-id-include-domain t
111 "Non-nil means, add the domain name to new IDs.
112This ensures global uniqueness of ID's, and is also suggested by
621f83e4
CD
113RFC 2445 in combination with RFC 822. This is only relevant if
114`org-id-method' is `org'. When uuidgen is used, the domain will never
115be added."
d4d1a4ac
CD
116 :group 'org-id
117 :type 'boolean)
118
ff4be292
CD
119(defcustom org-id-track-globally t
120 "Non-nil means, track ID's trhough files, so that links work globally.
121This work by maintaining a hash table for ID's and writing this table
122to disk when exiting Emacs. Because of this, it works best if you use
123a single Emacs process, not many.
124
125When nil, ID's are not tracked. Links to ID's will still work within
126a buffer, but not if the entry is located in another file.
127ID's can still be used if the entry with the id is in the same file as
128the link."
129 :group 'org-id
130 :type 'boolean)
131
71d35b24 132(defcustom org-id-locations-file (convert-standard-filename
ff4be292
CD
133 "~/.emacs.d/.org-id-locations")
134 "The file for remembering in which file an ID was defined.
135This variable is only relevant when `org-id-track-globally' is set."
d4d1a4ac
CD
136 :group 'org-id
137 :type 'file)
138
139(defvar org-id-locations nil
ff4be292
CD
140 "List of files with ID's in those files.
141Depending on `org-id-use-hash' this can also be a hash table mapping ID's
142to files.")
143
144(defvar org-id-files nil
145 "List of files that contain ID's.")
d4d1a4ac
CD
146
147(defcustom org-id-extra-files 'org-agenda-text-search-extra-files
ff4be292
CD
148 "Files to be searched for ID's, besides the agenda files.
149When Org reparses files to remake the list of files and ID's it is tracking,
150it will normally scan the agenda files, the archives related to agenda files,
151any files that are listed as ID containing in the current register, and
152any Org-mode files currently visited by Emacs.
153You can list additional files here.
154This variable is only relevant when `org-id-track-globally' is set."
d4d1a4ac
CD
155 :group 'org-id
156 :type
157 '(choice
158 (symbol :tag "Variable")
159 (repeat :tag "List of files"
160 (file))))
161
ff4be292
CD
162(defcustom org-id-search-archives t
163 "Non-nil means, search also the archive files of agenda files for entries.
164This is a possibility to reduce overhead, but it measn that entries moved
165to the archives can no longer be found by ID.
166This variable is only relevant when `org-id-track-globally' is set."
167 :group 'org-id
168 :type 'boolean)
169
d4d1a4ac
CD
170;;; The API functions
171
172;;;###autoload
173(defun org-id-get-create (&optional force)
174 "Create an ID for the current entry and return it.
175If the entry already has an ID, just return it.
176With optional argument FORCE, force the creation of a new ID."
177 (interactive "P")
178 (when force
179 (org-entry-put (point) "ID" nil))
180 (org-id-get (point) 'create))
ff4be292 181
d4d1a4ac
CD
182;;;###autoload
183(defun org-id-copy ()
184 "Copy the ID of the entry at point to the kill ring.
185Create an ID if necessary."
186 (interactive)
ff4be292 187 (kill-new (org-id-get nil 'create)))
d4d1a4ac
CD
188
189;;;###autoload
190(defun org-id-get (&optional pom create prefix)
191 "Get the ID property of the entry at point-or-marker POM.
192If POM is nil, refer to the entry at point.
193If the entry does not have an ID, the function returns nil.
194However, when CREATE is non nil, create an ID if none is present already.
195PREFIX will be passed through to `org-id-new'.
196In any case, the ID of the entry is returned."
197 (let ((id (org-entry-get pom "ID")))
198 (cond
199 ((and id (stringp id) (string-match "\\S-" id))
200 id)
201 (create
202 (setq id (org-id-new prefix))
203 (org-entry-put pom "ID" id)
204 (org-id-add-location id (buffer-file-name (buffer-base-buffer)))
205 id)
206 (t nil))))
207
208;;;###autoload
209(defun org-id-get-with-outline-path-completion (&optional targets)
210 "Use outline-path-completion to retrieve the ID of an entry.
211TARGETS may be a setting for `org-refile-targets' to define the eligible
212headlines. When omitted, all headlines in all agenda files are
213eligible.
214It returns the ID of the entry. If necessary, the ID is created."
215 (let* ((org-refile-targets (or targets '((nil . (:maxlevel . 10)))))
ff4be292 216 (org-refile-use-outline-path
d4d1a4ac
CD
217 (if (caar org-refile-targets) 'file t))
218 (spos (org-refile-get-location "Entry: "))
ff4be292 219 (pom (and spos (move-marker (make-marker) (nth 3 spos)
d4d1a4ac
CD
220 (get-file-buffer (nth 1 spos))))))
221 (prog1 (org-id-get pom 'create)
222 (move-marker pom nil))))
223
224;;;###autoload
225(defun org-id-get-with-outline-drilling (&optional targets)
226 "Use an outline-cycling interface to retrieve the ID of an entry.
227This only finds entries in the current buffer, using `org-get-location'.
228It returns the ID of the entry. If necessary, the ID is created."
229 (let* ((spos (org-get-location (current-buffer) org-goto-help))
230 (pom (and spos (move-marker (make-marker) (car spos)))))
231 (prog1 (org-id-get pom 'create)
232 (move-marker pom nil))))
233
234;;;###autoload
235(defun org-id-goto (id)
236 "Switch to the buffer containing the entry with id ID.
237Move the cursor to that entry in that buffer."
ff4be292 238 (interactive "sID: ")
d4d1a4ac
CD
239 (let ((m (org-id-find id 'marker)))
240 (unless m
241 (error "Cannot find entry with ID \"%s\"" id))
242 (switch-to-buffer (marker-buffer m))
243 (goto-char m)
244 (move-marker m nil)
ff4be292 245 (org-show-context)))
d4d1a4ac
CD
246
247;;;###autoload
248(defun org-id-find (id &optional markerp)
249 "Return the location of the entry with the id ID.
250The return value is a cons cell (file-name . position), or nil
251if there is no entry with that ID.
252With optional argument MARKERP, return the position as a new marker."
253 (let ((file (org-id-find-id-file id))
254 org-agenda-new-buffers where)
255 (when file
256 (setq where (org-id-find-id-in-file id file markerp)))
257 (unless where
258 (org-id-update-id-locations)
259 (setq file (org-id-find-id-file id))
260 (when file
261 (setq where (org-id-find-id-in-file id file markerp))))
262 where))
263
264;;; Internal functions
265
266;; Creating new IDs
267
268(defun org-id-new (&optional prefix)
269 "Create a new globally unique ID.
270
271An ID consists of two parts separated by a colon:
272- a prefix
621f83e4 273- a unique part that will be created according to `org-id-method'.
d4d1a4ac
CD
274
275PREFIX can specify the prefix, the default is given by the variable
276`org-id-prefix'. However, if PREFIX is the symbol `none', don't use any
277prefix even if `org-id-prefix' specifies one.
278
279So a typical ID could look like \"Org:4nd91V40HI\"."
280 (let* ((prefix (if (eq prefix 'none)
621f83e4
CD
281 ""
282 (concat (or prefix org-id-prefix) ":")))
283 unique)
284 (if (equal prefix ":") (setq prefix ""))
285 (cond
286 ((eq org-id-method 'uuidgen)
93b62de8 287 (setq unique (org-trim (shell-command-to-string "uuidgen"))))
621f83e4
CD
288 ((eq org-id-method 'org)
289 (let* ((etime (org-id-reverse-string (org-id-time-to-b36)))
290 (postfix (if org-id-include-domain
291 (progn
292 (require 'message)
293 (concat "@" (message-make-fqdn))))))
294 (setq unique (concat etime postfix))))
295 (t (error "Invalid `org-id-method'")))
296 (concat prefix unique)))
297
298(defun org-id-reverse-string (s)
299 (mapconcat 'char-to-string (nreverse (string-to-list s)) ""))
300
301(defun org-id-int-to-b36-one-digit (i)
d4d1a4ac
CD
302 "Turn an integer between 0 and 61 into a single character 0..9, A..Z, a..z."
303 (cond
304 ((< i 10) (+ ?0 i))
621f83e4
CD
305 ((< i 36) (+ ?a i -10))
306 (t (error "Larger that 35"))))
d4d1a4ac 307
621f83e4 308(defun org-id-b36-to-int-one-digit (i)
d4d1a4ac
CD
309 "Turn a character 0..9, A..Z, a..z into a number 0..61.
310The input I may be a character, or a single-letter string."
311 (and (stringp i) (setq i (string-to-char i)))
312 (cond
313 ((and (>= i ?0) (<= i ?9)) (- i ?0))
621f83e4
CD
314 ((and (>= i ?a) (<= i ?z)) (+ (- i ?a) 10))
315 (t (error "Invalid b36 letter"))))
d4d1a4ac 316
621f83e4
CD
317(defun org-id-int-to-b36 (i &optional length)
318 "Convert an integer to a base-36 number represented as a string."
d4d1a4ac
CD
319 (let ((s ""))
320 (while (> i 0)
321 (setq s (concat (char-to-string
621f83e4
CD
322 (org-id-int-to-b36-one-digit (mod i 36))) s)
323 i (/ i 36)))
d4d1a4ac
CD
324 (setq length (max 1 (or length 1)))
325 (if (< (length s) length)
326 (setq s (concat (make-string (- length (length s)) ?0) s)))
327 s))
328
621f83e4
CD
329(defun org-id-b36-to-int (s)
330 "Convert a base-36 string into the corresponding integer."
d4d1a4ac 331 (let ((r 0))
621f83e4 332 (mapc (lambda (i) (setq r (+ (* r 36) (org-id-b36-to-int-one-digit i))))
d4d1a4ac
CD
333 s)
334 r))
335
621f83e4 336(defun org-id-time-to-b36 (&optional time)
d4d1a4ac
CD
337 "Encode TIME as a 10-digit string.
338This string holds the time to micro-second accuracy, and can be decoded
339using `org-id-decode'."
340 (setq time (or time (current-time)))
621f83e4
CD
341 (concat (org-id-int-to-b36 (nth 0 time) 4)
342 (org-id-int-to-b36 (nth 1 time) 4)
343 (org-id-int-to-b36 (or (nth 2 time) 0) 4)))
d4d1a4ac
CD
344
345(defun org-id-decode (id)
346 "Split ID into the prefix and the time value that was used to create it.
347The return value is (prefix . time) where PREFIX is nil or a string,
348and time is the usual three-integer representation of time."
349 (let (prefix time parts)
350 (setq parts (org-split-string id ":"))
351 (if (= 2 (length parts))
352 (setq prefix (car parts) time (nth 1 parts))
353 (setq prefix nil time (nth 0 parts)))
621f83e4
CD
354 (setq time (org-id-reverse-string time))
355 (setq time (list (org-id-b36-to-int (substring time 0 4))
356 (org-id-b36-to-int (substring time 4 8))
357 (org-id-b36-to-int (substring time 8 12))))
d4d1a4ac
CD
358 (cons prefix time)))
359
360;; Storing ID locations (files)
361
ff4be292 362(defun org-id-update-id-locations (&optional files check)
d4d1a4ac 363 "Scan relevant files for ID's.
ff4be292
CD
364Store the relation between files and corresponding ID's.
365This will scan all agenda files, all associated archives, and all
366files currently mentioned in `org-id-locations'.
367When FILES is given, scan these files instead."
d4d1a4ac 368 (interactive)
ff4be292
CD
369 (if (not org-id-track-globally)
370 (error "Please turn on `org-id-track-globally' if you want to track id's.")
371 (let ((files
372 (or files
373 (append
374 ;; Agenda files and all associated archives
375 (org-agenda-files t org-id-search-archives)
376 ;; Explicit extra files
377 (if (symbolp org-id-extra-files)
378 (symbol-value org-id-extra-files)
379 org-id-extra-files)
380 ;; Files associated with live org-mode buffers
381 (delq nil
382 (mapcar (lambda (b)
383 (with-current-buffer b
384 (and (org-mode-p) (buffer-file-name))))
385 (buffer-list)))
386 ;; All files known to have id's
387 org-id-files)))
388 org-agenda-new-buffers
389 file nfiles tfile ids reg found id seen (ndup 0))
390 (setq nfiles (length files))
391 (while (setq file (pop files))
392 (message "Finding ID locations (%d/%d files): %s"
393 (- nfiles (length files)) nfiles file)
394 (setq tfile (file-truename file))
395 (when (and (file-exists-p file) (not (member tfile seen)))
396 (push tfile seen)
397 (setq ids nil)
398 (with-current-buffer (org-get-agenda-file-buffer file)
399 (save-excursion
400 (save-restriction
401 (widen)
402 (goto-char (point-min))
403 (while (re-search-forward "^[ \t]*:ID:[ \t]+\\(\\S-+\\)[ \t]*$"
404 nil t)
405 (setq id (org-match-string-no-properties 1))
406 (if (member id found)
407 (progn
408 (message "Duplicate ID \"%s\"" id)
409 (setq ndup (1+ ndup)))
410 (push id found)
411 (push id ids)))
412 (push (cons (abbreviate-file-name file) ids) reg))))))
413 (org-release-buffers org-agenda-new-buffers)
414 (setq org-agenda-new-buffers nil)
415 (setq org-id-locations reg)
416 (setq org-id-files (mapcar 'car org-id-locations))
417 (org-id-locations-save) ;; this function can also handle the alist form
418 ;; now convert to a hash
419 (setq org-id-locations (org-id-alist-to-hash org-id-locations))
420 (if (> ndup 0)
421 (message "WARNING: %d duplicate ID's found, check *Messages* buffer" ndup)
422 (message "%d unique files scanned for ID's" (length org-id-files)))
423 org-id-locations)))
d4d1a4ac
CD
424
425(defun org-id-locations-save ()
426 "Save `org-id-locations' in `org-id-locations-file'."
ff4be292
CD
427 (when org-id-track-globally
428 (let ((out (if (hash-table-p org-id-locations)
429 (org-id-hash-to-alist org-id-locations)
430 org-id-locations)))
431 (with-temp-file org-id-locations-file
432 (print out (current-buffer))))))
d4d1a4ac
CD
433
434(defun org-id-locations-load ()
435 "Read the data from `org-id-locations-file'."
436 (setq org-id-locations nil)
ff4be292
CD
437 (when org-id-track-globally
438 (with-temp-buffer
439 (condition-case nil
440 (progn
441 (insert-file-contents-literally org-id-locations-file)
442 (goto-char (point-min))
443 (setq org-id-locations (read (current-buffer))))
444 (error
445 (message "Could not read org-id-values from %s. Setting it to nil."
446 org-id-locations-file))))
447 (setq org-id-files (mapcar 'car org-id-locations))
448 (setq org-id-locations (org-id-alist-to-hash org-id-locations))))
d4d1a4ac
CD
449
450(defun org-id-add-location (id file)
451 "Add the ID with location FILE to the database of ID loations."
ff4be292
CD
452 ;; Only if global tracking is on, and when the buffer has a file
453 (when (and org-id-track-globally id file)
ce4fdcb9 454 (unless org-id-locations (org-id-locations-load))
ff4be292
CD
455 (puthash id (abbreviate-file-name file) org-id-locations)
456 (add-to-list 'org-id-files (abbreviate-file-name file))))
457
458(add-hook 'kill-emacs-hook 'org-id-locations-save)
459
460(defun org-id-hash-to-alist (hash)
461 "Turn an org-id hash into an alist, so that it can be written to a file."
462 (let (res x)
463 (maphash
464 (lambda (k v)
465 (if (setq x (member v res))
466 (push k (cdr x))
467 (push (list v k) res)))
468 hash)
469 res))
470
471(defun org-id-alist-to-hash (list)
472 "Turn an org-id location list into a hash table."
473 (let ((res (make-hash-table
474 :test 'equal
475 :size (apply '+ (mapcar 'length list))))
476 f i)
477 (mapc
478 (lambda (x)
479 (setq f (car x))
480 (mapc (lambda (i) (puthash i f res)) (cdr x)))
481 list)
482 res))
483
484(defun org-id-paste-tracker (txt &optional buffer-or-file)
485 "Update any ID's in TXT and assign BUFFER-OR-FILE to them."
486 (when org-id-track-globally
487 (save-match-data
488 (setq buffer-or-file (or buffer-or-file (current-buffer)))
489 (when (bufferp buffer-or-file)
490 (setq buffer-or-file (or (buffer-base-buffer buffer-or-file)
491 buffer-or-file))
492 (setq buffer-or-file (buffer-file-name buffer-or-file)))
493 (when buffer-or-file
494 (let ((fname (abbreviate-file-name buffer-or-file))
495 (s 0))
496 (while (string-match "^[ \t]*:ID:[ \t]+\\([^ \t\n\r]+\\)" txt s)
497 (setq s (match-end 0))
498 (org-id-add-location (match-string 1 txt) fname)))))))
d4d1a4ac
CD
499
500;; Finding entries with specified id
501
502(defun org-id-find-id-file (id)
503 "Query the id database for the file in which this ID is located."
504 (unless org-id-locations (org-id-locations-load))
ff4be292
CD
505 (or (gethash id org-id-locations)
506 ;; ball back on current buffer
507 (buffer-file-name (or (buffer-base-buffer (current-buffer))
508 (current-buffer)))))
d4d1a4ac
CD
509
510(defun org-id-find-id-in-file (id file &optional markerp)
511 "Return the position of the entry ID in FILE.
512If that files does not exist, or if it does not contain this ID,
513return nil.
514The position is returned as a cons cell (file-name . position). With
515optional argument MARKERP, return the position as a new marker."
516 (let (org-agenda-new-buffers m buf pos)
517 (cond
518 ((not file) nil)
519 ((not (file-exists-p file)) nil)
520 (t (with-current-buffer (setq buf (org-get-agenda-file-buffer file))
521 (setq pos (org-find-entry-with-id id))
522 (when pos
523 (if markerp
524 (move-marker (make-marker) pos buf)
525 (cons file pos))))))))
526
ff4be292
CD
527;; id link type
528
529;; Calling the following function is hard-coded into `org-store-link',
530;; so we do have to add it to `org-store-link-functions'.
531
532(defun org-id-store-link ()
533 "Store a link to the current entry, using it's ID."
534 (interactive)
535 (let* ((link (org-make-link "id:" (org-id-get-create)))
536 (desc (save-excursion
537 (org-back-to-heading t)
538 (or (and (looking-at org-complex-heading-regexp)
539 (if (match-end 4) (match-string 4) (match-string 0)))
540 link))))
541 (org-store-link-props :link link :description desc :type "id")
542 link))
543
544(defun org-id-open (id)
545 "Go to the entry with id ID."
546 (org-mark-ring-push)
547 (switch-to-buffer-other-window (current-buffer))
548 (org-id-goto id))
549
550(org-add-link-type "id" 'org-id-open)
551
d4d1a4ac
CD
552(provide 'org-id)
553
554;;; org-id.el ends here
555
a5ec381f 556;; arch-tag: e5abaca4-e16f-4b25-832a-540cfb63a712
ff4be292
CD
557
558