diff --git a/.github/actions/build-binary-packages/action.yml b/.github/actions/build-binary-packages/action.yml index e764a26ac7..1483f58e5c 100644 --- a/.github/actions/build-binary-packages/action.yml +++ b/.github/actions/build-binary-packages/action.yml @@ -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: | diff --git a/.github/labeler.yml b/.github/labeler.yml index fc37bf64b7..2549340624 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -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/** diff --git a/.github/workflows/api-verification.yml b/.github/workflows/api-verification.yml index af6bc682dc..56726817dc 100644 --- a/.github/workflows/api-verification.yml +++ b/.github/workflows/api-verification.yml @@ -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 diff --git a/.github/workflows/build-test-publish-docker.yml b/.github/workflows/build-test-publish-docker.yml index 3c85d4ff91..edb9ebb98f 100644 --- a/.github/workflows/build-test-publish-docker.yml +++ b/.github/workflows/build-test-publish-docker.yml @@ -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 diff --git a/.github/workflows/core-arm64-windows-test.yml b/.github/workflows/core-arm64-windows-test.yml index c8b7dd8a13..0eb4e8f8f6 100644 --- a/.github/workflows/core-arm64-windows-test.yml +++ b/.github/workflows/core-arm64-windows-test.yml @@ -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' diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml index 7503d51d21..d0c14de948 100644 --- a/.github/workflows/crowdin.yml +++ b/.github/workflows/crowdin.yml @@ -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 diff --git a/.github/workflows/deb-packaging.yml b/.github/workflows/deb-packaging.yml index 36bfd0d0ce..6e7fe54cb3 100644 --- a/.github/workflows/deb-packaging.yml +++ b/.github/workflows/deb-packaging.yml @@ -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 diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 42d9f79363..ee7fe7d03d 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -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 diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index fc94cc688e..551045c056 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -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 }}' diff --git a/.github/workflows/pr-build-status.yml b/.github/workflows/pr-build-status.yml index 978a2b0995..bd5fe2f46a 100644 --- a/.github/workflows/pr-build-status.yml +++ b/.github/workflows/pr-build-status.yml @@ -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 diff --git a/HISTORY.md b/HISTORY.md index 8a045c92e0..2ac9f698eb 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -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) diff --git a/VERSION.md b/VERSION.md index 78ca871179..ac94731296 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.216 \ No newline at end of file +19.0.223 \ No newline at end of file diff --git a/common/schemas/keyman-touch-layout/README.md b/common/schemas/keyman-touch-layout/README.md index 1276bed54c..e6984ca142 100644 --- a/common/schemas/keyman-touch-layout/README.md +++ b/common/schemas/keyman-touch-layout/README.md @@ -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. diff --git a/common/schemas/keyman-touch-layout/keyman-touch-layout.clean.spec.json b/common/schemas/keyman-touch-layout/keyman-touch-layout.clean.spec.json index 8bdbf7f66a..032d6ee49f 100644 --- a/common/schemas/keyman-touch-layout/keyman-touch-layout.clean.spec.json +++ b/common/schemas/keyman-touch-layout/keyman-touch-layout.clean.spec.json @@ -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 diff --git a/core/src/km_core_processevent_api.cpp b/core/src/km_core_processevent_api.cpp index a2bca95ea8..ade2665353 100644 --- a/core/src/km_core_processevent_api.cpp +++ b/core/src/km_core_processevent_api.cpp @@ -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; } diff --git a/core/src/ldml/ldml_processor.cpp b/core/src/ldml/ldml_processor.cpp index 9ef36b62fc..e79a1f90be 100644 --- a/core/src/ldml/ldml_processor.cpp +++ b/core/src/ldml/ldml_processor.cpp @@ -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()); diff --git a/core/src/ldml/ldml_processor.hpp b/core/src/ldml/ldml_processor.hpp index b673a9275c..a10294cc32 100644 --- a/core/src/ldml/ldml_processor.hpp +++ b/core/src/ldml/ldml_processor.hpp @@ -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; diff --git a/core/src/state.cpp b/core/src/state.cpp index c6142b3456..47f29d0a67 100644 --- a/core/src/state.cpp +++ b/core/src/state.cpp @@ -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(&_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); -} \ No newline at end of file +} diff --git a/core/src/state.hpp b/core/src/state.hpp index 49eea8ffd2..0cf58fb614 100644 --- a/core/src/state.hpp +++ b/core/src/state.hpp @@ -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 diff --git a/core/tests/unit/km_core_process_event.tests.cpp b/core/tests/unit/km_core_process_event.tests.cpp new file mode 100644 index 0000000000..67b84760b3 --- /dev/null +++ b/core/tests/unit/km_core_process_event.tests.cpp @@ -0,0 +1,355 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ +#include +#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 keydown_actions; + std::initializer_list keyup_actions; +}; + +std::string GenerateTestName(const testing::TestParamInfo& info) { + return info.param.test_name; +} + +class ProcessEventTests : public testing::TestWithParam { +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(); +} diff --git a/core/tests/unit/meson.build b/core/tests/unit/meson.build index 0637ace96e..5456650372 100644 --- a/core/tests/unit/meson.build +++ b/core/tests/unit/meson.build @@ -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') diff --git a/developer/src/kmc-model/src/lexical-model-compiler.ts b/developer/src/kmc-model/src/lexical-model-compiler.ts index 54b50ba503..9e0a03611a 100644 --- a/developer/src/kmc-model/src/lexical-model-compiler.ts +++ b/developer/src/kmc-model/src/lexical-model-compiler.ts @@ -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; diff --git a/developer/src/kmc-model/test/compile-model.tests.ts b/developer/src/kmc-model/test/compile-model.tests.ts index 5c6a8ab104..ac7baa373e 100644 --- a/developer/src/kmc-model/test/compile-model.tests.ts +++ b/developer/src/kmc-model/test/compile-model.tests.ts @@ -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'); + } }); } }); diff --git a/developer/src/kmc-model/test/fixtures/example.qaa.custom/ExampleCustomModel.ts b/developer/src/kmc-model/test/fixtures/example.qaa.custom/ExampleCustomModel.ts new file mode 100644 index 0000000000..6cd95e8a35 --- /dev/null +++ b/developer/src/kmc-model/test/fixtures/example.qaa.custom/ExampleCustomModel.ts @@ -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 { + 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 []; + } + } +} diff --git a/developer/src/kmc-model/test/fixtures/example.qaa.custom/example.qaa.custom.model.ts b/developer/src/kmc-model/test/fixtures/example.qaa.custom/example.qaa.custom.model.ts new file mode 100644 index 0000000000..8f1d188f9d --- /dev/null +++ b/developer/src/kmc-model/test/fixtures/example.qaa.custom/example.qaa.custom.model.ts @@ -0,0 +1,6 @@ +const source: LexicalModelSource = { + format: 'custom-1.0', + rootClass: 'ExampleCustomModel', + sources: ['ExampleCustomModel.ts'], +}; +export default source; \ No newline at end of file diff --git a/developer/src/server/src/win32/trayicon/addon-src/package-lock.json b/developer/src/server/src/win32/trayicon/addon-src/package-lock.json index 3b320611f1..f1e2d400b8 100644 --- a/developer/src/server/src/win32/trayicon/addon-src/package-lock.json +++ b/developer/src/server/src/win32/trayicon/addon-src/package-lock.json @@ -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", diff --git a/developer/src/test/auto/model-ts-parser/Keyman.System.Test.LexicalModelParserTest.pas b/developer/src/test/auto/model-ts-parser/Keyman.System.Test.LexicalModelParserTest.pas index 11dcd5a4a9..b26fb863f8 100644 --- a/developer/src/test/auto/model-ts-parser/Keyman.System.Test.LexicalModelParserTest.pas +++ b/developer/src/test/auto/model-ts-parser/Keyman.System.Test.LexicalModelParserTest.pas @@ -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; diff --git a/developer/src/test/auto/model-ts-parser/assets/custom.model.ts b/developer/src/test/auto/model-ts-parser/assets/custom.model.ts index 64e8f8e122..c22dfe0a1a 100644 --- a/developer/src/test/auto/model-ts-parser/assets/custom.model.ts +++ b/developer/src/test/auto/model-ts-parser/assets/custom.model.ts @@ -2,7 +2,7 @@ const source: LexicalModelSource = { format: 'custom-1.0', wordBreaker: 'ascii', - sources: ['foo.tsv', 'bar.tsv'] + sources: ['CustomModel.ts'] }; export default source; diff --git a/linux/ibus-keyman/src/engine.c b/linux/ibus-keyman/src/engine.c index 4ed09bc0ea..5b1ccb6252 100644 --- a/linux/ibus-keyman/src/engine.c +++ b/linux/ibus-keyman/src/engine.c @@ -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); } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m index dd9a8a3cee..9cf29e5b89 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -8,6 +8,7 @@ #import "KMInputMethodEventHandler.h" #import #import /* For kVK_ constants. */ +#import #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. diff --git a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.h b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.h index 1f5b55f17f..40c516a8db 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.h @@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN -(void) checkComplianceAfterInsert:(NSString *)insertedText deleted:(NSString *)deletedText; -(BOOL)isComplianceUncertain; -(BOOL)canReadText; +-(BOOL)canReplaceText; -(BOOL)mustBackspaceUsingEvents; @end diff --git a/package-lock.json b/package-lock.json index c05360af53..f9c71867b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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, diff --git a/resources/build/pr-build-status/pr-build-status.prefix.yml b/resources/build/pr-build-status/pr-build-status.prefix.yml index cbeba112a1..d82aa9cb77 100644 --- a/resources/build/pr-build-status/pr-build-status.prefix.yml +++ b/resources/build/pr-build-status/pr-build-status.prefix.yml @@ -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: | diff --git a/resources/teamcity/includes/tc-helpers.inc.sh b/resources/teamcity/includes/tc-helpers.inc.sh index b4c62826e1..6f704ef1f4 100644 --- a/resources/teamcity/includes/tc-helpers.inc.sh +++ b/resources/teamcity/includes/tc-helpers.inc.sh @@ -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[@]}" diff --git a/resources/teamcity/includes/tc-mac.inc.sh b/resources/teamcity/includes/tc-mac.inc.sh index 0e9c18013c..a138789cf5 100644 --- a/resources/teamcity/includes/tc-mac.inc.sh +++ b/resources/teamcity/includes/tc-mac.inc.sh @@ -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() {