plugin: extend double-click selection to '' "" `` quote pairs

This commit is contained in:
Levi Neely 2026-10-08 17:33:29 +02:00
parent 376a190b52
commit 03f209bfd7
2 changed files with 90 additions and 2 deletions

View File

@ -185,11 +185,16 @@ Therefore the palette is replaced, not extended, and the replacement is built on
- Double-clicking next to a bracket (`()[]{}`) selects the text strictly between
the matching pair, **multi-line aware** and nesting-aware. Adjacency is checked
on both sides of the click; openers select forward, closers select backward.
- Double-clicking next to a quote (`''`, `""`, `` `` ``) selects the text strictly
between the enclosing pair. Quotes are symmetric and non-nesting, so matching is
**line-local**: direction is decided by the parity of same-kind quotes preceding
the clicked one (even → opener, scan forward; odd → closer, scan backward), via
`isQuote` / `quoteIsOpener` / `findQuoteForward` / `findQuoteBackward`.
- Implemented in the event filter via `selectBracketPairAt`: maps the click to a
document cursor (`coordinatesToCursor`), reads neighbouring characters
(`characterAt`), scans with depth counting across line ends, and sets the
selection. When no bracket is adjacent it falls through to Kate's default
double-click (word select).
selection. When no bracket or quote is adjacent it falls through to Kate's
default double-click (word select).
- A left double-click leaves the button held, so it also arms the chord anchor:
the user can go straight from a double-click (bracket- or word-select) into a
left+middle / left+right chord without releasing.

View File

@ -89,6 +89,16 @@ QChar matchingBracket(QChar c, bool &isOpener)
}
}
// Quote delimiters understood by the double-click selection feature. Unlike
// brackets these are symmetric (same opening and closing char) and do not nest,
// so matching is handled separately: scan the click's line for the enclosing
// pair of identical quote chars.
bool isQuote(QChar c)
{
const ushort u = c.unicode();
return u == '\'' || u == '"' || u == '`';
}
// Advance a cursor by one character, wrapping across line ends. Returns false
// at end of document.
bool stepForward(KTextEditor::Document *doc, KTextEditor::Cursor &c)
@ -164,6 +174,45 @@ KTextEditor::Cursor findOpener(KTextEditor::Document *doc, KTextEditor::Cursor p
return KTextEditor::Cursor::invalid();
}
// Count occurrences of \a q on \a line in columns [0, col). An even count means
// the quote at \a col opens a pair; odd means it closes one. Quotes do not nest,
// so parity of preceding quotes decides direction.
bool quoteIsOpener(KTextEditor::Document *doc, int line, int col, QChar q)
{
int count = 0;
for (int c = 0; c < col; ++c) {
if (doc->characterAt(KTextEditor::Cursor(line, c)) == q) {
++count;
}
}
return (count % 2) == 0;
}
// Find the matching quote char of the same kind on \a line, searching columns
// (col, lineLength) forward. Returns the closer's cursor, or invalid if none.
KTextEditor::Cursor findQuoteForward(KTextEditor::Document *doc, int line, int col, QChar q)
{
const int len = doc->lineLength(line);
for (int c = col + 1; c < len; ++c) {
if (doc->characterAt(KTextEditor::Cursor(line, c)) == q) {
return KTextEditor::Cursor(line, c);
}
}
return KTextEditor::Cursor::invalid();
}
// Find the matching quote char of the same kind on \a line, searching columns
// [0, col) backward. Returns the opener's cursor, or invalid if none.
KTextEditor::Cursor findQuoteBackward(KTextEditor::Document *doc, int line, int col, QChar q)
{
for (int c = col - 1; c >= 0; --c) {
if (doc->characterAt(KTextEditor::Cursor(line, c)) == q) {
return KTextEditor::Cursor(line, c);
}
}
return KTextEditor::Cursor::invalid();
}
// Prefix marking a palette id that is a ":" command string to execute rather
// than a QAction to trigger.
const QLatin1String kCmdPrefix("ollie:cmd:");
@ -1167,6 +1216,40 @@ bool OllieView::selectBracketPairAt(KTextEditor::View *view, const QPoint &globa
const bool haveLeft = stepBackward(doc, leftCur);
const QChar leftCh = haveLeft ? doc->characterAt(leftCur) : QChar();
// Quote pairs ('' "" ``) are symmetric and non-nesting: resolve them on the
// click's line and select the content between the enclosing pair. Handled
// before brackets since the two delimiter sets are disjoint.
{
QChar q;
int qCol = -1;
if (isQuote(rightCh)) {
q = rightCh;
qCol = at.column();
} else if (haveLeft && isQuote(leftCh)) {
q = leftCh;
qCol = leftCur.column();
}
if (!q.isNull()) {
const int line = at.line();
KTextEditor::Cursor qOpen = KTextEditor::Cursor::invalid();
KTextEditor::Cursor qClose = KTextEditor::Cursor::invalid();
if (quoteIsOpener(doc, line, qCol, q)) {
qOpen = KTextEditor::Cursor(line, qCol);
qClose = findQuoteForward(doc, line, qCol, q);
} else {
qClose = KTextEditor::Cursor(line, qCol);
qOpen = findQuoteBackward(doc, line, qCol, q);
}
if (!qOpen.isValid() || !qClose.isValid()) {
return false; // unbalanced quote on this line
}
const KTextEditor::Cursor innerStart(line, qOpen.column() + 1);
view->setSelection(KTextEditor::Range(innerStart, qClose));
view->setCursorPosition(qClose);
return true;
}
}
bool rOpener = false;
const QChar rMatch = matchingBracket(rightCh, rOpener);
bool lOpener = false;