/
spotlight.el
363 lines (301 loc) · 14.2 KB
/
spotlight.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
;;; spotlight.el --- search files with Mac OS X spotlight -*- lexical-binding: t; -*-
;; Copyright (C) 2015 Ben Maughan <benmaughan@gmail.com>
;; Author: Ben Maughan <benmaughan@gmail.com>
;; URL: http://www.pragmaticemacs.com
;; Version: 0.2.0
;; Package-Requires: ((emacs "24.1") (swiper "0.6.0") (counsel "0.6.0"))
;; Keywords: search, external
;; This file 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.
;; For a full copy of the GNU General Public License
;; see <http://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; Provides two functions. These are:
;;
;; `spotlight' prompts for a query string and searches the
;; spotlight database with dynamic updates for each new character
;; entered. You'll be given a list of files that match. Selecting a
;; file will launch `swiper' for that file searching for the query
;; string.
;;
;; Alternatively, the user can use M-RET to dynamically
;; filter the list of matching files to reduce the number of matches
;; before selecting a file.
;;
;; `spotlight-fast' is the same as `spotlight' but the user is
;; prompted for a query string to search the spotlight database
;; without incremental updates. This can be much faster than
;; `spotlight'. The list of matching files containing the query string
;; in their bodies are presented and the user can select the file or
;; type a string to dynamically filter the list of files by filename.
;; The selected file is then opened and a `swiper' search using the
;; original query is launched.
;;
;; Customise the variable `spotlight-min-chars' to set the minimum
;; number of characters that must be entered before the first
;; spotlight search is performed in `spotlight'. Setting
;; `spotlight-min-chars' to a lower number will result in more matches
;; and can lead to slow performance.
;;
;; Customise the variable `spotlight-default-base-dir' to specify the default
;; base directory for the spotlight search for both `spotlight' and
;; `spotlight-live'. The spotlight database will be queried for files
;; below this directory. Default is user's home directory. Use '/' to
;; search everywhere. Alternatively, both `spotlight' and
;; `spotlight-fast' can be called with a prefix argument, in which
;; case they will prompt for a base directory.
;; Credits:
;; Some of the code is based on parts of counsel.el by Oleh Krehel
;; at https://github.com/abo-abo/swiper
;;
;; The dynamic filtering is done with the ivy library by the same
;; author
;;
;; Thanks to commenters on https://www.reddit.com/r/emacs for feedback
;; on an early version of the package
;;; Code:
(require 'swiper)
(require 'counsel)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; set up variables ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defgroup spotlight nil
"Search for files with Mac OS X spotlight."
:group 'external
:prefix "spotlight-")
(defcustom spotlight-default-base-dir "~"
"Search spotlight database for files below this directory. Default is user's home directory. Use '/' to search everywhere."
:group 'external
:type 'string)
(defcustom spotlight-min-chars 2
"Minimum number of characters required before running spotlight search in `spotlight'. After this many characters have been entered, the search is updated with each new character. Setting `spotlight-min-chars' to a lower number will result in more matches and can lead to slow performance."
:group 'external
:type 'integer)
(defcustom spotlight-ivy-height 10
"Height in characters of minibuffer displaying search results."
:group 'external
:type 'integer)
(defcustom spotlight-tmp-file "~/.emacs-spotlight-tmp-file"
"Temporary file to store spotlight results."
:group 'external
:type 'string)
(defvar spotlight-user-base-dir nil
"String containing base directory for spotlight search. May be used to override `spotlight-default-base-dir'.")
(defvar spotlight-file-filter-flag nil
"Flag to record if filename filtering is requested.")
(ivy-configure 'spotlight :more-chars spotlight-min-chars)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; functions ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; function to break out of spotlight and filter on filename
(defun spotlight-launch-file-filter ()
"Function to break out of the ivy spotlight search into the filename filter."
(interactive)
(setq spotlight-file-filter-flag t)
(ivy-done))
;; create keymap
(defvar spotlight-map nil
"Keymap for spotlight.")
(setq spotlight-map (make-sparse-keymap))
(define-key spotlight-map (kbd "M-RET") 'spotlight-launch-file-filter)
;; Function to be called by ivy to filter the spotlight file list
;; used by spotlight and spotlight-fast
(defun spotlight-filter (regex candidates)
"Filter spotlight results list of CANDIDATES to match REGEX."
(delq nil (mapcar (lambda (x) (and (string-match regex x) x)) candidates)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; spotlight functions ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Function to be called by ivy to run mdfind
(defun ivy-mdfind-function (string &rest _unused)
"Issue mdfind for STRING."
(or (ivy-more-chars)
(progn (spotlight-async-command
(concat "mdfind -onlyin "
(shell-quote-argument
(expand-file-name spotlight-user-base-dir))
" "
(shell-quote-argument string)
" > "
(expand-file-name spotlight-tmp-file)))
'("" "working..."))))
;; Modified version of counsel--async-command from counsel.el
(defun spotlight-async-command (cmd)
(let* ((counsel--process "*spotlight*")
(proc (get-process counsel--process))
(buff (get-buffer counsel--process)))
(when proc
(delete-process proc))
(when buff
(kill-buffer buff))
;; delete tmp file if exists
(when (file-exists-p spotlight-tmp-file)
(delete-file spotlight-tmp-file))
(setq proc (start-process-shell-command
counsel--process
counsel--process
cmd))
(set-process-sentinel proc #'spotlight-async-sentinel)))
;; Modified version of counsel--async-sentinel from counsel.el
(defun spotlight-async-sentinel (process event)
(if (string= event "finished\n")
(if (file-exists-p spotlight-tmp-file)
(progn
(with-current-buffer (process-buffer process)
(insert-file-contents spotlight-tmp-file)
;; check if no matches
(if (= (buffer-size (process-buffer process)) 0)
(setq ivy--all-candidates '("No matches"))
(setq ivy--all-candidates
(ivy--sort-maybe
(split-string (buffer-string) "\n" t))))
(setq ivy--old-cands ivy--all-candidates))
(ivy--exhibit))
(progn
(setq ivy--all-candidates '("Error - tmp file not found"))
(setq ivy--old-cands ivy--all-candidates)
(ivy--exhibit)))
(if (string= event "exited abnormally with code 1\n")
(progn
(setq ivy--all-candidates '("Error"))
(setq ivy--old-cands ivy--all-candidates)
(ivy--exhibit)))))
;; Function to run the ivy filter in the file list for the spotlight
;; search where the file list is in the buffer *spotlight*
(defun spotlight-file-select-cached (query)
"Filter file list in spotlight buffer and then open file with swiper search for string QUERY."
;;run query
(let (spotlight-list)
(with-current-buffer "*spotlight*"
(setq spotlight-list (split-string (buffer-string) "\n" t)))
(let ((ivy-height spotlight-ivy-height))
(ivy-read "filename filter: " spotlight-list
:matcher #'spotlight-filter
:re-builder #'ivy--regex
:action (lambda (x)
(find-file x)
(swiper query))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; main spotlight function ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;###autoload
(defun spotlight (arg &optional initial-input)
"Search for a string ARG in the spotlight database.
Uses `ivy-read' to perform dynamic updates for each new character
entered.
You'll be given a list of files that match. Selecting a file will
launch `swiper' for that file to search it for your query string.
INITIAL-INPUT can be given as the initial minibuffer input.
Customise the variable `spotlight-min-chars' to set the minimum
number of characters that must be entered before the first
spotlight search is performed. Setting `spotlight-min-chars' to a
lower number will result in more matches and can lead to slow
performance.
Use \\<spotlight-map> \\[spotlight-launch-file-filter] to filter the list of matching files by filename.
If used with a prefix argument then it will prompt the user for a
base directory to search below, otherwise it will use
`spotlight-default-base-dir' as the base directory."
(interactive "P")
;;see if prefix arg was used
(setq spotlight-user-base-dir (if arg
;;prompt for dir
(read-directory-name "base directory: ")
;;else use default
spotlight-default-base-dir))
;;run query with ivy
(let ((ivy-height spotlight-ivy-height))
(ivy-read "spotlight query: " 'ivy-mdfind-function
:initial-input initial-input
:dynamic-collection t
:sort nil
:keymap spotlight-map
:action (lambda (x)
(if spotlight-file-filter-flag
;;run filename filter
(progn (setq spotlight-file-filter-flag nil)
(spotlight-file-select-cached
ivy-text))
;;else open file
(progn (setq spotlight-file-filter-flag nil)
(find-file x)
(swiper ivy-text))))
:caller 'spotlight)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; spotlight-fast ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Function to run the ivy filter in the file list
(defun spotlight-file-select (dir query)
"Run spotlight in base directory DIR with query QUERY and filter list."
;;run query
(let (spotlight-command spotlight-result spotlight-list)
;;set up command
(setq spotlight-command (concat "mdfind -onlyin "
(shell-quote-argument
(expand-file-name dir))
" "
(shell-quote-argument query)))
;; capture to string
(setq spotlight-result (shell-command-to-string spotlight-command))
;; split to list
(setq spotlight-list (split-string spotlight-result "\n"))
(let ((ivy-height spotlight-ivy-height))
(ivy-read "filename filter: " spotlight-list
:matcher #'spotlight-filter
:re-builder #'ivy--regex
:action (lambda (x)
(find-file x)
(swiper query))))))
;; Main spotlight-fast function
;;;###autoload
(defun spotlight-fast (arg &optional initial-input)
"Search for a string in the spotlight database.
You'll be given a list of files that match. Narrow to the
filename you want by typing text to match the filename and then
selecting a file will launch `swiper' for that file to search for
your original query.
Optionally provide INITIAL-INPUT to specify the query string and
jump straight to the filename filter.
If used with a prefix argument then it will prompt the user for a
base directory to search below, otherwise it will use
`spotlight-default-base-dir' as the base directory."
(interactive "P")
;;see if prefix arg was used
(setq spotlight-user-base-dir (if arg
;;prompt for dir
(read-directory-name "base directory: ")
;;else use default
spotlight-default-base-dir))
;;run query
(let (spotlight-query spotlight-command spotlight-result spotlight-list)
;;test if initial input is used - this would be the case if called
;;from spotlight
(if initial-input
(setq spotlight-query initial-input)
;;else prompt for spotlight query
(setq spotlight-query (read-from-minibuffer "spotlight-fast query: ")))
;;set up command
(setq spotlight-command (concat "mdfind -onlyin "
(shell-quote-argument
(expand-file-name spotlight-user-base-dir))
" "
spotlight-query))
;; capture to string
(setq spotlight-result (shell-command-to-string spotlight-command))
;; split to list
(setq spotlight-list (split-string spotlight-result "\n"))
(let ((ivy-height spotlight-ivy-height))
(ivy-read "filename filter: " spotlight-list
:matcher #'spotlight-filter
:re-builder #'ivy--regex
:action (lambda (x)
(find-file x)
(swiper spotlight-query))))))
(provide 'spotlight)
;;; spotlight.el ends here