diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs index a8e9ca3c..0df4ef2d 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs @@ -12,9 +12,8 @@ using System.Linq; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; -using Microsoft.SqlTools.ServiceLayer.Utility; using Microsoft.SqlTools.Utility; - +using Microsoft.SqlTools.ServiceLayer.Utility; namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { @@ -130,7 +129,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices return context.IsConnected; } return false; - } + } } /// @@ -144,7 +143,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { key = "disconnected_binding_context"; } - + lock (this.bindingContextLock) { if (!this.BindingContextMap.ContainsKey(key)) @@ -155,7 +154,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } return this.BindingContextMap[key]; - } + } } protected IEnumerable GetBindingContexts(string keyPrefix) @@ -269,10 +268,10 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices this.itemQueuedEvent, token.WaitHandle }; - + while (true) { - // wait for with an item to be queued or the a cancellation request + // wait for with an item to be queued or the cancellation request WaitHandle.WaitAny(waitHandles); if (token.IsCancellationRequested) { @@ -283,7 +282,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { // dispatch all pending queue items while (this.HasPendingQueueItems) - { + { QueueItem queueItem = GetNextQueueItem(); if (queueItem == null) { @@ -291,7 +290,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } IBindingContext bindingContext = GetOrCreateBindingContext(queueItem.Key); - if (bindingContext == null) + if (bindingContext == null) { queueItem.ItemProcessed.Set(); continue; @@ -300,140 +299,16 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices var bindingContextTask = this.BindingContextTasks[bindingContext]; // Run in the binding context task in case this task has to wait for a previous binding operation - this.BindingContextTasks[bindingContext] = bindingContextTask.ContinueWith((task) => - { - bool lockTaken = false; - try - { - // prefer the queue item binding item, otherwise use the context default timeout - int bindTimeout = queueItem.BindingTimeout ?? bindingContext.BindingTimeout; - - // handle the case a previous binding operation is still running - if (!bindingContext.BindingLock.WaitOne(queueItem.WaitForLockTimeout ?? 0)) - { - try - { - Logger.Warning("Binding queue operation timed out waiting for previous operation to finish"); - queueItem.Result = queueItem.TimeoutOperation != null - ? queueItem.TimeoutOperation(bindingContext) - : null; - } - catch (Exception ex) - { - Logger.Error("Exception running binding queue lock timeout handler: " + ex.ToString()); - } - finally - { - queueItem.ItemProcessed.Set(); - } - - return; - } - - bindingContext.BindingLock.Reset(); - - lockTaken = true; - - // execute the binding operation - object result = null; - CancellationTokenSource cancelToken = new CancellationTokenSource(); - - // run the operation in a separate thread - var bindTask = Task.Run(() => - { - try - { - result = queueItem.BindOperation( - bindingContext, - cancelToken.Token); - } - catch (Exception ex) - { - Logger.Error("Unexpected exception on the binding queue: " + ex.ToString()); - if (queueItem.ErrorHandler != null) - { - try - { - result = queueItem.ErrorHandler(ex); - } - catch (Exception ex2) - { - Logger.Error("Unexpected exception in binding queue error handler: " + ex2.ToString()); - } - } - - if (IsExceptionOfType(ex, typeof(SqlException)) || IsExceptionOfType(ex, typeof(SocketException))) - { - if (this.OnUnhandledException != null) - { - this.OnUnhandledException(queueItem.Key, ex); - } - - RemoveBindingContext(queueItem.Key); - } - } - }); - - Task.Run(() => - { - try - { - // check if the binding tasks completed within the binding timeout - if (bindTask.Wait(bindTimeout)) - { - queueItem.Result = result; - } - else - { - cancelToken.Cancel(); - - // if the task didn't complete then call the timeout callback - if (queueItem.TimeoutOperation != null) - { - queueItem.Result = queueItem.TimeoutOperation(bindingContext); - } - - bindTask.ContinueWithOnFaulted(t => Logger.Error("Binding queue threw exception " + t.Exception.ToString())); - - // Give the task a chance to complete before moving on to the next operation - bindTask.Wait(); - } - } - catch (Exception ex) - { - Logger.Error("Binding queue task completion threw exception " + ex.ToString()); - } - finally - { - // set item processed to avoid deadlocks - if (lockTaken) - { - bindingContext.BindingLock.Set(); - } - queueItem.ItemProcessed.Set(); - } - }); - } - catch (Exception ex) - { - // catch and log any exceptions raised in the binding calls - // set item processed to avoid deadlocks - Logger.Error("Binding queue threw exception " + ex.ToString()); - // set item processed to avoid deadlocks - if (lockTaken) - { - bindingContext.BindingLock.Set(); - } - queueItem.ItemProcessed.Set(); - } - }, TaskContinuationOptions.None); + this.BindingContextTasks[bindingContext] = bindingContextTask.ContinueWith( + task => DispatchQueueItem(bindingContext, queueItem) + , TaskContinuationOptions.RunContinuationsAsynchronously); // if a queue processing cancellation was requested then exit the loop if (token.IsCancellationRequested) { break; } - } + } } finally { @@ -450,6 +325,127 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } } + private void DispatchQueueItem(IBindingContext bindingContext, QueueItem queueItem) + { + bool lockTaken = false; + try + { + // prefer the queue item binding item, otherwise use the context default timeout - timeout is in milliseconds + int bindTimeoutInMs = queueItem.BindingTimeout ?? bindingContext.BindingTimeout; + + // handle the case a previous binding operation is still running + if (!bindingContext.BindingLock.WaitOne(queueItem.WaitForLockTimeout ?? 0)) + { + try + { + Logger.Warning("Binding queue operation timed out waiting for previous operation to finish"); + queueItem.Result = queueItem.TimeoutOperation != null + ? queueItem.TimeoutOperation(bindingContext) + : null; + } + catch (Exception ex) + { + Logger.Error("Exception running binding queue lock timeout handler: " + ex.ToString()); + } + finally + { + queueItem.ItemProcessed.Set(); + } + } + + bindingContext.BindingLock.Reset(); + + lockTaken = true; + + // execute the binding operation + object result = null; + CancellationTokenSource cancelToken = new CancellationTokenSource(); + + // run the operation in a separate thread + var bindTask = Task.Run(() => + { + try + { + result = queueItem.BindOperation( + bindingContext, + cancelToken.Token); + } + catch (Exception ex) + { + Logger.Error("Unexpected exception on the binding queue: " + ex.ToString()); + if (queueItem.ErrorHandler != null) + { + try + { + result = queueItem.ErrorHandler(ex); + } + catch (Exception ex2) + { + Logger.Error("Unexpected exception in binding queue error handler: " + ex2.ToString()); + } + } + if (IsExceptionOfType(ex, typeof(SqlException)) || IsExceptionOfType(ex, typeof(SocketException))) + { + if (this.OnUnhandledException != null) + { + this.OnUnhandledException(queueItem.Key, ex); + } + RemoveBindingContext(queueItem.Key); + } + } + }); + + Task.Run(() => + { + try + { + // check if the binding tasks completed within the binding timeout + if (bindTask.Wait(bindTimeoutInMs)) + { + queueItem.Result = result; + } + else + { + cancelToken.Cancel(); + // if the task didn't complete then call the timeout callback + if (queueItem.TimeoutOperation != null) + { + queueItem.Result = queueItem.TimeoutOperation(bindingContext); + } + bindTask.ContinueWithOnFaulted(t => Logger.Error("Binding queue threw exception " + t.Exception.ToString())); + // Give the task a chance to complete before moving on to the next operation + bindTask.Wait(); + } + } + catch (Exception ex) + { + Logger.Error("Binding queue task completion threw exception " + ex.ToString()); + } + finally + { + // set item processed to avoid deadlocks + if (lockTaken) + { + bindingContext.BindingLock.Set(); + } + queueItem.ItemProcessed.Set(); + } + }); + } + catch (Exception ex) + { + // catch and log any exceptions raised in the binding calls + // set item processed to avoid deadlocks + Logger.Error("Binding queue threw exception " + ex.ToString()); + // set item processed to avoid deadlocks + if (lockTaken) + { + bindingContext.BindingLock.Set(); + } + queueItem.ItemProcessed.Set(); + } + } + /// /// Clear queued items /// @@ -487,7 +483,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } } } - + private bool IsExceptionOfType(Exception ex, Type t) { return ex.GetType() == t || (ex.InnerException != null && ex.InnerException.GetType() == t); diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs index b89d4a4d..41d1403e 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs @@ -107,18 +107,18 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices compatibilityLevel: DatabaseCompatibilityLevel.Current, transactSqlVersion: TransactSqlVersion.Current); - private ConcurrentDictionary nonMssqlUriMap = new ConcurrentDictionary(); + private ConcurrentDictionary nonMssqlUriMap = new(); - private Lazy> scriptParseInfoMap - = new Lazy>(() => new Dictionary()); + private Lazy> scriptParseInfoMap + = new Lazy>(() => new()); - private readonly ConcurrentDictionary completionExtensions = new ConcurrentDictionary(); - private readonly ConcurrentDictionary extAssemblyLastUpdateTime = new ConcurrentDictionary(); + private readonly ConcurrentDictionary completionExtensions = new(); + private readonly ConcurrentDictionary extAssemblyLastUpdateTime = new(); /// /// Gets a mapping dictionary for SQL file URIs to ScriptParseInfo objects /// - internal Dictionary ScriptParseInfoMap + internal ConcurrentDictionary ScriptParseInfoMap { get { @@ -289,7 +289,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices WorkspaceServiceInstance.RegisterTextDocCloseCallback(HandleDidCloseTextDocumentNotification); // Register a callback for when a connection is created - ConnectionServiceInstance.RegisterOnConnectionTask(UpdateLanguageServiceOnConnection); + ConnectionServiceInstance.RegisterOnConnectionTask(StartUpdateLanguageServiceOnConnection); // Register a callback for when a connection is closed ConnectionServiceInstance.RegisterOnDisconnectTask(RemoveAutoCompleteCacheUriReference); @@ -521,23 +521,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices }; } - // turn off this code until needed (10/28/2016) -#if false - private async Task HandleReferencesRequest( - ReferencesParams referencesParams, - RequestContext requestContext) - { - await requestContext.SendResult(null); - } - - private async Task HandleDocumentHighlightRequest( - TextDocumentPosition textDocumentPosition, - RequestContext requestContext) - { - await requestContext.SendResult(null); - } -#endif - internal async Task HandleSignatureHelpRequest( TextDocumentPosition textDocumentPosition, RequestContext requestContext) @@ -551,18 +534,26 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { ScriptFile scriptFile = CurrentWorkspace.GetFile( textDocumentPosition.TextDocument.Uri); - SignatureHelp help = null; if (scriptFile != null) { - help = GetSignatureHelp(textDocumentPosition, scriptFile); - } - if (help != null) - { - await requestContext.SendResult(help); - } - else - { - await requestContext.SendResult(new SignatureHelp()); + // Start task asynchronously without blocking main thread - this is by design. + // Explanation: STS message queues are single-threaded queues, which should be unblocked as soon as possible. + // All Long-running tasks should be performed in a non-blocking background task, and results should be sent when ready. +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + GetSignatureHelp(textDocumentPosition, scriptFile) + .ContinueWith(async task => + { + var result = await task; + if (result != null) + { + await requestContext.SendResult(result); + } + else + { + await requestContext.SendResult(new SignatureHelp()); + } + }); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed } } } @@ -683,9 +674,33 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// Handle the rebuild IntelliSense cache notification /// - public async Task HandleRebuildIntelliSenseNotification( + /// Rebuild params + /// Event context + /// Async task + public Task HandleRebuildIntelliSenseNotification( RebuildIntelliSenseParams rebuildParams, EventContext eventContext) + { + // Start task asynchronously without blocking main thread - this is by design. + // Explanation: STS message queues are single-threaded queues, which should be unblocked as soon as possible. + // All Long-running tasks should be performed in a non-blocking background task, and results should be sent when ready. + Task.Factory.StartNew(async () => + { + await DoHandleRebuildIntellisenseNotification(rebuildParams, eventContext); + }, CancellationToken.None, + TaskCreationOptions.None, + TaskScheduler.Default); + + return Task.CompletedTask; + } + + /// + /// Internal method to handle rebuild intellisense notification + /// + /// Rebuild params + /// Event context + /// Async task + public async Task DoHandleRebuildIntellisenseNotification(RebuildIntelliSenseParams rebuildParams, EventContext eventContext) { try { @@ -694,7 +709,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices // This URI doesn't come in escaped - so if it's a file path with reserved characters (such as %) // then we'll fail to find it since GetFile expects the URI to be a fully-escaped URI as that's // what the document events are sent in as. - var escapedOwnerUri = Uri.EscapeUriString(rebuildParams.OwnerUri); + var escapedOwnerUri = Uri.EscapeDataString(rebuildParams.OwnerUri); // Skip closing this file if the file doesn't exist var scriptFile = this.CurrentWorkspace.GetFile(escapedOwnerUri); if (scriptFile == null) @@ -710,47 +725,44 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices // check that there is an active connection for the current editor if (connInfo != null) { - await Task.Run(() => + // Get the current ScriptInfo if one exists so we can lock it while we're rebuilding the cache + ScriptParseInfo scriptInfo = GetScriptParseInfo(connInfo.OwnerUri, createIfNotExists: false); + if (scriptInfo != null && scriptInfo.IsConnected && + Monitor.TryEnter(scriptInfo.BuildingMetadataLock, LanguageService.OnConnectionWaitTimeout)) { - // Get the current ScriptInfo if one exists so we can lock it while we're rebuilding the cache - ScriptParseInfo scriptInfo = GetScriptParseInfo(connInfo.OwnerUri, createIfNotExists: false); - if (scriptInfo != null && scriptInfo.IsConnected && - Monitor.TryEnter(scriptInfo.BuildingMetadataLock, LanguageService.OnConnectionWaitTimeout)) + try { - try - { - this.BindingQueue.AddConnectionContext(connInfo, featureName: Constants.LanguageServiceFeature, overwrite: true); - RemoveScriptParseInfo(rebuildParams.OwnerUri); - UpdateLanguageServiceOnConnection(connInfo).Wait(); - } - catch (Exception ex) - { - Logger.Error("Unknown error " + ex.ToString()); - } - finally - { - // Set Metadata Build event to Signal state. - Monitor.Exit(scriptInfo.BuildingMetadataLock); - } + this.BindingQueue.AddConnectionContext(connInfo, featureName: Constants.LanguageServiceFeature, overwrite: true); + RemoveScriptParseInfo(rebuildParams.OwnerUri); + await UpdateLanguageServiceOnConnection(connInfo); + } + catch (Exception ex) + { + Logger.Error("Unknown error " + ex.ToString()); + } + finally + { + // Set Metadata Build event to Signal state. + Monitor.Exit(scriptInfo.BuildingMetadataLock); } // if not in the preview window and diagnostics are enabled then run diagnostics if (!IsPreviewWindow(scriptFile) && CurrentWorkspaceSettings.IsDiagnosticsEnabled) { - RunScriptDiagnostics( - new ScriptFile[] { scriptFile }, - eventContext); + await RunScriptDiagnostics( + new ScriptFile[] { scriptFile }, + eventContext); } // Send a notification to signal that autocomplete is ready - ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() { OwnerUri = connInfo.OwnerUri }); - }); - } - else - { - // Send a notification to signal that autocomplete is ready - await ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() { OwnerUri = rebuildParams.OwnerUri }); + await ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() { OwnerUri = connInfo.OwnerUri }); + } + else + { + // Send a notification to signal that autocomplete is ready + await ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() { OwnerUri = rebuildParams.OwnerUri }); + } } } catch (Exception ex) @@ -887,121 +899,125 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// /// The ParseResult instance returned from SQL Parser - public ParseResult ParseAndBind(ScriptFile scriptFile, ConnectionInfo connInfo) + public Task ParseAndBind(ScriptFile scriptFile, ConnectionInfo connInfo) { Logger.Verbose($"ParseAndBind - {scriptFile}"); // get or create the current parse info object ScriptParseInfo parseInfo = GetScriptParseInfo(scriptFile.ClientUri, createIfNotExists: true); - - if (Monitor.TryEnter(parseInfo.BuildingMetadataLock, LanguageService.BindingTimeout)) + return Task.Run(() => { - try + if (Monitor.TryEnter(parseInfo.BuildingMetadataLock, LanguageService.BindingTimeout)) { - if (connInfo == null || !parseInfo.IsConnected) + try { - // parse on separate thread so stack size can be increased - var parseThread = new Thread(() => + if (connInfo == null || !parseInfo.IsConnected) { - try - { - // parse current SQL file contents to retrieve a list of errors - ParseResult parseResult = Parser.IncrementalParse( - scriptFile.Contents, - parseInfo.ParseResult, - this.DefaultParseOptions); - - parseInfo.ParseResult = parseResult; - } - catch (Exception e) - { - // Log the exception but don't rethrow it to prevent parsing errors from crashing SQL Tools Service - Logger.Error(string.Format("An unexpected error occured while parsing: {0}", e.ToString())); - } - }, ConnectedBindingQueue.QueueThreadStackSize); - parseThread.Start(); - parseThread.Join(); - } - else - { - QueueItem queueItem = this.BindingQueue.QueueBindingOperation( - key: parseInfo.ConnectionKey, - bindingTimeout: LanguageService.BindingTimeout, - bindOperation: (bindingContext, cancelToken) => + // parse on separate thread so stack size can be increased + var parseThread = new Thread(() => { try { + // parse current SQL file contents to retrieve a list of errors ParseResult parseResult = Parser.IncrementalParse( - scriptFile.Contents, - parseInfo.ParseResult, - bindingContext.ParseOptions); + scriptFile.Contents, + parseInfo.ParseResult, + this.DefaultParseOptions); parseInfo.ParseResult = parseResult; - - List parseResults = new List(); - parseResults.Add(parseResult); - if (bindingContext.IsConnected && bindingContext.Binder != null) + } + catch (Exception e) + { + // Log the exception but don't rethrow it to prevent parsing errors from crashing SQL Tools Service + Logger.Error(string.Format("An unexpected error occured while parsing: {0}", e.ToString())); + } + }, ConnectedBindingQueue.QueueThreadStackSize); + parseThread.Start(); + parseThread.Join(); + } + else + { + QueueItem queueItem = this.BindingQueue.QueueBindingOperation( + key: parseInfo.ConnectionKey, + bindingTimeout: LanguageService.BindingTimeout, + bindOperation: (bindingContext, cancelToken) => + { + try { - bindingContext.Binder.Bind( - parseResults, - connInfo.ConnectionDetails.DatabaseName, - BindMode.Batch); + ParseResult parseResult = Parser.IncrementalParse( + scriptFile.Contents, + parseInfo.ParseResult, + bindingContext.ParseOptions); + + parseInfo.ParseResult = parseResult; + + List parseResults = new List(); + parseResults.Add(parseResult); + if (bindingContext.IsConnected && bindingContext.Binder != null) + { + bindingContext.Binder.Bind( + parseResults, + connInfo.ConnectionDetails.DatabaseName, + BindMode.Batch); + } + } + catch (ConnectionException) + { + Logger.Error("Hit connection exception while binding - disposing binder object..."); + } + catch (SqlParserInternalBinderError) + { + Logger.Error("Hit connection exception while binding - disposing binder object..."); + } + catch (Exception ex) + { + Logger.Error("Unknown exception during parsing " + ex.ToString()); } - } - catch (ConnectionException) - { - Logger.Error("Hit connection exception while binding - disposing binder object..."); - } - catch (SqlParserInternalBinderError) - { - Logger.Error("Hit connection exception while binding - disposing binder object..."); - } - catch (Exception ex) - { - Logger.Error("Unknown exception during parsing " + ex.ToString()); - } - return null; - }); + return null; + }); - queueItem.ItemProcessed.WaitOne(); + queueItem.ItemProcessed.WaitOne(); + } + } + catch (Exception ex) + { + // reset the parse result to do a full parse next time + parseInfo.ParseResult = null; + Logger.Error("Unknown exception during parsing " + ex.ToString()); + } + finally + { + Monitor.Exit(parseInfo.BuildingMetadataLock); } } - catch (Exception ex) + else { - // reset the parse result to do a full parse next time - parseInfo.ParseResult = null; - Logger.Error("Unknown exception during parsing " + ex.ToString()); + Logger.Warning("Binding metadata lock timeout in ParseAndBind"); } - finally - { - Monitor.Exit(parseInfo.BuildingMetadataLock); - } - } - else - { - Logger.Warning("Binding metadata lock timeout in ParseAndBind"); - } - return parseInfo.ParseResult; + return parseInfo.ParseResult; + }); + } + + /// + /// Runs UpdateLanguageServiceOnConnection as a background task + /// + /// Connection Info + /// + public Task StartUpdateLanguageServiceOnConnection(ConnectionInfo info) + { + // Start task asynchronously without blocking main thread - this is by design. + // Explanation: STS message queues are single-threaded queues, which should be unblocked as soon as possible. + // All Long-running tasks should be performed in a non-blocking background task, and results should be sent when ready. + Task.Factory.StartNew(() => UpdateLanguageServiceOnConnection(info)); + return Task.CompletedTask; } /// /// Starts a Task to update the autocomplete metadata provider when the user connects to a database /// - /// Connection info - public Task UpdateLanguageServiceOnConnection(ConnectionInfo info) - { - return Task.Run(() => - { - DoUpdateLanguageServiceOnConnection(info); - }); - } - - /// - /// Update the autocomplete metadata provider when the user connects to a database synchronously - /// - /// Connection info - public void DoUpdateLanguageServiceOnConnection(ConnectionInfo info) + /// + public async Task UpdateLanguageServiceOnConnection(ConnectionInfo info) { if (ConnectionService.IsDedicatedAdminConnection(info.ConnectionDetails)) { @@ -1028,10 +1044,11 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices Monitor.Exit(scriptInfo.BuildingMetadataLock); } } - PrepopulateCommonMetadata(info, scriptInfo, this.BindingQueue); - - // Send a notification to signal that autocomplete is ready - ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() { OwnerUri = info.OwnerUri }); + await PrepopulateCommonMetadata(info, scriptInfo, this.BindingQueue).ContinueWith(async _ => + { + // Send a notification to signal that autocomplete is ready + await ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() { OwnerUri = info.OwnerUri }); + }); } @@ -1043,7 +1060,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// /// - internal void PrepopulateCommonMetadata( + internal async Task PrepopulateCommonMetadata( ConnectionInfo info, ScriptParseInfo scriptInfo, ConnectedBindingQueue bindingQueue) @@ -1060,71 +1077,73 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices return; } - ParseAndBind(scriptFile, info); - - if (Monitor.TryEnter(scriptInfo.BuildingMetadataLock, LanguageService.OnConnectionWaitTimeout)) + await ParseAndBind(scriptFile, info).ContinueWith(t => { - try + if (Monitor.TryEnter(scriptInfo.BuildingMetadataLock, LanguageService.OnConnectionWaitTimeout)) { - 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); - if (bindingContext.IsConnected && bindingContext.Binder != null) + try + { + QueueItem queueItem = bindingQueue.QueueBindingOperation( + key: scriptInfo.ConnectionKey, + bindingTimeout: PrepopulateBindTimeout, + waitForLockTimeout: PrepopulateBindTimeout, + bindOperation: (bindingContext, cancelToken) => { - 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 ", + // parse a simple statement that returns common metadata + ParseResult parseResult = Parser.Parse( + "select ", bindingContext.ParseOptions); + if (bindingContext.IsConnected && bindingContext.Binder != null) + { + List parseResults = new List(); + parseResults.Add(parseResult); + bindingContext.Binder.Bind( + parseResults, + info.ConnectionDetails.DatabaseName, + BindMode.Batch); - 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); - // 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, 8, 8); - // this forces lazy evaluation of the suggestion metadata - AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 6, 6); - } - return null; - }); + parseResult = Parser.Parse( + "exec ", + bindingContext.ParseOptions); - queueItem.ItemProcessed.WaitOne(); + 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 (Exception ex) + { + Logger.Error("Exception in PrepopulateCommonMetadata " + ex.ToString()); + } + finally + { + Monitor.Exit(scriptInfo.BuildingMetadataLock); + } } - catch - { - } - finally - { - Monitor.Exit(scriptInfo.BuildingMetadataLock); - } - } + }); } } @@ -1348,7 +1367,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices if (RequiresReparse(scriptParseInfo, scriptFile)) { - scriptParseInfo.ParseResult = ParseAndBind(scriptFile, connInfo); + scriptParseInfo.ParseResult = ParseAndBind(scriptFile, connInfo).GetAwaiter().GetResult(); } // Get token from selected text @@ -1499,7 +1518,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// Get function signature help for the current position /// - internal SignatureHelp? GetSignatureHelp(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile) + internal async Task GetSignatureHelp(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile) { Logger.Verbose($"GetSignatureHelp - {scriptFile}"); int startLine = textDocumentPosition.Position.Line; @@ -1522,8 +1541,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices // reparse and bind the SQL statement if needed if (RequiresReparse(scriptParseInfo, scriptFile)) { - ParseAndBind(scriptFile, connInfo); - } else + await ParseAndBind(scriptFile, connInfo); + } + else { Logger.Verbose($"GetSignatureHelp - No reparse needed for {scriptFile}"); } @@ -1570,6 +1590,10 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices Logger.Verbose($"GetSignatureHelp - Got result {queueItem.Result}"); return queueItem.GetResultAsT(); } + catch (Exception ex) + { + Logger.Error("Exception in GetSignatureHelp " + ex.ToString()); + } finally { Monitor.Exit(scriptParseInfo.BuildingMetadataLock); @@ -1615,7 +1639,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices // reparse and bind the SQL statement if needed if (RequiresReparse(scriptParseInfo, scriptFile)) { - ParseAndBind(scriptFile, connInfo); + await ParseAndBind(scriptFile, connInfo); } // if the parse failed then return the default list @@ -1693,14 +1717,14 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// Gets a list of semantic diagnostic marks for the provided script file /// /// - internal ScriptFileMarker[] GetSemanticMarkers(ScriptFile scriptFile) + internal async Task GetSemanticMarkers(ScriptFile scriptFile) { ConnectionInfo connInfo; ConnectionServiceInstance.TryFindConnection( scriptFile.ClientUri, out connInfo); - var parseResult = ParseAndBind(scriptFile, connInfo); + var parseResult = await ParseAndBind(scriptFile, connInfo); // build a list of SQL script file markers from the errors List markers = new List(); @@ -1835,10 +1859,18 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } Logger.Verbose("Analyzing script file: " + scriptFile.FilePath); - ScriptFileMarker[] semanticMarkers = GetSemanticMarkers(scriptFile); - Logger.Verbose("Analysis complete."); - await DiagnosticsHelper.PublishScriptDiagnostics(scriptFile, semanticMarkers, eventContext); + // Start task asynchronously without blocking main thread - this is by design. + // Explanation: STS message queues are single-threaded queues, which should be unblocked as soon as possible. + // All Long-running tasks should be performed in a non-blocking background task, and results should be sent when ready. +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + GetSemanticMarkers(scriptFile).ContinueWith(async t => + { + var semanticMarkers = t.GetAwaiter().GetResult(); + Logger.Verbose("Analysis complete."); + await DiagnosticsHelper.PublishScriptDiagnostics(scriptFile, semanticMarkers, eventContext); + }, CancellationToken.None); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed } } @@ -1861,7 +1893,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices else { Logger.Verbose($"Adding ScriptParseInfo for uri {uri}"); - this.ScriptParseInfoMap.Add(uri, scriptInfo); + this.ScriptParseInfoMap.TryAdd(uri, scriptInfo); } } @@ -1887,7 +1919,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices Logger.Verbose($"ScriptParseInfo for uri {uri} did not exist, creating new one"); // create a new script parse info object and initialize with the current settings ScriptParseInfo scriptInfo = new ScriptParseInfo(); - this.ScriptParseInfoMap.Add(uri, scriptInfo); + this.ScriptParseInfoMap.TryAdd(uri, scriptInfo); return scriptInfo; } else @@ -1902,15 +1934,8 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { lock (this.parseMapLock) { - if (this.ScriptParseInfoMap.ContainsKey(uri)) - { - Logger.Verbose($"Removing ScriptParseInfo for uri {uri}"); - return this.ScriptParseInfoMap.Remove(uri); - } - else - { - return false; - } + Logger.Verbose($"Removing ScriptParseInfo for uri {uri}"); + return this.ScriptParseInfoMap.TryRemove(uri, out _); } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs index 86814202..9efa6a60 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs @@ -1064,22 +1064,19 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution // what the document events are sent in as. var escapedOwnerUri = Uri.EscapeUriString(request.OwnerUri); // If it is a document selection, we'll retrieve the text from the document - ExecuteDocumentSelectionParams docRequest = request as ExecuteDocumentSelectionParams; - if (docRequest != null) + if (request is ExecuteDocumentSelectionParams docRequest) { return GetSqlTextFromSelectionData(escapedOwnerUri, docRequest.QuerySelection); } // If it is a document statement, we'll retrieve the text from the document - ExecuteDocumentStatementParams stmtRequest = request as ExecuteDocumentStatementParams; - if (stmtRequest != null) + if (request is ExecuteDocumentStatementParams stmtRequest) { return GetSqlStatementAtPosition(escapedOwnerUri, stmtRequest.Line, stmtRequest.Column); } // If it is an ExecuteStringParams, return the text as is - ExecuteStringParams stringRequest = request as ExecuteStringParams; - if (stringRequest != null) + if (request is ExecuteStringParams stringRequest) { return stringRequest.Query; } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Workspace/Workspace.cs b/src/Microsoft.SqlTools.ServiceLayer/Workspace/Workspace.cs index 551056fc..d541cab3 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Workspace/Workspace.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Workspace/Workspace.cs @@ -15,6 +15,7 @@ using Microsoft.SqlTools.Utility; using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; using System.Runtime.InteropServices; using Microsoft.SqlTools.ServiceLayer.Utility; +using System.Collections.Concurrent; namespace Microsoft.SqlTools.ServiceLayer.Workspace { @@ -35,7 +36,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace "vscode-notebook-cell" }; - private Dictionary workspaceFiles = new Dictionary(); + private ConcurrentDictionary workspaceFiles = new ConcurrentDictionary(); #endregion @@ -121,7 +122,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace { scriptFile = new ScriptFile(resolvedFile.FilePath, resolvedFile.ClientUri,streamReader); - this.workspaceFiles.Add(keyName, scriptFile); + this.workspaceFiles.TryAdd(keyName, scriptFile); } Logger.Verbose("Opened file on disk: " + resolvedFile.FilePath); @@ -235,7 +236,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace { scriptFile = new ScriptFile(resolvedFile.FilePath, resolvedFile.ClientUri, initialBuffer); - this.workspaceFiles.Add(keyName, scriptFile); + this.workspaceFiles.TryAdd(keyName, scriptFile); Logger.Verbose("Opened file as in-memory buffer: " + resolvedFile.FilePath); } @@ -260,7 +261,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace { Validate.IsNotNull("scriptFile", scriptFile); - this.workspaceFiles.Remove(scriptFile.Id); + this.workspaceFiles.TryRemove(scriptFile.Id, out _); } internal string GetBaseFilePath(string filePath) diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/LanguageServer/LanguageServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/LanguageServer/LanguageServiceTests.cs index 3aec39fe..957ee80f 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/LanguageServer/LanguageServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/LanguageServer/LanguageServiceTests.cs @@ -177,7 +177,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer requestContext.Verify(x => x.SendError(It.IsAny(), 0, It.IsAny()), Times.Once); ScriptParseInfo scriptInfo = new ScriptParseInfo { IsConnected = true }; - autoCompleteService.ParseAndBind(result.ScriptFile, result.ConnectionInfo); + await autoCompleteService.ParseAndBind(result.ScriptFile, result.ConnectionInfo); scriptInfo.ConnectionKey = autoCompleteService.BindingQueue.AddConnectionContext(result.ConnectionInfo); //Invoke auto completion with extension enabled @@ -219,7 +219,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer /// provide signature help. /// [Test] - public void GetSignatureHelpReturnsNotNullIfParseInfoInitialized() + public async Task GetSignatureHelpReturnsNotNullIfParseInfoInitialized() { // When we make a connection to a live database Hosting.ServiceHost.SendEventIgnoreExceptions = true; @@ -243,14 +243,14 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer // If the SQL has already been parsed var service = CreateLanguageService(result.ScriptFile); - service.DoUpdateLanguageServiceOnConnection(result.ConnectionInfo); + await service.UpdateLanguageServiceOnConnection(result.ConnectionInfo); // We should get back a non-null ScriptParseInfo ScriptParseInfo? parseInfo = service.GetScriptParseInfo(result.ScriptFile.ClientUri); Assert.That(parseInfo, Is.Not.Null, "ScriptParseInfo"); // And we should get back a non-null SignatureHelp - SignatureHelp? signatureHelp = service.GetSignatureHelp(textDocument, result.ScriptFile); + SignatureHelp? signatureHelp = await service.GetSignatureHelp(textDocument, result.ScriptFile); Assert.That(signatureHelp, Is.Not.Null, "SignatureHelp"); } @@ -319,7 +319,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer testDb.RunQuery("CREATE TABLE dbo.foo(col1 int)"); // And refresh the cache - await langService.HandleRebuildIntelliSenseNotification( + await langService.DoHandleRebuildIntellisenseNotification( new RebuildIntelliSenseParams() { OwnerUri = connectionInfoResult.ScriptFile.ClientUri }, new TestEventContext()); @@ -446,7 +446,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer testDb.RunQuery(createTableQueries); // And refresh the cache - await langService.HandleRebuildIntelliSenseNotification( + await langService.DoHandleRebuildIntellisenseNotification( new RebuildIntelliSenseParams() { OwnerUri = connectionInfoResult.ScriptFile.ClientUri }, new TestEventContext()); diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/LanguageServer/PeekDefinitionTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/LanguageServer/PeekDefinitionTests.cs index 8aad63b4..71737a1b 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/LanguageServer/PeekDefinitionTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/LanguageServer/PeekDefinitionTests.cs @@ -259,7 +259,7 @@ GO"; scriptFile.Contents = "select * from dbo.func ()"; ScriptParseInfo scriptInfo = new ScriptParseInfo { IsConnected = true }; - languageService.ScriptParseInfoMap.Add(scriptFile.ClientUri, scriptInfo); + languageService.ScriptParseInfoMap.TryAdd(scriptFile.ClientUri, scriptInfo); // Pass in null connection info to force doing a local parse since that hits the BindingQueue timeout // before we want it to (this is testing the timeout trying to fetch the definitions after the parse) @@ -729,9 +729,9 @@ GO"; Thread.Sleep(2000); ScriptParseInfo scriptInfo = new ScriptParseInfo { IsConnected = true }; - service.ParseAndBind(scriptFile, connInfo); + await service.ParseAndBind(scriptFile, connInfo); scriptInfo.ConnectionKey = bindingQueue.AddConnectionContext(connInfo); - service.ScriptParseInfoMap.Add(OwnerUri, scriptInfo); + service.ScriptParseInfoMap.TryAdd(OwnerUri, scriptInfo); // When I call the language service var objectResult = service.GetDefinition(objectDocument, scriptFile, connInfo); @@ -750,7 +750,7 @@ GO"; Cleanup(objectResult.Locations); Cleanup(sysResult.Locations); Cleanup(masterResult.Locations); - service.ScriptParseInfoMap.Remove(OwnerUri); + service.ScriptParseInfoMap.TryRemove(OwnerUri, out _); connInfo.RemoveAllConnections(); } @@ -786,9 +786,9 @@ GO"; Thread.Sleep(2000); ScriptParseInfo scriptInfo = new ScriptParseInfo { IsConnected = true }; - service.ParseAndBind(scriptFile, connInfo); + await service.ParseAndBind(scriptFile, connInfo); scriptInfo.ConnectionKey = bindingQueue.AddConnectionContext(connInfo); - service.ScriptParseInfoMap.Add(TestUri, scriptInfo); + service.ScriptParseInfoMap.TryAdd(TestUri, scriptInfo); // When I call the language service var fnResult = service.GetDefinition(fnDocument, scriptFile, connInfo); @@ -807,7 +807,7 @@ GO"; Cleanup(fnResult.Locations); Cleanup(sysResult.Locations); Cleanup(masterResult.Locations); - service.ScriptParseInfoMap.Remove(TestUri); + service.ScriptParseInfoMap.TryRemove(TestUri, out _); connInfo.RemoveAllConnections(); } diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/AutocompleteTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/AutocompleteTests.cs index d4242c45..89642915 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/AutocompleteTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/AutocompleteTests.cs @@ -218,7 +218,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer }; - ParseResult parseResult = langService.ParseAndBind(scriptFile, null); + ParseResult parseResult = langService.ParseAndBind(scriptFile, null).GetAwaiter().GetResult(); ScriptParseInfo scriptParseInfo = langService.GetScriptParseInfo(scriptFile.ClientUri, true); return new ScriptDocumentInfo(textDocumentPosition, scriptFile, scriptParseInfo); diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/LanguageServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/LanguageServiceTests.cs index ce93f17c..03b553bd 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/LanguageServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/LanguageServiceTests.cs @@ -26,17 +26,17 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer public void LatestSqlParserIsUsedByDefault() { // This should only parse correctly on SQL server 2016 or newer - const string sql2016Text = + const string sql2016Text = @"CREATE SECURITY POLICY [FederatedSecurityPolicy]" + "\r\n" + - @"ADD FILTER PREDICATE [rls].[fn_securitypredicate]([CustomerId])" + "\r\n" + + @"ADD FILTER PREDICATE [rls].[fn_securitypredicate]([CustomerId])" + "\r\n" + @"ON [dbo].[Customer];"; - + LanguageService service = TestObjects.GetTestLanguageService(); // parse var scriptFile = new ScriptFile(); scriptFile.SetFileContents(sql2016Text); - ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile); + ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile).GetAwaiter().GetResult(); // verify that no errors are detected Assert.AreEqual(0, fileMarkers.Length); @@ -57,7 +57,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer // parse the sql statement var scriptFile = new ScriptFile(); scriptFile.SetFileContents(sqlWithErrors); - ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile); + ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile).GetAwaiter().GetResult(); // verify there are no errors Assert.AreEqual(0, fileMarkers.Length); @@ -78,7 +78,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer // parse sql statement var scriptFile = new ScriptFile(); scriptFile.SetFileContents(sqlWithErrors); - ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile); + ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile).GetAwaiter().GetResult(); // verify there is one error Assert.AreEqual(1, fileMarkers.Length); @@ -97,7 +97,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer public void ParseMultilineSqlWithErrors() { // multiline sql with errors - const string sqlWithErrors = + const string sqlWithErrors = "SELECT *** FROM sys.objects;\n" + "GO\n" + "SELECT *** FROM sys.objects;\n"; @@ -108,7 +108,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer // parse sql var scriptFile = new ScriptFile(); scriptFile.SetFileContents(sqlWithErrors); - ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile); + ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile).GetAwaiter().GetResult(); // verify there are two errors Assert.AreEqual(2, fileMarkers.Length); @@ -140,29 +140,29 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer scriptFile.SetFileContents(docContent); // When requesting SignatureHelp - SignatureHelp signatureHelp = service.GetSignatureHelp(TestObjects.GetTestDocPosition(), scriptFile); - + SignatureHelp signatureHelp = service.GetSignatureHelp(TestObjects.GetTestDocPosition(), scriptFile).GetAwaiter().GetResult(); + // Then null is returned as no parse info can be used to find the signature Assert.Null(signatureHelp); } [Test] public void EmptyCompletionListTest() - { + { Assert.AreEqual(0, AutoCompleteHelper.EmptyCompletionList.Length); } internal sealed class TestScriptDocumentInfo : ScriptDocumentInfo { - public TestScriptDocumentInfo(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile, ScriptParseInfo scriptParseInfo, + public TestScriptDocumentInfo(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile, ScriptParseInfo scriptParseInfo, string tokenText = null) - :base(textDocumentPosition, scriptFile, scriptParseInfo) + : base(textDocumentPosition, scriptFile, scriptParseInfo) { this.tokenText = string.IsNullOrEmpty(tokenText) ? "doesntmatchanythingintheintellisensedefaultlist" : tokenText; } private string tokenText; - + public override string TokenText { get @@ -174,7 +174,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer [Test] public void GetDefaultCompletionListWithNoMatchesTest() - { + { var scriptFile = new ScriptFile(); scriptFile.SetFileContents("koko wants a bananas"); @@ -183,10 +183,10 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer var scriptDocumentInfo = new TestScriptDocumentInfo( new TextDocumentPosition() { - TextDocument = new TextDocumentIdentifier() { Uri = TestObjects.ScriptUri }, + TextDocument = new TextDocumentIdentifier() { Uri = TestObjects.ScriptUri }, Position = new Position() { Line = 0, Character = 0 } }, scriptFile, scriptInfo); - + AutoCompleteHelper.GetDefaultCompletionItems(scriptDocumentInfo, false); }