Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion KNOWLEDGE_BASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
17 changes: 2 additions & 15 deletions resources/js/components/mention-textarea.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -19,7 +19,6 @@ type Trigger = {
kind: 'mention' | 'emoji' | 'issue';
start: number;
query: string;
complete?: boolean;
};

type IssueSuggestion = {
Expand Down Expand Up @@ -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 !== '');
Expand Down Expand Up @@ -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<HTMLTextAreaElement>) => {
if (isOpen && !event.nativeEvent.isComposing) {
const hasModifier = event.metaKey || event.ctrlKey || event.altKey;
Expand Down
15 changes: 3 additions & 12 deletions resources/js/lib/issue-reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
37 changes: 20 additions & 17 deletions tests/Unit/IssueReference.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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,
});
});
1 change: 1 addition & 0 deletions tests/draft-images.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions tests/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading