168 lines
4.8 KiB
JavaScript
168 lines
4.8 KiB
JavaScript
// 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)
|
|
}
|