Personal emacs config
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

352 lines
13 KiB

  1. ;;; dired-narrow.el --- Live-narrowing of search results for dired
  2. ;; Copyright (C) 2014-2015 Matúš Goljer
  3. ;; Author: Matúš Goljer <matus.goljer@gmail.com>
  4. ;; Maintainer: Matúš Goljer <matus.goljer@gmail.com>
  5. ;; Version: 0.0.1
  6. ;; Package-Version: 20181114.1723
  7. ;; Package-Commit: d6d4d1930969bbc22fd0551d5195887bf92cab3e
  8. ;; Created: 14th February 2014
  9. ;; Package-requires: ((dash "2.7.0") (dired-hacks-utils "0.0.1"))
  10. ;; Keywords: files
  11. ;; This program is free software; you can redistribute it and/or
  12. ;; modify it under the terms of the GNU General Public License
  13. ;; as published by the Free Software Foundation; either version 3
  14. ;; of the License, or (at your option) any later version.
  15. ;; This program is distributed in the hope that it will be useful,
  16. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. ;; GNU General Public License for more details.
  19. ;; You should have received a copy of the GNU General Public License
  20. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. ;;; Commentary:
  22. ;; This package provides live filtering of files in dired buffers. In
  23. ;; general, after calling the respective narrowing function you type a
  24. ;; filter string into the minibuffer. After each change the changes
  25. ;; automatically reflect in the buffer. Typing C-g will cancel the
  26. ;; narrowing and restore the original view, typing RET will exit the
  27. ;; live filtering mode and leave the dired buffer in the narrowed
  28. ;; state. To bring it back to the original view, you can call
  29. ;; `revert-buffer' (usually bound to `g').
  30. ;; During the filtering process, several special functions are
  31. ;; available. You can customize the binding by changing
  32. ;; `dired-narrow-map'.
  33. ;; * `dired-narrow-next-file' (<down> or C-n) - move the point to the
  34. ;; next file
  35. ;; * `dired-narrow-previous-file' (<up> or C-p) - move the point to the
  36. ;; previous file
  37. ;; * `dired-narrow-enter-directory' (<right> or C-j) - descend into the
  38. ;; directory under point and immediately go back to narrowing mode
  39. ;; You can customize what happens after exiting the live filtering
  40. ;; mode by customizing `dired-narrow-exit-action'.
  41. ;; These narrowing functions are provided:
  42. ;; * `dired-narrow'
  43. ;; * `dired-narrow-regexp'
  44. ;; * `dired-narrow-fuzzy'
  45. ;; You can also create your own narrowing functions quite easily. To
  46. ;; define new narrowing function, use `dired-narrow--internal' and
  47. ;; pass it an apropriate filter. The filter should take one argument
  48. ;; which is the filter string from the minibuffer. It is then called
  49. ;; at each line that describes a file with point at the beginning of
  50. ;; the file name. If the filter returns nil, the file is removed from
  51. ;; the view. As an inspiration, look at the built-in functions
  52. ;; mentioned above.
  53. ;; See https://github.com/Fuco1/dired-hacks for the entire collection.
  54. ;;; Code:
  55. (require 'dash)
  56. (require 'dired-hacks-utils)
  57. (require 'delsel)
  58. (defgroup dired-narrow ()
  59. "Live-narrowing of search results for dired."
  60. :group 'dired-hacks
  61. :prefix "dired-narrow-")
  62. (defvar dired-narrow-map
  63. (let ((map (make-sparse-keymap)))
  64. (define-key map (kbd "<up>") 'dired-narrow-previous-file)
  65. (define-key map (kbd "<down>") 'dired-narrow-next-file)
  66. (define-key map (kbd "<right>") 'dired-narrow-enter-directory)
  67. (define-key map (kbd "C-p") 'dired-narrow-previous-file)
  68. (define-key map (kbd "C-n") 'dired-narrow-next-file)
  69. (define-key map (kbd "C-j") 'dired-narrow-enter-directory)
  70. (define-key map (kbd "C-g") 'minibuffer-keyboard-quit)
  71. (define-key map (kbd "RET") 'exit-minibuffer)
  72. (define-key map (kbd "<return>") 'exit-minibuffer)
  73. map)
  74. "Keymap used while `dired-narrow' is reading the pattern.")
  75. (defcustom dired-narrow-exit-action 'ignore
  76. "Function to call after exiting minibuffer.
  77. Function takes no argument and is called with point over the file
  78. we should act on."
  79. :type '(choice (const :tag "Open file under point" dired-narrow-find-file)
  80. (function :tag "Use custom function."))
  81. :group 'dired-narrow)
  82. (defcustom dired-narrow-exit-when-one-left nil
  83. "If there is only one file left while narrowing,
  84. exit minibuffer and call `dired-narrow-exit-action'."
  85. :type 'boolean
  86. :group 'dired-narrow)
  87. (defcustom dired-narrow-enable-blinking t
  88. "If set to true highlight the chosen file shortly.
  89. This feature works only when `dired-narrow-exit-when-one-left' is true."
  90. :type 'boolean
  91. :group 'dired-narrow)
  92. (defcustom dired-narrow-blink-time 0.2
  93. "How long should be highlighted a chosen file.
  94. Units are seconds."
  95. :type 'float
  96. :group 'dired-narrow)
  97. (defface dired-narrow-blink
  98. '((t :background "#eadc62"
  99. :foreground "black"))
  100. "The face used to highlight a chosen file
  101. when `dired-narrow-exit-when-one-left' and `dired-narrow-enable-blinking' are true."
  102. :group 'dired-narrow)
  103. ;; Utils
  104. ;; this is `gnus-remove-text-with-property'
  105. (defun dired-narrow--remove-text-with-property (prop)
  106. "Delete all text in the current buffer with text property PROP."
  107. (let ((start (point-min))
  108. end)
  109. (unless (get-text-property start prop)
  110. (setq start (next-single-property-change start prop)))
  111. (while start
  112. (setq end (text-property-any start (point-max) prop nil))
  113. (delete-region start (or end (point-max)))
  114. (setq start (when end
  115. (next-single-property-change start prop))))))
  116. (defvar dired-narrow-filter-function 'identity
  117. "Filter function used to filter the dired view.")
  118. (defvar dired-narrow--current-file nil
  119. "Value of point just before exiting minibuffer.")
  120. (defun dired-narrow--update (filter)
  121. "Make the files not matching the FILTER invisible.
  122. Return the count of visible files that are left after update."
  123. (let ((inhibit-read-only t)
  124. (visible-files-cnt 0))
  125. (save-excursion
  126. (goto-char (point-min))
  127. ;; TODO: we might want to call this only if the filter gets less
  128. ;; specialized.
  129. (dired-narrow--restore)
  130. (while (dired-hacks-next-file)
  131. (if (funcall dired-narrow-filter-function filter)
  132. (progn
  133. (setq visible-files-cnt (1+ visible-files-cnt))
  134. (when (fboundp 'dired-insert-set-properties)
  135. (dired-insert-set-properties (line-beginning-position) (1+ (line-end-position)))))
  136. (put-text-property (line-beginning-position) (1+ (line-end-position)) :dired-narrow t)
  137. (put-text-property (line-beginning-position) (1+ (line-end-position)) 'invisible :dired-narrow))))
  138. (unless (dired-hacks-next-file)
  139. (dired-hacks-previous-file))
  140. (unless (dired-utils-get-filename)
  141. (dired-hacks-previous-file))
  142. visible-files-cnt))
  143. (defun dired-narrow--restore ()
  144. "Restore the invisible files of the current buffer."
  145. (let ((inhibit-read-only t))
  146. (remove-list-of-text-properties (point-min) (point-max)
  147. '(invisible :dired-narrow))
  148. (when (fboundp 'dired-insert-set-properties)
  149. (dired-insert-set-properties (point-min) (point-max)))))
  150. (defun dired-narrow--blink-current-file ()
  151. (let* ((beg (line-beginning-position))
  152. (end (line-end-position))
  153. (overlay (make-overlay beg end)))
  154. (overlay-put overlay 'face 'dired-narrow-blink)
  155. (redisplay)
  156. (sleep-for dired-narrow-blink-time)
  157. (discard-input)
  158. (delete-overlay overlay)))
  159. ;; Live filtering
  160. (defvar dired-narrow-buffer nil
  161. "Dired buffer we are currently filtering.")
  162. (defvar dired-narrow--minibuffer-content ""
  163. "Content of the minibuffer during narrowing.")
  164. (defun dired-narrow--minibuffer-setup ()
  165. "Set up the minibuffer for live filtering."
  166. (when dired-narrow-buffer
  167. (add-hook 'post-command-hook 'dired-narrow--live-update nil :local)))
  168. (add-hook 'minibuffer-setup-hook 'dired-narrow--minibuffer-setup)
  169. (defun dired-narrow--live-update ()
  170. "Update the dired buffer based on the contents of the minibuffer."
  171. (when dired-narrow-buffer
  172. (let ((current-filter (minibuffer-contents-no-properties))
  173. visible-files-cnt)
  174. (with-current-buffer dired-narrow-buffer
  175. (setq visible-files-cnt
  176. (unless (equal current-filter dired-narrow--minibuffer-content)
  177. (dired-narrow--update current-filter)))
  178. (setq dired-narrow--minibuffer-content current-filter)
  179. (setq dired-narrow--current-file (dired-utils-get-filename))
  180. (set-window-point (get-buffer-window (current-buffer)) (point))
  181. (when (and dired-narrow-exit-when-one-left
  182. visible-files-cnt
  183. (= visible-files-cnt 1))
  184. (when dired-narrow-enable-blinking
  185. (dired-narrow--blink-current-file))
  186. (exit-minibuffer))))))
  187. (defun dired-narrow--internal (filter-function)
  188. "Narrow a dired buffer to the files matching a filter.
  189. The function FILTER-FUNCTION is called on each line: if it
  190. returns non-nil, the line is kept, otherwise it is removed. The
  191. function takes one argument, which is the current filter string
  192. read from minibuffer."
  193. (let ((dired-narrow-buffer (current-buffer))
  194. (dired-narrow-filter-function filter-function)
  195. (disable-narrow nil))
  196. (unwind-protect
  197. (progn
  198. (dired-narrow-mode 1)
  199. (add-to-invisibility-spec :dired-narrow)
  200. (setq disable-narrow (read-from-minibuffer "Filter: " nil dired-narrow-map))
  201. (let ((inhibit-read-only t))
  202. (dired-narrow--remove-text-with-property :dired-narrow))
  203. ;; If the file no longer exists, we can't do anything, so
  204. ;; set to nil
  205. (unless (dired-utils-goto-line dired-narrow--current-file)
  206. (setq dired-narrow--current-file nil)))
  207. (with-current-buffer dired-narrow-buffer
  208. (unless disable-narrow (dired-narrow-mode -1))
  209. (remove-from-invisibility-spec :dired-narrow)
  210. (dired-narrow--restore))
  211. (when (and disable-narrow
  212. dired-narrow--current-file
  213. dired-narrow-exit-action)
  214. (funcall dired-narrow-exit-action))
  215. (cond
  216. ((equal disable-narrow "dired-narrow-enter-directory")
  217. (dired-narrow--internal filter-function))))))
  218. ;; Interactive
  219. (defun dired-narrow--regexp-filter (filter)
  220. (condition-case nil
  221. (string-match-p filter (dired-utils-get-filename 'no-dir))
  222. ;; Return t if your regexp is incomplete/has errors, thus
  223. ;; filtering nothing until you fix the regexp.
  224. (invalid-regexp t)))
  225. ;;;###autoload
  226. (defun dired-narrow-regexp ()
  227. "Narrow a dired buffer to the files matching a regular expression."
  228. (interactive)
  229. (dired-narrow--internal 'dired-narrow--regexp-filter))
  230. (defun dired-narrow--string-filter (filter)
  231. (let ((words (split-string filter " ")))
  232. (--all? (save-excursion (search-forward it (line-end-position) t)) words)))
  233. (defun dired-narrow-next-file ()
  234. "Move point to the next file."
  235. (interactive)
  236. (with-current-buffer dired-narrow-buffer
  237. (dired-hacks-next-file)))
  238. (defun dired-narrow-previous-file ()
  239. "Move point to the previous file."
  240. (interactive)
  241. (with-current-buffer dired-narrow-buffer
  242. (dired-hacks-previous-file)))
  243. (defun dired-narrow-find-file ()
  244. "Run `dired-find-file' or any remapped action on file under point."
  245. (interactive)
  246. (let ((function (or (command-remapping 'dired-find-file)
  247. 'dired-find-file)))
  248. (funcall function)))
  249. (defun dired-narrow-enter-directory ()
  250. "Descend into directory under point and initiate narrowing."
  251. (interactive)
  252. (let ((inhibit-read-only t))
  253. (erase-buffer)
  254. (insert "dired-narrow-enter-directory"))
  255. (exit-minibuffer))
  256. ;;;###autoload
  257. (defun dired-narrow ()
  258. "Narrow a dired buffer to the files matching a string.
  259. If the string contains spaces, then each word is matched against
  260. the file name separately. To succeed, all of them have to match
  261. but the order does not matter.
  262. For example \"foo bar\" matches filename \"bar-and-foo.el\"."
  263. (interactive)
  264. (dired-narrow--internal 'dired-narrow--string-filter))
  265. (defun dired-narrow--fuzzy-filter (filter)
  266. (re-search-forward
  267. (mapconcat 'regexp-quote
  268. (mapcar 'char-to-string (string-to-list filter))
  269. ".*")
  270. (line-end-position) t))
  271. ;;;###autoload
  272. (defun dired-narrow-fuzzy ()
  273. "Narrow a dired buffer to the files matching a fuzzy string.
  274. A fuzzy string is constructed from the filter string by inserting
  275. \".*\" between each letter. This is then matched as regular
  276. expression against the file name."
  277. (interactive)
  278. (dired-narrow--internal 'dired-narrow--fuzzy-filter))
  279. (define-minor-mode dired-narrow-mode
  280. "Minor mode for indicating when narrowing is in progress."
  281. :lighter " dired-narrow")
  282. (defun dired-narrow--disable-on-revert ()
  283. "Disable `dired-narrow-mode' after revert."
  284. (dired-narrow-mode -1))
  285. (add-hook 'dired-after-readin-hook 'dired-narrow--disable-on-revert)
  286. (provide 'dired-narrow)
  287. ;;; dired-narrow.el ends here