diff --git a/Makefile b/Makefile index 02e4efb90329540b48b915c6ca9798046abfa6d2..0935d2deae9df4c659dc4e45d82ce99997463420 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,13 @@ -VERSION := $(shell grep 'setq zero-version' zero-framework.el | cut -d'"' -f2) +VERSION := $(shell grep 'setq zero-input-version' zero-input-framework.el | cut -d'"' -f2) default: dist #=============== # multiple file #=============== check: - emacs -Q --batch -l zero-reload-all.el -f zero-rebuild -l zero-table.el -l zero-table-test.el -f ert-run-tests-batch + emacs -Q --batch -l zero-input-reload-all.el -f zero-input-rebuild -l zero-input-table.el -l zero-input-table-test.el -f ert-run-tests-batch zip: - git archive -o zero-el-$(VERSION).zip --prefix=zero/ HEAD + git archive -o zero-input-el-$(VERSION).zip --prefix=zero-input/ HEAD #========================== # single file distribution #========================== @@ -16,9 +16,12 @@ build: if [ ! -x ~/.local/bin/pytest ]; then python3 -m pip install --user pytest; fi ~/.local/bin/pytest build.py ./build.py - sed -i "s/PKG_VERSION/$(VERSION)/g" zero.el + sed -i "s/PKG_VERSION/$(VERSION)/g" zero-input.el dist-check: build - emacs -Q --batch -l ~/.emacs.d/elpa/s-1.11.0/s.el -l zero.el -l zero-panel-test.el -l zero-pinyin-service-test.el -l zero-framework-test.el -l zero-pinyin-test.el -l zero-table.el -l zero-table-test.el -f ert-run-tests-batch + @echo "testing byte-compile is clean..." + emacs -Q --batch -l ~/.emacs.d/elpa/s-1.11.0/s.el --eval='(byte-compile-file "zero-input.el")' + @echo "running unit tests..." + emacs -Q --batch -l ~/.emacs.d/elpa/s-1.11.0/s.el -l zero-input.el -l zero-input-panel-test.el -l zero-input-pinyin-service-test.el -l zero-input-framework-test.el -l zero-input-pinyin-test.el -l zero-input-table.el -l zero-input-table-test.el -f ert-run-tests-batch #==================== # other make targets #==================== diff --git a/NOTICE b/NOTICE index f33068961dde04afb77842ec07d6ab305b859cab..00d12340789b7be42239d8ad37666ab8d8c2d326 100644 --- a/NOTICE +++ b/NOTICE @@ -1,5 +1,6 @@ zero-el Copyright (C) 2019 Yuanle Song <sylecn@gmail.com> -zero--ibus-compute-pixel-position function in zero.el is copied from ibus.el. -This function is under GPLv3 license. Copyright (C) 2010-2012 S. Irie +zero-input--ibus-compute-pixel-position function in zero-input.el is copied +from ibus.el. This function is under GPLv3 license. Copyright (C) 2010-2012 +S. Irie diff --git a/README b/README index cecd0e271a77061e28d9f52c328d3d6b93ce59ad..3a0298528045b675754b118ec69f2d94d7652acc 100644 --- a/README +++ b/README @@ -1,44 +1,44 @@ * COMMENT -*- mode: org -*- #+Date: 2019-09-01 -Time-stamp: <2019-10-16> +Time-stamp: <2019-10-28> * zero-el -zero-el provides zero-pinyin, an Emacs pinyin input method for Chinese and -zero-framework, which is an emacs Chinese input method framework. +zero-el provides zero-input-pinyin, an Emacs pinyin input method for Chinese +and zero-input-framework, which is an emacs Chinese input method framework. * File list -- zero.el +- zero-input.el It's a generated file for one-file package distribution. Not used for development. -- zero-framework.el +- zero-input-framework.el zero framework source code. This provides the framework and user interface for zero-el. -- zero-panel.el +- zero-input-panel.el dbus client to zero-panel service. -- zero-pinyin-service.el +- zero-input-pinyin-service.el dbus client to zero-pinyin-service. -- zero-pinyin.el +- zero-input-pinyin.el Pinyin input method implemented using zero.el -- zero-quickdial.el +- zero-input-quickdial.el proof of concept of how to create an input method in emacs using minor mode. -- zero-reload-all.el +- zero-input-reload-all.el zero-el development utility. -- zero-table.el +- zero-input-table.el serves as an example of how to use zero framework to create new input methods. @@ -49,5 +49,5 @@ https://blog.emacsos.com/zero-el.html * License zero-el is under Apache License 2.0 -zero--ibus-compute-pixel-position function in zero-framework.el is under -GPLv3. see NOTICE file. +zero-input--ibus-compute-pixel-position function in zero-input-framework.el is +under GPLv3. see NOTICE file. diff --git a/build.py b/build.py index 389dd9079b7d4d5b87831bcfa10a8eef1658caee..79cd58eb272367e87928ca6c732cea881d067a3b 100755 --- a/build.py +++ b/build.py @@ -2,7 +2,7 @@ # coding=utf-8 """ -build zero.el from zero.el.in and other el files +build zero-input.el from zero-input.el.in and other el files """ import logging @@ -18,9 +18,10 @@ def placeholder_for_file(fn): def test_placeholder_for_file(): - assert placeholder_for_file("zero-panel.el") == "INCLUDE_ZERO_PANEL_EL" - assert placeholder_for_file("zero-pinyin-service.el") == ( - "INCLUDE_ZERO_PINYIN_SERVICE_EL") + assert placeholder_for_file("zero-input-panel.el") == ( + "INCLUDE_ZERO_INPUT_PANEL_EL") + assert placeholder_for_file("zero-input-pinyin-service.el") == ( + "INCLUDE_ZERO_INPUT_PINYIN_SERVICE_EL") def get_el_file_body(fn): @@ -57,16 +58,16 @@ def main(): logging.basicConfig( format='%(asctime)s [%(module)s] %(levelname)-8s %(message)s', level=logging.INFO) - with open("zero.el.in") as tpl: + with open("zero-input.el.in") as tpl: content = tpl.read() expanded_content = expand_placeholder_for_files(content, [ - "zero-panel.el", - "zero-framework.el", - "zero-table.el", - "zero-pinyin-service.el", - "zero-pinyin.el", + "zero-input-panel.el", + "zero-input-framework.el", + "zero-input-table.el", + "zero-input-pinyin-service.el", + "zero-input-pinyin.el", ]) - with open('zero.el', 'w') as out: + with open('zero-input.el', 'w') as out: out.write(expanded_content) diff --git a/git-hooks/pre-commit b/git-hooks/pre-commit index 823138729b6ee9497f2cf7b56fa35f193aabc759..bff5705d8bdf1b86f03419090f24fa6cf91e722a 100755 --- a/git-hooks/pre-commit +++ b/git-hooks/pre-commit @@ -2,5 +2,5 @@ set -e if git branch |grep '^\* master'; then make dist-check - git add zero.el + git add zero-input.el fi diff --git a/operational b/operational index d223d03313b93227c0a5c46d36fb3740c9b8317d..493fefee7373222fb7a9e40bee225727730ae5b0 100644 --- a/operational +++ b/operational @@ -1,9 +1,9 @@ * COMMENT -*- mode: org -*- #+Date: 2019-10-08 -Time-stamp: <2019-10-23> +Time-stamp: <2019-10-28> #+STARTUP: content * notes :entry: -** 2019-04-01 zero.el a Chinese IM framework in emacs; FSM :doc: +** 2019-04-01 zero-el a Chinese IM framework in emacs; FSM :doc: title was: how to write a modern im for emacs. cd ~/lisp/elisp/zero/ - DONE can I implement it as a minor mode? yes. @@ -13,8 +13,8 @@ cd ~/lisp/elisp/zero/ | Imp | state | action | next state | trigger action | |-----+------------------+-----------------------------------------------------+------------------+---------------------------------------------------------------------------------------------------------| - | Y | IM_OFF | M-x zero-on or zero-toggle | IM_WAITING_INPUT | turn on minor mode | - | Y | IM_WAITING_INPUT | type M-x zero-off or zero-toggle | IM_OFF | turn off minor mode | + | Y | IM_OFF | M-x zero-input-on or zero-input-toggle | IM_WAITING_INPUT | turn on minor mode | + | Y | IM_WAITING_INPUT | type M-x zero-input-off or zero-input-toggle | IM_OFF | turn off minor mode | | Y | IM_WAITING_INPUT | type character that can start a sequence | IM_PREEDITING | update preedit str, show candidate list | | Y | IM_WAITING_INPUT | type character that can not start a sequence | IM_WAITING_INPUT | insert character (full-width aware) | | Y | IM_WAITING_INPUT | type [,.?!\] | IM_WAITING_INPUT | insert Chinese punctuation character (full-width aware) | @@ -23,7 +23,7 @@ cd ~/lisp/elisp/zero/ | Y | IM_PREEDITING | type SPC | IM_WAITING_INPUT | commit first candidate or preedit str (full-width aware), reset preedit str | | Y | IM_PREEDITING | type digit keys | IM_WAITING_INPUT | commit nth candidate if it exists, otherwise, append to preedit str | | | IM_PREEDITING | type C-g | IM_WAITING_INPUT | reset IM (reset preedit str, hide candidate list) | - | Y | IM_PREEDITING | type M-x zero-off or zero-toggle | IM_OFF | reset IM, turn off minor mode | + | Y | IM_PREEDITING | type M-x zero-input-off or zero-input-toggle | IM_OFF | reset IM, turn off minor mode | | Y | IM_PREEDITING | type <backspace>, when preedit str is longer than 1 | IM_PREEDITING | update preedit str, update and show candidate list | | Y | IM_PREEDITING | type <backspace>, when preedit str is length 1 | IM_WAITING_INPUT | reset IM | | Y | IM_PREEDITING | focus in | IM_PREEDITING | show candidat list | @@ -35,10 +35,10 @@ cd ~/lisp/elisp/zero/ in IM_OFF state, zero should not do any preedit try nor do punctuation translate. -- DONE make zero-quickdial IM work in emacs. - see ~/lisp/elisp/zero/zero-quickdial.el -- DONE make zero-table IM work in emacs. with zero-panel. - see ~/lisp/elisp/zero/zero-table.el +- DONE make zero-input-quickdial IM work in emacs. + see ~/lisp/elisp/zero/zero-input-quickdial.el +- DONE make zero-input-table IM work in emacs. with zero-input-panel. + see ~/lisp/elisp/zero/zero-input-table.el - during development, press F8 to byte-compile and load the current el file. this will also look for errors in the file. @@ -52,7 +52,7 @@ cd ~/lisp/elisp/zero/ user can click mouse in another emacs window. whenever focus is moved outside current buffer, I need a hook to run - zero-focus-out. + zero-input-focus-out. how to reproduce the problem ============================= @@ -75,7 +75,7 @@ cd ~/lisp/elisp/zero/ - how to check whether string contains character? without converting char to string. - (zero-table-can-start-sequence) can use this. + (zero-input-table-can-start-sequence) can use this. ** 2019-10-10 documents - Using of D-Bus https://www.gnu.org/software/emacs/manual/html_mono/dbus.html @@ -92,8 +92,8 @@ I can't run them in git pre-commit hook. Need to add headers to make it happy. Because it doesn't have concept on multi-file package, it insists all - functions in file start with file name prefix. I can't have zero-* utility - function in zero-panel.el + functions in file start with file name prefix. I can't have zero-input-* utility + function in zero-input-panel.el - M-x package-lint-current-buffer fail on zero.el file this line: @@ -129,7 +129,7 @@ I can't run them in git pre-commit hook. package-lint doesn't support multiple file pkg yet. - create a single zero.el file for distribution. - rename current zero.el to zero-framework.el + rename current zero.el to zero-input-framework.el create zero.el.in - make build should generate zero.el and run checks on it. @@ -146,7 +146,7 @@ I can't run them in git pre-commit hook. - ** 2019-10-11 move tests to separated files. -otherwise (require 'zero-pinyin) will fail because (require 'ert) is not in +otherwise (require 'zero-input-pinyin) will fail because (require 'ert) is not in source code. ** 2019-10-09 release zero-el on melpa diff --git a/zero-framework-test.el b/zero-framework-test.el deleted file mode 100644 index 8247263fdcd5115785aae44c3fdc0540f4d0c623..0000000000000000000000000000000000000000 --- a/zero-framework-test.el +++ /dev/null @@ -1,44 +0,0 @@ -;;; zero-framework-test.el --- tests for zero-framework.el -*- lexical-binding: t -*- - -;; Licensed under the Apache License, Version 2.0 (the "License"); -;; you may not use this file except in compliance with the License. -;; You may obtain a copy of the License at -;; -;; http://www.apache.org/licenses/LICENSE-2.0 -;; -;; Unless required by applicable law or agreed to in writing, software -;; distributed under the License is distributed on an "AS IS" BASIS, -;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -;; See the License for the specific language governing permissions and -;; limitations under the License. - -;;; Commentary: - -;; tests for zero-framework.el - -;;; Code: - -(require 'zero-framework) -(require 'ert) - -(ert-deftest zero-cycle-list () - (should (= (zero-cycle-list '(1 2 3) 1) 2)) - (should (eq (zero-cycle-list '(a b c) 'a) 'b)) - (should (eq (zero-cycle-list '(a b c) 'b) 'c)) - (should (eq (zero-cycle-list '(a b c) 'c) 'a)) - (should (eq (zero-cycle-list '(a b c) 'd) nil))) - -(ert-deftest zero-convert-ch-to-full-width () - (should (= (zero-convert-ch-to-full-width ?\!) ?\!))) - -(ert-deftest zero-convert-str-to-full-width () - (should (string-equal "!" (zero-convert-str-to-full-width "!"))) - (should (string-equal "(" (zero-convert-str-to-full-width "("))) - (should (string-equal "(:)" (zero-convert-str-to-full-width "(:)"))) - (should (string-equal "ABab" (zero-convert-str-to-full-width "ABab"))) - (should (string-equal "hehe" (zero-convert-str-to-full-width "hehe"))) - (should (string-equal "(A)" (zero-convert-str-to-full-width "(A)")))) - -(provide 'zero-framework-test) - -;;; zero-framework-test.el ends here diff --git a/zero-framework.el b/zero-framework.el deleted file mode 100644 index 103eefaa30998275b4d46e216e66b33df4eafbab..0000000000000000000000000000000000000000 --- a/zero-framework.el +++ /dev/null @@ -1,860 +0,0 @@ -;;; zero-framework.el --- Zero Chinese input method framework -*- lexical-binding: t -*- - -;; Licensed under the Apache License, Version 2.0 (the "License"); -;; you may not use this file except in compliance with the License. -;; You may obtain a copy of the License at -;; -;; http://www.apache.org/licenses/LICENSE-2.0 -;; -;; Unless required by applicable law or agreed to in writing, software -;; distributed under the License is distributed on an "AS IS" BASIS, -;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -;; See the License for the specific language governing permissions and -;; limitations under the License. - -;;; Commentary: - -;; zero-framework is a Chinese input method framework for Emacs, implemented -;; as an Emacs minor mode. -;; -;; You can cycle zero-punctuation-level in current buffer by C-c , , -;; You can change default Chinese punctuation level: -;; -;; (setq-default zero-punctuation-level *zero-punctuation-level-full*) -;; -;; You can toggle full-width mode in current buffer by C-c , . -;; You can enable full-width mode by default: -;; -;; (setq-default zero-full-width-mode t) -;; - -;;; Code: - -;;============== -;; dependencies -;;============== - -(eval-when-compile (require 'cl-lib)) -(require 's) -(require 'zero-panel) - -;;======= -;; utils -;;======= - -;; this function is from ibus.el -(defun zero--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). -Origin (0, 0) is at screen top left corner." - (cl-destructuring-bind (x y line-height) (zero--ibus-compute-pixel-position) - (cond - ((functionp 'window-absolute-pixel-position) - ;; introduced in emacs 26 - (cl-destructuring-bind (x . y) (window-absolute-pixel-position) - (list x (+ y line-height)))) - ((functionp 'frame-edges) - ;; introduced in emacs 25 - (cl-destructuring-bind (frame-x frame-y &rest rest) - (frame-edges nil 'inner-edges) - (list (+ frame-x x) (+ frame-y y line-height)))) - (t - ;; <= emacs 24, used guessed pixel size for tool-bar, menu-bar, WM title - ;; bar. Since I can't get that from elisp. - (list (+ (frame-parameter nil 'left) - (if (and (> (frame-parameter nil 'tool-bar-lines) 0) - (eq (frame-parameter nil 'tool-bar-position) 'left)) - 96 0) - x) - (+ (frame-parameter nil 'top) - (if (and (> (frame-parameter nil 'tool-bar-lines) 0) - (eq (frame-parameter nil 'tool-bar-position) 'top)) - 42 0) - (if (> (frame-parameter nil 'menu-bar-lines) 0) (+ 30 30) 0) - line-height - y)))))) - -(defun zero-cycle-list (lst item) - "Return the object next to given ITEM in LST. - -If item is the last object, return the first object in lst. -If item is not in lst, return nil." - (let ((r (member item lst))) - (cond - ((null r) nil) - (t (or (cadr r) - (car lst)))))) - -;;===================== -;; key logic functions -;;===================== - -;; zero-el version -(defvar zero-version nil "Zero package version.") -(setq zero-version "1.3.4") - -;; 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) - -(defconst zero-punctuation-level-basic 'BASIC) -(defconst zero-punctuation-level-full 'FULL) -(defconst zero-punctuation-level-none 'NONE) - -(defvar zero-im nil - "Stores 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-full-width-mode nil - "Set to t to enable full-width mode. -In full-width mode, commit ascii char will insert full-width char if there is a -corresponding full-width char. This full-width char map is -independent from punctuation map. You can change this via -`zero-toggle-full-width-mode'") -(defvar zero-punctuation-level zero-punctuation-level-basic - "Punctuation level. - -Should be one of -`zero-punctuation-level-basic' -`zero-punctuation-level-full' -`zero-punctuation-level-none'") -(defvar zero-punctuation-levels (list zero-punctuation-level-basic - zero-punctuation-level-full - zero-punctuation-level-none) - "Punctuation levels to use when `zero-cycle-punctuation-level'.") -(defvar zero-double-quote-flag nil - "Non-nil means next double quote insert close quote. - -Used when converting double quote to Chinese quote. -If nil, next double quote insert open quote. -Otherwise, next double quote insert close quote.") -(defvar zero-single-quote-flag nil - "Non-nil means next single quote insert close quote. - -Used when converting single quote to Chinese quote. -If nil, next single quote insert open quote. -Otherwise, next single quote insert close quote.") -(defvar zero-preedit-str "") -(defvar zero-candidates nil) -(defcustom zero-candidates-per-page 10 - "How many candidates to show on each page." - :group 'zero - :type 'integer) -(defvar zero-current-page 0 "Current page number. count from 0.") -(defvar zero-initial-fetch-size 20 - "How many candidates to fetch for the first call to GetCandidates.") -;; zero-fetch-size is reset to 0 when preedit-str changes. -;; zero-fetch-size is set to fetch-size in build-candidates-async complete-func -;; lambda. -(defvar zero-fetch-size 0 "Last GetCandidates call's fetch-size.") -(defvar zero-previous-page-key ?\- "Previous page key.") -(defvar zero-next-page-key ?\= "Next page key.") - -;;; concrete input method should define these functions and set them in the -;;; corresponding *-func variable. -(defun zero-build-candidates-default (_preedit-str _fetch-size) - "Default implementation for `zero-build-candidates-func'." - nil) -(defun zero-can-start-sequence-default (_ch) - "Default implementation for `zero-can-start-sequence-func'." - nil) -(defun zero-get-preedit-str-for-panel-default () - "Default implementation for `zero-get-preedit-str-for-panel-func'." - zero-preedit-str) -(defvar zero-build-candidates-func 'zero-build-candidates-default - "Contains a function to build candidates from preedit-str. The function accepts param preedit-str, fetch-size, returns candidate list.") -(defvar zero-build-candidates-async-func 'zero-build-candidates-async-default - "Contains a function to build candidates from preedit-str. The function accepts param preedit-str, fetch-size, and a complete-func that should be called on returned candidate list.") -(defvar zero-can-start-sequence-func 'zero-can-start-sequence-default - "Contains a function to decide whether a char can start a preedit sequence.") -(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-handle-preedit-char-func 'zero-handle-preedit-char-default - "Hanlde character insert in `zero--state-im-preediting' mode.") -(defvar zero-preedit-start-func 'nil - "Called when enter `zero--state-im-preediting' state.") -(defvar zero-preedit-end-func 'nil - "Called when leave `zero--state-im-preediting' state.") - -(defvar zero-enable-debug nil - "Whether to enable debug. -if t, `zero-debug' will output debug msg in *zero-debug* buffer") -(defvar zero-debug-buffer-max-size 30000 - "Max characters in *zero-debug* buffer. If reached, first half data will be deleted.") - -(defun zero-debug (string &rest objects) - "Log debug message in *zero-debug* buffer. - -STRING and OBJECTS are passed to `format'" - (if zero-enable-debug - (with-current-buffer (get-buffer-create "*zero-debug*") - (goto-char (point-max)) - (insert (apply 'format string objects)) - (when (> (point) zero-debug-buffer-max-size) - (insert "removing old data\n") - (delete-region (point-min) (/ zero-debug-buffer-max-size 2)))))) - -;; (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-enter-preedit-state () - "Config keymap when enter preedit state." - (zero-enable-preediting-map) - (if (functionp zero-preedit-start-func) - (funcall zero-preedit-start-func))) - -(defun zero-leave-preedit-state () - "Config keymap when leave preedit state." - (zero-disable-preediting-map) - (if (functionp zero-preedit-end-func) - (funcall zero-preedit-end-func))) - -(defun zero-set-state (state) - "Set zero state to given STATE." - (zero-debug "set state to %s\n" state) - (setq zero-state state) - (if (eq state zero--state-im-preediting) - (zero-enter-preedit-state) - (zero-leave-preedit-state))) - -(defun zero-candidates-on-page (candidates) - "Return candidates on current page for given CANDIDATES list." - (cl-flet ((take (n lst) - "take the first n element from lst. if there is not -enough elements, return lst as it is." - (cl-loop - for lst* = lst then (cdr lst*) - for n* = n then (1- n*) - until (or (zerop n*) (null lst*)) - collect (car lst*))) - (drop (n lst) - "drop the first n elements from lst" - (cl-loop - for lst* = lst then (cdr lst*) - for n* = n then (1- n*) - until (or (zerop n*) (null lst*)) - finally (return lst*)))) - (take zero-candidates-per-page - (drop (* zero-candidates-per-page zero-current-page) candidates)))) - -(defun zero-show-candidates (&optional candidates) - "Show CANDIDATES using zero-panel via IPC/RPC." - (let ((candidates-on-page (zero-candidates-on-page (or candidates - zero-candidates)))) - (cl-destructuring-bind (x y) (zero-get-point-position) - (zero-panel-show-candidates - (funcall zero-get-preedit-str-for-panel-func) - (length candidates-on-page) - candidates-on-page - `(("in_emacs" t) - ("filename" ,(or (buffer-file-name) "")) - ("page_number" ,(1+ zero-current-page)) - ("has_next_page" ,(or (> (length (or candidates zero-candidates)) (* zero-candidates-per-page (1+ zero-current-page))) (< zero-fetch-size (* zero-candidates-per-page (+ 2 zero-current-page))))) - ("has_previous_page" ,(> zero-current-page 0)) - ("move_x" :int32 ,x) - ("move_y" :int32 ,y))) - (zero-debug "candidates: %s\n" (s-join ", " candidates-on-page))))) - -(defun zero-build-candidates (preedit-str fetch-size) - "Build candidates list synchronously. - -Try to find at least FETCH-SIZE number of candidates for PREEDIT-STR." - ;; (zero-debug "zero-build-candidates\n") - (unless (functionp zero-build-candidates-func) - (signal 'wrong-type-argument (list 'functionp zero-build-candidates-func))) - (prog1 (funcall zero-build-candidates-func preedit-str fetch-size) - (setq zero-fetch-size (max fetch-size (length zero-candidates))))) - -(defun zero-build-candidates-complete (candidates) - "Called when `zero-build-candidates-async' return. - -CANDIDATES is returned candidates list from async call." - (setq zero-candidates candidates) - (zero-show-candidates candidates)) - -(defun zero-build-candidates-async-default (preedit-str fetch-size complete-func) - "Build candidate list, when done show it via `zero-show-candidates'. - -PREEDIT-STR the preedit-str. -FETCH-SIZE try to find at least this many candidates for preedit-str. -COMPLETE-FUNC the function to call when build candidates completes." - ;; (zero-debug "zero-build-candidates-async-default\n") - (let ((candidates (zero-build-candidates preedit-str fetch-size))) - ;; update cache to make SPC and digit key selection possible. - (funcall complete-func candidates))) - -(defvar zero-full-width-char-map - ;; ascii 33 to 126 map to - ;; unicode FF01 to FF5E - (cl-loop - for i from 33 to 126 - collect (cons (make-char 'ascii i) (make-char 'unicode 0 255 (- i 32)))) - "An alist that map half-width char to full-width char.") - -(defun zero-convert-ch-to-full-width (ch) - "Convert half-width char CH to full-width. - -If there is no full-width char for CH, return it unchanged." - (let ((pair (assoc ch zero-full-width-char-map))) - (if pair (cdr pair) ch))) - -(defun zero-convert-str-to-full-width (s) - "Convert each char in S to their full-width char if there is one." - (concat (mapcar 'zero-convert-ch-to-full-width s))) - -(defun zero-convert-str-to-full-width-maybe (s) - "If in `zero-full-width-mode', convert char in S to their full-width char; otherwise, return s unchanged." - (if zero-full-width-mode (zero-convert-str-to-full-width s) s)) - -(defun zero-insert-full-width-char (ch) - "If in `zero-full-width-mode', insert full-width char for given CH and return true, otherwise just return nil." - (when zero-full-width-mode - (let ((full-width-ch (zero-convert-ch-to-full-width ch))) - (insert full-width-ch) - full-width-ch))) - -(defun zero-convert-punctuation-basic (ch) - "Convert punctuation for `zero-punctuation-level-basic'. - -Return CH's Chinese punctuation if CH is converted. Return nil otherwise." - (cl-case ch - (?, ",") - (?. "。") ; 0x3002 - (?? "?") - (?! "!") - (?\\ "、") ; 0x3001 - (?: ":") - (otherwise nil))) - -(defun zero-convert-punctuation-full (ch) - "Convert punctuation for `zero-punctuation-level-full'. - -Return CH's Chinese punctuation if CH is converted. Return nil otherwise" - (cl-case ch - (?_ "——") - (?< "《") ;0x300A - (?> "》") ;0x300B - (?\( "(") - (?\) ")") - (?\[ "【") ;0x3010 - (?\] "】") ;0x3011 - (?^ "……") - (?\" (setq zero-double-quote-flag (not zero-double-quote-flag)) - (if zero-double-quote-flag "“" "”")) - (?\' (setq zero-single-quote-flag (not zero-single-quote-flag)) - (if zero-single-quote-flag "‘" "’")) - (?~ "~") - (?\; ";") - (?$ "¥") - (t (zero-convert-punctuation-basic ch)))) - -(defun zero-convert-punctuation (ch) - "Convert punctuation based on `zero-punctuation-level'. -Return CH's Chinese punctuation if CH is converted. Return nil otherwise." - (cond - ((eq zero-punctuation-level zero-punctuation-level-basic) - (zero-convert-punctuation-basic ch)) - ((eq zero-punctuation-level zero-punctuation-level-full) - (zero-convert-punctuation-full ch)) - (t nil))) - -(defun zero-handle-punctuation (ch) - "If CH is a punctuation character, insert mapped Chinese punctuation and return true; otherwise, return false." - (let ((str (zero-convert-punctuation ch))) - (when str - (insert str) - t))) - -(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"))) - -(defun zero-page-up () - "If not at first page, show candidates on previous page." - (interactive) - (when (> zero-current-page 0) - (setq zero-current-page (1- zero-current-page)) - (zero-show-candidates))) - -(defun zero-just-page-down () - "Just page down using existing candidates." - (let ((len (length zero-candidates))) - (when (> len (* zero-candidates-per-page (1+ zero-current-page))) - (setq zero-current-page (1+ zero-current-page)) - (zero-show-candidates)))) - -(defun zero-page-down () - "If there is still candidates to be displayed, show candidates on next page." - (interactive) - (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)) - (funcall zero-build-candidates-async-func - zero-preedit-str - new-fetch-size - (lambda (candidates) - (zero-build-candidates-complete candidates) - (setq zero-fetch-size (max new-fetch-size - (length candidates))) - (zero-just-page-down))) - (zero-just-page-down)))) - -(defun zero-handle-preedit-char-default (ch) - "Hanlde character insert in `zero--state-im-preediting' state. - -CH is the char user has typed." - (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)))))) - -(defun zero-self-insert-command (n) - "Handle character `self-insert-command'. This includes characters and digits. - -N is the argument passed to `self-insert-command'." - (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) - (unless (zero-insert-full-width-char ch) - (self-insert-command n))))) - ((eq zero-state zero--state-im-preediting) - (zero-debug "still preediting\n") - (funcall zero-handle-preedit-char-func 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." - (setq zero-fetch-size 0) - (setq zero-current-page 0) - (funcall zero-build-candidates-async-func zero-preedit-str zero-initial-fetch-size 'zero-build-candidates-complete)) - -(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-backspace () - "Handle backspace key in `zero--state-im-preediting' state." - (interactive) - (unless (eq zero-state zero--state-im-preediting) - (error "Error: zero-backspace called in non preediting state")) - (zero-debug "zero-backspace\n") - (funcall zero-backspace-func)) - -(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) - (setq zero-current-page 0) - (zero-hide-candidate-list)) - -(defun zero-return () - "Handle RET key press in `zero--state-im-preediting' state." - (interactive) - (unless (eq zero-state zero--state-im-preediting) - (error "Error: zero-return called in non preediting state")) - (zero-debug "zero-return\n") - (zero-set-state zero--state-im-waiting-input) - (zero-commit-text (zero-convert-str-to-full-width-maybe zero-preedit-str))) - -(defun zero-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)))) - (if candidate - (progn - (zero-set-state zero--state-im-waiting-input) - (zero-commit-text candidate) - t) - nil))) - -(defun zero-commit-preedit-str () - "Commit current preedit-str." - (zero-set-state zero--state-im-waiting-input) - (zero-commit-text (zero-convert-str-to-full-width-maybe zero-preedit-str))) - -(defun zero-commit-first-candidate-or-preedit-str () - "Commit first candidate if there is one, otherwise commit preedit str." - (unless (zero-commit-nth-candidate 0) - (zero-commit-preedit-str))) - -(defun zero-hide-candidate-list () - "Hide candidate list." - (zero-panel-hide) - (zero-debug "hide candidate list\n")) - -(defun zero-reset () - "Reset zero states." - (interactive) - (zero-debug "zero-reset\n") - (zero-set-state zero--state-im-waiting-input) - (setq zero-preedit-str "") - (setq zero-candidates nil) - (setq zero-current-page 0) - (zero-hide-candidate-list)) - -(defun zero-focus-in () - "A hook function, run when focus in a buffer." - (when (eq zero-state zero--state-im-preediting) - (zero-show-candidates zero-candidates) - (zero-enter-preedit-state))) - -(defun zero-focus-out () - "A hook function, run when focus out a buffer." - (when (eq zero-state zero--state-im-preediting) - (zero-hide-candidate-list) - (zero-leave-preedit-state))) - -(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 -;;============ - -(defvar zero-mode-map - (let ((map (make-sparse-keymap))) - ;; build zero-prefix-map - (defvar zero-prefix-map (define-prefix-command 'zero-prefix-map)) - (let ((bindings '(("," zero-cycle-punctuation-level) - ("." zero-toggle-full-width-mode)))) - (dolist (b bindings) - (define-key zero-prefix-map (car b) (cadr b)))) - ;; mount zero-prefix-map in C-c , prefix key. - (define-key map (kbd "C-c ,") zero-prefix-map) - - ;; other keybindings - (define-key map [remap self-insert-command] - 'zero-self-insert-command) - map) - "Keymap for `zero-mode'.") - -(defun zero-enable-preediting-map () - "Enable preediting keymap in `zero-mode-map'." - (zero-debug "zero-enable-preediting-map\n") - (define-key zero-mode-map (kbd "<backspace>") 'zero-backspace) - (define-key zero-mode-map (kbd "RET") 'zero-return) - (define-key zero-mode-map (kbd "<escape>") 'zero-reset)) - -(defun zero-disable-preediting-map () - "Disable preediting keymap in `zero-mode-map'." - (zero-debug "zero-disable-preediting-map\n") - (define-key zero-mode-map (kbd "<backspace>") nil) - (define-key zero-mode-map (kbd "RET") nil) - (define-key zero-mode-map (kbd "<escape>") nil)) - -(defun zero-modeline-string () - "Build `zero-mode' modeline string aka lighter. - -If full-width mode is enabled, show ZeroF; -Otherwise, show Zero." - (if zero-full-width-mode " ZeroF" " Zero")) - -(define-minor-mode zero-mode - "a Chinese input method framework written as an emacs minor mode. - -\\{zero-mode-map}" - nil - (:eval (zero-modeline-string)) - zero-mode-map - ;; local variables and variable init - (make-local-variable 'zero-state) - (zero-set-state zero--state-im-off) - (make-local-variable 'zero-punctuation-level) - (make-local-variable 'zero-full-width-mode) - (make-local-variable 'zero-double-quote-flag) - (make-local-variable 'zero-single-quote-flag) - (set (make-local-variable 'zero-preedit-str) "") - (set (make-local-variable 'zero-candidates) nil) - (make-local-variable 'zero-candidates-per-page) - (make-local-variable 'zero-current-page) - (make-local-variable 'zero-fetch-size) - (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) - ;; hooks - (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 - '((:virtual-function-name . implementation-function-name)) - -virtual functions corresponding variable -=========================================================================== -:build-candidates `zero-build-candidates-func' -:can-start-sequence `zero-can-start-sequence-func' -:handle-preedit-char `zero-handle-preedit-char-func' -:get-preedit-str-for-panel `zero-get-preedit-str-for-panel-func' -:handle-backspace `zero-backspace-func' -:init nil -:shutdown nil -:preedit-start `zero-preedit-start-func' -:preedit-end `zero-preedit-end-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-toggle-full-width-mode () - "Toggle `zero-full-width-mode' on/off." - (interactive) - (setq zero-full-width-mode (not zero-full-width-mode)) - (message (if zero-full-width-mode - "Enabled full-width mode" - "Enabled half-width mode"))) - -(defun zero-set-punctuation-level (level) - "Set `zero-punctuation-level'. - -LEVEL the level to set to." - (interactive) - (if (not (member level (list zero-punctuation-level-basic - zero-punctuation-level-full - zero-punctuation-level-none))) - (error "Level not supported: %s" level) - (setq zero-punctuation-level level))) - -(defun zero-set-punctuation-levels (levels) - "Set `zero-punctuation-levels'. - -`zero-cycle-punctuation-level' will cycle current -`zero-punctuation-level' among defined LEVELS." - (dolist (level levels) - (if (not (member level (list zero-punctuation-level-basic - zero-punctuation-level-full - zero-punctuation-level-none))) - (error "Level not supported: %s" level))) - (setq zero-punctuation-levels levels)) - -(defun zero-cycle-punctuation-level () - "Cycle `zero-punctuation-level' among `zero-punctuation-levels'." - (interactive) - (setq zero-punctuation-level - (zero-cycle-list zero-punctuation-levels zero-punctuation-level)) - (message "punctuation level set to %s" zero-punctuation-level)) - -;;;###autoload -(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 auto completion for im-name - (interactive "SSet input method to: ") - ;; when switch away from an IM, run last IM's :shutdown function. - (if zero-im - (let ((shutdown-func (cdr (assq :shutdown (cdr (assq zero-im zero-ims)))))) - (if (functionp shutdown-func) - (funcall shutdown-func)))) - (if im-name - (let ((im-functions (cdr (assq im-name zero-ims)))) - (if im-functions - (progn - ;; TODO create a macro to reduce code duplication and human - ;; error. - ;; - ;; TODO do some functionp check for the slot functions. if check - ;; fail, keep (or revert to) the old IM. - (setq zero-build-candidates-func - (or (cdr (assq :build-candidates im-functions)) - 'zero-build-candidates-default)) - (setq zero-build-candidates-async-func - (or (cdr (assq :build-candidates-async im-functions)) - 'zero-build-candidates-async-default)) - (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)) - (setq zero-preedit-start-func - (cdr (assq :preedit-start im-functions))) - (setq zero-preedit-end-func - (cdr (assq :preedit-end im-functions))) - (unless (functionp zero-backspace-func) - (signal 'wrong-type-argument - (list 'functionp zero-backspace-func))) - ;; when switch to a IM, run its :init function - (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-build-candidates-async-func 'zero-build-candidates-async-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) - (setq zero-preedit-start-func nil) - (setq zero-preedit-end-func nil))) - -;;;###autoload -(defun zero-set-default-im (im-name) - "Set given IM-NAME as default zero input method." - (unless (symbolp im-name) - (signal 'wrong-type-argument (list 'symbolp im-name))) - (setq-default zero-im im-name)) - -;;;###autoload -(defun zero-on () - "Turn on `zero-mode'." - (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 () - "Turn off `zero-mode'." - (interactive) - (zero-debug "zero-off\n") - (zero-mode -1) - (zero-reset) - (zero-set-state zero--state-im-off)) - -;;;###autoload -(defun zero-toggle () - "Toggle `zero-mode'." - (interactive) - (if zero-mode - (zero-off) - (zero-on))) - -(provide 'zero-framework) - -;;; zero-framework.el ends here diff --git a/zero-input-framework-test.el b/zero-input-framework-test.el new file mode 100644 index 0000000000000000000000000000000000000000..64ae8517baff044608e61235dba6e29296a49f99 --- /dev/null +++ b/zero-input-framework-test.el @@ -0,0 +1,44 @@ +;;; zero-input-framework-test.el --- tests for zero-input-framework.el -*- lexical-binding: t -*- + +;; Licensed under the Apache License, Version 2.0 (the "License"); +;; you may not use this file except in compliance with the License. +;; You may obtain a copy of the License at +;; +;; http://www.apache.org/licenses/LICENSE-2.0 +;; +;; Unless required by applicable law or agreed to in writing, software +;; distributed under the License is distributed on an "AS IS" BASIS, +;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +;; See the License for the specific language governing permissions and +;; limitations under the License. + +;;; Commentary: + +;; tests for zero-input-framework.el + +;;; Code: + +(require 'zero-input-framework) +(require 'ert) + +(ert-deftest zero-input-cycle-list () + (should (= (zero-input-cycle-list '(1 2 3) 1) 2)) + (should (eq (zero-input-cycle-list '(a b c) 'a) 'b)) + (should (eq (zero-input-cycle-list '(a b c) 'b) 'c)) + (should (eq (zero-input-cycle-list '(a b c) 'c) 'a)) + (should (eq (zero-input-cycle-list '(a b c) 'd) nil))) + +(ert-deftest zero-input-convert-ch-to-full-width () + (should (= (zero-input-convert-ch-to-full-width ?\!) ?\!))) + +(ert-deftest zero-input-convert-str-to-full-width () + (should (string-equal "!" (zero-input-convert-str-to-full-width "!"))) + (should (string-equal "(" (zero-input-convert-str-to-full-width "("))) + (should (string-equal "(:)" (zero-input-convert-str-to-full-width "(:)"))) + (should (string-equal "ABab" (zero-input-convert-str-to-full-width "ABab"))) + (should (string-equal "hehe" (zero-input-convert-str-to-full-width "hehe"))) + (should (string-equal "(A)" (zero-input-convert-str-to-full-width "(A)")))) + +(provide 'zero-input-framework-test) + +;;; zero-input-framework-test.el ends here diff --git a/zero-input-framework.el b/zero-input-framework.el new file mode 100644 index 0000000000000000000000000000000000000000..017cd058031aa98f83755e3403196e2589a2a015 --- /dev/null +++ b/zero-input-framework.el @@ -0,0 +1,860 @@ +;;; zero-input-framework.el --- Zero Chinese input method framework -*- lexical-binding: t -*- + +;; Licensed under the Apache License, Version 2.0 (the "License"); +;; you may not use this file except in compliance with the License. +;; You may obtain a copy of the License at +;; +;; http://www.apache.org/licenses/LICENSE-2.0 +;; +;; Unless required by applicable law or agreed to in writing, software +;; distributed under the License is distributed on an "AS IS" BASIS, +;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +;; See the License for the specific language governing permissions and +;; limitations under the License. + +;;; Commentary: + +;; zero-input-framework is a Chinese input method framework for Emacs, +;; implemented as an Emacs minor mode. +;; +;; You can cycle zero-input-punctuation-level in current buffer by C-c , , +;; You can change default Chinese punctuation level: +;; +;; (setq-default zero-input-punctuation-level *zero-input-punctuation-level-full*) +;; +;; You can toggle full-width mode in current buffer by C-c , . +;; You can enable full-width mode by default: +;; +;; (setq-default zero-input-full-width-mode t) +;; + +;;; Code: + +;;============== +;; dependencies +;;============== + +(eval-when-compile (require 'cl-lib)) +(require 's) +(require 'zero-input-panel) + +;;======= +;; utils +;;======= + +;; this function is from ibus.el +(defun zero-input--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-input-get-point-position () + "Return current point's position (x y). +Origin (0, 0) is at screen top left corner." + (cl-destructuring-bind (x y line-height) (zero-input--ibus-compute-pixel-position) + (cond + ((functionp 'window-absolute-pixel-position) + ;; introduced in emacs 26 + (cl-destructuring-bind (x . y) (window-absolute-pixel-position) + (list x (+ y line-height)))) + ((functionp 'frame-edges) + ;; introduced in emacs 25 + (cl-destructuring-bind (frame-x frame-y &rest rest) + (frame-edges nil 'inner-edges) + (list (+ frame-x x) (+ frame-y y line-height)))) + (t + ;; <= emacs 24, used guessed pixel size for tool-bar, menu-bar, WM title + ;; bar. Since I can't get that from elisp. + (list (+ (frame-parameter nil 'left) + (if (and (> (frame-parameter nil 'tool-bar-lines) 0) + (eq (frame-parameter nil 'tool-bar-position) 'left)) + 96 0) + x) + (+ (frame-parameter nil 'top) + (if (and (> (frame-parameter nil 'tool-bar-lines) 0) + (eq (frame-parameter nil 'tool-bar-position) 'top)) + 42 0) + (if (> (frame-parameter nil 'menu-bar-lines) 0) (+ 30 30) 0) + line-height + y)))))) + +(defun zero-input-cycle-list (lst item) + "Return the object next to given ITEM in LST. + +If item is the last object, return the first object in lst. +If item is not in lst, return nil." + (let ((r (member item lst))) + (cond + ((null r) nil) + (t (or (cadr r) + (car lst)))))) + +;;===================== +;; key logic functions +;;===================== + +;; zero-input-el version +(defvar zero-input-version nil "Zero package version.") +(setq zero-input-version "2.0.0") + +;; FSM state +(defconst zero-input--state-im-off 'IM-OFF) +(defconst zero-input--state-im-waiting-input 'IM-WAITING-INPUT) +(defconst zero-input--state-im-preediting 'IM-PREEDITING) + +(defconst zero-input-punctuation-level-basic 'BASIC) +(defconst zero-input-punctuation-level-full 'FULL) +(defconst zero-input-punctuation-level-none 'NONE) + +(defvar zero-input-im nil + "Stores 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-input-ims nil + "A list of registered input methods.") + +(defvar zero-input-buffer nil + "Stores the associated buffer. +this is used to help with buffer focus in/out events") + +(defvar zero-input-state zero-input--state-im-off) +(defvar zero-input-full-width-mode nil + "Set to t to enable full-width mode. +In full-width mode, commit ascii char will insert full-width char if there is a +corresponding full-width char. This full-width char map is +independent from punctuation map. You can change this via +`zero-input-toggle-full-width-mode'") +(defvar zero-input-punctuation-level zero-input-punctuation-level-basic + "Punctuation level. + +Should be one of +`zero-input-punctuation-level-basic' +`zero-input-punctuation-level-full' +`zero-input-punctuation-level-none'") +(defvar zero-input-punctuation-levels (list zero-input-punctuation-level-basic + zero-input-punctuation-level-full + zero-input-punctuation-level-none) + "Punctuation levels to use when `zero-input-cycle-punctuation-level'.") +(defvar zero-input-double-quote-flag nil + "Non-nil means next double quote insert close quote. + +Used when converting double quote to Chinese quote. +If nil, next double quote insert open quote. +Otherwise, next double quote insert close quote.") +(defvar zero-input-single-quote-flag nil + "Non-nil means next single quote insert close quote. + +Used when converting single quote to Chinese quote. +If nil, next single quote insert open quote. +Otherwise, next single quote insert close quote.") +(defvar zero-input-preedit-str "") +(defvar zero-input-candidates nil) +(defcustom zero-input-candidates-per-page 10 + "How many candidates to show on each page." + :group 'zero + :type 'integer) +(defvar zero-input-current-page 0 "Current page number. count from 0.") +(defvar zero-input-initial-fetch-size 20 + "How many candidates to fetch for the first call to GetCandidates.") +;; zero-input-fetch-size is reset to 0 when preedit-str changes. +;; zero-input-fetch-size is set to fetch-size in build-candidates-async complete-func +;; lambda. +(defvar zero-input-fetch-size 0 "Last GetCandidates call's fetch-size.") +(defvar zero-input-previous-page-key ?\- "Previous page key.") +(defvar zero-input-next-page-key ?\= "Next page key.") + +;;; concrete input method should define these functions and set them in the +;;; corresponding *-func variable. +(defun zero-input-build-candidates-default (_preedit-str _fetch-size) + "Default implementation for `zero-input-build-candidates-func'." + nil) +(defun zero-input-can-start-sequence-default (_ch) + "Default implementation for `zero-input-can-start-sequence-func'." + nil) +(defun zero-input-get-preedit-str-for-panel-default () + "Default implementation for `zero-input-get-preedit-str-for-panel-func'." + zero-input-preedit-str) +(defvar zero-input-build-candidates-func 'zero-input-build-candidates-default + "Contains a function to build candidates from preedit-str. The function accepts param preedit-str, fetch-size, returns candidate list.") +(defvar zero-input-build-candidates-async-func 'zero-input-build-candidates-async-default + "Contains a function to build candidates from preedit-str. The function accepts param preedit-str, fetch-size, and a complete-func that should be called on returned candidate list.") +(defvar zero-input-can-start-sequence-func 'zero-input-can-start-sequence-default + "Contains a function to decide whether a char can start a preedit sequence.") +(defvar zero-input-handle-preedit-char-func 'zero-input-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-input-get-preedit-str-for-panel-func 'zero-input-get-preedit-str-for-panel-default + "Contains a function that return preedit-str to show in zero-input-panel.") +(defvar zero-input-backspace-func 'zero-input-backspace-default + "Contains a function to handle <backward> char.") +(defvar zero-input-handle-preedit-char-func 'zero-input-handle-preedit-char-default + "Hanlde character insert in `zero-input--state-im-preediting' mode.") +(defvar zero-input-preedit-start-func 'nil + "Called when enter `zero-input--state-im-preediting' state.") +(defvar zero-input-preedit-end-func 'nil + "Called when leave `zero-input--state-im-preediting' state.") + +(defvar zero-input-enable-debug nil + "Whether to enable debug. +if t, `zero-input-debug' will output debug msg in *zero-input-debug* buffer") +(defvar zero-input-debug-buffer-max-size 30000 + "Max characters in *zero-input-debug* buffer. If reached, first half data will be deleted.") + +(defun zero-input-debug (string &rest objects) + "Log debug message in *zero-input-debug* buffer. + +STRING and OBJECTS are passed to `format'" + (if zero-input-enable-debug + (with-current-buffer (get-buffer-create "*zero-input-debug*") + (goto-char (point-max)) + (insert (apply 'format string objects)) + (when (> (point) zero-input-debug-buffer-max-size) + (insert "removing old data\n") + (delete-region (point-min) (/ zero-input-debug-buffer-max-size 2)))))) + +;; (zero-input-debug "msg1\n") +;; (zero-input-debug "msg2: %s\n" "some obj") +;; (zero-input-debug "msg3: %s\n" 24) +;; (zero-input-debug "msg4: %s %s\n" 24 1) + +(defun zero-input-enter-preedit-state () + "Config keymap when enter preedit state." + (zero-input-enable-preediting-map) + (if (functionp zero-input-preedit-start-func) + (funcall zero-input-preedit-start-func))) + +(defun zero-input-leave-preedit-state () + "Config keymap when leave preedit state." + (zero-input-disable-preediting-map) + (if (functionp zero-input-preedit-end-func) + (funcall zero-input-preedit-end-func))) + +(defun zero-input-set-state (state) + "Set zero state to given STATE." + (zero-input-debug "set state to %s\n" state) + (setq zero-input-state state) + (if (eq state zero-input--state-im-preediting) + (zero-input-enter-preedit-state) + (zero-input-leave-preedit-state))) + +(defun zero-input-candidates-on-page (candidates) + "Return candidates on current page for given CANDIDATES list." + (cl-flet ((take (n lst) + "take the first n element from lst. if there is not +enough elements, return lst as it is." + (cl-loop + for lst* = lst then (cdr lst*) + for n* = n then (1- n*) + until (or (zerop n*) (null lst*)) + collect (car lst*))) + (drop (n lst) + "drop the first n elements from lst" + (cl-loop + for lst* = lst then (cdr lst*) + for n* = n then (1- n*) + until (or (zerop n*) (null lst*)) + finally (return lst*)))) + (take zero-input-candidates-per-page + (drop (* zero-input-candidates-per-page zero-input-current-page) candidates)))) + +(defun zero-input-show-candidates (&optional candidates) + "Show CANDIDATES using zero-input-panel via IPC/RPC." + (let ((candidates-on-page (zero-input-candidates-on-page (or candidates + zero-input-candidates)))) + (cl-destructuring-bind (x y) (zero-input-get-point-position) + (zero-input-panel-show-candidates + (funcall zero-input-get-preedit-str-for-panel-func) + (length candidates-on-page) + candidates-on-page + `(("in_emacs" t) + ("filename" ,(or (buffer-file-name) "")) + ("page_number" ,(1+ zero-input-current-page)) + ("has_next_page" ,(or (> (length (or candidates zero-input-candidates)) (* zero-input-candidates-per-page (1+ zero-input-current-page))) (< zero-input-fetch-size (* zero-input-candidates-per-page (+ 2 zero-input-current-page))))) + ("has_previous_page" ,(> zero-input-current-page 0)) + ("move_x" :int32 ,x) + ("move_y" :int32 ,y))) + (zero-input-debug "candidates: %s\n" (s-join ", " candidates-on-page))))) + +(defun zero-input-build-candidates (preedit-str fetch-size) + "Build candidates list synchronously. + +Try to find at least FETCH-SIZE number of candidates for PREEDIT-STR." + ;; (zero-input-debug "zero-input-build-candidates\n") + (unless (functionp zero-input-build-candidates-func) + (signal 'wrong-type-argument (list 'functionp zero-input-build-candidates-func))) + (prog1 (funcall zero-input-build-candidates-func preedit-str fetch-size) + (setq zero-input-fetch-size (max fetch-size (length zero-input-candidates))))) + +(defun zero-input-build-candidates-complete (candidates) + "Called when `zero-input-build-candidates-async' return. + +CANDIDATES is returned candidates list from async call." + (setq zero-input-candidates candidates) + (zero-input-show-candidates candidates)) + +(defun zero-input-build-candidates-async-default (preedit-str fetch-size complete-func) + "Build candidate list, when done show it via `zero-input-show-candidates'. + +PREEDIT-STR the preedit-str. +FETCH-SIZE try to find at least this many candidates for preedit-str. +COMPLETE-FUNC the function to call when build candidates completes." + ;; (zero-input-debug "zero-input-build-candidates-async-default\n") + (let ((candidates (zero-input-build-candidates preedit-str fetch-size))) + ;; update cache to make SPC and digit key selection possible. + (funcall complete-func candidates))) + +(defvar zero-input-full-width-char-map + ;; ascii 33 to 126 map to + ;; unicode FF01 to FF5E + (cl-loop + for i from 33 to 126 + collect (cons (make-char 'ascii i) (make-char 'unicode 0 255 (- i 32)))) + "An alist that map half-width char to full-width char.") + +(defun zero-input-convert-ch-to-full-width (ch) + "Convert half-width char CH to full-width. + +If there is no full-width char for CH, return it unchanged." + (let ((pair (assoc ch zero-input-full-width-char-map))) + (if pair (cdr pair) ch))) + +(defun zero-input-convert-str-to-full-width (s) + "Convert each char in S to their full-width char if there is one." + (concat (mapcar 'zero-input-convert-ch-to-full-width s))) + +(defun zero-input-convert-str-to-full-width-maybe (s) + "If in `zero-input-full-width-mode', convert char in S to their full-width char; otherwise, return s unchanged." + (if zero-input-full-width-mode (zero-input-convert-str-to-full-width s) s)) + +(defun zero-input-insert-full-width-char (ch) + "If in `zero-input-full-width-mode', insert full-width char for given CH and return true, otherwise just return nil." + (when zero-input-full-width-mode + (let ((full-width-ch (zero-input-convert-ch-to-full-width ch))) + (insert full-width-ch) + full-width-ch))) + +(defun zero-input-convert-punctuation-basic (ch) + "Convert punctuation for `zero-input-punctuation-level-basic'. + +Return CH's Chinese punctuation if CH is converted. Return nil otherwise." + (cl-case ch + (?, ",") + (?. "。") ; 0x3002 + (?? "?") + (?! "!") + (?\\ "、") ; 0x3001 + (?: ":") + (otherwise nil))) + +(defun zero-input-convert-punctuation-full (ch) + "Convert punctuation for `zero-input-punctuation-level-full'. + +Return CH's Chinese punctuation if CH is converted. Return nil otherwise" + (cl-case ch + (?_ "——") + (?< "《") ;0x300A + (?> "》") ;0x300B + (?\( "(") + (?\) ")") + (?\[ "【") ;0x3010 + (?\] "】") ;0x3011 + (?^ "……") + (?\" (setq zero-input-double-quote-flag (not zero-input-double-quote-flag)) + (if zero-input-double-quote-flag "“" "”")) + (?\' (setq zero-input-single-quote-flag (not zero-input-single-quote-flag)) + (if zero-input-single-quote-flag "‘" "’")) + (?~ "~") + (?\; ";") + (?$ "¥") + (t (zero-input-convert-punctuation-basic ch)))) + +(defun zero-input-convert-punctuation (ch) + "Convert punctuation based on `zero-input-punctuation-level'. +Return CH's Chinese punctuation if CH is converted. Return nil otherwise." + (cond + ((eq zero-input-punctuation-level zero-input-punctuation-level-basic) + (zero-input-convert-punctuation-basic ch)) + ((eq zero-input-punctuation-level zero-input-punctuation-level-full) + (zero-input-convert-punctuation-full ch)) + (t nil))) + +(defun zero-input-handle-punctuation (ch) + "If CH is a punctuation character, insert mapped Chinese punctuation and return true; otherwise, return false." + (let ((str (zero-input-convert-punctuation ch))) + (when str + (insert str) + t))) + +(defun zero-input-append-char-to-preedit-str (ch) + "Append char CH to preedit str, update and show candidate list." + (setq zero-input-preedit-str + (concat zero-input-preedit-str (make-string 1 ch))) + (zero-input-debug "appended %c, preedit str is: %s\n" ch zero-input-preedit-str) + (zero-input-preedit-str-changed)) + +(defun zero-input-can-start-sequence (ch) + "Return t if char CH can start a preedit sequence." + (if (functionp zero-input-can-start-sequence-func) + (funcall zero-input-can-start-sequence-func ch) + (error "`zero-input-can-start-sequence-func' is not a function"))) + +(defun zero-input-page-up () + "If not at first page, show candidates on previous page." + (interactive) + (when (> zero-input-current-page 0) + (setq zero-input-current-page (1- zero-input-current-page)) + (zero-input-show-candidates))) + +(defun zero-input-just-page-down () + "Just page down using existing candidates." + (let ((len (length zero-input-candidates))) + (when (> len (* zero-input-candidates-per-page (1+ zero-input-current-page))) + (setq zero-input-current-page (1+ zero-input-current-page)) + (zero-input-show-candidates)))) + +(defun zero-input-page-down () + "If there is still candidates to be displayed, show candidates on next page." + (interactive) + (let ((len (length zero-input-candidates)) + (new-fetch-size (* zero-input-candidates-per-page (+ 2 zero-input-current-page)))) + (if (and (< len new-fetch-size) + (< zero-input-fetch-size new-fetch-size)) + (funcall zero-input-build-candidates-async-func + zero-input-preedit-str + new-fetch-size + (lambda (candidates) + (zero-input-build-candidates-complete candidates) + (setq zero-input-fetch-size (max new-fetch-size + (length candidates))) + (zero-input-just-page-down))) + (zero-input-just-page-down)))) + +(defun zero-input-handle-preedit-char-default (ch) + "Hanlde character insert in `zero-input--state-im-preediting' state. + +CH is the char user has typed." + (cond + ((= ch ?\s) + (zero-input-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-input-commit-nth-candidate (mod (- (- ch ?0) 1) 10)) + (zero-input-append-char-to-preedit-str ch))) + ((= ch zero-input-previous-page-key) + (zero-input-page-up)) + ((= ch zero-input-next-page-key) + (zero-input-page-down)) + (t (let ((str (zero-input-convert-punctuation ch))) + (if str + (progn + (zero-input-set-state zero-input--state-im-waiting-input) + (zero-input-commit-first-candidate-or-preedit-str) + (insert str)) + (zero-input-append-char-to-preedit-str ch)))))) + +(defun zero-input-self-insert-command (n) + "Handle character `self-insert-command'. This includes characters and digits. + +N is the argument passed to `self-insert-command'." + (interactive "p") + (let ((ch (elt (this-command-keys-vector) 0))) + (zero-input-debug "user typed: %c\n" ch) + (cond + ((eq zero-input-state zero-input--state-im-waiting-input) + (if (zero-input-can-start-sequence ch) + (progn + (zero-input-debug "can start sequence, state=IM_PREEDITING\n") + (zero-input-set-state zero-input--state-im-preediting) + (zero-input-append-char-to-preedit-str ch)) + (zero-input-debug "cannot start sequence, state=IM_WAITING_INPUT\n") + (unless (zero-input-handle-punctuation ch) + (unless (zero-input-insert-full-width-char ch) + (self-insert-command n))))) + ((eq zero-input-state zero-input--state-im-preediting) + (zero-input-debug "still preediting\n") + (funcall zero-input-handle-preedit-char-func ch)) + (t + (zero-input-debug "unexpected state: %s\n" zero-input-state) + (self-insert-command n))))) + +(defun zero-input-preedit-str-changed () + "Called when preedit str is changed and not empty. Update and show candidate list." + (setq zero-input-fetch-size 0) + (setq zero-input-current-page 0) + (funcall zero-input-build-candidates-async-func zero-input-preedit-str zero-input-initial-fetch-size 'zero-input-build-candidates-complete)) + +(defun zero-input-backspace-default () + "Handle backspace key in `zero-input--state-im-preediting' state." + (let ((len (length zero-input-preedit-str))) + (if (> len 1) + (progn + (setq zero-input-preedit-str + (substring zero-input-preedit-str 0 (1- len))) + (zero-input-preedit-str-changed)) + (zero-input-set-state zero-input--state-im-waiting-input) + (zero-input-reset)))) + +(defun zero-input-backspace () + "Handle backspace key in `zero-input--state-im-preediting' state." + (interactive) + (unless (eq zero-input-state zero-input--state-im-preediting) + (error "Error: zero-input-backspace called in non preediting state")) + (zero-input-debug "zero-input-backspace\n") + (funcall zero-input-backspace-func)) + +(defun zero-input-commit-text (text) + "Commit given TEXT, reset preedit str, hide candidate list." + (zero-input-debug "commit text: %s\n" text) + (insert text) + (setq zero-input-preedit-str "") + (setq zero-input-candidates nil) + (setq zero-input-current-page 0) + (zero-input-hide-candidate-list)) + +(defun zero-input-return () + "Handle RET key press in `zero-input--state-im-preediting' state." + (interactive) + (unless (eq zero-input-state zero-input--state-im-preediting) + (error "Error: zero-input-return called in non preediting state")) + (zero-input-debug "zero-input-return\n") + (zero-input-set-state zero-input--state-im-waiting-input) + (zero-input-commit-text (zero-input-convert-str-to-full-width-maybe zero-input-preedit-str))) + +(defun zero-input-commit-nth-candidate (n) + "Commit Nth candidate and return true if it exists; otherwise, return false." + (let ((candidate (nth n (zero-input-candidates-on-page zero-input-candidates)))) + (if candidate + (progn + (zero-input-set-state zero-input--state-im-waiting-input) + (zero-input-commit-text candidate) + t) + nil))) + +(defun zero-input-commit-preedit-str () + "Commit current preedit-str." + (zero-input-set-state zero-input--state-im-waiting-input) + (zero-input-commit-text (zero-input-convert-str-to-full-width-maybe zero-input-preedit-str))) + +(defun zero-input-commit-first-candidate-or-preedit-str () + "Commit first candidate if there is one, otherwise commit preedit str." + (unless (zero-input-commit-nth-candidate 0) + (zero-input-commit-preedit-str))) + +(defun zero-input-hide-candidate-list () + "Hide candidate list." + (zero-input-panel-hide) + (zero-input-debug "hide candidate list\n")) + +(defun zero-input-reset () + "Reset zero states." + (interactive) + (zero-input-debug "zero-input-reset\n") + (zero-input-set-state zero-input--state-im-waiting-input) + (setq zero-input-preedit-str "") + (setq zero-input-candidates nil) + (setq zero-input-current-page 0) + (zero-input-hide-candidate-list)) + +(defun zero-input-focus-in () + "A hook function, run when focus in a buffer." + (when (eq zero-input-state zero-input--state-im-preediting) + (zero-input-show-candidates zero-input-candidates) + (zero-input-enter-preedit-state))) + +(defun zero-input-focus-out () + "A hook function, run when focus out a buffer." + (when (eq zero-input-state zero-input--state-im-preediting) + (zero-input-hide-candidate-list) + (zero-input-leave-preedit-state))) + +(defun zero-input-buffer-list-changed () + "A hook function, run when buffer list has changed. This includes user has switched buffer." + (if (eq (car (buffer-list)) zero-input-buffer) + (zero-input-focus-in))) + +;;============ +;; minor mode +;;============ + +(defvar zero-input-mode-map + (let ((map (make-sparse-keymap))) + ;; build zero-input-prefix-map + (defvar zero-input-prefix-map (define-prefix-command 'zero-input-prefix-map)) + (let ((bindings '(("," zero-input-cycle-punctuation-level) + ("." zero-input-toggle-full-width-mode)))) + (dolist (b bindings) + (define-key zero-input-prefix-map (car b) (cadr b)))) + ;; mount zero-input-prefix-map in C-c , prefix key. + (define-key map (kbd "C-c ,") zero-input-prefix-map) + + ;; other keybindings + (define-key map [remap self-insert-command] + 'zero-input-self-insert-command) + map) + "Keymap for `zero-input-mode'.") + +(defun zero-input-enable-preediting-map () + "Enable preediting keymap in `zero-input-mode-map'." + (zero-input-debug "zero-input-enable-preediting-map\n") + (define-key zero-input-mode-map (kbd "<backspace>") 'zero-input-backspace) + (define-key zero-input-mode-map (kbd "RET") 'zero-input-return) + (define-key zero-input-mode-map (kbd "<escape>") 'zero-input-reset)) + +(defun zero-input-disable-preediting-map () + "Disable preediting keymap in `zero-input-mode-map'." + (zero-input-debug "zero-input-disable-preediting-map\n") + (define-key zero-input-mode-map (kbd "<backspace>") nil) + (define-key zero-input-mode-map (kbd "RET") nil) + (define-key zero-input-mode-map (kbd "<escape>") nil)) + +(defun zero-input-modeline-string () + "Build `zero-input-mode' modeline string aka lighter. + +If full-width mode is enabled, show ZeroF; +Otherwise, show Zero." + (if zero-input-full-width-mode " ZeroF" " Zero")) + +(define-minor-mode zero-input-mode + "a Chinese input method framework written as an emacs minor mode. + +\\{zero-input-mode-map}" + nil + (:eval (zero-input-modeline-string)) + zero-input-mode-map + ;; local variables and variable init + (make-local-variable 'zero-input-state) + (zero-input-set-state zero-input--state-im-off) + (make-local-variable 'zero-input-punctuation-level) + (make-local-variable 'zero-input-full-width-mode) + (make-local-variable 'zero-input-double-quote-flag) + (make-local-variable 'zero-input-single-quote-flag) + (set (make-local-variable 'zero-input-preedit-str) "") + (set (make-local-variable 'zero-input-candidates) nil) + (make-local-variable 'zero-input-candidates-per-page) + (make-local-variable 'zero-input-current-page) + (make-local-variable 'zero-input-fetch-size) + (make-local-variable 'zero-input-im) + (make-local-variable 'zero-input-build-candidates-func) + (make-local-variable 'zero-input-can-start-sequence-func) + (zero-input-set-im zero-input-im) + ;; hooks + (add-hook 'focus-in-hook 'zero-input-focus-in) + (add-hook 'focus-out-hook 'zero-input-focus-out) + (set (make-local-variable 'zero-input-buffer) (current-buffer)) + (add-hook 'buffer-list-update-hook 'zero-input-buffer-list-changed)) + +;;================== +;; IM developer API +;;================== + +(defun zero-input-register-im (im-name im-functions-alist) + "(Re)register an input method in zero. + +After registration, you can use `zero-input-set-default-im' and +`zero-input-set-im' to select input method to use. + +IM-NAME should be a symbol. +IM-FUNCTIONS-ALIST should be a list of form + '((:virtual-function-name . implementation-function-name)) + +virtual functions corresponding variable +=========================================================================== +:build-candidates `zero-input-build-candidates-func' +:can-start-sequence `zero-input-can-start-sequence-func' +:handle-preedit-char `zero-input-handle-preedit-char-func' +:get-preedit-str-for-panel `zero-input-get-preedit-str-for-panel-func' +:handle-backspace `zero-input-backspace-func' +:init nil +:shutdown nil +:preedit-start `zero-input-preedit-start-func' +:preedit-end `zero-input-preedit-end-func' + +registered input method is saved in `zero-input-ims'" + ;; add or replace entry in `zero-input-ims' + (unless (symbolp im-name) + (signal 'wrong-type-argument (list 'symbolp im-name))) + (setq zero-input-ims (assq-delete-all im-name zero-input-ims)) + (setq zero-input-ims (push (cons im-name im-functions-alist) zero-input-ims))) + +;;============ +;; public API +;;============ + +(defun zero-input-toggle-full-width-mode () + "Toggle `zero-input-full-width-mode' on/off." + (interactive) + (setq zero-input-full-width-mode (not zero-input-full-width-mode)) + (message (if zero-input-full-width-mode + "Enabled full-width mode" + "Enabled half-width mode"))) + +(defun zero-input-set-punctuation-level (level) + "Set `zero-input-punctuation-level'. + +LEVEL the level to set to." + (interactive) + (if (not (member level (list zero-input-punctuation-level-basic + zero-input-punctuation-level-full + zero-input-punctuation-level-none))) + (error "Level not supported: %s" level) + (setq zero-input-punctuation-level level))) + +(defun zero-input-set-punctuation-levels (levels) + "Set `zero-input-punctuation-levels'. + +`zero-input-cycle-punctuation-level' will cycle current +`zero-input-punctuation-level' among defined LEVELS." + (dolist (level levels) + (if (not (member level (list zero-input-punctuation-level-basic + zero-input-punctuation-level-full + zero-input-punctuation-level-none))) + (error "Level not supported: %s" level))) + (setq zero-input-punctuation-levels levels)) + +(defun zero-input-cycle-punctuation-level () + "Cycle `zero-input-punctuation-level' among `zero-input-punctuation-levels'." + (interactive) + (setq zero-input-punctuation-level + (zero-input-cycle-list zero-input-punctuation-levels zero-input-punctuation-level)) + (message "punctuation level set to %s" zero-input-punctuation-level)) + +;;;###autoload +(defun zero-input-set-im (im-name) + "Select zero input method for current buffer. + +if IM-NAME is nil, use default empty input method" + ;; TODO provide auto completion for im-name + (interactive "SSet input method to: ") + ;; when switch away from an IM, run last IM's :shutdown function. + (if zero-input-im + (let ((shutdown-func (cdr (assq :shutdown (cdr (assq zero-input-im zero-input-ims)))))) + (if (functionp shutdown-func) + (funcall shutdown-func)))) + (if im-name + (let ((im-functions (cdr (assq im-name zero-input-ims)))) + (if im-functions + (progn + ;; TODO create a macro to reduce code duplication and human + ;; error. + ;; + ;; TODO do some functionp check for the slot functions. if check + ;; fail, keep (or revert to) the old IM. + (setq zero-input-build-candidates-func + (or (cdr (assq :build-candidates im-functions)) + 'zero-input-build-candidates-default)) + (setq zero-input-build-candidates-async-func + (or (cdr (assq :build-candidates-async im-functions)) + 'zero-input-build-candidates-async-default)) + (setq zero-input-can-start-sequence-func + (or (cdr (assq :can-start-sequence im-functions)) + 'zero-input-can-start-sequence-default)) + (setq zero-input-handle-preedit-char-func + (or (cdr (assq :handle-preedit-char im-functions)) + 'zero-input-handle-preedit-char-default)) + (setq zero-input-get-preedit-str-for-panel-func + (or (cdr (assq :get-preedit-str-for-panel im-functions)) + 'zero-input-get-preedit-str-for-panel-default)) + (setq zero-input-backspace-func + (or (cdr (assq :handle-backspace im-functions)) + 'zero-input-backspace-default)) + (setq zero-input-preedit-start-func + (cdr (assq :preedit-start im-functions))) + (setq zero-input-preedit-end-func + (cdr (assq :preedit-end im-functions))) + (unless (functionp zero-input-backspace-func) + (signal 'wrong-type-argument + (list 'functionp zero-input-backspace-func))) + ;; when switch to a IM, run its :init function + (let ((init-func (cdr (assq :init im-functions)))) + (if (functionp init-func) + (funcall init-func))) + (set (make-local-variable 'zero-input-im) im-name)) + (error "Input method %s not registered in zero" im-name))) + (zero-input-debug "using default empty input method") + (setq zero-input-build-candidates-func 'zero-input-build-candidates-default) + (setq zero-input-build-candidates-async-func 'zero-input-build-candidates-async-default) + (setq zero-input-can-start-sequence-func 'zero-input-can-start-sequence-default) + (setq zero-input-handle-preedit-char-func 'zero-input-handle-preedit-char-default) + (setq zero-input-get-preedit-str-for-panel-func 'zero-input-get-preedit-str-for-panel-default) + (setq zero-input-backspace-func 'zero-input-backspace-default) + (setq zero-input-preedit-start-func nil) + (setq zero-input-preedit-end-func nil))) + +;;;###autoload +(defun zero-input-set-default-im (im-name) + "Set given IM-NAME as default zero input method." + (unless (symbolp im-name) + (signal 'wrong-type-argument (list 'symbolp im-name))) + (setq-default zero-input-im im-name)) + +;;;###autoload +(defun zero-input-on () + "Turn on `zero-input-mode'." + (interactive) + (zero-input-debug "zero-input-on\n") + (zero-input-mode 1) + (if (eq zero-input-state zero-input--state-im-off) + (zero-input-set-state zero-input--state-im-waiting-input))) + +(defun zero-input-off () + "Turn off `zero-input-mode'." + (interactive) + (zero-input-debug "zero-input-off\n") + (zero-input-mode -1) + (zero-input-reset) + (zero-input-set-state zero-input--state-im-off)) + +;;;###autoload +(defun zero-input-toggle () + "Toggle `zero-input-mode'." + (interactive) + (if zero-input-mode + (zero-input-off) + (zero-input-on))) + +(provide 'zero-input-framework) + +;;; zero-input-framework.el ends here diff --git a/zero-panel-test.el b/zero-input-panel-test.el similarity index 68% rename from zero-panel-test.el rename to zero-input-panel-test.el index ecab78cfaf815f36d3701e159e2de1cb3bec6354..e0c8b17798f732cba3e7b35d82a841f4508369b9 100644 --- a/zero-panel-test.el +++ b/zero-input-panel-test.el @@ -1,4 +1,4 @@ -;;; zero-panel-test.el --- tests for zero-panel.el -*- lexical-binding: t -*- +;;; zero-input-panel-test.el --- tests for zero-input-panel.el -*- lexical-binding: t -*- ;; Licensed under the Apache License, Version 2.0 (the "License"); ;; you may not use this file except in compliance with the License. @@ -19,17 +19,17 @@ ;;; Code: (require 'ert) -(require 'zero-panel) +(require 'zero-input-panel) -(ert-deftest zero-alist-to-asv () - (should (equal (zero-alist-to-asv nil) '(:array :signature "{sv}"))) - (should (equal (zero-alist-to-asv +(ert-deftest zero-input-alist-to-asv () + (should (equal (zero-input-alist-to-asv nil) '(:array :signature "{sv}"))) + (should (equal (zero-input-alist-to-asv '(("name" "foo") ("timeout" :int32 10))) '(:array (:dict-entry "name" (:variant "foo")) (:dict-entry "timeout" (:variant :int32 10)))))) -(provide 'zero-panel-test) +(provide 'zero-input-panel-test) -;;; zero-panel-test.el ends here +;;; zero-input-panel-test.el ends here diff --git a/zero-panel.el b/zero-input-panel.el similarity index 67% rename from zero-panel.el rename to zero-input-panel.el index f31f8886c42dec2e7f27ca1746b7d0c6e6bf4ff3..2940905ca6da721cdf7db5370eb092392297f760 100644 --- a/zero-panel.el +++ b/zero-input-panel.el @@ -1,4 +1,4 @@ -;;; zero-panel --- Provide emacs interface for zero-panel dbus service. -*- lexical-binding: t -*- +;;; zero-input-panel --- Provide emacs interface for zero-input-panel dbus service. -*- lexical-binding: t -*- ;; Licensed under the Apache License, Version 2.0 (the "License"); ;; you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ ;;; Commentary: -;; use dbus to communicate with zero-panel service. +;; use dbus to communicate with zero-input-panel service. ;;; Code: @@ -25,19 +25,19 @@ (require 'dbus) (require 's) -(defun zero-panel-error-handler (event error) +(defun zero-input-panel-error-handler (event error) "Handle dbus errors. EVENT and ERROR are error-handler arguments." (when (or (string-equal "com.emacsos.zero.Panel" (dbus-event-interface-name event)) (s-contains-p "com.emacsos.zero.Panel" (cadr error))) - (error "Zero-panel dbus failed: %S" (cadr error)))) + (error "Zero-Input-panel dbus failed: %S" (cadr error)))) -(add-hook 'dbus-event-error-functions 'zero-panel-error-handler) +(add-hook 'dbus-event-error-functions 'zero-input-panel-error-handler) -(defun zero-panel-async-call (method _handler &rest args) - "Call METHOD on zero-panel service asynchronously. +(defun zero-input-panel-async-call (method _handler &rest args) + "Call METHOD on zero-input-panel service asynchronously. This is a wrapper around `dbus-call-method-asynchronously'. ARGS optional extra args to pass to the wrapped function." @@ -52,13 +52,13 @@ ARGS optional extra args to pass to the wrapped function." ;; public utility function ;;========================= -(defun zero-alist-to-asv (hints) +(defun zero-input-alist-to-asv (hints) "Convert Lisp alist to dbus a{sv} data structure. HINTS should be an alist of form '((k1 [v1type] v1) (k2 [v2type] v2)). For example, -\(zero-alist-to-asv +\(zero-input-alist-to-asv '((\"name\" \"foo\") (\"timeout\" :int32 10))) => @@ -76,34 +76,34 @@ For example, ;; public API ;;============ -(defun zero-panel-move (x y) +(defun zero-input-panel-move (x y) "Move panel to specific coordinate (X, Y). Origin (0, 0) is at screen top left corner." - (zero-panel-async-call "Move" nil :int32 x :int32 y)) + (zero-input-panel-async-call "Move" nil :int32 x :int32 y)) -(defun zero-panel-show-candidates (preedit_str candidate_length candidates &optional hints) +(defun zero-input-panel-show-candidates (preedit_str candidate_length candidates &optional hints) "Show CANDIDATES. Argument PREEDIT_STR the preedit string. Argument CANDIDATE_LENGTH how many candidates are in candidates list." - (zero-panel-async-call "ShowCandidates" nil + (zero-input-panel-async-call "ShowCandidates" nil :string preedit_str :uint32 candidate_length (or candidates '(:array)) - (zero-alist-to-asv hints))) + (zero-input-alist-to-asv hints))) -(defun zero-panel-show () +(defun zero-input-panel-show () "Show panel." - (zero-panel-async-call "Show" nil)) + (zero-input-panel-async-call "Show" nil)) -(defun zero-panel-hide () +(defun zero-input-panel-hide () "Hide panel." - (zero-panel-async-call "Hide" nil)) + (zero-input-panel-async-call "Hide" nil)) -(defun zero-panel-quit () +(defun zero-input-panel-quit () "Quit panel application." (interactive) - (zero-panel-async-call "Quit" nil)) + (zero-input-panel-async-call "Quit" nil)) -(provide 'zero-panel) +(provide 'zero-input-panel) -;;; zero-panel.el ends here +;;; zero-input-panel.el ends here diff --git a/zero-pinyin-service-test.el b/zero-input-pinyin-service-test.el similarity index 63% rename from zero-pinyin-service-test.el rename to zero-input-pinyin-service-test.el index c1845e795a40d218c7609da47e102e8bd40b286f..bda799362afee13def6ff60e765a16974fdb066d 100644 --- a/zero-pinyin-service-test.el +++ b/zero-input-pinyin-service-test.el @@ -1,4 +1,4 @@ -;;; zero-pinyin-service-test.el --- tests for zero-pinyin-service.el -*- lexical-binding: t -*- +;;; zero-input-pinyin-service-test.el --- tests for zero-input-pinyin-service.el -*- lexical-binding: t -*- ;; Licensed under the Apache License, Version 2.0 (the "License"); ;; you may not use this file except in compliance with the License. @@ -18,42 +18,42 @@ ;;; Code: -(require 'zero-pinyin-service) +(require 'zero-input-pinyin-service) (require 'ert) (eval-when-compile (require 'cl-macs)) -(ert-deftest zero-pinyin-candidate-pinyin-indices-to-dbus-format () - (should (equal (zero-pinyin-candidate-pinyin-indices-to-dbus-format '((22 31))) +(ert-deftest zero-input-pinyin-candidate-pinyin-indices-to-dbus-format () + (should (equal (zero-input-pinyin-candidate-pinyin-indices-to-dbus-format '((22 31))) '(:array (:struct :int32 22 :int32 31)))) - (should (equal (zero-pinyin-candidate-pinyin-indices-to-dbus-format + (should (equal (zero-input-pinyin-candidate-pinyin-indices-to-dbus-format '((17 46) (7 55))) '(:array (:struct :int32 17 :int32 46) (:struct :int32 7 :int32 55))))) -(ert-deftest zero-pinyin-service-get-candidates () +(ert-deftest zero-input-pinyin-service-get-candidates () (cl-destructuring-bind (cs ls &rest rest) - (zero-pinyin-service-get-candidates "liyifeng" 1) + (zero-input-pinyin-service-get-candidates "liyifeng" 1) (should (or (and (equal (car cs) "李易峰") (= (car ls) 8)) (and (equal (car cs) "利益") (= (car ls) 4))))) (cl-destructuring-bind (cs ls &rest rest) - (zero-pinyin-service-get-candidates "wenti" 1) + (zero-input-pinyin-service-get-candidates "wenti" 1) (should (equal (car cs) "问题")) (should (= (car ls) 5))) (cl-destructuring-bind (cs ls &rest rest) - (zero-pinyin-service-get-candidates "meiyou" 1) + (zero-input-pinyin-service-get-candidates "meiyou" 1) (should (equal (car cs) "没有")) (should (= (car ls) 6))) (cl-destructuring-bind (cs ls &rest rest) - (zero-pinyin-service-get-candidates "shi" 1) + (zero-input-pinyin-service-get-candidates "shi" 1) (should (equal (car cs) "是")) (should (= (car ls) 3))) (cl-destructuring-bind (cs ls &rest rest) - (zero-pinyin-service-get-candidates "de" 1) + (zero-input-pinyin-service-get-candidates "de" 1) (should (equal (car cs) "的")) (should (= (car ls) 2)))) -(provide 'zero-pinyin-service-test) +(provide 'zero-input-pinyin-service-test) -;;; zero-pinyin-service-test.el ends here +;;; zero-input-pinyin-service-test.el ends here diff --git a/zero-pinyin-service.el b/zero-input-pinyin-service.el similarity index 54% rename from zero-pinyin-service.el rename to zero-input-pinyin-service.el index 27e994dab7fe49d3e7079acbc78e3e01248c1b02..49248903797fd339225b922fb8969bd734e1e130 100644 --- a/zero-pinyin-service.el +++ b/zero-input-pinyin-service.el @@ -1,4 +1,4 @@ -;;; zero-pinyin-service.el --- Provide emacs interface for zero-pinyin-service dbus service. -*- lexical-binding: t -*- +;;; zero-input-pinyin-service.el --- Provide emacs interface for zero-input-pinyin-service dbus service. -*- lexical-binding: t -*- ;; Licensed under the Apache License, Version 2.0 (the "License"); ;; you may not use this file except in compliance with the License. @@ -23,67 +23,67 @@ (require 'dbus) (require 's) -(defvar zero-pinyin-service-service-name +(defvar zero-input-pinyin-service-service-name "com.emacsos.zero.ZeroPinyinService1") -(defvar zero-pinyin-service-path +(defvar zero-input-pinyin-service-path "/com/emacsos/zero/ZeroPinyinService1") -(defvar zero-pinyin-service-interface +(defvar zero-input-pinyin-service-interface "com.emacsos.zero.ZeroPinyinService1.ZeroPinyinServiceInterface") -(defvar zero-pinyin-fuzzy-flag 0) +(defvar zero-input-pinyin-fuzzy-flag 0) -(defun zero-pinyin-service-error-handler (event error) +(defun zero-input-pinyin-service-error-handler (event error) "Handle dbus errors. EVENT, ERROR are arguments passed to the handler." - (when (or (string-equal zero-pinyin-service-service-name + (when (or (string-equal zero-input-pinyin-service-service-name (dbus-event-interface-name event)) - (s-contains-p zero-pinyin-service-service-name (cadr error))) - (error "`zero-pinyin-service' dbus failed: %S" (cadr error)))) + (s-contains-p zero-input-pinyin-service-service-name (cadr error))) + (error "`zero-input-pinyin-service' dbus failed: %S" (cadr error)))) -(add-hook 'dbus-event-error-functions 'zero-pinyin-service-error-handler) +(add-hook 'dbus-event-error-functions 'zero-input-pinyin-service-error-handler) -(defun zero-pinyin-service-async-call (method handler &rest args) - "Call METHOD on zero-pinin-service asynchronously. +(defun zero-input-pinyin-service-async-call (method handler &rest args) + "Call METHOD on zero-input-pinin-service asynchronously. This is a wrapper around `dbus-call-method-asynchronously'. Argument HANDLER the handler function. Optional argument ARGS extra arguments to pass to the wrapped function." (apply 'dbus-call-method-asynchronously - :session zero-pinyin-service-service-name - zero-pinyin-service-path - zero-pinyin-service-interface + :session zero-input-pinyin-service-service-name + zero-input-pinyin-service-path + zero-input-pinyin-service-interface method handler :timeout 1000 args)) -(defun zero-pinyin-service-call (method &rest args) - "Call METHOD on zero-pinin-service synchronously. +(defun zero-input-pinyin-service-call (method &rest args) + "Call METHOD on zero-input-pinin-service synchronously. This is a wrapper around `dbus-call-method'. Optional argument ARGS extra arguments to pass to the wrapped function." (apply 'dbus-call-method - :session zero-pinyin-service-service-name - zero-pinyin-service-path - zero-pinyin-service-interface + :session zero-input-pinyin-service-service-name + zero-input-pinyin-service-path + zero-input-pinyin-service-interface method :timeout 1000 args)) ;;============ ;; public API ;;============ -(defun zero-pinyin-service-get-candidates (preedit-str fetch-size) +(defun zero-input-pinyin-service-get-candidates (preedit-str fetch-size) "Get candidates for pinyin in PREEDIT-STR synchronously. preedit-str the preedit-str, should be pure pinyin string FETCH-SIZE try to fetch this many candidates or more" - (zero-pinyin-service-call "GetCandidatesV2" :string preedit-str :uint32 fetch-size :uint32 zero-pinyin-fuzzy-flag)) + (zero-input-pinyin-service-call "GetCandidatesV2" :string preedit-str :uint32 fetch-size :uint32 zero-input-pinyin-fuzzy-flag)) -(defun zero-pinyin-service-get-candidates-async (preedit-str fetch-size get-candidates-complete) +(defun zero-input-pinyin-service-get-candidates-async (preedit-str fetch-size get-candidates-complete) "Get candidates for pinyin in PREEDIT-STR asynchronously. PREEDIT-STR the preedit string, should be pure pinyin string. FETCH-SIZE try to fetch this many candidates or more. GET-CANDIDATES-COMPLETE the async handler function." - (zero-pinyin-service-async-call - "GetCandidatesV2" get-candidates-complete :string preedit-str :uint32 fetch-size :uint32 zero-pinyin-fuzzy-flag)) + (zero-input-pinyin-service-async-call + "GetCandidatesV2" get-candidates-complete :string preedit-str :uint32 fetch-size :uint32 zero-input-pinyin-fuzzy-flag)) -(defun zero-pinyin-candidate-pinyin-indices-to-dbus-format (candidate_pinyin_indices) +(defun zero-input-pinyin-candidate-pinyin-indices-to-dbus-format (candidate_pinyin_indices) "Convert CANDIDATE_PINYIN_INDICES to Emacs dbus format." (let (result) (push :array result) @@ -94,28 +94,28 @@ GET-CANDIDATES-COMPLETE the async handler function." result)) (reverse result))) -(defun zero-pinyin-service-commit-candidate-async (candidate candidate_pinyin_indices) +(defun zero-input-pinyin-service-commit-candidate-async (candidate candidate_pinyin_indices) "Commit candidate asynchronously. CANDIDATE the candidate user selected. CANDIDATE_PINYIN_INDICES the candidate's pinyin shengmu and yunmu index." ;; don't care about the result, so no callback. - (zero-pinyin-service-async-call + (zero-input-pinyin-service-async-call "CommitCandidate" nil :string candidate - (zero-pinyin-candidate-pinyin-indices-to-dbus-format candidate_pinyin_indices))) + (zero-input-pinyin-candidate-pinyin-indices-to-dbus-format candidate_pinyin_indices))) -(defun zero-pinyin-service-delete-candidates-async (candidate delete-candidate-complete) +(defun zero-input-pinyin-service-delete-candidates-async (candidate delete-candidate-complete) "Delete CANDIDATE asynchronously. DELETE-CANDIDATE-COMPLETE the async handler function." - (zero-pinyin-service-async-call + (zero-input-pinyin-service-async-call "DeleteCandidate" delete-candidate-complete :string candidate)) -(defun zero-pinyin-service-quit () +(defun zero-input-pinyin-service-quit () "Quit panel application." - (zero-pinyin-service-async-call "Quit" nil)) + (zero-input-pinyin-service-async-call "Quit" nil)) -(provide 'zero-pinyin-service) +(provide 'zero-input-pinyin-service) -;;; zero-pinyin-service.el ends here +;;; zero-input-pinyin-service.el ends here diff --git a/zero-input-pinyin-test.el b/zero-input-pinyin-test.el new file mode 100644 index 0000000000000000000000000000000000000000..e27ddff1ca788b1884ddb2f96edb742986aedab2 --- /dev/null +++ b/zero-input-pinyin-test.el @@ -0,0 +1,37 @@ +;;; zero-input-pinyin-test.el --- tests for zero-input-pinyin.el + +;; Licensed under the Apache License, Version 2.0 (the "License"); +;; you may not use this file except in compliance with the License. +;; You may obtain a copy of the License at +;; +;; http://www.apache.org/licenses/LICENSE-2.0 +;; +;; Unless required by applicable law or agreed to in writing, software +;; distributed under the License is distributed on an "AS IS" BASIS, +;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +;; See the License for the specific language governing permissions and +;; limitations under the License. + +;;; Commentary: + +;; + +;;; Code: + +(require 'zero-input-pinyin) +(require 'ert) + +(ert-deftest zero-input-pinyin-can-start-sequence () + (should (zero-input-pinyin-can-start-sequence ?a)) + (should (zero-input-pinyin-can-start-sequence ?l)) + (should (zero-input-pinyin-can-start-sequence ?m)) + (should (zero-input-pinyin-can-start-sequence ?z)) + (should-not (zero-input-pinyin-can-start-sequence ?1)) + (should-not (zero-input-pinyin-can-start-sequence ?.)) + (should-not (zero-input-pinyin-can-start-sequence ?i)) + (should-not (zero-input-pinyin-can-start-sequence ?u)) + (should-not (zero-input-pinyin-can-start-sequence ?v))) + +(provide 'zero-input-pinyin-test) + +;;; zero-input-pinyin-test.el ends here diff --git a/zero-input-pinyin.el b/zero-input-pinyin.el new file mode 100644 index 0000000000000000000000000000000000000000..6fb279c3a45c450973efe365d558d3bf9753ceaf --- /dev/null +++ b/zero-input-pinyin.el @@ -0,0 +1,358 @@ +;;; zero-input-pinyin.el --- A pinyin input method for zero-input-framework -*- lexical-binding: t -*- + +;; Licensed under the Apache License, Version 2.0 (the "License"); +;; you may not use this file except in compliance with the License. +;; You may obtain a copy of the License at +;; +;; http://www.apache.org/licenses/LICENSE-2.0 +;; +;; Unless required by applicable law or agreed to in writing, software +;; distributed under the License is distributed on an "AS IS" BASIS, +;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +;; See the License for the specific language governing permissions and +;; limitations under the License. + +;;; Commentary: + +;; To use this input method, add in Emacs init file: +;; +;; (add-to-list 'load-path "~/fromsource/zero") ;; omit if install from melpa +;; (require 'zero-input-pinyin) +;; (zero-input-set-default-im 'pinyin) +;; ;; Now you may bind a key to zero-input-toggle to make it easy to +;; ;; switch on/off the input method. +;; (global-set-key (kbd "<f5>") 'zero-input-toggle) + +;;; Code: + +;;============== +;; dependencies +;;============== + +(require 'zero-input-framework) +(require 'zero-input-pinyin-service) + +;;=============================== +;; basic data and emacs facility +;;=============================== + +;; these two var is only used in docstring to avoid checkdoc line-too-long +;; error. +(defvar zero-input-pinyin-service-interface-xml-file + "/usr/share/dbus-1/interfaces/com.emacsos.zero.ZeroPinyinService1.ZeroPinyinServiceInterface.xml") +(defvar zero-input-pinyin-service-interface-xml-url + "https://gitlab.emacsos.com/sylecn/zero-input-pinyin-service/blob/master/com.emacsos.zero.ZeroPinyinService1.ZeroPinyinServiceInterface.xml") +(defcustom zero-input-pinyin-fuzzy-flag 0 + "Non-nil means use this value as GetCandidatesV2 fuzzy_flag param. +see zero-input-pinyin-service dbus interface xml for document. + +You can find the xml file locally at +`zero-input-pinyin-service-interface-xml-file' or online at +`zero-input-pinyin-service-interface-xml-url'." + :type 'integer + :group 'zero-input-pinyin) + +(defvar zero-input-pinyin-state nil "Zero-Input-pinyin internal state. could be nil or `*zero-input-pinyin-state-im-partial-commit*'.") +(defconst zero-input-pinyin--state-im-partial-commit 'IM-PARTIAL-COMMIT) + +(defvar zero-input-pinyin-used-preedit-str-lengths nil + "Accompany `zero-input-candidates', marks how many preedit-str chars are used for each candidate.") +(defvar zero-input-pinyin-candidates-pinyin-indices nil + "Store GetCandidates dbus method candidates_pinyin_indices field.") +(defvar zero-input-pinyin-pending-str "") +(defvar zero-input-pinyin-pending-preedit-str "") +(defvar zero-input-pinyin-pending-pinyin-indices nil + "Stores `zero-input-pinyin-pending-str' corresponds pinyin indices.") + +;;===================== +;; key logic functions +;;===================== + +(defun zero-input-pinyin-reset () + "Reset states." + (setq zero-input-pinyin-state nil) + (setq zero-input-pinyin-used-preedit-str-lengths nil) + (setq zero-input-pinyin-pending-str "") + (setq zero-input-pinyin-pending-preedit-str "")) + +(defun zero-input-pinyin-init () + "Called when this im is turned on." + (make-local-variable 'zero-input-pinyin-state) + (zero-input-pinyin-reset)) + +(defun zero-input-pinyin-preedit-start () + "Called when enter `*zero-input-state-im-preediting*' state." + (define-key zero-input-mode-map [remap digit-argument] 'zero-input-digit-argument)) + +(defun zero-input-pinyin-preedit-end () + "Called when leave `*zero-input-state-im-preediting*' state." + (define-key zero-input-mode-map [remap digit-argument] nil)) + +(defun zero-input-pinyin-shutdown () + "Called when this im is turned off." + (define-key zero-input-mode-map [remap digit-argument] nil)) + +(defvar zero-input-pinyin--build-candidates-use-test-data nil + "If t, `zero-input-pinyin-build-candidates' will use `zero-input-pinyin-build-candidates-test'.") + +(defun zero-input-pinyin-build-candidates (preedit-str fetch-size) + "Synchronously build candidates list. + +PREEDIT-STR the preedit string. +FETCH-SIZE fetch at least this many candidates if possible." + (if zero-input-pinyin--build-candidates-use-test-data + (progn + (zero-input-pinyin-build-candidates-test preedit-str) + (setq zero-input-fetch-size (max fetch-size (length zero-input-candidates)))) + (zero-input-debug "zero-input-pinyin building candidate list synchronously\n") + (let ((result (zero-input-pinyin-service-get-candidates preedit-str fetch-size))) + (setq zero-input-fetch-size (max fetch-size (length (cl-first result)))) + (setq zero-input-pinyin-used-preedit-str-lengths (cl-second result)) + (setq zero-input-pinyin-candidates-pinyin-indices (cl-third result)) + (cl-first result)))) + +(defun zero-input-pinyin-build-candidates-async (preedit-str fetch-size complete-func) + "Asynchronously build candidate list, when done call complete-func on it. + +PREEDIT-STR the preedit string. +FETCH-SIZE fetch at least this many candidates if possible. +COMPLETE-FUNC the callback function when async call completes. it's called with + fetched candidates list as parameter." + (zero-input-debug "zero-input-pinyin building candidate list asynchronously\n") + (zero-input-pinyin-service-get-candidates-async + preedit-str + fetch-size + (lambda (candidates matched_preedit_str_lengths candidates_pinyin_indices) + (setq zero-input-pinyin-used-preedit-str-lengths matched_preedit_str_lengths) + (setq zero-input-pinyin-candidates-pinyin-indices candidates_pinyin_indices) + (setq zero-input-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-input-pinyin-can-start-sequence (ch) + "Return t if char CH can start a preedit sequence." + (and (>= ch ?a) + (<= ch ?z) + (not (= ch ?i)) + (not (= ch ?u)) + (not (= ch ?v)))) + +(defun zero-input-pinyin-pending-preedit-str-changed () + "Update zero states when pending preedit string changed." + (setq zero-input-fetch-size 0) + (setq zero-input-current-page 0) + (zero-input-pinyin-build-candidates-async zero-input-pinyin-pending-preedit-str zero-input-initial-fetch-size 'zero-input-build-candidates-complete)) + +(defun zero-input-pinyin-commit-nth-candidate (n) + "Commit Nth candidate and return true if it exists, otherwise, return false." + (let* ((n-prime (+ n (* zero-input-candidates-per-page zero-input-current-page))) + (candidate (nth n-prime zero-input-candidates)) + (used-len (when candidate + (nth n-prime zero-input-pinyin-used-preedit-str-lengths)))) + (when candidate + (zero-input-debug + "zero-input-pinyin-commit-nth-candidate\n n=%s candidate=%s used-len=%s zero-input-pinyin-pending-preedit-str=%S\n" + n candidate used-len zero-input-pinyin-pending-preedit-str) + (cond + ((null zero-input-pinyin-state) + (if (= used-len (length zero-input-preedit-str)) + (progn + (zero-input-debug "commit in full\n") + (zero-input-set-state zero-input--state-im-waiting-input) + (zero-input-commit-text candidate) + (zero-input-pinyin-service-commit-candidate-async + candidate + (nth n-prime zero-input-pinyin-candidates-pinyin-indices)) + t) + (zero-input-debug "partial commit, in partial commit mode now.\n") + (setq zero-input-pinyin-state zero-input-pinyin--state-im-partial-commit) + (setq zero-input-pinyin-pending-str candidate) + (setq zero-input-pinyin-pending-preedit-str (substring zero-input-preedit-str used-len)) + (setq zero-input-pinyin-pending-pinyin-indices + (nth n-prime zero-input-pinyin-candidates-pinyin-indices)) + (zero-input-pinyin-pending-preedit-str-changed) + t)) + ((eq zero-input-pinyin-state zero-input-pinyin--state-im-partial-commit) + (if (= used-len (length zero-input-pinyin-pending-preedit-str)) + (progn + (zero-input-debug "finishes partial commit\n") + (setq zero-input-pinyin-state nil) + (zero-input-set-state zero-input--state-im-waiting-input) + (zero-input-commit-text (concat zero-input-pinyin-pending-str candidate)) + (zero-input-pinyin-service-commit-candidate-async + (concat zero-input-pinyin-pending-str candidate) + (append zero-input-pinyin-pending-pinyin-indices + (nth n-prime zero-input-pinyin-candidates-pinyin-indices))) + t) + (zero-input-debug "continue partial commit\n") + (setq zero-input-pinyin-pending-str (concat zero-input-pinyin-pending-str candidate)) + (setq zero-input-pinyin-pending-preedit-str (substring zero-input-pinyin-pending-preedit-str used-len)) + (setq zero-input-pinyin-pending-pinyin-indices + (append zero-input-pinyin-pending-pinyin-indices + (nth n-prime zero-input-pinyin-candidates-pinyin-indices))) + (zero-input-pinyin-service-commit-candidate-async + zero-input-pinyin-pending-str + zero-input-pinyin-pending-pinyin-indices) + (zero-input-pinyin-pending-preedit-str-changed) + t)) + (t (error "Unexpected zero-input-pinyin-state: %s" zero-input-pinyin-state)))))) + +(defun zero-input-pinyin-commit-first-candidate-or-preedit-str () + "Commit first candidate if there is one, otherwise, commit preedit string." + (unless (zero-input-pinyin-commit-nth-candidate 0) + (zero-input-commit-preedit-str))) + +(defun zero-input-pinyin-commit-first-candidate-in-full () + "Commit first candidate and return t if it consumes all preedit-str. +Otherwise, just return nil." + (let ((candidate (nth 0 (zero-input-candidates-on-page zero-input-candidates))) + (used-len (nth (* zero-input-candidates-per-page zero-input-current-page) zero-input-pinyin-used-preedit-str-lengths))) + (when candidate + (cond + ((null zero-input-pinyin-state) + (when (= used-len (length zero-input-preedit-str)) + (zero-input-set-state zero-input--state-im-waiting-input) + (zero-input-commit-text candidate) + t)) + ((eq zero-input-pinyin-state zero-input-pinyin--state-im-partial-commit) + (when (= used-len (length zero-input-pinyin-pending-preedit-str)) + (setq zero-input-pinyin-state nil) + (zero-input-set-state zero-input--state-im-waiting-input) + (zero-input-commit-text (concat zero-input-pinyin-pending-str candidate)) + t)) + (t (error "Unexpected zero-input-pinyin-state: %s" zero-input-pinyin-state)))))) + +(defun zero-input-pinyin-page-down () + "Handle page down for zero-input-pinyin. + +This is different from zero-input-framework because I need to support partial commit" + (let ((len (length zero-input-candidates)) + (new-fetch-size (* zero-input-candidates-per-page (+ 2 zero-input-current-page)))) + (if (and (< len new-fetch-size) + (< zero-input-fetch-size new-fetch-size)) + (let ((preedit-str (if (eq zero-input-pinyin-state zero-input-pinyin--state-im-partial-commit) zero-input-pinyin-pending-preedit-str zero-input-preedit-str))) + (zero-input-pinyin-build-candidates-async + preedit-str + new-fetch-size + (lambda (candidates) + (zero-input-build-candidates-complete candidates) + (zero-input-just-page-down)))) + (zero-input-just-page-down)))) + +(defun zero-input-pinyin-handle-preedit-char (ch) + "Hanlde character insert in `*zero-input-state-im-preediting*' state. +Override `zero-input-handle-preedit-char-default'. + +CH the character user typed." + (cond + ((= ch ?\s) + (zero-input-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-input-pinyin-commit-nth-candidate (mod (- (- ch ?0) 1) 10)) + (zero-input-append-char-to-preedit-str ch) + (setq zero-input-pinyin-state nil))) + ((= ch zero-input-previous-page-key) + (zero-input-handle-preedit-char-default ch)) + ((= ch zero-input-next-page-key) + (zero-input-pinyin-page-down)) + (t (let ((str (zero-input-convert-punctuation ch))) + (if str + (when (zero-input-pinyin-commit-first-candidate-in-full) + (zero-input-set-state zero-input--state-im-waiting-input) + (insert str)) + (setq zero-input-pinyin-state nil) + (zero-input-append-char-to-preedit-str ch)))))) + +(defun zero-input-pinyin-get-preedit-str-for-panel () + "Return the preedit string that should show in panel." + (if (eq zero-input-pinyin-state zero-input-pinyin--state-im-partial-commit) + (concat zero-input-pinyin-pending-str zero-input-pinyin-pending-preedit-str) + zero-input-preedit-str)) + +(defun zero-input-pinyin-preedit-str-changed () + "Start over for candidate selection process." + (setq zero-input-pinyin-state nil) + (zero-input-preedit-str-changed)) + +(defun zero-input-pinyin-backspace () + "Handle backspace key in `*zero-input-state-im-preediting*' state." + (if (eq zero-input-pinyin-state zero-input-pinyin--state-im-partial-commit) + (zero-input-pinyin-preedit-str-changed) + (zero-input-backspace-default))) + +(defun zero-input-pinyin-delete-candidate (digit) + "Tell backend to delete candidate at DIGIT position. + +DIGIT is the digit key used to select nth candidate. +DIGIT 1 means delete 1st candidate. +DIGIT 2 means delete 2st candidate. +... +DIGIT 0 means delete 10th candidate." + (let ((candidate (nth (mod (- digit 1) 10) + (zero-input-candidates-on-page zero-input-candidates)))) + (when candidate + (zero-input-pinyin-service-delete-candidates-async + candidate 'zero-input-pinyin-preedit-str-changed)))) + +(defun zero-input-digit-argument () + "Allow C-<digit> to DeleteCandidate in `*zero-input-state-im-preediting*' state." + (interactive) + (unless (eq zero-input-state zero-input--state-im-preediting) + (error "`zero-input-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-input-pinyin-delete-candidate digit)))) + +;;=============================== +;; register IM to zero framework +;;=============================== + +(zero-input-register-im + 'pinyin + '((:build-candidates . zero-input-pinyin-build-candidates) + ;; comment to use sync version, uncomment to use async version. + ;; (:build-candidates-async . zero-input-pinyin-build-candidates-async) + (:can-start-sequence . zero-input-pinyin-can-start-sequence) + (:handle-preedit-char . zero-input-pinyin-handle-preedit-char) + (:get-preedit-str-for-panel . zero-input-pinyin-get-preedit-str-for-panel) + (:handle-backspace . zero-input-pinyin-backspace) + (:init . zero-input-pinyin-init) + (:shutdown . zero-input-pinyin-shutdown) + (:preedit-start . zero-input-pinyin-preedit-start) + (:preedit-end . zero-input-pinyin-preedit-end))) + +;;============ +;; public API +;;============ + +;;=========== +;; test data +;;=========== + +(defun zero-input-pinyin-build-candidates-test (preedit-str) + "Test data for testing partial commit. + +PREEDIT-STR the preedit string." + (cond + ((equal preedit-str "liyifeng") + (setq zero-input-pinyin-used-preedit-str-lengths '(8 4 4 4 2 2 2)) + '("李易峰" "利益" "礼仪" "离异" "里" "理" "力")) + ((equal preedit-str "feng") + (setq zero-input-pinyin-used-preedit-str-lengths '(4 4 4 4 4)) + '("风" "封" "疯" "丰" "凤")) + ((equal preedit-str "yifeng") + (setq zero-input-pinyin-used-preedit-str-lengths '(6 6 2 2 2 2)) + '("一封" "遗风" "艺" "依" "一" "以")) + (t nil))) + +(provide 'zero-input-pinyin) + +;;; zero-input-pinyin.el ends here diff --git a/zero-quickdial.el b/zero-input-quickdial.el similarity index 59% rename from zero-quickdial.el rename to zero-input-quickdial.el index 989d92b39a0089356fb84fbe9859836d4fb95654..3c78ad3857c700c204020bc5f12d9df44204d4b2 100644 --- a/zero-quickdial.el +++ b/zero-input-quickdial.el @@ -1,4 +1,4 @@ -;;; zero-quickdial --- quickdial input method written as an emacs minor mode. -*- lexical-binding: t -*- +;;; zero-input-quickdial --- quickdial input method written as an emacs minor mode. -*- lexical-binding: t -*- ;; Licensed under the Apache License, Version 2.0 (the "License"); ;; you may not use this file except in compliance with the License. @@ -15,44 +15,44 @@ ;;; Commentary: ;; To use this input method, -;; M-x zero-quickdial-mode ; turn on IM +;; M-x zero-input-quickdial-mode ; turn on IM ;; type 1 will insert one ;; type 2 will insert two ;; type 3 will insert three. -;; M-x zero-quickdial-mode ; turn off IM +;; M-x zero-input-quickdial-mode ; turn off IM ;; ;; This is just a demo of how Emacs minor mode can work as input method. ;;; Code: -(defun zero-quickdial-insert-one () +(defun zero-input-quickdial-insert-one () "Insert \"one\"." (interactive) (insert "one")) -(defun zero-quickdial-insert-two () +(defun zero-input-quickdial-insert-two () "Insert \"two\"." (interactive) (insert "two")) -(defun zero-quickdial-insert-three () +(defun zero-input-quickdial-insert-three () "Insert \"three\"." (interactive) (insert "three")) -(defvar zero-quickdial-mode-map +(defvar zero-input-quickdial-mode-map '(keymap - (49 . zero-quickdial-insert-one) - (50 . zero-quickdial-insert-two) - (51 . zero-quickdial-insert-three)) - "Keymap for zero-quickdial-mode.") + (49 . zero-input-quickdial-insert-one) + (50 . zero-input-quickdial-insert-two) + (51 . zero-input-quickdial-insert-three)) + "Keymap for zero-input-quickdial-mode.") -(define-minor-mode zero-quickdial-mode +(define-minor-mode zero-input-quickdial-mode "a simple input method written as an emacs minor mode" nil " Quickdial" - zero-quickdial-mode-map) + zero-input-quickdial-mode-map) -(provide 'zero-quickdial) +(provide 'zero-input-quickdial) -;;; zero-quickdial.el ends here +;;; zero-input-quickdial.el ends here diff --git a/zero-reload-all.el b/zero-input-reload-all.el similarity index 50% rename from zero-reload-all.el rename to zero-input-reload-all.el index 98360b9473eb3404b0dc4a75b9306f8cb7f29ea1..a3b11eaa0dd8b6c994cf62f04bd92f1a1444448d 100644 --- a/zero-reload-all.el +++ b/zero-input-reload-all.el @@ -1,4 +1,4 @@ -;;; zero-reload-all.el --- reload zero-el in correct order -*- no-byte-compile: t; -*- +;;; zero-input-reload-all.el --- reload zero-input-el in correct order -*- no-byte-compile: t; -*- ;; Licensed under the Apache License, Version 2.0 (the "License"); ;; you may not use this file except in compliance with the License. @@ -16,42 +16,42 @@ ;;; Code: -(defun zero-rebuild (&optional source-dir) - "Rebuild zero-el. +(defun zero-input-rebuild (&optional source-dir) + "Rebuild zero-input-el. SOURCE-DIR where to find the zero source dir." (interactive) ;; for loading s (package-initialize) (let ((source-dir (or source-dir "~/lisp/elisp/zero/"))) - (dolist (f '("zero-quickdial.el" - "zero-panel.el" - "zero-panel-test.el" - "zero-framework.el" - "zero-framework-test.el" - "zero-pinyin-service.el" - "zero-pinyin-service-test.el" - "zero-pinyin.el" - "zero-pinyin-test.el" + (dolist (f '("zero-input-quickdial.el" + "zero-input-panel.el" + "zero-input-panel-test.el" + "zero-input-framework.el" + "zero-input-framework-test.el" + "zero-input-pinyin-service.el" + "zero-input-pinyin-service-test.el" + "zero-input-pinyin.el" + "zero-input-pinyin-test.el" )) (byte-compile-file (concat source-dir f) t)))) -(defun zero-reload-all () +(defun zero-input-reload-all () "Recompile and load all zero files." (interactive) (byte-recompile-directory "~/lisp/elisp/zero/" 0) - (dolist (f '("zero-quickdial.elc" - "zero-panel.elc" - "zero-panel-test.elc" - "zero-framework.elc" - "zero-framework-test.elc" - "zero-pinyin-service.elc" - "zero-pinyin-service-test.elc" - "zero-pinyin.elc" - "zero-pinyin-test.elc" - "zero-table.el" - "zero-table-test.el" + (dolist (f '("zero-input-quickdial.elc" + "zero-input-panel.elc" + "zero-input-panel-test.elc" + "zero-input-framework.elc" + "zero-input-framework-test.elc" + "zero-input-pinyin-service.elc" + "zero-input-pinyin-service-test.elc" + "zero-input-pinyin.elc" + "zero-input-pinyin-test.elc" + "zero-input-table.el" + "zero-input-table-test.el" )) (load-file f))) -;;; zero-reload-all.el ends here +;;; zero-input-reload-all.el ends here diff --git a/zero-table-test.el b/zero-input-table-test.el similarity index 51% rename from zero-table-test.el rename to zero-input-table-test.el index 00175ae65d44b41821ef0610d435b467e9ce1804..c562be14b8275ae7443027719809f676076059a4 100644 --- a/zero-table-test.el +++ b/zero-input-table-test.el @@ -1,4 +1,4 @@ -;;; zero-table-test.el --- tests for zero-table.el -*- no-byte-compile: t; lexical-binding: t -*- +;;; zero-input-table-test.el --- tests for zero-input-table.el -*- no-byte-compile: t; lexical-binding: t -*- ;; Licensed under the Apache License, Version 2.0 (the "License"); ;; you may not use this file except in compliance with the License. @@ -18,22 +18,22 @@ ;;; Code: -(require 'zero-table) +(require 'zero-input-table) (require 'ert) -(ert-deftest zero-table-build-candidates () - (should (equal (zero-table-build-candidates "ph") '("18612345678"))) - (should (equal (zero-table-build-candidates "m") +(ert-deftest zero-input-table-build-candidates () + (should (equal (zero-input-table-build-candidates "ph") '("18612345678"))) + (should (equal (zero-input-table-build-candidates "m") '("https://msdn.microsoft.com/en-us" "foo@example.com" "https://ditu.amap.com/")))) -(ert-deftest zero-table-can-start-sequence () - (should (zero-table-can-start-sequence ?a)) - (should (zero-table-can-start-sequence ?m)) - (should-not (zero-table-can-start-sequence ?1)) - (should-not (zero-table-can-start-sequence ?b))) +(ert-deftest zero-input-table-can-start-sequence () + (should (zero-input-table-can-start-sequence ?a)) + (should (zero-input-table-can-start-sequence ?m)) + (should-not (zero-input-table-can-start-sequence ?1)) + (should-not (zero-input-table-can-start-sequence ?b))) -(provide 'zero-table-test) +(provide 'zero-input-table-test) -;;; zero-table-test.el ends here +;;; zero-input-table-test.el ends here diff --git a/zero-table.el b/zero-input-table.el similarity index 65% rename from zero-table.el rename to zero-input-table.el index 9ce321d7113054bbc19e8a33cf234dc9487563b4..4c7a8c1f389f120d2fdd4802e67d154d0ec9317d 100644 --- a/zero-table.el +++ b/zero-input-table.el @@ -1,4 +1,4 @@ -;;; zero-table.el --- a demo table based input method based on zero.el -*- no-byte-compile: t; lexical-binding: t -*- +;;; zero-input-table.el --- a demo table based input method based on zero.el -*- no-byte-compile: t; lexical-binding: t -*- ;; Licensed under the Apache License, Version 2.0 (the "License"); ;; you may not use this file except in compliance with the License. @@ -14,14 +14,14 @@ ;;; Commentary: -;; when you type the key in `zero-table-table', IM will insert the +;; when you type the key in `zero-input-table-table', IM will insert the ;; corresponding value. ;; ;; To use this demo IM, ;; (add-to-list 'load-path "~/fromsource/zero") -;; (require 'zero-table) -;; (zero-set-default-im 'zero-table) ; set as default IM -;; or (zero-set-im 'zero-table) ; set as current buffer's IM +;; (require 'zero-input-table) +;; (zero-input-set-default-im 'zero-input-table) ; set as default IM +;; or (zero-input-set-im 'zero-input-table) ; set as current buffer's IM ;;; Code: @@ -29,54 +29,54 @@ ;; dependencies ;;============== -(require 'zero-framework) +(require 'zero-input-framework) ;;=============================== ;; basic data and emacs facility ;;=============================== -(defvar zero-table-table nil - "The table used by zero-table input method, map string to string.") -(defvar zero-table-sequence-initials nil "Used in `zero-table-can-start-sequence'.") +(defvar zero-input-table-table nil + "The table used by zero-input-table input method, map string to string.") +(defvar zero-input-table-sequence-initials nil "Used in `zero-input-table-can-start-sequence'.") ;;===================== ;; key logic functions ;;===================== -(defun zero-table-sort-key (lhs rhs) +(defun zero-input-table-sort-key (lhs rhs) "A predicate function to sort candidates. Return t if LHS should sort before RHS." (string< (car lhs) (car rhs))) -(defun zero-table-build-candidates (preedit-str &optional _fetch-size) - "Build candidates by looking up PREEDIT-STR in `zero-table-table'." - (mapcar 'cdr (sort (cl-remove-if-not (lambda (pair) (string-prefix-p preedit-str (car pair))) zero-table-table) 'zero-table-sort-key))) +(defun zero-input-table-build-candidates (preedit-str &optional _fetch-size) + "Build candidates by looking up PREEDIT-STR in `zero-input-table-table'." + (mapcar 'cdr (sort (cl-remove-if-not (lambda (pair) (string-prefix-p preedit-str (car pair))) zero-input-table-table) 'zero-input-table-sort-key))) -;; (defun zero-table-build-candidates-async (preedit-str) -;; "build candidate list, when done show it via `zero-table-show-candidates'" -;; (zero-table-debug "building candidate list\n") -;; (let ((candidates (zero-table-build-candidates preedit-str))) +;; (defun zero-input-table-build-candidates-async (preedit-str) +;; "build candidate list, when done show it via `zero-input-table-show-candidates'" +;; (zero-input-table-debug "building candidate list\n") +;; (let ((candidates (zero-input-table-build-candidates preedit-str))) ;; ;; update cache to make SPC and digit key selection possible. -;; (setq zero-table-candidates candidates) -;; (zero-table-show-candidates candidates))) +;; (setq zero-input-table-candidates candidates) +;; (zero-input-table-show-candidates candidates))) -(defun zero-table-can-start-sequence (ch) +(defun zero-input-table-can-start-sequence (ch) "Return t if char CH can start a preedit sequence." - (member (make-string 1 ch) zero-table-sequence-initials)) + (member (make-string 1 ch) zero-input-table-sequence-initials)) ;;=============================== ;; register IM to zero framework ;;=============================== -(zero-register-im - 'zero-table - '((:build-candidates . zero-table-build-candidates) - (:can-start-sequence . zero-table-can-start-sequence))) +(zero-input-register-im + 'zero-input-table + '((:build-candidates . zero-input-table-build-candidates) + (:can-start-sequence . zero-input-table-can-start-sequence))) ;;============ ;; public API ;;============ -(defun zero-table-set-table (alist) +(defun zero-input-table-set-table (alist) "Set the conversion table. the ALIST should be a list of (key . value) pairs. when user type @@ -88,17 +88,17 @@ e.g. (\"map\" . \"https://ditu.amap.com/\") (\"m\" . \"https://msdn.microsoft.com/en-us\") (\"address\" . \"123 Happy Street\"))" - (setq zero-table-table alist) - (setq zero-table-sequence-initials + (setq zero-input-table-table alist) + (setq zero-input-table-sequence-initials (delete-dups (mapcar (lambda (pair) (substring (car pair) 0 1)) - zero-table-table)))) + zero-input-table-table)))) ;;=========== ;; test data ;;=========== -(unless zero-table-table - (zero-table-set-table +(unless zero-input-table-table + (zero-input-table-set-table '(("phone" . "18612345678") ("pyl" . "http://localdocs.emacsos.com/python2/library/%s.html") ("pyli" . "http://localdocs.emacsos.com/python2/index.html") @@ -127,6 +127,6 @@ e.g. ("da" . "__da__") ("now" . "__now__")))) -(provide 'zero-table) +(provide 'zero-input-table) -;;; zero-table.el ends here +;;; zero-input-table.el ends here diff --git a/zero-input.el b/zero-input.el new file mode 100644 index 0000000000000000000000000000000000000000..af795a45f9df7e39704316ed3ffa9b0d8e81bab7 --- /dev/null +++ b/zero-input.el @@ -0,0 +1,1512 @@ +;;; zero-input.el --- Zero Chinese input method framework -*- lexical-binding: t -*- + +;; Licensed under the Apache License, Version 2.0 (the "License"); +;; you may not use this file except in compliance with the License. +;; You may obtain a copy of the License at +;; +;; http://www.apache.org/licenses/LICENSE-2.0 +;; +;; Unless required by applicable law or agreed to in writing, software +;; distributed under the License is distributed on an "AS IS" BASIS, +;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +;; See the License for the specific language governing permissions and +;; limitations under the License. + +;; Version: 2.0.0 +;; URL: https://gitlab.emacsos.com/sylecn/zero-el +;; Package-Version: 2.0.0 +;; Package-Requires: ((emacs "24.3") (s "1.2.0")) + +;;; Commentary: + +;; zero-input.el is auto-generated from multiple other files. see +;; zero-input.el.in and build.py for details. It's created because +;; package-lint doesn't support multi-file package yet (issue #111). +;; +;; zero-input is a Chinese input method framework for Emacs, implemented as an +;; Emacs minor mode. +;; +;; zero-input-pinyin is bundled with zero, to use pinyin input method, add to +;; ~/.emacs file: +;; +;; (require 'zero-input-pinyin) +;; (zero-input-set-default-im 'pinyin) +;; ;; Now you may bind a key to zero-input-toggle to make it easy to +;; ;; switch on/off the input method. +;; (global-set-key (kbd "<f5>") 'zero-input-toggle) +;; +;; zero-input supports Chinese punctuation mapping. There are three modes, +;; none, basic, and full. The default is basic mode, which only map most +;; essential punctuations. You can cycle zero-punctuation-level in current +;; buffer by C-c , , You can change default Chinese punctuation level: +;; +;; (setq-default zero-input-punctuation-level +;; *zero-input-punctuation-level-full*) +;; +;; zero-input supports full-width mode. You can toggle full-width mode in +;; current buffer by C-c , . You can enable full-width mode by default: +;; +;; (setq-default zero-input-full-width-mode t) +;; + +;;; Code: + +(require 'dbus) +(eval-when-compile (require 'cl-lib)) +(require 's) + +;; body of zero-input-panel.el + +;;================ +;; implementation +;;================ + + +(defun zero-input-panel-error-handler (event error) + "Handle dbus errors. + +EVENT and ERROR are error-handler arguments." + (when (or (string-equal "com.emacsos.zero.Panel" + (dbus-event-interface-name event)) + (s-contains-p "com.emacsos.zero.Panel" (cadr error))) + (error "Zero-Input-panel dbus failed: %S" (cadr error)))) + +(add-hook 'dbus-event-error-functions 'zero-input-panel-error-handler) + +(defun zero-input-panel-async-call (method _handler &rest args) + "Call METHOD on zero-input-panel service asynchronously. + +This is a wrapper around `dbus-call-method-asynchronously'. +ARGS optional extra args to pass to the wrapped function." + (apply 'dbus-call-method-asynchronously + :session + "com.emacsos.zero.Panel1" ; well known name + "/com/emacsos/zero/Panel1" ; object path + "com.emacsos.zero.Panel1.PanelInterface" ; interface name + method nil :timeout 500 args)) + +;;========================= +;; public utility function +;;========================= + +(defun zero-input-alist-to-asv (hints) + "Convert Lisp alist to dbus a{sv} data structure. + +HINTS should be an alist of form '((k1 [v1type] v1) (k2 [v2type] v2)). + +For example, +\(zero-input-alist-to-asv + '((\"name\" \"foo\") + (\"timeout\" :int32 10))) +=> +'(:array + (:dict-entry \"name\" (:variant \"foo\")) + (:dict-entry \"timeout\" (:variant :int32 10)))" + (if (null hints) + '(:array :signature "{sv}") + (let ((result '(:array))) + (dolist (item hints) + (push (list :dict-entry (car item) (cons :variant (cdr item))) result)) + (reverse result)))) + +;;============ +;; public API +;;============ + +(defun zero-input-panel-move (x y) + "Move panel to specific coordinate (X, Y). +Origin (0, 0) is at screen top left corner." + (zero-input-panel-async-call "Move" nil :int32 x :int32 y)) + +(defun zero-input-panel-show-candidates (preedit_str candidate_length candidates &optional hints) + "Show CANDIDATES. +Argument PREEDIT_STR the preedit string. +Argument CANDIDATE_LENGTH how many candidates are in candidates list." + (zero-input-panel-async-call "ShowCandidates" nil + :string preedit_str + :uint32 candidate_length + (or candidates '(:array)) + (zero-input-alist-to-asv hints))) + +(defun zero-input-panel-show () + "Show panel." + (zero-input-panel-async-call "Show" nil)) + +(defun zero-input-panel-hide () + "Hide panel." + (zero-input-panel-async-call "Hide" nil)) + +(defun zero-input-panel-quit () + "Quit panel application." + (interactive) + (zero-input-panel-async-call "Quit" nil)) + +(provide 'zero-input-panel) + +;; body of zero-input-framework.el + +;;============== +;; dependencies +;;============== + + +;;======= +;; utils +;;======= + +;; this function is from ibus.el +(defun zero-input--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-input-get-point-position () + "Return current point's position (x y). +Origin (0, 0) is at screen top left corner." + (cl-destructuring-bind (x y line-height) (zero-input--ibus-compute-pixel-position) + (cond + ((functionp 'window-absolute-pixel-position) + ;; introduced in emacs 26 + (cl-destructuring-bind (x . y) (window-absolute-pixel-position) + (list x (+ y line-height)))) + ((functionp 'frame-edges) + ;; introduced in emacs 25 + (cl-destructuring-bind (frame-x frame-y &rest rest) + (frame-edges nil 'inner-edges) + (list (+ frame-x x) (+ frame-y y line-height)))) + (t + ;; <= emacs 24, used guessed pixel size for tool-bar, menu-bar, WM title + ;; bar. Since I can't get that from elisp. + (list (+ (frame-parameter nil 'left) + (if (and (> (frame-parameter nil 'tool-bar-lines) 0) + (eq (frame-parameter nil 'tool-bar-position) 'left)) + 96 0) + x) + (+ (frame-parameter nil 'top) + (if (and (> (frame-parameter nil 'tool-bar-lines) 0) + (eq (frame-parameter nil 'tool-bar-position) 'top)) + 42 0) + (if (> (frame-parameter nil 'menu-bar-lines) 0) (+ 30 30) 0) + line-height + y)))))) + +(defun zero-input-cycle-list (lst item) + "Return the object next to given ITEM in LST. + +If item is the last object, return the first object in lst. +If item is not in lst, return nil." + (let ((r (member item lst))) + (cond + ((null r) nil) + (t (or (cadr r) + (car lst)))))) + +;;===================== +;; key logic functions +;;===================== + +;; zero-input-el version +(defvar zero-input-version nil "Zero package version.") +(setq zero-input-version "2.0.0") + +;; FSM state +(defconst zero-input--state-im-off 'IM-OFF) +(defconst zero-input--state-im-waiting-input 'IM-WAITING-INPUT) +(defconst zero-input--state-im-preediting 'IM-PREEDITING) + +(defconst zero-input-punctuation-level-basic 'BASIC) +(defconst zero-input-punctuation-level-full 'FULL) +(defconst zero-input-punctuation-level-none 'NONE) + +(defvar zero-input-im nil + "Stores 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-input-ims nil + "A list of registered input methods.") + +(defvar zero-input-buffer nil + "Stores the associated buffer. +this is used to help with buffer focus in/out events") + +(defvar zero-input-state zero-input--state-im-off) +(defvar zero-input-full-width-mode nil + "Set to t to enable full-width mode. +In full-width mode, commit ascii char will insert full-width char if there is a +corresponding full-width char. This full-width char map is +independent from punctuation map. You can change this via +`zero-input-toggle-full-width-mode'") +(defvar zero-input-punctuation-level zero-input-punctuation-level-basic + "Punctuation level. + +Should be one of +`zero-input-punctuation-level-basic' +`zero-input-punctuation-level-full' +`zero-input-punctuation-level-none'") +(defvar zero-input-punctuation-levels (list zero-input-punctuation-level-basic + zero-input-punctuation-level-full + zero-input-punctuation-level-none) + "Punctuation levels to use when `zero-input-cycle-punctuation-level'.") +(defvar zero-input-double-quote-flag nil + "Non-nil means next double quote insert close quote. + +Used when converting double quote to Chinese quote. +If nil, next double quote insert open quote. +Otherwise, next double quote insert close quote.") +(defvar zero-input-single-quote-flag nil + "Non-nil means next single quote insert close quote. + +Used when converting single quote to Chinese quote. +If nil, next single quote insert open quote. +Otherwise, next single quote insert close quote.") +(defvar zero-input-preedit-str "") +(defvar zero-input-candidates nil) +(defcustom zero-input-candidates-per-page 10 + "How many candidates to show on each page." + :group 'zero + :type 'integer) +(defvar zero-input-current-page 0 "Current page number. count from 0.") +(defvar zero-input-initial-fetch-size 20 + "How many candidates to fetch for the first call to GetCandidates.") +;; zero-input-fetch-size is reset to 0 when preedit-str changes. +;; zero-input-fetch-size is set to fetch-size in build-candidates-async complete-func +;; lambda. +(defvar zero-input-fetch-size 0 "Last GetCandidates call's fetch-size.") +(defvar zero-input-previous-page-key ?\- "Previous page key.") +(defvar zero-input-next-page-key ?\= "Next page key.") + +;;; concrete input method should define these functions and set them in the +;;; corresponding *-func variable. +(defun zero-input-build-candidates-default (_preedit-str _fetch-size) + "Default implementation for `zero-input-build-candidates-func'." + nil) +(defun zero-input-can-start-sequence-default (_ch) + "Default implementation for `zero-input-can-start-sequence-func'." + nil) +(defun zero-input-get-preedit-str-for-panel-default () + "Default implementation for `zero-input-get-preedit-str-for-panel-func'." + zero-input-preedit-str) +(defvar zero-input-build-candidates-func 'zero-input-build-candidates-default + "Contains a function to build candidates from preedit-str. The function accepts param preedit-str, fetch-size, returns candidate list.") +(defvar zero-input-build-candidates-async-func 'zero-input-build-candidates-async-default + "Contains a function to build candidates from preedit-str. The function accepts param preedit-str, fetch-size, and a complete-func that should be called on returned candidate list.") +(defvar zero-input-can-start-sequence-func 'zero-input-can-start-sequence-default + "Contains a function to decide whether a char can start a preedit sequence.") +(defvar zero-input-handle-preedit-char-func 'zero-input-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-input-get-preedit-str-for-panel-func 'zero-input-get-preedit-str-for-panel-default + "Contains a function that return preedit-str to show in zero-input-panel.") +(defvar zero-input-backspace-func 'zero-input-backspace-default + "Contains a function to handle <backward> char.") +(defvar zero-input-handle-preedit-char-func 'zero-input-handle-preedit-char-default + "Hanlde character insert in `zero-input--state-im-preediting' mode.") +(defvar zero-input-preedit-start-func 'nil + "Called when enter `zero-input--state-im-preediting' state.") +(defvar zero-input-preedit-end-func 'nil + "Called when leave `zero-input--state-im-preediting' state.") + +(defvar zero-input-enable-debug nil + "Whether to enable debug. +if t, `zero-input-debug' will output debug msg in *zero-input-debug* buffer") +(defvar zero-input-debug-buffer-max-size 30000 + "Max characters in *zero-input-debug* buffer. If reached, first half data will be deleted.") + +(defun zero-input-debug (string &rest objects) + "Log debug message in *zero-input-debug* buffer. + +STRING and OBJECTS are passed to `format'" + (if zero-input-enable-debug + (with-current-buffer (get-buffer-create "*zero-input-debug*") + (goto-char (point-max)) + (insert (apply 'format string objects)) + (when (> (point) zero-input-debug-buffer-max-size) + (insert "removing old data\n") + (delete-region (point-min) (/ zero-input-debug-buffer-max-size 2)))))) + +;; (zero-input-debug "msg1\n") +;; (zero-input-debug "msg2: %s\n" "some obj") +;; (zero-input-debug "msg3: %s\n" 24) +;; (zero-input-debug "msg4: %s %s\n" 24 1) + +(defun zero-input-enter-preedit-state () + "Config keymap when enter preedit state." + (zero-input-enable-preediting-map) + (if (functionp zero-input-preedit-start-func) + (funcall zero-input-preedit-start-func))) + +(defun zero-input-leave-preedit-state () + "Config keymap when leave preedit state." + (zero-input-disable-preediting-map) + (if (functionp zero-input-preedit-end-func) + (funcall zero-input-preedit-end-func))) + +(defun zero-input-set-state (state) + "Set zero state to given STATE." + (zero-input-debug "set state to %s\n" state) + (setq zero-input-state state) + (if (eq state zero-input--state-im-preediting) + (zero-input-enter-preedit-state) + (zero-input-leave-preedit-state))) + +(defun zero-input-candidates-on-page (candidates) + "Return candidates on current page for given CANDIDATES list." + (cl-flet ((take (n lst) + "take the first n element from lst. if there is not +enough elements, return lst as it is." + (cl-loop + for lst* = lst then (cdr lst*) + for n* = n then (1- n*) + until (or (zerop n*) (null lst*)) + collect (car lst*))) + (drop (n lst) + "drop the first n elements from lst" + (cl-loop + for lst* = lst then (cdr lst*) + for n* = n then (1- n*) + until (or (zerop n*) (null lst*)) + finally (return lst*)))) + (take zero-input-candidates-per-page + (drop (* zero-input-candidates-per-page zero-input-current-page) candidates)))) + +(defun zero-input-show-candidates (&optional candidates) + "Show CANDIDATES using zero-input-panel via IPC/RPC." + (let ((candidates-on-page (zero-input-candidates-on-page (or candidates + zero-input-candidates)))) + (cl-destructuring-bind (x y) (zero-input-get-point-position) + (zero-input-panel-show-candidates + (funcall zero-input-get-preedit-str-for-panel-func) + (length candidates-on-page) + candidates-on-page + `(("in_emacs" t) + ("filename" ,(or (buffer-file-name) "")) + ("page_number" ,(1+ zero-input-current-page)) + ("has_next_page" ,(or (> (length (or candidates zero-input-candidates)) (* zero-input-candidates-per-page (1+ zero-input-current-page))) (< zero-input-fetch-size (* zero-input-candidates-per-page (+ 2 zero-input-current-page))))) + ("has_previous_page" ,(> zero-input-current-page 0)) + ("move_x" :int32 ,x) + ("move_y" :int32 ,y))) + (zero-input-debug "candidates: %s\n" (s-join ", " candidates-on-page))))) + +(defun zero-input-build-candidates (preedit-str fetch-size) + "Build candidates list synchronously. + +Try to find at least FETCH-SIZE number of candidates for PREEDIT-STR." + ;; (zero-input-debug "zero-input-build-candidates\n") + (unless (functionp zero-input-build-candidates-func) + (signal 'wrong-type-argument (list 'functionp zero-input-build-candidates-func))) + (prog1 (funcall zero-input-build-candidates-func preedit-str fetch-size) + (setq zero-input-fetch-size (max fetch-size (length zero-input-candidates))))) + +(defun zero-input-build-candidates-complete (candidates) + "Called when `zero-input-build-candidates-async' return. + +CANDIDATES is returned candidates list from async call." + (setq zero-input-candidates candidates) + (zero-input-show-candidates candidates)) + +(defun zero-input-build-candidates-async-default (preedit-str fetch-size complete-func) + "Build candidate list, when done show it via `zero-input-show-candidates'. + +PREEDIT-STR the preedit-str. +FETCH-SIZE try to find at least this many candidates for preedit-str. +COMPLETE-FUNC the function to call when build candidates completes." + ;; (zero-input-debug "zero-input-build-candidates-async-default\n") + (let ((candidates (zero-input-build-candidates preedit-str fetch-size))) + ;; update cache to make SPC and digit key selection possible. + (funcall complete-func candidates))) + +(defvar zero-input-full-width-char-map + ;; ascii 33 to 126 map to + ;; unicode FF01 to FF5E + (cl-loop + for i from 33 to 126 + collect (cons (make-char 'ascii i) (make-char 'unicode 0 255 (- i 32)))) + "An alist that map half-width char to full-width char.") + +(defun zero-input-convert-ch-to-full-width (ch) + "Convert half-width char CH to full-width. + +If there is no full-width char for CH, return it unchanged." + (let ((pair (assoc ch zero-input-full-width-char-map))) + (if pair (cdr pair) ch))) + +(defun zero-input-convert-str-to-full-width (s) + "Convert each char in S to their full-width char if there is one." + (concat (mapcar 'zero-input-convert-ch-to-full-width s))) + +(defun zero-input-convert-str-to-full-width-maybe (s) + "If in `zero-input-full-width-mode', convert char in S to their full-width char; otherwise, return s unchanged." + (if zero-input-full-width-mode (zero-input-convert-str-to-full-width s) s)) + +(defun zero-input-insert-full-width-char (ch) + "If in `zero-input-full-width-mode', insert full-width char for given CH and return true, otherwise just return nil." + (when zero-input-full-width-mode + (let ((full-width-ch (zero-input-convert-ch-to-full-width ch))) + (insert full-width-ch) + full-width-ch))) + +(defun zero-input-convert-punctuation-basic (ch) + "Convert punctuation for `zero-input-punctuation-level-basic'. + +Return CH's Chinese punctuation if CH is converted. Return nil otherwise." + (cl-case ch + (?, ",") + (?. "。") ; 0x3002 + (?? "?") + (?! "!") + (?\\ "、") ; 0x3001 + (?: ":") + (otherwise nil))) + +(defun zero-input-convert-punctuation-full (ch) + "Convert punctuation for `zero-input-punctuation-level-full'. + +Return CH's Chinese punctuation if CH is converted. Return nil otherwise" + (cl-case ch + (?_ "——") + (?< "《") ;0x300A + (?> "》") ;0x300B + (?\( "(") + (?\) ")") + (?\[ "【") ;0x3010 + (?\] "】") ;0x3011 + (?^ "……") + (?\" (setq zero-input-double-quote-flag (not zero-input-double-quote-flag)) + (if zero-input-double-quote-flag "“" "”")) + (?\' (setq zero-input-single-quote-flag (not zero-input-single-quote-flag)) + (if zero-input-single-quote-flag "‘" "’")) + (?~ "~") + (?\; ";") + (?$ "¥") + (t (zero-input-convert-punctuation-basic ch)))) + +(defun zero-input-convert-punctuation (ch) + "Convert punctuation based on `zero-input-punctuation-level'. +Return CH's Chinese punctuation if CH is converted. Return nil otherwise." + (cond + ((eq zero-input-punctuation-level zero-input-punctuation-level-basic) + (zero-input-convert-punctuation-basic ch)) + ((eq zero-input-punctuation-level zero-input-punctuation-level-full) + (zero-input-convert-punctuation-full ch)) + (t nil))) + +(defun zero-input-handle-punctuation (ch) + "If CH is a punctuation character, insert mapped Chinese punctuation and return true; otherwise, return false." + (let ((str (zero-input-convert-punctuation ch))) + (when str + (insert str) + t))) + +(defun zero-input-append-char-to-preedit-str (ch) + "Append char CH to preedit str, update and show candidate list." + (setq zero-input-preedit-str + (concat zero-input-preedit-str (make-string 1 ch))) + (zero-input-debug "appended %c, preedit str is: %s\n" ch zero-input-preedit-str) + (zero-input-preedit-str-changed)) + +(defun zero-input-can-start-sequence (ch) + "Return t if char CH can start a preedit sequence." + (if (functionp zero-input-can-start-sequence-func) + (funcall zero-input-can-start-sequence-func ch) + (error "`zero-input-can-start-sequence-func' is not a function"))) + +(defun zero-input-page-up () + "If not at first page, show candidates on previous page." + (interactive) + (when (> zero-input-current-page 0) + (setq zero-input-current-page (1- zero-input-current-page)) + (zero-input-show-candidates))) + +(defun zero-input-just-page-down () + "Just page down using existing candidates." + (let ((len (length zero-input-candidates))) + (when (> len (* zero-input-candidates-per-page (1+ zero-input-current-page))) + (setq zero-input-current-page (1+ zero-input-current-page)) + (zero-input-show-candidates)))) + +(defun zero-input-page-down () + "If there is still candidates to be displayed, show candidates on next page." + (interactive) + (let ((len (length zero-input-candidates)) + (new-fetch-size (* zero-input-candidates-per-page (+ 2 zero-input-current-page)))) + (if (and (< len new-fetch-size) + (< zero-input-fetch-size new-fetch-size)) + (funcall zero-input-build-candidates-async-func + zero-input-preedit-str + new-fetch-size + (lambda (candidates) + (zero-input-build-candidates-complete candidates) + (setq zero-input-fetch-size (max new-fetch-size + (length candidates))) + (zero-input-just-page-down))) + (zero-input-just-page-down)))) + +(defun zero-input-handle-preedit-char-default (ch) + "Hanlde character insert in `zero-input--state-im-preediting' state. + +CH is the char user has typed." + (cond + ((= ch ?\s) + (zero-input-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-input-commit-nth-candidate (mod (- (- ch ?0) 1) 10)) + (zero-input-append-char-to-preedit-str ch))) + ((= ch zero-input-previous-page-key) + (zero-input-page-up)) + ((= ch zero-input-next-page-key) + (zero-input-page-down)) + (t (let ((str (zero-input-convert-punctuation ch))) + (if str + (progn + (zero-input-set-state zero-input--state-im-waiting-input) + (zero-input-commit-first-candidate-or-preedit-str) + (insert str)) + (zero-input-append-char-to-preedit-str ch)))))) + +(defun zero-input-self-insert-command (n) + "Handle character `self-insert-command'. This includes characters and digits. + +N is the argument passed to `self-insert-command'." + (interactive "p") + (let ((ch (elt (this-command-keys-vector) 0))) + (zero-input-debug "user typed: %c\n" ch) + (cond + ((eq zero-input-state zero-input--state-im-waiting-input) + (if (zero-input-can-start-sequence ch) + (progn + (zero-input-debug "can start sequence, state=IM_PREEDITING\n") + (zero-input-set-state zero-input--state-im-preediting) + (zero-input-append-char-to-preedit-str ch)) + (zero-input-debug "cannot start sequence, state=IM_WAITING_INPUT\n") + (unless (zero-input-handle-punctuation ch) + (unless (zero-input-insert-full-width-char ch) + (self-insert-command n))))) + ((eq zero-input-state zero-input--state-im-preediting) + (zero-input-debug "still preediting\n") + (funcall zero-input-handle-preedit-char-func ch)) + (t + (zero-input-debug "unexpected state: %s\n" zero-input-state) + (self-insert-command n))))) + +(defun zero-input-preedit-str-changed () + "Called when preedit str is changed and not empty. Update and show candidate list." + (setq zero-input-fetch-size 0) + (setq zero-input-current-page 0) + (funcall zero-input-build-candidates-async-func zero-input-preedit-str zero-input-initial-fetch-size 'zero-input-build-candidates-complete)) + +(defun zero-input-backspace-default () + "Handle backspace key in `zero-input--state-im-preediting' state." + (let ((len (length zero-input-preedit-str))) + (if (> len 1) + (progn + (setq zero-input-preedit-str + (substring zero-input-preedit-str 0 (1- len))) + (zero-input-preedit-str-changed)) + (zero-input-set-state zero-input--state-im-waiting-input) + (zero-input-reset)))) + +(defun zero-input-backspace () + "Handle backspace key in `zero-input--state-im-preediting' state." + (interactive) + (unless (eq zero-input-state zero-input--state-im-preediting) + (error "Error: zero-input-backspace called in non preediting state")) + (zero-input-debug "zero-input-backspace\n") + (funcall zero-input-backspace-func)) + +(defun zero-input-commit-text (text) + "Commit given TEXT, reset preedit str, hide candidate list." + (zero-input-debug "commit text: %s\n" text) + (insert text) + (setq zero-input-preedit-str "") + (setq zero-input-candidates nil) + (setq zero-input-current-page 0) + (zero-input-hide-candidate-list)) + +(defun zero-input-return () + "Handle RET key press in `zero-input--state-im-preediting' state." + (interactive) + (unless (eq zero-input-state zero-input--state-im-preediting) + (error "Error: zero-input-return called in non preediting state")) + (zero-input-debug "zero-input-return\n") + (zero-input-set-state zero-input--state-im-waiting-input) + (zero-input-commit-text (zero-input-convert-str-to-full-width-maybe zero-input-preedit-str))) + +(defun zero-input-commit-nth-candidate (n) + "Commit Nth candidate and return true if it exists; otherwise, return false." + (let ((candidate (nth n (zero-input-candidates-on-page zero-input-candidates)))) + (if candidate + (progn + (zero-input-set-state zero-input--state-im-waiting-input) + (zero-input-commit-text candidate) + t) + nil))) + +(defun zero-input-commit-preedit-str () + "Commit current preedit-str." + (zero-input-set-state zero-input--state-im-waiting-input) + (zero-input-commit-text (zero-input-convert-str-to-full-width-maybe zero-input-preedit-str))) + +(defun zero-input-commit-first-candidate-or-preedit-str () + "Commit first candidate if there is one, otherwise commit preedit str." + (unless (zero-input-commit-nth-candidate 0) + (zero-input-commit-preedit-str))) + +(defun zero-input-hide-candidate-list () + "Hide candidate list." + (zero-input-panel-hide) + (zero-input-debug "hide candidate list\n")) + +(defun zero-input-reset () + "Reset zero states." + (interactive) + (zero-input-debug "zero-input-reset\n") + (zero-input-set-state zero-input--state-im-waiting-input) + (setq zero-input-preedit-str "") + (setq zero-input-candidates nil) + (setq zero-input-current-page 0) + (zero-input-hide-candidate-list)) + +(defun zero-input-focus-in () + "A hook function, run when focus in a buffer." + (when (eq zero-input-state zero-input--state-im-preediting) + (zero-input-show-candidates zero-input-candidates) + (zero-input-enter-preedit-state))) + +(defun zero-input-focus-out () + "A hook function, run when focus out a buffer." + (when (eq zero-input-state zero-input--state-im-preediting) + (zero-input-hide-candidate-list) + (zero-input-leave-preedit-state))) + +(defun zero-input-buffer-list-changed () + "A hook function, run when buffer list has changed. This includes user has switched buffer." + (if (eq (car (buffer-list)) zero-input-buffer) + (zero-input-focus-in))) + +;;============ +;; minor mode +;;============ + +(defvar zero-input-mode-map + (let ((map (make-sparse-keymap))) + ;; build zero-input-prefix-map + (defvar zero-input-prefix-map (define-prefix-command 'zero-input-prefix-map)) + (let ((bindings '(("," zero-input-cycle-punctuation-level) + ("." zero-input-toggle-full-width-mode)))) + (dolist (b bindings) + (define-key zero-input-prefix-map (car b) (cadr b)))) + ;; mount zero-input-prefix-map in C-c , prefix key. + (define-key map (kbd "C-c ,") zero-input-prefix-map) + + ;; other keybindings + (define-key map [remap self-insert-command] + 'zero-input-self-insert-command) + map) + "Keymap for `zero-input-mode'.") + +(defun zero-input-enable-preediting-map () + "Enable preediting keymap in `zero-input-mode-map'." + (zero-input-debug "zero-input-enable-preediting-map\n") + (define-key zero-input-mode-map (kbd "<backspace>") 'zero-input-backspace) + (define-key zero-input-mode-map (kbd "RET") 'zero-input-return) + (define-key zero-input-mode-map (kbd "<escape>") 'zero-input-reset)) + +(defun zero-input-disable-preediting-map () + "Disable preediting keymap in `zero-input-mode-map'." + (zero-input-debug "zero-input-disable-preediting-map\n") + (define-key zero-input-mode-map (kbd "<backspace>") nil) + (define-key zero-input-mode-map (kbd "RET") nil) + (define-key zero-input-mode-map (kbd "<escape>") nil)) + +(defun zero-input-modeline-string () + "Build `zero-input-mode' modeline string aka lighter. + +If full-width mode is enabled, show ZeroF; +Otherwise, show Zero." + (if zero-input-full-width-mode " ZeroF" " Zero")) + +(define-minor-mode zero-input-mode + "a Chinese input method framework written as an emacs minor mode. + +\\{zero-input-mode-map}" + nil + (:eval (zero-input-modeline-string)) + zero-input-mode-map + ;; local variables and variable init + (make-local-variable 'zero-input-state) + (zero-input-set-state zero-input--state-im-off) + (make-local-variable 'zero-input-punctuation-level) + (make-local-variable 'zero-input-full-width-mode) + (make-local-variable 'zero-input-double-quote-flag) + (make-local-variable 'zero-input-single-quote-flag) + (set (make-local-variable 'zero-input-preedit-str) "") + (set (make-local-variable 'zero-input-candidates) nil) + (make-local-variable 'zero-input-candidates-per-page) + (make-local-variable 'zero-input-current-page) + (make-local-variable 'zero-input-fetch-size) + (make-local-variable 'zero-input-im) + (make-local-variable 'zero-input-build-candidates-func) + (make-local-variable 'zero-input-can-start-sequence-func) + (zero-input-set-im zero-input-im) + ;; hooks + (add-hook 'focus-in-hook 'zero-input-focus-in) + (add-hook 'focus-out-hook 'zero-input-focus-out) + (set (make-local-variable 'zero-input-buffer) (current-buffer)) + (add-hook 'buffer-list-update-hook 'zero-input-buffer-list-changed)) + +;;================== +;; IM developer API +;;================== + +(defun zero-input-register-im (im-name im-functions-alist) + "(Re)register an input method in zero. + +After registration, you can use `zero-input-set-default-im' and +`zero-input-set-im' to select input method to use. + +IM-NAME should be a symbol. +IM-FUNCTIONS-ALIST should be a list of form + '((:virtual-function-name . implementation-function-name)) + +virtual functions corresponding variable +=========================================================================== +:build-candidates `zero-input-build-candidates-func' +:can-start-sequence `zero-input-can-start-sequence-func' +:handle-preedit-char `zero-input-handle-preedit-char-func' +:get-preedit-str-for-panel `zero-input-get-preedit-str-for-panel-func' +:handle-backspace `zero-input-backspace-func' +:init nil +:shutdown nil +:preedit-start `zero-input-preedit-start-func' +:preedit-end `zero-input-preedit-end-func' + +registered input method is saved in `zero-input-ims'" + ;; add or replace entry in `zero-input-ims' + (unless (symbolp im-name) + (signal 'wrong-type-argument (list 'symbolp im-name))) + (setq zero-input-ims (assq-delete-all im-name zero-input-ims)) + (setq zero-input-ims (push (cons im-name im-functions-alist) zero-input-ims))) + +;;============ +;; public API +;;============ + +(defun zero-input-toggle-full-width-mode () + "Toggle `zero-input-full-width-mode' on/off." + (interactive) + (setq zero-input-full-width-mode (not zero-input-full-width-mode)) + (message (if zero-input-full-width-mode + "Enabled full-width mode" + "Enabled half-width mode"))) + +(defun zero-input-set-punctuation-level (level) + "Set `zero-input-punctuation-level'. + +LEVEL the level to set to." + (interactive) + (if (not (member level (list zero-input-punctuation-level-basic + zero-input-punctuation-level-full + zero-input-punctuation-level-none))) + (error "Level not supported: %s" level) + (setq zero-input-punctuation-level level))) + +(defun zero-input-set-punctuation-levels (levels) + "Set `zero-input-punctuation-levels'. + +`zero-input-cycle-punctuation-level' will cycle current +`zero-input-punctuation-level' among defined LEVELS." + (dolist (level levels) + (if (not (member level (list zero-input-punctuation-level-basic + zero-input-punctuation-level-full + zero-input-punctuation-level-none))) + (error "Level not supported: %s" level))) + (setq zero-input-punctuation-levels levels)) + +(defun zero-input-cycle-punctuation-level () + "Cycle `zero-input-punctuation-level' among `zero-input-punctuation-levels'." + (interactive) + (setq zero-input-punctuation-level + (zero-input-cycle-list zero-input-punctuation-levels zero-input-punctuation-level)) + (message "punctuation level set to %s" zero-input-punctuation-level)) + +;;;###autoload +(defun zero-input-set-im (im-name) + "Select zero input method for current buffer. + +if IM-NAME is nil, use default empty input method" + ;; TODO provide auto completion for im-name + (interactive "SSet input method to: ") + ;; when switch away from an IM, run last IM's :shutdown function. + (if zero-input-im + (let ((shutdown-func (cdr (assq :shutdown (cdr (assq zero-input-im zero-input-ims)))))) + (if (functionp shutdown-func) + (funcall shutdown-func)))) + (if im-name + (let ((im-functions (cdr (assq im-name zero-input-ims)))) + (if im-functions + (progn + ;; TODO create a macro to reduce code duplication and human + ;; error. + ;; + ;; TODO do some functionp check for the slot functions. if check + ;; fail, keep (or revert to) the old IM. + (setq zero-input-build-candidates-func + (or (cdr (assq :build-candidates im-functions)) + 'zero-input-build-candidates-default)) + (setq zero-input-build-candidates-async-func + (or (cdr (assq :build-candidates-async im-functions)) + 'zero-input-build-candidates-async-default)) + (setq zero-input-can-start-sequence-func + (or (cdr (assq :can-start-sequence im-functions)) + 'zero-input-can-start-sequence-default)) + (setq zero-input-handle-preedit-char-func + (or (cdr (assq :handle-preedit-char im-functions)) + 'zero-input-handle-preedit-char-default)) + (setq zero-input-get-preedit-str-for-panel-func + (or (cdr (assq :get-preedit-str-for-panel im-functions)) + 'zero-input-get-preedit-str-for-panel-default)) + (setq zero-input-backspace-func + (or (cdr (assq :handle-backspace im-functions)) + 'zero-input-backspace-default)) + (setq zero-input-preedit-start-func + (cdr (assq :preedit-start im-functions))) + (setq zero-input-preedit-end-func + (cdr (assq :preedit-end im-functions))) + (unless (functionp zero-input-backspace-func) + (signal 'wrong-type-argument + (list 'functionp zero-input-backspace-func))) + ;; when switch to a IM, run its :init function + (let ((init-func (cdr (assq :init im-functions)))) + (if (functionp init-func) + (funcall init-func))) + (set (make-local-variable 'zero-input-im) im-name)) + (error "Input method %s not registered in zero" im-name))) + (zero-input-debug "using default empty input method") + (setq zero-input-build-candidates-func 'zero-input-build-candidates-default) + (setq zero-input-build-candidates-async-func 'zero-input-build-candidates-async-default) + (setq zero-input-can-start-sequence-func 'zero-input-can-start-sequence-default) + (setq zero-input-handle-preedit-char-func 'zero-input-handle-preedit-char-default) + (setq zero-input-get-preedit-str-for-panel-func 'zero-input-get-preedit-str-for-panel-default) + (setq zero-input-backspace-func 'zero-input-backspace-default) + (setq zero-input-preedit-start-func nil) + (setq zero-input-preedit-end-func nil))) + +;;;###autoload +(defun zero-input-set-default-im (im-name) + "Set given IM-NAME as default zero input method." + (unless (symbolp im-name) + (signal 'wrong-type-argument (list 'symbolp im-name))) + (setq-default zero-input-im im-name)) + +;;;###autoload +(defun zero-input-on () + "Turn on `zero-input-mode'." + (interactive) + (zero-input-debug "zero-input-on\n") + (zero-input-mode 1) + (if (eq zero-input-state zero-input--state-im-off) + (zero-input-set-state zero-input--state-im-waiting-input))) + +(defun zero-input-off () + "Turn off `zero-input-mode'." + (interactive) + (zero-input-debug "zero-input-off\n") + (zero-input-mode -1) + (zero-input-reset) + (zero-input-set-state zero-input--state-im-off)) + +;;;###autoload +(defun zero-input-toggle () + "Toggle `zero-input-mode'." + (interactive) + (if zero-input-mode + (zero-input-off) + (zero-input-on))) + +(provide 'zero-input-framework) + +;; body of zero-input-table.el + +;;============== +;; dependencies +;;============== + + +;;=============================== +;; basic data and emacs facility +;;=============================== + +(defvar zero-input-table-table nil + "The table used by zero-input-table input method, map string to string.") +(defvar zero-input-table-sequence-initials nil "Used in `zero-input-table-can-start-sequence'.") + +;;===================== +;; key logic functions +;;===================== + +(defun zero-input-table-sort-key (lhs rhs) + "A predicate function to sort candidates. Return t if LHS should sort before RHS." + (string< (car lhs) (car rhs))) + +(defun zero-input-table-build-candidates (preedit-str &optional _fetch-size) + "Build candidates by looking up PREEDIT-STR in `zero-input-table-table'." + (mapcar 'cdr (sort (cl-remove-if-not (lambda (pair) (string-prefix-p preedit-str (car pair))) zero-input-table-table) 'zero-input-table-sort-key))) + +;; (defun zero-input-table-build-candidates-async (preedit-str) +;; "build candidate list, when done show it via `zero-input-table-show-candidates'" +;; (zero-input-table-debug "building candidate list\n") +;; (let ((candidates (zero-input-table-build-candidates preedit-str))) +;; ;; update cache to make SPC and digit key selection possible. +;; (setq zero-input-table-candidates candidates) +;; (zero-input-table-show-candidates candidates))) + +(defun zero-input-table-can-start-sequence (ch) + "Return t if char CH can start a preedit sequence." + (member (make-string 1 ch) zero-input-table-sequence-initials)) + +;;=============================== +;; register IM to zero framework +;;=============================== + +(zero-input-register-im + 'zero-input-table + '((:build-candidates . zero-input-table-build-candidates) + (:can-start-sequence . zero-input-table-can-start-sequence))) + +;;============ +;; public API +;;============ + +(defun zero-input-table-set-table (alist) + "Set the conversion table. + +the ALIST should be a list of (key . value) pairs. when user type +\(part of) key, the IM will show all matching value. + +e.g. +'((\"phone\" . \"18612345678\") + (\"mail\" . \"foo@example.com\") + (\"map\" . \"https://ditu.amap.com/\") + (\"m\" . \"https://msdn.microsoft.com/en-us\") + (\"address\" . \"123 Happy Street\"))" + (setq zero-input-table-table alist) + (setq zero-input-table-sequence-initials + (delete-dups (mapcar (lambda (pair) (substring (car pair) 0 1)) + zero-input-table-table)))) + +;;=========== +;; test data +;;=========== + +(unless zero-input-table-table + (zero-input-table-set-table + '(("phone" . "18612345678") + ("pyl" . "http://localdocs.emacsos.com/python2/library/%s.html") + ("pyli" . "http://localdocs.emacsos.com/python2/index.html") + ("pylm" . "http://localdocs.emacsos.com/python2/py-modindex.html") + ("py3li" . "http://localdocs.emacsos.com/python2/index.html") + ("py3l" . "http://localdocs.emacsos.com/python3/library/%s.html") + ("py3lm" . "http://localdocs.emacsos.com/python3/py-modindex.html") + ("pyop" . "http://docs.python.org/library/operator.html") + ("pyopl" . "http://localdocs.emacsos.com/python2/library/operator.html") + ("pympl" . "http://localdocs.emacsos.com/python2/library/multiprocessing.html") + ("py2" . "http://docs.python.org/2/library/%s.html") + ("py3" . "http://docs.python.org/3/library/%s.html") + ("py2i" . "http://docs.python.org/2/") + ("py2m" . "http://docs.python.org/2/py-modindex.html") + ("py3i" . "http://docs.python.org/3/") + ("py3m" . "http://docs.python.org/3/py-modindex.html") + ("pycodec" . "http://localdocs.emacsos.com/python2/library/codecs.html#standard-encodings") + ("pycodecs" . "http://localdocs.emacsos.com/python2/library/codecs.html#standard-encodings") + ("pycodecsr" . "http://docs.python.org/library/codecs.html#standard-encodings") + ("pycodecr" . "http://docs.python.org/library/codecs.html#standard-encodings") + ("pep328" . "http://www.python.org/dev/peps/pep-0328/") + ("mail" . "foo@example.com") + ("map" . "https://ditu.amap.com/") + ("m" . "https://msdn.microsoft.com/en-us") + ("address" . "123 Happy Street") + ("da" . "__da__") + ("now" . "__now__")))) + +(provide 'zero-input-table) + +;; body of zero-input-pinyin-service.el + +;;================ +;; implementation +;;================ + + +(defvar zero-input-pinyin-service-service-name + "com.emacsos.zero.ZeroPinyinService1") +(defvar zero-input-pinyin-service-path + "/com/emacsos/zero/ZeroPinyinService1") +(defvar zero-input-pinyin-service-interface + "com.emacsos.zero.ZeroPinyinService1.ZeroPinyinServiceInterface") +(defvar zero-input-pinyin-fuzzy-flag 0) + +(defun zero-input-pinyin-service-error-handler (event error) + "Handle dbus errors. + +EVENT, ERROR are arguments passed to the handler." + (when (or (string-equal zero-input-pinyin-service-service-name + (dbus-event-interface-name event)) + (s-contains-p zero-input-pinyin-service-service-name (cadr error))) + (error "`zero-input-pinyin-service' dbus failed: %S" (cadr error)))) + +(add-hook 'dbus-event-error-functions 'zero-input-pinyin-service-error-handler) + +(defun zero-input-pinyin-service-async-call (method handler &rest args) + "Call METHOD on zero-input-pinin-service asynchronously. +This is a wrapper around `dbus-call-method-asynchronously'. +Argument HANDLER the handler function. +Optional argument ARGS extra arguments to pass to the wrapped function." + (apply 'dbus-call-method-asynchronously + :session zero-input-pinyin-service-service-name + zero-input-pinyin-service-path + zero-input-pinyin-service-interface + method handler :timeout 1000 args)) + +(defun zero-input-pinyin-service-call (method &rest args) + "Call METHOD on zero-input-pinin-service synchronously. +This is a wrapper around `dbus-call-method'. +Optional argument ARGS extra arguments to pass to the wrapped function." + (apply 'dbus-call-method + :session zero-input-pinyin-service-service-name + zero-input-pinyin-service-path + zero-input-pinyin-service-interface + method :timeout 1000 args)) + +;;============ +;; public API +;;============ + +(defun zero-input-pinyin-service-get-candidates (preedit-str fetch-size) + "Get candidates for pinyin in PREEDIT-STR synchronously. + +preedit-str the preedit-str, should be pure pinyin string +FETCH-SIZE try to fetch this many candidates or more" + (zero-input-pinyin-service-call "GetCandidatesV2" :string preedit-str :uint32 fetch-size :uint32 zero-input-pinyin-fuzzy-flag)) + +(defun zero-input-pinyin-service-get-candidates-async (preedit-str fetch-size get-candidates-complete) + "Get candidates for pinyin in PREEDIT-STR asynchronously. + +PREEDIT-STR the preedit string, should be pure pinyin string. +FETCH-SIZE try to fetch this many candidates or more. +GET-CANDIDATES-COMPLETE the async handler function." + (zero-input-pinyin-service-async-call + "GetCandidatesV2" get-candidates-complete :string preedit-str :uint32 fetch-size :uint32 zero-input-pinyin-fuzzy-flag)) + +(defun zero-input-pinyin-candidate-pinyin-indices-to-dbus-format (candidate_pinyin_indices) + "Convert CANDIDATE_PINYIN_INDICES to Emacs dbus format." + (let (result) + (push :array result) + ;; (push :signature result) + ;; (push "(ii)" result) + (dolist (pypair candidate_pinyin_indices) + (push (list :struct :int32 (cl-first pypair) :int32 (cl-second pypair)) + result)) + (reverse result))) + +(defun zero-input-pinyin-service-commit-candidate-async (candidate candidate_pinyin_indices) + "Commit candidate asynchronously. + +CANDIDATE the candidate user selected. +CANDIDATE_PINYIN_INDICES the candidate's pinyin shengmu and yunmu index." + ;; don't care about the result, so no callback. + (zero-input-pinyin-service-async-call + "CommitCandidate" nil + :string candidate + (zero-input-pinyin-candidate-pinyin-indices-to-dbus-format candidate_pinyin_indices))) + +(defun zero-input-pinyin-service-delete-candidates-async (candidate delete-candidate-complete) + "Delete CANDIDATE asynchronously. + +DELETE-CANDIDATE-COMPLETE the async handler function." + (zero-input-pinyin-service-async-call + "DeleteCandidate" delete-candidate-complete :string candidate)) + +(defun zero-input-pinyin-service-quit () + "Quit panel application." + (zero-input-pinyin-service-async-call "Quit" nil)) + +(provide 'zero-input-pinyin-service) + +;; body of zero-input-pinyin.el + +;;============== +;; dependencies +;;============== + + +;;=============================== +;; basic data and emacs facility +;;=============================== + +;; these two var is only used in docstring to avoid checkdoc line-too-long +;; error. +(defvar zero-input-pinyin-service-interface-xml-file + "/usr/share/dbus-1/interfaces/com.emacsos.zero.ZeroPinyinService1.ZeroPinyinServiceInterface.xml") +(defvar zero-input-pinyin-service-interface-xml-url + "https://gitlab.emacsos.com/sylecn/zero-input-pinyin-service/blob/master/com.emacsos.zero.ZeroPinyinService1.ZeroPinyinServiceInterface.xml") +(defcustom zero-input-pinyin-fuzzy-flag 0 + "Non-nil means use this value as GetCandidatesV2 fuzzy_flag param. +see zero-input-pinyin-service dbus interface xml for document. + +You can find the xml file locally at +`zero-input-pinyin-service-interface-xml-file' or online at +`zero-input-pinyin-service-interface-xml-url'." + :type 'integer + :group 'zero-input-pinyin) + +(defvar zero-input-pinyin-state nil "Zero-Input-pinyin internal state. could be nil or `*zero-input-pinyin-state-im-partial-commit*'.") +(defconst zero-input-pinyin--state-im-partial-commit 'IM-PARTIAL-COMMIT) + +(defvar zero-input-pinyin-used-preedit-str-lengths nil + "Accompany `zero-input-candidates', marks how many preedit-str chars are used for each candidate.") +(defvar zero-input-pinyin-candidates-pinyin-indices nil + "Store GetCandidates dbus method candidates_pinyin_indices field.") +(defvar zero-input-pinyin-pending-str "") +(defvar zero-input-pinyin-pending-preedit-str "") +(defvar zero-input-pinyin-pending-pinyin-indices nil + "Stores `zero-input-pinyin-pending-str' corresponds pinyin indices.") + +;;===================== +;; key logic functions +;;===================== + +(defun zero-input-pinyin-reset () + "Reset states." + (setq zero-input-pinyin-state nil) + (setq zero-input-pinyin-used-preedit-str-lengths nil) + (setq zero-input-pinyin-pending-str "") + (setq zero-input-pinyin-pending-preedit-str "")) + +(defun zero-input-pinyin-init () + "Called when this im is turned on." + (make-local-variable 'zero-input-pinyin-state) + (zero-input-pinyin-reset)) + +(defun zero-input-pinyin-preedit-start () + "Called when enter `*zero-input-state-im-preediting*' state." + (define-key zero-input-mode-map [remap digit-argument] 'zero-input-digit-argument)) + +(defun zero-input-pinyin-preedit-end () + "Called when leave `*zero-input-state-im-preediting*' state." + (define-key zero-input-mode-map [remap digit-argument] nil)) + +(defun zero-input-pinyin-shutdown () + "Called when this im is turned off." + (define-key zero-input-mode-map [remap digit-argument] nil)) + +(defvar zero-input-pinyin--build-candidates-use-test-data nil + "If t, `zero-input-pinyin-build-candidates' will use `zero-input-pinyin-build-candidates-test'.") + +(defun zero-input-pinyin-build-candidates (preedit-str fetch-size) + "Synchronously build candidates list. + +PREEDIT-STR the preedit string. +FETCH-SIZE fetch at least this many candidates if possible." + (if zero-input-pinyin--build-candidates-use-test-data + (progn + (zero-input-pinyin-build-candidates-test preedit-str) + (setq zero-input-fetch-size (max fetch-size (length zero-input-candidates)))) + (zero-input-debug "zero-input-pinyin building candidate list synchronously\n") + (let ((result (zero-input-pinyin-service-get-candidates preedit-str fetch-size))) + (setq zero-input-fetch-size (max fetch-size (length (cl-first result)))) + (setq zero-input-pinyin-used-preedit-str-lengths (cl-second result)) + (setq zero-input-pinyin-candidates-pinyin-indices (cl-third result)) + (cl-first result)))) + +(defun zero-input-pinyin-build-candidates-async (preedit-str fetch-size complete-func) + "Asynchronously build candidate list, when done call complete-func on it. + +PREEDIT-STR the preedit string. +FETCH-SIZE fetch at least this many candidates if possible. +COMPLETE-FUNC the callback function when async call completes. it's called with + fetched candidates list as parameter." + (zero-input-debug "zero-input-pinyin building candidate list asynchronously\n") + (zero-input-pinyin-service-get-candidates-async + preedit-str + fetch-size + (lambda (candidates matched_preedit_str_lengths candidates_pinyin_indices) + (setq zero-input-pinyin-used-preedit-str-lengths matched_preedit_str_lengths) + (setq zero-input-pinyin-candidates-pinyin-indices candidates_pinyin_indices) + (setq zero-input-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-input-pinyin-can-start-sequence (ch) + "Return t if char CH can start a preedit sequence." + (and (>= ch ?a) + (<= ch ?z) + (not (= ch ?i)) + (not (= ch ?u)) + (not (= ch ?v)))) + +(defun zero-input-pinyin-pending-preedit-str-changed () + "Update zero states when pending preedit string changed." + (setq zero-input-fetch-size 0) + (setq zero-input-current-page 0) + (zero-input-pinyin-build-candidates-async zero-input-pinyin-pending-preedit-str zero-input-initial-fetch-size 'zero-input-build-candidates-complete)) + +(defun zero-input-pinyin-commit-nth-candidate (n) + "Commit Nth candidate and return true if it exists, otherwise, return false." + (let* ((n-prime (+ n (* zero-input-candidates-per-page zero-input-current-page))) + (candidate (nth n-prime zero-input-candidates)) + (used-len (when candidate + (nth n-prime zero-input-pinyin-used-preedit-str-lengths)))) + (when candidate + (zero-input-debug + "zero-input-pinyin-commit-nth-candidate\n n=%s candidate=%s used-len=%s zero-input-pinyin-pending-preedit-str=%S\n" + n candidate used-len zero-input-pinyin-pending-preedit-str) + (cond + ((null zero-input-pinyin-state) + (if (= used-len (length zero-input-preedit-str)) + (progn + (zero-input-debug "commit in full\n") + (zero-input-set-state zero-input--state-im-waiting-input) + (zero-input-commit-text candidate) + (zero-input-pinyin-service-commit-candidate-async + candidate + (nth n-prime zero-input-pinyin-candidates-pinyin-indices)) + t) + (zero-input-debug "partial commit, in partial commit mode now.\n") + (setq zero-input-pinyin-state zero-input-pinyin--state-im-partial-commit) + (setq zero-input-pinyin-pending-str candidate) + (setq zero-input-pinyin-pending-preedit-str (substring zero-input-preedit-str used-len)) + (setq zero-input-pinyin-pending-pinyin-indices + (nth n-prime zero-input-pinyin-candidates-pinyin-indices)) + (zero-input-pinyin-pending-preedit-str-changed) + t)) + ((eq zero-input-pinyin-state zero-input-pinyin--state-im-partial-commit) + (if (= used-len (length zero-input-pinyin-pending-preedit-str)) + (progn + (zero-input-debug "finishes partial commit\n") + (setq zero-input-pinyin-state nil) + (zero-input-set-state zero-input--state-im-waiting-input) + (zero-input-commit-text (concat zero-input-pinyin-pending-str candidate)) + (zero-input-pinyin-service-commit-candidate-async + (concat zero-input-pinyin-pending-str candidate) + (append zero-input-pinyin-pending-pinyin-indices + (nth n-prime zero-input-pinyin-candidates-pinyin-indices))) + t) + (zero-input-debug "continue partial commit\n") + (setq zero-input-pinyin-pending-str (concat zero-input-pinyin-pending-str candidate)) + (setq zero-input-pinyin-pending-preedit-str (substring zero-input-pinyin-pending-preedit-str used-len)) + (setq zero-input-pinyin-pending-pinyin-indices + (append zero-input-pinyin-pending-pinyin-indices + (nth n-prime zero-input-pinyin-candidates-pinyin-indices))) + (zero-input-pinyin-service-commit-candidate-async + zero-input-pinyin-pending-str + zero-input-pinyin-pending-pinyin-indices) + (zero-input-pinyin-pending-preedit-str-changed) + t)) + (t (error "Unexpected zero-input-pinyin-state: %s" zero-input-pinyin-state)))))) + +(defun zero-input-pinyin-commit-first-candidate-or-preedit-str () + "Commit first candidate if there is one, otherwise, commit preedit string." + (unless (zero-input-pinyin-commit-nth-candidate 0) + (zero-input-commit-preedit-str))) + +(defun zero-input-pinyin-commit-first-candidate-in-full () + "Commit first candidate and return t if it consumes all preedit-str. +Otherwise, just return nil." + (let ((candidate (nth 0 (zero-input-candidates-on-page zero-input-candidates))) + (used-len (nth (* zero-input-candidates-per-page zero-input-current-page) zero-input-pinyin-used-preedit-str-lengths))) + (when candidate + (cond + ((null zero-input-pinyin-state) + (when (= used-len (length zero-input-preedit-str)) + (zero-input-set-state zero-input--state-im-waiting-input) + (zero-input-commit-text candidate) + t)) + ((eq zero-input-pinyin-state zero-input-pinyin--state-im-partial-commit) + (when (= used-len (length zero-input-pinyin-pending-preedit-str)) + (setq zero-input-pinyin-state nil) + (zero-input-set-state zero-input--state-im-waiting-input) + (zero-input-commit-text (concat zero-input-pinyin-pending-str candidate)) + t)) + (t (error "Unexpected zero-input-pinyin-state: %s" zero-input-pinyin-state)))))) + +(defun zero-input-pinyin-page-down () + "Handle page down for zero-input-pinyin. + +This is different from zero-input-framework because I need to support partial commit" + (let ((len (length zero-input-candidates)) + (new-fetch-size (* zero-input-candidates-per-page (+ 2 zero-input-current-page)))) + (if (and (< len new-fetch-size) + (< zero-input-fetch-size new-fetch-size)) + (let ((preedit-str (if (eq zero-input-pinyin-state zero-input-pinyin--state-im-partial-commit) zero-input-pinyin-pending-preedit-str zero-input-preedit-str))) + (zero-input-pinyin-build-candidates-async + preedit-str + new-fetch-size + (lambda (candidates) + (zero-input-build-candidates-complete candidates) + (zero-input-just-page-down)))) + (zero-input-just-page-down)))) + +(defun zero-input-pinyin-handle-preedit-char (ch) + "Hanlde character insert in `*zero-input-state-im-preediting*' state. +Override `zero-input-handle-preedit-char-default'. + +CH the character user typed." + (cond + ((= ch ?\s) + (zero-input-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-input-pinyin-commit-nth-candidate (mod (- (- ch ?0) 1) 10)) + (zero-input-append-char-to-preedit-str ch) + (setq zero-input-pinyin-state nil))) + ((= ch zero-input-previous-page-key) + (zero-input-handle-preedit-char-default ch)) + ((= ch zero-input-next-page-key) + (zero-input-pinyin-page-down)) + (t (let ((str (zero-input-convert-punctuation ch))) + (if str + (when (zero-input-pinyin-commit-first-candidate-in-full) + (zero-input-set-state zero-input--state-im-waiting-input) + (insert str)) + (setq zero-input-pinyin-state nil) + (zero-input-append-char-to-preedit-str ch)))))) + +(defun zero-input-pinyin-get-preedit-str-for-panel () + "Return the preedit string that should show in panel." + (if (eq zero-input-pinyin-state zero-input-pinyin--state-im-partial-commit) + (concat zero-input-pinyin-pending-str zero-input-pinyin-pending-preedit-str) + zero-input-preedit-str)) + +(defun zero-input-pinyin-preedit-str-changed () + "Start over for candidate selection process." + (setq zero-input-pinyin-state nil) + (zero-input-preedit-str-changed)) + +(defun zero-input-pinyin-backspace () + "Handle backspace key in `*zero-input-state-im-preediting*' state." + (if (eq zero-input-pinyin-state zero-input-pinyin--state-im-partial-commit) + (zero-input-pinyin-preedit-str-changed) + (zero-input-backspace-default))) + +(defun zero-input-pinyin-delete-candidate (digit) + "Tell backend to delete candidate at DIGIT position. + +DIGIT is the digit key used to select nth candidate. +DIGIT 1 means delete 1st candidate. +DIGIT 2 means delete 2st candidate. +... +DIGIT 0 means delete 10th candidate." + (let ((candidate (nth (mod (- digit 1) 10) + (zero-input-candidates-on-page zero-input-candidates)))) + (when candidate + (zero-input-pinyin-service-delete-candidates-async + candidate 'zero-input-pinyin-preedit-str-changed)))) + +(defun zero-input-digit-argument () + "Allow C-<digit> to DeleteCandidate in `*zero-input-state-im-preediting*' state." + (interactive) + (unless (eq zero-input-state zero-input--state-im-preediting) + (error "`zero-input-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-input-pinyin-delete-candidate digit)))) + +;;=============================== +;; register IM to zero framework +;;=============================== + +(zero-input-register-im + 'pinyin + '((:build-candidates . zero-input-pinyin-build-candidates) + ;; comment to use sync version, uncomment to use async version. + ;; (:build-candidates-async . zero-input-pinyin-build-candidates-async) + (:can-start-sequence . zero-input-pinyin-can-start-sequence) + (:handle-preedit-char . zero-input-pinyin-handle-preedit-char) + (:get-preedit-str-for-panel . zero-input-pinyin-get-preedit-str-for-panel) + (:handle-backspace . zero-input-pinyin-backspace) + (:init . zero-input-pinyin-init) + (:shutdown . zero-input-pinyin-shutdown) + (:preedit-start . zero-input-pinyin-preedit-start) + (:preedit-end . zero-input-pinyin-preedit-end))) + +;;============ +;; public API +;;============ + +;;=========== +;; test data +;;=========== + +(defun zero-input-pinyin-build-candidates-test (preedit-str) + "Test data for testing partial commit. + +PREEDIT-STR the preedit string." + (cond + ((equal preedit-str "liyifeng") + (setq zero-input-pinyin-used-preedit-str-lengths '(8 4 4 4 2 2 2)) + '("李易峰" "利益" "礼仪" "离异" "里" "理" "力")) + ((equal preedit-str "feng") + (setq zero-input-pinyin-used-preedit-str-lengths '(4 4 4 4 4)) + '("风" "封" "疯" "丰" "凤")) + ((equal preedit-str "yifeng") + (setq zero-input-pinyin-used-preedit-str-lengths '(6 6 2 2 2 2)) + '("一封" "遗风" "艺" "依" "一" "以")) + (t nil))) + +(provide 'zero-input-pinyin) + + +(provide 'zero-input) + +;;; zero-input.el ends here diff --git a/zero-input.el.in b/zero-input.el.in new file mode 100644 index 0000000000000000000000000000000000000000..88484cb5703c0d4d55e8ab21c44c9464f6dbad21 --- /dev/null +++ b/zero-input.el.in @@ -0,0 +1,66 @@ +;;; zero-input.el --- Zero Chinese input method framework -*- lexical-binding: t -*- + +;; Licensed under the Apache License, Version 2.0 (the "License"); +;; you may not use this file except in compliance with the License. +;; You may obtain a copy of the License at +;; +;; http://www.apache.org/licenses/LICENSE-2.0 +;; +;; Unless required by applicable law or agreed to in writing, software +;; distributed under the License is distributed on an "AS IS" BASIS, +;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +;; See the License for the specific language governing permissions and +;; limitations under the License. + +;; Version: PKG_VERSION +;; URL: https://gitlab.emacsos.com/sylecn/zero-el +;; Package-Version: PKG_VERSION +;; Package-Requires: ((emacs "24.3") (s "1.2.0")) + +;;; Commentary: + +;; zero-input.el is auto-generated from multiple other files. see +;; zero-input.el.in and build.py for details. It's created because +;; package-lint doesn't support multi-file package yet (issue #111). +;; +;; zero-input is a Chinese input method framework for Emacs, implemented as an +;; Emacs minor mode. +;; +;; zero-input-pinyin is bundled with zero, to use pinyin input method, add to +;; ~/.emacs file: +;; +;; (require 'zero-input-pinyin) +;; (zero-input-set-default-im 'pinyin) +;; ;; Now you may bind a key to zero-input-toggle to make it easy to +;; ;; switch on/off the input method. +;; (global-set-key (kbd "<f5>") 'zero-input-toggle) +;; +;; zero-input supports Chinese punctuation mapping. There are three modes, +;; none, basic, and full. The default is basic mode, which only map most +;; essential punctuations. You can cycle zero-punctuation-level in current +;; buffer by C-c , , You can change default Chinese punctuation level: +;; +;; (setq-default zero-input-punctuation-level +;; *zero-input-punctuation-level-full*) +;; +;; zero-input supports full-width mode. You can toggle full-width mode in +;; current buffer by C-c , . You can enable full-width mode by default: +;; +;; (setq-default zero-input-full-width-mode t) +;; + +;;; Code: + +(require 'dbus) +(eval-when-compile (require 'cl-lib)) +(require 's) + +INCLUDE_ZERO_INPUT_PANEL_EL +INCLUDE_ZERO_INPUT_FRAMEWORK_EL +INCLUDE_ZERO_INPUT_TABLE_EL +INCLUDE_ZERO_INPUT_PINYIN_SERVICE_EL +INCLUDE_ZERO_INPUT_PINYIN_EL + +(provide 'zero-input) + +;;; zero-input.el ends here diff --git a/zero-pinyin-test.el b/zero-pinyin-test.el deleted file mode 100644 index 85ac701c85807fe71391d812815856afb83d4698..0000000000000000000000000000000000000000 --- a/zero-pinyin-test.el +++ /dev/null @@ -1,37 +0,0 @@ -;;; zero-pinyin-test.el --- tests for zero-pinyin.el - -;; Licensed under the Apache License, Version 2.0 (the "License"); -;; you may not use this file except in compliance with the License. -;; You may obtain a copy of the License at -;; -;; http://www.apache.org/licenses/LICENSE-2.0 -;; -;; Unless required by applicable law or agreed to in writing, software -;; distributed under the License is distributed on an "AS IS" BASIS, -;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -;; See the License for the specific language governing permissions and -;; limitations under the License. - -;;; Commentary: - -;; - -;;; Code: - -(require 'zero-pinyin) -(require 'ert) - -(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 ?i)) - (should-not (zero-pinyin-can-start-sequence ?u)) - (should-not (zero-pinyin-can-start-sequence ?v))) - -(provide 'zero-pinyin-test) - -;;; zero-pinyin-test.el ends here diff --git a/zero-pinyin.el b/zero-pinyin.el deleted file mode 100644 index 9f8594903af4b4db5157c829a9dcb0043bf23e31..0000000000000000000000000000000000000000 --- a/zero-pinyin.el +++ /dev/null @@ -1,357 +0,0 @@ -;;; zero-pinyin.el --- A pinyin input method for zero-framework -*- lexical-binding: t -*- - -;; Licensed under the Apache License, Version 2.0 (the "License"); -;; you may not use this file except in compliance with the License. -;; You may obtain a copy of the License at -;; -;; http://www.apache.org/licenses/LICENSE-2.0 -;; -;; Unless required by applicable law or agreed to in writing, software -;; distributed under the License is distributed on an "AS IS" BASIS, -;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -;; See the License for the specific language governing permissions and -;; limitations under the License. - -;;; Commentary: - -;; To use this input method, add in Emacs init file: -;; -;; (add-to-list 'load-path "~/fromsource/zero") ;; omit if install from melpa -;; (require 'zero-pinyin) -;; (zero-set-default-im 'pinyin) -;; ;; Now you may bind a key to zero-toggle to make it easy to -;; ;; switch on/off the input method. -;; (global-set-key (kbd "<f5>") 'zero-toggle) - -;;; Code: - -;;============== -;; dependencies -;;============== - -(require 'zero-framework) -(require 'zero-pinyin-service) - -;;=============================== -;; basic data and emacs facility -;;=============================== - -;; these two var is only used in docstring to avoid checkdoc line-too-long -;; error. -(defvar zero-pinyin-service-interface-xml-file - "/usr/share/dbus-1/interfaces/com.emacsos.zero.ZeroPinyinService1.ZeroPinyinServiceInterface.xml") -(defvar zero-pinyin-service-interface-xml-url - "https://gitlab.emacsos.com/sylecn/zero-pinyin-service/blob/master/com.emacsos.zero.ZeroPinyinService1.ZeroPinyinServiceInterface.xml") -(defcustom zero-pinyin-fuzzy-flag 0 - "Non-nil means use this value as GetCandidatesV2 fuzzy_flag param. -see zero-pinyin-service dbus interface xml for document. - -You can find the xml file locally at `zero-pinyin-service-interface-xml-file' -or online at `zero-pinyin-service-interface-xml-url'." - :type 'integer - :group 'zero-pinyin) - -(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-candidates-pinyin-indices nil - "Store GetCandidates dbus method candidates_pinyin_indices field.") -(defvar zero-pinyin-pending-str "") -(defvar zero-pinyin-pending-preedit-str "") -(defvar zero-pinyin-pending-pinyin-indices nil - "Stores `zero-pinyin-pending-str' corresponds pinyin indices.") - -;;===================== -;; key logic functions -;;===================== - -(defun zero-pinyin-reset () - "Reset states." - (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) - "Synchronously build candidates list. - -PREEDIT-STR the preedit string. -FETCH-SIZE fetch at least this many candidates if possible." - (if zero-pinyin--build-candidates-use-test-data - (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 (cl-first result)))) - (setq zero-pinyin-used-preedit-str-lengths (cl-second result)) - (setq zero-pinyin-candidates-pinyin-indices (cl-third result)) - (cl-first result)))) - -(defun zero-pinyin-build-candidates-async (preedit-str fetch-size complete-func) - "Asynchronously build candidate list, when done call complete-func on it. - -PREEDIT-STR the preedit string. -FETCH-SIZE fetch at least this many candidates if possible. -COMPLETE-FUNC the callback function when async call completes. it's called with - fetched candidates list as parameter." - (zero-debug "zero-pinyin building candidate list asynchronously\n") - (zero-pinyin-service-get-candidates-async - preedit-str - fetch-size - (lambda (candidates matched_preedit_str_lengths candidates_pinyin_indices) - (setq zero-pinyin-used-preedit-str-lengths matched_preedit_str_lengths) - (setq zero-pinyin-candidates-pinyin-indices candidates_pinyin_indices) - (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 ?i)) - (not (= ch ?u)) - (not (= ch ?v)))) - -(defun zero-pinyin-pending-preedit-str-changed () - "Update zero states when pending preedit string 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)) - -(defun zero-pinyin-commit-nth-candidate (n) - "Commit Nth candidate and return true if it exists, otherwise, return false." - (let* ((n-prime (+ n (* zero-candidates-per-page zero-current-page))) - (candidate (nth n-prime zero-candidates)) - (used-len (when candidate - (nth n-prime zero-pinyin-used-preedit-str-lengths)))) - (when candidate - (zero-debug - "zero-pinyin-commit-nth-candidate\n 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) - (zero-pinyin-service-commit-candidate-async - candidate - (nth n-prime zero-pinyin-candidates-pinyin-indices)) - 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)) - (setq zero-pinyin-pending-pinyin-indices - (nth n-prime zero-pinyin-candidates-pinyin-indices)) - (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)) - (zero-pinyin-service-commit-candidate-async - (concat zero-pinyin-pending-str candidate) - (append zero-pinyin-pending-pinyin-indices - (nth n-prime zero-pinyin-candidates-pinyin-indices))) - 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)) - (setq zero-pinyin-pending-pinyin-indices - (append zero-pinyin-pending-pinyin-indices - (nth n-prime zero-pinyin-candidates-pinyin-indices))) - (zero-pinyin-service-commit-candidate-async - zero-pinyin-pending-str - zero-pinyin-pending-pinyin-indices) - (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 () - "Commit first candidate if there is one, otherwise, commit preedit string." - (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 it 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. -Override `zero-handle-preedit-char-default'. - -CH the character user typed." - (cond - ((= ch ?\s) - (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))) - ((= ch zero-previous-page-key) - (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)) - (setq zero-pinyin-state nil) - (zero-append-char-to-preedit-str ch)))))) - -(defun zero-pinyin-get-preedit-str-for-panel () - "Return the preedit string that should show in 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) - (zero-backspace-default))) - -(defun zero-pinyin-delete-candidate (digit) - "Tell backend to delete candidate at DIGIT position. - -DIGIT is the digit key used to select nth candidate. -DIGIT 1 means delete 1st candidate. -DIGIT 2 means delete 2st candidate. -... -DIGIT 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. - -PREEDIT-STR the preedit string." - (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) - -;;; zero-pinyin.el ends here diff --git a/zero.el b/zero.el deleted file mode 100644 index f4ede4107c38e19d9b768c2399d690355b127d2f..0000000000000000000000000000000000000000 --- a/zero.el +++ /dev/null @@ -1,1510 +0,0 @@ -;;; zero.el --- Zero Chinese input method framework -*- lexical-binding: t -*- - -;; Licensed under the Apache License, Version 2.0 (the "License"); -;; you may not use this file except in compliance with the License. -;; You may obtain a copy of the License at -;; -;; http://www.apache.org/licenses/LICENSE-2.0 -;; -;; Unless required by applicable law or agreed to in writing, software -;; distributed under the License is distributed on an "AS IS" BASIS, -;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -;; See the License for the specific language governing permissions and -;; limitations under the License. - -;; Version: 1.3.4 -;; URL: https://gitlab.emacsos.com/sylecn/zero-el -;; Package-Version: 1.3.4 -;; Package-Requires: ((emacs "24.3") (s "1.2.0")) - -;;; Commentary: - -;; zero.el is auto-generated from multiple other files. see zero.el.in and -;; build.py for details. It's created because package-lint doesn't support -;; multi-file package yet (issue #111). -;; -;; zero is a Chinese input method framework for Emacs, implemented -;; as an Emacs minor mode. -;; -;; zero-pinyin is bundled with zero, to use pinyin input method, add to -;; ~/.emacs file: -;; -;; (require 'zero-pinyin) -;; (zero-set-default-im 'pinyin) -;; ;; Now you may bind a key to zero-toggle to make it easy to -;; ;; switch on/off the input method. -;; (global-set-key (kbd "<f5>") 'zero-toggle) -;; -;; zero supports Chinese punctuation mapping. There are three modes, none, -;; basic, and full. The default is basic mode, which only map most essential -;; punctuations. You can cycle zero-punctuation-level in current buffer by -;; C-c , , You can change default Chinese punctuation level: -;; -;; (setq-default zero-punctuation-level *zero-punctuation-level-full*) -;; -;; zero supports full-width mode. You can toggle full-width mode in current -;; buffer by C-c , . You can enable full-width mode by default: -;; -;; (setq-default zero-full-width-mode t) -;; - -;;; Code: - -(require 'dbus) -(eval-when-compile (require 'cl-lib)) -(require 's) - -;; body of zero-panel.el - -;;================ -;; implementation -;;================ - - -(defun zero-panel-error-handler (event error) - "Handle dbus errors. - -EVENT and ERROR are error-handler arguments." - (when (or (string-equal "com.emacsos.zero.Panel" - (dbus-event-interface-name event)) - (s-contains-p "com.emacsos.zero.Panel" (cadr error))) - (error "Zero-panel dbus failed: %S" (cadr error)))) - -(add-hook 'dbus-event-error-functions 'zero-panel-error-handler) - -(defun zero-panel-async-call (method _handler &rest args) - "Call METHOD on zero-panel service asynchronously. - -This is a wrapper around `dbus-call-method-asynchronously'. -ARGS optional extra args to pass to the wrapped function." - (apply 'dbus-call-method-asynchronously - :session - "com.emacsos.zero.Panel1" ; well known name - "/com/emacsos/zero/Panel1" ; object path - "com.emacsos.zero.Panel1.PanelInterface" ; interface name - method nil :timeout 500 args)) - -;;========================= -;; public utility function -;;========================= - -(defun zero-alist-to-asv (hints) - "Convert Lisp alist to dbus a{sv} data structure. - -HINTS should be an alist of form '((k1 [v1type] v1) (k2 [v2type] v2)). - -For example, -\(zero-alist-to-asv - '((\"name\" \"foo\") - (\"timeout\" :int32 10))) -=> -'(:array - (:dict-entry \"name\" (:variant \"foo\")) - (:dict-entry \"timeout\" (:variant :int32 10)))" - (if (null hints) - '(:array :signature "{sv}") - (let ((result '(:array))) - (dolist (item hints) - (push (list :dict-entry (car item) (cons :variant (cdr item))) result)) - (reverse result)))) - -;;============ -;; public API -;;============ - -(defun zero-panel-move (x y) - "Move panel to specific coordinate (X, Y). -Origin (0, 0) is at screen top left corner." - (zero-panel-async-call "Move" nil :int32 x :int32 y)) - -(defun zero-panel-show-candidates (preedit_str candidate_length candidates &optional hints) - "Show CANDIDATES. -Argument PREEDIT_STR the preedit string. -Argument CANDIDATE_LENGTH how many candidates are in candidates list." - (zero-panel-async-call "ShowCandidates" nil - :string preedit_str - :uint32 candidate_length - (or candidates '(:array)) - (zero-alist-to-asv hints))) - -(defun zero-panel-show () - "Show panel." - (zero-panel-async-call "Show" nil)) - -(defun zero-panel-hide () - "Hide panel." - (zero-panel-async-call "Hide" nil)) - -(defun zero-panel-quit () - "Quit panel application." - (interactive) - (zero-panel-async-call "Quit" nil)) - -(provide 'zero-panel) - -;; body of zero-framework.el - -;;============== -;; dependencies -;;============== - - -;;======= -;; utils -;;======= - -;; this function is from ibus.el -(defun zero--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). -Origin (0, 0) is at screen top left corner." - (cl-destructuring-bind (x y line-height) (zero--ibus-compute-pixel-position) - (cond - ((functionp 'window-absolute-pixel-position) - ;; introduced in emacs 26 - (cl-destructuring-bind (x . y) (window-absolute-pixel-position) - (list x (+ y line-height)))) - ((functionp 'frame-edges) - ;; introduced in emacs 25 - (cl-destructuring-bind (frame-x frame-y &rest rest) - (frame-edges nil 'inner-edges) - (list (+ frame-x x) (+ frame-y y line-height)))) - (t - ;; <= emacs 24, used guessed pixel size for tool-bar, menu-bar, WM title - ;; bar. Since I can't get that from elisp. - (list (+ (frame-parameter nil 'left) - (if (and (> (frame-parameter nil 'tool-bar-lines) 0) - (eq (frame-parameter nil 'tool-bar-position) 'left)) - 96 0) - x) - (+ (frame-parameter nil 'top) - (if (and (> (frame-parameter nil 'tool-bar-lines) 0) - (eq (frame-parameter nil 'tool-bar-position) 'top)) - 42 0) - (if (> (frame-parameter nil 'menu-bar-lines) 0) (+ 30 30) 0) - line-height - y)))))) - -(defun zero-cycle-list (lst item) - "Return the object next to given ITEM in LST. - -If item is the last object, return the first object in lst. -If item is not in lst, return nil." - (let ((r (member item lst))) - (cond - ((null r) nil) - (t (or (cadr r) - (car lst)))))) - -;;===================== -;; key logic functions -;;===================== - -;; zero-el version -(defvar zero-version nil "Zero package version.") -(setq zero-version "1.3.4") - -;; 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) - -(defconst zero-punctuation-level-basic 'BASIC) -(defconst zero-punctuation-level-full 'FULL) -(defconst zero-punctuation-level-none 'NONE) - -(defvar zero-im nil - "Stores 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-full-width-mode nil - "Set to t to enable full-width mode. -In full-width mode, commit ascii char will insert full-width char if there is a -corresponding full-width char. This full-width char map is -independent from punctuation map. You can change this via -`zero-toggle-full-width-mode'") -(defvar zero-punctuation-level zero-punctuation-level-basic - "Punctuation level. - -Should be one of -`zero-punctuation-level-basic' -`zero-punctuation-level-full' -`zero-punctuation-level-none'") -(defvar zero-punctuation-levels (list zero-punctuation-level-basic - zero-punctuation-level-full - zero-punctuation-level-none) - "Punctuation levels to use when `zero-cycle-punctuation-level'.") -(defvar zero-double-quote-flag nil - "Non-nil means next double quote insert close quote. - -Used when converting double quote to Chinese quote. -If nil, next double quote insert open quote. -Otherwise, next double quote insert close quote.") -(defvar zero-single-quote-flag nil - "Non-nil means next single quote insert close quote. - -Used when converting single quote to Chinese quote. -If nil, next single quote insert open quote. -Otherwise, next single quote insert close quote.") -(defvar zero-preedit-str "") -(defvar zero-candidates nil) -(defcustom zero-candidates-per-page 10 - "How many candidates to show on each page." - :group 'zero - :type 'integer) -(defvar zero-current-page 0 "Current page number. count from 0.") -(defvar zero-initial-fetch-size 20 - "How many candidates to fetch for the first call to GetCandidates.") -;; zero-fetch-size is reset to 0 when preedit-str changes. -;; zero-fetch-size is set to fetch-size in build-candidates-async complete-func -;; lambda. -(defvar zero-fetch-size 0 "Last GetCandidates call's fetch-size.") -(defvar zero-previous-page-key ?\- "Previous page key.") -(defvar zero-next-page-key ?\= "Next page key.") - -;;; concrete input method should define these functions and set them in the -;;; corresponding *-func variable. -(defun zero-build-candidates-default (_preedit-str _fetch-size) - "Default implementation for `zero-build-candidates-func'." - nil) -(defun zero-can-start-sequence-default (_ch) - "Default implementation for `zero-can-start-sequence-func'." - nil) -(defun zero-get-preedit-str-for-panel-default () - "Default implementation for `zero-get-preedit-str-for-panel-func'." - zero-preedit-str) -(defvar zero-build-candidates-func 'zero-build-candidates-default - "Contains a function to build candidates from preedit-str. The function accepts param preedit-str, fetch-size, returns candidate list.") -(defvar zero-build-candidates-async-func 'zero-build-candidates-async-default - "Contains a function to build candidates from preedit-str. The function accepts param preedit-str, fetch-size, and a complete-func that should be called on returned candidate list.") -(defvar zero-can-start-sequence-func 'zero-can-start-sequence-default - "Contains a function to decide whether a char can start a preedit sequence.") -(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-handle-preedit-char-func 'zero-handle-preedit-char-default - "Hanlde character insert in `zero--state-im-preediting' mode.") -(defvar zero-preedit-start-func 'nil - "Called when enter `zero--state-im-preediting' state.") -(defvar zero-preedit-end-func 'nil - "Called when leave `zero--state-im-preediting' state.") - -(defvar zero-enable-debug nil - "Whether to enable debug. -if t, `zero-debug' will output debug msg in *zero-debug* buffer") -(defvar zero-debug-buffer-max-size 30000 - "Max characters in *zero-debug* buffer. If reached, first half data will be deleted.") - -(defun zero-debug (string &rest objects) - "Log debug message in *zero-debug* buffer. - -STRING and OBJECTS are passed to `format'" - (if zero-enable-debug - (with-current-buffer (get-buffer-create "*zero-debug*") - (goto-char (point-max)) - (insert (apply 'format string objects)) - (when (> (point) zero-debug-buffer-max-size) - (insert "removing old data\n") - (delete-region (point-min) (/ zero-debug-buffer-max-size 2)))))) - -;; (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-enter-preedit-state () - "Config keymap when enter preedit state." - (zero-enable-preediting-map) - (if (functionp zero-preedit-start-func) - (funcall zero-preedit-start-func))) - -(defun zero-leave-preedit-state () - "Config keymap when leave preedit state." - (zero-disable-preediting-map) - (if (functionp zero-preedit-end-func) - (funcall zero-preedit-end-func))) - -(defun zero-set-state (state) - "Set zero state to given STATE." - (zero-debug "set state to %s\n" state) - (setq zero-state state) - (if (eq state zero--state-im-preediting) - (zero-enter-preedit-state) - (zero-leave-preedit-state))) - -(defun zero-candidates-on-page (candidates) - "Return candidates on current page for given CANDIDATES list." - (cl-flet ((take (n lst) - "take the first n element from lst. if there is not -enough elements, return lst as it is." - (cl-loop - for lst* = lst then (cdr lst*) - for n* = n then (1- n*) - until (or (zerop n*) (null lst*)) - collect (car lst*))) - (drop (n lst) - "drop the first n elements from lst" - (cl-loop - for lst* = lst then (cdr lst*) - for n* = n then (1- n*) - until (or (zerop n*) (null lst*)) - finally (return lst*)))) - (take zero-candidates-per-page - (drop (* zero-candidates-per-page zero-current-page) candidates)))) - -(defun zero-show-candidates (&optional candidates) - "Show CANDIDATES using zero-panel via IPC/RPC." - (let ((candidates-on-page (zero-candidates-on-page (or candidates - zero-candidates)))) - (cl-destructuring-bind (x y) (zero-get-point-position) - (zero-panel-show-candidates - (funcall zero-get-preedit-str-for-panel-func) - (length candidates-on-page) - candidates-on-page - `(("in_emacs" t) - ("filename" ,(or (buffer-file-name) "")) - ("page_number" ,(1+ zero-current-page)) - ("has_next_page" ,(or (> (length (or candidates zero-candidates)) (* zero-candidates-per-page (1+ zero-current-page))) (< zero-fetch-size (* zero-candidates-per-page (+ 2 zero-current-page))))) - ("has_previous_page" ,(> zero-current-page 0)) - ("move_x" :int32 ,x) - ("move_y" :int32 ,y))) - (zero-debug "candidates: %s\n" (s-join ", " candidates-on-page))))) - -(defun zero-build-candidates (preedit-str fetch-size) - "Build candidates list synchronously. - -Try to find at least FETCH-SIZE number of candidates for PREEDIT-STR." - ;; (zero-debug "zero-build-candidates\n") - (unless (functionp zero-build-candidates-func) - (signal 'wrong-type-argument (list 'functionp zero-build-candidates-func))) - (prog1 (funcall zero-build-candidates-func preedit-str fetch-size) - (setq zero-fetch-size (max fetch-size (length zero-candidates))))) - -(defun zero-build-candidates-complete (candidates) - "Called when `zero-build-candidates-async' return. - -CANDIDATES is returned candidates list from async call." - (setq zero-candidates candidates) - (zero-show-candidates candidates)) - -(defun zero-build-candidates-async-default (preedit-str fetch-size complete-func) - "Build candidate list, when done show it via `zero-show-candidates'. - -PREEDIT-STR the preedit-str. -FETCH-SIZE try to find at least this many candidates for preedit-str. -COMPLETE-FUNC the function to call when build candidates completes." - ;; (zero-debug "zero-build-candidates-async-default\n") - (let ((candidates (zero-build-candidates preedit-str fetch-size))) - ;; update cache to make SPC and digit key selection possible. - (funcall complete-func candidates))) - -(defvar zero-full-width-char-map - ;; ascii 33 to 126 map to - ;; unicode FF01 to FF5E - (cl-loop - for i from 33 to 126 - collect (cons (make-char 'ascii i) (make-char 'unicode 0 255 (- i 32)))) - "An alist that map half-width char to full-width char.") - -(defun zero-convert-ch-to-full-width (ch) - "Convert half-width char CH to full-width. - -If there is no full-width char for CH, return it unchanged." - (let ((pair (assoc ch zero-full-width-char-map))) - (if pair (cdr pair) ch))) - -(defun zero-convert-str-to-full-width (s) - "Convert each char in S to their full-width char if there is one." - (concat (mapcar 'zero-convert-ch-to-full-width s))) - -(defun zero-convert-str-to-full-width-maybe (s) - "If in `zero-full-width-mode', convert char in S to their full-width char; otherwise, return s unchanged." - (if zero-full-width-mode (zero-convert-str-to-full-width s) s)) - -(defun zero-insert-full-width-char (ch) - "If in `zero-full-width-mode', insert full-width char for given CH and return true, otherwise just return nil." - (when zero-full-width-mode - (let ((full-width-ch (zero-convert-ch-to-full-width ch))) - (insert full-width-ch) - full-width-ch))) - -(defun zero-convert-punctuation-basic (ch) - "Convert punctuation for `zero-punctuation-level-basic'. - -Return CH's Chinese punctuation if CH is converted. Return nil otherwise." - (cl-case ch - (?, ",") - (?. "。") ; 0x3002 - (?? "?") - (?! "!") - (?\\ "、") ; 0x3001 - (?: ":") - (otherwise nil))) - -(defun zero-convert-punctuation-full (ch) - "Convert punctuation for `zero-punctuation-level-full'. - -Return CH's Chinese punctuation if CH is converted. Return nil otherwise" - (cl-case ch - (?_ "——") - (?< "《") ;0x300A - (?> "》") ;0x300B - (?\( "(") - (?\) ")") - (?\[ "【") ;0x3010 - (?\] "】") ;0x3011 - (?^ "……") - (?\" (setq zero-double-quote-flag (not zero-double-quote-flag)) - (if zero-double-quote-flag "“" "”")) - (?\' (setq zero-single-quote-flag (not zero-single-quote-flag)) - (if zero-single-quote-flag "‘" "’")) - (?~ "~") - (?\; ";") - (?$ "¥") - (t (zero-convert-punctuation-basic ch)))) - -(defun zero-convert-punctuation (ch) - "Convert punctuation based on `zero-punctuation-level'. -Return CH's Chinese punctuation if CH is converted. Return nil otherwise." - (cond - ((eq zero-punctuation-level zero-punctuation-level-basic) - (zero-convert-punctuation-basic ch)) - ((eq zero-punctuation-level zero-punctuation-level-full) - (zero-convert-punctuation-full ch)) - (t nil))) - -(defun zero-handle-punctuation (ch) - "If CH is a punctuation character, insert mapped Chinese punctuation and return true; otherwise, return false." - (let ((str (zero-convert-punctuation ch))) - (when str - (insert str) - t))) - -(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"))) - -(defun zero-page-up () - "If not at first page, show candidates on previous page." - (interactive) - (when (> zero-current-page 0) - (setq zero-current-page (1- zero-current-page)) - (zero-show-candidates))) - -(defun zero-just-page-down () - "Just page down using existing candidates." - (let ((len (length zero-candidates))) - (when (> len (* zero-candidates-per-page (1+ zero-current-page))) - (setq zero-current-page (1+ zero-current-page)) - (zero-show-candidates)))) - -(defun zero-page-down () - "If there is still candidates to be displayed, show candidates on next page." - (interactive) - (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)) - (funcall zero-build-candidates-async-func - zero-preedit-str - new-fetch-size - (lambda (candidates) - (zero-build-candidates-complete candidates) - (setq zero-fetch-size (max new-fetch-size - (length candidates))) - (zero-just-page-down))) - (zero-just-page-down)))) - -(defun zero-handle-preedit-char-default (ch) - "Hanlde character insert in `zero--state-im-preediting' state. - -CH is the char user has typed." - (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)))))) - -(defun zero-self-insert-command (n) - "Handle character `self-insert-command'. This includes characters and digits. - -N is the argument passed to `self-insert-command'." - (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) - (unless (zero-insert-full-width-char ch) - (self-insert-command n))))) - ((eq zero-state zero--state-im-preediting) - (zero-debug "still preediting\n") - (funcall zero-handle-preedit-char-func 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." - (setq zero-fetch-size 0) - (setq zero-current-page 0) - (funcall zero-build-candidates-async-func zero-preedit-str zero-initial-fetch-size 'zero-build-candidates-complete)) - -(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-backspace () - "Handle backspace key in `zero--state-im-preediting' state." - (interactive) - (unless (eq zero-state zero--state-im-preediting) - (error "Error: zero-backspace called in non preediting state")) - (zero-debug "zero-backspace\n") - (funcall zero-backspace-func)) - -(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) - (setq zero-current-page 0) - (zero-hide-candidate-list)) - -(defun zero-return () - "Handle RET key press in `zero--state-im-preediting' state." - (interactive) - (unless (eq zero-state zero--state-im-preediting) - (error "Error: zero-return called in non preediting state")) - (zero-debug "zero-return\n") - (zero-set-state zero--state-im-waiting-input) - (zero-commit-text (zero-convert-str-to-full-width-maybe zero-preedit-str))) - -(defun zero-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)))) - (if candidate - (progn - (zero-set-state zero--state-im-waiting-input) - (zero-commit-text candidate) - t) - nil))) - -(defun zero-commit-preedit-str () - "Commit current preedit-str." - (zero-set-state zero--state-im-waiting-input) - (zero-commit-text (zero-convert-str-to-full-width-maybe zero-preedit-str))) - -(defun zero-commit-first-candidate-or-preedit-str () - "Commit first candidate if there is one, otherwise commit preedit str." - (unless (zero-commit-nth-candidate 0) - (zero-commit-preedit-str))) - -(defun zero-hide-candidate-list () - "Hide candidate list." - (zero-panel-hide) - (zero-debug "hide candidate list\n")) - -(defun zero-reset () - "Reset zero states." - (interactive) - (zero-debug "zero-reset\n") - (zero-set-state zero--state-im-waiting-input) - (setq zero-preedit-str "") - (setq zero-candidates nil) - (setq zero-current-page 0) - (zero-hide-candidate-list)) - -(defun zero-focus-in () - "A hook function, run when focus in a buffer." - (when (eq zero-state zero--state-im-preediting) - (zero-show-candidates zero-candidates) - (zero-enter-preedit-state))) - -(defun zero-focus-out () - "A hook function, run when focus out a buffer." - (when (eq zero-state zero--state-im-preediting) - (zero-hide-candidate-list) - (zero-leave-preedit-state))) - -(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 -;;============ - -(defvar zero-mode-map - (let ((map (make-sparse-keymap))) - ;; build zero-prefix-map - (defvar zero-prefix-map (define-prefix-command 'zero-prefix-map)) - (let ((bindings '(("," zero-cycle-punctuation-level) - ("." zero-toggle-full-width-mode)))) - (dolist (b bindings) - (define-key zero-prefix-map (car b) (cadr b)))) - ;; mount zero-prefix-map in C-c , prefix key. - (define-key map (kbd "C-c ,") zero-prefix-map) - - ;; other keybindings - (define-key map [remap self-insert-command] - 'zero-self-insert-command) - map) - "Keymap for `zero-mode'.") - -(defun zero-enable-preediting-map () - "Enable preediting keymap in `zero-mode-map'." - (zero-debug "zero-enable-preediting-map\n") - (define-key zero-mode-map (kbd "<backspace>") 'zero-backspace) - (define-key zero-mode-map (kbd "RET") 'zero-return) - (define-key zero-mode-map (kbd "<escape>") 'zero-reset)) - -(defun zero-disable-preediting-map () - "Disable preediting keymap in `zero-mode-map'." - (zero-debug "zero-disable-preediting-map\n") - (define-key zero-mode-map (kbd "<backspace>") nil) - (define-key zero-mode-map (kbd "RET") nil) - (define-key zero-mode-map (kbd "<escape>") nil)) - -(defun zero-modeline-string () - "Build `zero-mode' modeline string aka lighter. - -If full-width mode is enabled, show ZeroF; -Otherwise, show Zero." - (if zero-full-width-mode " ZeroF" " Zero")) - -(define-minor-mode zero-mode - "a Chinese input method framework written as an emacs minor mode. - -\\{zero-mode-map}" - nil - (:eval (zero-modeline-string)) - zero-mode-map - ;; local variables and variable init - (make-local-variable 'zero-state) - (zero-set-state zero--state-im-off) - (make-local-variable 'zero-punctuation-level) - (make-local-variable 'zero-full-width-mode) - (make-local-variable 'zero-double-quote-flag) - (make-local-variable 'zero-single-quote-flag) - (set (make-local-variable 'zero-preedit-str) "") - (set (make-local-variable 'zero-candidates) nil) - (make-local-variable 'zero-candidates-per-page) - (make-local-variable 'zero-current-page) - (make-local-variable 'zero-fetch-size) - (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) - ;; hooks - (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 - '((:virtual-function-name . implementation-function-name)) - -virtual functions corresponding variable -=========================================================================== -:build-candidates `zero-build-candidates-func' -:can-start-sequence `zero-can-start-sequence-func' -:handle-preedit-char `zero-handle-preedit-char-func' -:get-preedit-str-for-panel `zero-get-preedit-str-for-panel-func' -:handle-backspace `zero-backspace-func' -:init nil -:shutdown nil -:preedit-start `zero-preedit-start-func' -:preedit-end `zero-preedit-end-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-toggle-full-width-mode () - "Toggle `zero-full-width-mode' on/off." - (interactive) - (setq zero-full-width-mode (not zero-full-width-mode)) - (message (if zero-full-width-mode - "Enabled full-width mode" - "Enabled half-width mode"))) - -(defun zero-set-punctuation-level (level) - "Set `zero-punctuation-level'. - -LEVEL the level to set to." - (interactive) - (if (not (member level (list zero-punctuation-level-basic - zero-punctuation-level-full - zero-punctuation-level-none))) - (error "Level not supported: %s" level) - (setq zero-punctuation-level level))) - -(defun zero-set-punctuation-levels (levels) - "Set `zero-punctuation-levels'. - -`zero-cycle-punctuation-level' will cycle current -`zero-punctuation-level' among defined LEVELS." - (dolist (level levels) - (if (not (member level (list zero-punctuation-level-basic - zero-punctuation-level-full - zero-punctuation-level-none))) - (error "Level not supported: %s" level))) - (setq zero-punctuation-levels levels)) - -(defun zero-cycle-punctuation-level () - "Cycle `zero-punctuation-level' among `zero-punctuation-levels'." - (interactive) - (setq zero-punctuation-level - (zero-cycle-list zero-punctuation-levels zero-punctuation-level)) - (message "punctuation level set to %s" zero-punctuation-level)) - -;;;###autoload -(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 auto completion for im-name - (interactive "SSet input method to: ") - ;; when switch away from an IM, run last IM's :shutdown function. - (if zero-im - (let ((shutdown-func (cdr (assq :shutdown (cdr (assq zero-im zero-ims)))))) - (if (functionp shutdown-func) - (funcall shutdown-func)))) - (if im-name - (let ((im-functions (cdr (assq im-name zero-ims)))) - (if im-functions - (progn - ;; TODO create a macro to reduce code duplication and human - ;; error. - ;; - ;; TODO do some functionp check for the slot functions. if check - ;; fail, keep (or revert to) the old IM. - (setq zero-build-candidates-func - (or (cdr (assq :build-candidates im-functions)) - 'zero-build-candidates-default)) - (setq zero-build-candidates-async-func - (or (cdr (assq :build-candidates-async im-functions)) - 'zero-build-candidates-async-default)) - (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)) - (setq zero-preedit-start-func - (cdr (assq :preedit-start im-functions))) - (setq zero-preedit-end-func - (cdr (assq :preedit-end im-functions))) - (unless (functionp zero-backspace-func) - (signal 'wrong-type-argument - (list 'functionp zero-backspace-func))) - ;; when switch to a IM, run its :init function - (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-build-candidates-async-func 'zero-build-candidates-async-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) - (setq zero-preedit-start-func nil) - (setq zero-preedit-end-func nil))) - -;;;###autoload -(defun zero-set-default-im (im-name) - "Set given IM-NAME as default zero input method." - (unless (symbolp im-name) - (signal 'wrong-type-argument (list 'symbolp im-name))) - (setq-default zero-im im-name)) - -;;;###autoload -(defun zero-on () - "Turn on `zero-mode'." - (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 () - "Turn off `zero-mode'." - (interactive) - (zero-debug "zero-off\n") - (zero-mode -1) - (zero-reset) - (zero-set-state zero--state-im-off)) - -;;;###autoload -(defun zero-toggle () - "Toggle `zero-mode'." - (interactive) - (if zero-mode - (zero-off) - (zero-on))) - -(provide 'zero-framework) - -;; body of zero-table.el - -;;============== -;; dependencies -;;============== - - -;;=============================== -;; basic data and emacs facility -;;=============================== - -(defvar zero-table-table nil - "The table used by zero-table input method, map string to string.") -(defvar zero-table-sequence-initials nil "Used in `zero-table-can-start-sequence'.") - -;;===================== -;; key logic functions -;;===================== - -(defun zero-table-sort-key (lhs rhs) - "A predicate function to sort candidates. Return t if LHS should sort before RHS." - (string< (car lhs) (car rhs))) - -(defun zero-table-build-candidates (preedit-str &optional _fetch-size) - "Build candidates by looking up PREEDIT-STR in `zero-table-table'." - (mapcar 'cdr (sort (cl-remove-if-not (lambda (pair) (string-prefix-p preedit-str (car pair))) zero-table-table) 'zero-table-sort-key))) - -;; (defun zero-table-build-candidates-async (preedit-str) -;; "build candidate list, when done show it via `zero-table-show-candidates'" -;; (zero-table-debug "building candidate list\n") -;; (let ((candidates (zero-table-build-candidates preedit-str))) -;; ;; update cache to make SPC and digit key selection possible. -;; (setq zero-table-candidates candidates) -;; (zero-table-show-candidates candidates))) - -(defun zero-table-can-start-sequence (ch) - "Return t if char CH can start a preedit sequence." - (member (make-string 1 ch) zero-table-sequence-initials)) - -;;=============================== -;; register IM to zero framework -;;=============================== - -(zero-register-im - 'zero-table - '((:build-candidates . zero-table-build-candidates) - (:can-start-sequence . zero-table-can-start-sequence))) - -;;============ -;; public API -;;============ - -(defun zero-table-set-table (alist) - "Set the conversion table. - -the ALIST should be a list of (key . value) pairs. when user type -\(part of) key, the IM will show all matching value. - -e.g. -'((\"phone\" . \"18612345678\") - (\"mail\" . \"foo@example.com\") - (\"map\" . \"https://ditu.amap.com/\") - (\"m\" . \"https://msdn.microsoft.com/en-us\") - (\"address\" . \"123 Happy Street\"))" - (setq zero-table-table alist) - (setq zero-table-sequence-initials - (delete-dups (mapcar (lambda (pair) (substring (car pair) 0 1)) - zero-table-table)))) - -;;=========== -;; test data -;;=========== - -(unless zero-table-table - (zero-table-set-table - '(("phone" . "18612345678") - ("pyl" . "http://localdocs.emacsos.com/python2/library/%s.html") - ("pyli" . "http://localdocs.emacsos.com/python2/index.html") - ("pylm" . "http://localdocs.emacsos.com/python2/py-modindex.html") - ("py3li" . "http://localdocs.emacsos.com/python2/index.html") - ("py3l" . "http://localdocs.emacsos.com/python3/library/%s.html") - ("py3lm" . "http://localdocs.emacsos.com/python3/py-modindex.html") - ("pyop" . "http://docs.python.org/library/operator.html") - ("pyopl" . "http://localdocs.emacsos.com/python2/library/operator.html") - ("pympl" . "http://localdocs.emacsos.com/python2/library/multiprocessing.html") - ("py2" . "http://docs.python.org/2/library/%s.html") - ("py3" . "http://docs.python.org/3/library/%s.html") - ("py2i" . "http://docs.python.org/2/") - ("py2m" . "http://docs.python.org/2/py-modindex.html") - ("py3i" . "http://docs.python.org/3/") - ("py3m" . "http://docs.python.org/3/py-modindex.html") - ("pycodec" . "http://localdocs.emacsos.com/python2/library/codecs.html#standard-encodings") - ("pycodecs" . "http://localdocs.emacsos.com/python2/library/codecs.html#standard-encodings") - ("pycodecsr" . "http://docs.python.org/library/codecs.html#standard-encodings") - ("pycodecr" . "http://docs.python.org/library/codecs.html#standard-encodings") - ("pep328" . "http://www.python.org/dev/peps/pep-0328/") - ("mail" . "foo@example.com") - ("map" . "https://ditu.amap.com/") - ("m" . "https://msdn.microsoft.com/en-us") - ("address" . "123 Happy Street") - ("da" . "__da__") - ("now" . "__now__")))) - -(provide 'zero-table) - -;; body of zero-pinyin-service.el - -;;================ -;; implementation -;;================ - - -(defvar zero-pinyin-service-service-name - "com.emacsos.zero.ZeroPinyinService1") -(defvar zero-pinyin-service-path - "/com/emacsos/zero/ZeroPinyinService1") -(defvar zero-pinyin-service-interface - "com.emacsos.zero.ZeroPinyinService1.ZeroPinyinServiceInterface") -(defvar zero-pinyin-fuzzy-flag 0) - -(defun zero-pinyin-service-error-handler (event error) - "Handle dbus errors. - -EVENT, ERROR are arguments passed to the handler." - (when (or (string-equal zero-pinyin-service-service-name - (dbus-event-interface-name event)) - (s-contains-p zero-pinyin-service-service-name (cadr error))) - (error "`zero-pinyin-service' dbus failed: %S" (cadr error)))) - -(add-hook 'dbus-event-error-functions 'zero-pinyin-service-error-handler) - -(defun zero-pinyin-service-async-call (method handler &rest args) - "Call METHOD on zero-pinin-service asynchronously. -This is a wrapper around `dbus-call-method-asynchronously'. -Argument HANDLER the handler function. -Optional argument ARGS extra arguments to pass to the wrapped function." - (apply 'dbus-call-method-asynchronously - :session zero-pinyin-service-service-name - zero-pinyin-service-path - zero-pinyin-service-interface - method handler :timeout 1000 args)) - -(defun zero-pinyin-service-call (method &rest args) - "Call METHOD on zero-pinin-service synchronously. -This is a wrapper around `dbus-call-method'. -Optional argument ARGS extra arguments to pass to the wrapped function." - (apply 'dbus-call-method - :session zero-pinyin-service-service-name - zero-pinyin-service-path - zero-pinyin-service-interface - method :timeout 1000 args)) - -;;============ -;; public API -;;============ - -(defun zero-pinyin-service-get-candidates (preedit-str fetch-size) - "Get candidates for pinyin in PREEDIT-STR synchronously. - -preedit-str the preedit-str, should be pure pinyin string -FETCH-SIZE try to fetch this many candidates or more" - (zero-pinyin-service-call "GetCandidatesV2" :string preedit-str :uint32 fetch-size :uint32 zero-pinyin-fuzzy-flag)) - -(defun zero-pinyin-service-get-candidates-async (preedit-str fetch-size get-candidates-complete) - "Get candidates for pinyin in PREEDIT-STR asynchronously. - -PREEDIT-STR the preedit string, should be pure pinyin string. -FETCH-SIZE try to fetch this many candidates or more. -GET-CANDIDATES-COMPLETE the async handler function." - (zero-pinyin-service-async-call - "GetCandidatesV2" get-candidates-complete :string preedit-str :uint32 fetch-size :uint32 zero-pinyin-fuzzy-flag)) - -(defun zero-pinyin-candidate-pinyin-indices-to-dbus-format (candidate_pinyin_indices) - "Convert CANDIDATE_PINYIN_INDICES to Emacs dbus format." - (let (result) - (push :array result) - ;; (push :signature result) - ;; (push "(ii)" result) - (dolist (pypair candidate_pinyin_indices) - (push (list :struct :int32 (cl-first pypair) :int32 (cl-second pypair)) - result)) - (reverse result))) - -(defun zero-pinyin-service-commit-candidate-async (candidate candidate_pinyin_indices) - "Commit candidate asynchronously. - -CANDIDATE the candidate user selected. -CANDIDATE_PINYIN_INDICES the candidate's pinyin shengmu and yunmu index." - ;; don't care about the result, so no callback. - (zero-pinyin-service-async-call - "CommitCandidate" nil - :string candidate - (zero-pinyin-candidate-pinyin-indices-to-dbus-format candidate_pinyin_indices))) - -(defun zero-pinyin-service-delete-candidates-async (candidate delete-candidate-complete) - "Delete CANDIDATE asynchronously. - -DELETE-CANDIDATE-COMPLETE the async handler function." - (zero-pinyin-service-async-call - "DeleteCandidate" delete-candidate-complete :string candidate)) - -(defun zero-pinyin-service-quit () - "Quit panel application." - (zero-pinyin-service-async-call "Quit" nil)) - -(provide 'zero-pinyin-service) - -;; body of zero-pinyin.el - -;;============== -;; dependencies -;;============== - - -;;=============================== -;; basic data and emacs facility -;;=============================== - -;; these two var is only used in docstring to avoid checkdoc line-too-long -;; error. -(defvar zero-pinyin-service-interface-xml-file - "/usr/share/dbus-1/interfaces/com.emacsos.zero.ZeroPinyinService1.ZeroPinyinServiceInterface.xml") -(defvar zero-pinyin-service-interface-xml-url - "https://gitlab.emacsos.com/sylecn/zero-pinyin-service/blob/master/com.emacsos.zero.ZeroPinyinService1.ZeroPinyinServiceInterface.xml") -(defcustom zero-pinyin-fuzzy-flag 0 - "Non-nil means use this value as GetCandidatesV2 fuzzy_flag param. -see zero-pinyin-service dbus interface xml for document. - -You can find the xml file locally at `zero-pinyin-service-interface-xml-file' -or online at `zero-pinyin-service-interface-xml-url'." - :type 'integer - :group 'zero-pinyin) - -(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-candidates-pinyin-indices nil - "Store GetCandidates dbus method candidates_pinyin_indices field.") -(defvar zero-pinyin-pending-str "") -(defvar zero-pinyin-pending-preedit-str "") -(defvar zero-pinyin-pending-pinyin-indices nil - "Stores `zero-pinyin-pending-str' corresponds pinyin indices.") - -;;===================== -;; key logic functions -;;===================== - -(defun zero-pinyin-reset () - "Reset states." - (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) - "Synchronously build candidates list. - -PREEDIT-STR the preedit string. -FETCH-SIZE fetch at least this many candidates if possible." - (if zero-pinyin--build-candidates-use-test-data - (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 (cl-first result)))) - (setq zero-pinyin-used-preedit-str-lengths (cl-second result)) - (setq zero-pinyin-candidates-pinyin-indices (cl-third result)) - (cl-first result)))) - -(defun zero-pinyin-build-candidates-async (preedit-str fetch-size complete-func) - "Asynchronously build candidate list, when done call complete-func on it. - -PREEDIT-STR the preedit string. -FETCH-SIZE fetch at least this many candidates if possible. -COMPLETE-FUNC the callback function when async call completes. it's called with - fetched candidates list as parameter." - (zero-debug "zero-pinyin building candidate list asynchronously\n") - (zero-pinyin-service-get-candidates-async - preedit-str - fetch-size - (lambda (candidates matched_preedit_str_lengths candidates_pinyin_indices) - (setq zero-pinyin-used-preedit-str-lengths matched_preedit_str_lengths) - (setq zero-pinyin-candidates-pinyin-indices candidates_pinyin_indices) - (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 ?i)) - (not (= ch ?u)) - (not (= ch ?v)))) - -(defun zero-pinyin-pending-preedit-str-changed () - "Update zero states when pending preedit string 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)) - -(defun zero-pinyin-commit-nth-candidate (n) - "Commit Nth candidate and return true if it exists, otherwise, return false." - (let* ((n-prime (+ n (* zero-candidates-per-page zero-current-page))) - (candidate (nth n-prime zero-candidates)) - (used-len (when candidate - (nth n-prime zero-pinyin-used-preedit-str-lengths)))) - (when candidate - (zero-debug - "zero-pinyin-commit-nth-candidate\n 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) - (zero-pinyin-service-commit-candidate-async - candidate - (nth n-prime zero-pinyin-candidates-pinyin-indices)) - 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)) - (setq zero-pinyin-pending-pinyin-indices - (nth n-prime zero-pinyin-candidates-pinyin-indices)) - (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)) - (zero-pinyin-service-commit-candidate-async - (concat zero-pinyin-pending-str candidate) - (append zero-pinyin-pending-pinyin-indices - (nth n-prime zero-pinyin-candidates-pinyin-indices))) - 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)) - (setq zero-pinyin-pending-pinyin-indices - (append zero-pinyin-pending-pinyin-indices - (nth n-prime zero-pinyin-candidates-pinyin-indices))) - (zero-pinyin-service-commit-candidate-async - zero-pinyin-pending-str - zero-pinyin-pending-pinyin-indices) - (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 () - "Commit first candidate if there is one, otherwise, commit preedit string." - (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 it 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. -Override `zero-handle-preedit-char-default'. - -CH the character user typed." - (cond - ((= ch ?\s) - (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))) - ((= ch zero-previous-page-key) - (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)) - (setq zero-pinyin-state nil) - (zero-append-char-to-preedit-str ch)))))) - -(defun zero-pinyin-get-preedit-str-for-panel () - "Return the preedit string that should show in 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) - (zero-backspace-default))) - -(defun zero-pinyin-delete-candidate (digit) - "Tell backend to delete candidate at DIGIT position. - -DIGIT is the digit key used to select nth candidate. -DIGIT 1 means delete 1st candidate. -DIGIT 2 means delete 2st candidate. -... -DIGIT 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. - -PREEDIT-STR the preedit string." - (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) - - -(provide 'zero) - -;;; zero.el ends here diff --git a/zero.el.in b/zero.el.in deleted file mode 100644 index 55d26c56cb9445e7a8a26b3f0ea32a5d0a1ace72..0000000000000000000000000000000000000000 --- a/zero.el.in +++ /dev/null @@ -1,65 +0,0 @@ -;;; zero.el --- Zero Chinese input method framework -*- lexical-binding: t -*- - -;; Licensed under the Apache License, Version 2.0 (the "License"); -;; you may not use this file except in compliance with the License. -;; You may obtain a copy of the License at -;; -;; http://www.apache.org/licenses/LICENSE-2.0 -;; -;; Unless required by applicable law or agreed to in writing, software -;; distributed under the License is distributed on an "AS IS" BASIS, -;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -;; See the License for the specific language governing permissions and -;; limitations under the License. - -;; Version: PKG_VERSION -;; URL: https://gitlab.emacsos.com/sylecn/zero-el -;; Package-Version: PKG_VERSION -;; Package-Requires: ((emacs "24.3") (s "1.2.0")) - -;;; Commentary: - -;; zero.el is auto-generated from multiple other files. see zero.el.in and -;; build.py for details. It's created because package-lint doesn't support -;; multi-file package yet (issue #111). -;; -;; zero is a Chinese input method framework for Emacs, implemented -;; as an Emacs minor mode. -;; -;; zero-pinyin is bundled with zero, to use pinyin input method, add to -;; ~/.emacs file: -;; -;; (require 'zero-pinyin) -;; (zero-set-default-im 'pinyin) -;; ;; Now you may bind a key to zero-toggle to make it easy to -;; ;; switch on/off the input method. -;; (global-set-key (kbd "<f5>") 'zero-toggle) -;; -;; zero supports Chinese punctuation mapping. There are three modes, none, -;; basic, and full. The default is basic mode, which only map most essential -;; punctuations. You can cycle zero-punctuation-level in current buffer by -;; C-c , , You can change default Chinese punctuation level: -;; -;; (setq-default zero-punctuation-level *zero-punctuation-level-full*) -;; -;; zero supports full-width mode. You can toggle full-width mode in current -;; buffer by C-c , . You can enable full-width mode by default: -;; -;; (setq-default zero-full-width-mode t) -;; - -;;; Code: - -(require 'dbus) -(eval-when-compile (require 'cl-lib)) -(require 's) - -INCLUDE_ZERO_PANEL_EL -INCLUDE_ZERO_FRAMEWORK_EL -INCLUDE_ZERO_TABLE_EL -INCLUDE_ZERO_PINYIN_SERVICE_EL -INCLUDE_ZERO_PINYIN_EL - -(provide 'zero) - -;;; zero.el ends here