diff --git a/Extension/package.json b/Extension/package.json index 8e4db4ca7..fd39e0a54 100644 --- a/Extension/package.json +++ b/Extension/package.json @@ -68,6 +68,13 @@ "languages": [ { "id": "cpp", + "extensions": [ + ".ccm", + ".cppm", + ".hip", + ".ixx", + ".sycl" + ], "filenames": [ "algorithm", "any", diff --git a/Extension/src/Debugger/configurationProvider.ts b/Extension/src/Debugger/configurationProvider.ts index 5bc427759..e675516f8 100644 --- a/Extension/src/Debugger/configurationProvider.ts +++ b/Extension/src/Debugger/configurationProvider.ts @@ -81,7 +81,7 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv throw new Error("Default config not found in provideDebugConfigurations()"); } const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; - if (!editor || !util.isCppOrCFile(editor.document.uri) || configs.length <= 1) { + if (!editor || !util.isCppOrCFile(editor.document.uri, editor.document.languageId) || configs.length <= 1) { return [defaultTemplateConfig]; } @@ -546,12 +546,6 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv return; } - const fileExt: string = path.extname(editor.document.fileName); - if (!fileExt) { - DebugConfigurationProvider.detectedBuildTasks = emptyTasks; - return; - } - // Don't offer tasks for header files. const isHeader: boolean = util.isHeaderFile(editor.document.uri); if (isHeader) { @@ -559,9 +553,9 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv return; } - // Don't offer tasks if the active file's extension is not a recognized C/C++ extension. - const fileIsCpp: boolean = util.isCppFile(editor.document.uri); - const fileIsC: boolean = util.isCFile(editor.document.uri); + // Don't offer tasks if the active file is not a recognized C/C++ source file. + const fileIsCpp: boolean = util.isCppFile(editor.document.uri, editor.document.languageId); + const fileIsC: boolean = util.isCFile(editor.document.uri, editor.document.languageId); if (!(fileIsCpp || fileIsC)) { DebugConfigurationProvider.detectedBuildTasks = emptyTasks; return; @@ -982,7 +976,7 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv private async selectConfiguration(textEditor: vscode.TextEditor, pickDefault: boolean = true, onlyWorkspaceFolder: boolean = false): Promise { const folder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(textEditor.document.uri); - if (!util.isCppOrCFile(textEditor.document.uri)) { + if (!util.isCppOrCFile(textEditor.document.uri, textEditor.document.languageId)) { void vscode.window.showErrorMessage(localize("cannot.build.non.cpp", 'Cannot build and debug because the active file is not a C or C++ source file.')); return; } diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index e76eaa4bf..67836f6f3 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -35,6 +35,7 @@ import { ManualPromise } from '../Utility/Async/manualPromise'; import { logAndReturn } from '../Utility/Async/returns'; import * as util from '../common'; import { isWindows } from '../constants'; +import { FileTypeMappings, hasNativeFileTypeMappings, isTagParsableFile, resetFileTypeMappings, updateFileTypeMappings } from '../fileType'; import { instrument, isInstrumentationEnabled } from '../instrumentation'; import { DebugProtocolParams, Logger, ShowWarningParams, getDiagnosticsChannel, getOutputChannelLogger, logDebugProtocol, logLocalized, showWarning } from '../logger'; import { localizedStringCount, lookupString } from '../nativeStrings'; @@ -528,6 +529,7 @@ interface CppInitializationParams { interface CppInitializationResult { shouldShutdown: boolean; + fileTypeMappings?: FileTypeMappings; } interface TagParseStatus { @@ -683,6 +685,7 @@ const ReportStatusNotification: NotificationType = const DebugProtocolNotification: NotificationType = new NotificationType('cpptools/debugProtocol'); const DebugLogNotification: NotificationType = new NotificationType('cpptools/debugLog'); const CompileCommandsPathsNotification: NotificationType = new NotificationType('cpptools/compileCommandsPaths'); +const FileTypeMappingsNotification: NotificationType = new NotificationType('cpptools/fileTypeMappings'); const ReferencesNotification: NotificationType = new NotificationType('cpptools/references'); const ReportReferencesProgressNotification: NotificationType = new NotificationType('cpptools/reportReferencesProgress'); const RequestCustomConfigs: NotificationType = new NotificationType('cpptools/requestCustomConfigs'); @@ -1406,6 +1409,7 @@ export class DefaultClient implements Client { // Ideally this would be set earlier, but the task provider expects it to also mean that `this.innerConfiguration` is set. this.languageClient.isStarted = true; + clients.ActiveClient.updateActiveDocumentTextOptions(); telemetry.logLanguageServerEvent("NonDefaultInitialCppSettings", this.settingsTracker.getUserModifiedSettings()); failureMessageShown = false; @@ -1732,6 +1736,7 @@ export class DefaultClient implements Client { localizedStrings: localizedStrings, settings: this.getAllSettings() }; + resetFileTypeMappings(); this.loggingLevel = util.getNumericLoggingLevel(cppInitializationParams.settings.loggingLevel); const lspInitializationOptions: LspInitializationOptions = { @@ -1843,6 +1848,12 @@ export class DefaultClient implements Client { // A request is used in order to wait for completion and ensure that no subsequent // higher priority message may be processed before the Initialization request. const initializeResult = await client.sendRequest(InitializationRequest, cppInitializationParams); + if (initializeResult.fileTypeMappings) { + updateFileTypeMappings(initializeResult.fileTypeMappings); + } else { + resetFileTypeMappings(); + } + DebugConfigurationProvider.ClearDetectedBuildTasks(); // If the server requested shutdown, then reload with the failsafe (null) client. if (initializeResult.shouldShutdown) { @@ -2589,6 +2600,11 @@ export class DefaultClient implements Client { this.languageClient.onNotification(ReportStatusNotification, (e) => void this.updateStatus(e)); this.languageClient.onNotification(ReportTagParseStatusNotification, (e) => this.updateTagParseStatus(e)); this.languageClient.onNotification(CompileCommandsPathsNotification, (e) => void this.promptCompileCommands(e)); + this.languageClient.onNotification(FileTypeMappingsNotification, (mappings) => { + updateFileTypeMappings(mappings); + DebugConfigurationProvider.ClearDetectedBuildTasks(); + clients.ActiveClient.updateActiveDocumentTextOptions(); + }); this.languageClient.onNotification(ReferencesNotification, (e) => this.processReferencesPreview(e)); this.languageClient.onNotification(ReportReferencesProgressNotification, (e) => this.handleReferencesProgress(e)); this.languageClient.onNotification(RequestCustomConfigs, (e) => this.handleRequestCustomConfigs(e)); @@ -2718,14 +2734,14 @@ export class DefaultClient implements Client { void this.languageClient.sendNotification(FileCreatedNotification, { uri: uri.toString() }).catch(logAndReturn.undefined); }); - // TODO: Handle new associations without a reload. - this.associations_for_did_change = new Set(["cu", "cuh", "c", "i", "cpp", "cc", "cxx", "c++", "cp", "hpp", "hh", "hxx", "h++", "hp", "h", "ii", "ino", "inl", "ipp", "tcc", "txx", "tpp", "idl"]); + // Fallback for custom associations when native binaries do not publish effective file type mappings. + this.associations_for_did_change = new Set(); const assocs: any = new OtherSettings().filesAssociations; for (const assoc in assocs) { const dotIndex: number = assoc.lastIndexOf('.'); if (dotIndex !== -1) { const ext: string = assoc.substring(dotIndex + 1); - this.associations_for_did_change.add(ext); + this.associations_for_did_change.add(ext.toLowerCase()); } } this.rootPathFileWatcher.onDidChange(async (uri) => { @@ -2739,17 +2755,19 @@ export class DefaultClient implements Client { cachedEditorConfigLookups.clear(); this.updateActiveDocumentTextOptions(); } - if (dotIndex !== -1) { - const ext: string = uri.fsPath.substring(dotIndex + 1); - if (this.associations_for_did_change?.has(ext)) { - // VS Code has a bug that causes onDidChange events to happen to files that aren't changed, - // which causes a large backlog of "files to parse" to accumulate. - // We workaround this via only sending the change message if the modified time is within 10 seconds. - const mtime: Date = fs.statSync(uri.fsPath).mtime; - const duration: number = Date.now() - mtime.getTime(); - if (duration < 10000) { - void this.languageClient.sendNotification(FileChangedNotification, { uri: uri.toString() }).catch(logAndReturn.undefined); - } + const ext: string | undefined = dotIndex !== -1 ? uri.fsPath.substring(dotIndex + 1) : undefined; + const isTrackedFile: boolean = hasNativeFileTypeMappings() + ? isTagParsableFile(uri.fsPath) + : isTagParsableFile(uri.fsPath) || + (ext !== undefined && this.associations_for_did_change?.has(ext.toLowerCase()) === true); + if (isTrackedFile) { + // VS Code has a bug that causes onDidChange events to happen to files that aren't changed, + // which causes a large backlog of "files to parse" to accumulate. + // We workaround this via only sending the change message if the modified time is within 10 seconds. + const mtime: Date = fs.statSync(uri.fsPath).mtime; + const duration: number = Date.now() - mtime.getTime(); + if (duration < 10000) { + void this.languageClient.sendNotification(FileChangedNotification, { uri: uri.toString() }).catch(logAndReturn.undefined); } } }); @@ -3079,7 +3097,7 @@ export class DefaultClient implements Client { public updateActiveDocumentTextOptions(): void { const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; if (editor && util.isCpp(editor.document)) { - void SessionState.buildAndDebugIsSourceFile.set(util.isCppOrCFile(editor.document.uri)); + void SessionState.buildAndDebugIsSourceFile.set(util.isCppOrCFile(editor.document.uri, editor.document.languageId)); void SessionState.buildAndDebugIsFolderOpen.set(util.isFolderOpen(editor.document.uri)); // If using vcFormat, check for a ".editorconfig" file, and apply those text options to the active document. const settings: CppSettings = new CppSettings(this.RootUri); diff --git a/Extension/src/LanguageServer/cppBuildTaskProvider.ts b/Extension/src/LanguageServer/cppBuildTaskProvider.ts index 486e3df64..8112602f9 100644 --- a/Extension/src/LanguageServer/cppBuildTaskProvider.ts +++ b/Extension/src/LanguageServer/cppBuildTaskProvider.ts @@ -73,7 +73,7 @@ export class CppBuildTaskProvider implements TaskProvider { return _task; } - // Generate tasks to build the current file based on the user's detected compilers, the user's compilerPath setting, and the current file's extension. + // Generate tasks to build the current file based on the user's detected compilers, compilerPath setting, and file type. public async getTasks(appendSourceToName: boolean = false): Promise { const editor: TextEditor | undefined = window.activeTextEditor; const emptyTasks: CppBuildTask[] = []; @@ -81,20 +81,15 @@ export class CppBuildTaskProvider implements TaskProvider { return emptyTasks; } - const fileExt: string = path.extname(editor.document.fileName); - if (!fileExt) { - return emptyTasks; - } - // Don't offer tasks for header files. const isHeader: boolean = util.isHeaderFile(editor.document.uri); if (isHeader) { return emptyTasks; } - // Don't offer tasks if the active file's extension is not a recognized C/C++ extension. - const fileIsCpp: boolean = util.isCppFile(editor.document.uri); - const fileIsC: boolean = util.isCFile(editor.document.uri); + // Don't offer tasks if the active file is not a recognized C/C++ source file. + const fileIsCpp: boolean = util.isCppFile(editor.document.uri, editor.document.languageId); + const fileIsC: boolean = util.isCFile(editor.document.uri, editor.document.languageId); if (!(fileIsCpp || fileIsC)) { return emptyTasks; } @@ -421,7 +416,9 @@ class CustomBuildTaskTerminal implements Pseudoterminal { } async openAsync(_initialDimensions: TerminalDimensions | undefined): Promise { - if (this.buildOptions.taskUsesActiveFile && !util.isCppOrCFile(window.activeTextEditor?.document.uri)) { + if (this.buildOptions.taskUsesActiveFile && !util.isCppOrCFile( + window.activeTextEditor?.document.uri, + window.activeTextEditor?.document.languageId)) { this.writeEmitter.fire(localize("cannot.build.non.cpp", 'Cannot build and debug because the active file is not a C or C++ source file.') + this.endOfLine); this.closeEmitter.fire(-1); return; diff --git a/Extension/src/common.ts b/Extension/src/common.ts index 7bfda0ec8..700d8ab44 100644 --- a/Extension/src/common.ts +++ b/Extension/src/common.ts @@ -16,6 +16,7 @@ import * as nls from 'vscode-nls'; import { TargetPopulation } from 'vscode-tas-client'; import { ManualPromise } from './Utility/Async/manualPromise'; import { isWindows } from './constants'; +import { classifyFilePath, FileTypeMapping } from './fileType'; import { getOutputChannelLogger, showOutputChannel } from './logger'; import { PlatformInformation } from './platform'; import * as Telemetry from './telemetry'; @@ -157,34 +158,26 @@ export function getVcpkgRoot(): string { return vcpkgRoot; } -/** - * This is a fuzzy determination of whether a uri represents a header file. - * For the purposes of this function, a header file has no extension, or an extension that begins with the letter 'h'. - * @param document The document to check. - */ export function isHeaderFile(uri: vscode.Uri): boolean { - const fileExt: string = path.extname(uri.fsPath); - const fileExtLower: string = fileExt.toLowerCase(); - return !fileExt || [".cuh", ".hpp", ".hh", ".hxx", ".h++", ".hp", ".h", ".inl", ".ipp", ".tcc", ".txx", ".tpp", ".tlh", ".tli", ""].some(ext => fileExtLower === ext); + return classifyFilePath(uri.fsPath)?.kind === 'header'; } -export function isCppFile(uri: vscode.Uri): boolean { - const fileExt: string = path.extname(uri.fsPath); - const fileExtLower: string = fileExt.toLowerCase(); - return (fileExt === ".C") || [".cu", ".cpp", ".cc", ".cxx", ".c++", ".cp", ".ii", ".ino"].some(ext => fileExtLower === ext); +export function isCppFile(uri: vscode.Uri, languageId?: string): boolean { + const fileType: FileTypeMapping | undefined = classifyFilePath(uri.fsPath, languageId); + return fileType?.kind === 'source' && (fileType.language === 'cpp' || fileType.language === 'cuda'); } -export function isCFile(uri: vscode.Uri): boolean { - const fileExt: string = path.extname(uri.fsPath); - const fileExtLower: string = fileExt.toLowerCase(); - return fileExt === ".c" || fileExtLower === ".i"; +export function isCFile(uri: vscode.Uri, languageId?: string): boolean { + const fileType: FileTypeMapping | undefined = classifyFilePath(uri.fsPath, languageId); + return fileType?.kind === 'source' && fileType.language === 'c'; } -export function isCppOrCFile(uri: vscode.Uri | undefined): boolean { +export function isCppOrCFile(uri: vscode.Uri | undefined, languageId?: string): boolean { if (!uri) { return false; } - return isCppFile(uri) || isCFile(uri); + const fileType: FileTypeMapping | undefined = classifyFilePath(uri.fsPath, languageId); + return fileType?.kind === 'source' && fileType.language !== undefined; } export function isFolderOpen(uri: vscode.Uri): boolean { diff --git a/Extension/src/fileType.ts b/Extension/src/fileType.ts new file mode 100644 index 000000000..4712511ed --- /dev/null +++ b/Extension/src/fileType.ts @@ -0,0 +1,110 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import * as path from 'path'; + +export type FileTypeKind = 'source' | 'header' | 'idl' | 'resource' | 'other'; +export type FileTypeLanguage = 'c' | 'cpp' | 'cuda'; + +export interface FileTypeMapping { + name: string; + kind: FileTypeKind; + language?: FileTypeLanguage; +} + +export interface FileTypeMappings { + extensions: FileTypeMapping[]; + filenames: FileTypeMapping[]; +} + +const bootstrapMappings: FileTypeMappings = { + extensions: [ + ...['.cuh', '.hpp', '.hh', '.hxx', '.h++', '.hp', '.h', '.inl', '.ipp', '.tcc', '.txx', '.tpp', '.tlh', '.tli'] + .map(name => ({ name, kind: 'header' as const, language: name === '.cuh' ? 'cuda' as const : 'cpp' as const })), + ...['.cu', '.cpp', '.cc', '.ccm', '.cxx', '.c++', '.cp', '.cppm', '.hip', '.ii', '.ino', '.ixx', '.sycl'] + .map(name => ({ name, kind: 'source' as const, language: name === '.cu' ? 'cuda' as const : 'cpp' as const })), + ...['.c', '.i'].map(name => ({ name, kind: 'source' as const, language: 'c' as const })), + { name: '.idl', kind: 'idl' } + ], + filenames: [] +}; + +let extensionMappings: ReadonlyMap; +let filenameMappings: ReadonlyMap; +let nativeMappingsAvailable: boolean = false; + +function createMappingMap(mappings: FileTypeMapping[]): ReadonlyMap { + const result: Map = new Map(); + for (const mapping of mappings) { + // VS Code matches file associations case-insensitively on every platform. + result.set(mapping.name.toLowerCase(), { ...mapping }); + } + return result; +} + +export function resetFileTypeMappings(): void { + extensionMappings = createMappingMap(bootstrapMappings.extensions); + filenameMappings = createMappingMap(bootstrapMappings.filenames); + nativeMappingsAvailable = false; +} + +export function updateFileTypeMappings(mappings: FileTypeMappings | undefined): void { + if (!mappings) { + resetFileTypeMappings(); + return; + } + + extensionMappings = createMappingMap(mappings.extensions); + filenameMappings = createMappingMap(mappings.filenames); + nativeMappingsAvailable = true; +} + +export function hasNativeFileTypeMappings(): boolean { + return nativeMappingsAvailable; +} + +function getRegisteredFileType(filePath: string): FileTypeMapping | undefined { + const filename: string = path.basename(filePath); + const filenameMapping: FileTypeMapping | undefined = filenameMappings.get(filename.toLowerCase()); + if (filenameMapping) { + return filenameMapping; + } + + const extension: string = path.extname(filename); + // Uppercase .C is an exact, case-sensitive C++ association by convention. + if (extension === '.C') { + return { name: extension, kind: 'source', language: 'cpp' }; + } + return extensionMappings.get(extension.toLowerCase()); +} + +export function classifyFilePath(filePath: string, languageId?: string): FileTypeMapping | undefined { + const registeredType: FileTypeMapping | undefined = getRegisteredFileType(filePath); + if (registeredType) { + return registeredType; + } + + if (!nativeMappingsAvailable && !path.extname(filePath)) { + return { name: '', kind: 'header' }; + } + + switch (languageId) { + case 'c': + return { name: '', kind: 'source', language: 'c' }; + case 'cpp': + return { name: '', kind: 'source', language: 'cpp' }; + case 'cuda-cpp': + return { name: '', kind: 'source', language: 'cuda' }; + default: + return undefined; + } +} + +export function isTagParsableFile(filePath: string): boolean { + const type: FileTypeMapping | undefined = getRegisteredFileType(filePath); + return type?.kind === 'source' || type?.kind === 'header' || type?.kind === 'idl'; +} + +resetFileTypeMappings(); diff --git a/Extension/test/unit/fileType.test.ts b/Extension/test/unit/fileType.test.ts new file mode 100644 index 000000000..84c122b83 --- /dev/null +++ b/Extension/test/unit/fileType.test.ts @@ -0,0 +1,76 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import { deepStrictEqual, equal } from 'node:assert'; +import { afterEach, describe, it } from 'mocha'; +import { + classifyFilePath, + hasNativeFileTypeMappings, + isTagParsableFile, + resetFileTypeMappings, + updateFileTypeMappings +} from '../../src/fileType'; + +describe('file type mappings', () => { + afterEach(() => resetFileTypeMappings()); + + it('uses case-insensitive legacy classifications before native initialization', () => { + equal(hasNativeFileTypeMappings(), false); + deepStrictEqual(classifyFilePath('file.HPP'), { name: '.hpp', kind: 'header', language: 'cpp' }); + deepStrictEqual(classifyFilePath('file.C'), { name: '.C', kind: 'source', language: 'cpp' }); + deepStrictEqual(classifyFilePath('file.c'), { name: '.c', kind: 'source', language: 'c' }); + for (const extension of ['CCM', 'CPPM', 'HIP', 'IXX', 'SYCL']) { + deepStrictEqual(classifyFilePath(`file.${extension}`), { name: `.${extension.toLowerCase()}`, kind: 'source', language: 'cpp' }); + equal(isTagParsableFile(`file.${extension}`), true); + } + deepStrictEqual(classifyFilePath('Makefile'), { name: '', kind: 'header' }); + }); + + it('atomically replaces bootstrap mappings with native mappings', () => { + updateFileTypeMappings({ + extensions: [ + { name: '.c', kind: 'source', language: 'c' }, + { name: '.cppm', kind: 'source', language: 'cpp' }, + { name: '.h', kind: 'header', language: 'cpp' }, + { name: '.hpp', kind: 'header', language: 'cpp' }, + { name: '.idl', kind: 'idl' } + ], + filenames: [ + { name: 'foo.h', kind: 'source', language: 'c' }, + { name: 'build', kind: 'source', language: 'cpp' }, + { name: 'vector', kind: 'header' }, + { name: 'kernel.custom', kind: 'source', language: 'cuda' } + ] + }); + + equal(hasNativeFileTypeMappings(), true); + deepStrictEqual(classifyFilePath('module.CPPM', 'cpp'), { name: '.cppm', kind: 'source', language: 'cpp' }); + deepStrictEqual(classifyFilePath('header.HPP', 'cpp'), { name: '.hpp', kind: 'header', language: 'cpp' }); + deepStrictEqual(classifyFilePath('file.C'), { name: '.C', kind: 'source', language: 'cpp' }); + deepStrictEqual(classifyFilePath('file.c'), { name: '.c', kind: 'source', language: 'c' }); + deepStrictEqual(classifyFilePath('FOO.H', 'cpp'), { name: 'foo.h', kind: 'source', language: 'c' }); + deepStrictEqual(classifyFilePath('build'), { name: 'build', kind: 'source', language: 'cpp' }); + deepStrictEqual(classifyFilePath('VECTOR', 'cpp'), { name: 'vector', kind: 'header' }); + deepStrictEqual(classifyFilePath('kernel.custom'), { name: 'kernel.custom', kind: 'source', language: 'cuda' }); + equal(classifyFilePath('Makefile'), undefined); + equal(isTagParsableFile('schema.idl'), true); + equal(isTagParsableFile('kernel.custom'), true); + equal(isTagParsableFile('build'), true); + equal(isTagParsableFile('vector'), true); + equal(isTagParsableFile('unknown.txt'), false); + }); + + it('uses the editor language only for unregistered paths', () => { + updateFileTypeMappings({ + extensions: [{ name: '.h', kind: 'header', language: 'cpp' }], + filenames: [] + }); + + deepStrictEqual(classifyFilePath('file.h', 'c'), { name: '.h', kind: 'header', language: 'cpp' }); + deepStrictEqual(classifyFilePath('file.special', 'c'), { name: '', kind: 'source', language: 'c' }); + deepStrictEqual(classifyFilePath('file.special', 'cuda-cpp'), { name: '', kind: 'source', language: 'cuda' }); + deepStrictEqual(classifyFilePath('extensionless', 'cpp'), { name: '', kind: 'source', language: 'cpp' }); + }); +});