diff --git a/packages/core/__tests__/cli/check.test.ts b/packages/core/__tests__/cli/check.test.ts
index ece8ac78e..4e2b2b69f 100644
--- a/packages/core/__tests__/cli/check.test.ts
+++ b/packages/core/__tests__/cli/check.test.ts
@@ -200,9 +200,8 @@ describe('CLI: single-pass typechecking', () => {
let checkResult = await project.check({ reject: false });
- expect(checkResult.exitCode).toBe(1);
- expect(checkResult.stdout).toEqual('');
- expect(stripAnsi(checkResult.stderr)).toMatchInlineSnapshot(`
+ expect(checkResult.exitCode).not.toBe(0);
+ expect(stripAnsi(checkResult.stdout)).toMatchInlineSnapshot(`
"my-component.hbs:1:22 - error TS2551: Property 'targett' does not exist on type 'MyComponent'. Did you mean 'target'?
1 {{@message}}, {{this.targett}}
diff --git a/packages/core/__tests__/language-server/completions.test.ts b/packages/core/__tests__/language-server/completions.test.ts
index bc45b39ad..e7e321402 100644
--- a/packages/core/__tests__/language-server/completions.test.ts
+++ b/packages/core/__tests__/language-server/completions.test.ts
@@ -19,16 +19,14 @@ describe('Language Server: Completions', () => {
project.write('index.hbs', '');
let server = await project.startLanguageServer();
- let completions = server.getCompletions(project.fileURI('index.hbs'), {
- line: 0,
- character: 6,
- });
+ const { uri } = await server.openTextDocument(project.filePath('index.hbs'), 'handlebars');
+ let completions = await server.sendCompletionRequest(uri, Position.create(0, 6));
- let completion = completions?.find((item) => item.label === 'LinkTo');
+ let completion = completions?.items.find((item) => item.label === 'LinkTo');
expect(completion?.kind).toEqual(CompletionItemKind.Field);
- let details = server.getCompletionDetails(completion!);
+ let details = await server.sendCompletionResolveRequest(completion!);
expect(details.detail).toEqual('(property) Globals.LinkTo: LinkToComponent');
});
@@ -65,10 +63,8 @@ describe('Language Server: Completions', () => {
project.write('index.hbs', code);
let server = await project.startLanguageServer();
- let completions = server.getCompletions(project.fileURI('index.hbs'), {
- line: 0,
- character: 4,
- });
+ const { uri } = await server.openTextDocument(project.filePath('index.hbs'), 'handlebars');
+ let completions = await server.sendCompletionRequest(uri, Position.create(0, 4));
// Ensure we don't spew all ~900 completions available at the top level
// in module scope in a JS/TS file.
diff --git a/packages/core/__tests__/language-server/diagnostic-augmentation.test.ts b/packages/core/__tests__/language-server/diagnostic-augmentation.test.ts
index 8ae1cb791..2d2529f8c 100644
--- a/packages/core/__tests__/language-server/diagnostic-augmentation.test.ts
+++ b/packages/core/__tests__/language-server/diagnostic-augmentation.test.ts
@@ -940,7 +940,8 @@ describe('Language Server: Diagnostic Augmentation', () => {
});
let server = await project.startLanguageServer();
- let diagnostics = server.getDiagnostics(project.fileURI('index.hbs'));
+ const { uri } = await server.openTextDocument(project.filePath('index.hbs'), 'handlebars');
+ let diagnostics = await server.sendDocumentDiagnosticRequest(uri);
expect(diagnostics.items.reverse()).toMatchInlineSnapshot(`
[
diff --git a/packages/core/src/cli/run-volar-tsc.ts b/packages/core/src/cli/run-volar-tsc.ts
index 9a78f43c7..79698182a 100644
--- a/packages/core/src/cli/run-volar-tsc.ts
+++ b/packages/core/src/cli/run-volar-tsc.ts
@@ -1,5 +1,5 @@
import { runTsc } from '@volar/typescript/lib/quickstart/runTsc.js';
-import { createGtsLanguagePlugin } from '../volar/gts-language-plugin.js';
+import { createEmberLanguagePlugin } from '../volar/ember-language-plugin.js';
import { findConfig } from '../config/index.js';
import { createRequire } from 'node:module';
@@ -29,7 +29,7 @@ export function run(): void {
// not sure whether it's better to be lenient, but we were getting test failures
// on environment-ember-loose's `yarn run test`.
if (glintConfig) {
- const gtsLanguagePlugin = createGtsLanguagePlugin(glintConfig);
+ const gtsLanguagePlugin = createEmberLanguagePlugin(glintConfig);
return [gtsLanguagePlugin];
} else {
return [];
diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts
index bc98fa028..c3545102a 100644
--- a/packages/core/src/config/config.ts
+++ b/packages/core/src/config/config.ts
@@ -13,8 +13,6 @@ export class GlintConfig {
public readonly environment: GlintEnvironment;
public readonly checkStandaloneTemplates: boolean;
- private extensions: Array;
-
public constructor(
ts: typeof import('typescript'),
configPath: string,
@@ -25,32 +23,6 @@ export class GlintConfig {
this.rootDir = path.dirname(configPath);
this.environment = GlintEnvironment.load(config.environment, { rootDir: this.rootDir });
this.checkStandaloneTemplates = config.checkStandaloneTemplates ?? true;
- this.extensions = this.environment.getConfiguredFileExtensions();
- }
-
- /**
- * Indicates whether this configuration object applies to the file at the
- * given path.
- */
- public includesFile(rawFileName: string): boolean {
- return this.extensions.some((ext) => rawFileName.endsWith(ext));
- }
-
- // Given the path of a template or script (potentially with a custom extension),
- // returns the corresponding .js or .ts path we present to the TS language service.
- public getSynthesizedScriptPathForTS(filename: string): string {
- let extension = path.extname(filename);
- let filenameWithoutExtension = filename.slice(0, filename.lastIndexOf(extension));
- switch (this.environment.getSourceKind(filename)) {
- case 'template':
- return `${filenameWithoutExtension}${this.checkStandaloneTemplates ? '.ts' : '.js'}`;
- case 'typed-script':
- return `${filenameWithoutExtension}.ts`;
- case 'untyped-script':
- return `${filenameWithoutExtension}.js`;
- default:
- return filename;
- }
}
}
diff --git a/packages/core/src/transform/diagnostics/augmentation.ts b/packages/core/src/transform/diagnostics/augmentation.ts
index 327e405c6..42cf878cb 100644
--- a/packages/core/src/transform/diagnostics/augmentation.ts
+++ b/packages/core/src/transform/diagnostics/augmentation.ts
@@ -1,6 +1,6 @@
import type ts from 'typescript';
import { Diagnostic } from './index.js';
-import MappingTree, { MappingSource } from '../template/mapping-tree.js';
+import GlimmerASTMappingTree, { MappingSource } from '../template/glimmer-ast-mapping-tree.js';
/**
* Given a diagnostic and a mapping tree node corresponding to its location,
@@ -9,17 +9,20 @@ import MappingTree, { MappingSource } from '../template/mapping-tree.js';
*/
export function augmentDiagnostic(
diagnostic: T,
- mappingForDiagnostic: (diagnostic: T) => MappingTree | null,
+ mappingForDiagnostic: (diagnostic: T) => GlimmerASTMappingTree | null,
): T {
// TODO: fix any types, remove casting
return rewriteMessageText(diagnostic, mappingForDiagnostic as any) as T;
}
-type DiagnosticHandler = (diagnostic: Diagnostic, mapping: MappingTree) => Diagnostic | undefined;
+type DiagnosticHandler = (
+ diagnostic: Diagnostic,
+ mapping: GlimmerASTMappingTree,
+) => Diagnostic | undefined;
function rewriteMessageText(
diagnostic: Diagnostic,
- mappingGetter: (diagnostic: Diagnostic) => MappingTree | null,
+ mappingGetter: (diagnostic: Diagnostic) => GlimmerASTMappingTree | null,
): Diagnostic {
const handler = diagnosticHandlers[diagnostic.code?.toString() ?? ''];
if (!handler) {
@@ -48,7 +51,7 @@ const bindHelpers = ['component', 'helper', 'modifier'];
function checkAssignabilityError(
diagnostic: Diagnostic,
- mapping: MappingTree,
+ mapping: GlimmerASTMappingTree,
): Diagnostic | undefined {
let node = mapping.sourceNode;
let parentNode = mapping.parent?.sourceNode;
@@ -123,7 +126,7 @@ function checkAssignabilityError(
function noteNamedArgsAffectArity(
diagnostic: Diagnostic,
- mapping: MappingTree,
+ mapping: GlimmerASTMappingTree,
): Diagnostic | undefined {
// In normal template entity invocations, named args (if specified) are effectively
// passed as the final positional argument. Because of this, the reported "expected
@@ -153,7 +156,10 @@ function noteNamedArgsAffectArity(
}
}
-function checkResolveError(diagnostic: Diagnostic, mapping: MappingTree): Diagnostic | undefined {
+function checkResolveError(
+ diagnostic: Diagnostic,
+ mapping: GlimmerASTMappingTree,
+): Diagnostic | undefined {
// The diagnostic might fall on a lone identifier or a full path; if the former,
// we need to traverse up through the path to find the true parent.
let sourceMapping = mapping.sourceNode.type === 'Identifier' ? mapping.parent : mapping;
@@ -199,7 +205,7 @@ function checkResolveError(diagnostic: Diagnostic, mapping: MappingTree): Diagno
function checkImplicitAnyError(
diagnostic: Diagnostic,
- mapping: MappingTree,
+ mapping: GlimmerASTMappingTree,
): Diagnostic | undefined {
let message = diagnostic.message;
@@ -229,7 +235,7 @@ function checkImplicitAnyError(
function checkIndexAccessError(
diagnostic: Diagnostic,
- mapping: MappingTree,
+ mapping: GlimmerASTMappingTree,
): Diagnostic | undefined {
if (mapping.sourceNode.type === 'Identifier') {
let message = diagnostic.message;
@@ -252,10 +258,10 @@ function addGlintDetails(diagnostic: Diagnostic, details: string): Diagnostic {
// Find the nearest mapping node at or above the given one whose `source` AST node
// matches one of the given types.
function findAncestor(
- mapping: MappingTree,
+ mapping: GlimmerASTMappingTree,
...types: Array
): Extract | null {
- let current: MappingTree | null = mapping;
+ let current: GlimmerASTMappingTree | null = mapping;
do {
if (types.includes(current.sourceNode.type as K)) {
return current.sourceNode as Extract;
diff --git a/packages/core/src/transform/template/mapping-tree.ts b/packages/core/src/transform/template/glimmer-ast-mapping-tree.ts
similarity index 93%
rename from packages/core/src/transform/template/mapping-tree.ts
rename to packages/core/src/transform/template/glimmer-ast-mapping-tree.ts
index 57f41f6a4..ba3f0f025 100644
--- a/packages/core/src/transform/template/mapping-tree.ts
+++ b/packages/core/src/transform/template/glimmer-ast-mapping-tree.ts
@@ -48,13 +48,13 @@ export class TemplateEmbedding {
* level of granularity as TS itself uses when reporting on the transformed
* output.
*/
-export default class MappingTree {
- public parent: MappingTree | null = null;
+export default class GlimmerASTMappingTree {
+ public parent: GlimmerASTMappingTree | null = null;
public constructor(
public transformedRange: Range,
public originalRange: Range,
- public children: Array = [],
+ public children: Array = [],
public sourceNode: MappingSource,
) {
children.forEach((child) => (child.parent = this));
@@ -65,7 +65,7 @@ export default class MappingTree {
* that contains the given range, or `null` if that range doesn't fall within
* this mapping tree.
*/
- public narrowestMappingForTransformedRange(range: Range): MappingTree | null {
+ public narrowestMappingForTransformedRange(range: Range): GlimmerASTMappingTree | null {
if (range.start < this.transformedRange.start || range.end > this.transformedRange.end) {
return null;
}
@@ -85,7 +85,7 @@ export default class MappingTree {
* that contains the given range, or `null` if that range doesn't fall within
* this mapping tree.
*/
- public narrowestMappingForOriginalRange(range: Range): MappingTree | null {
+ public narrowestMappingForOriginalRange(range: Range): GlimmerASTMappingTree | null {
if (range.start < this.originalRange.start || range.end > this.originalRange.end) {
return null;
}
diff --git a/packages/core/src/transform/template/inlining/companion-file.ts b/packages/core/src/transform/template/inlining/companion-file.ts
index 4417b84e1..3ea14bcef 100644
--- a/packages/core/src/transform/template/inlining/companion-file.ts
+++ b/packages/core/src/transform/template/inlining/companion-file.ts
@@ -3,7 +3,7 @@ import type ts from 'typescript';
import { GlintEnvironment } from '../../../config/index.js';
import { CorrelatedSpansResult, isEmbeddedInClass, PartialCorrelatedSpan } from './index.js';
import { RewriteResult } from '../map-template-contents.js';
-import MappingTree, { ParseError } from '../mapping-tree.js';
+import GlimmerASTMappingTree, { ParseError } from '../glimmer-ast-mapping-tree.js';
import { templateToTypescript } from '../template-to-typescript.js';
import { Directive, SourceFile, TransformError } from '../transformed-module.js';
import { TSLib } from '../../util.js';
@@ -113,7 +113,7 @@ export function calculateCompanionTemplateSpans(
originalLength: template.contents.length,
insertionPoint: options.insertionPoint,
transformedSource: transformedTemplate.result.code,
- mapping: transformedTemplate.result.mapping,
+ glimmerAstMapping: transformedTemplate.result.mapping,
},
{
originalFile: template,
@@ -124,7 +124,7 @@ export function calculateCompanionTemplateSpans(
},
);
} else {
- let mapping = new MappingTree(
+ let mapping = new GlimmerASTMappingTree(
{ start: 0, end: 0 },
{ start: 0, end: template.contents.length },
[],
@@ -137,12 +137,19 @@ export function calculateCompanionTemplateSpans(
originalLength: template.contents.length,
insertionPoint: options.insertionPoint,
transformedSource: '',
- mapping,
+ glimmerAstMapping: mapping,
});
}
}
}
+/**
+ * Find and return the TS AST node which can serve as a proper insertion point
+ * for the transformed template code, which is:
+ *
+ * - The default export class declaration
+ * - a named export that matches a class declaration
+ */
function findCompanionTemplateTarget(
ts: TSLib,
sourceFile: ts.SourceFile,
@@ -155,6 +162,7 @@ function findCompanionTemplateTarget(
mods?.some((mod) => mod.kind === ts.SyntaxKind.DefaultKeyword) &&
mods.some((mod) => mod.kind === ts.SyntaxKind.ExportKeyword)
) {
+ // We've found a `export default class` statement; return it.
return statement;
}
@@ -164,6 +172,8 @@ function findCompanionTemplateTarget(
}
}
+ // We didn't find a default export, but maybe there is a named export that
+ // matches one of the class statements we found above.
for (let statement of sourceFile.statements) {
if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
if (ts.isIdentifier(statement.expression) && statement.expression.text in classes) {
diff --git a/packages/core/src/transform/template/inlining/tagged-strings.ts b/packages/core/src/transform/template/inlining/tagged-strings.ts
index 128834837..f4ead45c2 100644
--- a/packages/core/src/transform/template/inlining/tagged-strings.ts
+++ b/packages/core/src/transform/template/inlining/tagged-strings.ts
@@ -106,7 +106,7 @@ export function calculateTaggedTemplateSpans(
originalLength: templateLocation.end - templateLocation.start,
insertionPoint: templateLocation.start,
transformedSource: transformedTemplate.result.code,
- mapping: transformedTemplate.result.mapping,
+ glimmerAstMapping: transformedTemplate.result.mapping,
});
}
}
diff --git a/packages/core/src/transform/template/map-template-contents.ts b/packages/core/src/transform/template/map-template-contents.ts
index 10aebdb85..546a6703f 100644
--- a/packages/core/src/transform/template/map-template-contents.ts
+++ b/packages/core/src/transform/template/map-template-contents.ts
@@ -1,5 +1,8 @@
import { AST, preprocess } from '@glimmer/syntax';
-import MappingTree, { MappingSource, TemplateEmbedding } from './mapping-tree.js';
+import GlimmerASTMappingTree, {
+ MappingSource,
+ TemplateEmbedding,
+} from './glimmer-ast-mapping-tree.js';
import { Directive, DirectiveKind, Range } from './transformed-module.js';
import { assert } from '../util.js';
@@ -101,7 +104,7 @@ export type RewriteResult = {
result?: {
code: string;
directives: Array;
- mapping: MappingTree;
+ mapping: GlimmerASTMappingTree;
};
};
@@ -121,7 +124,8 @@ export type MapTemplateContentsOptions = {
};
/**
- * Given the text of an embedded template, invokes the given callback
+ * Given the text of a handlebars template (either standalone .hbs file, or the contents
+ * of an embedded `...` within a .gts file), invokes the given callback
* with a set of tools to emit mapped contents corresponding to
* that template, tracking the text emitted in order to provide
* a mapping of ranges in the input to ranges in the output.
@@ -162,7 +166,7 @@ export function mapTemplateContents(
});
let segmentsStack: string[][] = [[]];
- let mappingsStack: MappingTree[][] = [[]];
+ let mappingsStack: GlimmerASTMappingTree[][] = [[]];
let indent = '';
let offset = 0;
let needsIndent = false;
@@ -180,7 +184,7 @@ export function mapTemplateContents(
callback: () => void,
): void => {
let start = offset;
- let mappings: MappingTree[] = [];
+ let mappings: GlimmerASTMappingTree[] = [];
let segments: string[] = [];
segmentsStack.unshift(segments);
@@ -201,7 +205,7 @@ export function mapTemplateContents(
let end = offset;
let tsRange = { start, end };
- mappingsStack[0].push(new MappingTree(tsRange, hbsRange, mappings, source));
+ mappingsStack[0].push(new GlimmerASTMappingTree(tsRange, hbsRange, mappings, source));
segmentsStack[0].push(...segments);
}
};
@@ -270,7 +274,7 @@ export function mapTemplateContents(
assert(segmentsStack.length === 1);
let code = segmentsStack[0].join('');
- let mapping = new MappingTree(
+ let mapping = new GlimmerASTMappingTree(
{ start: 0, end: code.length },
{
start: 0,
diff --git a/packages/core/src/transform/template/rewrite-module.ts b/packages/core/src/transform/template/rewrite-module.ts
index b3488083f..bd9f8b149 100644
--- a/packages/core/src/transform/template/rewrite-module.ts
+++ b/packages/core/src/transform/template/rewrite-module.ts
@@ -54,7 +54,7 @@ export function rewriteModule(
/**
* Locates any embedded templates in the given AST and returns a corresponding
- * `PartialReplacedSpan` for each, as well as any errors encountered. These
+ * `PartialCorrelatedSpan` for each, as well as any errors encountered. These
* spans are then used in `rewriteModule` above to calculate the full set of
* source-to-source location information as well as the final transformed source
* string.
@@ -106,6 +106,12 @@ function calculateCorrelatedSpans(
ts.transform(ast, [
(context) =>
function visit(node: T): T {
+ // Here we look for ```hbs``` tagged template expressions, originally introduced
+ // in the now-removed GlimmerX environment. We can consider getting rid of this, but
+ // then again there are still some use cases in the wild (e.g. Glimmer Next / GXT)
+ // where have tagged templates closing over outer scope is desirable:
+ // https://github.com/lifeart/glimmer-next/tree/master/glint-environment-gxt
+ // https://discord.com/channels/480462759797063690/717767358743183412/1259061848632721480
if (ts.isTaggedTemplateExpression(node)) {
let meta = emitMetadata.get(node);
let result = calculateTaggedTemplateSpans(ts, node, meta, script, environment);
@@ -302,27 +308,27 @@ function calculateTransformedSource(
/**
* Given an array of `PartialCorrelatedSpan`s for a file, calculates
* their `transformedLength` and `transformedStart` values, resulting
- * in full `ReplacedSpan`s.
+ * in full `CorrelatedSpan`s.
*/
function completeCorrelatedSpans(
partialSpans: Array,
): Array {
- let replacedSpans: Array = [];
+ let correlatedSpans: Array = [];
for (let i = 0; i < partialSpans.length; i++) {
let current = partialSpans[i];
let transformedLength = current.transformedSource.length;
let transformedStart = current.insertionPoint;
if (i > 0) {
- let previous = replacedSpans[i - 1];
+ let previous = correlatedSpans[i - 1];
transformedStart =
previous.transformedStart +
previous.transformedSource.length +
(current.insertionPoint - previous.insertionPoint - previous.originalLength);
}
- replacedSpans.push({ ...current, transformedStart, transformedLength });
+ correlatedSpans.push({ ...current, transformedStart, transformedLength });
}
- return replacedSpans;
+ return correlatedSpans;
}
diff --git a/packages/core/src/transform/template/template-to-typescript.ts b/packages/core/src/transform/template/template-to-typescript.ts
index 00b8712d9..6d5a2df20 100644
--- a/packages/core/src/transform/template/template-to-typescript.ts
+++ b/packages/core/src/transform/template/template-to-typescript.ts
@@ -3,7 +3,7 @@ import { unreachable, assert } from '../util.js';
import { EmbeddingSyntax, mapTemplateContents, RewriteResult } from './map-template-contents.js';
import ScopeStack from './scope-stack.js';
import { GlintEmitMetadata, GlintSpecialForm } from '@glint/core/config-types';
-import { TextContent } from './mapping-tree.js';
+import { TextContent } from './glimmer-ast-mapping-tree.js';
const SPLATTRIBUTES = '...attributes';
diff --git a/packages/core/src/transform/template/transformed-module.ts b/packages/core/src/transform/template/transformed-module.ts
index fed8b3361..263809c54 100644
--- a/packages/core/src/transform/template/transformed-module.ts
+++ b/packages/core/src/transform/template/transformed-module.ts
@@ -1,10 +1,11 @@
-import MappingTree from './mapping-tree.js';
+import GlimmerASTMappingTree from './glimmer-ast-mapping-tree.js';
import { assert } from '../util.js';
import { CodeMapping } from '@volar/language-core';
export type Range = { start: number; end: number };
-export type RangeWithMapping = Range & { mapping?: MappingTree };
+export type RangeWithMapping = Range & { mapping?: GlimmerASTMappingTree };
export type RangeWithMappingAndSource = RangeWithMapping & { source: SourceFile };
+
export type CorrelatedSpan = {
/** Where this span of content originated */
originalFile: SourceFile;
@@ -20,8 +21,8 @@ export type CorrelatedSpan = {
transformedStart: number;
/** The length of this span in the transformed output */
transformedLength: number;
- /** A mapping of offsets within this span between its original and transformed versions */
- mapping?: MappingTree;
+ /** (Glimmer/Handlebars spans only:) A mapping of offsets within this span between its original and transformed versions */
+ glimmerAstMapping?: GlimmerASTMappingTree;
};
export type DirectiveKind = 'ignore' | 'expect-error';
@@ -50,6 +51,8 @@ export type SourceFile = {
* both the original and transformed source text of the module, as
* well any errors encountered during transformation.
*
+ * It is used heavily for bidirectional source mapping between the original TS/HBS code
+ * and the singular transformed TS output (aka the Intermediate Representation).
* It can be queried with an offset or range in either the
* original or transformed source to determine the corresponding
* offset or range in the other.
@@ -64,7 +67,7 @@ export default class TransformedModule {
public toDebugString(): string {
let mappingStrings = this.correlatedSpans.map((span) =>
- span.mapping?.toDebugString({
+ span.glimmerAstMapping?.toDebugString({
originalStart: span.originalStart,
originalSource: span.originalFile.contents.slice(
span.originalStart,
@@ -105,7 +108,7 @@ export default class TransformedModule {
if (startInfo.correlatedSpan === endInfo.correlatedSpan) {
let { correlatedSpan } = startInfo;
- let mapping = correlatedSpan.mapping?.narrowestMappingForTransformedRange({
+ let mapping = correlatedSpan.glimmerAstMapping?.narrowestMappingForTransformedRange({
start: start - correlatedSpan.originalStart,
end: end - correlatedSpan.originalStart,
});
@@ -133,7 +136,7 @@ export default class TransformedModule {
if (startInfo.correlatedSpan && startInfo.correlatedSpan === endInfo.correlatedSpan) {
let { correlatedSpan } = startInfo;
- let mapping = correlatedSpan.mapping?.narrowestMappingForOriginalRange({
+ let mapping = correlatedSpan.glimmerAstMapping?.narrowestMappingForOriginalRange({
start: start - correlatedSpan.transformedStart,
end: end - correlatedSpan.transformedStart,
});
@@ -157,14 +160,14 @@ export default class TransformedModule {
originalOffset,
);
- if (!correlatedSpan.mapping) {
+ if (!correlatedSpan.glimmerAstMapping) {
return null;
}
- let templateMapping = correlatedSpan.mapping?.children[0];
+ let templateMapping = correlatedSpan.glimmerAstMapping?.children[0];
assert(
- correlatedSpan.mapping?.sourceNode.type === 'TemplateEmbedding' &&
+ correlatedSpan.glimmerAstMapping?.sourceNode.type === 'TemplateEmbedding' &&
templateMapping?.sourceNode.type === 'Template',
'Internal error: unexpected mapping structure.' + ` (${templateMapping?.sourceNode.type})`,
);
@@ -234,7 +237,7 @@ export default class TransformedModule {
* - to
* - `[[ZEROLEN-A]]χ.emitContent(χ.resolveOrReturn([[expectsAtLeastOneArg]])());[[ZEROLEN-B]]`
*/
- public toVolarMappings(): CodeMapping[] {
+ public toVolarMappings(filenameFilter?: string): CodeMapping[] {
const sourceOffsets: number[] = [];
const generatedOffsets: number[] = [];
const lengths: number[] = [];
@@ -263,7 +266,7 @@ export default class TransformedModule {
lengths.push(length);
};
- let recurse = (span: CorrelatedSpan, mapping: MappingTree): void => {
+ let recurse = (span: CorrelatedSpan, mapping: GlimmerASTMappingTree): void => {
const children = mapping.children;
let { originalRange, transformedRange } = mapping;
let hbsStart = span.originalStart + originalRange.start;
@@ -298,12 +301,17 @@ export default class TransformedModule {
};
this.correlatedSpans.forEach((span) => {
- if (span.mapping) {
- // this span is transformation from embedded to TS.
+ if (filenameFilter && span.originalFile.filename !== filenameFilter) {
+ return;
+ }
- recurse(span, span.mapping);
+ if (span.glimmerAstMapping) {
+ // this span is transformation from HBS to TS (either the replaced contents
+ // within `` tags in a .gts file, or the inserted and transformed
+ // contents of a companion .hbs file in loose mode)
+ recurse(span, span.glimmerAstMapping);
} else {
- // untransformed TS code (between tags). Because there's no
+ // this span is untransformed TS content. Because there's no
// transformation, we expect these to be the same length (in fact, they
// should be the same string entirely)
diff --git a/packages/core/src/volar/gts-language-plugin.ts b/packages/core/src/volar/ember-language-plugin.ts
similarity index 58%
rename from packages/core/src/volar/gts-language-plugin.ts
rename to packages/core/src/volar/ember-language-plugin.ts
index 87de414d6..7af7ca892 100644
--- a/packages/core/src/volar/gts-language-plugin.ts
+++ b/packages/core/src/volar/ember-language-plugin.ts
@@ -1,19 +1,18 @@
-// import remarkMdx from 'remark-mdx'
-// import remarkParse from 'remark-parse'
-// import {unified} from 'unified'
import { LanguagePlugin } from '@volar/language-core';
import { VirtualGtsCode } from './gts-virtual-code.js';
import type ts from 'typescript';
-import { GlintConfig, loadConfig } from '../index.js';
-import { assert } from '../transform/util.js';
-import { VirtualHandlebarsCode } from './handlebars-virtual-code.js';
+import { GlintConfig } from '../index.js';
import { URI } from 'vscode-uri';
+import { LooseModeBackingComponentClassVirtualCode } from './loose-mode-backing-component-class-virtual-code.js';
export type TS = typeof ts;
/**
- * Create a [Volar](https://volarjs.dev) language module to support GTS.
+ * Create a [Volar](https://volarjs.dev) language plugin to support
+ *
+ * - .gts/.gjs files (the `ember-template-imports` environment)
+ * - .ts + .hbs files (the `ember-loose` environment)
*/
-export function createGtsLanguagePlugin(
+export function createEmberLanguagePlugin(
glintConfig: GlintConfig,
): LanguagePlugin {
return {
@@ -39,10 +38,25 @@ export function createGtsLanguagePlugin(
}
},
- createVirtualCode(uri, languageId, snapshot) {
- // TODO: won't we need to point the TS component code to the same thing?
- if (languageId === 'handlebars') {
- return new VirtualHandlebarsCode(glintConfig, snapshot);
+ // When does this get called?
+ createVirtualCode(scriptId: URI | string, languageId, snapshot /*, codegenContext */) {
+ const scriptIdStr = String(scriptId);
+
+ // See: https://github.com/JetBrains/intellij-plugins/blob/11a9149e20f4d4ba2c1600da9f2b81ff88bd7c97/Angular/src/angular-service/src/index.ts#L31
+ if (
+ languageId === 'typescript' &&
+ !scriptIdStr.endsWith('.d.ts') &&
+ scriptIdStr.indexOf('/node_modules/') < 0
+ ) {
+ // NOTE: scriptId might not be a path when we convert this plugin:
+ // https://github.com/withastro/language-tools/blob/eb7215cc0ab3a8f614455528cd71b81ea994cf68/packages/ts-plugin/src/language.ts#L19
+ // TODO: commented out for now because support for ember-loose is blocking behind converting to TS Plugin and this will just slow things down
+ // return new LooseModeBackingComponentClassVirtualCode(
+ // glintConfig,
+ // snapshot,
+ // scriptId,
+ // codegenContext,
+ // );
}
if (languageId === 'glimmer-ts' || languageId === 'glimmer-js') {
@@ -50,16 +64,20 @@ export function createGtsLanguagePlugin(
}
},
- updateVirtualCode(uri, virtualCode, snapshot) {
- (virtualCode as VirtualGtsCode).update(snapshot);
- return virtualCode;
+ isAssociatedFileOnly(_scriptId: string | URI, languageId: string): boolean {
+ // `ember-loose` only
+ //
+ // Because we declare handlebars files to be associated with "root" .ts files, we
+ // need to mark them here as "associated file only" so that TS doesn't attempt
+ // to type-check them directly, but rather indirectly via the .ts file.
+ return languageId === 'handlebars';
},
typescript: {
extraFileExtensions: [
- { extension: 'gts', isMixedContent: true, scriptKind: 7 },
- { extension: 'gjs', isMixedContent: true, scriptKind: 7 },
- { extension: 'hbs', isMixedContent: true, scriptKind: 7 },
+ { extension: 'gts', isMixedContent: true, scriptKind: 7 satisfies ts.ScriptKind.Deferred },
+ { extension: 'gjs', isMixedContent: true, scriptKind: 7 satisfies ts.ScriptKind.Deferred },
+ { extension: 'hbs', isMixedContent: true, scriptKind: 7 satisfies ts.ScriptKind.Deferred },
],
// Allow extension-less imports, e.g. `import Foo from './Foo`.
@@ -82,21 +100,26 @@ export function createGtsLanguagePlugin(
return {
code: transformedCode,
extension: '.ts',
- scriptKind: 3, // TS
+ scriptKind: 3 satisfies ts.ScriptKind.TS,
};
case 'glimmer-js':
return {
- // The first embeddedCode is always the TS Intermediate Representation code
code: transformedCode,
extension: '.js',
- scriptKind: 1, // JS
+ scriptKind: 1 satisfies ts.ScriptKind.JS,
};
case 'handlebars':
// TODO: companion file might be .js? Not sure if this is right
return {
code: transformedCode,
extension: '.ts',
- scriptKind: 3, // TS
+ scriptKind: 3 satisfies ts.ScriptKind.TS,
+ };
+ case 'typescript': // loose mode backing .ts
+ return {
+ code: transformedCode,
+ extension: '.ts',
+ scriptKind: 3 satisfies ts.ScriptKind.TS,
};
default:
throw new Error(`getScript: Unexpected languageId: ${rootVirtualCode.languageId}`);
diff --git a/packages/core/src/volar/gts-virtual-code.ts b/packages/core/src/volar/gts-virtual-code.ts
index 3592d79c6..b110ad3ca 100644
--- a/packages/core/src/volar/gts-virtual-code.ts
+++ b/packages/core/src/volar/gts-virtual-code.ts
@@ -11,7 +11,8 @@ interface EmbeddedCodeWithDirectives extends VirtualCode {
}
/**
- * A Volar virtual code that contains some additional metadata for MDX files.
+ * A Volar VirtualCode representing .gts/.gjs files, which includes 0+ embedded
+ * Handlebars templates within tags.
*/
export class VirtualGtsCode implements VirtualCode {
/**
@@ -98,14 +99,7 @@ export class VirtualGtsCode implements VirtualCode {
id: 'ts',
languageId: 'typescript',
mappings: [
- // The Volar mapping that maps all TS syntax of the MDX file to the virtual TS file.
- // So I think in the case of a Single-File-Component (1 tag surrounded by TS),
- // You'll end up with 2 entries in sourceOffets, representing before the and after the .
{
- // sourceOffsets: [],
- // generatedOffsets: [],
- // lengths: [],
-
// Hacked hardwired values for now.
sourceOffsets: [0],
generatedOffsets: [0],
diff --git a/packages/core/src/volar/handlebars-virtual-code.ts b/packages/core/src/volar/handlebars-virtual-code.ts
deleted file mode 100644
index 15b526a9c..000000000
--- a/packages/core/src/volar/handlebars-virtual-code.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-import { CodeMapping, VirtualCode } from '@volar/language-core';
-import { IScriptSnapshot } from 'typescript';
-import { ScriptSnapshot } from './script-snapshot.js';
-import type ts from 'typescript';
-import { rewriteModule } from '../transform/index.js';
-import { GlintConfig } from '../index.js';
-export type TS = typeof ts;
-
-/**
- * A Volar virtual code that contains some additional metadata for .hbs files.
- *
- * When the editor opens an .hbs file it'll request a virtual file to be created
- * for it. The scheme we use is:
- *
- * 1. Look for corresponding .js/.ts backing class, which might be:
- * - component
- * - route
- * - controller ???
- */
-export class VirtualHandlebarsCode implements VirtualCode {
- /**
- * For .hbs file, the embeddedCodes are the combined
- */
- embeddedCodes: VirtualCode[] = [];
-
- /**
- * The id is a unique (within the VirtualCode and its embedded files) id for Volar to identify it. It could be any string.
- */
- id = 'hbs';
-
- mappings: CodeMapping[] = [];
-
- languageId = 'handlebars';
-
- constructor(
- private glintConfig: GlintConfig,
- public snapshot: IScriptSnapshot,
- ) {
- this.update(snapshot);
- }
-
- // This gets called by the constructor and whenever the language server receives a file change event,
- // i.e. the user saved the file.
- update(snapshot: IScriptSnapshot): void {
- this.snapshot = snapshot;
- const length = snapshot.getLength();
-
- // Define a single mapping for the root virtual code (the untransformed .hbs file).
- this.mappings[0] = {
- sourceOffsets: [0],
- generatedOffsets: [0],
- lengths: [length],
- data: {
- completion: true,
- format: true,
- navigation: true,
- semantic: true,
- structure: true,
- verification: true,
- },
- };
-
- const contents = snapshot.getText(0, length);
-
- // The script we were asked for doesn't exist, but a corresponding template does, and
- // it doesn't have a companion script elsewhere.
- // We default to just `export {}` to reassure TypeScript that this is definitely a module
- // TODO: this `export {}` is falsely mapping (see in Volar Labs), not sure what impact / solution is.
- let script = { filename: 'disregard.ts', contents: 'export {}' };
- let template = {
- filename: 'disregard.hbs',
- contents,
- };
-
- const transformedModule = rewriteModule(
- this.glintConfig.ts,
- { script, template },
- this.glintConfig.environment,
- );
-
- if (transformedModule) {
- this.embeddedCodes = [
- {
- embeddedCodes: [],
- id: 'ts',
- languageId: 'typescript',
- mappings: transformedModule.toVolarMappings(),
- snapshot: new ScriptSnapshot(transformedModule.transformedContents),
- },
- ];
- } else {
- // Null transformed module means there's no embedded HBS templates,
- // so just return a full "no-op" mapping from source to transformed.
- this.embeddedCodes = [
- {
- embeddedCodes: [],
- id: 'ts',
- languageId: 'typescript',
- mappings: [
- // The Volar mapping that maps all TS syntax of the MDX file to the virtual TS file.
- // So I think in the case of a Single-File-Component (1 tag surrounded by TS),
- // You'll end up with 2 entries in sourceOffets, representing before the and after the .
- {
- // Hacked hardwired values for now.
- sourceOffsets: [0],
- generatedOffsets: [0],
- lengths: [length],
-
- data: {
- completion: true,
- format: false,
- navigation: true,
- semantic: true,
- structure: true,
- verification: true,
- },
- },
- ],
- snapshot: new ScriptSnapshot(contents),
- },
- ];
- }
- }
-}
diff --git a/packages/core/src/volar/language-server.ts b/packages/core/src/volar/language-server.ts
index e40d2caac..1e301a021 100644
--- a/packages/core/src/volar/language-server.ts
+++ b/packages/core/src/volar/language-server.ts
@@ -9,7 +9,7 @@ import {
createTypeScriptProject,
} from '@volar/language-server/node.js';
import { create as createTypeScriptServicePlugins } from 'volar-service-typescript';
-import { createGtsLanguagePlugin } from './gts-language-plugin.js';
+import { createEmberLanguagePlugin } from './ember-language-plugin.js';
import { assert } from '../transform/util.js';
import { ConfigLoader } from '../config/loader.js';
import ts from 'typescript';
@@ -18,7 +18,7 @@ import * as vscode from 'vscode-languageserver-protocol';
import { URI } from 'vscode-uri';
import { VirtualGtsCode } from './gts-virtual-code.js';
import { augmentDiagnostic } from '../transform/diagnostics/augmentation.js';
-import MappingTree from '../transform/template/mapping-tree.js';
+import GlimmerASTMappingTree from '../transform/template/glimmer-ast-mapping-tree.js';
import { Directive, TransformedModule } from '../transform/index.js';
import { Range } from '../transform/template/transformed-module.js';
import { offsetToPosition } from '../language-server/util/position.js';
@@ -52,7 +52,7 @@ connection.onInitialize((parameters) => {
// assert(glintConfig, 'Glint config is missing');
if (glintConfig) {
- languagePlugins.unshift(createGtsLanguagePlugin(glintConfig));
+ languagePlugins.unshift(createEmberLanguagePlugin(glintConfig));
}
}
@@ -141,7 +141,7 @@ function filterAndAugmentDiagnostics(
return cachedVirtualCode;
};
- const mappingForDiagnostic = (diagnostic: vscode.Diagnostic): MappingTree | null => {
+ const mappingForDiagnostic = (diagnostic: vscode.Diagnostic): GlimmerASTMappingTree | null => {
const transformedModule = fetchVirtualCode()?.transformedModule;
if (!transformedModule) {
diff --git a/packages/core/src/volar/loose-mode-backing-component-class-virtual-code.ts b/packages/core/src/volar/loose-mode-backing-component-class-virtual-code.ts
new file mode 100644
index 000000000..81b4878a1
--- /dev/null
+++ b/packages/core/src/volar/loose-mode-backing-component-class-virtual-code.ts
@@ -0,0 +1,164 @@
+import { CodeMapping, VirtualCode } from '@volar/language-core';
+import { IScriptSnapshot } from 'typescript';
+import { ScriptSnapshot } from './script-snapshot.js';
+import type ts from 'typescript';
+import { Directive, rewriteModule } from '../transform/index.js';
+import { GlintConfig } from '../index.js';
+import { CodegenContext, SourceScript } from '@volar/language-core/lib/types.js';
+import { URI } from 'vscode-uri';
+export type TS = typeof ts;
+
+interface EmbeddedCodeWithDirectives extends VirtualCode {
+ directives: readonly Directive[];
+}
+
+/**
+ * A Volar VirtualCode representing the .ts/.js file of a Ember/Glimmer component
+ * class that serves as a backing class for an associated .hbs template. These kinds of
+ * components are only supported when using `ember-loose` environment.
+ */
+export class LooseModeBackingComponentClassVirtualCode implements VirtualCode {
+ embeddedCodes: EmbeddedCodeWithDirectives[] = [];
+
+ /**
+ * The id is a unique (within the VirtualCode and its embedded files) id for Volar to identify it. It could be any string.
+ */
+ id = 'loose-mode-component-class';
+
+ mappings: CodeMapping[] = [];
+
+ transformedModule: ReturnType | null = null;
+
+ get languageId(): string {
+ return 'typescript';
+ }
+
+ constructor(
+ private glintConfig: GlintConfig,
+ public snapshot: IScriptSnapshot,
+ public fileId: URI | string,
+ public codegenContext: CodegenContext,
+ ) {
+ this.update(snapshot);
+ }
+
+ // This gets called by the constructor and whenever the language server receives a file change event,
+ // i.e. the user saved the file.
+ update(snapshot: IScriptSnapshot): void {
+ this.snapshot = snapshot;
+ const length = snapshot.getLength();
+
+ this.mappings[0] = {
+ sourceOffsets: [0],
+ generatedOffsets: [0],
+ lengths: [length],
+
+ // This controls which language service features are enabled within this root virtual code
+ data: {
+ completion: true,
+ format: true,
+ navigation: true,
+ semantic: true,
+ structure: true,
+ verification: true,
+ },
+ };
+
+ const contents = snapshot.getText(0, length);
+
+ const templatePathCandidate = this.glintConfig.environment.getPossibleTemplatePaths(
+ String(this.fileId),
+ )[0];
+
+ if (!templatePathCandidate) {
+ // TODO: this probably shouldn't be an error; just trying to fail fast for tests for now
+ throw new Error(`Could not find a template file candidate for ${this.fileId}`);
+ }
+
+ const associatedScriptFileId =
+ typeof this.fileId == 'string'
+ ? templatePathCandidate.path
+ : URI.parse(templatePathCandidate.path);
+ const hbsSourceScript = this.codegenContext.getAssociatedScript(associatedScriptFileId);
+
+ if (!hbsSourceScript) {
+ // TODO: this probably shouldn't be an error; just trying to fail fast for tests for now
+ let msg = `Could not find a source script for ${templatePathCandidate.path}`;
+ // throw new Error(msg);
+ return;
+ }
+
+ const hbsLength = hbsSourceScript.snapshot.getLength();
+ const hbsContent = hbsSourceScript.snapshot.getText(0, hbsLength);
+ const sourceTsFileName = String(this.fileId);
+
+ const transformedModule = rewriteModule(
+ this.glintConfig.ts,
+ {
+ script: {
+ filename: sourceTsFileName,
+ contents,
+ },
+ template: {
+ filename: templatePathCandidate.path,
+ contents: hbsContent,
+ },
+ },
+ this.glintConfig.environment,
+ );
+
+ this.transformedModule = transformedModule;
+
+ if (transformedModule) {
+ const volarTsMappings = transformedModule.toVolarMappings(sourceTsFileName);
+ const volarHbsMappings = transformedModule.toVolarMappings(templatePathCandidate.path);
+ this.embeddedCodes = [
+ {
+ embeddedCodes: [],
+ id: 'ts',
+ languageId: 'typescript',
+
+ // Mappings from the backing component class file to the transformed module.
+ mappings: volarTsMappings,
+ snapshot: new ScriptSnapshot(transformedModule.transformedContents),
+ directives: transformedModule.directives,
+
+ // Mappings from the .hbs template file to the transformed module.
+ associatedScriptMappings: new Map([[hbsSourceScript.id, volarHbsMappings]]),
+ },
+ ];
+ } else {
+ // TODO: when would we get here? I guess Handlebars script might have a parse error?
+ // Null transformed module means there's no embedded HBS templates,
+ // so just return a full "no-op" mapping from source to transformed.
+ this.embeddedCodes = [
+ {
+ embeddedCodes: [],
+ id: 'ts',
+ languageId: 'typescript',
+ mappings: [
+ {
+ // Hacked hardwired values for now.
+ sourceOffsets: [0],
+ generatedOffsets: [0],
+ lengths: [length],
+
+ // This controls which language service features are enabled within this root virtual code.
+ // Since this is just .ts, we want all of them enabled.
+ data: {
+ completion: true,
+ format: false,
+ navigation: true,
+ semantic: true,
+ structure: true,
+ verification: true,
+ },
+ },
+ ],
+ snapshot: new ScriptSnapshot(contents),
+ directives: [],
+ },
+ ];
+ }
+ }
+}