Add helper class to work with js/ts unified configs

For #292934
This commit is contained in:
Matt Bierner
2026-02-22 23:33:29 -08:00
parent 78fba4c219
commit 000d29cc4e
5 changed files with 174 additions and 63 deletions

View File

@@ -11,7 +11,7 @@ import type * as Proto from '../../tsServer/protocol/protocol';
import * as PConst from '../../tsServer/protocol/protocol.const';
import * as typeConverters from '../../typeConverters';
import { ClientCapability, ITypeScriptServiceClient } from '../../typescriptService';
import { readUnifiedConfig, unifiedConfigSection } from '../../utils/configuration';
import { ResourceUnifiedConfigValue } from '../../utils/configuration';
import { conditionalRegistration, requireHasModifiedUnifiedConfig, requireSomeCapability } from '../util/dependentRegistration';
import { ReferencesCodeLens, TypeScriptBaseCodeLensProvider, getSymbolRange } from './baseCodeLensProvider';
import { ExecutionTarget } from '../../tsServer/server';
@@ -23,31 +23,29 @@ const Config = Object.freeze({
});
export default class TypeScriptImplementationsCodeLensProvider extends TypeScriptBaseCodeLensProvider {
private readonly _enabled: ResourceUnifiedConfigValue<boolean>;
private readonly _showOnInterfaceMethods: ResourceUnifiedConfigValue<boolean>;
private readonly _showOnAllClassMethods: ResourceUnifiedConfigValue<boolean>;
public constructor(
client: ITypeScriptServiceClient,
protected _cachedResponse: CachedResponse<Proto.NavTreeResponse>,
private readonly language: LanguageDescription
) {
super(client, _cachedResponse);
this._register(
vscode.workspace.onDidChangeConfiguration(evt => {
if (
evt.affectsConfiguration(`${unifiedConfigSection}.${Config.enabled}`) ||
evt.affectsConfiguration(`${language.id}.${Config.enabled}`) ||
evt.affectsConfiguration(`${unifiedConfigSection}.${Config.showOnInterfaceMethods}`) ||
evt.affectsConfiguration(`${language.id}.${Config.showOnInterfaceMethods}`) ||
evt.affectsConfiguration(`${unifiedConfigSection}.${Config.showOnAllClassMethods}`) ||
evt.affectsConfiguration(`${language.id}.${Config.showOnAllClassMethods}`)
) {
this.changeEmitter.fire();
}
})
);
this._enabled = this._register(new ResourceUnifiedConfigValue<boolean>(Config.enabled, false));
this._register(this._enabled.onDidChange(() => this.changeEmitter.fire()));
this._showOnInterfaceMethods = this._register(new ResourceUnifiedConfigValue<boolean>(Config.showOnInterfaceMethods, false));
this._register(this._showOnInterfaceMethods.onDidChange(() => this.changeEmitter.fire()));
this._showOnAllClassMethods = this._register(new ResourceUnifiedConfigValue<boolean>(Config.showOnAllClassMethods, false));
this._register(this._showOnAllClassMethods.onDidChange(() => this.changeEmitter.fire()));
}
override async provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): Promise<ReferencesCodeLens[]> {
const enabled = readUnifiedConfig<boolean>(Config.enabled, false, { scope: document, fallbackSection: this.language.id });
const enabled = this._enabled.getValue(document);
if (!enabled) {
return [];
}
@@ -131,7 +129,7 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip
if (
item.kind === PConst.Kind.method &&
parent?.kind === PConst.Kind.interface &&
readUnifiedConfig<boolean>('implementationsCodeLens.showOnInterfaceMethods', false, { scope: document, fallbackSection: this.language.id })
this._showOnInterfaceMethods.getValue(document)
) {
return getSymbolRange(document, item);
}
@@ -141,7 +139,7 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip
if (
item.kind === PConst.Kind.method &&
parent?.kind === PConst.Kind.class &&
readUnifiedConfig<boolean>('implementationsCodeLens.showOnAllClassMethods', false, { scope: document, fallbackSection: this.language.id })
this._showOnAllClassMethods.getValue(document)
) {
// But not private ones as these can never be overridden
if (/\bprivate\b/.test(item.kindModifiers ?? '')) {
@@ -165,6 +163,6 @@ export function register(
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
return vscode.languages.registerCodeLensProvider(selector.semantic,
new TypeScriptImplementationsCodeLensProvider(client, cachedResponse, language));
new TypeScriptImplementationsCodeLensProvider(client, cachedResponse));
});
}

View File

@@ -12,7 +12,7 @@ import * as PConst from '../../tsServer/protocol/protocol.const';
import { ExecutionTarget } from '../../tsServer/server';
import * as typeConverters from '../../typeConverters';
import { ClientCapability, ITypeScriptServiceClient } from '../../typescriptService';
import { readUnifiedConfig, unifiedConfigSection } from '../../utils/configuration';
import { ResourceUnifiedConfigValue } from '../../utils/configuration';
import { conditionalRegistration, requireHasModifiedUnifiedConfig, requireSomeCapability } from '../util/dependentRegistration';
import { ReferencesCodeLens, TypeScriptBaseCodeLensProvider, getSymbolRange } from './baseCodeLensProvider';
@@ -22,28 +22,25 @@ const Config = Object.freeze({
});
export class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLensProvider {
private readonly _enabled: ResourceUnifiedConfigValue<boolean>;
private readonly _showOnAllFunctions: ResourceUnifiedConfigValue<boolean>;
public constructor(
client: ITypeScriptServiceClient,
protected _cachedResponse: CachedResponse<Proto.NavTreeResponse>,
private readonly language: LanguageDescription
) {
super(client, _cachedResponse);
this._register(
vscode.workspace.onDidChangeConfiguration(evt => {
if (
evt.affectsConfiguration(`${unifiedConfigSection}.${Config.enabled}`) ||
evt.affectsConfiguration(`${language.id}.${Config.enabled}`) ||
evt.affectsConfiguration(`${unifiedConfigSection}.${Config.showOnAllFunctions}`) ||
evt.affectsConfiguration(`${language.id}.${Config.showOnAllFunctions}`)
) {
this.changeEmitter.fire();
}
})
);
this._enabled = this._register(new ResourceUnifiedConfigValue<boolean>(Config.enabled, false));
this._register(this._enabled.onDidChange(() => this.changeEmitter.fire()));
this._showOnAllFunctions = this._register(new ResourceUnifiedConfigValue<boolean>(Config.showOnAllFunctions, false));
this._register(this._showOnAllFunctions.onDidChange(() => this.changeEmitter.fire()));
}
override async provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): Promise<ReferencesCodeLens[]> {
const enabled = readUnifiedConfig<boolean>(Config.enabled, false, { scope: document, fallbackSection: this.language.id });
const enabled = this._enabled.getValue(document);
if (!enabled) {
return [];
}
@@ -95,7 +92,7 @@ export class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLens
switch (item.kind) {
case PConst.Kind.function: {
const showOnAllFunctions = readUnifiedConfig<boolean>(Config.showOnAllFunctions, false, { scope: document, fallbackSection: this.language.id });
const showOnAllFunctions = this._showOnAllFunctions.getValue(document);
if (showOnAllFunctions && item.nameSpan) {
return getSymbolRange(document, item);
}
@@ -160,6 +157,6 @@ export function register(
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
return vscode.languages.registerCodeLensProvider(selector.semantic,
new TypeScriptReferencesCodeLensProvider(client, cachedResponse, language));
new TypeScriptReferencesCodeLensProvider(client, cachedResponse));
});
}

View File

@@ -18,7 +18,7 @@ import { ClientCapability } from './typescriptService';
import TypeScriptServiceClient from './typescriptServiceClient';
import TypingsStatus from './ui/typingsStatus';
import { Disposable } from './utils/dispose';
import { readUnifiedConfig } from './utils/configuration';
import { UnifiedConfigValue } from './utils/configuration';
import { isWeb, isWebAndHasSharedArrayBuffers, supportsReadableByteStreams } from './utils/platform';
@@ -34,8 +34,16 @@ export default class LanguageProvider extends Disposable {
private readonly onCompletionAccepted: (item: vscode.CompletionItem) => void,
) {
super();
vscode.workspace.onDidChangeConfiguration(this.configurationChanged, this, this._disposables);
this.configurationChanged();
const scope: vscode.ConfigurationScope = { languageId: this.description.languageIds[0] };
const validateConfig = this._register(new UnifiedConfigValue<boolean>('validate.enabled', true, { scope, fallbackSection: this.id, fallbackSubSectionNameOverride: 'validate.enable' }));
this.updateValidate(validateConfig.getValue());
this._register(validateConfig.onDidChange(value => this.updateValidate(value)));
const suggestionsConfig = this._register(new UnifiedConfigValue<boolean>('suggestionActions.enabled', true, { scope, fallbackSection: this.id }));
this.updateSuggestionDiagnostics(suggestionsConfig.getValue());
this._register(suggestionsConfig.onDidChange(value => this.updateSuggestionDiagnostics(value)));
client.onReady(() => this.registerProviders());
}
@@ -91,12 +99,6 @@ export default class LanguageProvider extends Disposable {
]);
}
private configurationChanged(): void {
const scope: vscode.ConfigurationScope = { languageId: this.description.languageIds[0] };
this.updateValidate(readUnifiedConfig<boolean>('validate.enabled', true, { scope, fallbackSection: this.id, fallbackSubSectionNameOverride: 'validate.enable' }));
this.updateSuggestionDiagnostics(readUnifiedConfig<boolean>('suggestionActions.enabled', true, { scope, fallbackSection: this.id }));
}
public handlesUri(resource: vscode.Uri): boolean {
const ext = extname(resource.path).slice(1).toLowerCase();
return this.description.standardFileExtensions.includes(ext) || this.handlesConfigFile(resource);

View File

@@ -10,7 +10,7 @@ import * as typeConverters from '../typeConverters';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import { inMemoryResourcePrefix } from '../typescriptServiceClient';
import { coalesce } from '../utils/arrays';
import { readUnifiedConfig, unifiedConfigSection } from '../utils/configuration';
import { ResourceUnifiedConfigValue } from '../utils/configuration';
import { Delayer, setImmediate } from '../utils/async';
import { nulToken } from '../utils/cancellation';
import { Disposable } from '../utils/dispose';
@@ -471,6 +471,8 @@ export default class BufferSyncSupport extends Disposable {
private listening: boolean = false;
private readonly synchronizer: BufferSynchronizer;
private readonly _validate: ResourceUnifiedConfigValue<boolean>;
private readonly _tabResources: TabResourceTracker;
constructor(
@@ -482,6 +484,8 @@ export default class BufferSyncSupport extends Disposable {
this.client = client;
this.modeIds = new Set<string>(modeIds);
this._validate = this._register(new ResourceUnifiedConfigValue<boolean>('validate.enabled', true, { fallbackSubSectionNameOverride: 'validate.enable' }));
this.diagnosticDelayer = new Delayer<any>(300);
const pathNormalizer = (path: vscode.Uri) => this.client.toTsFilePath(path);
@@ -511,14 +515,7 @@ export default class BufferSyncSupport extends Disposable {
}
}));
this._register(vscode.workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration(`${unifiedConfigSection}.validate.enabled`)
|| e.affectsConfiguration('typescript.validate.enable')
|| e.affectsConfiguration('javascript.validate.enable')
) {
this.requestAllDiagnostics();
}
}));
this._register(this._validate.onDidChange(() => this.requestAllDiagnostics()));
}
private readonly _onDelete = this._register(new vscode.EventEmitter<vscode.Uri>());
@@ -769,9 +766,6 @@ export default class BufferSyncSupport extends Disposable {
return false;
}
const fallbackSection = (buffer.languageId === languageModeIds.javascript || buffer.languageId === languageModeIds.javascriptreact)
? 'javascript'
: 'typescript';
return readUnifiedConfig<boolean>('validate.enabled', true, { scope: buffer.document, fallbackSection, fallbackSubSectionNameOverride: 'validate.enable' });
return this._validate.getValue(buffer.document);
}
}

View File

@@ -4,16 +4,17 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { Disposable } from './dispose';
export type UnifiedConfigurationScope = vscode.ConfigurationScope | null | undefined;
export const unifiedConfigSection = 'js/ts';
export type ReadUnifiedConfigOptions = {
readonly scope?: UnifiedConfigurationScope;
export interface ReadUnifiedConfigOptions<Scope = UnifiedConfigurationScope> {
readonly scope?: Scope;
readonly fallbackSection: string;
readonly fallbackSubSectionNameOverride?: string;
};
}
/**
* Gets a configuration value, checking the unified `js/ts` setting first,
@@ -75,3 +76,122 @@ export function hasModifiedUnifiedConfig(
const languageConfig = vscode.workspace.getConfiguration(options.fallbackSection, options.scope);
return hasModifiedValue(languageConfig.inspect(subSectionName));
}
/**
* A cached, observable unified configuration value.
*/
export class UnifiedConfigValue<T> extends Disposable {
private _value: T;
private readonly _onDidChange = this._register(new vscode.EventEmitter<T>());
public get onDidChange() { return this._onDidChange.event; }
constructor(
private readonly subSectionName: string,
private readonly defaultValue: T,
private readonly options: ReadUnifiedConfigOptions<{ languageId: string }>,
) {
super();
this._value = this.read();
this._register(vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(`${unifiedConfigSection}.${subSectionName}`, options.scope ?? undefined) ||
e.affectsConfiguration(`${options.fallbackSection}.${options.fallbackSubSectionNameOverride ?? subSectionName}`, options.scope ?? undefined)
) {
const newValue = this.read();
if (newValue !== this._value) {
this._value = newValue;
this._onDidChange.fire(newValue);
}
}
}));
}
private read(): T {
return readUnifiedConfig<T>(this.subSectionName, this.defaultValue, this.options);
}
public getValue(): T {
return this._value;
}
}
export interface ResourceUnifiedConfigScope {
readonly uri: vscode.Uri;
readonly languageId: string;
}
/**
* A cached, observable unified configuration value that varies per workspace folder.
*
* Values are keyed by the workspace folder the resource belongs to, with a separate
* entry for resources outside any workspace folder.
*/
export class ResourceUnifiedConfigValue<T> extends Disposable {
private readonly _cache = new Map</* workspace folder */ string, T>();
private readonly _onDidChange = this._register(new vscode.EventEmitter<void>());
public readonly onDidChange = this._onDidChange.event;
constructor(
private readonly subSectionName: string,
private readonly defaultValue: T,
private readonly options?: {
readonly fallbackSubSectionNameOverride?: string;
},
) {
super();
const fallbackName = options?.fallbackSubSectionNameOverride ?? subSectionName;
this._register(vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(`${unifiedConfigSection}.${subSectionName}`) ||
e.affectsConfiguration(`javascript.${fallbackName}`) ||
e.affectsConfiguration(`typescript.${fallbackName}`)
) {
this._cache.clear();
this._onDidChange.fire();
}
}));
this._register(vscode.workspace.onDidChangeWorkspaceFolders(() => {
this._cache.clear();
this._onDidChange.fire();
}));
}
public getValue(scope: ResourceUnifiedConfigScope): T {
const key = this.keyFor(scope);
const cached = this._cache.get(key);
if (cached !== undefined) {
return cached;
}
const fallbackSection = this.fallbackSectionFor(scope.languageId);
const value = readUnifiedConfig<T>(this.subSectionName, this.defaultValue, {
scope: { uri: scope.uri, languageId: scope.languageId },
fallbackSection,
fallbackSubSectionNameOverride: this.options?.fallbackSubSectionNameOverride,
});
this._cache.set(key, value);
return value;
}
private fallbackSectionFor(languageId: string): string {
switch (languageId) {
case 'javascript':
case 'javascriptreact':
return 'javascript';
default:
return 'typescript';
}
}
private keyFor(scope: ResourceUnifiedConfigScope): string {
const folder = vscode.workspace.getWorkspaceFolder(scope.uri);
return folder ? folder.uri.toString() : '';
}
}