Addressing PR 14 feedback

This commit is contained in:
Mitchell Sternke
2016-08-09 10:17:29 -07:00
parent 5c03ba336d
commit f3231fba56
7 changed files with 406 additions and 393 deletions

View File

@@ -9,7 +9,6 @@
<!-- Add the SSMS repo for private requirements --> <!-- Add the SSMS repo for private requirements -->
<add key="SQLDS - SSMS" value="http://SQLISNuget/DS-SSMS/nuget/" /> <add key="SQLDS - SSMS" value="http://SQLISNuget/DS-SSMS/nuget/" />
<add key="CrossPlat" value="W:/" />
</packageSources> </packageSources>
</configuration> </configuration>

View File

@@ -38,17 +38,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
public ConnectionDetails ConnectionDetails { get; private set; } public ConnectionDetails ConnectionDetails { get; private set; }
public DbConnection SqlConnection { get; private set; } public DbConnection SqlConnection { get; set; }
public void OpenConnection()
{
// build the connection string from the input parameters
string connectionString = ConnectionService.BuildConnectionString(ConnectionDetails);
// create a sql connection instance
SqlConnection = Factory.CreateSqlConnection(connectionString);
SqlConnection.Open();
}
} }
/// <summary> /// <summary>
@@ -170,7 +160,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
var response = new ConnectResponse(); var response = new ConnectResponse();
try try
{ {
connectionInfo.OpenConnection(); // build the connection string from the input parameters
string connectionString = ConnectionService.BuildConnectionString(connectionInfo.ConnectionDetails);
// create a sql connection instance
connectionInfo.SqlConnection = connectionInfo.Factory.CreateSqlConnection(connectionString);
connectionInfo.SqlConnection.Open();
} }
catch(Exception ex) catch(Exception ex)
{ {

View File

@@ -27,31 +27,19 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
} }
/// <summary> /// <summary>
/// Parameters for the Disconnect Request. /// Message format for the connection result response
/// </summary> /// </summary>
public class DisconnectParams public class ConnectResponse
{ {
/// <summary> /// <summary>
/// A URI identifying the owner of the connection. This will most commonly be a file in the workspace /// A GUID representing a unique connection ID
/// or a virtual file representing an object in a database.
/// </summary> /// </summary>
public string OwnerUri { get; set; } public string ConnectionId { get; set; }
}
/// <summary>
/// Parameters for the ConnectionChanged Notification.
/// </summary>
public class ConnectionChangedParams
{
/// <summary> /// <summary>
/// A URI identifying the owner of the connection. This will most commonly be a file in the workspace /// Gets or sets any connection error messages
/// or a virtual file representing an object in a database.
/// </summary> /// </summary>
public string OwnerUri { get; set; } public string Messages { get; set; }
/// <summary>
/// Contains the high-level properties about the connection, for display to the user.
/// </summary>
public ConnectionSummary Connection { get; set; }
} }
/// <summary> /// <summary>
@@ -74,6 +62,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
/// </summary> /// </summary>
public string UserName { get; set; } public string UserName { get; set; }
} }
/// <summary> /// <summary>
/// Message format for the initial connection request /// Message format for the initial connection request
/// </summary> /// </summary>
@@ -88,22 +77,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
// TODO Handle full set of properties // TODO Handle full set of properties
} }
/// <summary>
/// Message format for the connection result response
/// </summary>
public class ConnectResponse
{
/// <summary>
/// A GUID representing a unique connection ID
/// </summary>
public string ConnectionId { get; set; }
/// <summary>
/// Gets or sets any connection error messages
/// </summary>
public string Messages { get; set; }
}
/// <summary> /// <summary>
/// Connect request mapping entry /// Connect request mapping entry
/// </summary> /// </summary>
@@ -113,25 +86,4 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
RequestType<ConnectParams, ConnectResponse> Type = RequestType<ConnectParams, ConnectResponse> Type =
RequestType<ConnectParams, ConnectResponse>.Create("connection/connect"); RequestType<ConnectParams, ConnectResponse>.Create("connection/connect");
} }
/// <summary>
/// Disconnect request mapping entry
/// </summary>
public class DisconnectRequest
{
public static readonly
RequestType<DisconnectParams, bool> Type =
RequestType<DisconnectParams, bool>.Create("connection/disconnect");
}
/// <summary>
/// ConnectionChanged notification mapping entry
/// </summary>
public class ConnectionChangedNotification
{
public static readonly
EventType<ConnectionChangedParams> Type =
EventType<ConnectionChangedParams>.Create("connection/connectionchanged");
}
} }

View File

@@ -0,0 +1,36 @@
//
// 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.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
{
/// <summary>
/// Parameters for the ConnectionChanged Notification.
/// </summary>
public class ConnectionChangedParams
{
/// <summary>
/// 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.
/// </summary>
public string OwnerUri { get; set; }
/// <summary>
/// Contains the high-level properties about the connection, for display to the user.
/// </summary>
public ConnectionSummary Connection { get; set; }
}
/// <summary>
/// ConnectionChanged notification mapping entry
/// </summary>
public class ConnectionChangedNotification
{
public static readonly
EventType<ConnectionChangedParams> Type =
EventType<ConnectionChangedParams>.Create("connection/connectionchanged");
}
}

View File

@@ -0,0 +1,31 @@
//
// 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.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
{
/// <summary>
/// Parameters for the Disconnect Request.
/// </summary>
public class DisconnectParams
{
/// <summary>
/// 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.
/// </summary>
public string OwnerUri { get; set; }
}
/// <summary>
/// Disconnect request mapping entry
/// </summary>
public class DisconnectRequest
{
public static readonly
RequestType<DisconnectParams, bool> Type =
RequestType<DisconnectParams, bool>.Create("connection/disconnect");
}
}

View File

@@ -1,325 +1,325 @@
// //
// 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.
// //
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data; using System.Data;
using System.Data.Common; using System.Data.Common;
using System.Threading.Tasks; using System.Threading.Tasks;
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.Contracts; using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{ {
internal class IntellisenseCache internal class IntellisenseCache
{ {
// connection used to query for intellisense info // connection used to query for intellisense info
private DbConnection connection; private DbConnection connection;
// number of documents (URI's) that are using the cache for the same database // number of documents (URI's) that are using the cache for the same database
// the autocomplete service uses this to remove unreferenced caches // the autocomplete service uses this to remove unreferenced caches
public int ReferenceCount { get; set; } public int ReferenceCount { get; set; }
public IntellisenseCache(ISqlConnectionFactory connectionFactory, ConnectionDetails connectionDetails) public IntellisenseCache(ISqlConnectionFactory connectionFactory, ConnectionDetails connectionDetails)
{ {
ReferenceCount = 0; ReferenceCount = 0;
DatabaseInfo = CopySummary(connectionDetails); DatabaseInfo = CopySummary(connectionDetails);
// TODO error handling on this. Intellisense should catch or else the service should handle // TODO error handling on this. Intellisense should catch or else the service should handle
connection = connectionFactory.CreateSqlConnection(ConnectionService.BuildConnectionString(connectionDetails)); connection = connectionFactory.CreateSqlConnection(ConnectionService.BuildConnectionString(connectionDetails));
connection.Open(); connection.Open();
} }
/// <summary> /// <summary>
/// Used to identify a database for which this cache is used /// Used to identify a database for which this cache is used
/// </summary> /// </summary>
public ConnectionSummary DatabaseInfo public ConnectionSummary DatabaseInfo
{ {
get; get;
private set; private set;
} }
/// <summary> /// <summary>
/// Gets the current autocomplete candidate list /// Gets the current autocomplete candidate list
/// </summary> /// </summary>
public IEnumerable<string> AutoCompleteList { get; private set; } public IEnumerable<string> AutoCompleteList { get; private set; }
public async Task UpdateCache() public async Task UpdateCache()
{ {
DbCommand command = connection.CreateCommand(); DbCommand command = connection.CreateCommand();
command.CommandText = "SELECT name FROM sys.tables"; command.CommandText = "SELECT name FROM sys.tables";
command.CommandTimeout = 15; command.CommandTimeout = 15;
command.CommandType = CommandType.Text; command.CommandType = CommandType.Text;
var reader = await command.ExecuteReaderAsync(); var reader = await command.ExecuteReaderAsync();
List<string> results = new List<string>(); List<string> results = new List<string>();
while (await reader.ReadAsync()) while (await reader.ReadAsync())
{ {
results.Add(reader[0].ToString()); results.Add(reader[0].ToString());
} }
AutoCompleteList = results; AutoCompleteList = results;
await Task.FromResult(0); await Task.FromResult(0);
} }
public List<CompletionItem> GetAutoCompleteItems(TextDocumentPosition textDocumentPosition) public List<CompletionItem> GetAutoCompleteItems(TextDocumentPosition textDocumentPosition)
{ {
List<CompletionItem> completions = new List<CompletionItem>(); List<CompletionItem> completions = new List<CompletionItem>();
int i = 0; int i = 0;
// Take a reference to the list at a point in time in case we update and replace the list // Take a reference to the list at a point in time in case we update and replace the list
var suggestions = AutoCompleteList; var suggestions = AutoCompleteList;
// the completion list will be null is user not connected to server // the completion list will be null is user not connected to server
if (this.AutoCompleteList != null) if (this.AutoCompleteList != null)
{ {
foreach (var autoCompleteItem in suggestions) foreach (var autoCompleteItem in suggestions)
{ {
// convert the completion item candidates into CompletionItems // convert the completion item candidates into CompletionItems
completions.Add(new CompletionItem() completions.Add(new CompletionItem()
{ {
Label = autoCompleteItem, Label = autoCompleteItem,
Kind = CompletionItemKind.Keyword, Kind = CompletionItemKind.Keyword,
Detail = autoCompleteItem + " details", Detail = autoCompleteItem + " details",
Documentation = autoCompleteItem + " documentation", Documentation = autoCompleteItem + " documentation",
TextEdit = new TextEdit TextEdit = new TextEdit
{ {
NewText = autoCompleteItem, NewText = autoCompleteItem,
Range = new Range Range = new Range
{ {
Start = new Position Start = new Position
{ {
Line = textDocumentPosition.Position.Line, Line = textDocumentPosition.Position.Line,
Character = textDocumentPosition.Position.Character Character = textDocumentPosition.Position.Character
}, },
End = new Position End = new Position
{ {
Line = textDocumentPosition.Position.Line, Line = textDocumentPosition.Position.Line,
Character = textDocumentPosition.Position.Character + 5 Character = textDocumentPosition.Position.Character + 5
} }
} }
} }
}); });
// only show 50 items // only show 50 items
if (++i == 50) if (++i == 50)
{ {
break; break;
} }
} }
} }
return completions; return completions;
} }
private static ConnectionSummary CopySummary(ConnectionSummary summary) private static ConnectionSummary CopySummary(ConnectionSummary summary)
{ {
return new ConnectionSummary() return new ConnectionSummary()
{ {
ServerName = summary.ServerName, ServerName = summary.ServerName,
DatabaseName = summary.DatabaseName, DatabaseName = summary.DatabaseName,
UserName = summary.UserName UserName = summary.UserName
}; };
} }
} }
/// <summary> /// <summary>
/// Treats connections as the same if their server, db and usernames all match /// Treats connections as the same if their server, db and usernames all match
/// </summary> /// </summary>
public class ConnectionSummaryComparer : IEqualityComparer<ConnectionSummary> public class ConnectionSummaryComparer : IEqualityComparer<ConnectionSummary>
{ {
public bool Equals(ConnectionSummary x, ConnectionSummary y) public bool Equals(ConnectionSummary x, ConnectionSummary y)
{ {
if(x == y) { return true; } if(x == y) { return true; }
else if(x != null) else if(x != null)
{ {
if(y == null) { return false; } if(y == null) { return false; }
// Compare server, db, username. Note: server is case-insensitive in the driver // Compare server, db, username. Note: server is case-insensitive in the driver
return string.Compare(x.ServerName, y.ServerName, StringComparison.OrdinalIgnoreCase) == 0 return string.Compare(x.ServerName, y.ServerName, StringComparison.OrdinalIgnoreCase) == 0
&& string.Compare(x.DatabaseName, y.DatabaseName, StringComparison.Ordinal) == 0 && string.Compare(x.DatabaseName, y.DatabaseName, StringComparison.Ordinal) == 0
&& string.Compare(x.UserName, y.UserName, StringComparison.Ordinal) == 0; && string.Compare(x.UserName, y.UserName, StringComparison.Ordinal) == 0;
} }
return false; return false;
} }
public int GetHashCode(ConnectionSummary obj) public int GetHashCode(ConnectionSummary obj)
{ {
int hashcode = 31; int hashcode = 31;
if(obj != null) if(obj != null)
{ {
if(obj.ServerName != null) if(obj.ServerName != null)
{ {
hashcode ^= obj.ServerName.GetHashCode(); hashcode ^= obj.ServerName.GetHashCode();
} }
if (obj.DatabaseName != null) if (obj.DatabaseName != null)
{ {
hashcode ^= obj.DatabaseName.GetHashCode(); hashcode ^= obj.DatabaseName.GetHashCode();
} }
if (obj.UserName != null) if (obj.UserName != null)
{ {
hashcode ^= obj.UserName.GetHashCode(); hashcode ^= obj.UserName.GetHashCode();
} }
} }
return hashcode; return hashcode;
} }
} }
/// <summary> /// <summary>
/// Main class for Autocomplete functionality /// Main class for Autocomplete functionality
/// </summary> /// </summary>
public class AutoCompleteService public class AutoCompleteService
{ {
#region Singleton Instance Implementation #region Singleton Instance Implementation
/// <summary> /// <summary>
/// Singleton service instance /// Singleton service instance
/// </summary> /// </summary>
private static Lazy<AutoCompleteService> instance private static Lazy<AutoCompleteService> instance
= new Lazy<AutoCompleteService>(() => new AutoCompleteService()); = new Lazy<AutoCompleteService>(() => new AutoCompleteService());
/// <summary> /// <summary>
/// Gets the singleton service instance /// Gets the singleton service instance
/// </summary> /// </summary>
public static AutoCompleteService Instance public static AutoCompleteService Instance
{ {
get get
{ {
return instance.Value; return instance.Value;
} }
} }
/// <summary> /// <summary>
/// Default, parameterless constructor. /// Default, parameterless constructor.
/// TODO: Figure out how to make this truely singleton even with dependency injection for tests /// TODO: Figure out how to make this truely singleton even with dependency injection for tests
/// </summary> /// </summary>
public AutoCompleteService() public AutoCompleteService()
{ {
} }
#endregion #endregion
// Dictionary of unique intellisense caches for each Connection // Dictionary of unique intellisense caches for each Connection
private Dictionary<ConnectionSummary, IntellisenseCache> caches = private Dictionary<ConnectionSummary, IntellisenseCache> caches =
new Dictionary<ConnectionSummary, IntellisenseCache>(new ConnectionSummaryComparer()); new Dictionary<ConnectionSummary, IntellisenseCache>(new ConnectionSummaryComparer());
private Object cachesLock = new Object(); // Used when we insert/remove something from the cache dictionary private Object cachesLock = new Object(); // Used when we insert/remove something from the cache dictionary
private ISqlConnectionFactory factory; private ISqlConnectionFactory factory;
private Object factoryLock = new Object(); private Object factoryLock = new Object();
/// <summary> /// <summary>
/// Internal for testing purposes only /// Internal for testing purposes only
/// </summary> /// </summary>
internal ISqlConnectionFactory ConnectionFactory internal ISqlConnectionFactory ConnectionFactory
{ {
get get
{ {
lock(factoryLock) lock(factoryLock)
{ {
if(factory == null) if(factory == null)
{ {
factory = new SqlConnectionFactory(); factory = new SqlConnectionFactory();
} }
} }
return factory; return factory;
} }
set set
{ {
lock(factoryLock) lock(factoryLock)
{ {
factory = value; factory = value;
} }
} }
} }
public void InitializeService(ServiceHost serviceHost) public void InitializeService(ServiceHost serviceHost)
{ {
// Register a callback for when a connection is created // Register a callback for when a connection is created
ConnectionService.Instance.RegisterOnConnectionTask(UpdateAutoCompleteCache); ConnectionService.Instance.RegisterOnConnectionTask(UpdateAutoCompleteCache);
// Register a callback for when a connection is closed // Register a callback for when a connection is closed
ConnectionService.Instance.RegisterOnDisconnectTask(RemoveAutoCompleteCacheUriReference); ConnectionService.Instance.RegisterOnDisconnectTask(RemoveAutoCompleteCacheUriReference);
} }
private async Task UpdateAutoCompleteCache(ConnectionInfo connectionInfo) private async Task UpdateAutoCompleteCache(ConnectionInfo connectionInfo)
{ {
if (connectionInfo != null) if (connectionInfo != null)
{ {
await UpdateAutoCompleteCache(connectionInfo.ConnectionDetails); await UpdateAutoCompleteCache(connectionInfo.ConnectionDetails);
} }
} }
/// <summary> /// <summary>
/// Remove a reference to an autocomplete cache from a URI. If /// Remove a reference to an autocomplete cache from a URI. If
/// it is the last URI connected to a particular connection, /// it is the last URI connected to a particular connection,
/// then remove the cache. /// then remove the cache.
/// </summary> /// </summary>
public async Task RemoveAutoCompleteCacheUriReference(ConnectionSummary summary) public async Task RemoveAutoCompleteCacheUriReference(ConnectionSummary summary)
{ {
await Task.Run( () => await Task.Run( () =>
{ {
lock(cachesLock) lock(cachesLock)
{ {
IntellisenseCache cache; IntellisenseCache cache;
if( caches.TryGetValue(summary, out cache) ) if( caches.TryGetValue(summary, out cache) )
{ {
cache.ReferenceCount--; cache.ReferenceCount--;
// Remove unused caches // Remove unused caches
if( cache.ReferenceCount == 0 ) if( cache.ReferenceCount == 0 )
{ {
caches.Remove(summary); caches.Remove(summary);
} }
} }
} }
}); });
} }
/// <summary> /// <summary>
/// Update the cached autocomplete candidate list when the user connects to a database /// Update the cached autocomplete candidate list when the user connects to a database
/// </summary> /// </summary>
/// <param name="details"></param> /// <param name="details"></param>
public async Task UpdateAutoCompleteCache(ConnectionDetails details) public async Task UpdateAutoCompleteCache(ConnectionDetails details)
{ {
IntellisenseCache cache; IntellisenseCache cache;
lock(cachesLock) lock(cachesLock)
{ {
if(!caches.TryGetValue(details, out cache)) if(!caches.TryGetValue(details, out cache))
{ {
cache = new IntellisenseCache(ConnectionFactory, details); cache = new IntellisenseCache(ConnectionFactory, details);
caches[cache.DatabaseInfo] = cache; caches[cache.DatabaseInfo] = cache;
} }
cache.ReferenceCount++; cache.ReferenceCount++;
} }
await cache.UpdateCache(); await cache.UpdateCache();
} }
/// <summary> /// <summary>
/// Return the completion item list for the current text position. /// Return the completion item list for the current text position.
/// 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(TextDocumentPosition textDocumentPosition) public CompletionItem[] GetCompletionItems(TextDocumentPosition textDocumentPosition)
{ {
// Try to find a cache for the document's backing connection (if available) // Try to find a cache for the document's backing connection (if available)
// If we have a connection but no cache, we don't care - assuming the OnConnect and OnDisconnect listeners // If we have a connection but no cache, we don't care - assuming the OnConnect and OnDisconnect listeners
// behave well, there should be a cache for any actively connected document. This also helps skip documents // behave well, there should be a cache for any actively connected document. This also helps skip documents
// that are not backed by a SQL connection // that are not backed by a SQL connection
ConnectionInfo info; ConnectionInfo info;
IntellisenseCache cache; IntellisenseCache cache;
if (ConnectionService.Instance.TryFindConnection(textDocumentPosition.Uri, out info) if (ConnectionService.Instance.TryFindConnection(textDocumentPosition.Uri, out info)
&& caches.TryGetValue((ConnectionSummary)info.ConnectionDetails, out cache)) && caches.TryGetValue((ConnectionSummary)info.ConnectionDetails, out cache))
{ {
return cache.GetAutoCompleteItems(textDocumentPosition).ToArray(); return cache.GetAutoCompleteItems(textDocumentPosition).ToArray();
} }
return new CompletionItem[0]; return new CompletionItem[0];
} }
} }
} }