Don't require restart when switching to TS native

For #266087

Dynamically registers / unregisters the language features
This commit is contained in:
Matt Bierner
2025-09-10 13:13:55 -07:00
parent 07fdb50d0b
commit 5ba9efdf40
6 changed files with 74 additions and 58 deletions

View File

@@ -27,7 +27,7 @@ export class DisableTsgoCommand implements Command {
* @param enable Whether to enable or disable TypeScript Go * @param enable Whether to enable or disable TypeScript Go
*/ */
async function updateTsgoSetting(enable: boolean): Promise<void> { async function updateTsgoSetting(enable: boolean): Promise<void> {
const tsgoExtension = vscode.extensions.getExtension('typescript.typescript-lsp'); const tsgoExtension = vscode.extensions.getExtension('typescriptteam.native-preview');
// Error if the TypeScript Go extension is not installed with a button to open the GitHub repo // Error if the TypeScript Go extension is not installed with a button to open the GitHub repo
if (!tsgoExtension) { if (!tsgoExtension) {
const selection = await vscode.window.showErrorMessage( const selection = await vscode.window.showErrorMessage(

View File

@@ -25,27 +25,12 @@ import { onCaseInsensitiveFileSystem } from './utils/fs.electron';
import { Lazy } from './utils/lazy'; import { Lazy } from './utils/lazy';
import { getPackageInfo } from './utils/packageInfo'; import { getPackageInfo } from './utils/packageInfo';
import * as temp from './utils/temp.electron'; import * as temp from './utils/temp.electron';
import { conditionalRegistration, requireGlobalConfiguration } from './languageFeatures/util/dependentRegistration';
import { DisposableStore } from './utils/dispose';
export function activate( export function activate(
context: vscode.ExtensionContext context: vscode.ExtensionContext
): Api { ): Api {
const commandManager = new CommandManager();
context.subscriptions.push(commandManager);
// Disable extension if using the experimental TypeScript Go extension
const config = vscode.workspace.getConfiguration('typescript');
const useTsgo = config.get<boolean>('experimental.useTsgo', false);
if (useTsgo) {
commandManager.register(new DisableTsgoCommand());
// Return a no-op API when disabled
return {
getAPI() {
return undefined;
}
};
}
const pluginManager = new PluginManager(); const pluginManager = new PluginManager();
context.subscriptions.push(pluginManager); context.subscriptions.push(pluginManager);
@@ -55,9 +40,6 @@ export function activate(
const logDirectoryProvider = new NodeLogDirectoryProvider(context); const logDirectoryProvider = new NodeLogDirectoryProvider(context);
const versionProvider = new DiskTypeScriptVersionProvider(); const versionProvider = new DiskTypeScriptVersionProvider();
const activeJsTsEditorTracker = new ActiveJsTsEditorTracker();
context.subscriptions.push(activeJsTsEditorTracker);
let experimentTelemetryReporter: IExperimentationTelemetryReporter | undefined; let experimentTelemetryReporter: IExperimentationTelemetryReporter | undefined;
const packageInfo = getPackageInfo(context); const packageInfo = getPackageInfo(context);
if (packageInfo) { if (packageInfo) {
@@ -71,34 +53,55 @@ export function activate(
new ExperimentationService(experimentTelemetryReporter, id, version, context.globalState); new ExperimentationService(experimentTelemetryReporter, id, version, context.globalState);
} }
const logger = new Logger(); context.subscriptions.push(conditionalRegistration([
requireGlobalConfiguration('typescript', 'experimental.useTsgo'),
], () => {
// TSGO. Only register a small set of features that don't use TS Server
const disposables = new DisposableStore();
const lazyClientHost = createLazyClientHost(context, onCaseInsensitiveFileSystem(), { const commandManager = disposables.add(new CommandManager());
pluginManager, commandManager.register(new DisableTsgoCommand());
commandManager,
logDirectoryProvider,
cancellerFactory: nodeRequestCancellerFactory,
versionProvider,
processFactory: new ElectronServiceProcessFactory(),
activeJsTsEditorTracker,
serviceConfigurationProvider: new ElectronServiceConfigurationProvider(),
experimentTelemetryReporter,
logger,
}, item => {
onCompletionAccepted.fire(item);
});
registerBaseCommands(commandManager, lazyClientHost, pluginManager, activeJsTsEditorTracker); return disposables;
}, () => {
// Normal registration path
const disposables = new DisposableStore();
import('./task/taskProvider').then(module => { const commandManager = disposables.add(new CommandManager());
context.subscriptions.push(module.register(new Lazy(() => lazyClientHost.value.serviceClient))); const activeJsTsEditorTracker = disposables.add(new ActiveJsTsEditorTracker());
});
import('./languageFeatures/tsconfig').then(module => { const lazyClientHost = createLazyClientHost(context, onCaseInsensitiveFileSystem(), {
context.subscriptions.push(module.register()); pluginManager,
}); commandManager,
logDirectoryProvider,
cancellerFactory: nodeRequestCancellerFactory,
versionProvider,
processFactory: new ElectronServiceProcessFactory(),
activeJsTsEditorTracker,
serviceConfigurationProvider: new ElectronServiceConfigurationProvider(),
experimentTelemetryReporter,
logger: new Logger(),
}, item => {
onCompletionAccepted.fire(item);
}).map(clientHost => {
return disposables.add(clientHost);
});
context.subscriptions.push(lazilyActivateClient(lazyClientHost, pluginManager, activeJsTsEditorTracker)); // Register features
registerBaseCommands(commandManager, lazyClientHost, pluginManager, activeJsTsEditorTracker);
import('./task/taskProvider').then(module => {
disposables.add(module.register(new Lazy(() => lazyClientHost.value.serviceClient)));
});
import('./languageFeatures/tsconfig').then(module => {
disposables.add(module.register());
});
disposables.add(lazilyActivateClient(lazyClientHost, pluginManager, activeJsTsEditorTracker));
return disposables;
},));
return getExtensionApi(onCompletionAccepted.event, pluginManager); return getExtensionApi(onCompletionAccepted.event, pluginManager);
} }

View File

@@ -34,11 +34,15 @@ export class Condition extends Disposable {
} }
class ConditionalRegistration { class ConditionalRegistration {
private registration: vscode.Disposable | undefined = undefined; private state?: {
readonly enabled: boolean;
readonly registration: vscode.Disposable | undefined;
};
public constructor( public constructor(
private readonly conditions: readonly Condition[], private readonly conditions: readonly Condition[],
private readonly doRegister: () => vscode.Disposable private readonly doRegister: () => vscode.Disposable,
private readonly elseDoRegister?: () => vscode.Disposable
) { ) {
for (const condition of conditions) { for (const condition of conditions) {
condition.onDidChange(() => this.update()); condition.onDidChange(() => this.update());
@@ -47,17 +51,22 @@ class ConditionalRegistration {
} }
public dispose() { public dispose() {
this.registration?.dispose(); this.state?.registration?.dispose();
this.registration = undefined; this.state = undefined;
} }
private update() { private update() {
const enabled = this.conditions.every(condition => condition.value); const enabled = this.conditions.every(condition => condition.value);
if (enabled) { if (enabled) {
this.registration ??= this.doRegister(); if (!this.state?.enabled) {
this.state?.registration?.dispose();
this.state = { enabled: true, registration: this.doRegister() };
}
} else { } else {
this.registration?.dispose(); if (this.state?.enabled || !this.state) {
this.registration = undefined; this.state?.registration?.dispose();
this.state = { enabled: false, registration: this.elseDoRegister?.() };
}
} }
} }
} }
@@ -65,8 +74,9 @@ class ConditionalRegistration {
export function conditionalRegistration( export function conditionalRegistration(
conditions: readonly Condition[], conditions: readonly Condition[],
doRegister: () => vscode.Disposable, doRegister: () => vscode.Disposable,
elseDoRegister?: () => vscode.Disposable
): vscode.Disposable { ): vscode.Disposable {
return new ConditionalRegistration(conditions, doRegister); return new ConditionalRegistration(conditions, doRegister, elseDoRegister);
} }
export function requireMinVersion( export function requireMinVersion(

View File

@@ -38,16 +38,12 @@ export function createLazyClientHost(
onCompletionAccepted: (item: vscode.CompletionItem) => void, onCompletionAccepted: (item: vscode.CompletionItem) => void,
): Lazy<TypeScriptServiceClientHost> { ): Lazy<TypeScriptServiceClientHost> {
return new Lazy(() => { return new Lazy(() => {
const clientHost = new TypeScriptServiceClientHost( return new TypeScriptServiceClientHost(
standardLanguageDescriptions, standardLanguageDescriptions,
context, context,
onCaseInsensitiveFileSystem, onCaseInsensitiveFileSystem,
services, services,
onCompletionAccepted); onCompletionAccepted);
context.subscriptions.push(clientHost);
return clientHost;
}); });
} }

View File

@@ -108,7 +108,6 @@ interface WatchEvent {
export default class TypeScriptServiceClient extends Disposable implements ITypeScriptServiceClient { export default class TypeScriptServiceClient extends Disposable implements ITypeScriptServiceClient {
private readonly _onReady?: { promise: Promise<void>; resolve: () => void; reject: () => void }; private readonly _onReady?: { promise: Promise<void>; resolve: () => void; reject: () => void };
private _configuration: TypeScriptServiceConfiguration; private _configuration: TypeScriptServiceConfiguration;
private readonly pluginPathsProvider: TypeScriptPluginPathsProvider; private readonly pluginPathsProvider: TypeScriptPluginPathsProvider;
@@ -632,6 +631,10 @@ export default class TypeScriptServiceClient extends Disposable implements IType
this.serverState = ServerState.None; this.serverState = ServerState.None;
if (this.isDisposed) {
return;
}
if (restart) { if (restart) {
const diff = Date.now() - this.lastStart; const diff = Date.now() - this.lastStart;
this.numberRestarts++; this.numberRestarts++;

View File

@@ -44,4 +44,8 @@ export class Lazy<T> {
* Get the wrapped value without forcing evaluation. * Get the wrapped value without forcing evaluation.
*/ */
get rawValue(): T | undefined { return this._value; } get rawValue(): T | undefined { return this._value; }
map<R>(fn: (value: T) => R): Lazy<R> {
return new Lazy(() => fn(this.value));
}
} }