Make autocompletion work not just at end of line

This commit is contained in:
miruka 2020-08-21 04:44:55 -04:00
parent 1b919ec7be
commit 43b14f3129
4 changed files with 76 additions and 32 deletions

View File

@ -32,8 +32,22 @@ TextArea {
signal customImagePaste() signal customImagePaste()
function reset() { clear(); text = Qt.binding(() => defaultText || "") } function reset() {
function insertAtCursor(text) { insert(cursorPosition, text) } 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 || "" text: defaultText || ""

View File

@ -23,12 +23,12 @@ HTextArea {
ModelStore.get("accounts").find(writingUserId) ModelStore.get("accounts").find(writingUserId)
readonly property int cursorY: 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: readonly property int cursorX:
cursorPosition - lines.slice(0, cursorY).join("").length - cursorY 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 lineText: lines[cursorY] || ""
readonly property string lineTextUntilCursor: readonly property string lineTextUntilCursor:
@ -209,14 +209,17 @@ HTextArea {
} }
Keys.onBacktabPressed: ev => { Keys.onBacktabPressed: ev => {
// if previous char isn't a space/tab/newline
if (text.slice(cursorPosition - 1, cursorPosition).trim()) {
ev.accepted = true ev.accepted = true
autoCompletePrevious() autoCompletePrevious()
} }
}
Keys.onTabPressed: ev => { Keys.onTabPressed: ev => {
ev.accepted = true ev.accepted = true
if (text.slice(-1).trim()) { // previous char isn't a space/tab if (text.slice(cursorPosition - 1, cursorPosition).trim()) {
autoCompleteNext() autoCompleteNext()
return return
} }

View File

@ -6,28 +6,31 @@ import "../../.."
import "../../../Base" import "../../../Base"
import "../../../Base/HTile" import "../../../Base/HTile"
// FIXME: a b -> a @p b @p doesn't trigger completion
HListView { HListView {
id: root id: root
property HTextArea textArea property HTextArea textArea
property bool open: false property bool open: false
property string originalText: "" property var originalWord: null
property bool autoOpenCompleted: false property bool autoOpenCompleted: false
property var usersCompleted: ({}) // {displayName: userId} property var usersCompleted: ({}) // {displayName: userId}
readonly property bool autoOpen: readonly property bool autoOpen: {
autoOpenCompleted || textArea.text.match(/.*(^|\W)@[^\s]+$/) if (autoOpenCompleted) return true
const current = textArea.getWordBehindCursor()
return current ? /^@.+/.test(current.word) : false
}
readonly property string wordToComplete: readonly property var wordToComplete:
open ? open ? originalWord || textArea.getWordBehindCursor() : null
(originalText || textArea.text).split(/\s/).slice(-1)[0].replace(
autoOpen ? /^@/ : "", "", readonly property string modelFilter:
) : autoOpen && wordToComplete ? wordToComplete.word.replace(/^@/, "") :
open && wordToComplete ? wordToComplete.word :
"" ""
function getLastWordStart() { function getCurrentWordStart() {
const lastWordMatch = /(?:^|\s)[^\s]+$/.exec(textArea.text) const lastWordMatch = /(?:^|\s)[^\s]+$/.exec(textArea.text)
if (! lastWordMatch) return textArea.length if (! lastWordMatch) return textArea.length
@ -37,10 +40,13 @@ HListView {
return lastWordMatch.index return lastWordMatch.index
} }
function replaceLastWord(withText) { function replaceCurrentWord(withText) {
textArea.remove(getLastWordStart(), textArea.length) const current = textArea.getWordBehindCursor()
if (current) {
textArea.remove(current.start, current.end + 1)
textArea.insertAtCursor(withText) textArea.insertAtCursor(withText)
} }
}
function previous() { function previous() {
if (open) { if (open) {
@ -49,7 +55,7 @@ HListView {
} }
open = true open = true
const args = [model.modelId, wordToComplete] const args = [model.modelId, modelFilter]
py.callCoro("set_string_filter", args, decrementCurrentIndex) py.callCoro("set_string_filter", args, decrementCurrentIndex)
} }
@ -60,7 +66,7 @@ HListView {
} }
open = true open = true
const args = [model.modelId, wordToComplete] const args = [model.modelId, modelFilter]
py.callCoro("set_string_filter", args, incrementCurrentIndex) py.callCoro("set_string_filter", args, incrementCurrentIndex)
} }
@ -75,8 +81,7 @@ HListView {
} }
function cancel() { function cancel() {
if (originalText) if (originalWord) replaceCurrentWord(originalWord.word)
replaceLastWord(originalText.split(/\s/).splice(-1)[0])
currentIndex = -1 currentIndex = -1
open = false open = false
@ -97,26 +102,25 @@ HListView {
} }
} }
onCountChanged: if (! count && open) open = false
onAutoOpenChanged: open = autoOpen onAutoOpenChanged: open = autoOpen
onOpenChanged: if (! open) { onOpenChanged: if (! open) {
originalText = "" originalWord = null
currentIndex = -1 currentIndex = -1
autoOpenCompleted = false autoOpenCompleted = false
py.callCoro("set_string_filter", [model.modelId, ""]) py.callCoro("set_string_filter", [model.modelId, ""])
} }
onWordToCompleteChanged: { onModelFilterChanged: {
if (! open) return if (! open) return
py.callCoro("set_string_filter", [model.modelId, wordToComplete]) py.callCoro("set_string_filter", [model.modelId, modelFilter])
} }
onCurrentIndexChanged: { onCurrentIndexChanged: {
if (currentIndex === -1) return if (currentIndex === -1) return
if (! originalText) originalText = textArea.text if (! originalWord) originalWord = textArea.getWordBehindCursor()
if (autoOpen) autoOpenCompleted = true if (autoOpen) autoOpenCompleted = true
replaceLastWord(model.get(currentIndex).display_name) replaceCurrentWord(model.get(currentIndex).display_name)
} }
Behavior on opacity { HNumberAnimation {} } Behavior on opacity { HNumberAnimation {} }
@ -126,8 +130,17 @@ HListView {
target: root.textArea target: root.textArea
function onCursorPositionChanged() { function onCursorPositionChanged() {
if (root.open && root.textArea.cursorPosition < getLastWordStart()) if (! root.open) return
root.accept()
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() { function onTextChanged() {

View File

@ -497,4 +497,18 @@ QtObject {
return sum 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}
}
} }