diff --git a/packages/language-server/browser.ts b/packages/language-server/browser.ts index 8dd8722b..936c1682 100644 --- a/packages/language-server/browser.ts +++ b/packages/language-server/browser.ts @@ -8,6 +8,7 @@ export * from 'vscode-languageserver/browser'; export * from './index'; export * from './lib/project/simpleProjectProvider'; export * from './lib/project/typescriptProjectProvider'; +export * from './lib/server'; export function createConnection() { @@ -20,34 +21,32 @@ export function createConnection() { export function createServer(connection: vscode.Connection) { return createServerBase(connection, () => ({ - fs: { - async stat(uri) { - if (uri.startsWith('http://') || uri.startsWith('https://')) { // perf - const text = await this.readFile(uri); - if (text !== undefined) { - return { - type: FileType.File, - size: text.length, - ctime: -1, - mtime: -1, - }; - } - return undefined; + async stat(uri) { + if (uri.startsWith('http://') || uri.startsWith('https://')) { // perf + const text = await this.readFile(uri); + if (text !== undefined) { + return { + type: FileType.File, + size: text.length, + ctime: -1, + mtime: -1, + }; } - return await connection.sendRequest(FsStatRequest.type, uri); - }, - async readFile(uri) { - if (uri.startsWith('http://') || uri.startsWith('https://')) { // perf - return await httpSchemaRequestHandler(uri); - } - return await connection.sendRequest(FsReadFileRequest.type, uri) ?? undefined; - }, - async readDirectory(uri) { - if (uri.startsWith('http://') || uri.startsWith('https://')) { // perf - return []; - } - return await connection.sendRequest(FsReadDirectoryRequest.type, uri); - }, + return undefined; + } + return await connection.sendRequest(FsStatRequest.type, uri); + }, + async readFile(uri) { + if (uri.startsWith('http://') || uri.startsWith('https://')) { // perf + return await httpSchemaRequestHandler(uri); + } + return await connection.sendRequest(FsReadFileRequest.type, uri) ?? undefined; + }, + async readDirectory(uri) { + if (uri.startsWith('http://') || uri.startsWith('https://')) { // perf + return []; + } + return await connection.sendRequest(FsReadDirectoryRequest.type, uri); }, })); } diff --git a/packages/language-server/lib/project/inferredCompilerOptions.ts b/packages/language-server/lib/project/inferredCompilerOptions.ts index e6fe668c..4e893c9b 100644 --- a/packages/language-server/lib/project/inferredCompilerOptions.ts +++ b/packages/language-server/lib/project/inferredCompilerOptions.ts @@ -1,14 +1,14 @@ import type * as ts from 'typescript'; -import type { ServerContext } from '../server'; +import type { ServerBase } from '../types'; -export async function getInferredCompilerOptions(context: ServerContext) { +export async function getInferredCompilerOptions(server: ServerBase) { const [ implicitProjectConfig_1 = {}, implicitProjectConfig_2 = {}, ] = await Promise.all([ - context.getConfiguration('js/ts.implicitProjectConfig'), - context.getConfiguration('javascript.implicitProjectConfig'), + server.getConfiguration('js/ts.implicitProjectConfig'), + server.getConfiguration('javascript.implicitProjectConfig'), ]); const checkJs = readCheckJs(); const experimentalDecorators = readExperimentalDecorators(); diff --git a/packages/language-server/lib/project/simpleProject.ts b/packages/language-server/lib/project/simpleProject.ts index 0edbee7d..7dd29d58 100644 --- a/packages/language-server/lib/project/simpleProject.ts +++ b/packages/language-server/lib/project/simpleProject.ts @@ -1,18 +1,13 @@ -import { LanguageService, ServiceEnvironment, LanguageServicePlugin, createLanguage, createLanguageService } from '@volar/language-service'; -import type { ServerContext, ServerOptions } from '../server'; -import type { ServerProject } from '../types'; +import { LanguagePlugin, LanguageService, ServiceEnvironment, createLanguage, createLanguageService } from '@volar/language-service'; +import type { ServerBase, ServerProject } from '../types'; export async function createSimpleServerProject( - context: ServerContext, + server: ServerBase, serviceEnv: ServiceEnvironment, - servicePlugins: LanguageServicePlugin[], - getLanguagePlugins: ServerOptions['getLanguagePlugins'], + languagePlugins: LanguagePlugin[], ): Promise { - let languageService: LanguageService | undefined; - const languagePlugins = await getLanguagePlugins(serviceEnv, {}); - return { getLanguageService, getLanguageServiceDontCreate: () => languageService, @@ -24,7 +19,7 @@ export async function createSimpleServerProject( function getLanguageService() { if (!languageService) { const language = createLanguage(languagePlugins, false, uri => { - const script = context.documents.get(uri); + const script = server.documents.get(uri); if (script) { language.scripts.set(uri, script.languageId, script.getSnapshot()); } @@ -34,7 +29,7 @@ export async function createSimpleServerProject( }); languageService = createLanguageService( language, - servicePlugins, + server.languageServicePlugins, serviceEnv, ); } diff --git a/packages/language-server/lib/project/simpleProjectProvider.ts b/packages/language-server/lib/project/simpleProjectProvider.ts index bba09352..228e56fe 100644 --- a/packages/language-server/lib/project/simpleProjectProvider.ts +++ b/packages/language-server/lib/project/simpleProjectProvider.ts @@ -1,62 +1,43 @@ -import type { ServiceEnvironment } from '@volar/language-service'; +import type { LanguagePlugin, ServiceEnvironment } from '@volar/language-service'; import { URI } from 'vscode-uri'; -import type { ServerProject, ServerProjectProvider, ServerProjectProviderFactory } from '../types'; -import { createSimpleServerProject } from './simpleProject'; -import type { ServerContext } from '../server'; +import type { ServerBase, ServerProject, ServerProjectProvider } from '../types'; import { fileNameToUri, uriToFileName } from '../uri'; import type { UriMap } from '../utils/uriMap'; +import { createSimpleServerProject } from './simpleProject'; -export function createSimpleProjectProviderFactory(): ServerProjectProviderFactory { - return (context, servicePlugins, getLanguagePlugins): ServerProjectProvider => { - - const projects = new Map>(); - - return { - getProject(uri) { - - const workspaceFolder = getWorkspaceFolder(uri, context.workspaceFolders); - - let projectPromise = projects.get(workspaceFolder); - if (!projectPromise) { - const serviceEnv = createServiceEnvironment(context, workspaceFolder); - projectPromise = createSimpleServerProject(context, serviceEnv, servicePlugins, getLanguagePlugins); - projects.set(workspaceFolder, projectPromise); - } - - return projectPromise; - }, - async getProjects() { - return await Promise.all([...projects.values()]); - }, - reloadProjects() { - - for (const project of projects.values()) { - project.then(project => project.dispose()); - } - - projects.clear(); - - context.reloadDiagnostics(); - }, - }; +export function createSimpleProjectProvider(languagePlugins: LanguagePlugin[]): ServerProjectProvider { + const map = new Map>(); + return { + get(uri) { + const workspaceFolder = getWorkspaceFolder(uri, this.workspaceFolders); + let projectPromise = map.get(workspaceFolder); + if (!projectPromise) { + const serviceEnv = createServiceEnvironment(this, workspaceFolder); + projectPromise = createSimpleServerProject(this, serviceEnv, languagePlugins); + map.set(workspaceFolder, projectPromise); + } + return projectPromise; + }, + async all() { + return await Promise.all([...map.values()]); + }, }; } -export function createServiceEnvironment(context: ServerContext, workspaceFolder: string) { - const env: ServiceEnvironment = { +export function createServiceEnvironment(server: ServerBase, workspaceFolder: string): ServiceEnvironment { + return { workspaceFolder, - fs: context.runtimeEnv.fs, - locale: context.initializeParams.locale, - clientCapabilities: context.initializeParams.capabilities, - getConfiguration: context.getConfiguration, - onDidChangeConfiguration: context.onDidChangeConfiguration, - onDidChangeWatchedFiles: context.onDidChangeWatchedFiles, + fs: server.fs, + locale: server.initializeParams?.locale, + clientCapabilities: server.initializeParams?.capabilities, + getConfiguration: server.getConfiguration, + onDidChangeConfiguration: server.onDidChangeConfiguration, + onDidChangeWatchedFiles: server.onDidChangeWatchedFiles, typescript: { fileNameToUri: fileNameToUri, uriToFileName: uriToFileName, }, }; - return env; } export function getWorkspaceFolder(uri: string, workspaceFolders: UriMap) { diff --git a/packages/language-server/lib/project/typescriptProject.ts b/packages/language-server/lib/project/typescriptProject.ts index 9fb98fa9..b9d39ef0 100644 --- a/packages/language-server/lib/project/typescriptProject.ts +++ b/packages/language-server/lib/project/typescriptProject.ts @@ -1,12 +1,11 @@ -import { LanguagePlugin, LanguageService, ServiceEnvironment, LanguageServicePlugin, TypeScriptProjectHost, createLanguageService } from '@volar/language-service'; -import { createTypeScriptLanguage, createSys } from '@volar/typescript'; +import { LanguagePlugin, LanguageService, ProviderResult, ServiceEnvironment, TypeScriptProjectHost, createLanguageService, resolveCommonLanguageId } from '@volar/language-service'; +import { createSys, createTypeScriptLanguage } from '@volar/typescript'; import * as path from 'path-browserify'; import type * as ts from 'typescript'; import * as vscode from 'vscode-languageserver'; -import type { ServerProject } from '../types'; -import { UriMap, createUriMap } from '../utils/uriMap'; -import type { ServerContext, ServerOptions } from '../server'; +import type { ServerBase, ServerProject } from '../types'; import { fileNameToUri, uriToFileName } from '../uri'; +import { UriMap, createUriMap } from '../utils/uriMap'; export interface TypeScriptServerProject extends ServerProject { askedFiles: UriMap; @@ -18,11 +17,13 @@ export async function createTypeScriptServerProject( ts: typeof import('typescript'), tsLocalized: ts.MapLike | undefined, tsconfig: string | ts.CompilerOptions, - context: ServerContext, + server: ServerBase, serviceEnv: ServiceEnvironment, - servicePlugins: LanguageServicePlugin[], - getLanguagePlugins: ServerOptions['getLanguagePlugins'], - getLanguageId: (uri: string) => string, + getLanguagePlugins: (serviceEnv: ServiceEnvironment, projectContext: { + configFileName: string | undefined; + host: TypeScriptProjectHost; + sys: ReturnType; + }) => ProviderResult, ): Promise { let parsedCommandLine: ts.ParsedCommandLine; @@ -50,7 +51,7 @@ export async function createTypeScriptServerProject( }, getScriptSnapshot(fileName) { askedFiles.pathSet(fileName, true); - const doc = context.documents.get(fileNameToUri(fileName)); + const doc = server.documents.get(fileNameToUri(fileName)); if (doc) { return doc.getSnapshot(); } @@ -63,20 +64,18 @@ export async function createTypeScriptServerProject( return parsedCommandLine.projectReferences; }, getLanguageId(uri) { - return context.documents.get(uri)?.languageId ?? getLanguageId(uri); + return server.documents.get(uri)?.languageId ?? resolveCommonLanguageId(uri); }, fileNameToScriptId: serviceEnv.typescript!.fileNameToUri, scriptIdToFileName: serviceEnv.typescript!.uriToFileName, }; const languagePlugins = await getLanguagePlugins(serviceEnv, { - typescript: { - configFileName: typeof tsconfig === 'string' ? tsconfig : undefined, - host, - sys, - }, + configFileName: typeof tsconfig === 'string' ? tsconfig : undefined, + host, + sys, }); const askedFiles = createUriMap(fileNameToUri); - const docChangeWatcher = context.documents.onDidChangeContent(() => { + const docChangeWatcher = server.documents.onDidChangeContent(() => { projectVersion++; }); const fileWatch = serviceEnv.onDidChangeWatchedFiles?.(params => { @@ -118,7 +117,7 @@ export async function createTypeScriptServerProject( ); languageService = createLanguageService( language, - servicePlugins, + server.languageServicePlugins, serviceEnv, ); } diff --git a/packages/language-server/lib/project/typescriptProjectProvider.ts b/packages/language-server/lib/project/typescriptProjectProvider.ts index bfba9ac2..de560604 100644 --- a/packages/language-server/lib/project/typescriptProjectProvider.ts +++ b/packages/language-server/lib/project/typescriptProjectProvider.ts @@ -1,31 +1,58 @@ -import { FileType } from '@volar/language-service'; +import { FileType, LanguagePlugin, ProviderResult, ServiceEnvironment, TypeScriptProjectHost } from '@volar/language-service'; +import type { createSys } from '@volar/typescript'; import * as path from 'path-browserify'; import type * as ts from 'typescript'; import * as vscode from 'vscode-languageserver'; import { URI } from 'vscode-uri'; -import type { ServerProjectProvider, ServerProjectProviderFactory } from '../types'; +import type { ServerBase, ServerProjectProvider } from '../types'; +import { fileNameToUri, uriToFileName } from '../uri'; import { isFileInDir } from '../utils/isFileInDir'; import { createUriMap } from '../utils/uriMap'; import { getInferredCompilerOptions } from './inferredCompilerOptions'; import { createServiceEnvironment, getWorkspaceFolder } from './simpleProjectProvider'; import { createTypeScriptServerProject, type TypeScriptServerProject } from './typescriptProject'; -import { fileNameToUri, uriToFileName } from '../uri'; const rootTsConfigNames = ['tsconfig.json', 'jsconfig.json']; -export function createTypeScriptProjectProviderFactory( +export function createTypeScriptProjectProvider( ts: typeof import('typescript'), tsLocalized: ts.MapLike | undefined, -): ServerProjectProviderFactory { - return (context, servicePlugins, getLanguagePlugins, getLanguageId): ServerProjectProvider => { - - const { fs } = context.runtimeEnv; - const configProjects = createUriMap>(fileNameToUri); - const inferredProjects = createUriMap>(fileNameToUri); - const rootTsConfigs = new Set(); - const searchedDirs = new Set(); + getLanguagePlugins: (serviceEnv: ServiceEnvironment, projectContext: { + configFileName: string | undefined; + host: TypeScriptProjectHost; + sys: ReturnType; + }) => ProviderResult, +) { + let initialized = false; + + const configProjects = createUriMap>(fileNameToUri); + const inferredProjects = createUriMap>(fileNameToUri); + const rootTsConfigs = new Set(); + const searchedDirs = new Set(); + const projects: ServerProjectProvider = { + async get(uri) { + if (!initialized) { + initialized = true; + initialize(this); + } + const tsconfig = await findMatchTSConfig(this, URI.parse(uri)); + if (tsconfig) { + return await getOrCreateConfiguredProject(this, tsconfig); + } + const workspaceFolder = getWorkspaceFolder(uri, this.workspaceFolders); + return await getOrCreateInferredProject(this, uri, workspaceFolder); + }, + async all() { + return await Promise.all([ + ...configProjects.values(), + ...inferredProjects.values(), + ]); + }, + }; + return projects; - context.onDidChangeWatchedFiles(({ changes }) => { + function initialize(server: ServerBase) { + server.onDidChangeWatchedFiles(({ changes }) => { const tsConfigChanges = changes.filter(change => rootTsConfigNames.includes(change.uri.substring(change.uri.lastIndexOf('/') + 1))); for (const change of tsConfigChanges) { @@ -43,221 +70,187 @@ export function createTypeScriptProjectProviderFactory( } if (tsConfigChanges.length) { - context.reloadDiagnostics(); - } - else { - context.updateDiagnosticsAndSemanticTokens(); + server.clearPushDiagnostics(); } + server.refresh(projects); }); + } - return { - async getProject(uri) { - const tsconfig = await findMatchTSConfig(URI.parse(uri)); - if (tsconfig) { - return await getOrCreateConfiguredProject(tsconfig); - } - const workspaceFolder = getWorkspaceFolder(uri, context.workspaceFolders); - return await getOrCreateInferredProject(uri, workspaceFolder); - }, - async getProjects() { - return await Promise.all([ - ...configProjects.values(), - ...inferredProjects.values(), - ]); - }, - reloadProjects() { - - for (const project of [...configProjects.values(), ...inferredProjects.values()]) { - project.then(project => project.dispose()); - } - - configProjects.clear(); - inferredProjects.clear(); - - context.reloadDiagnostics(); - }, - }; - - async function findMatchTSConfig(uri: URI) { + async function findMatchTSConfig(server: ServerBase, uri: URI) { - const filePath = uriToFileName(uri.toString()); - let dir = path.dirname(filePath); + const filePath = uriToFileName(uri.toString()); + let dir = path.dirname(filePath); - while (true) { - if (searchedDirs.has(dir)) { - break; - } - searchedDirs.add(dir); - for (const tsConfigName of rootTsConfigNames) { - const tsconfigPath = path.join(dir, tsConfigName); - if ((await fs.stat?.(fileNameToUri(tsconfigPath)))?.type === FileType.File) { - rootTsConfigs.add(tsconfigPath); - } + while (true) { + if (searchedDirs.has(dir)) { + break; + } + searchedDirs.add(dir); + for (const tsConfigName of rootTsConfigNames) { + const tsconfigPath = path.join(dir, tsConfigName); + if ((await server.fs.stat?.(fileNameToUri(tsconfigPath)))?.type === FileType.File) { + rootTsConfigs.add(tsconfigPath); } - dir = path.dirname(dir); } + dir = path.dirname(dir); + } - await prepareClosestootParsedCommandLine(); + await prepareClosestootParsedCommandLine(); - return await findDirectIncludeTsconfig() ?? await findIndirectReferenceTsconfig(); + return await findDirectIncludeTsconfig() ?? await findIndirectReferenceTsconfig(); - async function prepareClosestootParsedCommandLine() { + async function prepareClosestootParsedCommandLine() { - let matches: string[] = []; + let matches: string[] = []; - for (const rootTsConfig of rootTsConfigs) { - if (isFileInDir(uriToFileName(uri.toString()), path.dirname(rootTsConfig))) { - matches.push(rootTsConfig); - } + for (const rootTsConfig of rootTsConfigs) { + if (isFileInDir(uriToFileName(uri.toString()), path.dirname(rootTsConfig))) { + matches.push(rootTsConfig); } + } - matches = matches.sort((a, b) => sortTSConfigs(uriToFileName(uri.toString()), a, b)); + matches = matches.sort((a, b) => sortTSConfigs(uriToFileName(uri.toString()), a, b)); - if (matches.length) { - await getParsedCommandLine(matches[0]); - } - } - function findIndirectReferenceTsconfig() { - return findTSConfig(async tsconfig => { - const project = await configProjects.pathGet(tsconfig); - return project?.askedFiles.uriHas(uri.toString()) ?? false; - }); - } - function findDirectIncludeTsconfig() { - return findTSConfig(async tsconfig => { - const map = createUriMap(fileNameToUri); - const parsedCommandLine = await getParsedCommandLine(tsconfig); - for (const fileName of parsedCommandLine?.fileNames ?? []) { - map.pathSet(fileName, true); - } - return map.uriHas(uri.toString()); - }); + if (matches.length) { + await getParsedCommandLine(matches[0]); } - async function findTSConfig(match: (tsconfig: string) => Promise | boolean) { + } + function findIndirectReferenceTsconfig() { + return findTSConfig(async tsconfig => { + const project = await configProjects.pathGet(tsconfig); + return project?.askedFiles.uriHas(uri.toString()) ?? false; + }); + } + function findDirectIncludeTsconfig() { + return findTSConfig(async tsconfig => { + const map = createUriMap(fileNameToUri); + const parsedCommandLine = await getParsedCommandLine(tsconfig); + for (const fileName of parsedCommandLine?.fileNames ?? []) { + map.pathSet(fileName, true); + } + return map.uriHas(uri.toString()); + }); + } + async function findTSConfig(match: (tsconfig: string) => Promise | boolean) { - const checked = new Set(); + const checked = new Set(); - for (const rootTsConfig of [...rootTsConfigs].sort((a, b) => sortTSConfigs(uriToFileName(uri.toString()), a, b))) { - const project = await configProjects.pathGet(rootTsConfig); - if (project) { + for (const rootTsConfig of [...rootTsConfigs].sort((a, b) => sortTSConfigs(uriToFileName(uri.toString()), a, b))) { + const project = await configProjects.pathGet(rootTsConfig); + if (project) { - let chains = await getReferencesChains(project.getParsedCommandLine(), rootTsConfig, []); + let chains = await getReferencesChains(project.getParsedCommandLine(), rootTsConfig, []); - // This is to be consistent with tsserver behavior - chains = chains.reverse(); + // This is to be consistent with tsserver behavior + chains = chains.reverse(); - for (const chain of chains) { - for (let i = chain.length - 1; i >= 0; i--) { - const tsconfig = chain[i]; + for (const chain of chains) { + for (let i = chain.length - 1; i >= 0; i--) { + const tsconfig = chain[i]; - if (checked.has(tsconfig)) { - continue; - } - checked.add(tsconfig); + if (checked.has(tsconfig)) { + continue; + } + checked.add(tsconfig); - if (await match(tsconfig)) { - return tsconfig; - } + if (await match(tsconfig)) { + return tsconfig; } } } } } - async function getReferencesChains(parsedCommandLine: ts.ParsedCommandLine, tsConfig: string, before: string[]) { + } + async function getReferencesChains(parsedCommandLine: ts.ParsedCommandLine, tsConfig: string, before: string[]) { - if (parsedCommandLine.projectReferences?.length) { + if (parsedCommandLine.projectReferences?.length) { - const newChains: string[][] = []; + const newChains: string[][] = []; - for (const projectReference of parsedCommandLine.projectReferences) { + for (const projectReference of parsedCommandLine.projectReferences) { - let tsConfigPath = projectReference.path.replace(/\\/g, '/'); + let tsConfigPath = projectReference.path.replace(/\\/g, '/'); - // fix https://github.com/johnsoncodehk/volar/issues/712 - if ((await fs.stat?.(fileNameToUri(tsConfigPath)))?.type === FileType.File) { - const newTsConfigPath = path.join(tsConfigPath, 'tsconfig.json'); - const newJsConfigPath = path.join(tsConfigPath, 'jsconfig.json'); - if ((await fs.stat?.(fileNameToUri(newTsConfigPath)))?.type === FileType.File) { - tsConfigPath = newTsConfigPath; - } - else if ((await fs.stat?.(fileNameToUri(newJsConfigPath)))?.type === FileType.File) { - tsConfigPath = newJsConfigPath; - } + // fix https://github.com/johnsoncodehk/volar/issues/712 + if ((await server.fs.stat?.(fileNameToUri(tsConfigPath)))?.type === FileType.File) { + const newTsConfigPath = path.join(tsConfigPath, 'tsconfig.json'); + const newJsConfigPath = path.join(tsConfigPath, 'jsconfig.json'); + if ((await server.fs.stat?.(fileNameToUri(newTsConfigPath)))?.type === FileType.File) { + tsConfigPath = newTsConfigPath; } - - const beforeIndex = before.indexOf(tsConfigPath); // cycle - if (beforeIndex >= 0) { - newChains.push(before.slice(0, Math.max(beforeIndex, 1))); + else if ((await server.fs.stat?.(fileNameToUri(newJsConfigPath)))?.type === FileType.File) { + tsConfigPath = newJsConfigPath; } - else { - const referenceParsedCommandLine = await getParsedCommandLine(tsConfigPath); - if (referenceParsedCommandLine) { - for (const chain of await getReferencesChains(referenceParsedCommandLine, tsConfigPath, [...before, tsConfig])) { - newChains.push(chain); - } + } + + const beforeIndex = before.indexOf(tsConfigPath); // cycle + if (beforeIndex >= 0) { + newChains.push(before.slice(0, Math.max(beforeIndex, 1))); + } + else { + const referenceParsedCommandLine = await getParsedCommandLine(tsConfigPath); + if (referenceParsedCommandLine) { + for (const chain of await getReferencesChains(referenceParsedCommandLine, tsConfigPath, [...before, tsConfig])) { + newChains.push(chain); } } } - - return newChains; - } - else { - return [[...before, tsConfig]]; } + + return newChains; } - async function getParsedCommandLine(tsConfig: string) { - const project = await getOrCreateConfiguredProject(tsConfig); - return project?.getParsedCommandLine(); + else { + return [[...before, tsConfig]]; } } + async function getParsedCommandLine(tsConfig: string) { + const project = await getOrCreateConfiguredProject(server, tsConfig); + return project?.getParsedCommandLine(); + } + } + + function getOrCreateConfiguredProject(server: ServerBase, tsconfig: string) { + tsconfig = tsconfig.replace(/\\/g, '/'); + let projectPromise = configProjects.pathGet(tsconfig); + if (!projectPromise) { + const workspaceFolder = getWorkspaceFolder(fileNameToUri(tsconfig), server.workspaceFolders); + const serviceEnv = createServiceEnvironment(server, workspaceFolder); + projectPromise = createTypeScriptServerProject( + ts, + tsLocalized, + tsconfig, + server, + serviceEnv, + getLanguagePlugins, + ); + configProjects.pathSet(tsconfig, projectPromise); + } + return projectPromise; + } + + async function getOrCreateInferredProject(server: ServerBase, uri: string, workspaceFolder: string) { - function getOrCreateConfiguredProject(tsconfig: string) { - tsconfig = tsconfig.replace(/\\/g, '/'); - let projectPromise = configProjects.pathGet(tsconfig); - if (!projectPromise) { - const workspaceFolder = getWorkspaceFolder(fileNameToUri(tsconfig), context.workspaceFolders); - const serviceEnv = createServiceEnvironment(context, workspaceFolder); - projectPromise = createTypeScriptServerProject( + if (!inferredProjects.uriHas(workspaceFolder)) { + inferredProjects.uriSet(workspaceFolder, (async () => { + const inferOptions = await getInferredCompilerOptions(server); + const serviceEnv = createServiceEnvironment(server, workspaceFolder); + return createTypeScriptServerProject( ts, tsLocalized, - tsconfig, - context, + inferOptions, + server, serviceEnv, - servicePlugins, getLanguagePlugins, - getLanguageId, ); - configProjects.pathSet(tsconfig, projectPromise); - } - return projectPromise; + })()); } - async function getOrCreateInferredProject(uri: string, workspaceFolder: string) { - - if (!inferredProjects.uriHas(workspaceFolder)) { - inferredProjects.uriSet(workspaceFolder, (async () => { - const inferOptions = await getInferredCompilerOptions(context); - const serviceEnv = createServiceEnvironment(context, workspaceFolder); - return createTypeScriptServerProject( - ts, - tsLocalized, - inferOptions, - context, - serviceEnv, - servicePlugins, - getLanguagePlugins, - getLanguageId, - ); - })()); - } - - const project = await inferredProjects.uriGet(workspaceFolder.toString())!; + const project = await inferredProjects.uriGet(workspaceFolder.toString())!; - project.tryAddFile(uriToFileName(uri)); + project.tryAddFile(uriToFileName(uri)); - return project; - } - }; + return project; + } } export function sortTSConfigs(file: string, a: string, b: string) { diff --git a/packages/language-server/lib/register/registerEditorFeatures.ts b/packages/language-server/lib/register/registerEditorFeatures.ts index 969259a6..468e7fa2 100644 --- a/packages/language-server/lib/register/registerEditorFeatures.ts +++ b/packages/language-server/lib/register/registerEditorFeatures.ts @@ -1,33 +1,28 @@ import type { CodeMapping, VirtualCode } from '@volar/language-core'; +import type { DataTransferItem } from '@volar/language-service'; import type * as ts from 'typescript'; -import type * as vscode from 'vscode-languageserver'; import { + DocumentDropRequest, + DocumentDrop_DataTransferItemAsStringRequest, + DocumentDrop_DataTransferItemFileDataRequest, GetMatchTsConfigRequest, + GetServicePluginsRequest, GetVirtualCodeRequest, GetVirtualFileRequest, LoadedTSFilesMetaRequest, - ReloadProjectNotification, - WriteVirtualFilesNotification, - DocumentDropRequest, - DocumentDrop_DataTransferItemAsStringRequest, - DocumentDrop_DataTransferItemFileDataRequest, - UpdateVirtualCodeStateNotification, UpdateServicePluginStateNotification, - GetServicePluginsRequest, + UpdateVirtualCodeStateNotification, + WriteVirtualFilesNotification, } from '../../protocol'; -import type { ServerProjectProvider } from '../types'; -import type { DataTransferItem } from '@volar/language-service'; +import type { ServerBase } from '../types'; import { fileNameToUri } from '../uri'; -export function registerEditorFeatures( - connection: vscode.Connection, - projects: ServerProjectProvider, -) { +export function registerEditorFeatures(server: ServerBase) { const scriptVersions = new Map(); const scriptVersionSnapshots = new WeakSet(); - connection.onRequest(DocumentDropRequest.type, async ({ textDocument, position, dataTransfer }, token) => { + server.connection.onRequest(DocumentDropRequest.type, async ({ textDocument, position, dataTransfer }, token) => { const dataTransferMap = new Map(); @@ -35,7 +30,7 @@ export function registerEditorFeatures( dataTransferMap.set(item.mimeType, { value: item.value, asString() { - return connection.sendRequest(DocumentDrop_DataTransferItemAsStringRequest.type, { mimeType: item.mimeType }); + return server.connection.sendRequest(DocumentDrop_DataTransferItemAsStringRequest.type, { mimeType: item.mimeType }); }, asFile() { if (item.file) { @@ -43,7 +38,7 @@ export function registerEditorFeatures( name: item.file.name, uri: item.file.uri, data() { - return connection.sendRequest(DocumentDrop_DataTransferItemFileDataRequest.type, { mimeType: item.mimeType }); + return server.connection.sendRequest(DocumentDrop_DataTransferItemFileDataRequest.type, { mimeType: item.mimeType }); }, }; } @@ -51,18 +46,18 @@ export function registerEditorFeatures( }); } - const languageService = (await projects.getProject(textDocument.uri)).getLanguageService(); + const languageService = (await server.projects!.get.call(server, textDocument.uri)).getLanguageService(); return languageService.doDocumentDrop(textDocument.uri, position, dataTransferMap, token); }); - connection.onRequest(GetMatchTsConfigRequest.type, async params => { - const languageService = (await projects.getProject(params.uri)).getLanguageService(); + server.connection.onRequest(GetMatchTsConfigRequest.type, async params => { + const languageService = (await server.projects!.get.call(server, params.uri)).getLanguageService(); const configFileName = languageService.context.language.typescript?.projectHost.configFileName; if (configFileName) { return { uri: fileNameToUri(configFileName) }; } }); - connection.onRequest(GetVirtualFileRequest.type, async document => { - const languageService = (await projects.getProject(document.uri)).getLanguageService(); + server.connection.onRequest(GetVirtualFileRequest.type, async document => { + const languageService = (await server.projects!.get.call(server, document.uri)).getLanguageService(); const sourceScript = languageService.context.language.scripts.get(document.uri); if (sourceScript?.generated) { return prune(sourceScript.generated.root); @@ -86,8 +81,8 @@ export function registerEditorFeatures( }; } }); - connection.onRequest(GetVirtualCodeRequest.type, async params => { - const languageService = (await projects.getProject(params.fileUri)).getLanguageService(); + server.connection.onRequest(GetVirtualCodeRequest.type, async params => { + const languageService = (await server.projects!.get.call(server, params.fileUri)).getLanguageService(); const sourceScript = languageService.context.language.scripts.get(params.fileUri); const virtualCode = sourceScript?.generated?.embeddedCodes.get(params.virtualCodeId); if (virtualCode) { @@ -102,14 +97,11 @@ export function registerEditorFeatures( }; } }); - connection.onNotification(ReloadProjectNotification.type, () => { - projects.reloadProjects(); - }); - connection.onNotification(WriteVirtualFilesNotification.type, async params => { + server.connection.onNotification(WriteVirtualFilesNotification.type, async params => { const fsModeName = 'fs'; // avoid bundle const fs: typeof import('fs') = await import(fsModeName); - const languageService = (await projects.getProject(params.uri)).getLanguageService(); + const languageService = (await server.projects!.get.call(server, params.uri)).getLanguageService(); if (languageService.context.language.typescript) { @@ -146,14 +138,14 @@ export function registerEditorFeatures( } } }); - connection.onRequest(LoadedTSFilesMetaRequest.type, async () => { + server.connection.onRequest(LoadedTSFilesMetaRequest.type, async () => { const sourceFilesData = new Map(); - for (const project of await projects.getProjects()) { + for (const project of await server.projects!.all.call(server)) { const languageService = project.getLanguageService(); const tsLanguageService: ts.LanguageService | undefined = languageService.context.inject('typescript/languageService'); const program = tsLanguageService?.getProgram(); @@ -210,8 +202,8 @@ export function registerEditorFeatures( return result; }); - connection.onNotification(UpdateVirtualCodeStateNotification.type, async params => { - const project = await projects.getProject(params.fileUri); + server.connection.onNotification(UpdateVirtualCodeStateNotification.type, async params => { + const project = await server.projects!.get.call(server, params.fileUri); const context = project.getLanguageServiceDontCreate()?.context; if (context) { const virtualFileUri = project.getLanguageService().context.encodeEmbeddedDocumentUri(params.fileUri, params.virtualCodeId); @@ -223,8 +215,8 @@ export function registerEditorFeatures( } } }); - connection.onNotification(UpdateServicePluginStateNotification.type, async params => { - const project = await projects.getProject(params.uri); + server.connection.onNotification(UpdateServicePluginStateNotification.type, async params => { + const project = await server.projects!.get.call(server, params.uri); const context = project.getLanguageServiceDontCreate()?.context; if (context) { const service = context.services[params.serviceId as any][1]; @@ -236,8 +228,8 @@ export function registerEditorFeatures( } } }); - connection.onRequest(GetServicePluginsRequest.type, async params => { - const project = await projects.getProject(params.uri); + server.connection.onRequest(GetServicePluginsRequest.type, async params => { + const project = await server.projects!.get.call(server, params.uri); const context = project.getLanguageServiceDontCreate()?.context; if (context) { const result: GetServicePluginsRequest.ResponseType = []; diff --git a/packages/language-server/lib/register/registerLanguageFeatures.ts b/packages/language-server/lib/register/registerLanguageFeatures.ts index e79ed7c8..72c4dba3 100644 --- a/packages/language-server/lib/register/registerLanguageFeatures.ts +++ b/packages/language-server/lib/register/registerLanguageFeatures.ts @@ -1,15 +1,9 @@ import * as embedded from '@volar/language-service'; import * as vscode from 'vscode-languageserver'; import { AutoInsertRequest, FindFileReferenceRequest } from '../../protocol'; -import type { ServerProjectProvider } from '../types'; - -export function registerLanguageFeatures( - connection: vscode.Connection, - projectProvider: ServerProjectProvider, - initParams: vscode.InitializeParams, - semanticTokensLegend: vscode.SemanticTokensLegend, -) { +import type { ServerBase } from '../types'; +export function registerLanguageFeatures(server: ServerBase) { let lastCompleteUri: string; let lastCompleteLs: embedded.LanguageService; let lastCodeLensLs: embedded.LanguageService; @@ -18,53 +12,53 @@ export function registerLanguageFeatures( let lastDocumentLinkLs: embedded.LanguageService; let lastInlayHintLs: embedded.LanguageService; - connection.onDocumentFormatting(async (params, token) => { + server.connection.onDocumentFormatting(async (params, token) => { return worker(params.textDocument.uri, token, service => { return service.format(params.textDocument.uri, params.options, undefined, undefined, token); }); }); - connection.onDocumentRangeFormatting(async (params, token) => { + server.connection.onDocumentRangeFormatting(async (params, token) => { return worker(params.textDocument.uri, token, service => { return service.format(params.textDocument.uri, params.options, params.range, undefined, token); }); }); - connection.onDocumentOnTypeFormatting(async (params, token) => { + server.connection.onDocumentOnTypeFormatting(async (params, token) => { return worker(params.textDocument.uri, token, service => { return service.format(params.textDocument.uri, params.options, undefined, params, token); }); }); - connection.onSelectionRanges(async (params, token) => { + server.connection.onSelectionRanges(async (params, token) => { return worker(params.textDocument.uri, token, service => { return service.getSelectionRanges(params.textDocument.uri, params.positions, token); }); }); - connection.onFoldingRanges(async (params, token) => { + server.connection.onFoldingRanges(async (params, token) => { return worker(params.textDocument.uri, token, service => { return service.getFoldingRanges(params.textDocument.uri, token); }); }); - connection.languages.onLinkedEditingRange(async (params, token) => { + server.connection.languages.onLinkedEditingRange(async (params, token) => { return worker(params.textDocument.uri, token, service => { return service.findLinkedEditingRanges(params.textDocument.uri, params.position, token); }); }); - connection.onDocumentSymbol(async (params, token) => { + server.connection.onDocumentSymbol(async (params, token) => { return worker(params.textDocument.uri, token, service => { return service.findDocumentSymbols(params.textDocument.uri, token); }); }); - connection.onDocumentColor(async (params, token) => { + server.connection.onDocumentColor(async (params, token) => { return worker(params.textDocument.uri, token, service => { return service.findDocumentColors(params.textDocument.uri, token); }); }); - connection.onColorPresentation(async (params, token) => { + server.connection.onColorPresentation(async (params, token) => { return worker(params.textDocument.uri, token, service => { return service.getColorPresentations(params.textDocument.uri, params.color, params.range, token); }); }); - connection.onCompletion(async (params, token) => { + server.connection.onCompletion(async (params, token) => { return worker(params.textDocument.uri, token, async service => { lastCompleteUri = params.textDocument.uri; lastCompleteLs = service; @@ -80,24 +74,24 @@ export function registerLanguageFeatures( return list; }); }); - connection.onCompletionResolve(async (item, token) => { + server.connection.onCompletionResolve(async (item, token) => { if (lastCompleteUri && lastCompleteLs) { item = await lastCompleteLs.doCompletionResolve(item, token); fixTextEdit(item); } return item; }); - connection.onHover(async (params, token) => { + server.connection.onHover(async (params, token) => { return worker(params.textDocument.uri, token, service => { return service.doHover(params.textDocument.uri, params.position, token); }); }); - connection.onSignatureHelp(async (params, token) => { + server.connection.onSignatureHelp(async (params, token) => { return worker(params.textDocument.uri, token, service => { return service.getSignatureHelp(params.textDocument.uri, params.position, params.context, token); }); }); - connection.onPrepareRename(async (params, token) => { + server.connection.onPrepareRename(async (params, token) => { return worker(params.textDocument.uri, token, async service => { const result = await service.prepareRename(params.textDocument.uri, params.position, token); if (result && 'message' in result) { @@ -106,21 +100,21 @@ export function registerLanguageFeatures( return result; }); }); - connection.onRenameRequest(async (params, token) => { + server.connection.onRenameRequest(async (params, token) => { return worker(params.textDocument.uri, token, service => { return service.doRename(params.textDocument.uri, params.position, params.newName, token); }); }); - connection.onCodeLens(async (params, token) => { + server.connection.onCodeLens(async (params, token) => { return worker(params.textDocument.uri, token, async service => { lastCodeLensLs = service; return service.doCodeLens(params.textDocument.uri, token); }); }); - connection.onCodeLensResolve(async (codeLens, token) => { + server.connection.onCodeLensResolve(async (codeLens, token) => { return await lastCodeLensLs?.doCodeLensResolve(codeLens, token) ?? codeLens; }); - connection.onCodeAction(async (params, token) => { + server.connection.onCodeAction(async (params, token) => { return worker(params.textDocument.uri, token, async service => { lastCodeActionLs = service; let codeActions = await service.doCodeActions(params.textDocument.uri, params.range, params.context, token) ?? []; @@ -132,104 +126,99 @@ export function registerLanguageFeatures( codeAction.data = { uri: params.textDocument.uri }; } } - if (!initParams.capabilities.textDocument?.codeAction?.disabledSupport) { + if (!server.initializeParams?.capabilities.textDocument?.codeAction?.disabledSupport) { codeActions = codeActions.filter(codeAction => !codeAction.disabled); } return codeActions; }); }); - connection.onCodeActionResolve(async (codeAction, token) => { + server.connection.onCodeActionResolve(async (codeAction, token) => { return await lastCodeActionLs.doCodeActionResolve(codeAction, token) ?? codeAction; }); - connection.onReferences(async (params, token) => { + server.connection.onReferences(async (params, token) => { return worker(params.textDocument.uri, token, service => { return service.findReferences(params.textDocument.uri, params.position, { includeDeclaration: true }, token); }); }); - connection.onRequest(FindFileReferenceRequest.type, async (params, token) => { + server.connection.onRequest(FindFileReferenceRequest.type, async (params, token) => { return worker(params.textDocument.uri, token, service => { return service.findFileReferences(params.textDocument.uri, token); }); }); - connection.onImplementation(async (params, token) => { + server.connection.onImplementation(async (params, token) => { return worker(params.textDocument.uri, token, service => { return service.findImplementations(params.textDocument.uri, params.position, token); }); }); - connection.onDefinition(async (params, token) => { + server.connection.onDefinition(async (params, token) => { return worker(params.textDocument.uri, token, service => { return service.findDefinition(params.textDocument.uri, params.position, token); }); }); - connection.onTypeDefinition(async (params, token) => { + server.connection.onTypeDefinition(async (params, token) => { return worker(params.textDocument.uri, token, service => { return service.findTypeDefinition(params.textDocument.uri, params.position, token); }); }); - connection.onDocumentHighlight(async (params, token) => { + server.connection.onDocumentHighlight(async (params, token) => { return worker(params.textDocument.uri, token, service => { return service.findDocumentHighlights(params.textDocument.uri, params.position, token); }); }); - connection.onDocumentLinks(async (params, token) => { + server.connection.onDocumentLinks(async (params, token) => { return await worker(params.textDocument.uri, token, service => { lastDocumentLinkLs = service; return service.findDocumentLinks(params.textDocument.uri, token); }); }); - connection.onDocumentLinkResolve(async (link, token) => { + server.connection.onDocumentLinkResolve(async (link, token) => { return await lastDocumentLinkLs.doDocumentLinkResolve(link, token); }); - connection.onWorkspaceSymbol(async (params, token) => { - + server.connection.onWorkspaceSymbol(async (params, token) => { let results: vscode.WorkspaceSymbol[] = []; - - for (const project of await projectProvider.getProjects()) { - + for (const project of await server.projects!.all.call(server)) { if (token.isCancellationRequested) { return; } - results = results.concat(await project.getLanguageService().findWorkspaceSymbols(params.query, token)); } - return results; }); - connection.languages.callHierarchy.onPrepare(async (params, token) => { + server.connection.languages.callHierarchy.onPrepare(async (params, token) => { return await worker(params.textDocument.uri, token, async service => { lastCallHierarchyLs = service; return service.callHierarchy.doPrepare(params.textDocument.uri, params.position, token); }) ?? []; }); - connection.languages.callHierarchy.onIncomingCalls(async (params, token) => { + server.connection.languages.callHierarchy.onIncomingCalls(async (params, token) => { return await lastCallHierarchyLs?.callHierarchy.getIncomingCalls(params.item, token) ?? []; }); - connection.languages.callHierarchy.onOutgoingCalls(async (params, token) => { + server.connection.languages.callHierarchy.onOutgoingCalls(async (params, token) => { return await lastCallHierarchyLs?.callHierarchy.getOutgoingCalls(params.item, token) ?? []; }); - connection.languages.semanticTokens.on(async (params, token, _, resultProgress) => { + server.connection.languages.semanticTokens.on(async (params, token, _, resultProgress) => { return await worker(params.textDocument.uri, token, async service => { return await service?.getSemanticTokens( params.textDocument.uri, undefined, - semanticTokensLegend, + server.semanticTokensLegend, token, tokens => resultProgress?.report(tokens), ); }) ?? { data: [] }; }); - connection.languages.semanticTokens.onRange(async (params, token, _, resultProgress) => { + server.connection.languages.semanticTokens.onRange(async (params, token, _, resultProgress) => { return await worker(params.textDocument.uri, token, async service => { return await service?.getSemanticTokens( params.textDocument.uri, params.range, - semanticTokensLegend, + server.semanticTokensLegend, token, tokens => resultProgress?.report(tokens), ); }) ?? { data: [] }; }); - connection.languages.diagnostics.on(async (params, token, _workDoneProgressReporter, resultProgressReporter) => { + server.connection.languages.diagnostics.on(async (params, token, _workDoneProgressReporter, resultProgressReporter) => { const result = await worker(params.textDocument.uri, token, service => { return service.doValidation( params.textDocument.uri, @@ -252,32 +241,29 @@ export function registerLanguageFeatures( items: result ?? [], }; }); - connection.languages.inlayHint.on(async (params, token) => { + server.connection.languages.inlayHint.on(async (params, token) => { return worker(params.textDocument.uri, token, async service => { lastInlayHintLs = service; return service.getInlayHints(params.textDocument.uri, params.range, token); }); }); - connection.languages.inlayHint.resolve(async (hint, token) => { + server.connection.languages.inlayHint.resolve(async (hint, token) => { return await lastInlayHintLs.doInlayHintResolve(hint, token); }); - connection.workspace.onWillRenameFiles(async (params, token) => { - + server.connection.workspace.onWillRenameFiles(async (params, token) => { const _edits = await Promise.all(params.files.map(async file => { return await worker(file.oldUri, token, service => { return service.getEditsForFileRename(file.oldUri, file.newUri, token) ?? null; }) ?? null; })); const edits = _edits.filter((edit): edit is NonNullable => !!edit); - if (edits.length) { embedded.mergeWorkspaceEdits(edits[0], ...edits.slice(1)); return edits[0]; } - return null; }); - connection.onRequest(AutoInsertRequest.type, async (params, token) => { + server.connection.onRequest(AutoInsertRequest.type, async (params, token) => { return worker(params.textDocument.uri, token, service => { return service.doAutoInsert(params.textDocument.uri, params.selection, params.change, token); }); @@ -291,7 +277,7 @@ export function registerLanguageFeatures( resolve(undefined); return; } - const languageService = (await projectProvider.getProject(uri)).getLanguageService(); + const languageService = (await server.projects!.get.call(server, uri)).getLanguageService(); try { // handle TS cancel throw const result = await cb(languageService); if (token.isCancellationRequested) { @@ -307,8 +293,9 @@ export function registerLanguageFeatures( }, 0); }); } + function fixTextEdit(item: vscode.CompletionItem) { - const insertReplaceSupport = initParams.capabilities.textDocument?.completion?.completionItem?.insertReplaceSupport ?? false; + const insertReplaceSupport = server.initializeParams?.capabilities.textDocument?.completion?.completionItem?.insertReplaceSupport ?? false; if (!insertReplaceSupport) { if (item.textEdit && vscode.InsertReplaceEdit.is(item.textEdit)) { item.textEdit = vscode.TextEdit.replace(item.textEdit.insert, item.textEdit.newText); diff --git a/packages/language-server/lib/server.ts b/packages/language-server/lib/server.ts index 77171dd2..dd398583 100644 --- a/packages/language-server/lib/server.ts +++ b/packages/language-server/lib/server.ts @@ -1,45 +1,29 @@ -import { FileSystem, LanguagePlugin, ServiceEnvironment, LanguageServicePlugin, standardSemanticTokensLegend } from '@volar/language-service'; +import { FileSystem, LanguageServicePlugin, standardSemanticTokensLegend } from '@volar/language-service'; import { SnapshotDocument } from '@volar/snapshot-document'; import * as l10n from '@vscode/l10n'; import { configure as configureHttpRequests } from 'request-light'; -import type { TextDocuments } from 'vscode-languageserver'; import * as vscode from 'vscode-languageserver'; import { URI } from 'vscode-uri'; import { getServerCapabilities } from './serverCapabilities.js'; -import { DiagnosticModel, InitializationOptions, ProjectContext, ServerProjectProvider, ServerProjectProviderFactory, ServerRuntimeEnvironment } from './types.js'; +import type { VolarInitializeParams, ServerProjectProvider } from './types.js'; import { fileNameToUri } from './uri.js'; -import { createUriMap, type UriMap } from './utils/uriMap.js'; - -export interface ServerContext { - initializeParams: Omit & { initializationOptions?: InitializationOptions; }; - runtimeEnv: ServerRuntimeEnvironment; - onDidChangeWatchedFiles: vscode.Connection['onDidChangeWatchedFiles']; - onDidChangeConfiguration: vscode.Connection['onDidChangeConfiguration']; - getConfiguration(section: string, scopeUri?: string): Promise; - workspaceFolders: UriMap; - documents: TextDocuments; - reloadDiagnostics(): void; - updateDiagnosticsAndSemanticTokens(): void; -} +import { createUriMap } from './utils/uriMap.js'; +import { registerEditorFeatures } from './register/registerEditorFeatures.js'; +import { registerLanguageFeatures } from './register/registerLanguageFeatures.js'; -export interface ServerOptions { - watchFileExtensions?: string[]; - getServicePlugins(): LanguageServicePlugin[] | Promise; - getLanguagePlugins(serviceEnv: ServiceEnvironment, projectContext: ProjectContext): LanguagePlugin[] | Promise; - getLanguageId(uri: string): string; -} +export * from '@volar/snapshot-document'; export function createServerBase( connection: vscode.Connection, - getRuntimeEnv: (params: ServerContext['initializeParams']) => ServerRuntimeEnvironment, + getFs: (initializeParams: VolarInitializeParams) => FileSystem, ) { - - let context: ServerContext; - let projects: ServerProjectProvider; - let serverOptions: ServerOptions; let semanticTokensReq = 0; let documentUpdatedReq = 0; + const didChangeWatchedFilesCallbacks = new Set>(); + const didChangeConfigurationCallbacks = new Set>(); + const workspaceFolders = createUriMap(fileNameToUri); + const configurations = new Map>(); const documents = new vscode.TextDocuments({ create(uri, languageId, version, text) { return new SnapshotDocument(uri, languageId, version, text); @@ -49,143 +33,129 @@ export function createServerBase( return snapshot; }, }); - const didChangeWatchedFilesCallbacks = new Set>(); - const didChangeConfigurationCallbacks = new Set>(); - const workspaceFolders = createUriMap(fileNameToUri); - const configurations = new Map>(); documents.listen(connection); - return { + const status = { + connection, + initializeParams: undefined as unknown as VolarInitializeParams, + languageServicePlugins: [] as unknown as LanguageServicePlugin[], + projects: undefined as unknown as ServerProjectProvider, + fs: undefined as unknown as FileSystem, + semanticTokensLegend: undefined as unknown as vscode.SemanticTokensLegend, + pullModelDiagnostics: false, + documents, + workspaceFolders, initialize, initialized, shutdown, - get projects() { - return projects; - }, - get env() { - return context.runtimeEnv; - }, + watchFiles, + getConfiguration, + onDidChangeConfiguration, + onDidChangeWatchedFiles, + clearPushDiagnostics, + refresh, }; - - async function initialize( - params: ServerContext['initializeParams'], - projectProviderFactory: ServerProjectProviderFactory, - _serverOptions: ServerOptions, + return status; + + function initialize( + initializeParams: VolarInitializeParams, + languageServicePlugins: LanguageServicePlugin[], + projects: ServerProjectProvider, + options?: { + semanticTokensLegend?: vscode.SemanticTokensLegend; + pullModelDiagnostics?: boolean; + }, ) { - - serverOptions = _serverOptions; - const env = getRuntimeEnv(params); - context = { - initializeParams: params, - runtimeEnv: { - ...env, - fs: createFsWithCache(env.fs), - }, - getConfiguration(section, scopeUri) { - if (!params.capabilities.workspace?.configuration) { - return Promise.resolve(undefined); - } - if (!scopeUri && params.capabilities.workspace?.didChangeConfiguration) { - if (!configurations.has(section)) { - configurations.set(section, getConfigurationWorker(section, scopeUri)); - } - return configurations.get(section)!; - } - return getConfigurationWorker(section, scopeUri); - - async function getConfigurationWorker(section: string, scopeUri?: string) { - return (await connection.workspace.getConfiguration({ scopeUri, section })) ?? undefined /* replace null to undefined */; - } - }, - onDidChangeConfiguration(cb) { - didChangeConfigurationCallbacks.add(cb); - return { - dispose() { - didChangeConfigurationCallbacks.delete(cb); - }, - }; - }, - onDidChangeWatchedFiles: cb => { - didChangeWatchedFilesCallbacks.add(cb); - return { - dispose: () => { - didChangeWatchedFilesCallbacks.delete(cb); - }, - }; - }, - documents, - workspaceFolders, - reloadDiagnostics, - updateDiagnosticsAndSemanticTokens, - }; - - if (context.initializeParams.initializationOptions?.l10n) { - await l10n.config({ uri: context.initializeParams.initializationOptions.l10n.location }); + status.initializeParams = initializeParams; + status.languageServicePlugins = languageServicePlugins; + status.projects = projects; + status.semanticTokensLegend = options?.semanticTokensLegend ?? standardSemanticTokensLegend; + status.pullModelDiagnostics = options?.pullModelDiagnostics ?? false; + status.fs = createFsWithCache(getFs(initializeParams)); + + if (initializeParams.initializationOptions?.l10n) { + l10n.config({ uri: initializeParams.initializationOptions.l10n.location }); } - if (params.capabilities.workspace?.workspaceFolders && params.workspaceFolders) { - for (const folder of params.workspaceFolders) { + if (initializeParams.workspaceFolders?.length) { + for (const folder of initializeParams.workspaceFolders) { workspaceFolders.uriSet(folder.uri, true); } } - else if (params.rootUri) { - workspaceFolders.uriSet(params.rootUri, true); + else if (initializeParams.rootUri) { + workspaceFolders.uriSet(initializeParams.rootUri, true); } - else if (params.rootPath) { - workspaceFolders.uriSet(URI.file(params.rootPath).toString(), true); + else if (initializeParams.rootPath) { + workspaceFolders.uriSet(URI.file(initializeParams.rootPath).toString(), true); } - const servicePlugins = await serverOptions.getServicePlugins(); - const semanticTokensLegend = getSemanticTokensLegend(); - - projects = projectProviderFactory( - { ...context }, - servicePlugins, - serverOptions.getLanguagePlugins, - serverOptions.getLanguageId, - ); - - documents.onDidChangeContent(({ document }) => { - updateDiagnostics(document.uri); - }); - documents.onDidClose(({ document }) => { - if (!isServerPushEnabled()) { - return; - } - connection.sendDiagnostics({ uri: document.uri, diagnostics: [] }); - }); - context.onDidChangeConfiguration(updateDiagnosticsAndSemanticTokens); - - (await import('./register/registerEditorFeatures.js')).registerEditorFeatures(connection, projects); - (await import('./register/registerLanguageFeatures.js')).registerLanguageFeatures(connection, projects, params, semanticTokensLegend); - const result: vscode.InitializeResult = { - capabilities: getServerCapabilities(serverOptions.watchFileExtensions ?? [], servicePlugins, semanticTokensLegend), + capabilities: getServerCapabilities(status), }; - if (params.initializationOptions?.diagnosticModel !== DiagnosticModel.Pull) { + if (!status.pullModelDiagnostics) { result.capabilities.diagnosticProvider = undefined; + activateServerPushDiagnostics(projects); } + registerEditorFeatures(status); + registerLanguageFeatures(status); + return result; } function initialized() { registerWorkspaceFolderWatcher(); registerConfigurationWatcher(); - registerFileWatcher(); updateHttpSettings(); - context.onDidChangeConfiguration(updateHttpSettings); + onDidChangeConfiguration(updateHttpSettings); + } - async function updateHttpSettings() { - const httpSettings = await context.getConfiguration<{ proxyStrictSSL: boolean; proxy: string; }>('http'); - configureHttpRequests(httpSettings?.proxy, httpSettings?.proxyStrictSSL ?? false); + async function shutdown() { + for (const project of await status.projects.all.call(status)) { + project.dispose(); } } - function shutdown() { - projects?.reloadProjects(); + async function updateHttpSettings() { + const httpSettings = await getConfiguration<{ proxyStrictSSL: boolean; proxy: string; }>('http'); + configureHttpRequests(httpSettings?.proxy, httpSettings?.proxyStrictSSL ?? false); + } + + function getConfiguration(section: string, scopeUri?: string): Promise { + if (!status.initializeParams?.capabilities.workspace?.configuration) { + return Promise.resolve(undefined); + } + if (!scopeUri && status.initializeParams.capabilities.workspace?.didChangeConfiguration) { + if (!configurations.has(section)) { + configurations.set(section, getConfigurationWorker(section, scopeUri)); + } + return configurations.get(section)!; + } + return getConfigurationWorker(section, scopeUri); + } + + async function getConfigurationWorker(section: string, scopeUri?: string) { + return (await connection.workspace.getConfiguration({ scopeUri, section })) ?? undefined /* replace null to undefined */; + } + + function onDidChangeConfiguration(cb: vscode.NotificationHandler) { + didChangeConfigurationCallbacks.add(cb); + return { + dispose() { + didChangeConfigurationCallbacks.delete(cb); + }, + }; + } + + function onDidChangeWatchedFiles(cb: vscode.NotificationHandler) { + didChangeWatchedFilesCallbacks.add(cb); + return { + dispose: () => { + didChangeWatchedFilesCallbacks.delete(cb); + }, + }; } function createFsWithCache(fs: FileSystem): FileSystem { @@ -194,7 +164,7 @@ export function createServerBase( const statCache = new Map>(); const readDirectoryCache = new Map>(); - didChangeWatchedFilesCallbacks.add(({ changes }) => { + onDidChangeWatchedFiles(({ changes }) => { for (const change of changes) { if (change.type === vscode.FileChangeType.Deleted) { readFileCache.set(change.uri, undefined); @@ -237,21 +207,8 @@ export function createServerBase( }; } - function getSemanticTokensLegend() { - return { - tokenTypes: [ - ...standardSemanticTokensLegend.tokenTypes, - ...context.initializeParams.initializationOptions?.semanticTokensLegend?.tokenTypes ?? [], - ], - tokenModifiers: [ - ...standardSemanticTokensLegend.tokenModifiers, - ...context.initializeParams.initializationOptions?.semanticTokensLegend?.tokenModifiers ?? [], - ], - }; - } - function registerConfigurationWatcher() { - const didChangeConfiguration = context.initializeParams.capabilities.workspace?.didChangeConfiguration; + const didChangeConfiguration = status.initializeParams?.capabilities.workspace?.didChangeConfiguration; if (didChangeConfiguration) { connection.onDidChangeConfiguration(params => { configurations.clear(); @@ -265,9 +222,10 @@ export function createServerBase( } } - function registerFileWatcher() { - const didChangeWatchedFiles = context.initializeParams.capabilities.workspace?.didChangeWatchedFiles; - if (didChangeWatchedFiles && serverOptions.watchFileExtensions?.length) { + function watchFiles(patterns: string[]) { + const didChangeWatchedFiles = status.initializeParams?.capabilities.workspace?.didChangeWatchedFiles; + const fileOperations = status.initializeParams?.capabilities.workspace?.fileOperations; + if (didChangeWatchedFiles) { connection.onDidChangeWatchedFiles(e => { for (const cb of didChangeWatchedFilesCallbacks) { cb(e); @@ -275,18 +233,19 @@ export function createServerBase( }); if (didChangeWatchedFiles.dynamicRegistration) { connection.client.register(vscode.DidChangeWatchedFilesNotification.type, { - watchers: [ - { - globPattern: `**/*.{${serverOptions.watchFileExtensions.join(',')}}` - }, - ] + watchers: patterns.map(pattern => ({ globPattern: pattern })), }); } } + if (fileOperations?.dynamicRegistration && fileOperations.willRename) { + connection.client.register(vscode.WillRenameFilesRequest.type, { + filters: patterns.map(pattern => ({ pattern: { glob: pattern } })), + }); + } } function registerWorkspaceFolderWatcher() { - if (context.initializeParams.capabilities.workspace?.workspaceFolders) { + if (status.initializeParams?.capabilities.workspace?.workspaceFolders) { connection.workspace.onDidChangeWorkspaceFolders(e => { for (const folder of e.added) { workspaceFolders.uriSet(folder.uri, true); @@ -294,51 +253,54 @@ export function createServerBase( for (const folder of e.removed) { workspaceFolders.uriDelete(folder.uri); } - projects.reloadProjects(); + // projects.reloadProjects(); }); } } - function reloadDiagnostics() { - if (!isServerPushEnabled()) { - return; - } - for (const document of documents.all()) { + function activateServerPushDiagnostics(projects: ServerProjectProvider) { + documents.onDidChangeContent(({ document }) => { + pushAllDiagnostics(projects, document.uri); + }); + documents.onDidClose(({ document }) => { connection.sendDiagnostics({ uri: document.uri, diagnostics: [] }); + }); + onDidChangeConfiguration(() => refresh(projects)); + } + + function clearPushDiagnostics() { + if (!status.pullModelDiagnostics) { + for (const document of documents.all()) { + connection.sendDiagnostics({ uri: document.uri, diagnostics: [] }); + } } - updateDiagnosticsAndSemanticTokens(); } - async function updateDiagnosticsAndSemanticTokens() { + async function refresh(projects: ServerProjectProvider) { const req = ++semanticTokensReq; - await updateDiagnostics(); + if (!status.pullModelDiagnostics) { + await pushAllDiagnostics(projects); + } const delay = 250; await sleep(delay); if (req === semanticTokensReq) { - if (context.initializeParams.capabilities.workspace?.semanticTokens?.refreshSupport) { + if (status.initializeParams?.capabilities.workspace?.semanticTokens?.refreshSupport) { connection.languages.semanticTokens.refresh(); } - if (context.initializeParams.capabilities.workspace?.inlayHint?.refreshSupport) { + if (status.initializeParams?.capabilities.workspace?.inlayHint?.refreshSupport) { connection.languages.inlayHint.refresh(); } - if (isServerPushEnabled()) { - if (context.initializeParams.capabilities.workspace?.diagnostics?.refreshSupport) { - connection.languages.diagnostics.refresh(); - } + if (status.pullModelDiagnostics && status.initializeParams?.capabilities.workspace?.diagnostics?.refreshSupport) { + connection.languages.diagnostics.refresh(); } } } - async function updateDiagnostics(docUri?: string) { - - if (!isServerPushEnabled()) { - return; - } - + async function pushAllDiagnostics(projects: ServerProjectProvider, docUri?: string) { const req = ++documentUpdatedReq; const delay = 250; const token: vscode.CancellationToken = { @@ -355,7 +317,7 @@ export function createServerBase( if (token.isCancellationRequested) { return; } - await sendDocumentDiagnostics(changeDoc.uri, changeDoc.version, token); + await pushDiagnostics(projects, changeDoc.uri, changeDoc.version, token); } for (const doc of otherDocs) { @@ -363,23 +325,18 @@ export function createServerBase( if (token.isCancellationRequested) { break; } - await sendDocumentDiagnostics(doc.uri, doc.version, token); + await pushDiagnostics(projects, doc.uri, doc.version, token); } } - async function sendDocumentDiagnostics(uri: string, version: number, cancel: vscode.CancellationToken) { - - const languageService = (await projects.getProject(uri)).getLanguageService(); + async function pushDiagnostics(projects: ServerProjectProvider, uri: string, version: number, cancel: vscode.CancellationToken) { + const languageService = (await projects.get.call(status, uri)).getLanguageService(); const errors = await languageService.doValidation(uri, cancel, result => { connection.sendDiagnostics({ uri: uri, diagnostics: result, version }); }); connection.sendDiagnostics({ uri: uri, diagnostics: errors, version }); } - - function isServerPushEnabled() { - return (context.initializeParams.initializationOptions?.diagnosticModel ?? DiagnosticModel.Push) === DiagnosticModel.Push; - } } function sleep(ms: number) { diff --git a/packages/language-server/lib/serverCapabilities.ts b/packages/language-server/lib/serverCapabilities.ts index cb11e82d..fb99a798 100644 --- a/packages/language-server/lib/serverCapabilities.ts +++ b/packages/language-server/lib/serverCapabilities.ts @@ -1,11 +1,7 @@ -import type { LanguageServicePlugin } from '@volar/language-service'; import * as vscode from 'vscode-languageserver'; +import type { ServerBase } from './types'; -export function getServerCapabilities( - watchExts: string[], - servicePlugins: LanguageServicePlugin[], - semanticTokensLegend: vscode.SemanticTokensLegend, -) { +export function getServerCapabilities(server: ServerBase) { const capabilities: vscode.ServerCapabilities = { textDocumentSync: vscode.TextDocumentSyncKind.Incremental, workspace: { @@ -30,11 +26,11 @@ export function getServerCapabilities( hoverProvider: true, renameProvider: { prepareProvider: true }, signatureHelpProvider: { - triggerCharacters: [...new Set(servicePlugins.map(service => service.signatureHelpTriggerCharacters ?? []).flat())], - retriggerCharacters: [...new Set(servicePlugins.map(service => service.signatureHelpRetriggerCharacters ?? []).flat())], + triggerCharacters: [...new Set(server.languageServicePlugins.map(service => service.signatureHelpTriggerCharacters ?? []).flat())], + retriggerCharacters: [...new Set(server.languageServicePlugins.map(service => service.signatureHelpRetriggerCharacters ?? []).flat())], }, completionProvider: { - triggerCharacters: [...new Set(servicePlugins.map(service => service.triggerCharacters ?? []).flat())], + triggerCharacters: [...new Set(server.languageServicePlugins.map(service => service.triggerCharacters ?? []).flat())], resolveProvider: true, }, documentHighlightProvider: true, @@ -43,7 +39,7 @@ export function getServerCapabilities( semanticTokensProvider: { range: true, full: false, - legend: semanticTokensLegend, + legend: server.semanticTokensLegend, }, codeActionProvider: { codeActionKinds: [ @@ -67,7 +63,7 @@ export function getServerCapabilities( }, }; - const characters = [...new Set(servicePlugins.map(service => service.autoFormatTriggerCharacters ?? []).flat())]; + const characters = [...new Set(server.languageServicePlugins.map(service => service.autoFormatTriggerCharacters ?? []).flat())]; if (characters.length) { capabilities.documentOnTypeFormattingProvider = { firstTriggerCharacter: characters[0], @@ -75,21 +71,5 @@ export function getServerCapabilities( }; } - if (watchExts.length) { - capabilities.workspace = { - fileOperations: { - willRename: { - filters: [ - { - pattern: { - glob: `**/*.{${watchExts.join(',')}}` - } - }, - ] - } - } - }; - } - return capabilities; } diff --git a/packages/language-server/lib/types.ts b/packages/language-server/lib/types.ts index 12bf803f..c114c0e4 100644 --- a/packages/language-server/lib/types.ts +++ b/packages/language-server/lib/types.ts @@ -1,39 +1,17 @@ -import type { FileSystem, LanguageService, LanguageServicePlugin, TypeScriptProjectHost } from '@volar/language-service'; +import type { LanguageService } from '@volar/language-service'; import type * as vscode from 'vscode-languageserver'; -import type { ServerContext, ServerOptions } from './server'; -import type { createSys } from '@volar/typescript'; - -export interface ServerRuntimeEnvironment { - fs: FileSystem; -} - -export interface ProjectContext { - typescript?: { - configFileName: string | undefined; - host: TypeScriptProjectHost; - sys: ReturnType; - }; -} - -export enum DiagnosticModel { - None = 0, - Push = 1, - Pull = 2, -} +import type { createServerBase } from './server'; export interface InitializationOptions { l10n?: { location: string; // uri }; - diagnosticModel?: DiagnosticModel; maxFileSize?: number; - /** - * Extra semantic token types and modifiers that are supported by the client. - */ - semanticTokensLegend?: vscode.SemanticTokensLegend; codegenStack?: boolean; } +export type VolarInitializeParams = Omit & { initializationOptions?: InitializationOptions; };; + export interface ServerProject { getLanguageService(): LanguageService; getLanguageServiceDontCreate(): LanguageService | undefined; @@ -41,16 +19,8 @@ export interface ServerProject { } export interface ServerProjectProvider { - getProject(uri: string): Promise; - getProjects(): Promise; - reloadProjects(): Promise | void; + get(this: ServerBase, uri: string): Promise; + all(this: ServerBase): Promise; } -export interface ServerProjectProviderFactory { - ( - context: ServerContext, - servicePlugins: LanguageServicePlugin[], - getLanguagePlugins: ServerOptions['getLanguagePlugins'], - getLanguageId: ServerOptions['getLanguageId'], - ): ServerProjectProvider; -} +export type ServerBase = ReturnType; diff --git a/packages/language-server/node.ts b/packages/language-server/node.ts index ecf6d3e9..cd7b1216 100644 --- a/packages/language-server/node.ts +++ b/packages/language-server/node.ts @@ -10,6 +10,7 @@ export * from 'vscode-languageserver/node'; export * from './index'; export * from './lib/project/simpleProjectProvider'; export * from './lib/project/typescriptProjectProvider'; +export * from './lib/server'; export function createFs(options: InitializationOptions): FileSystem { return { @@ -80,9 +81,7 @@ export function createConnection() { } export function createServer(connection: vscode.Connection) { - return createServerBase(connection, params => ({ - fs: createFs(params.initializationOptions ?? {}), - })); + return createServerBase(connection, params => createFs(params.initializationOptions ?? {})); } export function loadTsdkByPath(tsdk: string, locale: string | undefined) { diff --git a/packages/language-server/protocol.ts b/packages/language-server/protocol.ts index ed4f0894..8fadb3d8 100644 --- a/packages/language-server/protocol.ts +++ b/packages/language-server/protocol.ts @@ -57,10 +57,6 @@ export namespace WriteVirtualFilesNotification { export const type = new vscode.NotificationType('volar/client/writeVirtualFiles'); } -export namespace ReloadProjectNotification { - export const type = new vscode.NotificationType('volar/client/reloadProject'); -} - /** * Document Drop */ diff --git a/packages/language-service/lib/languageService.ts b/packages/language-service/lib/languageService.ts index 091a1eb4..ed12b37e 100644 --- a/packages/language-service/lib/languageService.ts +++ b/packages/language-service/lib/languageService.ts @@ -35,7 +35,7 @@ import * as documentLinkResolve from './features/resolveDocumentLink'; import * as inlayHintResolve from './features/resolveInlayHint'; import type { ServiceContext, ServiceEnvironment, LanguageServicePlugin } from './types'; -import type { CodeInformation, LinkedCodeMap, SourceMap, VirtualCode } from '@volar/language-core'; +import type { CodeInformation, LinkedCodeMap, SourceMap } from '@volar/language-core'; import type * as ts from 'typescript'; import { TextDocument } from 'vscode-languageserver-textdocument'; @@ -54,7 +54,7 @@ export function createLanguageService( const context: ServiceContext = { language, documents: { - get(uri: string, languageId: string, snapshot: ts.IScriptSnapshot) { + get(uri, languageId, snapshot) { if (!snapshot2Doc.has(snapshot)) { snapshot2Doc.set(snapshot, new Map()); } @@ -71,7 +71,7 @@ export function createLanguageService( } return map.get(uri)!; }, - *getMaps(virtualCode: VirtualCode) { + *getMaps(virtualCode) { for (const [uri, [snapshot, map]] of context.language.maps.forEach(virtualCode)) { if (!map2DocMap.has(map)) { const embeddedUri = context.encodeEmbeddedDocumentUri(uri, virtualCode.id); @@ -84,7 +84,7 @@ export function createLanguageService( yield map2DocMap.get(map)!; } }, - getLinkedCodeMap(virtualCode: VirtualCode, sourceScriptId: string) { + getLinkedCodeMap(virtualCode, sourceScriptId) { const map = context.language.linkedCodeMaps.get(virtualCode); if (map) { if (!mirrorMap2DocMirrorMap.has(map)) { diff --git a/packages/vscode/index.ts b/packages/vscode/index.ts index ac371e03..d15a8a15 100644 --- a/packages/vscode/index.ts +++ b/packages/vscode/index.ts @@ -4,7 +4,6 @@ export { activate as activateAutoInsertion } from './lib/features/autoInsertion' export { activate as activateDocumentDropEdit } from './lib/features/documentDropEdits'; export { activate as activateWriteVirtualFiles } from './lib/features/writeVirtualFiles'; export { activate as activateFindFileReferences } from './lib/features/fileReferences'; -export { activate as activateReloadProjects } from './lib/features/reloadProject'; export { activate as activateTsConfigStatusItem } from './lib/features/tsconfig'; export { activate as activateServerSys } from './lib/features/serverSys'; export { activate as activateTsVersionStatusItem, getTsdk } from './lib/features/tsVersion'; diff --git a/packages/vscode/lib/features/reloadProject.ts b/packages/vscode/lib/features/reloadProject.ts deleted file mode 100644 index 80f81284..00000000 --- a/packages/vscode/lib/features/reloadProject.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as vscode from 'vscode'; -import type { BaseLanguageClient } from 'vscode-languageclient'; -import { ReloadProjectNotification } from '@volar/language-server/protocol'; - -export function activate(cmd: string, client: BaseLanguageClient) { - return vscode.commands.registerCommand(cmd, () => { - if (vscode.window.activeTextEditor) { - client.sendNotification(ReloadProjectNotification.type, client.code2ProtocolConverter.asTextDocumentIdentifier(vscode.window.activeTextEditor.document)); - } - }); -}