Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/content-mapper-token-span-features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@css-modules-kit/content-mapper': patch
---

fix: stop showing TypeScript hover information on tokens in `*.module.css`
37 changes: 37 additions & 0 deletions packages/content-mapper/e2e-test/document-highlight.test.ts
Original file line number Diff line number Diff line change
@@ -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.<token>', 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.<token>', 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);
}
});
35 changes: 35 additions & 0 deletions packages/content-mapper/e2e-test/hover.test.ts
Original file line number Diff line number Diff line change
@@ -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.<token>', 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" });
});
31 changes: 31 additions & 0 deletions packages/content-mapper/e2e-test/test-util/language-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -37,6 +39,11 @@ interface LSPLocation {
range: Range;
}

interface LSPMultiDocumentHighlight {
uri: string;
highlights: { range: Range }[];
}

interface LSPTextEdit {
range: Range;
newText: string;
Expand All @@ -52,6 +59,8 @@ export interface LanguageClient {
sendDidOpen(files: readonly string[]): Promise<void>;
sendDefinition(args: FileLocation): Promise<FileSpan[]>;
sendReferences(args: FileLocation): Promise<FileSpan[]>;
sendDocumentHighlight(args: FileLocation, filesToSearch: readonly string[]): Promise<FileSpan[]>;
sendHover(args: FileLocation): Promise<HoverResult | null>;
sendPrepareRename(args: FileLocation): Promise<PrepareRenameResult | null>;
sendRename(args: FileLocation, newName?: string): Promise<RenameLocation[]>;
sendDocumentDiagnostic(file: string): Promise<Diagnostic[]>;
Expand Down Expand Up @@ -187,6 +196,28 @@ export async function launchLanguageClient(rootDir: string): Promise<LanguageCli
});
return normalizeLocations(result);
},
async sendDocumentHighlight(args, filesToSearch) {
// `textDocument/documentHighlight` returns no highlights when the path contains `@`, as the fixture path does.
const result: LSPMultiDocumentHighlight[] | null = await sendRequest(
'custom/textDocument/multiDocumentHighlight',
{
textDocument: { uri: toFileUri(args.file) },
position: args.position,
filesToSearch: filesToSearch.map(toFileUri),
},
);
return (result ?? [])
.flatMap(({ uri, highlights }) => 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',
Expand Down
28 changes: 0 additions & 28 deletions packages/content-mapper/src/test/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
`);
});
7 changes: 1 addition & 6 deletions packages/content-mapper/src/test/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('|')})`;
}

Expand Down
12 changes: 6 additions & 6 deletions packages/content-mapper/src/transformer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -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'];
Expand Down Expand Up @@ -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'];
Expand Down
4 changes: 2 additions & 2 deletions packages/content-mapper/src/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@ function generateText(tokens: Token[]): Pick<TransformOutput, 'text' | 'mappings
for (const { name, loc } of tokens) {
text += 'interface Styles { readonly ';
// The span includes the quotes because go-to-definition works only when the quotes are mapped as well.
// Rename is excluded because TypeScript writes rename edits back only through a Verbatim span.
// Rename is not enabled because TypeScript writes rename edits back only through a Verbatim span.
mappings.push([
text.length,
name.length + 2,
loc.start.offset,
name.length,
SpanMapKind.Atom,
SpanMapFeature.All & ~SpanMapFeature.Rename,
SpanMapFeature.Definition | SpanMapFeature.References | SpanMapFeature.DocumentHighlights,
]);
text += `'${name}': string }\n`;
}
Expand Down
Loading