Skip to content

Commit

Permalink
[ts-plugin] Support tag/prop references
Browse files Browse the repository at this point in the history
  • Loading branch information
mantou132 committed Jan 18, 2025
1 parent 2e1fbba commit ad2b787
Show file tree
Hide file tree
Showing 13 changed files with 838 additions and 721 deletions.
28 changes: 19 additions & 9 deletions packages/duoyun-ui/src/lib/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,41 @@ interface CacheItem<T> {
value: T;
}

/**
* 过期的不能自动清理,但超出最大容量时会被修剪
*/
export class Cache<T = any> {
#max: number;
#maxAge: number;
#renewal: boolean;

#map = new Map<string, CacheItem<T>>();
#reverseMap = new Map<T, string>();
#linkedList = new LinkedList<T>();
#addedLinked = new LinkedList<T>();

constructor({ max = Infinity, maxAge = Infinity, renewal = false }: CacheOptions = {}) {
this.#max = max;
this.#maxAge = maxAge;
this.#renewal = renewal;
}

setOptions(options: CacheOptions) {
this.#max = options.max ?? this.#max;
this.#maxAge = options.maxAge ?? this.#maxAge;
this.#renewal = options.renewal ?? this.#renewal;
}

#trim() {
for (let i = this.#linkedList.size - this.#max; i > 0; i--) {
const value = this.#linkedList.get();
const key = this.#reverseMap.get(value!)!;
this.#reverseMap.delete(value!);
for (let i = this.#addedLinked.size - this.#max; i > 0; i--) {
const value = this.#addedLinked.get()!;
const key = this.#reverseMap.get(value)!;
this.#reverseMap.delete(value);
this.#map.delete(key);
}
}

set(key: string, value: T) {
this.#linkedList.add(value);
this.#addedLinked.add(value);
this.#reverseMap.set(value, key);
this.#map.set(key, { value, timestamp: Date.now() });
this.#trim();
Expand All @@ -51,17 +60,18 @@ export class Cache<T = any> {
return init && this.set(key, init(key));
}
const { timestamp, value } = cache;
// 过期重新生成
if (Date.now() - timestamp > this.#maxAge) {
this.#linkedList.delete(value);
this.#addedLinked.delete(value);
this.#reverseMap.delete(value);
this.#map.delete(key);
return init && this.set(key, init(key));
}
if (this.#renewal) {
cache.timestamp = Date.now();
}
this.#linkedList.get();
this.#linkedList.add(value);
// 调整下位置
this.#addedLinked.add(value);
return value;
}
}
26 changes: 26 additions & 0 deletions packages/duoyun-ui/src/lib/map.ts
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;
}
}
}
11 changes: 6 additions & 5 deletions packages/gem/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,10 @@ export class LinkedList<T = any> extends EventTarget {
return this.#map.get(value);
}

// 添加到尾部,已存在时会删除老的项目
// 如果是添加第一个,start 事件会在添加前触发,避免处理事件重复的逻辑
/**
* 添加到尾部,已存在时会删除老的项目
* 如果是添加第一个,start 事件会在添加前触发,避免处理事件重复的逻辑
*/
add(value: T) {
if (!this.#lastItem) {
this.dispatchEvent(new CustomEvent('start'));
Expand All @@ -141,7 +143,7 @@ export class LinkedList<T = any> extends EventTarget {
this.#map.set(value, item);
}

// 删除这个元素后没有其他元素时立即出发 end 事件
/** 删除这个元素后没有其他元素时立即出发 end 事件 */
delete(value: T) {
const deleteItem = this.#delete(value);
if (!this.#firstItem) {
Expand All @@ -150,8 +152,7 @@ export class LinkedList<T = any> extends EventTarget {
return deleteItem;
}

// 获取头部元素
// 会从链表删除
/** 获取头部元素,会从链表删除 */
get(): T | undefined {
const firstItem = this.#firstItem;
if (!firstItem) return;
Expand Down
26 changes: 6 additions & 20 deletions packages/ts-gem-plugin/src/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,16 @@ import { Cache } from 'duoyun-ui/lib/cache';
export type CacheContext = Pick<TemplateContext, 'fileName' | 'text'>;

export class LRUCache<T extends object> {
#bucket = new Cache<T>({ max: 25, renewal: true });
#bucket: Cache<T>;
constructor(...args: ConstructorParameters<typeof Cache<T>>) {
this.#bucket = new Cache<T>({ max: 25, renewal: true, ...args });
}

#genKey(context: CacheContext, position?: Position) {
return [context.fileName, position?.line, position?.character, context.text].join(';');
}

get(context: CacheContext, init: () => T) {
return this.#bucket.get(this.#genKey(context), init);
}

getCached(context: CacheContext, position?: Position) {
return this.#bucket.get(this.#genKey(context, position));
}

updateCached(context: CacheContext, posOrContent: Position | T, contentOrUndefined?: T) {
let position: Position | undefined;
let content: T;
if ('line' in posOrContent && 'character' in posOrContent) {
position = posOrContent;
content = contentOrUndefined!;
} else {
position = undefined;
content = posOrContent;
}
return this.#bucket.set(this.#genKey(context, position), content);
get(context: CacheContext, position: Position | undefined, init: () => T) {
return this.#bucket.get(this.#genKey(context, position), init);
}
}
41 changes: 1 addition & 40 deletions packages/ts-gem-plugin/src/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import type { Logger } from '@mantou/typescript-template-language-service-decorator';
import type { VSCodeEmmetConfig } from '@mantou/vscode-emmet-helper';
import type { LanguageService } from 'typescript';
import type * as ts from 'typescript/lib/tsserverlibrary';
import { camelToKebabCase } from '@mantou/gem/lib/utils';
import type { VSCodeEmmetConfig } from '@mantou/vscode-emmet-helper';

export interface PluginConfiguration {
emmet: VSCodeEmmetConfig;
Expand Down Expand Up @@ -55,39 +52,3 @@ export class Configuration {
return this.#elementDefineRules;
}
}

export class Store<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;
}
}
}

export type Context = {
config: Configuration;
ts: typeof ts;
logger: Logger;
getProgram: LanguageService['getProgram'];
getProject: () => ts.server.Project;
elements: Store<ts.ClassDeclaration>;
};
156 changes: 156 additions & 0 deletions packages/ts-gem-plugin/src/context.ts
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;
}
}
Loading

0 comments on commit ad2b787

Please sign in to comment.