Updated vfs

This commit is contained in:
Ron Buckton 2017-10-10 13:07:03 -07:00
parent d23e5f1ee2
commit 5b4553d6a5
11 changed files with 1473 additions and 242 deletions

View File

@ -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",

213
src/harness/collections.ts Normal file
View File

@ -0,0 +1,213 @@
/// <reference path="./harness.ts" />
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<T>(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<K, V> {
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);
}
}
}
}

View File

@ -20,7 +20,7 @@
/// <reference path="..\server\client.ts" />
/// <reference path="sourceMapRecorder.ts"/>
/// <reference path="runnerbase.ts"/>
/// <reference path="virtualFileSystem.ts" />
/// <reference path="vfs.ts" />
/// <reference types="node" />
/// <reference types="mocha" />
/// <reference types="chai" />
@ -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 = <Utils.VirtualDirectory>entry;
return {
files: ts.map(directory.getFiles(), f => f.name),

View File

@ -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() ? (<Utils.VirtualFile>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((<Utils.VirtualDirectory>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 [];
}

View File

@ -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",

View File

@ -1,5 +1,5 @@
/// <reference path="..\harness.ts" />
/// <reference path="..\virtualFileSystem.ts" />
/// <reference path="..\vfs.ts" />
namespace ts {
const testContentsJson = createMapFromTemplate({

View File

@ -1,5 +1,5 @@
/// <reference path="..\harness.ts" />
/// <reference path="..\virtualFileSystem.ts" />
/// <reference path="..\vfs.ts" />
namespace ts {
const caseInsensitiveBasePath = "c:/dev/";

1073
src/harness/vfs.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,223 +0,0 @@
/// <reference path="harness.ts" />
/// <reference path="..\compiler\commandLineParser.ts"/>
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 <VirtualDirectory[]>ts.filter(this.getFileSystemEntries(), entry => entry.isDirectory());
}
getFiles(): VirtualFile[] {
return <VirtualFile[]>ts.filter(this.getFileSystemEntries(), entry => entry.isFile());
}
getDirectory(name: string): VirtualDirectory {
const entry = this.getFileSystemEntry(name);
return entry.isDirectory() ? <VirtualDirectory>entry : undefined;
}
getFile(name: string): VirtualFile {
const entry = this.getFileSystemEntry(name);
return entry.isFile() ? <VirtualFile>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 <VirtualDirectory>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 = <VirtualDirectory>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 = <VirtualDirectory>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> | 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<string>, excludes: ReadonlyArray<string>, includes: ReadonlyArray<string>, depth: number) {
return ts.matchFiles(path, extensions, excludes, includes, this.useCaseSensitiveFileNames, this.currentDirectory, depth, (path: string) => this.getAccessibleFileSystemEntries(path));
}
}
}

161
src/harness/vpath.ts Normal file
View File

@ -0,0 +1,161 @@
/// <reference path="./harness.ts" />
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;
}
}

View File

@ -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);