-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathesqlite-helm.el
394 lines (341 loc) · 13.4 KB
/
esqlite-helm.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
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
;;; esqlite-helm.el --- Define helm source for sqlite database
;; Author: Masahiro Hayashi <[email protected]>
;; Keywords: data
;; URL: https://github.com/mhayashi1120/Emacs-esqlite
;; Emacs: GNU Emacs 24 or later
;; Package-Requires: ((esqlite "0.2.0") (helm "20131207.845"))
;; Version: 0.3.0
;; 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:
;;; TODO:
;; * GLOB syntax is greater than LIKE syntax
;; http://pokutuna.hatenablog.com/entry/20111113/1321126659
;; * add samples
;;; Code:
(require 'cl-lib)
(require 'esqlite)
(require 'pcsv)
;; do not require helm
(declare-function helm-log "helm")
(declare-function helm-get-current-source "helm")
(defvar helm-pattern)
(defvar helm-async-processes)
;;;
;;; Esqlite for helm
;;;
(defvar esqlite-helm-history nil)
(defface esqlite-helm-finish
'((t (:foreground "Green")))
"Face used in mode line when sqlite is finish."
:group 'helm-grep)
;;;###autoload
(defun esqlite-helm-define (source)
"This function provides extension while `helm' source composing.
Normally, should not override `candidates-process', `candidates',
`candidate-transformer' directive.
Following esqlite specific directive:
* `sqlite-db' File name of sqlite database or esqlite stream or
symbol point to filename.
* `sqlite-async' Indicate SOURCE is async.
You must choose `sqlite-table' or `sqlite-composer' directive.
* `sqlite-table' Table name of result set. This directive only meaningful
`sqlite-composer' is not specified.
`sqlite-column' Column name of you desired to search.
`sqlite-display-column' Column name of select.
* `sqlite-composer' Function which accept one argument `helm-pattern' and return
a sql query string.
Following default directive:
`real-to-display' directive accept a list of string, generated by SELECT statement
from `sqlite-composer' or `sqlite-table'. Default behavior is displaying all columns
of SELECT. `volatile'
Other helm directive is overridable.
If SOURCE doesn't contain `sqlite-async', you should add LIMIT statement
to SQL which composed by `sqlite-composer'.
See the syntax of LIMIT statement.
http://www.sqlite.org/lang_select.html
Example:
\(helm (esqlite-helm-define
`((sqlite-db . \"/path/to/some/sqlite.db\")
(sqlite-table . \"tbl1\"))))
"
(let ((file-or-stream (assoc-default 'sqlite-db source))
(composer (assoc-default 'sqlite-composer source))
(async (assoc 'sqlite-async source))
file stream)
(cond
((stringp file-or-stream)
(setq file file-or-stream))
((esqlite-stream-p file-or-stream)
(setq stream file-or-stream))
((symbolp file-or-stream)
(setq file file-or-stream))
(t
(error "esqlite-helm: Not a valid filename or stream sqlite-db:%s"
file-or-stream)))
(cond
((and composer (not (functionp composer)))
(error "esqlite-helm: Not a valid function `sqlite-composer'"))
((and (null composer))
(setq composer (esqlite-helm--construct-composer
file-or-stream source))))
(let ((result
;; dequote all cdr to `cons' the list
`((name . ,"esqlite")
(real-to-display . ,'esqlite-helm-make-one-line)
,@(cond
(stream
`((candidates
.
(lambda ()
(esqlite-helm-call-stream
,stream
(funcall ',composer helm-pattern))))
;; suppress caching
(volatile)))
(async
`((candidates-process
.
(lambda ()
(esqlite-helm-start-command
,file
(funcall ',composer helm-pattern))))
(candidate-transformer . esqlite-helm-hack-for-multiline)))
(t
`((candidates
.
(lambda ()
(esqlite-helm-call-command
,file
(funcall ',composer helm-pattern))))
;; suppress caching
(volatile))))
(match . ,'identity)
(history . ,'esqlite-helm-history))))
(dolist (s source)
(let ((cell (assq (car-safe s) result)))
(cond
(cell
(setcdr cell (cdr s)))
(t
(setq result (cons s result))))))
result)))
(defun esqlite-helm--construct-composer (file-or-stream source)
(let* ((table (assoc-default 'sqlite-table source))
(column (assoc-default 'sqlite-column source))
(dispcolumn (assoc-default 'sqlite-display-column source))
(columns (esqlite-read-table-columns file-or-stream table))
(limit (or (assoc-default 'candidate-number-limit source) 100)))
(unless table
(error "esqlite-helm: `sqlite-table' is missing"))
(let ((dispcolumns (if dispcolumn (list dispcolumn) columns)))
`(lambda (pattern)
(let ((dispcolumns ',dispcolumns)
(table ,table)
(limit ,limit)
(search-columns ',(if column (list column) columns))
(likepat (esqlite-helm-glob-to-fuzzy-like pattern)))
(esqlite-prepare
'("SELECT %O{dispcolumns}"
" FROM %o{table}"
" WHERE %s{where}"
" LIMIT %s{limit}")
:where (mapconcat
(lambda (col)
(esqlite-prepare
"%o{col} LIKE %T{likepat} ESCAPE '\\'"
:col col))
search-columns
" OR ")
:dispcolumns dispcolumns
:table table
:limit limit))))))
(defun esqlite-helm-match-function (cand)
(string-match (esqlite-helm-glob-to-regexp helm-pattern) cand))
(defun esqlite-helm-call-stream (stream query)
(condition-case err
(mapcar
'esqlite-helm--construct-row
(esqlite-stream-read stream query))
(error
(helm-log "Error: esqlite-helm %s"
(replace-regexp-in-string
"\n" ""
(prin1-to-string (cdr err)))))))
(defun esqlite-helm-call-command (file query)
(condition-case err
(mapcar
'esqlite-helm--construct-row
(with-temp-buffer
(unless (zerop (esqlite-call-csv-process file query ""))
(error "esqlite: Process exited abnormally %s" (buffer-string)))
(goto-char (point-min))
(pcsv-parse-buffer)))
(error
(helm-log "Error: esqlite %s"
(replace-regexp-in-string
"\n" ""
(prin1-to-string (cdr err)))))))
(defun esqlite-helm-start-command (file query)
"Initialize async locate process for `helm-source-locate'."
;; esqlite-helm implementation ignore NULL. (NULL is same as a empty string)
(let ((proc (esqlite-start-csv-process file query "")))
(set-process-sentinel proc 'esqlite-helm--process-sentinel)
proc))
(defun esqlite-helm--process-sentinel (proc event)
(when (memq (process-status proc) '(exit signal))
(let ((buf (process-buffer proc)))
(when (buffer-live-p buf)
(kill-buffer buf)))
(unless (zerop (process-exit-status proc))
(helm-log "Error: esqlite %s"
(replace-regexp-in-string "\n" "" event)))))
(defun esqlite-helm-hack-for-multiline (candidates)
;; helm split csv stream by newline. restore the csv as one text
;; and try to parse it.
;; No newline in one value. (No problem)
;; OUTPUT-STRING: a,b\nc,d\ne CANDIDATES: ("a,b" "c,d") INCOMPLETE-LINE: "e"
;; OUTPUT-STRING: a,b\nc,d\n CANDIDATES: ("a,b" "c,d") INCOMPLETE-LINE: ""
;; OUTPUT-STRING: a,b\nc,d CANDIDATES: ("a,b") INCOMPLETE-LINE: "c,d"
;; There is newline and quote by double-quote.
;; OUTPUT-STRING: a,b\nc,"d\nD"\ne CANDIDATES: ("a,b" "c,\"d\nD\"") INCOMPLETE-LINE: "e"
;; OUTPUT-STRING: a,b\nc,"d\nD"\n CANDIDATES: ("a,b" "c,\"d\nD\"") INCOMPLETE-LINE: ""
;; OUTPUT-STRING: a,b\nc,"d\nD" CANDIDATES: ("a,b" "c,\"d") INCOMPLETE-LINE: "D\""
;; OUTPUT-STRING: a,b\nc,"d\nD CANDIDATES: ("a,b" "c,\"d") INCOMPLETE-LINE: "D"
;; OUTPUT-STRING: a,b\nc,"d\n CANDIDATES: ("a,b" "c,\"d") INCOMPLETE-LINE: ""
;; OUTPUT-STRING: a,b\nc,"d CANDIDATES: ("a,b") INCOMPLETE-LINE: "c,\"d"
(condition-case err
(let* ((rawtext (concat (mapconcat 'identity candidates "\n") "\n"))
(source (helm-get-current-source))
(incomplete-info (assq 'incomplete-line source)))
(cl-destructuring-bind (data rest) (esqlite-helm--read-csv rawtext)
(setcdr incomplete-info (concat rest (cdr incomplete-info)))
data))
(error
(message "%s" err)
nil)))
;; try to read STRING until end.
;; csv line may not terminated and may contain newline.
(defun esqlite-helm--read-csv (string)
(with-temp-buffer
(insert string)
(goto-char (point-min))
(let ((res '())
(start (point)))
(condition-case nil
(while (not (eobp))
(let ((ln (esqlite--read-csv-line)))
(setq start (point))
(setq res (cons (esqlite-helm--construct-row ln) res))))
(invalid-read-syntax))
(list (nreverse res)
(buffer-substring-no-properties start (point-max))))))
(defun esqlite-helm--construct-row (csv-line)
;; TODO investigate it! helm seems ignore first item of list. what is it?
(cons 'dummy csv-line))
;;;
;;; Any utilities
;;;
(defun esqlite-helm-make-one-line (row &optional width)
"To display TEXT as a helm line."
(let* ((text (mapconcat (lambda (x)
(cond
((stringp x) x)
((eq x :null) "")
(t "")))
row " "))
(oneline (replace-regexp-in-string "\n" " " text)))
(truncate-string-to-width oneline (or width (window-width)))))
;;FIXME regexp-quote
(defun esqlite-helm-glob-to-regexp (glob &optional escape-char)
(esqlite-parse-replace
glob
'((?* . ".*")
(?? . ".?")
(?\\ (?* . "*") (?\? . "?") (?\\ . "\\")))))
(defun esqlite-helm-split-fuzzy-glob (glob &optional escape-char)
(let (prefix suffix)
(cond
((string-match "\\`\\^" glob)
(setq glob (substring glob 1)))
((string-match "\\`\\\\\\^" glob)
;; escaped ^.
(setq glob (substring glob 1)
prefix t))
(t
(setq prefix t)))
(cond
((and (eq escape-char ?\\)
(string-match "\\`\\(?:\\\\.\\|[^\\\\]\\)*\\\\$\\'" glob))
;; end with escaped `$'.
;; consider "\\$" <- escaped backslash end with `$'
(setq glob (concat (substring glob 0 -2) "$"))
(setq suffix t))
((and (not (eq escape-char ?\\))
;; end with escaped `$' "\\$"
(string-match "\\\\\\$\\'" glob))
(setq glob (concat (substring glob 0 -2) "$"))
(setq suffix t))
((string-match "\\$\\'" glob)
;; end of non-escaped `$'
(setq glob (substring glob 0 -1)))
(t
(setq suffix t)))
(list prefix glob suffix)))
;;;###autoload
(defun esqlite-helm-glob-to-like (glob &optional escape-char)
"Convenient function to provide unix like GLOB convert to sql like pattern.
`*' Like glob, match to text more equal zero.
`?' Like glob, match to a char in text.
Above syntax can escape by \\ (backslash). But no relation to ESCAPE-CHAR.
See related information at `esqlite-escape-like'.
e.g. hoge*foo -> hoge%foo
hoge?foo -> hoge_foo"
(esqlite-parse-replace
glob
(esqlite-escape--like-table
escape-char
`((?* . "%")
(?\? . "_")
(?\\ (?\* . "*")
(?\? . "?")
,@(if (or (eq ?\\ escape-char) (null escape-char))
'((?\\ . "\\\\"))
'((?\\ . "\\"))))))))
;;;###autoload
(defun esqlite-helm-glob-to-fuzzy-like (glob &optional escape-char)
"Convert pseudo GLOB to like syntax to support helm behavior.
Following extended syntax:
`^': Like regexp, match to start of text.
`$': Like regexp, match to end of text.
Above syntax can escape by \\ (backslash). But no relation to ESCAPE-CHAR.
See related information at `esqlite-escape-like'.
ESCAPE-CHAR pass to `esqlite-helm-glob-to-like'"
(cl-destructuring-bind (prefix pattern suffix)
(esqlite-helm-split-fuzzy-glob glob (or escape-char ?\\))
(concat
(and prefix "%")
(esqlite-helm-glob-to-like pattern escape-char)
(and suffix "%"))))
;;;###autoload
(defun esqlite-helm-glob-to-fuzzy-glob (glob)
"Convert GLOB to fuzzy glob to support helm behavior
There are extended syntax `^' `$'. See `esqlite-helm-glob-to-fuzzy-like'."
(cl-destructuring-bind (prefix pattern suffix)
(esqlite-helm-split-fuzzy-glob glob)
(concat
(and prefix "*")
pattern
(and suffix "*"))))
(provide 'esqlite-helm)
;;; esqlite-helm.el ends here