forked from gucong/emacs-sdcv
-
Notifications
You must be signed in to change notification settings - Fork 0
/
sdcv-mode.el
413 lines (369 loc) · 13.8 KB
/
sdcv-mode.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
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
;;; sdcv-mode.el --- major mode to do dictionary query through sdcv
;; Copyright 2006~2008 pluskid,
;; 2011 gucong
;;
;; Author: pluskid <[email protected]>,
;; gucong <[email protected]>
;;
;; 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 2, 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, write to the Free Software
;; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
;;; Commentary:
;; This is a major mode to view output of dictionary search of sdcv.
;; Put this file into your load-path and the following into your
;; ~/.emacs:
;; (require 'sdcv-mode)
;; (global-set-key (kbd "C-c d") 'sdcv-search)
;;; Changelog:
;; 2012/01/02
;; * New variable: `sdcv-word-processor'
;; * Breaking change:
;; for `sdcv-dictionary-list' and `sdcv-dictionary-alist',
;; non-list (non-nil) value now means full dictionary list
;; * Rewrite `sdcv-search' for both interactive and non-interactive use
;; * `sdcv-dictionary-list' is left for customization use only
;; * Better highlighting.
;;
;; 2011/06/30
;; * New feature: parse output for failed lookup
;; * Keymap modification
;;
;; 2008/06/11
;; * sdcv-mode v 0.1 init (with background process)
;;; Code:
(require 'outline)
(provide 'sdcv-mode)
(eval-when-compile
(require 'cl))
;;; ==================================================================
;;; Frontend, search word and display sdcv buffer
(defun sdcv-search (word &optional dict-list-name dict-list interactive-p)
"Search WORD through the command-line tool sdcv.
The result will be displayed in buffer named with
`sdcv-buffer-name' with `sdcv-mode' if called interactively.
When provided with DICT-LIST-NAME, query `sdcv-dictionary-alist'
to get the new dictionary list before search.
Alternatively, dictionary list can be specified directly
by DICT-LIST. Any non-list value of it means using all dictionaries.
When called interactively, prompt for the word.
Prefix argument have the following meaning:
If `sdcv-dictionary-alist' is defined,
use prefix argument to select a new DICT-LIST-NAME.
Otherwise, prefix argument means using all dictionaries.
Word may contain some special characters:
* match zero or more characters
? match zero or one character
/ used at the beginning, for fuzzy search
| used at the beginning, for data search
\ escape the character right after"
(interactive
(let* ((dict-list-name
(and current-prefix-arg sdcv-dictionary-alist
(completing-read "Select dictionary list: "
sdcv-dictionary-alist nil t)))
(dict-list
(and current-prefix-arg (not sdcv-dictionary-alist)))
(guess (or (and transient-mark-mode mark-active
(buffer-substring-no-properties
(region-beginning) (region-end)))
(current-word nil t)))
(word (read-string (format "Search dict (default: %s): " guess)
nil nil guess)))
(list word dict-list-name dict-list t)))
;; init current dictionary list
(when (null sdcv-current-dictionary-list)
(setq sdcv-current-dictionary-list sdcv-dictionary-list))
;; dict-list-name to dict-list
(when (and (not dict-list) dict-list-name)
(if (not sdcv-dictionary-alist)
(error "`sdcv-dictionary-alist' not defined"))
(setq dict-list
(cdr (assoc dict-list-name sdcv-dictionary-alist))))
;; prepare new dictionary list
(when (and dict-list (not (equal sdcv-current-dictionary-list dict-list)))
(setq sdcv-current-dictionary-list dict-list)
;; kill sdcv process
(and (get-process sdcv-process-name)
(kill-process (get-process sdcv-process-name)))
(while (get-process sdcv-process-name)
(sleep-for 0.01)))
(let ((result
(concat ">>>"
(mapconcat
(lambda (w) (sdcv-do-lookup w))
(if sdcv-word-processor
(let ((processed (funcall sdcv-word-processor word)))
(if (listp processed) processed (list processed)))
(list word))
">>>"))))
(if (not interactive-p)
result
(with-current-buffer (get-buffer-create sdcv-buffer-name)
(setq buffer-read-only nil)
(erase-buffer)
(insert result))
(sdcv-goto-sdcv)
(sdcv-mode)
(sdcv-mode-reinit))))
(defun sdcv-list-dictionary ()
"Show available dictionaries."
(interactive)
(let (resize-mini-windows)
(shell-command "sdcv -l" sdcv-buffer-name)))
(defvar sdcv-current-dictionary-list nil)
(defun sdcv-generate-dictionary-argument ()
"Generate dictionary argument for sdcv from `sdcv-current-dictionary-list'
and `sdcv-dictionary-path'."
(append
(and sdcv-dictionary-path (list "--data-dir" sdcv-dictionary-path))
(and (listp sdcv-current-dictionary-list)
(mapcan (lambda (dict)
(list "-u" dict))
sdcv-current-dictionary-list))))
;;; ==================================================================
;;; utilities to switch from and to sdcv buffer
(defvar sdcv-previous-window-conf nil
"Window configuration before switching to sdcv buffer.")
(defun sdcv-goto-sdcv ()
"Switch to sdcv buffer in other window."
(interactive)
(unless (eq (current-buffer)
(sdcv-get-buffer))
(setq sdcv-previous-window-conf (current-window-configuration)))
(let* ((buffer (sdcv-get-buffer))
(window (get-buffer-window buffer)))
(if (null window)
(switch-to-buffer-other-window buffer)
(select-window window))))
(defun sdcv-return-from-sdcv ()
"Bury sdcv buffer and restore the previous window configuration."
(interactive)
(if (window-configuration-p sdcv-previous-window-conf)
(progn
(set-window-configuration sdcv-previous-window-conf)
(setq sdcv-previous-window-conf nil)
(bury-buffer (sdcv-get-buffer)))
(bury-buffer)))
(defun sdcv-get-buffer ()
"Get the sdcv buffer. Create one if there's none."
(let ((buffer (get-buffer-create sdcv-buffer-name)))
(with-current-buffer buffer
(unless (eq major-mode 'sdcv-mode)
(sdcv-mode)))
buffer))
;;; ==================================================================
;;; The very major mode
(defvar sdcv-mode-font-lock-keywords
'(
;; dictionary name
("^-->\\(.*\\)$" . (1 sdcv-hit-face))
("^==>\\(.*\\)$" . (1 sdcv-failed-face))
("^\\(>>>.*\\)$" . (1 sdcv-heading-face))
)
"Expressions to hilight in `sdcv-mode'")
(defvar sdcv-mode-map
(let ((map (make-sparse-keymap)))
(define-key map "q" 'sdcv-return-from-sdcv)
(define-key map (kbd "RET") 'sdcv-search)
(define-key map "a" 'show-all)
(define-key map "h" 'hide-body)
(define-key map "o" 'sdcv-toggle-entry)
(define-key map "n" 'sdcv-next-entry)
(define-key map "p" 'sdcv-previous-entry)
map)
"Keymap for `sdcv-mode'.")
(define-derived-mode sdcv-mode nil "sdcv"
"Major mode to look up word through sdcv.
\\{sdcv-mode-map}
Turning on Text mode runs the normal hook `sdcv-mode-hook'."
(setq font-lock-defaults '(sdcv-mode-font-lock-keywords))
(setq buffer-read-only t)
(set (make-local-variable 'outline-regexp) "-->.*\n-->\\|==>\\|>>>")
(set (make-local-variable font-lock-string-face) nil)
)
(defun sdcv-mode-reinit ()
"Re-initialize buffer.
Hide all entrys but the first one and goto
the beginning of the buffer."
(ignore-errors
(setq buffer-read-only nil)
(sdcv-parse-failed)
(setq buffer-read-only t)
(hide-body)
(goto-char (point-min))
(forward-line 1)
(show-entry)))
(defun sdcv-parse-failed ()
(goto-char (point-min))
(let (save-word)
(while (re-search-forward "^[0-9]+).*-->\\(.*\\)$" nil t)
(let ((cur-word (match-string-no-properties 1)))
(unless (string= save-word cur-word)
(setq save-word cur-word)
(re-search-backward "^\\(.\\)" nil t)
(match-string 1)
(insert (format "\n==>%s\n" save-word)))))))
(defun sdcv-next-entry ()
(interactive)
(outline-next-heading)
(show-entry)
(recenter-top-bottom 0))
(defun sdcv-previous-entry ()
(interactive)
(outline-previous-heading)
(show-entry)
(recenter-top-bottom 0))
(defun sdcv-toggle-entry ()
(interactive)
(save-excursion
(outline-back-to-heading)
(if (not (outline-invisible-p (line-end-position)))
(hide-entry)
(show-entry))))
;;; ==================================================================
;;; Support for sdcv process in background
(defun sdcv-do-lookup (word)
"Send the word to the sdcv process and return the result."
(let ((process (sdcv-get-process)))
(process-send-string process (concat word "\n"))
(with-current-buffer (process-buffer process)
(let ((i 0) rlt done)
(while (and (not done)
(< i sdcv-wait-timeout))
(when (sdcv-match-tail sdcv-word-prompts)
(setq rlt (buffer-substring-no-properties (point-min)
(point-max)))
(setq done t))
(when (sdcv-match-tail sdcv-choice-prompts)
(process-send-string process "-1\n"))
(unless done
(sleep-for sdcv-wait-interval)
(setq i (+ i sdcv-wait-interval))))
(unless (< i sdcv-wait-timeout)
;; timeout
(kill-process process)
(error "ERROR: timeout waiting for sdcv"))
(erase-buffer)
rlt))))
(defvar sdcv-wait-timeout 2
"The max time (in seconds) to wait for the sdcv process to
produce some output.")
(defvar sdcv-wait-interval 0.01
"The interval (in seconds) to sleep each time to wait for
sdcv's output.")
(defconst sdcv-process-name "%sdcv-mode-process%")
(defconst sdcv-process-buffer-name "*sdcv-mode-process*")
(defvar sdcv-word-prompts '("Enter word or phrase: "
"请输入单词或短语:"
"請輸入單字或片語:")
"A list of prompts that sdcv use to prompt for word.")
(defvar sdcv-choice-prompts '("Your choice[-1 to abort]: "
"您的选择为:"
"您的選擇為:")
"A list of prompts that sdcv use to prompt for a choice
of multiple candicates.")
(defvar sdcv-result-patterns '("^Found [0-9]+ items, similar to [*?/|]*\\(.+?\\)[*?]*\\."
"^发现 [0-9]+ 条记录和 [*?/|]*\\(.+?\\)[*?]* 相似。"
)
"A list of patterns to extract result word of sdcv. Special
characters are stripped.")
(defun sdcv-get-process ()
"Get or create the sdcv process."
(let ((process (get-process sdcv-process-name)))
(when (null process)
(with-current-buffer (get-buffer-create
sdcv-process-buffer-name)
(erase-buffer)
(setq process (apply 'start-process
sdcv-process-name
sdcv-process-buffer-name
sdcv-program-path
(sdcv-generate-dictionary-argument)))
;; kill the initial prompt
(let ((i 0))
(message "starting sdcv...")
(while (and (not (sdcv-match-tail sdcv-word-prompts))
(< i sdcv-wait-timeout))
(sleep-for sdcv-wait-interval)
(setq i (+ i sdcv-wait-interval)))
(unless (< i sdcv-wait-timeout)
;; timeout
(kill-process process)
(error "ERROR: timeout waiting for sdcv"))
(erase-buffer))))
process))
(defun sdcv-buffer-tail (length)
"Get a substring of length LENGTH at the end of
current buffer."
(let ((beg (- (point-max) length))
(end (point-max)))
(if (< beg (point-min))
(setq beg (point-min)))
(buffer-substring-no-properties beg end)))
(defun sdcv-match-tail (prompts)
(let ((done nil)
(prompt nil))
(while (and (not done)
prompts)
(setq prompt (car prompts))
(setq prompts (cdr prompts))
(when (string-equal prompt
(sdcv-buffer-tail (length prompt)))
(delete-region (- (point-max) (length prompt))
(point-max))
(setq done t)))
done))
;;;;##################################################################
;;;; User Options, Variables
;;;;##################################################################
(defvar sdcv-buffer-name "*sdcv*"
"The name of the buffer of sdcv.")
(defvar sdcv-dictionary-list t
"A list of dictionaries to use.
Each entry is a string denoting the name of a dictionary, which
is then passed to sdcv through the '-u' command line option.
Any non-list value means using all the dictionaries.")
(defvar sdcv-dictionary-alist nil
"An alist of dictionaries, used to interactively form
dictionary list. It has the form:
((\"full\" . t)
(\"group1\" \"dict1\" \"dict2\" ...)
(\"group2\" \"dict2\" \"dict3\"))
Any cons cell here means using all dictionaries.
")
(defvar sdcv-program-path "sdcv"
"The path of sdcv program.")
(defvar sdcv-dictionary-path nil
"The path of dictionaries.")
(defvar sdcv-word-processor nil
"This is the function that take a word (stirng)
and return a word or a list of words for lookup by `sdcv-search'.
All lookup result(s) will finally be concatenated together.
`nil' value means do nothing with the original word.
The following is an example. This function takes the original word and
compare whether simplified and traditional form of the word are the same.
If not, look up both of the words.
(lambda (word)
(let ((sim (chinese-conv word \"simplified\"))
(tra (chinese-conv word \"traditional\")))
(if (not (string= sim tra))
(list sim tra)
word)))
")
(defvar sdcv-hit-face 'font-lock-type-face
"Face for search hits")
(defvar sdcv-failed-face 'font-lock-keyword-face
"Face for suggestions for a failed lookup.")
(defvar sdcv-heading-face 'highlight
"Face for heading of lookup")
;;; sdcv-mode.el ends here