Skip to content

Commit 28a8b9f

Browse files
fix(language-server, vscode): Properly handle FS requests on browser (volarjs#73)
1 parent 3ee6507 commit 28a8b9f

File tree

10 files changed

+158
-123
lines changed

10 files changed

+158
-123
lines changed

packages/language-server/src/browser/index.ts

Lines changed: 47 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { startCommonLanguageServer } from '../common/server';
33
import { LanguageServerPlugin } from '../types';
44
import httpSchemaRequestHandler from '../common/schemaRequestHandlers/http';
55
import { URI } from 'vscode-uri';
6-
import { FsReadFileRequest, FsReadDirectoryRequest } from '../protocol';
7-
import { FileSystem, FileType } from '@volar/language-service';
6+
import { FsReadFileRequest, FsReadDirectoryRequest, FsStatRequest } from '../protocol';
7+
import { FileType } from '@volar/language-service';
88

99
export * from '../index';
1010

@@ -18,6 +18,7 @@ export function createConnection() {
1818
}
1919

2020
export function startLanguageServer(connection: vscode.Connection, ...plugins: LanguageServerPlugin[]) {
21+
2122
startCommonLanguageServer(connection, plugins, () => ({
2223
uriToFileName,
2324
fileNameToUri,
@@ -29,109 +30,76 @@ export function startLanguageServer(connection: vscode.Connection, ...plugins: L
2930
},
3031
},
3132
async loadTypeScript(options) {
32-
const tsdkUri = options.typescript && 'tsdkUrl' in options.typescript
33+
const tsdkUrl = options.typescript && 'tsdkUrl' in options.typescript
3334
? options.typescript.tsdkUrl
3435
: undefined;
35-
if (!tsdkUri) {
36+
if (!tsdkUrl) {
3637
return;
3738
}
3839
const _module = globalThis.module;
3940
globalThis.module = { exports: {} } as typeof _module;
40-
await import(`${tsdkUri}/typescript.js`);
41+
await import(`${tsdkUrl}/typescript.js`);
4142
const ts = globalThis.module.exports;
4243
globalThis.module = _module;
4344
return ts as typeof import('typescript/lib/tsserverlibrary');
4445
},
4546
async loadTypeScriptLocalized(options, locale) {
46-
const tsdkUri = options.typescript && 'tsdkUrl' in options.typescript
47+
const tsdkUrl = options.typescript && 'tsdkUrl' in options.typescript
4748
? options.typescript.tsdkUrl
4849
: undefined;
49-
if (!tsdkUri) {
50+
if (!tsdkUrl) {
5051
return;
5152
}
5253
try {
53-
const json = await httpSchemaRequestHandler(`${tsdkUri}/${locale}/diagnosticMessages.generated.json`);
54+
const json = await httpSchemaRequestHandler(`${tsdkUrl}/${locale}/diagnosticMessages.generated.json`);
5455
if (json) {
5556
return JSON.parse(json);
5657
}
5758
}
5859
catch { }
5960
},
60-
fs: createFs(connection),
61-
getCancellationToken(original) {
62-
return original ?? vscode.CancellationToken.None;
63-
},
64-
}));
65-
}
66-
67-
/**
68-
* To avoid hitting the API hourly limit, we keep requests as low as possible.
69-
*/
70-
function createFs(connection: vscode.Connection): FileSystem {
71-
72-
const readDirectoryResults = new Map<string, Promise<[string, FileType][]>>();
73-
74-
return {
75-
async stat(uri) {
76-
if (uri.startsWith('__invalid__:')) {
77-
return;
78-
}
79-
if (uri.startsWith('http://') || uri.startsWith('https://')) {
80-
const text = await this.readFile(uri); // TODO: perf
81-
if (text !== undefined) {
82-
return {
83-
type: FileType.File,
84-
size: text.length,
85-
ctime: -1,
86-
mtime: -1,
87-
};
61+
fs: {
62+
async stat(uri) {
63+
if (uri.startsWith('__invalid__:')) {
64+
return;
8865
}
89-
return undefined;
90-
}
91-
const dirUri = uri.substring(0, uri.lastIndexOf('/'));
92-
const baseName = uri.substring(uri.lastIndexOf('/') + 1);
93-
const entries = await this.readDirectory(dirUri);
94-
const matches = entries.filter(entry => entry[0] === baseName);
95-
if (matches.length) {
96-
return {
97-
type: matches.some(entry => entry[1] === FileType.File) ? FileType.File : matches[0][1],
98-
size: -1,
99-
ctime: -1,
100-
mtime: -1,
101-
};
102-
}
103-
},
104-
async readFile(uri) {
105-
if (uri.startsWith('__invalid__:')) {
106-
return;
107-
}
108-
if (uri.startsWith('http://') || uri.startsWith('https://')) {
109-
return await httpSchemaRequestHandler(uri);
110-
}
111-
const dirUri = uri.substring(0, uri.lastIndexOf('/'));
112-
const baseName = uri.substring(uri.lastIndexOf('/') + 1);
113-
const entries = await this.readDirectory(dirUri);
114-
const file = entries.filter(entry => entry[0] === baseName && entry[1] === FileType.File);
115-
if (file) {
116-
const text = await connection.sendRequest(FsReadFileRequest.type, uri);
117-
if (text !== undefined && text !== null) {
118-
return text;
66+
if (uri.startsWith('http://') || uri.startsWith('https://')) { // perf
67+
const text = await this.readFile(uri);
68+
if (text !== undefined) {
69+
return {
70+
type: FileType.File,
71+
size: text.length,
72+
ctime: -1,
73+
mtime: -1,
74+
};
75+
}
76+
return undefined;
11977
}
120-
}
78+
return await connection.sendRequest(FsStatRequest.type, uri);
79+
},
80+
async readFile(uri) {
81+
if (uri.startsWith('__invalid__:')) {
82+
return;
83+
}
84+
if (uri.startsWith('http://') || uri.startsWith('https://')) { // perf
85+
return await httpSchemaRequestHandler(uri);
86+
}
87+
return await connection.sendRequest(FsReadFileRequest.type, uri) ?? undefined;
88+
},
89+
async readDirectory(uri) {
90+
if (uri.startsWith('__invalid__:')) {
91+
return [];
92+
}
93+
if (uri.startsWith('http://') || uri.startsWith('https://')) { // perf
94+
return [];
95+
}
96+
return await connection.sendRequest(FsReadDirectoryRequest.type, uri);
97+
},
12198
},
122-
async readDirectory(uri) {
123-
if (uri.startsWith('__invalid__:')) {
124-
return [];
125-
}
126-
if (uri.startsWith('http://') || uri.startsWith('https://')) {
127-
return [];
128-
}
129-
if (!readDirectoryResults.has(uri)) {
130-
readDirectoryResults.set(uri, connection.sendRequest(FsReadDirectoryRequest.type, uri));
131-
}
132-
return await readDirectoryResults.get(uri)!;
99+
getCancellationToken(original) {
100+
return original ?? vscode.CancellationToken.None;
133101
},
134-
};
102+
}));
135103
}
136104

137105
function uriToFileName(uri: string) {

packages/language-server/src/common/project.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,17 +189,17 @@ export async function createProject(context: ProjectContext) {
189189
}
190190
}
191191

192-
for (const change of changes) {
192+
await Promise.all(changes.map(async change => {
193193
if (askedFiles.uriGet(change.uri) && globalSnapshots.get(fs)!.uriGet(change.uri)) {
194194
if (change.type === vscode.FileChangeType.Changed) {
195-
updateRootScriptSnapshot(change.uri);
195+
await updateRootScriptSnapshot(change.uri);
196196
}
197197
else if (change.type === vscode.FileChangeType.Deleted) {
198198
globalSnapshots.get(fs)!.uriSet(change.uri, undefined);
199199
}
200200
projectVersion++;
201201
}
202-
}
202+
}));
203203

204204
if (oldProjectVersion !== projectVersion) {
205205
token = context.server.runtimeEnv.getCancellationToken();

packages/vscode/browser.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from 'vscode-languageclient/browser';

packages/vscode/browser.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('vscode-languageclient/node');

packages/vscode/node.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from 'vscode-languageclient/browser';

packages/vscode/node.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('vscode-languageclient/node');

packages/vscode/package.json

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,12 @@
1515
"dependencies": {
1616
"@volar/language-server": "1.10.10",
1717
"path-browserify": "^1.0.1",
18+
"vscode-languageclient": "^9.0.1",
1819
"vscode-nls": "^5.2.0"
1920
},
2021
"devDependencies": {
2122
"@types/node": "latest",
2223
"@types/path-browserify": "latest",
23-
"@types/vscode": "^1.82.0",
24-
"vscode-languageclient": "^9.0.1"
25-
},
26-
"peerDependencies": {
27-
"vscode-languageclient": "^9.0.1"
28-
},
29-
"peerDependenciesMeta": {
30-
"vscode-languageclient": {
31-
"optional": true
32-
}
24+
"@types/vscode": "^1.82.0"
3325
}
3426
}

packages/vscode/src/features/serverSys.ts

Lines changed: 97 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,51 +6,122 @@ export async function activate(client: BaseLanguageClient) {
66

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

10-
addHandle();
11+
let startProgress = false;
12+
let totalJobs = 0;
13+
14+
addRequestHandlers();
1115

1216
subscriptions.push(client.onDidChangeState(() => {
1317
if (client.state === 2 satisfies State.Running) {
14-
addHandle();
18+
addRequestHandlers();
1519
}
1620
}));
1721

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

20-
function addHandle() {
24+
// To avoid hitting the API hourly limit, we keep requests as low as possible.
25+
function addRequestHandlers() {
2126

22-
subscriptions.push(client.onRequest(FsStatRequest.type, async uri => {
23-
const uri2 = client.protocol2CodeConverter.asUri(uri);
27+
subscriptions.push(client.onRequest(FsStatRequest.type, stat));
28+
subscriptions.push(client.onRequest(FsReadFileRequest.type, uri => {
29+
return withProgress(() => readFile(uri), uri);
30+
}));
31+
subscriptions.push(client.onRequest(FsReadDirectoryRequest.type, uri => {
32+
return withProgress(() => readDirectory(uri), uri);
33+
}));
34+
35+
async function withProgress<T>(fn: () => Promise<T>, asset: string): Promise<T> {
36+
asset = vscode.Uri.parse(asset).path;
37+
totalJobs++;
38+
let job!: Promise<T>;
2439
try {
25-
return await vscode.workspace.fs.stat(uri2);
40+
job = fn();
41+
jobs.set(job, asset);
42+
if (!startProgress && jobs.size >= 2) {
43+
startProgress = true;
44+
vscode.window.withProgress({ location: vscode.ProgressLocation.Window }, async progress => {
45+
progress.report({
46+
message: `Loading ${totalJobs} resources: ${asset}`
47+
});
48+
while (jobs.size) {
49+
for (const [_, asset] of jobs) {
50+
progress.report({
51+
message: `Loading ${totalJobs} resources: ${asset}`,
52+
});
53+
await sleep(100);
54+
break;
55+
}
56+
}
57+
startProgress = false;
58+
});
59+
}
60+
return await job;
61+
} finally {
62+
jobs.delete(job);
2663
}
27-
catch (err) {
28-
// ignore
64+
}
65+
66+
async function stat(uri: string) {
67+
68+
// return early
69+
const dirUri = uri.substring(0, uri.lastIndexOf('/'));
70+
const baseName = uri.substring(uri.lastIndexOf('/') + 1);
71+
const entries = await readDirectory(dirUri);
72+
if (!entries.some(entry => entry[0] === baseName)) {
73+
return;
2974
}
30-
}));
3175

32-
subscriptions.push(client.onRequest(FsReadFileRequest.type, async uri => {
3376
const uri2 = client.protocol2CodeConverter.asUri(uri);
34-
try {
35-
const data = await vscode.workspace.fs.readFile(uri2);
36-
const text = textDecoder.decode(data);
37-
return text;
38-
}
39-
catch (err) {
40-
// ignore
77+
return await _stat(uri2);
78+
}
79+
80+
async function readFile(uri: string) {
81+
82+
// return early
83+
const dirUri = uri.substring(0, uri.lastIndexOf('/'));
84+
const baseName = uri.substring(uri.lastIndexOf('/') + 1);
85+
const entries = await readDirectory(dirUri);
86+
const uri2 = client.protocol2CodeConverter.asUri(uri);
87+
88+
if (!entries.some(entry => entry[0] === baseName && entry[1] === vscode.FileType.File)) {
89+
return;
4190
}
42-
}));
4391

44-
subscriptions.push(client.onRequest(FsReadDirectoryRequest.type, async uri => {
92+
return await _readFile(uri2);
93+
}
94+
95+
async function readDirectory(uri: string): Promise<[string, vscode.FileType][]> {
96+
97+
const uri2 = client.protocol2CodeConverter.asUri(uri);
98+
99+
return await (await _readDirectory(uri2))
100+
.filter(([name]) => !name.startsWith('.'));
101+
}
102+
103+
async function _readFile(uri: vscode.Uri) {
45104
try {
46-
const uri2 = client.protocol2CodeConverter.asUri(uri);
47-
let data = await vscode.workspace.fs.readDirectory(uri2);
48-
data = data.filter(([name]) => !name.startsWith('.'));
49-
return data;
50-
}
51-
catch {
105+
return textDecoder.decode(await vscode.workspace.fs.readFile(uri));
106+
} catch { }
107+
}
108+
109+
async function _readDirectory(uri: vscode.Uri) {
110+
try {
111+
return await vscode.workspace.fs.readDirectory(uri);
112+
} catch {
52113
return [];
53114
}
54-
}));
115+
}
116+
117+
async function _stat(uri: vscode.Uri) {
118+
try {
119+
return await vscode.workspace.fs.stat(uri);
120+
} catch { }
121+
}
55122
}
56123
}
124+
125+
function sleep(ms: number) {
126+
return new Promise(resolve => setTimeout(resolve, ms));
127+
}

packages/vscode/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export { activate as activateTsConfigStatusItem } from './features/tsconfig';
88
export { activate as activateServerSys } from './features/serverSys';
99
export { activate as activateTsVersionStatusItem, getTsdk } from './features/tsVersion';
1010

11+
export * from 'vscode-languageclient';
12+
1113
export function takeOverModeActive(context: vscode.ExtensionContext) {
1214
if (vscode.workspace.getConfiguration('volar').get<string>('takeOverMode.extension') === context.extension.id) {
1315
return !vscode.extensions.getExtension('vscode.typescript-language-features');

0 commit comments

Comments
 (0)