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(); + } } }