Skip to content
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

feat(language-server): create project with tsconfig on hybrid mode #4211

Merged
merged 1 commit into from
Apr 5, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
126 changes: 126 additions & 0 deletions packages/language-server/lib/hybridModeProject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import type { LanguagePlugin, LanguageServicePlugin, ServerProject, ServerProjectProviderFactory } from '@volar/language-server';
import { createSimpleServerProject } from '@volar/language-server/lib/project/simpleProject';
import { createServiceEnvironment, getWorkspaceFolder } from '@volar/language-server/lib/project/simpleProjectProvider';
import type { ServerContext } from '@volar/language-server/lib/server';
import { FileMap, createLanguage } from '@vue/language-core';
import { LanguageService, ServiceEnvironment, createLanguageService } from '@vue/language-service';
import { searchNamedPipeServerForFile } from '@vue/typescript-plugin/lib/utils';
import type * as ts from 'typescript';

export function createHybridModeProjectProviderFactory(sys: ts.System): ServerProjectProviderFactory {
return (context, servicePlugins, getLanguagePlugins) => {
const serviceEnvs = new FileMap<ServiceEnvironment>(sys.useCaseSensitiveFileNames);
const tsconfigProjects = new FileMap<Promise<ServerProject>>(sys.useCaseSensitiveFileNames);
const simpleProjects = new FileMap<Promise<ServerProject>>(sys.useCaseSensitiveFileNames);
context.onDidChangeWatchedFiles(({ changes }) => {
for (const change of changes) {
if (tsconfigProjects.has(change.uri)) {
tsconfigProjects.get(change.uri)?.then(project => project.dispose());
tsconfigProjects.delete(change.uri);
context.reloadDiagnostics();
}
}
});
return {
async getProject(uri): Promise<ServerProject> {
const workspaceFolder = getWorkspaceFolder(uri, context.workspaceFolders);
let serviceEnv = serviceEnvs.get(workspaceFolder);
if (!serviceEnv) {
serviceEnv = createServiceEnvironment(context, workspaceFolder);
serviceEnvs.set(workspaceFolder, serviceEnv);
}
const fileName = serviceEnv.typescript!.uriToFileName(uri);
const projectInfo = (await searchNamedPipeServerForFile(fileName))?.projectInfo;
if (projectInfo?.kind === 1) {
const tsconfig = projectInfo.name;
const tsconfigUri = serviceEnv.typescript!.fileNameToUri(tsconfig);
if (!tsconfigProjects.has(tsconfigUri)) {
tsconfigProjects.set(tsconfigUri, (async () => {
const languagePlugins = await getLanguagePlugins(serviceEnv, {
typescript: {
configFileName: tsconfig,
host: {
getScriptFileNames() {
return [];
},
} as any,
sys: {
...sys,
version: 0,
async sync() {
return 0;
},
dispose() { },
},
},
});
return createTSConfigProject(context, serviceEnv, languagePlugins, servicePlugins);
})());
}
return await tsconfigProjects.get(tsconfigUri)!;
}
else {
if (!simpleProjects.has(workspaceFolder)) {
simpleProjects.set(workspaceFolder, (() => {
return createSimpleServerProject(context, serviceEnv, servicePlugins, getLanguagePlugins);
})());
}
return await simpleProjects.get(workspaceFolder)!;
}
},
async getProjects() {
return Promise.all([
...tsconfigProjects.values(),
...simpleProjects.values(),
]);
},
async reloadProjects() {
for (const project of [
...tsconfigProjects.values(),
...simpleProjects.values(),
]) {
(await project).dispose();
}
tsconfigProjects.clear();
},
};

function createTSConfigProject(
context: ServerContext,
serviceEnv: ServiceEnvironment,
languagePlugins: LanguagePlugin[],
servicePlugins: LanguageServicePlugin[],
): ServerProject {

let languageService: LanguageService | undefined;

return {
getLanguageService,
getLanguageServiceDontCreate: () => languageService,
dispose() {
languageService?.dispose();
},
};

function getLanguageService() {
if (!languageService) {
const language = createLanguage(languagePlugins, false, uri => {
const script = context.documents.get(uri);
if (script) {
language.scripts.set(uri, script.languageId, script.getSnapshot());
}
else {
language.scripts.delete(uri);
}
});
languageService = createLanguageService(
language,
servicePlugins,
serviceEnv,
);
}
return languageService;
}
}
};
}
12 changes: 6 additions & 6 deletions packages/language-server/node.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { Connection } from '@volar/language-server';
import { createConnection, createServer, createSimpleProjectProviderFactory, createTypeScriptProjectProviderFactory, loadTsdkByPath } from '@volar/language-server/node';
import { createConnection, createServer, createTypeScriptProjectProviderFactory, loadTsdkByPath } from '@volar/language-server/node';
import { ParsedCommandLine, VueCompilerOptions, createParsedCommandLine, createVueLanguagePlugin, parse, resolveCommonLanguageId, resolveVueCompilerOptions } from '@vue/language-core';
import { ServiceEnvironment, convertAttrName, convertTagName, createDefaultGetTsPluginClient, createVueServicePlugins, detect } from '@vue/language-service';
import { DetectNameCasingRequest, GetConvertAttrCasingEditsRequest, GetConvertTagCasingEditsRequest, ParseSFCRequest } from './lib/protocol';
import type { VueInitializationOptions } from './lib/types';
import * as tsPluginClient from '@vue/typescript-plugin/lib/client';
import { searchNamedPipeServerForFile } from '@vue/typescript-plugin/lib/utils';
import { GetConnectedNamedPipeServerRequest } from './lib/protocol';
import { createHybridModeProjectProviderFactory } from './lib/hybridModeProject';
import { DetectNameCasingRequest, GetConnectedNamedPipeServerRequest, GetConvertAttrCasingEditsRequest, GetConvertTagCasingEditsRequest, ParseSFCRequest } from './lib/protocol';
import type { VueInitializationOptions } from './lib/types';

export const connection: Connection = createConnection();

Expand Down Expand Up @@ -43,7 +43,7 @@ connection.onInitialize(async params => {
const result = await server.initialize(
params,
hybridMode
? createSimpleProjectProviderFactory()
? createHybridModeProjectProviderFactory(tsdk.typescript.sys)
: createTypeScriptProjectProviderFactory(tsdk.typescript, tsdk.diagnosticMessages),
{
watchFileExtensions: ['js', 'cjs', 'mjs', 'ts', 'cts', 'mts', 'jsx', 'tsx', 'json', ...vueFileExtensions],
Expand Down Expand Up @@ -160,7 +160,7 @@ connection.onRequest(GetConvertAttrCasingEditsRequest.type, async params => {
});

connection.onRequest(GetConnectedNamedPipeServerRequest.type, async fileName => {
const server = await searchNamedPipeServerForFile(fileName);
const server = (await searchNamedPipeServerForFile(fileName))?.server;
if (server) {
return server;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/typescript-plugin/lib/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export function getElementAttrs(
}

async function sendRequest<T>(request: Request) {
const server = await searchNamedPipeServerForFile(request.args[0]);
const server = (await searchNamedPipeServerForFile(request.args[0]))?.server;
if (!server) {
console.warn('[Vue Named Pipe Client] No server found for', request.args[0]);
return;
Expand Down
12 changes: 9 additions & 3 deletions packages/typescript-plugin/lib/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { NamedPipeServer, connect, readPipeTable, updatePipeTable } from './util
import type { Language, VueCompilerOptions } from '@vue/language-core';

export interface Request {
type: 'containsFile'
type: 'projectInfoForFile'
| 'collectExtractProps'
| 'getImportPathForFile'
| 'getPropertiesAtLocation'
Expand Down Expand Up @@ -45,8 +45,14 @@ export function startNamedPipeServer(
const request: Request = JSON.parse(text);
const fileName = request.args[0];
const project = getProject(fileName);
if (request.type === 'containsFile') {
connection.write(JSON.stringify(!!project));
if (request.type === 'projectInfoForFile') {
connection.write(JSON.stringify(project
? {
name: project.info.project.getProjectName(),
kind: project.info.project.projectKind,
}
: undefined
));
}
else if (project) {
const requestContext = {
Expand Down
14 changes: 10 additions & 4 deletions packages/typescript-plugin/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,23 @@ export async function searchNamedPipeServerForFile(fileName: string) {
for (const server of configuredServers) {
const client = await connect(server.path);
if (client) {
const response = await sendRequestWorker<boolean>({ type: 'containsFile', args: [fileName] }, client);
if (response) {
return server;
const projectInfo = await sendRequestWorker<{ name: string; kind: ts.server.ProjectKind; }>({ type: 'projectInfoForFile', args: [fileName] }, client);
if (projectInfo) {
return {
server,
projectInfo,
};
}
}
}
for (const server of inferredServers) {
if (!path.relative(server.currentDirectory, fileName).startsWith('..')) {
const client = await connect(server.path);
if (client) {
return server;
return {
server,
projectInfo: undefined,
};
}
}
}
Expand Down