Skip to content

Instantly share code, notes, and snippets.

@QiangF
Created January 29, 2020 12:53
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save QiangF/7673e50c837c72cd63c120b2633aec87 to your computer and use it in GitHub Desktop.
Save QiangF/7673e50c837c72cd63c120b2633aec87 to your computer and use it in GitHub Desktop.
;; https://github.com/merrickluo/liberime
;; https://github.com/tumashu/pyim/
;; https://github.com/d5884/mozc-im/blob/master/mozc-im.el
;; https://github.com/google/mozc/blob/master/src/unix/emacs/mozc.el
;; https://github.com/jwiegley/emacs-release/blob/master/lisp/international/quail.el
(require 'cl-lib)
(require 'popup nil t)
(defface rime-preview--face
'((t (:foreground "#ffff00" :underline t)))
"光标处预览字符串")
(defface rime-prompt--selection
'((t (:foreground "#ffff00" :background "gray44")))
"minibuffer 选词框")
(defvar rime-exhibit-delay 0.15)
(defvar rime-probe-list
'(rime-probe-evil-normal-mode
rime-probe-program-mode
rime-probe-cn-en-switch))
(defvar rime-prompt--candidate-per-page 5)
(defvar rime-title "中")
(defcustom rime-punctuation
'(
("'" "‘" "’")
("\"" "“" "”")
("?" "?")
(";" ";")
(":" ":")
("/" "、")
("." "。")
("," ",")
(")" ")")
("(" "(")
("]" "】")
("[" "【")
("_" "——")
("^" "…")
("!" "!")
("`" "・")
("~" "~")
("|" "÷")
(">" "》")
("=" "=")
("<" "《")
("-" "-")
("+" "+")
("$" "¥")
;; ("*" "×")
;; ("@" "◎")
;; ("&" "※")
;; ("%" "%")
;; ("#" "#")
;; ("{" "『")
;; ("}" "』")
)
"标点符号表."
:group 'rime
:type 'list)
(defun rime-load-pyim-dict (dict-file)
(with-temp-buffer
(insert-file-contents dict-file)
(buffer-string)))
(defvar rime-local-variable-list
'(input-method-function
deactivate-current-input-method-function
rime-backend
rime-preview--overlay
rime-preview-string
rime-prompt--candidate-list
;; all index start from 0
rime-prompt--index
rime-prompt--subindex
rime-exhibit-timer
rime-last-punctuation
rime-output
rime-translating))
(dolist (var rime-local-variable-list)
(defvar var nil)
(make-variable-buffer-local var)
(put var 'permanent-local t))
;; ASCII中的0~31为控制字符;32~126为打印字符;127为Delete(删除)命令 128~255为扩展字符
(defun rime-make-self-insert-map (binded-function)
(let ((map (make-sparse-keymap))
(i ?\ ))
(prog1 map
(setq i 32)
(while (< i 127)
(define-key map (vector i) binded-function)
(setq i (1+ i)))
(setq i 128)
(while (< i 256)
(define-key map (vector i) binded-function)
(setq i (1+ i))))))
;; this way rime.el be used in `ansi-term' Char-mode
(defvar rime-auxiliary-map
(let ((map (rime-make-self-insert-map 'rime-self-insert-command))
(i ?\ ))
(dolist (i (number-sequence ?1 ?9))
(define-key map (char-to-string i) 'rime-prompt--select-candidate-by-number))
(define-key map " " 'rime-prompt--select-candidate)
(define-key map "\C-d" 'rime-preedit-delete-forward)
(define-key map "\C-h" 'rime-preedit-delete-backward)
(define-key map [deletechar] 'rime-preedit-delete-forward)
;; delete和deletechar不同
(define-key map [delete] 'rime-preedit-delete-backward)
(define-key map [backspace] 'rime-preedit-delete-backward)
(define-key map "\C-?" 'rime-preedit-delete-backward)
(define-key map "\C-f" 'rime-preedit-forward-point)
(define-key map "\C-b" 'rime-preedit-backward-point)
(define-key map "\C-e" 'rime-preedit-end-of-line)
(define-key map "\C-a" 'rime-preedit-beginning-of-line)
(define-key map "\C-n" 'rime-prompt--next-word)
(define-key map "\C-p" 'rime-prompt--previous-word)
(define-key map "\M-n" 'rime-prompt--next-page)
(define-key map "\M-p" 'rime-prompt--previous-page)
;; "<return>" enter 键是newline, 即\n
;; Ctrl-m (or "\r") 是ASCII码为13的那个换行(Carriage Return), Emacs还有一个记法"RET"
;; 默认配置下按Enter键的时候实际产生的是"RET", 是通过translate得到的.
;; 可以通过帮助来证实这一点: 按F1然后输入?然后输入c, 此时会在mini
;; buffer中提示你输入按键组合, 此时按Enter键. mini buffer中会提示输
;; 入的是RET, (translated from <return>).
(define-key map (kbd "<return>") 'rime-quit-no-clear)
;; (define-key map [return] 'rime-quit-no-clear)
(define-key map "\C-m" 'rime-quit-no-clear)
(define-key map "\C-g" 'rime-quit-clear)
map)
"serves as a lookup table, not used as a mode map")
(defun rime-probe-program-mode ()
"中文输入限制在字符串和 comment 中"
(interactive)
(when (derived-mode-p 'prog-mode)
(let* ((ppss (syntax-ppss (point))))
(not
(or (car (setq ppss (nthcdr 3 ppss)))
(car (setq ppss (cdr ppss)))
(nth 3 ppss))))))
(defun rime-probe-evil-normal-mode ()
(evil-normal-state-p))
(defun rime-char-before-to-string (num)
"得到当前行光标前第 `num' 个字符,转换为字符串。"
(let* ((point (point))
(point-before (- point num)))
(when (and (> (- point-before (line-beginning-position)) 0)
(char-before point-before))
(char-to-string (char-before point-before)))))
(defun rime-string-match-p (regexp string &optional start)
"如果 REGEXP 和 STRING 是非字符串不会报错。"
(and (stringp regexp)
(stringp string)
(string-match-p regexp string start)))
(defun rime-probe-cn-en-switch ()
(let ((str-1 (rime-char-before-to-string 0))
(str-2 (rime-char-before-to-string 1)))
;; exclude exwm mode buffer
(when str-1
(unless (string= (buffer-name) " *temp*")
(if (rime-string-match-p " " str-1)
;; 中文后面紧接1个空格切换到英文输入
(rime-string-match-p "\\cc" str-2)
;; 输入数字或英文标点等不经过输入法转换的字符
;; 或强制输入英文字符后切换到英文输入
(and (not (rime-string-match-p "\\cc" str-1))
;; exclue punctuation
(not (looking-back "[[:punct:]]+"))
(= (length (rime-preedit-get)) 0)))))))
;;;###autoload
(defun rime-register-input-method ()
(defvar rime-user-symbol (rime-load-pyim-dict "~/.dotfiles/emacs/emacs.d/pyim/symbol.pyim"))
(defvar rime-user-acronym (rime-load-pyim-dict "~/.dotfiles/emacs/emacs.d/pyim/acronym.pyim"))
(register-input-method "rime" "euc-cn" 'rime-activate rime-title))
;;;###autoload
(defun rime-activate (name)
(interactive)
(mapc 'make-local-variable rime-local-variable-list)
(setq input-method-function 'rime-input-method)
(setq deactivate-current-input-method-function #'rime-deactivate)
(when (eq (selected-window) (minibuffer-window))
(add-hook 'minibuffer-exit-hook 'rime-exit-from-minibuffer)))
(defun rime-deactivate ()
(mapc 'kill-local-variable rime-local-variable-list))
(defun rime-exit-from-minibuffer ()
(deactivate-input-method)
(when (<= (minibuffer-depth) 1)
(remove-hook 'minibuffer-exit-hook 'rime-exit-from-minibuffer)))
;; start a translation session
(defun rime-input-method (key)
"when read-event reads a printing character (including <SPC>)
with no modifier bits, it calls that function, passing the
character as an argument.
Events returned by the input method function are not passed to
the input method function again, even if they are printing
characters with no modifier bits.
The input method function is not called when reading the second
and subsequent events of a key sequence. Thus, these characters
are not subject to input method processing. "
(if (or buffer-read-only
(not enable-multibyte-characters)
;; The input method function should test the values of overriding-local-map and
;; overriding-terminal-local-map; if either of these variables is non-nil, the input
;; method should put its argument into a list and return that list with no further
;; processing.
overriding-terminal-local-map
overriding-local-map)
(list key)
(rime-preview--setup-preview-overlay)
(with-silent-modifications
(unwind-protect
(let ((input-string (rime-start-translation key)))
(when (and (stringp input-string)
(> (length input-string) 0))
(mapcar 'identity input-string)))
(rime-preview--delete-overlay)))))
(defun rime-start-translation (key)
(let* ((echo-keystrokes 0)
(help-char nil)
(overriding-terminal-local-map rime-auxiliary-map)
;; bind input-method-function to nil to prevent recursion.
(input-method-function nil)
(input-method-exit-on-first-char nil)
; Hide the original `buffer-undo-list'.
(buffer-undo-list t)
;; preview string in buffer, see quail.el
(input-method-use-echo-area nil)
(modified-p (buffer-modified-p))
(inhibit-modification-hooks t)
(inhibit-quit t)
last-command-event last-command this-command result)
(setq rime-translating t)
(setq rime-output nil)
(rime-with-preedit (erase-buffer))
;; Push back the last event on the event queue.
(and key (push key unread-command-events))
;; (when key (setq unread-command-events
;; (cons key unread-command-events)))
(while rime-translating
(let* ((keyseq (read-key-sequence-vector nil nil nil t))
(rime-cmd (lookup-key rime-auxiliary-map keyseq)))
(if (and (not (rime-english-context-p))
(if (> (length (rime-preedit-get)) 0)
(commandp rime-cmd)
;; ignore command other than self-insert when preedit is empty
(eq rime-cmd 'rime-self-insert-command)))
(progn
(set-buffer-modified-p modified-p)
(setq current-input-method-title rime-title)
(setq last-command-event (aref keyseq (1- (length keyseq)))
last-command this-command
this-command rime-cmd)
(condition-case-unless-debug err
(call-interactively rime-cmd)
(error (message "Input method error: %S" err)
(beep)))
(setq result rime-output))
; keyseq doen't have any meaning in the context of the input method
(setq current-input-method-title "英")
(rime-quit-no-clear)
;; option 1: just return the keyseq string
(message "keyseq: %s" keyseq)
(setq result (this-command-keys))
;; option 2: lookup keybinding, execute the corresponding command
;; don't push to the event queue, unprocessed event on event queue will send to the input method again
;; (dolist (i (number-sequence 0 (1- (length keyseq))))
;; (push (aref keyseq i) unread-command-events))
)))
result))
(defun rime-fall-back-on-default-binding (last-event-or-keys)
"Execute a command as if the command loop does.
Read a complete set of user input and execute the command bound to
the key sequence in almost the same way of the command loop.
LAST-EVENT is the last event which a user has input. The last event is pushed
back and treated as if it's the first event of a next key sequence."
(unwind-protect
(let* (keys bind)
(if (eventp last-event-or-keys)
(progn
;; Push back the last event on the event queue.
(and last-event-or-keys (push last-event-or-keys unread-command-events))
(setq keys (read-key-sequence-vector nil))
(setq bind (key-binding keys t)))
(let ((overriding-terminal-local-map nil))
(setq keys last-event-or-keys)
(setq bind (key-binding keys t))))
(if bind
(call-interactively bind nil keys)
(let (message-log-max)
(message "%s is undefined" (key-description keys))
(undefined))))))
(defun rime-english-context-p ()
(cl-some #'(lambda (x)
(if (functionp x) (funcall x) nil))
rime-probe-list))
(defun rime-self-insert-command ()
(interactive "*")
(let* ((char-str (char-to-string last-command-event))
(match-punctuation-list (cl-some (lambda (x)
(when (member char-str x) x))
rime-punctuation)))
(cond
((and (= (length (rime-preedit-get)) 0) (string-equal char-str "v"))
(rime-punctuation-toggle))
;; puctuation
(match-punctuation-list
(if rime-last-punctuation
;; no conversion for two consecutive puctuations
(let* ((point (point)))
(electric-pair-delete-pair 1)
(setq char-str (concat rime-last-punctuation char-str))
(setq rime-output char-str)
(message "rime output %s" rime-output))
(rime-preview--delete-preview)
;; insert the first match, let electric pair mode finish the puctuation pairing
;; accecpt preview and terminate immediately
(setq rime-output (concat rime-output rime-preview-string (nth 1 match-punctuation-list))))
(rime-terminate-translation)
(setq rime-last-punctuation char-str))
;; other char
(t
(setq rime-last-punctuation nil)
(rime-with-preedit
(insert char-str))
(rime-refresh-candidate)))))
(defun rime-punctuation-toggle ()
(interactive)
(when (looking-back "[[:punct:]]+")
(let* ((matched-list (string-to-list (match-string-no-properties 0)))
(replacement (seq-reduce 'concat
(seq-map #'rime-find-punctuation-replacement matched-list) "")))
(save-excursion
(message "%s %s" (type-of replacement) replacement)
(replace-match replacement))))
(setq rime-output nil)
(rime-terminate-translation))
(defun rime-find-punctuation-replacement (input-char)
(let* ((input-str (char-to-string input-char))
(matching-list
(cl-some (lambda (x)
(when (member input-str x) x))
rime-punctuation))
;; input-char 在其对应的标点列表中的位置。
(position (cl-position input-str matching-list
:test #'equal))
(next-position (mod (1+ position) (length matching-list))))
(nth next-position matching-list)))
(defun rime-refresh-candidate (&optional no-delay)
"延迟(秒)显示备选词"
(let ((input (rime-preedit-get)))
(when rime-exhibit-timer
(cancel-timer rime-exhibit-timer))
(if (= (length input) 0)
(progn (rime-preview--delete-preview)
(rime-terminate-translation))
(cond
((or no-delay (not rime-exhibit-delay) (eq rime-exhibit-delay 0))
(rime-refresh-candidate-nodelay input))
(t (setq rime-exhibit-timer
(run-with-timer rime-exhibit-delay nil #'rime-refresh-candidate-nodelay input)))))))
(defun rime-refresh-candidate-nodelay (input)
(when (not rime-backend)
(if (and (or (eq ?i (elt input 0))
(eq ?u (elt input 0))))
(setq rime-backend "regex")
(setq rime-backend "rime")))
(setq rime-prompt--candidate-list
(or (delete-dups
(funcall (intern (format "rime-get-candidate-list:%s" rime-backend)) input))
(list input)))
;; init candidate position
(setq rime-prompt--index 0)
(rime-preview--refresh)
(rime-prompt--refresh))
(defun rime-get-candidate-list:rime (input)
(when (functionp 'liberime-clear-composition)
(liberime-clear-composition)
(dolist (key (string-to-list input))
(liberime-process-key key))
(let* ((context (liberime-get-context))
(menu (alist-get 'menu context)))
(alist-get 'candidates menu))))
(defun rime-get-candidate-list:regex (input)
(let ((case-fold-search t)
(start 0)
(pattern (concat "^\\(" input "[a-z]*\\)" " \\(.+\\)"))
content word output)
(cond ((eq ?i (elt input 0))
(setq content rime-user-symbol))
((eq ?u (elt input 0))
(setq content rime-user-acronym))
(t (message "Regex search begins with i or u!")))
(while (and content (< start (length content))
(setq start (string-match pattern content start)))
;; 提取词
(setq code (match-string-no-properties 1 content))
(setq word (match-string-no-properties 2 content))
(when word
(cond
((string-match "^[^ ]+$" word)
;; 单个词
(push word output))
(t
;; 多个字
(setq output (append (nreverse (split-string word " +")) output)))))
;; 继续搜索
(setq start (+ start 2 (length code) (length word))))
output))
(defun rime-terminate-translation ()
(message "terminating")
(setq rime-translating nil)
(setq rime-backend nil)
(setq rime-prompt--candidate-list nil)
(setq rime-preview-string nil)
(when rime-exhibit-timer
(cancel-timer rime-exhibit-timer))
;; (setq mini-modeline--command-state 'end
;; echo-keystrokes 0.25)
(rime-with-preedit (erase-buffer)))
(defun rime-prompt--refresh ()
(setq rime-prompt--subindex
(- rime-prompt--index (rime-prompt--page-start-index)))
;; Show page.
(when (and (null unread-command-events)
(null unread-post-input-method-events))
(if (eq (selected-window) (minibuffer-window))
;; minibuffer 中输入中文时,使用当前输入的下一行来显示候选词。
(rime-prompt--minibuffer-message
(concat "\n" (rime-prompt--format-prompt)))
;; 普通 buffer
(let ((message-log-max nil))
(cond
;; exwm-xim uses " *temp*" buffer for input, page should be showed in minibuffer.
((equal (buffer-name) " *temp*")
(message (rime-prompt--format-prompt)))
(t
(popup-tip (rime-prompt--format-prompt)
:point (overlay-start rime-preview--overlay)
:margin 1)))))))
(defun rime-prompt--current-page ()
(1+ (/ rime-prompt--index rime-prompt--candidate-per-page)))
(defun rime-prompt--total-page ()
(1+ (/ (1- (length rime-prompt--candidate-list)) rime-prompt--candidate-per-page)))
(defun rime-prompt--page-start-index ()
(max 0 (- rime-prompt--index
(mod rime-prompt--index rime-prompt--candidate-per-page))))
(defun rime-prompt--page-end-index (&optional finish)
(min (+ (rime-prompt--page-start-index) (1- rime-prompt--candidate-per-page))
(1- (length rime-prompt--candidate-list))))
(defun rime-prompt--next-page (arg)
(interactive "p")
(let ((new-index
(min (+ rime-prompt--index (* rime-prompt--candidate-per-page arg))
(1- (length rime-prompt--candidate-list)))))
(setq rime-prompt--index (if (> new-index 0) new-index 0)
;; move to page start
rime-prompt--index (rime-prompt--page-start-index))
(rime-preview--refresh)
(rime-prompt--refresh)))
(defun rime-prompt--previous-page (arg)
(interactive "p")
(rime-prompt--next-page (- arg)))
(defun rime-prompt--next-word (arg)
(interactive "p")
(let ((new-index (+ rime-prompt--index arg)))
(setq rime-prompt--index (if (> new-index 0) new-index 0))
(rime-preview--refresh)
(rime-prompt--refresh)))
(defun rime-prompt--previous-word (arg)
(interactive "p")
(rime-prompt--next-word (- arg)))
(defun rime-prompt--minibuffer-message (string)
"minibuffer 中需要将原来显示的信息和选词框整合在一起显示"
(message nil)
(let ((inhibit-quit t)
point-1)
(save-excursion
(insert string)
(setq point-1 (point)))
(sit-for 1000000)
(delete-region (point) point-1)
(when quit-flag
(setq quit-flag nil
unread-command-events '(7)))))
(defun rime-prompt--create-preedit-view ()
;; | 显示光标位置的字符
(rime-with-preedit
(concat (buffer-substring-no-properties 1 (point))
"|"
(buffer-substring-no-properties (point) (point-max)))))
(defun rime-prompt--create-candidate-list ()
"这个函数用于创建在 page 中显示的备选词条菜单。"
(let ((i 0)
(candidates (cl-subseq rime-prompt--candidate-list
(rime-prompt--page-start-index)
(1+ (rime-prompt--page-end-index))))
result)
(dolist (candidate candidates)
(when (> i 0)
(push " " result))
;; 高亮当前选择的词条
(push (if (= i rime-prompt--subindex)
(propertize (format "[%d.%s]" (1+ i) candidate)
'face 'rime-prompt--selection)
(format "%d.%s" (1+ i) candidate))
result)
(setq i (1+ i)))
(mapconcat #'identity (reverse result) "")))
(defun rime-prompt--format-prompt ()
(format "[%s]: %s(%s/%s)"
(rime-prompt--create-preedit-view)
(rime-prompt--create-candidate-list)
(rime-prompt--current-page)
(rime-prompt--total-page)))
(defun rime-prompt--select-candidate ()
"从选词框中选择当前词条,专门用于 rime 输入法支持。"
(interactive)
(if (> 1 (length rime-prompt--candidate-list))
(progn (setq rime-output " ")
(rime-terminate-translation))
(cond ((string-equal rime-backend "rime")
(liberime-select-candidate rime-prompt--index)
(rime-preview--delete-preview)
(setq rime-output (concat rime-output
(nth rime-prompt--index
rime-prompt--candidate-list)))
(let* ((context (liberime-get-context)))
(if (not context)
;; all translated
(rime-terminate-translation)
;; update rime-prompt--candidate-list
(setq rime-prompt--candidate-list
(let* ((menu (alist-get 'menu context)))
(alist-get 'candidates menu)))
(setq rime-prompt--index 0)
(rime-preview--refresh)
(rime-prompt--refresh))))
;; regex rime-backend
((string-equal rime-backend "regex")
(rime-preview--delete-preview)
(setq rime-output (concat rime-output
(nth rime-prompt--index
rime-prompt--candidate-list)))
(rime-terminate-translation)))))
(defun rime-prompt--select-candidate-by-number (&optional n)
"使用数字编号来选择对应的词条。"
(interactive)
(let ((index (if (numberp n)
(- n 1)
(- last-command-event ?1))))
(if (> (+ index (rime-prompt--page-start-index)) (rime-prompt--page-end-index))
(rime-prompt--refresh)
(setq rime-prompt--index (+ rime-prompt--index index))
(rime-prompt--select-candidate))))
;; exwm-mode buffer doesn't have preview
(defun rime-preview--setup-preview-overlay ()
(let ((point (point)))
(if (overlayp rime-preview--overlay)
(move-overlay rime-preview--overlay point point)
(setq rime-preview--overlay (make-overlay point point))
(if input-method-highlight-flag
(overlay-put rime-preview--overlay 'face 'rime-preview--face)))))
(defun rime-preview--delete-preview ()
(delete-region (overlay-start rime-preview--overlay)
(overlay-end rime-preview--overlay)))
(defun rime-preview--delete-overlay ()
(when (overlayp rime-preview--overlay)
(delete-overlay rime-preview--overlay)))
(defun rime-preview--refresh ()
(setq rime-preview-string (concat rime-output (nth rime-prompt--index rime-prompt--candidate-list)))
(rime-preview--delete-preview)
(insert rime-preview-string)
;; Highlight new preview string.
(move-overlay rime-preview--overlay
(overlay-start rime-preview--overlay) (point)))
(defmacro rime-with-preedit (&rest forms)
(declare (indent 0) (debug t))
`(with-current-buffer (get-buffer-create " *rime-preedit*")
,@forms))
(defun rime-preedit-get ()
(rime-with-preedit
(buffer-string)))
;; editing in the preedit buffer without abort the translation session
(defun rime-preedit-forward-point ()
(interactive)
(rime-with-preedit
(ignore-errors
(forward-char)))
(rime-refresh-candidate t))
(defun rime-preedit-backward-point ()
(interactive)
(rime-with-preedit
(ignore-errors
(backward-char)))
(rime-refresh-candidate t))
(defun rime-preedit-end-of-line ()
(interactive)
(rime-with-preedit
(end-of-line))
(rime-refresh-candidate t))
(defun rime-preedit-beginning-of-line ()
(interactive)
(rime-with-preedit
(beginning-of-line))
(rime-refresh-candidate t))
(defun rime-preedit-delete-backward (&optional n)
(interactive)
(rime-with-preedit
(ignore-errors
(delete-char (- 0 (or n 1)))))
(rime-refresh-candidate t))
(defun rime-preedit-delete-forward ()
"在rime-preedit中向前删除1个字符"
(interactive)
(rime-preedit-delete-backward -1))
(defun rime-quit-clear ()
"取消当前输入的命令."
(interactive)
(setq rime-output nil)
(rime-preview--delete-preview)
(rime-terminate-translation))
(defun rime-quit-no-clear ()
"字母上屏命令."
(interactive)
(rime-preview--delete-preview)
(setq rime-output (rime-preedit-get))
(rime-terminate-translation)
(message "quit-no-clear")
)
(add-hook 'emacs-startup-hook 'rime-register-input-method)
(rime-register-input-method)
(provide 'rime)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment