Skip to content

Commit

Permalink
Content Collections Intellisense (#915)
Browse files Browse the repository at this point in the history
* feat: cc integration

* fix: clean up

* chore: remove unused package

* feat: support maps and seqs

* feat: do hybrid ts / json

* feat: apply schemas dynamically

* feat: flag it

* feat: references support

* chore: lockfile

* fix: merge conflicts

* fix: handle errors

* feat: cleanup ts output to not include values

* refactor: ts-plugin

* fix: remove unneeded param

* refactor: don't hardcode frontmatter holders

* refactor: use consistent languageIds

* test: add tests

* fix: disable codelenses properly

* fix: use file urls

* fix: atempt to debug windows

* fix: windooooows

* Revert "fix: atempt to debug windows"

This reverts commit 4b86b00.

* chore: changeset

* refactor: remove some unused code

* refactor: remove more unused code

* refactor: use same frontmatter extraction as Astro itself

* docs: add comments explaining uri transformations

* nit: adjust for feedback

* chore: lockfile

* chore: update Astro version
  • Loading branch information
Princesseuh authored Aug 15, 2024
1 parent 3a4d60b commit d624646
Show file tree
Hide file tree
Showing 37 changed files with 1,368 additions and 229 deletions.
8 changes: 8 additions & 0 deletions .changeset/two-bulldogs-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@astrojs/language-server": minor
"@astrojs/ts-plugin": minor
"@astrojs/yaml2ts": minor
"astro-vscode": minor
---

Adds support for Content Collection Intellisense
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ packages/vscode/meta.json

# do not commit .env files or any files that end with `.env`
*.env

**/.astro
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@changesets/cli": "^2.26.1",
"prettier": "^3.2.5",
"turbo": "1.10.2",
"typescript": "^5.2.2",
"typescript": "^5.5.4",
"eslint": "^9.8.0",
"typescript-eslint": "^8.0.1",
"eslint-plugin-regexp": "^2.6.0"
Expand Down
9 changes: 6 additions & 3 deletions packages/language-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "mocha --timeout 10000 --require tsx --require test/takedown.ts test/misc/init.test.ts test/**/*.test.ts",
"sync-fixture": "pnpm --dir ./test/fixture sync",
"test": "pnpm run sync-fixture && mocha --timeout 10000 --require tsx --require test/takedown.ts test/misc/init.test.ts test/**/*.test.ts",
"test:match": "pnpm run test -g"
},
"dependencies": {
"@astrojs/compiler": "^2.10.3",
"@astrojs/yaml2ts": "^0.1.0",
"@jridgewell/sourcemap-codec": "^1.4.15",
"@volar/kit": "~2.4.0-alpha.15",
"@volar/language-core": "~2.4.0-alpha.15",
Expand All @@ -41,6 +43,7 @@
"volar-service-prettier": "0.0.59",
"volar-service-typescript": "0.0.59",
"volar-service-typescript-twoslash-queries": "0.0.59",
"volar-service-yaml": "0.0.59",
"vscode-html-languageservice": "^5.2.0",
"vscode-uri": "^3.0.8"
},
Expand All @@ -51,12 +54,12 @@
"@types/mocha": "^10.0.1",
"@types/node": "^18.17.8",
"@volar/test-utils": "~2.4.0-alpha.15",
"astro": "^4.3.5",
"astro": "^4.14.0",
"chai": "^4.3.7",
"mocha": "^10.2.0",
"svelte": "^4.2.10",
"tsx": "^3.12.7",
"typescript": "^5.2.2",
"typescript": "^5.5.4",
"vscode-languageserver-protocol": "^3.17.5",
"vscode-languageserver-textdocument": "^1.0.11"
},
Expand Down
161 changes: 161 additions & 0 deletions packages/language-server/src/core/frontmatterHolders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { yaml2ts, VIRTUAL_CODE_ID } from '@astrojs/yaml2ts';
import {
type CodeMapping,
type LanguagePlugin,
type VirtualCode,
forEachEmbeddedCode,
} from '@volar/language-core';
import type ts from 'typescript';
import type { URI } from 'vscode-uri';

export const SUPPORTED_FRONTMATTER_EXTENSIONS = { md: 'markdown', mdx: 'mdx', mdoc: 'mdoc' };
export const SUPPORTED_FRONTMATTER_EXTENSIONS_KEYS = Object.keys(SUPPORTED_FRONTMATTER_EXTENSIONS);
const SUPPORTED_FRONTMATTER_EXTENSIONS_VALUES = Object.values(SUPPORTED_FRONTMATTER_EXTENSIONS);

export const frontmatterRE = /^---(.*?)^---/ms;

export type CollectionConfig = {
folder: URI;
config: {
collections: {
hasSchema: boolean;
name: string;
}[];
entries: Record<string, string>;
};
};

function getCollectionName(collectionConfigs: CollectionConfig[], fileURI: string) {
for (const collection of collectionConfigs) {
if (collection.config.entries[fileURI]) {
return collection.config.entries[fileURI];
}
}
}

export function getFrontmatterLanguagePlugin(
collectionConfigs: CollectionConfig[],
): LanguagePlugin<URI, FrontmatterHolder> {
return {
getLanguageId(scriptId) {
const fileType = SUPPORTED_FRONTMATTER_EXTENSIONS_KEYS.find((ext) =>
scriptId.path.endsWith(`.${ext}`),
);

if (fileType) {
return SUPPORTED_FRONTMATTER_EXTENSIONS[
fileType as keyof typeof SUPPORTED_FRONTMATTER_EXTENSIONS
];
}
},
createVirtualCode(scriptId, languageId, snapshot) {
if (SUPPORTED_FRONTMATTER_EXTENSIONS_VALUES.includes(languageId)) {
return new FrontmatterHolder(
scriptId.fsPath.replace(/\\/g, '/'),
languageId,
snapshot,
getCollectionName(
collectionConfigs,
// The scriptId here is encoded and somewhat normalized, as such we can't use it directly to compare with
// the file URLs in the collection config entries that Astro generates.
decodeURIComponent(scriptId.toString()).toLowerCase(),
),
);
}
},
typescript: {
extraFileExtensions: SUPPORTED_FRONTMATTER_EXTENSIONS_KEYS.map((ext) => ({
extension: ext,
isMixedContent: true,
scriptKind: 7 satisfies ts.ScriptKind.Deferred,
})),
getServiceScript(astroCode) {
for (const code of forEachEmbeddedCode(astroCode)) {
if (code.id === VIRTUAL_CODE_ID) {
return {
code,
extension: '.ts',
scriptKind: 3 satisfies ts.ScriptKind.TS,
};
}
}
return undefined;
},
},
};
}

export class FrontmatterHolder implements VirtualCode {
id = 'frontmatter-holder';
mappings: CodeMapping[];
embeddedCodes: VirtualCode[];
public hasFrontmatter = false;

constructor(
public fileName: string,
public languageId: string,
public snapshot: ts.IScriptSnapshot,
public collection: string | undefined,
) {
this.mappings = [
{
sourceOffsets: [0],
generatedOffsets: [0],
lengths: [this.snapshot.getLength()],
data: {
verification: true,
completion: true,
semantic: true,
navigation: true,
structure: true,
format: true,
},
},
];

this.embeddedCodes = [];
this.snapshot = snapshot;

// If the file is not part of a collection, we don't need to do anything
if (!this.collection) {
return;
}

const frontmatterContent =
frontmatterRE
.exec(this.snapshot.getText(0, this.snapshot.getLength()))?.[0]
.replaceAll('---', ' ') ?? '';

this.hasFrontmatter = frontmatterContent.length > 0;

this.embeddedCodes.push({
id: `yaml_frontmatter_${this.collection}`,
languageId: 'yaml',
snapshot: {
getText: (start, end) => frontmatterContent.substring(start, end),
getLength: () => frontmatterContent.length,
getChangeRange: () => undefined,
},
mappings: [
{
sourceOffsets: [0],
generatedOffsets: [0],
lengths: [frontmatterContent.length],
data: {
verification: true,
completion: true,
semantic: true,
navigation: true,
structure: true,
format: false,
},
},
],
});

if (this.hasFrontmatter) {
const yaml2tsResult = yaml2ts(frontmatterContent, this.collection);
this.embeddedCodes.push(yaml2tsResult.virtualCode);
}
}
}
33 changes: 26 additions & 7 deletions packages/language-server/src/languageServerPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type {
Connection,
LanguagePlugin,
LanguageServiceEnvironment,
import {
MessageType,
ShowMessageNotification,
type Connection,
type LanguagePlugin,
type LanguageServiceEnvironment,
} from '@volar/language-server/node';
import { MessageType, ShowMessageNotification } from '@volar/language-server/node';
import { URI } from 'vscode-uri';
import { getAstroLanguagePlugin } from './core';
import { getSvelteLanguagePlugin } from './core/svelte.js';
Expand All @@ -17,16 +18,19 @@ import { create as createEmmetService } from 'volar-service-emmet';
import { create as createPrettierService } from 'volar-service-prettier';
import { create as createTypeScriptTwoSlashService } from 'volar-service-typescript-twoslash-queries';

import { type CollectionConfig, getFrontmatterLanguagePlugin } from './core/frontmatterHolders.js';
import { create as createAstroService } from './plugins/astro.js';
import { create as createHtmlService } from './plugins/html.js';
import { create as createTypescriptAddonsService } from './plugins/typescript-addons/index.js';
import { create as createTypeScriptServices } from './plugins/typescript/index.js';
import { create as createYAMLService } from './plugins/yaml.js';

export function getLanguagePlugins(
connection: Connection,
ts: typeof import('typescript'),
serviceEnv: LanguageServiceEnvironment,
tsconfig: string | undefined,
collectionConfigs: CollectionConfig[],
) {
const languagePlugins: LanguagePlugin<URI>[] = [
getVueLanguagePlugin(),
Expand All @@ -50,15 +54,23 @@ export function getLanguagePlugins(
});
}

if (collectionConfigs.length) {
languagePlugins.push(getFrontmatterLanguagePlugin(collectionConfigs));
}

languagePlugins.unshift(
getAstroLanguagePlugin(typeof astroInstall === 'string' ? undefined : astroInstall, ts),
);

return languagePlugins;
}

export function getLanguageServicePlugins(connection: Connection, ts: typeof import('typescript')) {
return [
export function getLanguageServicePlugins(
connection: Connection,
ts: typeof import('typescript'),
collectionConfigs: CollectionConfig[],
) {
const LanguageServicePlugins = [
createHtmlService(),
createCssService(),
createEmmetService(),
Expand All @@ -68,6 +80,13 @@ export function getLanguageServicePlugins(connection: Connection, ts: typeof imp
createAstroService(ts),
getPrettierService(),
];

if (collectionConfigs.length) {
LanguageServicePlugins.push(createYAMLService(collectionConfigs));
}

return LanguageServicePlugins;

function getPrettierService() {
let prettier: ReturnType<typeof importPrettier>;
let prettierPluginPath: ReturnType<typeof getPrettierPluginPath>;
Expand Down
Loading

0 comments on commit d624646

Please sign in to comment.