Skip to content

Commit

Permalink
Adds support for SCSS and LESS intellisense inside style tags (#923)
Browse files Browse the repository at this point in the history
* feat: properly handle style tags with different languages

* chore: changeset

* chore: update compiler
  • Loading branch information
Princesseuh authored Jul 30, 2024
1 parent 8fd9a6c commit b65d6b4
Show file tree
Hide file tree
Showing 11 changed files with 172 additions and 31 deletions.
7 changes: 7 additions & 0 deletions .changeset/wet-kangaroos-hide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@astrojs/language-server": minor
"@astrojs/check": minor
"astro-vscode": minor
---

Adds support for SCSS and LESS intellisense inside style tags
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.9.1",
"@astrojs/compiler": "^2.10.0",
"@jridgewell/sourcemap-codec": "^1.4.15",
"@volar/kit": "~2.4.0-alpha.15",
"@volar/language-core": "~2.4.0-alpha.15",
Expand Down
2 changes: 1 addition & 1 deletion packages/language-server/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export class AstroVirtualCode implements VirtualCode {

this.htmlDocument = htmlDocument;
htmlVirtualCode.embeddedCodes = [
extractStylesheets(tsx.ranges.styles),
...extractStylesheets(tsx.ranges.styles),
...extractScriptTags(tsx.ranges.scripts),
];

Expand Down
46 changes: 35 additions & 11 deletions packages/language-server/src/core/parseCSS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,30 @@ import type { CodeInformation, VirtualCode } from '@volar/language-core';
import { Segment, toString } from 'muggle-string';
import { buildMappings } from '../buildMappings.js';

export function extractStylesheets(styles: TSXExtractedStyle[]): VirtualCode {
return mergeCSSContexts(styles);
const SUPPORTED_LANGUAGES = ['css', 'scss', 'less'] as const;
type SupportedLanguages = (typeof SUPPORTED_LANGUAGES)[number];

function isSupportedLanguage(lang: string): lang is SupportedLanguages {
return SUPPORTED_LANGUAGES.includes(lang as SupportedLanguages);
}

function mergeCSSContexts(inlineStyles: TSXExtractedStyle[]): VirtualCode {
const codes: Segment<CodeInformation>[] = [];
export function extractStylesheets(styles: TSXExtractedStyle[]): VirtualCode[] {
return mergeCSSContextsByLanguage(styles);
}

function mergeCSSContextsByLanguage(inlineStyles: TSXExtractedStyle[]): VirtualCode[] {
const codes: Record<SupportedLanguages, Segment<CodeInformation>[]> = {
css: [],
scss: [],
less: [],
};

for (const cssContext of inlineStyles) {
const currentCode = isSupportedLanguage(cssContext.lang) ? codes[cssContext.lang] : codes.css;

const isStyleAttribute = cssContext.type === 'style-attribute';
if (isStyleAttribute) codes.push('__ { ');
codes.push([
if (isStyleAttribute) currentCode.push('__ { ');
currentCode.push([
cssContext.content,
undefined,
cssContext.position.start,
Expand All @@ -26,15 +39,26 @@ function mergeCSSContexts(inlineStyles: TSXExtractedStyle[]): VirtualCode {
format: false,
},
]);
if (isStyleAttribute) codes.push(' }\n');
if (isStyleAttribute) currentCode.push(' }\n');
}

const mappings = buildMappings(codes);
const text = toString(codes);
let virtualCodes: VirtualCode[] = [];
for (const lang of SUPPORTED_LANGUAGES) {
if (codes[lang].length) {
virtualCodes.push(createVirtualCodeForLanguage(codes[lang], lang));
}
}

return virtualCodes;
}

function createVirtualCodeForLanguage(code: Segment<CodeInformation>[], lang: string): VirtualCode {
const mappings = buildMappings(code);
const text = toString(code);

return {
id: 'style.css',
languageId: 'css',
id: `style.${lang}`,
languageId: lang,
snapshot: {
getText: (start, end) => text.substring(start, end),
getLength: () => text.length,
Expand Down
42 changes: 41 additions & 1 deletion packages/language-server/src/core/parseJS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ export function extractScriptTags(scripts: TSXExtractedScript[]): VirtualCode[]
.map(moduleScriptToVirtualCode) satisfies VirtualCode[];

const inlineScripts = scripts
.filter((script) => script.type === 'event-attribute' || script.type === 'inline')
.filter(
(script) =>
// TODO: Change this at some point so that unknown scripts are not included
// We can't guarantee that they are JavaScript, so we shouldn't treat them as such, even if it might work in some cases
// Perhaps we should make it so that the user has to specify the language of the script if it's not a known type (ex: lang="js"), not sure.
script.type === 'event-attribute' || script.type === 'inline' || script.type === 'unknown'
)
.sort((a, b) => a.position.start - b.position.start);

embeddedJSCodes.push(...moduleScripts);
Expand All @@ -20,6 +26,11 @@ export function extractScriptTags(scripts: TSXExtractedScript[]): VirtualCode[]
embeddedJSCodes.push(mergedJSContext);
}

const JSONScripts = scripts
.filter((script) => script.type === 'json')
.map(jsonScriptToVirtualCode) satisfies VirtualCode[];
embeddedJSCodes.push(...JSONScripts);

return embeddedJSCodes;
}

Expand Down Expand Up @@ -58,6 +69,35 @@ function moduleScriptToVirtualCode(script: TSXExtractedScript, index: number): V
};
}

function jsonScriptToVirtualCode(script: TSXExtractedScript, index: number): VirtualCode {
return {
id: `${index}.json`,
languageId: 'json',
snapshot: {
getText: (start, end) => script.content.substring(start, end),
getLength: () => script.content.length,
getChangeRange: () => undefined,
},
mappings: [
{
sourceOffsets: [script.position.start],
generatedOffsets: [0],
lengths: [script.content.length],
// TODO: Support JSON features
data: {
verification: false,
completion: false,
semantic: false,
navigation: false,
structure: false,
format: false,
},
},
],
embeddedCodes: [],
};
}

/**
* Merge all the inline and non-hoisted scripts into a single `.mjs` file
*/
Expand Down
43 changes: 43 additions & 0 deletions packages/language-server/test/css/completions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,47 @@ 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 SCSS blocks', async () => {
const document = await languageServer.openFakeDocument(
`<style lang="scss">
$c: red;
.foo {
color: $
}
</style>
`,
'astro'
);
const completions = await languageServer.handle.sendCompletionRequest(
document.uri,
Position.create(3, 10)
);

const allLabels = completions?.items.map((i) => i.label);

expect(completions!.items).to.not.be.empty;
expect(allLabels).to.include('$c');
});

it('Can provide completions inside LESS blocks', async () => {
const document = await languageServer.openFakeDocument(
`<style lang="less">
@link-color: #428bca;
h1 {
color: @
}
</style>
`,
'astro'
);
const completions = await languageServer.handle.sendCompletionRequest(
document.uri,
Position.create(3, 10)
);

const allLabels = completions?.items.map((i) => i.label);
expect(completions!.items).to.not.be.empty;
expect(allLabels).to.include('@link-color');
});
});
30 changes: 29 additions & 1 deletion packages/language-server/test/typescript/scripts.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FullDocumentDiagnosticReport } from '@volar/language-server';
import { FullDocumentDiagnosticReport, Position } from '@volar/language-server';
import { expect } from 'chai';
import { type LanguageServer, getLanguageServer } from '../server.js';

Expand Down Expand Up @@ -31,4 +31,32 @@ describe('TypeScript - Diagnostics', async () => {

expect(diagnostics.items).length(0);
});

it('still supports script tags with unknown types', async () => {
const document = await languageServer.openFakeDocument(
'<script type="something-else">const hello = "Hello, Astro!";</script>',
'astro'
);

const hoverInfo = await languageServer.handle.sendHoverRequest(
document.uri,
Position.create(0, 38)
);

expect(hoverInfo).to.not.be.undefined;
});

it('ignores is:raw script tags', async () => {
const document = await languageServer.openFakeDocument(
'<script is:raw>const hello = "Hello, Astro!";</script>',
'astro'
);

const hoverInfo = await languageServer.handle.sendHoverRequest(
document.uri,
Position.create(0, 38)
);

expect(hoverInfo).to.be.null;
});
});
5 changes: 2 additions & 3 deletions packages/language-server/test/units/parseJS.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@ describe('parseJS - Can find all the scripts in an Astro file', () => {
expect(scriptTags.length).to.equal(2);
});

// TODO: This will be outdated in the future, once we add JSON support
it('Ignore JSON scripts', () => {
it('Includes JSON scripts', () => {
const input = `<script type="application/json">{foo: "bar"}</script>`;
const { ranges } = astro2tsx(input, 'file.astro');
const scriptTags = extractScriptTags(ranges.scripts);

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

it('returns the proper capabilities for inline script tags', async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"dependencies": {
"@volar/language-core": "~2.4.0-alpha.15",
"@volar/typescript": "~2.4.0-alpha.15",
"@astrojs/compiler": "^2.7.0",
"@astrojs/compiler": "^2.10.0",
"@jridgewell/sourcemap-codec": "^1.4.15",
"semver": "^7.3.8",
"vscode-languageserver-textdocument": "^1.0.11"
Expand Down
2 changes: 1 addition & 1 deletion packages/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@
"vscode-tmgrammar-test": "^0.1.2"
},
"dependencies": {
"@astrojs/compiler": "^2.9.1",
"@astrojs/compiler": "^2.10.0",
"prettier": "^3.2.5",
"prettier-plugin-astro": "^0.14.1"
}
Expand Down
22 changes: 11 additions & 11 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit b65d6b4

Please sign in to comment.