diff --git a/Jakefile.js b/Jakefile.js index 0b3659e2051..49641cab710 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -93,7 +93,9 @@ var typesMapOutputPath = path.join(builtLocalDirectory, 'typesMap.json'); var harnessCoreSources = [ "harness.ts", - "virtualFileSystem.ts", + "collections.ts", + "vpath.ts", + "vfs.ts", "virtualFileSystemWithWatch.ts", "sourceMapRecorder.ts", "harnessLanguageService.ts", diff --git a/src/harness/collections.ts b/src/harness/collections.ts new file mode 100644 index 00000000000..e0f5e9a2aea --- /dev/null +++ b/src/harness/collections.ts @@ -0,0 +1,213 @@ +/// +namespace Collections { + import compareValues = ts.compareValues; + import binarySearch = ts.binarySearch; + import removeAt = ts.orderedRemoveItemAt; + + const caseInsensitiveComparisonCollator = typeof Intl === "object" ? new Intl.Collator(/*locales*/ undefined, { usage: "sort", sensitivity: "accent" }) : undefined; + const caseSensitiveComparisonCollator = typeof Intl === "object" ? new Intl.Collator(/*locales*/ undefined, { usage: "sort", sensitivity: "variant" }) : undefined; + + export function compareStrings(a: string | undefined, b: string | undefined, ignoreCase?: boolean) { + if (a === b) return 0; + if (a === undefined) return -1; + if (b === undefined) return +1; + const collator = ignoreCase ? caseInsensitiveComparisonCollator : caseSensitiveComparisonCollator; + if (collator) { + return collator.compare(a, b); + } + else if (ignoreCase) { + a = a.toUpperCase(); + b = b.toUpperCase(); + } + return a < b ? -1 : a > b ? +1 : 0; + } + + export namespace compareStrings { + export function caseSensitive(a: string | undefined, b: string | undefined) { + return compareStrings(a, b, /*ignoreCase*/ false); + } + + export function caseInsensitive(a: string | undefined, b: string | undefined) { + return compareStrings(a, b, /*ignoreCase*/ true); + } + } + + function insertAt(array: T[], index: number, value: T) { + if (index === 0) { + array.unshift(value); + } + else if (index === array.length) { + array.push(value); + } + else { + for (let i = array.length; i > index; i--) { + array[i] = array[i - 1]; + } + array[index] = value; + } + } + + /** + * A collection of key/value pairs sorted by key. + */ + export class KeyedCollection { + private _comparer: (a: K, b: K) => number; + private _keys: K[] = []; + private _values: V[] = []; + private _order: number[] = []; + private _version = 0; + private _copyOnWrite = false; + + constructor(comparer: (a: K, b: K) => number = compareValues) { + this._comparer = comparer; + } + + public get size() { + return this._keys.length; + } + + public has(key: K) { + return binarySearch(this._keys, key, this._comparer) >= 0; + } + + public get(key: K) { + const index = binarySearch(this._keys, key, this._comparer); + return index >= 0 ? this._values[index] : undefined; + } + + public set(key: K, value: V) { + const index = binarySearch(this._keys, key, this._comparer); + if (index >= 0) { + this._values[index] = value; + } + else { + this.writePreamble(); + insertAt(this._keys, ~index, key); + insertAt(this._values, ~index, value); + insertAt(this._order, ~index, this._version); + this._version++; + } + return this; + } + + public delete(key: K) { + const index = binarySearch(this._keys, key, this._comparer); + if (index >= 0) { + this.writePreamble(); + removeAt(this._keys, index); + removeAt(this._values, index); + removeAt(this._order, index); + this._version++; + return true; + } + return false; + } + + public clear() { + if (this.size > 0) { + this.writePreamble(); + this._keys.length = 0; + this._values.length = 0; + this._order.length = 0; + this._version = 0; + } + } + + public forEach(callback: (value: V, key: K, collection: this) => void) { + const keys = this._keys; + const values = this._values; + const order = this.getInsertionOrder(); + const version = this._version; + this._copyOnWrite = true; + for (let i = 0; i < order.length; i++) { + callback(values[order[i]], keys[order[i]], this); + } + if (version === this._version) { + this._copyOnWrite = false; + } + } + + private writePreamble() { + if (this._copyOnWrite) { + this._keys = this._keys.slice(); + this._values = this._values.slice(); + this._order = this._order.slice(); + this._copyOnWrite = false; + } + } + + private getInsertionOrder() { + return this._order + .map((_, i) => i) + .sort((x, y) => compareValues(this._order[x], this._order[y])); + } + } + + const undefinedSentinel = {}; + + /** + * A collection of metadata that supports inheritance. + */ + export class Metadata { + private _parent: Metadata | undefined; + private _map: { [key: string]: any }; + private _version = 0; + private _size = -1; + private _parentVersion: number | undefined; + + constructor(parent?: Metadata) { + this._parent = parent; + this._map = Object.create(parent ? parent._map : null); // tslint:disable-line:no-null-keyword + } + + public get size(): number { + if (this._size === -1 || (this._parent && this._parent._version !== this._parentVersion)) { + let size = 0; + for (const _ in this._map) size++; + this._size = size; + if (this._parent) { + this._parentVersion = this._parent._version; + } + } + return this._size; + } + + public has(key: string): boolean { + return this._map[key] !== undefined; + } + + public get(key: string): any { + const value = this._map[key]; + return value === undefinedSentinel ? undefined : value; + } + + public set(key: string, value: any): this { + this._map[key] = value === undefined ? undefinedSentinel : value; + this._size = -1; + this._version++; + return this; + } + + public delete(key: string): boolean { + if (this._map[key] !== undefined) { + delete this._map[key]; + this._size = -1; + this._version++; + return true; + } + return false; + } + + public clear(): void { + this._map = Object.create(this._parent ? this._parent._map : null); // tslint:disable-line:no-null-keyword + this._size = -1; + this._version++; + } + + public forEach(callback: (value: any, key: string, map: this) => void) { + for (const key in this._map) { + callback(this._map[key], key, this); + } + } + } +} \ No newline at end of file diff --git a/src/harness/harness.ts b/src/harness/harness.ts index a0fb88213ed..f24ee950d7d 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -20,7 +20,7 @@ /// /// /// -/// +/// /// /// /// @@ -743,8 +743,8 @@ namespace Harness { fs.addFile(file); } return ts.matchFiles(path, extension, exclude, include, useCaseSensitiveFileNames(), getCurrentDirectory(), depth, path => { - const entry = fs.traversePath(path); - if (entry && entry.isDirectory()) { + const entry = fs.getEntry(path); + if (entry instanceof Utils.VirtualDirectory) { const directory = entry; return { files: ts.map(directory.getFiles(), f => f.name), diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index ad79c96d833..9e353a14351 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -123,7 +123,7 @@ namespace Harness.LanguageService { } export class LanguageServiceAdapterHost { - protected virtualFileSystem: Utils.VirtualFileSystem = new Utils.VirtualFileSystem(virtualFileSystemRoot, /*useCaseSensitiveFilenames*/false); + protected virtualFileSystem: Utils.VirtualFileSystem = new Utils.VirtualFileSystem(virtualFileSystemRoot, /*useCaseSensitiveFilenames*/ false); constructor(protected cancellationToken = DefaultHostCancellationToken.Instance, protected settings = ts.getDefaultCompilerOptions()) { @@ -135,9 +135,9 @@ namespace Harness.LanguageService { public getFilenames(): string[] { const fileNames: string[] = []; - for (const virtualEntry of this.virtualFileSystem.getAllFileEntries()) { - const scriptInfo = virtualEntry.content; - if (scriptInfo.isRootFile) { + for (const virtualEntry of this.virtualFileSystem.getFiles({ recursive: true })) { + const scriptInfo = virtualEntry.metadata.get("scriptInfo"); + if (scriptInfo && scriptInfo.isRootFile) { // only include root files here // usually it means that we won't include lib.d.ts in the list of root files so it won't mess the computation of compilation root dir. fileNames.push(scriptInfo.fileName); @@ -147,18 +147,20 @@ namespace Harness.LanguageService { } public getScriptInfo(fileName: string): ScriptInfo { - const fileEntry = this.virtualFileSystem.traversePath(fileName); - return fileEntry && fileEntry.isFile() ? (fileEntry).content : undefined; + const fileEntry = this.virtualFileSystem.getFile(fileName); + return fileEntry ? fileEntry.metadata.get("scriptInfo") : undefined; } public addScript(fileName: string, content: string, isRootFile: boolean): void { - this.virtualFileSystem.addFile(fileName, new ScriptInfo(fileName, content, isRootFile)); + this.virtualFileSystem.addFile(fileName, content, { overwrite: true }).metadata.set("scriptInfo", new ScriptInfo(fileName, content, isRootFile)); } public editScript(fileName: string, start: number, end: number, newText: string) { - const script = this.getScriptInfo(fileName); - if (script !== undefined) { + const file = this.virtualFileSystem.getFile(fileName); + const script = file && file.metadata.get("scriptInfo") as ScriptInfo; + if (script) { script.editContent(start, end, newText); + file.setContent(script.content); return; } @@ -185,9 +187,9 @@ namespace Harness.LanguageService { getCompilationSettings() { return this.settings; } getCancellationToken() { return this.cancellationToken; } getDirectories(path: string): string[] { - const dir = this.virtualFileSystem.traversePath(path); - if (dir && dir.isDirectory()) { - return ts.map((dir).getDirectories(), (d) => ts.combinePaths(path, d.name)); + const dir = this.virtualFileSystem.getDirectory(path); + if (dir) { + return ts.map(dir.getDirectories(), (d) => ts.combinePaths(path, d.name)); } return []; } diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index a0c141f4f17..2838ba5b0c4 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -97,6 +97,9 @@ "./parallel/host.ts", "./parallel/worker.ts", "runner.ts", + "collections.ts", + "vpath.ts", + "vfs.ts", "virtualFileSystemWithWatch.ts", "../server/protocol.ts", "../server/session.ts", diff --git a/src/harness/unittests/configurationExtension.ts b/src/harness/unittests/configurationExtension.ts index 0032505aba1..be87b66ca74 100644 --- a/src/harness/unittests/configurationExtension.ts +++ b/src/harness/unittests/configurationExtension.ts @@ -1,5 +1,5 @@ /// -/// +/// namespace ts { const testContentsJson = createMapFromTemplate({ diff --git a/src/harness/unittests/matchFiles.ts b/src/harness/unittests/matchFiles.ts index f2c2369ef37..b56e3a5648b 100644 --- a/src/harness/unittests/matchFiles.ts +++ b/src/harness/unittests/matchFiles.ts @@ -1,5 +1,5 @@ /// -/// +/// namespace ts { const caseInsensitiveBasePath = "c:/dev/"; diff --git a/src/harness/vfs.ts b/src/harness/vfs.ts new file mode 100644 index 00000000000..bd33e632d6f --- /dev/null +++ b/src/harness/vfs.ts @@ -0,0 +1,1073 @@ +/// +/// +/// +/// +namespace Utils { + import compareStrings = Collections.compareStrings; + import removeAt = ts.orderedRemoveItemAt; + import KeyedCollection = Collections.KeyedCollection; + import Metadata = Collections.Metadata; + + // import IO = Harness.IO; + + // export interface PathMappings { + // [path: string]: string; + // } + + // export interface FileSystemResolver { + // getEntries(dir: VirtualDirectory): { files: string[], directories: string[] }; + // getContent(file: VirtualFile): string | undefined; + // } + + // function createMapper(ignoreCase: boolean, map: PathMappings | undefined) { + // if (!map) return identity; + // const roots = Object.keys(map); + // const patterns = roots.map(root => createPattern(root, ignoreCase)); + // return function (path: string) { + // for (let i = 0; i < patterns.length; i++) { + // const match = patterns[i].exec(path); + // if (match) { + // const prefix = path.slice(0, match.index); + // const suffix = path.slice(match.index + match[0].length); + // return vpath.combine(prefix, map[roots[i]], suffix); + // } + // } + // return path; + // }; + // } + + // function createPattern(path: string, ignoreCase: boolean) { + // path = vpath.normalizeSlashes(path); + // const components = vpath.parse(path); + // let pattern = ""; + // for (let i = 1; i < components.length; i++) { + // const component = components[i]; + // if (pattern) pattern += "/"; + // pattern += escapeRegExp(component); + // } + // pattern = (components[0] ? "^" + escapeRegExp(components[0]) : "/") + pattern + "(/|$)"; + // return new RegExp(pattern, ignoreCase ? "i" : ""); + // } + + // export function createResolver(io: IO, map?: PathMappings): FileSystemResolver { + // const mapper = createMapper(!io.useCaseSensitiveFileNames(), map); + // return { + // getEntries(dir) { + // return io.getAccessibleFileSystemEntries(mapper(dir.path)); + // }, + // getContent(file) { + // return io.readFile(mapper(file.path)); + // } + // }; + // } + + export type FindAxis = "ancestors" | "ancestors-or-self" | "self" | "descendents-or-self" | "descendents"; + + export abstract class VirtualFileSystemEntry { + private _readonly = false; + private _path: string; + private _metadata: Metadata; + + /** + * Gets the name of this entry. + */ + public readonly name: string; + + constructor(name: string) { + this.name = name; + } + + /** + * Gets the file system to which this entry belongs. + */ + public get fileSystem(): VirtualFileSystem { + return this.parent.fileSystem; + } + + /** + * Gets the container for this entry. + */ + public abstract get parent(): VirtualFileSystemContainer; + + /** + * Gets the entry that this entry shadows. + */ + public abstract get shadowRoot(): VirtualFileSystemEntry | undefined; + + /** + * Gets metadata about this entry. + */ + public get metadata(): Metadata { + return this._metadata || (this._metadata = new Metadata(this.shadowRoot ? this.shadowRoot.metadata : undefined)); + } + + /** + * Gets a value indicating whether this entry is read-only. + */ + public get isReadOnly(): boolean { + return this._readonly; + } + + public get path(): string { + return this._path || (this._path = vpath.combine(this.parent.path, this.name)); + } + + public get relative(): string { + return this.relativeTo(this.fileSystem.currentDirectory); + } + + public get exists(): boolean { + return this.parent.exists + && this.parent.getEntry(this.name) as VirtualFileSystemEntry === this; + } + + public makeReadOnly(): void { + this.makeReadOnlyCore(); + this._readonly = true; + } + + public relativeTo(other: string | VirtualFileSystemEntry) { + if (other) { + const otherPath = typeof other === "string" ? other : other.path; + const ignoreCase = !this.fileSystem.useCaseSensitiveFileNames; + return vpath.relative(otherPath, this.path, ignoreCase); + } + return this.path; + } + + /** + * Creates a file system entry that shadows this file system entry. + * @param parent The container for the shadowed entry. + */ + public abstract shadow(parent: VirtualFileSystemContainer): VirtualFileSystemEntry; + + protected abstract makeReadOnlyCore(): void; + + protected writePreamble(): void { + if (this._readonly) throw new Error("Cannot modify a frozen entry."); + } + + protected shadowPreamble(parent: VirtualFileSystemContainer): void { + if (this.parent !== parent.shadowRoot) throw new Error("Incorrect shadow parent"); + let fileSystem: VirtualFileSystem | undefined = this.fileSystem; + while (fileSystem) { + if (parent.fileSystem === fileSystem) throw new Error("Cannot create shadow for parent in the same file system."); + fileSystem = fileSystem.shadowRoot; + } + } + } + + export abstract class VirtualFileSystemContainer extends VirtualFileSystemEntry { + private _childAddedCallbacks: ((entry: VirtualFile | VirtualDirectory) => void)[]; + private _childRemovedCallbacks: ((entry: VirtualFile | VirtualDirectory) => void)[]; + + public abstract get shadowRoot(): VirtualFileSystemContainer | undefined; + + public addOnChildAdded(callback: (entry: VirtualFile | VirtualDirectory) => void) { + if (!this._childAddedCallbacks) { + this._childAddedCallbacks = [callback]; + } + else if (this._childAddedCallbacks.indexOf(callback) === -1) { + this._childAddedCallbacks.push(callback); + } + } + + public removeOnChildAdded(callback: (entry: VirtualFile | VirtualDirectory) => void) { + if (this._childAddedCallbacks) { + const index = this._childAddedCallbacks.indexOf(callback); + if (index >= 0) removeAt(this._childAddedCallbacks, index); + } + } + + public addOnChildRemoved(callback: (entry: VirtualFile | VirtualDirectory) => void) { + if (!this._childRemovedCallbacks) { + this._childRemovedCallbacks = [callback]; + } + else if (this._childRemovedCallbacks.indexOf(callback) === -1) { + this._childRemovedCallbacks.push(callback); + } + } + + public removeOnChildRemoved(callback: (entry: VirtualFile | VirtualDirectory) => void) { + if (this._childRemovedCallbacks) { + const index = this._childRemovedCallbacks.indexOf(callback); + if (index >= 0) removeAt(this._childRemovedCallbacks, index); + } + } + + public getEntries(options: { recursive?: boolean, pattern?: RegExp, kind: "file" }): VirtualFile[]; + public getEntries(options: { recursive?: boolean, pattern?: RegExp, kind: "directory" }): VirtualDirectory[]; + public getEntries(options?: { recursive?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): (VirtualFile | VirtualDirectory)[]; + public getEntries(options: { recursive?: boolean, pattern?: RegExp, kind?: "file" | "directory" } = {}): (VirtualFile | VirtualDirectory)[] { + const results: (VirtualFile | VirtualDirectory)[] = []; + if (options.recursive) { + this.getOwnEntries().forEach(entry => { + if (entry instanceof VirtualFile) { + if (isMatch(entry, options)) { + results.push(entry); + } + } + else if (entry instanceof VirtualDirectory) { + if (isMatch(entry, options)) { + results.push(entry); + } + for (const child of entry.getEntries(options)) { + results.push(child); + } + } + }); + } + else { + this.getOwnEntries().forEach(entry => { + if (isMatch(entry, options)) { + results.push(entry); + } + }); + } + return results; + } + + public getDirectories(options: { recursive?: boolean, pattern?: RegExp } = {}): VirtualDirectory[] { + return this.getEntries({ ...options, kind: "directory" }); + } + + public getFiles(options: { recursive?: boolean, pattern?: RegExp } = {}): VirtualFile[] { + return this.getEntries({ ...options, kind: "file" }); + } + + public getEntryNames(options: { recursive?: boolean, qualified?: boolean, pattern?: RegExp, kind?: "file" | "directory" } = {}): string[] { + return this.getEntries(options).map(entry => + options && options.qualified ? entry.path : + options && options.recursive ? entry.relativeTo(this) : + entry.name); + } + + public getDirectoryNames(options: { recursive?: boolean, qualified?: boolean, pattern?: RegExp } = {}): string[] { + return this.getEntryNames({ ...options, kind: "directory" }); + } + + public getFileNames(options: { recursive?: boolean, qualified?: boolean, pattern?: RegExp } = {}): string[] { + return this.getEntryNames({ ...options, kind: "file" }); + } + + public abstract getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "file" }): VirtualFile | undefined; + public abstract getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "directory" }): VirtualDirectory | undefined; + public abstract getEntry(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): VirtualFile | VirtualDirectory | undefined; + + public getDirectory(path: string, options: { followSymlinks?: boolean, pattern?: RegExp } = {}): VirtualDirectory | undefined { + return this.getEntry(path, { ...options, kind: "directory" }); + } + + public getFile(path: string, options: { followSymlinks?: boolean, pattern?: RegExp } = {}): VirtualFile | undefined { + return this.getEntry(path, { ...options, kind: "file" }); + } + + public findEntry(path: string, axis: FindAxis, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "file" }): VirtualFile | undefined; + public findEntry(path: string, axis: FindAxis, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "directory" }): VirtualDirectory | undefined; + public findEntry(path: string, axis: FindAxis, options?: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): VirtualFile | VirtualDirectory | undefined; + public findEntry(path: string, axis: FindAxis, options: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" } = {}): VirtualFile | VirtualDirectory | undefined { + const walkAncestors = axis === "ancestors-or-self" || axis === "ancestors"; + const walkDescendents = axis === "descendents-or-self" || axis === "descendents"; + const walkSelf = axis === "ancestors-or-self" || axis === "self" || axis === "descendents-or-self"; + if (walkSelf) { + const entry = this.getEntry(path, options); + if (entry) return entry; + } + if (walkAncestors) { + const entry = !(this instanceof VirtualFileSystem) && this.parent.findEntry(path, "ancestors-or-self", options); + if (entry) return entry; + } + if (walkDescendents) { + for (const child of this.getDirectories()) { + const entry = child.findEntry(path, "descendents-or-self", options); + if (entry) return entry; + } + } + } + + public findDirectory(path: string, axis: FindAxis, options: { followSymlinks?: boolean, pattern?: RegExp } = {}): VirtualDirectory | undefined { + return this.findEntry(path, axis, { ...options, kind: "directory" }); + } + + public findFile(path: string, axis: FindAxis, options: { followSymlinks?: boolean, pattern?: RegExp } = {}): VirtualFile | undefined { + return this.findEntry(path, axis, { ...options, kind: "file" }); + } + + protected abstract getOwnEntries(): KeyedCollection; + + protected raiseOnChildAdded(entry: VirtualFile | VirtualDirectory) { + if (this._childAddedCallbacks) { + for (const callback of this._childAddedCallbacks) { + callback(entry); + } + } + } + + protected raiseOnChildRemoved(entry: VirtualFile | VirtualDirectory) { + if (this._childRemovedCallbacks) { + for (const callback of this._childRemovedCallbacks) { + callback(entry); + } + } + } + } + + export class VirtualFileSystem extends VirtualFileSystemContainer { + // private static _builtLocal: VirtualFileSystem | undefined; + // private static _builtLocalCI: VirtualFileSystem | undefined; + // private static _builtLocalCS: VirtualFileSystem | undefined; + + private _root: VirtualDirectory; + private _useCaseSensitiveFileNames: boolean; + private _currentDirectory: string; + private _currentDirectoryStack: string[] | undefined; + private _shadowRoot: VirtualFileSystem | undefined; + + constructor(currentDirectory: string, useCaseSensitiveFileNames: boolean) { + super(""); + this._currentDirectory = currentDirectory.replace(/\\/g, "/"); + this._useCaseSensitiveFileNames = useCaseSensitiveFileNames; + } + + public get fileSystem(): VirtualFileSystem { + return this; + } + + public get parent(): VirtualFileSystemContainer { + return this; + } + + public get shadowRoot(): VirtualFileSystem | undefined { + return this._shadowRoot; + } + + public get useCaseSensitiveFileNames() { + return this._useCaseSensitiveFileNames; + } + + public get currentDirectory() { + return this._currentDirectory; + } + + public get path() { + return ""; + } + + public get relative() { + return ""; + } + + public get exists() { + return true; + } + + public get root() { + if (this._root === undefined) { + if (this._shadowRoot) { + this._root = this._shadowRoot.root.shadow(this); + } + else { + this._root = new VirtualDirectory(this, ""); + } + if (this.isReadOnly) this._root.makeReadOnly(); + } + return this._root; + } + + // public static getBuiltLocal(useCaseSensitiveFileNames: boolean = io.useCaseSensitiveFileNames()): VirtualFileSystem { + // let vfs = useCaseSensitiveFileNames ? this._builtLocalCS : this._builtLocalCI; + // if (!vfs) { + // vfs = this._builtLocal; + // if (!vfs) { + // const resolver = createResolver(io, { + // "/.ts": getBuiltDirectory(), + // "/.lib": getLibFilesDirectory() + // }); + // vfs = new VirtualFileSystem("/", io.useCaseSensitiveFileNames()); + // vfs.addDirectory("/.ts", resolver); + // vfs.addDirectory("/.lib", resolver); + // vfs.addDirectory("/.test"); + // vfs.changeDirectory("/.test"); + // vfs.makeReadOnly(); + // this._builtLocal = vfs; + // } + // if (vfs._useCaseSensitiveFileNames !== useCaseSensitiveFileNames) { + // vfs = vfs.shadow(); + // vfs._useCaseSensitiveFileNames = useCaseSensitiveFileNames; + // vfs.makeReadOnly(); + // } + // return useCaseSensitiveFileNames + // ? this._builtLocalCS = vfs + // : this._builtLocalCI = vfs; + // } + // return vfs; + // } + + // public static createFromOptions(options: { useCaseSensitiveFileNames?: boolean, currentDirectory?: string }) { + // const vfs = this.getBuiltLocal(options.useCaseSensitiveFileNames).shadow(); + // if (options.currentDirectory) { + // vfs.addDirectory(options.currentDirectory); + // vfs.changeDirectory(options.currentDirectory); + // } + // return vfs; + // } + + // public static createFromDocuments(options: { useCaseSensitiveFileNames?: boolean, currentDirectory?: string }, documents: TextDocument[], fileOptions?: { overwrite?: boolean }) { + // const vfs = this.createFromOptions(options); + // for (const document of documents) { + // const file = vfs.addFile(document.file, document.text, fileOptions)!; + // assert.isDefined(file, `Failed to add file: '${document.file}'`); + // file.metadata.set("document", document); + // // Add symlinks + // const symlink = document.meta.get("symlink"); + // if (file && symlink) { + // for (const link of symlink.split(",")) { + // const symlink = vfs.addSymlink(vpath.resolve(vfs.currentDirectory, link.trim()), file)!; + // assert.isDefined(symlink, `Failed to symlink: '${link}'`); + // symlink.metadata.set("document", document); + // } + // } + // } + // return vfs; + // } + + public changeDirectory(path: string) { + this.writePreamble(); + if (path) { + this._currentDirectory = vpath.resolve(this._currentDirectory, path); + } + } + + public pushDirectory(path = this.currentDirectory) { + this.writePreamble(); + if (this._currentDirectoryStack === undefined) { + this._currentDirectoryStack = [this.currentDirectory]; + } + else { + this._currentDirectoryStack.push(this.currentDirectory); + } + this.changeDirectory(path); + } + + public popDirectory() { + this.writePreamble(); + const previousDirectory = this._currentDirectoryStack && this._currentDirectoryStack.pop(); + if (previousDirectory !== undefined) { + this._currentDirectory = previousDirectory; + } + } + + public addDirectory(path: string /*, resolver?: FileSystemResolver */) { + return this.root.addDirectory(vpath.resolve(this.currentDirectory, path) /*, resolver */); + } + + public addFile(path: string, content?: /*FileSystemResolver["getContent"] |*/ string, options?: { overwrite?: boolean }) { + return this.root.addFile(vpath.resolve(this.currentDirectory, path), content, options); + } + + public addSymlink(path: string, target: VirtualFile): VirtualFileSymlink | undefined; + public addSymlink(path: string, target: VirtualDirectory): VirtualDirectorySymlink | undefined; + public addSymlink(path: string, target: string | VirtualFile | VirtualDirectory): VirtualSymlink | undefined; + public addSymlink(path: string, target: string | VirtualFile | VirtualDirectory) { + if (typeof target === "string") target = vpath.resolve(this.currentDirectory, target); + return this.root.addSymlink(vpath.resolve(this.currentDirectory, path), target); + } + + public removeDirectory(path: string): boolean { + return this.root.removeDirectory(vpath.resolve(this.currentDirectory, path)); + } + + public removeFile(path: string): boolean { + return this.root.removeFile(vpath.resolve(this.currentDirectory, path)); + } + + public readFile(path: string): string | undefined { + const file = this.getFile(vpath.resolve(this.currentDirectory, path)); + return file && file.getContent(); + } + + public writeFile(path: string, content: string): void { + path = vpath.resolve(this.currentDirectory, path); + const file = this.getFile(path) || this.addFile(path); + if (file) { + file.setContent(content); + } + } + + public directoryExists(path: string) { + return this.getEntry(path) instanceof VirtualDirectory; + } + + public fileExists(path: string) { + return this.getEntry(path) instanceof VirtualFile; + } + + public sameName(a: string, b: string) { + return compareStrings(a, b, !this.useCaseSensitiveFileNames) === 0; + } + + public getRealEntry(entry: VirtualDirectory): VirtualDirectory | undefined; + public getRealEntry(entry: VirtualFile): VirtualFile | undefined; + public getRealEntry(entry: VirtualFile | VirtualDirectory): VirtualFile | VirtualDirectory | undefined; + public getRealEntry(entry: VirtualFile | VirtualDirectory): VirtualFile | VirtualDirectory | undefined { + if (entry instanceof VirtualFileSymlink || entry instanceof VirtualDirectorySymlink) { + return findTarget(this, entry.target); + } + return entry; + } + + public getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "file" }): VirtualFile | undefined; + public getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "directory" }): VirtualDirectory | undefined; + public getEntry(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): VirtualFile | VirtualDirectory | undefined; + public getEntry(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" }) { + return this.root.getEntry(vpath.resolve(this.currentDirectory, path), options); + } + + public getFile(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp }): VirtualFile | undefined { + return this.root.getFile(vpath.resolve(this.currentDirectory, path), options); + } + + public getDirectory(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp }): VirtualDirectory | undefined { + return this.root.getDirectory(vpath.resolve(this.currentDirectory, path), options); + } + + public getAccessibleFileSystemEntries(path: string) { + const entry = this.getEntry(path); + if (entry instanceof VirtualDirectory) { + return { + files: entry.getFiles().map(f => f.name), + directories: entry.getDirectories().map(d => d.name) + }; + } + return { files: [], directories: [] }; + } + + public shadow(): VirtualFileSystem { + const fs = new VirtualFileSystem(this.currentDirectory, this.useCaseSensitiveFileNames); + fs._shadowRoot = this; + return fs; + } + + public debugPrint(): void { + console.log(`cwd: ${this.currentDirectory}`); + for (const entry of this.getEntries({ recursive: true })) { + if (entry instanceof VirtualDirectory) { + console.log(entry.path.endsWith("/") ? entry.path : entry.path + "/"); + if (entry instanceof VirtualDirectorySymlink) { + console.log(`-> ${entry.target.endsWith("/") ? entry.target : entry.target + "/"}`); + } + } + else { + console.log(entry.path); + if (entry instanceof VirtualFileSymlink) { + console.log(`-> ${entry.target}`); + } + } + } + } + + protected makeReadOnlyCore() { + this.root.makeReadOnly(); + } + + protected getOwnEntries() { + return this.root["getOwnEntries"](); + } + } + + export class VirtualDirectory extends VirtualFileSystemContainer { + protected _shadowRoot: VirtualDirectory | undefined; + private _parent: VirtualFileSystemContainer; + private _entries: KeyedCollection | undefined; + // private _resolver: FileSystemResolver | undefined; + + constructor(parent: VirtualFileSystemContainer, name: string /*, resolver?: FileSystemResolver */) { + super(name); + this._parent = parent; + this._entries = undefined; + // this._resolver = resolver; + this._shadowRoot = undefined; + } + + public get parent(): VirtualFileSystemContainer { + return this._parent; + } + + public get shadowRoot(): VirtualDirectory | undefined { + return this._shadowRoot; + } + + public getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "file" }): VirtualFile | undefined; + public getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "directory" }): VirtualDirectory | undefined; + public getEntry(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): VirtualFile | VirtualDirectory | undefined; + public getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" } = {}): VirtualFile | VirtualDirectory | undefined { + const components = this.parsePath(path); + const directory = this.walkContainers(components, /*create*/ false); + return directory && directory.getOwnEntry(components[components.length - 1], options); + } + + public addDirectory(path: string /*, resolver?: FileSystemResolver*/): VirtualDirectory | undefined { + this.writePreamble(); + const components = this.parsePath(path); + const directory = this.walkContainers(components, /*create*/ true); + return directory && directory.addOwnDirectory(components[components.length - 1] /*, resolver*/); + } + + public addFile(path: string, content?: /*FileSystemResolver["getContent"] |*/ string | undefined, options?: { overwrite?: boolean }): VirtualFile | undefined { + this.writePreamble(); + const components = this.parsePath(path); + const directory = this.walkContainers(components, /*create*/ true); + return directory && directory.addOwnFile(components[components.length - 1], content, options); + } + + public addSymlink(path: string, target: VirtualFile): VirtualFileSymlink | undefined; + public addSymlink(path: string, target: VirtualDirectory): VirtualDirectorySymlink | undefined; + public addSymlink(path: string, target: string | VirtualFile | VirtualDirectory): VirtualSymlink | undefined; + public addSymlink(path: string, target: string | VirtualFile | VirtualDirectory): VirtualSymlink | undefined { + this.writePreamble(); + const targetEntry = typeof target === "string" ? this.fileSystem.getEntry(vpath.resolve(this.path, target)) : target; + if (targetEntry === undefined) return undefined; + const components = this.parsePath(path); + const directory = this.walkContainers(components, /*create*/ true); + return directory && directory.addOwnSymlink(components[components.length - 1], targetEntry); + } + + public removeDirectory(path: string): boolean { + this.writePreamble(); + const components = this.parsePath(path); + const directory = this.walkContainers(components, /*create*/ false); + return directory ? directory.removeOwnDirectory(components[components.length - 1]) : false; + } + + public removeFile(path: string): boolean { + this.writePreamble(); + this.writePreamble(); + const components = this.parsePath(path); + const directory = this.walkContainers(components, /*create*/ false); + return directory ? directory.removeOwnFile(components[components.length - 1]) : false; + } + + public shadow(parent: VirtualFileSystemContainer): VirtualDirectory { + this.shadowPreamble(parent); + const shadow = new VirtualDirectory(parent, this.name); + shadow._shadowRoot = this; + return shadow; + } + + protected makeReadOnlyCore(): void { + if (this._entries) { + this._entries.forEach(entry => entry.makeReadOnly()); + } + } + + protected getOwnEntries() { + if (!this._entries) { + // const resolver = this._resolver; + const entries = new KeyedCollection(this.fileSystem.useCaseSensitiveFileNames ? compareStrings.caseSensitive : compareStrings.caseInsensitive); + // this._resolver = undefined; + /*if (resolver) { + const { files, directories } = resolver.getEntries(this); + for (const dir of directories) { + const vdir = new VirtualDirectory(this, dir, resolver); + if (this.isReadOnly) vdir.makeReadOnly(); + entries.set(vdir.name, vdir); + } + for (const file of files) { + const vfile = new VirtualFile(this, file, file => resolver.getContent(file)); + if (this.isReadOnly) vfile.makeReadOnly(); + entries.set(vfile.name, vfile); + } + } + else*/ if (this._shadowRoot) { + this._shadowRoot.getOwnEntries().forEach(entry => { + const clone = (entry).shadow(this); + if (this.isReadOnly) clone.makeReadOnly(); + entries.set(clone.name, clone); + }); + } + this._entries = entries; + } + return this._entries; + } + + private parsePath(path: string) { + return vpath.parse(vpath.normalize(path)); + } + + private walkContainers(components: string[], create: boolean) { + // no absolute paths (unless this is the root) + if (!!components[0] === !(this.parent instanceof VirtualFileSystem)) return undefined; + + // no relative paths + if (components[1] === "..") return undefined; + + // walk the components + let directory: VirtualDirectory | undefined = this; + for (let i = this.parent instanceof VirtualFileSystem ? 0 : 1; i < components.length - 1; i++) { + directory = create ? directory.getOrAddOwnDirectory(components[i]) : directory.getOwnDirectory(components[i]); + if (directory === undefined) return undefined; + } + + return directory; + } + + private getOwnEntry(name: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "file" }): VirtualFile | undefined; + private getOwnEntry(name: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "directory" }): VirtualDirectory | undefined; + private getOwnEntry(name: string, options?: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): VirtualFile | VirtualDirectory | undefined; + private getOwnEntry(name: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" } = {}): VirtualFile | VirtualDirectory | undefined { + const entry = this.getOwnEntries().get(name); + return entry && isMatch(entry, options) ? options.followSymlinks ? this.fileSystem.getRealEntry(entry) : entry : undefined; + } + + private getOwnDirectory(name: string) { + return this.getOwnEntry(name, { kind: "directory" }); + } + + private getOrAddOwnDirectory(name: string) { + return this.getOwnDirectory(name) || this.addOwnDirectory(name); + } + + private addOwnDirectory(name: string /*, resolver?: FileSystemResolver */): VirtualDirectory | undefined { + const existing = this.getOwnEntry(name); + if (existing) { + if (/*!resolver &&*/ existing instanceof VirtualDirectory) { + return existing; + } + return undefined; + } + + const entry = new VirtualDirectory(this, name /*, resolver */); + this.getOwnEntries().set(entry.name, entry); + this.raiseOnChildAdded(entry); + return entry; + } + + private addOwnFile(name: string, content?: /*FileSystemResolver["getContent"]*/ | string | undefined, options: { overwrite?: boolean } = {}): VirtualFile | undefined { + const existing = this.getOwnEntry(name); + if (existing) { + if (!options.overwrite || !(existing instanceof VirtualFile)) { + return undefined; + } + + // Remove the existing entry + this.getOwnEntries().delete(name); + } + + const entry = new VirtualFile(this, name, content); + this.getOwnEntries().set(entry.name, entry); + this.raiseOnChildAdded(entry); + return entry; + } + + private addOwnSymlink(name: string, target: VirtualFile | VirtualDirectory): VirtualSymlink | undefined { + if (this.getOwnEntry(name)) return undefined; + const entry = target instanceof VirtualFile ? new VirtualFileSymlink(this, name, target.path) : new VirtualDirectorySymlink(this, name, target.path); + this.getOwnEntries().set(entry.name, entry); + this.raiseOnChildAdded(entry); + return entry; + } + + private removeOwnDirectory(name: string) { + const entries = this.getOwnEntries(); + const entry = entries.get(name); + if (entry instanceof VirtualDirectory) { + entries.delete(name); + this.raiseOnChildRemoved(entry); + return true; + } + return false; + } + + private removeOwnFile(name: string) { + const entries = this.getOwnEntries(); + const entry = entries.get(name); + if (entry instanceof VirtualFile) { + entries.delete(name); + this.raiseOnChildRemoved(entry); + return true; + } + return false; + } + } + + export class VirtualDirectorySymlink extends VirtualDirectory { + private _targetPath: string; + private _target: VirtualDirectory | undefined; + private _symLinks = new Map(); + private _symEntries: KeyedCollection | undefined; + private _onTargetParentChildRemoved: (entry: VirtualFile | VirtualDirectory) => void; + private _onTargetChildRemoved: (entry: VirtualFile | VirtualDirectory) => void; + private _onTargetChildAdded: (entry: VirtualFile | VirtualDirectory) => void; + + constructor(parent: VirtualFileSystemContainer, name: string, target: string) { + super(parent, name); + this._targetPath = target; + this._onTargetParentChildRemoved = entry => this.onTargetParentChildRemoved(entry); + this._onTargetChildAdded = entry => this.onTargetChildAdded(entry); + this._onTargetChildRemoved = entry => this.onTargetChildRemoved(entry); + } + + public get target() { + return this._targetPath; + } + + public set target(value: string) { + this.writePreamble(); + if (this._targetPath !== value) { + this._targetPath = value; + this.invalidateTarget(); + } + } + + public get isBroken(): boolean { + return this.getRealDirectory() === undefined; + } + + public getRealDirectory(): VirtualDirectory | undefined { + this.resolveTarget(); + return this._target; + } + + public addDirectory(path: string /*, resolver?: FileSystemResolver*/): VirtualDirectory | undefined { + const target = this.getRealDirectory(); + return target && target.addDirectory(path /*, resolver*/); + } + + public addFile(path: string, content?: /*FileSystemResolver["getContent"]*/ | string | undefined): VirtualFile | undefined { + const target = this.getRealDirectory(); + return target && target.addFile(path, content); + } + + public removeDirectory(path: string): boolean { + const target = this.getRealDirectory(); + return target && target.removeDirectory(path) || false; + } + + public removeFile(path: string): boolean { + const target = this.getRealDirectory(); + return target && target.removeFile(path) || false; + } + + public shadow(parent: VirtualFileSystemContainer): VirtualDirectorySymlink { + this.shadowPreamble(parent); + const shadow = new VirtualDirectorySymlink(parent, this.name, this.target); + shadow._shadowRoot = this; + return shadow; + } + + public resolveTarget(): void { + if (!this._target) { + const entry = findTarget(this.fileSystem, this.target); + if (entry instanceof VirtualDirectory) { + this._target = entry; + this._target.parent.addOnChildRemoved(this._onTargetParentChildRemoved); + this._target.addOnChildAdded(this._onTargetChildAdded); + this._target.addOnChildRemoved(this._onTargetChildRemoved); + } + } + } + + protected getOwnEntries(): KeyedCollection { + if (!this._symEntries) { + const target = this.getRealDirectory(); + this._symEntries = new KeyedCollection(this.fileSystem.useCaseSensitiveFileNames ? compareStrings.caseSensitive : compareStrings.caseInsensitive); + if (target) { + for (const entry of target.getEntries()) { + this._symEntries.set(entry.name, this.getWrappedEntry(entry)); + } + } + } + return this._symEntries; + } + + private getWrappedEntry(entry: VirtualFile | VirtualDirectory) { + let symlink = this._symLinks.get(entry); + if (entry instanceof VirtualFile) { + if (symlink instanceof VirtualFileSymlink) { + return symlink; + } + symlink = new VirtualFileSymlink(this, entry.name, entry.path); + this._symLinks.set(entry, symlink); + } + else { + if (symlink instanceof VirtualDirectorySymlink) { + return symlink; + } + symlink = new VirtualDirectorySymlink(this, entry.name, entry.path); + this._symLinks.set(entry, symlink); + } + return symlink; + } + + private onTargetParentChildRemoved(entry: VirtualFileSystemEntry) { + if (entry !== this._target) return; + this.invalidateTarget(); + } + + private onTargetChildAdded(entry: VirtualFile | VirtualDirectory) { + const wrapped = this.getWrappedEntry(entry); + this.getOwnEntries().set(entry.name, wrapped); + this.raiseOnChildAdded(wrapped); + } + + private onTargetChildRemoved(entry: VirtualFile | VirtualDirectory) { + const wrapped = this.getWrappedEntry(entry); + this.getOwnEntries().delete(entry.name); + this._symLinks.delete(entry); + this.raiseOnChildRemoved(wrapped); + } + + private invalidateTarget() { + if (!this._target) return; + this._target.parent.removeOnChildRemoved(this._onTargetParentChildRemoved); + this._target.removeOnChildAdded(this._onTargetChildAdded); + this._target.removeOnChildRemoved(this._onTargetChildRemoved); + this._target = undefined; + this._symLinks.clear(); + this._symEntries = undefined; + } + } + + export class VirtualFile extends VirtualFileSystemEntry { + protected _shadowRoot: VirtualFile | undefined; + private _parent: VirtualDirectory; + private _content: string | undefined; + private _contentWasSet: boolean; + // private _resolver: FileSystemResolver["getContent"] | undefined; + + constructor(parent: VirtualDirectory, name: string, content?: /*FileSystemResolver["getContent"]*/ | string | undefined) { + super(name); + this._parent = parent; + this._content = typeof content === "string" ? content : undefined; + // this._resolver = typeof content === "function" ? content : undefined; + this._shadowRoot = undefined; + this._contentWasSet = this._content !== undefined; + } + + public get parent(): VirtualDirectory { + return this._parent; + } + + public get shadowRoot(): VirtualFile | undefined { + return this._shadowRoot; + } + + public getContent(): string | undefined { + if (!this._contentWasSet) { + // const resolver = this._resolver; + const shadowRoot = this._shadowRoot; + /* if (resolver) { + this._content = resolver(this); + this._contentWasSet = true; + } + else */ if (shadowRoot) { + this._content = shadowRoot.getContent(); + this._contentWasSet = true; + } + } + return this._content; + } + + public setContent(value: string | undefined) { + this.writePreamble(); + // this._resolver = undefined; + this._content = value; + this._contentWasSet = true; + } + + public shadow(parent: VirtualDirectory): VirtualFile { + this.shadowPreamble(parent); + const shadow = new VirtualFile(parent, this.name); + shadow._shadowRoot = this; + shadow._contentWasSet = false; + return shadow; + } + + protected makeReadOnlyCore(): void { + } + } + + export class VirtualFileSymlink extends VirtualFile { + private _target: string; + + constructor(parent: VirtualDirectory, name: string, target: string) { + super(parent, name); + this._target = target; + } + + public get target(): string { + return this._target; + } + + public set target(value: string) { + this.writePreamble(); + this._target = value; + } + + public get isBroken(): boolean { + return this.getRealFile() === undefined; + } + + public getRealFile(): VirtualFile | undefined { + const entry = findTarget(this.fileSystem, this.target); + return entry instanceof VirtualFile ? entry : undefined; + } + + public getContent(): string | undefined { + const target = this.getRealFile(); + return target && target.getContent(); + } + + public setContent(value: string | undefined) { + const target = this.getRealFile(); + if (target) target.setContent(value); + } + + public shadow(parent: VirtualDirectory) { + this.shadowPreamble(parent); + const shadow = new VirtualFileSymlink(parent, this.name, this.target); + shadow._shadowRoot = this; + return shadow; + } + } + + export type VirtualSymlink = VirtualDirectorySymlink | VirtualFileSymlink; + + function findTarget(vfs: VirtualFileSystem, target: string, set?: Set): VirtualFile | VirtualDirectory | undefined { + const entry = vfs.getEntry(target); + if (entry instanceof VirtualFileSymlink || entry instanceof VirtualDirectorySymlink) { + if (!set) set = new Set(); + if (set.has(entry)) return undefined; + set.add(entry); + return findTarget(vfs, entry.target, set); + } + return entry; + } + + function isMatch(entry: VirtualFile | VirtualDirectory, options: { pattern?: RegExp, kind?: "file" | "directory" }) { + return (options.pattern === undefined || options.pattern.test(entry.name)) + && (options.kind !== (entry instanceof VirtualFile ? "directory" : "file")); + } + + // TODO(rbuckton): Move or retire this. + export class MockParseConfigHost extends VirtualFileSystem implements ts.ParseConfigHost { + constructor(currentDirectory: string, ignoreCase: boolean, files: ts.Map | string[]) { + super(currentDirectory, ignoreCase); + if (files instanceof Array) { + for (const file of files) { + // this.addFile(file, new Harness.LanguageService.ScriptInfo(file, undefined, /*isRootFile*/false)); + this.addFile(file).metadata.set("scriptInfo", new Harness.LanguageService.ScriptInfo(file, undefined, /*isRootFile*/ false)); + } + } + else { + files.forEach((fileContent, fileName) => { + this.addFile(fileName, fileContent).metadata.set("scriptInfo", new Harness.LanguageService.ScriptInfo(fileName, fileContent, /*isRootFile*/ false)); + }); + } + } + + readDirectory(path: string, extensions: ReadonlyArray, excludes: ReadonlyArray, includes: ReadonlyArray, depth: number) { + return ts.matchFiles(path, extensions, excludes, includes, this.useCaseSensitiveFileNames, this.currentDirectory, depth, (path: string) => this.getAccessibleFileSystemEntries(path)); + } + } +} \ No newline at end of file diff --git a/src/harness/virtualFileSystem.ts b/src/harness/virtualFileSystem.ts deleted file mode 100644 index 8accd6f621e..00000000000 --- a/src/harness/virtualFileSystem.ts +++ /dev/null @@ -1,223 +0,0 @@ -/// -/// -namespace Utils { - export class VirtualFileSystemEntry { - fileSystem: VirtualFileSystem; - name: string; - - constructor(fileSystem: VirtualFileSystem, name: string) { - this.fileSystem = fileSystem; - this.name = name; - } - - isDirectory(): this is VirtualDirectory { return false; } - isFile(): this is VirtualFile { return false; } - isFileSystem(): this is VirtualFileSystem { return false; } - } - - export class VirtualFile extends VirtualFileSystemEntry { - content?: Harness.LanguageService.ScriptInfo; - isFile() { return true; } - } - - export abstract class VirtualFileSystemContainer extends VirtualFileSystemEntry { - abstract getFileSystemEntries(): VirtualFileSystemEntry[]; - - getFileSystemEntry(name: string): VirtualFileSystemEntry { - for (const entry of this.getFileSystemEntries()) { - if (this.fileSystem.sameName(entry.name, name)) { - return entry; - } - } - return undefined; - } - - getDirectories(): VirtualDirectory[] { - return ts.filter(this.getFileSystemEntries(), entry => entry.isDirectory()); - } - - getFiles(): VirtualFile[] { - return ts.filter(this.getFileSystemEntries(), entry => entry.isFile()); - } - - getDirectory(name: string): VirtualDirectory { - const entry = this.getFileSystemEntry(name); - return entry.isDirectory() ? entry : undefined; - } - - getFile(name: string): VirtualFile { - const entry = this.getFileSystemEntry(name); - return entry.isFile() ? entry : undefined; - } - } - - export class VirtualDirectory extends VirtualFileSystemContainer { - private entries: VirtualFileSystemEntry[] = []; - - isDirectory() { return true; } - - getFileSystemEntries() { return this.entries.slice(); } - - addDirectory(name: string): VirtualDirectory { - const entry = this.getFileSystemEntry(name); - if (entry === undefined) { - const directory = new VirtualDirectory(this.fileSystem, name); - this.entries.push(directory); - return directory; - } - else if (entry.isDirectory()) { - return entry; - } - else { - return undefined; - } - } - - addFile(name: string, content?: Harness.LanguageService.ScriptInfo): VirtualFile { - const entry = this.getFileSystemEntry(name); - if (entry === undefined) { - const file = new VirtualFile(this.fileSystem, name); - file.content = content; - this.entries.push(file); - return file; - } - else if (entry.isFile()) { - entry.content = content; - return entry; - } - else { - return undefined; - } - } - } - - export class VirtualFileSystem extends VirtualFileSystemContainer { - private root: VirtualDirectory; - - currentDirectory: string; - useCaseSensitiveFileNames: boolean; - - constructor(currentDirectory: string, useCaseSensitiveFileNames: boolean) { - super(/*fileSystem*/ undefined, ""); - this.fileSystem = this; - this.root = new VirtualDirectory(this, ""); - this.currentDirectory = currentDirectory; - this.useCaseSensitiveFileNames = useCaseSensitiveFileNames; - } - - isFileSystem() { return true; } - - getFileSystemEntries() { return this.root.getFileSystemEntries(); } - - addDirectory(path: string) { - path = ts.normalizePath(path); - const components = ts.getNormalizedPathComponents(path, this.currentDirectory); - let directory: VirtualDirectory = this.root; - for (const component of components) { - directory = directory.addDirectory(component); - if (directory === undefined) { - break; - } - } - - return directory; - } - - addFile(path: string, content?: Harness.LanguageService.ScriptInfo) { - const absolutePath = ts.normalizePath(ts.getNormalizedAbsolutePath(path, this.currentDirectory)); - const fileName = ts.getBaseFileName(path); - const directoryPath = ts.getDirectoryPath(absolutePath); - const directory = this.addDirectory(directoryPath); - return directory ? directory.addFile(fileName, content) : undefined; - } - - fileExists(path: string) { - const entry = this.traversePath(path); - return entry !== undefined && entry.isFile(); - } - - sameName(a: string, b: string) { - return this.useCaseSensitiveFileNames ? a === b : a.toLowerCase() === b.toLowerCase(); - } - - traversePath(path: string) { - path = ts.normalizePath(path); - let directory: VirtualDirectory = this.root; - for (const component of ts.getNormalizedPathComponents(path, this.currentDirectory)) { - const entry = directory.getFileSystemEntry(component); - if (entry === undefined) { - return undefined; - } - else if (entry.isDirectory()) { - directory = entry; - } - else { - return entry; - } - } - - return directory; - } - - /** - * Reads the directory at the given path and retrieves a list of file names and a list - * of directory names within it. Suitable for use with ts.matchFiles() - * @param path The path to the directory to be read - */ - getAccessibleFileSystemEntries(path: string) { - const entry = this.traversePath(path); - if (entry && entry.isDirectory()) { - const directory = entry; - return { - files: ts.map(directory.getFiles(), f => f.name), - directories: ts.map(directory.getDirectories(), d => d.name) - }; - } - return { files: [], directories: [] }; - } - - getAllFileEntries() { - const fileEntries: VirtualFile[] = []; - getFilesRecursive(this.root, fileEntries); - return fileEntries; - - function getFilesRecursive(dir: VirtualDirectory, result: VirtualFile[]) { - const files = dir.getFiles(); - const dirs = dir.getDirectories(); - for (const file of files) { - result.push(file); - } - for (const subDir of dirs) { - getFilesRecursive(subDir, result); - } - } - } - } - - export class MockParseConfigHost extends VirtualFileSystem implements ts.ParseConfigHost { - constructor(currentDirectory: string, ignoreCase: boolean, files: ts.Map | string[]) { - super(currentDirectory, ignoreCase); - if (files instanceof Array) { - for (const file of files) { - this.addFile(file, new Harness.LanguageService.ScriptInfo(file, undefined, /*isRootFile*/false)); - } - } - else { - files.forEach((fileContent, fileName) => { - this.addFile(fileName, new Harness.LanguageService.ScriptInfo(fileName, fileContent, /*isRootFile*/false)); - }); - } - } - - readFile(path: string): string | undefined { - const value = this.traversePath(path); - if (value && value.isFile()) { - return value.content.content; - } - } - - readDirectory(path: string, extensions: ReadonlyArray, excludes: ReadonlyArray, includes: ReadonlyArray, depth: number) { - return ts.matchFiles(path, extensions, excludes, includes, this.useCaseSensitiveFileNames, this.currentDirectory, depth, (path: string) => this.getAccessibleFileSystemEntries(path)); - } - } -} \ No newline at end of file diff --git a/src/harness/vpath.ts b/src/harness/vpath.ts new file mode 100644 index 00000000000..b2e98a4859f --- /dev/null +++ b/src/harness/vpath.ts @@ -0,0 +1,161 @@ +/// + +namespace vpath { + import compareStrings = ts.compareStrings; + + export function normalizeSlashes(path: string): string { + return path.replace(/\s*[\\/]\s*/g, "/").trim(); + } + + const rootRegExp = /^[\\/]([\\/](.*?[\\/](.*?[\\/])?)?)?|^[a-zA-Z]:[\\/]?|^\w+:\/{2}[^\\/]*\/?/; + + function getRootLength(path: string) { + const match = rootRegExp.exec(path); + return match ? match[0].length : 0; + } + + export function isAbsolute(path: string) { + return rootRegExp.test(path); + } + + const trailingSeperatorRegExp = /[\\/]$/; + + export function hasTrailingSeperator(path: string) { + return trailingSeperatorRegExp.test(path); + } + + function reduce(components: string[]) { + const normalized = [components[0]]; + for (let i = 1; i < components.length; i++) { + const component = components[i]; + if (component === ".") continue; + if (component === ".." && normalized.length > 0 && normalized[normalized.length - 1] !== "..") { + normalized.pop(); + } + else { + normalized.push(component); + } + } + return normalized; + } + + export function normalize(path: string): string { + const components = reduce(parse(path)); + return components.length > 1 && hasTrailingSeperator(path) ? format(components) + "/" : format(components); + } + + export function combine(path: string, ...paths: string[]) { + path = normalizeSlashes(path); + for (let name of paths) { + name = normalizeSlashes(name); + if (name.length === 0) continue; + if (path.length === 0 || isAbsolute(name)) { + path = name; + } + else { + path = hasTrailingSeperator(path) ? path + name : path + "/" + name; + } + } + return path; + } + + export function resolve(path: string, ...paths: string[]) { + return normalize(combine(path, ...paths)); + } + + export function relative(from: string, to: string, ignoreCase: boolean) { + if (!isAbsolute(from)) throw new Error("Path not absolute"); + if (!isAbsolute(to)) throw new Error("Path not absolute"); + + const fromComponents = reduce(parse(from)); + const toComponents = reduce(parse(to)); + + let start: number; + for (start = 0; start < fromComponents.length && start < toComponents.length; start++) { + if (compareStrings(fromComponents[start], toComponents[start], ignoreCase)) { + break; + } + } + + if (start === 0 || (start === 1 && fromComponents[0] === "/")) { + return format(toComponents); + } + + const components = toComponents.slice(start); + for (; start < fromComponents.length; start++) { + components.unshift(".."); + } + + return format(["", ...components]); + } + + export function beneath(ancestor: string, descendant: string, ignoreCase: boolean) { + if (!isAbsolute(ancestor)) throw new Error("Path not absolute"); + if (!isAbsolute(descendant)) throw new Error("Path not absolute"); + + const ancestorComponents = reduce(parse(ancestor)); + const descendantComponents = reduce(parse(descendant)); + + let start: number; + for (start = 0; start < ancestorComponents.length && start < descendantComponents.length; start++) { + if (compareStrings(ancestorComponents[start], descendantComponents[start], ignoreCase)) { + break; + } + } + + return start === ancestorComponents.length; + } + + export function parse(path: string) { + path = normalizeSlashes(path); + const rootLength = getRootLength(path); + const root = path.substring(0, rootLength); + const rest = path.substring(rootLength).split(/\/+/g); + if (rest.length && !rest[rest.length - 1]) rest.pop(); + return [root, ...rest.map(component => component.trim())]; + } + + export function format(components: string[]) { + return components.length ? components[0] + components.slice(1).join("/") : ""; + } + + export function dirname(path: string) { + path = normalizeSlashes(path); + return path.substr(0, Math.max(getRootLength(path), path.lastIndexOf("/"))); + } + + export function basename(path: string, ext?: string): string; + export function basename(path: string, options?: { extensions?: string[], ignoreCase?: boolean }): string; + export function basename(path: string, options?: { extensions?: string[], ignoreCase?: boolean } | string) { + path = normalizeSlashes(path); + const name = path.substr(Math.max(getRootLength(path), path.lastIndexOf("/") + 1)); + const extension = typeof options === "string" ? options.startsWith(".") ? options : "." + options : + options && options.extensions ? extname(name, options) : + undefined; + return extension ? name.slice(0, name.length - extension.length) : name; + } + + const extRegExp = /\.\w+$/; + export function extname(path: string, options?: { extensions?: string[], ignoreCase?: boolean }) { + if (options && options.extensions) { + for (let extension of options.extensions) { + if (!extension.startsWith(".")) extension = "." + extension; + if (path.length > extension.length) { + const ext = path.slice(path.length - extension.length); + if (compareStrings(ext, extension, options.ignoreCase) === 0) { + return ext; + } + } + } + return ""; + } + + const match = extRegExp.exec(path); + return match ? match[0] : ""; + } + + export function chext(path: string, ext: string, options?: { extensions?: string[], ignoreCase?: boolean }) { + const pathext = extname(path, options); + return pathext ? path.slice(0, path.length - pathext.length) + (ext.startsWith(".") ? ext : "." + ext) : path; + } +} \ No newline at end of file diff --git a/tests/cases/fourslash/findAllRefsForModule.ts b/tests/cases/fourslash/findAllRefsForModule.ts index ae6b216463f..cfed3346405 100644 --- a/tests/cases/fourslash/findAllRefsForModule.ts +++ b/tests/cases/fourslash/findAllRefsForModule.ts @@ -18,6 +18,6 @@ verify.noErrors(); const ranges = test.ranges(); const [r0, r1, r2] = ranges; -verify.referenceGroups([r0, r1], [{ definition: 'module "/a"', ranges: [r0, r2, r1] }]); +verify.referenceGroups([r0, r1], [{ definition: 'module "/a"', ranges: [r0, r1, r2] }]); // TODO:GH#15736 verify.referenceGroups(r2, undefined);