diff --git a/core/include/keyman/keyman_core_api.h b/core/include/keyman/keyman_core_api.h index 4d2789dd36..b6b4c4b0a0 100644 --- a/core/include/keyman/keyman_core_api.h +++ b/core/include/keyman/keyman_core_api.h @@ -242,6 +242,12 @@ typedef struct { // -1=unchanged, 0=off, 1=on km_core_caps_state new_caps_lock_state; + + // reference copy of actual UTF32 codepoints deleted from rhs of context + // exactly code_points_to_delete in length (plus null terminator). Used to + // determine encoding conversion differences when deleting; only set when + // using km_core_state_get_actions, otherwise nullptr. + const km_core_usv* deleted_context; } km_core_actions; /* diff --git a/core/src/action.cpp b/core/src/action.cpp index 941dcdf908..2670cd5bab 100644 --- a/core/src/action.cpp +++ b/core/src/action.cpp @@ -36,6 +36,10 @@ km_core_actions * km::core::action_item_list_to_actions_object( actions->emit_keystroke = KM_CORE_FALSE; actions->new_caps_lock_state = KM_CORE_CAPS_UNCHANGED; + // deleted_context data will be set in km_core_state_get_actions + // because it needs access to the state's app context + actions->deleted_context = nullptr; + // Clear output pointers, will be set later once we have sizes actions->output = nullptr; actions->persist_options = nullptr; diff --git a/core/src/km_core_action_api.cpp b/core/src/km_core_action_api.cpp index 9a57760418..35a53cda79 100644 --- a/core/src/km_core_action_api.cpp +++ b/core/src/km_core_action_api.cpp @@ -20,6 +20,8 @@ using namespace km::core; +km_core_usv const *get_deleted_context(context const &app_context, unsigned int code_points_to_delete); + km_core_actions const * km_core_state_get_actions( km_core_state const *state ) { @@ -35,6 +37,16 @@ km_core_actions const * km_core_state_get_actions( km_core_actions * result = action_item_list_to_actions_object(action_items); + // We keep a copy of the app context before normalization, as the + // code_points_to_delete value can be updated by normalization, but by the + // time we get it back from actions_normalize or + // actions_update_app_context_nfu, app_context has already been updated to + // remove the necessary codepoints + context* app_context = km_core_state_app_context(state); + context app_context_for_deletion; + std::copy(app_context->begin(), app_context->end(), std::back_inserter(app_context_for_deletion)); + + if(state->processor().supports_normalization()) { // Normalize to NFC for those keyboard processors that support it if(!actions_normalize(km_core_state_context(state), km_core_state_app_context(state), result)) { @@ -48,6 +60,9 @@ km_core_actions const * km_core_state_get_actions( return nullptr; } } + + result->deleted_context = get_deleted_context(app_context_for_deletion, result->code_points_to_delete); + return result; } @@ -62,6 +77,10 @@ km_core_status km_core_actions_dispose( delete[] actions->output; } + if(actions->deleted_context) { + delete[] actions->deleted_context; + } + if(actions->persist_options) { for(auto option = actions->persist_options; option->scope; option++) { delete[] option->key; @@ -75,4 +94,15 @@ km_core_status km_core_actions_dispose( return KM_CORE_STATUS_OK; } +km_core_usv const *get_deleted_context(context const &app_context, unsigned int code_points_to_delete) { + auto p = app_context.end(); + for(size_t i = code_points_to_delete; i > 0; i--, p--); + auto deleted_context = new km_core_usv[code_points_to_delete + 1]; + for(size_t i = 0; i < code_points_to_delete; i++) { + deleted_context[i] = p->character; + p++; + } + deleted_context[code_points_to_delete] = 0; + return deleted_context; +} diff --git a/core/tests/unit/kmnkbd/action_api.cpp b/core/tests/unit/kmnkbd/action_api.cpp index 6107c510aa..b0b3815a4a 100644 --- a/core/tests/unit/kmnkbd/action_api.cpp +++ b/core/tests/unit/kmnkbd/action_api.cpp @@ -46,6 +46,7 @@ void test_two_backspaces() { assert(actions->do_alert == false); assert(actions->emit_keystroke == false); assert(actions->new_caps_lock_state == -1); + assert(actions->deleted_context == nullptr); try_status(km_core_actions_dispose(actions)); } @@ -76,6 +77,7 @@ void test_marker_text_interleaved() { assert(actions->do_alert == false); assert(actions->emit_keystroke == false); assert(actions->new_caps_lock_state == -1); + assert(actions->deleted_context == nullptr); try_status(km_core_actions_dispose(actions)); } @@ -99,6 +101,7 @@ void test_alert() { assert(actions->do_alert == KM_CORE_TRUE); assert(actions->emit_keystroke == KM_CORE_FALSE); assert(actions->new_caps_lock_state == KM_CORE_CAPS_UNCHANGED); + assert(actions->deleted_context == nullptr); try_status(km_core_actions_dispose(actions)); } @@ -122,6 +125,7 @@ void test_emit_keystroke() { assert(actions->do_alert == KM_CORE_FALSE); assert(actions->emit_keystroke == KM_CORE_TRUE); assert(actions->new_caps_lock_state == KM_CORE_CAPS_UNCHANGED); + assert(actions->deleted_context == nullptr); try_status(km_core_actions_dispose(actions)); } @@ -146,6 +150,7 @@ void test_invalidate_context() { assert(actions->do_alert == KM_CORE_FALSE); assert(actions->emit_keystroke == KM_CORE_FALSE); assert(actions->new_caps_lock_state == KM_CORE_CAPS_UNCHANGED); + assert(actions->deleted_context == nullptr); try_status(km_core_actions_dispose(actions)); } @@ -185,6 +190,7 @@ void test_persist_opt() { assert(actions->do_alert == KM_CORE_FALSE); assert(actions->emit_keystroke == KM_CORE_FALSE); assert(actions->new_caps_lock_state == KM_CORE_CAPS_UNCHANGED); + assert(actions->deleted_context == nullptr); try_status(km_core_actions_dispose(actions)); } @@ -227,6 +233,7 @@ int main(int argc, char *argv []) { std::cout << "&km_core_actions.do_alert: " << ((intptr_t)(&act.do_alert)-(intptr_t)(&act)) << std::endl; std::cout << "&km_core_actions.emit_keystroke: " << ((intptr_t)(&act.emit_keystroke)-(intptr_t)(&act)) << std::endl; std::cout << "&km_core_actions.new_caps_lock_state: " << ((intptr_t)(&act.new_caps_lock_state)-(intptr_t)(&act)) << std::endl; + std::cout << "&km_core_actions.deleted_context: " << ((intptr_t)(&act.deleted_context)-(intptr_t)(&act)) << std::endl; // actions test_two_backspaces(); diff --git a/core/tests/unit/kmnkbd/action_set_api.cpp b/core/tests/unit/kmnkbd/action_set_api.cpp index 991eae1672..63e0ac2c92 100644 --- a/core/tests/unit/kmnkbd/action_set_api.cpp +++ b/core/tests/unit/kmnkbd/action_set_api.cpp @@ -125,7 +125,8 @@ void test_two_backspaces() { test_env_opts, // km_core_option_item* persist_options; KM_CORE_FALSE, // km_core_bool do_alert; KM_CORE_FALSE, // km_core_bool emit_keystroke; - KM_CORE_CAPS_UNCHANGED // new_caps_lock_state; + KM_CORE_CAPS_UNCHANGED, // new_caps_lock_state; + nullptr // km_core_usv* deleted_context; }; run_test(action_items, actions); @@ -144,7 +145,8 @@ void test_character() { test_env_opts, // km_core_option_item* persist_options; KM_CORE_FALSE, // km_core_bool do_alert; KM_CORE_FALSE, // km_core_bool emit_keystroke; - KM_CORE_CAPS_UNCHANGED // new_caps_lock_state; + KM_CORE_CAPS_UNCHANGED, // new_caps_lock_state; + nullptr // km_core_usv* deleted_context; }; run_test(action_items, actions); @@ -165,7 +167,8 @@ void test_alert() { test_env_opts, // km_core_option_item* persist_options; KM_CORE_TRUE, // km_core_bool do_alert; KM_CORE_FALSE, // km_core_bool emit_keystroke; - KM_CORE_CAPS_UNCHANGED // new_caps_lock_state; + KM_CORE_CAPS_UNCHANGED, // new_caps_lock_state; + nullptr // km_core_usv* deleted_context; }; run_test(action_items, actions); @@ -186,7 +189,8 @@ void test_emit_keystroke() { test_env_opts, // km_core_option_item* persist_options; KM_CORE_FALSE, // km_core_bool do_alert; KM_CORE_TRUE, // km_core_bool emit_keystroke; - KM_CORE_CAPS_UNCHANGED // new_caps_lock_state; + KM_CORE_CAPS_UNCHANGED, // new_caps_lock_state; + nullptr // km_core_usv* deleted_context; }; run_test(action_items, actions); @@ -208,7 +212,8 @@ void test_invalidate_context() { test_env_opts, // km_core_option_item* persist_options; KM_CORE_FALSE, // km_core_bool do_alert; KM_CORE_FALSE, // km_core_bool emit_keystroke; - KM_CORE_CAPS_UNCHANGED // new_caps_lock_state; + KM_CORE_CAPS_UNCHANGED, // new_caps_lock_state; + nullptr // km_core_usv* deleted_context; }; run_test(action_items, actions); @@ -243,7 +248,8 @@ void test_persist_opt() { options, // km_core_option_item* persist_options; KM_CORE_FALSE, // km_core_bool do_alert; KM_CORE_FALSE, // km_core_bool emit_keystroke; - KM_CORE_CAPS_UNCHANGED // new_caps_lock_state; + KM_CORE_CAPS_UNCHANGED, // new_caps_lock_state; + nullptr // km_core_usv* deleted_context; }; run_test(action_items, actions); @@ -266,7 +272,8 @@ void test_caps_lock() { test_env_opts, // km_core_option_item* persist_options; KM_CORE_FALSE, // km_core_bool do_alert; KM_CORE_FALSE, // km_core_bool emit_keystroke; - KM_CORE_CAPS_ON // new_caps_lock_state; + KM_CORE_CAPS_ON, // new_caps_lock_state; + nullptr // km_core_usv* deleted_context; }; run_test(action_items, actions); diff --git a/core/tests/unit/kmnkbd/meson.build b/core/tests/unit/kmnkbd/meson.build index 16f0c87e8d..bf3c845ff1 100644 --- a/core/tests/unit/kmnkbd/meson.build +++ b/core/tests/unit/kmnkbd/meson.build @@ -27,6 +27,7 @@ tests = [ ['kmx_xstring', 'test_kmx_xstring.cpp'], ['kmx_context', 'test_kmx_context.cpp'], ['test_actions_normalize', 'test_actions_normalize.cpp'], + ['test_actions_get_api', 'test_actions_get_api.cpp'], ] test_path = join_paths(meson.current_build_dir(), '..', 'kmx') diff --git a/core/tests/unit/kmnkbd/test_actions_get_api.cpp b/core/tests/unit/kmnkbd/test_actions_get_api.cpp new file mode 100644 index 0000000000..6651e8ad9f --- /dev/null +++ b/core/tests/unit/kmnkbd/test_actions_get_api.cpp @@ -0,0 +1,373 @@ +/* + Copyright: © 2024 SIL International. + Description: Tests for the km_core_state_get_actions API. + Create Date: 29 Jan 2024 + Authors: Marc Durdin + History: 29 Jan 2024 - MCD - Initial implementation. +*/ +#include +#include +#include "keyman_core.h" + +#include "path.hpp" +#include "action.hpp" +#include "context.hpp" + +#include +#include "../emscripten_filesystem.h" + +km_core_option_item test_env_opts[] = +{ + KM_CORE_OPTIONS_END +}; + +km_core_keyboard * test_kb = nullptr; +km_core_state * test_state = nullptr; +km_core_actions * test_actions = nullptr; +std::string arg_path; + +void teardown() { + if(test_state) { + km_core_state_dispose(test_state); + test_state = nullptr; + } + if(test_kb) { + km_core_keyboard_dispose(test_kb); + test_kb = nullptr; + } + if(test_actions) { + delete [] test_actions->output; + delete test_actions; + test_actions = nullptr; + } +} + + + +void setup(const km_core_cp *app_context, const km_core_cp *cached_context, int actions_code_points_to_delete, const std::u32string actions_output) { + teardown(); + + km::core::path path = km::core::path::join(arg_path, "..", "ldml", "keyboards", "k_001_tiny.kmx"); + try_status(km_core_keyboard_load(path.native().c_str(), &test_kb)); + try_status(km_core_state_create(test_kb, test_env_opts, &test_state)); + + try_status(set_context_from_string(km_core_state_context(test_state), cached_context)); + try_status(set_context_from_string(km_core_state_app_context(test_state), app_context)); + + test_actions = new km_core_actions; + test_actions->code_points_to_delete = actions_code_points_to_delete; + std::unique_ptr buf(new km_core_usv[actions_output.length() + 1]); + actions_output.copy(buf.get(), actions_output.length()); + // terminate the buffer + buf.get()[actions_output.length()] = 0; + test_actions->output = buf.release(); + + test_actions->persist_options = new km_core_option_item[1]; + test_actions->persist_options[0] = KM_CORE_OPTIONS_END; + test_actions->do_alert = KM_CORE_FALSE; + test_actions->emit_keystroke = KM_CORE_FALSE; + test_actions->new_caps_lock_state = KM_CORE_CAPS_UNCHANGED; + + test_actions->deleted_context = nullptr; + test_state->set_actions(*test_actions); + test_state->actions().commit(); +} + +//------------------------------------------------------------------------------------- + +/** + * Run a single test on actions_normalize. This is quite nuanced, because the + * input state is more than a little complex. We have inputs in NFU and NFD, and + * outputs counting NFU and inserting NFC. Be careful! + * + * @param name descriptive name for the test + * @param initial_app_context the app context stored in the state, + * _before_ transform is applied -- NFU + * @param final_cached_context cached context _after_ actions have been + * applied -- guaranteed NFD (essentially, + * this is initial_cached_context - + * actions_code_points_to_delete + + * actions_output) + * @param actions_code_points_to_delete number of NFD code points that the + * keyboard processor has asked to remove + * in its actions + * @param actions_output NFD string that the keyboard processor + * has asked to insert in its actions + * @param expected_delete expected: NFU code points to ask app to + * remove + * @param expected_output expected: adjusted NFC output to insert + * into the app + * @param expected_final_app_context expected: NFU adjusted final app + * context, which will be NFC from the + * boundary of the transform, but will not + * have been modified prior to that. Should + * match char-for-char what the app ends up + * with in its text buffer. + */ +void test( + const char *name, + const km_core_cp *initial_app_context, + const km_core_cp *final_cached_context, + int actions_code_points_to_delete, + const std::u32string actions_output, + + const unsigned int expected_delete, + const std::u32string expected_output, + const km_core_cp *expected_final_app_context, + const std::u32string expected_deleted_context +) { + std::cout << "test: " << name << std::endl; + + setup(initial_app_context, final_cached_context, actions_code_points_to_delete, actions_output); + + auto actual_actions = km_core_state_get_actions(test_state); + + std::cout << " (" << name << "): delete: " << expected_delete << " output: |" << std::u32string(actual_actions->output) << "|" << std::endl; + std::u32string o(actual_actions->output); + for(auto i = o.begin(); i < o.end(); i++) { + std::cout << " U+" << std::hex << (int)(*i); + } + std::cout << std::endl; + + std::cout << " deleted_context: " << std::u32string(actual_actions->deleted_context) << std::endl; + std::u32string dc(actual_actions->deleted_context); + for(auto i = dc.begin(); i < dc.end(); i++) { + std::cout << " U+" << std::hex << (int)(*i); + } + std::cout << std::endl; + + assert(expected_delete == actual_actions->code_points_to_delete); + assert(expected_output == actual_actions->output); + assert(expected_deleted_context == actual_actions->deleted_context); + + // assert(expected_deleted_context == actual_actions->deleted_context); + + auto actual_final_app_context = get_context_as_string(km_core_state_app_context(test_state)); + auto actual_final_app_context_string = std::u16string(actual_final_app_context); + auto expected_final_app_context_string = std::u16string(expected_final_app_context); + std::cout << " final app context: actual: |" << actual_final_app_context_string << "| expected: |" << expected_final_app_context_string << "|" << std::endl; + assert(actual_final_app_context_string == expected_final_app_context_string); + delete [] actual_final_app_context; + + teardown(); +} + +void run_tests() { + + // Null boundary tests + + test( + "noop", + /* app context pre transform: */ u"", + /* cached context post transform: */ u"", + /* action del, output: */ 0, U"", + // ---- results ---- + /* action del, output: */ 0, U"", + /* app_context: */ u"", + /* expected del */ U"" + ); + + test( + "no_output", + /* app context pre transform: */ u"abc", + /* cached context post transform: */ u"abc", + /* action del, output: */ 0, U"", + // ---- results ---- + /* action del, output: */ 0, U"", + /* app_context: */ u"abc", + /* expected del */ U"" + ); + + test( + "no_context", + /* app context pre transform: */ u"", + /* cached context post transform: */ u"def", + /* action del, output: */ 0, U"def", + // ---- results ---- + /* action del, output: */ 0, U"def", + /* app_context: */ u"def", + /* expected del */ U"" + ); + + // Simple tests -- no deletions involved + + test( + "no_normalization", + /* app context pre transform: */ u"abc", + /* cached context post transform: */ u"abcdef", + /* action del, output: */ 0, U"def", + // ---- results ---- + /* action del, output: */ 0, U"def", + /* app_context: */ u"abcdef", + /* expected del */ U"" + ); + + test( + "output_to_nfc_basic", + /* app context pre transform: */ u"abc", + /* cached context post transform: */ u"abcde\u0300f", + /* action del, output: */ 0, U"de\u0300f", + // ---- results ---- + /* action del, output: */ 0, U"dèf", + /* app_context: */ u"abcdèf", + /* expected del */ U"" + ); + + test( + "output_to_nfc_hefty", + /* app context pre transform: */ u"abc", + /* cached context post transform: */ u"abcA\u0300" u"e\u0316\u0301" u"\u0073\u0323\u0307" u"\u0041\u030a" u"\U000114B9\U000114B0", + /* action del, output: */ 0, U"A\u0300" U"e\u0316\u0301" U"\u0073\u0323\u0307" U"\u0041\u030a" U"\U000114B9\U000114B0", + // ---- results ---- + /* action del, output: */ 0, U"À" U"é̖" U"\u1e69" U"\u00c5" U"\U000114BC", + /* app_context: */ u"abcÀé̖\u1e69\u00c5\U000114BC", + /* expected del */ U"" + ); + + // Interaction with input context when not on normalization boundary + + test( + "Backtrack one character to combine as NFC", + /* app context pre transform: */ u"XYZA", + /* cached context post transform: */ u"XYZA\u0300abc", + /* action del, output: */ 0, U"\u0300abc", + // ---- results ---- + /* action del, output: */ 1, U"Àabc", + /* app_context: */ u"XYZÀabc", + /* expected del */ U"A" + ); + + test( + "Backtrack e+comb circ (2 chars) to combine as NFC", + /* app context pre transform: */ u"abce\u0302", + /* cached context post transform: */ u"abce\u0323\u0302", + /* action del, output: */ 1, U"\u0323\u0302", + // ---- results ---- + /* action del, output: */ 2, U"ệ", + /* app_context: */ u"abcệ", + /* expected del */ U"e\u0302" + ); + + test( + "One backspace for NFD converts into one char in NFC (ê) and recombine", + /* app context pre transform: */ u"abcê", + /* cached context post transform: */ u"abce\u0323\u0302", + /* action del, output: */ 1, U"\u0323\u0302", // NFD input; delete 1: \u0302 + // ---- results ---- + /* action del, output: */ 1, U"ệ", // NFC output; delete 1: ê + /* app_context: */ u"abcệ", + /* expected del */ U"ê" + ); + + // a\u0300 should not be normalized because it is not otherwise impacted by + // the action. + test( + "Avoid editing too far back in context when finding normalization boundary", + /* app context pre transform: */ u"a\u0300bcê", + /* cached context post transform: */ u"a\u0300bce\u0323\u0302", + /* action del, output: */ 1, U"\u0323\u0302", // NFD input; delete 1: \u0302 + // ---- results ---- + /* action del, output: */ 1, U"ệ", // NFC output; delete 1: ê + /* app_context: */ u"a\u0300bcệ", + /* expected del */ U"ê" + ); + + // If we don't reach a normalization boundary, we still should continue to work + test( + "Normalizable letters at start of context", + /* app context pre transform: */ u"\u0300", + /* cached context post transform: */ u"\u0323\u0300\u0302", + /* action del, output: */ 1, U"\u0323\u0300\u0302", // NFD input; + // ---- results ---- + /* action del, output: */ 1, U"\u0323\u0300\u0302", // NFC output is still decomposed because there is no base + /* app_context: */ u"\u0323\u0300\u0302", + /* expected del */ U"\u0300" + ); + + // Modifies the base as well as diacritic + + test( + "Two backspaces for NFD converts into one char in NFC (ê) and recombine", + /* app context pre transform: */ u"abcê", + /* cached context post transform: */ u"abca\u0323\u0302", + /* action del, output: */ 2, U"a\u0323\u0302", // NFD input; delete 2: e\u0302 + // ---- results ---- + /* action del, output: */ 1, U"ậ", // NFC output; delete 1: ê + /* app_context: */ u"abcậ", + /* expected del */ U"ê" + ); + + // surrogate pair tests + + test( + "Surrogate pair in context", + /* app context pre transform: */ u"abc\U0001F607ê", + /* cached context post transform: */ u"abc\U0001F607a\u0323\u0302", + /* action del, output: */ 2, U"a\u0323\u0302", + // ---- results ---- + /* action del, output: */ 1, U"ậ", + /* app_context: */ u"abc\U0001F607ậ", + /* expected del */ U"ê" + ); + + test( + "Surrogate pair in output", + /* app context pre transform: */ u"abc", + /* cached context post transform: */ u"abc\U0001F607", + /* action del, output: */ 0, U"\U0001F607", + // ---- results ---- + /* action del, output: */ 0, U"\U0001F607", + /* app_context: */ u"abc\U0001F607", + /* expected del */ U"" + ); + + test( + "Surrogate pairs in both context and output", + /* app context pre transform: */ u"a\U0001F607bcê", + /* cached context post transform: */ u"a\U0001F607bca\U0001F60E", + /* action del, output: */ 2, U"a\U0001F60E", + // ---- results ---- + /* action del, output: */ 1, U"a\U0001F60E", + /* app_context: */ u"a\U0001F607bca\U0001F60E", + /* expected del */ U"ê" + ); +} + +//------------------------------------------------------------------------------------- +// Launcher +//------------------------------------------------------------------------------------- + +constexpr const auto help_str = "\ +test_actions_get_api [--color] \n\ +\n\ + --color Force color output\n\ + BUILD_PATH Path where test_actions_get_api.exe is found; kmx files are\n\ + located relative to this path.\n"; + +int error_args() { + std::cerr << "test_actions_get_api: Invalid arguments." << std::endl; + std::cout << help_str; + return 1; +} + +int main(int argc, char *argv []) { + + if(argc < 2) { + return error_args(); + } + + auto arg_color = std::string(argv[1]) == "--color"; + if(arg_color && argc < 3) { + return error_args(); + } + console_color::enabled = console_color::isaterminal() || arg_color; + +#ifdef __EMSCRIPTEN__ + arg_path = get_wasm_file_path(argv[arg_color ? 2 : 1]); +#else + arg_path = argv[arg_color ? 2 : 1]; +#endif + + run_tests(); +} + diff --git a/core/tests/unit/kmnkbd/test_actions_normalize.cpp b/core/tests/unit/kmnkbd/test_actions_normalize.cpp index f2a7bc2e50..afbb784a53 100644 --- a/core/tests/unit/kmnkbd/test_actions_normalize.cpp +++ b/core/tests/unit/kmnkbd/test_actions_normalize.cpp @@ -61,6 +61,7 @@ void setup(const km_core_cp *app_context, const km_core_cp *cached_context, int // terminate the buffer buf.get()[actions_output.length()] = 0; test_actions->output = buf.release(); + test_actions->deleted_context = nullptr; } //------------------------------------------------------------------------------------- diff --git a/developer/src/tike/main/Keyman.System.KeymanCore.pas b/developer/src/tike/main/Keyman.System.KeymanCore.pas index 363abcc2f5..e346cf4eca 100644 --- a/developer/src/tike/main/Keyman.System.KeymanCore.pas +++ b/developer/src/tike/main/Keyman.System.KeymanCore.pas @@ -181,23 +181,29 @@ type ); km_core_actions = record - code_points_to_delete: uint32; // number of codepoints (not codeunits!) to delete from app context. + // number of codepoints (not codeunits!) to delete from app context. + code_points_to_delete: uint32; - // null-term string of characters to insert into document - output: pkm_core_usv; + // null-term string of characters to insert into document + output: pkm_core_usv; - // list of options to persist, terminated with KM_CORE_OPTIONS_END - persist_options: pkm_core_option_item; + // list of options to persist, terminated with KM_CORE_OPTIONS_END + persist_options: pkm_core_option_item; - // issue a beep, 0 = no, 1 = yes - do_alert: km_core_bool; + // issue a beep, 0 = no, 1 = yes + do_alert: km_core_bool; - // emit the (unmodified) input keystroke to the application, 0 = no, 1 = yes - emit_keystroke: km_core_bool; + // emit the (unmodified) input keystroke to the application, 0 = no, 1 = yes + emit_keystroke: km_core_bool; - // -1=unchanged, 0=off, 1=on - new_caps_lock_state: km_core_caps_state; + // -1=unchanged, 0=off, 1=on + new_caps_lock_state: km_core_caps_state; + // reference copy of actual UTF32 codepoints deleted from rhs of context + // exactly code_points_to_delete in length (plus null terminator). Used to + // determine encoding conversion differences when deleting; only set when + // using km_core_state_actions_get, otherwise nullptr. + deleted_context: pkm_core_usv; end; pkm_core_actions = ^km_core_actions; @@ -669,13 +675,14 @@ begin {$IFDEF WIN64} {$ERROR Struct size not yet verified for 64-bit} {$ENDIF} - assert(sizeof(km_core_actions) = 24); + assert(sizeof(km_core_actions) = 28); // &km_core_actions.code_points_to_delete: 0 assert(Uint32(@act.output) - Uint32(@act) = 4); assert(Uint32(@act.persist_options) - Uint32(@act) = 8); assert(Uint32(@act.do_alert) - Uint32(@act) = 12); assert(Uint32(@act.emit_keystroke) - Uint32(@act) = 16); assert(Uint32(@act.new_caps_lock_state) - Uint32(@act) = 20); + assert(Uint32(@act.deleted_context) - Uint32(@act) = 24); end; initialization