From 7d5cd0d249b04f9773f9aebba89c24711963044d Mon Sep 17 00:00:00 2001 From: isidor Date: Tue, 17 Dec 2019 13:05:46 +0100 Subject: [PATCH] Case-sensitivity and file system providers fixes #48258 --- .../browser/editors/fileEditorTracker.ts | 16 ++--- .../contrib/files/browser/fileActions.ts | 8 +-- .../files/browser/views/explorerViewer.ts | 10 ++-- .../contrib/files/common/explorerModel.ts | 41 +++++++------ .../contrib/files/common/explorerService.ts | 59 +++++++++++-------- .../workbench/contrib/files/common/files.ts | 1 + .../electron-browser/explorerModel.test.ts | 19 ++++-- 7 files changed, 92 insertions(+), 62 deletions(-) diff --git a/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts b/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts index a2fd13708d680..a253fbaea9262 100644 --- a/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts +++ b/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts @@ -5,7 +5,6 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { URI } from 'vs/base/common/uri'; -import * as resources from 'vs/base/common/resources'; import { IEditorViewState } from 'vs/editor/common/editorCommon'; import { toResource, SideBySideEditorInput, IWorkbenchEditorConfiguration, SideBySideEditor as SideBySideEditorChoice } from 'vs/workbench/common/editor'; import { ITextFileService, TextFileModelChangeEvent, ModelState } from 'vs/workbench/services/textfile/common/textfiles'; @@ -25,6 +24,8 @@ import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/editor import { timeout } from 'vs/base/common/async'; import { withNullAsUndefined } from 'vs/base/common/types'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { isEqualOrParent, joinPath } from 'vs/base/common/resources'; +import { IExplorerService } from 'vs/workbench/contrib/files/common/files'; export class FileEditorTracker extends Disposable implements IWorkbenchContribution { @@ -42,7 +43,8 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IHostService private readonly hostService: IHostService, - @ICodeEditorService private readonly codeEditorService: ICodeEditorService + @ICodeEditorService private readonly codeEditorService: ICodeEditorService, + @IExplorerService private readonly explorerService: IExplorerService ) { super(); @@ -101,13 +103,13 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut // Update Editor if file (or any parent of the input) got renamed or moved const resource = editor.getResource(); - if (resources.isEqualOrParent(resource, oldResource)) { + if (isEqualOrParent(resource, oldResource)) { let reopenFileResource: URI; if (oldResource.toString() === resource.toString()) { reopenFileResource = newResource; // file got moved } else { - const index = this.getIndexOfPath(resource.path, oldResource.path, resources.hasToIgnoreCase(resource)); - reopenFileResource = resources.joinPath(newResource, resource.path.substr(index + oldResource.path.length + 1)); // parent folder got moved + const index = this.getIndexOfPath(resource.path, oldResource.path, this.explorerService.shouldIgnoreCase(resource)); + reopenFileResource = joinPath(newResource, resource.path.substr(index + oldResource.path.length + 1)); // parent folder got moved } let encoding: string | undefined = undefined; @@ -195,7 +197,7 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut // Do NOT close any opened editor that matches the resource path (either equal or being parent) of the // resource we move to (movedTo). Otherwise we would close a resource that has been renamed to the same // path but different casing. - if (movedTo && resources.isEqualOrParent(resource, movedTo)) { + if (movedTo && isEqualOrParent(resource, movedTo)) { return; } @@ -203,7 +205,7 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut if (arg1 instanceof FileChangesEvent) { matches = arg1.contains(resource, FileChangeType.DELETED); } else { - matches = resources.isEqualOrParent(resource, arg1); + matches = isEqualOrParent(resource, arg1); } if (!matches) { diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index d682f3b8d6965..a2ae86d740800 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -328,12 +328,12 @@ function containsBothDirectoryAndFile(distinctElements: ExplorerItem[]): boolean } -export function findValidPasteFileTarget(targetFolder: ExplorerItem, fileToPaste: { resource: URI, isDirectory?: boolean, allowOverwrite: boolean }, incrementalNaming: 'simple' | 'smart'): URI { +export function findValidPasteFileTarget(explorerService: IExplorerService, targetFolder: ExplorerItem, fileToPaste: { resource: URI, isDirectory?: boolean, allowOverwrite: boolean }, incrementalNaming: 'simple' | 'smart'): URI { let name = resources.basenameOrAuthority(fileToPaste.resource); let candidate = resources.joinPath(targetFolder.resource, name); while (true && !fileToPaste.allowOverwrite) { - if (!targetFolder.root.find(candidate)) { + if (!explorerService.findClosest(candidate)) { break; } @@ -870,7 +870,7 @@ async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boole throw new Error('Parent folder is readonly.'); } - const newStat = new NewExplorerItem(folder, isFolder); + const newStat = new NewExplorerItem(explorerService, folder, isFolder); await folder.fetchChildren(fileService, explorerService); folder.addChild(newStat); @@ -1049,7 +1049,7 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => { } const incrementalNaming = configurationService.getValue().explorer.incrementalNaming; - const targetFile = findValidPasteFileTarget(target, { resource: fileToPaste, isDirectory: fileToPasteStat.isDirectory, allowOverwrite: pasteShouldMove }, incrementalNaming); + const targetFile = findValidPasteFileTarget(explorerService, target, { resource: fileToPaste, isDirectory: fileToPasteStat.isDirectory, allowOverwrite: pasteShouldMove }, incrementalNaming); // Move/Copy File if (pasteShouldMove) { diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 96ad2636400d5..c28b54aaaba38 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -20,7 +20,7 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IFilesConfiguration, IExplorerService } from 'vs/workbench/contrib/files/common/files'; -import { dirname, joinPath, isEqualOrParent, basename, hasToIgnoreCase, distinctParents } from 'vs/base/common/resources'; +import { dirname, joinPath, isEqualOrParent, basename, distinctParents } from 'vs/base/common/resources'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { localize } from 'vs/nls'; import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; @@ -95,7 +95,7 @@ export class ExplorerDataSource implements IAsyncDataSource { // Check for name collisions const targetNames = new Set(); if (targetStat.children) { - const ignoreCase = hasToIgnoreCase(target.resource); + const ignoreCase = this.explorerService.shouldIgnoreCase(target.resource); targetStat.children.forEach(child => { targetNames.add(ignoreCase ? child.name.toLowerCase() : child.name); }); @@ -929,7 +929,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { // Run add in sequence const addPromisesFactory: ITask>[] = []; await Promise.all(resources.map(async resource => { - if (targetNames.has(!hasToIgnoreCase(resource) ? basename(resource) : basename(resource).toLowerCase())) { + if (targetNames.has(this.explorerService.shouldIgnoreCase(resource) ? basename(resource).toLowerCase() : basename(resource))) { const confirmationResult = await this.dialogService.confirm(getFileOverwriteConfirm(basename(resource))); if (!confirmationResult.confirmed) { return; @@ -1031,7 +1031,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { // Reuse duplicate action if user copies if (isCopy) { const incrementalNaming = this.configurationService.getValue().explorer.incrementalNaming; - const stat = await this.textFileService.copy(source.resource, findValidPasteFileTarget(target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwrite: false }, incrementalNaming)); + const stat = await this.textFileService.copy(source.resource, findValidPasteFileTarget(this.explorerService, target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwrite: false }, incrementalNaming)); if (!stat.isDirectory) { await this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }); } diff --git a/src/vs/workbench/contrib/files/common/explorerModel.ts b/src/vs/workbench/contrib/files/common/explorerModel.ts index dd17f19dfb60a..e7b364edb86cd 100644 --- a/src/vs/workbench/contrib/files/common/explorerModel.ts +++ b/src/vs/workbench/contrib/files/common/explorerModel.ts @@ -6,7 +6,6 @@ import { URI } from 'vs/base/common/uri'; import { isEqual } from 'vs/base/common/extpath'; import { posix } from 'vs/base/common/path'; -import * as resources from 'vs/base/common/resources'; import { ResourceMap } from 'vs/base/common/map'; import { IFileStat, IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { rtrim, startsWithIgnoreCase, startsWith, equalsIgnoreCase } from 'vs/base/common/strings'; @@ -16,6 +15,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { memoize } from 'vs/base/common/decorators'; import { Emitter, Event } from 'vs/base/common/event'; import { IExplorerService } from 'vs/workbench/contrib/files/common/files'; +import { joinPath, isEqualOrParent, basenameOrAuthority } from 'vs/base/common/resources'; export class ExplorerModel implements IDisposable { @@ -23,9 +23,12 @@ export class ExplorerModel implements IDisposable { private _listener: IDisposable; private readonly _onDidChangeRoots = new Emitter(); - constructor(private readonly contextService: IWorkspaceContextService) { + constructor( + private readonly contextService: IWorkspaceContextService, + explorerService: IExplorerService + ) { const setRoots = () => this._roots = this.contextService.getWorkspace().folders - .map(folder => new ExplorerItem(folder.uri, undefined, true, false, false, folder.name)); + .map(folder => new ExplorerItem(folder.uri, explorerService, undefined, true, false, false, folder.name)); setRoots(); this._listener = this.contextService.onDidChangeWorkspaceFolders(() => { @@ -80,11 +83,12 @@ export class ExplorerItem { constructor( public resource: URI, + private readonly explorerService: IExplorerService, private _parent: ExplorerItem | undefined, private _isDirectory?: boolean, private _isSymbolicLink?: boolean, private _isReadonly?: boolean, - private _name: string = resources.basenameOrAuthority(resource), + private _name: string = basenameOrAuthority(resource), private _mtime?: number, ) { this._isDirectoryResolved = false; @@ -154,8 +158,8 @@ export class ExplorerItem { return this === this.root; } - static create(service: IFileService, raw: IFileStat, parent: ExplorerItem | undefined, resolveTo?: readonly URI[]): ExplorerItem { - const stat = new ExplorerItem(raw.resource, parent, raw.isDirectory, raw.isSymbolicLink, service.hasCapability(raw.resource, FileSystemProviderCapabilities.Readonly), raw.name, raw.mtime); + static create(explorerService: IExplorerService, fileService: IFileService, raw: IFileStat, parent: ExplorerItem | undefined, resolveTo?: readonly URI[]): ExplorerItem { + const stat = new ExplorerItem(raw.resource, explorerService, parent, raw.isDirectory, raw.isSymbolicLink, fileService.hasCapability(raw.resource, FileSystemProviderCapabilities.Readonly), raw.name, raw.mtime); // Recursively add children if present if (stat.isDirectory) { @@ -164,13 +168,13 @@ export class ExplorerItem { // 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 resources.isEqualOrParent(r, stat.resource); + return isEqualOrParent(r, stat.resource); })); // Recurse into children if (raw.children) { for (let i = 0, len = raw.children.length; i < len; i++) { - const child = ExplorerItem.create(service, raw.children[i], stat, resolveTo); + const child = ExplorerItem.create(explorerService, fileService, raw.children[i], stat, resolveTo); stat.addChild(child); } } @@ -262,7 +266,7 @@ export class ExplorerItem { const resolveMetadata = explorerService.sortOrder === 'modified'; try { const stat = await fileService.resolve(this.resource, { resolveSingleChildDescendants: true, resolveMetadata }); - const resolved = ExplorerItem.create(fileService, stat, this); + const resolved = ExplorerItem.create(explorerService, fileService, stat, this); ExplorerItem.mergeLocalWithDisk(resolved, this); } catch (e) { this.isError = true; @@ -302,7 +306,7 @@ export class ExplorerItem { } private getPlatformAwareName(name: string): string { - return (!name || !resources.hasToIgnoreCase(this.resource)) ? name : name.toLowerCase(); + return this.explorerService.shouldIgnoreCase(this.resource) ? name.toLowerCase() : name; } /** @@ -319,7 +323,7 @@ export class ExplorerItem { private updateResource(recursive: boolean): void { if (this._parent) { - this.resource = resources.joinPath(this._parent.resource, this.name); + this.resource = joinPath(this._parent.resource, this.name); } if (recursive) { @@ -352,16 +356,17 @@ export class ExplorerItem { find(resource: URI): ExplorerItem | null { // Return if path found // For performance reasons try to do the comparison as fast as possible + const ignoreCase = this.explorerService.shouldIgnoreCase(resource); if (resource && this.resource.scheme === resource.scheme && equalsIgnoreCase(this.resource.authority, resource.authority) && - (resources.hasToIgnoreCase(resource) ? startsWithIgnoreCase(resource.path, this.resource.path) : startsWith(resource.path, this.resource.path))) { - return this.findByPath(rtrim(resource.path, posix.sep), this.resource.path.length); + (ignoreCase ? startsWithIgnoreCase(resource.path, this.resource.path) : startsWith(resource.path, this.resource.path))) { + return this.findByPath(rtrim(resource.path, posix.sep), this.resource.path.length, ignoreCase); } return null; //Unable to find } - private findByPath(path: string, index: number): ExplorerItem | null { - if (isEqual(rtrim(this.resource.path, posix.sep), path, resources.hasToIgnoreCase(this.resource))) { + private findByPath(path: string, index: number, ignoreCase: boolean): ExplorerItem | null { + if (isEqual(rtrim(this.resource.path, posix.sep), path, ignoreCase)) { return this; } @@ -383,7 +388,7 @@ export class ExplorerItem { if (child) { // We found a child with the given name, search inside it - return child.findByPath(path, indexOfNextSep); + return child.findByPath(path, indexOfNextSep, ignoreCase); } } @@ -392,7 +397,7 @@ export class ExplorerItem { } export class NewExplorerItem extends ExplorerItem { - constructor(parent: ExplorerItem, isDirectory: boolean) { - super(URI.file(''), parent, isDirectory); + constructor(explorerService: IExplorerService, parent: ExplorerItem, isDirectory: boolean) { + super(URI.file(''), explorerService, parent, isDirectory); } } diff --git a/src/vs/workbench/contrib/files/common/explorerService.ts b/src/vs/workbench/contrib/files/common/explorerService.ts index 233112493670e..d2eed8997802a 100644 --- a/src/vs/workbench/contrib/files/common/explorerService.ts +++ b/src/vs/workbench/contrib/files/common/explorerService.ts @@ -9,8 +9,8 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { IExplorerService, IFilesConfiguration, SortOrder, SortOrderConfiguration, IContextProvider } from 'vs/workbench/contrib/files/common/files'; import { ExplorerItem, ExplorerModel } from 'vs/workbench/contrib/files/common/explorerModel'; import { URI } from 'vs/base/common/uri'; -import { FileOperationEvent, FileOperation, IFileStat, IFileService, FileChangesEvent, FILES_EXCLUDE_CONFIG, FileChangeType, IResolveFileOptions } from 'vs/platform/files/common/files'; -import { dirname } from 'vs/base/common/resources'; +import { FileOperationEvent, FileOperation, IFileStat, IFileService, FileChangesEvent, FILES_EXCLUDE_CONFIG, FileChangeType, IResolveFileOptions, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; +import { dirname, hasToIgnoreCase } from 'vs/base/common/resources'; import { memoize } from 'vs/base/common/decorators'; import { ResourceGlobMatcher } from 'vs/workbench/common/resources'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -41,8 +41,9 @@ export class ExplorerService implements IExplorerService { private editable: { stat: ExplorerItem, data: IEditableData } | undefined; private _sortOrder: SortOrder; private cutItems: ExplorerItem[] | undefined; - private fileSystemProviderSchemes = new Set(); private contextProvider: IContextProvider | undefined; + private fileSystemProviderCaseSensitivity = new Map(); + private model: ExplorerModel; constructor( @IFileService private fileService: IFileService, @@ -53,6 +54,29 @@ export class ExplorerService implements IExplorerService { @IEditorService private editorService: IEditorService, ) { this._sortOrder = this.configurationService.getValue('explorer.sortOrder'); + + this.model = new ExplorerModel(this.contextService, this); + this.disposables.add(this.model); + this.disposables.add(this.fileService.onAfterOperation(e => this.onFileOperation(e))); + this.disposables.add(this.fileService.onFileChanges(e => this.onFileChanges(e))); + this.disposables.add(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue()))); + this.disposables.add(this.fileService.onDidChangeFileSystemProviderRegistrations(e => { + const provider = e.provider; + if (e.added && provider) { + const alreadyRegistered = this.fileSystemProviderCaseSensitivity.has(e.scheme); + const readCapability = () => this.fileSystemProviderCaseSensitivity.set(e.scheme, !!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive)); + readCapability(); + + if (alreadyRegistered) { + // A file system provider got re-registered, we should update all file stats since they might change (got read-only) + this.model.roots.forEach(r => r.forgetChildren()); + this._onDidChangeItem.fire({ recursive: true }); + } else { + this.disposables.add(provider.onDidChangeCapabilities(() => readCapability())); + } + } + })); + this.disposables.add(this.model.onDidChangeRoots(() => this._onDidChangeRoots.fire())); } get roots(): ExplorerItem[] { @@ -107,24 +131,13 @@ export class ExplorerService implements IExplorerService { return fileEventsFilter; } - @memoize get model(): ExplorerModel { - const model = new ExplorerModel(this.contextService); - this.disposables.add(model); - this.disposables.add(this.fileService.onAfterOperation(e => this.onFileOperation(e))); - this.disposables.add(this.fileService.onFileChanges(e => this.onFileChanges(e))); - this.disposables.add(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue()))); - this.disposables.add(this.fileService.onDidChangeFileSystemProviderRegistrations(e => { - if (e.added && this.fileSystemProviderSchemes.has(e.scheme)) { - // A file system provider got re-registered, we should update all file stats since they might change (got read-only) - this.model.roots.forEach(r => r.forgetChildren()); - this._onDidChangeItem.fire({ recursive: true }); - } else { - this.fileSystemProviderSchemes.add(e.scheme); - } - })); - this.disposables.add(model.onDidChangeRoots(() => this._onDidChangeRoots.fire())); + shouldIgnoreCase(resource: URI): boolean { + const caseSensitive = this.fileSystemProviderCaseSensitivity.get(resource.scheme); + if (typeof caseSensitive === 'undefined') { + return hasToIgnoreCase(resource); + } - return model; + return !caseSensitive; } // IExplorerService methods @@ -187,7 +200,7 @@ export class ExplorerService implements IExplorerService { const stat = await this.fileService.resolve(rootUri, options); // Convert to model - const modelStat = ExplorerItem.create(this.fileService, stat, undefined, options.resolveTo); + const modelStat = ExplorerItem.create(this, this.fileService, stat, undefined, options.resolveTo); // Update Input with disk Stat ExplorerItem.mergeLocalWithDisk(modelStat, root); const item = root.find(resource); @@ -231,11 +244,11 @@ export class ExplorerService implements IExplorerService { const thenable: Promise = p.isDirectoryResolved ? Promise.resolve(undefined) : this.fileService.resolve(p.resource, { resolveMetadata }); thenable.then(stat => { if (stat) { - const modelStat = ExplorerItem.create(this.fileService, stat, p.parent); + const modelStat = ExplorerItem.create(this, this.fileService, stat, p.parent); ExplorerItem.mergeLocalWithDisk(modelStat, p); } - const childElement = ExplorerItem.create(this.fileService, addedElement, p.parent); + const childElement = ExplorerItem.create(this, this.fileService, addedElement, p.parent); // Make sure to remove any previous version of the file if any p.removeChild(childElement); p.addChild(childElement); diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index dea5204785414..86a60a95d14ec 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -55,6 +55,7 @@ export interface IExplorerService { refresh(): void; setToCopy(stats: ExplorerItem[], cut: boolean): void; isCut(stat: ExplorerItem): boolean; + shouldIgnoreCase(resource: URI): boolean; /** * Selects and reveal the file element provided by the given resource if its found in the explorer. diff --git a/src/vs/workbench/contrib/files/test/electron-browser/explorerModel.test.ts b/src/vs/workbench/contrib/files/test/electron-browser/explorerModel.test.ts index b269b6eb84c88..af77f1c87cd6d 100644 --- a/src/vs/workbench/contrib/files/test/electron-browser/explorerModel.test.ts +++ b/src/vs/workbench/contrib/files/test/electron-browser/explorerModel.test.ts @@ -10,9 +10,18 @@ import { join } from 'vs/base/common/path'; import { validateFileName } from 'vs/workbench/contrib/files/browser/fileActions'; import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; import { toResource } from 'vs/base/test/common/utils'; +import { hasToIgnoreCase } from 'vs/base/common/resources'; +import { IExplorerService } from 'vs/workbench/contrib/files/common/files'; + +class MockExplorerService { + shouldIgnoreCase(resource: URI) { + return hasToIgnoreCase(resource); + } +} +const mockExplorerService = new MockExplorerService() as IExplorerService; function createStat(this: any, path: string, name: string, isFolder: boolean, hasChildren: boolean, size: number, mtime: number): ExplorerItem { - return new ExplorerItem(toResource.call(this, path), undefined, isFolder, false, false, name, mtime); + return new ExplorerItem(toResource.call(this, path), mockExplorerService, undefined, isFolder, false, false, name, mtime); } suite('Files - View Model', function () { @@ -243,19 +252,19 @@ suite('Files - View Model', function () { }); test('Merge Local with Disk', function () { - const merge1 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), undefined, true, false, false, 'to', Date.now()); - const merge2 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), undefined, true, false, false, 'to', Date.now()); + const merge1 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), mockExplorerService, undefined, true, false, false, 'to', Date.now()); + const merge2 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), mockExplorerService, undefined, true, false, false, 'to', Date.now()); // Merge Properties ExplorerItem.mergeLocalWithDisk(merge2, merge1); assert.strictEqual(merge1.mtime, merge2.mtime); // Merge Child when isDirectoryResolved=false is a no-op - merge2.addChild(new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), undefined, true, false, false, 'foo.html', Date.now())); + merge2.addChild(new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), mockExplorerService, undefined, true, false, false, 'foo.html', Date.now())); ExplorerItem.mergeLocalWithDisk(merge2, merge1); // Merge Child with isDirectoryResolved=true - const child = new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), undefined, true, false, false, 'foo.html', Date.now()); + const child = new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), mockExplorerService, undefined, true, false, false, 'foo.html', Date.now()); merge2.removeChild(child); merge2.addChild(child); (merge2)._isDirectoryResolved = true;