From 484a237002c9e7ea1b777470c01870437ee23cc2 Mon Sep 17 00:00:00 2001 From: Sebastian Keller Date: Wed, 21 Feb 2024 15:20:24 +0100 Subject: [PATCH 1/2] keyboard: Fix deleting the previous word Since mutter@33088d59 the cursor we receive from mutter already is a character index while the code here still treated it like a byte offset. Further the code to detect the previous word position was treating the cursor parameter already like a character index, while passing the cursor that was prior to that commit a byte offset. The function also had some unreachable and redundant code paths. The pos < 0 case can never be reached due to the max(). Also the regex already ensures that all whitespace is considered, so the code to remove spaces not actually do anything except when deleting the first word in the text, in which it would cause the first character to not get deleted. Also it was not handling characters with more than 2 bytes correctly. In the presence of these JS string functions, such as search(), can not be considered to operate on character indices anymore but rather the number of UTF-16 byte pairs. Issues with this can be avoided by using iterators, which unlike anything else iterate on characters, not byte pairs and by not using the results returned by JS string functions for anything but JS strings. Part-of: --- js/ui/keyboard.js | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/js/ui/keyboard.js b/js/ui/keyboard.js index 1aa47ae47b..180bc6b294 100644 --- a/js/ui/keyboard.js +++ b/js/ui/keyboard.js @@ -1619,18 +1619,10 @@ export const Keyboard = GObject.registerClass({ } _previousWordPosition(text, cursor) { - /* Skip word prior to cursor */ - let pos = Math.max(0, text.slice(0, cursor).search(/\s+\S+\s*$/)); - if (pos < 0) - return 0; - - /* Skip contiguous spaces */ - for (; pos >= 0; pos--) { - if (text.charAt(pos) !== ' ') - return GLib.utf8_strlen(text.slice(0, pos + 1), -1); - } - - return 0; + const upToCursor = [...text].slice(0, cursor).join(''); + const jsStringPos = Math.max(0, upToCursor.search(/\s+\S+\s*$/)); + const charPos = GLib.utf8_strlen(text.slice(0, jsStringPos), -1); + return charPos; } _toggleDelete(enabled) { @@ -1662,17 +1654,10 @@ export const Keyboard = GObject.registerClass({ if (cursor === 0) return; - let encoder = new TextEncoder(); - let decoder = new TextDecoder(); - - /* Find cursor/anchor position in characters */ - const cursorIdx = GLib.utf8_strlen(decoder.decode(encoder.encode( - text).slice(0, cursor)), -1); - const anchorIdx = this._timesDeleted < BACKSPACE_WORD_DELETE_THRESHOLD - ? cursorIdx - 1 + const anchor = this._timesDeleted < BACKSPACE_WORD_DELETE_THRESHOLD + ? cursor - 1 : this._previousWordPosition(text, cursor); - /* Now get offset from cursor */ - const offset = anchorIdx - cursorIdx; + const offset = anchor - cursor; this._timesDeleted++; Main.inputMethod.delete_surrounding(offset, Math.abs(offset)); -- GitLab From 7a409bfffc87230d11d27d7e876e75f32632285b Mon Sep 17 00:00:00 2001 From: Sebastian Keller Date: Sat, 22 Apr 2023 00:05:22 +0200 Subject: [PATCH 2/2] keyboard: Delete selected text on backspace Previously backspace would only ever remove a single character left of the cursor, regardless of selection. This requires the application to correctly set the anchor position in text_input::set_surrounding_text(), which currently only gtk4 seems to do. When there is no selection or on other applications that always set cursor = anchor, like gtk3 does, the behavior is not changed and still only deletes one character. Part-of: --- js/misc/inputMethod.js | 8 +++++++- js/ui/keyboard.js | 35 +++++++++++++++++++++++------------ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/js/misc/inputMethod.js b/js/misc/inputMethod.js index d169ea8cbe..53af39d858 100644 --- a/js/misc/inputMethod.js +++ b/js/misc/inputMethod.js @@ -227,6 +227,7 @@ export const InputMethod = GObject.registerClass({ this._surroundingText = null; this._surroundingTextCursor = null; + this._surroundingTextAnchor = null; this._preeditStr = null; this._setTerminalMode(false); } @@ -247,6 +248,7 @@ export const InputMethod = GObject.registerClass({ vfunc_set_surrounding(text, cursor, anchor) { this._surroundingText = text; this._surroundingTextCursor = cursor; + this._surroundingTextAnchor = anchor; this.emit('surrounding-text-set'); if (!this._context || (!text && text !== '')) @@ -348,7 +350,11 @@ export const InputMethod = GObject.registerClass({ } getSurroundingText() { - return [this._surroundingText, this._surroundingTextCursor]; + return [ + this._surroundingText, + this._surroundingTextCursor, + this._surroundingTextAnchor, + ]; } hasPreedit() { diff --git a/js/ui/keyboard.js b/js/ui/keyboard.js index 180bc6b294..38f1b8ee3a 100644 --- a/js/ui/keyboard.js +++ b/js/ui/keyboard.js @@ -1650,24 +1650,35 @@ export const Keyboard = GObject.registerClass({ } if (enabled) { - let func = (text, cursor) => { - if (cursor === 0) + let func = (text, cursor, anchor) => { + if (cursor === 0 && anchor === 0) return; - const anchor = this._timesDeleted < BACKSPACE_WORD_DELETE_THRESHOLD - ? cursor - 1 - : this._previousWordPosition(text, cursor); - const offset = anchor - cursor; + let offset, len; + if (cursor > anchor) { + offset = anchor - cursor; + len = -offset; + } else if (cursor < anchor) { + offset = 0; + len = anchor - cursor; + } else if (this._timesDeleted < BACKSPACE_WORD_DELETE_THRESHOLD) { + offset = -1; + len = 1; + } else { + const wordLength = cursor - this._previousWordPosition(text, cursor); + offset = -wordLength; + len = wordLength; + } this._timesDeleted++; - Main.inputMethod.delete_surrounding(offset, Math.abs(offset)); + Main.inputMethod.delete_surrounding(offset, len); }; this._surroundingUpdateId = Main.inputMethod.connect( 'surrounding-text-set', () => { - let [text, cursor] = Main.inputMethod.getSurroundingText(); + let [text, cursor, anchor] = Main.inputMethod.getSurroundingText(); if (this._timesDeleted === 0) { - func(text, cursor); + func(text, cursor, anchor); } else { if (this._surroundingUpdateTimeoutId > 0) { GLib.source_remove(this._surroundingUpdateTimeoutId); @@ -1675,16 +1686,16 @@ export const Keyboard = GObject.registerClass({ } this._surroundingUpdateTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, KEY_RELEASE_TIMEOUT, () => { - func(text, cursor); + func(text, cursor, cursor); this._surroundingUpdateTimeoutId = 0; return GLib.SOURCE_REMOVE; }); } }); - let [text, cursor] = Main.inputMethod.getSurroundingText(); + let [text, cursor, anchor] = Main.inputMethod.getSurroundingText(); if (text) - func(text, cursor); + func(text, cursor, anchor); else Main.inputMethod.request_surrounding(); } else { -- GitLab