diff --git a/packages/cpp/package.json b/packages/cpp/package.json index 6ce6c0d6613bb..c66dfa4330dcf 100644 --- a/packages/cpp/package.json +++ b/packages/cpp/package.json @@ -10,18 +10,18 @@ "@theia/monaco": "^0.5.0", "@theia/preferences": "^0.5.0", "@theia/process": "^0.5.0", + "@theia/workspace": "^0.5.0", "@theia/task": "^0.5.0", - "string-argv": "^0.1.1" + "string-argv": "^0.1.1", + "fs-extra": "^4.0.2" }, "publishConfig": { "access": "public" }, - "theiaExtensions": [ - { - "frontend": "lib/browser/cpp-frontend-module", - "backend": "lib/node/cpp-backend-module" - } - ], + "theiaExtensions": [{ + "frontend": "lib/browser/cpp-frontend-module", + "backend": "lib/node/cpp-backend-module" + }], "keywords": [ "theia-extension" ], @@ -47,7 +47,8 @@ "test": "theiaext test" }, "devDependencies": { - "@theia/ext-scripts": "^0.5.0" + "@theia/ext-scripts": "^0.5.0", + "@types/fs-extra": "^4.0.2" }, "nyc": { "extends": "../../configs/nyc.json" 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 eee981a9cd69f..119919b12a6f0 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,88 +42,109 @@ 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).withoutScheme().toString() : undefined; + /** + * 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 { + + // Prompt users to determine working root. + const root = await this.selectWorkspaceRoot(); + if (!root) { + return; + } - // Item to create a new build configuration - items.push(this.createItem); + // Prompt users to determine action (set active config, reset active config, create new config). + const action = await this.selectCppAction(root); + if (!action) { + return; + } - // Only return 'Create New' when no build configurations present - if (!configurations.length) { - return acceptor(items); + // 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); } + } + + protected async selectWorkspaceRoot(): Promise { + const roots = this.workspaceService.tryGetRoots(); + return this.quickPick.show(roots.map( + ({ uri }) => ({ + label: this.labelProvider.getName(new URI(uri).withoutScheme()), + value: uri, + description: this.cppBuildConfigurations.getActiveConfig(uri) + ? this.cppBuildConfigurations.getActiveConfig(uri)!.name + : 'undefined' + }) + ), { placeholder: 'Select workspace root' }); + } - // Item to de-select any active build config - if (active) { + 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); } - - // Add one item per build config - configurations.forEach(config => { - const uri = new URI(config.directory); - items.push(new QuickOpenItem({ + // 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, - // 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; + 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 }, - })); - }); - - acceptor(items); - } - - 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, + }); }); + return this.quickPick.show(items, { placeholder: 'Select action' }); } /** Create a new build configuration with placeholder values. */ @@ -131,7 +154,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++'; @@ -183,7 +205,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 e3d8ebf4ee3de..25293a35f7950 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 @@ -14,20 +14,34 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom'; +let disableJSDOM = enableJSDOM(); + import { ContainerModule, Container } from 'inversify'; import { expect } from 'chai'; import { FileSystem } 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 { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; +import { CppBuildConfiguration, CppBuildConfigurationServer, MockCppBuildConfigurationServer } from '../common/cpp-build-configuration-protocol'; let container: Container; +disableJSDOM(); + +before(() => { + disableJSDOM = enableJSDOM(); +}); +after(() => { + disableJSDOM(); +}); + beforeEach(function () { const m = new ContainerModule(bind => { bind(CppBuildConfigurationManager).to(CppBuildConfigurationManagerImpl).inSingletonScope(); @@ -35,17 +49,26 @@ beforeEach(function () { bind(FileSystem).to(FileSystemNode).inSingletonScope(); bindCppPreferences(bind); bind(PreferenceService).to(MockPreferenceService).inSingletonScope(); + bind(CppBuildConfigurationServer).to(MockCppBuildConfigurationServer).inSingletonScope(); }); container = new Container(); + container.bind(WorkspaceService).toConstantValue(sinon.createStubInstance(WorkspaceService)); 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, @@ -66,8 +89,14 @@ async function initializeTest(buildConfigurations: CppBuildConfiguration[] | und // 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', + } + ]], }); } @@ -81,7 +110,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); @@ -91,7 +120,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); @@ -108,7 +137,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); @@ -125,8 +154,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); @@ -143,8 +175,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..8dab281532eef 100644 --- a/packages/cpp/src/browser/cpp-build-configurations.ts +++ b/packages/cpp/src/browser/cpp-build-configurations.ts @@ -18,39 +18,32 @@ 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'; /** - * 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 +54,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 +62,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): void; + setActiveConfig(config: CppBuildConfiguration | undefined, root?: string): void; /** + * Get the active build configurations for all roots. + */ + 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 +135,34 @@ export class CppBuildConfigurationManagerImpl implements CppBuildConfigurationMa @inject(StorageService) protected readonly storageService: StorageService; + @inject(WorkspaceService) + protected readonly workspaceService: WorkspaceService; + + @inject(CppBuildConfigurationServer) + protected readonly buildConfigurationServer: CppBuildConfigurationServer; + /** - * The current active build configuration. - * If `undefined` there is no current active build configuration selected. + * The current active build configurations map. */ - protected activeConfig: CppBuildConfiguration | undefined; + protected activeConfigs: Map + = 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; @@ -143,17 +180,12 @@ export class CppBuildConfigurationManagerImpl implements CppBuildConfigurationMa * 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); - } + 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.activeConfigs = new Map(savedConfig.configs); } } @@ -162,11 +194,10 @@ 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 saveActiveConfiguration(configs: Map): void { + this.storageService.setData( + this.ACTIVE_BUILD_CONFIGURATIONS_MAP_STORAGE_KEY, { configs: [...configs.entries()] } + ); } /** @@ -192,13 +223,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.activeConfigs.get(workspaceRoot); + } + + getAllActiveConfigs(): Map { + return this.activeConfigs; } - setActiveConfig(config: CppBuildConfiguration | undefined): void { - this.activeConfig = config; - this.saveActiveConfiguration(config); + 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.activeConfigs.set(workspaceRoot, config); + this.saveActiveConfiguration(this.activeConfigs); + + 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 +254,27 @@ export class CppBuildConfigurationManagerImpl implements CppBuildConfigurationMa return this.activeConfigChangeEmitter.event; } - getConfigs(): CppBuildConfiguration[] { + get onActiveConfigChange2(): Event> { + return this.activeConfigChange2Emitter.event; + } + + getConfigs(root?: string): CppBuildConfiguration[] { + if (root) { + return this.cppPreferences.get(CPP_BUILD_CONFIGURATIONS_PREFERENCE_KEY, [], root); + } return this.cppPreferences[CPP_BUILD_CONFIGURATIONS_PREFERENCE_KEY] || []; } - getValidConfigs(): CppBuildConfiguration[] { - return Array.from(this.getConfigs()) + getValidConfigs(root?: string): CppBuildConfiguration[] { + return Array.from(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 96a61c2db33b6..7ea9afdc25e06 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'; @@ -30,6 +30,7 @@ import { CppGrammarContribution } from './cpp-grammar-contribution'; 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(); @@ -51,5 +52,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..4e87440ee5dc7 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 configs = await this.createCompilationDatabaseMap(); + + if (configs.size === 1) { + clangdParams.compilationDatabasePath = [...configs.values()][0]; - if (config) { - clangdParams.compilationDatabasePath = config.directory; + } else if (configs.size > 1 && this.cppPreferences['cpp.experimentalCompilationDatabaseMap']) { + clangdParams.compilationDatabaseMap = [...configs.entries()].map( + ([sourceDir, dbPath]) => ({ sourceDir, 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 acb5362bc2446..28f934daa5fac 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,11 +17,12 @@ 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/src/common'; +import { TaskConfiguration } from '@theia/task/lib/common'; import { ProcessTaskConfiguration } from '@theia/task/lib/common/process/task-protocol'; +import { CppBuildConfiguration } from '../common/cpp-build-configuration-protocol'; // The object under test. let taskProvider: CppTaskProvider; @@ -56,6 +57,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. @@ -84,12 +88,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 936919d72bf01..2f5b26ca9da8e 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 } 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 { TaskConfiguration } from '@theia/task/lib/common/task-protocol'; /** 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..58c4a186794c8 --- /dev/null +++ b/packages/cpp/src/node/cpp-build-configuration-server.ts @@ -0,0 +1,70 @@ +/******************************************************************************** + * 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 fs from 'fs-extra'; +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; + } +} diff --git a/packages/filesystem/package.json b/packages/filesystem/package.json index b71f7cfe05131..01d3567763d52 100644 --- a/packages/filesystem/package.json +++ b/packages/filesystem/package.json @@ -32,8 +32,7 @@ "publishConfig": { "access": "public" }, - "theiaExtensions": [ - { + "theiaExtensions": [{ "frontend": "lib/browser/filesystem-frontend-module", "backend": "lib/node/filesystem-backend-module" },