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

Cross-file Typescript support in vscode-web #169311

Merged
merged 63 commits into from
Jan 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
bd9e555
recreate logging from other machine
sandersn Sep 16, 2022
052c6a8
Merge branch 'main' into wasm-typescript
sandersn Sep 16, 2022
ea42f84
comment out openSystemBrowser
sandersn Sep 16, 2022
0102053
Add vscode-wasm-typescript dep
sandersn Sep 16, 2022
f79188b
remove unused reference to module
sandersn Sep 16, 2022
bdee414
use require reference that linter allows
sandersn Sep 16, 2022
4f0ece1
Add vscode-wasm-typescript to tsserver.web.js
sandersn Sep 26, 2022
6f6c491
Update vscode-wasm-typescript dependency
sandersn Sep 27, 2022
21eac11
Fix minor syntax in webpack hack
sandersn Sep 27, 2022
2afbf36
Fix another typo in webpack hack!
sandersn Sep 27, 2022
80d2729
Fix provided typescript path
sandersn Sep 27, 2022
9d88cc9
Try to improve module.exports handling in webpack hac
sandersn Sep 27, 2022
7db6575
tsserver.web.js comes from local builds
sandersn Sep 29, 2022
f67a08e
First attempt to set up server-side support
sandersn Sep 30, 2022
092c565
Remove auto-imported identifier
sandersn Sep 30, 2022
bd93d21
Move sync-api setup code to serverProcess.browser.ts
sandersn Oct 4, 2022
293b6e3
Reorder webpack hack and clean up unused logging
sandersn Oct 6, 2022
124acc8
Update vscode-wasm/vscode-wasm-typescript dependencies
sandersn Oct 11, 2022
40a2626
Add file watching
sandersn Oct 13, 2022
060d671
Extract webpack hack
sandersn Oct 17, 2022
fac861b
Remove manual verbose logging
sandersn Oct 21, 2022
65b8c46
Merge branch 'main' into wasm-typescript
sandersn Oct 21, 2022
d8f495b
Add vscode-test-web to semantic-supported schemes
sandersn Oct 24, 2022
7a1eb58
Also update the webpack-hack-only build
sandersn Oct 26, 2022
4ee41e2
Switch to tsserverlibrary
sandersn Nov 4, 2022
72a972c
Merge branch 'main' into wasm-typescript
sandersn Nov 4, 2022
6509c48
Remove bogus auto-import and unneeded (?) dep
sandersn Nov 4, 2022
2c32ab4
Merge branch 'main' into wasm-typescript
sandersn Dec 1, 2022
6c56e69
remove webpack-like hack
sandersn Dec 1, 2022
673869c
move code from vscode-wasm-typescript
sandersn Dec 2, 2022
741e3a8
Initial prototype of cancellation
sandersn Dec 2, 2022
a24ae56
Switch tsserver to separate MessageChannel
sandersn Dec 7, 2022
6f1415e
Move watches to a separate MessagePort
sandersn Dec 7, 2022
4b19dee
switch vscode-web from in-memory to real filesystem
sandersn Dec 8, 2022
c17bf33
Make toResource translate / -> vscode-test-web
sandersn Dec 8, 2022
6fade7c
Encode scheme and authority in TS filenames
sandersn Dec 15, 2022
68047c7
Lift parseUri outside createServerHost
sandersn Dec 15, 2022
9ffdb8e
Special-case URI of lib*d.ts in webServer.toResource
sandersn Dec 15, 2022
629804c
Merge branch 'main' into wasm-typescript
sandersn Dec 15, 2022
0f0e155
Improve cancellation
sandersn Dec 16, 2022
675401e
Pass in current request instead of waiting for a fresh one.
sandersn Dec 16, 2022
41be877
Address initial PR comments
sandersn Dec 19, 2022
8639b4b
Add cancellation bit to each (cancellable) request, locally fix an is…
jrieken Dec 22, 2022
0138958
Switch to per-file/directory watches
sandersn Dec 22, 2022
a8774f3
Parse --serverMode partialSemantic in webServer
sandersn Dec 27, 2022
52506b6
Simplify logging code
sandersn Dec 27, 2022
6f1966f
Merge branch 'main' into wasm-typescript
sandersn Dec 27, 2022
a07f98c
Cleanup in webServer
sandersn Dec 29, 2022
0288f0f
Switch to markdown extension's FileWatcherManager
sandersn Jan 6, 2023
1c52093
Clean up host methods
sandersn Jan 9, 2023
f48e065
More logging/TODO cleanup
sandersn Jan 9, 2023
f5dade7
Remove duplicate dependency
mjbvz Jan 10, 2023
cde1ffc
Add setting to enable/disable semantic mode on web
mjbvz Jan 10, 2023
8f130c0
Re-order and re-arrange code to minimise PR diff
sandersn Jan 10, 2023
4952d5d
Copy fileWatchingManager to typescript extension
sandersn Jan 10, 2023
8a72e7d
Fix linting of webServer
mjbvz Jan 10, 2023
98ae48e
Align formatting of catch / else
mjbvz Jan 10, 2023
9069220
Extract isProjectWideIntellisenseOnWebEnabled and keep using in-memor…
mjbvz Jan 11, 2023
6057abc
Make sure we still work if SharedArrayBuffers aren't supported
mjbvz Jan 12, 2023
e36a6db
Remove symlink support and fix typo
sandersn Jan 12, 2023
0b2a13c
Merge branch 'main' into wasm-typescript
sandersn Jan 12, 2023
aad4f87
Merge branch 'main' into wasm-typescript
sandersn Jan 12, 2023
6a1b2c3
Fix compile errors
mjbvz Jan 12, 2023
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
1 change: 0 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
**/extensions/typescript-language-features/test-workspace/**
**/extensions/typescript-language-features/extension.webpack.config.js
**/extensions/typescript-language-features/extension-browser.webpack.config.js
**/extensions/typescript-language-features/web/**
**/extensions/vscode-api-tests/testWorkspace/**
**/extensions/vscode-api-tests/testWorkspace2/**
**/fixtures/**
Expand Down
12 changes: 12 additions & 0 deletions extensions/typescript-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
"jsonc-parser": "^3.2.0",
"semver": "5.5.1",
"vscode-tas-client": "^0.1.63",
"@vscode/sync-api-client": "^0.7.2",
"@vscode/sync-api-common": "^0.7.2",
"@vscode/sync-api-service": "^0.7.3",
"vscode-uri": "^3.0.3"
},
"devDependencies": {
Expand Down Expand Up @@ -1217,6 +1220,15 @@
"default": true,
"description": "%configuration.suggest.objectLiteralMethodSnippets.enabled%",
"scope": "resource"
},
"typescript.experimental.tsserver.web.enableProjectWideIntellisense": {
"type": "boolean",
"default": false,
"description": "%typescript.experimental.tsserver.web.enableProjectWideIntellisense%",
"scope": "window",
"tags": [
"experimental"
]
}
}
},
Expand Down
2 changes: 2 additions & 0 deletions extensions/typescript-language-features/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@
"configuration.suggest.classMemberSnippets.enabled": "Enable/disable snippet completions for class members.",
"configuration.suggest.objectLiteralMethodSnippets.enabled": "Enable/disable snippet completions for methods in object literals. Requires using TypeScript 4.7+ in the workspace.",

"typescript.experimental.tsserver.web.enableProjectWideIntellisense": "Enable/disable project-wide IntelliSense on web. Requires that VS Code is running in a trusted context.",

"walkthroughs.nodejsWelcome.title": "Get started with JavaScript and Node.js",
"walkthroughs.nodejsWelcome.description": "Make the most of Visual Studio Code's first-class JavaScript experience.",

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { Utils } from 'vscode-uri';
import { disposeAll, IDisposable } from '../utils/dispose';
import { ResourceMap } from '../utils/resourceMap';
import { Schemes } from '../utils/schemes';

type DirWatcherEntry = {
readonly uri: vscode.Uri;
readonly listeners: IDisposable[];
};


export class FileWatcherManager {

private readonly _fileWatchers = new Map<number, {
readonly watcher: vscode.FileSystemWatcher;
readonly dirWatchers: DirWatcherEntry[];
}>();

private readonly _dirWatchers = new ResourceMap<{
readonly watcher: vscode.FileSystemWatcher;
refCount: number;
}>(uri => uri.toString(), { onCaseInsensitiveFileSystem: false });

create(id: number, uri: vscode.Uri, watchParentDirs: boolean, isRecursive: boolean, listeners: { create?: (uri: vscode.Uri) => void; change?: (uri: vscode.Uri) => void; delete?: (uri: vscode.Uri) => void }): void {
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(uri, isRecursive ? '**' : '*'), !listeners.create, !listeners.change, !listeners.delete);
const parentDirWatchers: DirWatcherEntry[] = [];
this._fileWatchers.set(id, { watcher, dirWatchers: parentDirWatchers });

if (listeners.create) { watcher.onDidCreate(listeners.create); }
if (listeners.change) { watcher.onDidChange(listeners.change); }
if (listeners.delete) { watcher.onDidDelete(listeners.delete); }

if (watchParentDirs && uri.scheme !== Schemes.untitled) {
// We need to watch the parent directories too for when these are deleted / created
for (let dirUri = Utils.dirname(uri); dirUri.path.length > 1; dirUri = Utils.dirname(dirUri)) {
const dirWatcher: DirWatcherEntry = { uri: dirUri, listeners: [] };

let parentDirWatcher = this._dirWatchers.get(dirUri);
if (!parentDirWatcher) {
const glob = new vscode.RelativePattern(Utils.dirname(dirUri), Utils.basename(dirUri));
const parentWatcher = vscode.workspace.createFileSystemWatcher(glob, !listeners.create, true, !listeners.delete);
parentDirWatcher = { refCount: 0, watcher: parentWatcher };
this._dirWatchers.set(dirUri, parentDirWatcher);
}
parentDirWatcher.refCount++;

if (listeners.create) {
dirWatcher.listeners.push(parentDirWatcher.watcher.onDidCreate(async () => {
// Just because the parent dir was created doesn't mean our file was created
try {
const stat = await vscode.workspace.fs.stat(uri);
if (stat.type === vscode.FileType.File) {
listeners.create!(uri);
}
} catch {
// Noop
}
}));
}

if (listeners.delete) {
// When the parent dir is deleted, consider our file deleted too
// TODO: this fires if the file previously did not exist and then the parent is deleted
dirWatcher.listeners.push(parentDirWatcher.watcher.onDidDelete(listeners.delete));
}

parentDirWatchers.push(dirWatcher);
}
}
}

delete(id: number): void {
const entry = this._fileWatchers.get(id);
if (entry) {
for (const dirWatcher of entry.dirWatchers) {
disposeAll(dirWatcher.listeners);

const dirWatcherEntry = this._dirWatchers.get(dirWatcher.uri);
if (dirWatcherEntry) {
if (--dirWatcherEntry.refCount <= 0) {
dirWatcherEntry.watcher.dispose();
this._dirWatchers.delete(dirWatcher.uri);
}
}
}

entry.watcher.dispose();
}

this._fileWatchers.delete(id);
}
}
16 changes: 12 additions & 4 deletions extensions/typescript-language-features/src/tsServer/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { Cancellation } from '@vscode/sync-api-common/lib/common/messageCancellation';
import type * as Proto from '../protocol';
import { EventName } from '../protocol.const';
import { CallbackMap } from '../tsServer/callbackMap';
Expand All @@ -17,6 +18,7 @@ import Tracer from '../utils/tracer';
import { OngoingRequestCanceller } from './cancellation';
import { TypeScriptVersionManager } from './versionManager';
import { TypeScriptVersion } from './versionProvider';
import { isWebAndHasSharedArrayBuffers } from '../utils/platform';

export enum ExecutionTarget {
Semantic,
Expand Down Expand Up @@ -64,6 +66,7 @@ export interface TsServerProcessFactory {
kind: TsServerProcessKind,
configuration: TypeScriptServiceConfiguration,
versionManager: TypeScriptVersionManager,
extensionUri: vscode.Uri,
): TsServerProcess;
}

Expand Down Expand Up @@ -171,17 +174,16 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe
}
}

private tryCancelRequest(seq: number, command: string): boolean {
private tryCancelRequest(request: Proto.Request, command: string): boolean {
const seq = request.seq;
try {
if (this._requestQueue.tryDeletePendingRequest(seq)) {
this.logTrace(`Canceled request with sequence number ${seq}`);
return true;
}

if (this._requestCanceller.tryCancelOngoingRequest(seq)) {
return true;
}

this.logTrace(`Tried to cancel request with sequence number ${seq}. But request got already delivered.`);
return false;
} finally {
Expand Down Expand Up @@ -221,8 +223,14 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe
this._callbacks.add(request.seq, { onSuccess: resolve as () => ServerResponse.Response<Proto.Response> | undefined, onError: reject, queuingStartTime: Date.now(), isAsync: executeInfo.isAsync }, executeInfo.isAsync);

if (executeInfo.token) {

const cancelViaSAB = isWebAndHasSharedArrayBuffers()
? Cancellation.addData(request)
: undefined;

executeInfo.token.onCancellationRequested(() => {
this.tryCancelRequest(request.seq, command);
cancelViaSAB?.();
this.tryCancelRequest(request, command);
});
}
}).catch((err: Error) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,43 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

/// <reference lib='webworker' />
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { TypeScriptServiceConfiguration } from '../utils/configuration';
import { memoize } from '../utils/memoize';
import { TsServerProcess, TsServerProcessKind } from './server';
import { TypeScriptVersion } from './versionProvider';



declare const Worker: any;
declare type Worker = any;
import { ServiceConnection } from '@vscode/sync-api-common/browser';
import { Requests, ApiService } from '@vscode/sync-api-service';
import { TypeScriptVersionManager } from './versionManager';
import { FileWatcherManager } from './fileWatchingManager';
type BrowserWatchEvent = {
type: 'watchDirectory' | 'watchFile';
recursive?: boolean;
uri: {
scheme: string;
authority: string;
path: string;
};
id: number;
} | {
type: 'dispose';
id: number;
};

export class WorkerServerProcess implements TsServerProcess {

public static fork(
version: TypeScriptVersion,
args: readonly string[],
_kind: TsServerProcessKind,
_configuration: TypeScriptServiceConfiguration,
_versionManager: TypeScriptVersionManager,
extensionUri: vscode.Uri,
) {
const tsServerPath = version.tsServerPath;
const worker = new Worker(tsServerPath);
return new WorkerServerProcess(worker, [
return new WorkerServerProcess(worker, extensionUri, [
...args,

// Explicitly give TS Server its path so it can
Expand All @@ -37,27 +50,78 @@ export class WorkerServerProcess implements TsServerProcess {
private readonly _onDataHandlers = new Set<(data: Proto.Response) => void>();
private readonly _onErrorHandlers = new Set<(err: Error) => void>();
private readonly _onExitHandlers = new Set<(code: number | null, signal: string | null) => void>();
private readonly watches = new FileWatcherManager();
/** For communicating with TS server synchronously */
private readonly tsserver: MessagePort;
/** For communicating watches asynchronously */
private readonly watcher: MessagePort;
/** For communicating with filesystem synchronously */
private readonly syncFs: MessagePort;

public constructor(
private readonly worker: Worker,
/** For logging and initial setup */
private readonly mainChannel: Worker,
extensionUri: vscode.Uri,
args: readonly string[],
) {
worker.addEventListener('message', (msg: any) => {
if (msg.data.type === 'log') {
this.output.append(msg.data.body);
const tsserverChannel = new MessageChannel();
const watcherChannel = new MessageChannel();
const syncChannel = new MessageChannel();
this.tsserver = tsserverChannel.port2;
this.watcher = watcherChannel.port2;
this.syncFs = syncChannel.port2;
this.tsserver.onmessage = (event) => {
if (event.data.type === 'log') {
console.error(`unexpected log message on tsserver channel: ${JSON.stringify(event)}`);
return;
}

for (const handler of this._onDataHandlers) {
handler(msg.data);
handler(event.data);
}
});
worker.onerror = (err: Error) => {
};
this.watcher.onmessage = (event: MessageEvent<BrowserWatchEvent>) => {
switch (event.data.type) {
case 'dispose': {
this.watches.delete(event.data.id);
break;
}
case 'watchDirectory':
case 'watchFile': {
this.watches.create(event.data.id, vscode.Uri.from(event.data.uri), /*watchParentDirs*/ true, !!event.data.recursive, {
change: uri => this.watcher.postMessage({ type: 'watch', event: 'change', uri }),
create: uri => this.watcher.postMessage({ type: 'watch', event: 'create', uri }),
delete: uri => this.watcher.postMessage({ type: 'watch', event: 'delete', uri }),
});
break;
}
default:
console.error(`unexpected message on watcher channel: ${JSON.stringify(event)}`);
}
};
mainChannel.onmessage = (msg: any) => {
// for logging only
if (msg.data.type === 'log') {
this.output.append(msg.data.body);
return;
}
console.error(`unexpected message on main channel: ${JSON.stringify(msg)}`);
};
mainChannel.onerror = (err: ErrorEvent) => {
console.error('error! ' + JSON.stringify(err));
for (const handler of this._onErrorHandlers) {
handler(err);
// TODO: The ErrorEvent type might be wrong; previously this was typed as Error and didn't have the property access.
handler(err.error);
}
};
worker.postMessage(args);
this.output.append(`creating new MessageChannel and posting its port2 + args: ${args.join(' ')}\n`);
mainChannel.postMessage(
{ args, extensionUri },
[syncChannel.port1, tsserverChannel.port1, watcherChannel.port1]
);
const connection = new ServiceConnection<Requests>(syncChannel.port2);
new ApiService('vscode-wasm-typescript', connection);
connection.signalReady();
this.output.append('done constructing WorkerServerProcess\n');
}

@memoize
Expand All @@ -66,7 +130,7 @@ export class WorkerServerProcess implements TsServerProcess {
}

write(serverRequest: Proto.Request): void {
this.worker.postMessage(serverRequest);
this.tsserver.postMessage(serverRequest);
}

onData(handler: (response: Proto.Response) => void): void {
Expand All @@ -83,6 +147,10 @@ export class WorkerServerProcess implements TsServerProcess {
}

kill(): void {
this.worker.terminate();
this.mainChannel.terminate();
this.tsserver.close();
this.watcher.close();
this.syncFs.close();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export class TypeScriptServerSpawner {
private readonly _telemetryReporter: TelemetryReporter,
private readonly _tracer: Tracer,
private readonly _factory: TsServerProcessFactory,
private readonly _extensionUri: vscode.Uri,
) { }

public spawn(
Expand Down Expand Up @@ -152,7 +153,7 @@ export class TypeScriptServerSpawner {
}

this._logger.info(`<${kind}> Forking...`);
const process = this._factory.fork(version, args, kind, configuration, this._versionManager);
const process = this._factory.fork(version, args, kind, configuration, this._versionManager, this._extensionUri);
this._logger.info(`<${kind}> Starting...`);

return new ProcessBasedTsServer(
Expand Down
Loading