mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-28 04:23:32 +01:00
363 lines
9.9 KiB
TypeScript
363 lines
9.9 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
'use strict';
|
|
|
|
import assert = require('vs/base/common/assert');
|
|
import URI from 'vs/base/common/uri';
|
|
import paths = require('vs/base/common/paths');
|
|
import { IFileStat, isEqual, isParent } from 'vs/platform/files/common/files';
|
|
|
|
export enum StatType {
|
|
FILE,
|
|
FOLDER,
|
|
ANY
|
|
}
|
|
|
|
export class FileStat implements IFileStat {
|
|
public resource: URI;
|
|
public name: string;
|
|
public mtime: number;
|
|
public etag: string;
|
|
public isDirectory: boolean;
|
|
public hasChildren: boolean;
|
|
public children: FileStat[];
|
|
public parent: FileStat;
|
|
|
|
public isDirectoryResolved: boolean;
|
|
|
|
constructor(resource: URI, isDirectory?: boolean, hasChildren?: boolean, name: string = paths.basename(resource.fsPath), mtime?: number, etag?: string) {
|
|
this.resource = resource;
|
|
this.name = name;
|
|
this.isDirectory = !!isDirectory;
|
|
this.hasChildren = isDirectory && hasChildren;
|
|
this.etag = etag;
|
|
this.mtime = mtime;
|
|
|
|
// Prepare child stat array
|
|
if (this.isDirectory) {
|
|
this.children = [];
|
|
}
|
|
|
|
this.isDirectoryResolved = false;
|
|
}
|
|
|
|
public getId(): string {
|
|
return this.resource.toString();
|
|
}
|
|
|
|
public static create(raw: IFileStat, resolveTo?: URI[]): FileStat {
|
|
const stat = new FileStat(raw.resource, raw.isDirectory, raw.hasChildren, raw.name, raw.mtime, raw.etag);
|
|
|
|
// Recursively add children if present
|
|
if (stat.isDirectory) {
|
|
|
|
// isDirectoryResolved is a very important indicator in the stat model that tells if the folder was fully resolved
|
|
// the folder is fully resolved if either it has a list of children or the client requested this by using the resolveTo
|
|
// array of resource path to resolve.
|
|
stat.isDirectoryResolved = !!raw.children || (!!resolveTo && resolveTo.some((r) => {
|
|
return paths.isEqualOrParent(r.fsPath, stat.resource.fsPath);
|
|
}));
|
|
|
|
// Recurse into children
|
|
if (raw.children) {
|
|
for (let i = 0, len = raw.children.length; i < len; i++) {
|
|
const child = FileStat.create(raw.children[i], resolveTo);
|
|
child.parent = stat;
|
|
stat.children.push(child);
|
|
stat.hasChildren = stat.children.length > 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
return stat;
|
|
}
|
|
|
|
/**
|
|
* Merges the stat which was resolved from the disk with the local stat by copying over properties
|
|
* and children. The merge will only consider resolved stat elements to avoid overwriting data which
|
|
* exists locally.
|
|
*/
|
|
public static mergeLocalWithDisk(disk: FileStat, local: FileStat): void {
|
|
assert.ok(disk.resource.toString() === local.resource.toString(), 'Merging only supported for stats with the same resource');
|
|
|
|
// Stop merging when a folder is not resolved to avoid loosing local data
|
|
const mergingDirectories = disk.isDirectory || local.isDirectory;
|
|
if (mergingDirectories && local.isDirectoryResolved && !disk.isDirectoryResolved) {
|
|
return;
|
|
}
|
|
|
|
// Properties
|
|
local.resource = disk.resource;
|
|
local.name = disk.name;
|
|
local.isDirectory = disk.isDirectory;
|
|
local.hasChildren = disk.isDirectory && disk.hasChildren;
|
|
local.mtime = disk.mtime;
|
|
local.isDirectoryResolved = disk.isDirectoryResolved;
|
|
|
|
// Merge Children if resolved
|
|
if (mergingDirectories && disk.isDirectoryResolved) {
|
|
|
|
// Map resource => stat
|
|
const oldLocalChildren: { [resource: string]: FileStat; } = Object.create(null);
|
|
local.children.forEach((localChild: FileStat) => {
|
|
oldLocalChildren[localChild.resource.toString()] = localChild;
|
|
});
|
|
|
|
// Clear current children
|
|
local.children = [];
|
|
|
|
// Merge received children
|
|
disk.children.forEach((diskChild: FileStat) => {
|
|
const formerLocalChild = oldLocalChildren[diskChild.resource.toString()];
|
|
|
|
// Existing child: merge
|
|
if (formerLocalChild) {
|
|
FileStat.mergeLocalWithDisk(diskChild, formerLocalChild);
|
|
formerLocalChild.parent = local;
|
|
local.children.push(formerLocalChild);
|
|
}
|
|
|
|
// New child: add
|
|
else {
|
|
diskChild.parent = local;
|
|
local.children.push(diskChild);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a deep copy of this model object.
|
|
*/
|
|
public clone(): FileStat {
|
|
const stat = new FileStat(URI.parse(this.resource.toString()), this.isDirectory, this.hasChildren, this.name, this.mtime, this.etag);
|
|
stat.isDirectoryResolved = this.isDirectoryResolved;
|
|
|
|
if (this.parent) {
|
|
stat.parent = this.parent;
|
|
}
|
|
|
|
if (this.isDirectory) {
|
|
this.children.forEach((child: FileStat) => {
|
|
stat.addChild(child.clone());
|
|
});
|
|
}
|
|
|
|
return stat;
|
|
}
|
|
|
|
/**
|
|
* Adds a child element to this folder.
|
|
*/
|
|
public addChild(child: FileStat): void {
|
|
|
|
// Inherit some parent properties to child
|
|
child.parent = this;
|
|
child.updateResource(false);
|
|
|
|
this.children.push(child);
|
|
this.hasChildren = this.children.length > 0;
|
|
}
|
|
|
|
/**
|
|
* Returns true if this stat is a directory that contains a child with the given name.
|
|
*
|
|
* @param ignoreCase if true, will check for the name ignoring case.
|
|
* @param type the type of stat to check for.
|
|
*/
|
|
public hasChild(name: string, ignoreCase?: boolean, type: StatType = StatType.ANY): boolean {
|
|
for (let i = 0; i < this.children.length; i++) {
|
|
const child = this.children[i];
|
|
if ((type === StatType.FILE && child.isDirectory) || (type === StatType.FOLDER && !child.isDirectory)) {
|
|
continue;
|
|
}
|
|
|
|
// Check for Identity
|
|
if (child.name === name) {
|
|
return true;
|
|
}
|
|
|
|
// Also consider comparing without case
|
|
if (ignoreCase && child.name.toLowerCase() === name.toLowerCase()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Removes a child element from this folder.
|
|
*/
|
|
public removeChild(child: FileStat): void {
|
|
for (let i = 0; i < this.children.length; i++) {
|
|
if (this.children[i].resource.toString() === child.resource.toString()) {
|
|
this.children.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.hasChildren = this.children.length > 0;
|
|
}
|
|
|
|
/**
|
|
* Moves this element under a new parent element.
|
|
*/
|
|
public move(newParent: FileStat, fnBetweenStates?: (callback: () => void) => void, fnDone?: () => void): void {
|
|
if (!fnBetweenStates) {
|
|
fnBetweenStates = (cb: () => void) => { cb(); };
|
|
}
|
|
|
|
this.parent.removeChild(this);
|
|
|
|
fnBetweenStates(() => {
|
|
newParent.removeChild(this); // make sure to remove any previous version of the file if any
|
|
newParent.addChild(this);
|
|
this.updateResource(true);
|
|
if (fnDone) {
|
|
fnDone();
|
|
}
|
|
});
|
|
}
|
|
|
|
private updateResource(recursive: boolean): void {
|
|
this.resource = URI.file(paths.join(this.parent.resource.fsPath, this.name));
|
|
|
|
if (recursive) {
|
|
if (this.isDirectory && this.hasChildren && this.children) {
|
|
this.children.forEach((child: FileStat) => {
|
|
child.updateResource(true);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tells this stat that it was renamed. This requires changes to all children of this stat (if any)
|
|
* so that the path property can be updated properly.
|
|
*/
|
|
public rename(renamedStat: IFileStat): void {
|
|
|
|
// Merge a subset of Properties that can change on rename
|
|
this.name = renamedStat.name;
|
|
this.mtime = renamedStat.mtime;
|
|
|
|
// Update Paths including children
|
|
this.updateResource(true);
|
|
}
|
|
|
|
/**
|
|
* Returns a child stat from this stat that matches with the provided path.
|
|
* Will return "null" in case the child does not exist.
|
|
*/
|
|
public find(resource: URI): FileStat {
|
|
|
|
// Return if path found
|
|
if (isEqual(resource.toString(), this.resource.toString())) {
|
|
return this;
|
|
}
|
|
|
|
// Return if not having any children
|
|
if (!this.hasChildren) {
|
|
return null;
|
|
}
|
|
|
|
for (let i = 0; i < this.children.length; i++) {
|
|
const child = this.children[i];
|
|
|
|
if (isEqual(resource.toString(), child.resource.toString())) {
|
|
return child;
|
|
}
|
|
|
|
if (child.isDirectory && isParent(resource.fsPath, child.resource.fsPath)) {
|
|
return child.find(resource);
|
|
}
|
|
}
|
|
|
|
return null; //Unable to find
|
|
}
|
|
}
|
|
|
|
/* A helper that can be used to show a placeholder when creating a new stat */
|
|
export class NewStatPlaceholder extends FileStat {
|
|
|
|
private static ID = 0;
|
|
|
|
private id: number;
|
|
private directoryPlaceholder: boolean;
|
|
|
|
constructor(isDirectory: boolean) {
|
|
super(URI.file(''));
|
|
|
|
this.id = NewStatPlaceholder.ID++;
|
|
this.isDirectoryResolved = isDirectory;
|
|
this.directoryPlaceholder = isDirectory;
|
|
}
|
|
|
|
public destroy(): void {
|
|
this.parent.removeChild(this);
|
|
|
|
this.isDirectoryResolved = void 0;
|
|
this.name = void 0;
|
|
this.isDirectory = void 0;
|
|
this.hasChildren = void 0;
|
|
this.mtime = void 0;
|
|
}
|
|
|
|
public getId(): string {
|
|
return 'new-stat-placeholder:' + this.id + ':' + this.parent.resource.toString();
|
|
}
|
|
|
|
public isDirectoryPlaceholder(): boolean {
|
|
return this.directoryPlaceholder;
|
|
}
|
|
|
|
/**
|
|
* Returns a deep copy of this model object.
|
|
*/
|
|
public clone(): NewStatPlaceholder {
|
|
const stat = new NewStatPlaceholder(this.isDirectory);
|
|
stat.parent = this.parent;
|
|
|
|
return stat;
|
|
}
|
|
|
|
public addChild(child: NewStatPlaceholder): void {
|
|
throw new Error('Can\'t perform operations in NewStatPlaceholder.');
|
|
}
|
|
|
|
public hasChild(name: string, ignoreCase?: boolean): boolean {
|
|
return false;
|
|
}
|
|
|
|
public removeChild(child: NewStatPlaceholder): void {
|
|
throw new Error('Can\'t perform operations in NewStatPlaceholder.');
|
|
}
|
|
|
|
public move(newParent: NewStatPlaceholder): void {
|
|
throw new Error('Can\'t perform operations in NewStatPlaceholder.');
|
|
}
|
|
|
|
public rename(renamedStat: NewStatPlaceholder): void {
|
|
throw new Error('Can\'t perform operations in NewStatPlaceholder.');
|
|
}
|
|
|
|
public find(resource: URI): NewStatPlaceholder {
|
|
return null;
|
|
}
|
|
|
|
public static addNewStatPlaceholder(parent: FileStat, isDirectory: boolean): NewStatPlaceholder {
|
|
const child = new NewStatPlaceholder(isDirectory);
|
|
|
|
// Inherit some parent properties to child
|
|
child.parent = parent;
|
|
parent.children.push(child);
|
|
|
|
parent.hasChildren = parent.children.length > 0;
|
|
|
|
return child;
|
|
}
|
|
} |