Thanks to visit codestin.com
Credit goes to github.com

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
5 changes: 5 additions & 0 deletions .changeset/selfish-days-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lit-labs/tsserver-plugin': patch
---

Implement getDefinitionAtPosition() for template elements
117 changes: 117 additions & 0 deletions packages/labs/tsserver-plugin/src/lib/lit-language-service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import {Analyzer} from '@lit-labs/analyzer';
import {
isLitHtmlTaggedTemplateExpression,
parseLitTemplate,
} from '@lit-labs/analyzer/lib/lit/template.js';
import * as path from 'node:path';
import type ts from 'typescript';
import {Diagnostic, LanguageService} from 'typescript';
import {noBindingLikeAttributeNames} from './rules/no-binding-like-attribute-names.js';
import {type Element, traverse} from '@parse5/tools';

const rules = [noBindingLikeAttributeNames];

Expand Down Expand Up @@ -68,6 +73,118 @@ export const makeLitLanguageService = (
// TODO(justinfagnani): Add in analyzer diagnostics
return [...(prevDiagnostics ?? []), ...diagnostics];
}

override getDefinitionAtPosition(
fileName: string,
position: number
): readonly ts.DefinitionInfo[] | undefined {
const program = this.getProgram()!;
const checker = program.getTypeChecker();
const sourceFile = program.getSourceFile(fileName)!;

const tsNode = this.#findNodeAtPosition(sourceFile!, position);
if (tsNode !== undefined && typescript.isTemplateLiteral(tsNode)) {
if (
isLitHtmlTaggedTemplateExpression(tsNode.parent, typescript, checker)
) {
const litTemplate = parseLitTemplate(
tsNode.parent,
typescript,
checker
);

// Get the Lit template node at this position
const templatePosition = tsNode.getFullStart();
let foundDefinition: ts.DefinitionInfo | undefined;

traverse(litTemplate, {
element: (element: Element) => {
const {startTag} = element.sourceCodeLocation!;
if (
startTag !== undefined &&
startTag.startOffset + templatePosition < position &&
startTag.endOffset + templatePosition > position
) {
const definition = this.#getElementDefinition(element.tagName);
if (definition !== undefined) {
const tsDefinition = definition.node;

// Get a ts.DefinitionInfo from this tsDefinition
const sourceFile = tsDefinition.getSourceFile();
const start = tsDefinition.getStart();
const length = tsDefinition.getEnd() - start;

foundDefinition = {
fileName: sourceFile.fileName,
textSpan: {
start,
length,
},
kind: typescript.ScriptElementKind.classElement,
name: definition.name,
containerKind: typescript.ScriptElementKind.moduleElement,
containerName: '',
};
}
}
},
});

if (foundDefinition) {
return [foundDefinition];
}
}
}

return super.getDefinitionAtPosition(fileName, position);
}

override getDefinitionAndBoundSpan(
fileName: string,
position: number
): ts.DefinitionInfoAndBoundSpan | undefined {
console.log('getDefinitionAndBoundSpan', fileName, position);
return super.getDefinitionAndBoundSpan(fileName, position);
}

/**
* Find the TypeScript AST node at the given position using depth-first traversal.
*/
#findNodeAtPosition(
sourceFile: ts.SourceFile,
position: number
): ts.Node | undefined {
function find(node: ts.Node): ts.Node | undefined {
// Check if position is within this node's range
if (position >= node.getFullStart() && position < node.getEnd()) {
// Try to find a more specific child node first
let foundChild: ts.Node | undefined;
node.forEachChild((child) => {
if (!foundChild) {
foundChild = find(child);
}
});
// Return the most specific node found, or this node if no children match
return foundChild || node;
}
return undefined;
}

return find(sourceFile);
}

#getElementDefinition(tagname: string) {
const pkg = this.#analyzer.getPackage();
for (const module of pkg.modules) {
const customElementExports = module.getCustomElementExports();
for (const ce of customElementExports) {
if (ce.tagname === tagname) {
return ce;
}
}
}
return undefined;
}
}

// Set up the prototype chain to be:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import assert from 'node:assert';
import * as path from 'node:path';
import {describe as suite, test} from 'node:test';
import {createTestProjectService} from '../project-service.js';
import {getLitTemplateExpressions} from '@lit-labs/analyzer/lib/lit/template.js';
import ts from 'typescript';

suite('lit-language-service', () => {
test('test test', async () => {
const {projectService, loaded} = createTestProjectService();

const pathName = path.resolve(
'test-files/basic-templates/src/custom-element.ts'
);
const result = projectService.openClientFile(pathName);
assert.ok(result.configFileName);

// The plugin is loaded async, so we need to wait for it to be loaded
await loaded;

const info = projectService.getScriptInfo(pathName);
const project = info!.containingProjects[0];
const languageService = project.getLanguageService();
const program = languageService.getProgram()!;
const testSourceFile = program.getSourceFile(pathName);
assert.ok(testSourceFile);

const templates = getLitTemplateExpressions(
testSourceFile,
ts,
program.getTypeChecker()
);

assert.equal(templates.length, 2);
const standaloneTemplate = templates[1];
const xFooPosition =
standaloneTemplate.getFullStart() +
standaloneTemplate.getFullText().indexOf('x-foo') +
1;

const definitions = languageService.getDefinitionAtPosition(
pathName,
xFooPosition
);
const firstDefinition = definitions![0];

// get the source text for the definition
const definitionSourceFile = program.getSourceFile(
firstDefinition.fileName
);
const definitionSourceText = definitionSourceFile!
.getFullText()
.slice(
firstDefinition.textSpan.start,
firstDefinition.textSpan.start + firstDefinition.textSpan.length
);
assert.equal(
definitionSourceText,
`@customElement('x-foo')
export class XFoo extends LitElement {
render() {
return html\`<slot></slot>\`;
}
}`
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

import {LitElement, html} from 'lit';
import {customElement} from 'lit/decorators.js';

@customElement('x-foo')
export class XFoo extends LitElement {
render() {
return html`<slot></slot>`;
}
}

export const templateA = html`<x-foo></x-foo>`;
Loading