-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[ts-plugin] Support tag/prop references
- Loading branch information
Showing
13 changed files
with
838 additions
and
721 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
export class StringWeakMap<T extends WeakKey> { | ||
#map = new Map<string, WeakRef<T>>(); | ||
#weakMap = new WeakMap<T, string>(); | ||
#registry = new FinalizationRegistry<string>((key) => this.#map.delete(key)); | ||
|
||
set(key: string, val: T) { | ||
this.#map.set(key, new WeakRef(val)); | ||
this.#weakMap.set(val, key); | ||
this.#registry.register(val, key); | ||
} | ||
|
||
get(key: string) { | ||
return this.#map.get(key)?.deref(); | ||
} | ||
|
||
findKey(val: T) { | ||
return this.#weakMap.get(val); | ||
} | ||
|
||
*[Symbol.iterator]() { | ||
const entries = this.#map.entries(); | ||
for (const [tag, ref] of entries) { | ||
yield [tag, ref.deref()!] as const; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import type { Logger, TemplateSettings } from '@mantou/typescript-template-language-service-decorator'; | ||
import type * as ts from 'typescript/lib/tsserverlibrary'; | ||
import type { HTMLDocument, IHTMLDataProvider } from '@mantou/vscode-html-languageservice'; | ||
import { getLanguageService as getHTMLanguageService, TextDocument } from '@mantou/vscode-html-languageservice'; | ||
import StandardTemplateSourceHelper from '@mantou/typescript-template-language-service-decorator/lib/standard-template-source-helper'; | ||
import StandardScriptSourceHelper from '@mantou/typescript-template-language-service-decorator/lib/standard-script-source-helper'; | ||
import type { Stylesheet } from '@mantou/vscode-css-languageservice'; | ||
import { getCSSLanguageService } from '@mantou/vscode-css-languageservice'; | ||
import { StringWeakMap } from 'duoyun-ui/lib/map'; | ||
|
||
import { isDepElement } from './utils'; | ||
import type { Configuration } from './configuration'; | ||
import { dataProvider, HTMLDataProvider } from './data-provider'; | ||
import { LRUCache } from './cache'; | ||
|
||
/** | ||
* 全局上下文,数据共享 | ||
*/ | ||
export class Context { | ||
elements: StringWeakMap<ts.ClassDeclaration>; | ||
ts: typeof ts; | ||
config: Configuration; | ||
project: ts.server.Project; | ||
logger: Logger; | ||
dataProvider: IHTMLDataProvider; | ||
cssLanguageService: ReturnType<typeof getCSSLanguageService>; | ||
htmlLanguageService: ReturnType<typeof getHTMLanguageService>; | ||
htmlSourceHelper: StandardTemplateSourceHelper; | ||
htmlTemplateStringSettings: TemplateSettings; | ||
cssTemplateStringSettings: TemplateSettings; | ||
getProgram: () => ts.Program; | ||
|
||
constructor(typescript: typeof ts, config: Configuration, info: ts.server.PluginCreateInfo, logger: Logger) { | ||
this.ts = typescript; | ||
this.config = config; | ||
this.getProgram = () => info.languageService.getProgram()!; | ||
this.project = info.project; | ||
this.logger = logger; | ||
this.dataProvider = dataProvider; | ||
this.elements = new StringWeakMap<ts.ClassDeclaration>(); | ||
this.cssLanguageService = getCSSLanguageService({}); | ||
this.htmlLanguageService = getHTMLanguageService({ | ||
customDataProviders: [dataProvider, new HTMLDataProvider(typescript, this.elements, this.getProgram)], | ||
}); | ||
this.htmlTemplateStringSettings = { | ||
tags: ['html', 'raw', 'h'], | ||
enableForStringWithSubstitutions: true, | ||
getSubstitution, | ||
}; | ||
this.cssTemplateStringSettings = { | ||
tags: ['styled', 'css'], | ||
enableForStringWithSubstitutions: true, | ||
getSubstitution, | ||
isValidTemplate: (node) => isValidCSSTemplate(typescript, node, 'css'), | ||
}; | ||
this.htmlSourceHelper = new StandardTemplateSourceHelper( | ||
typescript, | ||
this.htmlTemplateStringSettings, | ||
new StandardScriptSourceHelper(typescript, info.project), | ||
logger, | ||
); | ||
} | ||
|
||
#virtualHtmlCache = new LRUCache<{ vDoc: TextDocument; vHtml: HTMLDocument }>({ max: 1000 }); | ||
#virtualCssCache = new LRUCache<{ vDoc: TextDocument; vCss: Stylesheet }>({ max: 1000 }); | ||
getCssDoc(text: string) { | ||
return this.#virtualCssCache.get({ text, fileName: '' }, undefined, () => { | ||
const vDoc = createVirtualDocument('css', text); | ||
const vCss = this.cssLanguageService.parseStylesheet(vDoc); | ||
return { vDoc, vCss }; | ||
}); | ||
} | ||
|
||
getHtmlDoc(text: string) { | ||
return this.#virtualHtmlCache.get({ text, fileName: '' }, undefined, () => { | ||
const vDoc = createVirtualDocument('html', text); | ||
const vHtml = this.htmlLanguageService.parseHTMLDocument(vDoc); | ||
return { vDoc, vHtml }; | ||
}); | ||
} | ||
|
||
getTagFromNode(node: ts.Node, supportClassName = isDepElement(node)) { | ||
if (!this.ts.isClassDeclaration(node)) return; | ||
|
||
for (const modifier of node.modifiers || []) { | ||
if ( | ||
this.ts.isDecorator(modifier) && | ||
this.ts.isCallExpression(modifier.expression) && | ||
modifier.expression.expression.getText() === 'customElement' | ||
) { | ||
const arg = modifier.expression.arguments.at(0); | ||
if (arg && this.ts.isStringLiteral(arg)) { | ||
return arg.text; | ||
} | ||
} | ||
} | ||
|
||
// 只有声明文件 | ||
if (supportClassName && node.name && this.ts.isIdentifier(node.name)) { | ||
return this.config.elementDefineRules.findTag(node.name.text); | ||
} | ||
} | ||
|
||
updateElement(file: ts.SourceFile) { | ||
const isDep = isDepElement(file); | ||
// 只支持顶级 class 声明 | ||
this.ts.forEachChild(file, (node) => { | ||
const tag = this.getTagFromNode(node, isDep); | ||
if (tag && this.ts.isClassDeclaration(node)) { | ||
this.elements.set(tag, node); | ||
} | ||
}); | ||
} | ||
|
||
#initElementsCache = new WeakSet<ts.Program>(); | ||
/** | ||
* 当 project 准备好了执行 | ||
*/ | ||
initElements() { | ||
const program = this.getProgram(); | ||
if (this.#initElementsCache.has(program)) return; | ||
program.getSourceFiles().forEach((file) => this.updateElement(file)); | ||
} | ||
} | ||
|
||
function createVirtualDocument(languageId: string, content: string) { | ||
return TextDocument.create(`embedded://document.${languageId}`, languageId, 1, content); | ||
} | ||
|
||
function getSubstitution(templateString: string, start: number, end: number) { | ||
return templateString.slice(start, end).replaceAll(/[^\n]/g, '_'); | ||
} | ||
|
||
function isValidCSSTemplate( | ||
typescript: typeof ts, | ||
node: ts.NoSubstitutionTemplateLiteral | ts.TaggedTemplateExpression | ts.TemplateExpression, | ||
callName: string, | ||
) { | ||
switch (node.kind) { | ||
case typescript.SyntaxKind.NoSubstitutionTemplateLiteral: | ||
case typescript.SyntaxKind.TemplateExpression: | ||
const parent = node.parent; | ||
if (typescript.isCallExpression(parent) && parent.expression.getText() === callName) { | ||
return true; | ||
} | ||
if (typescript.isPropertyAssignment(parent)) { | ||
const call = parent.parent.parent; | ||
if (typescript.isCallExpression(call) && call.expression.getText() === callName) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
default: | ||
return false; | ||
} | ||
} |
Oops, something went wrong.