Klimi's new dotfiles with stow.
Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.

1267 righe
47 KiB

  1. ;;; find-file-in-project.el --- Find file/directory and review Diff/Patch/Commit efficiently everywhere
  2. ;; Copyright (C) 2006-2009, 2011-2012, 2015-2018
  3. ;; Phil Hagelberg, Doug Alcorn, Will Farrington, Chen Bin
  4. ;;
  5. ;; Version: 5.7.7
  6. ;; Package-Version: 20190720.313
  7. ;; Author: Phil Hagelberg, Doug Alcorn, and Will Farrington
  8. ;; Maintainer: Chen Bin <chenbin.sh@gmail.com>
  9. ;; URL: https://github.com/technomancy/find-file-in-project
  10. ;; Package-Requires: ((ivy "0.10.0") (emacs "24.4"))
  11. ;; Created: 2008-03-18
  12. ;; Keywords: project, convenience
  13. ;; EmacsWiki: FindFileInProject
  14. ;; This file is NOT part of GNU Emacs.
  15. ;;; License:
  16. ;; This program is free software; you can redistribute it and/or modify
  17. ;; it under the terms of the GNU General Public License as published by
  18. ;; the Free Software Foundation; either version 3, or (at your option)
  19. ;; any later version.
  20. ;;
  21. ;; This program is distributed in the hope that it will be useful,
  22. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  23. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  24. ;; GNU General Public License for more details.
  25. ;;
  26. ;; You should have received a copy of the GNU General Public License
  27. ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
  28. ;;; Commentary:
  29. ;; This program provides methods to find file in project.
  30. ;; - Only dependency is BSD/GNU find
  31. ;; - Works on Windows with minimum setup
  32. ;; - Works on Tramp Mode (https://www.emacswiki.org/emacs/TrampMode)
  33. ;; - fd (faster alternative of find, see https://github.com/sharkdp/fd) is supported
  34. ;;
  35. ;; Usage,
  36. ;; - You can insert "(setq ffip-use-rust-fd t)" into ".emacs" to use fd (alternative of find)
  37. ;; - `find-file-in-project-at-point' guess the file path at point and
  38. ;; find file
  39. ;; - `find-file-in-project-by-selected' uses the selected region
  40. ;; as the keyword to search file. You can provide the keyword
  41. ;; if no region is selected.
  42. ;; - `find-directory-in-project-by-selected' uses the select region
  43. ;; to find directory. You can provide the keyword if no region
  44. ;; is selected.
  45. ;; - `find-file-in-project' will start search file immediately
  46. ;; - `ffip-create-project-file' creates ".dir-locals.el"
  47. ;;
  48. ;; A project is found by searching up the directory tree until a file
  49. ;; is found that matches `ffip-project-file'.
  50. ;; You can set `ffip-project-root-function' to provide an alternate
  51. ;; function to search for the project root. By default, it looks only
  52. ;; for files whose names match `ffip-patterns',
  53. ;; If you have so many files that it becomes unwieldy, you can set
  54. ;; `ffip-find-options' to a string which will be passed to the `find'
  55. ;; invocation in order to exclude irrelevant subdirectories/files.
  56. ;; For instance, in a Ruby on Rails project, you are interested in all
  57. ;; .rb files that don't exist in the "vendor" directory. In that case
  58. ;; you could set `ffip-find-options' to "-not -regex \".*vendor.*\"".
  59. ;; `ffip-insert-file' insert file content into current buffer.
  60. ;; `find-file-with-similar-name' find file with similar name to current
  61. ;; opened file. The regular expression `ffip-strip-file-name-regex' is
  62. ;; also used by `find-file-with-similar-name'.
  63. ;;
  64. ;; all these variables may be overridden on a per-directory basis in
  65. ;; your ".dir-locals.el". See (info "(Emacs) Directory Variables") for
  66. ;; details.
  67. ;;
  68. ;; Sample ".dir-locals.el",
  69. ;;
  70. ;; ((nil . ((ffip-project-root . "~/projs/PROJECT_DIR")
  71. ;; ;; ignore files bigger than 64k and directory "dist/" when searching
  72. ;; (ffip-find-options . "-not -size +64k -not -iwholename '*/dist/*'")
  73. ;; ;; only search files with following extensions
  74. ;; (ffip-patterns . ("*.html" "*.js" "*.css" "*.java" "*.xml" "*.js"))
  75. ;; (eval . (progn
  76. ;; (require 'find-file-in-project)
  77. ;; ;; ignore directory ".tox/" when searching
  78. ;; (setq ffip-prune-patterns `("*/.tox" ,@ffip-prune-patterns))
  79. ;; ignore BMP image file
  80. ;; (setq ffip-ignore-filenames `("*.bmp" ,@ffip-ignore-filenames))
  81. ;; ;; Do NOT ignore directory "bin/" when searching
  82. ;; (setq ffip-prune-patterns `(delete "*/bin" ,@ffip-prune-patterns))))
  83. ;; )))
  84. ;;
  85. ;; To find in current directory, use `find-file-in-current-directory'
  86. ;; and `find-file-in-current-directory-by-selected'.
  87. ;;
  88. ;; `ffip-split-window-horizontally' and `ffip-split-window-vertically' find&open file
  89. ;; in split window.
  90. ;; `ffip-show-diff' execute the backend from `ffip-diff-backends'.
  91. ;; The output is in Unified Diff Format and inserted into *ffip-diff* buffer.
  92. ;; Press "o" or "C-c C-c" or "ENTER" or `M-x ffip-diff-find-file' in the
  93. ;; buffer to open corresponding file.
  94. ;;
  95. ;; `ffip-diff-find-file-before-hook' is called before `ffip-diff-find-file'.
  96. ;;
  97. ;; `ffip-diff-apply-hunk' applies current hunk in `diff-mode' (please note
  98. ;; `ffip-diff-mode' inherits from `diff-mode') to the target.
  99. ;; file. The target file could be located by searching `recentf-list'.
  100. ;; Except this extra feature, `ffip-diff-apply-hunk' is same as `diff-apply-hunk'.
  101. ;; So `diff-apply-hunk' can be replaced by `ffip-diff-apply-hunk'.
  102. ;; If you use `evil-mode', insert below code into ~/.emacs,
  103. ;; (defun ffip-diff-mode-hook-setup ()
  104. ;; (evil-local-set-key 'normal "K" 'diff-hunk-prev)
  105. ;; (evil-local-set-key 'normal "J" 'diff-hunk-next)
  106. ;; (evil-local-set-key 'normal "P" 'diff-file-prev)
  107. ;; (evil-local-set-key 'normal "N" 'diff-file-next)
  108. ;; (evil-local-set-key 'normal (kbd "RET") 'ffip-diff-find-file)
  109. ;; (evil-local-set-key 'normal "o" 'ffip-diff-find-file))
  110. ;; (add-hook 'ffip-diff-mode-hook 'ffip-diff-mode-hook-setup)
  111. ;; `find-relative-path' find file/directory and copy its relative path
  112. ;; into `kill-ring'. You can customize `ffip-find-relative-path-callback'
  113. ;; to format the relative path,
  114. ;; (setq ffip-find-relative-path-callback 'ffip-copy-reactjs-import)
  115. ;; (setq ffip-find-relative-path-callback 'ffip-copy-org-file-link)
  116. ;; `ivy-mode' is used for filter/search UI
  117. ;; In `ivy-mode', SPACE is translated to regex ".*".
  118. ;; For example, the search string "dec fun pro" is transformed into
  119. ;; regular expression "\\(dec\\).*\\(fun\\).*\\(pro\\)"
  120. ;; `C-h i g (ivy)' for more key-binding tips.
  121. ;;
  122. ;; `ffip-save-ivy-last' saves the most recent search result.
  123. ;; `ffip-ivy-resume' re-use the save result. Both requires `ivy-mode'
  124. ;; installed.
  125. ;;
  126. ;; You can switch to `ido-mode' by `(setq ffip-prefer-ido-mode t)'
  127. ;; BSD/GNU Find can be installed through Cygwin or MYSYS2 on Windows.
  128. ;; Executable is automatically detected. But you can manually specify
  129. ;; the executable location by insert below code into ".emacs",
  130. ;;
  131. ;; (if (eq system-type 'windows-nt)
  132. ;; (setq ffip-find-executable "c:\\\\cygwin64\\\\bin\\\\find"))
  133. ;;
  134. ;; This program works on Windows/Cygwin/Linux/macOS
  135. ;;
  136. ;; See https://github.com/technomancy/find-file-in-project for advanced tips.
  137. ;;; Code:
  138. (require 'diff-mode)
  139. (require 'windmove)
  140. (require 'subr-x)
  141. (defvar ffip-use-rust-fd nil "Use use fd instead of find.")
  142. (defvar ffip-rust-fd-respect-ignore-files t
  143. "Don 't show search results from '.*ignore' files")
  144. (defvar ffip-rust-fd-extra-opts ""
  145. "rust fd extra options passed to cli.")
  146. (defvar ffip-window-ratio-alist
  147. '((1 . 1.61803398875)
  148. (2 . 2)
  149. (3 . 3)
  150. (4 . 4)
  151. (5 . 0.61803398875))
  152. "Dictionary to look up windows split ratio.
  153. Used by `ffip-split-window-horizontally' and `ffip-split-window-vertically'.")
  154. (defvar ffip-filename-history nil)
  155. (defvar ffip-strip-file-name-regex
  156. "\\(\\.mock\\|\\.test\\|\\.mockup\\)"
  157. "Strip file name to get minimum keyword with this regex.
  158. It's used by `find-file-with-similar-name'.")
  159. (defvar ffip-split-window-without-asking-for-keyword nil
  160. "`ffip-split-window-horizontally' or `ffip-split-window-vertically' don't ask keyword.")
  161. (defvar ffip-diff-find-file-before-hook nil
  162. "Hook before `ffip-diff-find-file' move focus out of *ffip-diff* buffer.")
  163. (defvar ffip-read-file-name-hijacked-p nil
  164. "Internal flag used by `ffip-diff-apply-hunk'.")
  165. (defvar ffip-diff-apply-hunk-hook nil
  166. "Hook when `ffip-diff-apply-hunk' find the file to apply hunk.
  167. The file path is passed to the hook as the first argument.")
  168. (defvar ffip-relative-path-pattern "^\\(\\.\\.*/\\)+"
  169. "Pattern of relative path.")
  170. (defun ffip-shell-command-to-string (command)
  171. "Execute shell COMMAND and return its output as a string."
  172. (with-output-to-string
  173. (with-current-buffer
  174. standard-output
  175. (shell-command command t))))
  176. (defun ffip-nonempty-lines (s)
  177. "Return non empty lines."
  178. (split-string s "[\r\n]+" t))
  179. (defun ffip-diff-git-versions ()
  180. "List all versions of code under Git."
  181. (let* ((git-cmd (concat "git --no-pager log --date=short --pretty=format:'%h|%ad|%s|%an' "
  182. buffer-file-name)))
  183. (nconc (ffip-nonempty-lines (shell-command-to-string "git branch --no-color --all"))
  184. (ffip-nonempty-lines (shell-command-to-string git-cmd)))))
  185. ;;;###autoload
  186. (defun ffip-git-diff-current-file ()
  187. "Run 'git diff version:current-file current-file'."
  188. (let* ((default-directory (locate-dominating-file default-directory ".git"))
  189. (line (ivy-read "diff current file:" (ffip-diff-git-versions))))
  190. (ffip-shell-command-to-string (format "git --no-pager diff %s:%s %s"
  191. (replace-regexp-in-string "^ *\\*? *" "" (car (split-string line "|" t)))
  192. (file-relative-name buffer-file-name default-directory)
  193. buffer-file-name))))
  194. (defun ffip-git-diff-project()
  195. "Run 'git diff version' in project."
  196. (let* ((default-directory (locate-dominating-file default-directory ".git"))
  197. (line (ivy-read "diff current file:" (ffip-diff-git-versions)))
  198. (version (replace-regexp-in-string "^ *\\*? *" "" (car (split-string line "|" t)))))
  199. (ffip-shell-command-to-string (format "git --no-pager diff %s" version))))
  200. (defvar ffip-diff-backends
  201. '(ffip-git-diff-current-file
  202. ffip-git-diff-project
  203. ("`git diff HEAD^` in project" . "cd $(git rev-parse --show-toplevel) && git diff HEAD^")
  204. ("`git diff --cached` in project" . "cd $(git rev-parse --show-toplevel) && git diff --cached")
  205. ("`git diff` in project" . "cd $(git rev-parse --show-toplevel) && git diff")
  206. ("`git diff` current file" . (ffip-shell-command-to-string (format "cd $(git rev-parse --show-toplevel) && git diff '%s'"
  207. (buffer-file-name))))
  208. ("`git log -p` current file" . (ffip-shell-command-to-string (format "cd $(git rev-parse --show-toplevel) && git --no-pager log --date=short -p '%s'"
  209. (buffer-file-name))))
  210. ("`git log -S keyword -p` in project" . (ffip-shell-command-to-string (format "cd $(git rev-parse --show-toplevel) && git --no-pager log --date=short -S'%s' -p"
  211. (read-string "Git search string:"))))
  212. ("Diff from `kill-ring'" . (car kill-ring)))
  213. "The list of back-ends.
  214. If back-end is string, it's run in `shell-command-to-string'.
  215. If it's a function or expression, it'll be executed and return a string.
  216. The output is inserted into *ffip-diff* buffer.")
  217. (defvar ffip-find-executable nil "Path of GNU find. If nil we will guess.")
  218. (defvar ffip-project-file '(".svn" ".hg" ".git")
  219. "The file/directory used to locate project root.
  220. May be set using .dir-locals.el. Checks each entry if set to a list.")
  221. (defvar ffip-prefer-ido-mode (not (require 'ivy nil t))
  222. "Use ido instead of ivy to display candidates.")
  223. (defvar ffip-patterns nil
  224. "List of glob patterns to look for with `find-file-in-project'.")
  225. (defvar ffip-match-path-instead-of-filename nil
  226. "Match full path instead of file name.")
  227. ;; For "GNU/BSD Find", "*/test/*" matches "./test/" and "./dir/test/"
  228. ;;
  229. ;; But for "rust fd", only "test/*" matches "./test/" and "./dir/test/";
  230. ;; "*/test/*" won't match "./test/" but matches "./dir/test/"
  231. ;; Maybe it's bug of fd.
  232. (defvar ffip-prune-patterns
  233. '(;; VCS
  234. "*/.git"
  235. "*/.svn"
  236. "*/.cvs"
  237. "*/.tox"
  238. "*/.bzr"
  239. "*/.hg"
  240. "*/bin"
  241. "*/.DS_Store"
  242. "*/.sass-cache"
  243. "*/.npm"
  244. "*/.tmp"
  245. "*/.idea"
  246. "*/node_modules"
  247. "*/bower_components"
  248. "*/.gradle"
  249. "*/.cask")
  250. "Ignored directories(prune patterns).")
  251. (defvar ffip-ignore-filenames
  252. '(;; VCS
  253. ;; project misc
  254. "*.log"
  255. ;; Ctags
  256. "tags"
  257. "TAGS"
  258. ;; compressed
  259. "*.tgz"
  260. "*.gz"
  261. "*.xz"
  262. "*.zip"
  263. "*.tar"
  264. "*.rar"
  265. ;; Global/Cscope
  266. "GTAGS"
  267. "GPATH"
  268. "GRTAGS"
  269. "cscope.files"
  270. ;; html/javascript/css
  271. "*bundle.js"
  272. "*min.js"
  273. "*min.css"
  274. ;; Images
  275. "*.png"
  276. "*.jpg"
  277. "*.jpeg"
  278. "*.gif"
  279. "*.bmp"
  280. "*.tiff"
  281. "*.ico"
  282. ;; documents
  283. "*.doc"
  284. "*.docx"
  285. "*.xls"
  286. "*.ppt"
  287. "*.pdf"
  288. "*.odt"
  289. ;; C/C++
  290. "*.obj"
  291. "*.so"
  292. "*.o"
  293. "*.a"
  294. "*.ifso"
  295. "*.tbd"
  296. "*.dylib"
  297. "*.lib"
  298. "*.d"
  299. "*.dll"
  300. "*.exe"
  301. ;; Java
  302. ".metadata*"
  303. "*.class"
  304. "*.war"
  305. "*.jar"
  306. ;; Emacs/Vim
  307. "*flymake"
  308. "#*#"
  309. ".#*"
  310. "*.swp"
  311. "*~"
  312. "*.elc"
  313. ;; Python
  314. "*.pyc")
  315. "Ignore file names. Wildcast is supported.")
  316. (defvar ffip-find-options ""
  317. "Extra options to pass to `find' when using `find-file-in-project'.
  318. Use this to exclude portions of your project: \"-not -regex \\\".*svn.*\\\"\".")
  319. (defvar ffip-find-pre-path-options ""
  320. "Extra options to pass to `find' before path name options when using `find-file-in-project'.
  321. As required by `find', `-H', `-L', `-P', `-D' and `-O' must appear before the first path name, `.'.
  322. For example, use this to follow symbolic links inside your project: \"-L\".")
  323. (defvar ffip-project-root nil
  324. "If non-nil, overrides the project root directory location.")
  325. (defvar ffip-project-root-function nil
  326. "If non-nil, this function is called to determine the project root.
  327. This overrides variable `ffip-project-root' when set.")
  328. (defvar ffip-ivy-last-saved nil
  329. "Backup of `ivy-last'. Requires ivy.")
  330. (defvar ffip-debug nil "Print debug information.")
  331. ;;;###autoload
  332. (defun ffip-copy-without-change (p)
  333. "Copy P without change."
  334. (kill-new p)
  335. (message "%s => kill-ring" p))
  336. ;;;###autoload
  337. (defun ffip-copy-reactjs-import(p)
  338. "Create ReactJS link from P and copy the result."
  339. (setq p (format "import str from '%s';" p))
  340. (kill-new p)
  341. (message "%s => kill-ring" p))
  342. ;;;###autoload
  343. (defun ffip-copy-org-file-link(p)
  344. "Create org link from P and copy the result."
  345. (setq p (format "[[file:%s]]" p))
  346. (kill-new p)
  347. (message "%s => kill-ring" p))
  348. ;;;###autoload
  349. (defvar ffip-find-relative-path-callback 'ffip-copy-without-change
  350. "The callback after calling `find-relative-path'.")
  351. (defun ffip--some (predicate seq)
  352. "Return if PREDICATE is of true of any element of SEQ"
  353. (let* (elem rlt)
  354. (while (and (setq elem (car seq))
  355. (not rlt))
  356. (setq seq (cdr seq))
  357. (setq rlt (funcall predicate elem)))
  358. rlt))
  359. ;;;###autoload
  360. (defun ffip-project-root ()
  361. "Return the root of the project."
  362. (let* ((project-root (or ffip-project-root
  363. (cond
  364. ((functionp ffip-project-root-function)
  365. (funcall ffip-project-root-function))
  366. ((listp ffip-project-file)
  367. (ffip--some (apply-partially 'locate-dominating-file
  368. default-directory)
  369. ffip-project-file))
  370. (t
  371. (locate-dominating-file default-directory
  372. ffip-project-file))))))
  373. (or (and project-root (file-name-as-directory project-root))
  374. (progn
  375. (message "Since NO project was found, use `default-directory' instead.")
  376. default-directory))))
  377. (defun ffip--read-file-text (file)
  378. "Read text from FILE."
  379. (read (decode-coding-string
  380. (with-temp-buffer
  381. (set-buffer-multibyte nil)
  382. (setq buffer-file-coding-system 'binary)
  383. (insert-file-contents-literally file)
  384. (buffer-substring-no-properties (point-min) (point-max))) 'utf-8)))
  385. ;;;###autoload
  386. (defun ffip-save-ivy-last ()
  387. "Save `ivy-last' into `ffip-ivy-last-saved'. Requires ivy."
  388. (interactive)
  389. (if (boundp 'ivy-last)
  390. (setq ffip-ivy-last-saved ivy-last)
  391. (message "Sorry. You need install `ivy-mode' first.")))
  392. ;;;###autoload
  393. (defun ffip-get-project-root-directory ()
  394. "Get the full path of project root directory."
  395. (if ffip-project-root (file-name-as-directory ffip-project-root)
  396. (ffip-project-root)))
  397. ;;;###autoload
  398. (defun ffip-ivy-resume ()
  399. "Wrapper of `ivy-resume'. Resume the search saved at `ffip-ivy-last-saved'."
  400. (interactive)
  401. (let* ((ivy-last (if ffip-ivy-last-saved ffip-ivy-last-saved ivy-last))
  402. (default-directory (ffip-get-project-root-directory)))
  403. (if (fboundp 'ivy-resume)
  404. (ivy-resume)
  405. (message "Sorry. You need install `ivy-mode' first."))))
  406. ;;;###autoload
  407. (defun ffip-filename-identity (keyword)
  408. "Return identical KEYWORD."
  409. keyword)
  410. ;;;###autoload
  411. (defun ffip-filename-camelcase-to-dashes (keyword &optional check-only)
  412. "Convert KEYWORD from camel cased to dash separated.
  413. If CHECK-ONLY is true, only do the check."
  414. (let* (rlt)
  415. (cond
  416. (check-only
  417. (setq rlt (string-match "^[a-z0-9]+[A-Z][A-Za-z0-9]+$" keyword))
  418. (if ffip-debug (message "ffip-filename-camelcase-to-dashes called. check-only keyword=%s rlt=%s" keyword rlt)))
  419. (t
  420. (let* ((case-fold-search nil))
  421. ;; case sensitive replace
  422. (setq rlt (downcase (replace-regexp-in-string "\\([a-z]\\)\\([A-Z]\\)" "\\1-\\2" keyword))))
  423. (if (string= rlt (downcase keyword)) (setq rlt nil))
  424. (if (and rlt ffip-debug) (message "ffip-filename-camelcase-to-dashes called. rlt=%s" rlt))))
  425. rlt))
  426. ;;;###autoload
  427. (defun ffip-filename-dashes-to-camelcase (keyword &optional check-only)
  428. "Convert KEYWORD from dash separated to camel cased.
  429. If CHECK-ONLY is true, only do the check."
  430. (let* (rlt)
  431. (cond
  432. (check-only
  433. (setq rlt (string-match "^[A-Za-z0-9]+\\(-[A-Za-z0-9]+\\)+$" keyword))
  434. (if ffip-debug (message "ffip-filename-dashes-to-camelcase called. check-only keyword=%s rlt=%s" keyword rlt)))
  435. (t
  436. (setq rlt (mapconcat (lambda (s) (capitalize s)) (split-string keyword "-") ""))
  437. (let ((first-char (substring rlt 0 1)))
  438. (setq rlt (concat "[" first-char (downcase first-char) "]" (substring rlt 1))))
  439. (if (and rlt ffip-debug) (message "ffip-filename-dashes-to-camelcase called. rlt=%s" rlt))))
  440. rlt))
  441. (defun ffip--create-filename-pattern-for-gnufind (keyword)
  442. "Create search pattern from KEYWORD."
  443. (let* ((rlt ""))
  444. (cond
  445. ((not keyword)
  446. (setq rlt ""))
  447. (t
  448. (setq rlt (concat (if ffip-match-path-instead-of-filename "-iwholename" "-iname")
  449. " \"*"
  450. keyword
  451. "*\"" ))))
  452. (if ffip-debug (message "ffip--create-filename-pattern-for-gnufind called. rlt=%s" rlt))
  453. rlt))
  454. (defun ffip--win-executable-find (exe)
  455. "Find EXE on windows."
  456. (let* ((drivers '("c" "d" "e" "g" "h" "i" "j" "k"))
  457. (i 0)
  458. j
  459. (dirs '(":\\\\cygwin64\\\\bin\\\\"
  460. ":\\\\msys64\\\\usr\\\\bin\\\\"))
  461. rlt)
  462. (while (and (not rlt)
  463. (< i (length dirs)))
  464. (setq j 0)
  465. (while (and (not rlt)
  466. (< j (length drivers)))
  467. (setq rlt (executable-find (concat (nth j drivers) (nth i dirs) exe)))))
  468. (unless rlt
  469. ;; nothing found, fall back to exe
  470. (setq rlt exe))
  471. rlt))
  472. (defun ffip--executable-find ()
  473. "Find EXE on all environments."
  474. (let* ((exe (if ffip-use-rust-fd "fd" "find"))
  475. rlt)
  476. (cond
  477. ((file-remote-p default-directory)
  478. ;; In tramp mode and local windows, remote nix-like,
  479. ;; the `ffip-find-executable' with windows path can't be applied.
  480. ;; Assume remote server has already added EXE into $PATH!
  481. ;; Thanks for ShuguangSun for the fix
  482. (setq rlt exe))
  483. ((setq rlt ffip-find-executable))
  484. ((eq system-type 'windows-nt)
  485. ;; in case PATH is not setup properly
  486. (cond
  487. (ffip-use-rust-fd
  488. (setq rlt (concat (getenv "USERPROFILE")
  489. "\\\\.cargo\\\\bin\\\\"
  490. exe
  491. ".exe"))
  492. (unless (file-exists-p rlt)
  493. (setq rlt exe)))
  494. (t
  495. (setq rlt (ffip--win-executable-find exe)))))
  496. ((setq rlt (executable-find exe)))
  497. (t
  498. ;; well, `executable-find' failed
  499. (setq rlt exe)))
  500. rlt))
  501. (defun ffip--join-patterns (patterns)
  502. "Convert PATTERNS into cli arguments."
  503. (cond
  504. ((and ffip-patterns (not ffip-use-rust-fd))
  505. (format "\\( %s \\)" (mapconcat (lambda (pat) (format "-iwholename \"%s\"" pat))
  506. patterns " -or ")))
  507. (t
  508. ;; rust fd only supports ONE pattern (and it's regular expression)
  509. ;; which is precious resource to waste here
  510. "")))
  511. (defun ffip--prune-patterns ()
  512. "Turn `ffip-prune-patterns' into a string that `find' can use."
  513. ;; Both fd and find use "glob pattern"
  514. ;; @see https://en.wikipedia.org/wiki/Glob_%28programming%29
  515. (cond
  516. (ffip-use-rust-fd
  517. ;; fd match relative path
  518. (mapconcat (lambda (p)
  519. (format "-E \"%s\"" (replace-regexp-in-string "^\*/" "" p)))
  520. ffip-prune-patterns " "))
  521. (t
  522. ;; find match whole path
  523. (mapconcat (lambda (p)
  524. (format "-iwholename \"%s\"" p))
  525. ffip-prune-patterns " -or "))))
  526. (defun ffip--ignore-file-names ()
  527. "Turn `ffip-ignore-filenames' into a string that `find' can use."
  528. ;; @see `ffip-prune-patterns' for fd vs find.
  529. (cond
  530. (ffip-use-rust-fd
  531. (mapconcat (lambda (p)
  532. (format "-E \"%s\"" p))
  533. ffip-ignore-filenames " "))
  534. (t
  535. (mapconcat (lambda (n) (format "-not -name \"%s\"" n))
  536. ffip-ignore-filenames " "))))
  537. ;;;###autoload
  538. (defun ffip-completing-read (prompt collection &optional action)
  539. "Read a string in minibuffer, with completion.
  540. PROMPT is a string with same format parameters in `ido-completing-read'.
  541. COLLECTION is a list of strings.
  542. ACTION is a lambda function to call after selecting a result.
  543. This function returns the selected candidate or nil."
  544. (cond
  545. ((and action (= 1 (length collection)))
  546. ;; open file directly
  547. (funcall action (car collection))
  548. (car collection))
  549. ;; If user prefer `ido-mode' or there is no ivy,
  550. ;; use `ido-completing-read'.
  551. ((or ffip-prefer-ido-mode (not (fboundp 'ivy-read)))
  552. ;; friendly UI for ido
  553. (let* ((list-of-pair (consp (car collection)))
  554. (ido-collection (if list-of-pair
  555. (mapcar 'car collection)
  556. collection))
  557. (ido-selected (ido-completing-read prompt ido-collection)))
  558. (if (and ido-selected action)
  559. (funcall action
  560. (if list-of-pair
  561. (cdar (delq nil
  562. (mapcar (lambda (x)
  563. (and (string= (car x)
  564. ido-selected)
  565. x))
  566. collection)))
  567. ido-selected)))
  568. ido-selected))
  569. (t
  570. (ivy-read prompt collection
  571. :action action))))
  572. (defun ffip-create-shell-command (keyword is-finding-directory)
  573. "Produce command to search KEYWORD.
  574. If IS-FINDING-DIRECTORY is t, we look up directory instead of file.
  575. Rust fd use regular expression.
  576. BSD/GNU Find use glob pattern."
  577. (let* (cmd fmt tgt)
  578. (cond
  579. (ffip-use-rust-fd
  580. ;; `-H` => search hidden files
  581. ;; `-E` => exclude pattern
  582. ;; `-c` => color
  583. ;; `-i` => case insensitive
  584. ;; `-t` => directory (d) or file (f)
  585. ;; `-p` => match full path
  586. (setq fmt (concat "%s %s -c never -H -i -t %s %s %s %s"
  587. (if ffip-rust-fd-respect-ignore-files "" " -I")
  588. (if ffip-match-path-instead-of-filename " -p" "")
  589. " "
  590. ffip-rust-fd-extra-opts
  591. " %s"))
  592. ;; fd use regular expression for target pattern (but glob pattern when excluding, sigh)
  593. (setq tgt (if keyword (format "\".*%s\"" keyword) "")))
  594. (t
  595. (setq tgt
  596. (if is-finding-directory (format "-iwholename \"*%s\"" keyword)
  597. (ffip--create-filename-pattern-for-gnufind keyword)))
  598. (setq fmt (concat "%s "
  599. ffip-find-pre-path-options
  600. " . \\( %s \\) -prune -o -type %s %s %s %s %s -print"))))
  601. (setq cmd (format fmt
  602. (ffip--executable-find)
  603. (ffip--prune-patterns)
  604. (if is-finding-directory "d" "f")
  605. (ffip--ignore-file-names)
  606. ffip-find-options
  607. (ffip--join-patterns ffip-patterns)
  608. tgt))
  609. cmd))
  610. (defun ffip-glob-to-regex (s)
  611. "Convert glob pattern S into regular expression."
  612. (setq s (replace-regexp-in-string "\\." "\\\\." s))
  613. (setq s (replace-regexp-in-string "\*" ".*" s))
  614. s)
  615. ;;;###autoload
  616. (defun ffip-project-search (keyword is-finding-directory &optional directory-to-search)
  617. "Return an alist of all filenames in the project and their path.
  618. Files with duplicate filenames are suffixed with the name of the
  619. directory they are found in so that they are unique.
  620. If KEYWORD is string, it's the file name or file path to find file.
  621. If KEYWORD is list, it's the list of file names.
  622. IF IS-FINDING-DIRECTORY is t, we are searching directories, else files.
  623. DIRECTORY-TO-SEARCH specify the root directory to search."
  624. (let* ((root (or directory-to-search
  625. (ffip-get-project-root-directory)))
  626. (default-directory (file-name-as-directory root))
  627. (cmd (ffip-create-shell-command keyword is-finding-directory))
  628. (collection (split-string (ffip-shell-command-to-string cmd) "[\r\n]+" t)))
  629. (if ffip-debug (message "run command at %s: %s" default-directory cmd))
  630. (when (and ffip-use-rust-fd ffip-patterns)
  631. ;; filter result with Lisp because fd does NOT support multiple patterns
  632. (let* ((r (concat "^" (mapconcat 'ffip-glob-to-regex ffip-patterns "\\|") "$")))
  633. (setq collection (delq nil (mapcar (lambda (s)
  634. (if (string-match-p r s) s))
  635. collection)))))
  636. (mapcar (lambda (file)
  637. (cons (replace-regexp-in-string "^\./" "" file)
  638. (expand-file-name file)))
  639. ;; #15 improving handling of directories containing space
  640. collection)))
  641. (defun ffip--forward-line (lnum)
  642. "Forward LNUM lines."
  643. (if ffip-debug (message "ffip--forward-line called => %s" lnum))
  644. (when (and lnum (> lnum 0))
  645. (goto-char (point-min))
  646. (forward-line (1- lnum))))
  647. ;;;###autoload
  648. (defun ffip-find-files (keyword open-another-window &optional find-directory fn)
  649. "Use KEYWORD to find files.
  650. If OPEN-ANOTHER-WINDOW is t, the results are displayed in a new window.
  651. If FIND-DIRECTORY is t, only search directories. FN is callback.
  652. This function is the API to find files."
  653. (let* (cands lnum file root)
  654. ;; extract line num if exists
  655. (when (and keyword (stringp keyword)
  656. (string-match "^\\(.*\\):\\([0-9]+\\):?$" keyword))
  657. (setq lnum (string-to-number (match-string 2 keyword)))
  658. (setq keyword (match-string 1 keyword)))
  659. (setq cands (ffip-project-search keyword find-directory))
  660. (cond
  661. ((> (length cands) 0)
  662. (setq root (file-name-nondirectory (directory-file-name (ffip-get-project-root-directory))))
  663. (ffip-completing-read
  664. (format "Find in %s/: " root)
  665. cands
  666. `(lambda (file)
  667. ;; only one item in project files
  668. (if (listp file) (setq file (cdr file)))
  669. (if ,find-directory
  670. (if (quote ,open-another-window)
  671. (dired-other-window file)
  672. (switch-to-buffer (dired file)))
  673. ;; open file
  674. (if (quote ,open-another-window)
  675. (find-file-other-window file)
  676. (find-file file))
  677. ;; goto line if needed
  678. (ffip--forward-line ,lnum)
  679. (if ,fn (funcall ,fn file))))))
  680. (t
  681. (message "Nothing found!")))))
  682. (defun ffip--prepare-root-data-for-project-file (root)
  683. "Prepare data for ROOT."
  684. (cons 'ffip-project-root root))
  685. (defun ffip--read-selected ()
  686. (buffer-substring-no-properties (region-beginning) (region-end)))
  687. (defun ffip-read-keyword ()
  688. "Read keyword from selected text or user input."
  689. (let* ((hint (if ffip-use-rust-fd "Enter regex (or press ENTER):"
  690. "Enter keyword (or press ENTER):"))
  691. rlt)
  692. (cond
  693. ((region-active-p)
  694. (setq ffip-filename-history (add-to-list 'ffip-filename-history
  695. (ffip--read-selected)))
  696. (setq rlt (ffip--read-selected)))
  697. (t
  698. (setq rlt (read-from-minibuffer hint nil nil nil 'ffip-filename-history))))
  699. (if rlt (string-trim rlt) rlt)))
  700. ;;;###autoload
  701. (defun ffip-create-project-file ()
  702. "Create or Append .dir-locals.el to set up per directory.
  703. You can move .dir-locals.el to root directory.
  704. See (info \"(Emacs) Directory Variables\") for details."
  705. (interactive)
  706. (let* ((root (read-directory-name "Project root directory?" default-directory))
  707. (file (if (and root (file-exists-p root))
  708. (concat (file-name-as-directory root) ".dir-locals.el"))))
  709. (when file
  710. (with-temp-buffer
  711. (let ((print-level nil) (print-length nil) sexp (rlt '(a)))
  712. (cond
  713. ;; modify existing .dir-locals.el
  714. ((file-exists-p file)
  715. (let (sub-sexp new-sub-sexp)
  716. (setq sexp (ffip--read-file-text file))
  717. ;; valid .dir-locals.el
  718. (when sexp
  719. ;; the list for nil
  720. (setq sub-sexp (assoc nil sexp))
  721. (cond
  722. ;; `(nil (prop1 . val1) (prop2 . val2))' exists
  723. (sub-sexp
  724. ;; remove (ffip-project-root . "/path/file")
  725. (if (assoc 'ffip-project-root sub-sexp)
  726. (setq new-sub-sexp (delete (assoc 'ffip-project-root sub-sexp) sub-sexp))
  727. (setq new-sub-sexp sub-sexp))
  728. (add-to-list 'new-sub-sexp (ffip--prepare-root-data-for-project-file root) t)
  729. ;; update sexp
  730. (setq sexp (delete sub-sexp sexp))
  731. (add-to-list 'sexp new-sub-sexp))
  732. (t
  733. ;; add `(nil (ffip-project-root . "path/file"))'
  734. (add-to-list 'sexp (list nil (ffip--prepare-root-data-for-project-file root))))))
  735. ))
  736. (t
  737. ;; a new .dir-locals.el
  738. (setq sexp (list (list nil (ffip--prepare-root-data-for-project-file root))))))
  739. (when sexp
  740. (insert (format "%S" sexp))
  741. (write-file file)
  742. (message "%s created." file)))))))
  743. ;;;###autoload
  744. (defun ffip-current-full-filename-match-pattern-p (regex)
  745. "Is current full file name (including directory) match the REGEX?"
  746. (let* ((dir (if (buffer-file-name) (buffer-file-name) "")))
  747. (string-match-p regex dir)))
  748. ;;;###autoload
  749. (defun find-file-in-project (&optional open-another-window)
  750. "More powerful and efficient `find-file-in-project-by-selected' is recommended.
  751. Prompt with a completing list of all files in the project to find one.
  752. If OPEN-ANOTHER-WINDOW is not nil, the file will be opened in new window.
  753. The project's scope is defined as the first directory containing
  754. a `ffip-project-file' whose value is \".git\" by default.
  755. You can override this by setting the variable `ffip-project-root'."
  756. (interactive "P")
  757. (ffip-find-files nil open-another-window))
  758. (defun ffip-file-name-relative-p (filename)
  759. "Is FILENAME relative?"
  760. (if (string-match-p ffip-relative-path-pattern filename) t))
  761. ;;;###autoload
  762. (defun find-file-in-project-at-point (&optional open-another-window)
  763. "Find file whose name is guessed around point.
  764. If OPEN-ANOTHER-WINDOW is not nil, the file will be opened in new window."
  765. (interactive "P")
  766. (let* ((fn (or (and (region-active-p) (ffip--read-selected))
  767. (thing-at-point 'filename)
  768. (thing-at-point 'symbol)
  769. (read-string "No file name at point. Please provide file name:")))
  770. ;; could be a path
  771. (ffip-match-path-instead-of-filename t)
  772. tfn)
  773. (cond
  774. (fn
  775. (cond
  776. ((file-name-absolute-p fn)
  777. ;; absolute path
  778. (cond
  779. ((file-exists-p fn)
  780. ;; if file has absolute path and file exists, open it directly
  781. (if open-another-window (find-file-other-window fn)
  782. (find-file fn)))
  783. (t
  784. ;; well, search by file name
  785. (let* ((ffip-match-path-instead-of-filename nil))
  786. (ffip-find-files (file-name-nondirectory fn) open-another-window)))))
  787. ((and (ffip-file-name-relative-p fn)
  788. (file-exists-p (setq tfn (file-truename fn))))
  789. ;; file has relative path and file exist
  790. (if open-another-window (find-file-other-window tfn)
  791. (find-file tfn)))
  792. (t
  793. ;; strip prefix "../../" or "././" from file name
  794. (setq tfn (replace-regexp-in-string ffip-relative-path-pattern "" fn))
  795. (ffip-find-files tfn open-another-window))))
  796. (t
  797. (message "No file name is provided.")))))
  798. (defun ffip-parent-directory (level directory)
  799. "Return LEVEL up parent directory of DIRECTORY."
  800. (let* ((rlt directory))
  801. (while (and (> level 0) (not (string= "" rlt)))
  802. (setq rlt (file-name-directory (directory-file-name rlt)))
  803. (setq level (1- level)))
  804. (if (string= "" rlt) (setq rlt nil))
  805. rlt))
  806. ;;;###autoload
  807. (defun find-file-in-current-directory (&optional level)
  808. "Search fil in current directory or LEVEL up parent directory."
  809. (interactive "P")
  810. (unless level (setq level 0))
  811. (let* ((ffip-project-root (ffip-parent-directory level default-directory)))
  812. (find-file-in-project nil)))
  813. ;;;###autoload
  814. (defun find-file-in-project-by-selected (&optional open-another-window)
  815. "Same as `find-file-in-project' but more powerful and faster.
  816. It use string from selected region to search files in the project.
  817. If no region is selected, you could provide a keyword.
  818. Keyword could be ANY part of the file's full path and support wildcard.
  819. For example, to find /home/john/proj1/test.js, below keywords are valid:
  820. - test.js
  821. - roj1/tes
  822. - john*test
  823. If keyword contains line number like \"hello.txt:32\" or \"hello.txt:32:\",
  824. we will move to that line in opened file.
  825. If keyword is empty, it behaves same as `find-file-in-project'.
  826. If OPEN-ANOTHER-WINDOW is not nil, the file will be opened in new window."
  827. (interactive "P")
  828. (ffip-find-files (ffip-read-keyword) open-another-window))
  829. ;;;###autoload
  830. (defun ffip-insert-file ()
  831. "Insert contents of file in current buffer.
  832. The file name is selected interactively from candidates in project."
  833. (interactive)
  834. (let* ((cands (ffip-project-search (ffip-read-keyword) nil))
  835. root)
  836. (when (> (length cands) 0)
  837. (setq root (file-name-nondirectory (directory-file-name (ffip-get-project-root-directory))))
  838. (ffip-completing-read
  839. (format "Read file in %s/: " root)
  840. cands
  841. `(lambda (file)
  842. ;; only one item in project files
  843. (if (listp file) (setq file (cdr file)))
  844. (insert-file file))))))
  845. ;;;###autoload
  846. (defun find-file-with-similar-name (&optional open-another-window)
  847. "Use base name of current file as keyword which could be further stripped.
  848. by `ffip-strip-file-name-regex'.
  849. If OPEN-ANOTHER-WINDOW is not nil, the file will be opened in new window."
  850. (interactive "P")
  851. (when buffer-file-name
  852. (let* ((keyword (concat (file-name-base buffer-file-name) ".*") ))
  853. (if ffip-strip-file-name-regex
  854. (setq keyword (replace-regexp-in-string ffip-strip-file-name-regex
  855. ""
  856. keyword)))
  857. (ffip-find-files keyword open-another-window))))
  858. ;;;###autoload
  859. (defun find-file-in-current-directory-by-selected (&optional open-another-window)
  860. "Like `find-file-in-project-by-selected' but search current directory.
  861. If OPEN-ANOTHER-WINDOW is not nil, the file will be opened in new window."
  862. (interactive "P")
  863. (let* ((ffip-project-root default-directory))
  864. (find-file-in-project-by-selected open-another-window)))
  865. ;;;###autoload
  866. (defun find-relative-path(&optional find-directory)
  867. "Find file/directory and copy its relative path into `kill-ring'.
  868. Optional prefix FIND-DIRECTORY copy the directory path; file path by default.
  869. You can set `ffip-find-relative-path-callback' to format the string before copying,
  870. (setq ffip-find-relative-path-callback 'ffip-copy-reactjs-import)
  871. (setq ffip-find-relative-path-callback 'ffip-copy-org-file-link)"
  872. (interactive "P")
  873. (let* ((cands (ffip-project-search (ffip-read-keyword) find-directory))
  874. root)
  875. (cond
  876. ((> (length cands) 0)
  877. (setq root (file-name-nondirectory (directory-file-name (ffip-get-project-root-directory))))
  878. (ffip-completing-read
  879. (format "Find in %s/: " root)
  880. cands
  881. `(lambda (p)
  882. ;; only one item in project files
  883. (if (listp p) (setq p (cdr p)))
  884. (if ,find-directory
  885. (setq p (file-name-as-directory p)))
  886. (setq p (file-relative-name p (file-name-directory buffer-file-name)))
  887. (funcall ffip-find-relative-path-callback p))))
  888. (t
  889. (message "Nothing found!")))))
  890. ;;;###autoload
  891. (defun find-directory-in-project-by-selected (&optional open-another-window)
  892. "Similar to `find-file-in-project-by-selected'.
  893. Use string from selected region to find directory in the project.
  894. If no region is selected, you need provide keyword.
  895. Keyword could be directory's base-name only or parent-directory+base-name
  896. For example, to find /home/john/proj1/test, below keywords are valid:
  897. - test
  898. - roj1/test
  899. - john*test
  900. If OPEN-ANOTHER-WINDOW is not nil, the file will be opened in new window."
  901. (interactive "P")
  902. (ffip-find-files (ffip-read-keyword) open-another-window t))
  903. ;;;###autoload
  904. (defalias 'ffip 'find-file-in-project)
  905. (defun ffip-path (candidate)
  906. "Get path from ivy CANDIDATE."
  907. (let* ((default-directory (ffip-project-root)))
  908. (file-truename (if (consp candidate) (cdr candidate)
  909. candidate))))
  910. (defun ffip-split-window-api (split-fn mv-fn ratio)
  911. "Use SPLIT-FN to split window and focus on new window by MV-FN.
  912. Window split in RATIO."
  913. (let* (ratio-val
  914. (keyword (if ffip-split-window-without-asking-for-keyword ""
  915. (ffip-read-keyword)))
  916. (cands (ffip-project-search keyword nil))
  917. (file (if (= 1 (length cands)) (ffip-path (car cands))
  918. (ffip-path (ffip-completing-read "Find file: " cands))))
  919. (buf (if (and file (file-exists-p file)) (find-file-noselect file)
  920. (other-buffer))))
  921. (cond
  922. (ratio
  923. (setq ratio-val (cdr (assoc ratio ffip-window-ratio-alist)))
  924. (funcall split-fn (floor (/ (window-body-width)
  925. (1+ ratio-val)))))
  926. (t
  927. (funcall split-fn)))
  928. (set-window-buffer (next-window) buf)
  929. (if (or (not ratio-val)
  930. (>= ratio-val 1))
  931. (funcall mv-fn))))
  932. ;;;###autoload
  933. (defun ffip-split-window-horizontally (&optional ratio)
  934. "Find&Open file in horizontal split window.
  935. New window size is looked up in `ffip-window-ratio-alist' by RATIO.
  936. Keyword to search new file is selected text or user input."
  937. (interactive "P")
  938. (ffip-split-window-api 'split-window-horizontally 'windmove-right ratio))
  939. ;;;###autoload
  940. (defun ffip-split-window-vertically (&optional ratio)
  941. "Find&Open file in vertical split window.
  942. New window size is looked up in `ffip-window-ratio-alist' by RATIO.
  943. Keyword to search new file is selected text or user input."
  944. (interactive "P")
  945. (ffip-split-window-api 'split-window-vertically 'windmove-down ratio))
  946. ;;;###autoload
  947. (defun ffip-diff-quit ()
  948. "Quit."
  949. (interactive)
  950. ;; kill buffer instead of bury it
  951. (quit-window t))
  952. ;;;###autoload
  953. (defun ffip-diff-find-file (&optional open-another-window)
  954. "File file(s) in current hunk.
  955. If OPEN-ANOTHER-WINDOW is not nil, the file will be opened in new window."
  956. (interactive "P")
  957. (let* ((files (mapcar 'file-name-nondirectory (diff-hunk-file-names)))
  958. (alnum 0)
  959. (blnum 0)
  960. (regex "\\(?:\\*\\{15\\}.*\n\\)?[-@* ]*\\([0-9,]+\\)\\([ acd+]+\\([0-9,]+\\)\\)?"))
  961. (save-excursion
  962. (diff-beginning-of-hunk t)
  963. (when (looking-at regex)
  964. (setq alnum (string-to-number (match-string 1)))
  965. (setq blnum (string-to-number (match-string 3)))))
  966. (cond
  967. ((and (> (length files) 1)
  968. (string= (nth 0 files) (nth 1 files)))
  969. (ffip-find-files (nth 0 files)
  970. open-another-window
  971. nil
  972. `(lambda (opened-file)
  973. ;; use line number in new file since there
  974. ;; is only one file name candidate
  975. (ffip--forward-line ,blnum))))
  976. (t
  977. (run-hook-with-args 'ffip-diff-find-file-before-hook)
  978. (ffip-find-files files
  979. open-another-window
  980. nil
  981. (lambda (opened-file)
  982. (cond
  983. ((string= (file-name-nondirectory opened-file) (nth 0 files))
  984. (ffip--forward-line alnum))
  985. (t
  986. (ffip--forward-line blnum)))))))))
  987. (defvar ffip-diff-mode-map
  988. (let ((map (make-sparse-keymap)))
  989. (set-keymap-parent map diff-mode-map)
  990. ;; EVIL friendly. ffip-diff-mode is read-only
  991. (define-key map "K" 'diff-hunk-prev)
  992. (define-key map "J" 'diff-hunk-next)
  993. (define-key map "P" 'diff-file-prev)
  994. (define-key map "N" 'diff-file-next)
  995. (define-key map [remap diff-goto-source] 'ffip-diff-find-file)
  996. map)
  997. "Mode map based on `diff-mode-map'.")
  998. (define-derived-mode ffip-diff-mode diff-mode "ffip"
  999. "Show diff/patch."
  1000. (setq buffer-read-only t)
  1001. (setq truncate-lines t)
  1002. (use-local-map ffip-diff-mode-map))
  1003. (defun ffip-show-content-in-diff-mode (content)
  1004. "Insert CONTENT into *ffip-diff* buffer."
  1005. (cond
  1006. ((and content (not (string= content "")))
  1007. (let (rlt-buf)
  1008. (if (get-buffer "*ffip-diff*")
  1009. (kill-buffer "*ffip-diff*"))
  1010. (setq rlt-buf (get-buffer-create "*ffip-diff*"))
  1011. (save-current-buffer
  1012. (switch-to-buffer-other-window rlt-buf)
  1013. (set-buffer rlt-buf)
  1014. (erase-buffer)
  1015. (insert content)
  1016. (ffip-diff-mode)
  1017. (goto-char (point-min)))))
  1018. (t
  1019. (message "Output is empty!"))))
  1020. (defun ffip-diff-execute-backend (backend)
  1021. "Execute BACKEND."
  1022. (if backend
  1023. (cond
  1024. ;; shell command
  1025. ((stringp backend)
  1026. (ffip-show-content-in-diff-mode (ffip-shell-command-to-string backend)))
  1027. ;; command
  1028. ((functionp backend)
  1029. (ffip-show-content-in-diff-mode (funcall backend)))
  1030. ;; lisp expression
  1031. ((consp backend)
  1032. (ffip-show-content-in-diff-mode (funcall `(lambda () ,backend)))))))
  1033. (defun ffip-backend-description (backend)
  1034. "Get BACKEND description."
  1035. (let* (rlt)
  1036. (cond
  1037. ;; shell command
  1038. ((stringp backend)
  1039. (setq rlt backend))
  1040. ;; command
  1041. ((functionp backend)
  1042. (setq rlt (symbol-name backend)))
  1043. ;; lisp expression
  1044. ((consp backend)
  1045. ;; (cons "description" actual-backend)
  1046. (if (stringp (car backend))
  1047. (setq rlt (car backend))
  1048. (setq rlt "unknown"))))
  1049. rlt))
  1050. ;;;###autoload
  1051. (defun ffip-show-diff-internal (&optional num)
  1052. "Show the diff output by executing selected `ffip-diff-backends'.
  1053. NUM is the index selected backend from `ffip-diff-backends'.
  1054. NUM is zero based whose default value is zero."
  1055. (interactive "P")
  1056. (cond
  1057. ((or (not num) (< num 0))
  1058. (setq num 0))
  1059. ((> num (length ffip-diff-backends))
  1060. (setq num (1- (length ffip-diff-backends)))))
  1061. (let* ((backend (nth num ffip-diff-backends)))
  1062. (if (and (consp backend)
  1063. (stringp (car backend)))
  1064. (setq backend (cdr backend)))
  1065. (ffip-diff-execute-backend backend)))
  1066. ;;;###autoload
  1067. (defun ffip-show-diff-by-description (&optional num)
  1068. "Show the diff output by executing selected `ffip-diff-backends'.
  1069. NUM is the backend index of `ffip-diff-backends'.
  1070. If NUM is not nil, the corresponding backend is executed directly."
  1071. (interactive "P")
  1072. (cond
  1073. (num
  1074. (ffip-show-diff-internal num))
  1075. (t
  1076. (let* (descriptions
  1077. (i 0))
  1078. ;; format backend descriptions
  1079. (dolist (b ffip-diff-backends)
  1080. (add-to-list 'descriptions
  1081. (format "%s: %s"
  1082. i
  1083. (ffip-backend-description b)) t)
  1084. (setq i (+ 1 i)))
  1085. (ffip-completing-read
  1086. "Run diff backend:"
  1087. descriptions
  1088. `(lambda (d)
  1089. (if (string-match "^\\([0-9]+\\): " d)
  1090. (ffip-show-diff-internal (string-to-number (match-string 1 d))))))))))
  1091. (defalias 'ffip-show-diff 'ffip-show-diff-by-description)
  1092. (defadvice read-file-name (around ffip-read-file-name-hack activate)
  1093. (cond
  1094. (ffip-read-file-name-hijacked-p
  1095. ;; only hack read-file-name once
  1096. (setq ffip-read-file-name-hijacked-p nil)
  1097. (let* ((args (ad-get-args 0))
  1098. (file-name (file-name-nondirectory (nth 2 args)))
  1099. (default-directory (ffip-project-root))
  1100. (cands (ffip-project-search file-name nil default-directory))
  1101. (rlt (if cands (ffip-completing-read "Files: " cands))))
  1102. (when rlt
  1103. (setq rlt (file-truename rlt))
  1104. (run-hook-with-args 'ffip-diff-apply-hunk-hook rlt)
  1105. (setq ad-return-value rlt))))
  1106. (t
  1107. ad-do-it)))
  1108. ;;;###autoload
  1109. (defun ffip-diff-apply-hunk (&optional reverse)
  1110. "Apply current hunk in `diff-mode'. Try to locate the file to patch.
  1111. Similar to `diff-apply-hunk' but smarter.
  1112. Please read documentation of `diff-apply-hunk' to get more details.
  1113. If REVERSE is t, applied patch is reverted."
  1114. (interactive "P")
  1115. (cond
  1116. ((derived-mode-p 'diff-mode)
  1117. (setq ffip-read-file-name-hijacked-p t)
  1118. (diff-apply-hunk reverse)
  1119. (setq ffip-read-file-name-hijacked-p nil))
  1120. (t
  1121. (message "This command only run in `diff-mode' and `ffip-diff-mode'."))))
  1122. ;; safe locals
  1123. (progn
  1124. (put 'ffip-diff-backends 'safe-local-variable 'listp)
  1125. (put 'ffip-patterns 'safe-local-variable 'listp)
  1126. (put 'ffip-prune-patterns 'safe-local-variable 'listp)
  1127. (put 'ffip-ignore-filenames 'safe-local-variable 'listp)
  1128. (put 'ffip-match-path-instead-of-filename 'safe-local-variable 'booleanp)
  1129. (put 'ffip-project-file 'safe-local-variable 'stringp)
  1130. (put 'ffip-strip-file-name-regex 'safe-local-variable 'stringp)
  1131. (put 'ffip-project-root 'safe-local-variable 'stringp))
  1132. (provide 'find-file-in-project)
  1133. ;;; find-file-in-project.el ends here