TSQL Formatter Service (#229)

- TSqlFormatterService with support for formatting document and text range inside document
- Settings support for all formatting options.
- Extensibility support so that the service can be initialized using MEF extensibility, and can find all necessary TSqlFormatters using the same process

Fix Initialize request error on startup
- Messages were being read from the input channel before all request handlers were registered
- In particular, the Initialize request which is key for any server to talk to the client was getting lost because the message reader thread begins consuming, and we take an extra few hundred milliseconds due to MEF startup before we register the handler
- The solution is to initialize the message handler so request handlers can register, but not actually start processing incoming messages until all handers are ready. This is a safer way to go and should improve reliability overall

Improvements from internal prototype:
- Normalizing baselines to handle the line ending differences on Mac & Linux vs. Windows
- Significantly shortened most lines by implementing base class methods to wrap common objects from Visitor.Context and removing unnecessary "this." syntax
- Refactored the SqlCommonTableExpressionFormatter and related classes to reduce code count significantly. This provides a pattern to follow when refactoring other classes for similar clarity. It's likely a lot of common logic could be found and reused across these.
- Reduced overall code size by adding utility methods
This commit is contained in:
Kevin Cunnane
2017-02-14 23:40:17 -08:00
committed by GitHub
parent eb4f2f2b91
commit 7477642854
212 changed files with 7905 additions and 184 deletions

View File

@@ -0,0 +1,184 @@
//
// 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;
using System.Collections.Generic;
using System.Composition.Convention;
using System.Composition.Hosting;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.Extensions.DependencyModel;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.Extensibility
{
public class ExtensionServiceProvider : RegisteredServiceProvider
{
private Func<ConventionBuilder, ContainerConfiguration> config;
public ExtensionServiceProvider(Func<ConventionBuilder, ContainerConfiguration> config)
{
Validate.IsNotNull(nameof(config), config);
this.config = config;
}
public static ExtensionServiceProvider CreateDefaultServiceProvider()
{
return Create(typeof(ExtensionStore).GetTypeInfo().Assembly.SingleItemAsEnumerable());
}
public static ExtensionServiceProvider Create(IEnumerable<Assembly> assemblies)
{
Validate.IsNotNull(nameof(assemblies), assemblies);
return new ExtensionServiceProvider(conventions => new ContainerConfiguration().WithAssemblies(assemblies, conventions));
}
public static ExtensionServiceProvider Create(IEnumerable<Type> types)
{
Validate.IsNotNull(nameof(types), types);
return new ExtensionServiceProvider(conventions => new ContainerConfiguration().WithParts(types, conventions));
}
protected override IEnumerable<T> GetServicesImpl<T>()
{
EnsureExtensionStoreRegistered<T>();
return base.GetServicesImpl<T>();
}
private void EnsureExtensionStoreRegistered<T>()
{
if (!services.ContainsKey(typeof(T)))
{
ExtensionStore store = new ExtensionStore(typeof(T), config);
base.Register<T>(() => store.GetExports<T>());
}
}
}
/// <summary>
/// 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.
/// </summary>
public class ExtensionStore
{
private CompositionHost host;
private IList exports;
private Type contractType;
/// <summary>
/// Initializes the store with a type to lookup exports of, and a function that configures the
/// lookup parameters.
/// </summary>
/// <param name="contractType">Type to use as a base for all extensions being looked up</param>
/// <param name="configure">Function that returns the configuration to be used</param>
public ExtensionStore(Type contractType, Func<ConventionBuilder, ContainerConfiguration> configure)
{
Validate.IsNotNull(nameof(contractType), contractType);
Validate.IsNotNull(nameof(configure), configure);
this.contractType = contractType;
ConventionBuilder builder = GetExportBuilder();
ContainerConfiguration config = configure(builder);
host = config.CreateContainer();
}
/// <summary>
/// Loads extensions from the current assembly
/// </summary>
/// <returns>ExtensionStore</returns>
public static ExtensionStore CreateDefaultLoader<T>()
{
return CreateAssemblyStore<T>(typeof(ExtensionStore).GetTypeInfo().Assembly);
}
public static ExtensionStore CreateAssemblyStore<T>(Assembly assembly)
{
Validate.IsNotNull(nameof(assembly), assembly);
return new ExtensionStore(typeof(T), (conventions) =>
new ContainerConfiguration().WithAssembly(assembly, conventions));
}
public static ExtensionStore CreateStoreForCurrentDirectory<T>()
{
string assemblyPath = typeof(ExtensionStore).GetTypeInfo().Assembly.Location;
string directory = Path.GetDirectoryName(assemblyPath);
return new ExtensionStore(typeof(T), (conventions) =>
new ContainerConfiguration().WithAssembliesInPath(directory, conventions));
}
public IEnumerable<T> GetExports<T>()
{
if (exports == null)
{
exports = host.GetExports(contractType).ToList();
}
return exports.Cast<T>();
}
private ConventionBuilder GetExportBuilder()
{
// Define exports as matching a parent type, export as that parent type
var builder = new ConventionBuilder();
builder.ForTypesDerivedFrom(contractType).Export(exportConventionBuilder => exportConventionBuilder.AsContractType(contractType));
return builder;
}
}
public static class ContainerConfigurationExtensions
{
public static ContainerConfiguration WithAssembliesInPath(this ContainerConfiguration configuration, string path, SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
return WithAssembliesInPath(configuration, path, null, searchOption);
}
public static ContainerConfiguration WithAssembliesInPath(this ContainerConfiguration configuration, string path, AttributedModelProvider conventions, SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
AssemblyLoadContext context = new AssemblyLoader(path);
var assemblyNames = Directory
.GetFiles(path, "*.dll", searchOption)
.Select(AssemblyLoadContext.GetAssemblyName);
var assemblies = assemblyNames
.Select(context.LoadFromAssemblyName)
.ToList();
configuration = configuration.WithAssemblies(assemblies, conventions);
return configuration;
}
}
public class AssemblyLoader : AssemblyLoadContext
{
private string folderPath;
public AssemblyLoader(string folderPath)
{
this.folderPath = folderPath;
}
protected override Assembly Load(AssemblyName assemblyName)
{
var deps = DependencyContext.Default;
var res = deps.CompileLibraries.Where(d => d.Name.Equals(assemblyName.Name)).ToList();
if (res.Count > 0)
{
return Assembly.Load(new AssemblyName(res.First().Name));
}
else
{
var apiApplicationFileInfo = new FileInfo($"{folderPath}{Path.DirectorySeparatorChar}{assemblyName.Name}.dll");
if (File.Exists(apiApplicationFileInfo.FullName))
{
var asl = new AssemblyLoader(apiApplicationFileInfo.DirectoryName);
return asl.LoadFromAssemblyPath(apiApplicationFileInfo.FullName);
}
}
return Assembly.Load(assemblyName);
}
}
}

View File

@@ -0,0 +1,22 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.Extensibility
{
/// <summary>
/// A Service that expects to lookup other services. Using this interface on an exported service
/// will ensure the <see cref="SetServiceProvider(IMultiServiceProvider)"/> method is called during
/// service initialization
/// </summary>
public interface IComposableService
{
/// <summary>
/// Supports settings the service provider being used to initialize the service.
/// This is useful to look up other services and use them in your own service.
/// </summary>
void SetServiceProvider(IMultiServiceProvider provider);
}
}

View File

@@ -0,0 +1,18 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Collections.Generic;
namespace Microsoft.SqlTools.ServiceLayer.Extensibility
{
internal static class IEnumerableExt
{
public static IEnumerable<T> SingleItemAsEnumerable<T>(this T item)
{
yield return item;
}
}
}

View File

@@ -0,0 +1,104 @@
//
// 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.Linq;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.Extensibility
{
public interface IMultiServiceProvider
{
/// <summary>
/// Gets a service of a specific type. It is expected that only 1 instance of this type will be
/// available
/// </summary>
/// <typeparam name="T">Type of service to be found</typeparam>
/// <returns>Instance of T or null if not found</returns>
/// <exception cref="InvalidOperationException">The input sequence contains more than one element.-or-The input sequence is empty.</exception>
T GetService<T>();
/// <summary>
/// Gets a service of a specific type. The first service matching the specified filter will be returned
/// available
/// </summary>
/// <typeparam name="T">Type of service to be found</typeparam>
/// <param name="filter">Filter to use in </param>
/// <returns>Instance of T or null if not found</returns>
/// <exception cref="InvalidOperationException">The input sequence contains more than one element.-or-The input sequence is empty.</exception>
T GetService<T>(Predicate<T> filter);
/// <summary>
/// Gets multiple services of a given type
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns>An enumerable of matching services</returns>
IEnumerable<T> GetServices<T>();
/// <summary>
/// Gets multiple services of a given type, where they match a filter
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="filter"></param>
/// <returns></returns>
IEnumerable<T> GetServices<T>(Predicate<T> filter);
}
public abstract class ServiceProviderBase : IMultiServiceProvider
{
public T GetService<T>()
{
return GetServices<T>().SingleOrDefault();
}
public T GetService<T>(Predicate<T> filter)
{
Validate.IsNotNull(nameof(filter), filter);
return GetServices<T>().Where(t => filter(t)).SingleOrDefault();
}
public IEnumerable<T> GetServices<T>(Predicate<T> filter)
{
Validate.IsNotNull(nameof(filter), filter);
return GetServices<T>().Where(t => filter(t));
}
public virtual IEnumerable<T> GetServices<T>()
{
var services = GetServicesImpl<T>();
if (services == null)
{
return Enumerable.Empty<T>();
}
return services.Select(t =>
{
InitComposableService(t);
return t;
});
}
private void InitComposableService<T>(T t)
{
IComposableService c = t as IComposableService;
if (c != null)
{
c.SetServiceProvider(this);
}
}
/// <summary>
/// Gets all services using the build in implementation
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
protected abstract IEnumerable<T> GetServicesImpl<T>();
}
}

View File

@@ -0,0 +1,110 @@
//
// 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;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.Extensibility
{
/// <summary>
/// A service provider implementation that allows registering of specific services
/// </summary>
public class RegisteredServiceProvider : ServiceProviderBase
{
public delegate IEnumerable ServiceLookup();
protected Dictionary<Type, ServiceLookup> services = new Dictionary<Type, ServiceLookup>();
/// <summary>
/// Registers a singular service to be returned during lookup
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns>this provider, to simplify fluent declarations</returns>
/// <exception cref="ArgumentNullException">If service is null</exception>
/// <exception cref="InvalidOperationException">If an existing service is already registered</exception>
public RegisteredServiceProvider RegisterSingleService<T>(T service)
{
Validate.IsNotNull(nameof(service), service);
ThrowIfAlreadyRegistered<T>();
services.Add(typeof(T), () => service.SingleItemAsEnumerable());
return this;
}
/// <summary>
/// Registers a singular service to be returned during lookup
/// </summary>
/// <param name="type">
/// Type or interface this service should be registed as. Any <see cref="IMultiServiceProvider.GetServices{T}"/> request
/// for that type will return this service
/// </param>
/// <param name="service">service object to be added</param>
/// <returns>this provider, to simplify fluent declarations</returns>
/// <exception cref="ArgumentNullException">If service is null</exception>
/// <exception cref="InvalidOperationException">If an existing service is already registered</exception>
public RegisteredServiceProvider RegisterSingleService(Type type, object service)
{
Validate.IsNotNull(nameof(type), type);
Validate.IsNotNull(nameof(service), service);
ThrowIfAlreadyRegistered(type);
ThrowIfIncompatible(type, service);
services.Add(type, () => service.SingleItemAsEnumerable());
return this;
}
/// <summary>
/// Registers a function that can look up multiple services
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns>this provider, to simplify fluent declarations</returns>
/// <exception cref="ArgumentNullException">If <paramref name="serviceLookup"/> is null</exception>
/// <exception cref="InvalidOperationException">If an existing service is already registered</exception>
public RegisteredServiceProvider Register<T>(Func<IEnumerable<T>> serviceLookup)
{
Validate.IsNotNull(nameof(serviceLookup), serviceLookup);
ThrowIfAlreadyRegistered<T>();
services.Add(typeof(T), () => serviceLookup());
return this;
}
private void ThrowIfAlreadyRegistered<T>()
{
ThrowIfAlreadyRegistered(typeof(T));
}
private void ThrowIfAlreadyRegistered(Type type)
{
if (services.ContainsKey(type))
{
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.ServiceAlreadyRegistered, type.Name));
}
}
private void ThrowIfIncompatible(Type type, object service)
{
if (!type.IsInstanceOfType(service))
{
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.ServiceNotOfExpectedType, service.GetType().Name, type.Name));
}
}
protected override IEnumerable<T> GetServicesImpl<T>()
{
ServiceLookup serviceLookup;
if (services.TryGetValue(typeof(T), out serviceLookup))
{
return serviceLookup().Cast<T>();
}
return Enumerable.Empty<T>();
}
}
}