mirror of
https://github.com/Microsoft/vscode
synced 2024-09-18 01:58:27 +00:00
parent
23ac679d4f
commit
6612ae0f8b
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
|
@ -100,5 +100,9 @@
|
||||||
"list",
|
"list",
|
||||||
"git",
|
"git",
|
||||||
"sash"
|
"sash"
|
||||||
]
|
],
|
||||||
|
"explorer.experimental.fileNesting.patterns": {
|
||||||
|
"*.js": "$(capture).*.js",
|
||||||
|
"bootstrap.js": "bootstrap-*.js"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@ export class ExplorerService implements IExplorerService {
|
||||||
this._sortOrder = this.configurationService.getValue('explorer.sortOrder');
|
this._sortOrder = this.configurationService.getValue('explorer.sortOrder');
|
||||||
this._lexicographicOptions = this.configurationService.getValue('explorer.sortOrderLexicographicOptions');
|
this._lexicographicOptions = this.configurationService.getValue('explorer.sortOrderLexicographicOptions');
|
||||||
|
|
||||||
this.model = new ExplorerModel(this.contextService, this.uriIdentityService, this.fileService);
|
this.model = new ExplorerModel(this.contextService, this.uriIdentityService, this.fileService, this.configurationService);
|
||||||
this.disposables.add(this.model);
|
this.disposables.add(this.model);
|
||||||
this.disposables.add(this.fileService.onDidRunOperation(e => this.onDidRunOperation(e)));
|
this.disposables.add(this.fileService.onDidRunOperation(e => this.onDidRunOperation(e)));
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ export class ExplorerService implements IExplorerService {
|
||||||
this.onFileChangesScheduler.schedule();
|
this.onFileChangesScheduler.schedule();
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
this.disposables.add(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue<IFilesConfiguration>())));
|
this.disposables.add(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue<IFilesConfiguration>(), e)));
|
||||||
this.disposables.add(Event.any<{ scheme: string }>(this.fileService.onDidChangeFileSystemProviderRegistrations, this.fileService.onDidChangeFileSystemProviderCapabilities)(async e => {
|
this.disposables.add(Event.any<{ scheme: string }>(this.fileService.onDidChangeFileSystemProviderRegistrations, this.fileService.onDidChangeFileSystemProviderCapabilities)(async e => {
|
||||||
let affected = false;
|
let affected = false;
|
||||||
this.model.roots.forEach(r => {
|
this.model.roots.forEach(r => {
|
||||||
|
@ -253,7 +253,7 @@ export class ExplorerService implements IExplorerService {
|
||||||
const stat = await this.fileService.resolve(root.resource, options);
|
const stat = await this.fileService.resolve(root.resource, options);
|
||||||
|
|
||||||
// Convert to model
|
// Convert to model
|
||||||
const modelStat = ExplorerItem.create(this.fileService, stat, undefined, options.resolveTo);
|
const modelStat = ExplorerItem.create(this.fileService, this.configurationService, stat, undefined, options.resolveTo);
|
||||||
// Update Input with disk Stat
|
// Update Input with disk Stat
|
||||||
ExplorerItem.mergeLocalWithDisk(modelStat, root);
|
ExplorerItem.mergeLocalWithDisk(modelStat, root);
|
||||||
const item = root.find(resource);
|
const item = root.find(resource);
|
||||||
|
@ -299,12 +299,12 @@ export class ExplorerService implements IExplorerService {
|
||||||
if (!p.isDirectoryResolved) {
|
if (!p.isDirectoryResolved) {
|
||||||
const stat = await this.fileService.resolve(p.resource, { resolveMetadata });
|
const stat = await this.fileService.resolve(p.resource, { resolveMetadata });
|
||||||
if (stat) {
|
if (stat) {
|
||||||
const modelStat = ExplorerItem.create(this.fileService, stat, p.parent);
|
const modelStat = ExplorerItem.create(this.fileService, this.configurationService, stat, p.parent);
|
||||||
ExplorerItem.mergeLocalWithDisk(modelStat, p);
|
ExplorerItem.mergeLocalWithDisk(modelStat, p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const childElement = ExplorerItem.create(this.fileService, addedElement, p.parent);
|
const childElement = ExplorerItem.create(this.fileService, this.configurationService, addedElement, p.parent);
|
||||||
// Make sure to remove any previous version of the file if any
|
// Make sure to remove any previous version of the file if any
|
||||||
p.removeChild(childElement);
|
p.removeChild(childElement);
|
||||||
p.addChild(childElement);
|
p.addChild(childElement);
|
||||||
|
@ -366,6 +366,10 @@ export class ExplorerService implements IExplorerService {
|
||||||
private async onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): Promise<void> {
|
private async onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): Promise<void> {
|
||||||
let shouldRefresh = false;
|
let shouldRefresh = false;
|
||||||
|
|
||||||
|
if (event?.affectedKeys.some(x => x.startsWith('explorer.experimental.fileNesting.'))) {
|
||||||
|
shouldRefresh = true;
|
||||||
|
}
|
||||||
|
|
||||||
const configSortOrder = configuration?.explorer?.sortOrder || SortOrder.Default;
|
const configSortOrder = configuration?.explorer?.sortOrder || SortOrder.Default;
|
||||||
if (this._sortOrder !== configSortOrder) {
|
if (this._sortOrder !== configSortOrder) {
|
||||||
shouldRefresh = this._sortOrder !== undefined;
|
shouldRefresh = this._sortOrder !== undefined;
|
||||||
|
|
|
@ -777,6 +777,7 @@ function onErrorWithRetry(notificationService: INotificationService, error: unkn
|
||||||
async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boolean): Promise<void> {
|
async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boolean): Promise<void> {
|
||||||
const explorerService = accessor.get(IExplorerService);
|
const explorerService = accessor.get(IExplorerService);
|
||||||
const fileService = accessor.get(IFileService);
|
const fileService = accessor.get(IFileService);
|
||||||
|
const configService = accessor.get(IConfigurationService);
|
||||||
const editorService = accessor.get(IEditorService);
|
const editorService = accessor.get(IEditorService);
|
||||||
const viewsService = accessor.get(IViewsService);
|
const viewsService = accessor.get(IViewsService);
|
||||||
const notificationService = accessor.get(INotificationService);
|
const notificationService = accessor.get(INotificationService);
|
||||||
|
@ -813,7 +814,7 @@ async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boole
|
||||||
throw new Error('Parent folder is readonly.');
|
throw new Error('Parent folder is readonly.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const newStat = new NewExplorerItem(fileService, folder, isFolder);
|
const newStat = new NewExplorerItem(fileService, configService, folder, isFolder);
|
||||||
folder.addChild(newStat);
|
folder.addChild(newStat);
|
||||||
|
|
||||||
const onSuccess = async (value: string): Promise<void> => {
|
const onSuccess = async (value: string): Promise<void> => {
|
||||||
|
|
|
@ -451,6 +451,36 @@ configurationRegistry.registerConfiguration({
|
||||||
],
|
],
|
||||||
'description': nls.localize('copyRelativePathSeparator', "The path separation character used when copying relative file paths."),
|
'description': nls.localize('copyRelativePathSeparator', "The path separation character used when copying relative file paths."),
|
||||||
'default': 'auto'
|
'default': 'auto'
|
||||||
|
},
|
||||||
|
'explorer.experimental.fileNesting.enabled': {
|
||||||
|
'type': 'boolean',
|
||||||
|
'markdownDescription': nls.localize('fileNestingEnabled', "Experimental. Controls whether file nesting is enabled in the explorer. File nesting allows for related files in a directory to be visually grouped together under a single parent file."),
|
||||||
|
'default': false,
|
||||||
|
},
|
||||||
|
'explorer.experimental.fileNesting.expand': {
|
||||||
|
'type': 'boolean',
|
||||||
|
'markdownDescription': nls.localize('fileNestingExpand', "Experimental. Controls whether file nests are automatically expended. `#explorer.fileNesting.enabled#` must be set for this to take effect."),
|
||||||
|
'default': true,
|
||||||
|
},
|
||||||
|
'explorer.experimental.fileNesting.patterns': {
|
||||||
|
'type': 'object',
|
||||||
|
'markdownDescription': nls.localize('fileNestingPatterns', "Experimental. Controls nesting of files in the explorer. `#explorer.fileNesting.enabled#` must be set for this to take effect. Each key describes a parent file pattern and each value should be a comma separated list of children file patterns that will be nested under the parent.\n\nA single `*` in a parent pattern may be used to capture any substring, which can then be matched against using `$(capture)` in a child pattern. Child patterns may also contain one `*` to match any substring."),
|
||||||
|
patternProperties: {
|
||||||
|
'^[^*]*\\*?[^*]*$': {
|
||||||
|
description: nls.localize('fileNesting.description', "Key patterns may contain a single `*` capture group which matches any string. Each value pattern may contain one `$(capture)` token to be substituted with the parent capture group and one `*` token to match any string"),
|
||||||
|
type: 'string',
|
||||||
|
pattern: '^([^,*]*\\*?[^,*]*)(, ?[^,*]*\\*?[^,*]*)*$',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
'default': {
|
||||||
|
'*.ts': '$(capture).js, $(capture).d.ts',
|
||||||
|
'*.js': '$(capture).js.map, $(capture).min.js, $(capture).d.ts',
|
||||||
|
'*.jsx': '$(capture).js',
|
||||||
|
'*.tsx': '$(capture).ts',
|
||||||
|
'tsconfig.json': 'tsconfig.*.json',
|
||||||
|
'package.json': 'package-lock.json, .npmrc, yarn.lock, .yarnrc',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -56,6 +56,7 @@ import { Codicon } from 'vs/base/common/codicons';
|
||||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||||
import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService';
|
import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService';
|
||||||
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
|
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
|
||||||
|
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||||
|
|
||||||
interface IExplorerViewColors extends IColorMapping {
|
interface IExplorerViewColors extends IColorMapping {
|
||||||
listDropBackground?: ColorValue | undefined;
|
listDropBackground?: ColorValue | undefined;
|
||||||
|
@ -185,6 +186,7 @@ export class ExplorerView extends ViewPane implements IExplorerView {
|
||||||
@IMenuService private readonly menuService: IMenuService,
|
@IMenuService private readonly menuService: IMenuService,
|
||||||
@ITelemetryService telemetryService: ITelemetryService,
|
@ITelemetryService telemetryService: ITelemetryService,
|
||||||
@IExplorerService private readonly explorerService: IExplorerService,
|
@IExplorerService private readonly explorerService: IExplorerService,
|
||||||
|
@INotificationService private readonly notificationService: INotificationService,
|
||||||
@IStorageService private readonly storageService: IStorageService,
|
@IStorageService private readonly storageService: IStorageService,
|
||||||
@IClipboardService private clipboardService: IClipboardService,
|
@IClipboardService private clipboardService: IClipboardService,
|
||||||
@IFileService private readonly fileService: IFileService,
|
@IFileService private readonly fileService: IFileService,
|
||||||
|
@ -380,6 +382,8 @@ export class ExplorerView extends ViewPane implements IExplorerView {
|
||||||
|
|
||||||
const isCompressionEnabled = () => this.configurationService.getValue<boolean>('explorer.compactFolders');
|
const isCompressionEnabled = () => this.configurationService.getValue<boolean>('explorer.compactFolders');
|
||||||
|
|
||||||
|
const getFileNestingSettings = () => this.configurationService.getValue<IFilesConfiguration>().explorer.experimental.fileNesting;
|
||||||
|
|
||||||
this.tree = <WorkbenchCompressibleAsyncDataTree<ExplorerItem | ExplorerItem[], ExplorerItem, FuzzyScore>>this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree, 'FileExplorer', container, new ExplorerDelegate(), new ExplorerCompressionDelegate(), [this.renderer],
|
this.tree = <WorkbenchCompressibleAsyncDataTree<ExplorerItem | ExplorerItem[], ExplorerItem, FuzzyScore>>this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree, 'FileExplorer', container, new ExplorerDelegate(), new ExplorerCompressionDelegate(), [this.renderer],
|
||||||
this.instantiationService.createInstance(ExplorerDataSource), {
|
this.instantiationService.createInstance(ExplorerDataSource), {
|
||||||
compressionEnabled: isCompressionEnabled(),
|
compressionEnabled: isCompressionEnabled(),
|
||||||
|
@ -405,7 +409,26 @@ export class ExplorerView extends ViewPane implements IExplorerView {
|
||||||
filter: this.filter,
|
filter: this.filter,
|
||||||
sorter: this.instantiationService.createInstance(FileSorter),
|
sorter: this.instantiationService.createInstance(FileSorter),
|
||||||
dnd: this.instantiationService.createInstance(FileDragAndDrop),
|
dnd: this.instantiationService.createInstance(FileDragAndDrop),
|
||||||
|
collapseByDefault: (e) => {
|
||||||
|
if (e instanceof ExplorerItem) {
|
||||||
|
if (e.hasNests && getFileNestingSettings().expand) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
autoExpandSingleChildren: true,
|
autoExpandSingleChildren: true,
|
||||||
|
expandOnlyOnTwistieClick: (e: unknown) => {
|
||||||
|
if (e instanceof ExplorerItem) {
|
||||||
|
if (e.hasNests) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (this.configurationService.getValue<'singleClick' | 'doubleClick'>('workbench.tree.expandMode') === 'doubleClick') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
additionalScrollHeight: ExplorerDelegate.ITEM_HEIGHT,
|
additionalScrollHeight: ExplorerDelegate.ITEM_HEIGHT,
|
||||||
overrideStyles: {
|
overrideStyles: {
|
||||||
listBackground: SIDE_BAR_BACKGROUND
|
listBackground: SIDE_BAR_BACKGROUND
|
||||||
|
@ -600,9 +623,23 @@ export class ExplorerView extends ViewPane implements IExplorerView {
|
||||||
}
|
}
|
||||||
|
|
||||||
const toRefresh = item || this.tree.getInput();
|
const toRefresh = item || this.tree.getInput();
|
||||||
return this.tree.updateChildren(toRefresh, recursive, false, {
|
if (this.configurationService.getValue<IFilesConfiguration>()?.explorer?.experimental?.fileNesting?.enabled) {
|
||||||
diffIdentityProvider: identityProvider
|
return (async () => {
|
||||||
});
|
try {
|
||||||
|
await this.tree.updateChildren(toRefresh, recursive, false, {
|
||||||
|
diffIdentityProvider: identityProvider
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
this.notificationService.error(e);
|
||||||
|
console.error('Unepxected error', e, 'in refreshing explorer. This may be due to experimental file nesting.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
} else {
|
||||||
|
return this.tree.updateChildren(toRefresh, recursive, false, {
|
||||||
|
diffIdentityProvider: identityProvider
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override getOptimalWidth(): number {
|
override getOptimalWidth(): number {
|
||||||
|
|
|
@ -75,6 +75,7 @@ export class ExplorerDataSource implements IAsyncDataSource<ExplorerItem | Explo
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@IProgressService private readonly progressService: IProgressService,
|
@IProgressService private readonly progressService: IProgressService,
|
||||||
|
@IConfigurationService private readonly configService: IConfigurationService,
|
||||||
@INotificationService private readonly notificationService: INotificationService,
|
@INotificationService private readonly notificationService: INotificationService,
|
||||||
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
|
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
|
||||||
@IFileService private readonly fileService: IFileService,
|
@IFileService private readonly fileService: IFileService,
|
||||||
|
@ -83,7 +84,7 @@ export class ExplorerDataSource implements IAsyncDataSource<ExplorerItem | Explo
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
hasChildren(element: ExplorerItem | ExplorerItem[]): boolean {
|
hasChildren(element: ExplorerItem | ExplorerItem[]): boolean {
|
||||||
return Array.isArray(element) || element.isDirectory;
|
return Array.isArray(element) || element.hasChildren;
|
||||||
}
|
}
|
||||||
|
|
||||||
getChildren(element: ExplorerItem | ExplorerItem[]): Promise<ExplorerItem[]> {
|
getChildren(element: ExplorerItem | ExplorerItem[]): Promise<ExplorerItem[]> {
|
||||||
|
@ -106,7 +107,7 @@ export class ExplorerDataSource implements IAsyncDataSource<ExplorerItem | Explo
|
||||||
if (element instanceof ExplorerItem && element.isRoot) {
|
if (element instanceof ExplorerItem && element.isRoot) {
|
||||||
if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) {
|
if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) {
|
||||||
// Single folder create a dummy explorer item to show error
|
// Single folder create a dummy explorer item to show error
|
||||||
const placeholder = new ExplorerItem(element.resource, this.fileService, undefined, false);
|
const placeholder = new ExplorerItem(element.resource, this.fileService, this.configService, undefined, undefined, false);
|
||||||
placeholder.isError = true;
|
placeholder.isError = true;
|
||||||
return [placeholder];
|
return [placeholder];
|
||||||
} else {
|
} else {
|
||||||
|
|
195
src/vs/workbench/contrib/files/common/explorerFileNestingTrie.ts
Normal file
195
src/vs/workbench/contrib/files/common/explorerFileNestingTrie.ts
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A sort of double-ended trie, used to efficiently query for matches to "star" patterns, where
|
||||||
|
* a given key representas a parent and may contain a capturing group ("*"), which can then be
|
||||||
|
* referenced via the token "$(capture)" in associated child patterns.
|
||||||
|
*
|
||||||
|
* The generated tree will have at most two levels, as subtrees are flattened rather than nested.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* The config: [
|
||||||
|
* [ *.ts , [ $(capture).*.ts ; $(capture).js ] ]
|
||||||
|
* [ *.js , [ $(capture).min.js ] ] ]
|
||||||
|
* Nests the files: [ a.ts ; a.d.ts ; a.js ; a.min.js ; b.ts ; b.min.js ]
|
||||||
|
* As:
|
||||||
|
* - a.ts => [ a.d.ts ; a.js ; a.min.js ]
|
||||||
|
* - b.ts => [ ]
|
||||||
|
* - b.min.ts => [ ]
|
||||||
|
*/
|
||||||
|
export class ExplorerFileNestingTrie {
|
||||||
|
private root = new PreTrie();
|
||||||
|
|
||||||
|
constructor(config: [string, string[]][]) {
|
||||||
|
for (const [parentPattern, childPatterns] of config) {
|
||||||
|
for (const childPattern of childPatterns) {
|
||||||
|
this.root.add(parentPattern, childPattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return this.root.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
nest(files: string[]): Map<string, Set<string>> {
|
||||||
|
const parentFinder = new PreTrie();
|
||||||
|
|
||||||
|
for (const potentialParent of files) {
|
||||||
|
const children = this.root.get(potentialParent);
|
||||||
|
for (const child of children) {
|
||||||
|
parentFinder.add(child, potentialParent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const findAllRootAncestors = (file: string, seen: Set<string> = new Set()): string[] => {
|
||||||
|
if (seen.has(file)) { return []; }
|
||||||
|
seen.add(file);
|
||||||
|
|
||||||
|
const ancestors = parentFinder.get(file);
|
||||||
|
if (ancestors.length === 0) {
|
||||||
|
return [file];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ancestors.length === 1 && ancestors[0] === file) {
|
||||||
|
return [file];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ancestors.flatMap(a => findAllRootAncestors(a, seen));
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = new Map<string, Set<string>>();
|
||||||
|
for (const file of files) {
|
||||||
|
let ancestors = findAllRootAncestors(file);
|
||||||
|
if (ancestors.length === 0) { ancestors = [file]; }
|
||||||
|
for (const ancestor of ancestors) {
|
||||||
|
let existing = result.get(ancestor);
|
||||||
|
if (!existing) { result.set(ancestor, existing = new Set()); }
|
||||||
|
if (file !== ancestor) {
|
||||||
|
existing.add(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Export for test only. */
|
||||||
|
export class PreTrie {
|
||||||
|
private value: SufTrie = new SufTrie();
|
||||||
|
|
||||||
|
private map: Map<string, PreTrie> = new Map();
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
add(key: string, value: string) {
|
||||||
|
if (key === '') {
|
||||||
|
this.value.add(key, value);
|
||||||
|
} else if (key[0] === '*') {
|
||||||
|
this.value.add(key, value);
|
||||||
|
} else {
|
||||||
|
const head = key[0];
|
||||||
|
const rest = key.slice(1);
|
||||||
|
let existing = this.map.get(head);
|
||||||
|
if (!existing) {
|
||||||
|
this.map.set(head, existing = new PreTrie());
|
||||||
|
}
|
||||||
|
existing.add(rest, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: string): string[] {
|
||||||
|
const results: string[] = [];
|
||||||
|
results.push(...this.value.get(key));
|
||||||
|
|
||||||
|
const head = key[0];
|
||||||
|
const rest = key.slice(1);
|
||||||
|
const existing = this.map.get(head);
|
||||||
|
if (existing) {
|
||||||
|
results.push(...existing.get(rest));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(indentation = ''): string {
|
||||||
|
const lines = [];
|
||||||
|
if (this.value.hasItems) {
|
||||||
|
lines.push('* => \n' + this.value.toString(indentation + ' '));
|
||||||
|
}
|
||||||
|
[...this.map.entries()].map(([key, trie]) =>
|
||||||
|
lines.push('^' + key + ' => \n' + trie.toString(indentation + ' ')));
|
||||||
|
return lines.map(l => indentation + l).join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Export for test only. */
|
||||||
|
export class SufTrie {
|
||||||
|
private star: string[] = [];
|
||||||
|
private epsilon: string[] = [];
|
||||||
|
|
||||||
|
private map: Map<string, SufTrie> = new Map();
|
||||||
|
hasItems: boolean = false;
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
add(key: string, value: string) {
|
||||||
|
this.hasItems = true;
|
||||||
|
if (key === '*') {
|
||||||
|
this.star.push(value);
|
||||||
|
} else if (key === '') {
|
||||||
|
this.epsilon.push(value);
|
||||||
|
} else {
|
||||||
|
const tail = key[key.length - 1];
|
||||||
|
const rest = key.slice(0, key.length - 1);
|
||||||
|
if (tail === '*') {
|
||||||
|
throw Error('Unexpected star in SufTrie key: ' + key);
|
||||||
|
} else {
|
||||||
|
let existing = this.map.get(tail);
|
||||||
|
if (!existing) {
|
||||||
|
this.map.set(tail, existing = new SufTrie());
|
||||||
|
}
|
||||||
|
existing.add(rest, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: string): string[] {
|
||||||
|
const results: string[] = [];
|
||||||
|
if (key === '') {
|
||||||
|
results.push(...this.epsilon);
|
||||||
|
}
|
||||||
|
if (this.star.length) {
|
||||||
|
results.push(...this.star.map(x => x.replace('$(capture)', key)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const tail = key[key.length - 1];
|
||||||
|
const rest = key.slice(0, key.length - 1);
|
||||||
|
const existing = this.map.get(tail);
|
||||||
|
if (existing) {
|
||||||
|
results.push(...existing.get(rest));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(indentation = ''): string {
|
||||||
|
const lines = [];
|
||||||
|
if (this.star.length) {
|
||||||
|
lines.push('* => ' + this.star.join('; '));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.epsilon.length) {
|
||||||
|
// allow-any-unicode-next-line
|
||||||
|
lines.push('ε => ' + this.epsilon.join('; '));
|
||||||
|
}
|
||||||
|
|
||||||
|
[...this.map.entries()].map(([key, trie]) =>
|
||||||
|
lines.push(key + '$' + ' => \n' + trie.toString(indentation + ' ')));
|
||||||
|
|
||||||
|
return lines.map(l => indentation + l).join('\n');
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,8 +15,11 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||||
import { memoize } from 'vs/base/common/decorators';
|
import { memoize } from 'vs/base/common/decorators';
|
||||||
import { Emitter, Event } from 'vs/base/common/event';
|
import { Emitter, Event } from 'vs/base/common/event';
|
||||||
import { joinPath, isEqualOrParent, basenameOrAuthority } from 'vs/base/common/resources';
|
import { joinPath, isEqualOrParent, basenameOrAuthority } from 'vs/base/common/resources';
|
||||||
import { SortOrder } from 'vs/workbench/contrib/files/common/files';
|
import { IFilesConfiguration, SortOrder } from 'vs/workbench/contrib/files/common/files';
|
||||||
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
|
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
|
||||||
|
import { ExplorerFileNestingTrie } from 'vs/workbench/contrib/files/common/explorerFileNestingTrie';
|
||||||
|
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||||
|
import { assertIsDefined } from 'vs/base/common/types';
|
||||||
|
|
||||||
export class ExplorerModel implements IDisposable {
|
export class ExplorerModel implements IDisposable {
|
||||||
|
|
||||||
|
@ -27,10 +30,11 @@ export class ExplorerModel implements IDisposable {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly contextService: IWorkspaceContextService,
|
private readonly contextService: IWorkspaceContextService,
|
||||||
private readonly uriIdentityService: IUriIdentityService,
|
private readonly uriIdentityService: IUriIdentityService,
|
||||||
fileService: IFileService
|
fileService: IFileService,
|
||||||
|
configService: IConfigurationService,
|
||||||
) {
|
) {
|
||||||
const setRoots = () => this._roots = this.contextService.getWorkspace().folders
|
const setRoots = () => this._roots = this.contextService.getWorkspace().folders
|
||||||
.map(folder => new ExplorerItem(folder.uri, fileService, undefined, true, false, false, folder.name));
|
.map(folder => new ExplorerItem(folder.uri, fileService, configService, undefined, true, false, false, folder.name));
|
||||||
setRoots();
|
setRoots();
|
||||||
|
|
||||||
this._listener = this.contextService.onDidChangeWorkspaceFolders(() => {
|
this._listener = this.contextService.onDidChangeWorkspaceFolders(() => {
|
||||||
|
@ -83,9 +87,12 @@ export class ExplorerItem {
|
||||||
public isError = false;
|
public isError = false;
|
||||||
private _isExcluded = false;
|
private _isExcluded = false;
|
||||||
|
|
||||||
|
private nestedChildren: ExplorerItem[] | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public resource: URI,
|
public resource: URI,
|
||||||
private readonly fileService: IFileService,
|
private readonly fileService: IFileService,
|
||||||
|
private readonly configService: IConfigurationService,
|
||||||
private _parent: ExplorerItem | undefined,
|
private _parent: ExplorerItem | undefined,
|
||||||
private _isDirectory?: boolean,
|
private _isDirectory?: boolean,
|
||||||
private _isSymbolicLink?: boolean,
|
private _isSymbolicLink?: boolean,
|
||||||
|
@ -112,6 +119,14 @@ export class ExplorerItem {
|
||||||
this._isExcluded = value;
|
this._isExcluded = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hasChildren() {
|
||||||
|
return this.isDirectory || this.hasNests;
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasNests() {
|
||||||
|
return !!(this.nestedChildren?.length);
|
||||||
|
}
|
||||||
|
|
||||||
get isDirectoryResolved(): boolean {
|
get isDirectoryResolved(): boolean {
|
||||||
return this._isDirectoryResolved;
|
return this._isDirectoryResolved;
|
||||||
}
|
}
|
||||||
|
@ -179,8 +194,8 @@ export class ExplorerItem {
|
||||||
return this === this.root;
|
return this === this.root;
|
||||||
}
|
}
|
||||||
|
|
||||||
static create(fileService: IFileService, raw: IFileStat, parent: ExplorerItem | undefined, resolveTo?: readonly URI[]): ExplorerItem {
|
static create(fileService: IFileService, configService: IConfigurationService, raw: IFileStat, parent: ExplorerItem | undefined, resolveTo?: readonly URI[]): ExplorerItem {
|
||||||
const stat = new ExplorerItem(raw.resource, fileService, parent, raw.isDirectory, raw.isSymbolicLink, raw.readonly, raw.name, raw.mtime, !raw.isFile && !raw.isDirectory);
|
const stat = new ExplorerItem(raw.resource, fileService, configService, parent, raw.isDirectory, raw.isSymbolicLink, raw.readonly, raw.name, raw.mtime, !raw.isFile && !raw.isDirectory);
|
||||||
|
|
||||||
// Recursively add children if present
|
// Recursively add children if present
|
||||||
if (stat.isDirectory) {
|
if (stat.isDirectory) {
|
||||||
|
@ -195,7 +210,7 @@ export class ExplorerItem {
|
||||||
// Recurse into children
|
// Recurse into children
|
||||||
if (raw.children) {
|
if (raw.children) {
|
||||||
for (let i = 0, len = raw.children.length; i < len; i++) {
|
for (let i = 0, len = raw.children.length; i < len; i++) {
|
||||||
const child = ExplorerItem.create(fileService, raw.children[i], stat, resolveTo);
|
const child = ExplorerItem.create(fileService, configService, raw.children[i], stat, resolveTo);
|
||||||
stat.addChild(child);
|
stat.addChild(child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -282,6 +297,9 @@ export class ExplorerItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchChildren(sortOrder: SortOrder): Promise<ExplorerItem[]> {
|
async fetchChildren(sortOrder: SortOrder): Promise<ExplorerItem[]> {
|
||||||
|
const nestingConfig = this.configService.getValue<IFilesConfiguration>().explorer.experimental.fileNesting;
|
||||||
|
if (nestingConfig.enabled && this.nestedChildren) { return this.nestedChildren; }
|
||||||
|
|
||||||
if (!this._isDirectoryResolved) {
|
if (!this._isDirectoryResolved) {
|
||||||
// Resolve metadata only when the mtime is needed since this can be expensive
|
// Resolve metadata only when the mtime is needed since this can be expensive
|
||||||
// Mtime is only used when the sort order is 'modified'
|
// Mtime is only used when the sort order is 'modified'
|
||||||
|
@ -289,7 +307,7 @@ export class ExplorerItem {
|
||||||
this.isError = false;
|
this.isError = false;
|
||||||
try {
|
try {
|
||||||
const stat = await this.fileService.resolve(this.resource, { resolveSingleChildDescendants: true, resolveMetadata });
|
const stat = await this.fileService.resolve(this.resource, { resolveSingleChildDescendants: true, resolveMetadata });
|
||||||
const resolved = ExplorerItem.create(this.fileService, stat, this);
|
const resolved = ExplorerItem.create(this.fileService, this.configService, stat, this);
|
||||||
ExplorerItem.mergeLocalWithDisk(resolved, this);
|
ExplorerItem.mergeLocalWithDisk(resolved, this);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.isError = true;
|
this.isError = true;
|
||||||
|
@ -299,9 +317,46 @@ export class ExplorerItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
const items: ExplorerItem[] = [];
|
const items: ExplorerItem[] = [];
|
||||||
this.children.forEach(child => {
|
if (nestingConfig.enabled) {
|
||||||
items.push(child);
|
const patterns = Object.entries(nestingConfig.patterns).map(
|
||||||
});
|
([parentPattern, childrenPatterns]) =>
|
||||||
|
[parentPattern.trim(), childrenPatterns.split(',').map(p => p.trim())] as [string, string[]]);
|
||||||
|
|
||||||
|
const nester = new ExplorerFileNestingTrie(patterns);
|
||||||
|
|
||||||
|
const fileChildren: [string, ExplorerItem][] = [];
|
||||||
|
const dirChildren: [string, ExplorerItem][] = [];
|
||||||
|
for (const child of this.children.entries()) {
|
||||||
|
if (child[1].isDirectory) {
|
||||||
|
dirChildren.push(child);
|
||||||
|
} else {
|
||||||
|
fileChildren.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nested = nester.nest(fileChildren.map(([name]) => name));
|
||||||
|
|
||||||
|
for (const [fileEntryName, fileEntryItem] of fileChildren) {
|
||||||
|
const nestedItems = nested.get(fileEntryName);
|
||||||
|
if (nestedItems !== undefined) {
|
||||||
|
fileEntryItem.nestedChildren = [];
|
||||||
|
for (const name of nestedItems.keys()) {
|
||||||
|
fileEntryItem.nestedChildren.push(assertIsDefined(this.children.get(name)));
|
||||||
|
}
|
||||||
|
items.push(fileEntryItem);
|
||||||
|
} else {
|
||||||
|
fileEntryItem.nestedChildren = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [_, dirEntryItem] of dirChildren.values()) {
|
||||||
|
items.push(dirEntryItem);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.children.forEach(child => {
|
||||||
|
items.push(child);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
@ -410,8 +465,8 @@ export class ExplorerItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NewExplorerItem extends ExplorerItem {
|
export class NewExplorerItem extends ExplorerItem {
|
||||||
constructor(fileService: IFileService, parent: ExplorerItem, isDirectory: boolean) {
|
constructor(fileService: IFileService, configService: IConfigurationService, parent: ExplorerItem, isDirectory: boolean) {
|
||||||
super(URI.file(''), fileService, parent, isDirectory);
|
super(URI.file(''), fileService, configService, parent, isDirectory);
|
||||||
this._isDirectoryResolved = true;
|
this._isDirectoryResolved = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,6 +97,13 @@ export interface IFilesConfiguration extends PlatformIFilesConfiguration, IWorkb
|
||||||
badges: boolean;
|
badges: boolean;
|
||||||
};
|
};
|
||||||
incrementalNaming: 'simple' | 'smart';
|
incrementalNaming: 'simple' | 'smart';
|
||||||
|
experimental: {
|
||||||
|
fileNesting: {
|
||||||
|
enabled: boolean;
|
||||||
|
expand: boolean;
|
||||||
|
patterns: { [parent: string]: string }
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
editor: IEditorOptions;
|
editor: IEditorOptions;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,409 @@
|
||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
import { PreTrie, ExplorerFileNestingTrie, SufTrie } from 'vs/workbench/contrib/files/common/explorerFileNestingTrie';
|
||||||
|
import * as assert from 'assert';
|
||||||
|
|
||||||
|
suite('SufTrie', () => {
|
||||||
|
test('exactMatches', () => {
|
||||||
|
const t = new SufTrie();
|
||||||
|
t.add('.npmrc', 'MyKey');
|
||||||
|
assert.deepStrictEqual(t.get('.npmrc'), ['MyKey']);
|
||||||
|
assert.deepStrictEqual(t.get('.npmrcs'), []);
|
||||||
|
assert.deepStrictEqual(t.get('a.npmrc'), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('starMatches', () => {
|
||||||
|
const t = new SufTrie();
|
||||||
|
t.add('*.npmrc', 'MyKey');
|
||||||
|
assert.deepStrictEqual(t.get('.npmrc'), ['MyKey']);
|
||||||
|
assert.deepStrictEqual(t.get('npmrc'), []);
|
||||||
|
assert.deepStrictEqual(t.get('.npmrcs'), []);
|
||||||
|
assert.deepStrictEqual(t.get('a.npmrc'), ['MyKey']);
|
||||||
|
assert.deepStrictEqual(t.get('a.b.c.d.npmrc'), ['MyKey']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('starSubstitutes', () => {
|
||||||
|
const t = new SufTrie();
|
||||||
|
t.add('*.npmrc', '$(capture).json');
|
||||||
|
assert.deepStrictEqual(t.get('.npmrc'), ['.json']);
|
||||||
|
assert.deepStrictEqual(t.get('npmrc'), []);
|
||||||
|
assert.deepStrictEqual(t.get('.npmrcs'), []);
|
||||||
|
assert.deepStrictEqual(t.get('a.npmrc'), ['a.json']);
|
||||||
|
assert.deepStrictEqual(t.get('a.b.c.d.npmrc'), ['a.b.c.d.json']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('multiMatches', () => {
|
||||||
|
const t = new SufTrie();
|
||||||
|
t.add('*.npmrc', 'Key1');
|
||||||
|
t.add('*.json', 'Key2');
|
||||||
|
t.add('*d.npmrc', 'Key3');
|
||||||
|
assert.deepStrictEqual(t.get('.npmrc'), ['Key1']);
|
||||||
|
assert.deepStrictEqual(t.get('npmrc'), []);
|
||||||
|
assert.deepStrictEqual(t.get('.npmrcs'), []);
|
||||||
|
assert.deepStrictEqual(t.get('.json'), ['Key2']);
|
||||||
|
assert.deepStrictEqual(t.get('a.json'), ['Key2']);
|
||||||
|
assert.deepStrictEqual(t.get('a.npmrc'), ['Key1']);
|
||||||
|
assert.deepStrictEqual(t.get('a.b.c.d.npmrc'), ['Key1', 'Key3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('multiSubstitutes', () => {
|
||||||
|
const t = new SufTrie();
|
||||||
|
t.add('*.npmrc', 'Key1.$(capture).js');
|
||||||
|
t.add('*.json', 'Key2.$(capture).js');
|
||||||
|
t.add('*d.npmrc', 'Key3.$(capture).js');
|
||||||
|
assert.deepStrictEqual(t.get('.npmrc'), ['Key1..js']);
|
||||||
|
assert.deepStrictEqual(t.get('npmrc'), []);
|
||||||
|
assert.deepStrictEqual(t.get('.npmrcs'), []);
|
||||||
|
assert.deepStrictEqual(t.get('.json'), ['Key2..js']);
|
||||||
|
assert.deepStrictEqual(t.get('a.json'), ['Key2.a.js']);
|
||||||
|
assert.deepStrictEqual(t.get('a.npmrc'), ['Key1.a.js']);
|
||||||
|
assert.deepStrictEqual(t.get('a.b.cd.npmrc'), ['Key1.a.b.cd.js', 'Key3.a.b.c.js']);
|
||||||
|
assert.deepStrictEqual(t.get('a.b.c.d.npmrc'), ['Key1.a.b.c.d.js', 'Key3.a.b.c..js']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('PreTrie', () => {
|
||||||
|
test('exactMatches', () => {
|
||||||
|
const t = new PreTrie();
|
||||||
|
t.add('.npmrc', 'MyKey');
|
||||||
|
assert.deepStrictEqual(t.get('.npmrc'), ['MyKey']);
|
||||||
|
assert.deepStrictEqual(t.get('.npmrcs'), []);
|
||||||
|
assert.deepStrictEqual(t.get('a.npmrc'), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('starMatches', () => {
|
||||||
|
const t = new PreTrie();
|
||||||
|
t.add('*.npmrc', 'MyKey');
|
||||||
|
assert.deepStrictEqual(t.get('.npmrc'), ['MyKey']);
|
||||||
|
assert.deepStrictEqual(t.get('npmrc'), []);
|
||||||
|
assert.deepStrictEqual(t.get('.npmrcs'), []);
|
||||||
|
assert.deepStrictEqual(t.get('a.npmrc'), ['MyKey']);
|
||||||
|
assert.deepStrictEqual(t.get('a.b.c.d.npmrc'), ['MyKey']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('starSubstitutes', () => {
|
||||||
|
const t = new PreTrie();
|
||||||
|
t.add('*.npmrc', '$(capture).json');
|
||||||
|
assert.deepStrictEqual(t.get('.npmrc'), ['.json']);
|
||||||
|
assert.deepStrictEqual(t.get('npmrc'), []);
|
||||||
|
assert.deepStrictEqual(t.get('.npmrcs'), []);
|
||||||
|
assert.deepStrictEqual(t.get('a.npmrc'), ['a.json']);
|
||||||
|
assert.deepStrictEqual(t.get('a.b.c.d.npmrc'), ['a.b.c.d.json']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('multiMatches', () => {
|
||||||
|
const t = new PreTrie();
|
||||||
|
t.add('*.npmrc', 'Key1');
|
||||||
|
t.add('*.json', 'Key2');
|
||||||
|
t.add('*d.npmrc', 'Key3');
|
||||||
|
assert.deepStrictEqual(t.get('.npmrc'), ['Key1']);
|
||||||
|
assert.deepStrictEqual(t.get('npmrc'), []);
|
||||||
|
assert.deepStrictEqual(t.get('.npmrcs'), []);
|
||||||
|
assert.deepStrictEqual(t.get('.json'), ['Key2']);
|
||||||
|
assert.deepStrictEqual(t.get('a.json'), ['Key2']);
|
||||||
|
assert.deepStrictEqual(t.get('a.npmrc'), ['Key1']);
|
||||||
|
assert.deepStrictEqual(t.get('a.b.c.d.npmrc'), ['Key1', 'Key3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('multiSubstitutes', () => {
|
||||||
|
const t = new PreTrie();
|
||||||
|
t.add('*.npmrc', 'Key1.$(capture).js');
|
||||||
|
t.add('*.json', 'Key2.$(capture).js');
|
||||||
|
t.add('*d.npmrc', 'Key3.$(capture).js');
|
||||||
|
assert.deepStrictEqual(t.get('.npmrc'), ['Key1..js']);
|
||||||
|
assert.deepStrictEqual(t.get('npmrc'), []);
|
||||||
|
assert.deepStrictEqual(t.get('.npmrcs'), []);
|
||||||
|
assert.deepStrictEqual(t.get('.json'), ['Key2..js']);
|
||||||
|
assert.deepStrictEqual(t.get('a.json'), ['Key2.a.js']);
|
||||||
|
assert.deepStrictEqual(t.get('a.npmrc'), ['Key1.a.js']);
|
||||||
|
assert.deepStrictEqual(t.get('a.b.cd.npmrc'), ['Key1.a.b.cd.js', 'Key3.a.b.c.js']);
|
||||||
|
assert.deepStrictEqual(t.get('a.b.c.d.npmrc'), ['Key1.a.b.c.d.js', 'Key3.a.b.c..js']);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('emptyMatches', () => {
|
||||||
|
const t = new PreTrie();
|
||||||
|
t.add('package*json', 'package');
|
||||||
|
assert.deepStrictEqual(t.get('package.json'), ['package']);
|
||||||
|
assert.deepStrictEqual(t.get('packagejson'), ['package']);
|
||||||
|
assert.deepStrictEqual(t.get('package-lock.json'), ['package']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('StarTrie', () => {
|
||||||
|
const assertMapEquals = (actual: Map<string, Set<string>>, expected: Record<string, string[]>) => {
|
||||||
|
const actualStr = [...actual.entries()].map(e => `${e[0]} => [${[...e[1].keys()].join()}]`);
|
||||||
|
const expectedStr = Object.entries(expected).map(e => `${e[0]}: [${[e[1]].join()}]`);
|
||||||
|
const bigMsg = actualStr + '===' + expectedStr;
|
||||||
|
assert.strictEqual(actual.size, Object.keys(expected).length, bigMsg);
|
||||||
|
for (const parent of actual.keys()) {
|
||||||
|
const act = actual.get(parent)!;
|
||||||
|
const exp = expected[parent];
|
||||||
|
const str = [...act.keys()].join() + '===' + exp.join();
|
||||||
|
const msg = bigMsg + '\n' + str;
|
||||||
|
assert(act.size === exp.length, msg);
|
||||||
|
for (const child of exp) {
|
||||||
|
assert(act.has(child), msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test('does added extension nesting', () => {
|
||||||
|
const t = new ExplorerFileNestingTrie([
|
||||||
|
['*', ['$(capture).*']],
|
||||||
|
]);
|
||||||
|
const nesting = t.nest([
|
||||||
|
'file',
|
||||||
|
'file.json',
|
||||||
|
'boop.test',
|
||||||
|
'boop.test1',
|
||||||
|
'boop.test.1',
|
||||||
|
'beep',
|
||||||
|
'beep.test1',
|
||||||
|
'beep.boop.test1',
|
||||||
|
'beep.boop.test2',
|
||||||
|
'beep.boop.a',
|
||||||
|
]);
|
||||||
|
assertMapEquals(nesting, {
|
||||||
|
'file': ['file.json'],
|
||||||
|
'boop.test': ['boop.test.1'],
|
||||||
|
'boop.test1': [],
|
||||||
|
'beep': ['beep.test1', 'beep.boop.test1', 'beep.boop.test2', 'beep.boop.a']
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does ext specific nesting', () => {
|
||||||
|
const t = new ExplorerFileNestingTrie([
|
||||||
|
['*.ts', ['$(capture).js']],
|
||||||
|
['*.js', ['$(capture).map']],
|
||||||
|
]);
|
||||||
|
const nesting = t.nest([
|
||||||
|
'a.ts',
|
||||||
|
'a.js',
|
||||||
|
'a.jss',
|
||||||
|
'ab.js',
|
||||||
|
'b.js',
|
||||||
|
'b.map',
|
||||||
|
'c.ts',
|
||||||
|
'c.js',
|
||||||
|
'c.map',
|
||||||
|
'd.ts',
|
||||||
|
'd.map',
|
||||||
|
]);
|
||||||
|
assertMapEquals(nesting, {
|
||||||
|
'a.ts': ['a.js'],
|
||||||
|
'ab.js': [],
|
||||||
|
'a.jss': [],
|
||||||
|
'b.js': ['b.map'],
|
||||||
|
'c.ts': ['c.js', 'c.map'],
|
||||||
|
'd.ts': [],
|
||||||
|
'd.map': [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles loops', () => {
|
||||||
|
const t = new ExplorerFileNestingTrie([
|
||||||
|
['*.a', ['$(capture).b', '$(capture).c']],
|
||||||
|
['*.b', ['$(capture).a']],
|
||||||
|
['*.c', ['$(capture).d']],
|
||||||
|
|
||||||
|
['*.aa', ['$(capture).bb']],
|
||||||
|
['*.bb', ['$(capture).cc', '$(capture).dd']],
|
||||||
|
['*.cc', ['$(capture).aa']],
|
||||||
|
['*.dd', ['$(capture).ee']],
|
||||||
|
]);
|
||||||
|
const nesting = t.nest([
|
||||||
|
'.a', '.b', '.c', '.d',
|
||||||
|
'a.a', 'a.b', 'a.d',
|
||||||
|
'a.aa', 'a.bb', 'a.cc',
|
||||||
|
'b.aa', 'b.bb',
|
||||||
|
'c.bb', 'c.cc',
|
||||||
|
'd.aa', 'd.cc',
|
||||||
|
'e.aa', 'e.bb', 'e.dd', 'e.ee',
|
||||||
|
'f.aa', 'f.bb', 'f.cc', 'f.dd', 'f.ee',
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertMapEquals(nesting, {
|
||||||
|
'.a': [], '.b': [], '.c': [], '.d': [],
|
||||||
|
'a.a': [], 'a.b': [], 'a.d': [],
|
||||||
|
'a.aa': [], 'a.bb': [], 'a.cc': [],
|
||||||
|
'b.aa': ['b.bb'],
|
||||||
|
'c.bb': ['c.cc'],
|
||||||
|
'd.cc': ['d.aa'],
|
||||||
|
'e.aa': ['e.bb', 'e.dd', 'e.ee'],
|
||||||
|
'f.aa': [], 'f.bb': [], 'f.cc': [], 'f.dd': [], 'f.ee': []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does general bidirectional suffix matching', () => {
|
||||||
|
const t = new ExplorerFileNestingTrie([
|
||||||
|
['*-vsdoc.js', ['$(capture).js']],
|
||||||
|
['*.js', [`$(capture)-vscdoc.js`]],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const nesting = t.nest([
|
||||||
|
'a-vsdoc.js',
|
||||||
|
'a.js',
|
||||||
|
'b.js',
|
||||||
|
'b-vscdoc.js',
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertMapEquals(nesting, {
|
||||||
|
'a-vsdoc.js': ['a.js'],
|
||||||
|
'b.js': ['b-vscdoc.js'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does general bidirectional prefix matching', () => {
|
||||||
|
const t = new ExplorerFileNestingTrie([
|
||||||
|
['vsdoc-*.js', ['$(capture).js']],
|
||||||
|
['*.js', [`vscdoc-$(capture).js`]],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const nesting = t.nest([
|
||||||
|
'vsdoc-a.js',
|
||||||
|
'a.js',
|
||||||
|
'b.js',
|
||||||
|
'vscdoc-b.js',
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertMapEquals(nesting, {
|
||||||
|
'vsdoc-a.js': ['a.js'],
|
||||||
|
'b.js': ['vscdoc-b.js'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does general bidirectional general matching', () => {
|
||||||
|
const t = new ExplorerFileNestingTrie([
|
||||||
|
['foo-*-bar.js', ['$(capture).js']],
|
||||||
|
['*.js', [`bib-$(capture)-bap.js`]],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const nesting = t.nest([
|
||||||
|
'foo-a-bar.js',
|
||||||
|
'a.js',
|
||||||
|
'b.js',
|
||||||
|
'bib-b-bap.js',
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertMapEquals(nesting, {
|
||||||
|
'foo-a-bar.js': ['a.js'],
|
||||||
|
'b.js': ['bib-b-bap.js'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does extension specific path segment matching', () => {
|
||||||
|
const t = new ExplorerFileNestingTrie([
|
||||||
|
['*.js', ['$(capture).*.js']],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const nesting = t.nest([
|
||||||
|
'foo.js',
|
||||||
|
'foo.test.js',
|
||||||
|
'fooTest.js',
|
||||||
|
'bar.js.js',
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertMapEquals(nesting, {
|
||||||
|
'foo.js': ['foo.test.js'],
|
||||||
|
'fooTest.js': [],
|
||||||
|
'bar.js.js': [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does exact match nesting', () => {
|
||||||
|
const t = new ExplorerFileNestingTrie([
|
||||||
|
['package.json', ['.npmrc', 'npm-shrinkwrap.json', 'yarn.lock', '.yarnclean', '.yarnignore', '.yarn-integrity', '.yarnrc']],
|
||||||
|
['bower.json', ['.bowerrc']],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const nesting = t.nest([
|
||||||
|
'package.json',
|
||||||
|
'.npmrc', 'npm-shrinkwrap.json', 'yarn.lock',
|
||||||
|
'.bowerrc',
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertMapEquals(nesting, {
|
||||||
|
'package.json': [
|
||||||
|
'.npmrc', 'npm-shrinkwrap.json', 'yarn.lock'],
|
||||||
|
'.bowerrc': [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('eslint test', () => {
|
||||||
|
const t = new ExplorerFileNestingTrie([
|
||||||
|
['.eslintrc*', ['.eslint*']],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const nesting1 = t.nest([
|
||||||
|
'.eslintrc.json',
|
||||||
|
'.eslintignore',
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertMapEquals(nesting1, {
|
||||||
|
'.eslintrc.json': ['.eslintignore'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const nesting2 = t.nest([
|
||||||
|
'.eslintrc',
|
||||||
|
'.eslintignore',
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertMapEquals(nesting2, {
|
||||||
|
'.eslintrc': ['.eslintignore'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip('is fast', () => {
|
||||||
|
const bigNester = new ExplorerFileNestingTrie([
|
||||||
|
['*', ['$(capture).*']],
|
||||||
|
['*.js', ['$(capture).*.js', '$(capture).map']],
|
||||||
|
['*.jsx', ['$(capture).js']],
|
||||||
|
['*.ts', ['$(capture).js', '$(capture).*.ts']],
|
||||||
|
['*.tsx', ['$(capture).js']],
|
||||||
|
['*.css', ['$(capture).*.css', '$(capture).map']],
|
||||||
|
['*.html', ['$(capture).*.html']],
|
||||||
|
['*.htm', ['$(capture).*.htm']],
|
||||||
|
['*.less', ['$(capture).*.less', '$(capture).css']],
|
||||||
|
['*.scss', ['$(capture).*.scss', '$(capture).css']],
|
||||||
|
['*.sass', ['$(capture).css']],
|
||||||
|
['*.styl', ['$(capture).css']],
|
||||||
|
['*.coffee', ['$(capture).*.coffee', '$(capture).js']],
|
||||||
|
['*.iced', ['$(capture).*.iced', '$(capture).js']],
|
||||||
|
['*.config', ['$(capture).*.config']],
|
||||||
|
['*.cs', ['$(capture).*.cs', '$(capture).cs.d.ts']],
|
||||||
|
['*.vb', ['$(capture).*.vb']],
|
||||||
|
['*.json', ['$(capture).*.json']],
|
||||||
|
['*.md', ['$(capture).html']],
|
||||||
|
['*.mdown', ['$(capture).html']],
|
||||||
|
['*.markdown', ['$(capture).html']],
|
||||||
|
['*.mdwn', ['$(capture).html']],
|
||||||
|
['*.svg', ['$(capture).svgz']],
|
||||||
|
['*.a', ['$(capture).b']],
|
||||||
|
['*.b', ['$(capture).a']],
|
||||||
|
['*.resx', ['$(capture).designer.cs']],
|
||||||
|
['package.json', ['.npmrc', 'npm-shrinkwrap.json', 'yarn.lock', '.yarnclean', '.yarnignore', '.yarn-integrity', '.yarnrc']],
|
||||||
|
['bower.json', ['.bowerrc']],
|
||||||
|
['*-vsdoc.js', ['$(capture).js']],
|
||||||
|
['*.tt', ['$(capture).*']]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const bigFiles = Array.from({ length: 50000 / 6 }).map((_, i) => [
|
||||||
|
'file' + i + '.js',
|
||||||
|
'file' + i + '.map',
|
||||||
|
'file' + i + '.css',
|
||||||
|
'file' + i + '.ts',
|
||||||
|
'file' + i + '.d.ts',
|
||||||
|
'file' + i + '.jsx',
|
||||||
|
]).flat();
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
// const _bigResult =
|
||||||
|
bigNester.nest(bigFiles);
|
||||||
|
const end = performance.now();
|
||||||
|
assert(end - start < 1000, 'too slow...' + (end - start));
|
||||||
|
// console.log(bigResult)
|
||||||
|
});
|
||||||
|
});
|
|
@ -11,13 +11,15 @@ import { validateFileName } from 'vs/workbench/contrib/files/browser/fileActions
|
||||||
import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel';
|
import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel';
|
||||||
import { toResource } from 'vs/base/test/common/utils';
|
import { toResource } from 'vs/base/test/common/utils';
|
||||||
import { TestFileService, TestPathService } from 'vs/workbench/test/browser/workbenchTestServices';
|
import { TestFileService, TestPathService } from 'vs/workbench/test/browser/workbenchTestServices';
|
||||||
|
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
|
||||||
|
|
||||||
|
|
||||||
suite('Files - View Model', function () {
|
suite('Files - View Model', function () {
|
||||||
|
|
||||||
const fileService = new TestFileService();
|
const fileService = new TestFileService();
|
||||||
|
const configService = new TestConfigurationService();
|
||||||
function createStat(this: any, path: string, name: string, isFolder: boolean, hasChildren: boolean, size: number, mtime: number): ExplorerItem {
|
function createStat(this: any, path: string, name: string, isFolder: boolean, hasChildren: boolean, size: number, mtime: number): ExplorerItem {
|
||||||
return new ExplorerItem(toResource.call(this, path), fileService, undefined, isFolder, false, false, name, mtime);
|
return new ExplorerItem(toResource.call(this, path), fileService, configService, undefined, isFolder, false, false, name, mtime);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathService = new TestPathService();
|
const pathService = new TestPathService();
|
||||||
|
@ -248,19 +250,19 @@ suite('Files - View Model', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Merge Local with Disk', function () {
|
test('Merge Local with Disk', function () {
|
||||||
const merge1 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), fileService, undefined, true, false, false, 'to', Date.now());
|
const merge1 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), fileService, configService, undefined, true, false, false, 'to', Date.now());
|
||||||
const merge2 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), fileService, undefined, true, false, false, 'to', Date.now());
|
const merge2 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), fileService, configService, undefined, true, false, false, 'to', Date.now());
|
||||||
|
|
||||||
// Merge Properties
|
// Merge Properties
|
||||||
ExplorerItem.mergeLocalWithDisk(merge2, merge1);
|
ExplorerItem.mergeLocalWithDisk(merge2, merge1);
|
||||||
assert.strictEqual(merge1.mtime, merge2.mtime);
|
assert.strictEqual(merge1.mtime, merge2.mtime);
|
||||||
|
|
||||||
// Merge Child when isDirectoryResolved=false is a no-op
|
// Merge Child when isDirectoryResolved=false is a no-op
|
||||||
merge2.addChild(new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), fileService, undefined, true, false, false, 'foo.html', Date.now()));
|
merge2.addChild(new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), fileService, configService, undefined, true, false, false, 'foo.html', Date.now()));
|
||||||
ExplorerItem.mergeLocalWithDisk(merge2, merge1);
|
ExplorerItem.mergeLocalWithDisk(merge2, merge1);
|
||||||
|
|
||||||
// Merge Child with isDirectoryResolved=true
|
// Merge Child with isDirectoryResolved=true
|
||||||
const child = new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), fileService, undefined, true, false, false, 'foo.html', Date.now());
|
const child = new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), fileService, configService, undefined, true, false, false, 'foo.html', Date.now());
|
||||||
merge2.removeChild(child);
|
merge2.removeChild(child);
|
||||||
merge2.addChild(child);
|
merge2.addChild(child);
|
||||||
(<any>merge2)._isDirectoryResolved = true;
|
(<any>merge2)._isDirectoryResolved = true;
|
||||||
|
|
|
@ -14,12 +14,14 @@ import { CompressedNavigationController } from 'vs/workbench/contrib/files/brows
|
||||||
import * as dom from 'vs/base/browser/dom';
|
import * as dom from 'vs/base/browser/dom';
|
||||||
import { Disposable } from 'vs/base/common/lifecycle';
|
import { Disposable } from 'vs/base/common/lifecycle';
|
||||||
import { provideDecorations } from 'vs/workbench/contrib/files/browser/views/explorerDecorationsProvider';
|
import { provideDecorations } from 'vs/workbench/contrib/files/browser/views/explorerDecorationsProvider';
|
||||||
|
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
|
||||||
const $ = dom.$;
|
const $ = dom.$;
|
||||||
|
|
||||||
const fileService = new TestFileService();
|
const fileService = new TestFileService();
|
||||||
|
const configService = new TestConfigurationService();
|
||||||
|
|
||||||
function createStat(this: any, path: string, name: string, isFolder: boolean, hasChildren: boolean, size: number, mtime: number, isSymLink = false, isUnknown = false): ExplorerItem {
|
function createStat(this: any, path: string, name: string, isFolder: boolean, hasChildren: boolean, size: number, mtime: number, isSymLink = false, isUnknown = false): ExplorerItem {
|
||||||
return new ExplorerItem(toResource.call(this, path), fileService, undefined, isFolder, isSymLink, false, name, mtime, isUnknown);
|
return new ExplorerItem(toResource.call(this, path), fileService, configService, undefined, isFolder, isSymLink, false, name, mtime, isUnknown);
|
||||||
}
|
}
|
||||||
|
|
||||||
suite('Files - ExplorerView', () => {
|
suite('Files - ExplorerView', () => {
|
||||||
|
|
Loading…
Reference in a new issue