From 019fed6db410e2a7516f8071f20aa0c236a71693 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Thu, 8 Oct 2026 18:11:23 +0200 Subject: [PATCH] plugin: plumb file token with acme isfilec expansion Replace Document::wordAt() in plumbAtCursor() with fileTokenAt(), an acme-style button-3 file expansion: grow over isFileChar() (alnum, '_', and '. - + / : @', matching plan9port acme's isfilec), with ':' ending the file name and only a digit-led ':line[:col]' suffix kept attached. This grabs whole paths like /a/b/dpg_identity.php:584 under the caret. Also reverts the earlier ^W change so acmeEraseWord again matches acme's C isalnum (which excludes '_'). --- docs/PLAN.md | 5 ++- src/plugin/ollieplugin.cpp | 79 +++++++++++++++++++++++++++++++++++--- 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/docs/PLAN.md b/docs/PLAN.md index 15cf1b5..755d767 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -348,7 +348,10 @@ Kate flow through the real plumber rules. - **Plugin wiring** (`src/plugin/ollieplugin.cpp`): an `ollie_plumb` QAction on **F2** (testing trigger; a function key, so no collision with the Alt+letter door policy — final gesture will be RMB). `plumbAtCursor()` plumbs the - selection, else `Document::wordAt(cursorPosition)`, with the active doc's dir + selection, else the acme-style file-path token under the caret + (`fileTokenAt` + `isFileChar`, matching plan9port acme's `isfilec`: alnum, + `_`, and `. - + / : @`; `:` ends the file name and only a digit-led + `:line[:col]` suffix is kept), with the active doc's dir as `wdir`; on send failure it falls back to an internal resolver (URL → `QDesktopServices`, path[:line] → open here). Incoming `edit` messages are opened via `MainWindow::openUrl` + `activateView` + `setCursorPosition`; a diff --git a/src/plugin/ollieplugin.cpp b/src/plugin/ollieplugin.cpp index 7cb0883..0ba9480 100644 --- a/src/plugin/ollieplugin.cpp +++ b/src/plugin/ollieplugin.cpp @@ -99,6 +99,29 @@ bool isQuote(QChar c) return u == '\'' || u == '"' || u == '`'; } +// Characters that may appear in a plumbable file path, mirroring plan9port +// acme's isfilec() (look.c). Acme treats a rune as a file char when its +// isalnum() is true OR it is one of ". - + / : @". Acme's isalnum() counts '_' +// as alphanumeric (it is not in acme's punctuation set), so '_' is a file char; +// QChar::isLetterOrNumber() excludes it, hence the explicit test. +bool isFileChar(QChar c) +{ + if (c.isLetterOrNumber() || c == QLatin1Char('_')) { + return true; + } + switch (c.unicode()) { + case '.': + case '-': + case '+': + case '/': + case ':': + case '@': + return true; + default: + return false; + } +} + // Advance a cursor by one character, wrapping across line ends. Returns false // at end of document. bool stepForward(KTextEditor::Document *doc, KTextEditor::Cursor &c) @@ -213,6 +236,51 @@ KTextEditor::Cursor findQuoteBackward(KTextEditor::Document *doc, int line, int return KTextEditor::Cursor::invalid(); } +// Expand the caret position into the surrounding file-path token, mirroring +// plan9port acme's button-3 file expansion (look.c). Grow left and right over +// isFileChar() runs, but treat ':' as acme does: it ends the file name and may +// introduce a numeric ":line[:col]" address, which is kept attached to the +// token (the plumber / onPlumbEdit splits it downstream). Expansion is +// line-local; acme stops at white space and a newline is never a file char. +QString fileTokenAt(KTextEditor::Document *doc, const KTextEditor::Cursor &at) +{ + const int line = at.line(); + const int len = doc->lineLength(line); + auto ch = [&](int c) { return doc->characterAt(KTextEditor::Cursor(line, c)); }; + auto isFileNoColon = [&](QChar c) { return isFileChar(c) && c != QLatin1Char(':'); }; + + // File name: grow left and right over file chars, stopping before any ':' + // (acme ends the name at the colon and parses an address after it). + int lo = at.column(); + int hi = at.column(); + while (hi < len && isFileNoColon(ch(hi))) { + ++hi; + } + while (lo > 0 && isFileNoColon(ch(lo - 1))) { + --lo; + } + if (hi <= lo) { + return QString(); + } + + // Optional ":line[:col]" address: only when the colon is immediately + // followed by a digit, matching acme's isaddrc gate and the numeric suffix + // onPlumbEdit expects. Consume up to two colon-separated digit runs. + int end = hi; + for (int part = 0; part < 2 && end < len && ch(end) == QLatin1Char(':'); ++part) { + int scan = end + 1; + if (scan >= len || !ch(scan).isDigit()) { + break; // ':' not introducing a number — leave it out of the token + } + while (scan < len && ch(scan).isDigit()) { + ++scan; + } + end = scan; + } + + return doc->text(KTextEditor::Range(line, lo, line, end)); +} + // Prefix marking a palette id that is a ":" command string to execute rather // than a QAction to trigger. const QLatin1String kCmdPrefix("ollie:cmd:"); @@ -873,14 +941,15 @@ void OllieView::plumbAtCursor() } // What to plumb: an explicit selection wins (lets the user plumb a path - // with a line suffix like "foo.cpp:42"), otherwise the word under the - // caret. + // with a line suffix like "foo.cpp:42"), otherwise the file-path token + // under the caret, expanded acme-style (isFileChar: alnum, '_', and + // ". - + / : @") so paths and their ":line" address are grabbed whole. QString data; if (view->selection()) { data = view->selectionText().trimmed(); } if (data.isEmpty()) { - data = doc->wordAt(view->cursorPosition()); + data = fileTokenAt(doc, view->cursorPosition()); } if (data.isEmpty()) { return; @@ -1702,9 +1771,7 @@ void OllieView::acmeEraseWord() break; } const QChar r = doc->characterAt(prev); - // Acme's isalnum() treats '_' as a word character (it is not in acme's - // punctuation set), so ^W does not stop on underscores. - const bool alnum = r.isLetterOrNumber() || r == QLatin1Char('_'); + const bool alnum = r.isLetterOrNumber(); if (alnum && skipping) { skipping = false; } else if (!alnum && !skipping) {