Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework custom config requests #12727

Merged
merged 4 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 50 additions & 91 deletions Extension/src/LanguageServer/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,15 +246,6 @@ interface CompileCommandsPaths extends WorkspaceFolderParams {
paths: string[];
}

interface QueryTranslationUnitSourceParams extends WorkspaceFolderParams {
uri: string;
ignoreExisting: boolean;
}

interface QueryTranslationUnitSourceResult {
candidates: string[];
}

interface GetDiagnosticsResult {
diagnostics: string;
}
Expand Down Expand Up @@ -554,7 +545,6 @@ export interface ChatContextResult {
const PreInitializationRequest: RequestType<void, string, void> = new RequestType<void, string, void>('cpptools/preinitialize');
const InitializationRequest: RequestType<CppInitializationParams, void, void> = new RequestType<CppInitializationParams, void, void>('cpptools/initialize');
const QueryCompilerDefaultsRequest: RequestType<QueryDefaultCompilerParams, configs.CompilerDefaults, void> = new RequestType<QueryDefaultCompilerParams, configs.CompilerDefaults, void>('cpptools/queryCompilerDefaults');
const QueryTranslationUnitSourceRequest: RequestType<QueryTranslationUnitSourceParams, QueryTranslationUnitSourceResult, void> = new RequestType<QueryTranslationUnitSourceParams, QueryTranslationUnitSourceResult, void>('cpptools/queryTranslationUnitSource');
const SwitchHeaderSourceRequest: RequestType<SwitchHeaderSourceParams, string, void> = new RequestType<SwitchHeaderSourceParams, string, void>('cpptools/didSwitchHeaderSource');
const GetDiagnosticsRequest: RequestType<void, GetDiagnosticsResult, void> = new RequestType<void, GetDiagnosticsResult, void>('cpptools/getDiagnostics');
export const GetDocumentSymbolRequest: RequestType<GetDocumentSymbolRequestParams, GetDocumentSymbolResult, void> = new RequestType<GetDocumentSymbolRequestParams, GetDocumentSymbolResult, void>('cpptools/getDocumentSymbols');
Expand Down Expand Up @@ -744,7 +734,7 @@ export interface Client {
onRegisterCustomConfigurationProvider(provider: CustomConfigurationProvider1): Thenable<void>;
updateCustomConfigurations(requestingProvider?: CustomConfigurationProvider1): Thenable<void>;
updateCustomBrowseConfiguration(requestingProvider?: CustomConfigurationProvider1): Thenable<void>;
provideCustomConfiguration(docUri: vscode.Uri, requestFile?: string, replaceExisting?: boolean): Promise<void>;
provideCustomConfiguration(docUri: vscode.Uri): Promise<void>;
logDiagnostics(): Promise<void>;
rescanFolder(): Promise<void>;
toggleReferenceResultsView(): void;
Expand Down Expand Up @@ -1871,9 +1861,6 @@ export class DefaultClient implements Client {
}

await this.clearCustomConfigurations();
await Promise.all([
...[...this.trackedDocuments].map(([_uri, document]) => this.provideCustomConfiguration(document.uri, undefined, true))
]);
}

public async updateCustomBrowseConfiguration(requestingProvider?: CustomConfigurationProvider1): Promise<void> {
Expand Down Expand Up @@ -2023,68 +2010,42 @@ export class DefaultClient implements Client {
return this.languageClient.sendNotification(RescanFolderNotification);
}

public async provideCustomConfiguration(docUri: vscode.Uri, requestFile?: string, replaceExisting?: boolean): Promise<void> {
public async provideCustomConfiguration(docUri: vscode.Uri): Promise<void> {
const onFinished: () => void = () => {
if (requestFile) {
void this.languageClient.sendNotification(FinishedRequestCustomConfig, { uri: requestFile });
}
void this.languageClient.sendNotification(FinishedRequestCustomConfig, { uri: docUri.toString() });
};
const providerId: string | undefined = this.configurationProvider;
if (!providerId) {
onFinished();
return;
}
const provider: CustomConfigurationProvider1 | undefined = getCustomConfigProviders().get(providerId);
if (!provider || !provider.isReady) {
onFinished();
return;
}
try {
DefaultClient.isStarted.reset();
const resultCode = await this.provideCustomConfigurationAsync(docUri, requestFile, replaceExisting, provider);
const providerId: string | undefined = this.configurationProvider;
if (!providerId) {
return;
}
const provider: CustomConfigurationProvider1 | undefined = getCustomConfigProviders().get(providerId);
if (!provider || !provider.isReady) {
return;
}
const resultCode = await this.provideCustomConfigurationAsync(docUri, provider);
telemetry.logLanguageServerEvent('provideCustomConfiguration', { providerId, resultCode });
} finally {
onFinished();
DefaultClient.isStarted.resolve();
}
}

private async provideCustomConfigurationAsync(docUri: vscode.Uri, requestFile: string | undefined, replaceExisting: boolean | undefined, provider: CustomConfigurationProvider1): Promise<string> {
private async provideCustomConfigurationAsync(docUri: vscode.Uri, provider: CustomConfigurationProvider1): Promise<string> {
const tokenSource: vscode.CancellationTokenSource = new vscode.CancellationTokenSource();

const params: QueryTranslationUnitSourceParams = {
uri: docUri.toString(),
ignoreExisting: !!replaceExisting,
workspaceFolderUri: this.RootUri?.toString()
};

const response: QueryTranslationUnitSourceResult = await this.languageClient.sendRequest(QueryTranslationUnitSourceRequest, params);
if (!response.candidates || response.candidates.length === 0) {
// If we didn't receive any candidates, no configuration is needed.
return "noCandidates";
}

// Need to loop through candidates, to see if we can get a custom configuration from any of them.
// Wrap all lookups in a single task, so we can apply a timeout to the entire duration.
const provideConfigurationAsync: () => Thenable<SourceFileConfigurationItem[] | undefined> = async () => {
const uris: vscode.Uri[] = [];
for (let i: number = 0; i < response.candidates.length; ++i) {
const candidate: string = response.candidates[i];
const tuUri: vscode.Uri = vscode.Uri.parse(candidate);
try {
if (await provider.canProvideConfiguration(tuUri, tokenSource.token)) {
uris.push(tuUri);
}
} catch (err) {
console.warn("Caught exception from canProvideConfiguration");
try {
if (!await provider.canProvideConfiguration(docUri, tokenSource.token)) {
return [];
}
}
if (!uris.length) {
return [];
} catch (err) {
console.warn("Caught exception from canProvideConfiguration");
}
let configs: util.Mutable<SourceFileConfigurationItem>[] = [];
try {
configs = await provider.provideConfigurations(uris, tokenSource.token);
configs = await provider.provideConfigurations([docUri], tokenSource.token);
} catch (err) {
console.warn("Caught exception from provideConfigurations");
}
Expand Down Expand Up @@ -2132,39 +2093,42 @@ export class DefaultClient implements Client {
try {
const configs: SourceFileConfigurationItem[] | undefined = await this.callTaskWithTimeout(provideConfigurationAsync, configProviderTimeout, tokenSource);
if (configs && configs.length > 0) {
this.sendCustomConfigurations(configs, provider.version, requestFile !== undefined);
this.sendCustomConfigurations(configs, provider.version);
} else {
result = "noConfigurations";
}
} catch (err) {
result = "timeout";
if (!requestFile) {
const settings: CppSettings = new CppSettings(this.RootUri);
if (settings.isConfigurationWarningsEnabled && !this.isExternalHeader(docUri) && !vscode.debug.activeDebugSession) {
const dismiss: string = localize("dismiss.button", "Dismiss");
const disable: string = localize("disable.warnings.button", "Disable Warnings");
const configName: string | undefined = this.configuration.CurrentConfiguration?.name;
if (!configName) {
return "noConfigName";
}
let message: string = localize("unable.to.provide.configuration",
"{0} is unable to provide IntelliSense configuration information for '{1}'. Settings from the '{2}' configuration will be used instead.",
provider.name, docUri.fsPath, configName);
if (err) {
message += ` (${err})`;
}
const settings: CppSettings = new CppSettings(this.RootUri);
if (settings.isConfigurationWarningsEnabled && !this.isExternalHeader(docUri) && !vscode.debug.activeDebugSession) {
const dismiss: string = localize("dismiss.button", "Dismiss");
const disable: string = localize("disable.warnings.button", "Disable Warnings");
const configName: string | undefined = this.configuration.CurrentConfiguration?.name;
if (!configName) {
return "noConfigName";
}
let message: string = localize("unable.to.provide.configuration",
"{0} is unable to provide IntelliSense configuration information for '{1}'. Settings from the '{2}' configuration will be used instead.",
provider.name, docUri.fsPath, configName);
if (err) {
message += ` (${err})`;
}

if (await vscode.window.showInformationMessage(message, dismiss, disable) === disable) {
settings.toggleSetting("configurationWarnings", "enabled", "disabled");
}
if (await vscode.window.showInformationMessage(message, dismiss, disable) === disable) {
settings.toggleSetting("configurationWarnings", "enabled", "disabled");
}
}
}
return result;
}

private async handleRequestCustomConfig(requestFile: string): Promise<void> {
await this.provideCustomConfiguration(vscode.Uri.file(requestFile), requestFile);
private handleRequestCustomConfig(file: string): void {
const uri: vscode.Uri = vscode.Uri.file(file);
const client: Client = clients.getClientFor(uri);
if (client instanceof DefaultClient) {
const defaultClient: DefaultClient = client as DefaultClient;
void defaultClient.provideCustomConfiguration(uri).catch(logAndReturn.undefined);
}
}

private isExternalHeader(uri: vscode.Uri): boolean {
Expand Down Expand Up @@ -2386,13 +2350,7 @@ export class DefaultClient implements Client {
this.languageClient.onNotification(CompileCommandsPathsNotification, (e) => void this.promptCompileCommands(e));
this.languageClient.onNotification(ReferencesNotification, (e) => this.processReferencesPreview(e));
this.languageClient.onNotification(ReportReferencesProgressNotification, (e) => this.handleReferencesProgress(e));
this.languageClient.onNotification(RequestCustomConfig, (requestFile: string) => {
const client: Client = clients.getClientFor(vscode.Uri.file(requestFile));
if (client instanceof DefaultClient) {
const defaultClient: DefaultClient = client as DefaultClient;
void defaultClient.handleRequestCustomConfig(requestFile);
}
});
this.languageClient.onNotification(RequestCustomConfig, (e) => this.handleRequestCustomConfig(e));
this.languageClient.onNotification(IntelliSenseResultNotification, (e) => this.handleIntelliSenseResult(e));
this.languageClient.onNotification(PublishRefactorDiagnosticsNotification, publishRefactorDiagnostics);
RegisterCodeAnalysisNotifications(this.languageClient);
Expand Down Expand Up @@ -3078,7 +3036,7 @@ export class DefaultClient implements Client {
util.isOptionalArrayOfString(input.configuration.forcedInclude);
}

private sendCustomConfigurations(configs: any, providerVersion: Version, wasRequested: boolean): void {
private sendCustomConfigurations(configs: any, providerVersion: Version): void {
// configs is marked as 'any' because it is untrusted data coming from a 3rd-party. We need to sanitize it before sending it to the language server.
if (!configs || !(configs instanceof Array)) {
console.warn("discarding invalid SourceFileConfigurationItems[]: " + configs);
Expand Down Expand Up @@ -3144,9 +3102,10 @@ export class DefaultClient implements Client {
workspaceFolderUri: this.RootUri?.toString()
};

if (wasRequested) {
void this.languageClient.sendNotification(CustomConfigurationHighPriorityNotification, params).catch(logAndReturn.undefined);
}
// We send the higher priority notification to ensure we don't deadlock if the request is blocking the queue.
// We send the normal priority notification to avoid a race that could result in a redundant request when racing with
// the reset of custom configurations.
void this.languageClient.sendNotification(CustomConfigurationHighPriorityNotification, params).catch(logAndReturn.undefined);
void this.languageClient.sendNotification(CustomConfigurationNotification, params).catch(logAndReturn.undefined);
}

Expand Down Expand Up @@ -4092,7 +4051,7 @@ class NullClient implements Client {
onRegisterCustomConfigurationProvider(provider: CustomConfigurationProvider1): Thenable<void> { return Promise.resolve(); }
updateCustomConfigurations(requestingProvider?: CustomConfigurationProvider1): Thenable<void> { return Promise.resolve(); }
updateCustomBrowseConfiguration(requestingProvider?: CustomConfigurationProvider1): Thenable<void> { return Promise.resolve(); }
provideCustomConfiguration(docUri: vscode.Uri, requestFile?: string, replaceExisting?: boolean): Promise<void> { return Promise.resolve(); }
provideCustomConfiguration(docUri: vscode.Uri): Promise<void> { return Promise.resolve(); }
logDiagnostics(): Promise<void> { return Promise.resolve(); }
rescanFolder(): Promise<void> { return Promise.resolve(); }
toggleReferenceResultsView(): void { }
Expand Down
4 changes: 1 addition & 3 deletions Extension/src/LanguageServer/protocolFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export function createProtocolFilter(): Middleware {
client.sendDidChangeSettings();
document = await vscode.languages.setTextDocumentLanguage(document, "cpp");
}
await client.provideCustomConfiguration(document.uri, undefined);
// client.takeOwnership() will call client.TrackedDocuments.add() again, but that's ok. It's a Set.
client.onDidOpenTextDocument(document);
client.takeOwnership(document);
Expand All @@ -52,8 +51,7 @@ export function createProtocolFilter(): Middleware {
// For a file already open when we activate, sometimes we don't get any notifications about visible
// or active text editors, visible ranges, or text selection. As a workaround, we trigger
// onDidChangeVisibleTextEditors here, only for the first file opened.
if (!anyFileOpened)
{
if (!anyFileOpened) {
anyFileOpened = true;
const cppEditors: vscode.TextEditor[] = vscode.window.visibleTextEditors.filter(e => util.isCpp(e.document));
await client.onDidChangeVisibleTextEditors(cppEditors);
Expand Down
Loading