Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions packages/language-tools/language-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
180 changes: 60 additions & 120 deletions packages/language-tools/language-server/src/core/astro2tsx.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -14,161 +13,102 @@ 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,
};
}

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: [],
};
}
34 changes: 13 additions & 21 deletions packages/language-tools/language-server/src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as path from 'node:path';
import type { DiagnosticMessage, DiagnosticSeverity } from '@astrojs/compiler/types';
import {
type CodeMapping,
forEachEmbeddedCode,
Expand All @@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
);
}
}
Loading
Loading