diff --git a/CHANGELOG.md b/CHANGELOG.md index e06f4d6ea8297..5a82bf1a4660b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,33 @@ - `test:references`: fails if typescript references are out of sync. - `prepare:references`: updates typescript references, if required. - [repo] the `prepare` script now updates typescript references. +- [core] From now on, downstream projects can refine where the configuration files (such as `settings.json`, `keymaps.json`, `recentworkspace.json`, etc.) will be stored by Theia. [#4488](https://github.com/eclipse-theia/theia/pull/4488) +The default location remains the same: `~/.theia`, however it can be customized by overriding the `#getConfigDirUri` method of the `EnvVariablesServer` API. The easiest way is to subclass the `EnvVariablesServerImpl` and rebind it in your backend module: + ```ts + // your-env-variables-server.ts: + + import { injectable } from 'inversify'; + import { EnvVariablesServerImpl } from '@theia/core/lib/node/env-variables'; + + @injectable() + export class YourEnvVariableServer extends EnvVariablesServerImpl { + + async getConfigDirUri(): Promise { + return 'file:///path/to/your/desired/config/dir'; + } + + } + + // your-backend-application-module.ts: + + import { ContainerModule } from 'inversify'; + import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; + import { YourEnvVariableServer } from './your-env-variables-server'; + + export default new ContainerModule((bind, unbind, isBound, rebind) => { + rebind(EnvVariablesServer).to(YourEnvVariableServer).inSingletonScope(); + }); + ``` Breaking changes: diff --git a/packages/core/src/browser/label-provider.ts b/packages/core/src/browser/label-provider.ts index ec548bce8f89c..055dd4b843b31 100644 --- a/packages/core/src/browser/label-provider.ts +++ b/packages/core/src/browser/label-provider.ts @@ -78,7 +78,7 @@ export interface LabelProviderContribution { /** * Check whether the given element is affected by the given change event. * Contributions delegating to the label provider can use this hook - * to perfrom a recursive check. + * to perform a recursive check. */ affects?(element: object, event: DidChangeLabelEvent): boolean; diff --git a/packages/core/src/browser/test/mock-env-variables-server.ts b/packages/core/src/browser/test/mock-env-variables-server.ts new file mode 100644 index 0000000000000..350bb243e8806 --- /dev/null +++ b/packages/core/src/browser/test/mock-env-variables-server.ts @@ -0,0 +1,40 @@ +/******************************************************************************** + * Copyright (C) 2020 Red Hat, Inc. 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 URI from '../../common/uri'; +import { EnvVariablesServer, EnvVariable } from '../../common/env-variables'; + +export class MockEnvVariablesServerImpl implements EnvVariablesServer { + + constructor(protected readonly configDirUri: URI) { } + + async getConfigDirUri(): Promise { + return this.configDirUri.toString(); + } + + getExecPath(): Promise { + throw new Error('Method not implemented.'); + } + + getVariables(): Promise { + throw new Error('Method not implemented.'); + } + + getValue(key: string): Promise { + throw new Error('Method not implemented.'); + } + +} diff --git a/packages/core/src/common/env-variables/env-variables-protocol.ts b/packages/core/src/common/env-variables/env-variables-protocol.ts index fca91897abdda..167f0b2a2e1e9 100644 --- a/packages/core/src/common/env-variables/env-variables-protocol.ts +++ b/packages/core/src/common/env-variables/env-variables-protocol.ts @@ -21,6 +21,7 @@ export interface EnvVariablesServer { getExecPath(): Promise getVariables(): Promise getValue(key: string): Promise + getConfigDirUri(): Promise; } export interface EnvVariable { diff --git a/packages/core/src/node/env-variables/env-variables-server.ts b/packages/core/src/node/env-variables/env-variables-server.ts index 166de48dedfed..165fc05c3d2d9 100644 --- a/packages/core/src/node/env-variables/env-variables-server.ts +++ b/packages/core/src/node/env-variables/env-variables-server.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2018 Red Hat, Inc. and others. + * Copyright (C) 2018-2020 Red Hat, Inc. 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,14 +14,18 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import { join } from 'path'; +import { homedir } from 'os'; import { injectable } from 'inversify'; import { EnvVariable, EnvVariablesServer } from '../../common/env-variables'; import { isWindows } from '../../common/os'; +import { FileUri } from '../file-uri'; @injectable() export class EnvVariablesServerImpl implements EnvVariablesServer { protected readonly envs: { [key: string]: EnvVariable } = {}; + protected readonly configDirUri = FileUri.create(join(homedir(), '.theia')).toString(); constructor() { const prEnv = process.env; @@ -44,4 +48,9 @@ export class EnvVariablesServerImpl implements EnvVariablesServer { } return this.envs[key]; } + + async getConfigDirUri(): Promise { + return this.configDirUri; + } + } diff --git a/packages/java/src/node/java-contribution.ts b/packages/java/src/node/java-contribution.ts index 4d36ce38991ac..217a72f56b370 100644 --- a/packages/java/src/node/java-contribution.ts +++ b/packages/java/src/node/java-contribution.ts @@ -14,7 +14,6 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import * as os from 'os'; import * as path from 'path'; import * as glob from 'glob'; import { Socket } from 'net'; @@ -22,7 +21,8 @@ import { injectable, inject, named } from 'inversify'; import { Message, isRequestMessage } from 'vscode-ws-jsonrpc'; import { InitializeParams, InitializeRequest } from 'vscode-languageserver-protocol'; import { createSocketConnection } from 'vscode-ws-jsonrpc/lib/server'; -import { DEBUG_MODE } from '@theia/core/lib/node'; +import { DEBUG_MODE, FileUri } from '@theia/core/lib/node'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { IConnection, BaseLanguageServerContribution, LanguageServerStartOptions } from '@theia/languages/lib/node'; import { JAVA_LANGUAGE_ID, JAVA_LANGUAGE_NAME, JavaStartParams } from '../common'; import { JavaCliContribution } from './java-cli-contribution'; @@ -52,6 +52,7 @@ export class JavaContribution extends BaseLanguageServerContribution { protected readonly ready: Promise; constructor( + @inject(EnvVariablesServer) protected readonly envServer: EnvVariablesServer, @inject(JavaCliContribution) protected readonly cli: JavaCliContribution, @inject(ContributionProvider) @named(JavaExtensionContribution) protected readonly contributions: ContributionProvider @@ -103,7 +104,9 @@ export class JavaContribution extends BaseLanguageServerContribution { this.activeDataFolders.add(dataFolderSuffix); clientConnection.onClose(() => this.activeDataFolders.delete(dataFolderSuffix)); - const workspacePath = path.resolve(os.homedir(), '.theia', 'jdt.ls', '_ws_' + dataFolderSuffix); + const configDirUri = await this.envServer.getConfigDirUri(); + const configDirFsPath = FileUri.fsPath(configDirUri); + const workspacePath = path.resolve(configDirFsPath, 'jdt.ls', '_ws_' + dataFolderSuffix); const configuration = configurations.get(process.platform); if (!configuration) { throw new Error('Cannot find Java server configuration for ' + process.platform); diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 46f0eee498ebe..7d5c500590acf 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -95,7 +95,7 @@ export interface Plugin { export interface ConfigStorage { hostLogPath: string; hostStoragePath?: string; - hostGlobalStoragePath?: string; + hostGlobalStoragePath: string; } export interface EnvInit { diff --git a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts index 2eae278986c0f..645340e7fecbf 100644 --- a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts +++ b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts @@ -27,7 +27,7 @@ import { injectable, inject, interfaces, named, postConstruct } from 'inversify' import { PluginWorker } from '../../main/browser/plugin-worker'; import { PluginMetadata, getPluginId, HostedPluginServer, DeployedPlugin } from '../../common/plugin-protocol'; import { HostedPluginWatcher } from './hosted-plugin-watcher'; -import { MAIN_RPC_CONTEXT, PluginManagerExt } from '../../common/plugin-api-rpc'; +import { MAIN_RPC_CONTEXT, PluginManagerExt, ConfigStorage } from '../../common/plugin-api-rpc'; import { setUpPluginApi } from '../../main/browser/main-context'; import { RPCProtocol, RPCProtocolImpl } from '../../common/rpc-protocol'; import { @@ -47,6 +47,7 @@ import { Deferred } from '@theia/core/lib/common/promise-util'; import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager'; import { DebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager'; import { WaitUntilEvent } from '@theia/core/lib/common/event'; +import { FileSystem } from '@theia/filesystem/lib/common'; import { FileSearchService } from '@theia/file-search/lib/common/file-search-service'; import { Emitter, isCancelled } from '@theia/core'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; @@ -56,6 +57,8 @@ import { WebviewEnvironment } from '../../main/browser/webview/webview-environme import { WebviewWidget } from '../../main/browser/webview/webview'; import { WidgetManager } from '@theia/core/lib/browser/widget-manager'; import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; +import URI from '@theia/core/lib/common/uri'; export type PluginHost = 'frontend' | string; export type DebugActivationEvent = 'onDebugResolve' | 'onDebugInitialConfigurations' | 'onDebugAdapterProtocolTracker'; @@ -109,6 +112,9 @@ export class HostedPluginSupport { @inject(DebugConfigurationManager) protected readonly debugConfigurationManager: DebugConfigurationManager; + @inject(FileSystem) + protected readonly fileSystem: FileSystem; + @inject(FileSearchService) protected readonly fileSearchService: FileSearchService; @@ -136,6 +142,9 @@ export class HostedPluginSupport { @inject(TerminalService) protected readonly terminalService: TerminalService; + @inject(EnvVariablesServer) + protected readonly envServer: EnvVariablesServer; + private theiaReadyPromise: Promise; protected readonly managers = new Map(); @@ -330,15 +339,20 @@ export class HostedPluginSupport { let started = 0; const startPluginsMeasurement = this.createMeasurement('startPlugins'); - const [hostLogPath, hostStoragePath] = await Promise.all([ + const [hostLogPath, hostStoragePath, hostGlobalStoragePath] = await Promise.all([ this.pluginPathsService.getHostLogPath(), - this.getStoragePath() + this.getStoragePath(), + this.getHostGlobalStoragePath() ]); if (toDisconnect.disposed) { return; } const thenable: Promise[] = []; - const configStorage = { hostLogPath, hostStoragePath }; + const configStorage: ConfigStorage = { + hostLogPath, + hostStoragePath, + hostGlobalStoragePath + }; for (const [host, hostContributions] of contributionsByHost) { const manager = await this.obtainManager(host, hostContributions, toDisconnect); if (!manager) { @@ -456,6 +470,21 @@ export class HostedPluginSupport { return this.pluginPathsService.getHostStoragePath(this.workspaceService.workspace, roots); } + protected async getHostGlobalStoragePath(): Promise { + const configDirUri = await this.envServer.getConfigDirUri(); + const globalStorageFolderUri = new URI(configDirUri).resolve('globalStorage').toString(); + + // Make sure that folder by the path exists + if (!await this.fileSystem.exists(globalStorageFolderUri)) { + await this.fileSystem.createFolder(globalStorageFolderUri); + } + const globalStorageFolderFsPath = await this.fileSystem.getFsPath(globalStorageFolderUri); + if (!globalStorageFolderFsPath) { + throw new Error(`Could not resolve the FS path for URI: ${globalStorageFolderUri}`); + } + return globalStorageFolderFsPath; + } + async activateByEvent(activationEvent: string): Promise { if (this.activationEvents.has(activationEvent)) { return; @@ -599,8 +628,8 @@ export class HostedPluginSupport { protected logMeasurement(prefix: string, count: number, measurement: () => number): void { const duration = measurement(); if (duration === Number.NaN) { - // Measurement was prevented by native API, do not log NaN duration - return; + // Measurement was prevented by native API, do not log NaN duration + return; } const pluginCount = `${count} plugin${count === 1 ? '' : 's'}`; diff --git a/packages/plugin-ext/src/main/common/plugin-paths-protocol.ts b/packages/plugin-ext/src/main/common/plugin-paths-protocol.ts index 10f736bd6b934..dfee0cdb4d24a 100644 --- a/packages/plugin-ext/src/main/common/plugin-paths-protocol.ts +++ b/packages/plugin-ext/src/main/common/plugin-paths-protocol.ts @@ -25,6 +25,4 @@ export interface PluginPathsService { getHostLogPath(): Promise; /** Returns storage path for given workspace */ getHostStoragePath(workspace: FileStat | undefined, roots: FileStat[]): Promise; - /** Returns Theia data directory (one for all Theia workspaces, so doesn't change) */ - getTheiaDirPath(): Promise; } diff --git a/packages/plugin-ext/src/main/node/paths/const.ts b/packages/plugin-ext/src/main/node/paths/const.ts index 79290a5b961b9..00c134417484f 100644 --- a/packages/plugin-ext/src/main/node/paths/const.ts +++ b/packages/plugin-ext/src/main/node/paths/const.ts @@ -15,10 +15,6 @@ ********************************************************************************/ export namespace PluginPaths { - export const WINDOWS_APP_DATA_DIR = 'AppData'; - export const WINDOWS_ROAMING_DIR = 'Roaming'; - - export const THEIA_DIR = '.theia'; export const PLUGINS_LOGS_DIR = 'logs'; export const PLUGINS_GLOBAL_STORAGE_DIR = 'plugin-storage'; export const PLUGINS_WORKSPACE_STORAGE_DIR = 'workspace-storage'; diff --git a/packages/plugin-ext/src/main/node/paths/plugin-paths-service.ts b/packages/plugin-ext/src/main/node/paths/plugin-paths-service.ts index 7cc35b6b75882..12532d521f7a1 100644 --- a/packages/plugin-ext/src/main/node/paths/plugin-paths-service.ts +++ b/packages/plugin-ext/src/main/node/paths/plugin-paths-service.ts @@ -20,10 +20,12 @@ import * as path from 'path'; import { readdir, remove } from 'fs-extra'; import * as crypto from 'crypto'; import URI from '@theia/core/lib/common/uri'; -import { ILogger, isWindows } from '@theia/core'; +import { ILogger } from '@theia/core'; +import { FileUri } from '@theia/core/lib/node'; import { PluginPaths } from './const'; import { PluginPathsService } from '../../common/plugin-paths-protocol'; import { THEIA_EXT, VSCODE_EXT, getTemporaryWorkspaceFileUri } from '@theia/workspace/lib/common'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { PluginCliContribution } from '../plugin-cli-contribution'; const SESSION_TIMESTAMP_PATTERN = /^\d{8}T\d{6}$/; @@ -32,14 +34,15 @@ const SESSION_TIMESTAMP_PATTERN = /^\d{8}T\d{6}$/; @injectable() export class PluginPathsServiceImpl implements PluginPathsService { - private readonly windowsDataFolders = [PluginPaths.WINDOWS_APP_DATA_DIR, PluginPaths.WINDOWS_ROAMING_DIR]; - @inject(ILogger) protected readonly logger: ILogger; @inject(FileSystem) protected readonly fileSystem: FileSystem; + @inject(EnvVariablesServer) + protected readonly envServer: EnvVariablesServer; + @inject(PluginCliContribution) protected readonly cliContribution: PluginCliContribution; @@ -82,8 +85,7 @@ export class PluginPathsServiceImpl implements PluginPathsService { } protected async buildWorkspaceId(workspace: FileStat, roots: FileStat[]): Promise { - const homeDir = await this.getUserHomeDir(); - const untitledWorkspace = getTemporaryWorkspaceFileUri(new URI(homeDir)); + const untitledWorkspace = await getTemporaryWorkspaceFileUri(this.envServer); if (untitledWorkspace.toString() === workspace.uri) { // if workspace is temporary @@ -116,31 +118,13 @@ export class PluginPathsServiceImpl implements PluginPathsService { } private async getLogsDirPath(): Promise { - const theiaDir = await this.getTheiaDirPath(); - return path.join(theiaDir, PluginPaths.PLUGINS_LOGS_DIR); + const configDirUri = await this.envServer.getConfigDirUri(); + return path.join(FileUri.fsPath(configDirUri), PluginPaths.PLUGINS_LOGS_DIR); } private async getWorkspaceStorageDirPath(): Promise { - const theiaDir = await this.getTheiaDirPath(); - return path.join(theiaDir, PluginPaths.PLUGINS_WORKSPACE_STORAGE_DIR); - } - - async getTheiaDirPath(): Promise { - const homeDir = await this.getUserHomeDir(); - return path.join( - homeDir, - ...(isWindows ? this.windowsDataFolders : ['']), - PluginPaths.THEIA_DIR - ); - } - - private async getUserHomeDir(): Promise { - const homeDirStat = await this.fileSystem.getCurrentUserHome(); - if (!homeDirStat) { - throw new Error('Unable to get user home directory'); - } - const homeDirPath = await this.fileSystem.getFsPath(homeDirStat.uri); - return homeDirPath!; + const configDirUri = await this.envServer.getConfigDirUri(); + return path.join(FileUri.fsPath(configDirUri), PluginPaths.PLUGINS_WORKSPACE_STORAGE_DIR); } private async cleanupOldLogs(parentLogsDir: string): Promise { @@ -150,7 +134,7 @@ export class PluginPathsServiceImpl implements PluginPathsService { // However, upgrading the @types/node in theia to 10.11 (as defined in engine field) // Causes other packages to break in compilation, so we are using the infamous `any` type... // eslint-disable-next-line @typescript-eslint/no-explicit-any - const subDirEntries = dirEntries.filter((dirent: any) => dirent.isDirectory() ); + const subDirEntries = dirEntries.filter((dirent: any) => dirent.isDirectory()); // eslint-disable-next-line @typescript-eslint/no-explicit-any const subDirNames = subDirEntries.map((dirent: any) => dirent.name); // We never clean a folder that is not a Theia logs session folder. diff --git a/packages/plugin-ext/src/main/node/plugins-key-value-storage.ts b/packages/plugin-ext/src/main/node/plugins-key-value-storage.ts index 86459038602be..e52bde12afb93 100644 --- a/packages/plugin-ext/src/main/node/plugins-key-value-storage.ts +++ b/packages/plugin-ext/src/main/node/plugins-key-value-storage.ts @@ -17,8 +17,9 @@ import { injectable, inject, postConstruct } from 'inversify'; import * as fs from 'fs-extra'; import * as path from 'path'; +import { FileUri } from '@theia/core/lib/node/file-uri'; import { Deferred } from '@theia/core/lib/common/promise-util'; -import { FileSystem } from '@theia/filesystem/lib/common'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { PluginPaths } from './paths/const'; import { PluginPathsService } from '../common/plugin-paths-protocol'; import { KeysToAnyValues, KeysToKeysToAnyValue } from '../../common/types'; @@ -32,19 +33,22 @@ export class PluginsKeyValueStorage { @inject(PluginPathsService) private readonly pluginPathsService: PluginPathsService; - @inject(FileSystem) - protected readonly fileSystem: FileSystem; + @inject(EnvVariablesServer) + protected readonly envServer: EnvVariablesServer; @postConstruct() protected async init(): Promise { try { - const theiaDirPath = await this.pluginPathsService.getTheiaDirPath(); - await this.fileSystem.createFolder(theiaDirPath); - const globalDataPath = path.join(theiaDirPath, PluginPaths.PLUGINS_GLOBAL_STORAGE_DIR, 'global-state.json'); - await this.fileSystem.createFolder(path.dirname(globalDataPath)); - this.deferredGlobalDataPath.resolve(globalDataPath); + const configDirUri = await this.envServer.getConfigDirUri(); + const globalStorageFsPath = path.join(FileUri.fsPath(configDirUri), PluginPaths.PLUGINS_GLOBAL_STORAGE_DIR); + const exists = await fs.pathExists(globalStorageFsPath); + if (!exists) { + await fs.mkdirs(globalStorageFsPath); + } + const globalDataFsPath = path.join(globalStorageFsPath, 'global-state.json'); + this.deferredGlobalDataPath.resolve(globalDataFsPath); } catch (e) { - console.error('Faild to initialize global state path: ', e); + console.error('Failed to initialize global state path: ', e); this.deferredGlobalDataPath.resolve(undefined); } } diff --git a/packages/plugin-ext/src/plugin/plugin-manager.ts b/packages/plugin-ext/src/plugin/plugin-manager.ts index 3e56b44f0514d..2916b0d9d6dcc 100644 --- a/packages/plugin-ext/src/plugin/plugin-manager.ts +++ b/packages/plugin-ext/src/plugin/plugin-manager.ts @@ -37,8 +37,6 @@ import { Memento, KeyValueStorageProxy } from './plugin-storage'; import { ExtPluginApi } from '../common/plugin-ext-api-contribution'; import { RPCProtocol } from '../common/rpc-protocol'; import { Emitter } from '@theia/core/lib/common/event'; -import * as os from 'os'; -import * as fs from 'fs-extra'; import { WebviewsExtImpl } from './webviews'; export interface PluginHost { @@ -317,12 +315,7 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager { const asAbsolutePath = (relativePath: string): string => join(plugin.pluginFolder, relativePath); const logPath = join(configStorage.hostLogPath, plugin.model.id); // todo check format const storagePath = join(configStorage.hostStoragePath || '', plugin.model.id); - async function defaultGlobalStorage(): Promise { - const globalStorage = join(os.homedir(), '.theia', 'globalStorage'); - await fs.ensureDir(globalStorage); - return globalStorage; - } - const globalStoragePath = join(configStorage.hostGlobalStoragePath || (await defaultGlobalStorage()), plugin.model.id); + const globalStoragePath = join(configStorage.hostGlobalStoragePath, plugin.model.id); const pluginContext: theia.PluginContext = { extensionPath: plugin.pluginFolder, globalState: new Memento(plugin.model.id, true, this.storageProxy), diff --git a/packages/preferences/src/browser/preferences-tree-widget.ts b/packages/preferences/src/browser/preferences-tree-widget.ts index d74c113bc24bf..ba525717713d4 100644 --- a/packages/preferences/src/browser/preferences-tree-widget.ts +++ b/packages/preferences/src/browser/preferences-tree-widget.ts @@ -45,11 +45,12 @@ import { EditorWidget, EditorManager } from '@theia/editor/lib/browser'; import { DisposableCollection, Emitter, Event, MessageService } from '@theia/core'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { FileSystem, FileSystemUtils } from '@theia/filesystem/lib/common'; -import { UserStorageUri, THEIA_USER_STORAGE_FOLDER } from '@theia/userstorage/lib/browser'; +import { UserStorageUri } from '@theia/userstorage/lib/browser'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import URI from '@theia/core/lib/common/uri'; import { FoldersPreferencesProvider } from './folders-preferences-provider'; import { PreferenceConfigurations } from '@theia/core/lib/browser/preferences/preference-configurations'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; @injectable() export class PreferencesContainer extends SplitPanel implements ApplicationShell.TrackableWidgetProvider, Saveable { @@ -264,6 +265,9 @@ export class PreferencesEditorsContainer extends DockPanel { @inject(PreferenceProvider) @named(PreferenceScope.Workspace) protected readonly workspacePreferenceProvider: WorkspacePreferenceProvider; + @inject(EnvVariablesServer) + protected readonly envServer: EnvVariablesServer; + private userPreferenceEditorWidget: PreferencesEditorWidget; private workspacePreferenceEditorWidget: PreferencesEditorWidget | undefined; private foldersPreferenceEditorWidget: PreferencesEditorWidget | undefined; @@ -450,7 +454,8 @@ export class PreferencesEditorsContainer extends DockPanel { let uri = preferenceUri; if (preferenceUri.scheme === UserStorageUri.SCHEME && homeUri) { - uri = homeUri.resolve(THEIA_USER_STORAGE_FOLDER).resolve(preferenceUri.path); + const configDirUri = await this.envServer.getConfigDirUri(); + uri = new URI(configDirUri).resolve(preferenceUri.path); } return homeUri ? FileSystemUtils.tildifyPath(uri.path.toString(), homeUri.path.toString()) @@ -469,8 +474,6 @@ export class PreferencesTreeWidget extends TreeWidget { private readonly onPreferenceSelectedEmitter: Emitter<{ [key: string]: string }>; readonly onPreferenceSelected: Event<{ [key: string]: string }>; - protected readonly toDispose: DisposableCollection; - @inject(PreferencesMenuFactory) protected readonly preferencesMenuFactory: PreferencesMenuFactory; @inject(PreferenceService) protected readonly preferenceService: PreferenceService; @inject(PreferencesDecorator) protected readonly decorator: PreferencesDecorator; @@ -486,14 +489,12 @@ export class PreferencesTreeWidget extends TreeWidget { this.onPreferenceSelectedEmitter = new Emitter<{ [key: string]: string }>(); this.onPreferenceSelected = this.onPreferenceSelectedEmitter.event; - this.toDispose = new DisposableCollection(); this.toDispose.push(this.onPreferenceSelectedEmitter); this.id = PreferencesTreeWidget.ID; } dispose(): void { - this.toDispose.dispose(); super.dispose(); } diff --git a/packages/task/src/browser/task-configurations.ts b/packages/task/src/browser/task-configurations.ts index a063506c83a06..e33d5ffb2724f 100644 --- a/packages/task/src/browser/task-configurations.ts +++ b/packages/task/src/browser/task-configurations.ts @@ -59,11 +59,6 @@ export class TaskConfigurations implements Disposable { */ protected taskCustomizationMap = new Map(); - /** last directory element under which we look for task config */ - protected readonly TASKFILEPATH = '.theia'; - /** task configuration file name */ - protected readonly TASKFILE = 'tasks.json'; - protected client: TaskConfigurationClient | undefined = undefined; /** @@ -239,11 +234,6 @@ export class TaskConfigurations implements Disposable { return undefined; } - /** returns the string uri of where the config file would be, if it existed under a given root directory */ - protected getConfigFileUri(rootDir: string): string { - return new URI(rootDir).resolve(this.TASKFILEPATH).resolve(this.TASKFILE).toString(); - } - /** * Called when a change, to a config file we watch, is detected. */ @@ -301,7 +291,7 @@ export class TaskConfigurations implements Disposable { try { await this.taskConfigurationManager.openConfiguration(sourceFolderUri); } catch (e) { - console.error(`Error occurred while opening: ${this.TASKFILE}.`, e); + console.error(`Error occurred while opening 'tasks.json' in ${sourceFolderUri}.`, e); } } diff --git a/packages/userstorage/src/browser/user-storage-service-filesystem.spec.ts b/packages/userstorage/src/browser/user-storage-service-filesystem.spec.ts index b03d8880fcfc3..9b9605a5bcd9c 100644 --- a/packages/userstorage/src/browser/user-storage-service-filesystem.spec.ts +++ b/packages/userstorage/src/browser/user-storage-service-filesystem.spec.ts @@ -16,6 +16,7 @@ import { Container } from 'inversify'; import * as chai from 'chai'; +import * as temp from 'temp'; import { UserStorageServiceFilesystemImpl } from './user-storage-service-filesystem'; import { UserStorageService } from './user-storage-service'; import { UserStorageResource } from './user-storage-resource'; @@ -29,8 +30,11 @@ import { PreferenceService } from '@theia/core/lib/browser/preferences'; import { MockPreferenceService } from '@theia/core/lib/browser/preferences/test/mock-preference-service'; import { FileSystemWatcherServer } from '@theia/filesystem/lib/common/filesystem-watcher-protocol'; import { MockFilesystem, MockFilesystemWatcherServer } from '@theia/filesystem/lib/common/test'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; +import { MockEnvVariablesServerImpl } from '@theia/core/lib/browser/test/mock-env-variables-server'; import { UserStorageUri } from './user-storage-uri'; import URI from '@theia/core/lib/common/uri'; +import { FileUri } from '@theia/core/lib/node'; import * as sinon from 'sinon'; @@ -39,9 +43,10 @@ let testContainer: Container; let userStorageService: UserStorageServiceFilesystemImpl; -const homeDir = '/home/test'; -const THEIA_USER_STORAGE_FOLDER = '.theia'; -const userStorageFolder = new URI('file://' + homeDir).resolve(THEIA_USER_STORAGE_FOLDER); +const track = temp.track(); +const userStorageFolder = FileUri.create(track.mkdirSync()); +const envVariableServer = new MockEnvVariablesServerImpl(userStorageFolder); + const mockOnFileChangedEmitter = new Emitter(); let files: { [key: string]: string; } = {}; @@ -80,13 +85,6 @@ before(async () => { testContainer.bind(FileSystem).toDynamicValue(ctx => { const fs = new MockFilesystem(); - sinon.stub(fs, 'getCurrentUserHome').callsFake(() => Promise.resolve( - { - uri: 'file://' + homeDir, - lastModification: 0, - isDirectory: true - })); - sinon.stub(fs, 'resolveContent').callsFake((uri): Promise<{ stat: FileStat, content: string }> => { const content = files[uri]; return Promise.resolve( @@ -105,9 +103,14 @@ before(async () => { return fs; }).inSingletonScope(); + testContainer.bind(EnvVariablesServer).toConstantValue(envVariableServer); testContainer.bind(UserStorageService).to(UserStorageServiceFilesystemImpl); }); +after(() => { + track.cleanupSync(); +}); + describe('User Storage Service (Filesystem implementation)', () => { let testFile: string; before(() => { @@ -125,21 +128,20 @@ describe('User Storage Service (Filesystem implementation)', () => { it('Should return a user storage uri from a filesystem uri', () => { - const test = UserStorageServiceFilesystemImpl.toUserStorageUri(userStorageFolder, new URI('file://' + homeDir + '/' + THEIA_USER_STORAGE_FOLDER + '/' + testFile)); + const test = UserStorageServiceFilesystemImpl.toUserStorageUri(userStorageFolder, userStorageFolder.resolve(testFile)); expect(test.scheme).eq(UserStorageUri.SCHEME); expect(test.toString()).eq(UserStorageUri.SCHEME + ':' + testFile); const testFragment = UserStorageServiceFilesystemImpl. - toUserStorageUri(userStorageFolder, new URI('file://' + homeDir + '/' + THEIA_USER_STORAGE_FOLDER + '/' + testFile + '#test')); + toUserStorageUri(userStorageFolder, userStorageFolder.resolve(testFile).withFragment('test')); expect(testFragment.fragment).eq('test'); const testQuery = UserStorageServiceFilesystemImpl. - toUserStorageUri(userStorageFolder, new URI('file://' + homeDir + '/' + THEIA_USER_STORAGE_FOLDER + '/' + testFile + '?test=1')); + toUserStorageUri(userStorageFolder, userStorageFolder.resolve(testFile).withQuery('test=1')); expect(testQuery.query).eq('test=1'); const testQueryAndFragment = UserStorageServiceFilesystemImpl. - toUserStorageUri(userStorageFolder, new URI('file://' + homeDir + '/' + THEIA_USER_STORAGE_FOLDER + '/' + testFile - + '?test=1' + '#test')); + toUserStorageUri(userStorageFolder, userStorageFolder.resolve(testFile).withQuery('test=1').withFragment('test')); expect(testQueryAndFragment.fragment).eq('test'); expect(testQueryAndFragment.query).eq('test=1'); }); @@ -148,10 +150,10 @@ describe('User Storage Service (Filesystem implementation)', () => { const test = UserStorageServiceFilesystemImpl.toFilesystemURI(userStorageFolder, new URI(UserStorageUri.SCHEME + ':' + testFile)); expect(test.scheme).eq('file'); - expect(test.path.toString()).eq(homeDir + '/' + THEIA_USER_STORAGE_FOLDER + '/' + testFile); + expect(test.path.toString()).eq(userStorageFolder.resolve(testFile).path.toString()); }); - it('Should register a client and notifies it of the fs changesby converting them to user storage changes', done => { + it('Should register a client and notifies it of the fs changes by converting them to user storage changes', done => { userStorageService.onUserStorageChanged(event => { const userStorageUri = event.uris[0]; expect(userStorageUri.scheme).eq(UserStorageUri.SCHEME); @@ -171,7 +173,7 @@ describe('User Storage Service (Filesystem implementation)', () => { it('Should save the contents correctly using a user storage uri to a filesystem uri', async () => { const userStorageUri = UserStorageServiceFilesystemImpl. - toUserStorageUri(userStorageFolder, new URI('file://' + homeDir + '/' + THEIA_USER_STORAGE_FOLDER + '/' + testFile)); + toUserStorageUri(userStorageFolder, userStorageFolder.resolve(testFile)); await userStorageService.saveContents(userStorageUri, 'test content'); @@ -194,7 +196,7 @@ describe('User Storage Resource (Filesystem implementation)', () => { testFile = 'test.json'; userStorageService = testContainer.get(UserStorageService); const userStorageUriTest = UserStorageServiceFilesystemImpl. - toUserStorageUri(userStorageFolder, new URI('file://' + homeDir + '/' + THEIA_USER_STORAGE_FOLDER + '/' + testFile)); + toUserStorageUri(userStorageFolder, userStorageFolder.resolve(testFile)); userStorageResource = new UserStorageResource(userStorageUriTest, userStorageService); }); diff --git a/packages/userstorage/src/browser/user-storage-service-filesystem.ts b/packages/userstorage/src/browser/user-storage-service-filesystem.ts index f73cc77a4fdb0..ec0917e9370a7 100644 --- a/packages/userstorage/src/browser/user-storage-service-filesystem.ts +++ b/packages/userstorage/src/browser/user-storage-service-filesystem.ts @@ -18,12 +18,11 @@ import { DisposableCollection, ILogger, Emitter, Event } from '@theia/core/lib/c import { UserStorageChangeEvent, UserStorageService } from './user-storage-service'; import { injectable, inject } from 'inversify'; import { FileSystemWatcher, FileChangeEvent } from '@theia/filesystem/lib/browser/filesystem-watcher'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { FileSystem } from '@theia/filesystem/lib/common'; import URI from '@theia/core/lib/common/uri'; import { UserStorageUri } from './user-storage-uri'; -export const THEIA_USER_STORAGE_FOLDER = '.theia'; - @injectable() export class UserStorageServiceFilesystemImpl implements UserStorageService { @@ -34,18 +33,17 @@ export class UserStorageServiceFilesystemImpl implements UserStorageService { constructor( @inject(FileSystem) protected readonly fileSystem: FileSystem, @inject(FileSystemWatcher) protected readonly watcher: FileSystemWatcher, - @inject(ILogger) protected readonly logger: ILogger + @inject(ILogger) protected readonly logger: ILogger, + @inject(EnvVariablesServer) protected readonly envServer: EnvVariablesServer ) { - this.userStorageFolder = this.fileSystem.getCurrentUserHome().then(home => { - if (home) { - const userStorageFolderUri = new URI(home.uri).resolve(THEIA_USER_STORAGE_FOLDER); - watcher.watchFileChanges(userStorageFolderUri).then(disposable => - this.toDispose.push(disposable) - ); - this.toDispose.push(this.watcher.onFilesChanged(changes => this.onDidFilesChanged(changes))); - return new URI(home.uri).resolve(THEIA_USER_STORAGE_FOLDER); - } + this.userStorageFolder = this.envServer.getConfigDirUri().then(configDirUri => { + const userDataFolderUri = new URI(configDirUri); + watcher.watchFileChanges(userDataFolderUri).then(disposable => + this.toDispose.push(disposable) + ); + this.toDispose.push(this.watcher.onFilesChanged(changes => this.onDidFilesChanged(changes))); + return userDataFolderUri; }); this.toDispose.push(this.onUserStorageChangedEmitter); diff --git a/packages/workspace/src/browser/quick-open-workspace.ts b/packages/workspace/src/browser/quick-open-workspace.ts index 1264fe12387e6..485b6bcd4a054 100644 --- a/packages/workspace/src/browser/quick-open-workspace.ts +++ b/packages/workspace/src/browser/quick-open-workspace.ts @@ -16,6 +16,7 @@ import { injectable, inject } from 'inversify'; import { QuickOpenService, QuickOpenModel, QuickOpenItem, QuickOpenGroupItem, QuickOpenMode, LabelProvider } from '@theia/core/lib/browser'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { WorkspaceService } from './workspace-service'; import { getTemporaryWorkspaceFileUri } from '../common'; import { WorkspacePreferences } from './workspace-preferences'; @@ -34,15 +35,15 @@ export class QuickOpenWorkspace implements QuickOpenModel { @inject(FileSystem) protected readonly fileSystem: FileSystem; @inject(LabelProvider) protected readonly labelProvider: LabelProvider; @inject(WorkspacePreferences) protected preferences: WorkspacePreferences; + @inject(EnvVariablesServer) protected readonly envServer: EnvVariablesServer; async open(workspaces: string[]): Promise { this.items = []; - const homeStat = await this.fileSystem.getCurrentUserHome(); - const home = (homeStat) ? new URI(homeStat.uri).path.toString() : undefined; - let tempWorkspaceFile: URI | undefined; - if (home) { - tempWorkspaceFile = getTemporaryWorkspaceFileUri(new URI(home)); - } + const [homeDirUri, tempWorkspaceFile] = await Promise.all([ + this.fileSystem.getCurrentUserHome(), + getTemporaryWorkspaceFileUri(this.envServer) + ]); + const home = homeDirUri ? await this.fileSystem.getFsPath(homeDirUri.uri) : undefined; await this.preferences.ready; if (!workspaces.length) { this.items.push(new QuickOpenGroupItem({ @@ -57,7 +58,7 @@ export class QuickOpenWorkspace implements QuickOpenModel { !this.preferences['workspace.supportMultiRootWorkspace'] && !stat.isDirectory) { continue; // skip the workspace files if multi root is not supported } - if (tempWorkspaceFile && uri.toString() === tempWorkspaceFile.toString()) { + if (uri.toString() === tempWorkspaceFile.toString()) { continue; // skip the temporary workspace files } const icon = this.labelProvider.getIcon(stat); diff --git a/packages/workspace/src/browser/workspace-service.spec.ts b/packages/workspace/src/browser/workspace-service.spec.ts index 0ab1e744f9f59..7a887f6ed278d 100644 --- a/packages/workspace/src/browser/workspace-service.spec.ts +++ b/packages/workspace/src/browser/workspace-service.spec.ts @@ -25,6 +25,8 @@ import { FileSystemNode } from '@theia/filesystem/lib/node/node-filesystem'; import { FileSystemWatcher, FileChangeEvent, FileChangeType } from '@theia/filesystem/lib/browser/filesystem-watcher'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; import { DefaultWindowService } from '@theia/core/lib/browser/window/default-window-service'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; +import { MockEnvVariablesServerImpl } from '@theia/core/lib/browser/test/mock-env-variables-server'; import { WorkspaceServer } from '../common'; import { DefaultWorkspaceServer } from '../node/default-workspace-server'; import { Emitter, Disposable, DisposableCollection, ILogger, Logger } from '@theia/core'; @@ -35,10 +37,13 @@ import * as jsoncparser from 'jsonc-parser'; import * as sinon from 'sinon'; import * as chai from 'chai'; import * as assert from 'assert'; +import * as temp from 'temp'; +import { FileUri } from '@theia/core/lib/node'; import URI from '@theia/core/lib/common/uri'; const expect = chai.expect; disableJSDOM(); +const track = temp.track(); const folderA = Object.freeze({ uri: 'file:///home/folderA', @@ -86,6 +91,7 @@ describe('WorkspaceService', () => { after(() => { disableJSDOM(); + track.cleanupSync(); }); beforeEach(() => { @@ -107,6 +113,7 @@ describe('WorkspaceService', () => { testContainer.bind(WindowService).toConstantValue(mockWindowService); testContainer.bind(ILogger).toConstantValue(mockILogger); testContainer.bind(WorkspacePreferences).toConstantValue(mockPref); + testContainer.bind(EnvVariablesServer).toConstantValue(new MockEnvVariablesServerImpl(FileUri.create(track.mkdirSync()))); testContainer.bind(PreferenceServiceImpl).toConstantValue(mockPreferenceServiceImpl); testContainer.bind(PreferenceSchemaProvider).toConstantValue(mockPreferenceSchemaProvider); diff --git a/packages/workspace/src/browser/workspace-service.ts b/packages/workspace/src/browser/workspace-service.ts index eaf63830219f2..64a2b86da72da 100644 --- a/packages/workspace/src/browser/workspace-service.ts +++ b/packages/workspace/src/browser/workspace-service.ts @@ -24,6 +24,7 @@ import { FrontendApplicationContribution, PreferenceServiceImpl, PreferenceScope, PreferenceSchemaProvider } from '@theia/core/lib/browser'; import { Deferred } from '@theia/core/lib/common/promise-util'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { ILogger, Disposable, DisposableCollection, Emitter, Event, MaybePromise } from '@theia/core'; import { WorkspacePreferences } from './workspace-preferences'; import * as jsoncparser from 'jsonc-parser'; @@ -65,6 +66,9 @@ export class WorkspaceService implements FrontendApplicationContribution { @inject(PreferenceSchemaProvider) protected readonly schemaProvider: PreferenceSchemaProvider; + @inject(EnvVariablesServer) + protected readonly envVariableServer: EnvVariablesServer; + protected applicationName: string; @postConstruct() @@ -376,8 +380,7 @@ export class WorkspaceService implements FrontendApplicationContribution { } protected async getUntitledWorkspace(): Promise { - const home = await this.fileSystem.getCurrentUserHome(); - return home && getTemporaryWorkspaceFileUri(new URI(home.uri)); + return getTemporaryWorkspaceFileUri(this.envVariableServer); } private async writeWorkspaceFile(workspaceFile: FileStat | undefined, workspaceData: WorkspaceData): Promise { diff --git a/packages/workspace/src/common/utils.ts b/packages/workspace/src/common/utils.ts index 7039c183ee987..6288e0e80542c 100644 --- a/packages/workspace/src/common/utils.ts +++ b/packages/workspace/src/common/utils.ts @@ -15,10 +15,12 @@ ********************************************************************************/ import URI from '@theia/core/lib/common/uri'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; export const THEIA_EXT = 'theia-workspace'; export const VSCODE_EXT = 'code-workspace'; -export function getTemporaryWorkspaceFileUri(home: URI): URI { - return home.resolve('.theia').resolve(`Untitled.${THEIA_EXT}`).withScheme('file'); +export async function getTemporaryWorkspaceFileUri(envVariableServer: EnvVariablesServer): Promise { + const configDirUri = await envVariableServer.getConfigDirUri(); + return new URI(configDirUri).resolve(`Untitled.${THEIA_EXT}`); } diff --git a/packages/workspace/src/node/default-workspace-server.ts b/packages/workspace/src/node/default-workspace-server.ts index f6e35ca98b350..dcf2cd425753a 100644 --- a/packages/workspace/src/node/default-workspace-server.ts +++ b/packages/workspace/src/node/default-workspace-server.ts @@ -17,7 +17,6 @@ import * as path from 'path'; import * as yargs from 'yargs'; import * as fs from 'fs-extra'; -import * as os from 'os'; import * as jsoncparser from 'jsonc-parser'; import { injectable, inject, postConstruct } from 'inversify'; @@ -25,6 +24,7 @@ import { FileUri } from '@theia/core/lib/node'; import { CliContribution } from '@theia/core/lib/node/cli'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { WorkspaceServer } from '../common'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; @injectable() export class WorkspaceCliContribution implements CliContribution { @@ -66,6 +66,9 @@ export class DefaultWorkspaceServer implements WorkspaceServer { @inject(WorkspaceCliContribution) protected readonly cliParams: WorkspaceCliContribution; + @inject(EnvVariablesServer) + protected readonly envServer: EnvVariablesServer; + @postConstruct() protected async init(): Promise { const root = await this.getRoot(); @@ -120,8 +123,8 @@ export class DefaultWorkspaceServer implements WorkspaceServer { return listUri; } - protected workspaceStillExist(wspath: string): boolean { - return fs.pathExistsSync(FileUri.fsPath(wspath)); + protected workspaceStillExist(workspaceRootUri: string): boolean { + return fs.pathExistsSync(FileUri.fsPath(workspaceRootUri)); } protected async getWorkspaceURIFromCli(): Promise { @@ -134,36 +137,37 @@ export class DefaultWorkspaceServer implements WorkspaceServer { * @param uri most recently used uri */ protected async writeToUserHome(data: RecentWorkspacePathsData): Promise { - const file = this.getUserStoragePath(); + const file = await this.getUserStoragePath(); await this.writeToFile(file, data); } - protected async writeToFile(filePath: string, data: object): Promise { - if (!await fs.pathExists(filePath)) { - await fs.mkdirs(path.resolve(filePath, '..')); + protected async writeToFile(fsPath: string, data: object): Promise { + if (!await fs.pathExists(fsPath)) { + await fs.mkdirs(path.resolve(fsPath, '..')); } - await fs.writeJson(filePath, data); + await fs.writeJson(fsPath, data); } /** * Reads the most recently used workspace root from the user's home directory. */ protected async readRecentWorkspacePathsFromUserHome(): Promise { - const filePath = this.getUserStoragePath(); - const data = await this.readJsonFromFile(filePath); + const fsPath = await this.getUserStoragePath(); + const data = await this.readJsonFromFile(fsPath); return RecentWorkspacePathsData.is(data) ? data : undefined; } - protected async readJsonFromFile(filePath: string): Promise { - if (await fs.pathExists(filePath)) { - const rawContent = await fs.readFile(filePath, 'utf-8'); + protected async readJsonFromFile(fsPath: string): Promise { + if (await fs.pathExists(fsPath)) { + const rawContent = await fs.readFile(fsPath, 'utf-8'); const strippedContent = jsoncparser.stripComments(rawContent); return jsoncparser.parse(strippedContent); } } - protected getUserStoragePath(): string { - return path.resolve(os.homedir(), '.theia', 'recentworkspace.json'); + protected async getUserStoragePath(): Promise { + const configDirUri = await this.envServer.getConfigDirUri(); + return path.resolve(FileUri.fsPath(configDirUri), 'recentworkspace.json'); } }