From b1196689657a9245732ab5deaf776bdd9961da27 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Thu, 8 Oct 2026 16:20:29 +0200 Subject: [PATCH] gui: B3-click on word expands to file path (acme-style) RMB on text without selection now expands to the word at cursor using acme's isfilec character set: alphanumeric + . - + / : @ This matches acme behavior for clicking on file paths like foo.go:42 --- kde/gui/ChatBlockDelegate.qml | 64 +++++++++++++++++++++++++++++++---- kde/gui/ChatPane.qml | 29 ++++++++++++++-- 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/kde/gui/ChatBlockDelegate.qml b/kde/gui/ChatBlockDelegate.qml index ee34501..86461da 100644 --- a/kde/gui/ChatBlockDelegate.qml +++ b/kde/gui/ChatBlockDelegate.qml @@ -250,10 +250,19 @@ Column { } mouse.accepted = true } else if (mouse.button === Qt.RightButton) { - // B3: Plumb selection (no context menu) + // B3: Plumb selection or word under cursor + if (!plumber.available()) { + mouse.accepted = true + return + } var sel = mainTextEdit.selectedText - if (sel && sel.trim() !== "" && plumber.available()) { + if (sel && sel.trim() !== "") { plumber.plumb(sel.trim(), sessionCwd()) + } else { + // No selection - expand word at click position (acme-style) + var pos = mainTextEdit.positionAt(mouse.x, mouse.y) + var word = wordAtCursor(mainTextEdit, pos) + if (word) plumber.plumb(word, sessionCwd()) } mouse.accepted = true } @@ -339,6 +348,33 @@ Column { return cfg ? (cfg.cwd || "") : "" } + // Expand cursor position to surrounding word/path for plumbing + // Matches acme's isfilec: alphanumeric + . - + / : @ + function wordAtCursor(textEdit, cursorPos) { + var text = textEdit.getText(0, textEdit.length) + if (cursorPos < 0 || cursorPos > text.length) return "" + + // isfilec from acme: alphanumeric + . - + / : @ + function isfilec(ch) { + return /[a-zA-Z0-9.\-+\/:@]/.test(ch) + } + + // Find start of word (acme also allows isaddrc chars going left) + var start = cursorPos + while (start > 0 && isfilec(text.charAt(start - 1))) { + start-- + } + + // Find end of word + var end = cursorPos + while (end < text.length && isfilec(text.charAt(end))) { + end++ + } + + if (start === end) return "" + return text.substring(start, end) + } + // Text segment component Component { id: textComponent @@ -396,10 +432,18 @@ Column { } mouse.accepted = true } else if (mouse.button === Qt.RightButton) { - // B3: Plumb selection (no context menu) + // B3: Plumb selection or word under cursor + if (!plumber.available()) { + mouse.accepted = true + return + } var sel = textEdit.selectedText - if (sel && sel.trim() !== "" && plumber.available()) { + if (sel && sel.trim() !== "") { plumber.plumb(sel.trim(), sessionCwd()) + } else { + var pos = textEdit.positionAt(mouse.x, mouse.y) + var word = wordAtCursor(textEdit, pos) + if (word) plumber.plumb(word, sessionCwd()) } mouse.accepted = true } @@ -505,10 +549,18 @@ Column { if (sel && sel.trim() !== "") executeText(sel) mouse.accepted = true } else if (mouse.button === Qt.RightButton) { - // B3: Plumb selection (no context menu) + // B3: Plumb selection or word under cursor + if (!plumber.available()) { + mouse.accepted = true + return + } var sel = sourceEdit.selectedText - if (sel && sel.trim() !== "" && plumber.available()) { + if (sel && sel.trim() !== "") { plumber.plumb(sel.trim(), sessionCwd()) + } else { + var pos = sourceEdit.positionAt(mouse.x, mouse.y) + var word = wordAtCursor(sourceEdit, pos) + if (word) plumber.plumb(word, sessionCwd()) } mouse.accepted = true } diff --git a/kde/gui/ChatPane.qml b/kde/gui/ChatPane.qml index 474769d..d0d64a2 100644 --- a/kde/gui/ChatPane.qml +++ b/kde/gui/ChatPane.qml @@ -44,6 +44,26 @@ Item { return cfg ? (cfg.cwd || "") : "" } + // Expand cursor position to surrounding word/path for plumbing + // Matches acme's isfilec: alphanumeric + . - + / : @ + function wordAtCursor(textEdit, cursorPos) { + var text = textEdit.getText(0, textEdit.length) + if (cursorPos < 0 || cursorPos > text.length) return "" + + function isfilec(ch) { + return /[a-zA-Z0-9.\-+\/:@]/.test(ch) + } + + var start = cursorPos + while (start > 0 && isfilec(text.charAt(start - 1))) start-- + + var end = cursorPos + while (end < text.length && isfilec(text.charAt(end))) end++ + + if (start === end) return "" + return text.substring(start, end) + } + Component.onCompleted: { loadChat() scrollToBottom() @@ -503,10 +523,15 @@ Item { // No selection - ignore } } else if (mouse.button === Qt.RightButton) { - // B3: Plumb selection (no context menu) + // B3: Plumb selection or word under cursor + if (!plumber.available()) return var sel = promptInput.selectedText - if (sel && sel.trim() !== "" && plumber.available()) { + if (sel && sel.trim() !== "") { plumber.plumb(sel.trim(), sessionCwd()) + } else { + var pos = promptInput.positionAt(mouse.x, mouse.y) + var word = wordAtCursor(promptInput, pos) + if (word) plumber.plumb(word, sessionCwd()) } } }