// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. // // This file contain implementations details that are subject to change without notice. // Use at your own risk. // namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation { using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Utilities; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; /// /// This class combines IBraceCompletionSessionProvider, IBraceCompletionContextProvider, and IBraceCompletionDefaultProvider /// session providers. The aggregator will create a session using the best match with the following priorities. /// /// 1. OpeningBrace /// 2. ContentType /// 3. Provider type: SessionProviders > ContextProviders > DefaultProviders /// internal sealed class BraceCompletionAggregator : IBraceCompletionAggregator { #region Private Members private BraceCompletionAggregatorFactory _factory; private Dictionary> _contentTypeCache; private Dictionary>> _providerCache; private string _openingBraces; private string _closingBraces; #endregion #region Constructors public BraceCompletionAggregator(BraceCompletionAggregatorFactory factory) { _factory = factory; Init(); } #endregion #region IBraceCompletionAggregator /// /// Attempt to create a session using the provider that best matches the buffer content type for /// the given opening brace. This is called only for appropriate buffers in the view's buffer graph. /// public bool TryCreateSession(ITextView textView, SnapshotPoint openingPoint, char openingBrace, out IBraceCompletionSession session) { ITextBuffer buffer = openingPoint.Snapshot.TextBuffer; IContentType bufferContentType = buffer.ContentType; List contentTypes; if (_contentTypeCache.TryGetValue(openingBrace, out contentTypes)) { foreach (IContentType providerContentType in contentTypes) { // find a matching content type if (bufferContentType.IsOfType(providerContentType.TypeName)) { // try all providers for that type List providersForBrace; if (_providerCache[openingBrace].TryGetValue(providerContentType, out providersForBrace)) { foreach (ProviderHelper provider in providersForBrace) { IBraceCompletionSession curSession = null; if (provider.TryCreate(_factory, textView, openingPoint, openingBrace, out curSession)) { session = curSession; return true; } } } // only try the best match break; } } } session = null; return false; } /// /// Checks the content type against the providers. /// /// True if providers exist for the given content type. public bool IsSupportedContentType(IContentType contentType, char openingBrace) { List contentTypes = null; if (_contentTypeCache.TryGetValue(openingBrace, out contentTypes)) { // check if any types match return contentTypes.Any(t => contentType.IsOfType(t.TypeName)); } return false; } /// /// Gives a string containing all opening brace chars that have providers /// public string OpeningBraces { get { return _openingBraces; } } /// /// Gives a string containing all closing brace chars that have providers /// public string ClosingBraces { get { return _closingBraces; } } #endregion #region Private Helpers private static char GetClosingBrace(IBraceCompletionMetadata metadata, char openingBrace) { IEnumerator opening = metadata.OpeningBraces.GetEnumerator(); IEnumerator closing = metadata.ClosingBraces.GetEnumerator(); while (opening.MoveNext() && closing.MoveNext()) { if (opening.Current == openingBrace) { return closing.Current; } } // This should never happen since we find the metadata based on the opening char Debug.Fail("Unable to find the given brace in the provider metadata"); return ' '; } // basic checks to avoid incorrect behavior such as char c = '\''' private static bool AllowDefaultSession(SnapshotPoint openingPoint, char openingBrace, char closingBrace) { // avoid opening a new session next to the same char if (openingBrace == closingBrace && openingPoint.Position > 0) { char prevChar = openingPoint.Subtract(1).GetChar(); if (openingBrace.Equals(prevChar)) { return false; } } return true; } /// /// Build the _providerCache /// Each opening brace is a key with a value of the providers and metadata in a /// sorted list. The list is order from most specific to least specific content /// types with the provider type sorted secondary. /// private void Init() { HashSet closing = new HashSet(); _providerCache = new Dictionary>>(); _contentTypeCache = new Dictionary>(); List providerHelpers = new List(); providerHelpers.AddRange(_factory.SessionProviders.Select(p => new ProviderHelper(p))); providerHelpers.AddRange(_factory.ContextProviders.Select(p => new ProviderHelper(p))); providerHelpers.AddRange(_factory.DefaultProviders.Select(p => new ProviderHelper(p))); // build the cache // opening brace -> content type -> provider foreach (ProviderHelper providerHelper in providerHelpers) { foreach (char closeChar in providerHelper.Metadata.ClosingBraces) { closing.Add(closeChar); } foreach (char openingBrace in providerHelper.Metadata.OpeningBraces) { // opening brace level Dictionary> providersForBrace; if (!_providerCache.TryGetValue(openingBrace, out providersForBrace)) { providersForBrace = new Dictionary>(); _providerCache.Add(openingBrace, providersForBrace); } // convert the type names into IContentTypes for the cache foreach (IContentType contentType in providerHelper.Metadata.ContentTypes.Select((typeName) => _factory.ContentTypeRegistryService.GetContentType(typeName)) .Where((t) => t != null)) { // content type level List curProviders; if (!providersForBrace.TryGetValue(contentType, out curProviders)) { curProviders = new List(); providersForBrace.Add(contentType, curProviders); } curProviders.Add(providerHelper); } Debug.Assert(providersForBrace != null && providersForBrace.Count > 0, "providersForBrace should not be empty"); } } _openingBraces = String.Join(String.Empty, _providerCache.Keys); _closingBraces = String.Join(String.Empty, closing); // sort caches foreach (KeyValuePair>> cache in _providerCache) { // sort the list of content types so the most specific ones are first _contentTypeCache.Add(cache.Key, SortContentTypes(cache.Value.Keys.ToList())); Debug.Assert(!_contentTypeCache[cache.Key].Any(t => _contentTypeCache[cache.Key].Where(tt => tt.TypeName.Equals(t.TypeName, StringComparison.OrdinalIgnoreCase)).Count() != 1), "duplicate content types"); // sort the providers by type foreach (IContentType t in cache.Value.Keys) { cache.Value[t].Sort(); } } } /// /// Sorts content types by most specific to least specific. /// This checks the type against all others until it finds one that it is /// a type of. List.Sort() does not work here since most types are unrelated. /// private static List SortContentTypes(List contentTypes) { List sorted = new List(contentTypes.Count); foreach (IContentType contentType in contentTypes) { int i; // sorted pos bool added = false; for (i = 0; i < sorted.Count; i++) { if (contentType.IsOfType(sorted[i].TypeName)) { sorted.Insert(i, contentType); added = true; break; } } if (!added) { // the type was unrelated to all others, add it at the end sorted.Add(contentType); } } return sorted; } /// /// A private helper class to wrap lazy instances of Session, Context, and Default providers into one type. /// private class ProviderHelper : IComparable { private Lazy _sessionPair; private Lazy _contextPair; private Lazy _defaultPair; public ProviderHelper(Lazy sessionPair) { _sessionPair = sessionPair; } public ProviderHelper(Lazy contextPair) { _contextPair = contextPair; } public ProviderHelper(Lazy defaultPair) { _defaultPair = defaultPair; } public bool IsSession { get { return _sessionPair != null; } } public bool IsContext { get { return _contextPair != null; } } public bool IsDefault { get { return _defaultPair != null; } } public IBraceCompletionMetadata Metadata { get { if (IsSession) { return _sessionPair.Metadata; } if (IsContext) { return _contextPair.Metadata; } return _defaultPair.Metadata; } } // Create the session public bool TryCreate(BraceCompletionAggregatorFactory factory, ITextView textView, SnapshotPoint openingPoint, char openingBrace, out IBraceCompletionSession session) { char closingBrace = GetClosingBrace(Metadata, openingBrace); if (IsSession) { bool created = false; IBraceCompletionSession currentSession = null; factory.GuardedOperations.CallExtensionPoint(() => { created = _sessionPair.Value.TryCreateSession(textView, openingPoint, openingBrace, closingBrace, out currentSession); }); if (created) { session = currentSession; return true; } session = null; return false; } else if (IsContext) { // Get a language specific context and add it to a default session. IBraceCompletionContext context = null; // check AllowDefaultSession to avoid starting a new "" session next to a " if (AllowDefaultSession(openingPoint, openingBrace, closingBrace)) { bool created = false; factory.GuardedOperations.CallExtensionPoint(() => { created = _contextPair.Value.TryCreateContext(textView, openingPoint, openingBrace, closingBrace, out context); }); if (created) { session = new BraceCompletionDefaultSession(textView, openingPoint.Snapshot.TextBuffer, openingPoint, openingBrace, closingBrace, factory.UndoManager, factory.EditorOperationsFactoryService, context); return true; } } session = null; return false; } else if (IsDefault) { // perform some basic checks on the buffer before going in if (AllowDefaultSession(openingPoint, openingBrace, closingBrace)) { session = new BraceCompletionDefaultSession(textView, openingPoint.Snapshot.TextBuffer, openingPoint, openingBrace, closingBrace, factory.UndoManager, factory.EditorOperationsFactoryService); return true; } } session = null; return false; } // Sort order: Session -> Context -> Default public int CompareTo(ProviderHelper other) { if (IsSession && !other.IsSession) { return -1; } else if (other.IsSession) { return 1; } else if (IsContext && !other.IsContext) { return -1; } else if (other.IsContext) { return 1; } // both providers are the same type return 0; } } #endregion } }