diff --git a/src/Microsoft.SqlTools.Hosting/Hosting/Protocol/EventContext.cs b/src/Microsoft.SqlTools.Hosting/Hosting/Protocol/EventContext.cs index 47225c56..27b0248e 100644 --- a/src/Microsoft.SqlTools.Hosting/Hosting/Protocol/EventContext.cs +++ b/src/Microsoft.SqlTools.Hosting/Hosting/Protocol/EventContext.cs @@ -26,8 +26,8 @@ namespace Microsoft.SqlTools.Hosting.Protocol this.messageWriter = messageWriter; } - public async Task SendEvent( - EventType eventType, + public virtual async Task SendEvent( + EventType eventType, TParams eventParams) { await this.messageWriter.WriteEvent( diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs index 18fb839c..9b9490df 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs @@ -290,17 +290,17 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// internal async Task HandleSyntaxParseRequest(SyntaxParseParams param, RequestContext requestContext) - { - await Task.Run(async () => + { + await Task.Run(async () => { try - { + { ParseResult result = Parser.Parse(param.Query); SyntaxParseResult syntaxResult = new SyntaxParseResult(); if (result != null && result.Errors.Count() == 0) { syntaxResult.Parseable = true; - } else + } else { syntaxResult.Parseable = false; string[] errorMessages = new string[result.Errors.Count()]; @@ -328,9 +328,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices internal async Task HandleCompletionRequest( TextDocumentPosition textDocumentPosition, RequestContext requestContext) - { + { try - { + { // check if Intellisense suggestions are enabled if (ShouldSkipIntellisense(textDocumentPosition.TextDocument.Uri)) { @@ -355,7 +355,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices var completionItems = GetCompletionItems( textDocumentPosition, scriptFile, connInfo); - await requestContext.SendResult(completionItems); + await requestContext.SendResult(completionItems); } } catch (Exception ex) @@ -397,7 +397,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices internal async Task HandleDefinitionRequest(TextDocumentPosition textDocumentPosition, RequestContext requestContext) { - try + try { DocumentStatusHelper.SendStatusChange(requestContext, textDocumentPosition, DocumentStatusHelper.DefinitionRequested); @@ -414,7 +414,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices isConnected = ConnectionServiceInstance.TryFindConnection(scriptFile.ClientFilePath, out connInfo); definitionResult = GetDefinition(textDocumentPosition, scriptFile, connInfo); } - + if (definitionResult != null && !definitionResult.IsErrorResult) { await requestContext.SendResult(definitionResult.Locations); @@ -628,13 +628,16 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { 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 && + if (scriptInfo != null && scriptInfo.IsConnected && Monitor.TryEnter(scriptInfo.BuildingMetadataLock, LanguageService.OnConnectionWaitTimeout)) { try { this.BindingQueue.AddConnectionContext(connInfo, featureName: "LanguageService", overwrite: true); + RemoveScriptParseInfo(rebuildParams.OwnerUri); + UpdateLanguageServiceOnConnection(connInfo).Wait(); } catch (Exception ex) { @@ -727,7 +730,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// public async Task HandleDidChangeLanguageFlavorNotification( LanguageFlavorChangeParams changeParams, - EventContext eventContext) + EventContext eventContext) { try { @@ -814,7 +817,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } }, ConnectedBindingQueue.QueueThreadStackSize); parseThread.Start(); - parseThread.Join(); + parseThread.Join(); } else { @@ -1109,7 +1112,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices return completionItem; } - + /// /// Queue a task to the binding queue /// @@ -1119,7 +1122,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// /// Returns the result of the task as a DefinitionResult - private DefinitionResult QueueTask(TextDocumentPosition textDocumentPosition, ScriptParseInfo scriptParseInfo, + private DefinitionResult QueueTask(TextDocumentPosition textDocumentPosition, ScriptParseInfo scriptParseInfo, ConnectionInfo connInfo, ScriptFile scriptFile, string tokenText) { // Queue the task with the binding queue @@ -1132,10 +1135,10 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices // Script object using SMO Scripter scripter = new Scripter(bindingContext.ServerConnection, connInfo); return scripter.GetScript( - scriptParseInfo.ParseResult, - textDocumentPosition.Position, - bindingContext.MetadataDisplayInfoProvider, - tokenText, + scriptParseInfo.ParseResult, + textDocumentPosition.Position, + bindingContext.MetadataDisplayInfoProvider, + tokenText, schemaName); }, timeoutOperation: (bindingContext) => @@ -1158,7 +1161,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices Locations = null }; }); - + // wait for the queue item queueItem.ItemProcessed.WaitOne(); var result = queueItem.GetResultAsT(); @@ -1235,7 +1238,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } // Get token from selected text - Tuple, Queue> selectedToken = ScriptDocumentInfo.GetPeekDefinitionTokens(scriptParseInfo, + Tuple, Queue> selectedToken = ScriptDocumentInfo.GetPeekDefinitionTokens(scriptParseInfo, textDocumentPosition.Position.Line + 1, textDocumentPosition.Position.Character + 1); if (selectedToken == null) @@ -1250,7 +1253,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices List tokenList = childrenTokens.ToList(); DefinitionResult childrenResult = GetDefinitionFromTokenList(textDocumentPosition, tokenList, scriptParseInfo, scriptFile, connInfo); - // if the children peak definition returned null then + // if the children peak definition returned null then // try the parents if (childrenResult == null || childrenResult.IsErrorResult) { @@ -1262,7 +1265,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices else { return childrenResult; - } + } } else { @@ -1275,9 +1278,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices }; } } - + /// - /// Wrapper around find token method + /// Wrapper around find token method /// /// /// @@ -1774,13 +1777,13 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices bool? lineHasSingleStatement = null; // check if the batch matches parameters - if (batch.StartLocation.LineNumber <= parserLine + if (batch.StartLocation.LineNumber <= parserLine && batch.EndLocation.LineNumber >= parserLine) { foreach (var statement in batch.Statements) { // check if the statement matches parameters - if (statement.StartLocation.LineNumber <= parserLine + if (statement.StartLocation.LineNumber <= parserLine && statement.EndLocation.LineNumber >= parserLine) { if (statement.EndLocation.LineNumber == parserLine && statement.EndLocation.ColumnNumber < parserColumn diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/LanguageServer/LanguageServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/LanguageServer/LanguageServiceTests.cs index 6e8c35d1..8c20dd17 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/LanguageServer/LanguageServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/LanguageServer/LanguageServiceTests.cs @@ -3,6 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; @@ -74,7 +75,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer } } - // This test currently requires a live database connection to initialize + // This test currently requires a live database connection to initialize // SMO connected metadata provider. Since we don't want a live DB dependency // in the CI unit tests this scenario is currently disabled. [Fact] @@ -155,7 +156,65 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer // add a new connection context connectionKey = LanguageService.Instance.BindingQueue.AddConnectionContext(result.ConnectionInfo, overwrite: true); Assert.True(LanguageService.Instance.BindingQueue.BindingContextMap.ContainsKey(connectionKey)); - Assert.False(object.ReferenceEquals(LanguageService.Instance.BindingQueue.BindingContextMap[connectionKey].ServerConnection, orgServerConnection)); - } + Assert.False(object.ReferenceEquals(LanguageService.Instance.BindingQueue.BindingContextMap[connectionKey].ServerConnection, orgServerConnection)); + } + + /// + /// Verifies that clearing the Intellisense cache correctly refreshes the cache with new info from the DB. + /// + [Fact] + public async Task RebuildIntellisenseCacheClearsScriptParseInfoCorrectly() + { + var testDb = SqlTestDb.CreateNew(TestServerType.OnPrem, false, null, null, "LangSvcTest"); + try + { + var connectionInfoResult = LiveConnectionHelper.InitLiveConnectionInfo(testDb.DatabaseName); + + var langService = LanguageService.Instance; + await langService.UpdateLanguageServiceOnConnection(connectionInfoResult.ConnectionInfo); + var queryText = "SELECT * FROM dbo."; + connectionInfoResult.ScriptFile.SetFileContents(queryText); + + var textDocumentPosition = + connectionInfoResult.TextDocumentPosition ?? + new TextDocumentPosition() + { + TextDocument = new TextDocumentIdentifier + { + Uri = connectionInfoResult.ScriptFile.ClientFilePath + }, + Position = new Position + { + Line = 0, + Character = queryText.Length + } + }; + + // First check that we don't have any items in the completion list as expected + var initialCompletionItems = langService.GetCompletionItems( + textDocumentPosition, connectionInfoResult.ScriptFile, connectionInfoResult.ConnectionInfo); + + Assert.True(initialCompletionItems.Length == 0, $"Should not have any completion items initially. Actual : [{string.Join(',', initialCompletionItems.Select(ci => ci.Label))}]"); + + // Now create a table that should show up in the completion list + testDb.RunQuery("CREATE TABLE dbo.foo(col1 int)"); + + // And refresh the cache + await langService.HandleRebuildIntelliSenseNotification( + new RebuildIntelliSenseParams() { OwnerUri = connectionInfoResult.ScriptFile.ClientFilePath }, + new TestEventContext()); + + // Now we should expect to see the item show up in the completion list + var afterTableCreationCompletionItems = langService.GetCompletionItems( + textDocumentPosition, connectionInfoResult.ScriptFile, connectionInfoResult.ConnectionInfo); + + Assert.True(afterTableCreationCompletionItems.Length == 1, $"Should only have a single completion item after rebuilding Intellisense cache. Actual : [{string.Join(',', initialCompletionItems.Select(ci => ci.Label))}]"); + Assert.True(afterTableCreationCompletionItems[0].InsertText == "foo", $"Expected single completion item 'foo'. Actual : [{string.Join(',', initialCompletionItems.Select(ci => ci.Label))}]"); + } + finally + { + testDb.Cleanup(); + } + } } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestEventContext.cs b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestEventContext.cs new file mode 100644 index 00000000..1fd970b7 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestEventContext.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.Test.Common +{ + /// + /// Simple EventContext for testing that just swallows all events. + /// + public class TestEventContext : EventContext + { + public override async Task SendEvent( + EventType eventType, + TParams eventParams) + { + await Task.FromResult(0); + } + } +}