Cross-file Typescript support in vscode-web (#169311)

* recreate logging from other machine

* comment out openSystemBrowser

Because I dont have the default browser set up correctnly on any of my
machiens

* Add vscode-wasm-typescript dep

And some logging as I figure out how to use it

* remove unused reference to module

* use require reference that linter allows

* Add vscode-wasm-typescript to tsserver.web.js

Use webpack's CopyPlugin transform pattern to do this manually. This is
probably a bad idea! It's just for prototyping purposes.

* Update vscode-wasm-typescript dependency

* Fix minor syntax in webpack hack

Nonetheless required for it to work!

* Fix another typo in webpack hack!

* Fix provided typescript path

Another typo. Guess my local test wasn't running the contents really

* Try to improve module.exports handling in webpack hac

* tsserver.web.js comes from local builds

Also:
- tsserver.js is no longer minified
- log crossOriginIsolated

* First attempt to set up server-side support

* Remove auto-imported identifier

* Move sync-api setup code to serverProcess.browser.ts

Because it's browser-specific

* Reorder webpack hack and clean up unused logging

* Update vscode-wasm/vscode-wasm-typescript dependencies

* Add file watching

* Extract webpack hack

Build only the ts parts of tsserver.web.js, don't rebuild the vscode
extension. This is a lot faster.

* Remove manual verbose logging

Sheetal showed me the correct way to create a verbose logger instead.

* Add vscode-test-web to semantic-supported schemes

And make isWeb support semantic mode.

* Also update the webpack-hack-only build

* Switch to tsserverlibrary

Also paste in some example code for cancellation, which is not finished
at all.

* Remove bogus auto-import and unneeded (?) dep

* remove webpack-like hack

* move code from vscode-wasm-typescript

* Initial prototype of cancellation

It compiles and looks kind of plausible. But I haven't tested it yet.

* Switch tsserver to separate MessageChannel

* Move watches to a separate MessagePort

Further simplifies the message dispatch code by shifting complexity to
setup. And the setup is straight-line code.

* switch vscode-web from in-memory to real filesystem

goto-def is currently broken because some part of main vscode still
needs treat the files as in-memory, though.

* Make toResource translate / -> vscode-test-web

* Encode scheme and authority in TS filenames

Like the previous host did, but without the leading ^ that TS hard-codes
as "in-memory".

The tsserver host needs to know about the encoding, but the translation
is in a single function. This also means that displayed file paths are
prefixed with /scheme/authority (/vscode-test-web/mount in my testing), but I think that's fine.

* Lift parseUri outside createServerHost

I'm not using it to set the schema/authority for getCurrentDirectory
right now, so there's no shared state to mutate.

* Special-case URI of lib*d.ts in webServer.toResource

Similar to the special-casing in typescriptServiceClient.toResource.
Also requires passing in the extensionUri. This feels like it's breaking
through at least one abstraction layer, which might be a problem.

* Improve cancellation

1. Simplify cancellation checking in web tsserver host to match the
checking in typescript's node host.
2. Move cancellation of request in the extension to tryCancelRequest
from sendNextRequests.
3. Only send cancellation via node or web cancellation, not both.

* Pass in current request instead of waiting for a fresh one.

* Address initial PR comments

Also add some TODO comments for the revision to watches.

* Add cancellation bit to each (cancellable) request, locally fix an issue with retrieving the cancellation bit

* Switch to per-file/directory watches

Watching the entire filesystem recursively is supposed to be
inefficient.

Not done yet: there is an error when watching directories, but it works.
And I can't tell whether watching files works yet.

* Parse --serverMode partialSemantic in webServer

Now the syntax server actually runs as a syntax server.

* Simplify logging code

* Cleanup in webServer

1. Remove a little logging.
2. Correct failure return value for getFileSize
3. Reorder some methods and parameters.

* Switch to markdown extension's FileWatcherManager

I'm not sure if it's OK to depend on a module from another extension;
it's probably better to include the files from a central place instead.

* Clean up host methods

1. Copy and adapt implementations from node host where possible.
2. Note questions for the PR elsewhere.
3. Remove logging except for caught exceptions.

* More logging/TODO cleanup

* Remove duplicate dependency

* Add setting to enable/disable semantic mode on web

Also gate it behind a check to `crossOriginIsolated`

* Re-order and re-arrange code to minimise PR diff

It won't minimise it *much*, but I also consolidated some
unnecessarily-spread-out code that will be easier to read in the long
term, and possibly easier to read in diff form as well.

* Copy fileWatchingManager to typescript extension

Copy from markdown extension to typescript extension. I used the
existing dependencies in the typescript extension, but verified that
they would work the same.

* Fix linting of webServer

* Align formatting of catch / else

* Extract isProjectWideIntellisenseOnWebEnabled and keep using in-memory prefix when project wide intellisense is disabled

* Make sure we still work if SharedArrayBuffers aren't supported

* Remove symlink support and fix typo

Symlinks are implicitly supported by the filesystem right now.

* Fix compile errors

Co-authored-by: Johannes <johannes.rieken@gmail.com>
Co-authored-by: Matt Bierner <matb@microsoft.com>
This commit is contained in:
Nathan Shively-Sanders
2023-01-12 11:54:39 -08:00
committed by GitHub
parent e14165e0f5
commit 3261c7d3af
17 changed files with 926 additions and 438 deletions

View File

@@ -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": {
@@ -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"
]
}
}
},

View File

@@ -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.",

View File

@@ -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);
}
}

View File

@@ -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';
@@ -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,
@@ -64,6 +66,7 @@ export interface TsServerProcessFactory {
kind: TsServerProcessKind,
configuration: TypeScriptServiceConfiguration,
versionManager: TypeScriptVersionManager,
extensionUri: vscode.Uri,
): TsServerProcess;
}
@@ -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 {
@@ -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) => {

View File

@@ -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
@@ -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) => {
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(event.data);
}
};
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;
}
for (const handler of this._onDataHandlers) {
handler(msg.data);
}
});
worker.onerror = (err: Error) => {
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
@@ -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 {
@@ -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();
}
}

View File

@@ -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(
@@ -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(

View File

@@ -19,16 +19,16 @@ import { TypeScriptVersionManager } from './tsServer/versionManager';
import { ITypeScriptVersionProvider, TypeScriptVersion } from './tsServer/versionProvider';
import { ClientCapabilities, ClientCapability, ExecConfig, ITypeScriptServiceClient, ServerResponse, TypeScriptRequests } from './typescriptService';
import API from './utils/api';
import { areServiceConfigurationsEqual, ServiceConfigurationProvider, SyntaxServerConfiguration, TsServerLogLevel, TypeScriptServiceConfiguration } from './utils/configuration';
import { ServiceConfigurationProvider, SyntaxServerConfiguration, TsServerLogLevel, TypeScriptServiceConfiguration, areServiceConfigurationsEqual } from './utils/configuration';
import { Disposable } from './utils/dispose';
import * as fileSchemes from './utils/fileSchemes';
import { Logger } from './utils/logger';
import { isWeb } from './utils/platform';
import { isWeb, isWebAndHasSharedArrayBuffers } from './utils/platform';
import { TypeScriptPluginPathsProvider } from './utils/pluginPathsProvider';
import { PluginManager, TypeScriptServerPlugin } from './utils/plugins';
import { TelemetryProperties, TelemetryReporter, VSCodeTelemetryReporter } from './utils/telemetry';
import Tracer from './utils/tracer';
import { inferredProjectCompilerOptions, ProjectType } from './utils/tsconfig';
import { ProjectType, inferredProjectCompilerOptions } from './utils/tsconfig';
export interface TsDiagnostics {
@@ -214,7 +214,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType
return this.apiVersion.fullVersionString;
});
this.typescriptServerSpawner = new TypeScriptServerSpawner(this.versionProvider, this._versionManager, this.logDirectoryProvider, this.pluginPathsProvider, this.logger, this.telemetryReporter, this.tracer, this.processFactory);
this.typescriptServerSpawner = new TypeScriptServerSpawner(this.versionProvider, this._versionManager, this.logDirectoryProvider, this.pluginPathsProvider, this.logger, this.telemetryReporter, this.tracer, this.processFactory, context.extensionUri);
this._register(this.pluginManager.onDidUpdateConfig(update => {
this.configurePlugin(update.pluginId, update.config);
@@ -233,9 +233,16 @@ export default class TypeScriptServiceClient extends Disposable implements IType
}
if (isWeb()) {
return new ClientCapabilities(
ClientCapability.Syntax,
ClientCapability.EnhancedSyntax);
if (this.isProjectWideIntellisenseOnWebEnabled()) {
return new ClientCapabilities(
ClientCapability.Syntax,
ClientCapability.EnhancedSyntax,
ClientCapability.Semantic);
} else {
return new ClientCapabilities(
ClientCapability.Syntax,
ClientCapability.EnhancedSyntax);
}
}
if (this.apiVersion.gte(API.v400)) {
@@ -253,6 +260,10 @@ export default class TypeScriptServiceClient extends Disposable implements IType
private readonly _onDidChangeCapabilities = this._register(new vscode.EventEmitter<void>());
readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event;
private isProjectWideIntellisenseOnWebEnabled(): boolean {
return isWebAndHasSharedArrayBuffers() && this._configuration.enableProjectWideIntellisenseOnWeb;
}
private cancelInflightRequestsForResource(resource: vscode.Uri): void {
if (this.serverState.type !== ServerState.Type.Running) {
return;
@@ -678,7 +689,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType
}
default:
{
return this.inMemoryResourcePrefix
return (this.isProjectWideIntellisenseOnWebEnabled() ? '' : this.inMemoryResourcePrefix)
+ '/' + resource.scheme
+ '/' + (resource.authority || this.emptyAuthority)
+ (resource.path.startsWith('/') ? resource.path : '/' + resource.path)
@@ -722,9 +733,16 @@ export default class TypeScriptServiceClient extends Disposable implements IType
public toResource(filepath: string): vscode.Uri {
if (isWeb()) {
// On web, the stdlib paths that TS return look like: '/lib.es2015.collection.d.ts'
// TODO: Find out what extensionUri is when testing (should be http://localhost:8080/static/sources/extensions/typescript-language-features/)
// TODO: make sure that this code path is getting hit
if (filepath.startsWith('/lib.') && filepath.endsWith('.d.ts')) {
return vscode.Uri.joinPath(this.context.extensionUri, 'dist', 'browser', 'typescript', filepath.slice(1));
}
const parts = filepath.match(/^\/([^\/]+)\/([^\/]*)\/(.+)$/);
if (parts) {
const resource = vscode.Uri.parse(parts[1] + '://' + (parts[2] === this.emptyAuthority ? '' : parts[2]) + '/' + parts[3]);
return this.bufferSyncSupport.toVsCodeResource(resource);
}
}
if (filepath.startsWith(this.inMemoryResourcePrefix)) {

View File

@@ -110,6 +110,7 @@ export interface TypeScriptServiceConfiguration {
readonly implicitProjectConfiguration: ImplicitProjectConfiguration;
readonly disableAutomaticTypeAcquisition: boolean;
readonly useSyntaxServer: SyntaxServerConfiguration;
readonly enableProjectWideIntellisenseOnWeb: boolean;
readonly enableProjectDiagnostics: boolean;
readonly maxTsServerMemory: number;
readonly enablePromptUseWorkspaceTsdk: boolean;
@@ -140,6 +141,7 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu
implicitProjectConfiguration: new ImplicitProjectConfiguration(configuration),
disableAutomaticTypeAcquisition: this.readDisableAutomaticTypeAcquisition(configuration),
useSyntaxServer: this.readUseSyntaxServer(configuration),
enableProjectWideIntellisenseOnWeb: this.readEnableProjectWideIntellisenseOnWeb(configuration),
enableProjectDiagnostics: this.readEnableProjectDiagnostics(configuration),
maxTsServerMemory: this.readMaxTsServerMemory(configuration),
enablePromptUseWorkspaceTsdk: this.readEnablePromptUseWorkspaceTsdk(configuration),
@@ -222,4 +224,8 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu
protected readEnableTsServerTracing(configuration: vscode.WorkspaceConfiguration): boolean {
return configuration.get<boolean>('typescript.tsserver.enableTracing', false);
}
private readEnableProjectWideIntellisenseOnWeb(configuration: vscode.WorkspaceConfiguration): boolean {
return configuration.get<boolean>('typescript.experimental.tsserver.web.enableProjectWideIntellisense', false);
}
}

View File

@@ -12,6 +12,10 @@ export function disposeAll(disposables: vscode.Disposable[]) {
}
}
export interface IDisposable {
dispose(): void;
}
export abstract class Disposable {
private _isDisposed = false;

View File

@@ -3,6 +3,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isWeb } from './platform';
import * as vscode from 'vscode';
export const file = 'file';
export const untitled = 'untitled';
export const git = 'git';
@@ -14,12 +17,13 @@ export const memFs = 'memfs';
export const vscodeVfs = 'vscode-vfs';
export const officeScript = 'office-script';
export const semanticSupportedSchemes = [
file,
untitled,
walkThroughSnippet,
vscodeNotebookCell,
];
export const semanticSupportedSchemes = isWeb() && vscode.workspace.workspaceFolders ?
vscode.workspace.workspaceFolders.map(folder => folder.uri.scheme) : [
file,
untitled,
walkThroughSnippet,
vscodeNotebookCell,
];
/**
* File scheme for which JS/TS language feature should be disabled

View File

@@ -6,6 +6,9 @@
import * as vscode from 'vscode';
export function isWeb(): boolean {
// @ts-expect-error
return typeof navigator !== 'undefined' && vscode.env.uiKind === vscode.UIKind.Web;
return 'navigator' in globalThis && vscode.env.uiKind === vscode.UIKind.Web;
}
export function isWebAndHasSharedArrayBuffers(): boolean {
return isWeb() && (globalThis as any)['crossOriginIsolated'];
}

View File

@@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const Schemes = Object.freeze({
file: 'file',
untitled: 'untitled',
mailto: 'mailto',
vscode: 'vscode',
'vscode-insiders': 'vscode-insiders',
notebookCell: 'vscode-notebook-cell',
});
export function isOfScheme(scheme: string, link: string): boolean {
return link.toLowerCase().startsWith(scheme + ':');
}

View File

@@ -0,0 +1,10 @@
module.exports = {
"parserOptions": {
"tsconfigRootDir": __dirname,
"project": "./tsconfig.json"
},
"rules": {
"@typescript-eslint/prefer-optional-chain": "warn",
"@typescript-eslint/prefer-readonly": "warn"
}
};

View File

@@ -0,0 +1,154 @@
# vscode-wasm-typescript
Language server host for typescript using vscode's sync-api in the browser
## TODOs
### Prototype
- [x] get semantic diagnostics rendering squigglies
- typescriptserviceclient.ts has some functions that look at `scheme` to determine some features (hasCapabilityForResource) (also getWorkspaceRootForResource)
- known schemes are in utils/fileSchemes.ts, but don't include vscode-test-web
- adding vscode-test-web in a couple places didn't help, maybe I need to be hackier
- nope, another predicate is `isWeb`, so I had to change place(s) it's used too
- [x] cancellation
### Cleanup
- [x] point webpack hack to node_modules; link those files to locally built ones
- [x] create one or more MessageChannels for various communication
- [x] shut down normal listener
- starting the server currently crashes because ts.sys isn't defined -- I think it's a race condition.
In any case it'll need to get shut down before then, which may not be possible without changing Typescript.
- LATER: Turns out you can skip the existing server by depending on tsserverlibrary instead of tsserver.
- [x] figure out a webpack-native way to generate tsserver.web.js if possible
- [x] path rewriting is pretty loosey-goosey; likely to be incorrect some of the time
- invert the logic from TypeScriptServiceClient.normalizedPath for requests
- invert the function from webServer.ts for responses (maybe)
- something with getWorkspaceRootForResource (or anything else that checks `resouce.scheme`)
- [x] put files one level down from virtual root
- [x] fill in missing environment files like lib.dom.d.ts
- toResource's isWeb branch *probably* knows where to find this, just need to put it in the virtual FS
- I guess during setup in serverProcess.browser.ts.
- Not sure whether it needs to have the data or just a fs entry.
- Wait, I don't know how files get added to the FS normally.
- [x] cancellation should only retain one cancellation checker
- the one that matches the current request id
- but that means tracking (or retrieving from tsserver) the request id (aka seq?)
- and correctly setting/resetting it on the cancellation token too.
- I looked at the tsserver code. I think the web case is close to the single-pipe node case,
so I just require that requestId is set in order to call the *current* cancellation checker.
- Any incoming message with a cancellation checker will overwrite the current one.
- [x] Cancellation code in vscode is suspiciously prototypey.
- Specifically, it adds the vscode-wasm cancellation to original cancellation code, but should actually switch to the former for web only.
- looks like `isWeb()` is a way to check for being on the web
- [x] create multiple watchers
- on-demand instead of watching everything and checking on watch firing
- [x] get file watching to work
- it could *already* work, I just don't know how to test it
- look at extensions/markdown-language-features/src/client/fileWatchingManager.ts to see if I can use that
- later: it is OK. its main difference is that you can watch files in not-yet-created directories, and it maintains
a web of directory watches that then check whether the file is eventually created.
- even later: well, it works even though it is similar to my code.
I'm not sure what is different.
- [x] copy fileWatchingManager.ts to web/ ; there's no sharing code between extensions
- [x] Find out scheme the web actually uses instead of vscode-test-web (or switch over entirely to isWeb)
- [x] Need to parse and pass args through so that the syntax server isn't hard-coded to actually be another semantic server
- [x] think about implementing all the other ServerHost methods
- [x] copy importPlugin from previous version of webServer.ts
- [x] also copy details from
- previous implementation (although it's syntax-only so only covers part)
- node implementation in typescript proper
- [x] make realpath support symlinks similarly to node's realpath.
- Johannes says that the filesystem automatically follows symlinks,
so I don't think this is needed.
- [x] organise webServer.ts into multiple files
- OR at least re-arrange it so the diff with the previous version is smaller
- split it into multiple files after the initial PR
- [x] clear out TODOs
- [x] add semicolons everywhere; vscode's lint doesn't seem to complain, but the code clearly uses them
- [x] Further questions about host methods based on existing implementations
- `require` -- is this needed? In TS, it's only used in project system
- `trace` -- is this needed? In TS, it's only used in project system
- `useCaseSensitiveFileNames` -- old version says 'false' is the
safest option, but the virtual fs is case sensitive. Is the old
version still better?
- `writeOutputIsTTY` -- I'm using apiClient.vscode.terminal.write -- is it a tty?
- `getWidthOfTerminal` -- I don't know where to find this on apiClient.vscode.terminal either
- `clearScreen` -- node version writes \x1BC to the terminal. Would
this work for vscode?
- `readFile/writeFile` -- TS handles utf8, utf16le and manually
converts big-endian to utf16 little-endian. How does the in-memory
filesystem handle this? There's no place to specify encoding. (And
`writeFile` currently ignores the flag to write a BOM.)
- `resolvePath` -- node version uses path.resolve. Is it OK to use
that? Or should I re-implement it? Just use identity like the old
web code?
- `getDirectories`/`readDirectory`
- the node code manually skips '.' and '..' in the array returned by
readDirectory. Is this needed?
- `createSHA256Hash` -- the browser version is async, so I skipped it
- `realpath` -- still skips symlinks, I need to figure out what node does
### Bugs
- [x] Response `seq` is always 0.
- [ ] current method of encoding /scheme/authority means that (node) module resolution looks for /scheme/node_modules and /node_modules
- even though they can't possibly exist
- probably not a problem though
- [x] problems pane doesn't clear problems issued on tsconfig.
- This is a known problem in normal usage as well.
- [x] renaming a file throws a No Project error to the console.
- [x] gotodef in another file throws and the editor has a special UI for it.
- definitionProviderBase.getSymbolLocations calls toOpenedFilePath which eventually calls the new / code
- then it calls client.execute which appears to actually request/response to the tsserver
- then the response body is mapped over location.file >> client.toResource >> fromTextSpan
- toResource has isWeb support, as well as (now unused) inMemoryResourcePrefix support
- so I can just redo whatever that did and it'll be fine
### Done
- [x] need to update 0.2 -> 0.7.* API (once it's working properly)
- [x] including reshuffling the webpack hack if needed
- [x] need to use the settings recommended by Sheetal
- [x] ProjectService always requests a typesMap.json at the cwd
- [x] sync-api-client says fs is rooted at memfs:/sample-folder; the protocol 'memfs:' is confusing our file parsing I think
- [x] nothing ever seems to find tsconfig.json
- [x] messages aren't actually coming through, just the message from the first request
- fixed by simplifying the listener setup for now
- [x] once messages work, you can probably log by postMessage({ type: 'log', body: "some logging text" })
- [x] implement realpath, modifiedtime, resolvepath, then turn semantic mode on
- [x] file watching implemented with saved map of filename to callback, and forwarding
### Also
- [ ] ATA will eventually need a host interface, or an improvement of the existing one (?)
## Notes
messages received by extension AND host use paths like ^/memfs/ts-nul-authority/sample-folder/file.ts
- problem: pretty sure the extension doesn't know what to do with that: it's not putting down error spans in file.ts
- question: why is the extension requesting quickinfo in that URI format? And it works! (probably because the result is a tooltip, not an in-file span)
- problem: weird concatenations with memfs:/ in the middle
- problem: weird concatenations with ^/memfs/ts-nul-authority in the middle
question: where is the population of sample-folder with a bunch of files happening?
question: Is that location writable while it's running?
but readFile is getting called with things like memfs:/sample-folder/memfs:/typesMap.json
directoryExists with /sample-folder/node_modules/@types and /node_modules/@types
same for watchDirectory
watchDirectory with /sample-folder/^ and directoryExists with /sample-folder/^/memfs/ts-nul-authority/sample-folder/workspaces/
watchFile with /sample-folder/memfs:/sample-folder/memfs:/lib.es2020.full.d.ts
### LATER:
OK, so the paths that tsserver has look like this: ^/scheme/mount/whatever.ts
but the paths the filesystem has look like this: scheme:/whatever.ts (not sure about 'mount', that's only when cloning from the fs)
so you have to shave off the scheme that the host combined with the path and put on the scheme that the vfs is using.
### LATER 2:
Some commands ask for getExecutingFilePath or getCurrentDirectory and cons up a path themselves.
This works, because URI.from({ scheme, path }) matches what the fs has in it
Problem: In *some* messages (all?), vscode then refers to /x.ts and ^/vscode-test-web/mount/x.ts (or ^/memfs/ts-nul-authority/x.ts)

View File

@@ -4,262 +4,390 @@
*--------------------------------------------------------------------------------------------*/
/// <reference lib='webworker.importscripts' />
/// <reference lib='dom' />
import * as ts from 'typescript/lib/tsserverlibrary';
// BEGIN misc internals
const hasArgument: (argumentName: string) => boolean = (ts as any).server.hasArgument;
const findArgument: (argumentName: string) => string | undefined = (ts as any).server.findArgument;
const nowString: () => string = (ts as any).server.nowString;
const noop = () => { };
const perfLogger = {
logEvent: noop,
logErrEvent(_: any) { },
logPerfEvent(_: any) { },
logInfoEvent(_: any) { },
logStartCommand: noop,
logStopCommand: noop,
logStartUpdateProgram: noop,
logStopUpdateProgram: noop,
logStartUpdateGraph: noop,
logStopUpdateGraph: noop,
logStartResolveModule: noop,
logStopResolveModule: noop,
logStartParseSourceFile: noop,
logStopParseSourceFile: noop,
logStartReadFile: noop,
logStopReadFile: noop,
logStartBindFile: noop,
logStopBindFile: noop,
logStartScheduledOperation: noop,
logStopScheduledOperation: noop,
};
const assertNever: (member: never) => never = (ts as any).Debug.assertNever;
const memoize: <T>(callback: () => T) => () => T = (ts as any).memoize;
const ensureTrailingDirectorySeparator: (path: string) => string = (ts as any).ensureTrailingDirectorySeparator;
const getDirectoryPath: (path: string) => string = (ts as any).getDirectoryPath;
const directorySeparator: string = (ts as any).directorySeparator;
const combinePaths: (path: string, ...paths: (string | undefined)[]) => string = (ts as any).combinePaths;
const noopFileWatcher: ts.FileWatcher = { close: noop };
const returnNoopFileWatcher = () => noopFileWatcher;
function getLogLevel(level: string | undefined) {
if (level) {
const l = level.toLowerCase();
for (const name in ts.server.LogLevel) {
if (isNaN(+name) && l === name.toLowerCase()) {
return ts.server.LogLevel[name] as any as ts.server.LogLevel;
}
}
}
return undefined;
}
const notImplemented: () => never = (ts as any).notImplemented;
const returnFalse: () => false = (ts as any).returnFalse;
const returnUndefined: () => undefined = (ts as any).returnUndefined;
const identity: <T>(x: T) => T = (ts as any).identity;
import * as ts from 'typescript/lib/tsserverlibrary';
import { ApiClient, FileType, Requests } from '@vscode/sync-api-client';
import { ClientConnection } from '@vscode/sync-api-common/browser';
import { URI } from 'vscode-uri';
// GLOBALS
const watchFiles: Map<string, { path: string; callback: ts.FileWatcherCallback; pollingInterval?: number; options?: ts.WatchOptions }> = new Map();
const watchDirectories: Map<string, { path: string; callback: ts.DirectoryWatcherCallback; recursive?: boolean; options?: ts.WatchOptions }> = new Map();
let session: WorkerSession | undefined;
// END GLOBALS
// BEGIN misc internals
const indent: (str: string) => string = (ts as any).server.indent;
const setSys: (s: ts.System) => void = (ts as any).setSys;
const validateLocaleAndSetLanguage: (
locale: string,
sys: { getExecutingFilePath(): string; resolvePath(path: string): string; fileExists(fileName: string): boolean; readFile(fileName: string): string | undefined },
) => void = (ts as any).validateLocaleAndSetLanguage;
const setStackTraceLimit: () => void = (ts as any).setStackTraceLimit;
const combinePaths: (path: string, ...paths: (string | undefined)[]) => string = (ts as any).combinePaths;
const byteOrderMarkIndicator = '\uFEFF';
const matchFiles: (
path: string,
extensions: readonly string[] | undefined,
excludes: readonly string[] | undefined,
includes: readonly string[] | undefined,
useCaseSensitiveFileNames: boolean,
currentDirectory: string,
depth: number | undefined,
getFileSystemEntries: (path: string) => { files: readonly string[]; directories: readonly string[] },
realpath: (path: string) => string
) => string[] = (ts as any).matchFiles;
const generateDjb2Hash = (ts as any).generateDjb2Hash;
// End misc internals
// BEGIN webServer/webServer.ts
interface HostWithWriteMessage {
writeMessage(s: any): void;
function fromResource(extensionUri: URI, uri: URI) {
if (uri.scheme === extensionUri.scheme
&& uri.authority === extensionUri.authority
&& uri.path.startsWith(extensionUri.path + '/dist/browser/typescript/lib.')
&& uri.path.endsWith('.d.ts')) {
return uri.path;
}
return `/${uri.scheme}/${uri.authority}${uri.path}`;
}
interface WebHost extends HostWithWriteMessage {
readFile(path: string): string | undefined;
fileExists(path: string): boolean;
}
class BaseLogger implements ts.server.Logger {
private seq = 0;
private inGroup = false;
private firstInGroup = true;
constructor(protected readonly level: ts.server.LogLevel) {
function updateWatch(event: 'create' | 'change' | 'delete', uri: URI, extensionUri: URI) {
const kind = event === 'create' ? ts.FileWatcherEventKind.Created
: event === 'change' ? ts.FileWatcherEventKind.Changed
: event === 'delete' ? ts.FileWatcherEventKind.Deleted
: ts.FileWatcherEventKind.Changed;
const path = fromResource(extensionUri, uri);
if (watchFiles.has(path)) {
watchFiles.get(path)!.callback(path, kind);
return;
}
static padStringRight(str: string, padding: string) {
return (str + padding).slice(0, padding.length);
let found = false;
for (const watch of Array.from(watchDirectories.keys()).filter(dir => path.startsWith(dir))) {
watchDirectories.get(watch)!.callback(path);
found = true;
}
close() {
}
getLogFileName(): string | undefined {
return undefined;
}
perftrc(s: string) {
this.msg(s, ts.server.Msg.Perf);
}
info(s: string) {
this.msg(s, ts.server.Msg.Info);
}
err(s: string) {
this.msg(s, ts.server.Msg.Err);
}
startGroup() {
this.inGroup = true;
this.firstInGroup = true;
}
endGroup() {
this.inGroup = false;
}
loggingEnabled() {
return true;
}
hasLevel(level: ts.server.LogLevel) {
return this.loggingEnabled() && this.level >= level;
}
msg(s: string, type: ts.server.Msg = ts.server.Msg.Err) {
switch (type) {
case ts.server.Msg.Info:
perfLogger.logInfoEvent(s);
break;
case ts.server.Msg.Perf:
perfLogger.logPerfEvent(s);
break;
default: // Msg.Err
perfLogger.logErrEvent(s);
break;
}
if (!this.canWrite()) { return; }
s = `[${nowString()}] ${s}\n`;
if (!this.inGroup || this.firstInGroup) {
const prefix = BaseLogger.padStringRight(type + ' ' + this.seq.toString(), ' ');
s = prefix + s;
}
this.write(s, type);
if (!this.inGroup) {
this.seq++;
}
}
protected canWrite() {
return true;
}
protected write(_s: string, _type: ts.server.Msg) {
if (!found) {
console.error(`no watcher found for ${path}`);
}
}
type MessageLogLevel = 'info' | 'perf' | 'error';
interface LoggingMessage {
readonly type: 'log';
readonly level: MessageLogLevel;
readonly body: string;
}
class MainProcessLogger extends BaseLogger {
constructor(level: ts.server.LogLevel, private host: HostWithWriteMessage) {
super(level);
}
protected override write(body: string, type: ts.server.Msg) {
let level: MessageLogLevel;
switch (type) {
case ts.server.Msg.Info:
level = 'info';
break;
case ts.server.Msg.Perf:
level = 'perf';
break;
case ts.server.Msg.Err:
level = 'error';
break;
default:
assertNever(type);
}
this.host.writeMessage({
type: 'log',
level,
body,
} as LoggingMessage);
}
}
type ServerHostWithImport = ts.server.ServerHost & { importPlugin(root: string, moduleName: string): Promise<ts.server.ModuleImportResult> };
function serverCreateWebSystem(host: WebHost, args: string[], getExecutingFilePath: () => string):
ts.server.ServerHost & {
importPlugin?(root: string, moduleName: string): Promise<ts.server.ModuleImportResult>;
getEnvironmentVariable(name: string): string;
} {
const returnEmptyString = () => '';
const getExecutingDirectoryPath = memoize(() => memoize(() => ensureTrailingDirectorySeparator(getDirectoryPath(getExecutingFilePath()))));
function createServerHost(extensionUri: URI, logger: ts.server.Logger, apiClient: ApiClient | undefined, args: string[], fsWatcher: MessagePort): ServerHostWithImport {
const currentDirectory = '/';
const fs = apiClient?.vscode.workspace.fileSystem;
let watchId = 0;
// Legacy web
const memoize: <T>(callback: () => T) => () => T = (ts as any).memoize;
const ensureTrailingDirectorySeparator: (path: string) => string = (ts as any).ensureTrailingDirectorySeparator;
const getDirectoryPath: (path: string) => string = (ts as any).getDirectoryPath;
const directorySeparator: string = (ts as any).directorySeparator;
const executingFilePath = findArgument(args, '--executingFilePath') || location + '';
const getExecutingDirectoryPath = memoize(() => memoize(() => ensureTrailingDirectorySeparator(getDirectoryPath(executingFilePath))));
// Later we could map ^memfs:/ to do something special if we want to enable more functionality like module resolution or something like that
const getWebPath = (path: string) => path.startsWith(directorySeparator) ? path.replace(directorySeparator, getExecutingDirectoryPath()) : undefined;
return {
args,
newLine: '\r\n', // This can be configured by clients
useCaseSensitiveFileNames: false, // Use false as the default on web since that is the safest option
readFile: path => {
const webPath = getWebPath(path);
return webPath && host.readFile(webPath);
watchFile(path: string, callback: ts.FileWatcherCallback, pollingInterval?: number, options?: ts.WatchOptions): ts.FileWatcher {
watchFiles.set(path, { path, callback, pollingInterval, options });
watchId++;
fsWatcher.postMessage({ type: 'watchFile', uri: toResource(path), id: watchId });
return {
close() {
watchFiles.delete(path);
fsWatcher.postMessage({ type: 'dispose', id: watchId });
}
};
},
write: host.writeMessage.bind(host),
watchFile: returnNoopFileWatcher,
watchDirectory: returnNoopFileWatcher,
getExecutingFilePath: () => directorySeparator,
getCurrentDirectory: returnEmptyString, // For inferred project root if projectRoot path is not set, normalizing the paths
/* eslint-disable no-restricted-globals */
setTimeout: (cb, ms, ...args) => setTimeout(cb, ms, ...args),
clearTimeout: handle => clearTimeout(handle),
setImmediate: x => setTimeout(x, 0),
clearImmediate: handle => clearTimeout(handle),
/* eslint-enable no-restricted-globals */
importPlugin: async (initialDir: string, moduleName: string): Promise<ts.server.ModuleImportResult> => {
const packageRoot = combinePaths(initialDir, moduleName);
watchDirectory(path: string, callback: ts.DirectoryWatcherCallback, recursive?: boolean, options?: ts.WatchOptions): ts.FileWatcher {
watchDirectories.set(path, { path, callback, recursive, options });
watchId++;
fsWatcher.postMessage({ type: 'watchDirectory', recursive, uri: toResource(path), id: watchId });
return {
close() {
watchDirectories.delete(path);
fsWatcher.postMessage({ type: 'dispose', id: watchId });
}
};
},
setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any {
return setTimeout(callback, ms, ...args);
},
clearTimeout(timeoutId: any): void {
clearTimeout(timeoutId);
},
setImmediate(callback: (...args: any[]) => void, ...args: any[]): any {
return this.setTimeout(callback, 0, ...args);
},
clearImmediate(timeoutId: any): void {
this.clearTimeout(timeoutId);
},
importPlugin: async (root, moduleName) => {
const packageRoot = combinePaths(root, moduleName);
let packageJson: any | undefined;
try {
const packageJsonResponse = await fetch(combinePaths(packageRoot, 'package.json'));
packageJson = await packageJsonResponse.json();
}
catch (e) {
return { module: undefined, error: new Error('Could not load plugin. Could not load "package.json".') };
} catch (e) {
return { module: undefined, error: new Error(`Could not load plugin. Could not load 'package.json'.`) };
}
const browser = packageJson.browser;
if (!browser) {
return { module: undefined, error: new Error('Could not load plugin. No "browser" field found in package.json.') };
return { module: undefined, error: new Error(`Could not load plugin. No 'browser' field found in package.json.`) };
}
const scriptPath = combinePaths(packageRoot, browser);
try {
const { default: module } = await import(/* webpackIgnore: true */scriptPath);
return { module, error: undefined };
}
catch (e) {
} catch (e) {
return { module: undefined, error: e };
}
},
exit: notImplemented,
// Debugging related
getEnvironmentVariable: returnEmptyString, // TODO:: Used to enable debugging info
// tryEnableSourceMapsForHost?(): void;
// debugMode?: boolean;
// For semantic server mode
fileExists: path => {
const webPath = getWebPath(path);
return !!webPath && host.fileExists(webPath);
args,
newLine: '\n',
useCaseSensitiveFileNames: true,
write: s => {
apiClient?.vscode.terminal.write(s);
},
directoryExists: returnFalse, // Module resolution
readDirectory: notImplemented, // Configured project, typing installer
getDirectories: () => [], // For automatic type reference directives
createDirectory: notImplemented, // compile On save
writeFile: notImplemented, // compile on save
resolvePath: identity, // Plugins
// realpath? // Module resolution, symlinks
// getModifiedTime // File watching
// createSHA256Hash // telemetry of the project
writeOutputIsTTY() {
return true;
},
readFile(path) {
if (!fs) {
const webPath = getWebPath(path);
if (webPath) {
const request = new XMLHttpRequest();
request.open('GET', webPath, /* asynchronous */ false);
request.send();
return request.status === 200 ? request.responseText : undefined;
} else {
return undefined;
}
}
// Logging related
// /*@internal*/ bufferFrom?(input: string, encoding?: string): Buffer;
// gc?(): void;
// getMemoryUsage?(): number;
try {
// @vscode/sync-api-common/connection says that Uint8Array is only a view on the bytes, so slice is needed
return new TextDecoder().decode(new Uint8Array(fs.readFile(toResource(path))).slice());
} catch (e) {
logger.info(`Error fs.readFile`);
logger.info(JSON.stringify(e));
return undefined;
}
},
getFileSize(path) {
if (!fs) {
throw new Error('not supported');
}
try {
return fs.stat(toResource(path)).size;
} catch (e) {
logger.info(`Error fs.getFileSize`);
logger.info(JSON.stringify(e));
return 0;
}
},
writeFile(path, data, writeByteOrderMark) {
if (!fs) {
throw new Error('not supported');
}
if (writeByteOrderMark) {
data = byteOrderMarkIndicator + data;
}
try {
fs.writeFile(toResource(path), new TextEncoder().encode(data));
} catch (e) {
logger.info(`Error fs.writeFile`);
logger.info(JSON.stringify(e));
}
},
resolvePath(path: string): string {
return path;
},
fileExists(path: string): boolean {
if (!fs) {
const webPath = getWebPath(path);
if (!webPath) {
return false;
}
const request = new XMLHttpRequest();
request.open('HEAD', webPath, /* asynchronous */ false);
request.send();
return request.status === 200;
}
try {
return fs.stat(toResource(path)).type === FileType.File;
} catch (e) {
logger.info(`Error fs.fileExists for ${path}`);
logger.info(JSON.stringify(e));
return false;
}
},
directoryExists(path: string): boolean {
if (!fs) {
return false;
}
try {
return fs.stat(toResource(path)).type === FileType.Directory;
} catch (e) {
logger.info(`Error fs.directoryExists for ${path}`);
logger.info(JSON.stringify(e));
return false;
}
},
createDirectory(path: string): void {
if (!fs) {
throw new Error('not supported');
}
try {
fs.createDirectory(toResource(path));
} catch (e) {
logger.info(`Error fs.createDirectory`);
logger.info(JSON.stringify(e));
}
},
getExecutingFilePath(): string {
return currentDirectory;
},
getCurrentDirectory(): string {
return currentDirectory;
},
getDirectories(path: string): string[] {
return getAccessibleFileSystemEntries(path).directories.slice();
},
readDirectory(path: string, extensions?: readonly string[], excludes?: readonly string[], includes?: readonly string[], depth?: number): string[] {
return matchFiles(path, extensions, excludes, includes, /*useCaseSensitiveFileNames*/ true, currentDirectory, depth, getAccessibleFileSystemEntries, realpath);
},
getModifiedTime(path: string): Date | undefined {
if (!fs) {
throw new Error('not supported');
}
try {
return new Date(fs.stat(toResource(path)).mtime);
} catch (e) {
logger.info(`Error fs.getModifiedTime`);
logger.info(JSON.stringify(e));
return undefined;
}
},
deleteFile(path: string): void {
if (!fs) {
throw new Error('not supported');
}
try {
fs.delete(toResource(path));
} catch (e) {
logger.info(`Error fs.deleteFile`);
logger.info(JSON.stringify(e));
}
},
createHash: generateDjb2Hash,
/** This must be cryptographically secure.
The browser implementation, crypto.subtle.digest, is async so not possible to call from tsserver. */
createSHA256Hash: undefined,
exit(): void {
removeEventListener('message', listener);
},
realpath,
base64decode: input => Buffer.from(input, 'base64').toString('utf8'),
base64encode: input => Buffer.from(input).toString('base64'),
};
/** For module resolution only; symlinks aren't supported yet. */
function realpath(path: string): string {
// skip paths without .. or ./ or /.
if (!path.match(/\.\.|\/\.|\.\//)) {
return path;
}
const uri = toResource(path);
const out = [uri.scheme];
if (uri.authority) { out.push(uri.authority); }
for (const part of uri.path.split('/')) {
switch (part) {
case '':
case '.':
break;
case '..':
//delete if there is something there to delete
out.pop();
break;
default:
out.push(part);
}
}
return '/' + out.join('/');
}
function getAccessibleFileSystemEntries(path: string): { files: readonly string[]; directories: readonly string[] } {
if (!fs) {
throw new Error('not supported');
}
try {
const uri = toResource(path || '.');
const entries = fs.readDirectory(uri);
const files: string[] = [];
const directories: string[] = [];
for (const [entry, type] of entries) {
// This is necessary because on some file system node fails to exclude
// '.' and '..'. See https://github.com/nodejs/node/issues/4002
if (entry === '.' || entry === '..') {
continue;
}
if (type === FileType.File) {
files.push(entry);
}
else if (type === FileType.Directory) {
directories.push(entry);
}
}
files.sort();
directories.sort();
return { files, directories };
} catch (e) {
return { files: [], directories: [] };
}
}
/**
* Copied from toResource in typescriptServiceClient.ts
*/
function toResource(filepath: string) {
if (filepath.startsWith('/lib.') && filepath.endsWith('.d.ts')) {
return URI.from({
scheme: extensionUri.scheme,
authority: extensionUri.authority,
path: extensionUri.path + '/dist/browser/typescript/' + filepath.slice(1)
});
}
const parts = filepath.match(/^\/([^\/]+)\/([^\/]*)(?:\/(.+))?$/);
if (!parts) {
throw new Error('complex regex failed to match ' + filepath);
}
return URI.parse(parts[1] + '://' + (parts[2] === 'ts-nul-authority' ? '' : parts[2]) + (parts[3] ? '/' + parts[3] : ''));
}
}
class WasmCancellationToken implements ts.server.ServerCancellationToken {
shouldCancel: (() => boolean) | undefined;
currentRequestId: number | undefined = undefined;
setRequest(requestId: number) {
this.currentRequestId = requestId;
}
resetRequest(requestId: number) {
if (requestId === this.currentRequestId) {
this.currentRequestId = undefined;
} else {
throw new Error(`Mismatched request id, expected ${this.currentRequestId} but got ${requestId}`);
}
}
isCancellationRequested(): boolean {
return this.currentRequestId !== undefined && !!this.shouldCancel && this.shouldCancel();
}
}
interface StartSessionOptions {
@@ -273,27 +401,47 @@ interface StartSessionOptions {
syntaxOnly: ts.server.SessionOptions['syntaxOnly'];
serverMode: ts.server.SessionOptions['serverMode'];
}
class ServerWorkerSession extends ts.server.Session<{}> {
class WorkerSession extends ts.server.Session<{}> {
wasmCancellationToken: WasmCancellationToken;
listener: (message: any) => void;
constructor(
host: ts.server.ServerHost,
private webHost: HostWithWriteMessage,
options: StartSessionOptions,
public port: MessagePort,
logger: ts.server.Logger,
cancellationToken: ts.server.ServerCancellationToken,
hrtime: ts.server.SessionOptions['hrtime']
) {
const cancellationToken = new WasmCancellationToken();
super({
host,
cancellationToken,
...options,
typingsInstaller: ts.server.nullTypingsInstaller,
byteLength: notImplemented, // Formats the message text in send of Session which is overriden in this class so not needed
typingsInstaller: ts.server.nullTypingsInstaller, // TODO: Someday!
byteLength: () => { throw new Error('Not implemented'); }, // Formats the message text in send of Session which is overriden in this class so not needed
hrtime,
logger,
canUseEvents: true,
});
}
this.wasmCancellationToken = cancellationToken;
this.listener = (message: any) => {
// TEMP fix since Cancellation.retrieveCheck is not correct
function retrieveCheck2(data: any) {
if (!globalThis.crossOriginIsolated || !(data.$cancellationData instanceof SharedArrayBuffer)) {
return () => false;
}
const typedArray = new Int32Array(data.$cancellationData, 0, 1);
return () => {
return Atomics.load(typedArray, 0) === 1;
};
}
const shouldCancel = retrieveCheck2(message.data);
if (shouldCancel) {
this.wasmCancellationToken.shouldCancel = shouldCancel;
}
this.onMessage(message.data);
};
}
public override send(msg: ts.server.protocol.Message) {
if (msg.type === 'event' && !this.canUseEvents) {
if (this.logger.hasLevel(ts.server.LogLevel.verbose)) {
@@ -304,35 +452,34 @@ class ServerWorkerSession extends ts.server.Session<{}> {
if (this.logger.hasLevel(ts.server.LogLevel.verbose)) {
this.logger.info(`${msg.type}:${indent(JSON.stringify(msg))}`);
}
this.webHost.writeMessage(msg);
this.port.postMessage(msg);
}
protected override parseMessage(message: {}): ts.server.protocol.Request {
return message as ts.server.protocol.Request;
}
protected override toStringMessage(message: {}) {
return JSON.stringify(message, undefined, 2);
}
override exit() {
this.logger.info('Exiting...');
this.port.removeEventListener('message', this.listener);
this.projectService.closeLog();
close();
}
listen() {
this.logger.info(`webServer.ts: tsserver starting to listen for messages on 'message'...`);
this.port.onmessage = this.listener;
}
}
// END webServer/webServer.ts
// BEGIN tsserver/webServer.ts
const nullLogger: ts.server.Logger = {
close: noop,
hasLevel: returnFalse,
loggingEnabled: returnFalse,
perftrc: noop,
info: noop,
msg: noop,
startGroup: noop,
endGroup: noop,
getLogFileName: returnUndefined,
};
function parseServerMode(): ts.LanguageServiceMode | string | undefined {
const mode = findArgument('--serverMode');
function parseServerMode(args: string[]): ts.LanguageServiceMode | string | undefined {
const mode = findArgument(args, '--serverMode');
if (!mode) { return undefined; }
switch (mode.toLowerCase()) {
case 'semantic':
return ts.LanguageServiceMode.Semantic;
case 'partialsemantic':
return ts.LanguageServiceMode.PartialSemantic;
case 'syntactic':
@@ -342,75 +489,6 @@ function parseServerMode(): ts.LanguageServiceMode | string | undefined {
}
}
function initializeWebSystem(args: string[]): StartInput {
createWebSystem(args);
const modeOrUnknown = parseServerMode();
let serverMode: ts.LanguageServiceMode | undefined;
let unknownServerMode: string | undefined;
if (typeof modeOrUnknown === 'number') { serverMode = modeOrUnknown; }
else { unknownServerMode = modeOrUnknown; }
const logger = createLogger();
// enable deprecation logging
(ts as any).Debug.loggingHost = {
log(level: unknown, s: string) {
switch (level) {
case (ts as any).LogLevel.Error:
case (ts as any).LogLevel.Warning:
return logger.msg(s, ts.server.Msg.Err);
case (ts as any).LogLevel.Info:
case (ts as any).LogLevel.Verbose:
return logger.msg(s, ts.server.Msg.Info);
}
}
};
return {
args,
logger,
cancellationToken: ts.server.nullCancellationToken,
// Webserver defaults to partial semantic mode
serverMode: serverMode ?? ts.LanguageServiceMode.PartialSemantic,
unknownServerMode,
startSession: startWebSession
};
}
function createLogger() {
const cmdLineVerbosity = getLogLevel(findArgument('--logVerbosity'));
return cmdLineVerbosity !== undefined ? new MainProcessLogger(cmdLineVerbosity, { writeMessage }) : nullLogger;
}
function writeMessage(s: any) {
postMessage(s);
}
function createWebSystem(args: string[]) {
(ts as any).Debug.assert(ts.sys === undefined);
const webHost: WebHost = {
readFile: webPath => {
const request = new XMLHttpRequest();
request.open('GET', webPath, /* asynchronous */ false);
request.send();
return request.status === 200 ? request.responseText : undefined;
},
fileExists: webPath => {
const request = new XMLHttpRequest();
request.open('HEAD', webPath, /* asynchronous */ false);
request.send();
return request.status === 200;
},
writeMessage,
};
// Do this after sys has been set as findArguments is going to work only then
const sys = serverCreateWebSystem(webHost, args, () => findArgument('--executingFilePath') || location + '');
setSys(sys);
const localeStr = findArgument('--locale');
if (localeStr) {
validateLocaleAndSetLanguage(localeStr, sys);
}
}
function hrtime(previous?: number[]) {
const now = self.performance.now() * 1e-3;
let seconds = Math.floor(now);
@@ -427,102 +505,87 @@ function hrtime(previous?: number[]) {
return [seconds, nanoseconds];
}
function startWebSession(options: StartSessionOptions, logger: ts.server.Logger, cancellationToken: ts.server.ServerCancellationToken) {
class WorkerSession extends ServerWorkerSession {
constructor() {
super(
ts.sys as ts.server.ServerHost & { tryEnableSourceMapsForHost?(): void; getEnvironmentVariable(name: string): string },
{ writeMessage },
options,
logger,
cancellationToken,
hrtime);
}
override exit() {
this.logger.info('Exiting...');
this.projectService.closeLog();
close();
}
listen() {
addEventListener('message', (message: any) => {
this.onMessage(message.data);
});
}
}
const session = new WorkerSession();
// Start listening
session.listen();
}
// END tsserver/webServer.ts
// BEGIN tsserver/server.ts
function findArgumentStringArray(argName: string): readonly string[] {
const arg = findArgument(argName);
if (arg === undefined) {
return [];
}
return arg.split(',').filter(name => name !== '');
function hasArgument(args: readonly string[], name: string): boolean {
return args.indexOf(name) >= 0;
}
function findArgument(args: readonly string[], name: string): string | undefined {
const index = args.indexOf(name);
return 0 <= index && index < args.length - 1
? args[index + 1]
: undefined;
}
function findArgumentStringArray(args: readonly string[], name: string): readonly string[] {
const arg = findArgument(args, name);
return arg === undefined ? [] : arg.split(',').filter(name => name !== '');
}
interface StartInput {
args: readonly string[];
logger: ts.server.Logger;
cancellationToken: ts.server.ServerCancellationToken;
serverMode: ts.LanguageServiceMode | undefined;
unknownServerMode?: string;
startSession: (option: StartSessionOptions, logger: ts.server.Logger, cancellationToken: ts.server.ServerCancellationToken) => void;
}
function start({ args, logger, cancellationToken, serverMode, unknownServerMode, startSession: startServer }: StartInput, platform: string) {
const syntaxOnly = hasArgument('--syntaxOnly');
async function initializeSession(args: string[], extensionUri: URI, platform: string, ports: { tsserver: MessagePort; sync: MessagePort; watcher: MessagePort }, logger: ts.server.Logger): Promise<void> {
const modeOrUnknown = parseServerMode(args);
const serverMode = typeof modeOrUnknown === 'number' ? modeOrUnknown : undefined;
const unknownServerMode = typeof modeOrUnknown === 'string' ? modeOrUnknown : undefined;
const syntaxOnly = hasArgument(args, '--syntaxOnly');
logger.info(`Starting TS Server`);
logger.info(`Version: Moved from Typescript 5.0.0-dev`);
logger.info(`Version: 0.0.0`);
logger.info(`Arguments: ${args.join(' ')}`);
logger.info(`Platform: ${platform} NodeVersion: N/A CaseSensitive: ${ts.sys.useCaseSensitiveFileNames}`);
logger.info(`ServerMode: ${serverMode} syntaxOnly: ${syntaxOnly} hasUnknownServerMode: ${unknownServerMode}`);
logger.info(`Platform: ${platform} CaseSensitive: true`);
logger.info(`ServerMode: ${serverMode} syntaxOnly: ${syntaxOnly} unknownServerMode: ${unknownServerMode}`);
const options = {
globalPlugins: findArgumentStringArray(args, '--globalPlugins'),
pluginProbeLocations: findArgumentStringArray(args, '--pluginProbeLocations'),
allowLocalPluginLoads: hasArgument(args, '--allowLocalPluginLoads'),
useSingleInferredProject: hasArgument(args, '--useSingleInferredProject'),
useInferredProjectPerProjectRoot: hasArgument(args, '--useInferredProjectPerProjectRoot'),
suppressDiagnosticEvents: hasArgument(args, '--suppressDiagnosticEvents'),
noGetErrOnBackgroundUpdate: hasArgument(args, '--noGetErrOnBackgroundUpdate'),
syntaxOnly,
serverMode
};
setStackTraceLimit();
let sys: ServerHostWithImport;
if (serverMode === ts.LanguageServiceMode.Semantic) {
const connection = new ClientConnection<Requests>(ports.sync);
await connection.serviceReady();
sys = createServerHost(extensionUri, logger, new ApiClient(connection), args, ports.watcher);
} else {
sys = createServerHost(extensionUri, logger, undefined, args, ports.watcher);
if ((ts as any).Debug.isDebugging) {
(ts as any).Debug.enableDebugInfo();
}
if ((ts as any).sys.tryEnableSourceMapsForHost && /^development$/i.test((ts as any).sys.getEnvironmentVariable('NODE_ENV'))) {
(ts as any).sys.tryEnableSourceMapsForHost();
}
// Overwrites the current console messages to instead write to
// the log. This is so that language service plugins which use
// console.log don't break the message passing between tsserver
// and the client
console.log = (...args) => logger.msg(args.length === 1 ? args[0] : args.join(', '), ts.server.Msg.Info);
console.warn = (...args) => logger.msg(args.length === 1 ? args[0] : args.join(', '), ts.server.Msg.Err);
console.error = (...args) => logger.msg(args.length === 1 ? args[0] : args.join(', '), ts.server.Msg.Err);
startServer(
{
globalPlugins: findArgumentStringArray('--globalPlugins'),
pluginProbeLocations: findArgumentStringArray('--pluginProbeLocations'),
allowLocalPluginLoads: hasArgument('--allowLocalPluginLoads'),
useSingleInferredProject: hasArgument('--useSingleInferredProject'),
useInferredProjectPerProjectRoot: hasArgument('--useInferredProjectPerProjectRoot'),
suppressDiagnosticEvents: hasArgument('--suppressDiagnosticEvents'),
noGetErrOnBackgroundUpdate: hasArgument('--noGetErrOnBackgroundUpdate'),
syntaxOnly,
serverMode
},
logger,
cancellationToken
);
setSys(sys);
session = new WorkerSession(sys, options, ports.tsserver, logger, hrtime);
session.listen();
}
// Get args from first message
const listener = (e: any) => {
removeEventListener('message', listener);
const args = e.data;
start(initializeWebSystem(args), 'web');
let hasInitialized = false;
const listener = async (e: any) => {
if (!hasInitialized) {
hasInitialized = true;
if ('args' in e.data) {
const logger: ts.server.Logger = {
close: () => { },
hasLevel: level => level <= ts.server.LogLevel.verbose,
loggingEnabled: () => true,
perftrc: () => { },
info: s => postMessage({ type: 'log', body: s + '\n' }),
msg: s => postMessage({ type: 'log', body: s + '\n' }),
startGroup: () => { },
endGroup: () => { },
getLogFileName: () => 'tsserver.log',
};
const [sync, tsserver, watcher] = e.ports as MessagePort[];
const extensionUri = URI.from(e.data.extensionUri);
watcher.onmessage = (e: any) => updateWatch(e.data.event, URI.from(e.data.uri), extensionUri);
await initializeSession(e.data.args, extensionUri, 'vscode-web', { sync, tsserver, watcher }, logger);
} else {
console.error('unexpected message in place of initial message: ' + JSON.stringify(e.data));
}
return;
}
console.error(`unexpected message on main channel: ${JSON.stringify(e)}`);
};
addEventListener('message', listener);
// END tsserver/server.ts

View File

@@ -206,6 +206,27 @@
"@microsoft/applicationinsights-web-basic" "^2.8.9"
applicationinsights "2.3.6"
"@vscode/sync-api-client@^0.7.2":
version "0.7.2"
resolved "https://registry.yarnpkg.com/@vscode/sync-api-client/-/sync-api-client-0.7.2.tgz#0644bff66a5eff636bcd8eb483d34796b9f90d2d"
integrity sha512-HQHz57RVKmR8sTEen1Y/T3r6mzDX7IaUJz/O2RJkn0Qu9ThvCsakLP0N+1iiwPnPfUfmNSwQXbSw8bEQFPcpYQ==
dependencies:
"@vscode/sync-api-common" "0.7.2"
vscode-uri "3.0.3"
"@vscode/sync-api-common@0.7.2", "@vscode/sync-api-common@^0.7.2":
version "0.7.2"
resolved "https://registry.yarnpkg.com/@vscode/sync-api-common/-/sync-api-common-0.7.2.tgz#705060ee6a0108c24e145e687613becdb4292b33"
integrity sha512-ne1XEeDIYA3mp4oo1QoF1fqFedd0Vf4ybMmLb9HixbTyXy/qwMNL2p6OjXjOsmx6w2q9eqzGA5W/OPRSJxTTIQ==
"@vscode/sync-api-service@^0.7.3":
version "0.7.3"
resolved "https://registry.yarnpkg.com/@vscode/sync-api-service/-/sync-api-service-0.7.3.tgz#6cb7bd23c4a7378e4b92ca3638501a9be1937152"
integrity sha512-m2AmmfG4uzfjLMgWRHQ3xnBkdwCiUTO68vdw1XuzMsOb39Jwm9xr5bVVxwOFR9lPC0FfO1H6FUxBhZQvg7itPA==
dependencies:
"@vscode/sync-api-common" "0.7.2"
vscode-uri "3.0.3"
applicationinsights@2.3.6:
version "2.3.6"
resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-2.3.6.tgz#91277ce44e5f6d2f85336922c05d90f8699c2e70"
@@ -451,7 +472,7 @@ vscode-tas-client@^0.1.63:
dependencies:
tas-client "0.1.58"
vscode-uri@^3.0.3:
vscode-uri@3.0.3, vscode-uri@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.3.tgz#a95c1ce2e6f41b7549f86279d19f47951e4f4d84"
integrity sha512-EcswR2S8bpR7fD0YPeS7r2xXExrScVMxg4MedACaWHEtx9ftCF/qHG1xGkolzTPcEmjTavCQgbVzHUIdTMzFGA==