-
-
Notifications
You must be signed in to change notification settings - Fork 4
/
searcher.el
242 lines (207 loc) · 8.58 KB
/
searcher.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
;;; searcher.el --- Searcher in pure elisp -*- lexical-binding: t; -*-
;; Copyright (C) 2020 Shen, Jen-Chieh
;; Created date 2020-06-19 20:12:01
;; Author: Shen, Jen-Chieh <jcs090218@gmail.com>
;; Description: Searcher in pure elisp
;; Keyword: search searcher project file text string
;; Version: 0.3.0
;; Package-Requires: ((emacs "25.1") (dash "2.10") (f "0.20.0"))
;; URL: https://github.com/jcs-elpa/searcher
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; Searcher in pure elisp
;;
;;; Code:
(require 'cl-lib)
(require 'dash)
(require 'f)
(require 'subr-x)
(defgroup searcher nil
"Searcher in pure elisp."
:prefix "searcher-"
:group 'tool
:link '(url-link :tag "Repository" "https://github.com/jcs-elpa/searcher"))
(defcustom searcher-ignore-dirs
'("/[.]log/"
"/[.]vs/" "/[.]vscode/"
"/[.]svn/" "/[.]git/" "/[.]hg/" "/[.]bzr/"
"/[.]idea/"
"/[.]tox/"
"/[.]stack-work/"
"/[.]ccls-cache/" "/[.]clangd/"
"/[.]ensime_cache/" "/[.]eunit/" "/[.]fslckout/"
"/[Bb]in/" "/[Bb]uild/" "/res/" "/[.]src/"
"/SCCS/" "/RCS/" "/CVS/" "/MCVS/" "/_MTN/" "/_FOSSIL_/"
"/_darcs/" "/{arch}/"
"/node_modules/")
"List of path you want to ignore by the searcher."
:type 'list
:group 'searcher)
(defcustom searcher-ignore-files
'("[.]gitignore" "[.]gitattributes"
"[.]meta" "[.]iso"
"[.]img" "[.]png" "[.]jpg" "[.]jpng" "[.]gif"
"[.]psd"
"[.]obj" "[.]maya" "[.]fbx"
"[.]mp3" "[.]wav"
"[.]mp4" "[.]avi" "[.]flv" "[.]mov" "[.]webm" "[.]mpg" "[.]mkv" "[.]wmv"
"[.]exe" "[.]bin"
"[.]elc" "[.]javac" "[.]pyc"
"[.]lib" "[.]dll" "[.]o" "[.]a")
"List of files you want to ignore by the searcher."
:type 'list
:group 'searcher)
(defcustom searcher-use-cache t
"Use cache to speed up the search speed."
:type 'boolean
:group 'searcher)
(defcustom searcher-search-type 'regex
"Type of the searching algorithm."
:type '(choice (const :tag "regex" regex)
(const :tag "flx" flx))
:group 'searcher)
(defcustom searcher-flx-threshold 25
"Target score we accept for outputting the search result."
:type 'integer
:group 'searcher)
(defvar searcher--cache-project-files nil
"Cache for valid project files.
Do `searcher-clean-cache' if project tree strucutre has been changed.")
;;; External
(declare-function flx-score "ext:flx.el")
;;; Util
(defun searcher--is-contain-list-string-regexp (in-list in-str)
"Check if IN-STR contain in any string in the IN-LIST."
(cl-some (lambda (lb-sub-str) (string-match-p lb-sub-str in-str)) in-list))
(defun searcher--f-directories-ignore-directories (path &optional rec)
"Find all directories in PATH by ignored common directories with FN and REC."
(let ((dirs (f-directories path)) (valid-dirs '()) (final-dirs '()))
(dolist (dir dirs)
(unless (searcher--is-contain-list-string-regexp searcher-ignore-dirs (f-slash dir))
(push dir valid-dirs)))
(when rec
(dolist (dir valid-dirs)
(push (searcher--f-directories-ignore-directories dir rec) final-dirs)))
(setq valid-dirs (reverse valid-dirs))
(setq final-dirs (reverse final-dirs))
(-flatten (append valid-dirs final-dirs))))
(defun searcher--f-files-ignore-directories (path &optional fn rec)
"Find all files in PATH by ignored common directories with FN and REC."
(let ((dirs (append (list path) (searcher--f-directories-ignore-directories path rec)))
(files '()))
(dolist (dir dirs) (push (f-files dir fn) files))
(-flatten (reverse files))))
(defun searcher--line-string ()
"Return string at line with current cursor position."
(substring (buffer-string) (1- (line-beginning-position)) (1- (line-end-position))))
(defun searcher--form-fuzzy-regex (str-or-regex)
"Convert STR-OR-REGEX to fuzzy regular expression."
(format "\\_<[%s][^ \t\n\r\f]*\\_>" str-or-regex))
;;; Core
(defun searcher--form-match (file ln-str start end ln col)
"Form a match candidate; data are FILE, START, END and LN-STR."
(list :file file :string ln-str :start start :end end :line-number ln :column col))
(defun searcher-clean-cache ()
"Clean up the cache files."
(setq searcher--cache-project-files nil))
(defun searcher--search-string (str-or-regex)
"Return search string depends on `searcher-search-type' and STR-OR-REGEX."
(cl-case searcher-search-type
('regex str-or-regex)
('flx (searcher--form-fuzzy-regex str-or-regex))))
(defun searcher--search-cons (str-or-regex start-pt fuzzy-regex)
"Return cons that form by (start point, end point).
Argument STR-OR-REGEX is the input of search string.
Argument START-PT is the starting search point.
Argument FUZZY-REGEX is regular expression for fuzzy matching."
(let ((buf-str (buffer-string)) start end
match-str score-data score good-score-p break-it)
(cl-case searcher-search-type
('regex
(setq start (ignore-errors (string-match str-or-regex buf-str start-pt)))
(when start (setq start (1+ start) end (1+ (match-end 0)))))
('flx
(while (not break-it)
(setq start (ignore-errors (string-match fuzzy-regex buf-str start-pt)))
(if (not start)
(setq break-it t)
(setq end (match-end 0)
match-str (substring buf-str start end)
score-data (flx-score match-str str-or-regex)
score (if score-data (nth 0 score-data) nil)
good-score-p (if score (< searcher-flx-threshold score) nil))
(if good-score-p
(progn
(setq start (1+ start) end (1+ end))
(setq break-it t))
(setq start-pt (1+ start)))))))
(if (and start end) (cons start end) nil)))
(defun searcher--init ()
"Initialize searcher."
(cl-case searcher-search-type
('regex )
('flx (require 'flx))))
;;;###autoload
(defun searcher-search-in-project (str-or-regex)
"Search STR-OR-REGEX from the root of project directory."
(let ((project-path (cdr (project-current))))
(if project-path
(searcher-search-in-path project-path str-or-regex)
(error "[ERROR] No project root folder found from default path"))))
;;;###autoload
(defun searcher-search-in-path (path str-or-regex)
"Search STR-OR-REGEX from PATH."
(let ((result '()))
(when (or (not searcher--cache-project-files)
(not searcher-use-cache))
(setq searcher--cache-project-files
(searcher--f-files-ignore-directories
path
(lambda (file) ; Filter it.
(not (searcher--is-contain-list-string-regexp searcher-ignore-files file)))
t)))
(dolist (file searcher--cache-project-files)
(push (searcher-search-in-file file str-or-regex) result))
(-flatten-n 1 result)))
;;;###autoload
(defun searcher-search-in-file (file str-or-regex)
"Search STR-OR-REGEX in FILE."
(searcher--init)
(let ((matchs '()) (match "") (ln-str "") (ln 1) (col nil)
(ln-pt 1) delta-ln
(search-cons t) start (end 0)
(fuzzy-regex (searcher--search-string str-or-regex)))
(unless (string-empty-p str-or-regex)
(with-temp-buffer
(if (file-exists-p file)
(insert-file-contents file)
(insert (with-current-buffer file (buffer-string))))
(while search-cons
(setq search-cons (searcher--search-cons str-or-regex end fuzzy-regex))
(when search-cons
(setq start (car search-cons) end (cdr search-cons))
(goto-char start)
(setq ln-str (searcher--line-string)
col (current-column))
(setq delta-ln (1- (count-lines ln-pt start)) ; Calculate lines.
;; Function `count-lines' missing 1 if column is at 0, so we
;; add 1 back to line if column is 0.
ln (+ ln delta-ln (if (= col 0) 1 0))
ln-pt start)
(setq match (searcher--form-match file ln-str start end ln col))
(push match matchs)
(setq end (1+ end))))))
matchs))
(provide 'searcher)
;;; searcher.el ends here