From 03dce95c6bd1ad469980a09ebb0d023ad16fcdc9 Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 24 Oct 2026 03:00:49 +0200 Subject: [PATCH] fix: expand issue references only when selected --- KNOWLEDGE_BASE.md | 2 +- resources/js/components/mention-textarea.tsx | 17 ++------- resources/js/lib/issue-reference.ts | 15 ++------ tests/Unit/IssueReference.test.tsx | 37 +++++++++++--------- tests/draft-images.config.ts | 1 + tests/tsconfig.json | 1 + 6 files changed, 28 insertions(+), 45 deletions(-) diff --git a/KNOWLEDGE_BASE.md b/KNOWLEDGE_BASE.md index e6a799b24..ac64cf9d7 100644 --- a/KNOWLEDGE_BASE.md +++ b/KNOWLEDGE_BASE.md @@ -205,7 +205,7 @@ In commits, replies, and remotes, start a line with `>` to render a quote block. Typing **@** in a composer opens developer suggestions. Use ↑/↓ to move, Enter or Tab to select, and Esc to dismiss. A bare @ suggests people you follow; typing more searches names and usernames. In replies, matching suggestions put the direct recipient first, then the root commit author, then other visible participants in that thread, followed by ordinary suggestions. Quotes prioritize only the quoted commit author. These priorities also apply to a bare @; typing a term still filters everyone, and yourself, muted developers, and blocked developers are excluded. In an org's messages, suggestions search the loaded member list instantly and update as membership refreshes. Suggestions are limited to the org's members, but you can still type anyone's handle yourself. Mentions link to profiles and can notify the mentioned developer after approval. Email addresses and scoped packages such as `@inertiajs/react` are not treated as mentions. -Typing **#123** in a composer looks up that exact issue or pull request in the /dev/null repository, `dev-null-sh/dev-null.sh`. The matching title appears beneath the number; click it or use Enter or Tab to insert its GitHub URL. A space or punctuation after the number automatically inserts the match, without treating a pause midway through a longer number as completion. Missing numbers and failed lookups leave the text unchanged. Code, URL fragments, and references qualified with another repository are not expanded. +Typing **#123** in a composer looks up that exact issue or pull request in the /dev/null repository, `dev-null-sh/dev-null.sh`. The matching title appears beneath the number; click it or use Enter or Tab to insert its GitHub URL, just like choosing an emoji suggestion. A space or punctuation leaves the number as typed and dismisses the suggestion. Missing numbers and failed lookups leave the text unchanged. Code, URL fragments, and references qualified with another repository are not expanded. Sources: [body renderer](resources/js/components/commit-body.tsx), [code blocks](resources/js/components/code-block.tsx), [mention suggestions](app/Http/Controllers/MentionSuggestionController.php), [mention parsing](app/Services/Mentions.php). diff --git a/resources/js/components/mention-textarea.tsx b/resources/js/components/mention-textarea.tsx index c2e33c52b..331f3f76d 100644 --- a/resources/js/components/mention-textarea.tsx +++ b/resources/js/components/mention-textarea.tsx @@ -1,6 +1,6 @@ import { usePage } from '@inertiajs/react'; import type { ComponentProps, KeyboardEvent, Ref, SyntheticEvent } from 'react'; -import { useEffect, useEffectEvent, useId, useRef, useState } from 'react'; +import { useEffect, useId, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { index as issueSuggestions } from '@/actions/App/Http/Controllers/IssueSuggestionController'; import MentionSuggestionController from '@/actions/App/Http/Controllers/MentionSuggestionController'; @@ -19,7 +19,6 @@ type Trigger = { kind: 'mention' | 'emoji' | 'issue'; start: number; query: string; - complete?: boolean; }; type IssueSuggestion = { @@ -495,7 +494,7 @@ export function MentionTextarea({ trigger !== null && position !== null && (trigger.kind === 'issue' - ? issue !== null && !trigger.complete + ? issue !== null : trigger.kind === 'emoji' ? emojis.length > 0 : suggestions.length > 0 || query !== ''); @@ -553,18 +552,6 @@ export function MentionTextarea({ const selectEmoji = (emoji: EmojiSuggestion) => insert(emoji.emoji, ''); - const autoLinkIssue = useEffectEvent(() => { - if (trigger?.kind === 'issue' && trigger.complete && issue) { - insert(issue.url, ''); - } - }); - - useEffect(() => { - const frame = requestAnimationFrame(autoLinkIssue); - - return () => cancelAnimationFrame(frame); - }, [trigger, issue]); - const handleKeyDown = (event: KeyboardEvent) => { if (isOpen && !event.nativeEvent.isComposing) { const hasModifier = event.metaKey || event.ctrlKey || event.altKey; diff --git a/resources/js/lib/issue-reference.ts b/resources/js/lib/issue-reference.ts index 52bbfaec6..a6106a6d8 100644 --- a/resources/js/lib/issue-reference.ts +++ b/resources/js/lib/issue-reference.ts @@ -2,27 +2,18 @@ import { typedProse } from '@/lib/code'; export function issueReferenceBeforeCaret(text: string, caret: number) { const prose = typedProse(text.slice(0, caret)).at(-1) ?? ''; - const match = [ - ...prose.matchAll( - /(?:^|[\s([{])#([1-9][0-9]{0,9})(?=$|[\s.,!?;:)\]}])/gu, - ), - ].at(-1); + const match = prose.match(/(?:^|[\s([{])#([1-9][0-9]{0,9})$/u); if (!match) { return null; } - const start = - caret - prose.length + match.index + match[0].lastIndexOf('#'); - const complete = start + match[1].length + 1 < caret; - - if (!complete && /^[\p{L}\p{N}_/#-]/u.test(text.slice(caret))) { + if (/^[\p{L}\p{N}_/#-]/u.test(text.slice(caret))) { return null; } return { - start, + start: caret - match[1].length - 1, query: match[1], - complete, }; } diff --git a/tests/Unit/IssueReference.test.tsx b/tests/Unit/IssueReference.test.tsx index 48314db74..91acdcd38 100644 --- a/tests/Unit/IssueReference.test.tsx +++ b/tests/Unit/IssueReference.test.tsx @@ -3,31 +3,35 @@ import assert from 'node:assert/strict'; import { test } from 'vite-plus/test'; import { issueReferenceBeforeCaret } from '../../resources/js/lib/issue-reference'; -test('waits for a delimiter before automatically linking an exact number', () => { +test('suggests only the exact number immediately before the caret', () => { assert.deepEqual(issueReferenceBeforeCaret('See #72', 7), { start: 4, query: '72', - complete: false, }); - assert.deepEqual(issueReferenceBeforeCaret('See #722 ', 9), { - start: 4, - query: '722', - complete: true, - }); - assert.deepEqual(issueReferenceBeforeCaret('See (#722).', 11), { + assert.deepEqual(issueReferenceBeforeCaret('See (#722).', 9), { start: 5, query: '722', - complete: true, }); }); -test('keeps a completed reference while its lookup is pending and typing continues', () => { - const text = 'See #722 for details'; - assert.deepEqual(issueReferenceBeforeCaret(text, text.length), { - start: 4, - query: '722', - complete: true, - }); +test('stops suggesting after a delimiter or continued prose', () => { + for (const ending of [ + ' ', + '.', + ',', + '!', + '?', + ';', + ':', + ')', + ']', + '}', + '\n', + ' for details', + ]) { + const text = `See #722${ending}`; + assert.equal(issueReferenceBeforeCaret(text, text.length), null, text); + } }); test('does not link code, colors, URL fragments, other repositories, or partial numbers', () => { @@ -55,6 +59,5 @@ test('preserves offsets after code and non-ASCII text, and chooses the latest re assert.deepEqual(issueReferenceBeforeCaret(text, text.length), { start: 19, query: '456', - complete: false, }); }); diff --git a/tests/draft-images.config.ts b/tests/draft-images.config.ts index a6543b9e5..11e32b471 100644 --- a/tests/draft-images.config.ts +++ b/tests/draft-images.config.ts @@ -24,6 +24,7 @@ export default defineConfig({ 'tests/Unit/ComposerDraft.test.tsx', 'tests/Unit/QuoteDraft.test.tsx', 'tests/Unit/EmojiSearch.test.tsx', + 'tests/Unit/IssueReference.test.tsx', 'tests/Unit/ReactionTray.test.tsx', 'tests/Unit/NotificationFilters.test.tsx', 'tests/Unit/EmojiShortcodes.test.tsx', diff --git a/tests/tsconfig.json b/tests/tsconfig.json index 63f7f001e..10118cf4f 100644 --- a/tests/tsconfig.json +++ b/tests/tsconfig.json @@ -25,6 +25,7 @@ "Unit/VoiceSpeed.test.tsx", "Unit/VoiceNoteSpeed.test.tsx", "Unit/OrgMentionSuggestions.test.tsx", + "Unit/IssueReference.test.tsx", "Unit/RemoteNotifications.test.tsx", "Unit/RemotePreferences.test.tsx", "Unit/ChatNavigation.test.tsx",