;;; find-file-in-project.el --- Find files in a project quickly, on any OS
|
|
|
|
;; Copyright (C) 2006-2009, 2011-2012, 2015
|
|
;; Phil Hagelberg, Doug Alcorn, and Will Farrington
|
|
;;
|
|
;; Version: 4.5
|
|
;; Package-Version: 20151216.1850
|
|
;; Author: Phil Hagelberg, Doug Alcorn, and Will Farrington
|
|
;; Maintainer: Chen Bin <chenbin.sh@gmail.com>
|
|
;; URL: https://github.com/technomancy/find-file-in-project
|
|
;; Package-Requires: ((swiper "0.7.0") (emacs "24.3"))
|
|
;; Created: 2008-03-18
|
|
;; Keywords: project, convenience
|
|
;; EmacsWiki: FindFileInProject
|
|
|
|
;; This file is NOT part of GNU Emacs.
|
|
|
|
;;; License:
|
|
|
|
;; 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, 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 GNU Emacs; see the file COPYING. If not, write to the
|
|
;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
;; Boston, MA 02110-1301, USA.
|
|
|
|
;;; Commentary:
|
|
|
|
;; This program provides a couple methods for quickly finding any file
|
|
;; in a given project. It depends on GNU find.
|
|
;;
|
|
;; Usage,
|
|
;; - `M-x find-file-in-project-by-selected' use the selected region
|
|
;; as the keyword to search file. Or you need provide the keyword
|
|
;; if no region selected.
|
|
;; - `M-x find-directory-in-project-by-selected' use the select region
|
|
;; to find directory. Or you need provide the keyword if no region
|
|
;; selected.
|
|
;; - `M-x find-file-in-project' will start search file immediately
|
|
;; - `M-x ffip-create-project-file' create .dir-locals.el
|
|
;;
|
|
;; A project is found by searching up the directory tree until a file
|
|
;; is found that matches `ffip-project-file'.
|
|
;; You can set `ffip-project-root-function' to provide an alternate
|
|
;; function to search for the project root. By default, it looks only
|
|
;; for files whose names match `ffip-patterns',
|
|
|
|
;; If you have so many files that it becomes unwieldy, you can set
|
|
;; `ffip-find-options' to a string which will be passed to the `find'
|
|
;; invocation in order to exclude irrelevant subdirectories/files.
|
|
;; For instance, in a Ruby on Rails project, you are interested in all
|
|
;; .rb files that don't exist in the "vendor" directory. In that case
|
|
;; you could set `ffip-find-options' to "-not -regex \".*vendor.*\"".
|
|
|
|
;; The variable `ffip-filename-rules' create some extra file names for
|
|
;; search when calling `find-file-in-project-by-selected'. For example,
|
|
;; When file basename `helloWorld' provided, `HelloWorld', `hello-world'
|
|
;; are added as the file name search patterns.
|
|
;; `C-h v ffip-filename-rules' to see its default value.
|
|
|
|
;; All these variables may be overridden on a per-directory basis in
|
|
;; your .dir-locals.el. See (info "(Emacs) Directory Variables") for
|
|
;; details.
|
|
|
|
;; ivy-mode is used for filter/search UI
|
|
;; In ivy-mode, SPACE is translated to regex ".*".
|
|
;; For example, the search string "dec fun pro" is transformed into
|
|
;; a regex "\\(dec\\).*\\(fun\\).*\\(pro\\)"
|
|
;; `C-h i g (ivy)' for more key-binding tips.
|
|
;;
|
|
;; You switch to ido-mode by `(setq ffip-prefer-ido-mode t)'
|
|
|
|
;; GNU Find can be installed,
|
|
;; - through `brew' on OS X
|
|
;; - through `cygwin' on Windows.
|
|
;;
|
|
;; This program works on Windows/Cygwin/Linux/Mac Emacs.
|
|
;;
|
|
;; Windows setup is as easy as installing Cygwin into default directory on
|
|
;; ANY driver. That's all.
|
|
;;
|
|
;; See https://github.com/technomancy/find-file-in-project for advanced tips
|
|
|
|
;; Recommended binding: (global-set-key (kbd "C-x f") 'find-file-in-project)
|
|
|
|
;;; Code:
|
|
|
|
(require 'ivy)
|
|
|
|
(defvar ffip-filename-rules
|
|
'(ffip-filename-identity
|
|
(ffip-filename-dashes-to-camelcase ffip-filename-camelcase-to-dashes)))
|
|
|
|
(defvar ffip-find-executable nil "Path of GNU find. If nil, we will find `find' path automatically.")
|
|
|
|
(defvar ffip-project-file '(".svn" ".git" ".hg")
|
|
"The file that should be used to define a project root.
|
|
May be set using .dir-locals.el. Checks each entry if set to a list.")
|
|
|
|
(defvar ffip-prefer-ido-mode nil
|
|
"Use `ido-mode' instead of `ivy-mode' for displaying candidates.")
|
|
|
|
(defvar ffip-patterns nil
|
|
"List of patterns to look for with `find-file-in-project'.")
|
|
|
|
(defvar ffip-match-path-instead-of-filename nil
|
|
"Match full path instead of file name when calling `find-file-in-project-by-selected'")
|
|
|
|
(defvar ffip-prune-patterns
|
|
'(;; VCS
|
|
"*/.git/*"
|
|
"*/.svn/*"
|
|
"*/.cvs/*"
|
|
"*/.bzr/*"
|
|
"*/.hg/*"
|
|
;; project misc
|
|
"*.log"
|
|
"*/bin/*"
|
|
;; Mac
|
|
"*/.DS_Store/*"
|
|
;; Ctags
|
|
"*/tags"
|
|
"*/TAGS"
|
|
;; Global/Cscope
|
|
"*/GTAGS"
|
|
"*/GPATH"
|
|
"*/GRTAGS"
|
|
"*/cscope.files"
|
|
;; html/javascript/css
|
|
"*/.npm/*"
|
|
"*/.idea/*"
|
|
"*min.js"
|
|
"*min.css"
|
|
"*/node_modules/*"
|
|
"*/bower_components/*"
|
|
;; Images
|
|
"*.png"
|
|
"*.jpg"
|
|
"*.jpeg"
|
|
"*.gif"
|
|
"*.bmp"
|
|
"*.tiff"
|
|
;; documents
|
|
"*.doc"
|
|
"*.docx"
|
|
"*.pdf"
|
|
;; C/C++
|
|
"*.obj"
|
|
"*.o"
|
|
"*.a"
|
|
"*.dylib"
|
|
"*.lib"
|
|
"*.d"
|
|
"*.dll"
|
|
"*.exe"
|
|
;; Java
|
|
"*/.metadata*"
|
|
"*/.gradle/*"
|
|
"*.class"
|
|
"*.war"
|
|
"*.jar"
|
|
;; Emacs/Vim
|
|
"*flymake"
|
|
"*/#*#"
|
|
".#*"
|
|
"*.swp"
|
|
"*~"
|
|
"*.elc"
|
|
"*/.cask/*"
|
|
;; Python
|
|
"*.pyc")
|
|
"List of directory/file patterns to not descend into when listing files in `find-file-in-project'.")
|
|
|
|
(defvar ffip-find-options ""
|
|
"Extra options to pass to `find' when using `find-file-in-project'.
|
|
|
|
Use this to exclude portions of your project: \"-not -regex \\\".*svn.*\\\"\".")
|
|
|
|
(defvar ffip-project-root nil
|
|
"If non-nil, overrides the project root directory location.")
|
|
|
|
(defvar ffip-project-root-function nil
|
|
"If non-nil, this function is called to determine the project root.
|
|
|
|
This overrides variable `ffip-project-root' when set.")
|
|
|
|
(defvar ffip-full-paths t
|
|
"If non-nil, show fully project-relative paths.")
|
|
|
|
(defvar ffip-debug nil "Print debug information.")
|
|
|
|
;;;###autoload
|
|
(defun ffip-project-root ()
|
|
"Return the root of the project."
|
|
(let ((project-root (or ffip-project-root
|
|
(if (functionp ffip-project-root-function)
|
|
(funcall ffip-project-root-function)
|
|
(if (listp ffip-project-file)
|
|
(cl-some (apply-partially 'locate-dominating-file
|
|
default-directory)
|
|
ffip-project-file)
|
|
(locate-dominating-file default-directory
|
|
ffip-project-file))))))
|
|
(or project-root
|
|
(progn (message "No project was defined for the current file.")
|
|
nil))))
|
|
|
|
(defun ffip--find-rule-to-execute (keyword f)
|
|
"If F is a function, return it.
|
|
|
|
If F is a list, assume each element is a function.
|
|
Run each element with keyword as 1st parameter as KEYWORD and 2nd parameter as t.
|
|
If the result is true, return the function."
|
|
(let (rlt found fn)
|
|
(cond
|
|
((functionp f) (setq rlt f))
|
|
|
|
((listp f)
|
|
(while (and f (not found))
|
|
(setq fn (car f))
|
|
(if (funcall fn keyword t)
|
|
(setq found t)
|
|
(setq f (cdr f))))
|
|
(setq rlt (if found fn 'identity)))
|
|
|
|
(t (setq rlt 'identity)))
|
|
|
|
rlt))
|
|
|
|
|
|
;;;###autoload
|
|
(defun ffip-filename-identity (keyword)
|
|
"Return identical KEYWORD."
|
|
keyword)
|
|
|
|
;;;###autoload
|
|
(defun ffip-filename-camelcase-to-dashes (keyword &optional check-only)
|
|
"Convert KEYWORD from camel cased to dash seperated.
|
|
If CHECK-ONLY is true, only do the check."
|
|
(let (rlt
|
|
(old-flag case-fold-search))
|
|
(cond
|
|
(check-only
|
|
(setq rlt (string-match "^[a-z0-9]+[A-Z][A-Za-z0-9]+$" keyword))
|
|
(if ffip-debug (message "ffip-filename-camelcase-to-dashes called. check-only keyword=%s rlt=%s" keyword rlt)))
|
|
(t
|
|
(setq case-fold-search nil)
|
|
;; case sensitive replace
|
|
(setq rlt (downcase (replace-regexp-in-string "\\([a-z]\\)\\([A-Z]\\)" "\\1-\\2" keyword)))
|
|
(setq case-fold-search old-flag)
|
|
|
|
(if (string= rlt (downcase keyword)) (setq rlt nil))
|
|
|
|
(if (and rlt ffip-debug)
|
|
(message "ffip-filename-camelcase-to-dashes called. rlt=%s" rlt))))
|
|
rlt))
|
|
|
|
;;;###autoload
|
|
(defun ffip-filename-dashes-to-camelcase (keyword &optional check-only)
|
|
"Convert KEYWORD from dash seperated to camel cased.
|
|
If CHECK-ONLY is true, only do the check."
|
|
(let (rlt)
|
|
(cond
|
|
(check-only
|
|
(setq rlt (string-match "^[A-Za-z0-9]+\\(-[A-Za-z0-9]+\\)+$" keyword))
|
|
(if ffip-debug (message "ffip-filename-dashes-to-camelcase called. check-only keyword=%s rlt=%s" keyword rlt)))
|
|
(t
|
|
(setq rlt (mapconcat (lambda (s) (capitalize s)) (split-string keyword "-") ""))
|
|
|
|
(let ((first-char (substring rlt 0 1)))
|
|
(setq rlt (concat "[" first-char (downcase first-char) "]" (substring rlt 1))))
|
|
(if (and rlt ffip-debug)
|
|
(message "ffip-filename-dashes-to-camelcase called. rlt=%s" rlt))))
|
|
rlt))
|
|
|
|
(defun ffip--create-filename-pattern-for-gnufind (keyword)
|
|
(let ((rlt ""))
|
|
(cond
|
|
((not keyword)
|
|
(setq rlt ""))
|
|
((not ffip-filename-rules)
|
|
(setq rlt (concat (if ffip-match-path-instead-of-filename "-iwholename" "-name")
|
|
" \"*"
|
|
keyword
|
|
"*\"" )))
|
|
(t
|
|
(dolist (f ffip-filename-rules rlt)
|
|
(let (tmp fn)
|
|
(setq fn (ffip--find-rule-to-execute keyword f))
|
|
(setq tmp (funcall fn keyword))
|
|
(when tmp
|
|
(setq rlt (concat rlt (unless (string= rlt "") " -o")
|
|
" "
|
|
(if ffip-match-path-instead-of-filename "-iwholename" "-name")
|
|
" \"*"
|
|
tmp
|
|
"*\"")))))
|
|
(unless (string= "" rlt)
|
|
(setq rlt (concat "\\(" rlt " \\)")))
|
|
))
|
|
(if ffip-debug (message "ffip--create-filename-pattern-for-gnufind called. rlt=%s" rlt))
|
|
rlt))
|
|
|
|
(defun ffip--guess-gnu-find ()
|
|
(let ((rlt "find"))
|
|
(if (eq system-type 'windows-nt)
|
|
(cond
|
|
((executable-find "c:\\\\cygwin64\\\\bin\\\\find")
|
|
(setq rlt "c:\\\\cygwin64\\\\bin\\\\find"))
|
|
((executable-find "d:\\\\cygwin64\\\\bin\\\\find")
|
|
(setq rlt "d:\\\\cygwin64\\\\bin\\\\find"))
|
|
((executable-find "e:\\\\cygwin64\\\\bin\\\\find")
|
|
(setq rlt "e:\\\\cygwin64\\\\bin\\\\find"))
|
|
((executable-find "c:\\\\cygwin\\\\bin\\\\find")
|
|
(setq rlt "c:\\\\cygwin\\\\bin\\\\find"))
|
|
((executable-find "d:\\\\cygwin\\\\bin\\\\find")
|
|
(setq rlt "d:\\\\cygwin\\\\bin\\\\find"))
|
|
((executable-find "e:\\\\cygwin\\\\bin\\\\find")
|
|
(setq rlt "e:\\\\cygwin\\\\bin\\\\find"))))
|
|
rlt))
|
|
|
|
(defun ffip--join-patterns (patterns)
|
|
"Turn `ffip-patterns' into a string that `find' can use."
|
|
(if ffip-patterns
|
|
(format "\\( %s \\)" (mapconcat (lambda (pat) (format "-iwholename \"%s\"" pat))
|
|
patterns " -or "))
|
|
""))
|
|
|
|
(defun ffip--prune-patterns ()
|
|
"Turn `ffip-prune-patterns' into a string that `find' can use."
|
|
(mapconcat (lambda (pat) (format "-iwholename \"%s\"" pat))
|
|
ffip-prune-patterns " -or "))
|
|
|
|
(defun ffip-completing-read (prompt collection action)
|
|
(cond
|
|
((= 1 (length collection))
|
|
;; open file directly
|
|
(funcall action (car collection)))
|
|
;; support ido-mode
|
|
((and ffip-prefer-ido-mode (boundp 'ido-mode) ido-mode)
|
|
(funcall action (ido-completing-read prompt collection)))
|
|
(t
|
|
(ivy-read prompt collection
|
|
:action action))))
|
|
|
|
(defun ffip-project-search (keyword find-directory)
|
|
"Return an alist of all filenames in the project and their path.
|
|
|
|
Files with duplicate filenames are suffixed with the name of the
|
|
directory they are found in so that they are unique."
|
|
(let (rlt
|
|
cmd
|
|
(old-default-directory default-directory)
|
|
(file-alist nil)
|
|
(root (expand-file-name (or ffip-project-root (ffip-project-root)
|
|
(error "No project root found")))))
|
|
(cd (file-name-as-directory root))
|
|
;; make the prune pattern more general
|
|
(setq cmd (format "%s . \\( %s \\) -prune -o -type %s %s %s %s -print"
|
|
(if ffip-find-executable ffip-find-executable (ffip--guess-gnu-find))
|
|
(ffip--prune-patterns)
|
|
(if find-directory "d" "f")
|
|
(ffip--join-patterns ffip-patterns)
|
|
;; When finding directory, the keyword is like:
|
|
;; "proj/hello/world"
|
|
(if find-directory (format "-iwholename \"*%s\"" keyword)
|
|
(ffip--create-filename-pattern-for-gnufind keyword))
|
|
ffip-find-options))
|
|
|
|
(if ffip-debug (message "run cmd at %s: %s" default-directory cmd))
|
|
(setq rlt
|
|
(mapcar (lambda (file)
|
|
(if ffip-full-paths
|
|
(cons (replace-regexp-in-string "^\./" "" file)
|
|
(expand-file-name file))
|
|
(let ((file-cons (cons (file-name-nondirectory file)
|
|
(expand-file-name file))))
|
|
(add-to-list 'file-alist file-cons)
|
|
file-cons)))
|
|
;; #15 improving handling of directories containing space
|
|
(split-string (shell-command-to-string cmd) "[\r\n]+" t)))
|
|
|
|
;; restore the original default-directory
|
|
(cd old-default-directory)
|
|
rlt))
|
|
|
|
(defun ffip-find-files (keyword open-another-window &optional find-directory)
|
|
(let* ((project-files (ffip-project-search keyword find-directory))
|
|
(files (mapcar 'car project-files))
|
|
file root)
|
|
(if (> (length files) 0)
|
|
(progn
|
|
(setq root (file-name-nondirectory
|
|
(directory-file-name
|
|
(or ffip-project-root (ffip-project-root)))))
|
|
(ffip-completing-read
|
|
(format "Find in %s/: " root)
|
|
files
|
|
(lambda (file)
|
|
(let ((rlt (cdr (assoc file project-files))))
|
|
(if find-directory
|
|
;; open dired because this rlt is a directory
|
|
(if open-another-window
|
|
(dired-other-window rlt)
|
|
(switch-to-buffer (dired rlt)))
|
|
(if open-another-window
|
|
(find-file-other-window rlt)
|
|
(find-file rlt)))))))
|
|
(message "Nothing found!"))))
|
|
|
|
;;;###autoload
|
|
(defun ffip-create-project-file ()
|
|
"Create .dir-locals.el to setup find-file-in-project per directory.
|
|
Modify and place .dir-locals.el to your project root.
|
|
See (info \"(Emacs) Directory Variables\") for details."
|
|
(interactive)
|
|
(let ((file (concat (file-name-as-directory default-directory) ".dir-locals.el")))
|
|
(with-temp-file file
|
|
(insert (concat ";; Generated by find-file-in-project.\n"
|
|
"((nil . ((ffip-project-root . \"" default-directory "\")\n"
|
|
";; (ffip-find-options . \"-not -size +64k\")\n"
|
|
";; (ffip-patterns . (\"*.html\" \"*.js\" \"*.css\" \"*.java\" \"*.el\" \"*.js\"))\n"
|
|
";; (ffip-prune-patterns . (\"*/.git/*\" \"*/node_modules/*\" \"*/index.js\"))\n"
|
|
" )))")))
|
|
(message "%s created." file)))
|
|
|
|
;;;###autoload
|
|
(defun ffip-current-full-filename-match-pattern-p (regex)
|
|
"Is current full file name (including directory) match the REGEX?"
|
|
(let ((dir (if (buffer-file-name) (buffer-file-name) "")))
|
|
(string-match-p regex dir)))
|
|
|
|
;;;###autoload
|
|
(defun find-file-in-project (&optional open-another-window)
|
|
"Prompt with a completing list of all files in the project to find one.
|
|
|
|
If OPEN-ANOTHER-WINDOW is not nil, the file will be opened in new window.
|
|
|
|
The project's scope is defined as the first directory containing
|
|
a `ffip-project-file' (It's value is \".git\" by default.
|
|
|
|
You can override this by setting the variable `ffip-project-root'."
|
|
|
|
(interactive "P")
|
|
(ffip-find-files nil open-another-window))
|
|
|
|
;;;###autoload
|
|
(defun ffip-get-project-root-directory ()
|
|
"Get the full path of project root directory."
|
|
(expand-file-name (or ffip-project-root
|
|
(ffip-project-root))))
|
|
|
|
;;;###autoload
|
|
(defun find-file-in-project-by-selected (&optional open-another-window)
|
|
"Similar to `find-file-in-project'.
|
|
But use string from selected region to search files in the project.
|
|
If no region is selected, you need provide keyword.
|
|
|
|
Keyword could be ANY part of the file's full path and support wildcard.
|
|
For example, to find /home/john/proj1/test.js, below keywords are valid:
|
|
- test.js
|
|
- roj1/tes
|
|
- john*test
|
|
|
|
If OPEN-ANOTHER-WINDOW is not nil, the file will be opened in new window."
|
|
(interactive "P")
|
|
(let ((keyword (if (region-active-p)
|
|
(buffer-substring-no-properties (region-beginning) (region-end))
|
|
(read-string "Enter keyword:"))))
|
|
(ffip-find-files keyword open-another-window)))
|
|
|
|
;;;###autoload
|
|
(defun find-directory-in-project-by-selected (&optional open-another-window)
|
|
"Similar to `find-file-in-project-by-selected'.
|
|
Use string from selected region to find directory in the project.
|
|
If no region is selected, you need provide keyword.
|
|
|
|
Keyword could be directory's base-name only or parent-directoy+base-name
|
|
For example, to find /home/john/proj1/test, below keywords are valid:
|
|
- test
|
|
- roj1/test
|
|
- john*test
|
|
|
|
If OPEN-ANOTHER-WINDOW is not nil, the file will be opened in new window."
|
|
(interactive "P")
|
|
(let ((keyword (if (region-active-p)
|
|
(buffer-substring-no-properties (region-beginning) (region-end))
|
|
(read-string "Enter keyword:"))))
|
|
(ffip-find-files keyword open-another-window t)))
|
|
|
|
;;;###autoload
|
|
(defalias 'ffip 'find-file-in-project)
|
|
|
|
;; safe locals
|
|
(progn
|
|
(put 'ffip-patterns 'safe-local-variable 'listp)
|
|
(put 'ffip-prune-patterns 'safe-local-variable 'listp)
|
|
(put 'ffip-filename-rules 'safe-local-variable 'listp)
|
|
(put 'ffip-match-path-instead-of-filename 'safe-local-variable 'booleanp)
|
|
(put 'ffip-project-file 'safe-local-variable 'stringp)
|
|
(put 'ffip-project-root 'safe-local-variable 'stringp))
|
|
|
|
(provide 'find-file-in-project)
|
|
;;; find-file-in-project.el ends here
|