Skip to content

Instantly share code, notes, and snippets.

@QiangF
Created January 25, 2020 14:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save QiangF/06c1a941449d9a705c14aa0db24ab2ae to your computer and use it in GitHub Desktop.
Save QiangF/06c1a941449d9a705c14aa0db24ab2ae to your computer and use it in GitHub Desktop.
;; https://github.com/tumashu/pyim/
;; https://github.com/d5884/rime-im/blob/master/rime-im.el
;; https://github.com/google/rime/blob/master/src/unix/emacs/rime.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 "中")
(defvar rime-local-variable-list
'(input-method-function
deactivate-current-input-method-function
rime-mode
rime-preview--overlay
rime-prompt--candidate-list
;; all index start from 0
rime-prompt--index
rime-prompt--subindex
rime-exhibit-timer
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))))))
;;;###autoload
(defvar rime-mode-map
(rime-make-self-insert-map 'rime-interactive-insert)
"Keymap for function `rime-mode'.")
;; 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-word-by-number))
(define-key map " " 'rime-prompt--select-word)
(define-key map "\C-d" 'rime-preedit-delete-forward)
(define-key map "\C-h" 'rime-preedit-delete-backward)
(define-key map [delete] 'rime-preedit-delete-forward)
(define-key map [backspace] 'rime-preedit-delete-backward)
(define-key map (char-to-string 127) 'rime-preedit-delete-forward)
(define-key map (char-to-string 8) '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)
(define-key map [return] 'rime-quit-no-clear)
(define-key map "\C-g" 'rime-quit-clear)
map)
"serves as a lookup table, not used as a mode map")
(defconst rime-empty-map (make-sparse-keymap)
"Empty keymap to disable Rime keymap.
This keymap is needed for `rime-fall-back-on-default-binding'.")
(defsubst rime-enable-keymap ()
"Enable Rime keymap again."
(setcdr (assq 'rime-mode minor-mode-map-alist)
rime-mode-map))
(defsubst rime-disable-keymap ()
"Disable Rime keymap temporarily."
(setcdr (assq 'rime-mode minor-mode-map-alist)
rime-empty-map))
(defun rime-mode (&optional arg)
(interactive (list current-prefix-arg))
;; Process the argument.
(setq rime-mode
(if (null arg)
(not rime-mode)
(> (prefix-numeric-value arg) 0)))
(if (not rime-mode)
(rime-deactivate)
;; enable, put the keymap at the top of the list of minor mode keymaps.
(setq minor-mode-map-alist
(cons (cons 'rime-mode rime-mode-map)
(assq-delete-all 'rime-mode minor-mode-map-alist))))
rime-mode)
(defun rime-fall-back-on-default-binding (last-event)
"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
(progn
;; Disable the keymap in this unwind-protect.
(rime-disable-keymap)
;; Push back the last event on the event queue.
(and last-event (push last-event unread-command-events))
;; Read and execute a command.
(let* ((keys (read-key-sequence-vector nil))
(bind (key-binding keys t)))
;; Pretend `rime-handle-event' command was not running and just
;; the default binding is running.
(setq last-command-event (aref keys (1- (length keys))))
(setq this-command bind)
(if bind
(call-interactively bind nil keys)
(let (message-log-max)
(message "%s is undefined" (key-description keys))
(undefined)))))
;; Recover the keymap.
(rime-enable-keymap)))
(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))
(= (length (rime-preedit-get)) 0)))))))
;;;###autoload
(defun rime-register-input-method ()
(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))
(rime-mode t))
(defun rime-deactivate ()
(setq rime-mode nil)
(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)))
(defun rime-interactive-insert (event)
(interactive (list last-command-event))
(cond
;; Keyboard event
((or (integerp event) (symbolp event))
(rime-input-method event))
;; Other events
(t
;; Fall back on a default binding.
(rime-fall-back-on-default-binding key-or-event))))
(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. The input method function should
return a list of events which should be used as input. (If the
list is nil, that means there is no input, so read-event waits
for another event.) These events are processed before the events
in unread-command-events (see Event Input Misc). 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.
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. 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."
(if (or buffer-read-only
(not enable-multibyte-characters)
overriding-terminal-local-map
overriding-local-map)
(list key)
(rime-preview--setup-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)
"1. 使用函数 `read-key-sequence' 得到 key-sequence 2. 使用函数
`lookup-key' 查询 `rime-mode-map' 中,与上述 key-sequence 对应 的
命令。3. 如果查询得到的命令是 `rime-self-insert-command' 时,
`rime-start-translation' 会调用这个函数。 4. 这个函数最终会返回需
要插入到 buffer 的字符串。 参考:`quail-input-method' 相关函数。"
(let* ((echo-keystrokes 0)
(help-char nil)
(overriding-terminal-local-map rime-mode-map)
(generated-events nil)
;; 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 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
(lv-message "preedit: %s keyseq: %s commandp: %s" (rime-preedit-get) keyseq rime-cmd)
(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)
;; (message "key: %s, rime-cmd:%s\nlcmd: %s, lcmdv: %s, tcmd: %s"
;; key rime-cmd last-command last-command-event this-command)
(condition-case-unless-debug err
(call-interactively rime-cmd)
(error (message "Input method error: %S" err)
(beep)))
(setq result rime-output))
(setq current-input-method-title "英")
(rime-terminate-translation)
(let ((key (aref keyseq (1- (length keyseq)))))
(if (aref printable-chars key)
;; keyseq: display char
(setq result key)
;; other char, such as not processed preedit editing command C-f etc, return the last event
(and keyseq (push key unread-command-events)))))))
result))
(defun rime-english-context-p ()
(cl-some #'(lambda (x)
(if (functionp x) (funcall x) nil))
rime-probe-list))
(defun rime-self-insert-command ()
(interactive "*")
(rime-with-preedit
(insert (char-to-string last-command-event)))
(rime-refresh-candidate))
(defun rime-refresh-candidate (&optional no-delay)
"延迟(秒)显示备选词"
(when rime-exhibit-timer
(cancel-timer rime-exhibit-timer))
(if (= (length (rime-preedit-get)) 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))
(t (setq rime-exhibit-timer
(run-with-timer rime-exhibit-delay nil #'rime-refresh-candidate-nodelay))))))
(defun rime-refresh-candidate-nodelay ()
(let ((input (rime-preedit-get)))
(setq rime-prompt--candidate-list
(or (delete-dups (rime-get-candidate-list input))
(list input))))
;; init candidate position
(setq rime-prompt--index 0)
(rime-preview--refresh)
(rime-prompt--refresh))
(defun rime-get-candidate-list (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-terminate-translation ()
(setq rime-translating nil)
(setq rime-prompt--candidate-list nil)
(when rime-exhibit-timer
(cancel-timer rime-exhibit-timer))
;; (setq mini-modeline--command-state 'end
;; echo-keystrokes 0.25)
(rime-preview--delete-overlay)
(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--get-string)))
;; 普通 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--get-string)))
(t
(popup-tip (rime-prompt--get-string)
: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) 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-preview ()
;; | 显示光标位置的字符
(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)
(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--get-string ()
(format "[%s]: %s(%s/%s)"
(rime-prompt--create-preview)
(rime-prompt--create-candidate-list)
(rime-prompt--current-page)
(rime-prompt--total-page)))
(defun rime-prompt--select-word ()
"从选词框中选择当前词条,专门用于 rime 输入法支持。"
(interactive)
(if (> 1 (length rime-prompt--candidate-list))
(progn (setq rime-output " ")
(rime-terminate-translation))
(rime-preview--delete-preview)
(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)))))
(defun rime-prompt--select-word-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-word))))
(defun rime-preview--setup-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 ()
(let* ((preview (concat rime-output (nth rime-prompt--index rime-prompt--candidate-list))))
(rime-preview--delete-preview)
(insert preview)
;; 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))
(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