Skip to content

Commit

Permalink
refactor(language-server): split code based on logical concerns (#231)
Browse files Browse the repository at this point in the history
  • Loading branch information
johnsoncodehk authored Aug 17, 2024
1 parent 7f93034 commit 682605b
Show file tree
Hide file tree
Showing 25 changed files with 1,473 additions and 1,443 deletions.
40 changes: 7 additions & 33 deletions packages/language-server/browser.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { FileType } from '@volar/language-service';
import * as vscode from 'vscode-languageserver/browser';
import { URI } from 'vscode-uri';
import httpSchemaRequestHandler from './lib/schemaRequestHandlers/http';
import { handler as httpSchemaRequestHandler } from './lib/fileSystemProviders/http';
import { createServerBase } from './lib/server';
import { FsReadDirectoryRequest, FsReadFileRequest, FsStatRequest } from './protocol';
import { provider as httpFsProvider, listenEditorSettings } from './lib/fileSystemProviders/http';

export * from 'vscode-languageserver/browser';
export * from './index';
Expand All @@ -12,7 +11,6 @@ export * from './lib/project/typescriptProject';
export * from './lib/server';

export function createConnection() {

const messageReader = new vscode.BrowserMessageReader(self);
const messageWriter = new vscode.BrowserMessageWriter(self);
const connection = vscode.createConnection(messageReader, messageWriter);
Expand All @@ -21,35 +19,11 @@ export function createConnection() {
}

export function createServer(connection: vscode.Connection) {
return createServerBase(connection, {
async stat(uri) {
if (uri.scheme === 'http' || uri.scheme === 'https') { // perf
const text = await this.readFile(uri);
if (text !== undefined) {
return {
type: FileType.File,
size: text.length,
ctime: -1,
mtime: -1,
};
}
return undefined;
}
return await connection.sendRequest(FsStatRequest.type, uri.toString());
},
async readFile(uri) {
if (uri.scheme === 'http' || uri.scheme === 'https') { // perf
return await httpSchemaRequestHandler(uri);
}
return await connection.sendRequest(FsReadFileRequest.type, uri.toString()) ?? undefined;
},
async readDirectory(uri) {
if (uri.scheme === 'http' || uri.scheme === 'https') { // perf
return [];
}
return await connection.sendRequest(FsReadDirectoryRequest.type, uri.toString());
},
});
const server = createServerBase(connection);
server.fileSystem.install('http', httpFsProvider);
server.fileSystem.install('https', httpFsProvider);
server.onInitialized(() => listenEditorSettings(server));
return server;
}

export async function loadTsdkByUrl(tsdkUrl: string, locale: string | undefined) {
Expand Down
52 changes: 52 additions & 0 deletions packages/language-server/lib/features/configurations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as vscode from 'vscode-languageserver';
import { LanguageServerState } from '../types';

export function register(server: LanguageServerState) {
const configurations = new Map<string, Promise<any>>();
const didChangeCallbacks = new Set<vscode.NotificationHandler<vscode.DidChangeConfigurationParams>>();

server.onInitialized(() => {
server.connection.onDidChangeConfiguration(params => {
configurations.clear(); // TODO: clear only the configurations that changed
for (const cb of didChangeCallbacks) {
cb(params);
}
});
const didChangeConfiguration = server.initializeParams.capabilities.workspace?.didChangeConfiguration;
if (didChangeConfiguration?.dynamicRegistration) {
server.connection.client.register(vscode.DidChangeConfigurationNotification.type);
}
});

return {
get,
onDidChange,
};

function get<T>(section: string, scopeUri?: string): Promise<T | undefined> {
if (!server.initializeParams.capabilities.workspace?.configuration) {
return Promise.resolve(undefined);
}
const didChangeConfiguration = server.initializeParams.capabilities.workspace?.didChangeConfiguration;
if (!scopeUri && didChangeConfiguration) {
if (!configurations.has(section)) {
configurations.set(section, getConfigurationWorker(section, scopeUri));
}
return configurations.get(section)!;
}
return getConfigurationWorker(section, scopeUri);
}

function onDidChange(cb: vscode.NotificationHandler<vscode.DidChangeConfigurationParams>) {
didChangeCallbacks.add(cb);
return {
dispose() {
didChangeCallbacks.delete(cb);
},
};
}

async function getConfigurationWorker(section: string, scopeUri?: string) {
return (await server.connection.workspace.getConfiguration({ scopeUri, section })) ?? undefined /* replace null to undefined */;
}
}
216 changes: 216 additions & 0 deletions packages/language-server/lib/features/editorFeatures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import type { CodeMapping, VirtualCode } from '@volar/language-core';
import { createUriMap } from '@volar/language-service';
import type * as ts from 'typescript';
import { URI } from 'vscode-uri';
import {
GetMatchTsConfigRequest,
GetServicePluginsRequest,
GetVirtualCodeRequest,
GetVirtualFileRequest,
LoadedTSFilesMetaRequest,
UpdateServicePluginStateNotification,
UpdateVirtualCodeStateNotification,
WriteVirtualFilesNotification
} from '../../protocol';
import type { LanguageServerState } from '../types';

export function register(server: LanguageServerState) {
server.onInitialize(() => {
const { project } = server;
const scriptVersions = createUriMap<number>();
const scriptVersionSnapshots = new WeakSet<ts.IScriptSnapshot>();

server.connection.onRequest(GetMatchTsConfigRequest.type, async params => {
const uri = URI.parse(params.uri);
const languageService = (await project.getLanguageService(uri));
const tsProject = languageService.context.project.typescript;
if (tsProject?.configFileName) {
const { configFileName, uriConverter } = tsProject;
return { uri: uriConverter.asUri(configFileName).toString() };
}
});
server.connection.onRequest(GetVirtualFileRequest.type, async document => {
const uri = URI.parse(document.uri);
const languageService = (await project.getLanguageService(uri));
const documentUri = URI.parse(document.uri);
const sourceScript = languageService.context.language.scripts.get(documentUri);
if (sourceScript?.generated) {
return prune(sourceScript.generated.root);
}

function prune(virtualCode: VirtualCode): GetVirtualFileRequest.VirtualCodeInfo {
const uri = languageService.context.encodeEmbeddedDocumentUri(sourceScript!.id, virtualCode.id);
let version = scriptVersions.get(uri) ?? 0;
if (!scriptVersionSnapshots.has(virtualCode.snapshot)) {
version++;
scriptVersions.set(uri, version);
scriptVersionSnapshots.add(virtualCode.snapshot);
}
return {
fileUri: sourceScript!.id.toString(),
virtualCodeId: virtualCode.id,
languageId: virtualCode.languageId,
embeddedCodes: virtualCode.embeddedCodes?.map(prune) || [],
version,
disabled: languageService.context.disabledEmbeddedDocumentUris.has(uri),
};
}
});
server.connection.onRequest(GetVirtualCodeRequest.type, async params => {
const uri = URI.parse(params.fileUri);
const languageService = (await project.getLanguageService(uri));
const sourceScript = languageService.context.language.scripts.get(URI.parse(params.fileUri));
const virtualCode = sourceScript?.generated?.embeddedCodes.get(params.virtualCodeId);
if (virtualCode) {
const mappings: Record<string, CodeMapping[]> = {};
for (const [sourceScript, map] of languageService.context.language.maps.forEach(virtualCode)) {
mappings[sourceScript.id.toString()] = map.mappings;
}
return {
content: virtualCode.snapshot.getText(0, virtualCode.snapshot.getLength()),
mappings,
};
}
});
server.connection.onNotification(WriteVirtualFilesNotification.type, async params => {
// webpack compatibility
const _require: NodeRequire = eval('require');
const fs = _require('fs');
const uri = URI.parse(params.uri);
const languageService = (await project.getLanguageService(uri));
const tsProject = languageService.context.project.typescript;

if (tsProject) {

const { languageServiceHost } = tsProject;

for (const fileName of languageServiceHost.getScriptFileNames()) {
if (!fs.existsSync(fileName)) {
// global virtual files
const snapshot = languageServiceHost.getScriptSnapshot(fileName);
if (snapshot) {
fs.writeFile(fileName, snapshot.getText(0, snapshot.getLength()), () => { });
}
}
else {
const uri = tsProject.uriConverter.asUri(fileName);
const sourceScript = languageService.context.language.scripts.get(uri);
if (sourceScript?.generated) {
const serviceScript = sourceScript.generated.languagePlugin.typescript?.getServiceScript(sourceScript.generated.root);
if (serviceScript) {
const { snapshot } = serviceScript.code;
fs.writeFile(fileName + serviceScript.extension, snapshot.getText(0, snapshot.getLength()), () => { });
}
if (sourceScript.generated.languagePlugin.typescript?.getExtraServiceScripts) {
for (const extraServiceScript of sourceScript.generated.languagePlugin.typescript.getExtraServiceScripts(uri.toString(), sourceScript.generated.root)) {
const { snapshot } = extraServiceScript.code;
fs.writeFile(fileName, snapshot.getText(0, snapshot.getLength()), () => { });
}
}
}
}
}
}
});
server.connection.onRequest(LoadedTSFilesMetaRequest.type, async () => {

const sourceFilesData = new Map<ts.SourceFile, {
projectNames: string[];
size: number;
}>();

for (const languageService of await project.getExistingLanguageServices()) {
const tsLanguageService: ts.LanguageService | undefined = languageService.context.inject<any>('typescript/languageService');
const program = tsLanguageService?.getProgram();
const tsProject = languageService.context.project.typescript;
if (program && tsProject) {
const { languageServiceHost, configFileName } = tsProject;
const projectName = configFileName ?? (languageServiceHost.getCurrentDirectory() + '(inferred)');
const sourceFiles = program.getSourceFiles() ?? [];
for (const sourceFile of sourceFiles) {
if (!sourceFilesData.has(sourceFile)) {
let nodes = 0;
sourceFile.forEachChild(function walk(node) {
nodes++;
node.forEachChild(walk);
});
sourceFilesData.set(sourceFile, {
projectNames: [],
size: nodes * 128,
});
}
sourceFilesData.get(sourceFile)!.projectNames.push(projectName);
};
}
}

const result: {
inputs: {};
outputs: Record<string, {
imports: string[];
exports: string[];
entryPoint: string;
inputs: Record<string, { bytesInOutput: number; }>;
bytes: number;
}>;
} = {
inputs: {},
outputs: {},
};

for (const [sourceFile, fileData] of sourceFilesData) {
let key = fileData.projectNames.sort().join(', ');
if (fileData.projectNames.length >= 2) {
key = `Shared in ${fileData.projectNames.length} projects (${key})`;
}
result.outputs[key] ??= {
imports: [],
exports: [],
entryPoint: '',
inputs: {},
bytes: 0,
};
result.outputs[key].inputs[sourceFile.fileName] = { bytesInOutput: fileData.size };
}

return result;
});
server.connection.onNotification(UpdateVirtualCodeStateNotification.type, async params => {
const uri = URI.parse(params.fileUri);
const languageService = await project.getLanguageService(uri);
const virtualFileUri = languageService.context.encodeEmbeddedDocumentUri(URI.parse(params.fileUri), params.virtualCodeId);
if (params.disabled) {
languageService.context.disabledEmbeddedDocumentUris.set(virtualFileUri, true);
}
else {
languageService.context.disabledEmbeddedDocumentUris.delete(virtualFileUri);
}
});
server.connection.onNotification(UpdateServicePluginStateNotification.type, async params => {
const uri = URI.parse(params.uri);
const languageService = await project.getLanguageService(uri);
const plugin = languageService.context.plugins[params.serviceId][1];
if (params.disabled) {
languageService.context.disabledServicePlugins.add(plugin);
}
else {
languageService.context.disabledServicePlugins.delete(plugin);
}
});
server.connection.onRequest(GetServicePluginsRequest.type, async params => {
const uri = URI.parse(params.uri);
const languageService = await project.getLanguageService(uri);
const result: GetServicePluginsRequest.ResponseType = [];
for (let pluginIndex = 0; pluginIndex < languageService.context.plugins.length; pluginIndex++) {
const plugin = languageService.context.plugins[pluginIndex];
result.push({
id: pluginIndex,
name: plugin[0].name,
disabled: languageService.context.disabledServicePlugins.has(plugin[1]),
features: Object.keys(plugin[1]),
});
}
return result;
});
});
}
64 changes: 64 additions & 0 deletions packages/language-server/lib/features/fileSystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { createUriMap, FileSystem } from '@volar/language-service';
import * as vscode from 'vscode-languageserver';
import { URI } from 'vscode-uri';

export function register(
documents: ReturnType<typeof import('./textDocuments').register>,
fileWatcher: ReturnType<typeof import('./fileWatcher').register>
) {
const providers = new Map<string, FileSystem>();
const readFileCache = createUriMap<ReturnType<FileSystem['readFile']>>();
const statCache = createUriMap<ReturnType<FileSystem['stat']>>();
const readDirectoryCache = createUriMap<ReturnType<FileSystem['readDirectory']>>();

documents.onDidSave(({ document }) => {
const uri = URI.parse(document.uri);
readFileCache.set(uri, document.getText());
statCache.delete(uri);
});

fileWatcher.onDidChangeWatchedFiles(({ changes }) => {
for (const change of changes) {
const changeUri = URI.parse(change.uri);
const dir = URI.parse(change.uri.substring(0, change.uri.lastIndexOf('/')));
if (change.type === vscode.FileChangeType.Deleted) {
readFileCache.set(changeUri, undefined);
statCache.set(changeUri, undefined);
readDirectoryCache.delete(dir);
}
else if (change.type === vscode.FileChangeType.Changed) {
readFileCache.delete(changeUri);
statCache.delete(changeUri);
}
else if (change.type === vscode.FileChangeType.Created) {
readFileCache.delete(changeUri);
statCache.delete(changeUri);
readDirectoryCache.delete(dir);
}
}
});

return {
readFile(uri: URI) {
if (!readFileCache.has(uri)) {
readFileCache.set(uri, providers.get(uri.scheme)?.readFile(uri));
}
return readFileCache.get(uri)!;
},
stat(uri: URI) {
if (!statCache.has(uri)) {
statCache.set(uri, providers.get(uri.scheme)?.stat(uri));
}
return statCache.get(uri)!;
},
readDirectory(uri: URI) {
if (!readDirectoryCache.has(uri)) {
readDirectoryCache.set(uri, providers.get(uri.scheme)?.readDirectory(uri) ?? []);
}
return readDirectoryCache.get(uri)!;
},
install(scheme: string, provider: FileSystem) {
providers.set(scheme, provider);
},
};
}
Loading

0 comments on commit 682605b

Please sign in to comment.