/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; import * as objects from 'vs/base/common/objects'; import { Emitter } from 'vs/base/common/event'; import { basename, dirname, extname, relativePath } from 'vs/base/common/resources'; import { RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IFileService } from 'vs/platform/files/common/files'; import { Disposable } from 'vs/base/common/lifecycle'; import { ParsedExpression, IExpression, parse } from 'vs/base/common/glob'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { withNullAsUndefined } from 'vs/base/common/types'; import { Schemas } from 'vs/base/common/network'; export class ResourceContextKey extends Disposable implements IContextKey { // NOTE: DO NOT CHANGE THE DEFAULT VALUE TO ANYTHING BUT // UNDEFINED! IT IS IMPORTANT THAT DEFAULTS ARE INHERITED // FROM THE PARENT CONTEXT AND ONLY UNDEFINED DOES THIS static readonly Scheme = new RawContextKey('resourceScheme', undefined); static readonly Filename = new RawContextKey('resourceFilename', undefined); static readonly Dirname = new RawContextKey('resourceDirname', undefined); static readonly Path = new RawContextKey('resourcePath', undefined); static readonly LangId = new RawContextKey('resourceLangId', undefined); static readonly Resource = new RawContextKey('resource', undefined); static readonly Extension = new RawContextKey('resourceExtname', undefined); static readonly HasResource = new RawContextKey('resourceSet', undefined); static readonly IsFileSystemResource = new RawContextKey('isFileSystemResource', undefined); private readonly _resourceKey: IContextKey; private readonly _schemeKey: IContextKey; private readonly _filenameKey: IContextKey; private readonly _dirnameKey: IContextKey; private readonly _pathKey: IContextKey; private readonly _langIdKey: IContextKey; private readonly _extensionKey: IContextKey; private readonly _hasResource: IContextKey; private readonly _isFileSystemResource: IContextKey; constructor( @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IFileService private readonly _fileService: IFileService, @IModeService private readonly _modeService: IModeService ) { super(); this._schemeKey = ResourceContextKey.Scheme.bindTo(this._contextKeyService); this._filenameKey = ResourceContextKey.Filename.bindTo(this._contextKeyService); this._dirnameKey = ResourceContextKey.Dirname.bindTo(this._contextKeyService); this._pathKey = ResourceContextKey.Path.bindTo(this._contextKeyService); this._langIdKey = ResourceContextKey.LangId.bindTo(this._contextKeyService); this._resourceKey = ResourceContextKey.Resource.bindTo(this._contextKeyService); this._extensionKey = ResourceContextKey.Extension.bindTo(this._contextKeyService); this._hasResource = ResourceContextKey.HasResource.bindTo(this._contextKeyService); this._isFileSystemResource = ResourceContextKey.IsFileSystemResource.bindTo(this._contextKeyService); this._register(_fileService.onDidChangeFileSystemProviderRegistrations(() => { const resource = this._resourceKey.get(); this._isFileSystemResource.set(Boolean(resource && _fileService.canHandleResource(resource))); })); this._register(_modeService.onDidCreateMode(() => { const value = this._resourceKey.get(); this._langIdKey.set(value ? this._modeService.getModeIdByFilepathOrFirstLine(value) : null); })); } set(value: URI | null) { if (!ResourceContextKey._uriEquals(this._resourceKey.get(), value)) { this._contextKeyService.bufferChangeEvents(() => { this._resourceKey.set(value); // NOTE@coder: Fixes source control context menus (#1104). this._schemeKey.set(value ? (value.scheme === Schemas.vscodeRemote ? Schemas.file : value.scheme) : null); this._filenameKey.set(value ? basename(value) : null); this._dirnameKey.set(value ? dirname(value).fsPath : null); this._pathKey.set(value ? value.fsPath : null); this._langIdKey.set(value ? this._modeService.getModeIdByFilepathOrFirstLine(value) : null); this._extensionKey.set(value ? extname(value) : null); this._hasResource.set(!!value); this._isFileSystemResource.set(value ? this._fileService.canHandleResource(value) : false); }); } } reset(): void { this._contextKeyService.bufferChangeEvents(() => { this._resourceKey.reset(); this._schemeKey.reset(); this._filenameKey.reset(); this._dirnameKey.reset(); this._pathKey.reset(); this._langIdKey.reset(); this._extensionKey.reset(); this._hasResource.reset(); this._isFileSystemResource.reset(); }); } get(): URI | undefined { return withNullAsUndefined(this._resourceKey.get()); } private static _uriEquals(a: URI | undefined | null, b: URI | undefined | null): boolean { if (a === b) { return true; } if (!a || !b) { return false; } return a.scheme === b.scheme // checks for not equals (fail fast) && a.authority === b.authority && a.path === b.path && a.query === b.query && a.fragment === b.fragment && a.toString() === b.toString(); // for equal we use the normalized toString-form } } export class ResourceGlobMatcher extends Disposable { private static readonly NO_ROOT: string | null = null; private readonly _onExpressionChange = this._register(new Emitter()); readonly onExpressionChange = this._onExpressionChange.event; private readonly mapRootToParsedExpression = new Map(); private readonly mapRootToExpressionConfig = new Map(); constructor( private globFn: (root?: URI) => IExpression, private shouldUpdate: (event: IConfigurationChangeEvent) => boolean, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); this.updateExcludes(false); this.registerListeners(); } private registerListeners(): void { this._register(this.configurationService.onDidChangeConfiguration(e => { if (this.shouldUpdate(e)) { this.updateExcludes(true); } })); this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.updateExcludes(true))); } private updateExcludes(fromEvent: boolean): void { let changed = false; // Add excludes per workspaces that got added this.contextService.getWorkspace().folders.forEach(folder => { const rootExcludes = this.globFn(folder.uri); if (!this.mapRootToExpressionConfig.has(folder.uri.toString()) || !objects.equals(this.mapRootToExpressionConfig.get(folder.uri.toString()), rootExcludes)) { changed = true; this.mapRootToParsedExpression.set(folder.uri.toString(), parse(rootExcludes)); this.mapRootToExpressionConfig.set(folder.uri.toString(), objects.deepClone(rootExcludes)); } }); // Remove excludes per workspace no longer present this.mapRootToExpressionConfig.forEach((value, root) => { if (root === ResourceGlobMatcher.NO_ROOT) { return; // always keep this one } if (root && !this.contextService.getWorkspaceFolder(URI.parse(root))) { this.mapRootToParsedExpression.delete(root); this.mapRootToExpressionConfig.delete(root); changed = true; } }); // Always set for resources outside root as well const globalExcludes = this.globFn(); if (!this.mapRootToExpressionConfig.has(ResourceGlobMatcher.NO_ROOT) || !objects.equals(this.mapRootToExpressionConfig.get(ResourceGlobMatcher.NO_ROOT), globalExcludes)) { changed = true; this.mapRootToParsedExpression.set(ResourceGlobMatcher.NO_ROOT, parse(globalExcludes)); this.mapRootToExpressionConfig.set(ResourceGlobMatcher.NO_ROOT, objects.deepClone(globalExcludes)); } if (fromEvent && changed) { this._onExpressionChange.fire(); } } matches(resource: URI): boolean { const folder = this.contextService.getWorkspaceFolder(resource); let expressionForRoot: ParsedExpression | undefined; if (folder && this.mapRootToParsedExpression.has(folder.uri.toString())) { expressionForRoot = this.mapRootToParsedExpression.get(folder.uri.toString()); } else { expressionForRoot = this.mapRootToParsedExpression.get(ResourceGlobMatcher.NO_ROOT); } // If the resource if from a workspace, convert its absolute path to a relative // path so that glob patterns have a higher probability to match. For example // a glob pattern of "src/**" will not match on an absolute path "/folder/src/file.txt" // but can match on "src/file.txt" let resourcePathToMatch: string | undefined; if (folder) { resourcePathToMatch = relativePath(folder.uri, resource); // always uses forward slashes } else { resourcePathToMatch = resource.fsPath; // TODO@isidor: support non-file URIs } return !!expressionForRoot && typeof resourcePathToMatch === 'string' && !!expressionForRoot(resourcePathToMatch); } }