;; the zero Chinese input method framework for emacs. ;; implemented as a minor mode. ;; (load-file "~/lisp/elisp/zero/zero-panel.elc") ;; (load-file "~/lisp/elisp/zero/zero-framework.elc") ;;============== ;; dependencies ;;============== (require 's) (require 'zero-panel) ;;======= ;; utils ;;======= ;; this function is from ibus.el (defun ibus-compute-pixel-position (&optional pos window) "Return geometry of object at POS in WINDOW as a list like \(X Y H). X and Y are pixel coordinates relative to top left corner of frame which WINDOW is in. H is the pixel height of the object. Omitting POS and WINDOW means use current position and selected window, respectively." (let* ((frame (window-frame (or window (selected-window)))) (posn (posn-at-point (or pos (window-point window)) window)) (line (cdr (posn-actual-col-row posn))) (line-height (and line (or (window-line-height line window) (and (redisplay t) (window-line-height line window))))) (x-y (or (posn-x-y posn) (let ((geom (pos-visible-in-window-p (or pos (window-point window)) window t))) (and geom (cons (car geom) (cadr geom)))) '(0 . 0))) (ax (+ (car (window-inside-pixel-edges window)) (car x-y))) (ay (+ (cadr (window-pixel-edges window)) (or (nth 2 line-height) (cdr x-y)))) (height (or (car line-height) (with-current-buffer (window-buffer window) (cond ;; `posn-object-width-height' returns an incorrect value ;; when the header line is displayed (Emacs bug #4426). ((and posn (null header-line-format)) (cdr (posn-object-width-height posn))) ((and (bound-and-true-p text-scale-mode) (not (zerop (with-no-warnings text-scale-mode-amount)))) (round (* (frame-char-height frame) (with-no-warnings (expt text-scale-mode-step text-scale-mode-amount))))) (t (frame-char-height frame))))))) (list ax ay height))) (defun zero-get-point-position () "return current point's position (x y) based on origin of screen top left corner" (destructuring-bind (x y h) (ibus-compute-pixel-position) (list (+ (frame-parameter nil 'left) x) (+ (frame-parameter nil 'top) h y)))) ;;===================== ;; key logic functions ;;===================== ;; FSM state (defconst *zero-state-im-off* 'IM-OFF) (defconst *zero-state-im-waiting-input* 'IM-WAITING-INPUT) (defconst *zero-state-im-preediting* 'IM-PREEDITING) ;;; 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) (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-im nil "current input method. if nil, the empty input method will be used. in the empty input method, only punctuation is handled. Other keys are pass through") (defvar zero-ims nil "a list of registered input methods") (defvar zero-buffer nil "stores the associated buffer. this is used to help with buffer focus in/out events") (defvar zero-state *zero-state-im-off*) (defvar zero-preedit-str "") (defvar zero-candidates nil) (defvar zero-enable-debug t "whether to enable debug. if t, `zero-debug' will output debug msg in *zero-debug* buffer") (defun zero-debug (string &rest objects) "log debug message in *zero-debug* buffer" (if zero-enable-debug (with-current-buffer (get-buffer-create "*zero-debug*") (insert (apply 'format string objects)) (goto-char (point-max))))) ;; (zero-debug "msg1\n") ;; (zero-debug "msg2: %s\n" "some obj") ;; (zero-debug "msg3: %s\n" 24) ;; (zero-debug "msg4: %s %s\n" 24 1) (defun zero-set-state (state) "set state to given state" (zero-debug "set state to %s\n" state) (setq zero-state state)) (defun zero-show-candidates (candidates) "show candidates using zero-panel via IPC/RPC" (zero-panel-show-candidates zero-preedit-str (length candidates) candidates) (zero-debug "candidates: %s\n " (s-join "\n " candidates)) (destructuring-bind (x y) (zero-get-point-position) (zero-panel-move x y))) (defun zero-build-candidates (preedit-str) "build candidates list synchronously" (if (functionp zero-build-candidates-func) (funcall zero-build-candidates-func preedit-str) (error "`zero-build-candidates-func' is not a function"))) (defun zero-build-candidates-async (preedit-str) "build candidate list, when done show it via `zero-show-candidates'" (zero-debug "building candidate list\n") (let ((candidates (zero-build-candidates preedit-str))) ;; update cache to make SPC and digit key selection possible. (setq zero-candidates candidates) (zero-show-candidates candidates))) (defun zero-handle-punctuation (n) "if n is a punctuation character, insert Chinese punctuation for it and return true, otherwise, return false." (case n (?, (insert ",") t) (?. (insert "。") t) (?? (insert "?") t) (?! (insert "!") t) (?\ (insert "、") t) (otherwise nil))) (defun zero-append-char-to-preedit-str (ch) "append char ch to preedit str, update and show candidate list" (setq zero-preedit-str (concat zero-preedit-str (make-string 1 ch))) (zero-debug "appended %c, preedit str is: %s\n" ch zero-preedit-str) (zero-preedit-str-changed)) (defun zero-can-start-sequence (ch) "return t if char ch can start a preedit sequence." (if (functionp zero-can-start-sequence-func) (funcall zero-can-start-sequence-func ch) (error "`zero-can-start-sequence-func' is not a function"))) (ert-deftest zero-can-start-sequence () (should (zero-can-start-sequence ?a)) (should (zero-can-start-sequence ?m)) (should-not (zero-can-start-sequence ?1)) (should-not (zero-can-start-sequence ?b))) (defun zero-self-insert-command (n) "handle character self-insert-command. This includes characters and digits" (interactive "p") (let ((ch (elt (this-command-keys-vector) 0))) (zero-debug "user typed: %c\n" ch) (cond ((eq zero-state *zero-state-im-waiting-input*) (if (zero-can-start-sequence ch) (progn (zero-debug "can start sequence, state=IM_PREEDITING\n") (zero-set-state *zero-state-im-preediting*) (zero-append-char-to-preedit-str ch)) (zero-debug "cannot start sequence, state=IM_WAITING_INPUT\n") (unless (zero-handle-punctuation ch) (self-insert-command n)))) ((eq zero-state *zero-state-im-preediting*) (zero-debug "still preediting\n") (if (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)) (zero-append-char-to-preedit-str ch))) (t (zero-debug "unexpected state: %s\n" zero-state) (self-insert-command n))))) (defun zero-preedit-str-changed () "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*) (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))) (delete-char (- n)))) (defun zero-commit-text (text) "commit given text, reset preedit str, hide candidate list" (zero-debug "commit text: %s\n" text) (insert text) (setq zero-preedit-str "") (setq zero-candidates nil) (zero-hide-candidate-list)) (defun zero-return () "handle RET key press" (interactive) (if (eq zero-state *zero-state-im-preediting*) (progn (zero-set-state *zero-state-im-waiting-input*) (zero-commit-text zero-preedit-str)) (newline-and-indent))) (defun zero-commit-nth-candidate (n) "commit nth candidate and return true if it exists, otherwise, return false" (let ((candidate (nth n zero-candidates))) (if candidate (progn (zero-set-state *zero-state-im-waiting-input*) (zero-commit-text candidate) t) nil))) (defun zero-commit-preedit-str () (zero-set-state *zero-state-im-waiting-input*) (zero-commit-text zero-preedit-str)) (defun zero-space (n) "handle SPC key press" (interactive "p") (if (eq zero-state *zero-state-im-preediting*) ;; commit first candidate if there is one, otherwise commit preedit str (unless (zero-commit-nth-candidate 0) (zero-commit-preedit-str)) (self-insert-command n))) (defun zero-hide-candidate-list () (zero-panel-hide) (zero-debug "hide candidate list\n")) (defun zero-reset () (zero-debug "reset\n") (setq zero-preedit-str "") (zero-hide-candidate-list)) (defun zero-focus-in () "a hook function. run when focus in a zero-mode buffer" (if (eq zero-state *zero-state-im-preediting*) (zero-show-candidates zero-candidates))) (defun zero-focus-out () "a hook function. run when focus out a zero-mode buffer" (if (eq zero-state *zero-state-im-preediting*) (zero-hide-candidate-list))) (defun zero-buffer-list-changed () "a hook function, run when buffer list has changed. This includes user has switched buffer" (if (eq (car (buffer-list)) zero-buffer) (zero-focus-in))) ;;============ ;; minor mode ;;============ ;; TODO when zero-framework is stable, move default value to defvar. ;; this will allow user to customize the keymap. (defvar zero-mode-map nil "zero-mode keymap") (setq zero-mode-map (list 'keymap '(13 . zero-return) '(32 . zero-space) '(remap keymap (self-insert-command . zero-self-insert-command) ;; (forward-char . zero-forward-char) ;; (backward-char . zero-backward-char) ;; (forward-word . zero-forward-word) ;; (backward-word . zero-backward-word) (delete-backward-char . zero-delete-backward-char) ;; (delete-char . zero-delete-char) ))) (define-minor-mode zero-mode "a simple input method written as an emacs minor mode" nil " Zero" zero-mode-map (set (make-local-variable 'zero-state) *zero-state-im-off*) (set (make-local-variable 'zero-preedit-str) "") (set (make-local-variable 'zero-candidates) nil) (make-local-variable 'zero-im) (make-local-variable 'zero-build-candidates-func) (make-local-variable 'zero-can-start-sequence-func) (zero-set-im zero-im) (add-hook 'focus-in-hook 'zero-focus-in) (add-hook 'focus-out-hook 'zero-focus-out) (set (make-local-variable 'zero-buffer) (current-buffer)) (add-hook 'buffer-list-update-hook 'zero-buffer-list-changed)) ;;================== ;; IM developer API ;;================== (defun zero-register-im (im-name im-functions-alist) "(re)register an input method in zero. After registration, you can use `zero-set-default-im' and `zero-set-im' to select input method to use. im-name should be a symbol. im-functions-alist should be a list of form '((:build-candidates . build-candidates-func) (:can-start-sequence . can-start-sequence-func)) registered input method is saved in `zero-ims'" ;; add or replace entry in `zero-ims' (unless (symbolp im-name) (signal 'wrong-type-argument (list 'symbolp im-name))) (setq zero-ims (assq-delete-all im-name zero-ims)) (setq zero-ims (push (cons im-name im-functions-alist) zero-ims))) ;;============ ;; public API ;;============ (defun zero-set-im (im-name) "select zero input method for current buffer. if im-name is nil, use default empty input method" ;; TODO provide completion (interactive "SSet input method to: ") (if im-name (let ((im-functions (cdr (assq im-name zero-ims)))) (if im-functions (progn (setq zero-build-candidates-func (or (cdr (assq :build-candidates im-functions)) 'zero-build-candidates-default)) (setq zero-can-start-sequence-func (or (cdr (assq :can-start-sequence im-functions)) 'zero-can-start-sequence-default)) (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))) (defun zero-set-default-im (im-name) "set given im as default zero input method" (unless (symbolp im-name) (signal 'wrong-type-argument (list 'symbolp im-name))) (setq-default zero-im im-name)) (defun zero-on () (interactive) (zero-debug "zero-on\n") (zero-mode 1) (if (eq zero-state *zero-state-im-off*) (zero-set-state *zero-state-im-waiting-input*))) (defun zero-off () (interactive) (zero-debug "zero-off\n") (zero-mode -1) (zero-reset) (zero-set-state *zero-state-im-off*)) (defun zero-toggle () (interactive) (if zero-mode (zero-off) (zero-on))) (provide 'zero-framework)