Add bracket-pair selection on double-click (multi-line, nesting-aware)
Double-clicking next to a bracket ()[]{} now selects the text strictly
between the matching pair, across lines and honouring nesting. The event
filter catches MouseButtonDblClick inside the view, maps the click to a
document cursor via coordinatesToCursor, inspects the characters on both
sides, and scans with depth counting (stepForward/stepBackward wrap across
line ends) to find the match. Openers select forward, closers backward;
with no bracket adjacent it falls through to Kate's default word-select.
All 5 ctest suites green.
This commit is contained in:
parent
3129662f31
commit
a804dbe521
10
docs/PLAN.md
10
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 <shell>`, etc. These populate M-x and the radial.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
#include <KTextEditor/MainWindow>
|
||||
#include <KTextEditor/View>
|
||||
#include <KTextEditor/Document>
|
||||
|
||||
#include <KActionCollection>
|
||||
#include <KXMLGUIClient>
|
||||
|
|
@ -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<QMouseEvent *>(event);
|
||||
if (mev->button() == Qt::LeftButton) {
|
||||
KTextEditor::View *v = m_mainWindow->activeView();
|
||||
auto *w = qobject_cast<QWidget *>(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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue