Skip to content

perf(typescript-plugin): re-implement subscription of component names and props #5329

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion packages/language-service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { create as createVueTemplatePlugin } from './lib/plugins/vue-template';
import { create as createVueTwoslashQueriesPlugin } from './lib/plugins/vue-twoslash-queries';

import { parse, VueCompilerOptions } from '@vue/language-core';
import { proxyLanguageServiceForVue } from '@vue/typescript-plugin/lib/common';
import { proxyLanguageServiceForVue } from '@vue/typescript-plugin/lib/proxy';
import { collectExtractProps } from '@vue/typescript-plugin/lib/requests/collectExtractProps';
import { getComponentDirectives } from '@vue/typescript-plugin/lib/requests/getComponentDirectives';
import { getComponentEvents } from '@vue/typescript-plugin/lib/requests/getComponentEvents';
Expand Down
109 changes: 4 additions & 105 deletions packages/typescript-plugin/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
import { createLanguageServicePlugin } from '@volar/typescript/lib/quickstart/createLanguageServicePlugin';
import * as vue from '@vue/language-core';
import type * as ts from 'typescript';
import { proxyLanguageServiceForVue } from './lib/common';
import { collectExtractProps } from './lib/requests/collectExtractProps';
import { getComponentDirectives } from './lib/requests/getComponentDirectives';
import { getComponentEvents } from './lib/requests/getComponentEvents';
import { getComponentNames } from './lib/requests/getComponentNames';
import { getComponentProps } from './lib/requests/getComponentProps';
import { getElementAttrs } from './lib/requests/getElementAttrs';
import { getElementNames } from './lib/requests/getElementNames';
import { getImportPathForFile } from './lib/requests/getImportPathForFile';
import { getPropertiesAtLocation } from './lib/requests/getPropertiesAtLocation';
import type { RequestContext } from './lib/requests/types';
import { addVueCommands } from './lib/commands';
import { proxyLanguageServiceForVue } from './lib/proxy';

const windowsPathReg = /\\/g;
const project2Service = new WeakMap<ts.server.Project, [vue.Language, ts.LanguageServiceHost, ts.LanguageService]>();
Expand All @@ -26,15 +17,15 @@ export = createLanguageServicePlugin(
id => id
);

addVueCommands();

return {
languagePlugins: [languagePlugin],
setup: language => {
project2Service.set(info.project, [language, info.languageServiceHost, info.languageService]);

info.languageService = proxyLanguageServiceForVue(ts, language, info.languageService, vueOptions, fileName => fileName);

addVueCommands(ts, info, project2Service);

// #3963
const timer = setInterval(() => {
if (info.project['program']) {
Expand All @@ -54,97 +45,5 @@ export = createLanguageServicePlugin(
return vue.createParsedCommandLineByJson(ts, ts.sys, info.languageServiceHost.getCurrentDirectory(), {}).vueOptions;
}
}

// https://github.com/JetBrains/intellij-plugins/blob/6435723ad88fa296b41144162ebe3b8513f4949b/Angular/src-js/angular-service/src/index.ts#L69
function addVueCommands() {
const projectService = info.project.projectService;
projectService.logger.info("Vue: called handler processing " + info.project.projectKind);

const session = info.session;
if (session == undefined) {
projectService.logger.info("Vue: there is no session in info.");
return;
}
if (session.addProtocolHandler == undefined) {
// addProtocolHandler was introduced in TS 4.4 or 4.5 in 2021, see https://github.com/microsoft/TypeScript/issues/43893
projectService.logger.info("Vue: there is no addProtocolHandler method.");
return;
}
if ((session as any).vueCommandsAdded) {
return;
}

(session as any).vueCommandsAdded = true;

session.addProtocolHandler('vue:collectExtractProps', ({ arguments: args }) => {
return {
response: collectExtractProps.apply(getRequestContext(args[0]), args),
};
});
session.addProtocolHandler('vue:getImportPathForFile', ({ arguments: args }) => {
return {
response: getImportPathForFile.apply(getRequestContext(args[0]), args),
};
});
session.addProtocolHandler('vue:getPropertiesAtLocation', ({ arguments: args }) => {
return {
response: getPropertiesAtLocation.apply(getRequestContext(args[0]), args),
};
});
session.addProtocolHandler('vue:getComponentNames', ({ arguments: args }) => {
return {
response: getComponentNames.apply(getRequestContext(args[0]), args) ?? [],
};
});
session.addProtocolHandler('vue:getComponentProps', ({ arguments: args }) => {
return {
response: getComponentProps.apply(getRequestContext(args[0]), args),
};
});
session.addProtocolHandler('vue:getComponentEvents', ({ arguments: args }) => {
return {
response: getComponentEvents.apply(getRequestContext(args[0]), args),
};
});
session.addProtocolHandler('vue:getComponentDirectives', ({ arguments: args }) => {
return {
response: getComponentDirectives.apply(getRequestContext(args[0]), args),
};
});
session.addProtocolHandler('vue:getElementAttrs', ({ arguments: args }) => {
return {
response: getElementAttrs.apply(getRequestContext(args[0]), args),
};
});
session.addProtocolHandler('vue:getElementNames', ({ arguments: args }) => {
return {
response: getElementNames.apply(getRequestContext(args[0]), args),
};
});

projectService.logger.info('Vue specific commands are successfully added.');
}

function getRequestContext(fileName: string): RequestContext {
const fileAndProject = (info.session as any).getFileAndProject({
file: fileName,
projectFileName: undefined,
}) as {
file: ts.server.NormalizedPath;
project: ts.server.Project;
};
const service = project2Service.get(fileAndProject.project);
if (!service) {
throw 'No RequestContext';
}
return {
typescript: ts,
languageService: service[2],
languageServiceHost: service[1],
language: service[0],
isTsPlugin: true,
getFileId: (fileName: string) => fileName,
};
}
}
);
193 changes: 193 additions & 0 deletions packages/typescript-plugin/lib/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { FileMap } from '@vue/language-core';
import { camelize, capitalize } from '@vue/shared';
import type * as ts from 'typescript';
import { collectExtractProps } from './requests/collectExtractProps';
import { getComponentDirectives } from './requests/getComponentDirectives';
import { getComponentEvents } from './requests/getComponentEvents';
import { getComponentNames } from './requests/getComponentNames';
import { ComponentPropInfo, getComponentProps } from './requests/getComponentProps';
import { getElementAttrs } from './requests/getElementAttrs';
import { getElementNames } from './requests/getElementNames';
import { getImportPathForFile } from './requests/getImportPathForFile';
import { getPropertiesAtLocation } from './requests/getPropertiesAtLocation';
import type { RequestContext } from './requests/types';

// https://github.com/JetBrains/intellij-plugins/blob/6435723ad88fa296b41144162ebe3b8513f4949b/Angular/src-js/angular-service/src/index.ts#L69
export function addVueCommands(
ts: typeof import('typescript'),
info: ts.server.PluginCreateInfo,
project2Service: WeakMap<ts.server.Project, [any, ts.LanguageServiceHost, ts.LanguageService]>
) {
const projectService = info.project.projectService;
projectService.logger.info("Vue: called handler processing " + info.project.projectKind);

const session = info.session;
if (session == undefined) {
projectService.logger.info("Vue: there is no session in info.");
return;
}
if (session.addProtocolHandler == undefined) {
// addProtocolHandler was introduced in TS 4.4 or 4.5 in 2021, see https://github.com/microsoft/TypeScript/issues/43893
projectService.logger.info("Vue: there is no addProtocolHandler method.");
return;
}
if ((session as any).vueCommandsAdded) {
return;
}
(session as any).vueCommandsAdded = true;

let lastProjectVersion: string | undefined;
const componentInfos = new FileMap<[
componentNames: string[],
componentProps: Record<string, ComponentPropInfo[]>,
]>(false);

listenComponentInfos();

async function listenComponentInfos() {
while (true) {
await sleep(500);
const projectVersion = info.project.getProjectVersion();
if (lastProjectVersion === projectVersion) {
continue;
}

const openedScriptInfos = info.project.getRootScriptInfos().filter(info => info.isScriptOpen());
if (!openedScriptInfos.length) {
continue;
}

const requestContexts = new Map<string, RequestContext>();
const token = info.languageServiceHost.getCancellationToken?.();

for (const scriptInfo of openedScriptInfos) {
await sleep(10);
if (token?.isCancellationRequested()) {
break;
}

let requestContext = requestContexts.get(scriptInfo.fileName);
if (!requestContext) {
requestContexts.set(
scriptInfo.fileName,
requestContext = getRequestContext(scriptInfo.fileName)
);
}

let data = getComponentInfo(scriptInfo.fileName);
const [oldComponentNames, componentProps] = data;
const newComponentNames = getComponentNames.apply(requestContext, [scriptInfo.fileName]) ?? [];

if (JSON.stringify(oldComponentNames) !== JSON.stringify(newComponentNames)) {
data[0] = newComponentNames;
}

for (const [name, props] of Object.entries(componentProps)) {
await sleep(10);
if (token?.isCancellationRequested()) {
break;
}

const newProps = getComponentProps.apply(requestContext, [scriptInfo.fileName, name]) ?? [];
if (JSON.stringify(props) !== JSON.stringify(newProps)) {
componentProps[name] = newProps;
}
}
}
lastProjectVersion = projectVersion;
}
}

function getComponentInfo(fileName: string, initialize?: boolean) {
let data = componentInfos.get(fileName);
if (!data) {
componentInfos.set(fileName, data = [
initialize && getComponentNames.apply(getRequestContext(fileName), [fileName]) || [],
{}
]);
}
return data;
}

session.addProtocolHandler('vue:collectExtractProps', ({ arguments: args }) => {
return {
response: collectExtractProps.apply(getRequestContext(args[0]), args),
};
});
session.addProtocolHandler('vue:getImportPathForFile', ({ arguments: args }) => {
return {
response: getImportPathForFile.apply(getRequestContext(args[0]), args),
};
});
session.addProtocolHandler('vue:getPropertiesAtLocation', ({ arguments: args }) => {
return {
response: getPropertiesAtLocation.apply(getRequestContext(args[0]), args),
};
});
session.addProtocolHandler('vue:getComponentNames', ({ arguments: [fileName] }) => {
return {
response: getComponentInfo(fileName, true)[0],
};
});
session.addProtocolHandler('vue:getComponentProps', ({ arguments: [fileName, tag] }) => {
const [, componentProps] = getComponentInfo(fileName, true);
let response = componentProps[tag]
?? componentProps[camelize(tag)]
?? componentProps[capitalize(camelize(tag))];

if (!response) {
const requestContext = getRequestContext(fileName);
const props = getComponentProps.apply(requestContext, [fileName, tag]) ?? [];
response = componentProps[tag] = props;
}
return { response };
});
session.addProtocolHandler('vue:getComponentEvents', ({ arguments: args }) => {
return {
response: getComponentEvents.apply(getRequestContext(args[0]), args),
};
});
session.addProtocolHandler('vue:getComponentDirectives', ({ arguments: args }) => {
return {
response: getComponentDirectives.apply(getRequestContext(args[0]), args),
};
});
session.addProtocolHandler('vue:getElementAttrs', ({ arguments: args }) => {
return {
response: getElementAttrs.apply(getRequestContext(args[0]), args),
};
});
session.addProtocolHandler('vue:getElementNames', ({ arguments: args }) => {
return {
response: getElementNames.apply(getRequestContext(args[0]), args),
};
});

projectService.logger.info('Vue specific commands are successfully added.');

function getRequestContext(fileName: string): RequestContext {
const fileAndProject = (info.session as any).getFileAndProject({
file: fileName,
projectFileName: undefined,
}) as {
file: ts.server.NormalizedPath;
project: ts.server.Project;
};
const service = project2Service.get(fileAndProject.project);
if (!service) {
throw 'No RequestContext';
}
return {
typescript: ts,
languageService: service[2],
languageServiceHost: service[1],
language: service[0],
isTsPlugin: true,
getFileId: (fileName: string) => fileName,
};
}
}

function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
Loading