Newer
Older
;; a pinyin input method for zero-framework
;; use use this input method, add in emacs init file:
;; (add-to-list 'load-path "~/lisp/elisp/zero")
;; (require 'zero-pinyin)
;; (zero-set-default-im 'pinyin)
;;==============
;; dependencies
;;==============
(require 's)
(require 'zero-framework)
(require 'zero-pinyin-service)
;;===============================
;; basic data and emacs facility
;;===============================
(defvar zero-pinyin-state nil "zero-pinyin internal state. could be nil or `*zero-pinyin-state-im-partial-commit*'")
(defconst *zero-pinyin-state-im-partial-commit* 'IM-PARTIAL-COMMIT)
(defvar zero-pinyin-used-preedit-str-lengths nil
"accompany `zero-candidates', marks how many preedit-str chars are used for each candidate")
(defvar zero-pinyin-pending-str "")
(defvar zero-pinyin-pending-preedit-str "")
;;=====================
;; key logic functions
;;=====================
(defun zero-pinyin-reset ()
(setq zero-pinyin-state nil)
(setq zero-pinyin-used-preedit-str-lengths nil)
(setq zero-pinyin-pending-str "")
(setq zero-pinyin-pending-preedit-str ""))
(defun zero-pinyin-init ()
"called when this im is turned on"
(make-local-variable 'zero-pinyin-state)
(zero-pinyin-reset))
(defun zero-pinyin-preedit-start ()
"called when enter `*zero-state-im-preediting*' state"
(define-key zero-mode-map [remap digit-argument] 'zero-digit-argument))
(defun zero-pinyin-preedit-end ()
"called when leave `*zero-state-im-preediting*' state"
(define-key zero-mode-map [remap digit-argument] nil))
(defun zero-pinyin-shutdown ()
"called when this im is turned off"
(define-key zero-mode-map [remap digit-argument] nil))
(defvar zero-pinyin--build-candidates-use-test-data nil
"if t, `zero-pinyin-build-candidates' will use `zero-pinyin-build-candidates-test'")
(defun zero-pinyin-build-candidates (preedit-str fetch-size)
"zero-pinyin-build-candidates synchronous version"
(progn
(zero-pinyin-build-candidates-test preedit-str)
(setq zero-fetch-size (max fetch-size (length zero-candidates))))
(zero-debug "zero-pinyin building candidate list synchronously\n")
(let ((result (zero-pinyin-service-get-candidates preedit-str fetch-size)))
(setq zero-fetch-size (max fetch-size (length (first result))))
(setq zero-pinyin-used-preedit-str-lengths (second result))
(first result))))
(defun zero-pinyin-build-candidates-async (preedit-str fetch-size complete-func)
"build candidate list, when done call complete-func on it"
(zero-debug "zero-pinyin building candidate list asynchronously\n")
(zero-pinyin-service-get-candidates-async
preedit-str
(lambda (candidates matched_preedit_str_lengths)
(setq zero-pinyin-used-preedit-str-lengths matched_preedit_str_lengths)
(setq zero-fetch-size (max fetch-size (length candidates)))
;; Note: with dynamic binding, this command result in (void-variable
;; complete-func) error.
(funcall complete-func candidates))))
(defun zero-pinyin-can-start-sequence (ch)
"return t if char ch can start a preedit sequence."
(and (>= ch ?a)
(<= ch ?z)
(not (= ch ?u))
(not (= ch ?v))))
(ert-deftest zero-pinyin-can-start-sequence ()
(should (zero-pinyin-can-start-sequence ?a))
(should (zero-pinyin-can-start-sequence ?l))
(should (zero-pinyin-can-start-sequence ?m))
(should (zero-pinyin-can-start-sequence ?z))
(should-not (zero-pinyin-can-start-sequence ?1))
(should-not (zero-pinyin-can-start-sequence ?.))
(should-not (zero-pinyin-can-start-sequence ?u))
(should-not (zero-pinyin-can-start-sequence ?v)))
(defun zero-pinyin-pending-preedit-str-changed ()
(setq zero-fetch-size 0)
(setq zero-current-page 0)
(zero-pinyin-build-candidates-async zero-pinyin-pending-preedit-str zero-initial-fetch-size 'zero-build-candidates-complete))
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
(defun zero-pinyin-commit-nth-candidate (n)
"commit nth candidate and return true if it exists, otherwise, return false"
(let* ((candidate (nth n (zero-candidates-on-page zero-candidates)))
(used-len (when candidate
(nth (+ n (* zero-candidates-per-page zero-current-page)) zero-pinyin-used-preedit-str-lengths))))
(when candidate
(zero-debug
"zero-pinyin-commit-nth-candidate n=%s candidate=%s used-len=%s zero-pinyin-pending-preedit-str=%s\n"
n candidate used-len zero-pinyin-pending-preedit-str)
(cond
((null zero-pinyin-state)
(if (= used-len (length zero-preedit-str))
(progn
(zero-debug "commit in full\n")
(zero-set-state *zero-state-im-waiting-input*)
(zero-commit-text candidate)
t)
(zero-debug "partial commit, in partial commit mode now.\n")
(setq zero-pinyin-state *zero-pinyin-state-im-partial-commit*)
(setq zero-pinyin-pending-str candidate)
(setq zero-pinyin-pending-preedit-str (substring zero-preedit-str used-len))
(zero-pinyin-pending-preedit-str-changed)
t))
((eq zero-pinyin-state *zero-pinyin-state-im-partial-commit*)
(if (= used-len (length zero-pinyin-pending-preedit-str))
(progn
(zero-debug "finishes partial commit\n")
(setq zero-pinyin-state nil)
(zero-set-state *zero-state-im-waiting-input*)
(zero-commit-text (concat zero-pinyin-pending-str candidate))
t)
(zero-debug "continue partial commit\n")
(setq zero-pinyin-pending-str (concat zero-pinyin-pending-str candidate))
(setq zero-pinyin-pending-preedit-str (substring zero-pinyin-pending-preedit-str used-len))
(zero-pinyin-pending-preedit-str-changed)
t))
(t (error "Unexpected zero-pinyin-state: %s" zero-pinyin-state))))))
(defun zero-pinyin-commit-first-candidate-or-preedit-str ()
(unless (zero-pinyin-commit-nth-candidate 0)
(zero-commit-preedit-str)))
(defun zero-pinyin-commit-first-candidate-in-full ()
"commit first candidate and return t if first candidate consumes all preedit-str.
otherwise, just return nil"
(let ((candidate (nth 0 (zero-candidates-on-page zero-candidates)))
(used-len (nth (* zero-candidates-per-page zero-current-page) zero-pinyin-used-preedit-str-lengths)))
(when candidate
(cond
((null zero-pinyin-state)
(when (= used-len (length zero-preedit-str))
(zero-set-state *zero-state-im-waiting-input*)
(zero-commit-text candidate)
t))
((eq zero-pinyin-state *zero-pinyin-state-im-partial-commit*)
(when (= used-len (length zero-pinyin-pending-preedit-str))
(setq zero-pinyin-state nil)
(zero-set-state *zero-state-im-waiting-input*)
(zero-commit-text (concat zero-pinyin-pending-str candidate))
t))
(t (error "Unexpected zero-pinyin-state: %s" zero-pinyin-state))))))
(defun zero-pinyin-page-down ()
"handle page down for zero-pinyin.
This is different from zero-framework because I need to support partial commit"
(let ((len (length zero-candidates))
(new-fetch-size (* zero-candidates-per-page (+ 2 zero-current-page))))
(if (and (< len new-fetch-size)
(< zero-fetch-size new-fetch-size))
(let ((preedit-str (if (eq zero-pinyin-state *zero-pinyin-state-im-partial-commit*) zero-pinyin-pending-preedit-str zero-preedit-str)))
(zero-pinyin-build-candidates-async
preedit-str
new-fetch-size
(lambda (candidates)
(zero-build-candidates-complete candidates)
(zero-just-page-down))))
(zero-just-page-down))))
(defun zero-pinyin-handle-preedit-char (ch)
"hanlde character insert in `*zero-state-im-preediting*' state. overrides `zero-handle-preedit-char-default'"
(zero-pinyin-commit-first-candidate-or-preedit-str))
((and (>= ch ?0) (<= ch ?9))
;; 1 commit the 0th candidate
;; 2 commit the 1st candidate
;; ...
;; 0 commit the 9th candidate
(unless (zero-pinyin-commit-nth-candidate (mod (- (- ch ?0) 1) 10))
(zero-append-char-to-preedit-str ch)
(setq zero-pinyin-state nil)))
(zero-handle-preedit-char-default ch))
((= ch zero-next-page-key)
(zero-pinyin-page-down))
(t (let ((str (zero-convert-punctuation ch)))
(if str
(when (zero-pinyin-commit-first-candidate-in-full)
(zero-set-state *zero-state-im-waiting-input*)
(insert str))
(zero-append-char-to-preedit-str ch))))))
(defun zero-pinyin-get-preedit-str-for-panel ()
(if (eq zero-pinyin-state *zero-pinyin-state-im-partial-commit*)
(concat zero-pinyin-pending-str zero-pinyin-pending-preedit-str)
zero-preedit-str))
(defun zero-pinyin-preedit-str-changed ()
"start over for candidate selection process."
(setq zero-pinyin-state nil)
(zero-preedit-str-changed))
(defun zero-pinyin-backspace ()
"handle backspace key in `*zero-state-im-preediting*' state"
(if (eq zero-pinyin-state *zero-pinyin-state-im-partial-commit*)
(zero-pinyin-preedit-str-changed)
(defun zero-pinyin-delete-candidate (digit)
"tell backend to delete nth candidate.
n is the digit selection number.
1 means delete 1st candidate.
2 means delete 2st candidate.
0 means delete 10th candidate."
(let ((candidate (nth (mod (- digit 1) 10)
(zero-candidates-on-page zero-candidates))))
(when candidate
(zero-pinyin-service-delete-candidates-async
candidate 'zero-pinyin-preedit-str-changed))))
(defun zero-digit-argument ()
"allow C-<digit> to DeleteCandidate in `*zero-state-im-preediting*' state"
(interactive)
(unless (eq zero-state *zero-state-im-preediting*)
(error "zero-digit-argument called in non preediting state"))
(if (memq 'control (event-modifiers last-command-event))
(let* ((char (if (integerp last-command-event)
last-command-event
(get last-command-event 'ascii-character)))
(digit (- (logand char ?\177) ?0)))
(zero-pinyin-delete-candidate digit))))
;;===============================
;; register IM to zero framework
;;===============================
(zero-register-im
'pinyin
'((:build-candidates . zero-pinyin-build-candidates)
;; comment to use sync version, uncomment to use async version.
;; (:build-candidates-async . zero-pinyin-build-candidates-async)
(:can-start-sequence . zero-pinyin-can-start-sequence)
(:handle-preedit-char . zero-pinyin-handle-preedit-char)
(:get-preedit-str-for-panel . zero-pinyin-get-preedit-str-for-panel)
(:handle-backspace . zero-pinyin-backspace)
(:init . zero-pinyin-init)
(:shutdown . zero-pinyin-shutdown)
(:preedit-start . zero-pinyin-preedit-start)
(:preedit-end . zero-pinyin-preedit-end)))
;;============
;; public API
;;============
;;===========
;; test data
;;===========
(defun zero-pinyin-build-candidates-test (preedit-str)
"test data for testing partial commit"
(cond
((equal preedit-str "liyifeng")
(setq zero-pinyin-used-preedit-str-lengths '(8 4 4 4 2 2 2))
'("李易峰" "利益" "礼仪" "离异" "里" "理" "力"))
((equal preedit-str "feng")
(setq zero-pinyin-used-preedit-str-lengths '(4 4 4 4 4))
'("风" "封" "疯" "丰" "凤"))
((equal preedit-str "yifeng")
(setq zero-pinyin-used-preedit-str-lengths '(6 6 2 2 2 2))
'("一封" "遗风" "艺" "依" "一" "以"))
(t nil)))
(provide 'zero-pinyin)