From e74a392ca909b8ab8357337c9d15fb92a55ce014 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Sun, 18 Sep 2016 01:54:32 -0700 Subject: [PATCH 1/6] Improve autocomplete locking --- .../Connection/ConnectionService.cs | 4 +- .../LanguageServices/AutoCompleteHelper.cs | 78 +++++- .../LanguageServices/LanguageService.cs | 240 ++++++++++++------ .../LanguageServices/ScriptParseInfo.cs | 15 +- .../Utility/Logger.cs | 38 +-- 5 files changed, 272 insertions(+), 103 deletions(-) diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs index a10948ec..6cdd62aa 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs @@ -73,7 +73,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection /// // Callback for ondisconnect handler /// - public delegate Task OnDisconnectHandler(ConnectionSummary summary); + public delegate Task OnDisconnectHandler(ConnectionSummary summary, string ownerUri); /// /// List of onconnection handlers @@ -241,7 +241,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection // Invoke callback notifications foreach (var activity in this.onDisconnectActivities) { - activity(info.ConnectionDetails); + activity(info.ConnectionDetails, disconnectParams.OwnerUri); } // Success diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs index 1069e18d..85e9b10b 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs @@ -4,8 +4,13 @@ // using System.Collections.Generic; +using Microsoft.SqlServer.Management.SqlParser.Binder; using Microsoft.SqlServer.Management.SqlParser.Intellisense; +using Microsoft.SqlServer.Management.SqlParser.Parser; +using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; +using Microsoft.SqlTools.ServiceLayer.SqlContext; +using Microsoft.SqlTools.ServiceLayer.Workspace; using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; namespace Microsoft.SqlTools.ServiceLayer.LanguageServices @@ -487,7 +492,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices Label = autoCompleteItem.Title, Kind = CompletionItemKind.Variable, Detail = autoCompleteItem.Title, - Documentation = autoCompleteItem.Description, + // Documentation = autoCompleteItem.Description, TextEdit = new TextEdit { NewText = autoCompleteItem.Title, @@ -510,5 +515,76 @@ 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) + { + if (scriptInfo.IsConnected) + { + var scriptFile = WorkspaceService.Instance.Workspace.GetFile(info.OwnerUri); + LanguageService.Instance.ParseAndBind(scriptFile, info); + + if (scriptInfo.BuildingMetadataEvent.WaitOne(LanguageService.OnConnectionWaitTimeout)) + { + try + { + scriptInfo.BuildingMetadataEvent.Reset(); + + // parse a simple statement that returns common metadata + ParseResult parseResult = Parser.Parse( + "select ", + scriptInfo.ParseOptions); + + List parseResults = new List(); + parseResults.Add(parseResult); + scriptInfo.Binder.Bind( + parseResults, + info.ConnectionDetails.DatabaseName, + BindMode.Batch); + + // get the completion list from SQL Parser + var suggestions = Resolver.FindCompletions( + parseResult, 1, 8, + scriptInfo.MetadataDisplayInfoProvider); + + // this forces lazy evaluation of the suggestion metadata + AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 8, 8); + + parseResult = Parser.Parse( + "exec ", + scriptInfo.ParseOptions); + + parseResults = new List(); + parseResults.Add(parseResult); + scriptInfo.Binder.Bind( + parseResults, + info.ConnectionDetails.DatabaseName, + BindMode.Batch); + + // get the completion list from SQL Parser + suggestions = Resolver.FindCompletions( + parseResult, 1, 6, + scriptInfo.MetadataDisplayInfoProvider); + + // this forces lazy evaluation of the suggestion metadata + AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 6, 6); + } + catch + { + } + finally + { + scriptInfo.BuildingMetadataEvent.Set(); + } + } + } + } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs index 6986314e..a87ab5b7 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs @@ -8,6 +8,13 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.SqlParser; +using Microsoft.SqlServer.Management.SqlParser.Binder; +using Microsoft.SqlServer.Management.SqlParser.Intellisense; +using Microsoft.SqlServer.Management.SqlParser.MetadataProvider; +using Microsoft.SqlServer.Management.SqlParser.Parser; +using Microsoft.SqlServer.Management.SmoMetadataProvider; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection; @@ -15,16 +22,9 @@ using Microsoft.SqlTools.ServiceLayer.Hosting; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; using Microsoft.SqlTools.ServiceLayer.SqlContext; +using Microsoft.SqlTools.ServiceLayer.Utility; using Microsoft.SqlTools.ServiceLayer.Workspace; using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; -using Microsoft.SqlServer.Management.Common; -using Microsoft.SqlServer.Management.SqlParser; -using Microsoft.SqlTools.ServiceLayer.Utility; -using Microsoft.SqlServer.Management.SqlParser.Binder; -using Microsoft.SqlServer.Management.SqlParser.Intellisense; -using Microsoft.SqlServer.Management.SqlParser.MetadataProvider; -using Microsoft.SqlServer.Management.SqlParser.Parser; -using Microsoft.SqlServer.Management.SmoMetadataProvider; using Location = Microsoft.SqlTools.ServiceLayer.Workspace.Contracts.Location; namespace Microsoft.SqlTools.ServiceLayer.LanguageServices @@ -35,15 +35,19 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// public sealed class LanguageService { - public const string DefaultBatchSeperator = "GO"; + internal const string DefaultBatchSeperator = "GO"; - private const int DiagnosticParseDelay = 750; + internal const int DiagnosticParseDelay = 750; - private const int FindCompletionsTimeout = 3000; + internal const int FindCompletionsTimeout = 3000; - private const int FindCompletionStartTimeout = 50; + internal const int FindCompletionStartTimeout = 50; - private const int OnConnectionWaitTimeout = 30000; + internal const int OnConnectionWaitTimeout = 300000; + + private object parseMapLock = new object(); + + private ScriptParseInfo currentCompletionParseInfo; private bool ShouldEnableAutocomplete() { @@ -196,6 +200,21 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices await requestContext.SendResult(completionItems); } + /// + /// Handle the resolve completion request event to provide additional + /// autocomplete metadata to the currently select completion item + /// + /// + /// + /// + private static async Task HandleCompletionResolveRequest( + CompletionItem completionItem, + RequestContext requestContext) + { + completionItem = LanguageService.Instance.ResolveCompletionItem(completionItem); + await requestContext.SendResult(completionItem); + } + private static async Task HandleDefinitionRequest( TextDocumentPosition textDocumentPosition, RequestContext requestContext) @@ -212,14 +231,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices await Task.FromResult(true); } - private static async Task HandleCompletionResolveRequest( - CompletionItem completionItem, - RequestContext requestContext) - { - Logger.Write(LogLevel.Verbose, "HandleCompletionResolveRequest"); - await Task.FromResult(true); - } - private static async Task HandleSignatureHelpRequest( TextDocumentPosition textDocumentPosition, RequestContext requestContext) @@ -327,8 +338,10 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// it is the last URI connected to a particular connection, /// then remove the cache. /// - public async Task RemoveAutoCompleteCacheUriReference(ConnectionSummary summary) + public async Task RemoveAutoCompleteCacheUriReference(ConnectionSummary summary, string ownerUri) { + RemoveScriptParseInfo(ownerUri); + // currently this method is disabled, but we need to reimplement now that the // implementation of the 'cache' has changed. await Task.FromResult(0); @@ -342,16 +355,16 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// public ParseResult ParseAndBind(ScriptFile scriptFile, ConnectionInfo connInfo) { - ScriptParseInfo parseInfo = null; - if (this.ScriptParseInfoMap.ContainsKey(scriptFile.ClientFilePath)) - { - parseInfo = this.ScriptParseInfoMap[scriptFile.ClientFilePath]; - } - else - { - parseInfo = new ScriptParseInfo(); - this.ScriptParseInfoMap.Add(scriptFile.ClientFilePath, parseInfo); - } + // get or create the current parse info object + ScriptParseInfo parseInfo = GetScriptParseInfo(scriptFile.ClientFilePath, createIfNotExists: true); + + // parse current SQL file contents to retrieve a list of errors + ParseResult parseResult = Parser.IncrementalParse( + scriptFile.Contents, + parseInfo.ParseResult, + parseInfo.ParseOptions); + + parseInfo.ParseResult = parseResult; if (parseInfo.BuildingMetadataEvent.WaitOne(LanguageService.FindCompletionsTimeout)) { @@ -359,14 +372,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { parseInfo.BuildingMetadataEvent.Reset(); - // parse current SQL file contents to retrieve a list of errors - ParseResult parseResult = Parser.IncrementalParse( - scriptFile.Contents, - parseInfo.ParseResult, - parseInfo.ParseOptions); - - parseInfo.ParseResult = parseResult; - if (connInfo != null && parseInfo.IsConnected) { try @@ -398,7 +403,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } /// - /// Update the cached autocomplete candidate list when the user connects to a database + /// Update the autocomplete metadata provider when the user connects to a database /// /// public async Task UpdateLanguageServiceOnConnection(ConnectionInfo info) @@ -407,43 +412,38 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { if (ShouldEnableAutocomplete()) { - ScriptParseInfo scriptInfo = - this.ScriptParseInfoMap.ContainsKey(info.OwnerUri) - ? this.ScriptParseInfoMap[info.OwnerUri] - : new ScriptParseInfo(); - - try + ScriptParseInfo scriptInfo = GetScriptParseInfo(info.OwnerUri, createIfNotExists: true); + if (scriptInfo.BuildingMetadataEvent.WaitOne(LanguageService.OnConnectionWaitTimeout)) { - scriptInfo.BuildingMetadataEvent.WaitOne(LanguageService.OnConnectionWaitTimeout); - scriptInfo.BuildingMetadataEvent.Reset(); - - var sqlConn = info.SqlConnection as ReliableSqlConnection; - if (sqlConn != null) + try { - ServerConnection serverConn = new ServerConnection(sqlConn.GetUnderlyingConnection()); - scriptInfo.MetadataDisplayInfoProvider = new MetadataDisplayInfoProvider(); - scriptInfo.MetadataProvider = SmoMetadataProvider.CreateConnectedProvider(serverConn); - scriptInfo.Binder = BinderProvider.CreateBinder(scriptInfo.MetadataProvider); - scriptInfo.ServerConnection = new ServerConnection(sqlConn.GetUnderlyingConnection()); - this.ScriptParseInfoMap[info.OwnerUri] = scriptInfo; + scriptInfo.BuildingMetadataEvent.Reset(); + var sqlConn = info.SqlConnection as ReliableSqlConnection; + if (sqlConn != null) + { + ServerConnection serverConn = new ServerConnection(sqlConn.GetUnderlyingConnection()); + scriptInfo.MetadataDisplayInfoProvider = new MetadataDisplayInfoProvider(); + scriptInfo.MetadataProvider = SmoMetadataProvider.CreateConnectedProvider(serverConn); + scriptInfo.Binder = BinderProvider.CreateBinder(scriptInfo.MetadataProvider); + scriptInfo.ServerConnection = new ServerConnection(sqlConn.GetUnderlyingConnection()); + scriptInfo.IsConnected = true; + AddOrUpdateScriptParseInfo(info.OwnerUri, scriptInfo); + } + } + catch (Exception) + { + scriptInfo.IsConnected = false; + } + finally + { + // Set Metadata Build event to Signal state. + // (Tell Language Service that I am ready with Metadata Provider Object) + scriptInfo.BuildingMetadataEvent.Set(); } } - catch (Exception) - { - scriptInfo.IsConnected = false; - } - finally - { - // Set Metadata Build event to Signal state. - // (Tell Language Service that I am ready with Metadata Provider Object) - scriptInfo.BuildingMetadataEvent.Set(); - } - if (scriptInfo.IsConnected) - { - var scriptFile = WorkspaceService.Instance.Workspace.GetFile(info.OwnerUri); - ParseAndBind(scriptFile, info); - } + // populate SMO metadata provider with most common info + AutoCompleteHelper.PrepopulateCommonMetadata(info, scriptInfo); } }); } @@ -466,6 +466,28 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices || !string.Equals(prevSqlText, currentSqlText); } + /// + /// Resolves the details and documentation for a completion item + /// + /// + internal CompletionItem ResolveCompletionItem(CompletionItem completionItem) + { + var scriptParseInfo = LanguageService.Instance.currentCompletionParseInfo; + if (scriptParseInfo != null && scriptParseInfo.CurrentSuggestions != null) + { + foreach (var suggestion in scriptParseInfo.CurrentSuggestions) + { + if (string.Equals(suggestion.Title, completionItem.Label)) + { + completionItem.Detail = suggestion.DatabaseQualifiedName; + completionItem.Documentation = suggestion.Description; + break; + } + } + } + return completionItem; + } + /// /// Return the completion item list for the current text position. /// This method does not await cache builds since it expects to return quickly @@ -479,20 +501,22 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices string filePath = textDocumentPosition.TextDocument.Uri; int startLine = textDocumentPosition.Position.Line; int startColumn = TextUtilities.PositionOfPrevDelimeter( - scriptFile.Contents, + scriptFile.Contents, textDocumentPosition.Position.Line, textDocumentPosition.Position.Character); int endColumn = textDocumentPosition.Position.Character; + this.currentCompletionParseInfo = null; + // Take a reference to the list at a point in time in case we update and replace the list - if (connInfo == null - || !LanguageService.Instance.ScriptParseInfoMap.ContainsKey(textDocumentPosition.TextDocument.Uri)) + + ScriptParseInfo scriptParseInfo = GetScriptParseInfo(textDocumentPosition.TextDocument.Uri); + if (connInfo == null || scriptParseInfo == null) { return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn); } // reparse and bind the SQL statement if needed - var scriptParseInfo = ScriptParseInfoMap[textDocumentPosition.TextDocument.Uri]; if (RequiresReparse(scriptParseInfo, scriptFile)) { ParseAndBind(scriptFile, connInfo); @@ -511,15 +535,18 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices try { // get the completion list from SQL Parser - var suggestions = Resolver.FindCompletions( + scriptParseInfo.CurrentSuggestions = Resolver.FindCompletions( scriptParseInfo.ParseResult, textDocumentPosition.Position.Line + 1, textDocumentPosition.Position.Character + 1, scriptParseInfo.MetadataDisplayInfoProvider); + // cache the current script parse info object to resolve completions later + this.currentCompletionParseInfo = scriptParseInfo; + // convert the suggestion list to the VS Code format return AutoCompleteHelper.ConvertDeclarationsToCompletionItems( - suggestions, + scriptParseInfo.CurrentSuggestions, startLine, startColumn, endColumn); @@ -685,5 +712,60 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } #endregion + + private void AddOrUpdateScriptParseInfo(string uri, ScriptParseInfo scriptInfo) + { + lock (this.parseMapLock) + { + if (this.ScriptParseInfoMap.ContainsKey(uri)) + { + this.ScriptParseInfoMap[uri] = scriptInfo; + } + else + { + this.ScriptParseInfoMap.Add(uri, scriptInfo); + } + + } + } + + private ScriptParseInfo GetScriptParseInfo(string uri, bool createIfNotExists = false) + { + lock (this.parseMapLock) + { + if (this.ScriptParseInfoMap.ContainsKey(uri)) + { + return this.ScriptParseInfoMap[uri]; + } + else if (createIfNotExists) + { + ScriptParseInfo scriptInfo = new ScriptParseInfo(); + this.ScriptParseInfoMap.Add(uri, scriptInfo); + return scriptInfo; + } + else + { + return null; + } + } + } + + private bool RemoveScriptParseInfo(string uri) + { + lock (this.parseMapLock) + { + if (this.ScriptParseInfoMap.ContainsKey(uri)) + { + var scriptInfo = this.ScriptParseInfoMap[uri]; + scriptInfo.ServerConnection.Disconnect(); + scriptInfo.ServerConnection = null; + return this.ScriptParseInfoMap.Remove(uri); + } + else + { + return false; + } + } + } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs index 48fb2cce..5c552531 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs @@ -3,14 +3,16 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System; +using System.Collections.Generic; +using System.Threading; using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.SmoMetadataProvider; using Microsoft.SqlServer.Management.SqlParser.Binder; using Microsoft.SqlServer.Management.SqlParser.Common; +using Microsoft.SqlServer.Management.SqlParser.Intellisense; using Microsoft.SqlServer.Management.SqlParser.MetadataProvider; using Microsoft.SqlServer.Management.SqlParser.Parser; -using System; -using System.Threading; namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { @@ -33,7 +35,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices get { return this.buildingMetadataEvent; } } - /// /// Gets or sets a flag determining is the LanguageService is connected /// @@ -56,7 +57,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices isQuotedIdentifierSet: true, compatibilityLevel: DatabaseCompatibilityLevel, transactSqlVersion: TransactSqlVersion); - this.IsConnected = true; } } @@ -143,6 +143,11 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// public MetadataDisplayInfoProvider MetadataDisplayInfoProvider { get; set; } + /// + /// Gets or sets the current autocomplete suggestion list + /// + public IEnumerable CurrentSuggestions { get; set; } + /// /// Gets the database compatibility level from a server version /// @@ -193,6 +198,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices default: return TransactSqlVersion.Current; } - } + } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Utility/Logger.cs b/src/Microsoft.SqlTools.ServiceLayer/Utility/Logger.cs index f9d966e8..c8552d15 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Utility/Logger.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Utility/Logger.cs @@ -131,6 +131,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Utility internal class LogWriter : IDisposable { + private object logLock = new object(); + private TextWriter textWriter; private LogLevel minimumLogLevel = LogLevel.Verbose; @@ -170,24 +172,28 @@ namespace Microsoft.SqlTools.ServiceLayer.Utility if (this.textWriter != null && logLevel >= this.minimumLogLevel) { - // Print the timestamp and log level - this.textWriter.WriteLine( - "{0} [{1}] - Method \"{2}\" at line {3} of {4}\r\n", - DateTime.Now, - logLevel.ToString().ToUpper(), - callerName, - callerLineNumber, - callerSourceFile); - - // Print out indented message lines - foreach (var messageLine in logMessage.Split('\n')) + // System.IO is not thread safe + lock (logLock) { - this.textWriter.WriteLine(" " + messageLine.TrimEnd()); - } + // Print the timestamp and log level + this.textWriter.WriteLine( + "{0} [{1}] - Method \"{2}\" at line {3} of {4}\r\n", + DateTime.Now, + logLevel.ToString().ToUpper(), + callerName, + callerLineNumber, + callerSourceFile); - // Finish with a newline and flush the writer - this.textWriter.WriteLine(); - this.textWriter.Flush(); + // Print out indented message lines + foreach (var messageLine in logMessage.Split('\n')) + { + this.textWriter.WriteLine(" " + messageLine.TrimEnd()); + } + + // Finish with a newline and flush the writer + this.textWriter.WriteLine(); + this.textWriter.Flush(); + } } } From 44914aa8b9e70c0c4299f81573f8216de8424220 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Sun, 18 Sep 2016 23:20:43 -0700 Subject: [PATCH 2/6] Move ParseBind out of locked block --- .../LanguageServices/LanguageService.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs index a87ab5b7..fd1afba4 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs @@ -358,20 +358,20 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices // get or create the current parse info object ScriptParseInfo parseInfo = GetScriptParseInfo(scriptFile.ClientFilePath, createIfNotExists: true); - // parse current SQL file contents to retrieve a list of errors - ParseResult parseResult = Parser.IncrementalParse( - scriptFile.Contents, - parseInfo.ParseResult, - parseInfo.ParseOptions); - - parseInfo.ParseResult = parseResult; - if (parseInfo.BuildingMetadataEvent.WaitOne(LanguageService.FindCompletionsTimeout)) { try { parseInfo.BuildingMetadataEvent.Reset(); + // parse current SQL file contents to retrieve a list of errors + ParseResult parseResult = Parser.IncrementalParse( + scriptFile.Contents, + parseInfo.ParseResult, + parseInfo.ParseOptions); + + parseInfo.ParseResult = parseResult; + if (connInfo != null && parseInfo.IsConnected) { try @@ -427,7 +427,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices scriptInfo.Binder = BinderProvider.CreateBinder(scriptInfo.MetadataProvider); scriptInfo.ServerConnection = new ServerConnection(sqlConn.GetUnderlyingConnection()); scriptInfo.IsConnected = true; - AddOrUpdateScriptParseInfo(info.OwnerUri, scriptInfo); + //AddOrUpdateScriptParseInfo(info.OwnerUri, scriptInfo); } } catch (Exception) From 03315475cb931e238272fc28e73b73ac7e2d5a89 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Mon, 19 Sep 2016 12:32:56 -0700 Subject: [PATCH 3/6] Misc. cleanup for PR --- .../LanguageServices/AutoCompleteHelper.cs | 1 - .../LanguageServices/LanguageService.cs | 1 - .../LanguageServices/ScriptParseInfo.cs | 2 +- src/Microsoft.SqlTools.ServiceLayer/Utility/Logger.cs | 3 +-- 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs index 85e9b10b..ffc9811c 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs @@ -492,7 +492,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices Label = autoCompleteItem.Title, Kind = CompletionItemKind.Variable, Detail = autoCompleteItem.Title, - // Documentation = autoCompleteItem.Description, TextEdit = new TextEdit { NewText = autoCompleteItem.Title, diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs index fd1afba4..3d412b56 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs @@ -427,7 +427,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices scriptInfo.Binder = BinderProvider.CreateBinder(scriptInfo.MetadataProvider); scriptInfo.ServerConnection = new ServerConnection(sqlConn.GetUnderlyingConnection()); scriptInfo.IsConnected = true; - //AddOrUpdateScriptParseInfo(info.OwnerUri, scriptInfo); } } catch (Exception) diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs index 5c552531..7dca96ab 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs @@ -198,6 +198,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices default: return TransactSqlVersion.Current; } - } + } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Utility/Logger.cs b/src/Microsoft.SqlTools.ServiceLayer/Utility/Logger.cs index c8552d15..5c4adae4 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Utility/Logger.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Utility/Logger.cs @@ -132,7 +132,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Utility internal class LogWriter : IDisposable { private object logLock = new object(); - private TextWriter textWriter; private LogLevel minimumLogLevel = LogLevel.Verbose; @@ -173,7 +172,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Utility logLevel >= this.minimumLogLevel) { // System.IO is not thread safe - lock (logLock) + lock (this.logLock) { // Print the timestamp and log level this.textWriter.WriteLine( From 30a6fdd26c0d142ef52c9812874d0a98f6147e94 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Mon, 19 Sep 2016 12:35:59 -0700 Subject: [PATCH 4/6] Fix Connection tests break --- .../Connection/ConnectionServiceTests.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/Connection/ConnectionServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/Connection/ConnectionServiceTests.cs index 07c202cb..82b5aca3 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/Connection/ConnectionServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/Connection/ConnectionServiceTests.cs @@ -138,8 +138,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Connection // register disconnect callback connectionService.RegisterOnDisconnectTask( - (result) => { + (result, uri) => { callbackInvoked = true; + Assert.True(uri.Equals(ownerUri)); return Task.FromResult(true); } ); @@ -433,8 +434,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Connection // register disconnect callback connectionService.RegisterOnDisconnectTask( - (result) => { + (result, uri) => { callbackInvoked = true; + Assert.True(uri.Equals(ownerUri)); return Task.FromResult(true); } ); From 0bd7edecf1daf8ba14453411c4d636f7bcfb63aa Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Mon, 19 Sep 2016 14:00:10 -0700 Subject: [PATCH 5/6] Check if script is output window before parsing. --- .../LanguageServices/LanguageService.cs | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs index 3d412b56..8c6f5b49 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs @@ -269,9 +269,12 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices ScriptFile scriptFile, EventContext eventContext) { - await this.RunScriptDiagnostics( - new ScriptFile[] { scriptFile }, - eventContext); + if (!IsPreviewWindow(scriptFile)) + { + await RunScriptDiagnostics( + new ScriptFile[] { scriptFile }, + eventContext); + } await Task.FromResult(true); } @@ -766,5 +769,21 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } } } + + /// + /// Returns a flag indicating if the ScriptFile refers to the output window. + /// + /// + private bool IsPreviewWindow(ScriptFile scriptFile) + { + if (scriptFile != null && !string.IsNullOrWhiteSpace(scriptFile.ClientFilePath)) + { + return scriptFile.ClientFilePath.StartsWith("tsqloutput:"); + } + else + { + return false; + } + } } } From 701aba8d46e3af0d72ffa53f2c7c31509e9bfcf4 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Mon, 19 Sep 2016 18:21:48 -0700 Subject: [PATCH 6/6] Add some tests to increase code coverage metrics --- .../LanguageServices/LanguageService.cs | 13 +- .../LanguageServer/LanguageServiceTests.cs | 148 ++++++++++++++++++ 2 files changed, 152 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs index 8c6f5b49..cc834adc 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs @@ -49,7 +49,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices private ScriptParseInfo currentCompletionParseInfo; - private bool ShouldEnableAutocomplete() + internal bool ShouldEnableAutocomplete() { return true; } @@ -109,12 +109,12 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices private static CancellationTokenSource ExistingRequestCancellation { get; set; } - private SqlToolsSettings CurrentSettings + internal SqlToolsSettings CurrentSettings { get { return WorkspaceService.Instance.CurrentSettings; } } - private Workspace.Workspace CurrentWorkspace + internal Workspace.Workspace CurrentWorkspace { get { return WorkspaceService.Instance.Workspace; } } @@ -123,7 +123,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// Gets or sets the current SQL Tools context /// /// - private SqlToolsContext Context { get; set; } + internal SqlToolsContext Context { get; set; } #endregion @@ -219,7 +219,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices TextDocumentPosition textDocumentPosition, RequestContext requestContext) { - Logger.Write(LogLevel.Verbose, "HandleDefinitionRequest"); await Task.FromResult(true); } @@ -227,7 +226,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices ReferencesParams referencesParams, RequestContext requestContext) { - Logger.Write(LogLevel.Verbose, "HandleReferencesRequest"); await Task.FromResult(true); } @@ -235,7 +233,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices TextDocumentPosition textDocumentPosition, RequestContext requestContext) { - Logger.Write(LogLevel.Verbose, "HandleSignatureHelpRequest"); await Task.FromResult(true); } @@ -243,7 +240,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices TextDocumentPosition textDocumentPosition, RequestContext requestContext) { - Logger.Write(LogLevel.Verbose, "HandleDocumentHighlightRequest"); await Task.FromResult(true); } @@ -251,7 +247,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices TextDocumentPosition textDocumentPosition, RequestContext requestContext) { - Logger.Write(LogLevel.Verbose, "HandleHoverRequest"); await Task.FromResult(true); } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs index bb07c56a..34f7e405 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs @@ -21,9 +21,13 @@ using Microsoft.SqlServer.Management.SqlParser.MetadataProvider; using Microsoft.SqlServer.Management.SqlParser.Parser; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; +using Microsoft.SqlTools.ServiceLayer.Credentials; using Microsoft.SqlTools.ServiceLayer.LanguageServices; +using Microsoft.SqlTools.ServiceLayer.QueryExecution; +using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.ServiceLayer.Test.QueryExecution; using Microsoft.SqlTools.ServiceLayer.Test.Utility; +using Microsoft.SqlTools.ServiceLayer.Workspace; using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; using Microsoft.SqlTools.Test.Utility; using Moq; @@ -148,6 +152,150 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices #endregion + #region "General Language Service tests" + + /// + /// Check that autocomplete is enabled by default + /// + [Fact] + public void CheckAutocompleteEnabledByDefault() + { + // get test service + LanguageService service = TestObjects.GetTestLanguageService(); + Assert.True(service.ShouldEnableAutocomplete()); + } + + /// + /// Test the service initialization code path and verify nothing throws + /// + [Fact] + public void ServiceInitiailzation() + { + InitializeTestServices(); + + Assert.True(LanguageService.Instance.Context != null); + Assert.True(LanguageService.Instance.ConnectionServiceInstance != null); + Assert.True(LanguageService.Instance.CurrentSettings != null); + Assert.True(LanguageService.Instance.CurrentWorkspace != null); + + LanguageService.Instance.ConnectionServiceInstance = null; + Assert.True(LanguageService.Instance.ConnectionServiceInstance == null); + } + + /// + /// Test the service initialization code path and verify nothing throws + /// + [Fact] + public void UpdateLanguageServiceOnConnection() + { + string ownerUri = "file://my/sample/file.sql"; + var connectionService = TestObjects.GetTestConnectionService(); + var connectionResult = + connectionService + .Connect(new ConnectParams() + { + OwnerUri = ownerUri, + Connection = TestObjects.GetTestConnectionDetails() + }); + + ConnectionInfo connInfo = null; + connectionService.TryFindConnection(ownerUri, out connInfo); + + var task = LanguageService.Instance.UpdateLanguageServiceOnConnection(connInfo); + task.Wait(); + } + + /// + /// Test the service initialization code path and verify nothing throws + /// + [Fact] + public void PrepopulateCommonMetadata() + { + InitializeTestServices(); + + string sqlFilePath = GetTestSqlFile(); + ScriptFile scriptFile = WorkspaceService.Instance.Workspace.GetFile(sqlFilePath); + + string ownerUri = scriptFile.ClientFilePath; + var connectionService = TestObjects.GetTestConnectionService(); + var connectionResult = + connectionService + .Connect(new ConnectParams() + { + OwnerUri = ownerUri, + Connection = TestObjects.GetTestConnectionDetails() + }); + + ConnectionInfo connInfo = null; + connectionService.TryFindConnection(ownerUri, out connInfo); + + ScriptParseInfo scriptInfo = new ScriptParseInfo(); + scriptInfo.IsConnected = true; + + AutoCompleteHelper.PrepopulateCommonMetadata(connInfo, scriptInfo); + } + + private string GetTestSqlFile() + { + string filePath = Path.Combine( + Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), + "sqltest.sql"); + + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + + File.WriteAllText(filePath, "SELECT * FROM sys.objects\n"); + + return filePath; + } + + private void InitializeTestServices() + { + const string hostName = "SQL Tools Service Host"; + const string hostProfileId = "SQLToolsService"; + Version hostVersion = new Version(1,0); + + // set up the host details and profile paths + var hostDetails = new HostDetails(hostName, hostProfileId, hostVersion); + var profilePaths = new ProfilePaths(hostProfileId, "baseAllUsersPath", "baseCurrentUserPath"); + SqlToolsContext sqlToolsContext = new SqlToolsContext(hostDetails, profilePaths); + + // Grab the instance of the service host + Hosting.ServiceHost serviceHost = Hosting.ServiceHost.Instance; + + // Start the service + serviceHost.Start().Wait(); + + // Initialize the services that will be hosted here + WorkspaceService.Instance.InitializeService(serviceHost); + LanguageService.Instance.InitializeService(serviceHost, sqlToolsContext); + ConnectionService.Instance.InitializeService(serviceHost); + CredentialService.Instance.InitializeService(serviceHost); + QueryExecutionService.Instance.InitializeService(serviceHost); + + serviceHost.Initialize(); + } + + private Hosting.ServiceHost GetTestServiceHost() + { + // set up the host details and profile paths + var hostDetails = new HostDetails("Test Service Host", "SQLToolsService", new Version(1,0)); + var profilePaths = new ProfilePaths("SQLToolsService", "baseAllUsersPath", "baseCurrentUserPath"); + SqlToolsContext context = new SqlToolsContext(hostDetails, profilePaths); + + // Grab the instance of the service host + Hosting.ServiceHost host = Hosting.ServiceHost.Instance; + + // Start the service + host.Start().Wait(); + + return host; + } + + #endregion + #region "Autocomplete Tests" // This test currently requires a live database connection to initialize