From 5f6fd432f3cd570e1ceaa6e7540cfcb6b4cb5fd2 Mon Sep 17 00:00:00 2001 From: Yuanle Song Date: Thu, 4 Apr 2019 01:25:56 +0800 Subject: [PATCH] add zero-pinyin.el; partial commit works. make partial commit work with zero-pinyin.el added a few "virtual functions" in zero-framework.el removed SPC key in keymap, just use self-insert-command remap. --- zero-framework.el | 119 ++++++++++++++++---------- zero-pinyin.el | 211 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 284 insertions(+), 46 deletions(-) create mode 100644 zero-pinyin.el diff --git a/zero-framework.el b/zero-framework.el index 5263beb..7d0b29b 100644 --- a/zero-framework.el +++ b/zero-framework.el @@ -95,17 +95,22 @@ if item is not in lst, return nil" ;;; concrete input method should define these functions and set them in the ;;; corresponding *-func variable. -(defun zero-build-candidates-default (preedit-str) - nil) -(defun zero-can-start-sequence-default (ch) - nil) +(defun zero-build-candidates-default (preedit-str) nil) +(defun zero-can-start-sequence-default (ch) nil) +(defun zero-handle-preedit-char-default (ch) nil) +(defun zero-get-preedit-str-for-panel-default () zero-preedit-str) (defvar zero-build-candidates-func 'zero-build-candidates-default "contains a function to build candidates from preedit-str") (defvar zero-can-start-sequence-func 'zero-can-start-sequence-default "contains a function to decide whether a char can start a preedit sequence") -;;; TODO provide hook functions. more complex IM may need to do clean up on these. -;; (defvar zero-on-hook nil) -;; (defvar zero-off-hook nil) +(defvar zero-handle-preedit-char-func 'zero-handle-preedit-char-default + "contains a function to handle IM-PREEDITING state char insert. +The function should return t if char is handled. +This allow input method to override default logic.") +(defvar zero-get-preedit-str-for-panel-func 'zero-get-preedit-str-for-panel-default + "contains a function that return preedit-str to show in zero-panel") +(defvar zero-backspace-func 'zero-backspace-default + "contains a function to handle char") (defvar zero-im nil "current input method. if nil, the empty input method will be used. @@ -175,7 +180,8 @@ if t, `zero-debug' will output debug msg in *zero-debug* buffer") (let ((candidates-on-page (zero-candidates-on-page (or candidates zero-candidates)))) (zero-panel-show-candidates - zero-preedit-str (length candidates-on-page) candidates-on-page) + (funcall zero-get-preedit-str-for-panel-func) + (length candidates-on-page) candidates-on-page) (zero-debug "candidates: %s\n " (s-join "\n " candidates-on-page)) (destructuring-bind (x y) (zero-get-point-position) (zero-panel-move x y)))) @@ -285,25 +291,28 @@ return ch's Chinese punctuation if ch is converted. return nil otherwise" (self-insert-command n)))) ((eq zero-state *zero-state-im-preediting*) (zero-debug "still preediting\n") - (cond - ((and (>= ch ?0) (<= ch ?9)) - ;; 1 commit the 0th candidate - ;; 2 commit the 1st candidate - ;; ... - ;; 0 commit the 9th candidate - (unless (zero-commit-nth-candidate (mod (- (- ch ?0) 1) 10)) - (zero-append-char-to-preedit-str ch))) - ((= ch zero-previous-page-key) - (zero-page-up)) - ((= ch zero-next-page-key) - (zero-page-down)) - (t (let ((str (zero-convert-punctuation ch))) - (if str - (progn - (zero-set-state *zero-state-im-waiting-input*) - (zero-commit-first-candidate-or-preedit-str) - (insert str)) - (zero-append-char-to-preedit-str ch)))))) + (unless (funcall zero-handle-preedit-char-func ch) + (cond + ((= ch ?\s) + (zero-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-commit-nth-candidate (mod (- (- ch ?0) 1) 10)) + (zero-append-char-to-preedit-str ch))) + ((= ch zero-previous-page-key) + (zero-page-up)) + ((= ch zero-next-page-key) + (zero-page-down)) + (t (let ((str (zero-convert-punctuation ch))) + (if str + (progn + (zero-set-state *zero-state-im-waiting-input*) + (zero-commit-first-candidate-or-preedit-str) + (insert str)) + (zero-append-char-to-preedit-str ch))))))) (t (zero-debug "unexpected state: %s\n" zero-state) (self-insert-command n))))) @@ -312,20 +321,24 @@ return ch's Chinese punctuation if ch is converted. return nil otherwise" "called when preedit str is changed and not empty. update and show candidate list" (zero-build-candidates-async zero-preedit-str)) +(defun zero-backspace-default () + "handle backspace key in `*zero-state-im-preediting*' state" + (let ((len (length zero-preedit-str))) + (if (> len 1) + (progn + (setq zero-preedit-str + (substring zero-preedit-str 0 (1- len))) + (zero-preedit-str-changed)) + (zero-set-state *zero-state-im-waiting-input*) + (zero-reset)))) + (defun zero-delete-backward-char (n) "handle backspace key" (interactive "p") (unless (integerp n) (signal 'wrong-type-argument (list 'integerp n))) (if (eq zero-state *zero-state-im-preediting*) - (let ((len (length zero-preedit-str))) - (if (> len 1) - (progn - (setq zero-preedit-str - (substring zero-preedit-str 0 (1- len))) - (zero-preedit-str-changed)) - (zero-set-state *zero-state-im-waiting-input*) - (zero-reset))) + (funcall zero-backspace-func) (delete-char (- n)))) (defun zero-commit-text (text) @@ -365,19 +378,14 @@ return ch's Chinese punctuation if ch is converted. return nil otherwise" (unless (zero-commit-nth-candidate 0) (zero-commit-preedit-str))) -(defun zero-space (n) - "handle SPC key press" - (interactive "p") - (if (eq zero-state *zero-state-im-preediting*) - (zero-commit-first-candidate-or-preedit-str) - (self-insert-command n))) - (defun zero-hide-candidate-list () (zero-panel-hide) (zero-debug "hide candidate list\n")) (defun zero-reset () + (interactive) (zero-debug "reset\n") + (setq zero-state *zero-state-im-waiting-input*) (setq zero-preedit-str "") (setq zero-candidates nil) (setq zero-current-page 0) @@ -407,8 +415,8 @@ return ch's Chinese punctuation if ch is converted. return nil otherwise" (defvar zero-mode-map nil "zero-mode keymap") (setq zero-mode-map '(keymap + (escape . zero-reset) (13 . zero-return) - (32 . zero-space) ;; C-. (67108910 . zero-cycle-punctuation-level) (remap keymap @@ -422,7 +430,9 @@ return ch's Chinese punctuation if ch is converted. return nil otherwise" ))) (define-minor-mode zero-mode - "a simple input method written as an emacs minor mode" + "a Chinese input method framework written as an emacs minor mode. + +\\{zero-mode-map}" nil " Zero" zero-mode-map @@ -497,8 +507,9 @@ registered input method is saved in `zero-ims'" "select zero input method for current buffer. if im-name is nil, use default empty input method" - ;; TODO provide completion + ;; TODO provide auto completion for im-name (interactive "SSet input method to: ") + ;; TODO create a macro to reduce code duplication and human error. (if im-name (let ((im-functions (cdr (assq im-name zero-ims)))) (if im-functions @@ -509,11 +520,27 @@ if im-name is nil, use default empty input method" (setq zero-can-start-sequence-func (or (cdr (assq :can-start-sequence im-functions)) 'zero-can-start-sequence-default)) + (setq zero-handle-preedit-char-func + (or (cdr (assq :handle-preedit-char im-functions)) + 'zero-handle-preedit-char-default)) + (setq zero-get-preedit-str-for-panel-func + (or (cdr (assq :get-preedit-str-for-panel im-functions)) + 'zero-get-preedit-str-for-panel-default)) + (setq zero-backspace-func + (or (cdr (assq :handle-backspace im-functions)) + 'zero-backspace-default)) + (let ((init-func (cdr (assq :init im-functions)))) + (if (functionp init-func) + (funcall init-func))) (set (make-local-variable 'zero-im) im-name)) (error "input method %s not registered in zero" im-name))) (zero-debug "using default empty input method") (setq zero-build-candidates-func 'zero-build-candidates-default) - (setq zero-can-start-sequence-func 'zero-can-start-sequence-default))) + (setq zero-can-start-sequence-func 'zero-can-start-sequence-default) + (setq zero-handle-preedit-char-func 'zero-handle-preedit-char-default) + (setq zero-get-preedit-str-for-panel-func 'zero-get-preedit-str-for-panel-default) + (setq zero-backspace-func 'zero-backspace-default) + )) (defun zero-set-default-im (im-name) "set given im as default zero input method" diff --git a/zero-pinyin.el b/zero-pinyin.el new file mode 100644 index 0000000..0302c55 --- /dev/null +++ b/zero-pinyin.el @@ -0,0 +1,211 @@ +;; a pinyin input method for zero-framework + +;;============== +;; dependencies +;;============== + +(require 's) +(require 'zero-framework) + +;;=============================== +;; 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 () + (make-local-variable 'zero-pinyin-state) + (zero-pinyin-reset)) + +(defun zero-pinyin-build-candidates (preedit-str) + (zero-pinyin-build-candidates-test preedit-str)) + +;; (defun zero-pinyin-build-candidates-async (preedit-str) +;; "build candidate list, when done show it via `zero-pinyin-show-candidates'" +;; (zero-pinyin-debug "building candidate list\n") +;; (let ((candidates (zero-pinyin-build-candidates preedit-str))) +;; ;; update cache to make SPC and digit key selection possible. +;; (setq zero-pinyin-candidates candidates) +;; (zero-pinyin-show-candidates 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 () + (zero-build-candidates-async zero-pinyin-pending-preedit-str)) + +(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-handle-preedit-char (ch) + "handle IM-PREEDITING state char insert. return t if char is handled." + (cond + ((or (= ch zero-previous-page-key) + (= ch zero-next-page-key)) + nil) + ((= ch ?\s) + (zero-pinyin-commit-first-candidate-or-preedit-str) + t) + ((and (>= ch ?0) (<= ch ?9)) + ;; 1 commit the 0th candidate + ;; 2 commit the 1st candidate + ;; ... + ;; 0 commit the 9th candidate + (if (zero-pinyin-commit-nth-candidate (mod (- (- ch ?0) 1) 10)) + t + (progn + (zero-append-char-to-preedit-str ch) + (setq zero-pinyin-state nil) + t))) + (t (let ((str (zero-convert-punctuation ch))) + (if str + (if (zero-pinyin-commit-first-candidate-in-full) + (progn + (zero-set-state *zero-state-im-waiting-input*) + (insert str) + t) + ;; can't commit in full, punctuation key is no-op + t) + (setq zero-pinyin-state nil) + (zero-append-char-to-preedit-str ch) + t))))) + +(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-backspace () + "handle backspace key in `*zero-state-im-preediting*' state" + (if (eq zero-pinyin-state *zero-pinyin-state-im-partial-commit*) + (progn + ;; start over for candidate selection process. + (setq zero-pinyin-state nil) + (zero-preedit-str-changed)) + (zero-backspace-default))) + +;;=============================== +;; register IM to zero framework +;;=============================== + +(zero-register-im + 'pinyin + '((:build-candidates . zero-pinyin-build-candidates) + (: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))) + +;;============ +;; 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) -- GitLab