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(js/css): Fixes intellisense inside inline styles and scripts #897

Closed
wants to merge 4 commits into from
Closed
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
7 changes: 7 additions & 0 deletions .changeset/weak-kiwis-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@astrojs/language-server": patch
"@astrojs/check": patch
"astro-vscode": patch
---

Fixes intellisense not working correctly inside HTML events and style attributes when multi bytes characters were present in the file
18 changes: 15 additions & 3 deletions packages/language-server/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import type { TypeScriptExtraServiceScript } from '@volar/typescript';
import type ts from 'typescript';
import type { HTMLDocument } from 'vscode-html-languageservice';
import { TextDocument } from 'vscode-languageserver-textdocument';
import type { URI } from 'vscode-uri';
import { type AstroInstall, getLanguageServerTypesDir } from '../utils.js';
import { astro2tsx } from './astro2tsx';
Expand Down Expand Up @@ -161,6 +162,13 @@ export class AstroVirtualCode implements VirtualCode {
];
this.compilerDiagnostics = [];

const astroDocument = TextDocument.create(
fileName + '.astro',
'astro',
0,
snapshot.getText(0, snapshot.getLength())
);

const astroMetadata = getAstroMetadata(
this.fileName,
this.snapshot.getText(0, this.snapshot.getLength())
Expand All @@ -178,13 +186,17 @@ export class AstroVirtualCode implements VirtualCode {
);
this.htmlDocument = htmlDocument;

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

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

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

Expand Down
7 changes: 4 additions & 3 deletions packages/language-server/src/core/parseCSS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { is } from '@astrojs/compiler/utils';
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 type { HTMLDocument, Node, TextDocument } from 'vscode-html-languageservice';
import { buildMappings } from '../buildMappings.js';
import type { AttributeNodeWithPosition } from './compilerUtils.js';
import { type AttributeNodeWithPosition, PointToPosition } from './compilerUtils.js';

export function extractStylesheets(
snapshot: ts.IScriptSnapshot,
astroDocument: TextDocument,
htmlDocument: HTMLDocument,
ast: ParseResult['ast']
): VirtualCode[] {
Expand All @@ -22,7 +23,7 @@ export function extractStylesheets(
codes.push([
inlineStyle.value,
undefined,
inlineStyle.position.start.offset + 'style="'.length,
astroDocument.offsetAt(PointToPosition(inlineStyle.position.start)) + 'style="'.length,
{
completion: true,
verification: false,
Expand Down
15 changes: 11 additions & 4 deletions packages/language-server/src/core/parseJS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@ import { is } from '@astrojs/compiler/utils';
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 type { HTMLDocument, Node, TextDocument } from 'vscode-html-languageservice';
import { buildMappings } from '../buildMappings';
import { PointToPosition } from './compilerUtils.js';

export function extractScriptTags(
snapshot: ts.IScriptSnapshot,
astroDocument: TextDocument,
htmlDocument: HTMLDocument,
ast: ParseResult['ast']
): VirtualCode[] {
const embeddedJSCodes: VirtualCode[] = findModuleScripts(snapshot, htmlDocument.roots);

const javascriptContexts = [
...findClassicScripts(htmlDocument, snapshot),
...findEventAttributes(ast),
...findEventAttributes(ast, astroDocument),
].sort((a, b) => a.startOffset - b.startOffset);

if (javascriptContexts.length > 0) {
Expand Down Expand Up @@ -153,7 +155,10 @@ function isJSON(type: string | null | undefined): boolean {
return JSON_TYPES.includes(type.slice(1, -1));
}

function findEventAttributes(ast: ParseResult['ast']): JavaScriptContext[] {
function findEventAttributes(
ast: ParseResult['ast'],
astroDocument: TextDocument
): JavaScriptContext[] {
const eventAttrs: JavaScriptContext[] = [];

// `@astrojs/compiler`'s `walk` method is async, so we can't use it here. Arf
Expand All @@ -172,7 +177,9 @@ function findEventAttributes(ast: ParseResult['ast']): JavaScriptContext[] {
// This is not perfect, but it's better than nothing
// See: https://github.com/microsoft/vscode/blob/e8e04769ec817a3374c3eaa26a08d3ae491820d5/extensions/html-language-features/server/src/modes/embeddedSupport.ts#L192
content: eventAttribute.value + ';',
startOffset: eventAttribute.position.start.offset + `${eventAttribute.name}="`.length,
startOffset:
astroDocument.offsetAt(PointToPosition(eventAttribute.position.start)) +
`${eventAttribute.name}="`.length,
});
}
}
Expand Down
14 changes: 14 additions & 0 deletions packages/language-server/test/css/completions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,18 @@ describe('CSS - Completions', () => {
expect(completions!.items).to.not.be.empty;
expect(completions?.items.map((i) => i.label)).to.include('aliceblue');
});

it('Can provide completions inside inline styles with multi-bytes characters in the file', async () => {
const document = await languageServer.openFakeDocument(
`<div>あ</div><div style="color: ;"></div>`,
'astro'
);
const completions = await languageServer.handle.sendCompletionRequest(
document.uri,
Position.create(0, 30)
);

expect(completions!.items).to.not.be.empty;
expect(completions?.items.map((i) => i.label)).to.include('aliceblue');
});
});
31 changes: 31 additions & 0 deletions packages/language-server/test/typescript/completions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,35 @@ describe('TypeScript - Completions', async () => {
);
expect(edits?.additionalTextEdits?.[0].range.start.line).to.equal(0);
});

it('Can get completions inside HTML events', async () => {
const document = await languageServer.openFakeDocument('<div onload="a"></div>', 'astro');
const completions = await languageServer.handle.sendCompletionRequest(
document.uri,
Position.create(0, 13)
);

expect(completions?.items).to.not.be.empty;

// Make sure we have the `alert` completion, which is a global function
const allLabels = completions?.items.map((item) => item.label);
expect(allLabels).to.include('alert');
});

it('Can get completions inside HTML events with multi-bytes characters in the file', async () => {
const document = await languageServer.openFakeDocument(
'<div>あ</div><div onload="a"></div>',
'astro'
);
const completions = await languageServer.handle.sendCompletionRequest(
document.uri,
Position.create(0, 24)
);

expect(completions?.items).to.not.be.empty;

// Make sure we have the `alert` completion, which is a global function
const allLabels = completions?.items.map((item) => item.label);
expect(allLabels).to.include('alert');
});
});
4 changes: 3 additions & 1 deletion packages/language-server/test/units/parseCSS.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect } from 'chai';
import ts from 'typescript/lib/typescript.js';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { getAstroMetadata } from '../../src/core/parseAstro.js';
import { extractStylesheets } from '../../src/core/parseCSS.js';
import { parseHTML } from '../../src/core/parseHTML.js';
Expand All @@ -8,10 +9,11 @@ describe('parseCSS - Can find all the styles in an Astro file', () => {
it('Can find all the styles in an Astro file, including nested tags', () => {
const input = `<style>h1{color: blue;}</style><div><style>h2{color: red;}</style></div>`;
const snapshot = ts.ScriptSnapshot.fromString(input);
const astroDocument = TextDocument.create('file.astro', 'astro', 0, input);
const html = parseHTML(snapshot, 0);
const astroAst = getAstroMetadata('file.astro', input).ast;

const styleTags = extractStylesheets(snapshot, html.htmlDocument, astroAst);
const styleTags = extractStylesheets(snapshot, astroDocument, html.htmlDocument, astroAst);

expect(styleTags.length).to.equal(2);
});
Expand Down
10 changes: 7 additions & 3 deletions packages/language-server/test/units/parseJS.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect } from 'chai';
import ts from 'typescript/lib/typescript.js';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { getAstroMetadata } from '../../src/core/parseAstro.js';
import { parseHTML } from '../../src/core/parseHTML.js';
import { extractScriptTags } from '../../src/core/parseJS.js';
Expand All @@ -8,32 +9,35 @@ describe('parseJS - Can find all the scripts in an Astro file', () => {
it('Can find all the scripts in an Astro file, including nested tags', () => {
const input = `<script>console.log('hi')</script><div><script>console.log('hi2')</script></div>`;
const snapshot = ts.ScriptSnapshot.fromString(input);
const astroDocument = TextDocument.create('file.astro', 'astro', 0, input);
const html = parseHTML(snapshot, 0);
const astroAst = getAstroMetadata('file.astro', input).ast;

const scriptTags = extractScriptTags(snapshot, html.htmlDocument, astroAst);
const scriptTags = extractScriptTags(snapshot, astroDocument, html.htmlDocument, astroAst);

expect(scriptTags.length).to.equal(2);
});

it('Ignore JSON scripts', () => {
const input = `<script type="application/json">{foo: "bar"}</script>`;
const snapshot = ts.ScriptSnapshot.fromString(input);
const astroDocument = TextDocument.create('file.astro', 'astro', 0, input);
const html = parseHTML(snapshot, 0);
const astroAst = getAstroMetadata('file.astro', input).ast;

const scriptTags = extractScriptTags(snapshot, html.htmlDocument, astroAst);
const scriptTags = extractScriptTags(snapshot, astroDocument, html.htmlDocument, astroAst);

expect(scriptTags.length).to.equal(0);
});

it('returns the proper capabilities for inline script tags', () => {
const input = `<script is:inline>console.log('hi')</script>`;
const snapshot = ts.ScriptSnapshot.fromString(input);
const astroDocument = TextDocument.create('file.astro', 'astro', 0, input);
const html = parseHTML(snapshot, 0);
const astroAst = getAstroMetadata('file.astro', input).ast;

const scriptTags = extractScriptTags(snapshot, html.htmlDocument, astroAst);
const scriptTags = extractScriptTags(snapshot, astroDocument, html.htmlDocument, astroAst);

scriptTags[0].mappings.forEach((mapping) => {
expect(mapping.data).to.deep.equal({
Expand Down
Loading