Skip to content

Commit 8f60746

Browse files
committed
fix(chat): follow URLs on RET even when wrapped in emphasis
A bare URL wrapped in markdown emphasis like **https://...** is fontified with a list face (markdown-plain-url-face plus the bold face), so the eq check in eca-chat--key-pressed-return missed it and RET did nothing. thing-at-point also pulls in the trailing ** since asterisks are valid URL chars. Detect link/url faces via membership (works for a symbol or a list) and strip surrounding * _ ~ ` markers before browse-url. Proper [text](url) links still go through markdown-follow-thing-at-point.
1 parent d227cf0 commit 8f60746

3 files changed

Lines changed: 118 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44

55
- Add `eca-chat-delete` command to delete the active chat from the server without prompting. Works from any buffer in the project (acts on the session's last visited chat), switches the chat window to another chat first when one exists, and is bound to `C-c C-S-k` plus a `Delete` entry in the transient menu.
6+
- Bugfix: pressing `RET` on a URL in the chat now opens it even when the URL is wrapped in markdown emphasis like `**https://...**` or `_https://..._`. The face at point is a list in that case, so the old `eq` check missed it; the trailing `**`/`_` markers are also stripped so the right URL opens. Proper `[text](url)` links still go through `markdown-follow-thing-at-point`.
67
- Bugfix: switching chat tabs via `tab-line-switch-to-next-tab`/`tab-line-switch-to-prev-tab` or clicking a tab now switches the chat in place instead of opening it in a new window.
78
- Bugfix: trigger `@`/`#` chat completion even when a char like `(` immediately precedes it.
89
- Bugfix: don't crash with `(wrong-type-argument stringp nil)` when `chat/askQuestion` sends options as plain strings or option objects without a `:label`. Options are normalized via `eca-chat--normalize-question-option` (string or plist) and the label always falls back to a non-nil string.

eca-chat.el

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1578,6 +1578,27 @@ Does not send directly — `eca-chat--send-queued-prompt' handles sending."
15781578
((bound-and-true-p completion-in-region-mode)
15791579
(completion-at-point))))
15801580

1581+
(defun eca-chat--face-at-point-member-p (faces)
1582+
"Return non-nil when the `face' text property at point includes any of FACES.
1583+
The `face' property may be a single symbol or a list of faces: a bare
1584+
URL wrapped in emphasis such as **https://...** carries both
1585+
`markdown-plain-url-face' and `markdown-bold-face', so a plain `eq'
1586+
against a single symbol would miss it."
1587+
(let ((prop (get-text-property (point) 'face)))
1588+
(seq-some (lambda (f) (memq f faces))
1589+
(if (listp prop) prop (list prop)))))
1590+
1591+
(defun eca-chat--follow-link-at-point ()
1592+
"Open the markdown link or URL at point in the chat buffer.
1593+
Handles inline [text](url) links as well as bare URLs, including bare
1594+
URLs wrapped in markdown emphasis like **https://...** or _https://..._
1595+
where `thing-at-point' would otherwise capture the trailing markup
1596+
characters as part of the URL."
1597+
(if (eca-chat--face-at-point-member-p '(markdown-plain-url-face))
1598+
(when-let* ((url (thing-at-point 'url t)))
1599+
(browse-url (string-trim url "[*_~`]+" "[*_~`]+")))
1600+
(markdown-follow-thing-at-point nil)))
1601+
15811602
(defun eca-chat--key-pressed-return ()
15821603
"Send the current prompt to eca process if in prompt."
15831604
(interactive)
@@ -1600,12 +1621,13 @@ Does not send directly — `eca-chat--send-queued-prompt' handles sending."
16001621
(let ((ov (eca-chat--expandable-content-at-point)))
16011622
(eca-chat--expandable-content-toggle (overlay-get ov 'eca-chat--expandable-content-id))))
16021623

1603-
;; follow markdown link [text](url)
1604-
((let ((face (get-text-property (point) 'face)))
1605-
(or (eq face 'markdown-link-face)
1606-
(eq face 'markdown-url-face)
1607-
(eq face 'markdown-plain-url-face)))
1608-
(markdown-follow-thing-at-point nil))
1624+
;; follow markdown link [text](url) or a bare URL, even when the URL
1625+
;; is wrapped in emphasis like **https://...** (face is then a list,
1626+
;; so check membership instead of `eq' against a single symbol).
1627+
((eca-chat--face-at-point-member-p '(markdown-link-face
1628+
markdown-url-face
1629+
markdown-plain-url-face))
1630+
(eca-chat--follow-link-at-point))
16091631

16101632
;; pending question + freeform allowed — answer with prompt text
16111633
((and eca-chat--pending-question

test/eca-chat-test.el

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ Returns the buffer. Caller must kill it."
3030
(buffer-substring-no-properties
3131
(eca-chat--prompt-field-start-point) (point-max))))
3232

33+
(defun eca-chat-test--call-on (text marker fn)
34+
"Fontify TEXT in a gfm buffer, move point onto MARKER, then call FN.
35+
TEXT is inserted on the third line (after a heading) so markdown
36+
does not treat the first line as metadata. Returns FN's value."
37+
(with-temp-buffer
38+
(insert "# Title\n\n" text)
39+
(delay-mode-hooks (gfm-mode))
40+
(font-lock-ensure)
41+
(goto-char (point-min))
42+
(search-forward marker)
43+
(goto-char (match-beginning 0))
44+
(funcall fn)))
45+
3346
;; ---------------------------------------------------------------------------
3447
;; Tests
3548
;; ---------------------------------------------------------------------------
@@ -273,4 +286,80 @@ Returns the buffer. Caller must kill it."
273286
(expect (stringp (car res)) :to-be-truthy)
274287
(expect (cdr res) :to-equal "no label"))))
275288

289+
(describe "eca-chat--face-at-point-member-p"
290+
;; A bare URL wrapped in emphasis (**url**, _url_) gets a list-valued
291+
;; `face' property, so detection must not rely on `eq' to a symbol.
292+
(it "matches when the face is a single symbol"
293+
(with-temp-buffer
294+
(insert (propertize "x" 'face 'markdown-plain-url-face))
295+
(goto-char (point-min))
296+
(expect (eca-chat--face-at-point-member-p '(markdown-plain-url-face))
297+
:to-be-truthy)))
298+
299+
(it "matches when the face is a list (emphasized URL)"
300+
(with-temp-buffer
301+
(insert (propertize "x" 'face '(markdown-plain-url-face markdown-bold-face)))
302+
(goto-char (point-min))
303+
(expect (eca-chat--face-at-point-member-p '(markdown-plain-url-face))
304+
:to-be-truthy)))
305+
306+
(it "returns nil when no listed face is present"
307+
(with-temp-buffer
308+
(insert (propertize "x" 'face 'font-lock-comment-face))
309+
(goto-char (point-min))
310+
(expect (eca-chat--face-at-point-member-p '(markdown-plain-url-face))
311+
:to-be nil)))
312+
313+
(it "returns nil when there is no face at point"
314+
(with-temp-buffer
315+
(insert "x")
316+
(goto-char (point-min))
317+
(expect (eca-chat--face-at-point-member-p '(markdown-plain-url-face))
318+
:to-be nil))))
319+
320+
(describe "eca-chat--follow-link-at-point"
321+
322+
(it "opens a bare URL"
323+
(eca-chat-test--call-on
324+
"see https://github.com/nubank/nucli/pull/10063 here" "github.com"
325+
(lambda ()
326+
(spy-on 'browse-url)
327+
(spy-on 'markdown-follow-thing-at-point)
328+
(eca-chat--follow-link-at-point)
329+
(expect 'browse-url :to-have-been-called-with
330+
"https://github.com/nubank/nucli/pull/10063")
331+
(expect 'markdown-follow-thing-at-point :not :to-have-been-called))))
332+
333+
(it "opens a bold URL without the trailing ** markers"
334+
;; Regression: **https://...** fontifies the URL with a list face and
335+
;; `thing-at-point' captures the trailing "**"; both used to break RET.
336+
(eca-chat-test--call-on
337+
"see **https://github.com/nubank/nucli/pull/10063** here" "github.com"
338+
(lambda ()
339+
(spy-on 'browse-url)
340+
(spy-on 'markdown-follow-thing-at-point)
341+
(eca-chat--follow-link-at-point)
342+
(expect 'browse-url :to-have-been-called-with
343+
"https://github.com/nubank/nucli/pull/10063")
344+
(expect 'markdown-follow-thing-at-point :not :to-have-been-called))))
345+
346+
(it "opens an italic URL without the trailing _ marker"
347+
(eca-chat-test--call-on
348+
"see _https://github.com/nubank/nucli/pull/10063_ here" "github.com"
349+
(lambda ()
350+
(spy-on 'browse-url)
351+
(eca-chat--follow-link-at-point)
352+
(expect 'browse-url :to-have-been-called-with
353+
"https://github.com/nubank/nucli/pull/10063"))))
354+
355+
(it "defers a proper [text](url) link to markdown-follow-thing-at-point"
356+
(eca-chat-test--call-on
357+
"see [PR](https://github.com/nubank/nucli/pull/10063) here" "github.com"
358+
(lambda ()
359+
(spy-on 'browse-url)
360+
(spy-on 'markdown-follow-thing-at-point)
361+
(eca-chat--follow-link-at-point)
362+
(expect 'markdown-follow-thing-at-point :to-have-been-called)
363+
(expect 'browse-url :not :to-have-been-called)))))
364+
276365
;;; eca-chat-test.el ends here

0 commit comments

Comments
 (0)