// Shared text selection helpers for acme-style double-click behavior. // Brackets select their nested content; quotes select by parity. function isQuote(ch) { return ch === "'" || ch === '"' || ch === '`' } // A quote at position qPos opens a pair when an even number of identical // quote chars precede it; odd means it closes one. 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 } function findQuoteForward(text, qPos, quote) { for (var c = qPos + 1; c < text.length; c++) { if (text.charAt(c) === quote) return c } return -1 } function findQuoteBackward(text, qPos, quote) { for (var c = qPos - 1; c >= 0; c--) { if (text.charAt(c) === quote) return c } return -1 } function matchingBracket(ch) { switch (ch) { case '(': return {match: ')', isOpener: true} case '[': return {match: ']', isOpener: true} case '{': return {match: '}', isOpener: true} case ')': return {match: '(', isOpener: false} case ']': return {match: '[', isOpener: false} case '}': return {match: '{', isOpener: false} default: return null } } function findCloser(text, startPos, opener, closer) { var depth = 1 var pos = startPos while (pos < text.length) { var ch = text.charAt(pos) if (ch === opener) depth++ else if (ch === closer) { depth-- if (depth === 0) return pos } pos++ } return -1 } function findOpener(text, startPos, opener, closer) { var depth = 1 var pos = startPos while (pos >= 0) { var ch = text.charAt(pos) if (ch === closer) depth++ else if (ch === opener) { depth-- if (depth === 0) return pos } pos-- } return -1 } // Select text between a matching delimiter pair on double-click. // 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 var rightCh = cursorPos < text.length ? text.charAt(cursorPos) : '' var leftCh = cursorPos > 0 ? text.charAt(cursorPos - 1) : '' // Quote pairs first. 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 textEdit.select(qOpen + 1, qClose) return true } // Bracket pairs. var rightMatch = matchingBracket(rightCh) var leftMatch = matchingBracket(leftCh) var innerStart = -1 var innerEnd = -1 if (rightMatch && rightMatch.isOpener) { var closerPos = findCloser(text, cursorPos + 1, rightCh, rightMatch.match) if (closerPos >= 0) { innerStart = cursorPos + 1 innerEnd = closerPos } } else if (leftMatch && !leftMatch.isOpener) { var openerPos = findOpener(text, cursorPos - 2, leftMatch.match, leftCh) if (openerPos >= 0) { innerStart = openerPos + 1 innerEnd = cursorPos - 1 } } else if (rightMatch && !rightMatch.isOpener) { var openerPos2 = findOpener(text, cursorPos - 1, rightMatch.match, rightCh) if (openerPos2 >= 0) { innerStart = openerPos2 + 1 innerEnd = cursorPos } } else if (leftMatch && leftMatch.isOpener) { var closerPos2 = findCloser(text, cursorPos, leftCh, leftMatch.match) if (closerPos2 >= 0) { innerStart = cursorPos innerEnd = closerPos2 } } if (innerStart >= 0 && innerEnd >= innerStart) { textEdit.select(innerStart, innerEnd) return true } return false } // Select the plain word under the mouse (double-click word select). function selectWordAt(textEdit, mouseX, mouseY) { var cursorPos = textEdit.positionAt(mouseX, mouseY) var text = textEdit.getText(0, textEdit.length) if (cursorPos < 0 || cursorPos > text.length) return function isword(ch) { return /[A-Za-z0-9_]/.test(ch) } var start = cursorPos while (start > 0 && isword(text.charAt(start - 1))) start-- var end = cursorPos while (end < text.length && isword(text.charAt(end))) end++ if (start === end) return textEdit.select(start, end) }