diff --git a/docs/PLAN.md b/docs/PLAN.md index fb4cdf0..15cf1b5 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -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. diff --git a/src/plugin/ollieplugin.cpp b/src/plugin/ollieplugin.cpp index dbfa21d..59d5209 100644 --- a/src/plugin/ollieplugin.cpp +++ b/src/plugin/ollieplugin.cpp @@ -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;