From e4ecaec5c245d2ef2ce1a3c187e2bebb5debf5ed Mon Sep 17 00:00:00 2001 From: mizdra Date: Fri, 13 Nov 2026 02:02:13 +0900 Subject: [PATCH 1/3] fix(content-mapper): enable only the language features that tokens need Co-Authored-By: Claude Opus 5.5 --- .changeset/content-mapper-token-span-features.md | 5 +++++ packages/content-mapper/src/transformer.test.ts | 12 ++++++------ packages/content-mapper/src/transformer.ts | 4 ++-- 3 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 .changeset/content-mapper-token-span-features.md diff --git a/.changeset/content-mapper-token-span-features.md b/.changeset/content-mapper-token-span-features.md new file mode 100644 index 00000000..e523f736 --- /dev/null +++ b/.changeset/content-mapper-token-span-features.md @@ -0,0 +1,5 @@ +--- +'@css-modules-kit/content-mapper': patch +--- + +fix: stop showing TypeScript hover information on tokens in `*.module.css` diff --git a/packages/content-mapper/src/transformer.test.ts b/packages/content-mapper/src/transformer.test.ts index 86f68381..cf39848d 100644 --- a/packages/content-mapper/src/transformer.test.ts +++ b/packages/content-mapper/src/transformer.test.ts @@ -42,11 +42,11 @@ test('generates a declaration for each occurrence of a local token', () => { === generated === interface Styles { readonly 'a_1': string } - ^^^^^ #0 Atom(All&~Rename) + ^^^^^ #0 Atom(Definition|References|DocumentHighlights) interface Styles { readonly 'a_2': string } - ^^^^^ #1 Atom(All&~Rename) + ^^^^^ #1 Atom(Definition|References|DocumentHighlights) interface Styles { readonly 'a_2': string } - ^^^^^ #2 Atom(All&~Rename) + ^^^^^ #2 Atom(Definition|References|DocumentHighlights) declare const styles: Styles; ^^^^^^ #3 Atom(Definition) styles['a_1']; @@ -83,7 +83,7 @@ test('omits token importers and token references', () => { === generated === interface Styles { readonly 'a_1': string } - ^^^^^ #0 Atom(All&~Rename) + ^^^^^ #0 Atom(Definition|References|DocumentHighlights) declare const styles: Styles; ^^^^^^ #1 Atom(Definition) styles['a_1']; @@ -129,9 +129,9 @@ test('converts parse diagnostics into mapper diagnostics', () => { === generated === interface Styles { readonly 'a_1': string } - ^^^^^ #0 Atom(All&~Rename) + ^^^^^ #0 Atom(Definition|References|DocumentHighlights) interface Styles { readonly 'a_2': string } - ^^^^^ #1 Atom(All&~Rename) + ^^^^^ #1 Atom(Definition|References|DocumentHighlights) declare const styles: Styles; ^^^^^^ #2 Atom(Definition) styles['a_1']; diff --git a/packages/content-mapper/src/transformer.ts b/packages/content-mapper/src/transformer.ts index 08303e2c..d30c3df8 100644 --- a/packages/content-mapper/src/transformer.ts +++ b/packages/content-mapper/src/transformer.ts @@ -35,14 +35,14 @@ function generateText(tokens: Token[]): Pick Date: Fri, 13 Nov 2026 02:02:13 +0900 Subject: [PATCH 2/3] test(content-mapper): render span map features only as the included ones Co-Authored-By: Claude Opus 5.5 --- .../content-mapper/src/test/render.test.ts | 28 ------------------- packages/content-mapper/src/test/render.ts | 7 +---- 2 files changed, 1 insertion(+), 34 deletions(-) diff --git a/packages/content-mapper/src/test/render.test.ts b/packages/content-mapper/src/test/render.test.ts index ab8278f5..ba70e9f5 100644 --- a/packages/content-mapper/src/test/render.test.ts +++ b/packages/content-mapper/src/test/render.test.ts @@ -51,31 +51,3 @@ test('renders the mappings and the diagnostics as markers under the source and t diag#0: Unclosed block" `); }); - -test('renders the features as the ones excluded from All when most features are included', () => { - const source = '.a_1 {}'; - const text = "interface Styles { readonly 'a_1': string }"; - const output: TransformOutput = { - text, - mappings: [ - [ - text.indexOf("'a_1'"), - 5, - source.indexOf('a_1'), - 3, - SpanMapKind.Atom, - SpanMapFeature.All & ~SpanMapFeature.Rename & ~SpanMapFeature.Hover, - ], - ], - diagnostics: [], - }; - expect(renderTransformOutput(source, output)).toMatchInlineSnapshot(` - "=== source === - .a_1 {} - ^^^ #0 - - === generated === - interface Styles { readonly 'a_1': string } - ^^^^^ #0 Atom(All&~Hover&~Rename)" - `); -}); diff --git a/packages/content-mapper/src/test/render.ts b/packages/content-mapper/src/test/render.ts index b5bdc9a8..c3b3af7d 100644 --- a/packages/content-mapper/src/test/render.ts +++ b/packages/content-mapper/src/test/render.ts @@ -16,14 +16,9 @@ function formatFeatures(features: number | undefined): string { if (features === undefined) return ''; // A numeric enum also has the entries of its reverse mapping. const flags = Object.entries(SpanMapFeature).filter( - (entry): entry is [string, SpanMapFeature] => - typeof entry[1] === 'number' && entry[1] !== SpanMapFeature.All && entry[1] !== SpanMapFeature.None, + (entry): entry is [string, SpanMapFeature] => typeof entry[1] === 'number' && entry[1] !== SpanMapFeature.All, ); const included = flags.filter(([, bit]) => (features & bit) !== 0).map(([name]) => name); - const excluded = flags.filter(([, bit]) => (features & bit) === 0).map(([name]) => name); - if (included.length > excluded.length) { - return `(${['All', ...excluded].join('&~')})`; - } return `(${included.join('|')})`; } From 4e4d9dabb2a716abd86665cc85bd830044a35be0 Mon Sep 17 00:00:00 2001 From: mizdra Date: Fri, 13 Nov 2026 02:02:13 +0900 Subject: [PATCH 3/3] test(content-mapper): add e2e tests for hover and document highlights Co-Authored-By: Claude Opus 5.5 --- .../e2e-test/document-highlight.test.ts | 37 +++++++++++++++++++ .../content-mapper/e2e-test/hover.test.ts | 35 ++++++++++++++++++ .../e2e-test/test-util/language-client.ts | 31 ++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 packages/content-mapper/e2e-test/document-highlight.test.ts create mode 100644 packages/content-mapper/e2e-test/hover.test.ts diff --git a/packages/content-mapper/e2e-test/document-highlight.test.ts b/packages/content-mapper/e2e-test/document-highlight.test.ts new file mode 100644 index 00000000..86052f82 --- /dev/null +++ b/packages/content-mapper/e2e-test/document-highlight.test.ts @@ -0,0 +1,37 @@ +import dedent from 'dedent'; +import { expect, test } from 'vite-plus/test'; +import { buildTSConfigJSON } from '../src/test/builder.js'; +import { fixtureDir, setupFixture } from './test-util/fixture.js'; +import { launchLanguageClient } from './test-util/language-client.js'; + +const client = await launchLanguageClient(fixtureDir); + +test('highlights every token definition and the TS-side styles.', async () => { + const { iff, getFileLocation, getFileSpan } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON(), + 'index.ts': dedent` + import styles from './a.module.css'; + styles.a_1; + `, + 'a.module.css': dedent` + .a_1 { color: red; } + .a_1 { color: red; } + `, + }); + const files = [iff.paths['index.ts'], iff.paths['a.module.css']]; + await client.sendDidOpen(files); + + const cases = [ + ['from the TS-side styles.', getFileLocation('index.ts', 'a_1')], + ['from the token definition', getFileLocation('a.module.css', 'a_1', 0)], + ] as const; + const expected = [ + getFileSpan('a.module.css', 'a_1', { index: 0 }), + getFileSpan('a.module.css', 'a_1', { index: 1 }), + getFileSpan('index.ts', 'a_1'), + ]; + for (const [name, origin] of cases) { + const highlights = await client.sendDocumentHighlight(origin, files); + expect.soft(highlights, name).toStrictEqual(expected); + } +}); diff --git a/packages/content-mapper/e2e-test/hover.test.ts b/packages/content-mapper/e2e-test/hover.test.ts new file mode 100644 index 00000000..ffdbb92b --- /dev/null +++ b/packages/content-mapper/e2e-test/hover.test.ts @@ -0,0 +1,35 @@ +import dedent from 'dedent'; +import { expect, test } from 'vite-plus/test'; +import { buildTSConfigJSON } from '../src/test/builder.js'; +import { fixtureDir, setupFixture } from './test-util/fixture.js'; +import { launchLanguageClient } from './test-util/language-client.js'; + +const client = await launchLanguageClient(fixtureDir); + +test('returns no hover on the token definition', async () => { + const { iff, getFileLocation } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON(), + 'a.module.css': `.a_1 { color: red; }`, + }); + await client.sendDidOpen([iff.paths['a.module.css']]); + + const hover = await client.sendHover(getFileLocation('a.module.css', 'a_1')); + + expect(hover).toBeNull(); +}); + +test('returns the type of the token on the TS-side styles.', async () => { + const { iff, getFileLocation, getFileSpan } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON(), + 'index.ts': dedent` + import styles from './a.module.css'; + styles.a_1; + `, + 'a.module.css': `.a_1 { color: red; }`, + }); + await client.sendDidOpen([iff.paths['index.ts']]); + + const hover = await client.sendHover(getFileLocation('index.ts', 'a_1')); + + expect(hover).toStrictEqual({ ...getFileSpan('index.ts', 'a_1'), contents: "(property) Styles['a_1']: string" }); +}); diff --git a/packages/content-mapper/e2e-test/test-util/language-client.ts b/packages/content-mapper/e2e-test/test-util/language-client.ts index 63341cf4..da249594 100644 --- a/packages/content-mapper/e2e-test/test-util/language-client.ts +++ b/packages/content-mapper/e2e-test/test-util/language-client.ts @@ -22,6 +22,8 @@ export type RenameLocation = FileSpan & { newText?: string }; export type PrepareRenameResult = FileSpan & { placeholder?: string }; +export type HoverResult = FileSpan & { contents: string }; + export type Diagnostic = { range: Range; severity?: number; @@ -37,6 +39,11 @@ interface LSPLocation { range: Range; } +interface LSPMultiDocumentHighlight { + uri: string; + highlights: { range: Range }[]; +} + interface LSPTextEdit { range: Range; newText: string; @@ -52,6 +59,8 @@ export interface LanguageClient { sendDidOpen(files: readonly string[]): Promise; sendDefinition(args: FileLocation): Promise; sendReferences(args: FileLocation): Promise; + sendDocumentHighlight(args: FileLocation, filesToSearch: readonly string[]): Promise; + sendHover(args: FileLocation): Promise; sendPrepareRename(args: FileLocation): Promise; sendRename(args: FileLocation, newName?: string): Promise; sendDocumentDiagnostic(file: string): Promise; @@ -187,6 +196,28 @@ export async function launchLanguageClient(rootDir: string): Promise highlights.map(({ range }) => ({ file: toFilePath(uri), range }))) + .toSorted(compareFileSpans); + }, + async sendHover(args) { + const result: { contents: { value: string }; range: Range } | null = await sendRequest('textDocument/hover', { + textDocument: { uri: toFileUri(args.file) }, + position: args.position, + }); + if (result === null) return null; + return { file: formatPath(args.file), range: result.range, contents: result.contents.value }; + }, async sendPrepareRename(args) { const result: Range | { range: Range; placeholder: string } | null = await sendRequest( 'textDocument/prepareRename',