4 ;;; $Id: notes-mode.el,v 1.71 2009/08/20 20:00:02 johnh Exp $
6 ;;; Copyright (C) 1994-2007 by John Heidemann
7 ;;; Comments to <johnh@isi.edu>.
9 ;;; This file is under the Gnu Public License.
12 (require 'notes-variables)
15 (defvar notes-mode-hooks nil
16 "Hooks to run when entering notes-mode.")
17 (defvar notes-load-mode-hooks nil
18 "Hooks to run when entering notes-mode is loaded.")
20 (defconst notes-beginning-of-defun-regexp "^\\* .*\n\\-"
21 "Regexp matching the beginning of notes section.")
23 (defvar notes-default-tab-binding nil
24 "Saved tab binding for notes-complete-subject.")
25 (defvar notes-default-return-binding nil
26 "Saved return binding for notes-electric-return.")
29 (defun notes-beginning-of-defun ()
30 "Go to the beginning of a notes ``section''."
35 ;; handle starting on a title
36 (if (and (looking-at notes-beginning-of-defun-regexp)
37 (/= (point) old-point))
40 (if (looking-at "^-") ;; handle starting on the underline under a title
42 (generic-beginning-of-defun notes-beginning-of-defun-regexp))))
44 (defun notes-end-of-defun ()
45 "Go to the end of a notes ``section''."
47 (generic-end-of-defun notes-beginning-of-defun-regexp))
49 (defun notes-follow-link (which)
50 "Go to the WHICH link for this topic.
51 WHICH is either \"next\" or \"prev\".
52 If there are no links for the current note,
53 we go to the last note based upon the index file."
57 (start-buffer (current-buffer))
58 ;; We have to handle links in the same buffer,
59 ;; so the following code figure out where we go
60 ;; and returns it out of the save-excursion.
61 ;; If we end up in another buffer, we let the save-excursion
62 ;; leave the original buffer unchanged. If we end up in
63 ;; the same buffer, we need to go wherever we end up.
64 ;; Can anyone suggest a better way?
68 (setq end-of-note (point))
69 (notes-beginning-of-defun)
70 (setq beginning-of-note (point))
71 (if (and (= beginning-of-note 1) (not (looking-at notes-beginning-of-defun-regexp)))
73 ;; When "above" the first note, search to end of first
74 ;; real note (otherwise end-of-note is just the start
75 ;; of the first real note and there are no links).
78 (setq end-of-note (point))
79 (goto-char beginning-of-note)))
80 (if (re-search-forward (concat "^"
81 (if (eq which 'next) "next" "prev")
82 ":[ ]+<") end-of-note t)
83 (progn ; link exists, just take it
85 (notes-w3-follow-link (point))
86 (cons (current-buffer) (point)))
87 ;; No link; go through the index file.
88 (if (notes-goto-index-entry which)
89 (let ((index-buffer (current-buffer)))
90 (notes-index-follow-link (point))
91 (bury-buffer index-buffer))
92 (error "No known notes in that direction.")
93 (bury-buffer (current-buffer)))
94 (cons (current-buffer) (point))))))
95 ;; Check for going to the same buffer (and the save-excursion
97 (if (eq start-buffer (car end-buffer-and-point))
98 (goto-char (cdr end-buffer-and-point)))))
101 (defun notes-follow-next-link ()
102 "Go to the next link for this topic."
104 (notes-follow-link 'next))
106 (defun notes-follow-prev-link ()
107 "Go to the previous link for this topic."
109 (notes-follow-link 'prev))
111 (defvar notes-complete-subject-abbrevs-alist
112 '(("SP2010" "USC/Classes/CS551/SP2010")
113 ("FA2011" "USC/Classes/CS551/FA2011"))
114 "notes-complete-subject-abbrevs-alist provides simple substitution of subjects.
115 If subject completion is requested, then subject that matches
116 the left-side of an alist value is replaced by the right-side value.")
118 (defun notes-complete-subject-abbrevs (key)
119 "Handle abbreviations on notes SUBJECTS.
120 Currently this is just a hack."
121 (let ((value (assoc key notes-complete-subject-abbrevs-alist)))
127 (defun notes-complete-subject ()
128 "Complete the notes subject under point."
131 ((subject (save-excursion
133 (notes-extract-subject t)))
134 old-completion-ignore-case
136 (if (not (and notes-mode-complete-subjects subject))
137 (call-interactively notes-default-tab-binding)
138 ;; Complete the title.
139 (if (null notes-subject-table)
141 (find-file-noselect (concat notes-dir "/index"))))
143 ;; Run completer if it's loaded,
144 ;; otherwise do our own thing.
145 (setq completion-ignore-case t)
147 ((fboundp 'completer-complete-goto)
148 (completer-complete-goto "^ \t\n\"" " " notes-subject-table nil))
149 ;; NEEDSWORK: should try other completers, too.
150 (t ;; Do our own completion.
151 (setq full-subject (try-completion subject notes-subject-table)
152 subject (completing-read "Subject: "
153 notes-subject-table nil nil
154 (if (stringp full-subject)
157 (delete-region (get-beginning-of-line) (get-end-of-line))
158 (insert "* " (notes-complete-subject-abbrevs subject))))
159 (setq completion-ignore-case old-completion-ignore-case))))
161 (defun notes-fix-prevnext-this-entry ()
162 "* Fix up the prev link for the current entry,
163 if necessary. Currently this code only handles brand new entries."
164 ;; Contributed from Takashi Nishimoto <g96p0935@mse.waseda.ac.jp>.
167 (let ((subject (notes-extract-subject nil t))
168 (this-url (notes-current-url))
171 (set-buffer (find-file-noselect (concat notes-dir "/index")))
172 (goto-char (point-min))
173 (if (re-search-forward
174 (concat "^" (regexp-quote subject) ":.* \\([0-9]+\\)$")
176 (save-window-excursion
177 (cond ((and (notes-w3-url
178 (notes-file-to-url (match-string 1) subject))
179 (re-search-forward "^next: " nil t)
180 (looking-at "<none>"))
182 (pre-modified (buffer-modified-p))
185 (setq last-url (notes-current-url))
186 (if (and (null pre-modified)
187 (>= notes-electric-prevnext 2))
191 (notes-beginning-of-defun)
193 (if (not (looking-at "prev: "))
194 (insert "prev: " last-url "\n" "next: <none>\n\n")
195 (forward-line 3))))))
197 (defun notes-electric-return (arg)
198 "* Return, underlining if we're on a subject."
200 (if (let ((cur-point (point)))
203 (and (not (eq cur-point (point))) ;; normal return if at b-o-ln
204 (notes-extract-subject t))))
205 (progn (notes-underline-line)
206 (if notes-electric-prevnext
207 (notes-fix-prevnext-this-entry)))
208 (call-interactively notes-default-return-binding)))
210 (defun notes-current-url ()
211 "* Returns the notes-URL of the current entry around the current point."
212 (let ((subject (notes-extract-subject nil t))
213 (date (file-name-nondirectory buffer-file-name)))
215 (abbreviate-file-name buffer-file-name)
216 (if subject (concat "#* " subject) "")
219 (defun notes-current-url-as-kill ()
220 "* Put the notes-URL of the current entry into the kill ring."
222 (kill-new (notes-current-url)))
224 (defun notes-goto-index-entry (&optional direction)
225 "* Jump to the index entry corresponding to our current note entry.
226 If we're not in an entry, we leave you in the index file.
227 If the current date doesn't exist, error in DIRECTION.
228 Returns nil if on errors (no index; no date in DIRECTION),
229 otherwise the point of the hit."
231 (let ((start-buffer (current-buffer))
232 (subject (notes-extract-subject)) ; get subject if on it
233 (date (if (null (buffer-file-name)) nil
234 (file-name-nondirectory (buffer-file-name)))))
235 ;; Try and get the subject, either forward...
238 (notes-beginning-of-defun)
239 (setq subject (notes-extract-subject))))
244 (setq subject (notes-extract-subject))))
245 ;; Form and jump to the url for the index-entry.
246 (if (and (notes-w3-url (concat notes-url-prefix
248 (if subject (concat "#" subject) ""))
250 ;; Go to the current date, if any.
251 (notes-index-goto-date date direction))
255 (defun notes-extract-subject (&optional relaxed search)
256 "Extract the subject under the point in the current buffer.
257 If RELAXED, then accept non-underlined subjects.
258 If SEARCH we'll search back in the buffer for the nearest
261 Returns nil if we're not on as subject."
264 ;; directly on a note
265 ((or (looking-at notes-beginning-of-defun-regexp)
267 (looking-at "^\\* ")))
270 ((start (+ (point) 2))
271 (end (progn (end-of-line) (point))))
272 (buffer-substring start end))))
275 (notes-beginning-of-defun)
276 (notes-extract-subject relaxed nil)))
282 (defun notes-underline-line ()
283 "* Create a row of dashes as long as this line, or adjust the current underline."
285 ;; check to see if it's already underlined
288 (looking-at "^[ \t]*--*$"))
289 (notes-old-underline-line)
291 (notes-new-underline-line)
294 (defun notes-new-underline-line ()
295 "Underline a line with a row of dashes. Moves the point after the dashes.
296 \\[notes-new-underline-line] reproduces leading spaces."
299 ((bol (progn (beginning-of-line)
301 (bospaces (progn (skip-chars-forward " \t")
303 (nospaces (- bospaces bol))
304 (eol (progn (end-of-line)
305 (untabify bol (point))
307 (insert "\n" (buffer-substring bol bospaces))
308 (insert-char ?- (- eol bospaces))))
310 (defun notes-old-underline-line ()
311 "Replace the following line with a row of dashes. Leave the point unchanged."
315 (delete-region (get-beginning-of-line) (1+ (get-end-of-line))))
316 (notes-new-underline-line)))
318 (defun notes-mode-initialize-note-from-cache ()
319 "Build a new note from the cache. Returns valid cache contents or nil."
322 ((new-buffer (current-buffer))
323 (cache-file (concat notes-dir "/mknew.cache"))
324 (buf (find-file cache-file))
332 (>= (count-lines (point-min) (point-max)) 3))
334 ;; If you know a more elegant way to extact the first
335 ;; three lines of a file, please let me know.
336 (goto-char (point-min))
339 (setq magic-line (buffer-substring m (- (point) 1)))
342 (setq prev-file (buffer-substring m (- (point) 1)))
345 (setq this-file (buffer-substring m (- (point) 1)))
346 (setq cache-contents (buffer-substring (point) (point-max)))
351 (string-equal magic-line "mknew.cache 830494922")
352 (file-newer-than-file-p cache-file prev-file)
353 (string-equal (file-name-nondirectory this-file)
354 (file-name-nondirectory (buffer-file-name
359 ;; Kill the buffer to avoid "buf changed, reload?" warnings.
364 (defun notes-mode-initialize-note ()
365 "Fill in an empty new note.
366 Create any directories as necessary.
367 Use the mknew cache if possible."
370 ((dir (directory-file-name (file-name-directory (buffer-file-name)))))
371 (if (file-exists-p dir)
373 (make-directory dir t)
374 (message "New intermediate directory created.")))
375 (if notes-mode-initialization-program
377 ((cache-contents (notes-mode-initialize-note-from-cache)))
379 (insert cache-contents)
380 (shell-command-on-region
383 (concat notes-bin-dir "/" notes-mode-initialization-program " '"
384 (buffer-file-name) "'") 't)))))
389 ;;; requires "PEM - PGP Enhanced Messaging for GNU Emacs"
390 ;;; from Roy Frederick Busdiecker, III (Rick)
391 ;;; or mailcrypt 3.4.x or >=3.5.x
394 (defvar notes-encryption-library
397 ; ((fboundp 'mc-encrypt-region) 'mailcrypt)
398 ; ((fboundp 'npgp:encrypt-region) 'npgp)
400 "what pgp library to use")
402 (defvar notes-encryption-sub-library
404 "what variant of mailcrypt to use ('pgp 'pgp50 'gpg).")
406 (defvar notes-encryption-npgp-userid nil
407 "PGP key for the current user.")
409 (defvar notes-encryption-npgp-key-id nil
410 "Keyid of PGP key for the current user.
411 Useful if your \\[user-full-name] doesn't match a unique key.
412 Should have a leading 0x.")
414 (defun notes-encryption-npgp-userid ()
415 "Return notes-encryption-userid, initializing it if necessary."
417 (if (and notes-encryption-userid
419 notes-encryption-userid
420 (setq notes-encryption-userid
422 (if notes-encryption-key-id
423 (npgp:get-key-by-key-id notes-encryption-key-id)
424 (pam:read-name-key (user-full-name)))))))
426 (defun notes-encryption-mailcrypt-keyid ()
427 "Do the right thing."
430 ((eq notes-encryption-sub-library 'pgp)
431 (cdr (mc-pgp-lookup-key mc-pgp-user-id)))
432 ((eq notes-encryption-sub-library 'pgp50)
433 (cdr (mc-pgp50-lookup-key mc-pgp50-user-id)))
434 ((eq notes-encryption-sub-library 'gpg)
435 (cdr (mc-gpg-lookup-key mc-gpg-user-id)))
436 (t (error "notes-encryption-decrypt-region: no pgp sub-library."))))
438 (defun notes-encryption-load-mailcrypt ()
440 ;; ick ick ick this code needs to be cleaned up
442 ((null (eq notes-encryption-library 'mailcrypt))
444 ((eq notes-encryption-sub-library 'pgp)
445 (load-library "mc-pgp"))
446 ((eq notes-encryption-sub-library 'pgp50)
447 (load-library "mc-pgp5"))
448 ((eq notes-encryption-sub-library 'gpg)
449 (load-library "mc-gpg"))
450 (t (error "notes-encryption-load-mailcrypt: no pgp sub-library."))))
452 (defun notes-encryption-decrypt-region (start end)
454 ((eq notes-encryption-library 'npgp)
457 (npgp:decrypt-region start end))
458 ((eq notes-encryption-library 'mailcrypt)
459 (notes-encryption-load-mailcrypt)
461 ((eq notes-encryption-sub-library 'pgp)
462 (mc-pgp-decrypt-region start end))
463 ((eq notes-encryption-sub-library 'pgp50)
464 (mc-pgp50-decrypt-region start end))
465 ((eq notes-encryption-sub-library 'gpg)
466 (mc-gpg-decrypt-region start end))
467 (t (error "notes-encryption-decrypt-region: no pgp sub-library."))))
468 (t (error "notes-encryption-decrypt-region: no pgp library."))))
470 (defun notes-encryption-encrypt-region (start end)
472 ((eq notes-encryption-library 'npgp)
473 (npgp:encrypt-region (notes-encryption-npgp-userid) start end))
474 ((eq notes-encryption-library 'mailcrypt)
475 (notes-encryption-load-mailcrypt)
476 (let ((old-sign mc-pgp-always-sign)
477 old-comment recipients)
478 (setq mc-pgp-always-sign 'never
479 recipients (list (notes-encryption-mailcrypt-keyid)))
481 ((eq notes-encryption-sub-library 'pgp)
482 (setq old-comment mc-pgp-comment
484 (mc-pgp-encrypt-region recipients start end
485 (notes-encryption-mailcrypt-keyid) nil)
486 (setq mc-pgp-comment old-comment))
487 ((eq notes-encryption-sub-library 'pgp50)
488 (setq old-comment mc-pgp50-comment
490 (mc-pgp50-encrypt-region recipients start end
491 (notes-encryption-mailcrypt-keyid) nil)
492 (setq mc-pgp50-comment old-comment))
493 ((eq notes-encryption-sub-library 'gpg)
494 (setq old-comment mc-gpg-comment
496 (mc-gpg-encrypt-region recipients start end
497 (notes-encryption-mailcrypt-keyid) nil)
498 (setq mc-gpg-comment old-comment))
499 (t (error "notes-encryption-decrypt-region: no gpg sub-library.")))
500 (setq mc-pgp-always-sign old-sign)))
501 (t (error "notes-encryption-decrypt-region: no pgp library."))))
503 (defun notes-encrypt-note (prefix)
504 "Encrypt the current note for the current user. With PREFIX, start from point."
508 ;; Unless a prefix arg, start at beginning-of-note.
511 (if (not (looking-at notes-beginning-of-defun-regexp))
512 (notes-beginning-of-defun))
513 ;; skip over the header
514 (while (and (or (looking-at notes-beginning-of-defun-regexp)
516 (looking-at "^\\(prev\\|next\\): ")
517 (looking-at "^[ \t]*$"))
518 (< (point) (point-max)))
522 (if (re-search-forward "^-----BEGIN PGP MESSAGE"
527 (error "Note is already encrypted."))
530 (while (or (looking-at notes-beginning-of-defun-regexp)
531 (looking-at "^[ \t]*$"))
535 (notes-encryption-encrypt-region start end))))
537 (defun notes-decrypt-note ()
538 "Decrypt the current note for the current user."
541 (if (not (looking-at notes-beginning-of-defun-regexp))
542 (notes-beginning-of-defun))
543 (if (null (re-search-forward "^-----BEGIN PGP"
548 (error "Note is not encrypted."))
550 (let ((start (point)))
551 (if (null (re-search-forward "^-----END PGP"
556 (error "Could not find end of encrypted note."))
559 (notes-encryption-decrypt-region start (point)))))
563 ;;; notes or notes-index?
565 (defun notes-summarize-subject (regexp-subject &optional subject)
566 "* Collect all of a subject."
568 (require 'notes-index-mode)
571 ((eq major-mode 'notes-mode)
572 (setq subject (notes-extract-subject nil t)))
573 ((eq major-mode 'notes-index-mode)
574 (setq subject (notes-index-extract-subject)))))
576 (error "notes-summarize-subject: no subject specified or inferable."))
578 ((buf (get-buffer-create (concat "*notes on " subject "*"))))
581 (apply 'call-process (concat notes-bin-dir "/catsubject") nil buf t
589 ;;; notes-rename-subject
591 (defun notes-rename-subject ()
592 "* Rename the current subject.
593 Assumes working next/prev linkage between the entries."
595 (let ((subject (notes-extract-subject)))
600 (if (not (looking-at "* "))
603 (error "not yet done")
613 ;; This use of define-derived-mode is a crock---maybe
614 ;; it's better to eval it?
615 ;; suggestions are welcome. ---johnh, 26-Oct-98
617 (if (fboundp 'indented-text-mode)
618 (define-derived-mode notes-mode indented-text-mode "Notes"
619 "See notes-mode-internal for documentation."
620 (notes-mode-internal))
621 (define-derived-mode notes-mode text-mode "Notes"
622 "See notes-mode-internal for documentation."
623 (notes-mode-internal)))
626 (defun notes-mode-internal ()
627 "Enable notes-mode for a buffer.
629 Inside a notes buffer one can click on URLs and follow them to
632 Notes are fontified if notes-use-font-lock is set.
633 See the file notes-variables.el for all customization options.
634 To change options, (require 'notes-variables) in your .emacs
635 and then change things.
637 Subjects in notes mode are lines beginning with an asterisk
638 and underlined with dashes. Subjects can be completed
639 with \\[notes-complete-subject] and are automatically underlined.
641 You may wish to add this code to your .emacs file:
642 (setq auto-mode-alist
643 (cons (cons \"/9[0-9][0-9][0-9][0-9][0-9].?$\" 'notes-mode)
645 (define-key global-map \"\C-cn\" 'notes-index-todays-link)
646 to automatically enter notes mode.
648 I have two suggestions for how to organize your notes files.
649 First, I collect my notes into a separate file per day. (If you have
650 fewer notes, you may find once-per-week or month more suitable.)
651 Second, at the beginning of each file I have a subject \"* Today\".
652 Since every file has this subject, I can use its prev and next links
653 to easily move around the collection of files.
655 The key-bindings of this mode are:
657 (interactive) ;; just so documentation can come up
659 (notes-platform-init)
662 ;; Emacs-19.30's define-derived-mode sets up a bogus syntax-table.
663 ;; (Evidence for the error is ``Wrong type argument: consp, nil''
664 ;; when typing in the buffer.)
666 ;; bug work-around 2:
667 ;; Klaus Zeitler <kzeitler@lucent.com>
668 ;; reports that the next line dies in emacs-21.1 with the error:
669 ;; "Attempt to make a chartable be its own parent".
670 ;; Work-around: more hackery.
671 (if (and (< emacs-major-version 21)
672 (or (>= emacs-major-version 20) (>= emacs-minor-version 30)))
673 (set-syntax-table (setq notes-mode-syntax-table text-mode-syntax-table)))
675 ;; now set up the mode
678 ;; random key-bindings
679 (define-key notes-mode-map "\M-\C-a" 'notes-beginning-of-defun)
680 (define-key notes-mode-map "\M-\C-e" 'notes-end-of-defun)
681 (define-key notes-mode-map "\C-c\C-d" 'notes-decrypt-note)
682 (define-key notes-mode-map "\C-c\C-e" 'notes-encrypt-note)
683 (define-key notes-mode-map "\C-c\r" 'notes-w3-follow-link)
684 (define-key notes-mode-map "\C-c\C-p" 'notes-follow-prev-link)
685 (define-key notes-mode-map "\C-c\C-n" 'notes-follow-next-link)
686 (define-key notes-mode-map "\C-c\C-i" 'notes-goto-index-entry)
687 (define-key notes-mode-map "\C-c\C-k" 'notes-current-url-as-kill)
688 (define-key notes-mode-map "\C-c\C-s" 'notes-summarize-subject)
689 (define-key notes-mode-map "\C-c-" 'notes-underline-line)
690 (if (null notes-default-tab-binding)
691 (setq notes-default-tab-binding (key-binding "\t")))
692 (define-key notes-mode-map "\t" 'notes-complete-subject)
693 (if (null notes-default-return-binding)
694 (setq notes-default-return-binding (key-binding "\r")))
695 (define-key notes-mode-map "\r" 'notes-electric-return)
696 (define-key notes-mode-map "\n" 'notes-electric-return) ; a more common synonym
697 (notes-platform-bind-mouse notes-mode-map 'S-mouse-2 'notes-w3-follow-link-mouse)
700 (make-variable-buffer-local 'imenu-prev-index-position-function)
701 (make-variable-buffer-local 'imenu-extract-index-name-function)
702 (setq imenu-prev-index-position-function 'notes-beginning-of-defun)
703 (setq imenu-extract-index-name-function 'notes-extract-subject)
705 (if notes-use-font-lock
706 (notes-platform-font-lock notes-font-lock-keywords))
708 ;; finally, try to fill in an empty note
709 (if (eq (point-min) (point-max))
710 (notes-mode-initialize-note))
712 ;; Enable outline-minor-mode (forcebly, in case someone already
713 ;; has it in their text-mode hook). Bug found by
714 ;; Nils Ackermann <nils@nieback.de>.
715 (if notes-use-outline-mode
716 (outline-minor-mode 1))
719 (run-mode-hooks 'notes-mode-hooks)))
726 (run-hooks 'notes-mode-load-hooks)
727 (provide 'notes-mode)