From 869cd1439f51e08250efb0c40fa60649329fbbea Mon Sep 17 00:00:00 2001 From: Kevin Cunnane Date: Mon, 12 Jun 2017 13:28:24 -0700 Subject: [PATCH] Add LanguageFlavorNotification handling and refactor LanguageService (#375) * Add LanguageFlavorNotification handling and refactor LanguageService - Added new notification handler for language flavor changed - Refactored the LanguageService so that it no longer relies on so many intertwined static calls, which meant it was impossible to test without modifying the static instance. This will help with test reliability in the future, and prep for replacing the instance with a service provider. * Skip if not an MSSQL doc and add test * Handle definition requests * Fix diagnostics handling --- .../Contracts/LanguageFlavorChange.cs | 42 ++ .../LanguageServices/AutoCompleteHelper.cs | 111 ----- .../LanguageServices/DiagnosticsHelper.cs | 26 ++ .../LanguageServices/LanguageService.cs | 381 +++++++++++++----- .../SmoModel/DatabaseTreeNode.cs | 8 +- .../ServiceHost.cs | 30 +- .../SqlContext/SqlToolsSettings.cs | 2 +- .../LanguageServer/LanguageServiceTests.cs | 6 +- .../LanguageServer/AutocompleteTests.cs | 135 ++----- .../LanguageServer/LanguageServiceTestBase.cs | 119 ++++++ .../LanguageServer/LanguageServiceTests.cs | 8 - .../LanguageServer/PeekDefinitionTests.cs | 93 +---- .../SqlContext/SettingsTests.cs | 10 +- 13 files changed, 537 insertions(+), 434 deletions(-) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/LanguageFlavorChange.cs create mode 100644 test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/LanguageServiceTestBase.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/LanguageFlavorChange.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/LanguageFlavorChange.cs new file mode 100644 index 00000000..bab9c206 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/LanguageFlavorChange.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts +{ + + /// + /// Parameters for the Language Flavor Change notification. + /// + public class LanguageFlavorChangeParams + { + /// + /// A URI identifying the affected resource + /// + public string Uri { get; set; } + + /// + /// The primary language + /// + public string Language { get; set; } + + /// + /// The specific language flavor that is being set + /// + public string Flavor { get; set; } + } + + /// + /// Defines an event that is sent from the client to notify that + /// the client is exiting and the server should as well. + /// + public class LanguageFlavorChangeNotification + { + public static readonly + EventType Type = + EventType.Create("connection/languageflavorchanged"); + } +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs index b702e4cd..24d7fc55 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs @@ -26,10 +26,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// public static class AutoCompleteHelper { - private const int PrepopulateBindTimeout = 60000; - - private static WorkspaceService workspaceServiceInstance; - private static CompletionItem[] emptyCompletionList = new CompletionItem[0]; private static readonly string[] DefaultCompletionText = new string[] @@ -354,26 +350,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices return pos > -1; } - /// - /// Gets or sets the current workspace service instance - /// Setter for internal testing purposes only - /// - internal static WorkspaceService WorkspaceServiceInstance - { - get - { - if (AutoCompleteHelper.workspaceServiceInstance == null) - { - AutoCompleteHelper.workspaceServiceInstance = WorkspaceService.Instance; - } - return AutoCompleteHelper.workspaceServiceInstance; - } - set - { - AutoCompleteHelper.workspaceServiceInstance = value; - } - } - /// /// Get the default completion list from hard-coded list /// @@ -491,93 +467,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices return completions.ToArray(); } - /// - /// Preinitialize the parser and binder with common metadata. - /// This should front load the long binding wait to the time the - /// connection is established. Once this is completed other binding - /// requests should be faster. - /// - /// - /// - internal static void PrepopulateCommonMetadata( - ConnectionInfo info, - ScriptParseInfo scriptInfo, - ConnectedBindingQueue bindingQueue) - { - if (scriptInfo.IsConnected) - { - var scriptFile = AutoCompleteHelper.WorkspaceServiceInstance.Workspace.GetFile(info.OwnerUri); - if (scriptFile == null) - { - return; - } - - LanguageService.Instance.ParseAndBind(scriptFile, info); - - if (Monitor.TryEnter(scriptInfo.BuildingMetadataLock, LanguageService.OnConnectionWaitTimeout)) - { - try - { - QueueItem queueItem = bindingQueue.QueueBindingOperation( - key: scriptInfo.ConnectionKey, - bindingTimeout: AutoCompleteHelper.PrepopulateBindTimeout, - waitForLockTimeout: AutoCompleteHelper.PrepopulateBindTimeout, - bindOperation: (bindingContext, cancelToken) => - { - // parse a simple statement that returns common metadata - ParseResult parseResult = Parser.Parse( - "select ", - bindingContext.ParseOptions); - - List parseResults = new List(); - parseResults.Add(parseResult); - bindingContext.Binder.Bind( - parseResults, - info.ConnectionDetails.DatabaseName, - BindMode.Batch); - - // get the completion list from SQL Parser - var suggestions = Resolver.FindCompletions( - parseResult, 1, 8, - bindingContext.MetadataDisplayInfoProvider); - - // this forces lazy evaluation of the suggestion metadata - AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 8, 8); - - parseResult = Parser.Parse( - "exec ", - bindingContext.ParseOptions); - - parseResults = new List(); - parseResults.Add(parseResult); - bindingContext.Binder.Bind( - parseResults, - info.ConnectionDetails.DatabaseName, - BindMode.Batch); - - // get the completion list from SQL Parser - suggestions = Resolver.FindCompletions( - parseResult, 1, 6, - bindingContext.MetadataDisplayInfoProvider); - - // this forces lazy evaluation of the suggestion metadata - AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 6, 6); - return null; - }); - - queueItem.ItemProcessed.WaitOne(); - } - catch - { - } - finally - { - Monitor.Exit(scriptInfo.BuildingMetadataLock); - } - } - } - } - /// /// Converts a SQL Parser QuickInfo object into a VS Code Hover object /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/DiagnosticsHelper.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/DiagnosticsHelper.cs index 41de8b55..0ae47a03 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/DiagnosticsHelper.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/DiagnosticsHelper.cs @@ -3,11 +3,14 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; +using Microsoft.SqlTools.Utility; namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { @@ -46,6 +49,29 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices }); } + /// + /// Send the diagnostic results back to the host application + /// + /// + /// + /// + internal static async Task ClearScriptDiagnostics( + string uri, + EventContext eventContext) + { + Validate.IsNotNullOrEmptyString(nameof(uri), uri); + Validate.IsNotNull(nameof(eventContext), eventContext); + // Always send syntax and semantic errors. We want to + // make sure no out-of-date markers are being displayed. + await eventContext.SendEvent( + PublishDiagnosticsNotification.Type, + new PublishDiagnosticsNotification + { + Uri = uri, + Diagnostics = Array.Empty() + }); + } + /// /// Convert a ScriptFileMarker to a Diagnostic that is Language Service compatible /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs index 44467a30..3ee1f4c5 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs @@ -4,6 +4,7 @@ // using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -36,6 +37,24 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// public sealed class LanguageService { + #region Singleton Instance Implementation + + private static readonly Lazy instance = new Lazy(() => new LanguageService()); + + /// + /// Gets the singleton instance object + /// + public static LanguageService Instance + { + get { return instance.Value; } + } + + #endregion + + #region Private / internal instance fields and constructor + private const int PrepopulateBindTimeout = 60000; + + public const string SQL_LANG = "SQL"; private const int OneSecond = 1000; internal const string DefaultBatchSeperator = "GO"; @@ -50,9 +69,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices internal const int PeekDefinitionTimeout = 10 * OneSecond; - private static ConnectionService connectionService = null; + private ConnectionService connectionService = null; - private static WorkspaceService workspaceServiceInstance; + private WorkspaceService workspaceServiceInstance; private object parseMapLock = new object(); @@ -60,51 +79,13 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices private ConnectedBindingQueue bindingQueue = new ConnectedBindingQueue(); - private ParseOptions defaultParseOptions = new ParseOptions( + private ParseOptions defaultParseOptions = new ParseOptions( batchSeparator: LanguageService.DefaultBatchSeperator, isQuotedIdentifierSet: true, compatibilityLevel: DatabaseCompatibilityLevel.Current, transactSqlVersion: TransactSqlVersion.Current); - /// - /// Gets or sets the binding queue instance - /// Internal for testing purposes only - /// - internal ConnectedBindingQueue BindingQueue - { - get - { - return this.bindingQueue; - } - set - { - this.bindingQueue = value; - } - } - - /// - /// Internal for testing purposes only - /// - internal static ConnectionService ConnectionServiceInstance - { - get - { - if (connectionService == null) - { - connectionService = ConnectionService.Instance; - } - return connectionService; - } - - set - { - connectionService = value; - } - } - - #region Singleton Instance Implementation - - private static readonly Lazy instance = new Lazy(() => new LanguageService()); + private ConcurrentDictionary nonMssqlUriMap = new ConcurrentDictionary(); private Lazy> scriptParseInfoMap = new Lazy>(() => new Dictionary()); @@ -120,14 +101,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } } - /// - /// Gets the singleton instance object - /// - public static LanguageService Instance - { - get { return instance.Value; } - } - private ParseOptions DefaultParseOptions { get @@ -147,42 +120,78 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices #region Properties - private static CancellationTokenSource ExistingRequestCancellation { get; set; } + /// + /// Gets or sets the binding queue instance + /// Internal for testing purposes only + /// + internal ConnectedBindingQueue BindingQueue + { + get + { + return this.bindingQueue; + } + set + { + this.bindingQueue = value; + } + } /// - /// Gets the current settings + /// Internal for testing purposes only /// - internal SqlToolsSettings CurrentSettings + internal ConnectionService ConnectionServiceInstance { - get { return WorkspaceService.Instance.CurrentSettings; } + get + { + if (connectionService == null) + { + connectionService = ConnectionService.Instance; + } + return connectionService; + } + + set + { + connectionService = value; + } } + private CancellationTokenSource existingRequestCancellation; + /// /// Gets or sets the current workspace service instance /// Setter for internal testing purposes only /// - internal static WorkspaceService WorkspaceServiceInstance + internal WorkspaceService WorkspaceServiceInstance { get { - if (LanguageService.workspaceServiceInstance == null) + if (workspaceServiceInstance == null) { - LanguageService.workspaceServiceInstance = WorkspaceService.Instance; + workspaceServiceInstance = WorkspaceService.Instance; } - return LanguageService.workspaceServiceInstance; + return workspaceServiceInstance; } set { - LanguageService.workspaceServiceInstance = value; + workspaceServiceInstance = value; } } + /// + /// Gets the current settings + /// + internal SqlToolsSettings CurrentWorkspaceSettings + { + get { return WorkspaceServiceInstance.CurrentSettings; } + } + /// /// Gets the current workspace instance /// internal Workspace.Workspace CurrentWorkspace { - get { return LanguageService.WorkspaceServiceInstance.Workspace; } + get { return WorkspaceServiceInstance.Workspace; } } /// @@ -214,6 +223,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices serviceHost.SetRequestHandler(CompletionRequest.Type, HandleCompletionRequest); serviceHost.SetRequestHandler(DefinitionRequest.Type, HandleDefinitionRequest); serviceHost.SetEventHandler(RebuildIntelliSenseNotification.Type, HandleRebuildIntelliSenseNotification); + serviceHost.SetEventHandler(LanguageFlavorChangeNotification.Type, HandleDidChangeLanguageFlavorNotification); // Register a no-op shutdown task for validation of the shutdown logic serviceHost.RegisterShutdownTask(async (shutdownParams, shutdownRequestContext) => @@ -224,13 +234,13 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices }); // Register the configuration update handler - WorkspaceService.Instance.RegisterConfigChangeCallback(HandleDidChangeConfigurationNotification); + WorkspaceServiceInstance.RegisterConfigChangeCallback(HandleDidChangeConfigurationNotification); // Register the file change update handler - WorkspaceService.Instance.RegisterTextDocChangeCallback(HandleDidChangeTextDocumentNotification); + WorkspaceServiceInstance.RegisterTextDocChangeCallback(HandleDidChangeTextDocumentNotification); // Register the file open update handler - WorkspaceService.Instance.RegisterTextDocOpenCallback(HandleDidOpenTextDocumentNotification); + WorkspaceServiceInstance.RegisterTextDocOpenCallback(HandleDidOpenTextDocumentNotification); // Register a callback for when a connection is created ConnectionServiceInstance.RegisterOnConnectionTask(UpdateLanguageServiceOnConnection); @@ -253,23 +263,23 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// /// - internal static async Task HandleCompletionRequest( + internal async Task HandleCompletionRequest( TextDocumentPosition textDocumentPosition, RequestContext requestContext) { // check if Intellisense suggestions are enabled - if (!WorkspaceService.Instance.CurrentSettings.IsSuggestionsEnabled) + if (ShouldSkipIntellisense(textDocumentPosition.TextDocument.Uri)) { await Task.FromResult(true); } else { // get the current list of completion items and return to client - var scriptFile = LanguageService.WorkspaceServiceInstance.Workspace.GetFile( + var scriptFile = CurrentWorkspace.GetFile( textDocumentPosition.TextDocument.Uri); ConnectionInfo connInfo; - LanguageService.ConnectionServiceInstance.TryFindConnection( + ConnectionServiceInstance.TryFindConnection( scriptFile.ClientFilePath, out connInfo); @@ -287,34 +297,35 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// /// - internal static async Task HandleCompletionResolveRequest( + internal async Task HandleCompletionResolveRequest( CompletionItem completionItem, RequestContext requestContext) { // check if Intellisense suggestions are enabled - if (!WorkspaceService.Instance.CurrentSettings.IsSuggestionsEnabled) + // Note: Do not know file, so no need to check for MSSQL flavor + if (!CurrentWorkspaceSettings.IsSuggestionsEnabled) { await Task.FromResult(true); } else { - completionItem = LanguageService.Instance.ResolveCompletionItem(completionItem); + completionItem = ResolveCompletionItem(completionItem); await requestContext.SendResult(completionItem); } } - internal static async Task HandleDefinitionRequest(TextDocumentPosition textDocumentPosition, RequestContext requestContext) + internal async Task HandleDefinitionRequest(TextDocumentPosition textDocumentPosition, RequestContext requestContext) { DocumentStatusHelper.SendStatusChange(requestContext, textDocumentPosition, DocumentStatusHelper.DefinitionRequested); - if (WorkspaceService.Instance.CurrentSettings.IsIntelliSenseEnabled) + if (!ShouldSkipIntellisense(textDocumentPosition.TextDocument.Uri)) { // Retrieve document and connection ConnectionInfo connInfo; - var scriptFile = LanguageService.WorkspaceServiceInstance.Workspace.GetFile(textDocumentPosition.TextDocument.Uri); - bool isConnected = LanguageService.ConnectionServiceInstance.TryFindConnection(scriptFile.ClientFilePath, out connInfo); + var scriptFile = CurrentWorkspace.GetFile(textDocumentPosition.TextDocument.Uri); + bool isConnected = ConnectionServiceInstance.TryFindConnection(scriptFile.ClientFilePath, out connInfo); bool succeeded = false; - DefinitionResult definitionResult = LanguageService.Instance.GetDefinition(textDocumentPosition, scriptFile, connInfo); + DefinitionResult definitionResult = GetDefinition(textDocumentPosition, scriptFile, connInfo); if (definitionResult != null) { if (definitionResult.IsErrorResult) @@ -327,6 +338,11 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices succeeded = true; } } + else + { + // Send an empty result so that processing does not hang + await requestContext.SendResult(Array.Empty()); + } DocumentStatusHelper.SendTelemetryEvent(requestContext, CreatePeekTelemetryProps(succeeded, isConnected)); } @@ -349,14 +365,14 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices // turn off this code until needed (10/28/2016) #if false - private static async Task HandleReferencesRequest( + private async Task HandleReferencesRequest( ReferencesParams referencesParams, RequestContext requestContext) { await Task.FromResult(true); } - private static async Task HandleDocumentHighlightRequest( + private async Task HandleDocumentHighlightRequest( TextDocumentPosition textDocumentPosition, RequestContext requestContext) { @@ -364,21 +380,21 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } #endif - internal static async Task HandleSignatureHelpRequest( + internal async Task HandleSignatureHelpRequest( TextDocumentPosition textDocumentPosition, RequestContext requestContext) { // check if Intellisense suggestions are enabled - if (!WorkspaceService.Instance.CurrentSettings.IsSuggestionsEnabled) + if (ShouldSkipNonMssqlFile(textDocumentPosition)) { await Task.FromResult(true); } else { - ScriptFile scriptFile = WorkspaceService.Instance.Workspace.GetFile( + ScriptFile scriptFile = CurrentWorkspace.GetFile( textDocumentPosition.TextDocument.Uri); - SignatureHelp help = LanguageService.Instance.GetSignatureHelp(textDocumentPosition, scriptFile); + SignatureHelp help = GetSignatureHelp(textDocumentPosition, scriptFile); if (help != null) { await requestContext.SendResult(help); @@ -390,17 +406,18 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } } - private static async Task HandleHoverRequest( + private async Task HandleHoverRequest( TextDocumentPosition textDocumentPosition, RequestContext requestContext) { // check if Quick Info hover tooltips are enabled - if (WorkspaceService.Instance.CurrentSettings.IsQuickInfoEnabled) + if (CurrentWorkspaceSettings.IsQuickInfoEnabled + && !ShouldSkipNonMssqlFile(textDocumentPosition)) { - var scriptFile = WorkspaceService.Instance.Workspace.GetFile( + var scriptFile = CurrentWorkspace.GetFile( textDocumentPosition.TextDocument.Uri); - var hover = LanguageService.Instance.GetHoverItem(textDocumentPosition, scriptFile); + var hover = GetHoverItem(textDocumentPosition, scriptFile); if (hover != null) { await requestContext.SendResult(hover); @@ -426,7 +443,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { // if not in the preview window and diagnostics are enabled then run diagnostics if (!IsPreviewWindow(scriptFile) - && WorkspaceService.Instance.CurrentSettings.IsDiagnositicsEnabled) + && CurrentWorkspaceSettings.IsDiagnosticsEnabled) { await RunScriptDiagnostics( new ScriptFile[] { scriptFile }, @@ -443,8 +460,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// public async Task HandleDidChangeTextDocumentNotification(ScriptFile[] changedFiles, EventContext eventContext) { - if (WorkspaceService.Instance.CurrentSettings.IsDiagnositicsEnabled) + if (CurrentWorkspaceSettings.IsDiagnosticsEnabled) { + // Only process files that are MSSQL flavor await this.RunScriptDiagnostics( changedFiles.ToArray(), eventContext); @@ -472,7 +490,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } ConnectionInfo connInfo; - LanguageService.ConnectionServiceInstance.TryFindConnection( + ConnectionServiceInstance.TryFindConnection( scriptFile.ClientFilePath, out connInfo); @@ -502,7 +520,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices // if not in the preview window and diagnostics are enabled then run diagnostics if (!IsPreviewWindow(scriptFile) - && WorkspaceService.Instance.CurrentSettings.IsDiagnositicsEnabled) + && CurrentWorkspaceSettings.IsDiagnosticsEnabled) { RunScriptDiagnostics( new ScriptFile[] { scriptFile }, @@ -541,20 +559,20 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices bool? oldEnableDiagnostics = oldSettings.SqlTools.IntelliSense.EnableErrorChecking; // update the current settings to reflect any changes - CurrentSettings.Update(newSettings); + CurrentWorkspaceSettings.Update(newSettings); // if script analysis settings have changed we need to clear the current diagnostic markers if (oldEnableIntelliSense != newSettings.SqlTools.IntelliSense.EnableIntellisense || oldEnableDiagnostics != newSettings.SqlTools.IntelliSense.EnableErrorChecking) { // if the user just turned off diagnostics then send an event to clear the error markers - if (!newSettings.IsDiagnositicsEnabled) + if (!newSettings.IsDiagnosticsEnabled) { ScriptFileMarker[] emptyAnalysisDiagnostics = new ScriptFileMarker[0]; - foreach (var scriptFile in WorkspaceService.Instance.Workspace.GetOpenedFiles()) + foreach (var scriptFile in CurrentWorkspace.GetOpenedFiles()) { - await DiagnosticsHelper.PublishScriptDiagnostics(scriptFile, emptyAnalysisDiagnostics, eventContext); + await DiagnosticsHelper.ClearScriptDiagnostics(scriptFile.ClientFilePath, eventContext); } } // otherwise rerun diagnostic analysis on all opened SQL files @@ -700,13 +718,158 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } } - AutoCompleteHelper.PrepopulateCommonMetadata(info, scriptInfo, this.BindingQueue); + PrepopulateCommonMetadata(info, scriptInfo, this.BindingQueue); // Send a notification to signal that autocomplete is ready ServiceHost.Instance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() {OwnerUri = info.OwnerUri}); }); } + + /// + /// Preinitialize the parser and binder with common metadata. + /// This should front load the long binding wait to the time the + /// connection is established. Once this is completed other binding + /// requests should be faster. + /// + /// + /// + internal void PrepopulateCommonMetadata( + ConnectionInfo info, + ScriptParseInfo scriptInfo, + ConnectedBindingQueue bindingQueue) + { + if (scriptInfo.IsConnected) + { + var scriptFile = CurrentWorkspace.GetFile(info.OwnerUri); + if (scriptFile == null) + { + return; + } + + ParseAndBind(scriptFile, info); + + if (Monitor.TryEnter(scriptInfo.BuildingMetadataLock, LanguageService.OnConnectionWaitTimeout)) + { + try + { + QueueItem queueItem = bindingQueue.QueueBindingOperation( + key: scriptInfo.ConnectionKey, + bindingTimeout: PrepopulateBindTimeout, + waitForLockTimeout: PrepopulateBindTimeout, + bindOperation: (bindingContext, cancelToken) => + { + // parse a simple statement that returns common metadata + ParseResult parseResult = Parser.Parse( + "select ", + bindingContext.ParseOptions); + + List parseResults = new List(); + parseResults.Add(parseResult); + bindingContext.Binder.Bind( + parseResults, + info.ConnectionDetails.DatabaseName, + BindMode.Batch); + + // get the completion list from SQL Parser + var suggestions = Resolver.FindCompletions( + parseResult, 1, 8, + bindingContext.MetadataDisplayInfoProvider); + + // this forces lazy evaluation of the suggestion metadata + AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 8, 8); + + parseResult = Parser.Parse( + "exec ", + bindingContext.ParseOptions); + + parseResults = new List(); + parseResults.Add(parseResult); + bindingContext.Binder.Bind( + parseResults, + info.ConnectionDetails.DatabaseName, + BindMode.Batch); + + // get the completion list from SQL Parser + suggestions = Resolver.FindCompletions( + parseResult, 1, 6, + bindingContext.MetadataDisplayInfoProvider); + + // this forces lazy evaluation of the suggestion metadata + AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 6, 6); + return null; + }); + + queueItem.ItemProcessed.WaitOne(); + } + catch + { + } + finally + { + Monitor.Exit(scriptInfo.BuildingMetadataLock); + } + } + } + } + + /// + /// Handles language flavor changes by disabling intellisense on a file if it does not match the specific + /// "MSSQL" language flavor returned by our service + /// + /// + public async Task HandleDidChangeLanguageFlavorNotification( + LanguageFlavorChangeParams changeParams, + EventContext eventContext) + { + Validate.IsNotNull(nameof(changeParams), changeParams); + Validate.IsNotNull(nameof(changeParams), changeParams.Uri); + bool shouldBlock = false; + if (SQL_LANG.Equals(changeParams.Language, StringComparison.OrdinalIgnoreCase)) { + shouldBlock = !ServiceHost.ProviderName.Equals(changeParams.Flavor, StringComparison.OrdinalIgnoreCase); + } + + if (shouldBlock) { + this.nonMssqlUriMap.AddOrUpdate(changeParams.Uri, true, (k, oldValue) => true); + if (CurrentWorkspace.ContainsFile(changeParams.Uri)) + { + await DiagnosticsHelper.ClearScriptDiagnostics(changeParams.Uri, eventContext); + } + } + else + { + bool value; + this.nonMssqlUriMap.TryRemove(changeParams.Uri, out value); + } + } + + private bool ShouldSkipNonMssqlFile(TextDocumentPosition textDocPosition) + { + return ShouldSkipNonMssqlFile(textDocPosition.TextDocument.Uri); + } + + private bool ShouldSkipNonMssqlFile(ScriptFile scriptFile) + { + return ShouldSkipNonMssqlFile(scriptFile.ClientFilePath); + } + + private bool ShouldSkipNonMssqlFile(string uri) + { + bool isNonMssql = false; + nonMssqlUriMap.TryGetValue(uri, out isNonMssql); + return isNonMssql; + } + + /// + /// Determines whether intellisense should be skipped for a document. + /// If IntelliSense is disabled or it's a non-MSSQL doc this will be skipped + /// + private bool ShouldSkipIntellisense(string uri) + { + return !CurrentWorkspaceSettings.IsSuggestionsEnabled + || ShouldSkipNonMssqlFile(uri); + } + /// /// Determines whether a reparse and bind is required to provide autocomplete /// @@ -731,7 +894,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// internal CompletionItem ResolveCompletionItem(CompletionItem completionItem) { - var scriptParseInfo = LanguageService.Instance.currentCompletionParseInfo; + var scriptParseInfo = currentCompletionParseInfo; if (scriptParseInfo != null && scriptParseInfo.CurrentSuggestions != null) { if (Monitor.TryEnter(scriptParseInfo.BuildingMetadataLock)) @@ -1055,7 +1218,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } ConnectionInfo connInfo; - LanguageService.ConnectionServiceInstance.TryFindConnection( + ConnectionServiceInstance.TryFindConnection( scriptFile.ClientFilePath, out connInfo); @@ -1131,7 +1294,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices this.currentCompletionParseInfo = null; CompletionItem[] resultCompletionItems = null; CompletionService completionService = new CompletionService(BindingQueue); - bool useLowerCaseSuggestions = this.CurrentSettings.SqlTools.IntelliSense.LowerCaseSuggestions.Value; + bool useLowerCaseSuggestions = this.CurrentWorkspaceSettings.SqlTools.IntelliSense.LowerCaseSuggestions.Value; // get the current script parse info object ScriptParseInfo scriptParseInfo = GetScriptParseInfo(textDocumentPosition.TextDocument.Uri); @@ -1179,7 +1342,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices internal ScriptFileMarker[] GetSemanticMarkers(ScriptFile scriptFile) { ConnectionInfo connInfo; - ConnectionService.Instance.TryFindConnection( + ConnectionServiceInstance.TryFindConnection( scriptFile.ClientFilePath, out connInfo); @@ -1219,7 +1382,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// private Task RunScriptDiagnostics(ScriptFile[] filesToAnalyze, EventContext eventContext) { - if (!CurrentSettings.IsDiagnositicsEnabled) + if (!CurrentWorkspaceSettings.IsDiagnosticsEnabled) { // If the user has disabled script analysis, skip it entirely return Task.FromResult(true); @@ -1228,15 +1391,15 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices // If there's an existing task, attempt to cancel it try { - if (ExistingRequestCancellation != null) + if (existingRequestCancellation != null) { // Try to cancel the request - ExistingRequestCancellation.Cancel(); + existingRequestCancellation.Cancel(); // If cancellation didn't throw an exception, // clean up the existing token - ExistingRequestCancellation.Dispose(); - ExistingRequestCancellation = null; + existingRequestCancellation.Dispose(); + existingRequestCancellation = null; } } catch (Exception e) @@ -1251,14 +1414,14 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices // Create a fresh cancellation token and then start the task. // We create this on a different TaskScheduler so that we // don't block the main message loop thread. - ExistingRequestCancellation = new CancellationTokenSource(); + existingRequestCancellation = new CancellationTokenSource(); Task.Factory.StartNew( () => DelayThenInvokeDiagnostics( LanguageService.DiagnosticParseDelay, filesToAnalyze, eventContext, - ExistingRequestCancellation.Token), + existingRequestCancellation.Token), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); @@ -1305,6 +1468,12 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { continue; } + else if (ShouldSkipNonMssqlFile(scriptFile.ClientFilePath)) + { + // Clear out any existing markers in case file type was changed + await DiagnosticsHelper.ClearScriptDiagnostics(scriptFile.ClientFilePath, eventContext); + continue; + } Logger.Write(LogLevel.Verbose, "Analyzing script file: " + scriptFile.FilePath); ScriptFileMarker[] semanticMarkers = GetSemanticMarkers(scriptFile); @@ -1405,4 +1574,4 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } } } -} +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/SmoModel/DatabaseTreeNode.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/SmoModel/DatabaseTreeNode.cs index ee92cfa3..777791a0 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/SmoModel/DatabaseTreeNode.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/SmoModel/DatabaseTreeNode.cs @@ -47,7 +47,11 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel } else { - ErrorMessage = string.Format(CultureInfo.InvariantCulture, SR.DatabaseNotAccessible, context.Database.Name); + if (string.IsNullOrEmpty(ErrorMessage)) + { + // Write error message if it wasn't already set during IsAccessible check + ErrorMessage = string.Format(CultureInfo.InvariantCulture, SR.DatabaseNotAccessible, context.Database.Name); + } } } @@ -63,11 +67,11 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel } catch (Exception ex) { - return true; string error = string.Format(CultureInfo.InvariantCulture, "Failed to get IsAccessible. error:{0} inner:{1} stacktrace:{2}", ex.Message, ex.InnerException != null ? ex.InnerException.Message : "", ex.StackTrace); Logger.Write(LogLevel.Error, error); ErrorMessage = ex.Message; + return false; } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/ServiceHost.cs b/src/Microsoft.SqlTools.ServiceLayer/ServiceHost.cs index 09356f69..b865179f 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ServiceHost.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ServiceHost.cs @@ -8,15 +8,15 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; -using Microsoft.SqlTools.Extensibility; +using Microsoft.SqlTools.Extensibility; using Microsoft.SqlTools.Hosting; using Microsoft.SqlTools.Hosting.Contracts; using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.Hosting.Protocol.Channel; using Microsoft.SqlTools.Utility; -using Microsoft.SqlTools.ServiceLayer.Connection; -using Microsoft.SqlTools.ServiceLayer.Admin; - +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Admin; + namespace Microsoft.SqlTools.ServiceLayer.Hosting { /// @@ -26,7 +26,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Hosting /// public sealed class ServiceHost : ServiceHostBase { - private const string ProviderName = "MSSQL"; + public const string ProviderName = "MSSQL"; private const string ProviderDescription = "Microsoft SQL Server"; private const string ProviderProtocolVersion = "1.0"; @@ -62,17 +62,17 @@ namespace Microsoft.SqlTools.ServiceLayer.Hosting // Initialize the shutdown activities shutdownCallbacks = new List(); initializeCallbacks = new List(); - } - + } + public IMultiServiceProvider ServiceProvider { - get - { - return serviceProvider; + get + { + return serviceProvider; } - internal set - { - serviceProvider = value; + internal set + { + serviceProvider = value; } } @@ -192,8 +192,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Hosting }); } - /// - /// Handles a request for the capabilities request + /// + /// Handles a request for the capabilities request /// internal async Task HandleCapabilitiesRequest( CapabilitiesRequest initializeParams, diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlContext/SqlToolsSettings.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlContext/SqlToolsSettings.cs index d91a3453..d2ddf9b3 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/SqlContext/SqlToolsSettings.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlContext/SqlToolsSettings.cs @@ -58,7 +58,7 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlContext /// /// Gets a flag determining if diagnostics are enabled /// - public bool IsDiagnositicsEnabled + public bool IsDiagnosticsEnabled { get { diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/LanguageServer/LanguageServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/LanguageServer/LanguageServiceTests.cs index 1458c202..d6ef38e3 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/LanguageServer/LanguageServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/LanguageServer/LanguageServiceTests.cs @@ -52,8 +52,8 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer } Assert.True(LanguageService.Instance.Context != null); - Assert.True(LanguageService.ConnectionServiceInstance != null); - Assert.True(LanguageService.Instance.CurrentSettings != null); + Assert.True(LanguageService.Instance.ConnectionServiceInstance != null); + Assert.True(LanguageService.Instance.CurrentWorkspaceSettings != null); Assert.True(LanguageService.Instance.CurrentWorkspace != null); } @@ -68,7 +68,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer ScriptParseInfo scriptInfo = new ScriptParseInfo { IsConnected = true }; - AutoCompleteHelper.PrepopulateCommonMetadata(connInfo, scriptInfo, null); + LanguageService.Instance.PrepopulateCommonMetadata(connInfo, scriptInfo, null); } // This test currently requires a live database connection to initialize diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/AutocompleteTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/AutocompleteTests.cs index c6125823..d8bacbbd 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/AutocompleteTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/AutocompleteTests.cs @@ -19,126 +19,71 @@ using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; using GlobalCommon = Microsoft.SqlTools.ServiceLayer.Test.Common; using Moq; using Xunit; +using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer { /// /// Tests for the language service autocomplete component /// - public class AutocompleteTests + public class AutocompleteTests : LanguageServiceTestBase { - private const int TaskTimeout = 60000; - - private readonly string testScriptUri = TestObjects.ScriptUri; - - private readonly string testConnectionKey = "testdbcontextkey"; - - private Mock bindingQueue; - - private Mock> workspaceService; - - private Mock> requestContext; - - private Mock scriptFile; - - private Mock binder; - - private ScriptParseInfo scriptParseInfo; - - private TextDocumentPosition textDocument; - - private void InitializeTestObjects() - { - // initial cursor position in the script file - textDocument = new TextDocumentPosition - { - TextDocument = new TextDocumentIdentifier {Uri = this.testScriptUri}, - Position = new Position - { - Line = 0, - Character = 0 - } - }; - - // default settings are stored in the workspace service - WorkspaceService.Instance.CurrentSettings = new SqlToolsSettings(); - - // set up file for returning the query - scriptFile = new Mock(); - scriptFile.SetupGet(file => file.Contents).Returns(GlobalCommon.Constants.StandardQuery); - scriptFile.SetupGet(file => file.ClientFilePath).Returns(this.testScriptUri); - - // set up workspace mock - workspaceService = new Mock>(); - workspaceService.Setup(service => service.Workspace.GetFile(It.IsAny())) - .Returns(scriptFile.Object); - - // setup binding queue mock - bindingQueue = new Mock(); - bindingQueue.Setup(q => q.AddConnectionContext(It.IsAny(), It.IsAny())) - .Returns(this.testConnectionKey); - - // inject mock instances into the Language Service - LanguageService.WorkspaceServiceInstance = workspaceService.Object; - LanguageService.ConnectionServiceInstance = TestObjects.GetTestConnectionService(); - ConnectionInfo connectionInfo = TestObjects.GetTestConnectionInfo(); - LanguageService.ConnectionServiceInstance.OwnerToConnectionMap.Add(this.testScriptUri, connectionInfo); - LanguageService.Instance.BindingQueue = bindingQueue.Object; - - // setup the mock for SendResult - requestContext = new Mock>(); - requestContext.Setup(rc => rc.SendResult(It.IsAny())) - .Returns(Task.FromResult(0)); - - // setup the IBinder mock - binder = new Mock(); - binder.Setup(b => b.Bind( - It.IsAny>(), - It.IsAny(), - It.IsAny())); - - scriptParseInfo = new ScriptParseInfo(); - LanguageService.Instance.AddOrUpdateScriptParseInfo(this.testScriptUri, scriptParseInfo); - scriptParseInfo.IsConnected = true; - scriptParseInfo.ConnectionKey = LanguageService.Instance.BindingQueue.AddConnectionContext(connectionInfo); - - // setup the binding context object - ConnectedBindingContext bindingContext = new ConnectedBindingContext(); - bindingContext.Binder = binder.Object; - bindingContext.MetadataDisplayInfoProvider = new MetadataDisplayInfoProvider(); - LanguageService.Instance.BindingQueue.BindingContextMap.Add(scriptParseInfo.ConnectionKey, bindingContext); - } [Fact] public void HandleCompletionRequestDisabled() { InitializeTestObjects(); - WorkspaceService.Instance.CurrentSettings.SqlTools.IntelliSense.EnableIntellisense = false; - Assert.NotNull(LanguageService.HandleCompletionRequest(null, null)); + langService.CurrentWorkspaceSettings.SqlTools.IntelliSense.EnableIntellisense = false; + Assert.NotNull(langService.HandleCompletionRequest(null, null)); } [Fact] public void HandleCompletionResolveRequestDisabled() { InitializeTestObjects(); - WorkspaceService.Instance.CurrentSettings.SqlTools.IntelliSense.EnableIntellisense = false; - Assert.NotNull(LanguageService.HandleCompletionResolveRequest(null, null)); + langService.CurrentWorkspaceSettings.SqlTools.IntelliSense.EnableIntellisense = false; + Assert.NotNull(langService.HandleCompletionResolveRequest(null, null)); } [Fact] public void HandleSignatureHelpRequestDisabled() { InitializeTestObjects(); - WorkspaceService.Instance.CurrentSettings.SqlTools.IntelliSense.EnableIntellisense = false; - Assert.NotNull(LanguageService.HandleSignatureHelpRequest(null, null)); + langService.CurrentWorkspaceSettings.SqlTools.IntelliSense.EnableIntellisense = false; + Assert.NotNull(langService.HandleSignatureHelpRequest(null, null)); + } + + [Fact] + public void HandleSignatureHelpRequestNonMssqlFile() + { + InitializeTestObjects(); + + // setup the mock for SendResult + var signatureRequestContext = new Mock>(); + signatureRequestContext.Setup(rc => rc.SendResult(It.IsAny())) + .Returns(Task.FromResult(0)); + signatureRequestContext.Setup(rc => rc.SendError(It.IsAny(), It.IsAny())).Returns(Task.FromResult(0)); + + + langService.CurrentWorkspaceSettings.SqlTools.IntelliSense.EnableIntellisense = true; + langService.HandleDidChangeLanguageFlavorNotification(new LanguageFlavorChangeParams { + Uri = textDocument.TextDocument.Uri, + Language = LanguageService.SQL_LANG.ToLower(), + Flavor = "NotMSSQL" + }, null); + Assert.NotNull(langService.HandleSignatureHelpRequest(textDocument, signatureRequestContext.Object)); + + // verify that no events were sent + signatureRequestContext.Verify(m => m.SendResult(It.IsAny()), Times.Never()); + signatureRequestContext.Verify(m => m.SendError(It.IsAny(), It.IsAny()), Times.Never()); } [Fact] public void AddOrUpdateScriptParseInfoNullUri() { InitializeTestObjects(); - LanguageService.Instance.AddOrUpdateScriptParseInfo("abracadabra", scriptParseInfo); - Assert.True(LanguageService.Instance.ScriptParseInfoMap.ContainsKey("abracadabra")); + langService.AddOrUpdateScriptParseInfo("abracadabra", scriptParseInfo); + Assert.True(langService.ScriptParseInfoMap.ContainsKey("abracadabra")); } [Fact] @@ -146,21 +91,21 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer { InitializeTestObjects(); textDocument.TextDocument.Uri = "invaliduri"; - Assert.Null(LanguageService.Instance.GetDefinition(textDocument, null, null)); + Assert.Null(langService.GetDefinition(textDocument, null, null)); } [Fact] public void RemoveScriptParseInfoNullUri() { InitializeTestObjects(); - Assert.False(LanguageService.Instance.RemoveScriptParseInfo("abc123")); + Assert.False(langService.RemoveScriptParseInfo("abc123")); } [Fact] public void IsPreviewWindowNullScriptFileTest() { InitializeTestObjects(); - Assert.False(LanguageService.Instance.IsPreviewWindow(null)); + Assert.False(langService.IsPreviewWindow(null)); } [Fact] @@ -168,7 +113,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer { InitializeTestObjects(); textDocument.TextDocument.Uri = "somethinggoeshere"; - Assert.True(LanguageService.Instance.GetCompletionItems(textDocument, scriptFile.Object, null).Length > 0); + Assert.True(langService.GetCompletionItems(textDocument, scriptFile.Object, null).Length > 0); } [Fact] @@ -215,7 +160,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer InitializeTestObjects(); // request the completion list - Task handleCompletion = LanguageService.HandleCompletionRequest(textDocument, requestContext.Object); + Task handleCompletion = langService.HandleCompletionRequest(textDocument, requestContext.Object); handleCompletion.Wait(TaskTimeout); // verify that send result was called with a completion array diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/LanguageServiceTestBase.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/LanguageServiceTestBase.cs new file mode 100644 index 00000000..4642c727 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/LanguageServiceTestBase.cs @@ -0,0 +1,119 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.SqlServer.Management.SqlParser.Binder; +using Microsoft.SqlServer.Management.SqlParser.MetadataProvider; +using Microsoft.SqlServer.Management.SqlParser.Parser; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.LanguageServices; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; +using Microsoft.SqlTools.ServiceLayer.SqlContext; +using Microsoft.SqlTools.ServiceLayer.UnitTests.Utility; +using Microsoft.SqlTools.ServiceLayer.Workspace; +using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; +using GlobalCommon = Microsoft.SqlTools.ServiceLayer.Test.Common; +using Moq; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer +{ + /// + /// Tests for the language service autocomplete component + /// + public abstract class LanguageServiceTestBase + { + protected const int TaskTimeout = 60000; + + protected readonly string testScriptUri = TestObjects.ScriptUri; + + protected readonly string testConnectionKey = "testdbcontextkey"; + + protected LanguageService langService; + + protected Mock bindingQueue; + + protected Mock> workspaceService; + + protected Mock> requestContext; + + protected Mock scriptFile; + + protected Mock binder; + + internal ScriptParseInfo scriptParseInfo; + + protected TextDocumentPosition textDocument; + + protected void InitializeTestObjects() + { + // initial cursor position in the script file + textDocument = new TextDocumentPosition + { + TextDocument = new TextDocumentIdentifier { Uri = this.testScriptUri }, + Position = new Position + { + Line = 0, + Character = 23 + } + }; + + // default settings are stored in the workspace service + WorkspaceService.Instance.CurrentSettings = new SqlToolsSettings(); + + // set up file for returning the query + scriptFile = new Mock(); + scriptFile.SetupGet(file => file.Contents).Returns(GlobalCommon.Constants.StandardQuery); + scriptFile.SetupGet(file => file.ClientFilePath).Returns(this.testScriptUri); + + // set up workspace mock + workspaceService = new Mock>(); + workspaceService.Setup(service => service.Workspace.GetFile(It.IsAny())) + .Returns(scriptFile.Object); + + // setup binding queue mock + bindingQueue = new Mock(); + bindingQueue.Setup(q => q.AddConnectionContext(It.IsAny(), It.IsAny())) + .Returns(this.testConnectionKey); + + langService = new LanguageService(); + // inject mock instances into the Language Service + langService.WorkspaceServiceInstance = workspaceService.Object; + langService.ConnectionServiceInstance = TestObjects.GetTestConnectionService(); + ConnectionInfo connectionInfo = TestObjects.GetTestConnectionInfo(); + langService.ConnectionServiceInstance.OwnerToConnectionMap.Add(this.testScriptUri, connectionInfo); + langService.BindingQueue = bindingQueue.Object; + + // setup the mock for SendResult + requestContext = new Mock>(); + requestContext.Setup(rc => rc.SendResult(It.IsAny())) + .Returns(Task.FromResult(0)); + requestContext.Setup(rc => rc.SendError(It.IsAny(), It.IsAny())).Returns(Task.FromResult(0)); + requestContext.Setup(r => r.SendEvent(It.IsAny>(), It.IsAny())).Returns(Task.FromResult(0)); + requestContext.Setup(r => r.SendEvent(It.IsAny>(), It.IsAny())).Returns(Task.FromResult(0)); + + // setup the IBinder mock + binder = new Mock(); + binder.Setup(b => b.Bind( + It.IsAny>(), + It.IsAny(), + It.IsAny())); + + scriptParseInfo = new ScriptParseInfo(); + langService.AddOrUpdateScriptParseInfo(this.testScriptUri, scriptParseInfo); + scriptParseInfo.IsConnected = true; + scriptParseInfo.ConnectionKey = langService.BindingQueue.AddConnectionContext(connectionInfo); + + // setup the binding context object + ConnectedBindingContext bindingContext = new ConnectedBindingContext(); + bindingContext.Binder = binder.Object; + bindingContext.MetadataDisplayInfoProvider = new MetadataDisplayInfoProvider(); + langService.BindingQueue.BindingContextMap.Add(scriptParseInfo.ConnectionKey, bindingContext); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/LanguageServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/LanguageServiceTests.cs index 57ad0fc4..8e84a9ec 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/LanguageServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/LanguageServiceTests.cs @@ -150,14 +150,6 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer Assert.Equal(AutoCompleteHelper.EmptyCompletionList.Length, 0); } - [Fact] - public void SetWorkspaceServiceInstanceTest() - { - AutoCompleteHelper.WorkspaceServiceInstance = null; - // workspace will be recreated if it's set to null - Assert.NotNull(AutoCompleteHelper.WorkspaceServiceInstance); - } - internal class TestScriptDocumentInfo : ScriptDocumentInfo { public TestScriptDocumentInfo(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile, ScriptParseInfo scriptParseInfo, diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/PeekDefinitionTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/PeekDefinitionTests.cs index b10f5877..4a18f4df 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/PeekDefinitionTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/PeekDefinitionTests.cs @@ -33,90 +33,8 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer /// /// Tests for the language service peek definition/ go to definition feature /// - public class PeekDefinitionTests + public class PeekDefinitionTests : LanguageServiceTestBase { - private const int TaskTimeout = 30000; - - private readonly string testScriptUri = TestObjects.ScriptUri; - - private readonly string testConnectionKey = "testdbcontextkey"; - - private Mock bindingQueue; - - private Mock> workspaceService; - - private Mock> requestContext; - - private Mock binder; - - private TextDocumentPosition textDocument; - - private void InitializeTestObjects() - { - // initial cursor position in the script file - textDocument = new TextDocumentPosition - { - TextDocument = new TextDocumentIdentifier {Uri = this.testScriptUri}, - Position = new Position - { - Line = 0, - Character = 23 - } - }; - - // default settings are stored in the workspace service - WorkspaceService.Instance.CurrentSettings = new SqlToolsSettings(); - - // set up file for returning the query - var fileMock = new Mock(); - fileMock.SetupGet(file => file.Contents).Returns(GlobalCommon.Constants.StandardQuery); - fileMock.SetupGet(file => file.ClientFilePath).Returns(this.testScriptUri); - - // set up workspace mock - workspaceService = new Mock>(); - workspaceService.Setup(service => service.Workspace.GetFile(It.IsAny())) - .Returns(fileMock.Object); - - // setup binding queue mock - bindingQueue = new Mock(); - bindingQueue.Setup(q => q.AddConnectionContext(It.IsAny(), It.IsAny())) - .Returns(this.testConnectionKey); - - // inject mock instances into the Language Service - LanguageService.WorkspaceServiceInstance = workspaceService.Object; - LanguageService.ConnectionServiceInstance = TestObjects.GetTestConnectionService(); - ConnectionInfo connectionInfo = TestObjects.GetTestConnectionInfo(); - LanguageService.ConnectionServiceInstance.OwnerToConnectionMap.Add(this.testScriptUri, connectionInfo); - LanguageService.Instance.BindingQueue = bindingQueue.Object; - - // setup the mock for SendResult - requestContext = new Mock>(); - requestContext.Setup(rc => rc.SendResult(It.IsAny())) - .Returns(Task.FromResult(0)); - requestContext.Setup(rc => rc.SendError(It.IsAny(), It.IsAny())).Returns(Task.FromResult(0)); - requestContext.Setup(r => r.SendEvent(It.IsAny>(), It.IsAny())).Returns(Task.FromResult(0)); - requestContext.Setup(r => r.SendEvent(It.IsAny>(), It.IsAny())).Returns(Task.FromResult(0)); - - // setup the IBinder mock - binder = new Mock(); - binder.Setup(b => b.Bind( - It.IsAny>(), - It.IsAny(), - It.IsAny())); - - var testScriptParseInfo = new ScriptParseInfo(); - LanguageService.Instance.AddOrUpdateScriptParseInfo(this.testScriptUri, testScriptParseInfo); - testScriptParseInfo.IsConnected = false; - testScriptParseInfo.ConnectionKey = LanguageService.Instance.BindingQueue.AddConnectionContext(connectionInfo); - - // setup the binding context object - ConnectedBindingContext bindingContext = new ConnectedBindingContext(); - bindingContext.Binder = binder.Object; - bindingContext.MetadataDisplayInfoProvider = new MetadataDisplayInfoProvider(); - LanguageService.Instance.BindingQueue.BindingContextMap.Add(testScriptParseInfo.ConnectionKey, bindingContext); - } - - /// /// Tests the definition event handler. When called with no active connection, an error is sent /// @@ -124,8 +42,9 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer public async Task DefinitionsHandlerWithNoConnectionTest() { InitializeTestObjects(); + scriptParseInfo.IsConnected = false; // request definition - var definitionTask = await Task.WhenAny(LanguageService.HandleDefinitionRequest(textDocument, requestContext.Object), Task.Delay(TaskTimeout)); + var definitionTask = await Task.WhenAny(langService.HandleDefinitionRequest(textDocument, requestContext.Object), Task.Delay(TaskTimeout)); await definitionTask; // verify that send result was not called and send error was called requestContext.Verify(m => m.SendResult(It.IsAny()), Times.Never()); @@ -199,9 +118,8 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer public void DeletePeekDefinitionScriptsTest() { Scripter peekDefinition = new Scripter(null, null); - var languageService = LanguageService.Instance; Assert.True(Directory.Exists(FileUtilities.PeekDefinitionTempFolder)); - languageService.DeletePeekDefinitionScripts(); + LanguageService.Instance.DeletePeekDefinitionScripts(); Assert.False(Directory.Exists(FileUtilities.PeekDefinitionTempFolder)); } @@ -211,12 +129,11 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer [Fact] public void DeletePeekDefinitionScriptsWhenFolderDoesNotExistTest() { - var languageService = LanguageService.Instance; Scripter peekDefinition = new Scripter(null, null); FileUtilities.SafeDirectoryDelete(FileUtilities.PeekDefinitionTempFolder, true); Assert.False(Directory.Exists(FileUtilities.PeekDefinitionTempFolder)); // Expected not to throw any exception - languageService.DeletePeekDefinitionScripts(); + LanguageService.Instance.DeletePeekDefinitionScripts(); } /// diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/SqlContext/SettingsTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/SqlContext/SettingsTests.cs index db2093ba..f76ac8e5 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/SqlContext/SettingsTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/SqlContext/SettingsTests.cs @@ -20,7 +20,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.SqlContext public void ValidateLanguageServiceDefaults() { var sqlToolsSettings = new SqlToolsSettings(); - Assert.True(sqlToolsSettings.IsDiagnositicsEnabled); + Assert.True(sqlToolsSettings.IsDiagnosticsEnabled); Assert.True(sqlToolsSettings.IsSuggestionsEnabled); Assert.True(sqlToolsSettings.SqlTools.IntelliSense.EnableIntellisense); Assert.True(sqlToolsSettings.SqlTools.IntelliSense.EnableErrorChecking); @@ -30,7 +30,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.SqlContext } /// - /// Validate that the IsDiagnositicsEnabled flag behavior + /// Validate that the IsDiagnosticsEnabled flag behavior /// [Fact] public void ValidateIsDiagnosticsEnabled() @@ -40,16 +40,16 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.SqlContext // diagnostics is enabled if IntelliSense and Diagnostics flags are set sqlToolsSettings.SqlTools.IntelliSense.EnableIntellisense = true; sqlToolsSettings.SqlTools.IntelliSense.EnableErrorChecking = true; - Assert.True(sqlToolsSettings.IsDiagnositicsEnabled); + Assert.True(sqlToolsSettings.IsDiagnosticsEnabled); // diagnostics is disabled if either IntelliSense and Diagnostics flags is not set sqlToolsSettings.SqlTools.IntelliSense.EnableIntellisense = false; sqlToolsSettings.SqlTools.IntelliSense.EnableErrorChecking = true; - Assert.False(sqlToolsSettings.IsDiagnositicsEnabled); + Assert.False(sqlToolsSettings.IsDiagnosticsEnabled); sqlToolsSettings.SqlTools.IntelliSense.EnableIntellisense = true; sqlToolsSettings.SqlTools.IntelliSense.EnableErrorChecking = false; - Assert.False(sqlToolsSettings.IsDiagnositicsEnabled); + Assert.False(sqlToolsSettings.IsDiagnosticsEnabled); } ///