// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; using Microsoft.SqlTools.ServiceLayer.Hosting; using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { /// /// Main class for Autocomplete functionality /// public class AutoCompleteService { #region Singleton Instance Implementation /// /// Singleton service instance /// private static Lazy instance = new Lazy(() => new AutoCompleteService()); /// /// Gets the singleton service instance /// public static AutoCompleteService Instance { get { return instance.Value; } } /// /// Default, parameterless constructor. /// TODO: Figure out how to make this truely singleton even with dependency injection for tests /// public AutoCompleteService() { } #endregion // Dictionary of unique intellisense caches for each Connection private Dictionary caches = new Dictionary(new ConnectionSummaryComparer()); private Object cachesLock = new Object(); // Used when we insert/remove something from the cache dictionary private ISqlConnectionFactory factory; private Object factoryLock = new Object(); /// /// Internal for testing purposes only /// internal ISqlConnectionFactory ConnectionFactory { get { lock(factoryLock) { if(factory == null) { factory = new SqlConnectionFactory(); } } return factory; } set { lock(factoryLock) { factory = value; } } } public void InitializeService(ServiceHost serviceHost) { // Register a callback for when a connection is created ConnectionService.Instance.RegisterOnConnectionTask(UpdateAutoCompleteCache); // Register a callback for when a connection is closed ConnectionService.Instance.RegisterOnDisconnectTask(RemoveAutoCompleteCacheUriReference); } private async Task UpdateAutoCompleteCache(ConnectionInfo connectionInfo) { if (connectionInfo != null) { await UpdateAutoCompleteCache(connectionInfo.ConnectionDetails); } } /// /// Remove a reference to an autocomplete cache from a URI. If /// it is the last URI connected to a particular connection, /// then remove the cache. /// public async Task RemoveAutoCompleteCacheUriReference(ConnectionSummary summary) { await Task.Run( () => { lock(cachesLock) { IntellisenseCache cache; if( caches.TryGetValue(summary, out cache) ) { cache.ReferenceCount--; // Remove unused caches if( cache.ReferenceCount == 0 ) { caches.Remove(summary); } } } }); } /// /// Update the cached autocomplete candidate list when the user connects to a database /// /// public async Task UpdateAutoCompleteCache(ConnectionDetails details) { IntellisenseCache cache; lock(cachesLock) { if(!caches.TryGetValue(details, out cache)) { cache = new IntellisenseCache(ConnectionFactory, details); caches[cache.DatabaseInfo] = cache; } cache.ReferenceCount++; } await cache.UpdateCache(); } /// /// Return the completion item list for the current text position. /// This method does not await cache builds since it expects to return quickly /// /// public CompletionItem[] GetCompletionItems(TextDocumentPosition textDocumentPosition) { // Try to find a cache for the document's backing connection (if available) // If we have a connection but no cache, we don't care - assuming the OnConnect and OnDisconnect listeners // behave well, there should be a cache for any actively connected document. This also helps skip documents // that are not backed by a SQL connection ConnectionInfo info; IntellisenseCache cache; if (ConnectionService.Instance.TryFindConnection(textDocumentPosition.Uri, out info) && caches.TryGetValue((ConnectionSummary)info.ConnectionDetails, out cache)) { return cache.GetAutoCompleteItems(textDocumentPosition).ToArray(); } return new CompletionItem[0]; } } }