From 4c7c0e3640294871d4169e528bf02554446dc59c Mon Sep 17 00:00:00 2001 From: Guillermo Casanova Date: Thu, 27 Aug 2026 12:39:35 -0400 Subject: [PATCH 1/4] fix(web): keep pasted decorators as text --- packages/shared/src/composerInlineTokens.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/composerInlineTokens.ts b/packages/shared/src/composerInlineTokens.ts index e7f57f260aea..8e2f035d7c3f 100644 --- a/packages/shared/src/composerInlineTokens.ts +++ b/packages/shared/src/composerInlineTokens.ts @@ -45,9 +45,10 @@ const FILE_LINK_TOKEN_REGEX = new RegExp( ); const URI_SCHEME_REGEX = /^[A-Za-z][A-Za-z0-9+.-]*:/; const WINDOWS_DRIVE_PATH_REGEX = /^[A-Za-z]:[\\/]/; -// Autocomplete emits canonical file links, so ambiguous bare @scope/package text stays a package. +// Autocomplete emits canonical file links, so ambiguous bare references stay text. const SCOPED_PACKAGE_REFERENCE_REGEX = /^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*(?:\/[^\s@"]+)*$/; +const DECORATOR_CALL_REFERENCE_REGEX = /^[A-Za-z_$][\w.$]*\(/; function collectMentionTokens(text: string): ComposerInlineToken[] { const matches: ComposerInlineToken[] = []; @@ -85,7 +86,11 @@ function collectMentionTokens(text: string): ComposerInlineToken[] { const prefix = match[1] ?? ""; const quotedPath = match[2]; const path = quotedPath !== undefined ? quotedPath.replace(/\\(.)/g, "$1") : (match[3] ?? ""); - if (!path || (quotedPath === undefined && SCOPED_PACKAGE_REFERENCE_REGEX.test(path))) { + if ( + !path || + (quotedPath === undefined && + (SCOPED_PACKAGE_REFERENCE_REGEX.test(path) || DECORATOR_CALL_REFERENCE_REGEX.test(path))) + ) { continue; } const start = (match.index ?? 0) + prefix.length; From 588079b3f5d6a4d596e929a7b751070f316b0f00 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:10:21 -0700 Subject: [PATCH 2/4] test(shared): retain explicit decorator-shaped file mentions --- .../shared/src/composerInlineTokens.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/shared/src/composerInlineTokens.test.ts b/packages/shared/src/composerInlineTokens.test.ts index 5f19fb2a9989..8bd6e567baa9 100644 --- a/packages/shared/src/composerInlineTokens.test.ts +++ b/packages/shared/src/composerInlineTokens.test.ts @@ -125,6 +125,28 @@ describe("collectComposerInlineTokens", () => { expect(collectComposerInlineTokens("入力 @expo/ui を追加")).toEqual([]); }); + it.each(["@foo(bar)", "@namespace.foo(bar)", "@_private()", "@$inject(service)"])( + "keeps decorator call %s as plain text", + (reference) => { + expect(collectComposerInlineTokens(`${reference}\nclass Example {}`)).toEqual([]); + }, + ); + + it.each(['@"foo(bar)"', "[foo(bar)](foo%28bar%29)"])( + "keeps an explicit decorator-shaped file reference as a mention: %s", + (reference) => { + expect(collectComposerInlineTokens(`${reference} `)).toEqual([ + { + type: "mention", + value: "foo(bar)", + source: reference, + start: 0, + end: reference.length, + }, + ]); + }, + ); + it("keeps bare non-scoped file paths as mentions", () => { expect(collectComposerInlineTokens("Inspect @README.md next")).toEqual([ { From 4fec03d2e062dd8250e6adeab4121347a9c2ee33 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:17:15 -0700 Subject: [PATCH 3/4] fix(shared): keep Unicode decorator calls as plain text --- .../shared/src/composerInlineTokens.test.ts | 18 ++++++++++++------ packages/shared/src/composerInlineTokens.ts | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/shared/src/composerInlineTokens.test.ts b/packages/shared/src/composerInlineTokens.test.ts index 8bd6e567baa9..ca1bfbae7acf 100644 --- a/packages/shared/src/composerInlineTokens.test.ts +++ b/packages/shared/src/composerInlineTokens.test.ts @@ -125,12 +125,18 @@ describe("collectComposerInlineTokens", () => { expect(collectComposerInlineTokens("入力 @expo/ui を追加")).toEqual([]); }); - it.each(["@foo(bar)", "@namespace.foo(bar)", "@_private()", "@$inject(service)"])( - "keeps decorator call %s as plain text", - (reference) => { - expect(collectComposerInlineTokens(`${reference}\nclass Example {}`)).toEqual([]); - }, - ); + it.each([ + "@foo(bar)", + "@namespace.foo(bar)", + "@_private()", + "@$inject(service)", + "@écrire(option)", + "@工場.監査(値)", + "@e\u0301crire(option)", + '@factories["audit"](options)', + ])("keeps decorator call %s as plain text", (reference) => { + expect(collectComposerInlineTokens(`${reference}\nclass Example {}`)).toEqual([]); + }); it.each(['@"foo(bar)"', "[foo(bar)](foo%28bar%29)"])( "keeps an explicit decorator-shaped file reference as a mention: %s", diff --git a/packages/shared/src/composerInlineTokens.ts b/packages/shared/src/composerInlineTokens.ts index 8e2f035d7c3f..6c6c0112cc5f 100644 --- a/packages/shared/src/composerInlineTokens.ts +++ b/packages/shared/src/composerInlineTokens.ts @@ -48,7 +48,7 @@ const WINDOWS_DRIVE_PATH_REGEX = /^[A-Za-z]:[\\/]/; // Autocomplete emits canonical file links, so ambiguous bare references stay text. const SCOPED_PACKAGE_REFERENCE_REGEX = /^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*(?:\/[^\s@"]+)*$/; -const DECORATOR_CALL_REFERENCE_REGEX = /^[A-Za-z_$][\w.$]*\(/; +const DECORATOR_CALL_REFERENCE_REGEX = /^[\p{ID_Start}_$][\p{ID_Continue}.$\u200c\u200d]*\(/u; function collectMentionTokens(text: string): ComposerInlineToken[] { const matches: ComposerInlineToken[] = []; From c3a13f20d029dc7380c2a8a12dc2c3e632d6c69a Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 19 Oct 2026 02:25:38 +0000 Subject: [PATCH 4/4] test(shared): keep decorator regression coverage focused --- .../shared/src/composerInlineTokens.test.ts | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/packages/shared/src/composerInlineTokens.test.ts b/packages/shared/src/composerInlineTokens.test.ts index 16582e8a35e9..1025fc770412 100644 --- a/packages/shared/src/composerInlineTokens.test.ts +++ b/packages/shared/src/composerInlineTokens.test.ts @@ -125,18 +125,12 @@ describe("collectComposerInlineTokens", () => { expect(collectComposerInlineTokens("入力 @expo/ui を追加")).toEqual([]); }); - it.each([ - "@foo(bar)", - "@namespace.foo(bar)", - "@_private()", - "@$inject(service)", - "@écrire(option)", - "@工場.監査(値)", - "@e\u0301crire(option)", - '@factories["audit"](options)', - ])("keeps decorator call %s as plain text", (reference) => { - expect(collectComposerInlineTokens(`${reference}\nclass Example {}`)).toEqual([]); - }); + it.each(["@namespace.foo(bar)", "@écrire(option)", "@e\u0301crire(option)"])( + "keeps decorator call %s as plain text", + (reference) => { + expect(collectComposerInlineTokens(`${reference}\nclass Example {}`)).toEqual([]); + }, + ); it.each(['@"foo(bar)"', "[foo(bar)](foo%28bar%29)"])( "keeps an explicit decorator-shaped file reference as a mention: %s", @@ -153,8 +147,8 @@ describe("collectComposerInlineTokens", () => { }, ); - it("keeps bare non-scoped file paths as mentions", () => { - expect(collectComposerInlineTokens("Inspect @README.md next")).toEqual([ + it("keeps bare file mentions before whitespace-separated parentheses", () => { + expect(collectComposerInlineTokens("Inspect @README.md (notes)")).toEqual([ { type: "mention", value: "README.md",