diff --git a/docs/guide/jsonrpc_protocol.md b/docs/guide/jsonrpc_protocol.md index 4fe19340..89722305 100644 --- a/docs/guide/jsonrpc_protocol.md +++ b/docs/guide/jsonrpc_protocol.md @@ -66,6 +66,10 @@ The SQL Tools Service implements the following portion Language Service Protocol * :leftwards_arrow_with_hook: [textDocument/references](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#textDocument_references) * :leftwards_arrow_with_hook: [textDocument/definition](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#textDocument_definition) +### Language Service Protocol Extensions + +* :leftwards_arrow_with_hook: [completion/extLoad](#completion_extLoad) + # Message Protocol A message consists of two parts: a header section and the message body. For now, there is @@ -450,7 +454,7 @@ Disconnect the connection specified in the request. { /// /// A URI identifying the owner of the connection. This will most commonly be a file in the workspace - /// or a virtual file representing an object in a database. + /// or a virtual file representing an object in a database. /// public string OwnerUri { get; set; } } @@ -619,7 +623,7 @@ Save a resultset as CSV to a file. /// End index of the selected rows (inclusive) /// public int? RowEndIndex { get; set; } - + /// /// Start index of the selected columns (inclusive) /// @@ -661,7 +665,7 @@ Save a resultset as CSV to a file. public class SaveResultRequestResult { /// - /// Error messages for saving to file. + /// Error messages for saving to file. /// public string Messages { get; set; } } @@ -705,7 +709,7 @@ Save a resultset as JSON to a file. /// End index of the selected rows (inclusive) /// public int? RowEndIndex { get; set; } - + /// /// Start index of the selected columns (inclusive) /// @@ -739,8 +743,42 @@ Save a resultset as JSON to a file. public class SaveResultRequestResult { /// - /// Error messages for saving to file. + /// Error messages for saving to file. /// public string Messages { get; set; } } ``` + +## Language Service Protocol Extensions + +### `completion/extLoad` + +Load a completion extension. + +#### Request + +```typescript + public class CompletionExtensionParams + { + /// + /// Absolute path for the assembly containing the completion extension + /// + public string AssemblyPath { get; set; } + + /// + /// The type name for the completion extension + /// + public string TypeName { get; set; } + + /// + /// Property bag for initializing the completion extension + /// + public Dictionary Properties { get; set; } + } +``` + +#### Response + +```typescript + bool +``` \ No newline at end of file diff --git a/sqltoolsservice.sln b/sqltoolsservice.sln index 4943615d..daf76b39 100644 --- a/sqltoolsservice.sln +++ b/sqltoolsservice.sln @@ -95,6 +95,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SqlTools.ManagedB EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SqlTools.ManagedBatchParser.IntegrationTests", "test\Microsoft.SqlTools.ManagedBatchParser.IntegrationTests\Microsoft.SqlTools.ManagedBatchParser.IntegrationTests.csproj", "{D3696EFA-FB1E-4848-A726-FF7B168AFB96}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SqlTools.Test.CompletionExtension", "test\CompletionExtSample\Microsoft.SqlTools.Test.CompletionExtension.csproj", "{0EC2B30C-0652-49AE-9594-85B3C3E9CA21}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -228,6 +230,12 @@ Global {D3696EFA-FB1E-4848-A726-FF7B168AFB96}.Integration|Any CPU.Build.0 = Debug|Any CPU {D3696EFA-FB1E-4848-A726-FF7B168AFB96}.Release|Any CPU.ActiveCfg = Release|Any CPU {D3696EFA-FB1E-4848-A726-FF7B168AFB96}.Release|Any CPU.Build.0 = Release|Any CPU + {0EC2B30C-0652-49AE-9594-85B3C3E9CA21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0EC2B30C-0652-49AE-9594-85B3C3E9CA21}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0EC2B30C-0652-49AE-9594-85B3C3E9CA21}.Integration|Any CPU.ActiveCfg = Debug|Any CPU + {0EC2B30C-0652-49AE-9594-85B3C3E9CA21}.Integration|Any CPU.Build.0 = Debug|Any CPU + {0EC2B30C-0652-49AE-9594-85B3C3E9CA21}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0EC2B30C-0652-49AE-9594-85B3C3E9CA21}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -256,6 +264,7 @@ Global {EF02F89F-417E-4A40-B7E6-B102EE2DF24D} = {2BBD7364-054F-4693-97CD-1C395E3E84A9} {3F82F298-700A-48DF-8A69-D048DFBA782C} = {2BBD7364-054F-4693-97CD-1C395E3E84A9} {D3696EFA-FB1E-4848-A726-FF7B168AFB96} = {AB9CA2B8-6F70-431C-8A1D-67479D8A7BE4} + {0EC2B30C-0652-49AE-9594-85B3C3E9CA21} = {AB9CA2B8-6F70-431C-8A1D-67479D8A7BE4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B31CDF4B-2851-45E5-8C5F-BE97125D9DD8} diff --git a/src/Microsoft.SqlTools.Hosting.v2/Extensibility/ExtensionServiceProvider.cs b/src/Microsoft.SqlTools.Hosting.v2/Extensibility/ExtensionServiceProvider.cs index 2fd89909..457596b5 100644 --- a/src/Microsoft.SqlTools.Hosting.v2/Extensibility/ExtensionServiceProvider.cs +++ b/src/Microsoft.SqlTools.Hosting.v2/Extensibility/ExtensionServiceProvider.cs @@ -5,6 +5,7 @@ using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Composition.Convention; using System.Composition.Hosting; @@ -17,19 +18,19 @@ using Microsoft.SqlTools.Hosting.Utility; namespace Microsoft.SqlTools.Hosting.Extensibility { /// - /// A MEF-based service provider. Supports any MEF-based configuration but is optimized for + /// A MEF-based service provider. Supports any MEF-based configuration but is optimized for /// service discovery over a set of DLLs in an application scope. Any service registering using /// the [Export(IServiceContract)] attribute will be discovered and used by this service /// provider if it's in the set of Assemblies / Types specified during its construction. Manual - /// override of this is supported by calling + /// override of this is supported by calling /// and similar methods, since - /// this will initialize that service contract and avoid the MEF-based search and discovery + /// this will initialize that service contract and avoid the MEF-based search and discovery /// process. This allows the service provider to link into existing singleton / known services /// while using MEF-based dependency injection and inversion of control for most of the code. /// public class ExtensionServiceProvider : RegisteredServiceProvider { - private readonly Func config; + private Func config; public ExtensionServiceProvider(Func config) { @@ -106,8 +107,23 @@ namespace Microsoft.SqlTools.Hosting.Extensibility Register(() => store.GetExports()); } } + + /// + /// Merges in new assemblies to the existing container configuration. + /// + public void AddAssembliesToConfiguration(IEnumerable assemblies) + { + Validate.IsNotNull(nameof(assemblies), assemblies); + var previousConfig = config; + this.config = conventions => { + // Chain in the existing configuration function's result, then include additional + // assemblies + ContainerConfiguration containerConfig = previousConfig(conventions); + return containerConfig.WithAssemblies(assemblies, conventions); + }; + } } - + /// /// A store for MEF exports of a specific type. Provides basic wrapper functionality around MEF to standarize how /// we lookup types and return to callers. @@ -117,7 +133,7 @@ namespace Microsoft.SqlTools.Hosting.Extensibility private readonly CompositionHost host; private IList exports; private readonly Type contractType; - + /// /// Initializes the store with a type to lookup exports of, and a function that configures the /// lookup parameters. @@ -142,7 +158,7 @@ namespace Microsoft.SqlTools.Hosting.Extensibility } return exports.Cast(); } - + private ConventionBuilder GetExportBuilder() { // Define exports as matching a parent type, export as that parent type diff --git a/src/Microsoft.SqlTools.Hosting/Extensibility/ExtensionServiceProvider.cs b/src/Microsoft.SqlTools.Hosting/Extensibility/ExtensionServiceProvider.cs index e1bf025b..5bfad152 100644 --- a/src/Microsoft.SqlTools.Hosting/Extensibility/ExtensionServiceProvider.cs +++ b/src/Microsoft.SqlTools.Hosting/Extensibility/ExtensionServiceProvider.cs @@ -5,6 +5,7 @@ using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Composition.Convention; using System.Composition.Hosting; @@ -19,7 +20,7 @@ namespace Microsoft.SqlTools.Extensibility { public class ExtensionServiceProvider : RegisteredServiceProvider { - private static readonly string[] defaultInclusionList = + private static readonly string[] defaultInclusionList = { "microsofsqltoolscredentials.dll", "microsoft.sqltools.hosting.dll", @@ -36,8 +37,8 @@ namespace Microsoft.SqlTools.Extensibility public static ExtensionServiceProvider CreateDefaultServiceProvider(string[] inclusionList = null) { - // only allow loading MEF dependencies from our assemblies until we can - // better seperate out framework assemblies and extension assemblies + // only allow loading MEF dependencies from our assemblies until we can + // better seperate out framework assemblies and extension assemblies return CreateFromAssembliesInDirectory(inclusionList ?? defaultInclusionList); } @@ -114,8 +115,24 @@ namespace Microsoft.SqlTools.Extensibility base.Register(() => store.GetExports()); } } + + /// + /// Merges in new assemblies to the existing container configuration. + /// + public void AddAssembliesToConfiguration(IEnumerable assemblies) + { + Validate.IsNotNull(nameof(assemblies), assemblies); + var previousConfig = config; + this.config = conventions => { + // Chain in the existing configuration function's result, then include additional + // assemblies + ContainerConfiguration containerConfig = previousConfig(conventions); + return containerConfig.WithAssemblies(assemblies, conventions); + }; + } + } - + /// /// A store for MEF exports of a specific type. Provides basic wrapper functionality around MEF to standarize how /// we lookup types and return to callers. @@ -125,7 +142,7 @@ namespace Microsoft.SqlTools.Extensibility private CompositionHost host; private IList exports; private Type contractType; - + /// /// Initializes the store with a type to lookup exports of, and a function that configures the /// lookup parameters. @@ -150,7 +167,7 @@ namespace Microsoft.SqlTools.Extensibility { return CreateAssemblyStore(typeof(ExtensionStore).GetTypeInfo().Assembly); } - + public static ExtensionStore CreateAssemblyStore(Assembly assembly) { Validate.IsNotNull(nameof(assembly), assembly); @@ -162,7 +179,7 @@ namespace Microsoft.SqlTools.Extensibility { string assemblyPath = typeof(ExtensionStore).GetTypeInfo().Assembly.Location; string directory = Path.GetDirectoryName(assemblyPath); - return new ExtensionStore(typeof(T), (conventions) => + return new ExtensionStore(typeof(T), (conventions) => new ContainerConfiguration().WithAssembliesInPath(directory, conventions)); } @@ -174,7 +191,7 @@ namespace Microsoft.SqlTools.Extensibility } return exports.Cast(); } - + private ConventionBuilder GetExportBuilder() { // Define exports as matching a parent type, export as that parent type @@ -183,7 +200,7 @@ namespace Microsoft.SqlTools.Extensibility return builder; } } - + public static class ContainerConfigurationExtensions { public static ContainerConfiguration WithAssembliesInPath(this ContainerConfiguration configuration, string path, SearchOption searchOption = SearchOption.TopDirectoryOnly) @@ -230,7 +247,7 @@ namespace Microsoft.SqlTools.Extensibility var apiApplicationFileInfo = new FileInfo($"{folderPath}{Path.DirectorySeparatorChar}{assemblyName.Name}.dll"); if (File.Exists(apiApplicationFileInfo.FullName)) { - // Creating a new AssemblyContext instance for the same folder puts us at risk + // Creating a new AssemblyContext instance for the same folder puts us at risk // of loading the same DLL in multiple contexts, which leads to some unpredictable // behavior in the loader. See https://github.com/dotnet/coreclr/issues/19632 diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs index f13f19fd..2a468577 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs @@ -29,305 +29,394 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices private static CompletionItem[] emptyCompletionList = new CompletionItem[0]; private static readonly string[] DefaultCompletionText = new string[] - { - "all", - "alter", - "and", - "apply", - "as", - "asc", - "at", - "backup", - "begin", - "between", - "binary", - "bit", - "break", - "bulk", - "by", - "call", - "cascade", - "case", - "catch", - "char", - "character", - "check", - "checkpoint", - "close", - "clustered", - "column", - "columnstore", - "commit", - "connect", - "constraint", - "continue", - "create", - "cross", - "current_date", - "cursor", - "cursor_close_on_commit", - "cursor_default", - "data", - "data_compression", - "database", - "date", - "datetime", - "datetime2", - "days", - "dbcc", - "dec", - "decimal", - "declare", - "default", - "delete", - "deny", - "desc", - "description", - "disabled", - "disk", - "distinct", - "double", - "drop", - "drop_existing", - "dump", - "dynamic", - "else", - "enable", - "encrypted", - "end", - "end-exec", - "except", - "exec", - "execute", - "exists", - "exit", - "external", - "fast_forward", - "fetch", - "file", - "filegroup", - "filename", - "filestream", - "filter", - "first", - "float", - "for", - "foreign", - "from", - "full", - "function", - "geography", - "get", - "global", - "go", - "goto", - "grant", - "group", - "hash", - "hashed", - "having", - "hidden", - "hierarchyid", - "holdlock", - "hours", - "identity", - "identitycol", - "if", - "image", - "immediate", - "include", - "index", - "inner", - "insert", - "instead", - "int", - "integer", - "intersect", - "into", - "isolation", - "join", - "json", - "key", - "language", - "last", - "left", - "level", - "lineno", - "load", - "local", - "locate", - "location", - "login", - "masked", - "maxdop", - "merge", - "message", - "modify", - "move", - "namespace", - "native_compilation", - "nchar", - "next", - "no", - "nocheck", - "nocount", - "nonclustered", - "none", - "norecompute", - "not", - "now", - "null", - "numeric", - "nvarchar", - "object", - "of", - "off", - "offsets", - "on", - "online", - "open", - "openrowset", - "openxml", - "option", - "or", - "order", - "out", - "outer", - "output", - "over", - "owner", - "partial", - "partition", - "password", - "path", - "percent", - "percentage", - "period", - "persisted", - "plan", - "policy", - "precision", - "predicate", - "primary", - "print", - "prior", - "proc", - "procedure", - "public", - "query_store", - "quoted_identifier", - "raiserror", - "range", - "raw", - "read", - "read_committed_snapshot", - "read_only", - "read_write", - "readonly", - "readtext", - "real", - "rebuild", - "receive", - "reconfigure", - "recovery", - "recursive", - "recursive_triggers", - "references", - "relative", - "remove", - "reorganize", - "required", - "restart", - "restore", - "restrict", - "resume", - "return", - "returns", - "revert", - "revoke", - "rollback", - "rollup", - "row", - "rowcount", - "rowguidcol", - "rows", - "rule", - "sample", - "save", - "schema", - "schemabinding", - "scoped", - "scroll", - "secondary", - "security", - "select", - "send", - "sent", - "sequence", - "server", - "session", - "set", - "sets", - "setuser", - "simple", - "smallint", - "smallmoney", - "snapshot", - "sql", - "standard", - "start", - "started", - "state", - "statement", - "static", - "statistics", - "statistics_norecompute", - "status", - "stopped", - "sysname", - "system", - "system_time", - "table", - "take", - "target", - "then", - "throw", - "time", - "timestamp", - "tinyint", - "to", - "top", - "tran", - "transaction", - "trigger", - "truncate", - "try", - "tsql", - "type", - "uncommitted", - "union", - "unique", - "uniqueidentifier", - "update", - "updatetext", - "use", - "user", - "using", - "value", - "values", - "varchar", - "version", - "view", - "waitfor", - "when", - "where", - "while", - "with", - "within", - "without", - "writetext", - "xact_abort", - "xml", + { + "abs", + "acos", + "all", + "alter", + "and", + "apply", + "approx_count_distinct", + "as", + "asc", + "ascii", + "asin", + "at", + "atan", + "atn2", + "avg", + "backup", + "begin", + "between", + "binary", + "bit", + "break", + "bulk", + "by", + "call", + "cascade", + "case", + "cast", + "catch", + "ceiling", + "char", + "character", + "charindex", + "check", + "checkpoint", + "checksum_agg", + "close", + "clustered", + "coalesce", + "column", + "columnstore", + "commit", + "concat", + "concat_ws", + "connect", + "constraint", + "continue", + "convert", + "cos", + "cot", + "count", + "count_big", + "create", + "cross", + "current_date", + "current_timestamp", + "current_user", + "cursor", + "cursor_close_on_commit", + "cursor_default", + "data", + "data_compression", + "database", + "datalength", + "date", + "dateadd", + "datediff", + "datefromparts", + "datename", + "datepart", + "datetime", + "datetime2", + "day", + "days", + "dbcc", + "dec", + "decimal", + "declare", + "default", + "degrees", + "delete", + "deny", + "desc", + "description", + "difference", + "disabled", + "disk", + "distinct", + "double", + "drop", + "drop_existing", + "dump", + "dynamic", + "else", + "enable", + "encrypted", + "end", + "end-exec", + "except", + "exec", + "execute", + "exists", + "exit", + "exp", + "external", + "fast_forward", + "fetch", + "file", + "filegroup", + "filename", + "filestream", + "filter", + "first", + "float", + "floor", + "for", + "foreign", + "format", + "from", + "full", + "function", + "geography", + "get", + "getdate", + "getutcdate", + "global", + "go", + "goto", + "grant", + "group", + "grouping", + "grouping_id", + "hash", + "hashed", + "having", + "hidden", + "hierarchyid", + "holdlock", + "hours", + "identity", + "identitycol", + "if", + "iif", + "image", + "immediate", + "include", + "index", + "inner", + "insert", + "instead", + "int", + "integer", + "intersect", + "into", + "isdate", + "isnull", + "isnumeric", + "isolation", + "join", + "json", + "key", + "language", + "last", + "left", + "len", + "level", + "lineno", + "load", + "local", + "locate", + "location", + "log", + "log10", + "login", + "lower", + "ltrim", + "masked", + "max", + "maxdop", + "merge", + "message", + "min", + "modify", + "month", + "move", + "namespace", + "native_compilation", + "nchar", + "next", + "no", + "nocheck", + "nocount", + "nonclustered", + "none", + "norecompute", + "not", + "now", + "null", + "nullif", + "numeric", + "nvarchar", + "object", + "of", + "off", + "offsets", + "on", + "online", + "open", + "openrowset", + "openxml", + "option", + "or", + "order", + "out", + "outer", + "output", + "over", + "owner", + "partial", + "partition", + "password", + "path", + "patindex", + "percent", + "percentage", + "period", + "persisted", + "pi", + "plan", + "policy", + "power", + "precision", + "predicate", + "primary", + "print", + "prior", + "proc", + "procedure", + "public", + "query_store", + "quoted_identifier", + "quotename", + "radians", + "raiserror", + "rand", + "range", + "raw", + "read", + "read_committed_snapshot", + "read_only", + "read_write", + "readonly", + "readtext", + "real", + "rebuild", + "receive", + "reconfigure", + "recovery", + "recursive", + "recursive_triggers", + "references", + "relative", + "remove", + "reorganize", + "replace", + "replicate", + "required", + "restart", + "restore", + "restrict", + "resume", + "return", + "returns", + "reverse", + "revert", + "revoke", + "right", + "rollback", + "rollup", + "round", + "row", + "rowcount", + "rowguidcol", + "rows", + "rtrim", + "rule", + "sample", + "save", + "schema", + "schemabinding", + "scoped", + "scroll", + "secondary", + "security", + "select", + "send", + "sent", + "sequence", + "server", + "session", + "session_user", + "sessionproperty", + "set", + "sets", + "setuser", + "sign", + "simple", + "sin", + "smallint", + "smallmoney", + "snapshot", + "soundex", + "space", + "sql", + "sqrt", + "square", + "standard", + "start", + "started", + "state", + "statement", + "static", + "statistics", + "statistics_norecompute", + "status", + "stdev", + "stdevp", + "stopped", + "str", + "string_agg", + "stuff", + "substring", + "sum", + "sysdatetime", + "sysname", + "system", + "system_time", + "system_user", + "table", + "take", + "tan", + "target", + "then", + "throw", + "time", + "timestamp", + "tinyint", + "to", + "top", + "tran", + "transaction", + "translate", + "trigger", + "trim", + "truncate", + "try", + "tsql", + "type", + "uncommitted", + "unicode", + "union", + "unique", + "uniqueidentifier", + "update", + "updatetext", + "upper", + "use", + "user", + "user_name", + "using", + "value", + "values", + "var", + "varchar", + "varp", + "version", + "view", + "waitfor", + "when", + "where", + "while", + "with", + "within", + "without", + "writetext", + "xact_abort", + "xml", + "year", }; /// @@ -367,7 +456,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices int startColumn = scriptDocumentInfo.StartColumn; int endColumn = scriptDocumentInfo.EndColumn; string tokenText = scriptDocumentInfo.TokenText; - // determine how many default completion items there will be + // determine how many default completion items there will be int listSize = DefaultCompletionText.Length; if (!string.IsNullOrWhiteSpace(tokenText)) { @@ -392,14 +481,14 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices int completionItemIndex = 0; foreach (var completionText in DefaultCompletionText) { - // add item to list if the tokenText is null (meaning return whole list) + // add item to list if the tokenText is null (meaning return whole list) // or if the completion item begins with the tokenText if (string.IsNullOrWhiteSpace(tokenText) || completionText.StartsWith(tokenText, StringComparison.OrdinalIgnoreCase)) { completionItems[completionItemIndex] = CreateDefaultCompletionItem( useLowerCase ? completionText.ToLowerInvariant() : completionText.ToUpperInvariant(), - row, - startColumn, + row, + startColumn, endColumn); ++completionItemIndex; } @@ -417,8 +506,8 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// private static CompletionItem CreateDefaultCompletionItem( string label, - int row, - int startColumn, + int row, + int startColumn, int endColumn) { return SqlCompletionItem.CreateCompletionItem(label, label + " keyword", label, CompletionItemKind.Keyword, row, startColumn, endColumn); @@ -450,14 +539,14 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// internal static CompletionItem[] ConvertDeclarationsToCompletionItems( - IEnumerable suggestions, + IEnumerable suggestions, int row, int startColumn, - int endColumn, + int endColumn, string tokenText = null) - { + { List completions = new List(); - + foreach (var autoCompleteItem in suggestions) { SqlCompletionItem sqlCompletionItem = new SqlCompletionItem(autoCompleteItem, tokenText); @@ -489,7 +578,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices markedStrings[0] = new MarkedString() { Language = "SQL", - Value = quickInfo.Text + Value = quickInfo.Text }; return new Hover() @@ -535,7 +624,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices // Signature label format: param1, param2, ..., paramn RETURNS Label = method.Name + " " + method.Parameters.Select(parameter => parameter.Display).Aggregate((l, r) => l + "," + r) + " " + method.Type, Documentation = method.Description, - Parameters = method.Parameters.Select(parameter => + Parameters = method.Parameters.Select(parameter => { return new ParameterInformation() { @@ -560,7 +649,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices if (locations.ParamStartLocation != null) { // Is the cursor past the function name? - var location = locations.ParamStartLocation.Value; + var location = locations.ParamStartLocation.Value; if (line > location.LineNumber || (line == location.LineNumber && line == location.LineNumber && column >= location.ColumnNumber)) { currentParameter = 0; @@ -577,7 +666,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices if (locations.ParamEndLocation != null) { // Is the cursor past the end of the parameter list on a different token? - var location = locations.ParamEndLocation.Value; + var location = locations.ParamEndLocation.Value; if (line > location.LineNumber || (line == location.LineNumber && line == location.LineNumber && column > location.ColumnNumber)) { currentParameter = -1; diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Completion/Extension/CompletionExtensionParams.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Completion/Extension/CompletionExtensionParams.cs new file mode 100644 index 00000000..c6093eb4 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Completion/Extension/CompletionExtensionParams.cs @@ -0,0 +1,32 @@ +// +// 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 System.Text; + +namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion.Extension +{ + + [Serializable] + public class CompletionExtensionParams + { + /// + /// Absolute path for the assembly containing the completion extension + /// + public string AssemblyPath { get; set; } + + /// + /// The type name for the completion extension + /// + public string TypeName { get; set; } + + /// + /// Property bag for initializing the completion extension + /// + public Dictionary Properties { get; set; } + } + +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Completion/Extension/ICompletionExtension.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Completion/Extension/ICompletionExtension.cs new file mode 100644 index 00000000..2a6552bf --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Completion/Extension/ICompletionExtension.cs @@ -0,0 +1,40 @@ +// +// 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.Connection; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion.Extension +{ + public interface ICompletionExtension : IDisposable + { + /// + /// Unique name for the extension + /// + string Name { get; } + + /// + /// Method for initializing the extension, this is called once when the extension is loaded + /// + /// Parameters needed by the extension + /// Cancellation token used to indicate that the initialization should be cancelled + /// + Task Initialize(IReadOnlyDictionary properties, CancellationToken token); + + /// + /// Handles the completion request, returning the modified CompletionItemList if used + /// + /// Connection information for the completion session + /// Script parsing information + /// Current completion list + /// Token used to indicate that the completion request should be cancelled + /// + Task HandleCompletionAsync(ConnectionInfo connInfo, ScriptDocumentInfo scriptDocumentInfo, CompletionItem[] completions, CancellationToken cancelToken); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Contracts/Completion.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Contracts/Completion.cs index 9ecd7763..b4d85eaf 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Contracts/Completion.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/Contracts/Completion.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion.Extension; using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts @@ -23,6 +24,13 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts RequestType.Create("completionItem/resolve"); } + public class CompletionExtLoadRequest + { + public static readonly + RequestType Type = + RequestType.Create("completion/extLoad"); + } + public enum CompletionItemKind { Text = 1, @@ -45,6 +53,19 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts Reference = 18 } + public class Command + { + /// + /// The identifier of the actual command handler, like `vsintellicode.completionItemSelected`. + /// + public string CommandStr { get; set; } + + /// + /// Arguments that the command handler should be invoked with. + /// + public object[] Arguments { get; set; } + } + [DebuggerDisplay("Kind = {Kind.ToString()}, Label = {Label}, Detail = {Detail}")] public class CompletionItem { @@ -74,5 +95,15 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts /// resolve request. /// public object Data { get; set; } + + /// + /// Exposing a command field for a completion item for passing telemetry + /// + public Command Command { get; set; } + + /// + /// Whether this completion item is preselected or not + /// + public bool? Preselect { get; set; } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs index 9b9490df..6348ac9b 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs @@ -7,7 +7,10 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; +using System.Reflection; +using System.Runtime.Loader; using System.Threading; using System.Threading.Tasks; using Microsoft.SqlServer.Management.Common; @@ -17,13 +20,14 @@ using Microsoft.SqlServer.Management.SqlParser.Common; using Microsoft.SqlServer.Management.SqlParser.Intellisense; using Microsoft.SqlServer.Management.SqlParser.Parser; using Microsoft.SqlServer.Management.SqlParser.SqlCodeDom; +using Microsoft.SqlTools.Extensibility; using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; using Microsoft.SqlTools.ServiceLayer.Hosting; using Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion.Extension; using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; -using Microsoft.SqlTools.ServiceLayer.QueryExecution; using Microsoft.SqlTools.ServiceLayer.Scripting; using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.ServiceLayer.Utility; @@ -72,6 +76,10 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices internal const int PeekDefinitionTimeout = 10 * OneSecond; + internal const int ExtensionLoadingTimeout = 10 * OneSecond; + + internal const int CompletionExtTimeout = 200; + private ConnectionService connectionService = null; private WorkspaceService workspaceServiceInstance; @@ -95,6 +103,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices private Lazy> scriptParseInfoMap = new Lazy>(() => new Dictionary()); + private readonly ConcurrentDictionary completionExtensions = new ConcurrentDictionary(); + private readonly ConcurrentDictionary extAssemblyLastUpdateTime = new ConcurrentDictionary(); + /// /// Gets a mapping dictionary for SQL file URIs to ScriptParseInfo objects /// @@ -245,6 +256,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices serviceHost.SetRequestHandler(CompletionRequest.Type, HandleCompletionRequest); serviceHost.SetRequestHandler(DefinitionRequest.Type, HandleDefinitionRequest); serviceHost.SetRequestHandler(SyntaxParseRequest.Type, HandleSyntaxParseRequest); + serviceHost.SetRequestHandler(CompletionExtLoadRequest.Type, HandleCompletionExtLoadRequest); serviceHost.SetEventHandler(RebuildIntelliSenseNotification.Type, HandleRebuildIntelliSenseNotification); serviceHost.SetEventHandler(LanguageFlavorChangeNotification.Type, HandleDidChangeLanguageFlavorNotification); @@ -283,6 +295,90 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices #region Request Handlers + /// + /// Completion extension load request callback + /// + /// + /// + /// + internal async Task HandleCompletionExtLoadRequest(CompletionExtensionParams param, RequestContext requestContext) + { + try + { + //register the new assembly + var serviceProvider = (ExtensionServiceProvider)ServiceHostInstance.ServiceProvider; + var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(param.AssemblyPath); + var assemblies = new Assembly[] { assembly }; + serviceProvider.AddAssembliesToConfiguration(assemblies); + foreach (var ext in serviceProvider.GetServices()) + { + var cancellationTokenSource = new CancellationTokenSource(ExtensionLoadingTimeout); + var cancellationToken = cancellationTokenSource.Token; + string extName = ext.Name; + string extTypeName = ext.GetType().FullName; + if (extTypeName != param.TypeName) + { + continue; + } + + if (!CheckIfAssemblyShouldBeLoaded(param.AssemblyPath, extTypeName)) + { + await requestContext.SendError(string.Format("Skip loading {0} because it's already loaded", param.AssemblyPath)); + return; + } + + await ext.Initialize(param.Properties, cancellationToken).WithTimeout(ExtensionLoadingTimeout); + cancellationTokenSource.Dispose(); + if (!string.IsNullOrEmpty(extName)) + { + completionExtensions[extName] = ext; + await requestContext.SendResult(true); + return; + } + else + { + await requestContext.SendError(string.Format("Skip loading an unnamed completion extension from {0}", param.AssemblyPath)); + return; + } + } + } + catch (Exception ex) + { + await requestContext.SendError(ex.Message); + return; + } + + await requestContext.SendError(string.Format("Couldn't discover completion extension with type {0} in {1}", param.TypeName, param.AssemblyPath)); + } + + /// + /// Check whether a particular assembly should be reloaded based on + /// whether it's been updated since it was last loaded. + /// + /// The assembly path + /// The type loading from the assembly + /// + private bool CheckIfAssemblyShouldBeLoaded(string assemblyPath, string extTypeName) + { + var lastModified = File.GetLastWriteTime(assemblyPath); + if (extAssemblyLastUpdateTime.ContainsKey(extTypeName)) + { + if (lastModified > extAssemblyLastUpdateTime[extTypeName]) + { + extAssemblyLastUpdateTime[extTypeName] = lastModified; + return true; + } + } + else + { + extAssemblyLastUpdateTime[extTypeName] = lastModified; + return true; + } + + return false; + + } + /// /// T-SQL syntax parse request callback /// @@ -352,7 +448,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices scriptFile.ClientFilePath, out connInfo); - var completionItems = GetCompletionItems( + var completionItems = await GetCompletionItems( textDocumentPosition, scriptFile, connInfo); await requestContext.SendResult(completionItems); @@ -1466,7 +1562,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// This method does not await cache builds since it expects to return quickly /// /// - public CompletionItem[] GetCompletionItems( + public async Task GetCompletionItems( TextDocumentPosition textDocumentPosition, ScriptFile scriptFile, ConnectionInfo connInfo) @@ -1482,7 +1578,11 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices if (scriptParseInfo == null) { - return AutoCompleteHelper.GetDefaultCompletionItems(ScriptDocumentInfo.CreateDefaultDocumentInfo(textDocumentPosition, scriptFile), useLowerCaseSuggestions); + var scriptDocInfo = ScriptDocumentInfo.CreateDefaultDocumentInfo(textDocumentPosition, scriptFile); + resultCompletionItems = AutoCompleteHelper.GetDefaultCompletionItems(scriptDocInfo, useLowerCaseSuggestions); + //call completion extensions only for default completion list + resultCompletionItems = await ApplyCompletionExtensions(connInfo, resultCompletionItems, scriptDocInfo); + return resultCompletionItems; } ScriptDocumentInfo scriptDocumentInfo = new ScriptDocumentInfo(textDocumentPosition, scriptFile, scriptParseInfo); @@ -1496,7 +1596,10 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices // if the parse failed then return the default list if (scriptParseInfo.ParseResult == null) { - return AutoCompleteHelper.GetDefaultCompletionItems(scriptDocumentInfo, useLowerCaseSuggestions); + resultCompletionItems = AutoCompleteHelper.GetDefaultCompletionItems(scriptDocumentInfo, useLowerCaseSuggestions); + //call completion extensions only for default completion list + resultCompletionItems = await ApplyCompletionExtensions(connInfo, resultCompletionItems, scriptDocumentInfo); + return resultCompletionItems; } AutoCompletionResult result = completionService.CreateCompletions(connInfo, scriptDocumentInfo, useLowerCaseSuggestions); // cache the current script parse info object to resolve completions later @@ -1507,6 +1610,38 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices if (resultCompletionItems == null) { resultCompletionItems = AutoCompleteHelper.GetDefaultCompletionItems(scriptDocumentInfo, useLowerCaseSuggestions); + //call completion extensions only for default completion list + resultCompletionItems = await ApplyCompletionExtensions(connInfo, resultCompletionItems, scriptDocumentInfo); + } + + return resultCompletionItems; + } + + /// + /// Run all completion extensions + /// + /// + /// + /// + /// + private async Task ApplyCompletionExtensions(ConnectionInfo connInfo, CompletionItem[] resultCompletionItems, ScriptDocumentInfo scriptDocumentInfo) + { + //invoke the completion extensions + foreach (var completionExt in completionExtensions.Values) + { + var cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource.CancelAfter(CompletionExtTimeout); + var cancellationToken = cancellationTokenSource.Token; + try + { + resultCompletionItems = await completionExt.HandleCompletionAsync(connInfo, scriptDocumentInfo, resultCompletionItems, cancellationToken).WithTimeout(CompletionExtTimeout); + } + catch (Exception e) + { + Logger.Write(TraceEventType.Error, string.Format("Exception in calling completion extension {0}:\n{1}", completionExt.Name, e.ToString())); + } + + cancellationTokenSource.Dispose(); } return resultCompletionItems; diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptDocumentInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptDocumentInfo.cs index f46480e2..5a5ed5f1 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptDocumentInfo.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptDocumentInfo.cs @@ -14,7 +14,7 @@ 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 + public class ScriptDocumentInfo { /// /// Create new instance diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs index ec87ae6f..ce80c058 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs @@ -12,7 +12,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// Class for storing cached metadata regarding a parsed SQL file /// - internal class ScriptParseInfo + public class ScriptParseInfo { private object buildingMetadataLock = new object(); diff --git a/src/Microsoft.SqlTools.ServiceLayer/Utility/TaskExtensions.cs b/src/Microsoft.SqlTools.ServiceLayer/Utility/TaskExtensions.cs index 0d611f58..1afc27bf 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Utility/TaskExtensions.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Utility/TaskExtensions.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -6,6 +6,7 @@ using System; using System.Diagnostics; using System.Text; +using System.Threading; using System.Threading.Tasks; using Microsoft.SqlTools.Utility; @@ -17,7 +18,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Utility /// Adds handling to check the Exception field of a task and log it if the task faulted /// /// - /// This will effectively swallow exceptions in the task chain. + /// This will effectively swallow exceptions in the task chain. /// /// The task to continue /// @@ -33,7 +34,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Utility { return; } - + LogTaskExceptions(task.Exception); // Run the continuation task that was provided @@ -54,7 +55,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Utility /// This version allows for async code to be ran in the continuation function. /// /// - /// This will effectively swallow exceptions in the task chain. + /// This will effectively swallow exceptions in the task chain. /// /// The task to continue /// @@ -97,5 +98,38 @@ namespace Microsoft.SqlTools.ServiceLayer.Utility } Logger.Write(TraceEventType.Error, sb.ToString()); } + + /// + /// This will enforce time out to run an async task with returning result + /// + /// + /// The async task to run + /// Time out in milliseconds + /// + public static async Task WithTimeout(this Task task, int timeout) + { + if (task == await Task.WhenAny(task, Task.Delay(timeout))) + { + return await task; + } + throw new TimeoutException(); + } + + /// + /// This will enforce time out to run an async task without returning result + /// + /// + /// The async task to run + /// Time out in milliseconds + /// + public static async Task WithTimeout(this Task task, int timeout) + { + if (task == await Task.WhenAny(task, Task.Delay(timeout))) + { + await task; + return; + } + throw new TimeoutException(); + } } } \ No newline at end of file diff --git a/test/CompletionExtSample/CompletionExt.cs b/test/CompletionExtSample/CompletionExt.cs new file mode 100644 index 00000000..3d277461 --- /dev/null +++ b/test/CompletionExtSample/CompletionExt.cs @@ -0,0 +1,81 @@ +// +// 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 System.ComponentModel.Composition; +using System.Threading; +using System.Threading.Tasks; +using System.Linq; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion.Extension; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; + +namespace Microsoft.SqlTools.Test.CompletionExtension +{ + + [Export(typeof(ICompletionExtension))] + public class CompletionExt : ICompletionExtension + { + public string Name => "CompletionExt"; + + private string modelPath; + + public CompletionExt() + { + } + + void IDisposable.Dispose() + { + } + + async Task ICompletionExtension.HandleCompletionAsync(ConnectionInfo connInfo, ScriptDocumentInfo scriptDocumentInfo, CompletionItem[] completions, CancellationToken token) + { + if (completions == null || completions == null || completions.Length == 0) + { + return completions; + } + return await Run(completions, token); + } + + async Task ICompletionExtension.Initialize(IReadOnlyDictionary properties, CancellationToken token) + { + modelPath = (string)properties["modelPath"]; + await LoadModel(token).ConfigureAwait(false); + return; + } + + private async Task LoadModel(CancellationToken token) + { + //loading model logic here + await Task.Delay(2000).ConfigureAwait(false); //for testing + token.ThrowIfCancellationRequested(); + Console.WriteLine("Model loaded from: " + modelPath); + } + + private async Task Run(CompletionItem[] completions, CancellationToken token) + { + Console.WriteLine("Enter ExecuteAsync"); + + var sortedItems = completions.OrderBy(item => item.SortText); + sortedItems.First().Preselect = true; + foreach(var item in sortedItems) + { + item.Command = new Command + { + CommandStr = "vsintellicode.completionItemSelected", + Arguments = new object[] { new Dictionary { { "IsCommit", "True" } } } + }; + } + + //code to augment the default completion list + await Task.Delay(20); // for testing + token.ThrowIfCancellationRequested(); + Console.WriteLine("Exit ExecuteAsync"); + return sortedItems.ToArray(); + } + } +} diff --git a/test/CompletionExtSample/Microsoft.SqlTools.Test.CompletionExtension.csproj b/test/CompletionExtSample/Microsoft.SqlTools.Test.CompletionExtension.csproj new file mode 100644 index 00000000..f08c925b --- /dev/null +++ b/test/CompletionExtSample/Microsoft.SqlTools.Test.CompletionExtension.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp2.2 + + + + + + + diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/LanguageServer/LanguageServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/LanguageServer/LanguageServiceTests.cs index 8c20dd17..2cb2d362 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/LanguageServer/LanguageServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/LanguageServer/LanguageServiceTests.cs @@ -3,14 +3,21 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; using Microsoft.SqlTools.ServiceLayer.LanguageServices; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion.Extension; using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; using Microsoft.SqlTools.ServiceLayer.Test.Common; +using Microsoft.SqlTools.ServiceLayer.UnitTests.ServiceHost; using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; +using Moq; using Xunit; namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer @@ -75,9 +82,9 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer } } - // This test currently requires a live database connection to initialize - // SMO connected metadata provider. Since we don't want a live DB dependency - // in the CI unit tests this scenario is currently disabled. + /// + /// This test tests auto completion + /// [Fact] public void AutoCompleteFindCompletions() { @@ -90,11 +97,116 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer var completions = autoCompleteService.GetCompletionItems( result.TextDocumentPosition, result.ScriptFile, - result.ConnectionInfo); + result.ConnectionInfo).Result; Assert.True(completions.Length > 0); } + public static string AssemblyDirectory + { + get + { + string codeBase = Assembly.GetExecutingAssembly().CodeBase; + UriBuilder uri = new UriBuilder(codeBase); + string path = Uri.UnescapeDataString(uri.Path); + return Path.GetDirectoryName(path); + } + } + + /// + /// This test tests completion extension interface in following aspects + /// 1. Loading a sample completion extension assembly + /// 2. Initializing a completion extension implementation + /// 3. Excuting an auto completion with extension enabled + /// + [Fact] + public async void AutoCompleteWithExtension() + { + var result = GetLiveAutoCompleteTestObjects(); + + result.TextDocumentPosition.Position.Character = 10; + result.ScriptFile = ScriptFileTests.GetTestScriptFile("select * f"); + result.TextDocumentPosition.TextDocument.Uri = result.ScriptFile.FilePath; + + var autoCompleteService = LanguageService.Instance; + var requestContext = new Mock>(); + requestContext.Setup(x => x.SendResult(It.IsAny())) + .Returns(Task.FromResult(true)); + requestContext.Setup(x => x.SendError(It.IsAny(), 0)) + .Returns(Task.FromResult(true)); + + //Create completion extension parameters + var extensionParams = new CompletionExtensionParams() + { + AssemblyPath = Path.Combine(AssemblyDirectory, "Microsoft.SqlTools.Test.CompletionExtension.dll"), + TypeName = "Microsoft.SqlTools.Test.CompletionExtension.CompletionExt", + Properties = new Dictionary { { "modelPath", "testModel" } } + }; + + //load and initialize completion extension, expect a success + await autoCompleteService.HandleCompletionExtLoadRequest(extensionParams, requestContext.Object); + + requestContext.Verify(x => x.SendResult(It.IsAny()), Times.Once); + requestContext.Verify(x => x.SendError(It.IsAny(), 0), Times.Never); + + //Try to load the same completion extension second time, expect an error sent + await autoCompleteService.HandleCompletionExtLoadRequest(extensionParams, requestContext.Object); + + requestContext.Verify(x => x.SendResult(It.IsAny()), Times.Once); + requestContext.Verify(x => x.SendError(It.IsAny(), 0), Times.Once); + + //Try to load the completion extension with new modified timestamp, expect a success + var assemblyCopyPath = CopyFileWithNewModifiedTime(extensionParams.AssemblyPath); + extensionParams = new CompletionExtensionParams() + { + AssemblyPath = assemblyCopyPath, + TypeName = "Microsoft.SqlTools.Test.CompletionExtension.CompletionExt", + Properties = new Dictionary { { "modelPath", "testModel" } } + }; + //load and initialize completion extension + await autoCompleteService.HandleCompletionExtLoadRequest(extensionParams, requestContext.Object); + + requestContext.Verify(x => x.SendResult(It.IsAny()), Times.Exactly(2)); + requestContext.Verify(x => x.SendError(It.IsAny(), 0), Times.Once); + + ScriptParseInfo scriptInfo = new ScriptParseInfo { IsConnected = true }; + autoCompleteService.ParseAndBind(result.ScriptFile, result.ConnectionInfo); + scriptInfo.ConnectionKey = autoCompleteService.BindingQueue.AddConnectionContext(result.ConnectionInfo); + + //Invoke auto completion with extension enabled + var completions = autoCompleteService.GetCompletionItems( + result.TextDocumentPosition, + result.ScriptFile, + result.ConnectionInfo).Result; + + //Validate completion list is not empty + Assert.True(completions != null && completions.Length > 0, "The completion list is null or empty!"); + //Validate the first completion item in the list is preselected + Assert.True(completions[0].Preselect.HasValue && completions[0].Preselect.Value, "Preselect is not set properly in the first completion item by the completion extension!"); + //Validate the Command object attached to the completion item by the extension + Assert.True(completions[0].Command != null && completions[0].Command.CommandStr == "vsintellicode.completionItemSelected", "Command is not set properly in the first completion item by the completion extension!"); + + //clean up the temp file + File.Delete(assemblyCopyPath); + } + + /// + /// Make a copy of a file and update the last modified time + /// + /// + /// + private string CopyFileWithNewModifiedTime(string filePath) + { + var tempPath = Path.Combine(Path.GetTempPath(), Path.GetFileName(filePath)); + if (File.Exists(tempPath)) + { + File.Delete(tempPath); + } + File.Copy(filePath, tempPath); + File.SetLastWriteTimeUtc(tempPath, DateTime.UtcNow); + return tempPath; + } + /// /// Verify that GetSignatureHelp returns not null when the provided TextDocumentPosition /// has an associated ScriptParseInfo and the provided query has a function that should @@ -191,7 +303,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer }; // First check that we don't have any items in the completion list as expected - var initialCompletionItems = langService.GetCompletionItems( + var initialCompletionItems = await langService.GetCompletionItems( textDocumentPosition, connectionInfoResult.ScriptFile, connectionInfoResult.ConnectionInfo); Assert.True(initialCompletionItems.Length == 0, $"Should not have any completion items initially. Actual : [{string.Join(',', initialCompletionItems.Select(ci => ci.Label))}]"); @@ -205,7 +317,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer new TestEventContext()); // Now we should expect to see the item show up in the completion list - var afterTableCreationCompletionItems = langService.GetCompletionItems( + var afterTableCreationCompletionItems = await langService.GetCompletionItems( textDocumentPosition, connectionInfoResult.ScriptFile, connectionInfoResult.ConnectionInfo); Assert.True(afterTableCreationCompletionItems.Length == 1, $"Should only have a single completion item after rebuilding Intellisense cache. Actual : [{string.Join(',', initialCompletionItems.Select(ci => ci.Label))}]"); diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Microsoft.SqlTools.ServiceLayer.IntegrationTests.csproj b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Microsoft.SqlTools.ServiceLayer.IntegrationTests.csproj index 8894a852..19db819e 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Microsoft.SqlTools.ServiceLayer.IntegrationTests.csproj +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Microsoft.SqlTools.ServiceLayer.IntegrationTests.csproj @@ -27,6 +27,7 @@ + diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/AutocompleteTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/AutocompleteTests.cs index 39389f0b..d0a1c9ca 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/AutocompleteTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/LanguageServer/AutocompleteTests.cs @@ -117,7 +117,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer { InitializeTestObjects(); textDocument.TextDocument.Uri = "somethinggoeshere"; - Assert.True(langService.GetCompletionItems(textDocument, scriptFile.Object, null).Length > 0); + Assert.True(langService.GetCompletionItems(textDocument, scriptFile.Object, null).Result.Length > 0); } [Fact] diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ServiceHost/ScriptFileTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ServiceHost/ScriptFileTests.cs index d35a1f54..89cd1aec 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ServiceHost/ScriptFileTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ServiceHost/ScriptFileTests.cs @@ -22,7 +22,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ServiceHost "SELECT * FROM sys.objects as o2" + Environment.NewLine + "SELECT * FROM sys.objects as o3" + Environment.NewLine; - internal static ScriptFile GetTestScriptFile(string initialText = null) + public static ScriptFile GetTestScriptFile(string initialText = null) { if (initialText == null) {