UNPKG

@theia/workspace

Version:
272 lines (234 loc) • 11.3 kB
// ***************************************************************************** // Copyright (C) 2017 TypeFox 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-only WITH Classpath-exception-2.0 // ***************************************************************************** import * as path from 'path'; import * as yargs from '@theia/core/shared/yargs'; import * as fs from '@theia/core/shared/fs-extra'; import * as jsoncparser from 'jsonc-parser'; import { injectable, inject, postConstruct, named } from '@theia/core/shared/inversify'; import { FileUri, BackendApplicationContribution } from '@theia/core/lib/node'; import { CliContribution } from '@theia/core/lib/node/cli'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { WorkspaceServer, UntitledWorkspaceService } from '../common'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import URI from '@theia/core/lib/common/uri'; import { ContributionProvider, notEmpty } from '@theia/core'; export const WorkspaceHandlerContribution = Symbol('workspaceHandlerContribution'); export interface WorkspaceHandlerContribution { canHandle(uri: URI): boolean; workspaceStillExists(uri: URI): Promise<boolean>; } @injectable() export class WorkspaceCliContribution implements CliContribution { @inject(EnvVariablesServer) protected readonly envVariablesServer: EnvVariablesServer; @inject(UntitledWorkspaceService) protected readonly untitledWorkspaceService: UntitledWorkspaceService; workspaceRoot = new Deferred<string | undefined>(); configure(conf: yargs.Argv): void { conf.usage('$0 [workspace-directories] [options]'); conf.option('root-dir', { description: 'DEPRECATED: Sets the workspace directory.', }); } async setArguments(args: yargs.Arguments): Promise<void> { const workspaceArguments = args._.map(probablyAlreadyString => String(probablyAlreadyString)); if (workspaceArguments.length === 0 && args['root-dir']) { workspaceArguments.push(String(args['root-dir'])); } if (workspaceArguments.length === 0) { this.workspaceRoot.resolve(undefined); } else if (workspaceArguments.length === 1) { this.workspaceRoot.resolve(this.normalizeWorkspaceArg(workspaceArguments[0])); } else { this.workspaceRoot.resolve(this.buildWorkspaceForMultipleArguments(workspaceArguments)); } } protected normalizeWorkspaceArg(raw: string): string { return path.resolve(raw).replace(/\/$/, ''); } protected async buildWorkspaceForMultipleArguments(workspaceArguments: string[]): Promise<string | undefined> { try { const dirs = await Promise.all(workspaceArguments.map(async maybeDir => (await fs.stat(maybeDir).catch(() => undefined))?.isDirectory())); const folders = workspaceArguments.filter((_, index) => dirs[index]).map(dir => ({ path: this.normalizeWorkspaceArg(dir) })); if (folders.length < 2) { return folders[0]?.path; } const untitledWorkspaceUri = await this.untitledWorkspaceService.getUntitledWorkspaceUri( new URI(await this.envVariablesServer.getConfigDirUri()), async uri => !await fs.pathExists(uri.path.fsPath()), ); const untitledWorkspacePath = untitledWorkspaceUri.path.fsPath(); await fs.ensureDir(path.dirname(untitledWorkspacePath)); await fs.writeFile(untitledWorkspacePath, JSON.stringify({ folders }, undefined, 4)); return untitledWorkspacePath; } catch { return undefined; } } } @injectable() export class DefaultWorkspaceServer implements WorkspaceServer, BackendApplicationContribution { protected root: Deferred<string | undefined> = new Deferred(); /** * Untitled workspaces that are not among the most recent N workspaces will be deleted on start. Increase this number to keep older files, * lower it to delete stale untitled workspaces more aggressively. */ protected untitledWorkspaceStaleThreshold = 10; @inject(WorkspaceCliContribution) protected readonly cliParams: WorkspaceCliContribution; @inject(EnvVariablesServer) protected readonly envServer: EnvVariablesServer; @inject(UntitledWorkspaceService) protected readonly untitledWorkspaceService: UntitledWorkspaceService; @inject(ContributionProvider) @named(WorkspaceHandlerContribution) protected readonly workspaceHandlers: ContributionProvider<WorkspaceHandlerContribution>; @postConstruct() protected init(): void { this.doInit(); } protected async doInit(): Promise<void> { const root = await this.getRoot(); this.root.resolve(root); } async onStart(): Promise<void> { await this.removeOldUntitledWorkspaces(); } protected async getRoot(): Promise<string | undefined> { let root = await this.getWorkspaceURIFromCli(); if (!root) { const data = await this.readRecentWorkspacePathsFromUserHome(); if (data && data.recentRoots) { root = data.recentRoots[0]; } } return root; } getMostRecentlyUsedWorkspace(): Promise<string | undefined> { return this.root.promise; } async setMostRecentlyUsedWorkspace(rawUri: string): Promise<void> { const uri = rawUri && new URI(rawUri).toString(); // the empty string is used as a signal from the frontend not to load a workspace. this.root = new Deferred(); this.root.resolve(uri); const recentRoots = Array.from(new Set([uri, ...await this.getRecentWorkspaces()])); this.writeToUserHome({ recentRoots }); } async removeRecentWorkspace(rawUri: string): Promise<void> { const uri = rawUri && new URI(rawUri).toString(); // the empty string is used as a signal from the frontend not to load a workspace. const recentRoots = await this.getRecentWorkspaces(); const index = recentRoots.indexOf(uri); if (index !== -1) { recentRoots.splice(index, 1); this.writeToUserHome({ recentRoots }); } } async getRecentWorkspaces(): Promise<string[]> { const data = await this.readRecentWorkspacePathsFromUserHome(); if (data && data.recentRoots) { const allRootUris = await Promise.all(data.recentRoots.map(async element => element && await this.workspaceStillExist(element) ? element : undefined)); return allRootUris.filter(notEmpty); } return []; } protected async workspaceStillExist(workspaceRootUri: string): Promise<boolean> { const uri = new URI(workspaceRootUri); for (const handler of this.workspaceHandlers.getContributions()) { if (handler.canHandle(uri)) { return handler.workspaceStillExists(uri); } } return false; } protected async getWorkspaceURIFromCli(): Promise<string | undefined> { const arg = await this.cliParams.workspaceRoot.promise; return arg !== undefined ? FileUri.create(arg).toString() : undefined; } /** * Writes the given uri as the most recently used workspace root to the user's home directory. * @param uri most recently used uri */ protected async writeToUserHome(data: RecentWorkspacePathsData): Promise<void> { const file = await this.getUserStoragePath(); await this.writeToFile(file, data); } protected async writeToFile(fsPath: string, data: object): Promise<void> { if (!await fs.pathExists(fsPath)) { await fs.mkdirs(path.resolve(fsPath, '..')); } await fs.writeJson(fsPath, data); } /** * Reads the most recently used workspace root from the user's home directory. */ protected async readRecentWorkspacePathsFromUserHome(): Promise<RecentWorkspacePathsData | undefined> { const fsPath = await this.getUserStoragePath(); const data = await this.readJsonFromFile(fsPath); return RecentWorkspacePathsData.create(data); } protected async readJsonFromFile(fsPath: string): Promise<object | undefined> { if (await fs.pathExists(fsPath)) { const rawContent = await fs.readFile(fsPath, 'utf-8'); const strippedContent = jsoncparser.stripComments(rawContent); return jsoncparser.parse(strippedContent); } } protected async getUserStoragePath(): Promise<string> { const configDirUri = await this.envServer.getConfigDirUri(); return path.resolve(FileUri.fsPath(configDirUri), 'recentworkspace.json'); } /** * Removes untitled workspaces that are not among the most recently used workspaces. * Use the `untitledWorkspaceStaleThreshold` to configure when to delete workspaces. */ protected async removeOldUntitledWorkspaces(): Promise<void> { const recents = (await this.getRecentWorkspaces()).map(FileUri.fsPath); const olderUntitledWorkspaces = recents .slice(this.untitledWorkspaceStaleThreshold) .filter(workspace => this.untitledWorkspaceService.isUntitledWorkspace(FileUri.create(workspace))); await Promise.all(olderUntitledWorkspaces.map(workspace => fs.promises.unlink(FileUri.fsPath(workspace)).catch(() => { }))); if (olderUntitledWorkspaces.length > 0) { await this.writeToUserHome({ recentRoots: await this.getRecentWorkspaces() }); } } } @injectable() export class FileWorkspaceHandlerContribution implements WorkspaceHandlerContribution { canHandle(uri: URI): boolean { return uri.scheme === 'file'; } async workspaceStillExists(uri: URI): Promise<boolean> { return fs.pathExists(uri.path.fsPath()); } } export interface RecentWorkspacePathsData { recentRoots: string[]; } export namespace RecentWorkspacePathsData { /** * Parses `data` as `RecentWorkspacePathsData` but removes any non-string array entry. * * Returns undefined if the given `data` does not contain a `recentRoots` array property. */ export function create(data: unknown): RecentWorkspacePathsData | undefined { if (typeof data !== 'object' || !data || !Array.isArray((data as RecentWorkspacePathsData).recentRoots)) { return; } return { recentRoots: (data as RecentWorkspacePathsData).recentRoots.filter(root => typeof root === 'string') }; } }