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

fix: scripts affecting intellisense #900

Merged
merged 5 commits into from
Jul 16, 2024
Merged
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
6 changes: 6 additions & 0 deletions .changeset/light-bananas-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@astrojs/language-server": minor
"astro-vscode": minor
---

Improves the handling of script and style tags. This release fixes numerous issues where the presence of those tags could break intellisense in certain parts of the file.
2 changes: 1 addition & 1 deletion packages/language-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"test:match": "pnpm run test -g"
},
"dependencies": {
"@astrojs/compiler": "^2.7.0",
"@astrojs/compiler": "^2.9.1",
"@jridgewell/sourcemap-codec": "^1.4.15",
"@volar/kit": "~2.4.0-alpha.15",
"@volar/language-core": "~2.4.0-alpha.15",
Expand Down
59 changes: 25 additions & 34 deletions packages/language-server/src/core/astro2tsx.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { convertToTSX } from '@astrojs/compiler/sync';
import type { ConvertToTSXOptions, TSXResult } from '@astrojs/compiler/types';
import type {
ConvertToTSXOptions,
TSXExtractedScript,
TSXExtractedStyle,
TSXResult,
} from '@astrojs/compiler/types';
import { decode } from '@jridgewell/sourcemap-codec';
import type { CodeInformation, CodeMapping, VirtualCode } from '@volar/language-core';
import type { CodeMapping, VirtualCode } from '@volar/language-core';
import { Range } from '@volar/language-server';
import { HTMLDocument, TextDocument } from 'vscode-html-languageservice';
import { TextDocument } from 'vscode-html-languageservice';
import { patchTSX } from './utils.js';

export interface LSPTSXRanges {
frontmatter: Range;
body: Range;
scripts: TSXExtractedScript[];
styles: TSXExtractedStyle[];
}

export function safeConvertToTSX(content: string, options: ConvertToTSXOptions) {
Expand Down Expand Up @@ -47,6 +54,8 @@ export function safeConvertToTSX(content: string, options: ConvertToTSXOptions)
start: 0,
end: 0,
},
scripts: [],
styles: [],
},
} satisfies TSXResult;
}
Expand All @@ -64,25 +73,22 @@ export function getTSXRangesAsLSPRanges(tsx: TSXResult): LSPTSXRanges {
textDocument.positionAt(tsx.metaRanges.body.start),
textDocument.positionAt(tsx.metaRanges.body.end)
),
scripts: tsx.metaRanges.scripts ?? [],
styles: tsx.metaRanges.styles ?? [],
};
}

export function astro2tsx(input: string, fileName: string, htmlDocument: HTMLDocument) {
export function astro2tsx(input: string, fileName: string) {
const tsx = safeConvertToTSX(input, { filename: fileName });

return {
virtualCode: getVirtualCodeTSX(input, tsx, fileName, htmlDocument),
virtualCode: getVirtualCodeTSX(input, tsx, fileName),
diagnostics: tsx.diagnostics,
ranges: getTSXRangesAsLSPRanges(tsx),
};
}

function getVirtualCodeTSX(
input: string,
tsx: TSXResult,
fileName: string,
htmlDocument: HTMLDocument
): VirtualCode {
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);
Expand Down Expand Up @@ -123,33 +129,18 @@ function getVirtualCodeTSX(
) {
lastMapping.lengths[0] += length;
} else {
// Disable features inside script tags. This is a bit annoying to do, I wonder if maybe leaving script tags
// unmapped would be better.
const node = htmlDocument.findNodeAt(current.sourceOffset);
const rangeCapabilities: CodeInformation =
node.tag !== 'script'
? {
verification: true,
completion: true,
semantic: true,
navigation: true,
structure: true,
format: false,
}
: {
verification: false,
completion: false,
semantic: false,
navigation: false,
structure: false,
format: false,
};

mappings.push({
sourceOffsets: [current.sourceOffset],
generatedOffsets: [current.genOffset],
lengths: [length],
data: rangeCapabilities,
data: {
verification: true,
completion: true,
semantic: true,
navigation: true,
structure: true,
format: false,
},
});
}
}
Expand Down
35 changes: 8 additions & 27 deletions packages/language-server/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@ export class AstroVirtualCode implements VirtualCode {
astroMeta!: AstroMetadata;
compilerDiagnostics!: DiagnosticMessage[];
htmlDocument!: HTMLDocument;
scriptCodeIds!: string[];
codegenStacks = [];

constructor(
Expand All @@ -159,47 +158,29 @@ export class AstroVirtualCode implements VirtualCode {
},
},
];
this.compilerDiagnostics = [];

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

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

const { htmlDocument, virtualCode: htmlVirtualCode } = parseHTML(
this.snapshot,
astroMetadata.frontmatter.status === 'closed'
? astroMetadata.frontmatter.position.end.offset
: 0
);
this.htmlDocument = htmlDocument;

const scriptTags = extractScriptTags(this.snapshot, htmlDocument, astroMetadata.ast);

this.scriptCodeIds = scriptTags.map((scriptTag) => scriptTag.id);

htmlVirtualCode.embeddedCodes = [];
htmlVirtualCode.embeddedCodes.push(
...extractStylesheets(this.snapshot, htmlDocument, astroMetadata.ast),
...scriptTags
);

this.embeddedCodes = [];
this.embeddedCodes.push(htmlVirtualCode);

const tsx = astro2tsx(
this.snapshot.getText(0, this.snapshot.getLength()),
this.fileName,
htmlDocument
);
this.htmlDocument = htmlDocument;
htmlVirtualCode.embeddedCodes = [
extractStylesheets(tsx.ranges.styles),
...extractScriptTags(tsx.ranges.scripts),
];

this.astroMeta = { ...astroMetadata, tsxRanges: tsx.ranges };
this.compilerDiagnostics.push(...tsx.diagnostics);
this.embeddedCodes.push(tsx.virtualCode);
this.compilerDiagnostics = [...tsx.diagnostics, ...astroMetadata.diagnostics];
this.embeddedCodes = [htmlVirtualCode, tsx.virtualCode];
}

get hasCompilationErrors(): boolean {
Expand Down
196 changes: 36 additions & 160 deletions packages/language-server/src/core/parseCSS.ts
Original file line number Diff line number Diff line change
@@ -1,169 +1,45 @@
import type { ParentNode, ParseResult } from '@astrojs/compiler/types';
import { is } from '@astrojs/compiler/utils';
import type { TSXExtractedStyle } from '@astrojs/compiler/types';
import type { CodeInformation, VirtualCode } from '@volar/language-core';
import { Segment, toString } from 'muggle-string';
import type ts from 'typescript';
import type { HTMLDocument, Node } from 'vscode-html-languageservice';
import { buildMappings } from '../buildMappings.js';
import type { AttributeNodeWithPosition } from './compilerUtils.js';

export function extractStylesheets(
snapshot: ts.IScriptSnapshot,
htmlDocument: HTMLDocument,
ast: ParseResult['ast']
): VirtualCode[] {
const embeddedCSSCodes: VirtualCode[] = findEmbeddedStyles(snapshot, htmlDocument.roots);

const inlineStyles = findInlineStyles(ast);
if (inlineStyles.length > 0) {
const codes: Segment<CodeInformation>[] = [];
for (const inlineStyle of inlineStyles) {
codes.push('x { ');
codes.push([
inlineStyle.value,
undefined,
inlineStyle.position.start.offset + 'style="'.length,
{
completion: true,
verification: false,
semantic: true,
navigation: true,
structure: true,
format: false,
},
]);
codes.push(' }\n');
}

const mappings = buildMappings(codes);
const text = toString(codes);

embeddedCSSCodes.push({
id: 'inline.css',
languageId: 'css',
snapshot: {
getText: (start, end) => text.substring(start, end),
getLength: () => text.length,
getChangeRange: () => undefined,
},
embeddedCodes: [],
mappings,
});
}

return embeddedCSSCodes;
}

/**
* Find all embedded styles in a document.
* Embedded styles are styles that are defined in `<style>` tags.
*/
function findEmbeddedStyles(snapshot: ts.IScriptSnapshot, roots: Node[]): VirtualCode[] {
const embeddedCSSCodes: VirtualCode[] = [];
let cssIndex = 0;

getEmbeddedCSSInNodes(roots);

function getEmbeddedCSSInNodes(nodes: Node[]) {
for (const [_, node] of nodes.entries()) {
if (
node.tag === 'style' &&
node.startTagEnd !== undefined &&
node.endTagStart !== undefined
) {
const styleText = snapshot.getText(node.startTagEnd, node.endTagStart);
embeddedCSSCodes.push({
id: `${cssIndex}.css`,
languageId: 'css',
snapshot: {
getText: (start, end) => styleText.substring(start, end),
getLength: () => styleText.length,
getChangeRange: () => undefined,
},
mappings: [
{
sourceOffsets: [node.startTagEnd],
generatedOffsets: [0],
lengths: [styleText.length],
data: {
verification: false,
completion: true,
semantic: true,
navigation: true,
structure: true,
format: false,
},
},
],
embeddedCodes: [],
});
cssIndex++;
}

if (node.children) getEmbeddedCSSInNodes(node.children);
}
}

return embeddedCSSCodes;
export function extractStylesheets(styles: TSXExtractedStyle[]): VirtualCode {
return mergeCSSContexts(styles);
}

/**
* Find all inline styles using the Astro AST
* Inline styles are styles that are defined in the `style` attribute of an element.
* TODO: Merge this with `findEmbeddedCSS`? Unlike scripts, there's no scoping being done here, so merging all of it in
* the same virtual file is possible, though it might make mapping more tricky.
Comment on lines -113 to -114
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the case now in the new format, which improves intellisense for class names and stuff when you have multiple style tags in the same file. Kinda unlikely, but still funny!

*/
function findInlineStyles(ast: ParseResult['ast']): AttributeNodeWithPosition[] {
const styleAttrs: AttributeNodeWithPosition[] = [];

// `@astrojs/compiler`'s `walk` method is async, so we can't use it here. Arf
function walkDown(parent: ParentNode) {
if (!parent.children) return;

parent.children.forEach((child) => {
if (is.element(child)) {
const styleAttribute = child.attributes.find(
(attr) => attr.name === 'style' && attr.kind === 'quoted'
);

if (styleAttribute && styleAttribute.position) {
styleAttrs.push(styleAttribute as AttributeNodeWithPosition);
}
}

if (is.parent(child)) {
walkDown(child);
}
});
}

walkDown(ast);

return styleAttrs;
}

// TODO: Provide completion for classes and IDs
export function collectClassesAndIdsFromDocument(ast: ParseResult['ast']): string[] {
const classesAndIds: string[] = [];
function walkDown(parent: ParentNode) {
if (!parent.children) return;

parent.children.forEach((child) => {
if (is.element(child)) {
const classOrIDAttributes = child.attributes
.filter((attr) => attr.kind === 'quoted' && (attr.name === 'class' || attr.name === 'id'))
.flatMap((attr) => attr.value.split(' '));

classesAndIds.push(...classOrIDAttributes);
}

if (is.parent(child)) {
walkDown(child);
}
});
function mergeCSSContexts(inlineStyles: TSXExtractedStyle[]): VirtualCode {
const codes: Segment<CodeInformation>[] = [];

for (const javascriptContext of inlineStyles) {
if (javascriptContext.type === 'style-attribute') codes.push('__ { ');
codes.push([
javascriptContext.content,
undefined,
javascriptContext.position.start,
{
verification: true,
completion: true,
semantic: true,
navigation: true,
structure: true,
format: false,
},
]);
if (javascriptContext.type === 'style-attribute') codes.push(' }\n');
}

walkDown(ast);

return classesAndIds;
const mappings = buildMappings(codes);
const text = toString(codes);

return {
id: 'style.css',
languageId: 'css',
snapshot: {
getText: (start, end) => text.substring(start, end),
getLength: () => text.length,
getChangeRange: () => undefined,
},
embeddedCodes: [],
mappings,
};
}
Loading