UNPKG

@theia/cpp

Version:
412 lines (363 loc) • 15.1 kB
/******************************************************************************** * Copyright (C) 2018-2019 Ericsson and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at * http://www.eclipse.org/legal/epl-2.0. * * This Source Code may also be made available under the following Secondary * Licenses when the conditions for such availability set forth in the Eclipse * Public License v. 2.0 are satisfied: GNU General Public License, version 2 * with the GNU Classpath Exception which is available at * https://www.gnu.org/software/classpath/license.html. * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ import { injectable, inject, postConstruct } from 'inversify'; import { Emitter, Event } from '@theia/core'; import { CppPreferences } from './cpp-preferences'; import { StorageService } from '@theia/core/lib/browser/storage-service'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { CppBuildConfiguration, CppBuildConfigurationServer } from '../common/cpp-build-configuration-protocol'; import { VariableResolverService } from '@theia/variable-resolver/lib/browser'; import URI from '@theia/core/lib/common/uri'; import { deepClone } from '@theia/core'; /** * @deprecated Import from `@theia/cpp/lib/common` instead */ export { CppBuildConfiguration }; /** * Determine if the argument is a C/C++ build configuration. * * @returns `true` if the argument is a C/C++ build configuration. */ // tslint:disable-next-line:no-any export function isCppBuildConfiguration(arg: any): arg is CppBuildConfiguration { return arg.name !== undefined && arg.directory !== undefined; } /** * Determine if two C/C++ build configurations are equal. * @param a the first C/C++ build configuration. * @param b the second C/C++ build configuration. * * @returns `true` if both `a` and `b` are equal. */ export function equals(a: CppBuildConfiguration, b: CppBuildConfiguration): boolean { return ( a.name === b.name && a.directory === b.directory && a.commands === b.commands ); } /** * Representation of all saved build configurations per workspace root in local storage. */ class SavedActiveBuildConfigurations { configs: [string, CppBuildConfiguration | undefined][]; } export const CppBuildConfigurationManager = Symbol('CppBuildConfigurationManager'); /** * Representation of a C/C++ build configuration manager. */ export interface CppBuildConfigurationManager { /** * Get the list of defined build configurations. * * @returns an array of defined `CppBuildConfiguration`. */ getConfigs(root?: string): CppBuildConfiguration[]; /** * Get the list of valid defined build configurations. * * @returns an array of valid defined `CppBuildConfiguration`. * A `CppBuildConfiguration` is considered valid if it has a `name` and `directory`. */ getValidConfigs(root?: string): CppBuildConfiguration[]; /** * Get the active build configuration. * * @param root the optional workspace root. * @returns the active `CppBuildConfiguration` if it exists, else `undefined`. */ getActiveConfig(root?: string): CppBuildConfiguration | undefined; /** * Set the active build configuration. * * @param config the active `CppBuildConfiguration`. If `undefined` no active build configuration will be set. * @param root the optional workspace root. */ setActiveConfig(config: CppBuildConfiguration | undefined, root?: string): void; /** * Get the active build configurations for all roots. */ getAllActiveConfigs?(): Map<string, CppBuildConfiguration | undefined>; /** * Experimental: * * Get a filesystem path to a `compile_commands.json` file which will be the result of all * configurations merged together (provided through the `configs` parameter). * * This covers the case when `clangd` is not able to take multiple compilation database * in its initialization, so this is mostly a hack-around to still get diagnostics for all * projects and most importantly being able to cross reference project symbols. */ getMergedCompilationDatabase?(configs: { directories: string[] }): Promise<string>; /** * @deprecated use `onActiveConfigChange2` instead. * * Event emitted when the active build configuration changes. * * @returns an event with the active `CppBuildConfiguration` if it exists, else `undefined`. */ onActiveConfigChange: Event<CppBuildConfiguration | undefined>; /** * Updated `onActiveConfigChange` to support multi-root. * * @returns all the configurations to use. */ onActiveConfigChange2: Event<Map<string, CppBuildConfiguration>>; /** * Promise resolved when the list of build configurations has been read * once, and the active configuration has been set, if relevant. */ ready: Promise<void>; } export const CPP_BUILD_CONFIGURATIONS_PREFERENCE_KEY = 'cpp.buildConfigurations'; /** * Entry point to get the list of build configurations and get/set the active * build configuration. */ @injectable() export class CppBuildConfigurationManagerImpl implements CppBuildConfigurationManager { @inject(CppPreferences) protected readonly cppPreferences: CppPreferences; @inject(StorageService) protected readonly storageService: StorageService; @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; @inject(VariableResolverService) protected readonly variableResolver: VariableResolverService; @inject(CppBuildConfigurationServer) protected readonly buildConfigurationServer: CppBuildConfigurationServer; /** * Resolved configurations, coming from the preferences. */ protected resolvedConfigurations = new Map<string, CppBuildConfiguration[]>(); /** * The current active build configurations map. */ protected activeConfigurations = new Map<string, CppBuildConfiguration | undefined>(); /** * @deprecated use `activeConfigChange2Emitter` instead. * * Emitter for when the active build configuration changes. */ protected readonly activeConfigChangeEmitter = new Emitter<CppBuildConfiguration | undefined>(); /** * Emitter for when an active build configuration changes. */ protected readonly activeConfigChange2Emitter = new Emitter<Map<string, CppBuildConfiguration>>(); /** * Persistent storage key for the active build configurations map. */ readonly ACTIVE_BUILD_CONFIGURATIONS_MAP_STORAGE_KEY = 'cpp.active-build-configurations-map'; public ready: Promise<void>; /** * Initialize the manager. */ @postConstruct() async init(): Promise<void> { // Try to read the active build config from local storage. this.ready = new Promise(async resolve => { const loadActiveConfigurations = this.loadActiveConfigs(); await this.cppPreferences.ready; await Promise.all([ this.handlePreferencesUpdate(), loadActiveConfigurations, ]); this.cppPreferences.onPreferenceChanged(() => this.handlePreferencesUpdate()); resolve(); }); } /** * Get the C/C++ build configuration from the preferences. * @param root the optional workspace root. * * @returns an array of build configurations. */ protected getConfigsFromPreferences(root?: string): CppBuildConfiguration[] { if (root) { return Array.from(this.cppPreferences.get(CPP_BUILD_CONFIGURATIONS_PREFERENCE_KEY, [], root)); } return Array.from(this.cppPreferences[CPP_BUILD_CONFIGURATIONS_PREFERENCE_KEY] || []); } /** * Load the active build configuration from persistent storage. */ protected async loadActiveConfigs(): Promise<void> { const savedConfig = await this.storageService.getData<SavedActiveBuildConfigurations>( this.ACTIVE_BUILD_CONFIGURATIONS_MAP_STORAGE_KEY ); if (savedConfig !== undefined) { // read from local storage and update the map. this.activeConfigurations = new Map(savedConfig.configs); } } /** * Save the active build configuration to persistent storage. * * @param config the active `CppBuildConfiguration`. */ protected saveActiveConfigs(configs: Map<string, CppBuildConfiguration | undefined>): void { this.storageService.setData<SavedActiveBuildConfigurations>( this.ACTIVE_BUILD_CONFIGURATIONS_MAP_STORAGE_KEY, { configs: [...configs.entries()] } ); } /** * Update the active build configuration if applicable. */ protected async handlePreferencesUpdate(): Promise<void> { // tslint:disable:no-any const roots = this.workspaceService.tryGetRoots(); // Resolve variables for all configurations. await Promise.all(roots.map(async ({ uri: root }) => { const context = new URI(root); const configs = this.getConfigsFromPreferences(root); const resolvedConfigs = configs.map(config => deepClone(config)); // copy await Promise.all(resolvedConfigs.map(async config => Promise.all<any>([ this.variableResolver.resolve(config.directory, { context }) .then(resolved => config.directory = resolved), config.commands && Promise.all(Object.keys(config.commands) .map(command => this.variableResolver.resolve((config.commands as any)[command], { context }) .then(resolved => (config.commands as any)[command] = resolved) ) ), ]))); this.resolvedConfigurations.set(root, resolvedConfigs); })); // Look for missing active configurations. for (const [root, active] of this.activeConfigurations.entries()) { if (!active) { continue; } const configs = this.getValidConfigs(root); const stillExists = configs.some(config => this.equals(config, active)); if (!stillExists) { this.setActiveConfig(undefined, root); } } // tslint:enable:no-any } /** * Determine if two `CppBuildConfiguration` are equal. * * @param a `CppBuildConfiguration`. * @param b `CppBuildConfiguration`. */ protected equals(a: CppBuildConfiguration, b: CppBuildConfiguration): boolean { return a.name === b.name && a.directory === b.directory; } /** * Get the active build configuration. * @param root the optional workspace root. * * @returns the active build configuration if it exists, else `undefined`. */ getActiveConfig(root?: string): CppBuildConfiguration | undefined { // Get the active workspace root for the given uri, else for the first workspace root. const workspaceRoot = this.getRoot(root); if (!workspaceRoot) { return undefined; } return this.activeConfigurations.get(workspaceRoot); } /** * Get all active build configurations. * - If for a given root the build configuration is `undefined`, the root does not contain * an active build configuration. * * @returns the map of all active configurations if available, for each workspace root. */ getAllActiveConfigs(): Map<string, CppBuildConfiguration | undefined> { return this.activeConfigurations; } /** * Set the active build configuration. * @param config the build configuration to be set. If `undefined` there will be no active configuration. * @param root the optional workspace root. If unprovided, fallback to the first workspace root if available. */ setActiveConfig(config: CppBuildConfiguration | undefined, root?: string): void { // Set the active workspace root for the given uri, else for the first workspace root. const workspaceRoot = this.getRoot(root); if (!workspaceRoot) { return; } this.activeConfigurations.set(workspaceRoot, config); this.saveActiveConfigs(this.activeConfigurations); const activeConfigurations = new Map<string, CppBuildConfiguration>(); for (const [source, cppConfig] of this.getAllActiveConfigs()) { if (typeof cppConfig !== 'undefined') { activeConfigurations.set(source, cppConfig); } } this.activeConfigChange2Emitter.fire(activeConfigurations); this.activeConfigChangeEmitter.fire(config); } get onActiveConfigChange(): Event<CppBuildConfiguration | undefined> { return this.activeConfigChangeEmitter.event; } get onActiveConfigChange2(): Event<Map<string, CppBuildConfiguration>> { return this.activeConfigChange2Emitter.event; } /** * Get all build configurations. * @param root the optional workspace root. * * @returns an array of build configurations. */ getConfigs(root?: string): CppBuildConfiguration[] { const workspaceRoot = this.getRoot(root); if (!workspaceRoot) { return []; } let configs = this.resolvedConfigurations.get(workspaceRoot); if (!configs) { this.resolvedConfigurations.set(workspaceRoot, configs = []); } return configs; } /** * Get all valid build configurations. * @param root the optional workspace root. * * @returns an array of build configurations. */ getValidConfigs(root?: string): CppBuildConfiguration[] { return this.getConfigs(root) .filter(a => a.name !== '' && a.directory !== '') .sort((a, b) => (a.name.localeCompare(b.name))); } /** * Get the merged compilation database. */ async getMergedCompilationDatabase(params: { directories: string[] }): Promise<string> { // TODO: Optimize by caching the merge result, based on the `CppBuildConfiguration.directory` field? return this.buildConfigurationServer.getMergedCompilationDatabase(params); } /** * Get the root directory. * @param root the optional workspace root. * * @returns the root directory if it is present, else `undefined`. */ protected getRoot(root?: string): string | undefined { if (root) { return root; } const roots = this.workspaceService.tryGetRoots(); if (roots.length > 0) { return roots[0].uri; } return undefined; } }