Skip to content

Commit

Permalink
fix(language-server): memorize URI conversion results of synchronized…
Browse files Browse the repository at this point in the history
… documents (#181)
  • Loading branch information
johnsoncodehk committed May 15, 2024
1 parent 6367893 commit 20ad313
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 178 deletions.
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

0 comments on commit 20ad313

Please sign in to comment.