Skip to content
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
7 changes: 4 additions & 3 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,13 @@ Glint implements two VirtualCodes in order to provide Language Tooling for moder
- Ideally every Ember app will migrate to .gts but we still need to support classic components until that time comes
- The backing .ts file is already type-checkable on its own, but for type-checking an .hbs file:
- This Virtual Code will combine the .hbs+.ts and generate a singular type-checkable TS file in a similar mapping structure as `VirtualGtsCode` mentioned above
Any VirtualCode, whether implemented by Glint or Vue tooling, essentially takes a language that embeds other languages (.gts is TS with embedded Glimmer, .vue has `<script>` + `<template>` + `<style>` tags, etc.) and produces a tree of embedded codes where the "leaves" of that tree are simpler, single-language codes (that don't include any other embedded languages). These single-language "leaves" can then be used as inputs for a variety of Language Services (see below), either ones already provided by Volar or custom ones implemented by Glint.

Any VirtualCode, whether implemented by Glint or Vue tooling, essentially takes a language that embeds other languages (.gts is TS with embedded Glimmer, .vue has `<script>` +`<template>`+`<style>` tags, etc), and produces a tree of embedded codes where the "leaves" of that tree are simpler, single-language codes (that don't include any other embedded languages). These single-language "leaves" can then be used as inputs for a variety of Language Services, either ones already provided by Volar or custom ones implemented by Glint.
## Core Primitive: Language Service

Language Services operate on singular languages, e.g. a Language Service could be used to implement code completions in HTML, or to provide formatting in CSS, etc. Volar makes it possible/easy for Language Services to operate on the single-language "leaves" of your VirtualCode tree, and then, using the source maps that were built up as part of the VirtualCode implementation, reverse-source map those transformations, diagnostics, code hints, etc, back to the source file.
Language Services operate on singular languages; for example, a Language Service could be used to implement code completions in HTML or provide formatting in CSS. Volar makes it possible and easy for Language Services to operate on the single-language "leaves" of your VirtualCode tree, and then, using the source maps that were built up as part of the VirtualCode implementation, reverse-source map those transformations, diagnostics, code hints, etc., back to the source file.

NOTE: at the time of writing, I don't believe Glint is currently making use of Language Services due to the fact that Glint primarily has been concerned with providing TS functionality to Ember/Glimmer templates, and most of that logic has been shifted towards the Glint TS Plugin (described below). If it is decided to enhance Glint functionality, or perhaps merge it into Ember Language Server (something that has been discussed), then likely more of the Language Service framework/infrastructure will be used.
Volar.js maintains a number of [shared Language Plugins](https://github.com/volarjs/services/tree/master/packages) that can be consumed by many different language tooling systems (Vue, MDX, Astro, Glint, etc.). For example, while Glint uses a TS Plugin for diagnostics, there are some TS features that still require using the Volar TS LanguageService. For instance, to provide Symbols (which drive the Outline panel in VSCode, among other things), Glint uses a lightweight syntax-only TS Language Service provided by Volar.

## Glint V2 Moves Type-Checking Features out from Language Server and into TS Plugin

Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"silent-error": "^1.1.1",
"uuid": "^8.3.2",
"volar-service-typescript": "volar-2.4",
"volar-service-html": "volar-2.4",
"vscode-languageserver-protocol": "^3.17.5",
"vscode-languageserver-textdocument": "^1.0.5",
"vscode-uri": "^3.0.8",
Expand Down
197 changes: 197 additions & 0 deletions packages/core/src/plugins/gts-gjs-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import type { LanguageServiceContext, LanguageServicePlugin } from '@volar/language-service';
import { VirtualGtsCode } from '../volar/gts-virtual-code.js';
import type * as vscode from 'vscode-languageserver-protocol';
import type { TextDocument } from 'vscode-languageserver-textdocument';
import { URI } from 'vscode-uri';

/**
* This is a LanguageServicePlugin that provides language features for the top-level .gts/.gjs files.
* For now, this just provides document symbols for `<template>` tags, which are a language
* construct specific to .gts/.gjs files. Note that .gts/.gjs files will have TypeScript symbols
* provided by our syntactic TS LanguageServicePlugin configured elsewhere, and these will be
* combined with the symbols provided here.
*/
export function create(): LanguageServicePlugin {
return {
name: 'gts-gjs',
capabilities: {
documentSymbolProvider: true,
},
create(context) {
return {
provideDocumentSymbols(document) {
return worker(document, context, (root) => {
const result: vscode.DocumentSymbol[] = [];
const { transformedModule } = root;

if (transformedModule) {
const templateSymbols = transformedModule.templateSymbols();
for (const templateSymbol of templateSymbols) {
result.push({
name: 'template',
kind: 2 satisfies typeof vscode.SymbolKind.Module,
range: {
start: document.positionAt(templateSymbol.start),
end: document.positionAt(templateSymbol.end),
},
selectionRange: {
start: document.positionAt(templateSymbol.start),
end: document.positionAt(templateSymbol.startTagEnd),
},
});
}
}

return result;
});
},

// TODO: this is copied from Vue; this might be the proper way for surfacing Glimmer syntax parsing errors to the top-level .gts file.
// provideDiagnostics(document, token) {
// return worker(document, context, async (root) => {
// const { vueSfc, sfc } = root;
// if (!vueSfc) {
// return;
// }

// const originalResult = await htmlServiceInstance.provideDiagnostics?.(document, token);
// const sfcErrors: vscode.Diagnostic[] = [];
// const { template } = sfc;

// const { startTagEnd = Infinity, endTagStart = -Infinity } = template ?? {};

// for (const error of vueSfc.errors) {
// if ('code' in error) {
// const start = error.loc?.start.offset ?? 0;
// const end = error.loc?.end.offset ?? 0;
// if (end < startTagEnd || start >= endTagStart) {
// sfcErrors.push({
// range: {
// start: document.positionAt(start),
// end: document.positionAt(end),
// },
// severity: 1 satisfies typeof vscode.DiagnosticSeverity.Error,
// code: error.code,
// source: 'vue',
// message: error.message,
// });
// }
// }
// }

// return [...(originalResult ?? []), ...sfcErrors];
// });
// },

// TODO: this is copied from Vue; this might be a good place to implement auto-completing
// the `<template>` tag and other top-level concerns for .gts files

// async provideCompletionItems(document, position, context, token) {
// const result = await htmlServiceInstance.provideCompletionItems?.(
// document,
// position,
// context,
// token,
// );
// if (!result) {
// return;
// }
// result.items = result.items.filter(
// (item) =>
// item.label !== '!DOCTYPE' && item.label !== 'Custom Blocks' && item.label !== 'data-',
// );

// const tags = sfcDataProvider?.provideTags();

// const scriptLangs = getLangs('script');
// const scriptItems = result.items.filter(
// (item) => item.label === 'script' || item.label === 'script setup',
// );
// for (const scriptItem of scriptItems) {
// scriptItem.kind = 17 satisfies typeof vscode.CompletionItemKind.File;
// scriptItem.detail = '.js';
// for (const lang of scriptLangs) {
// result.items.push({
// ...scriptItem,
// detail: `.${lang}`,
// kind: 17 satisfies typeof vscode.CompletionItemKind.File,
// label: scriptItem.label + ' lang="' + lang + '"',
// textEdit: scriptItem.textEdit
// ? {
// ...scriptItem.textEdit,
// newText: scriptItem.textEdit.newText + ' lang="' + lang + '"',
// }
// : undefined,
// });
// }
// }

// const styleLangs = getLangs('style');
// const styleItem = result.items.find((item) => item.label === 'style');
// if (styleItem) {
// styleItem.kind = 17 satisfies typeof vscode.CompletionItemKind.File;
// styleItem.detail = '.css';
// for (const lang of styleLangs) {
// result.items.push(
// getStyleCompletionItem(styleItem, lang),
// getStyleCompletionItem(styleItem, lang, 'scoped'),
// getStyleCompletionItem(styleItem, lang, 'module'),
// );
// }
// }

// const templateLangs = getLangs('template');
// const templateItem = result.items.find((item) => item.label === 'template');
// if (templateItem) {
// templateItem.kind = 17 satisfies typeof vscode.CompletionItemKind.File;
// templateItem.detail = '.html';
// for (const lang of templateLangs) {
// if (lang === 'html') {
// continue;
// }
// result.items.push({
// ...templateItem,
// kind: 17 satisfies typeof vscode.CompletionItemKind.File,
// detail: `.${lang}`,
// label: templateItem.label + ' lang="' + lang + '"',
// textEdit: templateItem.textEdit
// ? {
// ...templateItem.textEdit,
// newText: templateItem.textEdit.newText + ' lang="' + lang + '"',
// }
// : undefined,
// });
// }
// }
// return result;

// function getLangs(label: string) {
// return (
// tags
// ?.find((tag) => tag.name === label)
// ?.attributes.find((attr) => attr.name === 'lang')
// ?.values?.map(({ name }) => name) ?? []
// );
// }
// },
};
},
};

function worker<T>(
document: TextDocument,
context: LanguageServiceContext,
callback: (root: VirtualGtsCode) => T,
): T | undefined {
if (document.languageId !== 'glimmer-ts' && document.languageId !== 'glimmer-js') {
return;
}
const uri = URI.parse(document.uri);
const decoded = context.decodeEmbeddedDocumentUri(uri);
const sourceScript = decoded && context.language.scripts.get(decoded[0]);
const root = sourceScript?.generated?.root;
if (root instanceof VirtualGtsCode) {
return callback(root);
}
}
}
30 changes: 28 additions & 2 deletions packages/core/src/transform/template/transformed-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ export type SourceFile = {
contents: string;
};

export type TemplateSymbol = {
start: number;
end: number;
startTagEnd: number;
};

/**
* This class represents the result of transforming a TypeScript
* module with one or more embedded HBS templates. It contains
Expand All @@ -61,7 +67,7 @@ export default class TransformedModule {
public readonly transformedContents: string,
public readonly errors: ReadonlyArray<TransformError>,
public readonly directives: ReadonlyArray<Directive>,
private readonly correlatedSpans: Array<CorrelatedSpan>,
public readonly correlatedSpans: Array<CorrelatedSpan>,
) {}

public toDebugString(): string {
Expand Down Expand Up @@ -334,11 +340,31 @@ export default class TransformedModule {
// TODO: audit usage of `codeFeatures.all` here: https://github.com/typed-ember/glint/issues/769
// There are cases where we need to be disabling certain features to prevent, e.g., navigation
// that targets an "in-between" piece of generated code.
push(span.originalStart, span.transformedStart, span.originalLength, codeFeatures.all);
push(span.originalStart, span.transformedStart, span.originalLength, {
...codeFeatures.all,

// This enables symbol/outline info for the transformed TS to appear in for the .gts file.
structure: true,
});
}
}
});

return codeMappings;
}

templateSymbols(): Array<TemplateSymbol> {
const result: Array<TemplateSymbol> = [];
this.correlatedSpans.forEach((span) => {
if (span.glimmerAstMapping) {
// This is a template span
result.push({
start: span.originalStart,
end: span.originalStart + span.originalLength,
startTagEnd: span.originalStart + span.originalLength,
});
}
});
return result;
}
}
45 changes: 44 additions & 1 deletion packages/core/src/volar/gts-virtual-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export class VirtualGtsCode implements VirtualCode {
// .gts file has embedded templates, so lets generate a new embedded code
// that contains the transformed TS code.
const mappings = transformedModule.toVolarMappings();
this.embeddedCodes = [
const embeddedCodes = [
{
embeddedCodes: [],
id: 'ts',
Expand All @@ -155,6 +155,49 @@ export class VirtualGtsCode implements VirtualCode {
directives: transformedModule.directives,
},
];

// Add an embedded code for each <template> tag in the .gts file
// so that the HTML Language Service can kick in and provide symbols
// and other functionality.
transformedModule.correlatedSpans.forEach((span, index) => {
if (!span.glimmerAstMapping) {
return;
}

const openTemplateTagLength = 10; // "<template>"
const closeTemplateTagLength = 11; // "</template>"

embeddedCodes.push({
embeddedCodes: [],
id: `template_html_${index}`,
languageId: 'html', // technically this is 'handlebars' but 'html' causes the HTML Language Service to kick in
mappings: [
{
sourceOffsets: [span.originalStart + openTemplateTagLength],
generatedOffsets: [0],
lengths: [span.originalLength - openTemplateTagLength - closeTemplateTagLength],

data: {
completion: true,
format: true,
navigation: true,
semantic: true,
structure: true,
verification: false,
} satisfies CodeInformation,
},
],
snapshot: new ScriptSnapshot(
contents.slice(
span.originalStart + openTemplateTagLength,
span.originalStart + span.originalLength - closeTemplateTagLength,
),
),
directives: [],
});
});

this.embeddedCodes = embeddedCodes;
} else {
// Null transformed module means there's no embedded HBS templates,
// so just return a full "no-op" mapping from source to transformed.
Expand Down
Loading
Loading