diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionInfo.cs index 31d0026d..506b043e 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionInfo.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionInfo.cs @@ -23,6 +23,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection OwnerUri = ownerUri; ConnectionDetails = details; ConnectionId = Guid.NewGuid(); + IntellisenseMetrics = new InteractionMetrics(new int[] { 50, 100, 200, 500, 1000, 2000 }); } /// @@ -49,5 +50,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection /// The connection to the SQL database that commands will be run against. /// public DbConnection SqlConnection { get; set; } + + /// + /// Intellisense Metrics + /// + public InteractionMetrics IntellisenseMetrics { get; private set; } + + /// + /// Returns true is the db connection is to a SQL db + /// + public bool IsAzure { get; set; } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs index f9ae8543..afe86bd0 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs @@ -15,6 +15,7 @@ using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection; 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; @@ -290,6 +291,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection AzureVersion = serverInfo.AzureVersion, OsVersion = serverInfo.OsVersion }; + connectionInfo.IsAzure = serverInfo.IsCloud; } catch(Exception ex) { @@ -356,6 +358,21 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection return false; } + // Send a telemetry notification for intellisense performance metrics + ServiceHost.SendEvent(TelemetryNotification.Type, new TelemetryParams() + { + Params = new TelemetryProperties + { + Properties = new Dictionary + { + { "IsAzure", info.IsAzure ? "1" : "0" } + }, + EventName = TelemetryEventNames.IntellisenseQuantile, + Measures = info.IntellisenseMetrics.Quantile + } + }); + + // Close the connection info.SqlConnection.Close(); diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs index a1568d7a..df5ea3c9 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs @@ -5,14 +5,13 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; -using System.Text.RegularExpressions; using System.Threading; 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.Completion; using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.ServiceLayer.Utility; @@ -371,12 +370,13 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// internal static CompletionItem[] GetDefaultCompletionItems( - int row, - int startColumn, - int endColumn, - bool useLowerCase, - string tokenText = null) + ScriptDocumentInfo scriptDocumentInfo, + bool useLowerCase) { + int row = scriptDocumentInfo.StartLine; + int startColumn = scriptDocumentInfo.StartColumn; + int endColumn = scriptDocumentInfo.EndColumn; + string tokenText = scriptDocumentInfo.TokenText; // determine how many default completion items there will be int listSize = DefaultCompletionText.Length; if (!string.IsNullOrWhiteSpace(tokenText)) diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Completion/AutoCompletionResult.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Completion/AutoCompletionResult.cs new file mode 100644 index 00000000..b0678781 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Completion/AutoCompletionResult.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.SqlServer.Management.SqlParser.Intellisense; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion +{ + /// + /// Includes the objects created by auto completion service + /// + public class AutoCompletionResult + { + /// + /// Creates new instance + /// + public AutoCompletionResult() + { + Stopwatch = new Stopwatch(); + Stopwatch.Start(); + } + + private Stopwatch Stopwatch { get; set; } + + /// + /// Completes the results to calculate the duration + /// + public void CompleteResult(CompletionItem[] completionItems) + { + Stopwatch.Stop(); + CompletionItems = completionItems; + } + + /// + /// The number of milliseconds to process the result + /// + public double Duration + { + get + { + return Stopwatch.ElapsedMilliseconds; + } + } + + /// + /// Completion list + /// + public CompletionItem[] CompletionItems { get; private set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Completion/CompletionService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Completion/CompletionService.cs new file mode 100644 index 00000000..cf7eb6c3 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Completion/CompletionService.cs @@ -0,0 +1,166 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Threading; +using Microsoft.SqlServer.Management.SqlParser.Intellisense; +using Microsoft.SqlServer.Management.SqlParser.MetadataProvider; +using Microsoft.SqlServer.Management.SqlParser.Parser; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion +{ + /// + /// A service to create auto complete list for given script document + /// + internal class CompletionService + { + private ConnectedBindingQueue BindingQueue { get; set; } + + /// + /// Created new instance given binding queue + /// + public CompletionService(ConnectedBindingQueue bindingQueue) + { + BindingQueue = bindingQueue; + } + + private ISqlParserWrapper sqlParserWrapper; + + /// + /// SQL parser wrapper to create the completion list + /// + public ISqlParserWrapper SqlParserWrapper + { + get + { + if(this.sqlParserWrapper == null) + { + this.sqlParserWrapper = new SqlParserWrapper(); + } + return this.sqlParserWrapper; + } + set + { + this.sqlParserWrapper = value; + } + } + + /// + /// Creates a completion list given connection and document info + /// + public AutoCompletionResult CreateCompletions( + ConnectionInfo connInfo, + ScriptDocumentInfo scriptDocumentInfo, + bool useLowerCaseSuggestions) + { + AutoCompletionResult result = new AutoCompletionResult(); + // check if the file is connected and the file lock is available + if (scriptDocumentInfo.ScriptParseInfo.IsConnected && Monitor.TryEnter(scriptDocumentInfo.ScriptParseInfo.BuildingMetadataLock)) + { + try + { + QueueItem queueItem = AddToQueue(connInfo, scriptDocumentInfo.ScriptParseInfo, scriptDocumentInfo, useLowerCaseSuggestions); + + // wait for the queue item + queueItem.ItemProcessed.WaitOne(); + var completionResult = queueItem.GetResultAsT(); + if (completionResult != null && completionResult.CompletionItems != null && completionResult.CompletionItems.Length > 0) + { + result = completionResult; + } + else if (!ShouldShowCompletionList(scriptDocumentInfo.Token)) + { + result.CompleteResult(AutoCompleteHelper.EmptyCompletionList); + } + } + finally + { + Monitor.Exit(scriptDocumentInfo.ScriptParseInfo.BuildingMetadataLock); + } + } + + return result; + } + + private QueueItem AddToQueue( + ConnectionInfo connInfo, + ScriptParseInfo scriptParseInfo, + ScriptDocumentInfo scriptDocumentInfo, + bool useLowerCaseSuggestions) + { + // queue the completion task with the binding queue + QueueItem queueItem = this.BindingQueue.QueueBindingOperation( + key: scriptParseInfo.ConnectionKey, + bindingTimeout: LanguageService.BindingTimeout, + bindOperation: (bindingContext, cancelToken) => + { + return CreateCompletionsFromSqlParser(connInfo, scriptParseInfo, scriptDocumentInfo, bindingContext.MetadataDisplayInfoProvider); + }, + timeoutOperation: (bindingContext) => + { + // return the default list if the connected bind fails + return CreateDefaultCompletionItems(scriptParseInfo, scriptDocumentInfo, useLowerCaseSuggestions); + }); + return queueItem; + } + + private static bool ShouldShowCompletionList(Token token) + { + bool result = true; + if (token != null) + { + switch (token.Id) + { + case (int)Tokens.LEX_MULTILINE_COMMENT: + case (int)Tokens.LEX_END_OF_LINE_COMMENT: + result = false; + break; + } + } + return result; + } + + private AutoCompletionResult CreateDefaultCompletionItems(ScriptParseInfo scriptParseInfo, ScriptDocumentInfo scriptDocumentInfo, bool useLowerCaseSuggestions) + { + AutoCompletionResult result = new AutoCompletionResult(); + CompletionItem[] completionList = AutoCompleteHelper.GetDefaultCompletionItems(scriptDocumentInfo, useLowerCaseSuggestions); + result.CompleteResult(completionList); + return result; + } + + private AutoCompletionResult CreateCompletionsFromSqlParser( + ConnectionInfo connInfo, + ScriptParseInfo scriptParseInfo, + ScriptDocumentInfo scriptDocumentInfo, + MetadataDisplayInfoProvider metadataDisplayInfoProvider) + { + AutoCompletionResult result = new AutoCompletionResult(); + IEnumerable suggestions = SqlParserWrapper.FindCompletions( + scriptParseInfo.ParseResult, + scriptDocumentInfo.ParserLine, + scriptDocumentInfo.ParserColumn, + metadataDisplayInfoProvider); + + // get the completion list from SQL Parser + scriptParseInfo.CurrentSuggestions = suggestions; + + // convert the suggestion list to the VS Code format + CompletionItem[] completionList = AutoCompleteHelper.ConvertDeclarationsToCompletionItems( + scriptParseInfo.CurrentSuggestions, + scriptDocumentInfo.StartLine, + scriptDocumentInfo.StartColumn, + scriptDocumentInfo.EndColumn, + scriptDocumentInfo.TokenText); + + result.CompleteResult(completionList); + + //The bucket for number of milliseconds will take to send back auto complete list + connInfo.IntellisenseMetrics.UpdateMetrics(result.Duration, 1, (k2, v2) => v2 + 1); + return result; + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/SqlCompletionItem.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Completion/SqlCompletionItem.cs similarity index 98% rename from src/Microsoft.SqlTools.ServiceLayer/LanguageServices/SqlCompletionItem.cs rename to src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Completion/SqlCompletionItem.cs index 8aade3ec..39247f34 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/SqlCompletionItem.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Completion/SqlCompletionItem.cs @@ -3,7 +3,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using System; using System.Globalization; using System.Text.RegularExpressions; using Microsoft.SqlServer.Management.SqlParser.Intellisense; @@ -11,7 +10,7 @@ using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; using Microsoft.SqlTools.ServiceLayer.Utility; using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; -namespace Microsoft.SqlTools.ServiceLayer.LanguageServices +namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion { /// /// Creates a completion item from SQL parser declaration item diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Completion/SqlParserWrapper.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Completion/SqlParserWrapper.cs new file mode 100644 index 00000000..7e949aa9 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Completion/SqlParserWrapper.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using Microsoft.SqlServer.Management.SqlParser.Intellisense; +using Microsoft.SqlServer.Management.SqlParser.MetadataProvider; +using Microsoft.SqlServer.Management.SqlParser.Parser; + +namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion +{ + /// + /// SqlParserWrapper interface + /// + public interface ISqlParserWrapper + { + IEnumerable FindCompletions(ParseResult parseResult, int line, int col, IMetadataDisplayInfoProvider displayInfoProvider); + } + + /// + /// A wrapper class around SQL parser methods to make the operations testable + /// + public class SqlParserWrapper : ISqlParserWrapper + { + /// + /// Creates completion list given SQL script info + /// + public IEnumerable FindCompletions(ParseResult parseResult, int line, int col, IMetadataDisplayInfoProvider displayInfoProvider) + { + return Resolver.FindCompletions(parseResult, line, col, displayInfoProvider); + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Contracts/TelemetryNotification.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Contracts/TelemetryNotification.cs new file mode 100644 index 00000000..5fbda525 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Contracts/TelemetryNotification.cs @@ -0,0 +1,59 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts +{ + public class TelemetryProperties + { + public string EventName { get; set; } + + /// + /// Telemetry properties + /// + public Dictionary Properties { get; set; } + + /// + /// Telemetry measures + /// + public Dictionary Measures { get; set; } + } + + /// + /// Parameters sent back with an IntelliSense ready event + /// + public class TelemetryParams + { + public TelemetryProperties Params { get; set; } + } + + /// + /// Event sent when the language service needs to add a telemetry event + /// + public class TelemetryNotification + { + public static readonly + EventType Type = + EventType.Create("telemetry/event"); + } + + /// + /// List of telemetry events + /// + public static class TelemetryEventNames + { + /// + /// telemetry event name for auto complete response time + /// + public const string IntellisenseQuantile = "IntellisenseQuantile"; + + /// + /// telemetry even name for when definition is requested + /// + public const string PeekDefinitionRequested = "PeekDefinitionRequested"; + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/InteractionMetrics.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/InteractionMetrics.cs new file mode 100644 index 00000000..db04157f --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/InteractionMetrics.cs @@ -0,0 +1,98 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer +{ + /// + /// A class to calculate the value for the metrics using the given bucket + /// + public class InteractionMetrics + { + /// + /// Creates new instance given a bucket of metrics + /// + public InteractionMetrics(int[] metrics) + { + Validate.IsNotNull("metrics", metrics); + if(metrics.Length == 0) + { + throw new ArgumentOutOfRangeException("metrics"); + } + + Counters = new ConcurrentDictionary(); + if (!IsSorted(metrics)) + { + Array.Sort(metrics); + } + Metrics = metrics; + } + + private ConcurrentDictionary Counters { get; } + + private object perfCountersLock = new object(); + + /// + /// The metrics bucket + /// + public int[] Metrics { get; private set; } + + /// + /// Returns true if the given list is sorted + /// + private bool IsSorted(int[] metrics) + { + if (metrics.Length > 1) + { + int previous = metrics[0]; + for (int i = 1; i < metrics.Length; i++) + { + if(metrics[i] < previous) + { + return false; + } + previous = metrics[i]; + } + } + return true; + } + + /// + /// Update metric value given new number + /// + public void UpdateMetrics(double duration, T newValue, Func updateValueFactory) + { + int metric = Metrics[Metrics.Length - 1]; + for (int i = 0; i < Metrics.Length; i++) + { + if (duration <= Metrics[i]) + { + metric = Metrics[i]; + break; + } + } + string key = metric.ToString(); + Counters.AddOrUpdate(key, newValue, updateValueFactory); + } + + /// + /// Returns the quantile + /// + public Dictionary Quantile + { + get + { + return Counters.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + } + } + } +} + diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs index 32d812f6..cd655179 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs @@ -18,6 +18,7 @@ using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; using Microsoft.SqlTools.ServiceLayer.Hosting; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion; using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.ServiceLayer.Utility; @@ -310,6 +311,15 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices if (locations != null) { await requestContext.SendResult(locations); + + // Send a notification to signal that definition is sent + await ServiceHost.Instance.SendEvent(TelemetryNotification.Type, new TelemetryParams() + { + Params = new TelemetryProperties + { + EventName = TelemetryEventNames.PeekDefinitionRequested + } + }); } } } @@ -690,7 +700,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } // Get token from selected text - Token selectedToken = GetToken(scriptParseInfo, textDocumentPosition.Position.Line + 1, textDocumentPosition.Position.Character); + Token selectedToken = ScriptDocumentInfo.GetToken(scriptParseInfo, textDocumentPosition.Position.Line + 1, textDocumentPosition.Position.Character); if (selectedToken == null) { return null; @@ -871,12 +881,19 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices startLine + 1, endColumn + 1, bindingContext.MetadataDisplayInfoProvider); - - // convert from the parser format to the VS Code wire format - return AutoCompleteHelper.ConvertMethodHelpTextListToSignatureHelp(methods, - methodLocations, - startLine + 1, - endColumn + 1); + + if (methodLocations != null) + { + // convert from the parser format to the VS Code wire format + return AutoCompleteHelper.ConvertMethodHelpTextListToSignatureHelp(methods, + methodLocations, + startLine + 1, + endColumn + 1); + } + else + { + return null; + } }); queueItem.ItemProcessed.WaitOne(); @@ -900,168 +917,49 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// public CompletionItem[] GetCompletionItems( TextDocumentPosition textDocumentPosition, - ScriptFile scriptFile, + ScriptFile scriptFile, ConnectionInfo connInfo) { // initialize some state to parse and bind the current script file this.currentCompletionParseInfo = null; CompletionItem[] resultCompletionItems = null; - string filePath = textDocumentPosition.TextDocument.Uri; - int startLine = textDocumentPosition.Position.Line; - int parserLine = textDocumentPosition.Position.Line + 1; - int startColumn = TextUtilities.PositionOfPrevDelimeter( - scriptFile.Contents, - textDocumentPosition.Position.Line, - textDocumentPosition.Position.Character); - int endColumn = TextUtilities.PositionOfNextDelimeter( - scriptFile.Contents, - textDocumentPosition.Position.Line, - textDocumentPosition.Position.Character); - int parserColumn = textDocumentPosition.Position.Character + 1; + CompletionService completionService = new CompletionService(BindingQueue); bool useLowerCaseSuggestions = this.CurrentSettings.SqlTools.IntelliSense.LowerCaseSuggestions.Value; // get the current script parse info object ScriptParseInfo scriptParseInfo = GetScriptParseInfo(textDocumentPosition.TextDocument.Uri); + ScriptDocumentInfo scriptDocumentInfo = new ScriptDocumentInfo(textDocumentPosition, scriptFile, scriptParseInfo); + if (scriptParseInfo == null) { - return AutoCompleteHelper.GetDefaultCompletionItems( - startLine, - startColumn, - endColumn, - useLowerCaseSuggestions); + return AutoCompleteHelper.GetDefaultCompletionItems(scriptDocumentInfo, useLowerCaseSuggestions); } // reparse and bind the SQL statement if needed if (RequiresReparse(scriptParseInfo, scriptFile)) - { + { ParseAndBind(scriptFile, connInfo); } // if the parse failed then return the default list if (scriptParseInfo.ParseResult == null) { - return AutoCompleteHelper.GetDefaultCompletionItems( - startLine, - startColumn, - endColumn, - useLowerCaseSuggestions); + return AutoCompleteHelper.GetDefaultCompletionItems(scriptDocumentInfo, useLowerCaseSuggestions); } - - // need to adjust line & column for base-1 parser indices - Token token = GetToken(scriptParseInfo, parserLine, parserColumn); - string tokenText = token != null ? token.Text : null; + AutoCompletionResult result = completionService.CreateCompletions(connInfo, scriptDocumentInfo, useLowerCaseSuggestions); + // cache the current script parse info object to resolve completions later + this.currentCompletionParseInfo = scriptParseInfo; + resultCompletionItems = result.CompletionItems; - // check if the file is connected and the file lock is available - if (scriptParseInfo.IsConnected && Monitor.TryEnter(scriptParseInfo.BuildingMetadataLock)) - { - try - { - // queue the completion task with the binding queue - QueueItem queueItem = this.BindingQueue.QueueBindingOperation( - key: scriptParseInfo.ConnectionKey, - bindingTimeout: LanguageService.BindingTimeout, - bindOperation: (bindingContext, cancelToken) => - { - // get the completion list from SQL Parser - scriptParseInfo.CurrentSuggestions = Resolver.FindCompletions( - scriptParseInfo.ParseResult, - parserLine, - parserColumn, - bindingContext.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( - scriptParseInfo.CurrentSuggestions, - startLine, - startColumn, - endColumn, - tokenText); - }, - timeoutOperation: (bindingContext) => - { - // return the default list if the connected bind fails - return AutoCompleteHelper.GetDefaultCompletionItems( - startLine, - startColumn, - endColumn, - useLowerCaseSuggestions, - tokenText); - }); - - // wait for the queue item - queueItem.ItemProcessed.WaitOne(); - - var completionItems = queueItem.GetResultAsT(); - if (completionItems != null && completionItems.Length > 0) - { - resultCompletionItems = completionItems; - } - else if (!ShouldShowCompletionList(token)) - { - resultCompletionItems = AutoCompleteHelper.EmptyCompletionList; - } - } - finally - { - Monitor.Exit(scriptParseInfo.BuildingMetadataLock); - } - } - // if there are no completions then provide the default list if (resultCompletionItems == null) { - resultCompletionItems = AutoCompleteHelper.GetDefaultCompletionItems( - startLine, - startColumn, - endColumn, - useLowerCaseSuggestions, - tokenText); + resultCompletionItems = AutoCompleteHelper.GetDefaultCompletionItems(scriptDocumentInfo, useLowerCaseSuggestions); } return resultCompletionItems; } - private static Token GetToken(ScriptParseInfo scriptParseInfo, int startLine, int startColumn) - { - if (scriptParseInfo != null && scriptParseInfo.ParseResult != null && scriptParseInfo.ParseResult.Script != null && scriptParseInfo.ParseResult.Script.Tokens != null) - { - var tokenIndex = scriptParseInfo.ParseResult.Script.TokenManager.FindToken(startLine, startColumn); - if (tokenIndex >= 0) - { - // return the current token - int currentIndex = 0; - foreach (var token in scriptParseInfo.ParseResult.Script.Tokens) - { - if (currentIndex == tokenIndex) - { - return token; - } - ++currentIndex; - } - } - } - return null; - } - - private static bool ShouldShowCompletionList(Token token) - { - bool result = true; - if (token != null) - { - switch (token.Id) - { - case (int)Tokens.LEX_MULTILINE_COMMENT: - case (int)Tokens.LEX_END_OF_LINE_COMMENT: - result = false; - break; - } - } - return result; - } - #endregion #region Diagnostic Provider methods diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptDocumentInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptDocumentInfo.cs new file mode 100644 index 00000000..b4120733 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptDocumentInfo.cs @@ -0,0 +1,116 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlServer.Management.SqlParser.Parser; +using Microsoft.SqlTools.ServiceLayer.Utility; +using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion +{ + /// + /// A class to calculate the numbers used by SQL parser using the text positions and content + /// + internal class ScriptDocumentInfo + { + /// + /// Create new instance + /// + public ScriptDocumentInfo(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile, ScriptParseInfo scriptParseInfo) + { + StartLine = textDocumentPosition.Position.Line; + ParserLine = textDocumentPosition.Position.Line + 1; + StartColumn = TextUtilities.PositionOfPrevDelimeter( + scriptFile.Contents, + textDocumentPosition.Position.Line, + textDocumentPosition.Position.Character); + EndColumn = TextUtilities.PositionOfNextDelimeter( + scriptFile.Contents, + textDocumentPosition.Position.Line, + textDocumentPosition.Position.Character); + ParserColumn = textDocumentPosition.Position.Character + 1; + ScriptParseInfo = scriptParseInfo; + Contents = scriptFile.Contents; + + // need to adjust line & column for base-1 parser indices + Token = GetToken(scriptParseInfo, ParserLine, ParserColumn); + } + + /// + /// Gets a string containing the full contents of the file. + /// + public string Contents { get; private set; } + + /// + /// Script Parse Info Instance + /// + public ScriptParseInfo ScriptParseInfo { get; private set; } + + /// + /// Start Line + /// + public int StartLine { get; private set; } + + /// + /// Parser Line + /// + public int ParserLine { get; private set; } + + /// + /// Start Column + /// + public int StartColumn { get; private set; } + + /// + /// end Column + /// + public int EndColumn { get; private set; } + + /// + /// Parser Column + /// + public int ParserColumn { get; private set; } + + /// + /// The token text in the file content used for completion list + /// + public string TokenText + { + get + { + return Token != null ? Token.Text : null; + } + } + + /// + /// The token in the file content used for completion list + /// + public Token Token { get; private set; } + + /// + /// Returns the token that will be used by SQL parser for creating the completion list + /// + internal static Token GetToken(ScriptParseInfo scriptParseInfo, int startLine, int startColumn) + { + if (scriptParseInfo != null && scriptParseInfo.ParseResult != null && scriptParseInfo.ParseResult.Script != null && scriptParseInfo.ParseResult.Script.Tokens != null) + { + var tokenIndex = scriptParseInfo.ParseResult.Script.TokenManager.FindToken(startLine, startColumn); + if (tokenIndex >= 0) + { + // return the current token + int currentIndex = 0; + foreach (var token in scriptParseInfo.ParseResult.Script.Tokens) + { + if (currentIndex == tokenIndex) + { + return token; + } + ++currentIndex; + } + } + } + return null; + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/Completion/AutoCompletionResultTest.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/Completion/AutoCompletionResultTest.cs new file mode 100644 index 00000000..0fde1329 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/Completion/AutoCompletionResultTest.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + + +using System.Threading; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.Test.Completion +{ + public class AutoCompletionResultTest + { + [Fact] + public void MetricsShouldGetSortedGivenUnSortedArray() + { + AutoCompletionResult result = new AutoCompletionResult(); + int duration = 2000; + Thread.Sleep(duration); + result.CompleteResult(new CompletionItem[] { }); + Assert.True(result.Duration >= duration); + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/Completion/ScriptDocumentInfoTest.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/Completion/ScriptDocumentInfoTest.cs new file mode 100644 index 00000000..cc35edfb --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/Completion/ScriptDocumentInfoTest.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.ServiceLayer.LanguageServices; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion; +using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.Test.Completion +{ + public class ScriptDocumentInfoTest + { + [Fact] + public void MetricsShouldGetSortedGivenUnSortedArray() + { + TextDocumentPosition doc = new TextDocumentPosition() + { + TextDocument = new TextDocumentIdentifier + { + Uri = "script file" + }, + Position = new Position() + { + Line = 1, + Character = 14 + } + }; + ScriptFile scriptFile = new ScriptFile() + { + Contents = "Select * from sys.all_objects" + }; + + ScriptParseInfo scriptParseInfo = new ScriptParseInfo(); + ScriptDocumentInfo docInfo = new ScriptDocumentInfo(doc, scriptFile, scriptParseInfo); + + Assert.Equal(docInfo.StartLine, 1); + Assert.Equal(docInfo.ParserLine, 2); + Assert.Equal(docInfo.StartColumn, 44); + Assert.Equal(docInfo.EndColumn, 14); + Assert.Equal(docInfo.ParserColumn, 15); + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/CompletionServiceTest.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/CompletionServiceTest.cs new file mode 100644 index 00000000..50c2bb06 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/CompletionServiceTest.cs @@ -0,0 +1,93 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Microsoft.SqlServer.Management.SqlParser.Intellisense; +using Microsoft.SqlServer.Management.SqlParser.MetadataProvider; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.LanguageServices; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; +using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; +using Moq; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServer +{ + public class CompletionServiceTest + { + [Fact] + public void CompletionItemsShouldCreatedUsingSqlParserIfTheProcessDoesNotTimeout() + { + ConnectedBindingQueue bindingQueue = new ConnectedBindingQueue(); + ScriptDocumentInfo docInfo = CreateScriptDocumentInfo(); + CompletionService completionService = new CompletionService(bindingQueue); + ConnectionInfo connectionInfo = new ConnectionInfo(null, null, null); + bool useLowerCaseSuggestions = true; + CompletionItem[] defaultCompletionList = AutoCompleteHelper.GetDefaultCompletionItems(docInfo, useLowerCaseSuggestions); + + List declarations = new List(); + + var sqlParserWrapper = new Mock(); + sqlParserWrapper.Setup(x => x.FindCompletions(docInfo.ScriptParseInfo.ParseResult, docInfo.ParserLine, docInfo.ParserColumn, + It.IsAny())).Returns(declarations); + completionService.SqlParserWrapper = sqlParserWrapper.Object; + + AutoCompletionResult result = completionService.CreateCompletions(connectionInfo, docInfo, useLowerCaseSuggestions); + Assert.NotNull(result); + Assert.NotEqual(result.CompletionItems == null ? 0 : result.CompletionItems.Count(), defaultCompletionList.Count()); + } + + [Fact] + public void CompletionItemsShouldCreatedUsingDefaultListIfTheSqlParserProcessTimesout() + { + ConnectedBindingQueue bindingQueue = new ConnectedBindingQueue(); + ScriptDocumentInfo docInfo = CreateScriptDocumentInfo(); + CompletionService completionService = new CompletionService(bindingQueue); + ConnectionInfo connectionInfo = new ConnectionInfo(null, null, null); + bool useLowerCaseSuggestions = true; + List declarations = new List(); + CompletionItem[] defaultCompletionList = AutoCompleteHelper.GetDefaultCompletionItems(docInfo, useLowerCaseSuggestions); + + var sqlParserWrapper = new Mock(); + sqlParserWrapper.Setup(x => x.FindCompletions(docInfo.ScriptParseInfo.ParseResult, docInfo.ParserLine, docInfo.ParserColumn, + It.IsAny())).Callback(() => Thread.Sleep(LanguageService.BindingTimeout + 100)).Returns(declarations); + completionService.SqlParserWrapper = sqlParserWrapper.Object; + + AutoCompletionResult result = completionService.CreateCompletions(connectionInfo, docInfo, useLowerCaseSuggestions); + Assert.NotNull(result); + Assert.Equal(result.CompletionItems.Count(), defaultCompletionList.Count()); + Thread.Sleep(3000); + Assert.True(connectionInfo.IntellisenseMetrics.Quantile.Any()); + } + + private ScriptDocumentInfo CreateScriptDocumentInfo() + { + TextDocumentPosition doc = new TextDocumentPosition() + { + TextDocument = new TextDocumentIdentifier + { + Uri = "script file" + }, + Position = new Position() + { + Line = 1, + Character = 14 + } + }; + ScriptFile scriptFile = new ScriptFile() + { + Contents = "Select * from sys.all_objects" + }; + + ScriptParseInfo scriptParseInfo = new ScriptParseInfo() { IsConnected = true }; + ScriptDocumentInfo docInfo = new ScriptDocumentInfo(doc, scriptFile, scriptParseInfo); + + return docInfo; + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/InteractionMetricsTest.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/InteractionMetricsTest.cs new file mode 100644 index 00000000..0db21b14 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/InteractionMetricsTest.cs @@ -0,0 +1,84 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServer +{ + public class InteractionMetricsTest + { + [Fact] + public void MetricsShouldGetSortedGivenUnSortedArray() + { + int[] metrics = new int[] { 4, 8, 1, 11, 3 }; + int[] expected = new int[] { 1, 3, 4, 8, 11 }; + InteractionMetrics interactionMetrics = new InteractionMetrics(metrics); + + Assert.Equal(interactionMetrics.Metrics, expected); + } + + [Fact] + public void MetricsShouldThrowExceptionGivenNullInput() + { + int[] metrics = null; + Assert.Throws(() => new InteractionMetrics(metrics)); + } + + [Fact] + public void MetricsShouldThrowExceptionGivenEmptyInput() + { + int[] metrics = new int[] { }; + Assert.Throws(() => new InteractionMetrics(metrics)); + } + + [Fact] + public void MetricsShouldNotChangeGivenSortedArray() + { + int[] metrics = new int[] { 1, 3, 4, 8, 11 }; + int[] expected = new int[] { 1, 3, 4, 8, 11 }; + InteractionMetrics interactionMetrics = new InteractionMetrics(metrics); + + Assert.Equal(interactionMetrics.Metrics, expected); + } + + [Fact] + public void MetricsShouldNotChangeGivenArrayWithOneItem() + { + int[] metrics = new int[] { 11 }; + int[] expected = new int[] { 11 }; + InteractionMetrics interactionMetrics = new InteractionMetrics(metrics); + + Assert.Equal(interactionMetrics.Metrics, expected); + } + + [Fact] + public void MetricsCalculateQuantileCorrectlyGivenSeveralUpdates() + { + int[] metrics = new int[] { 50, 100, 300, 500, 1000, 2000 }; + Func updateValueFactory = (k, current) => current + 1; + InteractionMetrics interactionMetrics = new InteractionMetrics(metrics); + interactionMetrics.UpdateMetrics(54.4, 1, updateValueFactory); + interactionMetrics.UpdateMetrics(345, 1, updateValueFactory); + interactionMetrics.UpdateMetrics(23, 1, updateValueFactory); + interactionMetrics.UpdateMetrics(51, 1, updateValueFactory); + interactionMetrics.UpdateMetrics(500, 1, updateValueFactory); + interactionMetrics.UpdateMetrics(4005, 1, updateValueFactory); + interactionMetrics.UpdateMetrics(2500, 1, updateValueFactory); + interactionMetrics.UpdateMetrics(123, 1, updateValueFactory); + + Dictionary quantile = interactionMetrics.Quantile; + Assert.NotNull(quantile); + Assert.Equal(quantile.Count, 5); + Assert.Equal(quantile["50"], 1); + Assert.Equal(quantile["100"], 2); + Assert.Equal(quantile["300"], 1); + Assert.Equal(quantile["500"], 2); + Assert.Equal(quantile["2000"], 2); + + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/SqlCompletionItemTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/SqlCompletionItemTests.cs index 4688b739..727e379d 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/SqlCompletionItemTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/SqlCompletionItemTests.cs @@ -5,6 +5,7 @@ using Microsoft.SqlServer.Management.SqlParser.Intellisense; using Microsoft.SqlTools.ServiceLayer.LanguageServices; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion; using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; using Xunit;