From 794cecfa6c17d31f1552bc7db872ca732aef3fd5 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Thu, 8 Oct 2026 17:42:50 +0200 Subject: [PATCH] gui: quote-pair selection spans full document, not one line Quote matching now scans the whole block text and computes opener/closer parity document-wide, so content between a '' "" `` pair that spans multiple lines is selected in full. The adjacent quote (left or right of the cursor) sets the direction; the matching end quote is found by scanning that way. --- kde/gui/ChatBlockDelegate.qml | 116 +++++++++++++++++++++------------- 1 file changed, 72 insertions(+), 44 deletions(-) diff --git a/kde/gui/ChatBlockDelegate.qml b/kde/gui/ChatBlockDelegate.qml index 16f3ac8..5691533 100644 --- a/kde/gui/ChatBlockDelegate.qml +++ b/kde/gui/ChatBlockDelegate.qml @@ -421,18 +421,29 @@ Column { return ch === "'" || ch === '"' || ch === '`' } - // Scan forward for the next occurrence of a quote char - function findQuoteForward(text, startPos, quote) { - for (var pos = startPos; pos < text.length; pos++) { - if (text.charAt(pos) === quote) return pos + // A quote at position qPos opens a pair when an even number of identical + // quote chars precede it; odd means it closes one. Quotes do not nest, so + // parity decides direction. + function quoteIsOpener(text, qPos, quote) { + var count = 0 + for (var c = 0; c < qPos; c++) { + if (text.charAt(c) === quote) count++ + } + return (count % 2) === 0 + } + + // Next identical quote char forward from qPos. -1 if none. + function findQuoteForward(text, qPos, quote) { + for (var c = qPos + 1; c < text.length; c++) { + if (text.charAt(c) === quote) return c } return -1 } - // Scan backward for the previous occurrence of a quote char - function findQuoteBackward(text, startPos, quote) { - for (var pos = startPos; pos >= 0; pos--) { - if (text.charAt(pos) === quote) return pos + // Previous identical quote char backward from qPos. -1 if none. + function findQuoteBackward(text, qPos, quote) { + for (var c = qPos - 1; c >= 0; c--) { + if (text.charAt(c) === quote) return c } return -1 } @@ -469,70 +480,87 @@ Column { return -1 // unbalanced } - // Select text between matching brackets on double-click - // Returns true if a bracket pair was found and selected + // Select text between a matching delimiter pair on double-click. + // Quotes ('' "" ``) are resolved first (symmetric, non-nesting, line-scoped, + // direction by parity); then brackets (nesting, document-wide). The adjacent + // delimiter can be on either side of the cursor and determines the search + // direction and the matching end delimiter. + // Returns true if a pair was found and the inner content selected. function selectBracketPair(textEdit, mouseX, mouseY) { var cursorPos = textEdit.positionAt(mouseX, mouseY) var text = textEdit.getText(0, textEdit.length) - + if (cursorPos < 0 || cursorPos > text.length) return false - - // Check char to the right of cursor + + // Char to the right and left of the click position. var rightCh = cursorPos < text.length ? text.charAt(cursorPos) : '' - var rightMatch = matchingBracket(rightCh) - - // Check char to the left of cursor var leftCh = cursorPos > 0 ? text.charAt(cursorPos - 1) : '' + + // Quote pairs first. Pick the adjacent quote (right preferred), decide + // opener/closer by parity, then scan that direction for the matching + // quote. Content between may span multiple lines; all of it is selected. + var q = '' + var qPos = -1 + if (isQuote(rightCh)) { + q = rightCh + qPos = cursorPos + } else if (isQuote(leftCh)) { + q = leftCh + qPos = cursorPos - 1 + } + if (q !== '') { + var qOpen = -1 + var qClose = -1 + if (quoteIsOpener(text, qPos, q)) { + qOpen = qPos + qClose = findQuoteForward(text, qPos, q) + } else { + qClose = qPos + qOpen = findQuoteBackward(text, qPos, q) + } + if (qOpen < 0 || qClose < 0) return false // unbalanced + textEdit.select(qOpen + 1, qClose) + return true + } + + var rightMatch = matchingBracket(rightCh) var leftMatch = matchingBracket(leftCh) - + var innerStart = -1 var innerEnd = -1 - - // Prefer opener to the right, then closer to the left + + // Prefer opener to the right, then closer to the left, then the + // remaining adjacency cases. Inner range excludes the delimiters. if (rightMatch && rightMatch.isOpener) { - // Opener to right: find closer + // Opener to right: find closer forward. var closerPos = findCloser(text, cursorPos + 1, rightCh, rightMatch.match) if (closerPos >= 0) { innerStart = cursorPos + 1 innerEnd = closerPos } } else if (leftMatch && !leftMatch.isOpener) { - // Closer to left: find opener + // Closer to left: find opener backward. var openerPos = findOpener(text, cursorPos - 2, leftMatch.match, leftCh) if (openerPos >= 0) { innerStart = openerPos + 1 innerEnd = cursorPos - 1 } } else if (rightMatch && !rightMatch.isOpener) { - // Closer to right: find opener backwards - var openerPos = findOpener(text, cursorPos - 1, rightMatch.match, rightCh) - if (openerPos >= 0) { - innerStart = openerPos + 1 + // Closer to right: find opener backward from just before it. + var openerPos2 = findOpener(text, cursorPos - 1, rightMatch.match, rightCh) + if (openerPos2 >= 0) { + innerStart = openerPos2 + 1 innerEnd = cursorPos } } else if (leftMatch && leftMatch.isOpener) { - // Opener to left: find closer - var closerPos = findCloser(text, cursorPos, leftCh, leftMatch.match) - if (closerPos >= 0) { + // Opener to left: find closer forward from the click position. + var closerPos2 = findCloser(text, cursorPos, leftCh, leftMatch.match) + if (closerPos2 >= 0) { innerStart = cursorPos - innerEnd = closerPos - } - } else if (isQuote(rightCh)) { - // Quote to right: treat as opener, find next matching quote forward - var qClose = findQuoteForward(text, cursorPos + 1, rightCh) - if (qClose >= 0) { - innerStart = cursorPos + 1 - innerEnd = qClose - } - } else if (isQuote(leftCh)) { - // Quote to left: treat as closer, find previous matching quote backward - var qOpen = findQuoteBackward(text, cursorPos - 2, leftCh) - if (qOpen >= 0) { - innerStart = qOpen + 1 - innerEnd = cursorPos - 1 + innerEnd = closerPos2 } } - + if (innerStart >= 0 && innerEnd >= innerStart) { textEdit.select(innerStart, innerEnd) return true