diff --git a/docs/PLAN.md b/docs/PLAN.md index e243c86..5121e47 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -141,6 +141,16 @@ Therefore the palette is replaced, not extended, and the replacement is built on swallowing only the press leaked it). Paste in the editor is the KDE-clipboard paste (`Ctrl+V`, palette, or the left+right chord). +### Bracket-pair double-click selection — DONE +- 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. +- 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). + ### M5 — Command vocabulary + project model + switchers — NEXT - `:`-verb pack via `KTextEditor::Command`: `sort`, `align`, `json`, `b64`, `uuid`, `case`, `pipe `, etc. These populate M-x and the radial. diff --git a/src/plugin/ollieplugin.cpp b/src/plugin/ollieplugin.cpp index 45637df..a7d0775 100644 --- a/src/plugin/ollieplugin.cpp +++ b/src/plugin/ollieplugin.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -28,6 +29,98 @@ namespace katecustom { +namespace +{ +// Bracket pairs understood by the double-click selection feature. +QChar matchingBracket(QChar c, bool &isOpener) +{ + switch (c.unicode()) { + case '(': isOpener = true; return QLatin1Char(')'); + case '[': isOpener = true; return QLatin1Char(']'); + case '{': isOpener = true; return QLatin1Char('}'); + case ')': isOpener = false; return QLatin1Char('('); + case ']': isOpener = false; return QLatin1Char('['); + case '}': isOpener = false; return QLatin1Char('{'); + default: isOpener = false; return QChar(); + } +} + +// Advance a cursor by one character, wrapping across line ends. Returns false +// at end of document. +bool stepForward(KTextEditor::Document *doc, KTextEditor::Cursor &c) +{ + if (c.column() < doc->lineLength(c.line())) { + c.setColumn(c.column() + 1); + } else if (c.line() < doc->documentEnd().line()) { + c.setLine(c.line() + 1); + c.setColumn(0); + } else { + return false; + } + return true; +} + +// Retreat a cursor by one character, wrapping across line starts. Returns false +// at start of document. +bool stepBackward(KTextEditor::Document *doc, KTextEditor::Cursor &c) +{ + if (c.column() > 0) { + c.setColumn(c.column() - 1); + } else if (c.line() > 0) { + c.setLine(c.line() - 1); + c.setColumn(doc->lineLength(c.line())); + } else { + return false; + } + return true; +} + +// Scan forward from the position just after an opener for its matching closer, +// honouring nesting. \a pos must point at the first char after the opener. +// Returns the closer's cursor, or an invalid cursor if unbalanced. +KTextEditor::Cursor findCloser(KTextEditor::Document *doc, KTextEditor::Cursor pos, + QChar opener, QChar closer) +{ + int depth = 1; + while (true) { + const QChar ch = doc->characterAt(pos); + if (ch == opener) { + ++depth; + } else if (ch == closer) { + if (--depth == 0) { + return pos; + } + } + if (!stepForward(doc, pos)) { + break; + } + } + return KTextEditor::Cursor::invalid(); +} + +// Scan backward from the position just before a closer for its matching opener. +// \a pos must point at the char immediately before the closer. +KTextEditor::Cursor findOpener(KTextEditor::Document *doc, KTextEditor::Cursor pos, + QChar closer, QChar opener) +{ + int depth = 1; + while (true) { + const QChar ch = doc->characterAt(pos); + if (ch == closer) { + ++depth; + } else if (ch == opener) { + if (--depth == 0) { + return pos; + } + } + if (!stepBackward(doc, pos)) { + break; + } + } + return KTextEditor::Cursor::invalid(); +} +} // namespace + // --------------------------------------------------------------------------- // OlliePlugin // --------------------------------------------------------------------------- @@ -321,8 +414,99 @@ void OllieView::setupRadials() } } +bool OllieView::selectBracketPairAt(KTextEditor::View *view, const QPoint &globalPos) +{ + if (!view) { + return false; + } + KTextEditor::Document *doc = view->document(); + if (!doc) { + return false; + } + + const QPoint local = view->mapFromGlobal(globalPos); + const KTextEditor::Cursor at = view->coordinatesToCursor(local); + if (!at.isValid()) { + return false; + } + + // Characters immediately to the right and left of the click position. + const QChar rightCh = doc->characterAt(at); + KTextEditor::Cursor leftCur = at; + const bool haveLeft = stepBackward(doc, leftCur); + const QChar leftCh = haveLeft ? doc->characterAt(leftCur) : QChar(); + + bool rOpener = false; + const QChar rMatch = matchingBracket(rightCh, rOpener); + bool lOpener = false; + const QChar lMatch = matchingBracket(leftCh, lOpener); + + KTextEditor::Cursor opener = KTextEditor::Cursor::invalid(); + KTextEditor::Cursor closer = KTextEditor::Cursor::invalid(); + + // Prefer an opener to the right (select its contents forward), then a closer + // to the left (select its contents backward), then the remaining adjacency + // cases. "Everything between" excludes the bracket characters themselves. + if (!rMatch.isNull() && rOpener) { + KTextEditor::Cursor after = at; + if (stepForward(doc, after)) { + opener = at; + closer = findCloser(doc, after, rightCh, rMatch); + } + } else if (!lMatch.isNull() && !lOpener) { + KTextEditor::Cursor before = leftCur; + if (stepBackward(doc, before)) { + closer = leftCur; + opener = findOpener(doc, before, leftCh, lMatch); + } + } else if (!rMatch.isNull() && !rOpener) { + // Closer to the right: match backward from just before it. + KTextEditor::Cursor before = at; + if (stepBackward(doc, before)) { + closer = at; + opener = findOpener(doc, before, rightCh, rMatch); + } + } else if (!lMatch.isNull() && lOpener) { + // Opener to the left: match forward from the click position. + opener = leftCur; + closer = findCloser(doc, at, leftCh, lMatch); + } else { + return false; // no bracket adjacent — let Kate's default run + } + + if (!opener.isValid() || !closer.isValid()) { + return false; + } + + // Select the content strictly between the pair. The inner range starts just + // after the opener and ends at the closer. + KTextEditor::Cursor innerStart = opener; + if (!stepForward(doc, innerStart)) { + return false; + } + const KTextEditor::Range inner(innerStart, closer); + view->setSelection(inner); + view->setCursorPosition(closer); + return true; +} + bool OllieView::eventFilter(QObject *obj, QEvent *event) { + // Bracket-pair selection: double-clicking next to a bracket selects the + // text between the matching pair (multi-line). Falls through to Kate's + // default double-click (word select) when no bracket is adjacent. + if (event->type() == QEvent::MouseButtonDblClick) { + auto *mev = static_cast(event); + if (mev->button() == Qt::LeftButton) { + KTextEditor::View *v = m_mainWindow->activeView(); + auto *w = qobject_cast(obj); + if (v && w && v->isAncestorOf(w) + && selectBracketPairAt(v, mev->globalPosition().toPoint())) { + return true; // handled: suppress default word-select + } + } + } + // While a drag gesture is active, drive the radial directly from the raw // events: the opening press leaves an implicit grab on the editor view, so // the live move/release events are delivered there, and this application diff --git a/src/plugin/ollieplugin.h b/src/plugin/ollieplugin.h index 045026e..03830cf 100644 --- a/src/plugin/ollieplugin.h +++ b/src/plugin/ollieplugin.h @@ -23,6 +23,7 @@ class QAction; namespace KTextEditor { class MainWindow; +class View; } namespace katecustom @@ -83,6 +84,10 @@ private: // Catches configured mouse-button gestures inside the active view. bool eventFilter(QObject *obj, QEvent *event) override; + // On double-click adjacent to a bracket, select the text between the + // matching pair (multi-line aware). Returns true if a pair was selected. + bool selectBracketPairAt(KTextEditor::View *view, const QPoint &globalPos); + KTextEditor::MainWindow *m_mainWindow; PaletteWidget *m_palette = nullptr; RadialMenu *m_radial = nullptr;