From 72f7192e9be27643c55e304ab86430a235d1f45e Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Fri, 22 Aug 2025 10:55:09 -0700 Subject: [PATCH 1/6] Fix broken doc link. (#13873) --- Documentation/LanguageServer/c_cpp_properties.json.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/LanguageServer/c_cpp_properties.json.md b/Documentation/LanguageServer/c_cpp_properties.json.md index e4dd1e58e..d9bd52a0c 100644 --- a/Documentation/LanguageServer/c_cpp_properties.json.md +++ b/Documentation/LanguageServer/c_cpp_properties.json.md @@ -1 +1 @@ -The documentation for c_cpp_properties.json has moved to https://code.visualstudio.com/docs/cpp/c-cpp-properties-schema-reference. \ No newline at end of file +The documentation for c_cpp_properties.json has moved to https://code.visualstudio.com/docs/cpp/customize-cpp-settings. \ No newline at end of file From eb6d6abf86dbc339c863fa9be750e904d28ef0dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dirk=20B=C3=A4umer?= Date: Wed, 27 Aug 2025 04:05:29 +0200 Subject: [PATCH 2/6] Register the CPP context provider with Copilot Chat as well. (#13877) * Register context provider API with copilot chat as well. --- .../copilotCompletionContextProvider.ts | 59 +++++++++++++++---- .../src/LanguageServer/copilotProviders.ts | 39 ++++++++++-- 2 files changed, 81 insertions(+), 17 deletions(-) diff --git a/Extension/src/LanguageServer/copilotCompletionContextProvider.ts b/Extension/src/LanguageServer/copilotCompletionContextProvider.ts index cdf75308f..a456ab73c 100644 --- a/Extension/src/LanguageServer/copilotCompletionContextProvider.ts +++ b/Extension/src/LanguageServer/copilotCompletionContextProvider.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All Rights Reserved. * See 'LICENSE' in the project root for license information. * ------------------------------------------------------------------------------------------ */ -import { ContextResolver, ResolveRequest, SupportedContextItem } from '@github/copilot-language-server'; +import { ContextResolver, ResolveRequest, SupportedContextItem, type ContextProvider } from '@github/copilot-language-server'; import { randomUUID } from 'crypto'; import * as vscode from 'vscode'; import { DocumentSelector } from 'vscode-languageserver-protocol'; @@ -11,7 +11,7 @@ import { getOutputChannelLogger, Logger } from '../logger'; import * as telemetry from '../telemetry'; import { CopilotCompletionContextResult } from './client'; import { CopilotCompletionContextTelemetry } from './copilotCompletionContextTelemetry'; -import { getCopilotApi } from './copilotProviders'; +import { getCopilotChatApi, getCopilotClientApi, type CopilotContextProviderAPI } from './copilotProviders'; import { clients } from './extension'; import { CppSettings } from './settings'; @@ -83,7 +83,7 @@ export class CopilotCompletionContextProvider implements ContextResolver = {}; const registerCopilotContextProvider = 'registerCopilotContextProvider'; try { - const copilotApi = await getCopilotApi(); - if (!copilotApi) { throw new CopilotContextProviderException("getCopilotApi() returned null, Copilot is missing or inactive."); } - const hasGetContextProviderAPI = "getContextProviderAPI" in copilotApi; - if (!hasGetContextProviderAPI) { throw new CopilotContextProviderException("getContextProviderAPI() is not available."); } - const contextAPI = await copilotApi.getContextProviderAPI("v1"); - if (!contextAPI) { throw new CopilotContextProviderException("getContextProviderAPI(v1) returned null."); } - this.contextProviderDisposable = contextAPI.registerContextProvider({ + const copilotApi = await getCopilotClientApi(); + const copilotChatApi = await getCopilotChatApi(); + if (!copilotApi && !copilotChatApi) { throw new CopilotContextProviderException("getCopilotApi() returned null, Copilot is missing or inactive."); } + const contextProvider = { id: CopilotCompletionContextProvider.providerId, selector: CopilotCompletionContextProvider.defaultCppDocumentSelector, resolver: this - }); - properties["cppCodeSnippetsProviderRegistered"] = "true"; + }; + type InstallSummary = { hasGetContextProviderAPI: boolean; hasAPI: boolean }; + const installSummary: { client?: InstallSummary; chat?: InstallSummary } = {}; + if (copilotApi) { + installSummary.client = await this.installContextProvider(copilotApi, contextProvider); + } + if (copilotChatApi) { + installSummary.chat = await this.installContextProvider(copilotChatApi, contextProvider); + } + if (installSummary.client?.hasAPI || installSummary.chat?.hasAPI) { + properties["cppCodeSnippetsProviderRegistered"] = "true"; + } else { + if (installSummary.client?.hasGetContextProviderAPI === false && + installSummary.chat?.hasGetContextProviderAPI === false) { + throw new CopilotContextProviderException("getContextProviderAPI() is not available."); + } else { + throw new CopilotContextProviderException("getContextProviderAPI(v1) returned null."); + } + } } catch (e) { console.debug("Failed to register the Copilot Context Provider."); properties["error"] = "Failed to register the Copilot Context Provider"; @@ -466,4 +485,18 @@ ${copilotCompletionContext?.areSnippetsMissing ? "(missing code snippets)" : ""} telemetry.logCopilotEvent(registerCopilotContextProvider, { ...properties }); } } + + private async installContextProvider(copilotAPI: CopilotContextProviderAPI, contextProvider: ContextProvider): Promise<{ hasGetContextProviderAPI: boolean; hasAPI: boolean }> { + const hasGetContextProviderAPI = typeof copilotAPI.getContextProviderAPI === 'function'; + if (hasGetContextProviderAPI) { + const contextAPI = await copilotAPI.getContextProviderAPI("v1"); + if (contextAPI) { + this.contextProviderDisposables = this.contextProviderDisposables ?? []; + this.contextProviderDisposables.push(contextAPI.registerContextProvider(contextProvider)); + } + return { hasGetContextProviderAPI, hasAPI: contextAPI !== undefined }; + } else { + return { hasGetContextProviderAPI: false, hasAPI: false }; + } + } } diff --git a/Extension/src/LanguageServer/copilotProviders.ts b/Extension/src/LanguageServer/copilotProviders.ts index e0551edcb..31cf21f3e 100644 --- a/Extension/src/LanguageServer/copilotProviders.ts +++ b/Extension/src/LanguageServer/copilotProviders.ts @@ -24,7 +24,11 @@ export interface CopilotTrait { promptTextOverride?: string; } -export interface CopilotApi { +export interface CopilotContextProviderAPI { + getContextProviderAPI(version: string): Promise; +} + +export interface CopilotApi extends CopilotContextProviderAPI { registerRelatedFilesProvider( providerId: { extensionId: string; languageId: string }, callback: ( @@ -33,11 +37,10 @@ export interface CopilotApi { cancellationToken: vscode.CancellationToken ) => Promise<{ entries: vscode.Uri[]; traits?: CopilotTrait[] } | undefined> ): Disposable; - getContextProviderAPI(version: string): Promise; } export async function registerRelatedFilesProvider(): Promise { - const api = await getCopilotApi(); + const api = await getCopilotClientApi(); if (util.extensionContext && api) { try { for (const languageId of ['c', 'cpp', 'cuda-cpp']) { @@ -129,7 +132,7 @@ async function getIncludes(uri: vscode.Uri, maxDepth: number): Promise { +export async function getCopilotClientApi(): Promise { const copilotExtension = vscode.extensions.getExtension('github.copilot'); if (!copilotExtension) { return undefined; @@ -145,3 +148,31 @@ export async function getCopilotApi(): Promise { return copilotExtension.exports; } } + +export async function getCopilotChatApi(): Promise { + type CopilotChatApi = { getAPI?(version: number): CopilotContextProviderAPI | undefined }; + const copilotExtension = vscode.extensions.getExtension('github.copilot-chat'); + if (!copilotExtension) { + return undefined; + } + + let exports: CopilotChatApi | undefined; + if (!copilotExtension.isActive) { + try { + exports = await copilotExtension.activate(); + } catch { + return undefined; + } + } else { + exports = copilotExtension.exports; + } + if (!exports || typeof exports.getAPI !== 'function') { + return undefined; + } + const result = exports.getAPI(1); + return result; +} + +interface Disposable { + dispose(): void; +} From 6479acdb78c060e6a1b0793c09c2ce22ebf00ac7 Mon Sep 17 00:00:00 2001 From: Bob Brown Date: Thu, 28 Aug 2025 15:35:07 -0700 Subject: [PATCH 3/6] Fix for input delays when editing c_cpp_properties.json (#13878) --- .../src/LanguageServer/configurations.ts | 167 +++++++++++++----- Extension/src/LanguageServer/dataBinding.ts | 18 +- Extension/src/LanguageServer/utils.ts | 20 +++ 3 files changed, 141 insertions(+), 64 deletions(-) diff --git a/Extension/src/LanguageServer/configurations.ts b/Extension/src/LanguageServer/configurations.ts index 8a08b9987..ea135109d 100644 --- a/Extension/src/LanguageServer/configurations.ts +++ b/Extension/src/LanguageServer/configurations.ts @@ -24,6 +24,7 @@ import { PersistentFolderState } from './persistentState'; import { CppSettings, OtherSettings } from './settings'; import { SettingsPanel } from './settingsPanel'; import { ConfigurationType, getUI } from './ui'; +import { Deferral } from './utils'; import escapeStringRegExp = require('escape-string-regexp'); nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); @@ -152,7 +153,6 @@ export class CppProperties { private defaultCStandard: string | null = null; private defaultCppStandard: string | null = null; private defaultWindowsSdkVersion: string | null = null; - private isCppPropertiesJsonVisible: boolean = false; private vcpkgIncludes: string[] = []; private vcpkgPathReady: boolean = false; private nodeAddonIncludes: string[] = []; @@ -250,7 +250,6 @@ export class CppProperties { } public setupConfigurations(): void { - // defaultPaths is only used when there isn't a c_cpp_properties.json, but we don't send the configuration changed event // to the language server until the default include paths and frameworks have been sent. @@ -280,21 +279,21 @@ export class CppProperties { }); vscode.workspace.onDidChangeTextDocument((e) => { - if (e.document.uri.fsPath === settingsPath && this.isCppPropertiesJsonVisible) { - void this.handleSquiggles().catch(logAndReturn.undefined); + if (e.document.uri.fsPath === settingsPath) { + this.handleSquiggles(e.document); } }); - vscode.window.onDidChangeVisibleTextEditors((editors) => { - const wasVisible: boolean = this.isCppPropertiesJsonVisible; - editors.forEach(editor => { - if (editor.document.uri.fsPath === settingsPath) { - this.isCppPropertiesJsonVisible = true; - if (!wasVisible) { - void this.handleSquiggles().catch(logAndReturn.undefined); - } - } - }); + vscode.workspace.onDidOpenTextDocument(document => { + if (document.uri.fsPath === settingsPath) { + this.handleSquiggles(); + } + }); + + vscode.workspace.onDidCloseTextDocument(document => { + if (document.uri.fsPath === settingsPath) { + this.diagnosticCollection.clear(); + } }); vscode.workspace.onDidSaveTextDocument((doc: vscode.TextDocument) => { @@ -331,6 +330,7 @@ export class CppProperties { } }); } + public set CompilerDefaults(compilerDefaults: CompilerDefaults) { this.defaultCompilerPath = compilerDefaults.trustedCompilerFound ? compilerDefaults.compilerPath : null; this.knownCompilers = compilerDefaults.knownCompilers; @@ -353,7 +353,7 @@ export class CppProperties { private onSelectionChanged(): void { this.selectionChanged.fire(this.CurrentConfigurationIndex); - void this.handleSquiggles().catch(logAndReturn.undefined); + this.handleSquiggles(); } private onCompileCommandsChanged(path: string): void { @@ -493,9 +493,10 @@ export class CppProperties { }); } } - } catch (error) { /*ignore*/ } finally { + } catch (error) { + /*ignore*/ + } finally { this.vcpkgPathReady = true; - this.handleConfigurationChange(); } } @@ -795,7 +796,7 @@ export class CppProperties { return result; } - private resolveAndSplit(paths: string[] | undefined, defaultValue: string[] | undefined, env: Environment, assumeRelative: boolean = true, glob: boolean = false): string[] { + private async resolveAndSplit(paths: string[] | undefined, defaultValue: string[] | undefined, env: Environment, assumeRelative: boolean = true, glob: boolean = false): Promise { const resolvedVariables: string[] = []; if (paths === undefined) { return resolvedVariables; @@ -846,7 +847,7 @@ export class CppProperties { if (isGlobPattern) { // fastGlob silently strips non-found paths. Limit that behavior to dynamic paths only. const matches: string[] = fastGlob.isDynamicPattern(normalized) ? - fastGlob.sync(normalized, { onlyDirectories: true, cwd, suppressErrors: true, deep: 15 }) : [res]; + await fastGlob.async(normalized, { onlyDirectories: true, cwd, suppressErrors: true, deep: 15 }) : [res]; resolvedGlob.push(...matches.map(s => s + suffix)); if (resolvedGlob.length === 0) { resolvedGlob.push(normalized); @@ -881,14 +882,14 @@ export class CppProperties { return property; } - private updateConfigurationPathsArray(paths: string[] | undefined, defaultValue: string[] | undefined, env: Environment, assumeRelative: boolean = true): string[] | undefined { + private updateConfigurationPathsArray(paths: string[] | undefined, defaultValue: string[] | undefined, env: Environment, assumeRelative: boolean = true): Promise { if (paths) { return this.resolveAndSplit(paths, defaultValue, env, assumeRelative, true); } - if (!paths && defaultValue) { + if (defaultValue) { return this.resolveAndSplit(defaultValue, [], env, assumeRelative, true); } - return paths; + return Promise.resolve(undefined); } private updateConfigurationBoolean(property: boolean | string | undefined | null, defaultValue: boolean | undefined | null): boolean | undefined { @@ -933,7 +934,7 @@ export class CppProperties { return this.configProviderAutoSelected; } - private updateServerOnFolderSettingsChange(): void { + private async updateServerOnFolderSettingsChange(): Promise { this.configProviderAutoSelected = false; if (!this.configurationJson) { return; @@ -946,7 +947,7 @@ export class CppProperties { configuration.compilerPathInCppPropertiesJson = configuration.compilerPath; configuration.compileCommandsInCppPropertiesJson = configuration.compileCommands; configuration.configurationProviderInCppPropertiesJson = configuration.configurationProvider; - configuration.includePath = this.updateConfigurationPathsArray(configuration.includePath, settings.defaultIncludePath, env); + configuration.includePath = await this.updateConfigurationPathsArray(configuration.includePath, settings.defaultIncludePath, env); // in case includePath is reset below const origIncludePath: string[] | undefined = configuration.includePath; if (userSettings.addNodeAddonIncludePaths) { @@ -964,7 +965,7 @@ export class CppProperties { configuration.macFrameworkPath = this.updateConfigurationStringArray(configuration.macFrameworkPath, settings.defaultMacFrameworkPath, env); configuration.windowsSdkVersion = this.updateConfigurationString(configuration.windowsSdkVersion, settings.defaultWindowsSdkVersion, env); - configuration.forcedInclude = this.updateConfigurationPathsArray(configuration.forcedInclude, settings.defaultForcedInclude, env, false); + configuration.forcedInclude = await this.updateConfigurationPathsArray(configuration.forcedInclude, settings.defaultForcedInclude, env, false); configuration.compileCommands = this.updateConfigurationStringArray(configuration.compileCommands, settings.defaultCompileCommands, env); configuration.compilerArgs = this.updateConfigurationStringArray(configuration.compilerArgs, settings.defaultCompilerArgs, env); configuration.cStandard = this.updateConfigurationString(configuration.cStandard, settings.defaultCStandard, env); @@ -1042,7 +1043,7 @@ export class CppProperties { // Otherwise, if the browse path is not set, let the native process populate it // with include paths, including any parsed from compilerArgs. } else { - configuration.browse.path = this.updateConfigurationPathsArray(configuration.browse.path, settings.defaultBrowsePath, env); + configuration.browse.path = await this.updateConfigurationPathsArray(configuration.browse.path, settings.defaultBrowsePath, env); } configuration.browse.limitSymbolsToIncludedHeaders = this.updateConfigurationBoolean(configuration.browse.limitSymbolsToIncludedHeaders, settings.defaultLimitSymbolsToIncludedHeaders); @@ -1261,7 +1262,7 @@ export class CppProperties { this.settingsPanel.selectedConfigIndex = this.CurrentConfigurationIndex; this.settingsPanel.createOrShow(configNames, this.configurationJson.configurations[this.settingsPanel.selectedConfigIndex], - this.getErrorsForConfigUI(this.settingsPanel.selectedConfigIndex), + await this.getErrorsForConfigUI(this.settingsPanel.selectedConfigIndex), viewColumn); } } @@ -1292,7 +1293,7 @@ export class CppProperties { } this.settingsPanel.updateConfigUI(configNames, this.configurationJson.configurations[this.settingsPanel.selectedConfigIndex], - this.getErrorsForConfigUI(this.settingsPanel.selectedConfigIndex)); + await this.getErrorsForConfigUI(this.settingsPanel.selectedConfigIndex)); } else { // Parse failed, open json file void vscode.workspace.openTextDocument(this.propertiesFile).then(undefined, logAndReturn.undefined); @@ -1321,13 +1322,13 @@ export class CppProperties { return trimmedPaths; } - private saveConfigurationUI(): void { + private async saveConfigurationUI(): Promise { this.parsePropertiesFile(); // Clear out any modifications we may have made internally. if (this.settingsPanel && this.configurationJson) { const config: Configuration = this.settingsPanel.getLastValuesFromConfigUI(); this.configurationJson.configurations[this.settingsPanel.selectedConfigIndex] = config; this.configurationJson.configurations[this.settingsPanel.selectedConfigIndex].includePath = this.trimPathWhitespace(this.configurationJson.configurations[this.settingsPanel.selectedConfigIndex].includePath); - this.settingsPanel.updateErrors(this.getErrorsForConfigUI(this.settingsPanel.selectedConfigIndex)); + this.settingsPanel.updateErrors(await this.getErrorsForConfigUI(this.settingsPanel.selectedConfigIndex)); this.writeToJson(); } // Any time parsePropertiesFile is called, configurationJson gets @@ -1335,12 +1336,12 @@ export class CppProperties { this.handleConfigurationChange(); } - private onConfigSelectionChanged(): void { + private async onConfigSelectionChanged(): Promise { const configNames: string[] | undefined = this.ConfigurationNames; if (configNames && this.settingsPanel && this.configurationJson) { this.settingsPanel.updateConfigUI(configNames, this.configurationJson.configurations[this.settingsPanel.selectedConfigIndex], - this.getErrorsForConfigUI(this.settingsPanel.selectedConfigIndex)); + await this.getErrorsForConfigUI(this.settingsPanel.selectedConfigIndex)); } } @@ -1393,7 +1394,9 @@ export class CppProperties { } void this.applyDefaultIncludePathsAndFrameworks().catch(logAndReturn.undefined); - this.updateServerOnFolderSettingsChange(); + void this.timeOperation(() => this.updateServerOnFolderSettingsChange()).then(result => { + getOutputChannelLogger().appendLineAtLevel(5, localize('resolve.configuration.processed', "Processed c_cpp_properties.json in {0}s", result.duration)); + }).catch(logAndReturn.undefined); } private async ensurePropertiesFile(): Promise { @@ -1454,6 +1457,7 @@ export class CppProperties { return false; } let success: boolean = true; + const firstParse = this.configurationJson === undefined; try { const readResults: string = fs.readFileSync(this.propertiesFile.fsPath, 'utf8'); if (readResults === "") { @@ -1584,6 +1588,14 @@ export class CppProperties { e.recursiveIncludesOrderIsExplicit = e.recursiveIncludes?.order !== undefined; }); + if (firstParse) { + // Check documents that are already open since no event will fire for them. + const fsPath = this.propertiesFile.fsPath; + const document = vscode.workspace.textDocuments.find(doc => doc.uri.fsPath === fsPath); + if (document) { + this.handleSquiggles(document); + } + } } catch (errJS) { const err: Error = errJS as Error; const failedToParse: string = localize("failed.to.parse.properties", 'Failed to parse "{0}"', this.propertiesFile.fsPath); @@ -1712,7 +1724,21 @@ export class CppProperties { return compilerPathAndArgs; } - private getErrorsForConfigUI(configIndex: number): ConfigurationErrors { + /** + * Time an operation and return the result plus the length of the operation. + * + * @param operation The async operation to perform. + * @returns The result of the operation and the time it took to complete in seconds. + */ + private async timeOperation(operation: () => Promise): Promise<{ result: T | undefined; duration: number }> { + const start = process.hrtime(); + const result = await operation(); + const diff = process.hrtime(start); + const duration = diff[0] + diff[1] / 1e9; // diff[0] is in seconds, diff[1] is in nanoseconds + return { result, duration }; + } + + private async getErrorsForConfigUI(configIndex: number): Promise { const errors: ConfigurationErrors = {}; if (!this.configurationJson) { return errors; @@ -1731,15 +1757,20 @@ export class CppProperties { errors.compilerPath = compilerPathAndArgs.error; // Validate paths (directories) - errors.includePath = this.validatePath(config.includePath, { globPaths: true }); - errors.macFrameworkPath = this.validatePath(config.macFrameworkPath); - errors.browsePath = this.validatePath(config.browse ? config.browse.path : undefined); + try { + const { result, duration } = await this.timeOperation(() => this.validatePath(config.includePath, { globPaths: true })); + errors.includePath = result ?? duration >= 10 ? localize('resolve.includePath.took.too.long', "The include path validation took {0}s to evaluate", duration) : undefined; + } catch (e) { + errors.includePath = localize('resolve.includePath.failed', "Failed to resolve include path. Error: {0}", (e as Error).message); + } + errors.macFrameworkPath = await this.validatePath(config.macFrameworkPath); + errors.browsePath = await this.validatePath(config.browse ? config.browse.path : undefined); // Validate files - errors.forcedInclude = this.validatePath(config.forcedInclude, { isDirectory: false, assumeRelative: false }); - errors.compileCommands = this.validatePath(config.compileCommands, { isDirectory: false }); - errors.dotConfig = this.validatePath(config.dotConfig, { isDirectory: false }); - errors.databaseFilename = this.validatePath(config.browse ? config.browse.databaseFilename : undefined, { isDirectory: false }); + errors.forcedInclude = await this.validatePath(config.forcedInclude, { isDirectory: false, assumeRelative: false }); + errors.compileCommands = await this.validatePath(config.compileCommands, { isDirectory: false }); + errors.dotConfig = await this.validatePath(config.dotConfig, { isDirectory: false }); + errors.databaseFilename = await this.validatePath(config.browse ? config.browse.databaseFilename : undefined, { isDirectory: false }); // Validate intelliSenseMode if (isWindows) { @@ -1752,7 +1783,7 @@ export class CppProperties { return errors; } - private validatePath(input: string | string[] | undefined, { isDirectory = true, assumeRelative = true, globPaths = false } = {}): string | undefined { + private async validatePath(input: string | string[] | undefined, { isDirectory = true, assumeRelative = true, globPaths = false } = {}): Promise { if (!input) { return undefined; } @@ -1768,7 +1799,7 @@ export class CppProperties { } // Resolve and split any environment variables - paths = this.resolveAndSplit(paths, undefined, this.ExtendedEnvironment, assumeRelative, globPaths); + paths = await this.resolveAndSplit(paths, undefined, this.ExtendedEnvironment, assumeRelative, globPaths); for (const p of paths) { let pathExists: boolean = true; @@ -1834,7 +1865,24 @@ export class CppProperties { return errorMsg; } - private async handleSquiggles(): Promise { + private lastConfigurationVersion: number = 0; + private handleSquigglesDeferral: Deferral | undefined; + + private handleSquiggles(doc?: vscode.TextDocument): void { + // When we open the doc or the active config changes, we don't pass the doc in since we always want to process squiggles. + if (doc?.version !== this.lastConfigurationVersion) { + this.lastConfigurationVersion = doc?.version ?? 0; + + // Debounce the squiggles requests. + this.handleSquigglesDeferral?.cancel(); + this.handleSquigglesDeferral = new Deferral(() => void this.handleSquigglesImpl().catch(logAndReturn.undefined), 1000); + } + } + + /** + * Not to be called directly. Use the `handleSquiggles` method instead which will debounce the calls. + */ + private async handleSquigglesImpl(): Promise { if (!this.propertiesFile) { return; } @@ -1949,9 +1997,9 @@ export class CppProperties { curText = curText.substring(0, nextNameStart2); } if (this.prevSquiggleMetrics.get(currentConfiguration.name) === undefined) { - this.prevSquiggleMetrics.set(currentConfiguration.name, { PathNonExistent: 0, PathNotAFile: 0, PathNotADirectory: 0, CompilerPathMissingQuotes: 0, CompilerModeMismatch: 0, MultiplePathsNotAllowed: 0, MultiplePathsShouldBeSeparated: 0 }); + this.prevSquiggleMetrics.set(currentConfiguration.name, { PathNonExistent: 0, PathNotAFile: 0, PathNotADirectory: 0, SlowPathResolution: 0, CompilerPathMissingQuotes: 0, CompilerModeMismatch: 0, MultiplePathsNotAllowed: 0, MultiplePathsShouldBeSeparated: 0 }); } - const newSquiggleMetrics: { [key: string]: number } = { PathNonExistent: 0, PathNotAFile: 0, PathNotADirectory: 0, CompilerPathMissingQuotes: 0, CompilerModeMismatch: 0, MultiplePathsNotAllowed: 0, MultiplePathsShouldBeSeparated: 0 }; + const newSquiggleMetrics: { [key: string]: number } = { PathNonExistent: 0, PathNotAFile: 0, PathNotADirectory: 0, SlowPathResolution: 0, CompilerPathMissingQuotes: 0, CompilerModeMismatch: 0, MultiplePathsNotAllowed: 0, MultiplePathsShouldBeSeparated: 0 }; const isWindows: boolean = os.platform() === 'win32'; // TODO: Add other squiggles. @@ -2085,8 +2133,33 @@ export class CppProperties { // and extend that pattern to the next quote before and next quote after it. const pattern: RegExp = new RegExp(`"[^"]*?(?<="|;)${escapedPath}(?="|;).*?"`, "g"); const configMatches: string[] | null = curText.match(pattern); + let expandedPaths: string[]; - const expandedPaths: string[] = this.resolveAndSplit([curPath], undefined, this.ExtendedEnvironment, true, true); + try { + const { result, duration } = await this.timeOperation(() => this.resolveAndSplit([curPath], undefined, this.ExtendedEnvironment, true, true)); + expandedPaths = result ?? []; + if (duration > 10 && configMatches) { + newSquiggleMetrics.SlowPathResolution++; + const curOffset = curText.indexOf(configMatches[0]); + const endOffset = curOffset + curPath.length; + const diagnostic: vscode.Diagnostic = new vscode.Diagnostic( + new vscode.Range(document.positionAt(curTextStartOffset + curOffset), document.positionAt(curTextStartOffset + endOffset)), + localize('resolve.path.took.too.long', "Path took {0}s to evaluate", duration), + vscode.DiagnosticSeverity.Warning); + diagnostics.push(diagnostic); + } + } catch (e) { + expandedPaths = []; + if (configMatches) { + const curOffset = curText.indexOf(configMatches[0]); + const endOffset = curOffset + curPath.length; + const diagnostic: vscode.Diagnostic = new vscode.Diagnostic( + new vscode.Range(document.positionAt(curTextStartOffset + curOffset), document.positionAt(curTextStartOffset + endOffset)), + localize('resolve.path.failed', "Failed to resolve path {0}. Error: {1}", curPath, (e as Error).message), + vscode.DiagnosticSeverity.Warning); + diagnostics.push(diagnostic); + } + } const incorrectExpandedPaths: string[] = []; if (expandedPaths.length <= 0) { diff --git a/Extension/src/LanguageServer/dataBinding.ts b/Extension/src/LanguageServer/dataBinding.ts index 26a4691b7..5dcceaf5d 100644 --- a/Extension/src/LanguageServer/dataBinding.ts +++ b/Extension/src/LanguageServer/dataBinding.ts @@ -3,23 +3,7 @@ * See 'LICENSE' in the project root for license information. * ------------------------------------------------------------------------------------------ */ import * as vscode from 'vscode'; - -class Deferral { - private timer?: NodeJS.Timeout; - - constructor(callback: () => void, timeout: number) { - this.timer = setTimeout(() => { - this.timer = undefined; - callback(); - }, timeout); - } - public cancel() { - if (this.timer) { - clearTimeout(this.timer); - this.timer = undefined; - } - } -} +import { Deferral } from './utils'; export class DataBinding { private valueChanged = new vscode.EventEmitter(); diff --git a/Extension/src/LanguageServer/utils.ts b/Extension/src/LanguageServer/utils.ts index 3a46a486a..fff791eef 100644 --- a/Extension/src/LanguageServer/utils.ts +++ b/Extension/src/LanguageServer/utils.ts @@ -118,3 +118,23 @@ export async function checkDuration(fn: () => Promise): Promise<{ result: const result = await fn(); return { result, duration: performance.now() - start }; } + +/** + * A class that delays the execution of a callback until a specified timeout period has elapsed. + */ +export class Deferral { + private timer?: NodeJS.Timeout; + + constructor(callback: () => void, timeout: number) { + this.timer = setTimeout(() => { + this.timer = undefined; + callback(); + }, timeout); + } + public cancel() { + if (this.timer) { + clearTimeout(this.timer); + this.timer = undefined; + } + } +} From 9f04a5e9055b8be92a298aedc15ac0037367f73d Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Tue, 2 Sep 2025 13:07:54 -0700 Subject: [PATCH 4/6] Add more state info to cpptools crash logging. (#13888) * Add more state info to cpptools crash logging. --- Extension/src/LanguageServer/client.ts | 127 +++++++++++++--------- Extension/src/LanguageServer/extension.ts | 3 + 2 files changed, 79 insertions(+), 51 deletions(-) diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 7345b0c78..f8d810d42 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -59,7 +59,7 @@ import { CopilotCompletionContextFeatures, CopilotCompletionContextProvider } fr import { CustomConfigurationProvider1, getCustomConfigProviders, isSameProviderExtensionId } from './customProviders'; import { DataBinding } from './dataBinding'; import { cachedEditorConfigSettings, getEditorConfigSettings } from './editorConfig'; -import { CppSourceStr, clients, configPrefix, initializeIntervalTimer, updateLanguageConfigurations, usesCrashHandler, watchForCrashes } from './extension'; +import { CppSourceStr, clients, configPrefix, initializeIntervalTimer, isWritingCrashCallStack, updateLanguageConfigurations, usesCrashHandler, watchForCrashes } from './extension'; import { LocalizeStringParams, getLocaleId, getLocalizedString } from './localization'; import { PersistentFolderState, PersistentState, PersistentWorkspaceState } from './persistentState'; import { RequestCancelled, ServerCancelled, createProtocolFilter } from './protocolFilter'; @@ -953,6 +953,8 @@ export class DefaultClient implements Client { public getShowConfigureIntelliSenseButton(): boolean { return this.showConfigureIntelliSenseButton; } public setShowConfigureIntelliSenseButton(show: boolean): void { this.showConfigureIntelliSenseButton = show; } + private lastInvokedLspMessage: string = ""; // e.g. cpptools/hover + /** * don't use this.rootFolder directly since it can be undefined */ @@ -1688,7 +1690,6 @@ export class DefaultClient implements Client { closed: () => { languageClientCrashTimes.push(Date.now()); languageClientCrashedNeedsRestart = true; - telemetry.logLanguageServerEvent("languageClientCrash"); let restart: boolean = true; if (languageClientCrashTimes.length < 5) { void clients.recreateClients(); @@ -1702,6 +1703,26 @@ export class DefaultClient implements Client { void clients.recreateClients(); } } + + // Wait 1 second to allow time for the file watcher to signal a crash call stack write has occurred. + setTimeout(() => { + telemetry.logLanguageServerEvent("languageClientCrash", + { + lastInvokedLspMessage: this.lastInvokedLspMessage + }, + { + restarting: Number(restart), + writingCrashCallStack: Number(isWritingCrashCallStack), + initializingWorkspace: Number(this.model.isInitializingWorkspace.Value), + indexingWorkspace: Number(this.model.isIndexingWorkspace.Value), + parsingWorkspace: Number(this.model.isParsingWorkspace.Value), + parsingFiles: Number(this.model.isParsingFiles.Value), + updatingIntelliSense: Number(this.model.isUpdatingIntelliSense.Value), + runningCodeAnalysis: Number(this.model.isRunningCodeAnalysis.Value) + } + ); + }, 1000); + const message: string = restart ? localize('server.crashed.restart', 'The language server crashed. Restarting...') : localize('server.crashed2', 'The language server crashed 5 times in the last 3 minutes. It will not be restarted.'); @@ -2768,55 +2789,59 @@ export class DefaultClient implements Client { const message: string = notificationBody.status; util.setProgress(util.getProgressExecutableSuccess()); const testHook: TestHook = getTestHook(); - if (message.endsWith("Idle")) { - const status: IntelliSenseStatus = { status: Status.Idle }; - testHook.updateStatus(status); - } else if (message.endsWith("Parsing")) { - this.model.isParsingWorkspace.Value = true; - this.model.isInitializingWorkspace.Value = false; - this.model.isIndexingWorkspace.Value = false; - const status: IntelliSenseStatus = { status: Status.TagParsingBegun }; - testHook.updateStatus(status); - } else if (message.endsWith("Initializing")) { - this.model.isInitializingWorkspace.Value = true; - this.model.isIndexingWorkspace.Value = false; - this.model.isParsingWorkspace.Value = false; - } else if (message.endsWith("Indexing")) { - this.model.isIndexingWorkspace.Value = true; - this.model.isInitializingWorkspace.Value = false; - this.model.isParsingWorkspace.Value = false; - } else if (message.endsWith("files")) { - this.model.isParsingFiles.Value = true; - } else if (message.endsWith("IntelliSense")) { - timeStamp = Date.now(); - this.model.isUpdatingIntelliSense.Value = true; - const status: IntelliSenseStatus = { status: Status.IntelliSenseCompiling }; - testHook.updateStatus(status); - } else if (message.endsWith("IntelliSense done")) { - getOutputChannelLogger().appendLineAtLevel(6, localize("update.intellisense.time", "Update IntelliSense time (sec): {0}", (Date.now() - timeStamp) / 1000)); - this.model.isUpdatingIntelliSense.Value = false; - const status: IntelliSenseStatus = { status: Status.IntelliSenseReady }; - testHook.updateStatus(status); - } else if (message.endsWith("Parsing done")) { // Tag Parser Ready - this.model.isParsingWorkspace.Value = false; - const status: IntelliSenseStatus = { status: Status.TagParsingDone }; - testHook.updateStatus(status); - util.setProgress(util.getProgressParseRootSuccess()); - } else if (message.endsWith("files done")) { - this.model.isParsingFiles.Value = false; - } else if (message.endsWith("Analysis")) { - this.model.isRunningCodeAnalysis.Value = true; - this.model.codeAnalysisTotal.Value = 1; - this.model.codeAnalysisProcessed.Value = 0; - } else if (message.endsWith("Analysis done")) { - this.model.isRunningCodeAnalysis.Value = false; - } else if (message.includes("Squiggles Finished - File name:")) { - const index: number = message.lastIndexOf(":"); - const name: string = message.substring(index + 2); - const status: IntelliSenseStatus = { status: Status.IntelliSenseReady, filename: name }; - testHook.updateStatus(status); - } else if (message.endsWith("No Squiggles")) { - util.setIntelliSenseProgress(util.getProgressIntelliSenseNoSquiggles()); + if (message.startsWith("C_Cpp: ")) { + if (message.endsWith("Idle")) { + const status: IntelliSenseStatus = { status: Status.Idle }; + testHook.updateStatus(status); + } else if (message.endsWith("Parsing")) { + this.model.isParsingWorkspace.Value = true; + this.model.isInitializingWorkspace.Value = false; + this.model.isIndexingWorkspace.Value = false; + const status: IntelliSenseStatus = { status: Status.TagParsingBegun }; + testHook.updateStatus(status); + } else if (message.endsWith("Initializing")) { + this.model.isInitializingWorkspace.Value = true; + this.model.isIndexingWorkspace.Value = false; + this.model.isParsingWorkspace.Value = false; + } else if (message.endsWith("Indexing")) { + this.model.isIndexingWorkspace.Value = true; + this.model.isInitializingWorkspace.Value = false; + this.model.isParsingWorkspace.Value = false; + } else if (message.endsWith("files")) { + this.model.isParsingFiles.Value = true; + } else if (message.endsWith("IntelliSense")) { + timeStamp = Date.now(); + this.model.isUpdatingIntelliSense.Value = true; + const status: IntelliSenseStatus = { status: Status.IntelliSenseCompiling }; + testHook.updateStatus(status); + } else if (message.endsWith("IntelliSense done")) { + getOutputChannelLogger().appendLineAtLevel(6, localize("update.intellisense.time", "Update IntelliSense time (sec): {0}", (Date.now() - timeStamp) / 1000)); + this.model.isUpdatingIntelliSense.Value = false; + const status: IntelliSenseStatus = { status: Status.IntelliSenseReady }; + testHook.updateStatus(status); + } else if (message.endsWith("Parsing done")) { // Tag Parser Ready + this.model.isParsingWorkspace.Value = false; + const status: IntelliSenseStatus = { status: Status.TagParsingDone }; + testHook.updateStatus(status); + util.setProgress(util.getProgressParseRootSuccess()); + } else if (message.endsWith("files done")) { + this.model.isParsingFiles.Value = false; + } else if (message.endsWith("Analysis")) { + this.model.isRunningCodeAnalysis.Value = true; + this.model.codeAnalysisTotal.Value = 1; + this.model.codeAnalysisProcessed.Value = 0; + } else if (message.endsWith("Analysis done")) { + this.model.isRunningCodeAnalysis.Value = false; + } else if (message.includes("Squiggles Finished - File name:")) { + const index: number = message.lastIndexOf(":"); + const name: string = message.substring(index + 2); + const status: IntelliSenseStatus = { status: Status.IntelliSenseReady, filename: name }; + testHook.updateStatus(status); + } else if (message.endsWith("No Squiggles")) { + util.setIntelliSenseProgress(util.getProgressIntelliSenseNoSquiggles()); + } + } else if (message.includes("/")) { + this.lastInvokedLspMessage = message; } } diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index db96623bf..a20d6e100 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -55,6 +55,7 @@ let languageConfigurations: vscode.Disposable[] = []; let intervalTimer: NodeJS.Timeout; let codeActionProvider: vscode.Disposable; export const intelliSenseDisabledError: string = "Do not activate the extension when IntelliSense is disabled."; +export let isWritingCrashCallStack: boolean = false; type VcpkgDatabase = Record; // Stored as
-> [] let vcpkgDbPromise: Promise; @@ -1023,9 +1024,11 @@ export function watchForCrashes(crashDirectory: string): void { return; } const crashDate: Date = new Date(); + isWritingCrashCallStack = true; // Wait 5 seconds to allow time for the crash log to finish being written. setTimeout(() => { + isWritingCrashCallStack = false; fs.readFile(path.resolve(crashDirectory, filename), 'utf8', (err, data) => { void handleCrashFileRead(crashDirectory, filename, crashDate, err, data); }); From 968332d3c1426ccc1e5dc6ceab77f0f0c3fb9c0e Mon Sep 17 00:00:00 2001 From: Bob Brown Date: Tue, 2 Sep 2025 14:36:34 -0700 Subject: [PATCH 5/6] Warn users about too many files processed in the workspace (#13889) --- Extension/src/LanguageServer/client.ts | 32 ++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index f8d810d42..9b1cd57ba 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -1744,7 +1744,7 @@ export class DefaultClient implements Client { languageClient = new LanguageClient(`cpptools`, serverOptions, clientOptions); languageClient.onNotification(DebugProtocolNotification, logDebugProtocol); languageClient.onNotification(DebugLogNotification, logLocalized); - languageClient.onNotification(LogTelemetryNotification, (e) => this.logTelemetry(e)); + languageClient.onNotification(LogTelemetryNotification, (e) => void this.logTelemetry(e)); languageClient.onNotification(ShowMessageWindowNotification, showMessageWindow); languageClient.registerProposedFeatures(); await languageClient.start(); @@ -2778,10 +2778,38 @@ export class DefaultClient implements Client { } } - private logTelemetry(notificationBody: TelemetryPayload): void { + private excessiveFilesWarningShown: boolean = false; + private async logTelemetry(notificationBody: TelemetryPayload): Promise { if (notificationBody.event === "includeSquiggles" && this.configurationProvider && notificationBody.properties) { notificationBody.properties["providerId"] = this.configurationProvider; } + + const showExcessiveFilesWarning = new PersistentWorkspaceState('CPP.showExcessiveFilesWarning', true); + if (!this.excessiveFilesWarningShown && showExcessiveFilesWarning.Value && notificationBody.event === 'ParsingStats') { + const filesDiscovered = notificationBody.metrics?.filesDiscovered ?? 0; + const parsableFiles = notificationBody.metrics?.parsableFiles ?? 0; + if (filesDiscovered > 250000 || parsableFiles > 100000) { + // According to telemetry, less than 3% of workspaces have this many files so it seems like a reasonable threshold. + + const message = localize( + "parsing.stats.large.project", + 'Enumerated {0} files with {1} C/C++ source files detected. You may want to consider excluding some files for better performance.', + filesDiscovered, + parsableFiles); + const learnMore = localize('learn.more', 'Learn More'); + const dontShowAgain = localize('dont.show.again', 'Don\'t Show Again'); + + // We only want to show this once per session. + this.excessiveFilesWarningShown = true; + const response = await vscode.window.showInformationMessage(message, learnMore, dontShowAgain); + + if (response === dontShowAgain) { + showExcessiveFilesWarning.Value = false; + } else if (response === learnMore) { + void vscode.commands.executeCommand('vscode.open', vscode.Uri.parse('https://go.microsoft.com/fwlink/?linkid=2333292')); + } + } + } telemetry.logLanguageServerEvent(notificationBody.event, notificationBody.properties, notificationBody.metrics); } From 854fecf0eaba3d87becf43d1d42db64aae6307f3 Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Tue, 2 Sep 2025 15:51:50 -0700 Subject: [PATCH 6/6] Update changelog and version for 1.27.3. (#13894) * Update changelog and version for 1.27.3. --- Extension/CHANGELOG.md | 12 ++++++++++++ Extension/package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Extension/CHANGELOG.md b/Extension/CHANGELOG.md index 3265680cc..fd018dd63 100644 --- a/Extension/CHANGELOG.md +++ b/Extension/CHANGELOG.md @@ -1,5 +1,17 @@ # C/C++ for Visual Studio Code Changelog +## Version 1.27.3: September 3, 2025 +### Enhancements +* Show a warning when too many files are processed in a workspace. [#10828](https://github.com/microsoft/vscode-cpptools/issues/10828) +* Update GitHub Copilot APIs. [PR #13877](https://github.com/microsoft/vscode-cpptools/pull/13877) + * Thank you for the contribution. [@dbaeumer (Dirk Bäumer)](https://github.com/dbaeumer) + +### Bug Fixes +* Fix input delays when editing `c_cpp_properties.json`. [#13591](https://github.com/microsoft/vscode-cpptools/issues/13591) +* Fix non-recursive browse paths from configuration providers. [#13886](https://github.com/microsoft/vscode-cpptools/issues/13886) +* Fix an IntelliSense process crash involving `requires` expressions and templates. +* Fix an IntelliSense process crash with `class_has_mutable_member`. + ## Version 1.27.2: August 21, 2025 ### Bug Fix * Fix a crash regression with `__BASE_FILE__`. [#13866](https://github.com/microsoft/vscode-cpptools/issues/13866) diff --git a/Extension/package.json b/Extension/package.json index 7f3bd9c08..1bb91ae14 100644 --- a/Extension/package.json +++ b/Extension/package.json @@ -2,7 +2,7 @@ "name": "cpptools", "displayName": "C/C++", "description": "C/C++ IntelliSense, debugging, and code browsing.", - "version": "1.27.2-main", + "version": "1.27.3-main", "publisher": "ms-vscode", "icon": "LanguageCCPP_color_128x.png", "readme": "README.md",