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)
{