diff --git a/packages/language-tools/language-server/package.json b/packages/language-tools/language-server/package.json index a61918374118..5dea95a67697 100644 --- a/packages/language-tools/language-server/package.json +++ b/packages/language-tools/language-server/package.json @@ -26,9 +26,8 @@ "test:match": "pnpm run test --match" }, "dependencies": { - "@astrojs/compiler": "^2.13.1", + "@astrojs/astro2tsx": "link:/home/erika/worktrees/compiler-rs/feat-biome-tsx/crates/astro2tsx", "@astrojs/yaml2ts": "^0.2.4", - "@jridgewell/sourcemap-codec": "^1.5.5", "@volar/kit": "~2.4.28", "@volar/language-core": "~2.4.28", "@volar/language-server": "~2.4.28", diff --git a/packages/language-tools/language-server/src/core/astro2tsx.ts b/packages/language-tools/language-server/src/core/astro2tsx.ts index 78b122c6ea8f..eebb5e3ca7e1 100644 --- a/packages/language-tools/language-server/src/core/astro2tsx.ts +++ b/packages/language-tools/language-server/src/core/astro2tsx.ts @@ -1,11 +1,10 @@ -import { convertToTSX } from '@astrojs/compiler/sync'; -import type { - ConvertToTSXOptions, - TSXExtractedScript, - TSXExtractedStyle, - TSXResult, -} from '@astrojs/compiler/types'; -import { decode } from '@jridgewell/sourcemap-codec'; +import { + AstroFrontmatterStatus, + type ConvertToTsxResult, + type ExtractedScript, + type ExtractedStyle, + convertToTsx, +} from '@astrojs/astro2tsx'; import type { CodeMapping, VirtualCode } from '@volar/language-core'; import { Range } from '@volar/language-server'; import { TextDocument } from 'vscode-html-languageservice'; @@ -14,71 +13,59 @@ import { patchTSX } from './utils.js'; export interface LSPTSXRanges { frontmatter: Range; body: Range; - scripts: TSXExtractedScript[]; - styles: TSXExtractedStyle[]; + scripts: ExtractedScript[]; + styles: ExtractedStyle[]; } -export function safeConvertToTSX(content: string, options: ConvertToTSXOptions) { +export function safeConvertToTSX( + content: string, + options: { filename?: string }, +): ConvertToTsxResult { + const fileName = options.filename ?? ''; try { - const tsx = convertToTSX(content, { - filename: options.filename, - includeScripts: false, - includeStyles: false, - }); - return tsx; + return convertToTsx(content, { filename: fileName, sourcemap: false }); } catch (e) { console.error( - `There was an error transforming ${options.filename} to TSX. An empty file will be returned instead. Please create an issue: https://github.com/withastro/astro/issues\nError: ${e}.`, + `There was an error transforming ${fileName} to TSX. An empty file will be returned instead. Please create an issue: https://github.com/withastro/astro/issues\nError: ${e}.`, ); return { code: '', - map: { - file: options.filename ?? '', - sources: [], - sourcesContent: [], - names: [], - mappings: '', - version: 0, - }, + generatedOffsets: new Uint32Array(), + sourceOffsets: new Uint32Array(), + lengths: new Uint32Array(), + frontmatter: { start: 0, end: 0 }, + body: { start: 0, end: 0 }, + frontmatterStatus: AstroFrontmatterStatus.DoesntExist, + frontmatterSource: { start: 0, end: 0 }, + scripts: [], + styles: [], diagnostics: [ { - code: 1000, - location: { file: options.filename!, line: 1, column: 1, length: content.length }, + message: `The Astro compiler encountered an unknown error while transforming this file to TSX. Please create an issue with your code and the error shown in the server's logs: https://github.com/withastro/astro/issues`, severity: 1, - text: `The Astro compiler encountered an unknown error while transform this file to TSX. Please create an issue with your code and the error shown in the server's logs: https://github.com/withastro/astro/issues`, + position: { start: 0, end: content.length }, }, ], - metaRanges: { - frontmatter: { - start: 0, - end: 0, - }, - body: { - start: 0, - end: 0, - }, - scripts: [], - styles: [], - }, - } satisfies TSXResult; + hasParseErrors: true, + } satisfies ConvertToTsxResult; } } -export function getTSXRangesAsLSPRanges(tsx: TSXResult): LSPTSXRanges { +export function getTSXRangesAsLSPRanges(tsx: ConvertToTsxResult): LSPTSXRanges { const textDocument = TextDocument.create('', 'typescriptreact', 0, tsx.code); return { frontmatter: Range.create( - textDocument.positionAt(tsx.metaRanges.frontmatter.start), - textDocument.positionAt(tsx.metaRanges.frontmatter.end), + textDocument.positionAt(tsx.frontmatter.start), + textDocument.positionAt(tsx.frontmatter.end), ), body: Range.create( - textDocument.positionAt(tsx.metaRanges.body.start), - textDocument.positionAt(tsx.metaRanges.body.end), + textDocument.positionAt(tsx.body.start), + textDocument.positionAt(tsx.body.end), ), - scripts: tsx.metaRanges.scripts ?? [], - styles: tsx.metaRanges.styles ?? [], + scripts: tsx.scripts, + styles: tsx.styles, }; } @@ -86,89 +73,42 @@ export function astro2tsx(input: string, fileName: string) { const tsx = safeConvertToTSX(input, { filename: fileName }); return { - virtualCode: getVirtualCodeTSX(input, tsx, fileName), + virtualCode: getVirtualCodeTSX(tsx, fileName), diagnostics: tsx.diagnostics, ranges: getTSXRangesAsLSPRanges(tsx), + frontmatterStatus: tsx.frontmatterStatus, + frontmatterSource: tsx.frontmatterSource, }; } -function getVirtualCodeTSX(input: string, tsx: TSXResult, fileName: string): VirtualCode { - tsx.code = patchTSX(tsx.code, fileName); - const v3Mappings = decode(tsx.map.mappings); - const sourcedDoc = TextDocument.create('', 'astro', 0, input); - const genDoc = TextDocument.create('', 'typescriptreact', 0, tsx.code); - const mappings: CodeMapping[] = []; - - let current: - | { - genOffset: number; - sourceOffset: number; - } - | undefined; - - for (let genLine = 0; genLine < v3Mappings.length; genLine++) { - for (const segment of v3Mappings[genLine]) { - const genCharacter = segment[0]; - const genOffset = genDoc.offsetAt({ line: genLine, character: genCharacter }); - if (current) { - let length = genOffset - current.genOffset; - const sourceText = input.substring(current.sourceOffset, current.sourceOffset + length); - const genText = tsx.code.substring(current.genOffset, current.genOffset + length); - if (sourceText !== genText) { - length = 0; - for (let i = 0; i < genOffset - current.genOffset; i++) { - if (sourceText[i] === genText[i]) { - length = i + 1; - } else { - break; - } - } - } - if (length > 0) { - const lastMapping = mappings.length ? mappings[mappings.length - 1] : undefined; - if ( - lastMapping && - lastMapping.generatedOffsets[0] + lastMapping.lengths[0] === current.genOffset && - lastMapping.sourceOffsets[0] + lastMapping.lengths[0] === current.sourceOffset - ) { - lastMapping.lengths[0] += length; - } else { - mappings.push({ - sourceOffsets: [current.sourceOffset], - generatedOffsets: [current.genOffset], - lengths: [length], - data: { - verification: true, - completion: true, - semantic: true, - navigation: true, - structure: true, - format: false, - }, - }); - } - } - current = undefined; - } - if (segment[2] !== undefined && segment[3] !== undefined) { - const sourceOffset = sourcedDoc.offsetAt({ line: segment[2], character: segment[3] }); - current = { - genOffset, - sourceOffset, - }; - } - } - } +function getVirtualCodeTSX(tsx: ConvertToTsxResult, fileName: string): VirtualCode { + // Only the trailing scaffolding is rewritten, so mapped offsets keep their meaning. + const code = patchTSX(tsx.code, fileName); + const mappings: CodeMapping[] = [ + { + sourceOffsets: Array.from(tsx.sourceOffsets), + generatedOffsets: Array.from(tsx.generatedOffsets), + lengths: Array.from(tsx.lengths), + data: { + verification: true, + completion: true, + semantic: true, + navigation: true, + structure: true, + format: false, + }, + }, + ]; return { id: 'tsx', languageId: 'typescriptreact', snapshot: { - getText: (start, end) => tsx.code.substring(start, end), - getLength: () => tsx.code.length, + getText: (start, end) => code.substring(start, end), + getLength: () => code.length, getChangeRange: () => undefined, }, - mappings: mappings, + mappings, embeddedCodes: [], }; } diff --git a/packages/language-tools/language-server/src/core/index.ts b/packages/language-tools/language-server/src/core/index.ts index 6bffb144cded..87c7bfc72b47 100644 --- a/packages/language-tools/language-server/src/core/index.ts +++ b/packages/language-tools/language-server/src/core/index.ts @@ -1,5 +1,4 @@ import * as path from 'node:path'; -import type { DiagnosticMessage, DiagnosticSeverity } from '@astrojs/compiler/types'; import { type CodeMapping, forEachEmbeddedCode, @@ -12,9 +11,10 @@ import type { HTMLDocument } from 'vscode-html-languageservice'; import type { URI } from 'vscode-uri'; import type { PackageInfo } from '../importPackage.js'; import { getLanguageServerTypesDir } from '../utils.js'; +import type { AstroDiagnostic } from '@astrojs/astro2tsx'; import { astro2tsx } from './astro2tsx.js'; import type { AstroMetadata } from './parseAstro.js'; -import { getAstroMetadata } from './parseAstro.js'; +import { getFrontmatterStatus } from './parseAstro.js'; import { extractStylesheets } from './parseCSS.js'; import { parseHTML } from './parseHTML.js'; import { extractScriptTags } from './parseJS.js'; @@ -175,7 +175,10 @@ export class AstroVirtualCode implements VirtualCode { mappings!: CodeMapping[]; embeddedCodes!: VirtualCode[]; astroMeta!: AstroMetadata; - compilerDiagnostics!: DiagnosticMessage[]; + compilerDiagnostics!: AstroDiagnostic[]; + /// Conversion recovers from most syntax errors, so a file with diagnostics + /// still type-checks; only an outright failure leaves nothing to check. + hasUsableTSX!: boolean; htmlDocument!: HTMLDocument; codegenStacks = []; public fileName: string; @@ -200,17 +203,13 @@ export class AstroVirtualCode implements VirtualCode { }, ]; - const tsx = astro2tsx(this.snapshot.getText(0, this.snapshot.getLength()), this.fileName); - const astroMetadata = getAstroMetadata( - this.fileName, - this.snapshot.getText(0, this.snapshot.getLength()), - ); + const input = this.snapshot.getText(0, this.snapshot.getLength()); + const tsx = astro2tsx(input, this.fileName); + const frontmatter = getFrontmatterStatus(tsx.frontmatterStatus, tsx.frontmatterSource, input); const { htmlDocument, virtualCode: htmlVirtualCode } = parseHTML( this.snapshot, - astroMetadata.frontmatter.status === 'closed' - ? astroMetadata.frontmatter.position.end.offset - : 0, + frontmatter.status === 'closed' ? frontmatter.position.end.offset : 0, ); this.htmlDocument = htmlDocument; @@ -219,16 +218,9 @@ export class AstroVirtualCode implements VirtualCode { ...extractScriptTags(tsx.ranges.scripts), ]; - this.astroMeta = { ...astroMetadata, tsxRanges: tsx.ranges }; - this.compilerDiagnostics = [...tsx.diagnostics, ...astroMetadata.diagnostics]; + this.astroMeta = { frontmatter, tsxRanges: tsx.ranges }; + this.compilerDiagnostics = tsx.diagnostics; + this.hasUsableTSX = tsx.virtualCode.snapshot.getLength() > 0; this.embeddedCodes = [htmlVirtualCode, tsx.virtualCode]; } - - get hasCompilationErrors(): boolean { - return ( - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison - this.compilerDiagnostics.filter((diag) => diag.severity === (1 satisfies DiagnosticSeverity)) - .length > 0 - ); - } } diff --git a/packages/language-tools/language-server/src/core/parseAstro.ts b/packages/language-tools/language-server/src/core/parseAstro.ts index d69bdba11b16..5d345c63baae 100644 --- a/packages/language-tools/language-server/src/core/parseAstro.ts +++ b/packages/language-tools/language-server/src/core/parseAstro.ts @@ -1,54 +1,16 @@ -import { parse } from '@astrojs/compiler/sync'; -import type { ParseOptions, ParseResult, Point } from '@astrojs/compiler/types'; -import type { LSPTSXRanges } from './astro2tsx.js'; +import { astro2tsx, type LSPTSXRanges } from './astro2tsx.js'; -export type AstroMetadata = ParseResult & { +export type AstroMetadata = { frontmatter: FrontmatterStatus; tsxRanges: LSPTSXRanges; }; -export function getAstroMetadata( - fileName: string, - input: string, - options: ParseOptions = { position: true }, -): Omit { - const parseResult = safeParseAst(fileName, input, options); - - return { - ...parseResult, - frontmatter: getFrontmatterStatus(parseResult.ast, input), - }; -} - -function safeParseAst(fileName: string, input: string, parseOptions: ParseOptions): ParseResult { - try { - const parseResult = parse(input, parseOptions); - return parseResult; - } catch (e) { - console.error( - `There was an error parsing ${fileName}'s AST. An empty AST will be returned instead to avoid breaking the server. Please create an issue: https://github.com/withastro/astro/issues\nError: ${e}.`, - ); - - return { - ast: { - type: 'root', - children: [], - }, - diagnostics: [ - { - code: 1000, - location: { - file: fileName, - line: 1, - column: 1, - length: input.length, - }, - severity: 1, - text: `The Astro compiler encountered an unknown error while parsing this file's AST. Please create an issue with your code and the error shown in the server's logs: https://github.com/withastro/astro/issues`, - }, - ], - }; - } +export interface Point { + /** 1-based. */ + line: number; + /** 1-based. */ + column: number; + offset: number; } interface FrontmatterOpen { @@ -74,51 +36,46 @@ interface FrontmatterNull { export type FrontmatterStatus = FrontmatterOpen | FrontmatterClosed | FrontmatterNull; -function getFrontmatterStatus(ast: ParseResult['ast'], text: string): FrontmatterStatus { - if (!ast.children || (ast.children && ast.children.length === 0)) { - return { - status: 'doesnt-exist', - position: undefined, - }; +function pointAt(input: string, offset: number): Point { + let line = 1; + let lineStart = 0; + for (let index = 0; index < offset && index < input.length; index++) { + if (input[index] === '\n') { + line++; + lineStart = index + 1; + } } + return { line, column: offset - lineStart + 1, offset }; +} - if (ast.children[0].type === 'frontmatter') { - const frontmatter = ast.children[0]; - if (frontmatter.position) { - if (frontmatter.position.end) { - // HACK: The compiler as of 1.5.5 always return an ending position, even if there's only a frontmatter opening - // This hack checks if the frontmatter's ending is the end of the file, and if so, checks if there's a `---`. - // If there's not, it means the compiler returned the EOF with an opened frontmatter - if (frontmatter.position.end.offset === text.length && !text.endsWith('---')) { - return { - status: 'open', - position: { - start: frontmatter.position.start, - end: undefined, - }, - }; - } - - return { - status: 'closed', - position: { - start: frontmatter.position.start, - end: frontmatter.position.end, - }, - }; - } +export function getFrontmatterStatus( + status: 'closed' | 'open' | 'doesnt-exist', + source: { start: number; end: number }, + input: string, +): FrontmatterStatus { + switch (status) { + case 'closed': + return { + status: 'closed', + position: { start: pointAt(input, source.start), end: pointAt(input, source.end) }, + }; + case 'open': return { status: 'open', - position: { - start: frontmatter.position.start, - end: undefined, - }, + position: { start: pointAt(input, source.start), end: undefined }, }; - } + case 'doesnt-exist': + return { status: 'doesnt-exist', position: undefined }; } +} + +export function getAstroMetadata( + fileName: string, + input: string, +): Omit { + const tsx = astro2tsx(input, fileName); return { - status: 'doesnt-exist', - position: undefined, + frontmatter: getFrontmatterStatus(tsx.frontmatterStatus, tsx.frontmatterSource, input), }; } diff --git a/packages/language-tools/language-server/src/core/parseCSS.ts b/packages/language-tools/language-server/src/core/parseCSS.ts index 82b0073e37e6..fc426aa21a0f 100644 --- a/packages/language-tools/language-server/src/core/parseCSS.ts +++ b/packages/language-tools/language-server/src/core/parseCSS.ts @@ -1,4 +1,4 @@ -import type { TSXExtractedStyle } from '@astrojs/compiler/types'; +import type { ExtractedStyle } from '@astrojs/astro2tsx'; import type { CodeInformation, VirtualCode } from '@volar/language-core'; import type { Segment } from 'muggle-string'; import { toString } from 'muggle-string'; @@ -11,11 +11,11 @@ function isSupportedLanguage(lang: string): lang is SupportedLanguages { return SUPPORTED_LANGUAGES.includes(lang as SupportedLanguages); } -export function extractStylesheets(styles: TSXExtractedStyle[]): VirtualCode[] { +export function extractStylesheets(styles: ExtractedStyle[]): VirtualCode[] { return mergeCSSContextsByLanguage(styles); } -function mergeCSSContextsByLanguage(inlineStyles: TSXExtractedStyle[]): VirtualCode[] { +function mergeCSSContextsByLanguage(inlineStyles: ExtractedStyle[]): VirtualCode[] { const codes: Record[]> = { css: [], scss: [], diff --git a/packages/language-tools/language-server/src/core/parseJS.ts b/packages/language-tools/language-server/src/core/parseJS.ts index 0a3f4c2a581b..b707ec57dc4a 100644 --- a/packages/language-tools/language-server/src/core/parseJS.ts +++ b/packages/language-tools/language-server/src/core/parseJS.ts @@ -1,10 +1,10 @@ -import type { TSXExtractedScript } from '@astrojs/compiler/types'; +import type { ExtractedScript } from '@astrojs/astro2tsx'; import type { CodeInformation, VirtualCode } from '@volar/language-core'; import type { Segment } from 'muggle-string'; import { toString } from 'muggle-string'; import { buildMappings } from '../buildMappings'; -export function extractScriptTags(scripts: TSXExtractedScript[]): VirtualCode[] { +export function extractScriptTags(scripts: ExtractedScript[]): VirtualCode[] { const embeddedJSCodes: VirtualCode[] = []; const moduleScripts = scripts @@ -35,7 +35,7 @@ export function extractScriptTags(scripts: TSXExtractedScript[]): VirtualCode[] return embeddedJSCodes; } -function moduleScriptToVirtualCode(script: TSXExtractedScript, index: number): VirtualCode { +function moduleScriptToVirtualCode(script: ExtractedScript, index: number): VirtualCode { let extension = 'mts'; let languageId = 'typescript'; if (script.type === 'module') { @@ -70,7 +70,7 @@ function moduleScriptToVirtualCode(script: TSXExtractedScript, index: number): V }; } -function jsonScriptToVirtualCode(script: TSXExtractedScript, index: number): VirtualCode { +function jsonScriptToVirtualCode(script: ExtractedScript, index: number): VirtualCode { return { id: `${index}.json`, languageId: 'json', @@ -102,7 +102,7 @@ function jsonScriptToVirtualCode(script: TSXExtractedScript, index: number): Vir /** * Merge all the inline and non-hoisted scripts into a single `.mjs` file */ -function mergeJSContexts(inlineScripts: TSXExtractedScript[]): VirtualCode | undefined { +function mergeJSContexts(inlineScripts: ExtractedScript[]): VirtualCode | undefined { if (inlineScripts.length === 0) { return undefined; } diff --git a/packages/language-tools/language-server/src/plugins/astro.ts b/packages/language-tools/language-server/src/plugins/astro.ts index 2346a092ebbe..59b3570c329b 100644 --- a/packages/language-tools/language-server/src/plugins/astro.ts +++ b/packages/language-tools/language-server/src/plugins/astro.ts @@ -1,19 +1,20 @@ -import type { DiagnosticMessage } from '@astrojs/compiler/types'; import type { CompletionItem, Diagnostic, + DiagnosticSeverity, LanguageServicePlugin, LanguageServicePluginInstance, } from '@volar/language-server'; import { CompletionItemKind, InsertTextFormat, - Position, + type Position, Range, TextEdit, } from '@volar/language-server'; import type { TextDocument } from 'vscode-html-languageservice'; import { URI } from 'vscode-uri'; +import type { AstroDiagnostic } from '@astrojs/astro2tsx'; import { AstroVirtualCode } from '../core/index.js'; export const create = (): LanguageServicePlugin => { @@ -58,14 +59,14 @@ export const create = (): LanguageServicePlugin => { return virtualCode.compilerDiagnostics.map(compilerMessageToDiagnostic); - function compilerMessageToDiagnostic(message: DiagnosticMessage): Diagnostic { - const start = Position.create(message.location.line - 1, message.location.column - 1); - const end = document.positionAt(document.offsetAt(start) + message.location.length); + function compilerMessageToDiagnostic(diagnostic: AstroDiagnostic): Diagnostic { return { - message: message.text + (message.hint ? '\n\n' + message.hint : ''), - range: Range.create(start, end), - code: message.code, - severity: message.severity, + message: diagnostic.message, + range: Range.create( + document.positionAt(diagnostic.position.start), + document.positionAt(diagnostic.position.end), + ), + severity: diagnostic.severity as DiagnosticSeverity, source: 'astro', }; } diff --git a/packages/language-tools/language-server/src/plugins/typescript/index.ts b/packages/language-tools/language-server/src/plugins/typescript/index.ts index 2f1766c7fc64..066970f8d62c 100644 --- a/packages/language-tools/language-server/src/plugins/typescript/index.ts +++ b/packages/language-tools/language-server/src/plugins/typescript/index.ts @@ -83,8 +83,7 @@ export const create = ( let tsxLineCount = undefined; if (root instanceof AstroVirtualCode && decoded?.[1] === 'tsx') { - // If we have compiler errors, our TSX isn't valid so don't bother showing TS errors - if (root.hasCompilationErrors) return null; + if (!root.hasUsableTSX) return null; // We'll use this to filter out diagnostics that are outside the mapped range of the TSX tsxLineCount = root.astroMeta.tsxRanges.body.end.line; diff --git a/packages/language-tools/language-server/test/test-utils.ts b/packages/language-tools/language-server/test/test-utils.ts index 0993470cd7ff..a46cb7422679 100644 --- a/packages/language-tools/language-server/test/test-utils.ts +++ b/packages/language-tools/language-server/test/test-utils.ts @@ -4,30 +4,4 @@ import { fileURLToPath } from 'node:url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -interface Point { - line: number; - column: number; - offset: number; -} - -interface Position { - start: Point; - end?: Point; -} - export const fixtureDir = path.join(__dirname, './fixture'); - -export function createCompilerPosition(start: Point, end: Point): Position { - return { - start, - end, - }; -} - -export function createCompilerPoint(line: number, column: number, offset: number): Point { - return { - line, - column, - offset, - }; -} diff --git a/packages/language-tools/language-server/test/typescript/diagnostics.test.ts b/packages/language-tools/language-server/test/typescript/diagnostics.test.ts index 790e58b278d1..4526e26da34d 100644 --- a/packages/language-tools/language-server/test/typescript/diagnostics.test.ts +++ b/packages/language-tools/language-server/test/typescript/diagnostics.test.ts @@ -11,6 +11,22 @@ describe('TypeScript - Diagnostics', async () => { before(async () => (languageServer = await getLanguageServer())); + it('still type-checks a file whose markup has a syntax error', async () => { + // A tag left unclosed mid-typing must not blank out every TS error. + const document = await languageServer.openFakeDocument( + '---\nNotAThing\n---\n
item.source === 'ts' && item.code === 2304), + `expected the frontmatter error to survive:\n${JSON.stringify(diagnostics.items, null, 1)}`, + ); + }); + it('Can get diagnostics in the frontmatter', async () => { const document = await languageServer.openFakeDocument('---\nNotAThing\n---', 'astro'); const diagnostics = (await languageServer.handle.sendDocumentDiagnosticRequest( diff --git a/packages/language-tools/language-server/test/units/astro2tsx.test.ts b/packages/language-tools/language-server/test/units/astro2tsx.test.ts new file mode 100644 index 000000000000..f414bab66489 --- /dev/null +++ b/packages/language-tools/language-server/test/units/astro2tsx.test.ts @@ -0,0 +1,62 @@ +import assert from 'node:assert'; +import { describe, it } from 'node:test'; +import { astro2tsx } from '../../dist/core/astro2tsx.js'; + +describe('astro2tsx - virtual code mappings', () => { + it('produces ascending, in-bounds, non-overlapping runs', () => { + const input = [ + '---', + 'const title = "hi";', + '---', + '
', + ' {title}
', + ' ', + '
', + ].join('\n'); + + const { virtualCode } = astro2tsx(input, 'Card.astro'); + const generated = virtualCode.snapshot.getText(0, virtualCode.snapshot.getLength()); + const [mapping] = virtualCode.mappings; + + assert.ok(mapping.generatedOffsets.length > 0); + for (let i = 0; i < mapping.generatedOffsets.length; i++) { + const gen = mapping.generatedOffsets[i]; + const src = mapping.sourceOffsets[i]; + const len = mapping.lengths[i]; + assert.ok(len > 0, `run ${i} is empty`); + assert.ok(gen + len <= generated.length, `run ${i} runs past the generated code`); + assert.ok(src + len <= input.length, `run ${i} runs past the source`); + if (i > 0) { + const previousEnd = mapping.generatedOffsets[i - 1] + mapping.lengths[i - 1]; + assert.ok(gen >= previousEnd, `run ${i} overlaps its predecessor`); + } + } + }); + + it('resolves attribute values to their own source text', () => { + const input = '
'; + const { virtualCode, ranges } = astro2tsx(input, 'Card.astro'); + const [mapping] = virtualCode.mappings; + + const sourceOffsetOf = (generatedOffset: number) => { + for (let i = 0; i < mapping.generatedOffsets.length; i++) { + const delta = generatedOffset - mapping.generatedOffsets[i]; + if (delta >= 0 && delta < mapping.lengths[i]) return mapping.sourceOffsets[i] + delta; + } + return null; + }; + + const generated = virtualCode.snapshot.getText(0, virtualCode.snapshot.getLength()); + for (const needle of ['unquoted', 'go()']) { + const source = sourceOffsetOf(generated.indexOf(needle)); + assert.notEqual(source, null, `${needle} is unmapped`); + assert.equal(input.slice(source, source + needle.length), needle); + } + + // Extracted tags carry source ranges the embedded documents rely on. + assert.equal( + input.slice(ranges.scripts[0].position.start, ranges.scripts[0].position.end), + ranges.scripts[0].content, + ); + }); +}); diff --git a/packages/language-tools/language-server/test/units/parseAstro.test.ts b/packages/language-tools/language-server/test/units/parseAstro.test.ts index 89daf2a2c5c0..b125b5679bc8 100644 --- a/packages/language-tools/language-server/test/units/parseAstro.test.ts +++ b/packages/language-tools/language-server/test/units/parseAstro.test.ts @@ -1,45 +1,12 @@ import assert from 'node:assert'; import { describe, it } from 'node:test'; import { getAstroMetadata } from '../../dist/core/parseAstro.js'; -import { createCompilerPoint, createCompilerPosition } from '../test-utils.ts'; describe('parseAstro - Can parse astro files', () => { - it('Can parse files', () => { + it('Reports the frontmatter range', () => { const input = `---\n---
Astro!
`; const metadata = getAstroMetadata('file.astro', input); - assert.deepStrictEqual(metadata.ast, { - children: [ - { - position: createCompilerPosition( - createCompilerPoint(1, 1, 0), - createCompilerPoint(2, 4, 7), - ), - type: 'frontmatter', - value: '\n', - }, - { - attributes: [], - children: [ - { - position: createCompilerPosition( - createCompilerPoint(2, 10, 13), - createCompilerPoint(2, 16, 19), - ), - type: 'text', - value: 'Astro!', - }, - ], - name: 'div', - position: createCompilerPosition( - createCompilerPoint(2, 5, 8), - createCompilerPoint(2, 22, 25), - ), - type: 'element', - }, - ], - type: 'root', - }); assert.deepStrictEqual(metadata.frontmatter, { status: 'closed', position: { @@ -55,7 +22,6 @@ describe('parseAstro - Can parse astro files', () => { }, }, }); - assert.deepStrictEqual(metadata.diagnostics, []); }); it('properly return frontmatter states', () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4914fd6068f..c96a09c8fe6f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6704,15 +6704,12 @@ importers: packages/language-tools/language-server: dependencies: - '@astrojs/compiler': - specifier: ^2.13.1 - version: 2.13.1 + '@astrojs/astro2tsx': + specifier: link:/home/erika/worktrees/compiler-rs/feat-biome-tsx/crates/astro2tsx + version: link:../../../../../compiler-rs/feat-biome-tsx/crates/astro2tsx '@astrojs/yaml2ts': specifier: ^0.2.4 version: link:../yaml2ts - '@jridgewell/sourcemap-codec': - specifier: ^1.5.5 - version: 1.5.5 '@volar/kit': specifier: ~2.4.28 version: 2.4.28(typescript@6.0.3)