diff --git a/packages/language-server/lib/project/simpleProjectProvider.ts b/packages/language-server/lib/project/simpleProjectProvider.ts index 4d700e7a..9694ba05 100644 --- a/packages/language-server/lib/project/simpleProjectProvider.ts +++ b/packages/language-server/lib/project/simpleProjectProvider.ts @@ -1,15 +1,14 @@ import type { LanguagePlugin, ServiceEnvironment } from '@volar/language-service'; import { URI } from 'vscode-uri'; import type { ServerBase, ServerProject, ServerProjectProvider } from '../types'; -import { fileNameToUri, uriToFileName } from '../uri'; -import type { UriMap } from '../utils/uriMap'; +import { createUriMap, type UriMap } from '../utils/uriMap'; import { createSimpleServerProject } from './simpleProject'; export function createSimpleProjectProvider(languagePlugins: LanguagePlugin[]): ServerProjectProvider { - const map = new Map>(); + const map = createUriMap>(); return { get(uri) { - const workspaceFolder = getWorkspaceFolder(uri, this.workspaceFolders); + const workspaceFolder = getWorkspaceFolder(URI.parse(uri), this.workspaceFolders); let projectPromise = map.get(workspaceFolder); if (!projectPromise) { const serviceEnv = createServiceEnvironment(this, workspaceFolder); @@ -30,9 +29,9 @@ export function createSimpleProjectProvider(languagePlugins: LanguagePlugin[]): }; } -export function createServiceEnvironment(server: ServerBase, workspaceFolder: string): ServiceEnvironment { +export function createServiceEnvironment(server: ServerBase, workspaceFolder: URI): ServiceEnvironment { return { - workspaceFolder, + workspaceFolder: workspaceFolder.toString(), fs: server.fs, locale: server.initializeParams?.locale, clientCapabilities: server.initializeParams?.capabilities, @@ -40,30 +39,27 @@ export function createServiceEnvironment(server: ServerBase, workspaceFolder: st onDidChangeConfiguration: server.onDidChangeConfiguration, onDidChangeWatchedFiles: server.onDidChangeWatchedFiles, typescript: { - fileNameToUri: fileNameToUri, - uriToFileName: uriToFileName, + fileNameToUri: server.uriConverter.fileNameToUri, + uriToFileName: server.uriConverter.uriToFileName, }, }; } -export function getWorkspaceFolder(uri: string, workspaceFolders: UriMap) { - - let parsed = URI.parse(uri); - +export function getWorkspaceFolder(uri: URI, workspaceFolders: UriMap) { while (true) { - if (workspaceFolders.uriHas(parsed.toString())) { - return parsed.toString(); + if (workspaceFolders.has(uri)) { + return uri; } - const next = URI.parse(uri).with({ path: parsed.path.substring(0, parsed.path.lastIndexOf('/')) }); - if (next.path === parsed.path) { + const next = uri.with({ path: uri.path.substring(0, uri.path.lastIndexOf('/')) }); + if (next.path === uri.path) { break; } - parsed = next; + uri = next; } - for (const folder of workspaceFolders.uriKeys()) { + for (const folder of workspaceFolders.keys()) { return folder; } - return URI.parse(uri).with({ path: '/' }).toString(); + return uri.with({ path: '/' }); } diff --git a/packages/language-server/lib/project/typescriptProject.ts b/packages/language-server/lib/project/typescriptProject.ts index 8a377d72..1709e2da 100644 --- a/packages/language-server/lib/project/typescriptProject.ts +++ b/packages/language-server/lib/project/typescriptProject.ts @@ -3,8 +3,8 @@ 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 { URI } from 'vscode-uri'; import type { ServerBase, ServerProject } from '../types'; -import { fileNameToUri, uriToFileName } from '../uri'; import { UriMap, createUriMap } from '../utils/uriMap'; export interface TypeScriptServerProject extends ServerProject { @@ -30,7 +30,7 @@ export async function createTypeScriptServerProject( let projectVersion = 0; let languageService: LanguageService | undefined; - const sys = createSys(ts, serviceEnv, uriToFileName(serviceEnv.workspaceFolder)); + const sys = createSys(ts, serviceEnv, server.uriConverter.uriToFileName(serviceEnv.workspaceFolder)); const host: TypeScriptProjectHost = { ...sys, configFileName: typeof tsconfig === 'string' ? tsconfig : undefined, @@ -41,7 +41,7 @@ export async function createTypeScriptServerProject( return sys.sync(); }, getCurrentDirectory() { - return uriToFileName(serviceEnv.workspaceFolder); + return server.uriConverter.uriToFileName(serviceEnv.workspaceFolder); }, getProjectVersion() { return projectVersion.toString(); @@ -50,10 +50,11 @@ export async function createTypeScriptServerProject( return rootFiles; }, getScriptSnapshot(fileName) { - askedFiles.pathSet(fileName, true); - const doc = server.documents.get(fileNameToUri(fileName)); - if (doc) { - return doc.getSnapshot(); + const uri = server.uriConverter.fileNameToUri(fileName); + askedFiles.set(URI.parse(uri), true); + const document = server.documents.get(uri); + if (document) { + return document.getSnapshot(); } }, getCompilationSettings() { @@ -71,7 +72,7 @@ export async function createTypeScriptServerProject( host, sys, }); - const askedFiles = createUriMap(fileNameToUri); + const askedFiles = createUriMap(); const docChangeWatcher = server.documents.onDidChangeContent(() => { projectVersion++; }); @@ -99,7 +100,7 @@ export async function createTypeScriptServerProject( parsedCommandLine = await createParsedCommandLine( ts, sys, - uriToFileName(serviceEnv.workspaceFolder), + server.uriConverter.uriToFileName(serviceEnv.workspaceFolder), tsconfig, languagePlugins.map(plugin => plugin.typescript?.extraFileExtensions ?? []).flat(), ); diff --git a/packages/language-server/lib/project/typescriptProjectProvider.ts b/packages/language-server/lib/project/typescriptProjectProvider.ts index 52a76665..3bcb5fb8 100644 --- a/packages/language-server/lib/project/typescriptProjectProvider.ts +++ b/packages/language-server/lib/project/typescriptProjectProvider.ts @@ -5,7 +5,6 @@ import type * as ts from 'typescript'; import * as vscode from 'vscode-languageserver'; import { URI } from 'vscode-uri'; 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'; @@ -25,8 +24,8 @@ export function createTypeScriptProjectProvider( ) { let initialized = false; - const configProjects = createUriMap>(fileNameToUri); - const inferredProjects = createUriMap>(fileNameToUri); + const configProjects = createUriMap>(); + const inferredProjects = createUriMap>(); const rootTsConfigs = new Set(); const searchedDirs = new Set(); const projects: ServerProjectProvider = { @@ -35,23 +34,24 @@ export function createTypeScriptProjectProvider( initialized = true; initialize(this); } - const tsconfig = await findMatchTSConfig(this, URI.parse(uri)); + const parsedUri = URI.parse(uri); + const tsconfig = await findMatchTSConfig(this, parsedUri); if (tsconfig) { return await getOrCreateConfiguredProject(this, tsconfig); } - const workspaceFolder = getWorkspaceFolder(uri, this.workspaceFolders); + const workspaceFolder = getWorkspaceFolder(parsedUri, this.workspaceFolders); return await getOrCreateInferredProject(this, uri, workspaceFolder); }, async all() { return await Promise.all([ - ...configProjects.values(), - ...inferredProjects.values(), + ...configProjects.values() ?? [], + ...inferredProjects.values() ?? [], ]); }, reload() { for (const project of [ - ...configProjects.values(), - ...inferredProjects.values(), + ...configProjects.values() ?? [], + ...inferredProjects.values() ?? [], ]) { project.then(p => p.dispose()); } @@ -66,15 +66,17 @@ export function createTypeScriptProjectProvider( const tsConfigChanges = changes.filter(change => rootTsConfigNames.includes(change.uri.substring(change.uri.lastIndexOf('/') + 1))); for (const change of tsConfigChanges) { + const changeUri = URI.parse(change.uri); + const changeFileName = server.uriConverter.uriToFileName(change.uri, changeUri); if (change.type === vscode.FileChangeType.Created) { - rootTsConfigs.add(uriToFileName(change.uri)); + rootTsConfigs.add(changeFileName); } - else if ((change.type === vscode.FileChangeType.Changed || change.type === vscode.FileChangeType.Deleted) && configProjects.uriHas(change.uri)) { + else if ((change.type === vscode.FileChangeType.Changed || change.type === vscode.FileChangeType.Deleted) && configProjects.has(changeUri)) { if (change.type === vscode.FileChangeType.Deleted) { - rootTsConfigs.delete(uriToFileName(change.uri)); + rootTsConfigs.delete(changeFileName); } - const project = configProjects.uriGet(change.uri); - configProjects.uriDelete(change.uri); + const project = configProjects.get(changeUri); + configProjects.delete(changeUri); project?.then(project => project.dispose()); } } @@ -88,8 +90,9 @@ export function createTypeScriptProjectProvider( async function findMatchTSConfig(server: ServerBase, uri: URI) { - const filePath = uriToFileName(uri.toString()); - let dir = path.dirname(filePath); + const fileName = server.uriConverter.uriToFileName(uri.toString()); + + let dir = path.dirname(fileName); while (true) { if (searchedDirs.has(dir)) { @@ -98,7 +101,7 @@ export function createTypeScriptProjectProvider( searchedDirs.add(dir); for (const tsConfigName of rootTsConfigNames) { const tsconfigPath = path.join(dir, tsConfigName); - if ((await server.fs.stat?.(fileNameToUri(tsconfigPath)))?.type === FileType.File) { + if ((await server.fs.stat?.(server.uriConverter.fileNameToUri(tsconfigPath)))?.type === FileType.File) { rootTsConfigs.add(tsconfigPath); } } @@ -114,12 +117,12 @@ export function createTypeScriptProjectProvider( let matches: string[] = []; for (const rootTsConfig of rootTsConfigs) { - if (isFileInDir(uriToFileName(uri.toString()), path.dirname(rootTsConfig))) { + if (isFileInDir(fileName, path.dirname(rootTsConfig))) { matches.push(rootTsConfig); } } - matches = matches.sort((a, b) => sortTSConfigs(uriToFileName(uri.toString()), a, b)); + matches = matches.sort((a, b) => sortTSConfigs(fileName, a, b)); if (matches.length) { await getParsedCommandLine(matches[0]); @@ -127,26 +130,29 @@ export function createTypeScriptProjectProvider( } function findIndirectReferenceTsconfig() { return findTSConfig(async tsconfig => { - const project = await configProjects.pathGet(tsconfig); - return project?.askedFiles.uriHas(uri.toString()) ?? false; + const tsconfigUri = URI.parse(server.uriConverter.fileNameToUri(tsconfig)); + const project = await configProjects.get(tsconfigUri); + return project?.askedFiles.has(uri) ?? false; }); } function findDirectIncludeTsconfig() { return findTSConfig(async tsconfig => { - const map = createUriMap(fileNameToUri); + const map = createUriMap(); const parsedCommandLine = await getParsedCommandLine(tsconfig); for (const fileName of parsedCommandLine?.fileNames ?? []) { - map.pathSet(fileName, true); + const uri = URI.parse(server.uriConverter.fileNameToUri(fileName)); + map.set(uri, true); } - return map.uriHas(uri.toString()); + return map.has(uri); }); } async function findTSConfig(match: (tsconfig: string) => Promise | boolean) { const checked = new Set(); - for (const rootTsConfig of [...rootTsConfigs].sort((a, b) => sortTSConfigs(uriToFileName(uri.toString()), a, b))) { - const project = await configProjects.pathGet(rootTsConfig); + for (const rootTsConfig of [...rootTsConfigs].sort((a, b) => sortTSConfigs(fileName, a, b))) { + const tsconfigUri = URI.parse(server.uriConverter.fileNameToUri(rootTsConfig)); + const project = await configProjects.get(tsconfigUri); if (project) { let chains = await getReferencesChains(project.getParsedCommandLine(), rootTsConfig, []); @@ -182,13 +188,13 @@ export function createTypeScriptProjectProvider( let tsConfigPath = projectReference.path.replace(/\\/g, '/'); // fix https://github.com/johnsoncodehk/volar/issues/712 - if ((await server.fs.stat?.(fileNameToUri(tsConfigPath)))?.type === FileType.File) { + if ((await server.fs.stat?.(server.uriConverter.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) { + if ((await server.fs.stat?.(server.uriConverter.fileNameToUri(newTsConfigPath)))?.type === FileType.File) { tsConfigPath = newTsConfigPath; } - else if ((await server.fs.stat?.(fileNameToUri(newJsConfigPath)))?.type === FileType.File) { + else if ((await server.fs.stat?.(server.uriConverter.fileNameToUri(newJsConfigPath)))?.type === FileType.File) { tsConfigPath = newJsConfigPath; } } @@ -221,9 +227,10 @@ export function createTypeScriptProjectProvider( function getOrCreateConfiguredProject(server: ServerBase, tsconfig: string) { tsconfig = tsconfig.replace(/\\/g, '/'); - let projectPromise = configProjects.pathGet(tsconfig); + const tsconfigUri = URI.parse(server.uriConverter.fileNameToUri(tsconfig)); + let projectPromise = configProjects.get(tsconfigUri); if (!projectPromise) { - const workspaceFolder = getWorkspaceFolder(fileNameToUri(tsconfig), server.workspaceFolders); + const workspaceFolder = getWorkspaceFolder(tsconfigUri, server.workspaceFolders); const serviceEnv = createServiceEnvironment(server, workspaceFolder); projectPromise = createTypeScriptServerProject( ts, @@ -233,15 +240,15 @@ export function createTypeScriptProjectProvider( serviceEnv, getLanguagePlugins, ); - configProjects.pathSet(tsconfig, projectPromise); + configProjects.set(tsconfigUri, projectPromise); } return projectPromise; } - async function getOrCreateInferredProject(server: ServerBase, uri: string, workspaceFolder: string) { + async function getOrCreateInferredProject(server: ServerBase, uri: string, workspaceFolder: URI) { - if (!inferredProjects.uriHas(workspaceFolder)) { - inferredProjects.uriSet(workspaceFolder, (async () => { + if (!inferredProjects.has(workspaceFolder)) { + inferredProjects.set(workspaceFolder, (async () => { const inferOptions = await getInferredCompilerOptions(server); const serviceEnv = createServiceEnvironment(server, workspaceFolder); return createTypeScriptServerProject( @@ -255,9 +262,9 @@ export function createTypeScriptProjectProvider( })()); } - const project = await inferredProjects.uriGet(workspaceFolder.toString())!; + const project = await inferredProjects.get(workspaceFolder)!; - project.tryAddFile(uriToFileName(uri)); + project.tryAddFile(server.uriConverter.uriToFileName(uri)); return project; } diff --git a/packages/language-server/lib/register/registerEditorFeatures.ts b/packages/language-server/lib/register/registerEditorFeatures.ts index 9f145dd8..10c50cfc 100644 --- a/packages/language-server/lib/register/registerEditorFeatures.ts +++ b/packages/language-server/lib/register/registerEditorFeatures.ts @@ -15,7 +15,6 @@ import { WriteVirtualFilesNotification, } from '../../protocol'; import type { ServerBase } from '../types'; -import { fileNameToUri } from '../uri'; export function registerEditorFeatures(server: ServerBase) { @@ -53,7 +52,7 @@ export function registerEditorFeatures(server: ServerBase) { 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) }; + return { uri: server.uriConverter.fileNameToUri(configFileName) }; } }); server.connection.onRequest(GetVirtualFileRequest.type, async document => { diff --git a/packages/language-server/lib/server.ts b/packages/language-server/lib/server.ts index 77801ba9..3d48aad6 100644 --- a/packages/language-server/lib/server.ts +++ b/packages/language-server/lib/server.ts @@ -7,22 +7,21 @@ import { URI } from 'vscode-uri'; import { registerEditorFeatures } from './register/registerEditorFeatures.js'; import { registerLanguageFeatures } from './register/registerLanguageFeatures.js'; import { getServerCapabilities } from './serverCapabilities.js'; -import type { ServerProjectProvider, VolarInitializeParams } from './types.js'; -import { fileNameToUri } from './uri.js'; +import type { InitializationOptions, ServerProjectProvider, VolarInitializeParams } from './types.js'; +import { UriConverter, createUriConverter } from './uri.js'; import { createUriMap } from './utils/uriMap.js'; export * from '@volar/snapshot-document'; export function createServerBase( connection: vscode.Connection, - getFs: (initializeParams: VolarInitializeParams) => FileSystem, + getFs: (options: InitializationOptions, uriConverter: UriConverter) => FileSystem, ) { 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) { @@ -33,6 +32,8 @@ export function createServerBase( return snapshot; }, }); + const uriConverter = createUriConverter(documents); + const workspaceFolders = createUriMap(); documents.listen(connection); @@ -45,6 +46,7 @@ export function createServerBase( semanticTokensLegend: undefined as unknown as vscode.SemanticTokensLegend, pullModelDiagnostics: false, documents, + uriConverter, workspaceFolders, initialize, initialized, @@ -72,7 +74,7 @@ export function createServerBase( status.projects = projects; status.semanticTokensLegend = options?.semanticTokensLegend ?? standardSemanticTokensLegend; status.pullModelDiagnostics = options?.pullModelDiagnostics ?? false; - status.fs = createFsWithCache(getFs(initializeParams)); + status.fs = createFsWithCache(getFs(initializeParams.initializationOptions ?? {}, status.uriConverter)); if (initializeParams.initializationOptions?.l10n) { l10n.config({ uri: initializeParams.initializationOptions.l10n.location }); @@ -80,14 +82,14 @@ export function createServerBase( if (initializeParams.workspaceFolders?.length) { for (const folder of initializeParams.workspaceFolders) { - workspaceFolders.uriSet(folder.uri, true); + workspaceFolders.set(URI.parse(folder.uri), true); } } else if (initializeParams.rootUri) { - workspaceFolders.uriSet(initializeParams.rootUri, true); + workspaceFolders.set(URI.parse(initializeParams.rootUri), true); } else if (initializeParams.rootPath) { - workspaceFolders.uriSet(URI.file(initializeParams.rootPath).toString(), true); + workspaceFolders.set(URI.file(initializeParams.rootPath), true); } const result: vscode.InitializeResult = { @@ -248,10 +250,10 @@ export function createServerBase( if (status.initializeParams?.capabilities.workspace?.workspaceFolders) { connection.workspace.onDidChangeWorkspaceFolders(e => { for (const folder of e.added) { - workspaceFolders.uriSet(folder.uri, true); + workspaceFolders.set(URI.parse(folder.uri), true); } for (const folder of e.removed) { - workspaceFolders.uriDelete(folder.uri); + workspaceFolders.delete(URI.parse(folder.uri)); } status.projects.reload.call(status); }); diff --git a/packages/language-server/lib/uri.ts b/packages/language-server/lib/uri.ts index 95fc1500..adc1e33e 100644 --- a/packages/language-server/lib/uri.ts +++ b/packages/language-server/lib/uri.ts @@ -1,27 +1,65 @@ +import type { TextDocument, TextDocuments } from 'vscode-languageserver'; import { URI } from 'vscode-uri'; -const encodeds = new Map(); +export type UriConverter = ReturnType; -export function uriToFileName(uri: string) { - const parsed = URI.parse(uri); - if (parsed.scheme === 'file') { - return parsed.fsPath.replace(/\\/g, '/'); +export function createUriConverter(documents?: TextDocuments) { + const syncedDocumentUriToFileName = new Map(); + const syncedDocumentFileNameToUri = new Map(); + const encodeds = new Map(); + + documents?.onDidOpen(({ document }) => { + const fileName = uriToFileName(document.uri); + syncedDocumentUriToFileName.set(document.uri, fileName); + syncedDocumentFileNameToUri.set(fileName, document.uri); + }); + documents?.onDidClose(e => { + const fileName = syncedDocumentUriToFileName.get(e.document.uri); + assert(fileName, 'fileName not found'); + syncedDocumentUriToFileName.delete(e.document.uri); + syncedDocumentFileNameToUri.delete(fileName); + }); + + return { + uriToFileName, + fileNameToUri, + }; + + function uriToFileName(uri: string, parsed?: URI) { + const syncedDocumentFileName = syncedDocumentUriToFileName.get(uri); + if (syncedDocumentFileName) { + return syncedDocumentFileName; + } + parsed ??= URI.parse(uri); + if (parsed.scheme === 'file') { + return parsed.fsPath.replace(/\\/g, '/'); + } + const encoded = encodeURIComponent(`${parsed.scheme}://${parsed.authority}`); + encodeds.set(encoded, parsed); + return `/${encoded}${parsed.path}`; } - const encoded = encodeURIComponent(`${parsed.scheme}://${parsed.authority}`); - encodeds.set(encoded, parsed); - return `/${encoded}${parsed.path}`; -} -export function fileNameToUri(fileName: string) { - for (const [encoded, uri] of encodeds) { - const prefix = `/${encoded}`; - if (fileName.startsWith(prefix)) { - return URI.from({ - scheme: uri.scheme, - authority: uri.authority, - path: fileName.substring(prefix.length), - }).toString(); + function fileNameToUri(fileName: string) { + const syncedDocumentUri = syncedDocumentFileNameToUri.get(fileName); + if (syncedDocumentUri) { + return syncedDocumentUri; + } + for (const [encoded, uri] of encodeds) { + const prefix = `/${encoded}`; + if (fileName.startsWith(prefix)) { + return URI.from({ + scheme: uri.scheme, + authority: uri.authority, + path: fileName.substring(prefix.length), + }).toString(); + } } + return URI.file(fileName).toString(); + } +} + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) { + throw new Error(message); } - return URI.file(fileName).toString(); } diff --git a/packages/language-server/lib/utils/uriMap.ts b/packages/language-server/lib/utils/uriMap.ts index 128b1b7a..4be639a9 100644 --- a/packages/language-server/lib/utils/uriMap.ts +++ b/packages/language-server/lib/utils/uriMap.ts @@ -1,94 +1,63 @@ -import { URI } from 'vscode-uri'; -export * as _ from 'vscode-uri'; - -interface Options { - delete(key: string): boolean; - get(key: string): T | undefined; - has(key: string): boolean; - set(key: string, value: T): void; - clear(): void; - values(): IterableIterator; - keys(): IterableIterator; -} +import type { URI } from 'vscode-uri'; export type UriMap = ReturnType>; -export function createUriMap( - fileNameToUri: (fileName: string) => string, - map: Options = new Map() -) { - - const uriToUri = new Map(); - const pathToUri = new Map(); +export function createUriMap(caseSensitive = false) { + const map = new Map(); + const rawUriToNormalizedUri = new Map(); + const normalizedUriToRawUri = new Map(); return { - clear, - values, - uriKeys: keys, - uriDelete, - uriGet, - uriHas, - uriSet, - pathDelete, - pathGet, - pathHas, - pathSet, + clear: _clear, + values: _values, + keys: _keys, + delete: _delete, + get: _get, + has: _has, + set: _set, }; - function getUriByUri(uri: string) { - if (!uriToUri.has(uri)) { - uriToUri.set(uri, normalizeUri(uri).toLowerCase()); - } - return uriToUri.get(uri)!; - } - function getUriByPath(path: string) { - if (!pathToUri.has(path)) { - pathToUri.set(path, fileNameToUri(path).toLowerCase()); - } - return pathToUri.get(path)!; - } - - function clear() { + function _clear() { + rawUriToNormalizedUri.clear(); + normalizedUriToRawUri.clear(); return map.clear(); } - function values() { + + function _values() { return map.values(); } - function keys() { - return map.keys(); - } - function uriDelete(_uri: string) { - return map.delete(getUriByUri(_uri)); - } - function uriGet(_uri: string) { - return map.get(getUriByUri(_uri)); - } - function uriHas(_uri: string) { - return map.has(getUriByUri(_uri)); + function* _keys() { + for (const normalizedUri of map.keys()) { + yield normalizedUriToRawUri.get(normalizedUri)!; + } } - function uriSet(_uri: string, item: T) { - return map.set(getUriByUri(_uri), item); + function _delete(uri: URI) { + return map.delete(getUriByUri(uri)); } - function pathDelete(path: string) { - return uriDelete(getUriByPath(path)); - } - function pathGet(path: string) { - return uriGet(getUriByPath(path)); + function _get(uri: URI) { + return map.get(getUriByUri(uri)); } - function pathHas(path: string) { - return uriGet(getUriByPath(path)); + + function _has(uri: URI) { + return map.has(getUriByUri(uri)); } - function pathSet(path: string, item: T) { - return uriSet(getUriByPath(path), item); + + function _set(uri: URI, item: T) { + return map.set(getUriByUri(uri), item); } -} -function normalizeUri(uri: string) { - try { - return URI.parse(uri).toString(); - } catch { - return ''; + function getUriByUri(uri: URI) { + const rawUri = uri.toString(); + if (!rawUriToNormalizedUri.has(rawUri)) { + let normalizedUri = uri.toString(); + if (!caseSensitive) { + normalizedUri = normalizedUri.toLowerCase(); + } + rawUriToNormalizedUri.set(rawUri, normalizedUri); + normalizedUriToRawUri.set(normalizedUri, uri); + } + return rawUriToNormalizedUri.get(rawUri)!; } } diff --git a/packages/language-server/node.ts b/packages/language-server/node.ts index cd7b1216..535d0ae6 100644 --- a/packages/language-server/node.ts +++ b/packages/language-server/node.ts @@ -4,7 +4,7 @@ import * as vscode from 'vscode-languageserver/node'; import httpSchemaRequestHandler from './lib/schemaRequestHandlers/http'; import { createServerBase } from './lib/server'; import type { InitializationOptions } from './lib/types'; -import { uriToFileName } from './lib/uri'; +import type { UriConverter } from './lib/uri'; export * from 'vscode-languageserver/node'; export * from './index'; @@ -12,12 +12,12 @@ export * from './lib/project/simpleProjectProvider'; export * from './lib/project/typescriptProjectProvider'; export * from './lib/server'; -export function createFs(options: InitializationOptions): FileSystem { +export function createFs(options: InitializationOptions, uriConverter: UriConverter): FileSystem { return { stat(uri) { if (uri.startsWith('file://')) { try { - const stats = fs.statSync(uriToFileName(uri), { throwIfNoEntry: false }); + const stats = fs.statSync(uriConverter.uriToFileName(uri), { throwIfNoEntry: false }); if (stats) { return { type: stats.isFile() ? FileType.File @@ -39,13 +39,13 @@ export function createFs(options: InitializationOptions): FileSystem { if (uri.startsWith('file://')) { try { if (options.maxFileSize) { - const stats = fs.statSync(uriToFileName(uri), { throwIfNoEntry: false }); + const stats = fs.statSync(uriConverter.uriToFileName(uri), { throwIfNoEntry: false }); if (stats && stats.size > options.maxFileSize) { console.warn(`[volar] file size exceeded limit: ${uri} (${stats.size} > ${options.maxFileSize})`); return undefined; } } - return fs.readFileSync(uriToFileName(uri), { encoding: encoding as 'utf-8' ?? 'utf-8' }); + return fs.readFileSync(uriConverter.uriToFileName(uri), { encoding: encoding as 'utf-8' ?? 'utf-8' }); } catch { return undefined; @@ -58,7 +58,7 @@ export function createFs(options: InitializationOptions): FileSystem { readDirectory(uri) { if (uri.startsWith('file://')) { try { - const dirName = uriToFileName(uri); + const dirName = uriConverter.uriToFileName(uri); const files = fs.readdirSync(dirName, { withFileTypes: true }); return files.map<[string, FileType]>(file => { return [file.name, file.isFile() ? FileType.File @@ -81,7 +81,7 @@ export function createConnection() { } export function createServer(connection: vscode.Connection) { - return createServerBase(connection, params => createFs(params.initializationOptions ?? {})); + return createServerBase(connection, createFs); } export function loadTsdkByPath(tsdk: string, locale: string | undefined) { diff --git a/packages/language-server/tests/uri.spec.ts b/packages/language-server/tests/uri.spec.ts index 15071f80..531bbdb2 100644 --- a/packages/language-server/tests/uri.spec.ts +++ b/packages/language-server/tests/uri.spec.ts @@ -1,11 +1,12 @@ import { describe, expect, test } from 'vitest'; import { URI } from 'vscode-uri'; -import { fileNameToUri, uriToFileName } from '../lib/uri'; +import { createUriConverter } from '../lib/uri'; describe('URI', () => { test('recoverable', () => { + const uriConverter = createUriConverter(); const cases = [ 'file:///a/b/c', 'test://test/test.html', @@ -15,7 +16,7 @@ describe('URI', () => { ]; for (const uri of cases) { - expect(fileNameToUri(uriToFileName(uri))).toBe(URI.parse(uri).toString()); + expect(uriConverter.fileNameToUri(uriConverter.uriToFileName(uri))).toBe(URI.parse(uri).toString()); } }); });