;;; hippie-file-cache.el --- context-sensitively find files by name.

;; Copyleft (C) 2009 Cecil Curry <http://raiazome.com>

;; Author:     Cecil Curry <http://raiazome.com>
;; Maintainer: Cecil Curry <http://raiazome.com>
;; Time-stamp: "2009-05-21 18:09:43 leycec"
;; Created: 1 Mar 2009
;; Version: 0.0.1
;; URL: http://hippie.raiazome.com
;; Keywords: abbrev,c,convenience,data,emulations,faces,files,lisp,matching,tools

;; This file is not part of GNU Emacs.

;;; Commentary:
; --------------------( SYNOPSIS                           )--------------------
; `hippie-file-cache' extends `file-cache' functionality with IDo- or iswitchb-
; style completion of cached filenames and caching of those filenames on a per-
; project basis. (This functionality, when conjoined, mimics the "Jump to File"
; functionality of Eclipse, NetBeans, or other modern IDEs.)
;
; --------------------( INSTALLATION                       )--------------------
; ; Move this file to a path on your current `load-path'. As example:
; ; mv hippie-file-cache.el ~/.emacs.d/site-lisp/
;
; ; Add this line to your Emacs startup file (typically, "~/.emacs").
; (require 'hippie-file-cache)
;
; ; Alternatively, add these lines to to delay importation of this file.
; (autoload 'hippie-file-cache/ido-find-file "hippie-file-cache"
;   "Find a file via IDo-style completion from the `file-cache'." t)
; (autoload 'hippie-file-cache/iswitchb-find-file "hippie-file-cache"
;   "Find a file via iswitchb-style completion from the `file-cache'." t)
; (autoload 'hippie-file-cache/add-load-path "hippie-file-cache"
;   "Add all Emacs Lisp files in the `load-path' to the `file-cache'." t)
; (autoload 'hippie-file-cache/add-path-perl "hippie-file-cache"
;   "Recursively add all Perl files in the passed PATH to the `file-cache'." t)
; (autoload 'hippie-file-cache/add-path-python "hippie-file-cache"
;   "Recursively add all Python files in the passed PATH to the `file-cache'." t)
; (autoload 'hippie-file-cache/add-path-ruby "hippie-file-cache"
;   "Recursively add all Ruby files in the passed PATH to the `file-cache'." t)
;
; ; Optionally, bind <Alt-:> to `hippie-file-cache/ido-find-file'.
; (global-set-key (kbd "M-:") 'hippie-file-cache/ido-find-file)
;
; ; Optionally, add all system-wide and user-specific Emacs Lisp files in the
; ; `load-path' to the `file-cache', when opening any Emacs lisp file. (This is
; ; deferred until the first Emacs Lisp file is opened, and will not be
; ; repeated when subsequent Emacs Lisp files are opened. As this is an
; ; expensive operation, that is probably what you want.)
; (add-hook 'emacs-lisp-mode-hook 'hippie-file-cache/lisp-load-path)
;
; ; Optionally, as example, recursively add all project-specific Perl files in
; ; some path to the `file-cache', when opening any one specific Perl file in
; ; that path; and do the same for all project-specific Python files in some
; ; other path and project-specific Ruby files in another path. (This, also, is
; ; deferred until the first file for each project is opened and will not be
; ; repeated when subsequent files for the project are opened.)
; (defun change-major-mode-settings (file-name)
;   "Change settings for the current major-mode, according to the FILE-NAME
; associated with the buffer for the major-mode. (Typically, this function is
; called by `after-change-major-mode-hook' on opening a new file.)"
;   (cond
;    ((string-match            "^/home/leycec/src/perl/oddmuse/" file-name)
;     (hippie-file-cache/perl   "/home/leycec/src/perl/oddmuse/"))
;    ((string-match            "^/home/leycec/src/python/moinmoin/" file-name)
;     (hippie-file-cache/python "/home/leycec/src/python/moinmoin/"))
;    ((string-match            "^/home/leycec/src/ruby/tiddlywiki/" file-name)
;     (hippie-file-cache/ruby   "/home/leycec/src/ruby/tiddlywiki/"))
; ))
;
; --------------------( DESCRIPTION                        )--------------------
; `hippie-file-cache' extends `file-cache' functionality with improvements to
; the user-interface for that `file-cache'--so that, ideally, Emacs users may
; find and open recently-opened files, project-specific files, and shared-
; library files with all the convenience and (mostly) masterful simplicity of
; Eclipse's "Jump to File" functionality. (...more on that, if you're not that
; fond of Eclipse. We hardly fault you, there. Here is something, though, that
; Eclipse has "up" on Emacs--and from which Emacs could heartily benefit.)
;
; `hippie-file-cache' also auto-populates the `file-cache' with sane defaults--
; namely, the list of all recently-opened files (as recorded by `recentf').
;
; `hippie-file-cache' does not obsolete `file-cache', but still depends on that
; feature for its internal handiwork.
;
; `file-cache' caches filenames in an internal alist, mapping filenames to the
; parent path of that filename. Typically, you access this cache by typing
; <Ctrl-x Ctrl-f> (to invoke the "Find file:" command), typing the beginning of
; the cached filename you wish to find, typing <Ctrl-Tab> to complete and expand
; the cached filename to its absolute path, and typing <Enter> to open that
; file. However, this is several keystrokes too many. It should be possible to
; interactively show all matching cached filenames as you type the beginning of
; the cached filename you wish to find, without having to explicitly type
; <Ctrl-Tab> after typing every character of that filename. This is how filename
; and buffer-name completion in `ido-mode', `iswitchb-mode', and Eclipse's "Jump
; to File" dialog operates--and how `file-cache' filename completion should
; operate, too.
;
; Also, it should be possible to defer population of the `file-cache' on a per-
; project and major-mode-specific basis with only the files pertaining to that
; project and major-mode. Populating the `file-cache' is expensive--especially
; when recursively caching filenames from a path having many paths or files--
; and pollutes the cache with superfluous, semantically unrelated filenames--
; especally when caching filenames across many separate paths and projects.
;
; `hippie-file-cache' addresses both of these issues: the former, by providing
; `hippie-file-cache/iswitchb-find-file' and
; `hippie-file-cache/ido-find-file'; the latter, by providing
; `hippie-file-perl', `hippie-file-python', `hippie-file-ruby', et al.
;
; `hippie-file-cache/iswitchb-find-file' and
; `hippie-file-cache/ido-find-file' perform "on the fly" filename completion
; across filenames cached in the `file-cache'. Assuming you bind either of
; these two functions to <Alt-:>, you may open any file you have recently
; opened or closed, or any project-specific file you have already cached (by
; application of the "Installation" instructions, above), by typing <Alt-:>
; and then the beginning of the cached filename you wish to find; by leveraging
; `iswitchb-mode' or `ido-mode' completions, we automatically filter candidates
; for the cached filename as you type it. As we do leverage existing completion
; modes, you may use key bindings for those modes to manually filter candidates.
; `hippie-file-cache/ido-find-file', for example, uses IDo-style key bindings to
; cycle through, narrow, and widen filename candidates. After typing typing
; <Alt-:> and the beginning of the cached filename you wish to find, type <Tab>
; to show an IDo-style buffer having all possible filename candidates, type
; <Left> and <Right> to cycle left and right between these candidates, and so
; on. (`iswitchb-mode', though not used by this author, has equivalent key
; bindings--presumably.)
;
; `hippie-file-perl', `hippie-file-python', and `hippie-file-ruby', et al. add
; all Perl-, Python-, and Ruby-specific files under the passed path to the
; `file-cache'--if and only if that path has not already been added to the
; `file-cache'. By calling one or several of these functions from the built-in
; `after-change-major-mode-hook' function (see "Installation", above), you can
; easily emulate the the Eclipse-ish ability to cache and recall filenames on a
; per-project basis. Futher, this mechanism of calling arbitrary functions on a
; per-project basis can be used to emulate the full-blown project support found
; in most modern IDEs--without maintaining external project files, maintained in
; extravagantly folding XML, found in most modern IDEs. As example, you could
; just as readily augment the Emacs Lisp code snippets, above, to additionally
; establish a top-level TAGS file for each project:
;
;    ((string-match              "^/home/leycec/src/perl/oddmuse/" file-name)
;     (hippie-file-cache/perl     "/home/leycec/src/perl/oddmuse/")
;     (setq tags-table-list (cons "/home/leycec/src/perl/oddmuse/" tags-table-list)))
;
; --------------------( ORIGINS                            )--------------------
; `hippie-file-cache/iswitchb-find-file' and
; `hippie-file-cache/ido-find-file' were harvested, wholesale, from the
; EmacsWiki `file-cache' entry at:
;
;     http://www.emacswiki.org/cgi-bin/wiki/FileNameCache
;
; These functions are sufficiently powerful and not, at present, provided by any
; other Emacs Lisp files or features. This file's present author provides them,
; here--with lively documentation, light patching, and liberal love, that they
; may find a larger audiences and bright applause, elsewhere.
;
; --------------------( CONTRIBUTORS                       )--------------------
; * Mathais Dahl <ma...as.dahl@gmail.com> contributed portions of
;   `hippie-file-cache/iswitchb-find-file'.
; * Martin Nordholts <http://chromecode.com> contributed portions of
;   `hippie-file-cache/ido-find-file' function. My zesty thanks, for that!
; * Rubikitch <http://www.emacswiki.org/cgi-bin/wiki/rubikitch> contributed
;   portions of the wily heart of `hippie-file-cache/cache-buffer-file-name'.
;
; --------------------( TODO                               )--------------------
; * Hooks to all functions that manipulate files should be added, so as to
;   synchronize filesystem changes made via Emacs Lisp functions with the
;   `file-cache'. In particular, `dired' changes (like file removal and move)
;   should be reflected in the `file-cache'. Also, the `file-cache' appears to
;   become desynchronized by simply moving a file externally after that file
;   has already been cached; manually re-opening the file in its new location 
;   does not appear to update the `file-cache' entry for that file. Craziness!
;   The functions to consider advising more or less include those that produce
;   side-effects in the list of "magic" file handlers at:
;   http://www.gnu.org/software/emacs/manual/html_node/elisp/Magic-File-Names.html

;;; History:
;; 
;; 2009-05-01  Cecil Curry  <http://raiazome.com>
;;   * Created.

;;; Code:
; ....................{ DEPENDENCIES                       }....................
(require 'hippie)

; `filecache' maintains our internal list of cached filenames.
(require 'filecache)

; ....................{ VARIABLES                         }....................
(defvar hippie-file-cache/added-load-path-flag nil
  "Non-nil means elisp files on the `load-path' have already been cached.")

(defvar hippie-file-cache/path-cached-hash (make-hash-table :test 'equal)
  "Hash table recording paths whose files have already been cached. Each entry
of this table is a cons cell (PATH . IS-CACHED-FLAG), when PATH is a path and
IS-CACHED-FLAG is a boolean that, when non-nil, means that PATH has already
been cached.")

; ....................{ LIFECYCLE                         }....................
(defun hippie-file-cache/load ()
  "Load `hippie-file-cache'. Specifically, cache:

* All recently used files, as recorded by `recentf-mode'.
* All files under '~/bin', if that path exists."
  ; Maintain a sufficiently large list of recently-opened filenames, since
  ; `hippie-file-cache' uses that list as the basis of its `file-cache'.
  (clrhash hippie-file-cache/path-cached-hash)
  (file-cache-clear-cache)
  (file-cache-add-file-list recentf-list)
  (if (file-accessible-directory-p "~/bin") (file-cache-add-directory "~/bin/"))
  ; When closing filename-associated buffers, cache that filename (so that you may
  ; return to it, later, by invoking that file-cache).
  (hippie/add-hook 'kill-buffer-hook 'hippie-file-cache/cache-buffer-file-name))

(defun hippie-file-cache/unload ()
  "Unload `hippie-file-cache'."
  )

; ....................{ COMMANDS                          }....................
;FIXME: Redefine `hippie-file-cache/cache-buffer-file-name' so as to cache both
;the buffer's file and a set of files "around" that path. What "around" means
;depends on whether that file belongs to a `hippie-project' or not. If it does,
;then we may safely recursively cache all files belonging to that project if
;that project has not already been cached or if that project has already been
;cached but this file was not found in the cache. In the latter case, we would
;remove all files for that project from the cache and re-add them. If that file
;does not belong to a project, then we must not recursively cache all files and
;paths in that file's path. (Consider "~/.emacs", for example.) We may, however,
;safely cache all files immediately in that file's path, since that operation is
;unlikely to ever be costly.
;
;Note that we should programmatically construct the regular expression of what
;files to consider from the cons cells in the `auto-mode-alist'
;variable. Research programmatic construction of regular expressions in Emacs, as
;well as which modes should be considered (as those are "programmatic") and which
;modes should be ignored. The latter could be optimized by integration with
;`hippie-modes', which should define a new function:
;`hippie-modes/map-filetype-to-major-mode'. This performs the customary
;(add-to-list 'auto-mode-alist '("\\.txt'" text-mode)) operation given those
;passed parameters, as well as registering whether that mode is programmatic or
;not. Hmmmmmm. That is not the best way, actually. We should, rather, provide a
;new function `hippie-modes/add-cast-to-mode-hook'. "Cast," in this case, is a
;verb suggesting that the call to (code-mode-init) or (text-mode-init) is
;actually a "cast" that recasts the mode on which it operates as either a
;programmatic or non-programmatic mode. This new function accepts ... Hmm.
;This function should, if a
;new variable `hippie-file-cache/auto-mode-alist' is also boundp, 

;;;###autoload
(defun hippie-file-cache/cache-load-path ()
  "Add Emacs Lisp files in all paths in the Emacs Lisp `load-path' to the
`file-cache', if `load-path' hasn't already been added (i.e., this is the
first time you've called this function this session)."
  (interactive)
  (unless hippie-file-cache/added-load-path-flag
    (message "Caching all elisp files on the `load-path'..."
    (file-cache-add-directory-list load-path (regexp-opt (list ".el" ".el.gz")))
    (setq hippie-file-cache/added-load-path-flag t))
    (message "Caching all elisp files on the `load-path'...done")))

;;;###autoload
(defun hippie-file-cache/add-path-perl (path)
  "Adds all Perl-specific files in PATH to the `file-cache', if that path has not
already been added."
  (interactive)
  (hippie-file-cache/cache-path-recursively
   path (regexp-opt '(".cgi" ".sh" ".pl" ".pm" ".pod")) "perl"))

;;;###autoload
(defun hippie-file-cache/add-path-python (path)
  "Adds all Python-specific files in PATH to the `file-cache', if that path has not
already been added."
  (interactive)
  (hippie-file-cache/cache-path-recursively
   path (regexp-opt '(".cgi" ".sh" ".py")) "python"))

;;;###autoload
(defun hippie-file-cache/add-path-ruby (path)
  "Adds all Ruby-specific files in PATH to the `file-cache', if that path has not
already been added."
  (interactive)
  (hippie-file-cache/cache-path-recursively
   path (regexp-opt '(".cgi" ".sh" ".rb")) "ruby"))

; Find files by interactively matching filenames against filenames cached by the
; filecache and narrow likely matches (filename candidates) via IDo-style text
; completions. (This mimics "Jump to File" functionality found in Eclipse, and
; other heavyweights.)
;
; A hearty thanks to Martin Nordholts for this function.
;;;###autoload
(defun hippie-file-cache/ido-find-file (file)
  "Using ido, interactively open file from file cache. First select a file,
matched using ido-switch-buffer against the contents in `file-cache-alist'. If
the file exist in more than one directory, select directory. Lastly the file is
opened."
  (interactive (list (hippie-file-cache/ido-read "File: "
                                                 (mapcar
                                                  (lambda (x)
                                                    (car x))
                                                  file-cache-alist))))
  (let* ((record (assoc file file-cache-alist)))
    (find-file
     (expand-file-name
      file
      (if (= (length record) 2)
          (car (cdr record))
        (hippie-file-cache/ido-read
         (format "Find %s in dir: " file) (cdr record)))))))

;;;###autoload
(defun hippie-file-cache/iswitchb-find-file ()
  "Using iswitchb, interactively open file from file cache'.
First select a file, matched using iswitchb against the contents
in `file-cache-alist'. If the file exist in more than one
directory, select directory. Lastly the file is opened."
  (interactive)
  (let* ((file (hippie-file-cache/iswitchb-read "File: "
                                                (mapcar
                                                 (lambda (x)
                                                   (car x))
                                                 file-cache-alist)))
         (record (assoc file file-cache-alist)))
    (find-file
     (concat
      (if (= (length record) 2)
          (car (cdr record))
        (hippie-file-cache/iswitchb-read 
         (format "Find %s in dir: " file) (cdr record))) file))))

; ....................{ FUNCTIONS                         }....................
(defun hippie-file-cache/cache-path-recursively
  (path &optional regexp path-type)
  "Adds all files in PATH to the `file-cache', if that path has not already been
added.

When the optional argument REGEXP is passed, this function adds only those files
matching that regular expression. All others are, suitably, ignored.

When the optional argument PATH-TYPE is passed, this function prints an
explanatory, echo area message before adding those files."
  (when (not (gethash path hippie-file-cache/path-cached-hash))
    (when path-type
         (message
          (format
           "hippie-file-cache: recursively caching filenames... [%s: %s]"
           path-type
           path)))
    (file-cache-cache-path-recursively path regexp)
    (puthash path t hippie-file-cache/path-cached-hash)))

(defun hippie-file-cache/cache-buffer-file-name ()
  "Adds the filename associated with the current buffer to the `file-cache', if
that buffer is associated with a filename."
  (condition-case error-description
    (and buffer-file-name
         (file-exists-p buffer-file-name)
         (file-cache-add-file buffer-file-name))
    (error (message "%s" (error-message-string error-description)))))

(defun hippie-file-cache/ido-read (prompt choices)
  "`hippie-file-cache' calls this function, internally."
  (let ((ido-make-buffer-list-hook
         (lambda () (setq ido-temp-list choices))))
    (ido-read-buffer prompt)))

(defun hippie-file-cache/iswitchb-read (prompt choices)
  "`hippie-file-cache' calls this function, internally."
  (let ((iswitchb-make-buflist-hook
         (lambda () (setq iswitchb-temp-buflist choices))))
    (iswitchb-read-buffer prompt)))

; --------------------( COPYRIGHT AND LICENSE              )--------------------
; The information below applies to everything in this distribution,
; except where noted.
; 
; Copyleft 2009 by Cecil Curry <http://hippie.raiazome.com>.
; 
; 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 3 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, see L<http://www.gnu.org/licenses/>.
;
; --------------------( LIBRARY                            )--------------------
(unless (featurep 'hippie-file-cache) (hippie-file-cache/load))

(defvar hippie-unload-hook (lambda () (hippie-file-cache/unload) nil)
  "Call `hippie-file-cache/unload' on unload of this library.")

(provide 'hippie-file-cache)

;;; hippie-ediff.el ends here
