Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

fix(language-server): memorize URI conversion results of synchronized documents #181

Merged
merged 5 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 15 additions & 19 deletions packages/language-server/lib/project/simpleProjectProvider.ts
Original file line number Diff line number Diff line change
@@ -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<string, Promise<ServerProject>>();
const map = createUriMap<Promise<ServerProject>>();
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);
Expand All @@ -30,40 +29,37 @@ 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,
getConfiguration: server.getConfiguration,
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<boolean>) {

let parsed = URI.parse(uri);

export function getWorkspaceFolder(uri: URI, workspaceFolders: UriMap<boolean>) {
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: '/' });
}
19 changes: 10 additions & 9 deletions packages/language-server/lib/project/typescriptProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand All @@ -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();
Expand All @@ -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() {
Expand All @@ -71,7 +72,7 @@ export async function createTypeScriptServerProject(
host,
sys,
});
const askedFiles = createUriMap<boolean>(fileNameToUri);
const askedFiles = createUriMap<boolean>();
const docChangeWatcher = server.documents.onDidChangeContent(() => {
projectVersion++;
});
Expand Down Expand Up @@ -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(),
);
Expand Down
81 changes: 44 additions & 37 deletions packages/language-server/lib/project/typescriptProjectProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -25,8 +24,8 @@ export function createTypeScriptProjectProvider(
) {
let initialized = false;

const configProjects = createUriMap<Promise<TypeScriptServerProject>>(fileNameToUri);
const inferredProjects = createUriMap<Promise<TypeScriptServerProject>>(fileNameToUri);
const configProjects = createUriMap<Promise<TypeScriptServerProject>>();
const inferredProjects = createUriMap<Promise<TypeScriptServerProject>>();
const rootTsConfigs = new Set<string>();
const searchedDirs = new Set<string>();
const projects: ServerProjectProvider = {
Expand All @@ -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());
}
Expand All @@ -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());
}
}
Expand All @@ -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)) {
Expand All @@ -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);
}
}
Expand All @@ -114,39 +117,42 @@ 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]);
}
}
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<boolean>(fileNameToUri);
const map = createUriMap<boolean>();
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> | boolean) {

const checked = new Set<string>();

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, []);
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
WriteVirtualFilesNotification,
} from '../../protocol';
import type { ServerBase } from '../types';
import { fileNameToUri } from '../uri';

export function registerEditorFeatures(server: ServerBase) {

Expand Down Expand Up @@ -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 => {
Expand Down
Loading
Loading