From 43b14f3129b830d06f44d682b07ca1f5278d1854 Mon Sep 17 00:00:00 2001 From: miruka Date: Fri, 21 Aug 2020 04:44:55 -0400 Subject: [PATCH] Make autocompletion work not just at end of line --- src/gui/Base/HTextArea.qml | 18 +++++- src/gui/Pages/Chat/Composer/MessageArea.qml | 13 ++-- .../Chat/Composer/UserAutoCompletion.qml | 63 +++++++++++-------- src/gui/Utils.qml | 14 +++++ 4 files changed, 76 insertions(+), 32 deletions(-) diff --git a/src/gui/Base/HTextArea.qml b/src/gui/Base/HTextArea.qml index 6ac93cb0..87be2086 100644 --- a/src/gui/Base/HTextArea.qml +++ b/src/gui/Base/HTextArea.qml @@ -32,8 +32,22 @@ TextArea { signal customImagePaste() - function reset() { clear(); text = Qt.binding(() => defaultText || "") } - function insertAtCursor(text) { insert(cursorPosition, text) } + function reset() { + clear() + text = Qt.binding(() => defaultText || "") + } + + function insertAtCursor(text) { + insert(cursorPosition, text) + } + + function getWordAt(position) { + return utils.getWordAtPosition(text, position) + } + + function getWordBehindCursor() { + return cursorPosition === 0 ? null : getWordAt(cursorPosition - 1) + } text: defaultText || "" diff --git a/src/gui/Pages/Chat/Composer/MessageArea.qml b/src/gui/Pages/Chat/Composer/MessageArea.qml index 326051c3..e0b7af18 100644 --- a/src/gui/Pages/Chat/Composer/MessageArea.qml +++ b/src/gui/Pages/Chat/Composer/MessageArea.qml @@ -23,12 +23,12 @@ HTextArea { ModelStore.get("accounts").find(writingUserId) readonly property int cursorY: - area.text.substring(0, cursorPosition).split("\n").length - 1 + text.substring(0, cursorPosition).split("\n").length - 1 readonly property int cursorX: cursorPosition - lines.slice(0, cursorY).join("").length - cursorY - readonly property var lines: area.text.split("\n") + readonly property var lines: text.split("\n") readonly property string lineText: lines[cursorY] || "" readonly property string lineTextUntilCursor: @@ -209,14 +209,17 @@ HTextArea { } Keys.onBacktabPressed: ev => { - ev.accepted = true - autoCompletePrevious() + // if previous char isn't a space/tab/newline + if (text.slice(cursorPosition - 1, cursorPosition).trim()) { + ev.accepted = true + autoCompletePrevious() + } } Keys.onTabPressed: ev => { ev.accepted = true - if (text.slice(-1).trim()) { // previous char isn't a space/tab + if (text.slice(cursorPosition - 1, cursorPosition).trim()) { autoCompleteNext() return } diff --git a/src/gui/Pages/Chat/Composer/UserAutoCompletion.qml b/src/gui/Pages/Chat/Composer/UserAutoCompletion.qml index 68ba002a..67265ccc 100644 --- a/src/gui/Pages/Chat/Composer/UserAutoCompletion.qml +++ b/src/gui/Pages/Chat/Composer/UserAutoCompletion.qml @@ -6,28 +6,31 @@ import "../../.." import "../../../Base" import "../../../Base/HTile" -// FIXME: a b -> a @p b → @p doesn't trigger completion HListView { id: root property HTextArea textArea property bool open: false - property string originalText: "" + property var originalWord: null property bool autoOpenCompleted: false property var usersCompleted: ({}) // {displayName: userId} - readonly property bool autoOpen: - autoOpenCompleted || textArea.text.match(/.*(^|\W)@[^\s]+$/) + readonly property bool autoOpen: { + if (autoOpenCompleted) return true + const current = textArea.getWordBehindCursor() + return current ? /^@.+/.test(current.word) : false + } - readonly property string wordToComplete: - open ? - (originalText || textArea.text).split(/\s/).slice(-1)[0].replace( - autoOpen ? /^@/ : "", "", - ) : + readonly property var wordToComplete: + open ? originalWord || textArea.getWordBehindCursor() : null + + readonly property string modelFilter: + autoOpen && wordToComplete ? wordToComplete.word.replace(/^@/, "") : + open && wordToComplete ? wordToComplete.word : "" - function getLastWordStart() { + function getCurrentWordStart() { const lastWordMatch = /(?:^|\s)[^\s]+$/.exec(textArea.text) if (! lastWordMatch) return textArea.length @@ -37,9 +40,12 @@ HListView { return lastWordMatch.index } - function replaceLastWord(withText) { - textArea.remove(getLastWordStart(), textArea.length) - textArea.insertAtCursor(withText) + function replaceCurrentWord(withText) { + const current = textArea.getWordBehindCursor() + if (current) { + textArea.remove(current.start, current.end + 1) + textArea.insertAtCursor(withText) + } } function previous() { @@ -49,7 +55,7 @@ HListView { } open = true - const args = [model.modelId, wordToComplete] + const args = [model.modelId, modelFilter] py.callCoro("set_string_filter", args, decrementCurrentIndex) } @@ -60,7 +66,7 @@ HListView { } open = true - const args = [model.modelId, wordToComplete] + const args = [model.modelId, modelFilter] py.callCoro("set_string_filter", args, incrementCurrentIndex) } @@ -75,8 +81,7 @@ HListView { } function cancel() { - if (originalText) - replaceLastWord(originalText.split(/\s/).splice(-1)[0]) + if (originalWord) replaceCurrentWord(originalWord.word) currentIndex = -1 open = false @@ -97,26 +102,25 @@ HListView { } } - onCountChanged: if (! count && open) open = false onAutoOpenChanged: open = autoOpen onOpenChanged: if (! open) { - originalText = "" + originalWord = null currentIndex = -1 autoOpenCompleted = false py.callCoro("set_string_filter", [model.modelId, ""]) } - onWordToCompleteChanged: { + onModelFilterChanged: { if (! open) return - py.callCoro("set_string_filter", [model.modelId, wordToComplete]) + py.callCoro("set_string_filter", [model.modelId, modelFilter]) } onCurrentIndexChanged: { if (currentIndex === -1) return - if (! originalText) originalText = textArea.text + if (! originalWord) originalWord = textArea.getWordBehindCursor() if (autoOpen) autoOpenCompleted = true - replaceLastWord(model.get(currentIndex).display_name) + replaceCurrentWord(model.get(currentIndex).display_name) } Behavior on opacity { HNumberAnimation {} } @@ -126,8 +130,17 @@ HListView { target: root.textArea function onCursorPositionChanged() { - if (root.open && root.textArea.cursorPosition < getLastWordStart()) - root.accept() + if (! root.open) return + + const pos = root.textArea.cursorPosition + const start = root.wordToComplete.start + const end = + currentIndex === -1 ? + root.wordToComplete.end + 1 : + root.wordToComplete.start + + model.get(currentIndex).display_name.length + + if (pos < start || pos > end) root.accept() } function onTextChanged() { diff --git a/src/gui/Utils.qml b/src/gui/Utils.qml index af488926..48a3c14f 100644 --- a/src/gui/Utils.qml +++ b/src/gui/Utils.qml @@ -497,4 +497,18 @@ QtObject { return sum } + + + function getWordAtPosition(text, position) { + // getWordAtPosition("foo bar", 1) → {word: "foo", start: 0, end: 2} + let seen = -1 + + for (var word of text.split(/(\s+)/)) { + var start = seen + 1 + seen += word.length + if (seen >= position) return {word, start, end: seen} + } + + return {word, start, end: seen} + } }