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 '_').
This commit is contained in:
Levi Neely 2026-10-08 18:11:23 +02:00
parent 08a75c06af
commit 019fed6db4
2 changed files with 77 additions and 7 deletions

View File

@ -348,7 +348,10 @@ Kate flow through the real plumber rules.
- **Plugin wiring** (`src/plugin/ollieplugin.cpp`): an `ollie_plumb` QAction on - **Plugin wiring** (`src/plugin/ollieplugin.cpp`): an `ollie_plumb` QAction on
**F2** (testing trigger; a function key, so no collision with the Alt+letter **F2** (testing trigger; a function key, so no collision with the Alt+letter
door policy — final gesture will be RMB). `plumbAtCursor()` plumbs the 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 → as `wdir`; on send failure it falls back to an internal resolver (URL →
`QDesktopServices`, path[:line] → open here). Incoming `edit` messages are `QDesktopServices`, path[:line] → open here). Incoming `edit` messages are
opened via `MainWindow::openUrl` + `activateView` + `setCursorPosition`; a opened via `MainWindow::openUrl` + `activateView` + `setCursorPosition`; a

View File

@ -99,6 +99,29 @@ bool isQuote(QChar c)
return u == '\'' || u == '"' || u == '`'; 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 // 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)
@ -213,6 +236,51 @@ KTextEditor::Cursor findQuoteBackward(KTextEditor::Document *doc, int line, int
return KTextEditor::Cursor::invalid(); 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 // 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:");
@ -873,14 +941,15 @@ void OllieView::plumbAtCursor()
} }
// What to plumb: an explicit selection wins (lets the user plumb a path // 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 // with a line suffix like "foo.cpp:42"), otherwise the file-path token
// caret. // under the caret, expanded acme-style (isFileChar: alnum, '_', and
// ". - + / : @") so paths and their ":line" address are grabbed whole.
QString data; QString data;
if (view->selection()) { if (view->selection()) {
data = view->selectionText().trimmed(); data = view->selectionText().trimmed();
} }
if (data.isEmpty()) { if (data.isEmpty()) {
data = doc->wordAt(view->cursorPosition()); data = fileTokenAt(doc, view->cursorPosition());
} }
if (data.isEmpty()) { if (data.isEmpty()) {
return; return;
@ -1702,9 +1771,7 @@ void OllieView::acmeEraseWord()
break; break;
} }
const QChar r = doc->characterAt(prev); const QChar r = doc->characterAt(prev);
// Acme's isalnum() treats '_' as a word character (it is not in acme's const bool alnum = r.isLetterOrNumber();
// punctuation set), so ^W does not stop on underscores.
const bool alnum = r.isLetterOrNumber() || r == QLatin1Char('_');
if (alnum && skipping) { if (alnum && skipping) {
skipping = false; skipping = false;
} else if (!alnum && !skipping) { } else if (!alnum && !skipping) {