Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Properly account for new TSX prefix #733

Merged
merged 13 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from 12 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
7 changes: 7 additions & 0 deletions .changeset/good-ants-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astrojs/language-server': minor
'@astrojs/ts-plugin': minor
'astro-vscode': minor
---

Internally type Astro files individually instead of applying a specific configuration to every file loaded in the Astro language server, this should generally result in more accurate types when using JSX frameworks. But should generally be an invisible change for most users.
32 changes: 16 additions & 16 deletions packages/language-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,22 @@
"test:match": "pnpm run test -g"
},
"dependencies": {
"@astrojs/compiler": "^2.2.2",
"@astrojs/compiler": "^2.4.0",
"@jridgewell/sourcemap-codec": "^1.4.15",
"@volar/kit": "~1.10.9",
"@volar/language-core": "~1.10.9",
"@volar/language-server": "~1.10.9",
"@volar/language-service": "~1.10.9",
"@volar/source-map": "~1.10.9",
"@volar/typescript": "~1.10.9",
"@volar/kit": "~1.11.1",
"@volar/language-core": "~1.11.1",
"@volar/language-server": "~1.11.1",
"@volar/language-service": "~1.11.1",
"@volar/source-map": "~1.11.1",
"@volar/typescript": "~1.11.1",
"fast-glob": "^3.2.12",
"muggle-string": "^0.3.1",
"volar-service-css": "0.0.16",
"volar-service-emmet": "0.0.16",
"volar-service-html": "0.0.16",
"volar-service-prettier": "0.0.16",
"volar-service-typescript": "0.0.16",
"volar-service-typescript-twoslash-queries": "0.0.16",
"volar-service-css": "0.0.17",
"volar-service-emmet": "0.0.17",
"volar-service-html": "0.0.17",
"volar-service-prettier": "0.0.17",
"volar-service-typescript": "0.0.17",
"volar-service-typescript-twoslash-queries": "0.0.17",
"vscode-html-languageservice": "^5.1.0",
"vscode-uri": "^3.0.8"
},
Expand All @@ -51,13 +51,13 @@
"@types/chai": "^4.3.5",
"@types/mocha": "^10.0.1",
"@types/node": "^18.17.8",
"astro": "^3.3.0",
"astro": "^4.1.0",
"chai": "^4.3.7",
"mocha": "^10.2.0",
"tsx": "^3.12.7",
"typescript": "^5.2.2",
"vscode-languageserver-protocol": "^3.17.5",
"vscode-languageserver-textdocument": "^1.0.11",
"typescript": "^5.2.2"
"vscode-languageserver-textdocument": "^1.0.11"
},
"peerDependencies": {
"prettier": "^3.0.0",
Expand Down
44 changes: 41 additions & 3 deletions packages/language-server/src/core/astro2tsx.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
import { convertToTSX } from '@astrojs/compiler/sync';
import type { ConvertToTSXOptions, TSXResult } from '@astrojs/compiler/types';
import type { ConvertToTSXOptions, DiagnosticMessage, TSXResult } from '@astrojs/compiler/types';
import { decode } from '@jridgewell/sourcemap-codec';
import { FileKind, FileRangeCapabilities, VirtualFile } from '@volar/language-core';
import { Range } from '@volar/language-server';
import { HTMLDocument, TextDocument } from 'vscode-html-languageservice';
import { patchTSX } from './utils.js';

function safeConvertToTSX(content: string, options: ConvertToTSXOptions) {
export interface LSPTSXRanges {
frontmatter: Range;
body: Range;
}

interface Astro2TSXResult {
virtualFile: VirtualFile;
diagnostics: DiagnosticMessage[];
ranges: LSPTSXRanges;
}

export function safeConvertToTSX(content: string, options: ConvertToTSXOptions) {
try {
const tsx = convertToTSX(content, { filename: options.filename });
return tsx;
Expand All @@ -32,21 +44,47 @@ function safeConvertToTSX(content: string, options: ConvertToTSXOptions) {
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/language-tools/issues`,
},
],
metaRanges: {
frontmatter: {
start: 0,
end: 0,
},
body: {
start: 0,
end: 0,
},
},
} satisfies TSXResult;
}
}

export function getTSXRangesAsLSPRanges(tsx: TSXResult): 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)
),
body: Range.create(
textDocument.positionAt(tsx.metaRanges.body.start),
textDocument.positionAt(tsx.metaRanges.body.end)
),
};
}

export function astro2tsx(
input: string,
fileName: string,
ts: typeof import('typescript/lib/tsserverlibrary.js'),
htmlDocument: HTMLDocument
) {
): Astro2TSXResult {
const tsx = safeConvertToTSX(input, { filename: fileName });

return {
virtualFile: getVirtualFileTSX(input, tsx, fileName, ts, htmlDocument),
diagnostics: tsx.diagnostics,
ranges: getTSXRangesAsLSPRanges(tsx),
};
}

Expand Down
75 changes: 54 additions & 21 deletions packages/language-server/src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DiagnosticMessage, ParseResult } from '@astrojs/compiler/types';
import type { DiagnosticMessage } from '@astrojs/compiler/types';
import {
FileCapabilities,
FileKind,
Expand All @@ -11,7 +11,7 @@ import type ts from 'typescript/lib/tsserverlibrary';
import type { HTMLDocument } from 'vscode-html-languageservice';
import type { AstroInstall } from '../utils.js';
import { astro2tsx } from './astro2tsx';
import { FrontmatterStatus, getAstroMetadata } from './parseAstro';
import { AstroMetadata, getAstroMetadata } from './parseAstro';
import { extractStylesheets } from './parseCSS';
import { parseHTML } from './parseHTML';
import { extractScriptTags } from './parseJS.js';
Expand Down Expand Up @@ -44,15 +44,49 @@ export function getLanguageModule(
return host.resolveModuleName?.(moduleName, impliedNodeFormat) ?? moduleName;
},
getScriptFileNames() {
let languageServerDirectory: string;
try {
languageServerDirectory = path.dirname(require.resolve('@astrojs/language-server'));
} catch (e) {
languageServerDirectory = __dirname;
}

const fileNames = host.getScriptFileNames();
return [
...fileNames,
...(astroInstall
? ['./env.d.ts', './astro-jsx.d.ts'].map((filePath) =>
ts.sys.resolvePath(path.resolve(astroInstall.path, filePath))
)
: []),
];

const addedFileNames = [];

if (astroInstall) {
addedFileNames.push(
...['./env.d.ts', './astro-jsx.d.ts'].map((filePath) =>
ts.sys.resolvePath(path.resolve(astroInstall.path, filePath))
)
);

// If Astro version is < 4.0.8, add jsx-runtime-augment.d.ts to the files to fake `JSX` being available from "astro/jsx-runtime".
// TODO: Remove this once a majority of users are on Astro 4.0.8+, erika - 2023-12-28
if (
astroInstall.version.major >= 4 &&
(astroInstall.version.minor > 0 || astroInstall.version.patch >= 8)
) {
addedFileNames.push(
...['../jsx-runtime-augment.d.ts'].map((filePath) =>
ts.sys.resolvePath(path.resolve(languageServerDirectory, filePath))
)
);
}
} else {
// If we don't have an Astro installation, add the fallback types from the language server.
// See the README in packages/language-server/types for more information.
addedFileNames.push(
...[
'../types/env.d.ts',
'../types/astro-jsx.d.ts',
'../types/jsx-runtime-fallback.d.ts',
].map((f) => ts.sys.resolvePath(path.resolve(languageServerDirectory, f)))
);
}

return [...fileNames, ...addedFileNames];
},
getCompilationSettings() {
const baseCompilationSettings = host.getCompilationSettings();
Expand All @@ -61,10 +95,8 @@ export function getLanguageModule(
module: ts.ModuleKind.ESNext ?? 99,
target: ts.ScriptTarget.ESNext ?? 99,
jsx: ts.JsxEmit.Preserve ?? 1,
jsxImportSource: undefined,
jsxFactory: 'astroHTML',
resolveJsonModule: true,
allowJs: true,
allowJs: true, // Needed for inline scripts, which are virtual .js files
isolatedModules: true,
moduleResolution:
baseCompilationSettings.moduleResolution === ts.ModuleResolutionKind.Classic ||
Expand All @@ -85,7 +117,7 @@ export class AstroFile implements VirtualFile {
fileName: string;
mappings!: VirtualFile['mappings'];
embeddedFiles!: VirtualFile['embeddedFiles'];
astroMeta!: ParseResult & { frontmatter: FrontmatterStatus };
astroMeta!: AstroMetadata;
compilerDiagnostics!: DiagnosticMessage[];
htmlDocument!: HTMLDocument;
scriptFiles!: string[];
Expand Down Expand Up @@ -119,20 +151,20 @@ export class AstroFile implements VirtualFile {
];
this.compilerDiagnostics = [];

this.astroMeta = getAstroMetadata(
const astroMetadata = getAstroMetadata(
this.fileName,
this.snapshot.getText(0, this.snapshot.getLength())
);

if (this.astroMeta.diagnostics.length > 0) {
this.compilerDiagnostics.push(...this.astroMeta.diagnostics);
if (astroMetadata.diagnostics.length > 0) {
this.compilerDiagnostics.push(...astroMetadata.diagnostics);
}

const { htmlDocument, virtualFile: htmlVirtualFile } = parseHTML(
this.fileName,
this.snapshot,
this.astroMeta.frontmatter.status === 'closed'
? this.astroMeta.frontmatter.position.end.offset
astroMetadata.frontmatter.status === 'closed'
? astroMetadata.frontmatter.position.end.offset
: 0
);
this.htmlDocument = htmlDocument;
Expand All @@ -141,13 +173,13 @@ export class AstroFile implements VirtualFile {
this.fileName,
this.snapshot,
htmlDocument,
this.astroMeta.ast
astroMetadata.ast
);

this.scriptFiles = scriptTags.map((scriptTag) => scriptTag.fileName);

htmlVirtualFile.embeddedFiles.push(
...extractStylesheets(this.fileName, this.snapshot, htmlDocument, this.astroMeta.ast),
...extractStylesheets(this.fileName, this.snapshot, htmlDocument, astroMetadata.ast),
...scriptTags
);

Expand All @@ -161,6 +193,7 @@ export class AstroFile implements VirtualFile {
htmlDocument
);

this.astroMeta = { ...astroMetadata, tsxRanges: tsx.ranges };
this.compilerDiagnostics.push(...tsx.diagnostics);
this.embeddedFiles.push(tsx.virtualFile);
}
Expand Down
8 changes: 6 additions & 2 deletions packages/language-server/src/core/parseAstro.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { parse } from '@astrojs/compiler/sync';
import type { ParseOptions, ParseResult, Point } from '@astrojs/compiler/types';
import type { LSPTSXRanges } from './astro2tsx.js';

type AstroMetadata = ParseResult & { frontmatter: FrontmatterStatus };
export type AstroMetadata = ParseResult & {
frontmatter: FrontmatterStatus;
tsxRanges: LSPTSXRanges;
};

export function getAstroMetadata(
fileName: string,
input: string,
options: ParseOptions = { position: true }
): AstroMetadata {
): Omit<AstroMetadata, 'tsxRanges'> {
const parseResult = safeParseAst(fileName, input, options);

return {
Expand Down
43 changes: 43 additions & 0 deletions packages/language-server/src/plugins/typescript/codeActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { TextDocumentEdit } from '@volar/language-server';
import type { CodeAction, ServiceContext } from '@volar/language-service';
import { AstroFile } from '../../core/index.js';
import { editShouldBeInFrontmatter, ensureProperEditForFrontmatter } from '../utils.js';

export function enhancedProvideCodeActions(codeActions: CodeAction[], context: ServiceContext) {
return codeActions.map((codeAction) => mapCodeAction(codeAction, context));
}

export function enhancedResolveCodeAction(codeAction: CodeAction, context: ServiceContext) {
/**
* TypeScript code actions don't come through here, as they're considered to be already fully resolved
* A lot of the code actions we'll encounter here are more tricky ones, such as fixAll or refactor
* For now, it seems like we don't need to do anything special here, but we'll keep this function around
*/
return mapCodeAction(codeAction, context);
}

function mapCodeAction(codeAction: CodeAction, context: ServiceContext<any>) {
if (!codeAction.edit || !codeAction.edit.documentChanges) return codeAction;

codeAction.edit.documentChanges = codeAction.edit.documentChanges.map((change) => {
if (TextDocumentEdit.is(change)) {
const [virtualFile, source] = context.documents.getVirtualFileByUri(change.textDocument.uri);
const file = source?.root;
if (!virtualFile || !(file instanceof AstroFile) || !context.host) return change;

change.edits = change.edits.map((edit) => {
const shouldModifyEdit = editShouldBeInFrontmatter(edit.range, file.astroMeta);

if (shouldModifyEdit.itShould) {
edit = ensureProperEditForFrontmatter(edit, file.astroMeta, '\n');
}

return edit;
});
}

return change;
});

return codeAction;
}
30 changes: 28 additions & 2 deletions packages/language-server/src/plugins/typescript/completions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { CompletionItem, CompletionItemKind, CompletionList } from '@volar/language-server';
import {
CompletionItem,
CompletionItemKind,
CompletionList,
ServiceContext,
} from '@volar/language-server';
import { AstroFile } from '../../core/index.js';
import { editShouldBeInFrontmatter, ensureProperEditForFrontmatter } from '../utils.js';

export function enhancedProvideCompletionItems(completions: CompletionList): CompletionList {
completions.items = completions.items.filter(isValidCompletion).map((completion) => {
Expand All @@ -24,7 +31,10 @@ export function enhancedProvideCompletionItems(completions: CompletionList): Com
return completions;
}

export function enhancedResolveCompletionItem(resolvedCompletion: CompletionItem): CompletionItem {
export function enhancedResolveCompletionItem(
resolvedCompletion: CompletionItem,
context: ServiceContext
): CompletionItem {
// Make sure we keep our icons even when the completion is resolved
if (resolvedCompletion.data.isComponent) {
resolvedCompletion.detail = getDetailForFileCompletion(
Expand All @@ -33,6 +43,22 @@ export function enhancedResolveCompletionItem(resolvedCompletion: CompletionItem
);
}

if (resolvedCompletion.additionalTextEdits) {
const [virtualFile, source] = context.documents.getVirtualFileByUri(
resolvedCompletion.data.uri
);
const file = source?.root;
if (!virtualFile || !(file instanceof AstroFile) || !context.host) return resolvedCompletion;

resolvedCompletion.additionalTextEdits = resolvedCompletion.additionalTextEdits.map((edit) => {
if (editShouldBeInFrontmatter(edit.range, file.astroMeta).itShould) {
edit = ensureProperEditForFrontmatter(edit, file.astroMeta, '\n');
}

return edit;
});
}

return resolvedCompletion;
}

Expand Down
Loading