Adding completion extension loading and execution logic (#833)

* Adding ICompletionExtension interface

* Adding extension loading and execution logic

* Fixing compilation error in VS 2017

* Using MEF for completion extension discovery

* using await on GetCompletionItems

* Adding an integration test for completion extension and update the completion extension interface

* Update the completion extension test

* Fix issues based on review comments

* Remove try/cache based on review comments, fix a integration test.

* More changes based on review comments

* Fixing SendResult logic for completion extension loading

* Only load completion extension from the assembly passed in, add more comments in the test

* Adding right assert messages in the test.

* More fixes based on review comments

* Dropping ICompletionExtensionProvider, load assembly only if it's loaded at the first time or updated since last load.

* Fix based on the latest review comments

* Adding missing TSQL functions in default completion list

* Update jsonrpc documentation for completion/extLoad
This commit is contained in:
Shengyu Fu
2019-07-19 12:04:03 -07:00
committed by Karl Burtram
parent e3ec6eb739
commit e1b9890f5c
18 changed files with 1000 additions and 354 deletions

View File

@@ -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/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) * :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 # Message Protocol
A message consists of two parts: a header section and the message body. For now, there is 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.
{ {
/// <summary> /// <summary>
/// A URI identifying the owner of the connection. This will most commonly be a file in the workspace /// 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.
/// </summary> /// </summary>
public string OwnerUri { get; set; } public string OwnerUri { get; set; }
} }
@@ -619,7 +623,7 @@ Save a resultset as CSV to a file.
/// End index of the selected rows (inclusive) /// End index of the selected rows (inclusive)
/// </summary> /// </summary>
public int? RowEndIndex { get; set; } public int? RowEndIndex { get; set; }
/// <summary> /// <summary>
/// Start index of the selected columns (inclusive) /// Start index of the selected columns (inclusive)
/// </summary> /// </summary>
@@ -661,7 +665,7 @@ Save a resultset as CSV to a file.
public class SaveResultRequestResult public class SaveResultRequestResult
{ {
/// <summary> /// <summary>
/// Error messages for saving to file. /// Error messages for saving to file.
/// </summary> /// </summary>
public string Messages { get; set; } public string Messages { get; set; }
} }
@@ -705,7 +709,7 @@ Save a resultset as JSON to a file.
/// End index of the selected rows (inclusive) /// End index of the selected rows (inclusive)
/// </summary> /// </summary>
public int? RowEndIndex { get; set; } public int? RowEndIndex { get; set; }
/// <summary> /// <summary>
/// Start index of the selected columns (inclusive) /// Start index of the selected columns (inclusive)
/// </summary> /// </summary>
@@ -739,8 +743,42 @@ Save a resultset as JSON to a file.
public class SaveResultRequestResult public class SaveResultRequestResult
{ {
/// <summary> /// <summary>
/// Error messages for saving to file. /// Error messages for saving to file.
/// </summary> /// </summary>
public string Messages { get; set; } public string Messages { get; set; }
} }
``` ```
## Language Service Protocol Extensions
### <a name="completion_extload"></a>`completion/extLoad`
Load a completion extension.
#### Request
```typescript
public class CompletionExtensionParams
{
/// <summary>
/// Absolute path for the assembly containing the completion extension
/// </summary>
public string AssemblyPath { get; set; }
/// <summary>
/// The type name for the completion extension
/// </summary>
public string TypeName { get; set; }
/// <summary>
/// Property bag for initializing the completion extension
/// </summary>
public Dictionary<string, object> Properties { get; set; }
}
```
#### Response
```typescript
bool
```

View File

@@ -95,6 +95,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SqlTools.ManagedB
EndProject 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}" 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 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{D3696EFA-FB1E-4848-A726-FF7B168AFB96}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -256,6 +264,7 @@ Global
{EF02F89F-417E-4A40-B7E6-B102EE2DF24D} = {2BBD7364-054F-4693-97CD-1C395E3E84A9} {EF02F89F-417E-4A40-B7E6-B102EE2DF24D} = {2BBD7364-054F-4693-97CD-1C395E3E84A9}
{3F82F298-700A-48DF-8A69-D048DFBA782C} = {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} {D3696EFA-FB1E-4848-A726-FF7B168AFB96} = {AB9CA2B8-6F70-431C-8A1D-67479D8A7BE4}
{0EC2B30C-0652-49AE-9594-85B3C3E9CA21} = {AB9CA2B8-6F70-431C-8A1D-67479D8A7BE4}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B31CDF4B-2851-45E5-8C5F-BE97125D9DD8} SolutionGuid = {B31CDF4B-2851-45E5-8C5F-BE97125D9DD8}

View File

@@ -5,6 +5,7 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Composition.Convention; using System.Composition.Convention;
using System.Composition.Hosting; using System.Composition.Hosting;
@@ -17,19 +18,19 @@ using Microsoft.SqlTools.Hosting.Utility;
namespace Microsoft.SqlTools.Hosting.Extensibility namespace Microsoft.SqlTools.Hosting.Extensibility
{ {
/// <summary> /// <summary>
/// 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 /// service discovery over a set of DLLs in an application scope. Any service registering using
/// the <c>[Export(IServiceContract)]</c> attribute will be discovered and used by this service /// the <c>[Export(IServiceContract)]</c> attribute will be discovered and used by this service
/// provider if it's in the set of Assemblies / Types specified during its construction. Manual /// 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
/// <see cref="RegisteredServiceProvider.RegisterSingleService" /> and similar methods, since /// <see cref="RegisteredServiceProvider.RegisterSingleService" /> 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 /// 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. /// while using MEF-based dependency injection and inversion of control for most of the code.
/// </summary> /// </summary>
public class ExtensionServiceProvider : RegisteredServiceProvider public class ExtensionServiceProvider : RegisteredServiceProvider
{ {
private readonly Func<ConventionBuilder, ContainerConfiguration> config; private Func<ConventionBuilder, ContainerConfiguration> config;
public ExtensionServiceProvider(Func<ConventionBuilder, ContainerConfiguration> config) public ExtensionServiceProvider(Func<ConventionBuilder, ContainerConfiguration> config)
{ {
@@ -106,8 +107,23 @@ namespace Microsoft.SqlTools.Hosting.Extensibility
Register(() => store.GetExports<T>()); Register(() => store.GetExports<T>());
} }
} }
/// <summary>
/// Merges in new assemblies to the existing container configuration.
/// </summary>
public void AddAssembliesToConfiguration(IEnumerable<Assembly> 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);
};
}
} }
/// <summary> /// <summary>
/// A store for MEF exports of a specific type. Provides basic wrapper functionality around MEF to standarize how /// 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. /// we lookup types and return to callers.
@@ -117,7 +133,7 @@ namespace Microsoft.SqlTools.Hosting.Extensibility
private readonly CompositionHost host; private readonly CompositionHost host;
private IList exports; private IList exports;
private readonly Type contractType; private readonly Type contractType;
/// <summary> /// <summary>
/// Initializes the store with a type to lookup exports of, and a function that configures the /// Initializes the store with a type to lookup exports of, and a function that configures the
/// lookup parameters. /// lookup parameters.
@@ -142,7 +158,7 @@ namespace Microsoft.SqlTools.Hosting.Extensibility
} }
return exports.Cast<T>(); return exports.Cast<T>();
} }
private ConventionBuilder GetExportBuilder() private ConventionBuilder GetExportBuilder()
{ {
// Define exports as matching a parent type, export as that parent type // Define exports as matching a parent type, export as that parent type

View File

@@ -5,6 +5,7 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Composition.Convention; using System.Composition.Convention;
using System.Composition.Hosting; using System.Composition.Hosting;
@@ -19,7 +20,7 @@ namespace Microsoft.SqlTools.Extensibility
{ {
public class ExtensionServiceProvider : RegisteredServiceProvider public class ExtensionServiceProvider : RegisteredServiceProvider
{ {
private static readonly string[] defaultInclusionList = private static readonly string[] defaultInclusionList =
{ {
"microsofsqltoolscredentials.dll", "microsofsqltoolscredentials.dll",
"microsoft.sqltools.hosting.dll", "microsoft.sqltools.hosting.dll",
@@ -36,8 +37,8 @@ namespace Microsoft.SqlTools.Extensibility
public static ExtensionServiceProvider CreateDefaultServiceProvider(string[] inclusionList = null) public static ExtensionServiceProvider CreateDefaultServiceProvider(string[] inclusionList = null)
{ {
// only allow loading MEF dependencies from our assemblies until we can // only allow loading MEF dependencies from our assemblies until we can
// better seperate out framework assemblies and extension assemblies // better seperate out framework assemblies and extension assemblies
return CreateFromAssembliesInDirectory(inclusionList ?? defaultInclusionList); return CreateFromAssembliesInDirectory(inclusionList ?? defaultInclusionList);
} }
@@ -114,8 +115,24 @@ namespace Microsoft.SqlTools.Extensibility
base.Register<T>(() => store.GetExports<T>()); base.Register<T>(() => store.GetExports<T>());
} }
} }
/// <summary>
/// Merges in new assemblies to the existing container configuration.
/// </summary>
public void AddAssembliesToConfiguration(IEnumerable<Assembly> 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);
};
}
} }
/// <summary> /// <summary>
/// A store for MEF exports of a specific type. Provides basic wrapper functionality around MEF to standarize how /// 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. /// we lookup types and return to callers.
@@ -125,7 +142,7 @@ namespace Microsoft.SqlTools.Extensibility
private CompositionHost host; private CompositionHost host;
private IList exports; private IList exports;
private Type contractType; private Type contractType;
/// <summary> /// <summary>
/// Initializes the store with a type to lookup exports of, and a function that configures the /// Initializes the store with a type to lookup exports of, and a function that configures the
/// lookup parameters. /// lookup parameters.
@@ -150,7 +167,7 @@ namespace Microsoft.SqlTools.Extensibility
{ {
return CreateAssemblyStore<T>(typeof(ExtensionStore).GetTypeInfo().Assembly); return CreateAssemblyStore<T>(typeof(ExtensionStore).GetTypeInfo().Assembly);
} }
public static ExtensionStore CreateAssemblyStore<T>(Assembly assembly) public static ExtensionStore CreateAssemblyStore<T>(Assembly assembly)
{ {
Validate.IsNotNull(nameof(assembly), assembly); Validate.IsNotNull(nameof(assembly), assembly);
@@ -162,7 +179,7 @@ namespace Microsoft.SqlTools.Extensibility
{ {
string assemblyPath = typeof(ExtensionStore).GetTypeInfo().Assembly.Location; string assemblyPath = typeof(ExtensionStore).GetTypeInfo().Assembly.Location;
string directory = Path.GetDirectoryName(assemblyPath); string directory = Path.GetDirectoryName(assemblyPath);
return new ExtensionStore(typeof(T), (conventions) => return new ExtensionStore(typeof(T), (conventions) =>
new ContainerConfiguration().WithAssembliesInPath(directory, conventions)); new ContainerConfiguration().WithAssembliesInPath(directory, conventions));
} }
@@ -174,7 +191,7 @@ namespace Microsoft.SqlTools.Extensibility
} }
return exports.Cast<T>(); return exports.Cast<T>();
} }
private ConventionBuilder GetExportBuilder() private ConventionBuilder GetExportBuilder()
{ {
// Define exports as matching a parent type, export as that parent type // Define exports as matching a parent type, export as that parent type
@@ -183,7 +200,7 @@ namespace Microsoft.SqlTools.Extensibility
return builder; return builder;
} }
} }
public static class ContainerConfigurationExtensions public static class ContainerConfigurationExtensions
{ {
public static ContainerConfiguration WithAssembliesInPath(this ContainerConfiguration configuration, string path, SearchOption searchOption = SearchOption.TopDirectoryOnly) 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"); var apiApplicationFileInfo = new FileInfo($"{folderPath}{Path.DirectorySeparatorChar}{assemblyName.Name}.dll");
if (File.Exists(apiApplicationFileInfo.FullName)) 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 // 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 // behavior in the loader. See https://github.com/dotnet/coreclr/issues/19632

View File

@@ -29,305 +29,394 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
private static CompletionItem[] emptyCompletionList = new CompletionItem[0]; private static CompletionItem[] emptyCompletionList = new CompletionItem[0];
private static readonly string[] DefaultCompletionText = new string[] private static readonly string[] DefaultCompletionText = new string[]
{ {
"all", "abs",
"alter", "acos",
"and", "all",
"apply", "alter",
"as", "and",
"asc", "apply",
"at", "approx_count_distinct",
"backup", "as",
"begin", "asc",
"between", "ascii",
"binary", "asin",
"bit", "at",
"break", "atan",
"bulk", "atn2",
"by", "avg",
"call", "backup",
"cascade", "begin",
"case", "between",
"catch", "binary",
"char", "bit",
"character", "break",
"check", "bulk",
"checkpoint", "by",
"close", "call",
"clustered", "cascade",
"column", "case",
"columnstore", "cast",
"commit", "catch",
"connect", "ceiling",
"constraint", "char",
"continue", "character",
"create", "charindex",
"cross", "check",
"current_date", "checkpoint",
"cursor", "checksum_agg",
"cursor_close_on_commit", "close",
"cursor_default", "clustered",
"data", "coalesce",
"data_compression", "column",
"database", "columnstore",
"date", "commit",
"datetime", "concat",
"datetime2", "concat_ws",
"days", "connect",
"dbcc", "constraint",
"dec", "continue",
"decimal", "convert",
"declare", "cos",
"default", "cot",
"delete", "count",
"deny", "count_big",
"desc", "create",
"description", "cross",
"disabled", "current_date",
"disk", "current_timestamp",
"distinct", "current_user",
"double", "cursor",
"drop", "cursor_close_on_commit",
"drop_existing", "cursor_default",
"dump", "data",
"dynamic", "data_compression",
"else", "database",
"enable", "datalength",
"encrypted", "date",
"end", "dateadd",
"end-exec", "datediff",
"except", "datefromparts",
"exec", "datename",
"execute", "datepart",
"exists", "datetime",
"exit", "datetime2",
"external", "day",
"fast_forward", "days",
"fetch", "dbcc",
"file", "dec",
"filegroup", "decimal",
"filename", "declare",
"filestream", "default",
"filter", "degrees",
"first", "delete",
"float", "deny",
"for", "desc",
"foreign", "description",
"from", "difference",
"full", "disabled",
"function", "disk",
"geography", "distinct",
"get", "double",
"global", "drop",
"go", "drop_existing",
"goto", "dump",
"grant", "dynamic",
"group", "else",
"hash", "enable",
"hashed", "encrypted",
"having", "end",
"hidden", "end-exec",
"hierarchyid", "except",
"holdlock", "exec",
"hours", "execute",
"identity", "exists",
"identitycol", "exit",
"if", "exp",
"image", "external",
"immediate", "fast_forward",
"include", "fetch",
"index", "file",
"inner", "filegroup",
"insert", "filename",
"instead", "filestream",
"int", "filter",
"integer", "first",
"intersect", "float",
"into", "floor",
"isolation", "for",
"join", "foreign",
"json", "format",
"key", "from",
"language", "full",
"last", "function",
"left", "geography",
"level", "get",
"lineno", "getdate",
"load", "getutcdate",
"local", "global",
"locate", "go",
"location", "goto",
"login", "grant",
"masked", "group",
"maxdop", "grouping",
"merge", "grouping_id",
"message", "hash",
"modify", "hashed",
"move", "having",
"namespace", "hidden",
"native_compilation", "hierarchyid",
"nchar", "holdlock",
"next", "hours",
"no", "identity",
"nocheck", "identitycol",
"nocount", "if",
"nonclustered", "iif",
"none", "image",
"norecompute", "immediate",
"not", "include",
"now", "index",
"null", "inner",
"numeric", "insert",
"nvarchar", "instead",
"object", "int",
"of", "integer",
"off", "intersect",
"offsets", "into",
"on", "isdate",
"online", "isnull",
"open", "isnumeric",
"openrowset", "isolation",
"openxml", "join",
"option", "json",
"or", "key",
"order", "language",
"out", "last",
"outer", "left",
"output", "len",
"over", "level",
"owner", "lineno",
"partial", "load",
"partition", "local",
"password", "locate",
"path", "location",
"percent", "log",
"percentage", "log10",
"period", "login",
"persisted", "lower",
"plan", "ltrim",
"policy", "masked",
"precision", "max",
"predicate", "maxdop",
"primary", "merge",
"print", "message",
"prior", "min",
"proc", "modify",
"procedure", "month",
"public", "move",
"query_store", "namespace",
"quoted_identifier", "native_compilation",
"raiserror", "nchar",
"range", "next",
"raw", "no",
"read", "nocheck",
"read_committed_snapshot", "nocount",
"read_only", "nonclustered",
"read_write", "none",
"readonly", "norecompute",
"readtext", "not",
"real", "now",
"rebuild", "null",
"receive", "nullif",
"reconfigure", "numeric",
"recovery", "nvarchar",
"recursive", "object",
"recursive_triggers", "of",
"references", "off",
"relative", "offsets",
"remove", "on",
"reorganize", "online",
"required", "open",
"restart", "openrowset",
"restore", "openxml",
"restrict", "option",
"resume", "or",
"return", "order",
"returns", "out",
"revert", "outer",
"revoke", "output",
"rollback", "over",
"rollup", "owner",
"row", "partial",
"rowcount", "partition",
"rowguidcol", "password",
"rows", "path",
"rule", "patindex",
"sample", "percent",
"save", "percentage",
"schema", "period",
"schemabinding", "persisted",
"scoped", "pi",
"scroll", "plan",
"secondary", "policy",
"security", "power",
"select", "precision",
"send", "predicate",
"sent", "primary",
"sequence", "print",
"server", "prior",
"session", "proc",
"set", "procedure",
"sets", "public",
"setuser", "query_store",
"simple", "quoted_identifier",
"smallint", "quotename",
"smallmoney", "radians",
"snapshot", "raiserror",
"sql", "rand",
"standard", "range",
"start", "raw",
"started", "read",
"state", "read_committed_snapshot",
"statement", "read_only",
"static", "read_write",
"statistics", "readonly",
"statistics_norecompute", "readtext",
"status", "real",
"stopped", "rebuild",
"sysname", "receive",
"system", "reconfigure",
"system_time", "recovery",
"table", "recursive",
"take", "recursive_triggers",
"target", "references",
"then", "relative",
"throw", "remove",
"time", "reorganize",
"timestamp", "replace",
"tinyint", "replicate",
"to", "required",
"top", "restart",
"tran", "restore",
"transaction", "restrict",
"trigger", "resume",
"truncate", "return",
"try", "returns",
"tsql", "reverse",
"type", "revert",
"uncommitted", "revoke",
"union", "right",
"unique", "rollback",
"uniqueidentifier", "rollup",
"update", "round",
"updatetext", "row",
"use", "rowcount",
"user", "rowguidcol",
"using", "rows",
"value", "rtrim",
"values", "rule",
"varchar", "sample",
"version", "save",
"view", "schema",
"waitfor", "schemabinding",
"when", "scoped",
"where", "scroll",
"while", "secondary",
"with", "security",
"within", "select",
"without", "send",
"writetext", "sent",
"xact_abort", "sequence",
"xml", "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",
}; };
/// <summary> /// <summary>
@@ -367,7 +456,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
int startColumn = scriptDocumentInfo.StartColumn; int startColumn = scriptDocumentInfo.StartColumn;
int endColumn = scriptDocumentInfo.EndColumn; int endColumn = scriptDocumentInfo.EndColumn;
string tokenText = scriptDocumentInfo.TokenText; 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; int listSize = DefaultCompletionText.Length;
if (!string.IsNullOrWhiteSpace(tokenText)) if (!string.IsNullOrWhiteSpace(tokenText))
{ {
@@ -392,14 +481,14 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
int completionItemIndex = 0; int completionItemIndex = 0;
foreach (var completionText in DefaultCompletionText) 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 // or if the completion item begins with the tokenText
if (string.IsNullOrWhiteSpace(tokenText) || completionText.StartsWith(tokenText, StringComparison.OrdinalIgnoreCase)) if (string.IsNullOrWhiteSpace(tokenText) || completionText.StartsWith(tokenText, StringComparison.OrdinalIgnoreCase))
{ {
completionItems[completionItemIndex] = CreateDefaultCompletionItem( completionItems[completionItemIndex] = CreateDefaultCompletionItem(
useLowerCase ? completionText.ToLowerInvariant() : completionText.ToUpperInvariant(), useLowerCase ? completionText.ToLowerInvariant() : completionText.ToUpperInvariant(),
row, row,
startColumn, startColumn,
endColumn); endColumn);
++completionItemIndex; ++completionItemIndex;
} }
@@ -417,8 +506,8 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// <param name="endColumn"></param> /// <param name="endColumn"></param>
private static CompletionItem CreateDefaultCompletionItem( private static CompletionItem CreateDefaultCompletionItem(
string label, string label,
int row, int row,
int startColumn, int startColumn,
int endColumn) int endColumn)
{ {
return SqlCompletionItem.CreateCompletionItem(label, label + " keyword", label, CompletionItemKind.Keyword, row, startColumn, endColumn); return SqlCompletionItem.CreateCompletionItem(label, label + " keyword", label, CompletionItemKind.Keyword, row, startColumn, endColumn);
@@ -450,14 +539,14 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// <param name="cursorColumn"></param> /// <param name="cursorColumn"></param>
/// <returns></returns> /// <returns></returns>
internal static CompletionItem[] ConvertDeclarationsToCompletionItems( internal static CompletionItem[] ConvertDeclarationsToCompletionItems(
IEnumerable<Declaration> suggestions, IEnumerable<Declaration> suggestions,
int row, int row,
int startColumn, int startColumn,
int endColumn, int endColumn,
string tokenText = null) string tokenText = null)
{ {
List<CompletionItem> completions = new List<CompletionItem>(); List<CompletionItem> completions = new List<CompletionItem>();
foreach (var autoCompleteItem in suggestions) foreach (var autoCompleteItem in suggestions)
{ {
SqlCompletionItem sqlCompletionItem = new SqlCompletionItem(autoCompleteItem, tokenText); SqlCompletionItem sqlCompletionItem = new SqlCompletionItem(autoCompleteItem, tokenText);
@@ -489,7 +578,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
markedStrings[0] = new MarkedString() markedStrings[0] = new MarkedString()
{ {
Language = "SQL", Language = "SQL",
Value = quickInfo.Text Value = quickInfo.Text
}; };
return new Hover() return new Hover()
@@ -535,7 +624,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
// Signature label format: <name> param1, param2, ..., paramn RETURNS <type> // Signature label format: <name> param1, param2, ..., paramn RETURNS <type>
Label = method.Name + " " + method.Parameters.Select(parameter => parameter.Display).Aggregate((l, r) => l + "," + r) + " " + method.Type, Label = method.Name + " " + method.Parameters.Select(parameter => parameter.Display).Aggregate((l, r) => l + "," + r) + " " + method.Type,
Documentation = method.Description, Documentation = method.Description,
Parameters = method.Parameters.Select(parameter => Parameters = method.Parameters.Select(parameter =>
{ {
return new ParameterInformation() return new ParameterInformation()
{ {
@@ -560,7 +649,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
if (locations.ParamStartLocation != null) if (locations.ParamStartLocation != null)
{ {
// Is the cursor past the function name? // 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)) if (line > location.LineNumber || (line == location.LineNumber && line == location.LineNumber && column >= location.ColumnNumber))
{ {
currentParameter = 0; currentParameter = 0;
@@ -577,7 +666,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
if (locations.ParamEndLocation != null) if (locations.ParamEndLocation != null)
{ {
// Is the cursor past the end of the parameter list on a different token? // 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)) if (line > location.LineNumber || (line == location.LineNumber && line == location.LineNumber && column > location.ColumnNumber))
{ {
currentParameter = -1; currentParameter = -1;

View File

@@ -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
{
/// <summary>
/// Absolute path for the assembly containing the completion extension
/// </summary>
public string AssemblyPath { get; set; }
/// <summary>
/// The type name for the completion extension
/// </summary>
public string TypeName { get; set; }
/// <summary>
/// Property bag for initializing the completion extension
/// </summary>
public Dictionary<string, object> Properties { get; set; }
}
}

View File

@@ -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
{
/// <summary>
/// Unique name for the extension
/// </summary>
string Name { get; }
/// <summary>
/// Method for initializing the extension, this is called once when the extension is loaded
/// </summary>
/// <param name="properties">Parameters needed by the extension</param>
/// <param name="cancelToken">Cancellation token used to indicate that the initialization should be cancelled</param>
/// <returns></returns>
Task Initialize(IReadOnlyDictionary<string, object> properties, CancellationToken token);
/// <summary>
/// Handles the completion request, returning the modified CompletionItemList if used
/// </summary>
/// <param name="connInfo">Connection information for the completion session</param>
/// <param name="scriptDocumentInfo">Script parsing information</param>
/// <param name="completions">Current completion list</param>
/// <param name="cancelToken">Token used to indicate that the completion request should be cancelled</param>
/// <returns></returns>
Task<CompletionItem[]> HandleCompletionAsync(ConnectionInfo connInfo, ScriptDocumentInfo scriptDocumentInfo, CompletionItem[] completions, CancellationToken cancelToken);
}
}

View File

@@ -5,6 +5,7 @@
using System.Diagnostics; using System.Diagnostics;
using Microsoft.SqlTools.Hosting.Protocol.Contracts; using Microsoft.SqlTools.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion.Extension;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
@@ -23,6 +24,13 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
RequestType<CompletionItem, CompletionItem>.Create("completionItem/resolve"); RequestType<CompletionItem, CompletionItem>.Create("completionItem/resolve");
} }
public class CompletionExtLoadRequest
{
public static readonly
RequestType<CompletionExtensionParams, bool> Type =
RequestType<CompletionExtensionParams, bool>.Create("completion/extLoad");
}
public enum CompletionItemKind public enum CompletionItemKind
{ {
Text = 1, Text = 1,
@@ -45,6 +53,19 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
Reference = 18 Reference = 18
} }
public class Command
{
/// <summary>
/// The identifier of the actual command handler, like `vsintellicode.completionItemSelected`.
/// </summary>
public string CommandStr { get; set; }
/// <summary>
/// Arguments that the command handler should be invoked with.
/// </summary>
public object[] Arguments { get; set; }
}
[DebuggerDisplay("Kind = {Kind.ToString()}, Label = {Label}, Detail = {Detail}")] [DebuggerDisplay("Kind = {Kind.ToString()}, Label = {Label}, Detail = {Detail}")]
public class CompletionItem public class CompletionItem
{ {
@@ -74,5 +95,15 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
/// resolve request. /// resolve request.
/// </summary> /// </summary>
public object Data { get; set; } public object Data { get; set; }
/// <summary>
/// Exposing a command field for a completion item for passing telemetry
/// </summary>
public Command Command { get; set; }
/// <summary>
/// Whether this completion item is preselected or not
/// </summary>
public bool? Preselect { get; set; }
} }
} }

View File

@@ -7,7 +7,10 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.SqlServer.Management.Common; 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.Intellisense;
using Microsoft.SqlServer.Management.SqlParser.Parser; using Microsoft.SqlServer.Management.SqlParser.Parser;
using Microsoft.SqlServer.Management.SqlParser.SqlCodeDom; using Microsoft.SqlServer.Management.SqlParser.SqlCodeDom;
using Microsoft.SqlTools.Extensibility;
using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
using Microsoft.SqlTools.ServiceLayer.Hosting; using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion; using Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion.Extension;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
using Microsoft.SqlTools.ServiceLayer.QueryExecution;
using Microsoft.SqlTools.ServiceLayer.Scripting; using Microsoft.SqlTools.ServiceLayer.Scripting;
using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.ServiceLayer.SqlContext;
using Microsoft.SqlTools.ServiceLayer.Utility; using Microsoft.SqlTools.ServiceLayer.Utility;
@@ -72,6 +76,10 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
internal const int PeekDefinitionTimeout = 10 * OneSecond; internal const int PeekDefinitionTimeout = 10 * OneSecond;
internal const int ExtensionLoadingTimeout = 10 * OneSecond;
internal const int CompletionExtTimeout = 200;
private ConnectionService connectionService = null; private ConnectionService connectionService = null;
private WorkspaceService<SqlToolsSettings> workspaceServiceInstance; private WorkspaceService<SqlToolsSettings> workspaceServiceInstance;
@@ -95,6 +103,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
private Lazy<Dictionary<string, ScriptParseInfo>> scriptParseInfoMap private Lazy<Dictionary<string, ScriptParseInfo>> scriptParseInfoMap
= new Lazy<Dictionary<string, ScriptParseInfo>>(() => new Dictionary<string, ScriptParseInfo>()); = new Lazy<Dictionary<string, ScriptParseInfo>>(() => new Dictionary<string, ScriptParseInfo>());
private readonly ConcurrentDictionary<string, ICompletionExtension> completionExtensions = new ConcurrentDictionary<string, ICompletionExtension>();
private readonly ConcurrentDictionary<string, DateTime> extAssemblyLastUpdateTime = new ConcurrentDictionary<string, DateTime>();
/// <summary> /// <summary>
/// Gets a mapping dictionary for SQL file URIs to ScriptParseInfo objects /// Gets a mapping dictionary for SQL file URIs to ScriptParseInfo objects
/// </summary> /// </summary>
@@ -245,6 +256,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
serviceHost.SetRequestHandler(CompletionRequest.Type, HandleCompletionRequest); serviceHost.SetRequestHandler(CompletionRequest.Type, HandleCompletionRequest);
serviceHost.SetRequestHandler(DefinitionRequest.Type, HandleDefinitionRequest); serviceHost.SetRequestHandler(DefinitionRequest.Type, HandleDefinitionRequest);
serviceHost.SetRequestHandler(SyntaxParseRequest.Type, HandleSyntaxParseRequest); serviceHost.SetRequestHandler(SyntaxParseRequest.Type, HandleSyntaxParseRequest);
serviceHost.SetRequestHandler(CompletionExtLoadRequest.Type, HandleCompletionExtLoadRequest);
serviceHost.SetEventHandler(RebuildIntelliSenseNotification.Type, HandleRebuildIntelliSenseNotification); serviceHost.SetEventHandler(RebuildIntelliSenseNotification.Type, HandleRebuildIntelliSenseNotification);
serviceHost.SetEventHandler(LanguageFlavorChangeNotification.Type, HandleDidChangeLanguageFlavorNotification); serviceHost.SetEventHandler(LanguageFlavorChangeNotification.Type, HandleDidChangeLanguageFlavorNotification);
@@ -283,6 +295,90 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
#region Request Handlers #region Request Handlers
/// <summary>
/// Completion extension load request callback
/// </summary>
/// <param name="param"></param>
/// <param name="requestContext"></param>
/// <returns></returns>
internal async Task HandleCompletionExtLoadRequest(CompletionExtensionParams param, RequestContext<bool> 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<ICompletionExtension>())
{
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));
}
/// <summary>
/// Check whether a particular assembly should be reloaded based on
/// whether it's been updated since it was last loaded.
/// </summary>
/// <param name="assemblyPath">The assembly path</param>
/// <param name="extTypeName">The type loading from the assembly</param>
/// <returns></returns>
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;
}
/// <summary> /// <summary>
/// T-SQL syntax parse request callback /// T-SQL syntax parse request callback
/// </summary> /// </summary>
@@ -352,7 +448,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
scriptFile.ClientFilePath, scriptFile.ClientFilePath,
out connInfo); out connInfo);
var completionItems = GetCompletionItems( var completionItems = await GetCompletionItems(
textDocumentPosition, scriptFile, connInfo); textDocumentPosition, scriptFile, connInfo);
await requestContext.SendResult(completionItems); 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 /// This method does not await cache builds since it expects to return quickly
/// </summary> /// </summary>
/// <param name="textDocumentPosition"></param> /// <param name="textDocumentPosition"></param>
public CompletionItem[] GetCompletionItems( public async Task<CompletionItem[]> GetCompletionItems(
TextDocumentPosition textDocumentPosition, TextDocumentPosition textDocumentPosition,
ScriptFile scriptFile, ScriptFile scriptFile,
ConnectionInfo connInfo) ConnectionInfo connInfo)
@@ -1482,7 +1578,11 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
if (scriptParseInfo == null) 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); 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 the parse failed then return the default list
if (scriptParseInfo.ParseResult == null) 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); AutoCompletionResult result = completionService.CreateCompletions(connInfo, scriptDocumentInfo, useLowerCaseSuggestions);
// cache the current script parse info object to resolve completions later // cache the current script parse info object to resolve completions later
@@ -1507,6 +1610,38 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
if (resultCompletionItems == null) if (resultCompletionItems == null)
{ {
resultCompletionItems = 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;
}
/// <summary>
/// Run all completion extensions
/// </summary>
/// <param name="connInfo"></param>
/// <param name="resultCompletionItems"></param>
/// <param name="scriptDocumentInfo"></param>
/// <returns></returns>
private async Task<CompletionItem[]> 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; return resultCompletionItems;

View File

@@ -14,7 +14,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion
/// <summary> /// <summary>
/// A class to calculate the numbers used by SQL parser using the text positions and content /// A class to calculate the numbers used by SQL parser using the text positions and content
/// </summary> /// </summary>
internal class ScriptDocumentInfo public class ScriptDocumentInfo
{ {
/// <summary> /// <summary>
/// Create new instance /// Create new instance

View File

@@ -12,7 +12,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// <summary> /// <summary>
/// Class for storing cached metadata regarding a parsed SQL file /// Class for storing cached metadata regarding a parsed SQL file
/// </summary> /// </summary>
internal class ScriptParseInfo public class ScriptParseInfo
{ {
private object buildingMetadataLock = new object(); private object buildingMetadataLock = new object();

View File

@@ -1,4 +1,4 @@
// //
// Copyright (c) Microsoft. All rights reserved. // Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information. // Licensed under the MIT license. See LICENSE file in the project root for full license information.
// //
@@ -6,6 +6,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.SqlTools.Utility; 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 /// Adds handling to check the Exception field of a task and log it if the task faulted
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This will effectively swallow exceptions in the task chain. /// This will effectively swallow exceptions in the task chain.
/// </remarks> /// </remarks>
/// <param name="antecedent">The task to continue</param> /// <param name="antecedent">The task to continue</param>
/// <param name="continuationAction"> /// <param name="continuationAction">
@@ -33,7 +34,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Utility
{ {
return; return;
} }
LogTaskExceptions(task.Exception); LogTaskExceptions(task.Exception);
// Run the continuation task that was provided // 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 version allows for async code to be ran in the continuation function.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This will effectively swallow exceptions in the task chain. /// This will effectively swallow exceptions in the task chain.
/// </remarks> /// </remarks>
/// <param name="antecedent">The task to continue</param> /// <param name="antecedent">The task to continue</param>
/// <param name="continuationFunc"> /// <param name="continuationFunc">
@@ -97,5 +98,38 @@ namespace Microsoft.SqlTools.ServiceLayer.Utility
} }
Logger.Write(TraceEventType.Error, sb.ToString()); Logger.Write(TraceEventType.Error, sb.ToString());
} }
/// <summary>
/// This will enforce time out to run an async task with returning result
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <param name="task">The async task to run</param>
/// <param name="timeout">Time out in milliseconds</param>
/// <returns></returns>
public static async Task<TResult> WithTimeout<TResult>(this Task<TResult> task, int timeout)
{
if (task == await Task.WhenAny(task, Task.Delay(timeout)))
{
return await task;
}
throw new TimeoutException();
}
/// <summary>
/// This will enforce time out to run an async task without returning result
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <param name="task">The async task to run</param>
/// <param name="timeout">Time out in milliseconds</param>
/// <returns></returns>
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();
}
} }
} }

View File

@@ -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<CompletionItem[]> 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<string, object> 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<CompletionItem[]> 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<string, string> { { "IsCommit", "True" } } }
};
}
//code to augment the default completion list
await Task.Delay(20); // for testing
token.ThrowIfCancellationRequested();
Console.WriteLine("Exit ExecuteAsync");
return sortedItems.ToArray();
}
}
}

View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../../Common.props" />
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.SqlTools.ServiceLayer\Microsoft.SqlTools.ServiceLayer.csproj" />
</ItemGroup>
</Project>

View File

@@ -3,14 +3,21 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information. // 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.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility;
using Microsoft.SqlTools.ServiceLayer.LanguageServices; using Microsoft.SqlTools.ServiceLayer.LanguageServices;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion.Extension;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
using Microsoft.SqlTools.ServiceLayer.Test.Common; using Microsoft.SqlTools.ServiceLayer.Test.Common;
using Microsoft.SqlTools.ServiceLayer.UnitTests.ServiceHost;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer 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 /// <summary>
// SMO connected metadata provider. Since we don't want a live DB dependency /// This test tests auto completion
// in the CI unit tests this scenario is currently disabled. /// </summary>
[Fact] [Fact]
public void AutoCompleteFindCompletions() public void AutoCompleteFindCompletions()
{ {
@@ -90,11 +97,116 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer
var completions = autoCompleteService.GetCompletionItems( var completions = autoCompleteService.GetCompletionItems(
result.TextDocumentPosition, result.TextDocumentPosition,
result.ScriptFile, result.ScriptFile,
result.ConnectionInfo); result.ConnectionInfo).Result;
Assert.True(completions.Length > 0); 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);
}
}
/// <summary>
/// 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
/// </summary>
[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<SqlTools.Hosting.Protocol.RequestContext<bool>>();
requestContext.Setup(x => x.SendResult(It.IsAny<bool>()))
.Returns(Task.FromResult(true));
requestContext.Setup(x => x.SendError(It.IsAny<string>(), 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<string, object> { { "modelPath", "testModel" } }
};
//load and initialize completion extension, expect a success
await autoCompleteService.HandleCompletionExtLoadRequest(extensionParams, requestContext.Object);
requestContext.Verify(x => x.SendResult(It.IsAny<bool>()), Times.Once);
requestContext.Verify(x => x.SendError(It.IsAny<string>(), 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<bool>()), Times.Once);
requestContext.Verify(x => x.SendError(It.IsAny<string>(), 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<string, object> { { "modelPath", "testModel" } }
};
//load and initialize completion extension
await autoCompleteService.HandleCompletionExtLoadRequest(extensionParams, requestContext.Object);
requestContext.Verify(x => x.SendResult(It.IsAny<bool>()), Times.Exactly(2));
requestContext.Verify(x => x.SendError(It.IsAny<string>(), 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);
}
/// <summary>
/// Make a copy of a file and update the last modified time
/// </summary>
/// <param name="filePath"></param>
/// <returns></returns>
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;
}
/// <summary> /// <summary>
/// Verify that GetSignatureHelp returns not null when the provided TextDocumentPosition /// Verify that GetSignatureHelp returns not null when the provided TextDocumentPosition
/// has an associated ScriptParseInfo and the provided query has a function that should /// 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 // 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); 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))}]"); 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()); new TestEventContext());
// Now we should expect to see the item show up in the completion list // 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); 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))}]"); Assert.True(afterTableCreationCompletionItems.Length == 1, $"Should only have a single completion item after rebuilding Intellisense cache. Actual : [{string.Join(',', initialCompletionItems.Select(ci => ci.Label))}]");

View File

@@ -27,6 +27,7 @@
<ProjectReference Include="../Microsoft.SqlTools.ServiceLayer.Test.Common/Microsoft.SqlTools.ServiceLayer.Test.Common.csproj" /> <ProjectReference Include="../Microsoft.SqlTools.ServiceLayer.Test.Common/Microsoft.SqlTools.ServiceLayer.Test.Common.csproj" />
<ProjectReference Include="../../src/Microsoft.SqlTools.ManagedBatchParser/Microsoft.SqlTools.ManagedBatchParser.csproj" /> <ProjectReference Include="../../src/Microsoft.SqlTools.ManagedBatchParser/Microsoft.SqlTools.ManagedBatchParser.csproj" />
<ProjectReference Include="../Microsoft.SqlTools.ServiceLayer.UnitTests/Microsoft.SqlTools.ServiceLayer.UnitTests.csproj" /> <ProjectReference Include="../Microsoft.SqlTools.ServiceLayer.UnitTests/Microsoft.SqlTools.ServiceLayer.UnitTests.csproj" />
<ProjectReference Include="..\CompletionExtSample\Microsoft.SqlTools.Test.CompletionExtension.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Net.Http" Version="4.3.1" /> <PackageReference Include="System.Net.Http" Version="4.3.1" />

View File

@@ -117,7 +117,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
{ {
InitializeTestObjects(); InitializeTestObjects();
textDocument.TextDocument.Uri = "somethinggoeshere"; 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] [Fact]

View File

@@ -22,7 +22,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ServiceHost
"SELECT * FROM sys.objects as o2" + Environment.NewLine + "SELECT * FROM sys.objects as o2" + Environment.NewLine +
"SELECT * FROM sys.objects as o3" + 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) if (initialText == null)
{ {