;;; -*- Mode: Emacs-Lisp -*-
;;; File: bookmark.el
;;; 
;;; A generic bookmark package for Emacs.  Records location in file
;;; using both the point in the file and surrounding text.
;;; 
;;; Author: Karl Fogel
;;; (kfogel@cs.oberlin.edu)
;;;
;;; Copyright (C) 1993 Karl Fogel, Ken Olstad
;;; (kfogel@cs.oberlin.edu, olstad@msc.edu)
;;;
;;; This program is free software; you can redistribute it and/or modify
;;; it under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 2 of the License, or
;;; (at your option) any later version.
;;;
;;; This program is distributed in the hope that it will be useful,
;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with this program; if not, write to the Free Software
;;; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
;;; 
;;; LCD Archive Entry:
;;; bookmark|Karl Fogel|kfogel@cs.oberlin.edu|
;;; Setting bookmarks in files, jumping to them later.|
;;; 16-July-93|Version: 0.1 $|~/misc/bookmark.el.Z|
;;;
;;; For more information about the copyright and lack of warranty,
;;; write to the author.
;;; If you do not have electronic mail access, write to me at
;;;
;;; Karl Fogel
;;; 1123 N. Oak Park Ave.
;;; Oak Park, ILL
;;; USA      60302
;;;
;;; for more information.
;;;
;;; This file is the result of extensive modifications of
;;; info-bookmark.el, by Karl Fogel and Ken Olstad.  If you use
;;; Info-bookmarks, most of the code and user-level functionality here
;;; will be very familiar.
;;;
;;; Please let me know what you think of it... I like messages with
;;; complaints almost as much as ones without complaints! :-)
;;;
;;; INSTALLATION:
;;; 
;;; Put this file in a directory that is in your load-path, and in
;;; your .emacs put something like this (remember to take out the
;;; semicolons!): 
;;; 
;;; (global-set-key "\C-cb" 'bookmark-map)
;;; (autoload 'bookmark-map "bookmark" "Bookmarks, yay!" t)
;;;
;;; You may make minor customizations by setting bookmark-file,
;;; bookmark-completion-ignore-case, and/or
;;; bookmark-save-flag, etc, to preferred values in your
;;; .emacs file.  Do describe-variable on these for more information.
;;;
;;; If for some reason autoloading doesn't work, use this instead:
;;;
;;; (load "bookmark.el")
;;;
;;; and don't worry about doing anything fancier.
;;; 
;;; USAGE:
;;;
;;; * Simple * : When you want to save a spot in a certain file, go to
;;; that spot in the file (inside an Emacs buffer) and use
;;; bookmark-set (probably bound to "C-c b x").  Later, to go back
;;; to that spot, use bookmark-jump (probably "C-c b j"), and you
;;; should be put back to that spot.  Completion is available on
;;; bookmark names when jumping: hit TAB or whatever to see all
;;; currently set bookmarks.
;;;
;;; Jumping can handle a certain amount of change to the file, but be
;;; aware that if the file has been modified, you might end up in
;;; unfamiliar-looking territory.  For those of you using this to save
;;; your place in Shakespeare and other immortal, immutable texts,
;;; this will not be a problem.
;;;
;;; Naturally, if the file no longer exists, or has been moved to a
;;; new location (which is pretty much the same thing), you won't be
;;; able to jump to a bookmark in it.
;;;
;;; * Verbose * :
;;; (Read the "Simple" section above, then skip to the end of this
;;; sentence so you don't get caught in a recursive loop).
;;;
;;; (All keybindings given below are relative to bookmark-map, so
;;; mentally insert the prefix key sequence before them all.  If you
;;; installed this exactly as the instructions say, then that prefix
;;; is probably "C-c b").
;;;
;;; Bookmarks are saved automatically in the file ~/.emacs-bkmrks and
;;; reloaded so that they will not be lost between different Emacs
;;; sessions.  (The reloading takes place at the very end of
;;; bookmark.el, in fact).  If you have turned off automatic saving of
;;; new bookmarks by setting the value of bookmark-save-flag to nil,
;;; you can cause all bookmarks currently in effect to be saved by
;;; using "w".  Giving a prefix argument (typing "C-u", then the
;;; prefix key sequence, then "w") will cause you to be prompted for a
;;; file, instead of automatically using the value of bookmark-file.
;;;
;;; At any time, you can revert to the bookmarks in ~/.emacs-bkmrks with
;;; "r". You can also load in bookmarks from another file (perhaps someone
;;; else's bookmark file) with "l". If you wish to delete a bookmark (rare),
;;; use "d". You will be prompted for a bookmark to remove, and completion
;;; will aid you in the arduous task of typing in its name.
;;;
;;; Also, while setting a bookmark, you can type C-w to grab a word
;;; from the buffer text and use it as part of the bookmark name.
;;; Another C-w appends the next word, then the next, etc.  C-w's need
;;; not be consecutive.  This feature is similar to (and inspired by)
;;; isearch mode's use of C-w to add words from the buffer to the
;;; search string.
;;;
;;; If you give a prefix argument to bookmark-set, then setting a
;;; bookmark with the same name as one that already exists will *not*
;;; overwrite the previous bookmark as it normally would; instead, the
;;; newer one will be prepended to the bookmark list, so that it
;;; shadows out the old one until the newer one is deleted.  Deleting
;;; a bookmark named "Farley" always deletes the most recently set one
;;; with the name "Farley", uncovering the next most recent "Farley",
;;; if there is one.
;;; 
;;; Functions you might want to do describe-function on, for more details:
;;; bookmark-set
;;; bookmark-jump
;;; bookmark-revert
;;; bookmark-delete
;;; bookmark-load
;;; bookmark-write
;;;
;;; NOTE (REINVENTED WHEEL WARNING): There is a previous bookmark
;;; package by someone named T. V. Raman (raman@cs.cornell.edu).  I
;;; have not used it, I have just looked at the source code. I wrote
;;; my own because I was already using a package called
;;; info-bookmark.el (bookmarks in Info, needless to say), and was
;;; familiar with its keybindings and code, being one of its authors.
;;; Therefore I cannibalized info-bookmark and turned it into a
;;; generic bookmark package.  This is not a comment on the quality of
;;; any other bookmark package out there -- I did it actually because
;;; a friend sort of dared me to write this in one night.  He was Ben
;;; Sussman (bms1@midway.uchicago.edu, sussman@antares.mcs.anl.gov):
;;; if you don't like this, blame him, not me :-) This package,
;;; Raman's, and perhaps others, are now or soon will be archived in
;;; the Ohio State LCD Archive, open for anonymous ftp at:
;;; archive.cis.ohio-state.edu:/pub/gnu/emacs/elisp-archive/*
;;;
;;; I have also heard that Hyperbole (available from the LCD Archive
;;; too), implements bookmarks, among other things.  It's pretty big,
;;; but comes highly recommended, so you might want to give that a
;;; try.  I haven't used it, but plan to learn it as soon as I get
;;; time. 
;;;
;;; NOTES ON THE CODE, and TODO LIST:
;;; bookmark alist format:
;;;                 (...
;;;                  (bookmark-name
;;;                   (filename
;;;
;;;                    ;; str-in-front is nil if not enough room,
;;;                    ;; which would tell us that it was close to 
;;;                    ;; point-min
;;;                    string-in-front
;;;
;;;                    ;; str-in-back is nil if not enough room,
;;;                    ;; which would tell us that it was close to 
;;;                    ;; point-max 
;;;                    string-behind
;;;                    point))
;;;                  ...)
;;;
;;; What else...??? Better support for multiple bookmark files?
;;;
;;; features equivalent to isearch-yank-word/line when setting
;;; bookmarks? 
;;;
;;; above is now done, hah!
;;;
;;; Suggestions welcome!

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Date: Tue, 20 Jul 1993 01:34:49 -0400
;; From: kfogel@occs.cs.oberlin.edu (Karl Fogel)
;; To: gnu-emacs-sources@prep.ai.mit.edu
;; Subject: updated bookmark.el, glitzy new feature :-)
;; 
;; 	The new feature du jour allows you to automatically yank in
;; text from the buffer to use as a bookmark name when setting a
;; bookmark.  Just type C-w when setting a bookmark, and the word (or
;; fragment) following point will be be appended to the bookmark name.
;; Another C-w grabs the next word, etc.  For those who enjoy the C-w and
;; C-y features of isearch mode, this is the same idea (inspired by
;; isearch, in fact). 
;; 
;; 	Completion when setting bookmarks is gone, because it was not
;; a useful feature and often got in the way.  Completion when *jumping*
;; to a bookmark is still there, of course :-)
;; 
;; 	Also, a bug has been pointed out by Ken Manheimer:
;; 
;; (global-set-key "\C-cb" 'bookmark-map) ;set the bookmark prefix
;; (autoload 'bookmark-map "bookmark" "Bookmark functions." t)
;; 
;; 	gives an error the first time C-c b is typed, although the
;; file does get loaded correctly and bookmark functions work fine
;; thereafter.  To avoid that initial error, just use a load instead of
;; an autoload:
;; 
;; (load "bookmark")
;; 
;; 	I'm hoping that autoload will learn to deal with loading
;; prefix commands, in some future Emacs release.  If it becomes clear
;; that this will not be the case, Ken wrote a little function to "boot"
;; the keymap without errors, and we'll just use that, I guess.
;; 
;; 	bookmark.el is still small, so here is the whole thing:
;; 
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;;; okay, here begins the fun: ;;;;

;; We're looking for a few good keymaps...
(define-prefix-command 'bookmark-map)

;; Read the help on all of these functions for details...
;; "x" marks the spot!
(define-key bookmark-map "x" 'bookmark-set)
(define-key bookmark-map "j" 'bookmark-jump)
;; resets bookmark list
(define-key bookmark-map "r" 'bookmark-revert)
;; deletes bookmarks
(define-key bookmark-map "d" 'bookmark-delete)
;; loads new file
(define-key bookmark-map "l" 'bookmark-load)
;; saves them in file
(define-key bookmark-map "w" 'bookmark-write)  
(define-key bookmark-map "s" 'bookmark-save) ;alias for bookmark-write

;; just add the hook to make sure that people don't lose bookmarks
;; when they kill Emacs, unless they don't want to save them.
(add-hook 'kill-emacs-hook
	  (function 
	   (lambda ()
	     (and (featurep 'bookmark)
		  bookmark-alist
		  (bookmark-time-to-save-p)
		  (bookmark-write nil)))))

(defvar bookmark-save-flag 0
  "*Nil means never save bookmarks, except when bookmark-save is
explicitly called \(\\[bookmark-save]\).

t means save bookmarks when Emacs is killed.

Otherise, it should be a number that is the frequency with which the
bookmark list is saved \(i.e.: the number of times which Emacs\'
bookmark list may be modified before it is automatically saved.\).  If
it is a number, Emacs will also automatically save bookmarks when it
is killed.

Therefore, the way to get it to save every time you make or delete a
bookmark is to set this variable to 1.  If it is 0, bookmarks will be
saved when Emacs is killed, and not until.

To specify the file in which to save them, modify the variable
bookmark-file, which is \"~/.emacs-bkmrks\" by default.")

(defvar bookmark-alist-modification-count 0
  "Number of times the bookmark list has been modified since last
saved.")

(defvar bookmark-file (if (eq system-type 'ms-dos)
			  "~/_emacsbk.mrk"
			"~/.emacs-bkmrks")
  "*File in which to save bookmarks by default.")

(defvar bookmark-completion-ignore-case t
  "*Non-nil means that the various bookmark functions that
do completion will be case-insensitive in completion.")

(defvar bookmark-search-size 500 "Number of chars resolution used
in creating tag strings to record a bookmark.  Bookmark functions will
search for these strings in deciding where to jump to, to deal with
changing values of point.  I can\'t think of any reason you would want
to modify this, and doing so might have side effects, so on your own
head be it...")

(defvar bookmark-alist ()
  "Association list of bookmarks.
You probably don't want to change the value of this alist yourself;
instead, let the various bookmark functions do it for you.")

(defvar bookmark-word-grab-default 3 "*The default number of words
\(from the buffer text\) to use as the default bookmark name when
setting a bookmark.")

(defun bookmark-set (&optional parg)
  "Set a bookmark named NAME inside a file.  With prefix arg, will not
overwrite a bookmark that has the same name as NAME if such a bookmark
already exists, but instead will \"push\" the new bookmark onto the
bookmark alist.  Thus the most recently set bookmark with name NAME would
be the one in effect at any given time, but the others are still there,
should you decide to delete the most recent one.

Typing C-w while setting a bookmark inserts the word (or fragment)
after point, another C-w yanks the next word into the bookmark name,
and so on \(similar to the way C-w behaves in isearch-mode\).  This is
so that you can set bookmarks using text directly from the file that
the bookmark refers to, without having to - ugh! - type it yourself.

Use \\[bookmark-delete] to remove bookmarks \(you give it a name,
and it removes only the first instance of a bookmark with that name from
the list of bookmarks.\)"
  (interactive "P")
  (if (not buffer-file-name)
      (error "Buffer not visiting a file."))
  (setq bookmark-current-point (point))
  (setq bookmark-yank-point (point))
  (setq bookmark-current-buffer (current-buffer))
  (bookmark-make
   parg
   (read-from-minibuffer
    "Set bookmark: "
    nil
    (let ((now-map (copy-keymap minibuffer-local-map)))
      (define-key
	now-map
	"\C-w"
	'bookmark-yank-word)
      now-map)))
  (goto-char bookmark-current-point))

(defun bookmark-yank-word ()
  (interactive)
  ;; get the next word from the buffer and append it to the name of
  ;; the bookmark currently being set.
  (let ((string (save-excursion
		  (set-buffer bookmark-current-buffer)
		  (goto-char bookmark-yank-point)
		  (buffer-substring
		   (point)
		   (save-excursion
		     (forward-word 1)
		     (setq bookmark-yank-point (point)))))))
    (insert string)))

(defun bookmark-make (parg str)
  (if (and (assoc str bookmark-alist) (not parg))
      ;; already existing boookmark under that name and
      ;; no prefix arg means just overwrite old bookmark
      (setcdr (assoc str bookmark-alist)
	      (list (bookmark-make-cell)))

    ;; otherwise just cons it onto the front (either the bookmark
    ;; doesn't exist already, or there is no prefix arg.  In either
    ;; case, we want the new bookmark consed onto the alist...)

    (setq bookmark-alist
	  (cons
	   (list str 
		 (bookmark-make-cell))
	   bookmark-alist)))
  (setq bookmark-alist-modification-count
	(1+ bookmark-alist-modification-count))
  (if (bookmark-time-to-save-p)
      (bookmark-write nil)))

(defun bookmark-make-cell ()
  ;; make the cell that is the cdr of a bookmark alist element.  It
  ;; looks like this:
  ;; (filename search-forward-str search-back-str point)
  (list
   buffer-file-name
   (if (>= (- (point-max) (point)) bookmark-search-size)
       (buffer-substring 
	(point)
	(+ (point) bookmark-search-size))
     nil)
   (if (>= (- (point) (point-min)) bookmark-search-size)
       (buffer-substring 
	(point)
	(- (point) bookmark-search-size))
     nil)
   (point)))
   

(defun bookmark-jump (str)
  "Go to the location saved in the bookmark BOOKMARK.  You may have a
problem using this function if the value of variable bookmark-alist
is nil.  If that happens, you need to load in some bookmarks.  See
help on function bookmark-load for more about this."
  (interactive (progn
		 (if (and 
		      (null bookmark-alist)
		      (file-readable-p (expand-file-name bookmark-file)))
		     (bookmark-load bookmark-file))
		 (let ((completion-ignore-case
			bookmark-completion-ignore-case))
		   (list (completing-read
			  "Jump to bookmark: "
			  bookmark-alist
			  nil
			  0)))))
  (let ((whereto-list (car (cdr (assoc str bookmark-alist)))))
    (let ((file (car whereto-list))
	  (forward-str (car (cdr whereto-list)))
	  (behind-str (car (cdr (cdr whereto-list))))p
	  (place (car (cdr (cdr (cdr whereto-list))))))
      (if (file-exists-p (expand-file-name file))
	  (progn 
	    (find-file (expand-file-name file))
	    (goto-char place)
	    ;; Go searching forward first.  Then, if forward-str exists and
	    ;; was found in the file, we can search backward for behind-str.
	    ;; Rationale is that if text was inserted between the two in the
	    ;; file, it's better to be put before it so you can read it,
	    ;; rather than after and remain perhaps unaware of the changes.
	    (if forward-str
		(if (search-forward forward-str (point-max) t)
		    (backward-char bookmark-search-size)))
	    (if behind-str
		(if (search-backward behind-str (point-min) t)
		    (forward-char bookmark-search-size))))
	(error 
	 (concat "File "
		 file
		 " does not exist.  Suggest deleting bookmark \""
		 str
		 "\""))))))

(defun bookmark-delete (str)
  "Delete the bookmark named NAME from the bookmark list.  Removes only
the first instance of a bookmark with that name.  If there is another
bookmark with the same name, it will become \"current\" as soon as the
old one is removed from the bookmark list."
    (interactive (let ((completion-ignore-case
			bookmark-completion-ignore-case))
		   (list (completing-read
			  "Delete bookmark: "
			  bookmark-alist
			  nil
			  0))))
    (let ((will-go (assoc str bookmark-alist)))
      (if (string-equal str (car (car bookmark-alist)))
	  (setq bookmark-alist
		(delq will-go bookmark-alist))
	(delq will-go bookmark-alist)))
    (setq bookmark-alist-modification-count
	  (1+ bookmark-alist-modification-count))
    (if (bookmark-time-to-save-p)
	(bookmark-write nil)))

(defun bookmark-time-to-save-p ()
  ;; finds out whether it's time to save bookmarks to a file, by
  ;; examining the value of variable bookmark-save-flag, and maybe
  ;; bookmark-alist-modification-count.  Returns t if they should be
  ;; saved, nil otherwise.
  (cond
   ((null bookmark-save-flag)
    nil)
   ((numberp bookmark-save-flag) 
    ;; oh, goodie, nested conds.  Yecch.
    (cond 
     ((= bookmark-alist-modification-count 0)
      nil)
     ((< bookmark-alist-modification-count bookmark-save-flag)
      nil)
     (t ; modification count is >= than desired frequency, so return
        ; t, meaning it's time to save.
      t)))
   (t   ; only other reasonable possiblity is that bookmark-save-flag
	; is t, in which case, we only save when Emacs is killed.
    nil)))

(defalias 'bookmark-save 'bookmark-write)

(defun bookmark-write (parg &optional file)
  "Saves currently defined bookmarks in the file defined by
the variable bookmark-file.  With a prefix arg, save it in file
FILE.

If you are calling this from Lisp, the two arguments are PREFIX-ARG
and FILE, and if you just want it to write to the default file,
then pass in nil as the only argument.  Or pass in nil and FILE, and
it will save in FILE instead.  If you pass in one argument, and it is
non-nil, then the user will be interactively queried for a file to
save in.

When you want to load in the bookmarks from a file, use bookmark-load,
\\[bookmark-load].  With a prefix arg, that function will prompt you
for a file, otherwise, it uses the file defined by the variable
bookmark-file."
  (interactive "P")
  (cond
   ((and (null parg) (null file))
    ;;whether interactive or not, write to default file
    (bookmark-write-file bookmark-file))
   ((and (null parg) file)
    ;;whether interactive or not, write to given file
    (bookmark-write-file file))
   ((and parg (not file))
    ;;have been called interactively w/ prefix arg
    (let ((file (read-file-name "File to save bookmarks in: ")))
      (bookmark-write-file file)))
   (t ; someone called us with prefix-arg *and* a file, so just write to file
    (bookmark-write-file file)))
  ;; signal that we have synced the bookmark file by setting this to
  ;; 0.  If there was an error at any point before, it will not get
  ;; set, which is what we want.
  (setq bookmark-alist-modification-count 0))

(defun bookmark-write-file (file)
  (save-excursion
    (message (format "Saving bookmarks to file %s." file))
    (set-buffer (find-file-noselect file))
    (goto-char (point-min))
    (delete-region (point-min) (point-max))
    (print bookmark-alist (current-buffer))
    (write-file file)
    (kill-buffer (current-buffer))))

(defun bookmark-load (file &optional revert)
  "Loads bookmarks from FILE, appending loaded bookmarks to the front
of the list of bookmarks.  If optional second argument REVERT is
non-nil, existing bookmarks are destroyed.

If you load a file that doesn't contain a proper bookmark alist, you
will corrupt Emacs\' bookmark list.  Generally, you should only load
in files that were created with the bookmark functions in the
first place.  If the bookmark alist does become corrupted, use
bookmark-revert \(\\[bookmark-revert\) to reset it \(assuming that
your bookmark file itself is not corrupt\)."
  (interactive
   (list (read-file-name
	  (format "Load bookmarks from: (%s) "
		  bookmark-file)	
	  ;;Default might not be used often,
	  ;;but there's no better default, and
	  ;;I guess it's better than none at all.
	  "~/" bookmark-file 'confirm)))
  (setq file (expand-file-name file))
  (if (file-readable-p file)
      (save-excursion
	(message (format "Loading bookmarks from %s..." file))
	(set-buffer (find-file-noselect file))
	(goto-char (point-min))
	(let ((blist (car (read-from-string
			   (buffer-substring (point-min) (point-max))))))
	  (if (listp blist)
	      (progn
		(if (not revert)
		    (setq bookmark-alist-modification-count
			  (1+ bookmark-alist-modification-count))
		  (setq bookmark-alist-modification-count 0))
		(setq bookmark-alist
		      (append blist (if (not revert) bookmark-alist))))
	    (error (format "Invalid bookmark list in %s." file))))
	(kill-buffer (current-buffer))
	(message (format "Loading bookmarks from %s...done" file)))
    (error (format "Cannot read bookmark file %s." file))))

(defun bookmark-revert (&optional not-really)
  "Resets the bookmark list, using bookmarks stored in the default
bookmark file.  This will have the effect of deleting any bookmarks
that are not saved in the file.  Asks for confirmation first.

This is desirable when you want to get rid of a whole bunch of bookmarks
that you set yourself or loaded in from somewhere else, without you
having to delete them one by one.

Use bookmark-load (\\[bookmark-load]) to load bookmarks from
a file without destroying your current bookmarks."

  (interactive
   (list (not (y-or-n-p
	       (format "Revert to bookmarks from %s? " bookmark-file)))))
  (if not-really
      (message "Bookmark list not reverted.")
    (bookmark-load bookmark-file t)))

;; load the default bookmark file, if it exists.
(if (file-exists-p bookmark-file)
    (bookmark-load bookmark-file t))

(provide 'bookmark)

;;; bookmark.el end here ;;;
