From 41fc2c42cffa5fca1983aa1fd3dc188b6450526b Mon Sep 17 00:00:00 2001 From: Vincent Fugnitto Date: Mon, 18 Mar 2019 11:53:44 -0400 Subject: [PATCH] Support multiple root cpp build configurations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added support for multiple root cpp build configurations. Instead of maintaining a single active configuration, a map is used to maintain the relationship between the workspace root and its given `CppBuildConfiguration`. This feature is an experimental PR based on new developments in clangd to support multiple compilation databases. By default, when selecting multiple build configurations, the extension will create a merged compilation database. Added a preference to support an experimental clangd feature (that might either change or be removed) which would not generate this aggregated database, but rather just notify clangd of all the databases the user wants to use. Until this is officially supported you shouldn't set this experimental preference, unless for testing purposes. - Update the `cpp` manager to handle the new data structure. - CppBuildConfigurationManager creates a merged compilation database when multiple configs are used. - Update the test cases (fix the task test case with incorrect imports and false positive results). Signed-off-by: Vincent Fugnitto Signed-off-by: Paul Maréchal --- packages/cpp/package.json | 2 + ...-build-configurations-statusbar-element.ts | 23 +- .../browser/cpp-build-configurations-ui.ts | 165 +++++++----- .../browser/cpp-build-configurations.spec.ts | 92 +++++-- .../src/browser/cpp-build-configurations.ts | 242 +++++++++++++----- .../cpp/src/browser/cpp-frontend-module.ts | 7 +- .../cpp-language-client-contribution.ts | 72 +++++- packages/cpp/src/browser/cpp-preferences.ts | 11 +- .../cpp/src/browser/cpp-task-provider.spec.ts | 14 +- packages/cpp/src/browser/cpp-task-provider.ts | 3 +- .../cpp-build-configuration-protocol.ts | 69 +++++ packages/cpp/src/node/cpp-backend-module.ts | 14 + .../node/cpp-build-configuration-server.ts | 69 +++++ 13 files changed, 600 insertions(+), 183 deletions(-) create mode 100644 packages/cpp/src/common/cpp-build-configuration-protocol.ts create mode 100644 packages/cpp/src/node/cpp-build-configuration-server.ts diff --git a/packages/cpp/package.json b/packages/cpp/package.json index 5a848469366c2..26a8b2e0c236e 100644 --- a/packages/cpp/package.json +++ b/packages/cpp/package.json @@ -11,6 +11,8 @@ "@theia/preferences": "^0.9.0", "@theia/process": "^0.9.0", "@theia/task": "^0.9.0", + "@theia/workspace": "^0.9.0", + "@theia/variable-resolver": "0.9.0", "string-argv": "^0.1.1" }, "publishConfig": { diff --git a/packages/cpp/src/browser/cpp-build-configurations-statusbar-element.ts b/packages/cpp/src/browser/cpp-build-configurations-statusbar-element.ts index fbd8e6c70ad8d..15744e2437ee8 100644 --- a/packages/cpp/src/browser/cpp-build-configurations-statusbar-element.ts +++ b/packages/cpp/src/browser/cpp-build-configurations-statusbar-element.ts @@ -18,6 +18,7 @@ import { injectable, inject } from 'inversify'; import { StatusBar, StatusBarAlignment } from '@theia/core/lib/browser'; import { CppBuildConfigurationManager, CppBuildConfiguration } from './cpp-build-configurations'; import { CPP_CHANGE_BUILD_CONFIGURATION } from './cpp-build-configurations-ui'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; @injectable() export class CppBuildConfigurationsStatusBarElement { @@ -28,6 +29,9 @@ export class CppBuildConfigurationsStatusBarElement { @inject(StatusBar) protected readonly statusBar: StatusBar; + @inject(WorkspaceService) + protected readonly workspaceService: WorkspaceService; + protected readonly cppIdentifier = 'cpp-configurator'; /** @@ -35,8 +39,8 @@ export class CppBuildConfigurationsStatusBarElement { * and listen to changes to the active build configuration. */ show(): void { - this.setCppBuildConfigElement(this.cppManager.getActiveConfig()); - this.cppManager.onActiveConfigChange(config => this.setCppBuildConfigElement(config)); + this.setCppBuildConfigElement(this.getValidActiveCount()); + this.cppManager.onActiveConfigChange2(configs => this.setCppBuildConfigElement(configs.size)); } /** @@ -45,9 +49,9 @@ export class CppBuildConfigurationsStatusBarElement { * * @param config the active `CppBuildConfiguration`. */ - protected setCppBuildConfigElement(config: CppBuildConfiguration | undefined): void { + protected setCppBuildConfigElement(count: number): void { this.statusBar.setElement(this.cppIdentifier, { - text: `$(wrench) C/C++ ${config ? '(' + config.name + ')' : 'Build Config'}`, + text: `$(wrench) C/C++ Build Config (${count} of ${this.workspaceService.tryGetRoots().length})`, tooltip: 'C/C++ Build Config', alignment: StatusBarAlignment.RIGHT, command: CPP_CHANGE_BUILD_CONFIGURATION.id, @@ -55,4 +59,15 @@ export class CppBuildConfigurationsStatusBarElement { }); } + /** + * Get the valid active configuration count. + */ + protected getValidActiveCount(): number { + let items: (CppBuildConfiguration | undefined)[] = []; + if (this.cppManager.getAllActiveConfigs) { + items = [...this.cppManager.getAllActiveConfigs().values()].filter(config => !!config); + } + return items.length; + } + } diff --git a/packages/cpp/src/browser/cpp-build-configurations-ui.ts b/packages/cpp/src/browser/cpp-build-configurations-ui.ts index ff4dc05ec7a26..656f0c1810b63 100644 --- a/packages/cpp/src/browser/cpp-build-configurations-ui.ts +++ b/packages/cpp/src/browser/cpp-build-configurations-ui.ts @@ -17,16 +17,18 @@ import { Command, CommandContribution, CommandRegistry, CommandService } from '@theia/core'; import { injectable, inject } from 'inversify'; import { QuickOpenService } from '@theia/core/lib/browser/quick-open/quick-open-service'; -import { QuickOpenModel, QuickOpenItem, QuickOpenMode, } from '@theia/core/lib/browser/quick-open/quick-open-model'; -import { FileSystem, FileSystemUtils } from '@theia/filesystem/lib/common'; +import { FileSystem } from '@theia/filesystem/lib/common'; import URI from '@theia/core/lib/common/uri'; import { PreferenceScope, PreferenceService } from '@theia/preferences/lib/browser'; -import { CppBuildConfigurationManager, CppBuildConfiguration, CPP_BUILD_CONFIGURATIONS_PREFERENCE_KEY } from './cpp-build-configurations'; +import { CppBuildConfigurationManager, CPP_BUILD_CONFIGURATIONS_PREFERENCE_KEY, isCppBuildConfiguration, equals } from './cpp-build-configurations'; import { EditorManager } from '@theia/editor/lib/browser'; -import { CommonCommands } from '@theia/core/lib/browser'; +import { CommonCommands, LabelProvider } from '@theia/core/lib/browser'; +import { QuickPickService, QuickPickItem } from '@theia/core/lib/common/quick-pick-service'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; +import { CppBuildConfiguration } from '../common/cpp-build-configuration-protocol'; @injectable() -export class CppBuildConfigurationChanger implements QuickOpenModel { +export class CppBuildConfigurationChanger { @inject(CommandService) protected readonly commandService: CommandService; @@ -40,89 +42,119 @@ export class CppBuildConfigurationChanger implements QuickOpenModel { @inject(FileSystem) protected readonly fileSystem: FileSystem; + @inject(LabelProvider) + protected readonly labelProvider: LabelProvider; + + @inject(QuickPickService) + protected readonly quickPick: QuickPickService; + @inject(QuickOpenService) protected readonly quickOpenService: QuickOpenService; @inject(PreferenceService) protected readonly preferenceService: PreferenceService; - readonly createItem: QuickOpenItem = new QuickOpenItem({ + @inject(WorkspaceService) + protected readonly workspaceService: WorkspaceService; + + /** + * Item used to trigger creation of a new build configuration. + */ + protected readonly createItem: QuickPickItem<'createNew'> = ({ label: 'Create New', - iconClass: 'fa fa-plus', + value: 'createNew', description: 'Create a new build configuration', - run: (mode: QuickOpenMode): boolean => { - if (mode !== QuickOpenMode.OPEN) { - return false; - } - this.commandService.executeCommand(CPP_CREATE_NEW_BUILD_CONFIGURATION.id); - return true; - }, + iconClass: 'fa fa-plus' }); - readonly resetItem: QuickOpenItem = new QuickOpenItem({ + /** + * Item used to trigger reset of the active build configuration. + */ + protected readonly resetItem: QuickPickItem<'reset'> = ({ label: 'None', - iconClass: 'fa fa-times', - description: 'Reset active build configuration', - run: (mode: QuickOpenMode): boolean => { - if (mode !== QuickOpenMode.OPEN) { - return false; - } - this.commandService.executeCommand(CPP_RESET_BUILD_CONFIGURATION.id); - return true; - }, + value: 'reset', + description: 'Reset the active build configuration', + iconClass: 'fa fa-times' }); - async onType(lookFor: string, acceptor: (items: QuickOpenItem[]) => void): Promise { - const items: QuickOpenItem[] = []; - const active: CppBuildConfiguration | undefined = this.cppBuildConfigurations.getActiveConfig(); - const configurations = this.cppBuildConfigurations.getValidConfigs(); - - const homeStat = await this.fileSystem.getCurrentUserHome(); - const home = (homeStat) ? new URI(homeStat.uri).path.toString() : undefined; - - // Item to create a new build configuration - items.push(this.createItem); + /** + * Change the build configuration for a given root. + * If multiple roots are available, prompt users a first time to select their desired root. + * Once a root is determined, prompt users to select an active build configuration if applicable. + */ + async change(): Promise { - // Only return 'Create New' when no build configurations present - if (!configurations.length) { - acceptor(items); + // Prompt users to determine working root. + const root = await this.selectWorkspaceRoot(); + if (!root) { return; } - // Item to de-select any active build config - if (active) { - items.push(this.resetItem); + // Prompt users to determine action (set active config, reset active config, create new config). + const action = await this.selectCppAction(root); + if (!action) { + return; } - // Add one item per build config - configurations.forEach(config => { - const uri = new URI(config.directory); - items.push(new QuickOpenItem({ - label: config.name, - // add an icon for active build config, and an empty placeholder for all others - iconClass: (config === active) ? 'fa fa-check' : 'fa fa-empty-item', - description: (home) ? FileSystemUtils.tildifyPath(uri.path.toString(), home) : uri.path.toString(), - run: (mode: QuickOpenMode): boolean => { - if (mode !== QuickOpenMode.OPEN) { - return false; - } - - this.cppBuildConfigurations.setActiveConfig(config); - return true; - }, - })); - }); + // Perform desired action. + if (action === 'createNew') { + this.commandService.executeCommand(CPP_CREATE_NEW_BUILD_CONFIGURATION.id); + } + if (action === 'reset') { + this.cppBuildConfigurations.setActiveConfig(undefined, root); + } + if (action && isCppBuildConfiguration(action)) { + this.cppBuildConfigurations.setActiveConfig(action, root); + } + } - acceptor(items); + /** + * Pick a workspace root using the quick open menu. + */ + protected async selectWorkspaceRoot(): Promise { + const roots = this.workspaceService.tryGetRoots(); + return this.quickPick.show(roots.map(({ uri: root }) => { + const active = this.cppBuildConfigurations.getActiveConfig(root); + return { + // See: WorkspaceUriLabelProviderContribution + // It will transform the path to a prettier display (adding a ~, etc). + label: this.labelProvider.getName(new URI(root).withScheme('file')), + description: active ? active.name : 'undefined', + value: root, + }; + }), { placeholder: 'Select workspace root' }); } - open() { - const configs = this.cppBuildConfigurations.getValidConfigs(); - this.quickOpenService.open(this, { - placeholder: (configs.length) ? 'Choose a build configuration...' : 'No build configurations present', - fuzzyMatchLabel: true, - fuzzyMatchDescription: true, + /** + * Lists the different options for a given root if specified, first else. + * In this case, the options are to set/unset/create a build configuration. + * + * @param root + */ + protected async selectCppAction(root: string | undefined): Promise { + const items: QuickPickItem<'createNew' | 'reset' | CppBuildConfiguration>[] = []; + // Add the 'Create New' item at all times. + items.push(this.createItem); + // Add the 'Reset' item if there currently is an active config. + if (this.cppBuildConfigurations.getActiveConfig(root)) { + items.push(this.resetItem); + } + // Display all valid configurations for a given root. + const configs = this.cppBuildConfigurations.getValidConfigs(root); + const active = this.cppBuildConfigurations.getActiveConfig(root); + configs.map(config => { + items.push({ + label: config.name, + description: config.directory, + iconClass: active && equals(config, active) ? 'fa fa-check' : 'fa fa-empty-item', + value: { + name: config.name, + directory: config.directory, + commands: config.commands + }, + }); }); + return this.quickPick.show(items, { placeholder: 'Select action' }); } /** Create a new build configuration with placeholder values. */ @@ -132,7 +164,6 @@ export class CppBuildConfigurationChanger implements QuickOpenModel { configs.push({ name: '', directory: '' }); await this.preferenceService.set(CPP_BUILD_CONFIGURATIONS_PREFERENCE_KEY, configs, PreferenceScope.Workspace); } - } export const CPP_CATEGORY = 'C/C++'; @@ -184,7 +215,7 @@ export class CppBuildConfigurationsContributions implements CommandContribution execute: () => this.cppChangeBuildConfiguration.createConfig() }); commands.registerCommand(CPP_CHANGE_BUILD_CONFIGURATION, { - execute: () => this.cppChangeBuildConfiguration.open() + execute: () => this.cppChangeBuildConfiguration.change() }); } } diff --git a/packages/cpp/src/browser/cpp-build-configurations.spec.ts b/packages/cpp/src/browser/cpp-build-configurations.spec.ts index 3ad288af72aba..47456c1fe5978 100644 --- a/packages/cpp/src/browser/cpp-build-configurations.spec.ts +++ b/packages/cpp/src/browser/cpp-build-configurations.spec.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2018 Ericsson and others. + * 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 @@ -19,22 +19,34 @@ let disableJSDOM = enableJSDOM(); import { ContainerModule, Container } from 'inversify'; import { expect } from 'chai'; -import { FileSystem } from '@theia/filesystem/lib/common'; +import { FileSystem, FileStat } from '@theia/filesystem/lib/common'; import { StorageService } from '@theia/core/lib/browser/storage-service'; import { MockStorageService } from '@theia/core/lib/browser/test/mock-storage-service'; -import sinon = require('sinon'); -import { CppBuildConfigurationManager, CppBuildConfiguration, CppBuildConfigurationManagerImpl } from './cpp-build-configurations'; +import { CppBuildConfigurationManager, CppBuildConfigurationManagerImpl } from './cpp-build-configurations'; import { FileSystemNode } from '@theia/filesystem/lib/node/node-filesystem'; import { bindCppPreferences } from './cpp-preferences'; import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service'; import { MockPreferenceService } from '@theia/core/lib/browser/preferences/test/mock-preference-service'; import { TaskDefinitionRegistry } from '@theia/task/lib/browser'; +import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; +import { CppBuildConfiguration, CppBuildConfigurationServer, MockCppBuildConfigurationServer } from '../common/cpp-build-configuration-protocol'; +import { VariableResolverService, VariableRegistry, Variable } from '@theia/variable-resolver/lib/browser'; + +let container: Container; +let variableValues: { [name: string]: string | undefined }; disableJSDOM(); -let container: Container; +before(() => { + disableJSDOM = enableJSDOM(); +}); +after(() => { + disableJSDOM(); +}); beforeEach(function () { + variableValues = {}; + const m = new ContainerModule(bind => { bind(CppBuildConfigurationManager).to(CppBuildConfigurationManagerImpl).inSingletonScope(); bind(StorageService).to(MockStorageService).inSingletonScope(); @@ -42,39 +54,67 @@ beforeEach(function () { bind(TaskDefinitionRegistry).toSelf().inSingletonScope(); bindCppPreferences(bind); bind(PreferenceService).to(MockPreferenceService).inSingletonScope(); + bind(CppBuildConfigurationServer).to(MockCppBuildConfigurationServer).inSingletonScope(); + bind(WorkspaceService).toConstantValue({ + tryGetRoots: () => [{ uri: '/tmp' }] as FileStat[], + }); + bind(VariableResolverService).toSelf(); + bind(VariableRegistry).toConstantValue({ + getVariable(name: string): Variable | undefined { + return name in variableValues ? { + name, resolve: () => variableValues[name], + } : undefined; + }, + getVariables(): Variable[] { + return Object.keys(variableValues).map(name => ({ + name, resolve: () => variableValues[name], + })); + }, + dispose() { }, + }); }); container = new Container(); container.load(m); }); +/** + * Get an instance of the `CppBuildConfigurationManager`. + */ +function getManager(): CppBuildConfigurationManager { + return container.get(CppBuildConfigurationManager); +} + /** * Create the .theia/builds.json file with `buildsJsonContent` as its content * and create/return an instance of the build configuration service. If * `buildsJsonContent` is undefined, don't create .theia/builds.json. - * If `activeBuildConfigName` is not undefined, also create an entrty in the + * If `activeBuildConfigName` is not undefined, also create an entry in the * storage service representing the saved active build config. */ async function initializeTest(buildConfigurations: CppBuildConfiguration[] | undefined, activeBuildConfigName: string | undefined) : Promise { - if (buildConfigurations !== undefined) { - const prefService = container.get(PreferenceService); - sinon.stub(prefService, 'get').callsFake((preferenceName: string) => { - if (preferenceName === 'cpp.buildConfigurations') { - return buildConfigurations; - } - - return undefined; - }); - } + const preferenceService = container.get(PreferenceService); + preferenceService.get = (preferenceName: string, fallback?: T) => { + if (preferenceName === 'cpp.buildConfigurations') { + return buildConfigurations || fallback; + } + return undefined; + }; // Save active build config if (activeBuildConfigName !== undefined) { const storage = container.get(StorageService); - storage.setData('cpp.active-build-configuration', { - configName: activeBuildConfigName, + storage.setData('cpp.active-build-configurations-map', { + configs: [[ + '/tmp', + { + name: 'Release', + directory: '/tmp/builds/release', + } + ]], }); } @@ -96,7 +136,7 @@ describe('build-configurations', function () { const cppBuildConfigurations = await initializeTest(undefined, undefined); const configs = cppBuildConfigurations.getConfigs(); - const active = cppBuildConfigurations.getActiveConfig(); + const active = cppBuildConfigurations.getActiveConfig('/tmp'); expect(active).eq(undefined); expect(configs).lengthOf(0); @@ -106,7 +146,7 @@ describe('build-configurations', function () { const cppBuildConfigurations = await initializeTest([], undefined); const configs = cppBuildConfigurations.getConfigs(); - const active = cppBuildConfigurations.getActiveConfig(); + const active = cppBuildConfigurations.getActiveConfig('/tmp'); expect(active).eq(undefined); expect(configs).lengthOf(0); @@ -123,7 +163,7 @@ describe('build-configurations', function () { const cppBuildConfigurations = await initializeTest(builds, undefined); const configs = cppBuildConfigurations.getConfigs(); - const active = cppBuildConfigurations.getActiveConfig(); + const active = cppBuildConfigurations.getActiveConfig('/tmp'); expect(active).eq(undefined); expect(configs).to.be.an('array').of.lengthOf(2); @@ -140,8 +180,11 @@ describe('build-configurations', function () { }]; const cppBuildConfigurations = await initializeTest(builds, 'Debug'); + const manager = getManager(); + manager.setActiveConfig(builds[1], '/tmp'); + const configs = cppBuildConfigurations.getConfigs(); - const active = cppBuildConfigurations.getActiveConfig(); + const active = cppBuildConfigurations.getActiveConfig('/tmp'); expect(active).to.be.deep.eq(builds[1]); expect(configs).to.be.an('array').of.lengthOf(2); @@ -158,8 +201,11 @@ describe('build-configurations', function () { }]; const cppBuildConfigurations = await initializeTest(builds, 'foobar'); + const manager = getManager(); + manager.setActiveConfig(undefined, '/tmp'); + const configs = cppBuildConfigurations.getConfigs(); - const active = cppBuildConfigurations.getActiveConfig(); + const active = cppBuildConfigurations.getActiveConfig('/tmp'); expect(active).to.be.eq(undefined); expect(configs).to.be.an('array').of.lengthOf(2); diff --git a/packages/cpp/src/browser/cpp-build-configurations.ts b/packages/cpp/src/browser/cpp-build-configurations.ts index 90ee3fb226330..92e1fc50a1867 100644 --- a/packages/cpp/src/browser/cpp-build-configurations.ts +++ b/packages/cpp/src/browser/cpp-build-configurations.ts @@ -18,39 +18,34 @@ 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'; /** - * Representation of a cpp build configuration. + * @deprecated Import from `@theia/cpp/lib/common` instead */ -export interface CppBuildConfiguration { +export { CppBuildConfiguration }; - /** - * The human-readable build configuration name. - */ - name: string; - - /** - * The base directory of the build configuration. - */ - directory: string; +// tslint:disable-next-line:no-any +export function isCppBuildConfiguration(arg: any): arg is CppBuildConfiguration { + return arg.name !== undefined && arg.directory !== undefined; +} - /** - * The list of commands for the build configuration. - */ - commands?: { - 'build'?: string - }; +export function equals(a: CppBuildConfiguration, b: CppBuildConfiguration): boolean { + return ( + a.name === b.name && + a.directory === b.directory && + a.commands === b.commands + ); } /** - * Representation of a saved build configuration in local storage. + * Representation of all saved build configurations per workspace root in local storage. */ -class SavedActiveBuildConfiguration { - - /** - * The name of the build configuration. - */ - configName?: string; +class SavedActiveBuildConfigurations { + configs: [string, CppBuildConfiguration | undefined][]; } export const CppBuildConfigurationManager = Symbol('CppBuildConfigurationManager'); @@ -61,7 +56,7 @@ export interface CppBuildConfigurationManager { * * @returns an array of defined `CppBuildConfiguration`. */ - getConfigs(): CppBuildConfiguration[]; + getConfigs(root?: string): CppBuildConfiguration[]; /** * Get the list of valid defined build configurations. @@ -69,29 +64,57 @@ export interface CppBuildConfigurationManager { * @returns an array of valid defined `CppBuildConfiguration`. * A `CppBuildConfiguration` is considered valid if it has a `name` and `directory`. */ - getValidConfigs(): CppBuildConfiguration[]; + 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(): CppBuildConfiguration | 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. */ - setActiveConfig(config: CppBuildConfiguration | undefined): void; + getAllActiveConfigs?(): Map; /** + * 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; + + /** + * @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; + /** + * Updated `onActiveConfigChange` to support multi-root. + * + * @returns all the configurations to use. + */ + onActiveConfigChange2: Event>; + /** * Promise resolved when the list of build configurations has been read * once, and the active configuration has been set, if relevant. @@ -114,18 +137,41 @@ export class CppBuildConfigurationManagerImpl implements CppBuildConfigurationMa @inject(StorageService) protected readonly storageService: StorageService; + @inject(WorkspaceService) + protected readonly workspaceService: WorkspaceService; + + @inject(VariableResolverService) + protected readonly variableResolver: VariableResolverService; + + @inject(CppBuildConfigurationServer) + protected readonly buildConfigurationServer: CppBuildConfigurationServer; + /** - * The current active build configuration. - * If `undefined` there is no current active build configuration selected. + * Resolved configurations, coming from the preferences. */ - protected activeConfig: CppBuildConfiguration | undefined; + protected resolvedConfigurations = new Map(); /** + * The current active build configurations map. + */ + protected activeConfigurations = new Map(); + + /** + * @deprecated use `activeConfigChange2Emitter` instead. + * * Emitter for when the active build configuration changes. */ protected readonly activeConfigChangeEmitter = new Emitter(); - readonly ACTIVE_BUILD_CONFIGURATION_STORAGE_KEY = 'cpp.active-build-configuration'; + /** + * Emitter for when an active build configuration changes. + */ + protected readonly activeConfigChange2Emitter = new Emitter>(); + + /** + * Persistent storage key for the active build configurations map. + */ + readonly ACTIVE_BUILD_CONFIGURATIONS_MAP_STORAGE_KEY = 'cpp.active-build-configurations-map'; public ready: Promise; @@ -133,27 +179,34 @@ export class CppBuildConfigurationManagerImpl implements CppBuildConfigurationMa async init() { // Try to read the active build config from local storage. this.ready = new Promise(async resolve => { + const loadActiveConfigurations = this.loadActiveConfigs(); await this.cppPreferences.ready; - this.loadActiveConfiguration().then(resolve); + await Promise.all([ + this.handlePreferencesUpdate(), + loadActiveConfigurations, + ]); this.cppPreferences.onPreferenceChanged(() => this.handlePreferencesUpdate()); + resolve(); }); } + protected getConfigsFromPreferences(root?: string) { + 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 loadActiveConfiguration(): Promise { - const savedConfig = - await this.storageService.getData( - this.ACTIVE_BUILD_CONFIGURATION_STORAGE_KEY); - - if (savedConfig !== undefined && savedConfig.configName !== undefined) { - // Try to find an existing config with that name. - const configs = this.getConfigs(); - const config = configs.find(cfg => savedConfig.configName === cfg.name); - if (config) { - this.setActiveConfig(config); - } + protected async loadActiveConfigs(): Promise { + const savedConfig = await this.storageService.getData( + this.ACTIVE_BUILD_CONFIGURATIONS_MAP_STORAGE_KEY + ); + if (savedConfig !== undefined) { + // read from local storage and update the map. + this.activeConfigurations = new Map(savedConfig.configs); } } @@ -162,24 +215,48 @@ export class CppBuildConfigurationManagerImpl implements CppBuildConfigurationMa * * @param config the active `CppBuildConfiguration`. */ - protected saveActiveConfiguration(config: CppBuildConfiguration | undefined): void { - this.storageService.setData( - this.ACTIVE_BUILD_CONFIGURATION_STORAGE_KEY, { - configName: config ? config.name : undefined, - }); + protected saveActiveConfigs(configs: Map): void { + this.storageService.setData( + this.ACTIVE_BUILD_CONFIGURATIONS_MAP_STORAGE_KEY, { configs: [...configs.entries()] } + ); } /** * Update the active build configuration if applicable. */ - protected handlePreferencesUpdate(): void { - const active = this.getActiveConfig(); - const valid = (active) - ? this.getValidConfigs().some(a => this.equals(a, active)) - : false; - if (!valid) { - this.setActiveConfig(undefined); + protected async handlePreferencesUpdate(): Promise { + // 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 => ({ ...config })); // copy + await Promise.all(resolvedConfigs.map(async config => Promise.all([ + 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 } /** @@ -192,13 +269,30 @@ export class CppBuildConfigurationManagerImpl implements CppBuildConfigurationMa return a.name === b.name && a.directory === b.directory; } - getActiveConfig(): CppBuildConfiguration | undefined { - return this.activeConfig; + getActiveConfig(root?: string): CppBuildConfiguration | undefined { + // Get the active workspace root for the given uri, else for the first workspace root. + const workspaceRoot = root ? root : this.workspaceService.tryGetRoots()[0].uri; + return this.activeConfigurations.get(workspaceRoot); } - setActiveConfig(config: CppBuildConfiguration | undefined): void { - this.activeConfig = config; - this.saveActiveConfiguration(config); + getAllActiveConfigs(): Map { + return this.activeConfigurations; + } + + setActiveConfig(config: CppBuildConfiguration | undefined, root?: string): void { + // Set the active workspace root for the given uri, else for the first workspace root. + const workspaceRoot = root ? root : this.workspaceService.tryGetRoots()[0].uri; + this.activeConfigurations.set(workspaceRoot, config); + this.saveActiveConfigs(this.activeConfigurations); + + const activeConfigurations = new Map(); + for (const [source, cppConfig] of this.getAllActiveConfigs()) { + if (typeof cppConfig !== 'undefined') { + activeConfigurations.set(source, cppConfig); + } + } + + this.activeConfigChange2Emitter.fire(activeConfigurations); this.activeConfigChangeEmitter.fire(config); } @@ -206,13 +300,29 @@ export class CppBuildConfigurationManagerImpl implements CppBuildConfigurationMa return this.activeConfigChangeEmitter.event; } - getConfigs(): CppBuildConfiguration[] { - return this.cppPreferences[CPP_BUILD_CONFIGURATIONS_PREFERENCE_KEY] || []; + get onActiveConfigChange2(): Event> { + return this.activeConfigChange2Emitter.event; } - getValidConfigs(): CppBuildConfiguration[] { - return Array.from(this.getConfigs()) + getConfigs(root?: string): CppBuildConfiguration[] { + const workspaceRoot = root ? root : this.workspaceService.tryGetRoots()[0].uri; + let configs = this.resolvedConfigurations.get(workspaceRoot); + if (!configs) { + this.resolvedConfigurations.set(workspaceRoot, configs = []); + } + return configs; + } + + getValidConfigs(root?: string): CppBuildConfiguration[] { + return this.getConfigs(root) .filter(a => a.name !== '' && a.directory !== '') .sort((a, b) => (a.name.localeCompare(b.name))); } + + /** + * @todo Optimize by caching the merge result, based on the `CppBuildConfiguration.directory` field? + */ + async getMergedCompilationDatabase(params: { directories: string[] }): Promise { + return this.buildConfigurationServer.getMergedCompilationDatabase(params); + } } diff --git a/packages/cpp/src/browser/cpp-frontend-module.ts b/packages/cpp/src/browser/cpp-frontend-module.ts index 2abc44f5e917f..3e888335b9247 100644 --- a/packages/cpp/src/browser/cpp-frontend-module.ts +++ b/packages/cpp/src/browser/cpp-frontend-module.ts @@ -16,7 +16,7 @@ import { ContainerModule } from 'inversify'; import { CommandContribution } from '@theia/core/lib/common'; -import { KeybindingContribution, KeybindingContext } from '@theia/core/lib/browser'; +import { KeybindingContribution, KeybindingContext, WebSocketConnectionProvider } from '@theia/core/lib/browser'; import { CppCommandContribution } from './cpp-commands'; import { LanguageClientContribution } from '@theia/languages/lib/browser'; @@ -28,6 +28,7 @@ import { CppBuildConfigurationManager, CppBuildConfigurationManagerImpl } from ' import { CppBuildConfigurationsStatusBarElement } from './cpp-build-configurations-statusbar-element'; import { CppTaskProvider } from './cpp-task-provider'; import { TaskContribution } from '@theia/task/lib/browser/task-contribution'; +import { CppBuildConfigurationServer, cppBuildConfigurationServerPath } from '../common/cpp-build-configuration-protocol'; export default new ContainerModule(bind => { bind(CommandContribution).to(CppCommandContribution).inSingletonScope(); @@ -48,5 +49,9 @@ export default new ContainerModule(bind => { bind(CppBuildConfigurationsStatusBarElement).toSelf().inSingletonScope(); + bind(CppBuildConfigurationServer).toDynamicValue(ctx => + WebSocketConnectionProvider.createProxy(ctx.container, cppBuildConfigurationServerPath) + ).inSingletonScope(); + bindCppPreferences(bind); }); diff --git a/packages/cpp/src/browser/cpp-language-client-contribution.ts b/packages/cpp/src/browser/cpp-language-client-contribution.ts index 5200adf995d33..ec7fbf1f3dea3 100644 --- a/packages/cpp/src/browser/cpp-language-client-contribution.ts +++ b/packages/cpp/src/browser/cpp-language-client-contribution.ts @@ -24,9 +24,11 @@ import { Languages, Workspace } from '@theia/languages/lib/browser'; import { ILogger } from '@theia/core/lib/common/logger'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; import { CPP_LANGUAGE_ID, CPP_LANGUAGE_NAME, HEADER_AND_SOURCE_FILE_EXTENSIONS, CppStartParameters } from '../common'; -import { CppBuildConfigurationManager, CppBuildConfiguration } from './cpp-build-configurations'; +import { CppBuildConfigurationManager } from './cpp-build-configurations'; import { CppBuildConfigurationsStatusBarElement } from './cpp-build-configurations-statusbar-element'; +import { CppBuildConfiguration } from '../common/cpp-build-configuration-protocol'; import { CppPreferences } from './cpp-preferences'; +import URI from '@theia/core/lib/common/uri'; /** * Clangd extension to set clangd-specific "initializationOptions" in the @@ -35,6 +37,14 @@ import { CppPreferences } from './cpp-preferences'; */ interface ClangdConfigurationParamsChange { compilationDatabasePath?: string; + + /** + * Experimental field. + */ + compilationDatabaseMap?: Array<{ + sourceDir: string; + dbPath: string; + }>; } @injectable() @@ -68,7 +78,7 @@ export class CppLanguageClientContribution extends BaseLanguageClientContributio @postConstruct() protected init() { - this.cppBuildConfigurations.onActiveConfigChange(config => this.onActiveBuildConfigChanged(config)); + this.cppBuildConfigurations.onActiveConfigChange2(() => this.onActiveBuildConfigChanged()); this.cppPreferences.onPreferenceChanged(e => { if (this.running) { this.restart(); @@ -83,22 +93,52 @@ export class CppLanguageClientContribution extends BaseLanguageClientContributio this.cppBuildConfigurationsStatusBarElement.show(); } - private createClangdConfigurationParams(config: CppBuildConfiguration | undefined): ClangdConfigurationParamsChange { + protected async createCompilationDatabaseMap(): Promise> { + const activeConfigurations = new Map(); + const databaseMap = new Map(); + + for (const [source, config] of this.cppBuildConfigurations.getAllActiveConfigs!().entries()) { + if (config) { + activeConfigurations.set(source, config); + databaseMap.set(source, config.directory); + } + } + + if (activeConfigurations.size > 1 && !this.cppPreferences['cpp.experimentalCompilationDatabaseMap']) { + databaseMap.clear(); // Use only one configuration. + const configs = [...activeConfigurations.values()]; + try { + const mergedDatabaseUri = new URI(await this.cppBuildConfigurations.getMergedCompilationDatabase!({ + directories: configs.map(config => config.directory), + })); + databaseMap.set('undefined', mergedDatabaseUri.parent.path.toString()); + } catch (error) { + this.logger.error(error); + databaseMap.set('undefined', configs[0].directory); + } + } + + return databaseMap; + } + + private async updateInitializationOptions(): Promise { const clangdParams: ClangdConfigurationParamsChange = {}; + const databaseMap = await this.createCompilationDatabaseMap(); + + if (databaseMap.size === 1) { + clangdParams.compilationDatabasePath = [...databaseMap.values()][0]; - if (config) { - clangdParams.compilationDatabasePath = config.directory; + } else if (databaseMap.size > 1 && this.cppPreferences['cpp.experimentalCompilationDatabaseMap']) { + clangdParams.compilationDatabaseMap = [...databaseMap.entries()].map( + ([sourceDir, dbPath]) => ({ sourceDir: new URI(sourceDir).path.toString(), dbPath, })); } - return clangdParams; + const lc = await this.languageClient; + lc.clientOptions.initializationOptions = clangdParams; } - async onActiveBuildConfigChanged(config: CppBuildConfiguration | undefined) { - // Override the initializationOptions to put the new path to the build, - // then restart clangd. + protected onActiveBuildConfigChanged() { if (this.running) { - const lc = await this.languageClient; - lc.clientOptions.initializationOptions = this.createClangdConfigurationParams(config); this.restart(); } } @@ -124,8 +164,6 @@ export class CppLanguageClientContribution extends BaseLanguageClientContributio protected createOptions(): LanguageClientOptions { const clientOptions = super.createOptions(); - clientOptions.initializationOptions = this.createClangdConfigurationParams(this.cppBuildConfigurations.getActiveConfig()); - clientOptions.initializationFailedHandler = () => { const READ_INSTRUCTIONS_ACTION = 'Read Instructions'; const ERROR_MESSAGE = 'Error starting C/C++ language server. ' + @@ -142,7 +180,13 @@ export class CppLanguageClientContribution extends BaseLanguageClientContributio return clientOptions; } - protected getStartParameters(): CppStartParameters { + protected async getStartParameters(): Promise { + + // getStartParameters is one of the only async steps in the LC + // initialization sequence, so we will update asynchronously the + // options here + await this.updateInitializationOptions(); + return { clangdExecutable: this.cppPreferences['cpp.clangdExecutable'], clangdArgs: this.cppPreferences['cpp.clangdArgs'], diff --git a/packages/cpp/src/browser/cpp-preferences.ts b/packages/cpp/src/browser/cpp-preferences.ts index 731c6eb652a20..aeb481ca91529 100644 --- a/packages/cpp/src/browser/cpp-preferences.ts +++ b/packages/cpp/src/browser/cpp-preferences.ts @@ -14,9 +14,9 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { PreferenceSchema, PreferenceProxy, PreferenceService, createPreferenceProxy, PreferenceContribution } from '@theia/core/lib/browser/preferences'; import { interfaces } from 'inversify'; -import { CppBuildConfiguration } from './cpp-build-configurations'; +import { PreferenceSchema, PreferenceProxy, PreferenceService, createPreferenceProxy, PreferenceContribution } from '@theia/core/lib/browser/preferences'; +import { CppBuildConfiguration } from '../common/cpp-build-configuration-protocol'; import { CLANGD_EXECUTABLE_DEFAULT } from '../common'; export const cppPreferencesSchema: PreferenceSchema = { @@ -45,8 +45,14 @@ export const cppPreferencesSchema: PreferenceSchema = { }, required: ['name', 'directory'], }, + scope: 'resource', default: [], }, + 'cpp.experimentalCompilationDatabaseMap': { + description: 'Enable experimental support for multiple compilation databases.', + default: false, + type: 'boolean' + }, 'cpp.experimentalCommands': { description: 'Enable experimental commands mostly intended for Clangd developers.', default: false, @@ -77,6 +83,7 @@ export const cppPreferencesSchema: PreferenceSchema = { export class CppConfiguration { 'cpp.buildConfigurations': CppBuildConfiguration[]; + 'cpp.experimentalCompilationDatabaseMap': boolean; 'cpp.experimentalCommands': boolean; 'cpp.clangdExecutable': string; 'cpp.clangdArgs': string; diff --git a/packages/cpp/src/browser/cpp-task-provider.spec.ts b/packages/cpp/src/browser/cpp-task-provider.spec.ts index d41da0d04689a..8bcc85638eb40 100644 --- a/packages/cpp/src/browser/cpp-task-provider.spec.ts +++ b/packages/cpp/src/browser/cpp-task-provider.spec.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2018 Ericsson and others. + * 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 @@ -17,7 +17,7 @@ import { Container, injectable } from 'inversify'; import { CppTaskProvider } from './cpp-task-provider'; import { TaskResolverRegistry } from '@theia/task/lib/browser/task-contribution'; -import { CppBuildConfigurationManager, CppBuildConfiguration } from './cpp-build-configurations'; +import { CppBuildConfigurationManager } from './cpp-build-configurations'; import { Event } from '@theia/core'; import { expect } from 'chai'; import { TaskConfiguration } from '@theia/task/lib/common'; @@ -25,6 +25,7 @@ import { ProcessTaskConfiguration } from '@theia/task/lib/common/process/task-pr import { TaskDefinitionRegistry } from '@theia/task/lib/browser/task-definition-registry'; import { ProblemMatcherRegistry } from '@theia/task/lib/browser/task-problem-matcher-registry'; import { ProblemPatternRegistry } from '@theia/task/lib/browser/task-problem-pattern-registry'; +import { CppBuildConfiguration } from '../common/cpp-build-configuration-protocol'; // The object under test. let taskProvider: CppTaskProvider; @@ -59,6 +60,9 @@ class MockCppBuildConfigurationManager implements CppBuildConfigurationManager { /** Event emitted when the active build configuration changes. */ onActiveConfigChange: Event; + /** Event emitted when an active build configuration changes. */ + onActiveConfigChange2: Event>; + /** * Promise resolved when the list of build configurations has been read * once, and the active configuration has been set, if relevant. @@ -91,12 +95,12 @@ describe('CppTaskProvider', function () { it('provide a task for each build config with a build command', async function () { const tasks = await taskProvider.provideTasks(); expect(tasks).length(1); - expect(tasks[0].config.name === 'Build 1'); + expect(tasks[0].config.name).to.be.equal('Build 2'); const resolvedTask = await taskProvider.resolveTask(tasks[0]); expect(resolvedTask.type === 'shell'); - expect((resolvedTask).cwd === '/tmp/build1'); - expect((resolvedTask).command === 'very'); + expect((resolvedTask).cwd).to.be.equal('/tmp/build2'); + expect((resolvedTask).command).to.be.equal('very'); expect((resolvedTask).args).to.deep.equal(['complex', 'command']); }); }); diff --git a/packages/cpp/src/browser/cpp-task-provider.ts b/packages/cpp/src/browser/cpp-task-provider.ts index e6dbb715d57dd..655cdbd85d3b0 100644 --- a/packages/cpp/src/browser/cpp-task-provider.ts +++ b/packages/cpp/src/browser/cpp-task-provider.ts @@ -18,7 +18,8 @@ import parseArgv = require('string-argv'); import { inject, injectable, postConstruct } from 'inversify'; import { ProcessTaskConfiguration } from '@theia/task/lib/common/process/task-protocol'; import { TaskContribution, TaskProvider, TaskProviderRegistry, TaskResolver, TaskResolverRegistry } from '@theia/task/lib/browser/task-contribution'; -import { CppBuildConfigurationManager, CppBuildConfiguration } from './cpp-build-configurations'; +import { CppBuildConfiguration } from '../common/cpp-build-configuration-protocol'; +import { CppBuildConfigurationManager } from './cpp-build-configurations'; import { ContributedTaskConfiguration, TaskConfiguration } from '@theia/task/lib/common/task-protocol'; import { TaskDefinitionRegistry } from '@theia/task/lib/browser/task-definition-registry'; import { ProblemMatcherRegistry } from '@theia/task/lib/browser/task-problem-matcher-registry'; diff --git a/packages/cpp/src/common/cpp-build-configuration-protocol.ts b/packages/cpp/src/common/cpp-build-configuration-protocol.ts new file mode 100644 index 0000000000000..c09c08eba612f --- /dev/null +++ b/packages/cpp/src/common/cpp-build-configuration-protocol.ts @@ -0,0 +1,69 @@ +/******************************************************************************** + * Copyright (C) 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 } from 'inversify'; + +export const cppBuildConfigurationServerPath = '/services/cppbuildconfigurationserver'; + +/** + * Representation of a cpp build configuration. + */ +export interface CppBuildConfiguration { + + /** + * The human-readable build configuration name. + */ + name: string; + + /** + * The base directory of the build configuration. + */ + directory: string; + + /** + * The list of commands for the build configuration. + */ + commands?: { + 'build'?: string + }; +} + +export const CppBuildConfigurationServer = Symbol('CppBuildConfigurationServer'); +/** + * A `CppBuildConfigurationServer` is meant to do heavy disk operations on the + * project's filesystem, such as merging multiple compilation databases together. + */ +export interface CppBuildConfigurationServer { + + /** + * Compilation databases get fairly big fairly quickly, so we want to + * offload this to the backend server somehow. Could be optimized by using + * sub-processing or anything else that would avoid stalling the application. + * + * @param params.configurations The list of configs to merge together. + */ + getMergedCompilationDatabase(params: { directories: string[] }): Promise; + +} + +@injectable() +export class MockCppBuildConfigurationServer implements CppBuildConfigurationServer { + constructor() { } + dispose() { } + getMergedCompilationDatabase(params: { directories: string[] }): Promise { + return Promise.resolve(''); + } +} diff --git a/packages/cpp/src/node/cpp-backend-module.ts b/packages/cpp/src/node/cpp-backend-module.ts index efabb533cadae..aac66b3bcfdcd 100644 --- a/packages/cpp/src/node/cpp-backend-module.ts +++ b/packages/cpp/src/node/cpp-backend-module.ts @@ -17,7 +17,21 @@ import { ContainerModule } from 'inversify'; import { LanguageServerContribution } from '@theia/languages/lib/node'; import { CppContribution } from './cpp-contribution'; +import { CppBuildConfigurationServer, cppBuildConfigurationServerPath } from '../common/cpp-build-configuration-protocol'; +import { CppBuildConfigurationServerImpl } from './cpp-build-configuration-server'; +import { JsonRpcConnectionHandler, ConnectionHandler, ILogger } from '@theia/core/lib/common'; export default new ContainerModule(bind => { bind(LanguageServerContribution).to(CppContribution).inSingletonScope(); + + bind(ILogger).toDynamicValue(ctx => { + const logger = ctx.container.get(ILogger); + return logger.child('cpp'); + }).inSingletonScope().whenTargetNamed('cpp'); + + bind(CppBuildConfigurationServerImpl).toSelf().inSingletonScope(); + bind(ConnectionHandler).toDynamicValue(ctx => + new JsonRpcConnectionHandler(cppBuildConfigurationServerPath, () => + ctx.container.get(CppBuildConfigurationServerImpl)) + ).inSingletonScope(); }); diff --git a/packages/cpp/src/node/cpp-build-configuration-server.ts b/packages/cpp/src/node/cpp-build-configuration-server.ts new file mode 100644 index 0000000000000..375fce5b5f729 --- /dev/null +++ b/packages/cpp/src/node/cpp-build-configuration-server.ts @@ -0,0 +1,69 @@ +/******************************************************************************** + * Copyright (C) 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 * as crypto from 'crypto'; +import { injectable, inject, named } from 'inversify'; +import { EOL, tmpdir } from 'os'; +import { join } from 'path'; +import { CppBuildConfigurationServer } from '../common/cpp-build-configuration-protocol'; +import { FileSystem } from '@theia/filesystem/lib/common'; +import { FileUri } from '@theia/core/lib/node'; +import { isArray } from 'util'; +import { ILogger } from '@theia/core/lib/common/logger'; + +@injectable() +export class CppBuildConfigurationServerImpl implements CppBuildConfigurationServer { + + @inject(ILogger) @named('cpp') + protected readonly logger: ILogger; + + @inject(FileSystem) + protected readonly fileSystem: FileSystem; + + async getMergedCompilationDatabase(params: { directories: string[] }): Promise { + const directories = params.directories.sort(); + const hash = crypto.createHash('sha256'); + // tslint:disable-next-line:no-any + const entries: any = []; + + for (const directory of directories) { + hash.update(directory); + } + + await Promise.all(directories.map(async directory => { + const file = await this.fileSystem.resolveContent( + FileUri.create(directory).resolve('compile_commands.json').toString()); + const parsed = JSON.parse(file.content); + if (!isArray(parsed)) { + throw new Error(`content is not a JSON array: ${file.stat.uri}`); + } + entries.push(...parsed); + })); + + const databaseFolder = join(tmpdir(), 'theia-cpp-databases', hash.digest('hex').toLowerCase()); + const databasePath = FileUri.create(databaseFolder) + .resolve('compile_commands.json').toString(); + + if (await this.fileSystem.exists(databasePath)) { + await this.fileSystem.delete(databasePath); + } + await this.fileSystem.createFile(databasePath, { + content: JSON.stringify(entries) + EOL + }); + this.logger.debug(`Wrote merged compilation database into ${databaseFolder}`); + return databasePath; + } +}