UNPKG

@theia/core

Version:

Theia is a cloud & desktop IDE framework implemented in TypeScript.

335 lines (307 loc) 11.9 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 // ***************************************************************************** /** * On POSIX: * ┌──────────────────────┬────────────┐ * │ dir │ base │ * ├──────┬ ├──────┬─────┤ * │ root │ │ name │ ext │ * " / home/user/dir / file .txt " * └──────┴───────────────┴──────┴─────┘ * * On Windows: * ┌──────────────────────┬────────────┐ * │ dir │ base │ * ├──────┬ ├──────┬─────┤ * │ root │ │ name │ ext │ * " /c: / home/user/dir / file .txt " * └──────┴───────────────┴──────┴─────┘ */ import { OS } from './os'; export class Path { static separator: '/' = '/'; static isDrive(segment: string): boolean { return segment.endsWith(':'); } /** * vscode-uri always normalizes drive letters to lower case: * https://github.com/Microsoft/vscode-uri/blob/b1d3221579f97f28a839b6f996d76fc45e9964d8/src/index.ts#L1025 * Theia path should be adjusted to this. */ static normalizeDrive(path: string): string { // lower-case windows drive letters in /C:/fff or C:/fff if (path.length >= 3 && path.charCodeAt(0) === 47 /* '/' */ && path.charCodeAt(2) === 58 /* ':' */) { const code = path.charCodeAt(1); if (code >= 65 /* A */ && code <= 90 /* Z */) { path = `/${String.fromCharCode(code + 32)}:${path.substring(3)}`; // "/c:".length === 3 } } else if (path.length >= 2 && path.charCodeAt(1) === 58 /* ':' */) { const code = path.charCodeAt(0); if (code >= 65 /* A */ && code <= 90 /* Z */) { path = `${String.fromCharCode(code + 32)}:${path.substring(2)}`; // "c:".length === 2 } if (path.charCodeAt(0) !== 47 /* '/' */) { path = `${String.fromCharCode(47)}${path}`; } } return path; } /** * Normalize path separator to use Path.separator * @param Path candidate to normalize * @returns Normalized string path */ static normalizePathSeparator(path: string): string { return path.split(/[\\]/).join(Path.separator); } /** * Creates a windows path from the given path string. * A windows path uses an upper case drive letter and backwards slashes. * @param path The input path * @returns Windows style path */ static windowsPath(path: string): string { const offset = path.charAt(0) === '/' ? 1 : 0; if (path.charAt(offset + 1) === ':') { const driveLetter = path.charAt(offset).toUpperCase(); const substring = path.substring(offset + 2).replace(/\//g, '\\'); return `${driveLetter}:${substring || '\\'}`; } return path.replace(/\//g, '\\'); } /** * Tildify path, replacing `home` with `~` if user's `home` is present at the beginning of the path. * This is a non-operation for Windows. * * @param resourcePath * @param home */ static tildify(resourcePath: string, home: string): string { const path = new Path(resourcePath); const isWindows = path.root && Path.isDrive(path.root.base); if (!isWindows && home && resourcePath.indexOf(`${home}/`) === 0) { return resourcePath.replace(`${home}/`, '~/'); } return resourcePath; } /** * Untildify path, replacing `~` with `home` if `~` present at the beginning of the path. * This is a non-operation for Windows. * * @param resourcePath * @param home */ static untildify(resourcePath: string, home: string): string { if (resourcePath.startsWith('~')) { const untildifiedResource = resourcePath.replace(/^~/, home); const untildifiedPath = new Path(untildifiedResource); const isWindows = untildifiedPath.root && Path.isDrive(untildifiedPath.root.base); if (!isWindows && home && untildifiedResource.startsWith(`${home}`)) { return untildifiedResource; } } return resourcePath; } readonly isAbsolute: boolean; readonly isRoot: boolean; readonly root: Path | undefined; readonly base: string; readonly name: string; readonly ext: string; private _dir: Path; private readonly raw: string; /** * The raw should be normalized, meaning that only '/' is allowed as a path separator. */ constructor( raw: string ) { raw = Path.normalizePathSeparator(raw); this.raw = Path.normalizeDrive(raw); const firstIndex = this.raw.indexOf(Path.separator); const lastIndex = this.raw.lastIndexOf(Path.separator); this.isAbsolute = firstIndex === 0; this.base = lastIndex === -1 ? this.raw : this.raw.substring(lastIndex + 1); this.isRoot = this.isAbsolute && firstIndex === lastIndex && (!this.base || Path.isDrive(this.base)); this.root = this.computeRoot(); const extIndex = this.base.lastIndexOf('.'); this.name = extIndex === -1 ? this.base : this.base.substring(0, extIndex); this.ext = extIndex === -1 ? '' : this.base.substring(extIndex); } protected computeRoot(): Path | undefined { // '/' -> '/' // '/c:' -> '/c:' if (this.isRoot) { return this; } // 'foo/bar' -> `undefined` if (!this.isAbsolute) { return undefined; } const index = this.raw.indexOf(Path.separator, Path.separator.length); if (index === -1) { // '/foo/bar' -> '/' return new Path(Path.separator); } // '/c:/foo/bar' -> '/c:' // '/foo/bar' -> '/' return new Path(this.raw.substring(0, index)).root; } /** * Returns the parent directory if it exists (`hasDir === true`) or `this` otherwise. */ get dir(): Path { if (this._dir === undefined) { this._dir = this.computeDir(); } return this._dir; } /** * Returns `true` if this has a parent directory, `false` otherwise. * * _This implementation returns `true` if and only if this is not the root dir and * there is a path separator in the raw path._ */ get hasDir(): boolean { return !this.isRoot && this.raw.lastIndexOf(Path.separator) !== -1; } protected computeDir(): Path { if (!this.hasDir) { return this; } const lastIndex = this.raw.lastIndexOf(Path.separator); if (this.isAbsolute) { const firstIndex = this.raw.indexOf(Path.separator); if (firstIndex === lastIndex) { return new Path(this.raw.substring(0, firstIndex + 1)); } } return new Path(this.raw.substring(0, lastIndex)); } join(...paths: string[]): Path { const relativePath = paths.filter(s => !!s).join(Path.separator); if (!relativePath) { return this; } if (this.raw.endsWith(Path.separator)) { return new Path(this.raw + relativePath); } return new Path(this.raw + Path.separator + relativePath); } /** * * @param paths portions of a path * @returns a new Path if an absolute path can be computed from the segments passed in + this.raw * If no absolute path can be computed, returns undefined. * * Processes the path segments passed in from right to left (reverse order) concatenating until an * absolute path is found. */ resolve(...paths: string[]): Path | undefined { const segments = paths.slice().reverse(); // Don't mutate the caller's array. segments.push(this.raw); let result = new Path(''); for (const segment of segments) { if (segment) { const next = new Path(segment).join(result.raw); if (next.isAbsolute) { return next.normalize(); } result = next; } } } toString(): string { return this.raw; } /** * Converts the current path into a file system path. * @param format Determines the format of the path. * If `undefined`, the format will be determined by the `OS.backend.type` value. * @returns A file system path. */ fsPath(format?: Path.Format): string { if (format === Path.Format.Windows || (format === undefined && OS.backend.isWindows)) { return Path.windowsPath(this.raw); } else { return this.raw; } } relative(path: Path): Path | undefined { if (this.raw === path.raw) { return new Path(''); } if (!this.raw || !path.raw) { return undefined; } const raw = this.base ? this.raw + Path.separator : this.raw; if (!path.raw.startsWith(raw)) { return undefined; } const relativePath = path.raw.substring(raw.length); return new Path(relativePath); } isEqualOrParent(path: Path): boolean { return !!this.relative(path); } relativity(path: Path): number { const relative = this.relative(path); if (relative) { const relativeStr = relative.toString(); if (relativeStr === '') { return 0; } return relativeStr.split(Path.separator).length; } return -1; } /* * return a normalized Path, resolving '..' and '.' segments */ normalize(): Path { const trailingSlash = this.raw.endsWith('/'); const pathArray = this.toString().split('/'); const resultArray: string[] = []; pathArray.forEach((value, index) => { if (!value || value === '.') { return; } if (value === '..') { if (resultArray.length && resultArray[resultArray.length - 1] !== '..') { resultArray.pop(); } else if (!this.isAbsolute) { resultArray.push('..'); } } else { resultArray.push(value); } }); if (resultArray.length === 0) { if (this.isRoot) { return new Path('/'); } else { return new Path('.'); } } return new Path((this.isAbsolute ? '/' : '') + resultArray.join('/') + (trailingSlash ? '/' : '')); } } export namespace Path { export enum Format { Posix, Windows } }