Make autocompletion work not just at end of line
This commit is contained in:
		| @@ -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 || "" | ||||||
|   | |||||||
| @@ -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 | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -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() { | ||||||
|   | |||||||
| @@ -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} | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	