diff --git a/packages/ts-plugin/e2e-test/code-fix.test.ts b/packages/ts-plugin/e2e-test/code-fix.test.ts index b0df7b86..e3c924a0 100644 --- a/packages/ts-plugin/e2e-test/code-fix.test.ts +++ b/packages/ts-plugin/e2e-test/code-fix.test.ts @@ -7,7 +7,7 @@ import { } from '../src/language-service/feature/code-fix.js'; import { buildStylesImport, buildTSConfigJSON } from '../src/test/builder.js'; import { setupFixture } from './test-util/fixture.js'; -import { formatPath, launchTsserver, normalizeCodeFixActions } from './test-util/tsserver.js'; +import { formatPath, launchTsserver } from './test-util/tsserver.js'; const tsserver = launchTsserver(); @@ -17,7 +17,7 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: { errorCode: PROPERTY_DOES_NOT_EXIST_ERROR_CODES[0] }, { errorCode: PROPERTY_DOES_NOT_EXIST_ERROR_CODES[1] }, ])('inserts a new CSS rule into an empty CSS module for diagnostic $errorCode', async ({ errorCode }) => { - const { iff, getLoc } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -27,35 +27,31 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const loc = getLoc('index.ts', 'a_1'); - const res = await tsserver.sendGetCodeFixes({ + const { start, end } = getFileSpan('index.ts', 'a_1'); + const actions = await tsserver.sendGetCodeFixes({ errorCodes: [errorCode], file: iff.paths['index.ts'], - startLine: loc.line, - startOffset: loc.offset, - endLine: loc.line, - endOffset: loc.offset, + startLine: start.line, + startOffset: start.offset, + endLine: end.line, + endOffset: end.offset, }); - expect(normalizeCodeFixActions(res.body!)).toStrictEqual( - normalizeCodeFixActions([ - { - fixName: 'fixMissingCSSRule', - changes: [ - { - fileName: formatPath(iff.paths['a.module.css']), - textChanges: [ - { start: { line: 1, offset: 1 }, end: { line: 1, offset: 1 }, newText: '\n.a_1 {\n \n}' }, - ], - }, - ], - }, - ]), - ); + expect(actions).toStrictEqual([ + { + fixName: 'fixMissingCSSRule', + changes: [ + { + fileName: formatPath(iff.paths['a.module.css']), + textChanges: [{ start: { line: 1, offset: 1 }, end: { line: 1, offset: 1 }, newText: '\n.a_1 {\n \n}' }], + }, + ], + }, + ]); }); test('appends a new CSS rule to a non-empty CSS module', async () => { - const { iff, getLoc } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -69,35 +65,31 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const loc = getLoc('index.ts', 'a_2'); - const res = await tsserver.sendGetCodeFixes({ + const { start, end } = getFileSpan('index.ts', 'a_2'); + const actions = await tsserver.sendGetCodeFixes({ errorCodes: [PROPERTY_DOES_NOT_EXIST_ERROR_CODES[0]], file: iff.paths['index.ts'], - startLine: loc.line, - startOffset: loc.offset, - endLine: loc.line, - endOffset: loc.offset, + startLine: start.line, + startOffset: start.offset, + endLine: end.line, + endOffset: end.offset, }); - expect(normalizeCodeFixActions(res.body!)).toStrictEqual( - normalizeCodeFixActions([ - { - fixName: 'fixMissingCSSRule', - changes: [ - { - fileName: formatPath(iff.paths['a.module.css']), - textChanges: [ - { start: { line: 3, offset: 2 }, end: { line: 3, offset: 2 }, newText: '\n.a_2 {\n \n}' }, - ], - }, - ], - }, - ]), - ); + expect(actions).toStrictEqual([ + { + fixName: 'fixMissingCSSRule', + changes: [ + { + fileName: formatPath(iff.paths['a.module.css']), + textChanges: [{ start: { line: 3, offset: 2 }, end: { line: 3, offset: 2 }, newText: '\n.a_2 {\n \n}' }], + }, + ], + }, + ]); }); test('inserts the rule into the CSS module bound to the accessed identifier', async () => { - const { iff, getLoc } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -109,77 +101,71 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const loc = getLoc('index.ts', 'b_1'); - const res = await tsserver.sendGetCodeFixes({ + const { start, end } = getFileSpan('index.ts', 'b_1'); + const actions = await tsserver.sendGetCodeFixes({ errorCodes: [PROPERTY_DOES_NOT_EXIST_ERROR_CODES[0]], file: iff.paths['index.ts'], - startLine: loc.line, - startOffset: loc.offset, - endLine: loc.line, - endOffset: loc.offset, + startLine: start.line, + startOffset: start.offset, + endLine: end.line, + endOffset: end.offset, }); - expect(normalizeCodeFixActions(res.body!)).toStrictEqual( - normalizeCodeFixActions([ - { - fixName: 'fixMissingCSSRule', - changes: [ - { - fileName: formatPath(iff.paths['b.module.css']), - textChanges: [ - { start: { line: 1, offset: 1 }, end: { line: 1, offset: 1 }, newText: '\n.b_1 {\n \n}' }, - ], - }, - ], - }, - ]), - ); + expect(actions).toStrictEqual([ + { + fixName: 'fixMissingCSSRule', + changes: [ + { + fileName: formatPath(iff.paths['b.module.css']), + textChanges: [{ start: { line: 1, offset: 1 }, end: { line: 1, offset: 1 }, newText: '\n.b_1 {\n \n}' }], + }, + ], + }, + ]); }); }); describe('auto-import', () => { test('inserts the import statement when accepted', async () => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': `styles;`, 'a.module.css': '', }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const range = getRange('index.ts', 'styles'); - const res = await tsserver.sendGetCodeFixes({ + const { start, end } = getFileSpan('index.ts', 'styles'); + const actions = await tsserver.sendGetCodeFixes({ errorCodes: [CANNOT_FIND_NAME_ERROR_CODE], file: iff.paths['index.ts'], - startLine: range.start.line, - startOffset: range.start.offset, - endLine: range.end.line, - endOffset: range.end.offset, + startLine: start.line, + startOffset: start.offset, + endLine: end.line, + endOffset: end.offset, }); const importStatement = buildStylesImport('./a.module.css', { namedExports, quote: 'double' }); - expect(normalizeCodeFixActions(res.body!)).toStrictEqual( - normalizeCodeFixActions([ - { - fixName: 'import', - changes: [ - { - fileName: formatPath(iff.paths['index.ts']), - textChanges: [ - { - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 1 }, - newText: `${importStatement}${ts.sys.newLine}${ts.sys.newLine}`, - }, - ], - }, - ], - }, - ]), - ); + expect(actions).toStrictEqual([ + { + fixName: 'import', + changes: [ + { + fileName: formatPath(iff.paths['index.ts']), + textChanges: [ + { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + newText: `${importStatement}${ts.sys.newLine}${ts.sys.newLine}`, + }, + ], + }, + ], + }, + ]); }); test('excludes generated files from suggestions', async () => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports, dtsOutDir: 'generated' }, }), @@ -191,17 +177,17 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const range = getRange('index.ts', 'styles'); - const res = await tsserver.sendGetCodeFixes({ + const { start, end } = getFileSpan('index.ts', 'styles'); + const actions = await tsserver.sendGetCodeFixes({ errorCodes: [CANNOT_FIND_NAME_ERROR_CODE], file: iff.paths['index.ts'], - startLine: range.start.line, - startOffset: range.start.offset, - endLine: range.end.line, - endOffset: range.end.offset, + startLine: start.line, + startOffset: start.offset, + endLine: end.line, + endOffset: end.offset, }); - expect(normalizeCodeFixActions(res.body!)).toStrictEqual([]); + expect(actions).toStrictEqual([]); }); }); }); @@ -209,7 +195,7 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: describe('named import code fix (namedExports: true)', () => { describe('prioritizeNamedImports: false', () => { test('omits the named import code fix', async () => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports: true, prioritizeNamedImports: false }, }), @@ -218,23 +204,23 @@ describe('named import code fix (namedExports: true)', () => { }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const range = getRange('index.ts', 'a_1'); - const res = await tsserver.sendGetCodeFixes({ + const { start, end } = getFileSpan('index.ts', 'a_1'); + const actions = await tsserver.sendGetCodeFixes({ errorCodes: [CANNOT_FIND_NAME_ERROR_CODE], file: iff.paths['index.ts'], - startLine: range.start.line, - startOffset: range.start.offset, - endLine: range.end.line, - endOffset: range.end.offset, + startLine: start.line, + startOffset: start.offset, + endLine: end.line, + endOffset: end.offset, }); - expect(normalizeCodeFixActions(res.body!)).toStrictEqual([]); + expect(actions).toStrictEqual([]); }); }); describe('prioritizeNamedImports: true', () => { test('omits the default styles binding code fix', async () => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports: true, prioritizeNamedImports: true }, }), @@ -243,21 +229,21 @@ describe('named import code fix (namedExports: true)', () => { }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const range = getRange('index.ts', 'styles'); - const res = await tsserver.sendGetCodeFixes({ + const { start, end } = getFileSpan('index.ts', 'styles'); + const actions = await tsserver.sendGetCodeFixes({ errorCodes: [CANNOT_FIND_NAME_ERROR_CODE], file: iff.paths['index.ts'], - startLine: range.start.line, - startOffset: range.start.offset, - endLine: range.end.line, - endOffset: range.end.offset, + startLine: start.line, + startOffset: start.offset, + endLine: end.line, + endOffset: end.offset, }); - expect(normalizeCodeFixActions(res.body!)).toStrictEqual([]); + expect(actions).toStrictEqual([]); }); test('suggests a named import code fix', async () => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports: true, prioritizeNamedImports: true }, }), @@ -266,35 +252,33 @@ describe('named import code fix (namedExports: true)', () => { }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const range = getRange('index.ts', 'a_1'); - const res = await tsserver.sendGetCodeFixes({ + const { start, end } = getFileSpan('index.ts', 'a_1'); + const actions = await tsserver.sendGetCodeFixes({ errorCodes: [CANNOT_FIND_NAME_ERROR_CODE], file: iff.paths['index.ts'], - startLine: range.start.line, - startOffset: range.start.offset, - endLine: range.end.line, - endOffset: range.end.offset, + startLine: start.line, + startOffset: start.offset, + endLine: end.line, + endOffset: end.offset, }); - expect(normalizeCodeFixActions(res.body!)).toStrictEqual( - normalizeCodeFixActions([ - { - fixName: 'import', - changes: [ - { - fileName: formatPath(iff.paths['index.ts']), - textChanges: [ - { - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 1 }, - newText: `import { a_1 } from "./a.module.css";${ts.sys.newLine}${ts.sys.newLine}`, - }, - ], - }, - ], - }, - ]), - ); + expect(actions).toStrictEqual([ + { + fixName: 'import', + changes: [ + { + fileName: formatPath(iff.paths['index.ts']), + textChanges: [ + { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + newText: `import { a_1 } from "./a.module.css";${ts.sys.newLine}${ts.sys.newLine}`, + }, + ], + }, + ], + }, + ]); }); }); }); diff --git a/packages/ts-plugin/e2e-test/completion.test.ts b/packages/ts-plugin/e2e-test/completion.test.ts index ab32fa06..db5cdb7d 100644 --- a/packages/ts-plugin/e2e-test/completion.test.ts +++ b/packages/ts-plugin/e2e-test/completion.test.ts @@ -4,7 +4,7 @@ import ts from 'typescript'; import { describe, expect, test } from 'vite-plus/test'; import { buildStylesImport, buildTSConfigJSON } from '../src/test/builder.js'; import { setupFixture } from './test-util/fixture.js'; -import { launchTsserver, normalizeCompletionDetails, normalizeCompletionEntry } from './test-util/tsserver.js'; +import { launchTsserver } from './test-util/tsserver.js'; const reactDtsPath = join(require.resolve('@types/react/package.json'), '../index.d.ts'); const tsserver = launchTsserver(); @@ -12,7 +12,7 @@ const tsserver = launchTsserver(); describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { describe('styles binding suggestion', () => { test('prioritizes the CSS module corresponding to the current component file', async () => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.tsx': `styles;`, 'a.module.css': '', @@ -26,23 +26,19 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }, }); - const res = await tsserver.sendCompletionInfo({ + const entries = await tsserver.sendCompletionInfo({ file: iff.paths['a.tsx'], - ...getRange('a.tsx', 'styles').end, + ...getFileSpan('a.tsx', 'styles').end, }); - expect( - normalizeCompletionEntry(res.body?.entries.filter((entry) => entry.name === 'styles') ?? []), - ).toStrictEqual( - normalizeCompletionEntry([ - { name: 'styles', sortText: '0', source: './a.module.css' }, - { name: 'styles', sortText: '16', source: './b.module.css' }, - ]), - ); + expect(entries.filter((entry) => entry.name === 'styles')).toStrictEqual([ + { name: 'styles', sortText: '0', source: './a.module.css' }, + { name: 'styles', sortText: '16', source: './b.module.css' }, + ]); }); test('excludes generated files from suggestions', async () => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports, dtsOutDir: 'generated' }, }), @@ -61,18 +57,18 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }, }); - const res = await tsserver.sendCompletionInfo({ + const entries = await tsserver.sendCompletionInfo({ file: iff.paths['a.tsx'], - ...getRange('a.tsx', 'styles').end, + ...getFileSpan('a.tsx', 'styles').end, }); - expect( - normalizeCompletionEntry(res.body?.entries.filter((entry) => entry.name === 'styles') ?? []), - ).toStrictEqual(normalizeCompletionEntry([{ name: 'styles', sortText: '0', source: './a.module.css' }])); + expect(entries.filter((entry) => entry.name === 'styles')).toStrictEqual([ + { name: 'styles', sortText: '0', source: './a.module.css' }, + ]); }); test('inserts the import statement when accepted', async () => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': `styles;`, 'a.module.css': '', @@ -82,9 +78,9 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: preferences: { quotePreference: 'single' }, }); - const res = await tsserver.sendCompletionDetails({ + const details = await tsserver.sendCompletionDetails({ file: iff.paths['index.ts'], - ...getRange('index.ts', 'styles').end, + ...getFileSpan('index.ts', 'styles').end, entryNames: [ { name: 'styles', @@ -99,7 +95,7 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); const importStatement = buildStylesImport('./a.module.css', { namedExports }); - expect(normalizeCompletionDetails(res.body!)).toStrictEqual([ + expect(details).toStrictEqual([ { codeActions: [ { @@ -122,7 +118,7 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); test('suggests the styles entry with an import-alias source', async () => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ compilerOptions: { paths: { '@/*': ['./*'] } }, cmkOptions: { namedExports }, @@ -138,14 +134,14 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }, }); - const res = await tsserver.sendCompletionInfo({ + const entries = await tsserver.sendCompletionInfo({ file: iff.paths['index.ts'], - ...getRange('index.ts', 'styles').end, + ...getFileSpan('index.ts', 'styles').end, }); - expect( - normalizeCompletionEntry(res.body?.entries.filter((entry) => entry.name === 'styles') ?? []), - ).toStrictEqual(normalizeCompletionEntry([{ name: 'styles', sortText: '16', source: '@/a.module.css' }])); + expect(entries.filter((entry) => entry.name === 'styles')).toStrictEqual([ + { name: 'styles', sortText: '16', source: '@/a.module.css' }, + ]); }); }); @@ -153,7 +149,7 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: test.each([{ quotePreference: 'single' as const }, { quotePreference: 'double' as const }])( 'completes as className={$$1} with quotePreference: $quotePreference', async ({ quotePreference }) => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ compilerOptions: { jsx: 'react-jsx', types: [reactDtsPath] }, cmkOptions: { namedExports }, @@ -174,16 +170,14 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }, }); - const res = await tsserver.sendCompletionInfo({ + const entries = await tsserver.sendCompletionInfo({ file: iff.paths['a.tsx'], - ...getRange('a.tsx', 'className').end, + ...getFileSpan('a.tsx', 'className').end, }); - expect( - normalizeCompletionEntry(res.body?.entries.filter((entry) => entry.name === 'className') ?? []), - ).toStrictEqual( - normalizeCompletionEntry([{ name: 'className', insertText: 'className={$1}', sortText: expect.anything() }]), - ); + expect(entries.filter((entry) => entry.name === 'className')).toStrictEqual([ + { name: 'className', insertText: 'className={$1}', sortText: expect.anything() }, + ]); }, ); }); @@ -192,7 +186,7 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: describe('prioritizeNamedImports (namedExports: true)', () => { describe('prioritizeNamedImports: false', () => { test('omits named token auto-imports', async () => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports: true, prioritizeNamedImports: false }, }), @@ -204,18 +198,16 @@ describe('prioritizeNamedImports (namedExports: true)', () => { preferences: { includeCompletionsForModuleExports: true }, }); - const res = await tsserver.sendCompletionInfo({ + const entries = await tsserver.sendCompletionInfo({ file: iff.paths['index.ts'], - ...getRange('index.ts', 'a_1').end, + ...getFileSpan('index.ts', 'a_1').end, }); - expect(normalizeCompletionEntry(res.body?.entries.filter((entry) => entry.name === 'a_1') ?? [])).toStrictEqual( - [], - ); + expect(entries.filter((entry) => entry.name === 'a_1')).toStrictEqual([]); }); test('omits the default export from namespace member completion', async () => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports: true, prioritizeNamedImports: false }, }), @@ -230,12 +222,12 @@ describe('prioritizeNamedImports (namedExports: true)', () => { preferences: { includeCompletionsForModuleExports: true }, }); - const res = await tsserver.sendCompletionInfo({ + const entries = await tsserver.sendCompletionInfo({ file: iff.paths['index.ts'], - ...getRange('index.ts', 'styles.').end, + ...getFileSpan('index.ts', 'styles.').end, }); - const names = res.body?.entries.map((entry) => entry.name) ?? []; + const names = entries.map((entry) => entry.name); expect(names).toContain('a_1'); expect(names).not.toContain('default'); }); @@ -243,7 +235,7 @@ describe('prioritizeNamedImports (namedExports: true)', () => { describe('prioritizeNamedImports: true', () => { test('omits the styles binding auto-import', async () => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports: true, prioritizeNamedImports: true }, }), @@ -255,18 +247,16 @@ describe('prioritizeNamedImports (namedExports: true)', () => { preferences: { includeCompletionsForModuleExports: true }, }); - const res = await tsserver.sendCompletionInfo({ + const entries = await tsserver.sendCompletionInfo({ file: iff.paths['index.ts'], - ...getRange('index.ts', 'styles').end, + ...getFileSpan('index.ts', 'styles').end, }); - expect( - normalizeCompletionEntry(res.body?.entries.filter((entry) => entry.name === 'styles') ?? []), - ).toStrictEqual([]); + expect(entries.filter((entry) => entry.name === 'styles')).toStrictEqual([]); }); test('suggests named token auto-imports', async () => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports: true, prioritizeNamedImports: true }, }), @@ -278,14 +268,14 @@ describe('prioritizeNamedImports (namedExports: true)', () => { preferences: { includeCompletionsForModuleExports: true }, }); - const res = await tsserver.sendCompletionInfo({ + const entries = await tsserver.sendCompletionInfo({ file: iff.paths['index.ts'], - ...getRange('index.ts', 'a_1').end, + ...getFileSpan('index.ts', 'a_1').end, }); - expect(normalizeCompletionEntry(res.body?.entries.filter((entry) => entry.name === 'a_1') ?? [])).toStrictEqual( - normalizeCompletionEntry([{ name: 'a_1', sortText: '16', source: './a.module.css' }]), - ); + expect(entries.filter((entry) => entry.name === 'a_1')).toStrictEqual([ + { name: 'a_1', sortText: '16', source: './a.module.css' }, + ]); }); }); }); diff --git a/packages/ts-plugin/e2e-test/enabled.test.ts b/packages/ts-plugin/e2e-test/enabled.test.ts index d4555a5d..afb0e7ce 100644 --- a/packages/ts-plugin/e2e-test/enabled.test.ts +++ b/packages/ts-plugin/e2e-test/enabled.test.ts @@ -1,12 +1,12 @@ import dedent from 'dedent'; import { expect, test } from 'vite-plus/test'; import { setupFixture } from './test-util/fixture.js'; -import { launchTsserver, normalizeDefinitions } from './test-util/tsserver.js'; +import { launchTsserver } from './test-util/tsserver.js'; const tsserver = launchTsserver(); test('returns no Go to Definition results when cmkOptions.enabled is false', async () => { - const { iff, getLoc } = await setupFixture({ + const { iff, getFileLocation } = await setupFixture({ 'tsconfig.json': `{ "cmkOptions": { "enabled": false } }`, 'index.ts': dedent` import styles from './a.module.css'; @@ -16,10 +16,7 @@ test('returns no Go to Definition results when cmkOptions.enabled is false', asy }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'a_1'), - }); + const definitions = await tsserver.sendDefinitionAndBoundSpan(getFileLocation('index.ts', 'a_1')); - expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual([]); + expect(definitions).toStrictEqual([]); }); diff --git a/packages/ts-plugin/e2e-test/file-events.test.ts b/packages/ts-plugin/e2e-test/file-events.test.ts index fcad565a..225fd207 100644 --- a/packages/ts-plugin/e2e-test/file-events.test.ts +++ b/packages/ts-plugin/e2e-test/file-events.test.ts @@ -9,7 +9,7 @@ const tsserver = launchTsserver(); describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { describe('when adding a CSS module', () => { test("updates the importer's diagnostic when a CSS module is added", async () => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -19,12 +19,14 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); const before = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['index.ts'] }); + const { start, end } = getFileSpan('index.ts', "'./a.module.css'"); expect(before.body).toStrictEqual([ { category: 'error', code: 2307, text: "Cannot find module './a.module.css' or its corresponding type declarations.", - ...getRange('index.ts', "'./a.module.css'"), + start, + end, }, ]); @@ -40,7 +42,8 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: category: 'error', code: 2307, text: "Cannot find module './a.module.css' or its corresponding type declarations.", - ...getRange('index.ts', "'./a.module.css'"), + start, + end, }, ]); }); @@ -48,7 +51,7 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: describe('when updating a CSS module', () => { test("updates the importer's diagnostic when a CSS module is modified", async () => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -59,11 +62,13 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); const before = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['index.ts'] }); + const { start, end } = getFileSpan('index.ts', 'a_1'); expect(before.body).toStrictEqual([ { category: 'error', code: 2339, - ...getRange('index.ts', 'a_1'), + start, + end, text: expect.any(String), }, ]); diff --git a/packages/ts-plugin/e2e-test/find-all-references.test.ts b/packages/ts-plugin/e2e-test/find-all-references.test.ts index 761977a2..3432e596 100644 --- a/packages/ts-plugin/e2e-test/find-all-references.test.ts +++ b/packages/ts-plugin/e2e-test/find-all-references.test.ts @@ -2,14 +2,14 @@ import dedent from 'dedent'; import { describe, expect, test } from 'vite-plus/test'; import { buildStylesImport, buildTSConfigJSON } from '../src/test/builder.js'; import { setupFixture } from './test-util/fixture.js'; -import { formatPath, launchTsserver, normalizeRefItems } from './test-util/tsserver.js'; +import { launchTsserver } from './test-util/tsserver.js'; const tsserver = launchTsserver(); describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { describe('for a TS-side import statement', () => { test('from the styles binding', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -23,24 +23,19 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendReferences({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'styles', 0), - }); + const refs = await tsserver.sendReferences(getFileLocation('index.ts', 'styles', 0)); - expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( - normalizeRefItems([ - { file: formatPath(iff.paths['index.ts']), ...getRange('index.ts', 'styles', 0) }, - { file: formatPath(iff.paths['index.ts']), ...getRange('index.ts', 'styles', 1) }, - { file: formatPath(iff.paths['index.ts']), ...getRange('index.ts', 'styles', 2) }, - ]), - ); + expect(refs).toStrictEqual([ + getFileSpan('index.ts', 'styles', { index: 0 }), + getFileSpan('index.ts', 'styles', { index: 1 }), + getFileSpan('index.ts', 'styles', { index: 2 }), + ]); }); }); describe('for a token definition', () => { test('from a TS-side styles.', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -50,21 +45,13 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendReferences({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'a_1'), - }); + const refs = await tsserver.sendReferences(getFileLocation('index.ts', 'a_1')); - expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( - normalizeRefItems([ - { file: formatPath(iff.paths['index.ts']), ...getRange('index.ts', 'a_1') }, - { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'a_1') }, - ]), - ); + expect(refs).toStrictEqual([getFileSpan('a.module.css', 'a_1'), getFileSpan('index.ts', 'a_1')]); }); test('from a TS-side styles[]', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -74,21 +61,13 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendReferences({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'a-1'), - }); + const refs = await tsserver.sendReferences(getFileLocation('index.ts', 'a-1')); - expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( - normalizeRefItems([ - { file: formatPath(iff.paths['index.ts']), ...getRange('index.ts', 'a-1') }, - { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'a-1') }, - ]), - ); + expect(refs).toStrictEqual([getFileSpan('a.module.css', 'a-1'), getFileSpan('index.ts', 'a-1')]); }); test('when the token is declared multiple times', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -101,22 +80,17 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendReferences({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'a_1'), - }); + const refs = await tsserver.sendReferences(getFileLocation('index.ts', 'a_1')); - expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( - normalizeRefItems([ - { file: formatPath(iff.paths['index.ts']), ...getRange('index.ts', 'a_1') }, - { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'a_1', 0) }, - { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'a_1', 1) }, - ]), - ); + expect(refs).toStrictEqual([ + getFileSpan('a.module.css', 'a_1', { index: 0 }), + getFileSpan('a.module.css', 'a_1', { index: 1 }), + getFileSpan('index.ts', 'a_1'), + ]); }); test('from a CSS-side token definition', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -126,23 +100,15 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendReferences({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'a_1'), - }); + const refs = await tsserver.sendReferences(getFileLocation('a.module.css', 'a_1')); - expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( - normalizeRefItems([ - { file: formatPath(iff.paths['index.ts']), ...getRange('index.ts', 'a_1') }, - { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'a_1') }, - ]), - ); + expect(refs).toStrictEqual([getFileSpan('a.module.css', 'a_1'), getFileSpan('index.ts', 'a_1')]); }); }); describe('for an all token importer', () => { test('from a TS-side styles.', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -153,23 +119,15 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendReferences({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'b_1'), - }); + const refs = await tsserver.sendReferences(getFileLocation('index.ts', 'b_1')); - expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( - normalizeRefItems([ - { file: formatPath(iff.paths['index.ts']), ...getRange('index.ts', 'b_1') }, - { file: formatPath(iff.paths['b.module.css']), ...getRange('b.module.css', 'b_1') }, - ]), - ); + expect(refs).toStrictEqual([getFileSpan('b.module.css', 'b_1'), getFileSpan('index.ts', 'b_1')]); }); }); describe('for a named token importer', () => { test('from a TS-side styles.', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -180,23 +138,18 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendReferences({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'b_1'), - }); + const refs = await tsserver.sendReferences(getFileLocation('index.ts', 'b_1')); - expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( - normalizeRefItems([ - { file: formatPath(iff.paths['index.ts']), ...getRange('index.ts', 'b_1') }, - { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'b_1') }, - { file: formatPath(iff.paths['b.module.css']), ...getRange('b.module.css', 'b_1') }, - ]), - ); + expect(refs).toStrictEqual([ + getFileSpan('a.module.css', 'b_1'), + getFileSpan('b.module.css', 'b_1'), + getFileSpan('index.ts', 'b_1'), + ]); }); // NOTE: Ideally only `b_alias` should be returned, but `b_1` is also returned for implementation simplicity. test('from a TS-side styles.', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -207,92 +160,69 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendReferences({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'b_alias'), - }); + const refs = await tsserver.sendReferences(getFileLocation('index.ts', 'b_alias')); - expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( - normalizeRefItems([ - { file: formatPath(iff.paths['index.ts']), ...getRange('index.ts', 'b_alias') }, - { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'b_1') }, - { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'b_alias') }, - { file: formatPath(iff.paths['b.module.css']), ...getRange('b.module.css', 'b_1') }, - ]), - ); + expect(refs).toStrictEqual([ + getFileSpan('a.module.css', 'b_1'), + getFileSpan('a.module.css', 'b_alias'), + getFileSpan('b.module.css', 'b_1'), + getFileSpan('index.ts', 'b_alias'), + ]); }); test('from a CSS-side ', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': `@value b_1 from './b.module.css';`, 'b.module.css': `@value b_1: red;`, }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendReferences({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'b_1'), - }); + const refs = await tsserver.sendReferences(getFileLocation('a.module.css', 'b_1')); - expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( - normalizeRefItems([ - { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'b_1') }, - { file: formatPath(iff.paths['b.module.css']), ...getRange('b.module.css', 'b_1') }, - ]), - ); + expect(refs).toStrictEqual([getFileSpan('a.module.css', 'b_1'), getFileSpan('b.module.css', 'b_1')]); }); // NOTE: Ideally only `b_1` should be returned, but `b_alias` is also returned for implementation simplicity. test('from a CSS-side with alias', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': `@value b_1 as b_alias from './b.module.css';`, 'b.module.css': `@value b_1: red;`, }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendReferences({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'b_1'), - }); + const refs = await tsserver.sendReferences(getFileLocation('a.module.css', 'b_1')); - expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( - normalizeRefItems([ - { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'b_1') }, - { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'b_alias') }, - { file: formatPath(iff.paths['b.module.css']), ...getRange('b.module.css', 'b_1') }, - ]), - ); + expect(refs).toStrictEqual([ + getFileSpan('a.module.css', 'b_1'), + getFileSpan('a.module.css', 'b_alias'), + getFileSpan('b.module.css', 'b_1'), + ]); }); // NOTE: Ideally only `b_alias` should be returned, but `b_1` is also returned for implementation simplicity. test('from a CSS-side ', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': `@value b_1 as b_alias from './b.module.css';`, 'b.module.css': `@value b_1: red;`, }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendReferences({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'b_alias'), - }); + const refs = await tsserver.sendReferences(getFileLocation('a.module.css', 'b_alias')); - expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( - normalizeRefItems([ - { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'b_1') }, - { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'b_alias') }, - { file: formatPath(iff.paths['b.module.css']), ...getRange('b.module.css', 'b_1') }, - ]), - ); + expect(refs).toStrictEqual([ + getFileSpan('a.module.css', 'b_1'), + getFileSpan('a.module.css', 'b_alias'), + getFileSpan('b.module.css', 'b_1'), + ]); }); }); describe('for a local token reference', () => { test('from a token definition', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': dedent` @keyframes a_1 { from {} to {} } @@ -302,22 +232,17 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendReferences({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'a_1', 0), - }); + const refs = await tsserver.sendReferences(getFileLocation('a.module.css', 'a_1', 0)); - expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( - normalizeRefItems([ - { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'a_1', 0) }, - { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'a_1', 1) }, - { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'a_1', 2) }, - ]), - ); + expect(refs).toStrictEqual([ + getFileSpan('a.module.css', 'a_1', { index: 0 }), + getFileSpan('a.module.css', 'a_1', { index: 1 }), + getFileSpan('a.module.css', 'a_1', { index: 2 }), + ]); }); test('from a local token reference', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': dedent` @keyframes a_1 { from {} to {} } @@ -327,41 +252,28 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendReferences({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'a_1', 1), - }); + const refs = await tsserver.sendReferences(getFileLocation('a.module.css', 'a_1', 1)); - expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( - normalizeRefItems([ - { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'a_1', 0) }, - { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'a_1', 1) }, - { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'a_1', 2) }, - ]), - ); + expect(refs).toStrictEqual([ + getFileSpan('a.module.css', 'a_1', { index: 0 }), + getFileSpan('a.module.css', 'a_1', { index: 1 }), + getFileSpan('a.module.css', 'a_1', { index: 2 }), + ]); }); }); describe('for an external token reference', () => { test('from an external token reference', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': `.a_1 { composes: b_1 from './b.module.css'; }`, 'b.module.css': `.b_1 { color: red; }`, }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendReferences({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'b_1'), - }); + const refs = await tsserver.sendReferences(getFileLocation('a.module.css', 'b_1')); - expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( - normalizeRefItems([ - { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'b_1') }, - { file: formatPath(iff.paths['b.module.css']), ...getRange('b.module.css', 'b_1') }, - ]), - ); + expect(refs).toStrictEqual([getFileSpan('a.module.css', 'b_1'), getFileSpan('b.module.css', 'b_1')]); }); }); }); diff --git a/packages/ts-plugin/e2e-test/go-to-definition.test.ts b/packages/ts-plugin/e2e-test/go-to-definition.test.ts index 37287340..765e9732 100644 --- a/packages/ts-plugin/e2e-test/go-to-definition.test.ts +++ b/packages/ts-plugin/e2e-test/go-to-definition.test.ts @@ -2,64 +2,54 @@ import dedent from 'dedent'; import { describe, expect, test } from 'vite-plus/test'; import { buildStylesImport, buildTSConfigJSON } from '../src/test/builder.js'; import { setupFixture } from './test-util/fixture.js'; -import { formatPath, launchTsserver, normalizeDefinitions } from './test-util/tsserver.js'; +import { formatPath, launchTsserver } from './test-util/tsserver.js'; const tsserver = launchTsserver(); describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { describe('for a TS-side import statement', () => { test('from the styles binding', async () => { - const { iff, getLoc } = await setupFixture({ + const { iff, getFileLocation } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': buildStylesImport('./a.module.css', { namedExports }), 'a.module.css': '', }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'styles'), - }); + const definitions = await tsserver.sendDefinitionAndBoundSpan(getFileLocation('index.ts', 'styles')); - expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual( - normalizeDefinitions([ - { - file: formatPath(iff.paths['a.module.css']), - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 1 }, - }, - ]), - ); + expect(definitions).toStrictEqual([ + { + file: formatPath(iff.paths['a.module.css']), + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + }, + ]); }); test('from the import specifier', async () => { - const { iff, getLoc } = await setupFixture({ + const { iff, getFileLocation } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': buildStylesImport('./a.module.css', { namedExports }), 'a.module.css': '', }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', "'./a.module.css'"), - }); + const definitions = await tsserver.sendDefinitionAndBoundSpan(getFileLocation('index.ts', "'./a.module.css'")); - expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual( - normalizeDefinitions([ - { - file: formatPath(iff.paths['a.module.css']), - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 1 }, - }, - ]), - ); + expect(definitions).toStrictEqual([ + { + file: formatPath(iff.paths['a.module.css']), + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + }, + ]); }); }); describe('for a token definition', () => { test('from a TS-side styles.', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -69,26 +59,13 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'a_1'), - }); + const definitions = await tsserver.sendDefinitionAndBoundSpan(getFileLocation('index.ts', 'a_1')); - const { start: contextStart, end: contextEnd } = getRange('a.module.css', '.a_1 { color: red; }'); - expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual( - normalizeDefinitions([ - { - file: formatPath(iff.paths['a.module.css']), - ...getRange('a.module.css', 'a_1'), - contextStart, - contextEnd, - }, - ]), - ); + expect(definitions).toStrictEqual([getFileSpan('a.module.css', 'a_1', { context: '.a_1 { color: red; }' })]); }); test('from a TS-side styles[]', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -98,26 +75,13 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'a-1'), - }); + const definitions = await tsserver.sendDefinitionAndBoundSpan(getFileLocation('index.ts', 'a-1')); - const { start: contextStart, end: contextEnd } = getRange('a.module.css', '.a-1 { color: red; }'); - expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual( - normalizeDefinitions([ - { - file: formatPath(iff.paths['a.module.css']), - ...getRange('a.module.css', 'a-1'), - contextStart, - contextEnd, - }, - ]), - ); + expect(definitions).toStrictEqual([getFileSpan('a.module.css', 'a-1', { context: '.a-1 { color: red; }' })]); }); test('when the token is declared multiple times', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -130,33 +94,16 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'a_1'), - }); + const definitions = await tsserver.sendDefinitionAndBoundSpan(getFileLocation('index.ts', 'a_1')); - const decl1Range = getRange('a.module.css', '.a_1 { color: red; }', 0); - const decl2Range = getRange('a.module.css', '.a_1 { color: red; }', 1); - expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual( - normalizeDefinitions([ - { - file: formatPath(iff.paths['a.module.css']), - ...getRange('a.module.css', 'a_1', 0), - contextStart: decl1Range.start, - contextEnd: decl1Range.end, - }, - { - file: formatPath(iff.paths['a.module.css']), - ...getRange('a.module.css', 'a_1', 1), - contextStart: decl2Range.start, - contextEnd: decl2Range.end, - }, - ]), - ); + expect(definitions).toStrictEqual([ + getFileSpan('a.module.css', 'a_1', { index: 0, context: '.a_1 { color: red; }' }), + getFileSpan('a.module.css', 'a_1', { index: 1, context: '.a_1 { color: red; }' }), + ]); }); test('from a CSS-side token definition', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -166,28 +113,15 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'a_1'), - }); + const definitions = await tsserver.sendDefinitionAndBoundSpan(getFileLocation('a.module.css', 'a_1')); - const { start: contextStart, end: contextEnd } = getRange('a.module.css', '.a_1 { color: red; }'); - expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual( - normalizeDefinitions([ - { - file: formatPath(iff.paths['a.module.css']), - ...getRange('a.module.css', 'a_1'), - contextStart, - contextEnd, - }, - ]), - ); + expect(definitions).toStrictEqual([getFileSpan('a.module.css', 'a_1', { context: '.a_1 { color: red; }' })]); }); }); describe('for an all token importer', () => { test('from a TS-side styles.', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -198,26 +132,13 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'b_1'), - }); + const definitions = await tsserver.sendDefinitionAndBoundSpan(getFileLocation('index.ts', 'b_1')); - const { start: contextStart, end: contextEnd } = getRange('b.module.css', '.b_1 { color: red; }'); - expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual( - normalizeDefinitions([ - { - file: formatPath(iff.paths['b.module.css']), - ...getRange('b.module.css', 'b_1'), - contextStart, - contextEnd, - }, - ]), - ); + expect(definitions).toStrictEqual([getFileSpan('b.module.css', 'b_1', { context: '.b_1 { color: red; }' })]); }); test('from a CSS-side specifier', async () => { - const { iff, getLoc } = await setupFixture({ + const { iff, getFileLocation } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': buildStylesImport('./a.module.css', { namedExports }), 'a.module.css': `@import './b.module.css';`, @@ -225,25 +146,22 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', "'./b.module.css'"), - }); - - expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual( - normalizeDefinitions([ - { - file: formatPath(iff.paths['b.module.css']), - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 1 }, - }, - ]), + const definitions = await tsserver.sendDefinitionAndBoundSpan( + getFileLocation('a.module.css', "'./b.module.css'"), ); + + expect(definitions).toStrictEqual([ + { + file: formatPath(iff.paths['b.module.css']), + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + }, + ]); }); // NOTE: It is strange that `(` has a definition, but we allow it to keep the implementation simple. test('from inside a CSS-side url() specifier', async () => { - const { iff, getLoc } = await setupFixture({ + const { iff, getFileLocation } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': buildStylesImport('./a.module.css', { namedExports }), 'a.module.css': `@import url(./b.module.css);`, @@ -251,26 +169,23 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', '(./b.module.css)'), - }); - - expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual( - normalizeDefinitions([ - { - file: formatPath(iff.paths['b.module.css']), - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 1 }, - }, - ]), + const definitions = await tsserver.sendDefinitionAndBoundSpan( + getFileLocation('a.module.css', '(./b.module.css)'), ); + + expect(definitions).toStrictEqual([ + { + file: formatPath(iff.paths['b.module.css']), + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + }, + ]); }); }); describe('for a named token importer', () => { test('from a TS-side styles.', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -281,26 +196,13 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'b_1'), - }); + const definitions = await tsserver.sendDefinitionAndBoundSpan(getFileLocation('index.ts', 'b_1')); - const { start: contextStart, end: contextEnd } = getRange('b.module.css', '@value b_1: red'); - expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual( - normalizeDefinitions([ - { - file: formatPath(iff.paths['b.module.css']), - ...getRange('b.module.css', 'b_1'), - contextStart, - contextEnd, - }, - ]), - ); + expect(definitions).toStrictEqual([getFileSpan('b.module.css', 'b_1', { context: '@value b_1: red' })]); }); test('from a TS-side styles.', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -311,26 +213,13 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'b_alias'), - }); + const definitions = await tsserver.sendDefinitionAndBoundSpan(getFileLocation('index.ts', 'b_alias')); - const { start: contextStart, end: contextEnd } = getRange('b.module.css', '@value b_1: red'); - expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual( - normalizeDefinitions([ - { - file: formatPath(iff.paths['b.module.css']), - ...getRange('b.module.css', 'b_1'), - contextStart, - contextEnd, - }, - ]), - ); + expect(definitions).toStrictEqual([getFileSpan('b.module.css', 'b_1', { context: '@value b_1: red' })]); }); test('from a CSS-side specifier', async () => { - const { iff, getLoc } = await setupFixture({ + const { iff, getFileLocation } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': buildStylesImport('./a.module.css', { namedExports }), 'a.module.css': `@value b_1 from './b.module.css';`, @@ -338,104 +227,62 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', "'./b.module.css'"), - }); - - expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual( - normalizeDefinitions([ - { - file: formatPath(iff.paths['b.module.css']), - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 1 }, - }, - ]), + const definitions = await tsserver.sendDefinitionAndBoundSpan( + getFileLocation('a.module.css', "'./b.module.css'"), ); + + expect(definitions).toStrictEqual([ + { + file: formatPath(iff.paths['b.module.css']), + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + }, + ]); }); test('from a CSS-side ', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': `@value b_1 from './b.module.css';`, 'b.module.css': `@value b_1: red;`, }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'b_1'), - }); + const definitions = await tsserver.sendDefinitionAndBoundSpan(getFileLocation('a.module.css', 'b_1')); - const { start: contextStart, end: contextEnd } = getRange('b.module.css', '@value b_1: red'); - expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual( - normalizeDefinitions([ - { - file: formatPath(iff.paths['b.module.css']), - ...getRange('b.module.css', 'b_1'), - contextStart, - contextEnd, - }, - ]), - ); + expect(definitions).toStrictEqual([getFileSpan('b.module.css', 'b_1', { context: '@value b_1: red' })]); }); test('from a CSS-side with alias', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': `@value b_1 as b_alias from './b.module.css';`, 'b.module.css': `@value b_1: red;`, }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'b_1'), - }); + const definitions = await tsserver.sendDefinitionAndBoundSpan(getFileLocation('a.module.css', 'b_1')); - const { start: contextStart, end: contextEnd } = getRange('b.module.css', '@value b_1: red'); - expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual( - normalizeDefinitions([ - { - file: formatPath(iff.paths['b.module.css']), - ...getRange('b.module.css', 'b_1'), - contextStart, - contextEnd, - }, - ]), - ); + expect(definitions).toStrictEqual([getFileSpan('b.module.css', 'b_1', { context: '@value b_1: red' })]); }); test('from a CSS-side ', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': `@value b_1 as b_alias from './b.module.css';`, 'b.module.css': `@value b_1: red;`, }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'b_alias'), - }); + const definitions = await tsserver.sendDefinitionAndBoundSpan(getFileLocation('a.module.css', 'b_alias')); - const { start: contextStart, end: contextEnd } = getRange('b.module.css', '@value b_1: red'); - expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual( - normalizeDefinitions([ - { - file: formatPath(iff.paths['b.module.css']), - ...getRange('b.module.css', 'b_1'), - contextStart, - contextEnd, - }, - ]), - ); + expect(definitions).toStrictEqual([getFileSpan('b.module.css', 'b_1', { context: '@value b_1: red' })]); }); }); describe('for a local token reference', () => { test('from a local token reference', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': dedent` @keyframes a_1 { from {} to {} } @@ -444,26 +291,15 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'a_1', 1), - }); + const definitions = await tsserver.sendDefinitionAndBoundSpan(getFileLocation('a.module.css', 'a_1', 1)); - const { start: contextStart, end: contextEnd } = getRange('a.module.css', '@keyframes a_1 { from {} to {} }'); - expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual( - normalizeDefinitions([ - { - file: formatPath(iff.paths['a.module.css']), - ...getRange('a.module.css', 'a_1', 0), - contextStart, - contextEnd, - }, - ]), - ); + expect(definitions).toStrictEqual([ + getFileSpan('a.module.css', 'a_1', { index: 0, context: '@keyframes a_1 { from {} to {} }' }), + ]); }); test('from each in a multi-value local token reference', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': dedent` @keyframes a_1 { from {} to {} } @@ -473,41 +309,19 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const a1Res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'a_1', 1), - }); - expect(normalizeDefinitions(a1Res.body?.definitions ?? [])).toStrictEqual( - normalizeDefinitions([ - { - file: formatPath(iff.paths['a.module.css']), - ...getRange('a.module.css', 'a_1', 0), - ...(({ start, end }) => ({ contextStart: start, contextEnd: end }))( - getRange('a.module.css', '@keyframes a_1 { from {} to {} }'), - ), - }, - ]), - ); + const a1Definitions = await tsserver.sendDefinitionAndBoundSpan(getFileLocation('a.module.css', 'a_1', 1)); + expect(a1Definitions).toStrictEqual([ + getFileSpan('a.module.css', 'a_1', { index: 0, context: '@keyframes a_1 { from {} to {} }' }), + ]); - const a2Res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'a_2', 1), - }); - expect(normalizeDefinitions(a2Res.body?.definitions ?? [])).toStrictEqual( - normalizeDefinitions([ - { - file: formatPath(iff.paths['a.module.css']), - ...getRange('a.module.css', 'a_2', 0), - ...(({ start, end }) => ({ contextStart: start, contextEnd: end }))( - getRange('a.module.css', '@keyframes a_2 { from {} to {} }'), - ), - }, - ]), - ); + const a2Definitions = await tsserver.sendDefinitionAndBoundSpan(getFileLocation('a.module.css', 'a_2', 1)); + expect(a2Definitions).toStrictEqual([ + getFileSpan('a.module.css', 'a_2', { index: 0, context: '@keyframes a_2 { from {} to {} }' }), + ]); }); test('from a kebab-case local token reference', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': dedent` @keyframes a-1 { from {} to {} } @@ -516,26 +330,15 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'a-1', 1), - }); + const definitions = await tsserver.sendDefinitionAndBoundSpan(getFileLocation('a.module.css', 'a-1', 1)); - const { start: contextStart, end: contextEnd } = getRange('a.module.css', '@keyframes a-1 { from {} to {} }'); - expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual( - normalizeDefinitions([ - { - file: formatPath(iff.paths['a.module.css']), - ...getRange('a.module.css', 'a-1', 0), - contextStart, - contextEnd, - }, - ]), - ); + expect(definitions).toStrictEqual([ + getFileSpan('a.module.css', 'a-1', { index: 0, context: '@keyframes a-1 { from {} to {} }' }), + ]); }); test('from a local token reference whose target is imported', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': dedent` @import './b.module.css'; @@ -545,50 +348,26 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'b_1'), - }); + const definitions = await tsserver.sendDefinitionAndBoundSpan(getFileLocation('a.module.css', 'b_1')); - const { start: contextStart, end: contextEnd } = getRange('b.module.css', '@keyframes b_1 { from {} to {} }'); - expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual( - normalizeDefinitions([ - { - file: formatPath(iff.paths['b.module.css']), - ...getRange('b.module.css', 'b_1'), - contextStart, - contextEnd, - }, - ]), - ); + expect(definitions).toStrictEqual([ + getFileSpan('b.module.css', 'b_1', { context: '@keyframes b_1 { from {} to {} }' }), + ]); }); }); describe('for an external token reference', () => { test('from an external token reference', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': `.a_1 { composes: b_1 from './b.module.css'; }`, 'b.module.css': `.b_1 { color: red; }`, }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'b_1'), - }); + const definitions = await tsserver.sendDefinitionAndBoundSpan(getFileLocation('a.module.css', 'b_1')); - const { start: contextStart, end: contextEnd } = getRange('b.module.css', '.b_1 { color: red; }'); - expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual( - normalizeDefinitions([ - { - file: formatPath(iff.paths['b.module.css']), - ...getRange('b.module.css', 'b_1'), - contextStart, - contextEnd, - }, - ]), - ); + expect(definitions).toStrictEqual([getFileSpan('b.module.css', 'b_1', { context: '.b_1 { color: red; }' })]); }); }); }); diff --git a/packages/ts-plugin/e2e-test/ignore-generated-files.test.ts b/packages/ts-plugin/e2e-test/ignore-generated-files.test.ts index 93fb56fd..f381bc72 100644 --- a/packages/ts-plugin/e2e-test/ignore-generated-files.test.ts +++ b/packages/ts-plugin/e2e-test/ignore-generated-files.test.ts @@ -6,7 +6,7 @@ import { launchTsserver } from './test-util/tsserver.js'; const tsserver = launchTsserver(); test('excludes generated .d.ts files from module resolution even when listed in rootDirs', async () => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': dedent` { "compilerOptions": { @@ -25,11 +25,13 @@ test('excludes generated .d.ts files from module resolution even when listed in const res = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['index.ts'] }); + const { start, end } = getFileSpan('index.ts', `'./a.module.css'`); expect(res.body).toStrictEqual([ { category: 'error', code: 2307, - ...getRange('index.ts', `'./a.module.css'`), + start, + end, text: `Cannot find module './a.module.css' or its corresponding type declarations.`, }, ]); diff --git a/packages/ts-plugin/e2e-test/invalid-css-syntax.test.ts b/packages/ts-plugin/e2e-test/invalid-css-syntax.test.ts index 6ac55e34..7e2f2b7f 100644 --- a/packages/ts-plugin/e2e-test/invalid-css-syntax.test.ts +++ b/packages/ts-plugin/e2e-test/invalid-css-syntax.test.ts @@ -2,13 +2,13 @@ import dedent from 'dedent'; import { describe, expect, test } from 'vite-plus/test'; import { buildStylesImport, buildTSConfigJSON } from '../src/test/builder.js'; import { setupFixture } from './test-util/fixture.js'; -import { formatPath, launchTsserver, normalizeDefinitions } from './test-util/tsserver.js'; +import { launchTsserver } from './test-util/tsserver.js'; const tsserver = launchTsserver(); describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { test('resolves Go to Definition on a valid token even when later rules contain invalid syntax', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -21,22 +21,9 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendDefinitionAndBoundSpan({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'a_1'), - }); + const definitions = await tsserver.sendDefinitionAndBoundSpan(getFileLocation('index.ts', 'a_1')); - const { start: contextStart, end: contextEnd } = getRange('a.module.css', '.a_1 { color: red; }'); - expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual( - normalizeDefinitions([ - { - file: formatPath(iff.paths['a.module.css']), - ...getRange('a.module.css', 'a_1'), - contextStart, - contextEnd, - }, - ]), - ); + expect(definitions).toStrictEqual([getFileSpan('a.module.css', 'a_1', { context: '.a_1 { color: red; }' })]); }); test('reports no syntactic diagnostics for a CSS module with parse errors', async () => { diff --git a/packages/ts-plugin/e2e-test/rename-file.test.ts b/packages/ts-plugin/e2e-test/rename-file.test.ts index bf6f2395..6b3f8fd3 100644 --- a/packages/ts-plugin/e2e-test/rename-file.test.ts +++ b/packages/ts-plugin/e2e-test/rename-file.test.ts @@ -8,7 +8,7 @@ const tsserver = launchTsserver(); describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { describe('reports `fileToRename` so editors can initiate a file rename from a CSS specifier', () => { test('from all token importer', async () => { - const { iff, getLoc } = await setupFixture({ + const { iff, getFileLocation } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': `@import './b.module.css';`, 'b.module.css': '', @@ -16,12 +16,9 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: await tsserver.sendConfigure({ preferences: { allowRenameOfImportPath: true } }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendRename({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'b.module.css'), - }); + const { info } = await tsserver.sendRename(getFileLocation('a.module.css', 'b.module.css')); - expect(res.body?.info).toMatchObject({ + expect(info).toMatchObject({ canRename: true, kind: 'module', fileToRename: formatPath(iff.paths['b.module.css']), @@ -29,7 +26,7 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); test('from named token importer', async () => { - const { iff, getLoc } = await setupFixture({ + const { iff, getFileLocation } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': `@value b_1 from './b.module.css';`, 'b.module.css': `@value b_1: red;`, @@ -37,12 +34,9 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: await tsserver.sendConfigure({ preferences: { allowRenameOfImportPath: true } }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendRename({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'b.module.css'), - }); + const { info } = await tsserver.sendRename(getFileLocation('a.module.css', 'b.module.css')); - expect(res.body?.info).toMatchObject({ + expect(info).toMatchObject({ canRename: true, kind: 'module', fileToRename: formatPath(iff.paths['b.module.css']), @@ -52,7 +46,7 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: describe('rewrites the import specifier when a CSS module is renamed', () => { test('from `import ... from` in TS', async () => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': buildStylesImport('./a.module.css', { namedExports }), 'a.module.css': '', @@ -64,16 +58,17 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: newFilePath: iff.join('aa.module.css'), }); + const { start, end } = getFileSpan('index.ts', './a.module.css'); expect(res.body).toStrictEqual([ { fileName: formatPath(iff.paths['index.ts']), - textChanges: [{ ...getRange('index.ts', './a.module.css'), newText: './aa.module.css' }], + textChanges: [{ start, end, newText: './aa.module.css' }], }, ]); }); test('from all token importer', async () => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': `@import './b.module.css';`, 'b.module.css': '', @@ -85,16 +80,17 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: newFilePath: iff.join('bb.module.css'), }); + const { start, end } = getFileSpan('a.module.css', './b.module.css'); expect(res.body).toStrictEqual([ { fileName: formatPath(iff.paths['a.module.css']), - textChanges: [{ ...getRange('a.module.css', './b.module.css'), newText: './bb.module.css' }], + textChanges: [{ start, end, newText: './bb.module.css' }], }, ]); }); test('from named token importer', async () => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': `@value b_1 from './b.module.css';`, 'b.module.css': `@value b_1: red;`, @@ -106,10 +102,11 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: newFilePath: iff.join('bb.module.css'), }); + const { start, end } = getFileSpan('a.module.css', './b.module.css'); expect(res.body).toStrictEqual([ { fileName: formatPath(iff.paths['a.module.css']), - textChanges: [{ ...getRange('a.module.css', './b.module.css'), newText: './bb.module.css' }], + textChanges: [{ start, end, newText: './bb.module.css' }], }, ]); }); diff --git a/packages/ts-plugin/e2e-test/rename-symbol.test.ts b/packages/ts-plugin/e2e-test/rename-symbol.test.ts index efab4217..23fb48ab 100644 --- a/packages/ts-plugin/e2e-test/rename-symbol.test.ts +++ b/packages/ts-plugin/e2e-test/rename-symbol.test.ts @@ -2,14 +2,14 @@ import dedent from 'dedent'; import { describe, expect, test } from 'vite-plus/test'; import { buildStylesImport, buildTSConfigJSON } from '../src/test/builder.js'; import { setupFixture } from './test-util/fixture.js'; -import { formatPath, launchTsserver, normalizeSpanGroups } from './test-util/tsserver.js'; +import { launchTsserver } from './test-util/tsserver.js'; const tsserver = launchTsserver(); describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { describe('for a token definition', () => { test('from a TS-side styles.', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -19,21 +19,13 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendRename({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'a_1'), - }); + const { locs } = await tsserver.sendRename(getFileLocation('index.ts', 'a_1')); - expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( - normalizeSpanGroups([ - { file: formatPath(iff.paths['index.ts']), locs: [getRange('index.ts', 'a_1')] }, - { file: formatPath(iff.paths['a.module.css']), locs: [getRange('a.module.css', 'a_1')] }, - ]), - ); + expect(locs).toStrictEqual([getFileSpan('a.module.css', 'a_1'), getFileSpan('index.ts', 'a_1')]); }); test('from a TS-side styles[]', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -43,21 +35,13 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendRename({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'a-1'), - }); + const { locs } = await tsserver.sendRename(getFileLocation('index.ts', 'a-1')); - expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( - normalizeSpanGroups([ - { file: formatPath(iff.paths['index.ts']), locs: [getRange('index.ts', 'a-1')] }, - { file: formatPath(iff.paths['a.module.css']), locs: [getRange('a.module.css', 'a-1')] }, - ]), - ); + expect(locs).toStrictEqual([getFileSpan('a.module.css', 'a-1'), getFileSpan('index.ts', 'a-1')]); }); test('when the token is declared multiple times', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -70,24 +54,17 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendRename({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'a_1'), - }); + const { locs } = await tsserver.sendRename(getFileLocation('index.ts', 'a_1')); - expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( - normalizeSpanGroups([ - { file: formatPath(iff.paths['index.ts']), locs: [getRange('index.ts', 'a_1')] }, - { - file: formatPath(iff.paths['a.module.css']), - locs: [getRange('a.module.css', 'a_1', 0), getRange('a.module.css', 'a_1', 1)], - }, - ]), - ); + expect(locs).toStrictEqual([ + getFileSpan('a.module.css', 'a_1', { index: 0 }), + getFileSpan('a.module.css', 'a_1', { index: 1 }), + getFileSpan('index.ts', 'a_1'), + ]); }); test('from a CSS-side token definition', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -97,23 +74,15 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendRename({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'a_1'), - }); + const { locs } = await tsserver.sendRename(getFileLocation('a.module.css', 'a_1')); - expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( - normalizeSpanGroups([ - { file: formatPath(iff.paths['index.ts']), locs: [getRange('index.ts', 'a_1')] }, - { file: formatPath(iff.paths['a.module.css']), locs: [getRange('a.module.css', 'a_1')] }, - ]), - ); + expect(locs).toStrictEqual([getFileSpan('a.module.css', 'a_1'), getFileSpan('index.ts', 'a_1')]); }); }); describe('for an all token importer', () => { test('from a TS-side styles.', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -124,17 +93,9 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendRename({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'b_1'), - }); + const { locs } = await tsserver.sendRename(getFileLocation('index.ts', 'b_1')); - expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( - normalizeSpanGroups([ - { file: formatPath(iff.paths['index.ts']), locs: [getRange('index.ts', 'b_1')] }, - { file: formatPath(iff.paths['b.module.css']), locs: [getRange('b.module.css', 'b_1')] }, - ]), - ); + expect(locs).toStrictEqual([getFileSpan('b.module.css', 'b_1'), getFileSpan('index.ts', 'b_1')]); }); }); @@ -143,7 +104,7 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: // The ideal behavior would attach `prefixText: 'b_1 as '` to the binding loc in `a.module.css` // so that renaming changes only the alias side. Currently the binding loc is rewritten directly. test('from a TS-side styles.', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -154,23 +115,18 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendRename({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'b_1'), - }); + const { locs } = await tsserver.sendRename(getFileLocation('index.ts', 'b_1')); - expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( - normalizeSpanGroups([ - { file: formatPath(iff.paths['index.ts']), locs: [getRange('index.ts', 'b_1')] }, - { file: formatPath(iff.paths['a.module.css']), locs: [getRange('a.module.css', 'b_1')] }, - { file: formatPath(iff.paths['b.module.css']), locs: [getRange('b.module.css', 'b_1')] }, - ]), - ); + expect(locs).toStrictEqual([ + getFileSpan('a.module.css', 'b_1'), + getFileSpan('b.module.css', 'b_1'), + getFileSpan('index.ts', 'b_1'), + ]); }); // NOTE: Ideally only `b_alias` should be returned, but `b_1` is also returned for implementation simplicity. test('from a TS-side styles.', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -181,98 +137,69 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendRename({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'b_alias'), - }); + const { locs } = await tsserver.sendRename(getFileLocation('index.ts', 'b_alias')); - expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( - normalizeSpanGroups([ - { file: formatPath(iff.paths['index.ts']), locs: [getRange('index.ts', 'b_alias')] }, - { - file: formatPath(iff.paths['a.module.css']), - locs: [getRange('a.module.css', 'b_1'), getRange('a.module.css', 'b_alias')], - }, - { file: formatPath(iff.paths['b.module.css']), locs: [getRange('b.module.css', 'b_1')] }, - ]), - ); + expect(locs).toStrictEqual([ + getFileSpan('a.module.css', 'b_1'), + getFileSpan('a.module.css', 'b_alias'), + getFileSpan('b.module.css', 'b_1'), + getFileSpan('index.ts', 'b_alias'), + ]); }); test('from a CSS-side ', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': `@value b_1 from './b.module.css';`, 'b.module.css': `@value b_1: red;`, }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendRename({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'b_1'), - }); + const { locs } = await tsserver.sendRename(getFileLocation('a.module.css', 'b_1')); - expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( - normalizeSpanGroups([ - { file: formatPath(iff.paths['a.module.css']), locs: [getRange('a.module.css', 'b_1')] }, - { file: formatPath(iff.paths['b.module.css']), locs: [getRange('b.module.css', 'b_1')] }, - ]), - ); + expect(locs).toStrictEqual([getFileSpan('a.module.css', 'b_1'), getFileSpan('b.module.css', 'b_1')]); }); // NOTE: Ideally only `b_1` should be returned, but `b_alias` is also returned for implementation simplicity. test('from a CSS-side with alias', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': `@value b_1 as b_alias from './b.module.css';`, 'b.module.css': `@value b_1: red;`, }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendRename({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'b_1'), - }); + const { locs } = await tsserver.sendRename(getFileLocation('a.module.css', 'b_1')); - expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( - normalizeSpanGroups([ - { - file: formatPath(iff.paths['a.module.css']), - locs: [getRange('a.module.css', 'b_1'), getRange('a.module.css', 'b_alias')], - }, - { file: formatPath(iff.paths['b.module.css']), locs: [getRange('b.module.css', 'b_1')] }, - ]), - ); + expect(locs).toStrictEqual([ + getFileSpan('a.module.css', 'b_1'), + getFileSpan('a.module.css', 'b_alias'), + getFileSpan('b.module.css', 'b_1'), + ]); }); // NOTE: Ideally only `b_alias` should be returned, but `b_1` is also returned for implementation simplicity. test('from a CSS-side ', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': `@value b_1 as b_alias from './b.module.css';`, 'b.module.css': `@value b_1: red;`, }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendRename({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'b_alias'), - }); + const { locs } = await tsserver.sendRename(getFileLocation('a.module.css', 'b_alias')); - expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( - normalizeSpanGroups([ - { - file: formatPath(iff.paths['a.module.css']), - locs: [getRange('a.module.css', 'b_1'), getRange('a.module.css', 'b_alias')], - }, - { file: formatPath(iff.paths['b.module.css']), locs: [getRange('b.module.css', 'b_1')] }, - ]), - ); + expect(locs).toStrictEqual([ + getFileSpan('a.module.css', 'b_1'), + getFileSpan('a.module.css', 'b_alias'), + getFileSpan('b.module.css', 'b_1'), + ]); }); }); describe('for a local token reference', () => { test('from a token definition', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': dedent` @keyframes a_1 { from {} to {} } @@ -282,27 +209,17 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendRename({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'a_1', 0), - }); + const { locs } = await tsserver.sendRename(getFileLocation('a.module.css', 'a_1', 0)); - expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( - normalizeSpanGroups([ - { - file: formatPath(iff.paths['a.module.css']), - locs: [ - getRange('a.module.css', 'a_1', 0), - getRange('a.module.css', 'a_1', 1), - getRange('a.module.css', 'a_1', 2), - ], - }, - ]), - ); + expect(locs).toStrictEqual([ + getFileSpan('a.module.css', 'a_1', { index: 0 }), + getFileSpan('a.module.css', 'a_1', { index: 1 }), + getFileSpan('a.module.css', 'a_1', { index: 2 }), + ]); }); test('from a local token reference', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': dedent` @keyframes a_1 { from {} to {} } @@ -312,46 +229,28 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendRename({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'a_1', 1), - }); + const { locs } = await tsserver.sendRename(getFileLocation('a.module.css', 'a_1', 1)); - expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( - normalizeSpanGroups([ - { - file: formatPath(iff.paths['a.module.css']), - locs: [ - getRange('a.module.css', 'a_1', 0), - getRange('a.module.css', 'a_1', 1), - getRange('a.module.css', 'a_1', 2), - ], - }, - ]), - ); + expect(locs).toStrictEqual([ + getFileSpan('a.module.css', 'a_1', { index: 0 }), + getFileSpan('a.module.css', 'a_1', { index: 1 }), + getFileSpan('a.module.css', 'a_1', { index: 2 }), + ]); }); }); describe('for an external token reference', () => { test('from an external token reference', async () => { - const { iff, getLoc, getRange } = await setupFixture({ + const { iff, getFileLocation, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': `.a_1 { composes: b_1 from './b.module.css'; }`, 'b.module.css': `.b_1 { color: red; }`, }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - const res = await tsserver.sendRename({ - file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'b_1'), - }); + const { locs } = await tsserver.sendRename(getFileLocation('a.module.css', 'b_1')); - expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( - normalizeSpanGroups([ - { file: formatPath(iff.paths['a.module.css']), locs: [getRange('a.module.css', 'b_1')] }, - { file: formatPath(iff.paths['b.module.css']), locs: [getRange('b.module.css', 'b_1')] }, - ]), - ); + expect(locs).toStrictEqual([getFileSpan('a.module.css', 'b_1'), getFileSpan('b.module.css', 'b_1')]); }); }); }); diff --git a/packages/ts-plugin/e2e-test/semantic-diagnostics.test.ts b/packages/ts-plugin/e2e-test/semantic-diagnostics.test.ts index e957eae9..2ff034db 100644 --- a/packages/ts-plugin/e2e-test/semantic-diagnostics.test.ts +++ b/packages/ts-plugin/e2e-test/semantic-diagnostics.test.ts @@ -8,7 +8,7 @@ const tsserver = launchTsserver(); describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { test('reports an unknown property access on a styles binding', async () => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} @@ -20,11 +20,13 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: const res = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['index.ts'] }); + const { start, end } = getFileSpan('index.ts', 'unknown'); expect(res.body).toStrictEqual([ { category: 'error', code: 2339, - ...getRange('index.ts', 'unknown'), + start, + end, // The `text` is not asserted because the message contains the type shape that // varies with `namedExports` and is owned by the TypeScript compiler, not ts-plugin. text: expect.any(String), @@ -50,7 +52,7 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); test('reports a semantic diagnostic on a CSS module file', async () => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': `@import './unresolvable.module.css';`, }); @@ -58,13 +60,15 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: const res = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['a.module.css'] }); + const { start, end } = getFileSpan('a.module.css', './unresolvable.module.css'); expect(res.body).toStrictEqual([ { category: 'error', code: 0, source: 'css-modules-kit', text: "Cannot import module './unresolvable.module.css'", - ...getRange('a.module.css', './unresolvable.module.css'), + start, + end, }, ]); }); diff --git a/packages/ts-plugin/e2e-test/syntactic-diagnostics.test.ts b/packages/ts-plugin/e2e-test/syntactic-diagnostics.test.ts index 521daa01..c0fed3b6 100644 --- a/packages/ts-plugin/e2e-test/syntactic-diagnostics.test.ts +++ b/packages/ts-plugin/e2e-test/syntactic-diagnostics.test.ts @@ -7,7 +7,7 @@ const tsserver = launchTsserver(); describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { test('reports a syntactic diagnostic on a CSS module file', async () => { - const { iff, getRange } = await setupFixture({ + const { iff, getFileSpan } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': `@value;`, }); @@ -15,13 +15,15 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: const res = await tsserver.sendSyntacticDiagnosticsSync({ file: iff.paths['a.module.css'] }); + const { start, end } = getFileSpan('a.module.css', '@value;'); expect(res.body).toStrictEqual([ { category: 'error', code: 0, source: 'css-modules-kit', text: '`@value` is a invalid syntax.', - ...getRange('a.module.css', '@value;'), + start, + end, }, ]); }); diff --git a/packages/ts-plugin/e2e-test/test-util/fixture.ts b/packages/ts-plugin/e2e-test/test-util/fixture.ts index a7b32da9..e6eb93f0 100644 --- a/packages/ts-plugin/e2e-test/test-util/fixture.ts +++ b/packages/ts-plugin/e2e-test/test-util/fixture.ts @@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto'; import { tmpdir } from 'node:os'; import { join } from '@css-modules-kit/core'; import { type CreateIFFResult, defineIFFCreator } from '@mizdra/inline-fixture-files'; +import { type FileSpan, type FileSpanWithContext, formatPath } from './tsserver.js'; const fixtureDir = join(tmpdir(), '@css-modules-kit/ts-plugin', process.env['VITEST_POOL_ID']!); export const createIFF = defineIFFCreator({ @@ -10,6 +11,7 @@ export const createIFF = defineIFFCreator({ }); export type Loc = { line: number; offset: number }; +export type FileLocation = { file: string; line: number; offset: number }; function findAllMatches(content: string, search: string): number[] { if (search.length === 0) throw new Error('Empty search string is not allowed.'); @@ -34,36 +36,53 @@ function offsetToLoc(content: string, offset: number): Loc { type Files = Record; +export interface GetFileSpanOptions { + /** 0-based index of the match when `search` matches multiple times. */ + index?: number; + /** + * A substring enclosing the `search` match. When given, the returned span also carries + * `contextStart` / `contextEnd` pointing to the occurrence of `context` that encloses the match. + */ + context?: string; +} + export interface SetupFixtureResult { iff: CreateIFFResult; /** - * Get the (1-based) line/offset of the first character of `search` in `file`. + * Get the absolute path of `file` and the (1-based) line/offset of the first character of `search` in it. + * The result can be passed directly as tsserver request arguments. * * - If `search` matches exactly once, returns that position. * - If `search` matches multiple times, an `index` (0-based) must be passed. * - Throws if `search` does not match, or `index` is out of range. */ - getLoc: (file: string, search: string, index?: number) => Loc; + getFileLocation: (file: string, search: string, index?: number) => FileLocation; /** - * Get the (1-based) start/end range of `search` in `file`. + * Get the absolute (path-normalized) path of `file` and the (1-based) start/end range of `search` in it. + * The result can be compared directly with spans returned by the tsserver client. * - * - `start` is identical to `getLoc(file, search, index)`. + * - `start` is identical to the position returned by `getFileLocation`. * - `end` points to the position immediately AFTER the last character of `search` * (exclusive end, matching tsserver's convention). - * - Same matching/error semantics as `getLoc`. + * - Same matching/error semantics as `getFileLocation`. */ - getRange: (file: string, search: string, index?: number) => { start: Loc; end: Loc }; + getFileSpan: (file: string, search: string, options?: GetFileSpanOptions) => FileSpanWithContext; } export async function setupFixture(files: T): Promise> { // oxlint-disable-next-line typescript/no-explicit-any const iff = (await createIFF(files)) as any; - function getLoc(file: string, search: string, index?: number): Loc { + function getFileContent(file: string): string { const content = files[file]; if (content === undefined) { throw new Error(`File "${file}" was not registered in the fixture.`); } + return content; + } + + function findOffset(file: string, search: string, index?: number): number { + const content = getFileContent(file); const matches = findAllMatches(content, search); if (matches.length === 0) { throw new Error(`Substring ${JSON.stringify(search)} not found in "${file}".`); @@ -80,24 +99,41 @@ export async function setupFixture(files: T): Promise offset <= start && end <= offset + context.length, + ); + if (enclosing === undefined) { + throw new Error(`No occurrence of context ${JSON.stringify(context)} encloses the match in "${file}".`); } - const lastLine = lines[lines.length - 1] ?? ''; + return enclosing; + } + + function getFileLocation(file: string, search: string, index?: number): FileLocation { + return { file: iff.paths[file], ...offsetToLoc(getFileContent(file), findOffset(file, search, index)) }; + } + + function getFileSpan(file: string, search: string, options?: GetFileSpanOptions): FileSpanWithContext { + const content = getFileContent(file); + const start = findOffset(file, search, options?.index); + const end = start + search.length; + const span: FileSpan = { + file: formatPath(iff.paths[file]), + start: offsetToLoc(content, start), + end: offsetToLoc(content, end), + }; + if (options?.context === undefined) return span; + const contextStart = findEnclosingContextOffset(file, options.context, start, end); return { - start, - end: { - line: start.line + lines.length - 1, - offset: lastLine.length + 1, - }, + ...span, + contextStart: offsetToLoc(content, contextStart), + contextEnd: offsetToLoc(content, contextStart + options.context.length), }; } - return { iff, getLoc, getRange }; + return { iff, getFileLocation, getFileSpan }; } diff --git a/packages/ts-plugin/e2e-test/test-util/tsserver.ts b/packages/ts-plugin/e2e-test/test-util/tsserver.ts index 01a37fce..809022fa 100644 --- a/packages/ts-plugin/e2e-test/test-util/tsserver.ts +++ b/packages/ts-plugin/e2e-test/test-util/tsserver.ts @@ -2,14 +2,57 @@ import serverHarness from '@typescript/server-harness'; import type { server } from 'typescript'; import ts from 'typescript'; +export type FileSpan = { + file: string; + start: server.protocol.Location; + end: server.protocol.Location; +}; + +export type FileSpanWithContext = FileSpan & { + contextStart?: server.protocol.Location; + contextEnd?: server.protocol.Location; +}; + +export type RenameLocation = FileSpan & { + prefixText?: string; + suffixText?: string; +}; + +export type RenameResult = { + info: server.protocol.RenameInfo; + locs: RenameLocation[]; +}; + +export type CompletionEntry = { + name: string; + sortText: string; + source?: string; + insertText?: string; +}; + +export type CompletionDetails = { + codeActions?: { changes: server.protocol.FileCodeEdits[] }[]; +}; + +export type CodeFixAction = { + fixName: string; + changes: server.protocol.FileCodeEdits[]; +}; + +/** + * A thin client for tsserver. + * + * Methods returning language feature results (definitions, references, rename, completion, code fixes) + * return normalized values: only the fields relevant to tests are kept, paths are normalized with + * `formatPath`, and the results are sorted so that they can be compared with `toStrictEqual`. + * Spans are sorted by file path, then by position. Duplicates are not removed. + */ interface Tsserver { sendUpdateOpen(args: server.protocol.UpdateOpenRequest['arguments']): Promise; sendConfigure(args: server.protocol.ConfigureRequest['arguments']): Promise; - sendDefinitionAndBoundSpan( - args: server.protocol.FileLocationRequestArgs, - ): Promise; - sendReferences(args: server.protocol.ReferencesRequest['arguments']): Promise; - sendRename(args: server.protocol.RenameRequest['arguments']): Promise; + sendDefinitionAndBoundSpan(args: server.protocol.FileLocationRequestArgs): Promise; + sendReferences(args: server.protocol.ReferencesRequest['arguments']): Promise; + sendRename(args: server.protocol.RenameRequest['arguments']): Promise; sendSemanticDiagnosticsSync( args: server.protocol.SemanticDiagnosticsSyncRequest['arguments'], ): Promise; @@ -25,13 +68,9 @@ interface Tsserver { sendGetEditsForRefactor( args: server.protocol.GetEditsForRefactorRequest['arguments'], ): Promise; - sendCompletionInfo( - args: server.protocol.CompletionsRequest['arguments'], - ): Promise; - sendCompletionDetails( - args: server.protocol.CompletionDetailsRequest['arguments'], - ): Promise; - sendGetCodeFixes(args: server.protocol.CodeFixRequest['arguments']): Promise; + sendCompletionInfo(args: server.protocol.CompletionsRequest['arguments']): Promise; + sendCompletionDetails(args: server.protocol.CompletionDetailsRequest['arguments']): Promise; + sendGetCodeFixes(args: server.protocol.CodeFixRequest['arguments']): Promise; } export function launchTsserver(): Tsserver { @@ -67,10 +106,25 @@ export function launchTsserver(): Tsserver { return { sendUpdateOpen: async (args) => sendRequest(ts.server.protocol.CommandTypes.UpdateOpen, args), sendConfigure: async (args) => sendRequest(ts.server.protocol.CommandTypes.Configure, args), - sendDefinitionAndBoundSpan: async (args) => - sendRequest(ts.server.protocol.CommandTypes.DefinitionAndBoundSpan, args), - sendReferences: async (args) => sendRequest(ts.server.protocol.CommandTypes.References, args), - sendRename: async (args) => sendRequest(ts.server.protocol.CommandTypes.Rename, args), + sendDefinitionAndBoundSpan: async (args) => { + const res: server.protocol.DefinitionInfoAndBoundSpanResponse = await sendRequest( + ts.server.protocol.CommandTypes.DefinitionAndBoundSpan, + args, + ); + return normalizeDefinitions(res.body?.definitions ?? []); + }, + sendReferences: async (args) => { + const res: server.protocol.ReferencesResponse = await sendRequest( + ts.server.protocol.CommandTypes.References, + args, + ); + return normalizeFileSpans(res.body?.refs ?? []); + }, + sendRename: async (args) => { + const res: server.protocol.RenameResponse = await sendRequest(ts.server.protocol.CommandTypes.Rename, args); + if (res.body === undefined) throw new Error('Expected rename response to have a body'); + return { info: res.body.info, locs: normalizeRenameLocations(res.body.locs) }; + }, sendSemanticDiagnosticsSync: async (args) => sendRequest(ts.server.protocol.CommandTypes.SemanticDiagnosticsSync, args), sendSyntacticDiagnosticsSync: async (args) => @@ -79,9 +133,27 @@ export function launchTsserver(): Tsserver { sendGetApplicableRefactors: async (args) => sendRequest(ts.server.protocol.CommandTypes.GetApplicableRefactors, args), sendGetEditsForRefactor: async (args) => sendRequest(ts.server.protocol.CommandTypes.GetEditsForRefactor, args), - sendCompletionInfo: async (args) => sendRequest(ts.server.protocol.CommandTypes.CompletionInfo, args), - sendCompletionDetails: async (args) => sendRequest(ts.server.protocol.CommandTypes.CompletionDetails, args), - sendGetCodeFixes: async (args) => sendRequest(ts.server.protocol.CommandTypes.GetCodeFixes, args), + sendCompletionInfo: async (args) => { + const res: server.protocol.CompletionInfoResponse = await sendRequest( + ts.server.protocol.CommandTypes.CompletionInfo, + args, + ); + return normalizeCompletionEntries(res.body?.entries ?? []); + }, + sendCompletionDetails: async (args) => { + const res: server.protocol.CompletionDetailsResponse = await sendRequest( + ts.server.protocol.CommandTypes.CompletionDetails, + args, + ); + return normalizeCompletionDetails(res.body ?? []); + }, + sendGetCodeFixes: async (args) => { + const res: server.protocol.CodeFixResponse = await sendRequest( + ts.server.protocol.CommandTypes.GetCodeFixes, + args, + ); + return normalizeCodeFixActions(res.body ?? []); + }, }; } @@ -90,156 +162,66 @@ export function formatPath(path: string) { return path.replaceAll('\\', '/'); } -type SimplifiedDefinitionInfo = { - file: string; - start: ts.server.protocol.Location; - end: ts.server.protocol.Location; - contextStart?: ts.server.protocol.Location; - contextEnd?: ts.server.protocol.Location; -}; - -export function normalizeDefinitions(definitions: readonly SimplifiedDefinitionInfo[]): SimplifiedDefinitionInfo[] { - return definitions - .map((definition) => { - return { - file: formatPath(definition.file), - start: definition.start, - end: definition.end, - ...('contextStart' in definition ? { contextStart: definition.contextStart } : {}), - ...('contextEnd' in definition ? { contextEnd: definition.contextEnd } : {}), - }; - }) - .toSorted((a, b) => { - return a.file.localeCompare(b.file) || a.start.line - b.start.line || a.start.offset - b.start.offset; - }); +function compareFileSpans(a: FileSpan, b: FileSpan): number { + return a.file.localeCompare(b.file) || a.start.line - b.start.line || a.start.offset - b.start.offset; } -type SimplifiedSpanGroup = { - file: string; - locs: ts.server.protocol.TextSpan[]; -}; - -export function normalizeSpanGroups(spanGroups: readonly SimplifiedSpanGroup[]): SimplifiedSpanGroup[] { - const sortedLocs = spanGroups - .map((loc) => { - return { - file: formatPath(loc.file), - locs: loc.locs.map((loc) => ({ - start: loc.start, - end: loc.end, - ...('prefixText' in loc ? { prefixText: loc.prefixText } : {}), - ...('suffixText' in loc ? { suffixText: loc.suffixText } : {}), - })), - }; - }) - .toSorted((a, b) => { - return a.file.localeCompare(b.file); - }); - for (const loc of sortedLocs) { - loc.locs.sort((a, b) => { - return a.start.line - b.start.line || a.start.offset - b.start.offset; - }); - } - return sortedLocs; +function normalizeDefinitions(definitions: readonly server.protocol.DefinitionInfo[]): FileSpanWithContext[] { + return definitions + .map((definition) => ({ + file: formatPath(definition.file), + start: definition.start, + end: definition.end, + ...('contextStart' in definition ? { contextStart: definition.contextStart } : {}), + ...('contextEnd' in definition ? { contextEnd: definition.contextEnd } : {}), + })) + .toSorted(compareFileSpans); } -type SimplifiedReferencesResponseItem = { - file: string; - start: ts.server.protocol.Location; - end: ts.server.protocol.Location; -}; - -export function normalizeRefItems(refs: readonly SimplifiedReferencesResponseItem[]) { - return refs - .map((ref) => { - return { - file: formatPath(ref.file), - start: ref.start, - end: ref.end, - }; - }) - .toSorted((a, b) => { - return a.file.localeCompare(b.file) || a.start.line - b.start.line || a.start.offset - b.start.offset; - }); +function normalizeFileSpans(spans: readonly server.protocol.FileSpan[]): FileSpan[] { + return spans + .map((span) => ({ file: formatPath(span.file), start: span.start, end: span.end })) + .toSorted(compareFileSpans); } -export function mergeSpanGroups(fileSpans: ts.server.protocol.FileSpan[]): SimplifiedSpanGroup[] { - const spanGroups: SimplifiedSpanGroup[] = []; - for (const fileSpan of fileSpans) { - const existingGroup = spanGroups.find((group) => group.file === fileSpan.file); - if (existingGroup) { - existingGroup.locs.push({ start: fileSpan.start, end: fileSpan.end }); - } else { - spanGroups.push({ - file: fileSpan.file, - locs: [{ start: fileSpan.start, end: fileSpan.end }], - }); - } - } - return spanGroups; +function normalizeRenameLocations(spanGroups: readonly server.protocol.SpanGroup[]): RenameLocation[] { + return spanGroups + .flatMap((group) => + group.locs.map((loc) => ({ + file: formatPath(group.file), + start: loc.start, + end: loc.end, + ...('prefixText' in loc ? { prefixText: loc.prefixText } : {}), + ...('suffixText' in loc ? { suffixText: loc.suffixText } : {}), + })), + ) + .toSorted(compareFileSpans); } -type SimplifiedCompletionEntry = { - name: string; - sortText: string; - source?: string; - insertText?: string; -}; - -export function normalizeCompletionEntry(entries: readonly SimplifiedCompletionEntry[]): SimplifiedCompletionEntry[] { +function normalizeCompletionEntries(entries: readonly server.protocol.CompletionEntry[]): CompletionEntry[] { return entries - .map((entry) => { - return { - name: entry.name, - sortText: entry.sortText, - ...('source' in entry ? { source: entry.source } : {}), - ...('insertText' in entry ? { insertText: entry.insertText } : {}), - }; - }) + .map((entry) => ({ + name: entry.name, + sortText: entry.sortText, + ...('source' in entry ? { source: entry.source } : {}), + ...('insertText' in entry ? { insertText: entry.insertText } : {}), + })) .toSorted( (a, b) => - a.sortText?.localeCompare(b.sortText ?? '') || - a.source?.localeCompare(b.source ?? '') || + a.sortText.localeCompare(b.sortText) || + (a.source ?? '').localeCompare(b.source ?? '') || a.name.localeCompare(b.name), ); } -type SimplifiedCodeAction = { - changes: ts.server.protocol.FileCodeEdits[]; -}; - -type SimplifiedCompletionDetails = { - codeActions?: SimplifiedCodeAction[]; -}; - -export function normalizeCompletionDetails( - entries: readonly SimplifiedCompletionDetails[], -): SimplifiedCompletionDetails[] { - return entries.map((entry) => { - return entry.codeActions - ? { - codeActions: entry.codeActions.map((action) => { - return { changes: action.changes }; - }), - } - : {}; - }); +function normalizeCompletionDetails(entries: readonly server.protocol.CompletionEntryDetails[]): CompletionDetails[] { + return entries.map((entry) => + entry.codeActions ? { codeActions: entry.codeActions.map((action) => ({ changes: action.changes })) } : {}, + ); } -type SimplifiedCodeFixAction = { - fixName: string; - changes: ts.server.protocol.FileCodeEdits[]; -}; - -export function normalizeCodeFixActions(actions: readonly SimplifiedCodeFixAction[]): SimplifiedCodeFixAction[] { +function normalizeCodeFixActions(actions: readonly server.protocol.CodeFixAction[]): CodeFixAction[] { return actions - .map((action) => { - return { - fixName: action.fixName, - changes: action.changes, - }; - }) - .toSorted((a, b) => { - return a.fixName.localeCompare(b.fixName); - }); + .map((action) => ({ fixName: action.fixName, changes: action.changes })) + .toSorted((a, b) => a.fixName.localeCompare(b.fixName)); }