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, vscode): Properly handle FS requests on browser #73

Merged
merged 12 commits into from
Nov 3, 2023
126 changes: 47 additions & 79 deletions packages/language-server/src/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { startCommonLanguageServer } from '../common/server';
import { LanguageServerPlugin } from '../types';
import httpSchemaRequestHandler from '../common/schemaRequestHandlers/http';
import { URI } from 'vscode-uri';
import { FsReadFileRequest, FsReadDirectoryRequest } from '../protocol';
import { FileSystem, FileType } from '@volar/language-service';
import { FsReadFileRequest, FsReadDirectoryRequest, FsStatRequest } from '../protocol';
import { FileType } from '@volar/language-service';

export * from '../index';

Expand All @@ -18,6 +18,7 @@ export function createConnection() {
}

export function startLanguageServer(connection: vscode.Connection, ...plugins: LanguageServerPlugin[]) {

startCommonLanguageServer(connection, plugins, () => ({
uriToFileName,
fileNameToUri,
Expand All @@ -29,109 +30,76 @@ export function startLanguageServer(connection: vscode.Connection, ...plugins: L
},
},
async loadTypeScript(options) {
const tsdkUri = options.typescript && 'tsdkUrl' in options.typescript
const tsdkUrl = options.typescript && 'tsdkUrl' in options.typescript
? options.typescript.tsdkUrl
: undefined;
if (!tsdkUri) {
if (!tsdkUrl) {
return;
}
const _module = globalThis.module;
globalThis.module = { exports: {} } as typeof _module;
await import(`${tsdkUri}/typescript.js`);
await import(`${tsdkUrl}/typescript.js`);
const ts = globalThis.module.exports;
globalThis.module = _module;
return ts as typeof import('typescript/lib/tsserverlibrary');
},
async loadTypeScriptLocalized(options, locale) {
const tsdkUri = options.typescript && 'tsdkUrl' in options.typescript
const tsdkUrl = options.typescript && 'tsdkUrl' in options.typescript
? options.typescript.tsdkUrl
: undefined;
if (!tsdkUri) {
if (!tsdkUrl) {
return;
}
try {
const json = await httpSchemaRequestHandler(`${tsdkUri}/${locale}/diagnosticMessages.generated.json`);
const json = await httpSchemaRequestHandler(`${tsdkUrl}/${locale}/diagnosticMessages.generated.json`);
if (json) {
return JSON.parse(json);
}
}
catch { }
},
fs: createFs(connection),
getCancellationToken(original) {
return original ?? vscode.CancellationToken.None;
},
}));
}

/**
* To avoid hitting the API hourly limit, we keep requests as low as possible.
*/
function createFs(connection: vscode.Connection): FileSystem {

const readDirectoryResults = new Map<string, Promise<[string, FileType][]>>();

return {
async stat(uri) {
if (uri.startsWith('__invalid__:')) {
return;
}
if (uri.startsWith('http://') || uri.startsWith('https://')) {
const text = await this.readFile(uri); // TODO: perf
if (text !== undefined) {
return {
type: FileType.File,
size: text.length,
ctime: -1,
mtime: -1,
};
fs: {
async stat(uri) {
if (uri.startsWith('__invalid__:')) {
return;
}
return undefined;
}
const dirUri = uri.substring(0, uri.lastIndexOf('/'));
const baseName = uri.substring(uri.lastIndexOf('/') + 1);
const entries = await this.readDirectory(dirUri);
const matches = entries.filter(entry => entry[0] === baseName);
if (matches.length) {
return {
type: matches.some(entry => entry[1] === FileType.File) ? FileType.File : matches[0][1],
size: -1,
ctime: -1,
mtime: -1,
};
}
},
async readFile(uri) {
if (uri.startsWith('__invalid__:')) {
return;
}
if (uri.startsWith('http://') || uri.startsWith('https://')) {
return await httpSchemaRequestHandler(uri);
}
const dirUri = uri.substring(0, uri.lastIndexOf('/'));
const baseName = uri.substring(uri.lastIndexOf('/') + 1);
const entries = await this.readDirectory(dirUri);
const file = entries.filter(entry => entry[0] === baseName && entry[1] === FileType.File);
if (file) {
const text = await connection.sendRequest(FsReadFileRequest.type, uri);
if (text !== undefined && text !== null) {
return text;
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;
}
}
return await connection.sendRequest(FsStatRequest.type, uri);
},
async readFile(uri) {
if (uri.startsWith('__invalid__:')) {
return;
}
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('__invalid__:')) {
return [];
}
if (uri.startsWith('http://') || uri.startsWith('https://')) { // perf
return [];
}
return await connection.sendRequest(FsReadDirectoryRequest.type, uri);
},
},
async readDirectory(uri) {
if (uri.startsWith('__invalid__:')) {
return [];
}
if (uri.startsWith('http://') || uri.startsWith('https://')) {
return [];
}
if (!readDirectoryResults.has(uri)) {
readDirectoryResults.set(uri, connection.sendRequest(FsReadDirectoryRequest.type, uri));
}
return await readDirectoryResults.get(uri)!;
getCancellationToken(original) {
return original ?? vscode.CancellationToken.None;
},
};
}));
}

function uriToFileName(uri: string) {
Expand Down
6 changes: 3 additions & 3 deletions packages/language-server/src/common/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,17 +189,17 @@ export async function createProject(context: ProjectContext) {
}
}

for (const change of changes) {
await Promise.all(changes.map(async change => {
if (askedFiles.uriGet(change.uri) && globalSnapshots.get(fs)!.uriGet(change.uri)) {
if (change.type === vscode.FileChangeType.Changed) {
updateRootScriptSnapshot(change.uri);
await updateRootScriptSnapshot(change.uri);
}
else if (change.type === vscode.FileChangeType.Deleted) {
globalSnapshots.get(fs)!.uriSet(change.uri, undefined);
}
projectVersion++;
}
}
}));

if (oldProjectVersion !== projectVersion) {
token = context.server.runtimeEnv.getCancellationToken();
Expand Down
1 change: 1 addition & 0 deletions packages/vscode/browser.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from 'vscode-languageclient/browser';
1 change: 1 addition & 0 deletions packages/vscode/browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('vscode-languageclient/node');
1 change: 1 addition & 0 deletions packages/vscode/node.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from 'vscode-languageclient/browser';
1 change: 1 addition & 0 deletions packages/vscode/node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('vscode-languageclient/node');
12 changes: 2 additions & 10 deletions packages/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,12 @@
"dependencies": {
"@volar/language-server": "1.10.10",
"path-browserify": "^1.0.1",
"vscode-languageclient": "^9.0.1",
"vscode-nls": "^5.2.0"
},
"devDependencies": {
"@types/node": "latest",
"@types/path-browserify": "latest",
"@types/vscode": "^1.82.0",
"vscode-languageclient": "^9.0.1"
},
"peerDependencies": {
"vscode-languageclient": "^9.0.1"
},
"peerDependenciesMeta": {
"vscode-languageclient": {
"optional": true
}
"@types/vscode": "^1.82.0"
}
}
123 changes: 97 additions & 26 deletions packages/vscode/src/features/serverSys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,51 +6,122 @@ export async function activate(client: BaseLanguageClient) {

const subscriptions: vscode.Disposable[] = [];
const textDecoder = new TextDecoder();
const jobs = new Map<Promise<any>, string>();

addHandle();
let startProgress = false;
let totalJobs = 0;

addRequestHandlers();

subscriptions.push(client.onDidChangeState(() => {
if (client.state === 2 satisfies State.Running) {
addHandle();
addRequestHandlers();
}
}));

return vscode.Disposable.from(...subscriptions);

function addHandle() {
// To avoid hitting the API hourly limit, we keep requests as low as possible.
function addRequestHandlers() {

subscriptions.push(client.onRequest(FsStatRequest.type, async uri => {
const uri2 = client.protocol2CodeConverter.asUri(uri);
subscriptions.push(client.onRequest(FsStatRequest.type, stat));
subscriptions.push(client.onRequest(FsReadFileRequest.type, uri => {
return withProgress(() => readFile(uri), uri);
}));
subscriptions.push(client.onRequest(FsReadDirectoryRequest.type, uri => {
return withProgress(() => readDirectory(uri), uri);
}));

async function withProgress<T>(fn: () => Promise<T>, asset: string): Promise<T> {
asset = vscode.Uri.parse(asset).path;
totalJobs++;
let job!: Promise<T>;
try {
return await vscode.workspace.fs.stat(uri2);
job = fn();
jobs.set(job, asset);
if (!startProgress && jobs.size >= 2) {
startProgress = true;
vscode.window.withProgress({ location: vscode.ProgressLocation.Window }, async progress => {
progress.report({
message: `Loading ${totalJobs} resources: ${asset}`
});
while (jobs.size) {
for (const [_, asset] of jobs) {
progress.report({
message: `Loading ${totalJobs} resources: ${asset}`,
});
await sleep(100);
break;
}
}
startProgress = false;
});
}
return await job;
} finally {
jobs.delete(job);
}
catch (err) {
// ignore
}

async function stat(uri: string) {

// return early
const dirUri = uri.substring(0, uri.lastIndexOf('/'));
const baseName = uri.substring(uri.lastIndexOf('/') + 1);
const entries = await readDirectory(dirUri);
if (!entries.some(entry => entry[0] === baseName)) {
return;
}
}));

subscriptions.push(client.onRequest(FsReadFileRequest.type, async uri => {
const uri2 = client.protocol2CodeConverter.asUri(uri);
try {
const data = await vscode.workspace.fs.readFile(uri2);
const text = textDecoder.decode(data);
return text;
}
catch (err) {
// ignore
return await _stat(uri2);
}

async function readFile(uri: string) {

// return early
const dirUri = uri.substring(0, uri.lastIndexOf('/'));
const baseName = uri.substring(uri.lastIndexOf('/') + 1);
const entries = await readDirectory(dirUri);
const uri2 = client.protocol2CodeConverter.asUri(uri);

if (!entries.some(entry => entry[0] === baseName && entry[1] === vscode.FileType.File)) {
return;
}
}));

subscriptions.push(client.onRequest(FsReadDirectoryRequest.type, async uri => {
return await _readFile(uri2);
}

async function readDirectory(uri: string): Promise<[string, vscode.FileType][]> {

const uri2 = client.protocol2CodeConverter.asUri(uri);

return await (await _readDirectory(uri2))
.filter(([name]) => !name.startsWith('.'));
}

async function _readFile(uri: vscode.Uri) {
try {
const uri2 = client.protocol2CodeConverter.asUri(uri);
let data = await vscode.workspace.fs.readDirectory(uri2);
data = data.filter(([name]) => !name.startsWith('.'));
return data;
}
catch {
return textDecoder.decode(await vscode.workspace.fs.readFile(uri));
} catch { }
}

async function _readDirectory(uri: vscode.Uri) {
try {
return await vscode.workspace.fs.readDirectory(uri);
} catch {
return [];
}
}));
}

async function _stat(uri: vscode.Uri) {
try {
return await vscode.workspace.fs.stat(uri);
} catch { }
}
}
}

function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
2 changes: 2 additions & 0 deletions packages/vscode/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export { activate as activateTsConfigStatusItem } from './features/tsconfig';
export { activate as activateServerSys } from './features/serverSys';
export { activate as activateTsVersionStatusItem, getTsdk } from './features/tsVersion';

export * from 'vscode-languageclient';

export function takeOverModeActive(context: vscode.ExtensionContext) {
if (vscode.workspace.getConfiguration('volar').get<string>('takeOverMode.extension') === context.extension.id) {
return !vscode.extensions.getExtension('vscode.typescript-language-features');
Expand Down
Loading
Loading