Commit 5f6fd432 authored by Yuanle Song's avatar Yuanle Song
Browse files

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.
parent 68f7148b
Loading
Loading
Loading
Loading
+73 −46
Original line number Diff line number Diff line
@@ -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 <backward> 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,7 +291,10 @@ 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")
      (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
@@ -303,7 +312,7 @@ return ch's Chinese punctuation if ch is converted. return nil otherwise"
		    (zero-set-state *zero-state-im-waiting-input*)
		    (zero-commit-first-candidate-or-preedit-str)
		    (insert str))
	      (zero-append-char-to-preedit-str ch))))))
		(zero-append-char-to-preedit-str ch)))))))
     (t
      (zero-debug "unexpected state: %s\n" zero-state)
      (self-insert-command n)))))
@@ -312,12 +321,8 @@ 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-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*)
(defun zero-backspace-default ()
  "handle backspace key in `*zero-state-im-preediting*' state"
  (let ((len (length zero-preedit-str)))
    (if (> len 1)
	(progn
@@ -325,7 +330,15 @@ return ch's Chinese punctuation if ch is converted. return nil otherwise"
		(substring zero-preedit-str 0 (1- len)))
	  (zero-preedit-str-changed))
      (zero-set-state *zero-state-im-waiting-input*)
	  (zero-reset)))
      (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*)
      (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"

zero-pinyin.el

0 → 100644
+211 −0
Original line number Diff line number Diff line
;; 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)