Merge branch 'master' into fix/windows/15571-contextual-help-chm-file-has-blank-pages
Some checks failed
Keyman Build Summary / Summarize build status checks (push) Has been cancelled

This commit is contained in:
rc-swag 2026-04-19 20:00:43 +10:00 committed by GitHub
commit 2f61208dec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 990 additions and 252 deletions

View file

@ -25,7 +25,7 @@ runs:
using: 'composite'
steps:
- name: Download Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: keyman-srcpkg
path: artifacts/keyman-srcpkg
@ -49,7 +49,7 @@ runs:
echo '```' >> $GITHUB_STEP_SUMMARY
- name: Store binary packages
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: keyman-binarypkgs-${{ inputs.dist }}_${{ inputs.arch }}
path: |

195
.github/labeler.yml vendored
View file

@ -5,86 +5,173 @@
# common ones. The others are commented out. There is still some variance between
# folder names and labels; consider this documentation of that ;-)
docs: docs/**
docs:
- changed-files:
- any-glob-to-any-file: 'docs/**'
#
# Add labels based on changed files using actions/labeler
#
android/:
- android/**
- resources/teamcity/includes/**
- resources/teamcity/android/**
android/app/: android/KMAPro/**
android/engine/: android/KMEA/**
android/samples/: android/Samples/**
- changed-files:
- any-glob-to-any-file:
- android/**
- resources/teamcity/includes/**
- resources/teamcity/android/**
android/app/:
- changed-files:
- any-glob-to-any-file:
- android/KMAPro/**
android/engine/:
- changed-files:
- any-glob-to-any-file:
- android/KMEA/**
android/samples/:
- changed-files:
- any-glob-to-any-file:
- android/Samples/**
common/:
- common/**
- resources/teamcity/includes/**
- resources/teamcity/common/**
common/web/: common/web/**
- changed-files:
- any-glob-to-any-file:
- common/**
- resources/teamcity/includes/**
- resources/teamcity/common/**
common/web/:
- changed-files:
- any-glob-to-any-file:
- common/web/**
core/:
- core/**
- resources/teamcity/includes/**
- resources/teamcity/core/**
- changed-files:
- any-glob-to-any-file:
- core/**
- resources/teamcity/includes/**
- resources/teamcity/core/**
developer/:
- developer/**
- resources/teamcity/includes/**
- resources/teamcity/developer/**
- changed-files:
- any-glob-to-any-file:
- developer/**
- resources/teamcity/includes/**
- resources/teamcity/developer/**
developer/compilers/:
- developer/src/kmc/**
- developer/src/kmcmplib/**
- developer/src/kmc-*/**
- changed-files:
- any-glob-to-any-file:
- developer/src/kmc/**
- developer/src/kmcmplib/**
- developer/src/kmc-*/**
developer/ide/:
- developer/src/server/**
- developer/src/tike/**
- changed-files:
- any-glob-to-any-file:
- developer/src/server/**
- developer/src/tike/**
ios/:
- ios/**
- resources/teamcity/includes/**
- resources/teamcity/ios/**
ios/app/: ios/keyman/**
ios/engine/: ios/engine/**
ios/samples/: ios/samples/**
- changed-files:
- any-glob-to-any-file:
- ios/**
- resources/teamcity/includes/**
- resources/teamcity/ios/**
ios/app/:
- changed-files:
- any-glob-to-any-file:
- ios/keyman/**
ios/engine/:
- changed-files:
- any-glob-to-any-file:
- ios/engine/**
ios/samples/:
- changed-files:
- any-glob-to-any-file:
- ios/samples/**
linux/:
- linux/**
- resources/teamcity/includes/**
- resources/teamcity/linux/**
linux/config/: linux/keyman-config/**
linux/engine/: linux/ibus-keyman/**
- changed-files:
- any-glob-to-any-file:
- linux/**
- resources/teamcity/includes/**
- resources/teamcity/linux/**
linux/config/:
- changed-files:
- any-glob-to-any-file:
- linux/keyman-config/**
linux/engine/:
- changed-files:
- any-glob-to-any-file:
- linux/ibus-keyman/**
mac/:
- mac/**
- resources/teamcity/includes/**
- resources/teamcity/mac/**
- changed-files:
- any-glob-to-any-file:
- mac/**
- resources/teamcity/includes/**
- resources/teamcity/mac/**
# mac/config/:
# mac/engine/: mac/**
oem/: oem/**
oem/fv/: oem/firstvoices/**
oem/fv/android/: oem/firstvoices/android/**
oem/fv/ios/: oem/firstvoices/ios/**
oem/fv/windows/: oem/firstvoices/windows/**
oem/:
- changed-files:
- any-glob-to-any-file:
- oem/**
oem/fv/:
- changed-files:
- any-glob-to-any-file:
- oem/firstvoices/**
oem/fv/android/:
- changed-files:
- any-glob-to-any-file:
- oem/firstvoices/android/**
oem/fv/ios/:
- changed-files:
- any-glob-to-any-file:
- oem/firstvoices/ios/**
oem/fv/windows/:
- changed-files:
- any-glob-to-any-file:
- oem/firstvoices/windows/**
resources/: resources/**
resources/:
- changed-files:
- any-glob-to-any-file:
- resources/**
web/:
- web/**
- resources/teamcity/includes/**
- resources/teamcity/web/**
- changed-files:
- any-glob-to-any-file:
- web/**
- resources/teamcity/includes/**
- resources/teamcity/web/**
# web/bookmarklet/
web/engine/: web/source/**
web/ui/: web/source/kmwui*
web/samples/: web/samples/**
web/predictive-text/: web/src/engine/predictive-text/**
web/engine/:
- changed-files:
- any-glob-to-any-file:
- web/source/**
web/ui/:
- changed-files:
- any-glob-to-any-file:
- web/source/kmwui*
web/samples/:
- changed-files:
- any-glob-to-any-file:
- web/samples/**
web/predictive-text/:
- changed-files:
- any-glob-to-any-file:
- web/src/engine/predictive-text/**
windows/:
- windows/**
- resources/teamcity/includes/**
- resources/teamcity/windows/**
windows/config/: windows/src/desktop/**
windows/engine/: windows/src/engine/**
- changed-files:
- any-glob-to-any-file:
- windows/**
- resources/teamcity/includes/**
- resources/teamcity/windows/**
windows/config/:
- changed-files:
- any-glob-to-any-file:
- windows/src/desktop/**
windows/engine/:
- changed-files:
- any-glob-to-any-file:
- windows/src/engine/**

View file

@ -27,7 +27,7 @@ jobs:
steps:
- name: Restore artifacts
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
artifacts
@ -55,7 +55,7 @@ jobs:
- name: Checkout
if: steps.environment_step.outputs.SKIP_API_CHECK != 'true'
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: '${{ steps.environment_step.outputs.GIT_SHA }}'
fetch-depth: 0
@ -84,7 +84,7 @@ jobs:
- name: Archive .symbols file
if: steps.environment_step.outputs.SKIP_API_CHECK != 'true' && always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: libkeymancore.symbols
path: ${{ github.workspace }}/keyman/linux/debian/tmp/DEBIAN/symbols

View file

@ -41,7 +41,7 @@ jobs:
steps:
- name: Checkout repository
id: checkout
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Build Docker images
id: build
run: |
@ -67,7 +67,7 @@ jobs:
steps:
- name: Checkout repository
id: checkout
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Test Docker images
id: test

View file

@ -40,7 +40,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: '${{ github.event.client_payload.buildSha }}'
@ -64,7 +64,7 @@ jobs:
npm -v
node -v
- uses: actions/setup-python@v6
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.13'

View file

@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: crowdin action
uses: crowdin/github-action@v2.7.0

View file

@ -43,7 +43,7 @@ jobs:
PRERELEASE_TAG: ${{ steps.prerelease_tag.outputs.PRERELEASE_TAG }}
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: '${{ github.event.client_payload.buildSha }}'
@ -121,7 +121,7 @@ jobs:
echo "- $(find . -name keyman_\*.dsc)" >> $GITHUB_STEP_SUMMARY
- name: Store source package
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: keyman-srcpkg
path: |
@ -140,7 +140,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: '${{ github.event.client_payload.buildSha }}'
sparse-checkout: '.github/actions/'
@ -166,7 +166,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: '${{ github.event.client_payload.buildSha }}'
sparse-checkout: '.github/actions/'
@ -204,13 +204,13 @@ jobs:
sudo rm -rf /usr/share/dotnet
- name: Download Source Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: keyman-srcpkg
path: artifacts
- name: Download Binary Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: artifacts
pattern: keyman-binarypkgs-*
@ -281,7 +281,7 @@ jobs:
echo "::endgroup::"
- name: Download Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: keyman-signedpkgs
@ -343,7 +343,7 @@ jobs:
steps:
- name: Download Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: artifacts
pattern: keyman-*
@ -362,7 +362,7 @@ jobs:
echo "SKIP_API_CHECK=${{ github.event.client_payload.skipApiCheck }}" >> artifacts/env
- name: Cache artifacts
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
artifacts

View file

@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Update labels based on changed files
uses: actions/labeler@v4
uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Update labels based on PR title

View file

@ -50,7 +50,7 @@ jobs:
fi
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: '${{ github.event.client_payload.buildSha }}'

View file

@ -1,40 +1,40 @@
# GENERATED FILE - DO NOT EDIT!
#
# Keyman is copyright (C) SIL Global. MIT License.
#
# Do not modify the script in .github/workflows/pr-build-status.yml directly;
# instead work on the sources in resources/build/pr-build-status and use the
# build.sh script to rebuild the .github/workflows/pr-build-status.yml file from
# them.
#
# build.sh will append the relevant portions of
# resources/build/pr-build-status/pr-build-status.mjs to the content in
# resources/build/pr-build-status/pr-build-status.prefix.yml to form the .github
# workflow file.
#
name: Keyman Build Summary
on:
status:
push:
branches-ignore:
- master
- beta
- stable-*
workflow_dispatch:
inputs:
commit:
description: 'Commit sha'
required: true
type: string
jobs:
run_pr_build_status:
name: Summarize build status checks
runs-on: ubuntu-latest
steps:
- name: Check PR build status
id: run_pr_build_status_script
uses: actions/github-script@v7
with:
#
# Keyman is copyright (C) SIL Global. MIT License.
#
# Do not modify the script in .github/workflows/pr-build-status.yml directly;
# instead work on the sources in resources/build/pr-build-status and use the
# build.sh script to rebuild the .github/workflows/pr-build-status.yml file from
# them.
#
# build.sh will append the relevant portions of
# resources/build/pr-build-status/pr-build-status.mjs to the content in
# resources/build/pr-build-status/pr-build-status.prefix.yml to form the .github
# workflow file.
#
name: Keyman Build Summary
on:
status:
push:
branches-ignore:
- master
- beta
- stable-*
workflow_dispatch:
inputs:
commit:
description: 'Commit sha'
required: true
type: string
jobs:
run_pr_build_status:
name: Summarize build status checks
runs-on: ubuntu-latest
steps:
- name: Check PR build status
id: run_pr_build_status_script
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
// This code is copied out of resources/build/pr-build-status/pr-build-status.mjs
// where it is tested. It is copied inline here in order to avoid requiring the

View file

@ -1,5 +1,43 @@
# Keyman Version History
## 19.0.222 alpha 2026-04-08
* fix(linux): fix memory problem (#15823)
## 19.0.221 alpha 2026-03-27
* fix(common): add missing `default` property to keyman-touch-layout.clean.spec.json (#15787)
* chore(deps): bump picomatch from 4.0.3 to 4.0.4 in /developer/src/server/src/win32/trayicon/addon-src (#15786)
* chore(deps): bump picomatch from 2.3.1 to 2.3.2 (#15784)
* chore(deps-dev): bump flatted from 3.2.5 to 3.4.2 (#15773)
* chore(deps): bump brace-expansion from 5.0.4 to 5.0.5 in /developer/src/server/src/win32/trayicon/addon-src (#15791)
## 19.0.220 alpha 2026-03-26
* fix(mac): improved adherence to backspace rules for compliant apps (#15561)
## 19.0.219 alpha 2026-03-25
* maint(common): Update GitHub actions for Node 24 (#15762)
* fix(core): fix keydown/up handling for LDML keyboards (#15609)
## 19.0.218 alpha 2026-03-24
* fix(developer): define globalThis for compiled custom lexical models (#15777)
## 19.0.217 alpha 2026-03-17
* maint(mac): search for brew-installed rsync (#15765)
## 19.0.216 alpha 2026-03-16
* docs: tweak walkthrough content (#15712)
* chore(deps): bump tar from 7.5.10 to 7.5.11 in /developer/src/server/src/win32/trayicon/addon-src (#15723)
* docs(linux): add documentation how input methods work in GTK (#15749)
* docs(core): improve keyhandling doc (#15736)
* maint(resources): try meson 1.10.1 (#15754)
* maint(ios): clean carthage before builds (#15757)
## 19.0.215 alpha 2026-03-13
* chore(linux): fix dependency of Debian test suite (#15735)
@ -1288,6 +1326,15 @@
* refactor(windows): rename `TKeymanMutex.MutexOwned` to `TakeOwnership` and add `ReleaseOwnership` (#13168)
* chore: increment to alpha 19.0 (#13187)
## 18.0.249 stable 2026-03-27
* chore(linux): Update debian changelog (#15717)
* chore(linux): fix dependency of Debian test suite (#15734)
* chore(linux): Update debian changelog (#15731)
* maint(ios): Upgrade Sentry to 8.58.0 to support XCode 26 (#15755)
* fix(developer): define globalThis for compiled custom lexical models (#15778)
* fix(common): add missing `default` property to keyman-touch-layout.clean.spec.json (#15792)
## 18.0.248 stable 2026-03-12
* fix(ios): Revert Sentry to 8.38.0 to fix FirstVoices crash on startup with 18.0.247 (#15726)

View file

@ -1 +1 @@
19.0.216
19.0.223

View file

@ -457,6 +457,10 @@ string") into the appropriate spec format.
# .keyman-touch-layout version history
## 2026-03-26 2.1.2 stable
* Add missing 'default' property for longpress (sk) keys to clean spec. No other
changes.
## 2024-02-23 2.1.1 stable
* Loosen `layer.id` requirements to any non-whitespace characters, recommend
only alphanumeric, -, _. clean spec enforces this recommendation.

View file

@ -85,9 +85,9 @@
"text": { "type": "string" },
"layer": { "$ref": "#/definitions/layer-id" },
"nextlayer": { "$ref": "#/definitions/layer-id" },
"font": { "$ref": "#/definitions/font-spec" },
"fontsize": { "$ref": "#/definitions/fontsize-spec" },
"sp": { "$ref": "#/definitions/key-sp" },
"font": { "$ref": "#/definitions/font-spec" },
"sp": { "$ref" : "#/definitions/key-sp" },
"pad": { "$ref" : "#/definitions/key-pad" },
"width": { "$ref" : "#/definitions/key-width" },
"sk": { "$ref": "#/definitions/subkeys" },
@ -140,11 +140,12 @@
"text": { "type": "string" },
"layer": { "$ref": "#/definitions/layer-id" },
"nextlayer": { "$ref": "#/definitions/layer-id" },
"font": { "$ref": "#/definitions/font-spec" },
"fontsize": { "$ref": "#/definitions/fontsize-spec" },
"sp": { "$ref": "#/definitions/key-sp" },
"sp": { "$ref" : "#/definitions/key-sp" },
"pad": { "$ref" : "#/definitions/key-pad" },
"width": { "$ref" : "#/definitions/key-width" }
"width": { "$ref" : "#/definitions/key-width" },
"fontsize": { "$ref": "#/definitions/fontsize-spec" },
"font": { "$ref": "#/definitions/font-spec" },
"default": { "type": "boolean" }
},
"required": ["id"],
"additionalProperties": false

View file

@ -50,6 +50,9 @@ km_core_process_event(km_core_state *state,
if(state == nullptr) {
return KM_CORE_STATUS_INVALID_ARGUMENT;
}
if (vk == KM_CORE_VKEY_BKSP && is_key_down) {
state->set_backspace_handled_internally(false);
}
km_core_status status = state->processor().process_event(state, vk, modifier_state, is_key_down, event_flags);
if (state_should_invalidate_context(state, vk, modifier_state, is_key_down, event_flags)) {
@ -78,6 +81,10 @@ km_core_process_event(km_core_state *state,
state->apply_actions_and_merge_app_context();
if (vk == KM_CORE_VKEY_BKSP) {
state->set_backspace_handled_internally(!state->action_struct().emit_keystroke);
}
return status;
}

View file

@ -179,21 +179,13 @@ ldml_processor::process_event(
ldml_state.clear();
try {
if (!is_key_down) {
process_key_up(ldml_state);
if (vk == KM_CORE_VKEY_BKSP) {
process_backspace(ldml_state);
} else if (is_key_down) {
process_key_down(ldml_state);
} else {
switch (vk) {
// Currently, only one VK gets spoecial treatment.
// Special handling for backspace VK
case KM_CORE_VKEY_BKSP:
process_backspace(ldml_state);
break;
default:
// all other VKs
process_key_down(ldml_state);
} // end of switch
} // end of normal processing
process_key_up(ldml_state);
}
// all key-up and key-down events end up here.
// commit the ldml state into the core state
ldml_state.commit();
@ -210,11 +202,31 @@ void
ldml_processor::process_key_up(ldml_event_state &ldml_state)
const {
// TODO-LDML: Implement caps lock handling
ldml_state.clear();
// Look up the key
bool found = false;
const std::u16string key_str = keys.lookup(ldml_state.get_vk(), ldml_state.get_modifier_state(), found);
if (!found) {
ldml_state.emit_passthrough_keystroke();
}
}
void
ldml_processor::process_backspace(ldml_event_state &ldml_state) const {
if (ldml_state.get_modifier_state() & K_MODIFIERFLAG) {
// we never process modifier+bksp
ldml_state.emit_passthrough_keystroke();
return;
}
if (!ldml_state.is_key_down()) {
if (!ldml_state.get_state()->backspace_handled_internally()) {
ldml_state.emit_passthrough_keystroke();
}
return;
}
if (!!bksp_transforms) {
// process with an empty string via the bksp transforms
auto matchedContext = process_output(ldml_state, std::u32string(), bksp_transforms.get());

View file

@ -176,6 +176,9 @@ public:
* @return the number of context items consumed
*/
size_t context_to_string(std::u32string &str, bool include_markers = true);
km_core_state* get_state() const { return state; }
uint8_t is_key_down() const { return _is_key_down; }
private:
km_core_virtual_key _vk;

View file

@ -52,6 +52,7 @@ state::state(km::core::abstract_processor & ap, km_core_option_item const *env)
env->key,
env->value);
}
_backspace_handled_internally = false;
_imx_callback = nullptr;
_imx_object = nullptr;
memset(const_cast<km_core_actions*>(&_action_struct), 0, sizeof(km_core_actions));
@ -117,4 +118,4 @@ void state::apply_actions_and_merge_app_context() {
}
this->_action_struct.deleted_context = km::core::get_deleted_context(app_context_for_deletion, this->_action_struct.code_points_to_delete);
}
}

View file

@ -130,6 +130,7 @@ protected:
core::debug_items _debug_items;
km_core_keyboard_imx_platform _imx_callback;
void *_imx_object;
bool _backspace_handled_internally;
public:
state(core::abstract_processor & kb, km_core_option_item const *env);
@ -174,6 +175,22 @@ public:
km_core_actions const &actions
);
void apply_actions_and_merge_app_context();
/**
* This is used to track whether the backspace key was handled internally
* during the keydown event. This is needed so that we can return the same
* value from the keyup event as we did for the keydown event.
*
* Backspace is the only key that we sometimes handle internally (if we
* have enough context) and sometimes not. By the time we get the keyup
* event the context already got updated and so we have no way of knowing
* whether or not the keydown handled it internally. Therefore this
* flag exists.
*
* Only used when processing KM_CORE_VKEY_BKSP with LDML keyboards.
*/
void set_backspace_handled_internally(bool handled) { _backspace_handled_internally = handled; }
bool backspace_handled_internally() const { return _backspace_handled_internally; }
};
} // namespace core
} // namespace km

View file

@ -0,0 +1,355 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
#include <gtest/gtest.h>
#include "path.hpp"
#include "state.hpp"
#include "kmx/kmx_processevent.h"
#include "emscripten_filesystem.h"
#include "kmnkbd/action_items.hpp"
#include "load_kmx_file.hpp"
using namespace km::core::kmx;
km::core::path test_dir;
km_core_option_item test_env_opts[] = {KM_CORE_OPTIONS_END};
#define KEY_DOWN TRUE
#define KEY_UP FALSE
struct TestData {
const char* test_name;
const char* keyboard_name;
km_core_virtual_key vkey;
uint16_t modifier_state;
// Initial context
km_core_cu const* context;
// Whether keydown handled the event. Only relevant for keyup tests.
bool keydown_handled;
// Expected actions for keydown and keyup
std::initializer_list<km_core_action_item> keydown_actions;
std::initializer_list<km_core_action_item> keyup_actions;
};
std::string GenerateTestName(const testing::TestParamInfo<TestData>& info) {
return info.param.test_name;
}
class ProcessEventTests : public testing::TestWithParam<TestData> {
protected:
km_core_keyboard* keyboard = nullptr;
km_core_state* state = nullptr;
KMX_ProcessEvent process_event;
void Initialize(TestData const& data) {
km::core::path kmxfile = km::core::path(test_dir / data.keyboard_name);
auto blob = km::tests::load_kmx_file(kmxfile.native().c_str());
EXPECT_EQ(km_core_keyboard_load_from_blob(kmxfile.stem().c_str(), blob.data(), blob.size(), &this->keyboard), KM_CORE_STATUS_OK);
EXPECT_EQ(km_core_state_create(this->keyboard, test_env_opts, &this->state), KM_CORE_STATUS_OK);
EXPECT_TRUE(this->process_event.Load(blob.data(), blob.size()));
if (data.context) {
EXPECT_EQ(km_core_state_context_set_if_needed(this->state, data.context), KM_CORE_CONTEXT_STATUS_UPDATED);
}
((km::core::state*)this->state)->set_backspace_handled_internally(data.keydown_handled);
}
void TearDown() override {
if (this->state) {
km_core_state_dispose(this->state);
this->state = nullptr;
}
if (this->keyboard) {
km_core_keyboard_dispose(this->keyboard);
this->keyboard = nullptr;
}
}
};
void print_all_action_items(km_core_state const* state) {
size_t n = 0;
auto act = km_core_state_action_items(state, &n);
std::cout << "Action items:" << std::endl;
for (size_t i = 0; i < n; i++) {
print_action_item("", *act++);
}
std::cout << "---------------" << std::endl;
}
TEST_P(ProcessEventTests, ReturnsExpectedActionsForKeyDown) {
auto data = GetParam();
Initialize(data);
EXPECT_EQ(km_core_process_event(this->state, data.vkey, data.modifier_state,
KEY_DOWN, KM_CORE_EVENT_FLAG_DEFAULT), KM_CORE_STATUS_OK);
// print_all_action_items(this->state);
EXPECT_TRUE(action_items(this->state, data.keydown_actions));
}
TEST_P(ProcessEventTests, ReturnsExpectedActionsForKeyUp) {
auto data = GetParam();
Initialize(data);
EXPECT_EQ(km_core_process_event(this->state, data.vkey, data.modifier_state, KEY_UP, KM_CORE_EVENT_FLAG_DEFAULT), KM_CORE_STATUS_OK);
// print_all_action_items(this->state);
EXPECT_TRUE(action_items(this->state, data.keyup_actions));
}
union backspace_union {
km_core_backspace_item backspace;
uint32_t value;
};
// const backspace_union backspace_with_context = {{KM_CORE_BT_CHAR, 'x'}};
const backspace_union backspace_no_context = {{KM_CORE_BT_CHAR, 0}};
const TestData values[] = {
//--------------------------------------------------------------------
// KMN
// Key with rule
{"KMN_VKey_A", "kmx/k_005___nul_with_initial_context.kmx", KM_CORE_VKEY_A, 0, u"x", true,
{ // KeyDown
{KM_CORE_IT_CHAR, { 0, }, {'d'}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
{"KMN_Ctrl_VKey_A", "kmx/k_005___nul_with_initial_context.kmx", KM_CORE_VKEY_A, KM_CORE_MODIFIER_LCTRL, u"x", true,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// Key without rule
{"KMN_VKey_X", "kmx/k_005___nul_with_initial_context.kmx", KM_CORE_VKEY_X, 0, u"x", true,
{ // KeyDown
{KM_CORE_IT_CHAR, { 0, }, {'x'}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
{"KMN_Ctrl_VKey_X", "kmx/k_005___nul_with_initial_context.kmx", KM_CORE_VKEY_X, KM_CORE_MODIFIER_LCTRL, u"x", true,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// Frame key without rule
{"KMN_VKey_Enter", "kmx/k_000___null_keyboard.kmx", KM_CORE_VKEY_ENTER, 0, u"x", false,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// // Backspace (with available context)
// // TODO: fix the implementation to make this test pass. Currently failing in KeyUp
// {"KMN_VKey_Backspace_Ctxt", "kmx/k_000___null_keyboard.kmx", KM_CORE_VKEY_BKSP, 0, u"x", true,
// { // KeyDown
// // Once we use C++ 20 we can use:
// //{KM_CORE_IT_BACK, { 0, }, {.backspace = {KM_CORE_BT_CHAR, 'x'}}},
// {KM_CORE_IT_BACK, { 0, }, {backspace_with_context.value}},
// {KM_CORE_IT_END}
// },
// { // KeyUp
// {KM_CORE_IT_END}
// }
// },
// Backspace (without context)
{"KMN_VKey_Backspace_NoCtxt", "kmx/k_000___null_keyboard.kmx", KM_CORE_VKEY_BKSP, 0, NULL, false,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// Ctrl+Backspace (with context)
{"KMN_Ctrl_VKey_Backspace_Ctxt", "kmx/k_000___null_keyboard.kmx", KM_CORE_VKEY_BKSP, KM_CORE_MODIFIER_LCTRL, u"x", true,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// Ctrl+Backspace (no context)
{"KMN_Ctrl_VKey_Backspace_NoCtxt", "kmx/k_000___null_keyboard.kmx", KM_CORE_VKEY_BKSP, KM_CORE_MODIFIER_LCTRL, NULL, false,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// Modifier frame key
{"KMN_VKey_Shift", "kmx/k_000___null_keyboard.kmx", KM_CORE_VKEY_SHIFT, 0, u"x", false,
{ // KeyDown
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_END}
}
},
//--------------------------------------------------------------------
// LDML
// Key with rule
{"LDML_VKey_A", "ldml/keyboards/k_020_fr.kmx", KM_CORE_VKEY_A, 0, u"x", true,
{ // KeyDown
{KM_CORE_IT_CHAR, { 0, }, {'q'}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_END}
}
},
{"LDML_Ctrl_VKey_A", "ldml/keyboards/k_020_fr.kmx", KM_CORE_VKEY_A, KM_CORE_MODIFIER_LCTRL, u"x", true,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// Key without rule
{"LDML_VKey_X", "ldml/keyboards/k_000_minimal_keyboard.kmx", KM_CORE_VKEY_X, 0, u"x", true,
{ // KeyDown
{KM_CORE_IT_END} // LDML: no output without rule
},
{ // KeyUp
{KM_CORE_IT_END}
}
},
{"LDML_Ctrl_VKey_X", "ldml/keyboards/k_000_minimal_keyboard.kmx", KM_CORE_VKEY_X, KM_CORE_MODIFIER_LCTRL, u"x", true,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END} // LDML: no output without rule
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// Frame key without rule
{"LDML_VKey_Enter", "ldml/keyboards/k_000_minimal_keyboard.kmx", KM_CORE_VKEY_ENTER, 0, u"x", false,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// Backspace (with available context)
{"LDML_VKey_Backspace_Ctxt", "ldml/keyboards/k_000_minimal_keyboard.kmx", KM_CORE_VKEY_BKSP, 0, u"x", true,
{ // KeyDown
// Once we use C++ 20 we can use:
//{KM_CORE_IT_BACK, { 0, }, {.backspace = {KM_CORE_BT_CHAR, 0}}},
{KM_CORE_IT_BACK, { 0, }, {backspace_no_context.value}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_END}
}
},
// Backspace (without context)
{"LDML_VKey_Backspace_NoCtxt", "ldml/keyboards/k_000_minimal_keyboard.kmx", KM_CORE_VKEY_BKSP, 0, NULL, false,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// Ctrl+Backspace (with context)
{"LDML_Ctrl_VKey_Backspace_Ctxt", "ldml/keyboards/k_000_minimal_keyboard.kmx", KM_CORE_VKEY_BKSP, KM_CORE_MODIFIER_LCTRL, u"x", false,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// Ctrl+Backspace (no context)
{"LDML_Ctrl_VKey_Backspace_NoCtxt", "ldml/keyboards/k_000_minimal_keyboard.kmx", KM_CORE_VKEY_BKSP, KM_CORE_MODIFIER_LCTRL, NULL, false,
{ // KeyDown
{KM_CORE_IT_INVALIDATE_CONTEXT, { 0, }, {0}},
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
// Modifier frame key
{"LDML_VKey_Shift", "ldml/keyboards/k_000_minimal_keyboard.kmx", KM_CORE_VKEY_SHIFT, 0, u"x", false,
{ // KeyDown
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
},
{ // KeyUp
{KM_CORE_IT_EMIT_KEYSTROKE, { 0, }, {0}},
{KM_CORE_IT_END}
}
},
};
INSTANTIATE_TEST_SUITE_P(KMXProcessEvent, ProcessEventTests, testing::ValuesIn(values), GenerateTestName);
// provide our own `main` so that we can get the path of the exe so that
// we have a well-defined location to find our test keyboards
int main(int argc, char** argv) {
#ifdef __EMSCRIPTEN__
test_dir = get_wasm_file_path(km::core::path(argv[0]).parent());
#else
test_dir = km::core::path(argv[0]).parent();
#endif
std::cout << "test_dir=" << test_dir.c_str() << std::endl;
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View file

@ -48,6 +48,18 @@ kmcorekeyboardapitests = executable('km_core_keyboard_api.tests',
test('km-core-keyboard-api-tests', kmcorekeyboardapitests)
# tests for km_core_process_event
km_core_process_event_tests_exe = executable(
'km_core_process_event_tests',
['km_core_process_event.tests.cpp', common_test_files],
include_directories: [inc, libsrc],
cpp_args: defns + warns,
link_args: [ links, extra_link_args ],
dependencies: [icu_uc, icu_i18n, gtest_dep, gmock_dep],
objects: lib.extract_all_objects(recursive: false),
)
test('km_core_process_event', km_core_process_event_tests_exe)
subdir('json')
subdir('utftest')
subdir('kmnkbd')

View file

@ -182,6 +182,7 @@ export class LexicalModelCompiler implements KeymanCompiler {
const sources: string[] = modelSource.sources.map(function(source) {
return new TextDecoder().decode(callbacks.loadFile(callbacks.path.join(sourcePath, source)));
});
func += `globalThis.exports = globalThis.exports ?? {};\n`;
func += this.transpileSources(sources).join('\n');
func += `LMLayerWorker.loadModel(new ${modelSource.rootClass}());\n`;
break;

View file

@ -19,6 +19,7 @@ describe('LexicalModelCompiler', function () {
'example.qaa.wordbreaker',
'example.qaa.joinwordbreaker',
'example.qaa.scriptusesspaces',
'example.qaa.custom',
];
for (const modelID of MODELS) {
@ -37,7 +38,11 @@ describe('LexicalModelCompiler', function () {
assert.isFalse(compilation.hasSyntaxError, 'model code had syntax error');
assert.isNull(compilation.error, `compilation error: ${compilation.error}`);
assert.equal(compilation.modelConstructorName, 'TrieModel');
if(modelID == 'example.qaa.custom') {
assert.isNull(compilation.modelConstructorName);
} else {
assert.equal(compilation.modelConstructorName, 'TrieModel');
}
});
}
});

View file

@ -0,0 +1,25 @@
import { LexicalModelTypes } from '@keymanapp/common-types';
export class ExampleCustomModel implements LexicalModelTypes.LexicalModel {
configure(capabilities: LexicalModelTypes.Capabilities): LexicalModelTypes.Configuration {
return {
leftContextCodePoints: 16,
rightContextCodePoints: 0,
wordbreaksAfterSuggestions: false,
}
}
languageUsesCasing: boolean = true;
predict(transform: LexicalModelTypes.Transform, context: LexicalModelTypes.Context): LexicalModelTypes.Distribution<LexicalModelTypes.Suggestion> {
if(transform.deleteLeft == 0 && context.left.endsWith('te') && transform.insert == 'h') {
return [
{ p: 0.3, sample: { displayAs: 'the', transform: { deleteLeft: 2, insert: 'the' }, tag: 'correction' } },
{ p: 0.2, sample: { displayAs: 'them', transform: { deleteLeft: 2, insert: 'them' }, tag: 'correction' } },
{ p: 0.1, sample: { displayAs: 'tee hee', transform: { deleteLeft: 2, insert: 'tee hee' }, tag: 'correction' } },
];
} else {
return [];
}
}
}

View file

@ -0,0 +1,6 @@
const source: LexicalModelSource = {
format: 'custom-1.0',
rootClass: 'ExampleCustomModel',
sources: ['ExampleCustomModel.ts'],
};
export default source;

View file

@ -88,9 +88,9 @@
}
},
"node_modules/brace-expansion": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dependencies": {
"balanced-match": "^4.0.2"
},
@ -540,9 +540,9 @@
}
},
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"engines": {
"node": ">=12"
},
@ -769,9 +769,9 @@
}
},
"brace-expansion": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"requires": {
"balanced-match": "^4.0.2"
}
@ -1099,9 +1099,9 @@
}
},
"picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="
},
"proc-log": {
"version": "6.1.0",

View file

@ -91,12 +91,17 @@ var
begin
lm := TLexicalModelParser.Create(m.Text);
try
// TODO: LexicalModelParser does not support `rootClass` which is needed for
// custom lexical models, and the property `Wordlists` is inappropriate for
// the list of sources files. This is part of a bigger project for better
// custom model support in TIKE
//
// For now, this test excludes the `rootClass` property in order to pass
lm.Comment := 'Testing';
lm.Format := lmfCustom10;
lm.WordBreaker := lmwbAscii;
lm.Wordlists.Clear;
lm.Wordlists.Add('foo.tsv');
lm.Wordlists.Add('bar.tsv');
lm.Wordlists.Add('CustomModel.ts');
Assert.AreEqual(mcustom.Text.Trim, lm.Text.Trim); // Ignoring whitespace before/after
finally
lm.Free;

View file

@ -2,7 +2,7 @@
const source: LexicalModelSource = {
format: 'custom-1.0',
wordBreaker: 'ascii',
sources: ['foo.tsv', 'bar.tsv']
sources: ['CustomModel.ts']
};
export default source;

View file

@ -782,7 +782,7 @@ commit_current_queue_item(IBusKeymanEngine *keyman) {
ibus_engine_forward_key_event(engine, current_item->keyval, current_item->keycode, current_item->state);
}
keyman->commit_item--;
memmove(keyman->commit_queue, &keyman->commit_queue[1], sizeof(commit_queue_item) * MAX_QUEUE_SIZE - 1);
memmove(keyman->commit_queue, &keyman->commit_queue[1], sizeof(commit_queue_item) * (MAX_QUEUE_SIZE - 1));
initialize_queue_items(keyman, MAX_QUEUE_SIZE - 1, 1);
}

View file

@ -8,6 +8,7 @@
#import "KMInputMethodEventHandler.h"
#import <KeymanEngine4Mac/KeymanEngine4Mac.h>
#import <Carbon/Carbon.h> /* For kVK_ constants. */
#import <CoreFoundation/CoreFoundation.h>
#import "KeySender.h"
#import "TextApiCompliance.h"
#import "KMSettingsRepository.h"
@ -370,7 +371,7 @@ CGEventSourceRef _sourceForGeneratedEvent = nil;
contextString = attributedString.string;
//only uncomment for testing as we do not want to write context in logs
//os_log_debug([KMLogs testLog], " length: %lu result: %{public}@", contextString.length, contextString);
//os_log_debug([KMLogs keyTraceLog], " length: %lu result: %{public}@", contextString.length, contextString);
}
}
}
@ -405,18 +406,7 @@ CGEventSourceRef _sourceForGeneratedEvent = nil;
[KMSentryHelper addDebugBreadCrumb:@"event" message:message];
[self insertAndReplaceTextForOutput:output client:client];
} else if (output.isDeleteOnlyScenario) {
if ((event.keyCode == kVK_Delete) && output.codePointsToDeleteBeforeInsert == 1) {
// let the delete pass through in the original event rather than sending a new delete
NSString *message = @"applyOutputToTextInputClient, delete only scenario with passthrough";
os_log_debug([KMLogs keyTraceLog], "%@", message);
[KMSentryHelper addDebugBreadCrumb:@"event" message:message];
handledEvent = NO;
} else {
NSString *message = @"applyOutputToTextInputClient, delete only scenario";
os_log_debug([KMLogs keyTraceLog], "%@", message);
[KMSentryHelper addDebugBreadCrumb:@"event" message:message];
[self sendEvents:event forOutput:output];
}
handledEvent = [self handleDeleteOnlyScenario:output keyDownEvent:event client:client];
} else if (output.isDeleteAndInsertScenario) {
// TODO: fix issue #10246
/*
@ -511,6 +501,220 @@ CGEventSourceRef _sourceForGeneratedEvent = nil;
}
}
/**
* Handles deleting without an associated insert in one of three methods:
* 1. delete via replace: do the delete by replacing two (or more) characters with one.
* 2. backspace passthrough : if the original keydown event was a backspace, pass it through unhandled
* 3. generate event: generate keydown backspace events as necessary
*/
-(BOOL)handleDeleteOnlyScenario:(CoreKeyOutput*)output keyDownEvent:(nonnull NSEvent *)event client:(id) client {
// attempt to delete by replacing -- for compliant apps only
if ([self handleDeleteWithReplacement:output keyDownEvent:event client:client]) {
return YES;
}
// pass through if this was a backspace keydown event
if (event.keyCode == kVK_Delete && output.codePointsToDeleteBeforeInsert == 1) {
// let the delete pass through in the original event rather than sending a new delete
NSString *message = @"handleDeleteOnlyForOutput, delete only scenario with passthrough";
os_log_debug([KMLogs keyTraceLog], "%@", message);
[KMSentryHelper addDebugBreadCrumb:@"event" message:message];
// instruct system to handle the event
return NO;
}
else {
// otherwise generate a backspace
NSString *message = @"handleDeleteOnlyForOutput, send backspace event";
os_log_debug([KMLogs keyTraceLog], "%@", message);
[KMSentryHelper addDebugBreadCrumb:@"event" message:message];
[self sendEvents:event forOutput:output];
return YES;
}
}
/**
* For compliant apps only.
*
* This an attempt to make sure that deletion removes only the expected codepoints.
* When handling a transform which only deletes a character, or when allowing a
* backspace to pass through, the OS or application may not use the same rules
* around deletion as Keyman -- especially when deleting clusters such as letter +
* combining diacritic (e.g. `U+0062 U+0301`), where some applications may delete
* both together as they represent a single 'grapheme cluster'.
*
* (Note, the question of whether it is appropriate for backspace to delete a
* cluster rather than a codepoint from an end-user perspective is not relevant
* here, because what is important is that we match the rules that the keyboard has
* provided, which means we need a method of deleting a precise number of
* codepoints. The keyboard author can and should include rules for cluster
* deletion that meet end-user expectations.)
*
* The `insertText` API takes two parameters: a string to insert, and a range to
* replace with that string. However, we cannot simply pass through a zero-length
* insertion string along with the range to delete, because the `insertText` API
* treats this as an invalid call and ignores it.
*
* Instead, we can delete the desired number of codepoints only by using the
* `insertText` API to replace e.g. two codepoints with one.
*
* This method only works for compliant apps because non-compliant apps do not
* support the `insertText` API.
*
* Ref: https://developer.apple.com/documentation/appkit/nstextinputclient/inserttext(_:replacementrange:)
*/
-(BOOL)handleDeleteWithReplacement:(CoreKeyOutput*)output keyDownEvent:(nonnull NSEvent *)event client:(id) client {
BOOL handledEvent = NO;
NSString *context = [self readContext:event forClient:client];
// guard: only for compliant apps with sufficient context
if (!(self.apiCompliance.canReplaceText) || ([context length] <= output.textToDelete.length)) {
os_log_debug([KMLogs keyTraceLog], "cannot replace text, non-compliant or insufficient context");
return NO; // return without deleting/replacing
}
// guard: the logic of this method depends on locating textToDelete in the context
if (![self stringToDeleteMatchesContextSuffix:output.textToDelete context:context]) {
os_log_debug([KMLogs keyTraceLog], "cannot replace text, textToDelete not found at end of context");
return NO; // return without deleting/replacing
}
NSUInteger deletionTargetLength = output.textToDelete.length;
NSUInteger deletionTargetLocation = context.length-deletionTargetLength;
NSUInteger precedingCharacterLocation = deletionTargetLocation - 1;
// if the preceding character is the trailing half of a surrogate pair
// then delete by replacing with the entire surrogate pair
if ([self precededBySurrogatePair:precedingCharacterLocation context:context]) {
handledEvent = [self deleteByReplacingWithPrecedingSurrogate:precedingCharacterLocation deleteLength:deletionTargetLength context:context client:client];
} else {
// otherwise replace with only the preceding character
handledEvent = [self deleteByReplacingWithPrecedingCharacter:precedingCharacterLocation deleteLength:deletionTargetLength context:context client:client];
}
return handledEvent;
}
/**
* Check whether the string to be deleted is found at the tail end of the current context.
*/
-(BOOL) stringToDeleteMatchesContextSuffix:(NSString*)textToDelete context:(NSString*) context {
BOOL doesMatch = NO;
// get length of string to delete and compare to end of context
NSUInteger deleteLength = textToDelete.length;
NSUInteger locationOfDeletionTarget = context.length-deleteLength;
NSUInteger locationOfPrecedingCharacter = locationOfDeletionTarget - 1;
NSString *contextSuffix = [context substringFromIndex:context.length-deleteLength];
os_log_debug([KMLogs keyTraceLog], "stringToDeleteMatchesSuffix, textToDelete: '%{public}@', contextSuffix: '%{public}@', locationOfDeletionTarget: %u, locationOfprecedingCharacter: %u", textToDelete, contextSuffix, (int)locationOfDeletionTarget, (int)locationOfPrecedingCharacter);
doesMatch = [textToDelete isEqualToString:contextSuffix];
os_log_debug([KMLogs keyTraceLog], "stringToDeleteMatchesSuffix: %{public}@", doesMatch?@"YES":@"NO");
return doesMatch;
}
/**
* Check whether the preceding character, which is to be used for the replacement,
* is part of a surrogate pair that is distinct from the character being deleted.
*/
-(BOOL) precededBySurrogatePair: (NSUInteger)precedingLocation context:(NSString*) context {
BOOL precedingCharacterIsLowSurrogate = false;
unichar precedingCharacter = [context characterAtIndex:precedingLocation];
if (CFStringIsSurrogateHighCharacter(precedingCharacter)) {
// preceding character is high character -- unexpected from Keyman Core
// write to log and return false
precedingCharacterIsLowSurrogate = false;
NSString *message = [NSString stringWithFormat:@"Unexpected high surrogate found for preceding character at %ld", (long)precedingCharacter];
os_log_debug([KMLogs keyTraceLog], "%{public}@", message);
[KMSentryHelper addDebugBreadCrumb:@"event" message:message];
} else if (CFStringIsSurrogateLowCharacter(precedingCharacter)) {
// preceding character is low surrogate
precedingCharacterIsLowSurrogate = true;
}
return precedingCharacterIsLowSurrogate;
}
/**
* Replace both the text to delete and the character preceding it solely with the character that precedes it.
* Returns YES if executing the replace/delete and NO otherwise.
*/
-(BOOL) deleteByReplacingWithPrecedingCharacter:(NSUInteger)precedingCharacterLocation deleteLength:(NSUInteger)deleteLength context:(NSString*) context client:(id) client {
os_log_debug([KMLogs keyTraceLog], "deleteByReplacingWithPrecedingCharacter");
// get the preceding character
NSRange precedingCharacterRange = NSMakeRange(precedingCharacterLocation, 1);
NSString *replacementString = [context substringWithRange:precedingCharacterRange];
// guard: if preceding character is a control character, return NO
if ([self containsControlCharacter:replacementString]) {
NSString *message = @"replacementString contains control characters, cannot delete with replace";
os_log_debug([KMLogs keyTraceLog], "%@", message);
[KMSentryHelper addDebugBreadCrumb:@"event" message:message];
return NO;
}
// perform the replacement
NSUInteger replacementLength = [replacementString length] + deleteLength;
NSRange replacementRange = NSMakeRange(precedingCharacterLocation, replacementLength);
os_log_debug([KMLogs keyTraceLog], "replacementRange: %{public}@", NSStringFromRange(replacementRange));
[client insertText:replacementString replacementRange:replacementRange];
return YES;
}
/**
* Replace both the text to delete and the surrogate pair preceding it with the surrogate pair preceding it.
* Returns YES if executing the replace/delete and NO otherwise.
*/
-(BOOL) deleteByReplacingWithPrecedingSurrogate:(NSUInteger)precedingCharacterLocation deleteLength:(NSUInteger)deleteLength context:(NSString*) context client:(id) client {
os_log_debug([KMLogs keyTraceLog], "deleteByReplacingWithPrecedingSurrogate");
// guard: return NO if there is no character before the precedingCharacterLocation
if (precedingCharacterLocation <= 0) {
NSString *message = @"no characters exist before precedingCharacterLocation, so it cannot be a surrogate pair";
os_log_debug([KMLogs keyTraceLog], "%@", message);
return NO;
}
// get the preceding character
NSRange precedingCharacterRange = NSMakeRange(precedingCharacterLocation - 1, 2);
NSString *replacementString = [context substringWithRange:precedingCharacterRange];
unichar highCharacter = [replacementString characterAtIndex:0];
unichar lowCharacter = [replacementString characterAtIndex:1];
// verify that preceding characters comprise a surrogate pair
if ((CFStringIsSurrogateHighCharacter(highCharacter)) && (CFStringIsSurrogateLowCharacter(lowCharacter))) {
// found preceding surrogate as expected
} else {
NSString *message = [NSString stringWithFormat:@"Preceding characters of string do not comprise a surrogate pair: 0x%02x, 0x%02x", (unsigned int)highCharacter, (unsigned int)lowCharacter];
os_log_debug([KMLogs keyTraceLog], "%@", message);
[KMSentryHelper addDebugBreadCrumb:@"event" message:message];
return NO;
}
// perform the replacement
NSUInteger replacementLength = [replacementString length] + deleteLength;
NSRange replacementRange = NSMakeRange(precedingCharacterRange.location, replacementLength);
os_log_debug([KMLogs keyTraceLog], "replacementRange: %{public}@", NSStringFromRange(replacementRange));
[client insertText:replacementString replacementRange:replacementRange];
return YES;
}
-(BOOL) containsControlCharacter:(NSString*)text {
NSCharacterSet *controlSet = [NSCharacterSet controlCharacterSet];
NSRange range = [text rangeOfCharacterFromSet:controlSet];
return (range.location != NSNotFound);
}
/**
* Calculates the range where text will be inserted and replace existing text.
* Returning {NSNotFound, NSNotFound} for range signifies to insert at current location without replacement.

View file

@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN
-(void) checkComplianceAfterInsert:(NSString *)insertedText deleted:(NSString *)deletedText;
-(BOOL)isComplianceUncertain;
-(BOOL)canReadText;
-(BOOL)canReplaceText;
-(BOOL)mustBackspaceUsingEvents;
@end

91
package-lock.json generated
View file

@ -4823,15 +4823,6 @@
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
"node_modules/arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/argparse": {
"version": "2.0.1",
"dev": true,
@ -5955,15 +5946,6 @@
"version": "1.0.3",
"license": "MIT"
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -7963,7 +7945,9 @@
}
},
"node_modules/flatted": {
"version": "3.2.5",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true,
"license": "ISC"
},
@ -9882,15 +9866,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true,
"license": "ISC",
"optional": true,
"peer": true
},
"node_modules/make-fetch-happen": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz",
@ -11288,10 +11263,11 @@
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
@ -12895,47 +12871,6 @@
"typescript": ">=4.2.0"
}
},
"node_modules/ts-node": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz",
"integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"source-map-support": "^0.5.17",
"yn": "3.1.1"
},
"bin": {
"ts-node": "dist/bin.js",
"ts-node-script": "dist/bin-script.js",
"ts-node-transpile-only": "dist/bin-transpile.js",
"ts-script": "dist/bin-script-deprecated.js"
},
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"typescript": ">=2.7"
}
},
"node_modules/ts-node/node_modules/diff": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
"integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"peer": true,
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/tsc-watch": {
"version": "4.6.2",
"dev": true,
@ -13772,18 +13707,6 @@
"node": ">= 4.0.0"
}
},
"node_modules/yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=6"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"dev": true,

View file

@ -32,6 +32,6 @@ jobs:
steps:
- name: Check PR build status
id: run_pr_build_status_script
uses: actions/github-script@v7
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |

View file

@ -65,8 +65,26 @@ _tc_rsync() {
else
local RSYNC=rsync
if builder_is_macos; then
# We need to look for a newer version of rsync because the version bundled
# with macos does not work correctly; we assume that this is installed
# with Homebrew. On Intel, this is found in /usr/local/bin, and on M1, it
# is found in /opt/homebrew/bin.
RSYNC=/usr/local/bin/rsync
[[ -f /opt/homebrew/bin/rsync ]] && RSYNC=/opt/homebrew/bin/rsync
[[ -x /opt/homebrew/bin/rsync ]] && RSYNC=/opt/homebrew/bin/rsync
# On build agents, we unlink rsync from homebrew, because otherwise xcode
# builds which use rsync internally fail, so we need to specify the full
# path to rsync. Generally, rsync is not needed on developer machines as
# this is only used for release builds from the build agents. #15764
if [[ ! -x "${RSYNC}" ]]; then
# Try and find the rsync binary with `brew list`. It should be something
# like `/opt/homebrew/Cellar/rsync/3.4.1/bin/rsync`
RSYNC="$(brew list rsync | grep /bin/rsync\$)"
if [[ ! -x "${RSYNC}" ]]; then
builder_die "Could not find Homebrew-installed rsync anywhere"
fi
fi
fi
${RSYNC} "${rsync_args[@]}"

View file

@ -12,9 +12,13 @@ ba_mac_unlock_keychain() {
}
ba_mac_clean_xcode_derived_data() {
builder_echo start "clean" "Cleaning XCode DerivedData mess"
builder_echo start "clean" "Cleaning XCode DerivedData and Carthage cache mess"
rm -rf "${HOME}/Library/Developer/Xcode/DerivedData"
builder_echo end "clean" success "Finished cleaning XCode DerivedData mess"
# https://stackoverflow.com/a/45504898/1836776
rm -rf "${HOME}/Library/Caches/org.carthage.CarthageKit"
builder_echo end "clean" success "Finished cleaning XCode DerivedData and Carthage cache mess"
}
ba_mac_unmount_volumes_keyman() {