plugin: extend double-click selection to '' "" `` quote pairs
This commit is contained in:
parent
376a190b52
commit
03f209bfd7
|
|
@ -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
|
- Double-clicking next to a bracket (`()[]{}`) selects the text strictly between
|
||||||
the matching pair, **multi-line aware** and nesting-aware. Adjacency is checked
|
the matching pair, **multi-line aware** and nesting-aware. Adjacency is checked
|
||||||
on both sides of the click; openers select forward, closers select backward.
|
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
|
- Implemented in the event filter via `selectBracketPairAt`: maps the click to a
|
||||||
document cursor (`coordinatesToCursor`), reads neighbouring characters
|
document cursor (`coordinatesToCursor`), reads neighbouring characters
|
||||||
(`characterAt`), scans with depth counting across line ends, and sets the
|
(`characterAt`), scans with depth counting across line ends, and sets the
|
||||||
selection. When no bracket is adjacent it falls through to Kate's default
|
selection. When no bracket or quote is adjacent it falls through to Kate's
|
||||||
double-click (word select).
|
default double-click (word select).
|
||||||
- A left double-click leaves the button held, so it also arms the chord anchor:
|
- 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
|
the user can go straight from a double-click (bracket- or word-select) into a
|
||||||
left+middle / left+right chord without releasing.
|
left+middle / left+right chord without releasing.
|
||||||
|
|
|
||||||
|
|
@ -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
|
// Advance a cursor by one character, wrapping across line ends. Returns false
|
||||||
// at end of document.
|
// at end of document.
|
||||||
bool stepForward(KTextEditor::Document *doc, KTextEditor::Cursor &c)
|
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();
|
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
|
// Prefix marking a palette id that is a ":" command string to execute rather
|
||||||
// than a QAction to trigger.
|
// than a QAction to trigger.
|
||||||
const QLatin1String kCmdPrefix("ollie:cmd:");
|
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 bool haveLeft = stepBackward(doc, leftCur);
|
||||||
const QChar leftCh = haveLeft ? doc->characterAt(leftCur) : QChar();
|
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;
|
bool rOpener = false;
|
||||||
const QChar rMatch = matchingBracket(rightCh, rOpener);
|
const QChar rMatch = matchingBracket(rightCh, rOpener);
|
||||||
bool lOpener = false;
|
bool lOpener = false;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue