Merge dev to master for 0.0.7 release (#86)

* Disable failing test while investigating

* Made connection errors more user-friendly (#57)

* Bug/negativeOneRowsAffected strings file fix (#61)

* Adding changes to sr.strings files

* Fixing bug by changing valid filename check to fail on whitespace (#55)

Fixing a bug from the unit tests on OSX/Unix where attempting to create a file with a name that's all whitespace succeeds when it should fail. This was passing in Windows because File.Open throws an ArgumentException when an all whitespace name is provided. In Unix systems, File.Open does not throw, causing the test to fail.

Solution is to check for whitespace in the sanity check.

* Format Cell Values (#62)

* WIP for ability to localize cell values

* Changing how DateTimeOffsets are stored, getting unit tests going

* Reworking BufferFileStreamWriter to use dictionary approach

* Plumbing the DbCellValue type the rest of the way through

* Removing unused components to simplify contract

* Cleanup and making sure byte[] appears in parity with SSMS

* CR comments, small tweaks for optimizing LINQ

* Feature/batch line info (#56)

* inital pipe of line numbers and getting text from workspace services

* tests compile

* Fixed bug regarding tests using connections on mac

* updated tests

* fixed workspace service and fixed tests

* integrated feedback

* Remove deleted file with whitespace name

* Feature/autocomp options (#63)

* Enable IntelliSense settings

* Fix up some bugs in the IntelliSense settings.

* Code cleans for PR

* Fix a couple exceptions that are breaks query execute and intellisense.

* Add useLowerCase flag and settings tests

* Remove task.wait from test to avoid break on build machine. (#67)

This is to fix test breaks so I'll merge now.  Please let me know if there are comments on this commit.

* Do not use ReliableCommand in the query execution service (#66)

* Do not use ReliableCommand in the query execution service.

* Fixing the logic to remove InfoMessage handlers from ReliableSqlConnection

* Adding test to query UDT

* Bump SMO to 140.1.6 to pick up perf fixes (#69)

* Enable Quick Info hover tooltips (#65)

Pushing to include in tomorrow's partner release build.  Please send me any feedback and I'll address in the next Intellisense PR.

* Added grouping between system/user dbs when listing (#70)

* Feature/timestamp messages (#68)

* added support for timestamps

* fixed tests

* Moved message class to own file; added 'z' to end of date strings

* added default time constructor

* removed unnecessary z

* added time string format info in comment

* changed from utc time to using local time

* Feature/save selection (#64)

* Save selection

* Add tests

* Change filename in test

* Code cleanup

* Refactor handler

* Code cleanup

* Modify tests to have query selection

* Change variable declaration

* Bump SMO to 14.0.7 to pick Batchparser updates (#72)

...bumping versions.  No review needed.

* Lingering File Handles (#71)

Fixing a bug where in various situations, the files used for temporary storage of query results would be leftover. In particular, the following changes were made:
* When the dispose query request is submitted, the corresponding query is now disposed in addition from being removed from the list of active queries
* When a query is cancelled, it is disposed after it is cancelled
* If a query already exists for a given ownerURI, the existing query is disposed before creating a new query
* All queries are disposed when the query execution service is disposed (ie, at shutdown of the service)

A unit test to verify the action of the dispose method for a ResultSet was added.

* Ensuring queries are disposed

Adding logic to dispose any queries when:
* URI that already has a query executes another query
* A request to dispose a query is submitted
* A request to cancel a query is submitted

* Small tweaks for cleanup of query execution service

* Add IntelliSense binding queue (#73)

* Initial code for binding queue

* Fix-up some of the timeout wait code

* Add some initial test code

* Add missing test file

* Update the binding queue tests

* Add more test coverage and refactor a bit.  Disable reliabile connection until we can fix it..it's holding an open data reader connection.

* A few more test updates

* Initial integrate queue with language service.

* Hook up the connected binding queue into al binding calls.

* Cleanup comments and remove dead code

* More missing comments

* Fix build break.  Reenable ReliabileConnection.

* Revert all changes to SqlConnectionFactory

* Resolve merge conflicts

* Cleanup some more of the timeouts and sync code

* Address code review feedback

* Address more code review feedback

* Feature/connect cancel (#74)

* Implemented connection cancellation

* Made connect requests return immediately and created a separate connection complete notification

* Fix spelling

* Fix sorting

* Add separate lock for cancellation source map

* Fix an issue with queue deadlocks causing test failures (#77)

* Fixed issue where connecting could take very long and cancellation would not work (#78)

* Fixed issue where connecting could take very long and cancellation would not work

* Addressing feedback

* Remove warning suppression

* Adding unlimited timeout for query execution (#76)

Adding explicitly setting the timeout for command execution to unlimited. We can change this to be user configurable at a later time

* Support 'for XML and for JSON' queries (#75)

* Set isXMl and isJson for 'for xml/json' resultSets

* Change string comparison

* Modify if-else

* VSTS 8499785. Close SqlToolsService after VS Code exits. (#80)

VSTS 8499785. Close SqlToolsService after VS Code exits.

* Remove extra level of tasks in binding queue (#79)

* Remove extra layer of tasks in binding queue

* Change order of assigning result to avoid race condition

* Add timeout log for the metadata lock event

* Fix test cases
This commit is contained in:
Karl Burtram
2016-10-08 19:18:26 +00:00
committed by GitHub
parent 7d6aabb618
commit 52b5f222db
65 changed files with 3739 additions and 1800 deletions

View File

@@ -21,6 +21,10 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// </summary>
public static class AutoCompleteHelper
{
private const int PrepopulateBindTimeout = 60000;
private static WorkspaceService<SqlToolsSettings> workspaceServiceInstance;
private static readonly string[] DefaultCompletionText = new string[]
{
"absolute",
@@ -421,16 +425,44 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
"zone"
};
/// <summary>
/// Gets or sets the current workspace service instance
/// Setter for internal testing purposes only
/// </summary>
internal static WorkspaceService<SqlToolsSettings> WorkspaceServiceInstance
{
get
{
if (AutoCompleteHelper.workspaceServiceInstance == null)
{
AutoCompleteHelper.workspaceServiceInstance = WorkspaceService<SqlToolsSettings>.Instance;
}
return AutoCompleteHelper.workspaceServiceInstance;
}
set
{
AutoCompleteHelper.workspaceServiceInstance = value;
}
}
/// <summary>
/// Get the default completion list from hard-coded list
/// </summary>
/// <param name="row"></param>
/// <param name="startColumn"></param>
/// <param name="endColumn"></param>
/// <param name="useLowerCase"></param>
internal static CompletionItem[] GetDefaultCompletionItems(
int row,
int startColumn,
int endColumn)
int endColumn,
bool useLowerCase)
{
var completionItems = new CompletionItem[DefaultCompletionText.Length];
for (int i = 0; i < DefaultCompletionText.Length; ++i)
{
completionItems[i] = CreateDefaultCompletionItem(
DefaultCompletionText[i].ToUpper(),
useLowerCase ? DefaultCompletionText[i].ToLower() : DefaultCompletionText[i].ToUpper(),
row,
startColumn,
endColumn);
@@ -438,6 +470,13 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
return completionItems;
}
/// <summary>
/// Create a completion item from the default item text
/// </summary>
/// <param name="label"></param>
/// <param name="row"></param>
/// <param name="startColumn"></param>
/// <param name="endColumn"></param>
private static CompletionItem CreateDefaultCompletionItem(
string label,
int row,
@@ -523,11 +562,14 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// </summary>
/// <param name="info"></param>
/// <param name="scriptInfo"></param>
internal static void PrepopulateCommonMetadata(ConnectionInfo info, ScriptParseInfo scriptInfo)
internal static void PrepopulateCommonMetadata(
ConnectionInfo info,
ScriptParseInfo scriptInfo,
ConnectedBindingQueue bindingQueue)
{
if (scriptInfo.IsConnected)
{
var scriptFile = WorkspaceService<SqlToolsSettings>.Instance.Workspace.GetFile(info.OwnerUri);
var scriptFile = AutoCompleteHelper.WorkspaceServiceInstance.Workspace.GetFile(info.OwnerUri);
LanguageService.Instance.ParseAndBind(scriptFile, info);
if (scriptInfo.BuildingMetadataEvent.WaitOne(LanguageService.OnConnectionWaitTimeout))
@@ -536,44 +578,53 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
scriptInfo.BuildingMetadataEvent.Reset();
// parse a simple statement that returns common metadata
ParseResult parseResult = Parser.Parse(
"select ",
scriptInfo.ParseOptions);
QueueItem queueItem = bindingQueue.QueueBindingOperation(
key: scriptInfo.ConnectionKey,
bindingTimeout: AutoCompleteHelper.PrepopulateBindTimeout,
bindOperation: (bindingContext, cancelToken) =>
{
// parse a simple statement that returns common metadata
ParseResult parseResult = Parser.Parse(
"select ",
bindingContext.ParseOptions);
List<ParseResult> parseResults = new List<ParseResult>();
parseResults.Add(parseResult);
scriptInfo.Binder.Bind(
parseResults,
info.ConnectionDetails.DatabaseName,
BindMode.Batch);
List<ParseResult> parseResults = new List<ParseResult>();
parseResults.Add(parseResult);
bindingContext.Binder.Bind(
parseResults,
info.ConnectionDetails.DatabaseName,
BindMode.Batch);
// get the completion list from SQL Parser
var suggestions = Resolver.FindCompletions(
parseResult, 1, 8,
scriptInfo.MetadataDisplayInfoProvider);
// get the completion list from SQL Parser
var suggestions = Resolver.FindCompletions(
parseResult, 1, 8,
bindingContext.MetadataDisplayInfoProvider);
// this forces lazy evaluation of the suggestion metadata
AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 8, 8);
// this forces lazy evaluation of the suggestion metadata
AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 8, 8);
parseResult = Parser.Parse(
"exec ",
scriptInfo.ParseOptions);
parseResult = Parser.Parse(
"exec ",
bindingContext.ParseOptions);
parseResults = new List<ParseResult>();
parseResults.Add(parseResult);
scriptInfo.Binder.Bind(
parseResults,
info.ConnectionDetails.DatabaseName,
BindMode.Batch);
parseResults = new List<ParseResult>();
parseResults.Add(parseResult);
bindingContext.Binder.Bind(
parseResults,
info.ConnectionDetails.DatabaseName,
BindMode.Batch);
// get the completion list from SQL Parser
suggestions = Resolver.FindCompletions(
parseResult, 1, 6,
scriptInfo.MetadataDisplayInfoProvider);
// get the completion list from SQL Parser
suggestions = Resolver.FindCompletions(
parseResult, 1, 6,
bindingContext.MetadataDisplayInfoProvider);
// this forces lazy evaluation of the suggestion metadata
AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 6, 6);
// this forces lazy evaluation of the suggestion metadata
AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 6, 6);
return null;
});
queueItem.ItemProcessed.WaitOne();
}
catch
{
@@ -585,5 +636,53 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
}
}
/// <summary>
/// Converts a SQL Parser QuickInfo object into a VS Code Hover object
/// </summary>
/// <param name="quickInfo"></param>
/// <param name="row"></param>
/// <param name="startColumn"></param>
/// <param name="endColumn"></param>
internal static Hover ConvertQuickInfoToHover(
Babel.CodeObjectQuickInfo quickInfo,
int row,
int startColumn,
int endColumn)
{
// convert from the parser format to the VS Code wire format
var markedStrings = new MarkedString[1];
if (quickInfo != null)
{
markedStrings[0] = new MarkedString()
{
Language = "SQL",
Value = quickInfo.Text
};
return new Hover()
{
Contents = markedStrings,
Range = new Range
{
Start = new Position
{
Line = row,
Character = startColumn
},
End = new Position
{
Line = row,
Character = endColumn
}
}
};
}
else
{
return null;
}
}
}
}

View File

@@ -0,0 +1,259 @@
//
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
/// <summary>
/// Main class for the Binding Queue
/// </summary>
public class BindingQueue<T> where T : IBindingContext, new()
{
private CancellationTokenSource processQueueCancelToken = new CancellationTokenSource();
private ManualResetEvent itemQueuedEvent = new ManualResetEvent(initialState: false);
private object bindingQueueLock = new object();
private LinkedList<QueueItem> bindingQueue = new LinkedList<QueueItem>();
private object bindingContextLock = new object();
private Task queueProcessorTask;
/// <summary>
/// Map from context keys to binding context instances
/// Internal for testing purposes only
/// </summary>
internal Dictionary<string, IBindingContext> BindingContextMap { get; set; }
/// <summary>
/// Constructor for a binding queue instance
/// </summary>
public BindingQueue()
{
this.BindingContextMap = new Dictionary<string, IBindingContext>();
this.queueProcessorTask = StartQueueProcessor();
}
/// <summary>
/// Stops the binding queue by sending cancellation request
/// </summary>
/// <param name="timeout"></param>
public bool StopQueueProcessor(int timeout)
{
this.processQueueCancelToken.Cancel();
return this.queueProcessorTask.Wait(timeout);
}
/// <summary>
/// Queue a binding request item
/// </summary>
public QueueItem QueueBindingOperation(
string key,
Func<IBindingContext, CancellationToken, object> bindOperation,
Func<IBindingContext, object> timeoutOperation = null,
int? bindingTimeout = null)
{
// don't add null operations to the binding queue
if (bindOperation == null)
{
return null;
}
QueueItem queueItem = new QueueItem()
{
Key = key,
BindOperation = bindOperation,
TimeoutOperation = timeoutOperation,
BindingTimeout = bindingTimeout
};
lock (this.bindingQueueLock)
{
this.bindingQueue.AddLast(queueItem);
}
this.itemQueuedEvent.Set();
return queueItem;
}
/// <summary>
/// Gets or creates a binding context for the provided context key
/// </summary>
/// <param name="key"></param>
protected IBindingContext GetOrCreateBindingContext(string key)
{
// use a default binding context for disconnected requests
if (string.IsNullOrWhiteSpace(key))
{
key = "disconnected_binding_context";
}
lock (this.bindingContextLock)
{
if (!this.BindingContextMap.ContainsKey(key))
{
this.BindingContextMap.Add(key, new T());
}
return this.BindingContextMap[key];
}
}
private bool HasPendingQueueItems
{
get
{
lock (this.bindingQueueLock)
{
return this.bindingQueue.Count > 0;
}
}
}
/// <summary>
/// Gets the next pending queue item
/// </summary>
private QueueItem GetNextQueueItem()
{
lock (this.bindingQueueLock)
{
if (this.bindingQueue.Count == 0)
{
return null;
}
QueueItem queueItem = this.bindingQueue.First.Value;
this.bindingQueue.RemoveFirst();
return queueItem;
}
}
/// <summary>
/// Starts the queue processing thread
/// </summary>
private Task StartQueueProcessor()
{
return Task.Factory.StartNew(
ProcessQueue,
this.processQueueCancelToken.Token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
}
/// <summary>
/// The core queue processing method
/// </summary>
/// <param name="state"></param>
private void ProcessQueue()
{
CancellationToken token = this.processQueueCancelToken.Token;
WaitHandle[] waitHandles = new WaitHandle[2]
{
this.itemQueuedEvent,
token.WaitHandle
};
while (true)
{
// wait for with an item to be queued or the a cancellation request
WaitHandle.WaitAny(waitHandles);
if (token.IsCancellationRequested)
{
break;
}
try
{
// dispatch all pending queue items
while (this.HasPendingQueueItems)
{
QueueItem queueItem = GetNextQueueItem();
if (queueItem == null)
{
continue;
}
IBindingContext bindingContext = GetOrCreateBindingContext(queueItem.Key);
if (bindingContext == null)
{
queueItem.ItemProcessed.Set();
continue;
}
try
{
// prefer the queue item binding item, otherwise use the context default timeout
int bindTimeout = queueItem.BindingTimeout ?? bindingContext.BindingTimeout;
// handle the case a previous binding operation is still running
if (!bindingContext.BindingLocked.WaitOne(bindTimeout))
{
queueItem.Result = queueItem.TimeoutOperation(bindingContext);
queueItem.ItemProcessed.Set();
continue;
}
// execute the binding operation
object result = null;
CancellationTokenSource cancelToken = new CancellationTokenSource();
var bindTask = Task.Run(() =>
{
result = queueItem.BindOperation(
bindingContext,
cancelToken.Token);
});
// check if the binding tasks completed within the binding timeout
if (bindTask.Wait(bindTimeout))
{
queueItem.Result = result;
}
else
{
// if the task didn't complete then call the timeout callback
if (queueItem.TimeoutOperation != null)
{
cancelToken.Cancel();
queueItem.Result = queueItem.TimeoutOperation(bindingContext);
}
}
}
catch (Exception ex)
{
// catch and log any exceptions raised in the binding calls
// set item processed to avoid deadlocks
Logger.Write(LogLevel.Error, "Binding queue threw exception " + ex.ToString());
}
finally
{
bindingContext.BindingLocked.Set();
queueItem.ItemProcessed.Set();
}
// if a queue processing cancellation was requested then exit the loop
if (token.IsCancellationRequested)
{
break;
}
}
}
finally
{
// reset the item queued event since we've processed all the pending items
this.itemQueuedEvent.Reset();
}
}
}
}
}

View File

@@ -0,0 +1,208 @@
//
// 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.Threading;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.SmoMetadataProvider;
using Microsoft.SqlServer.Management.SqlParser.Binder;
using Microsoft.SqlServer.Management.SqlParser.Common;
using Microsoft.SqlServer.Management.SqlParser.MetadataProvider;
using Microsoft.SqlServer.Management.SqlParser.Parser;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
/// <summary>
/// Class for the binding context for connected sessions
/// </summary>
public class ConnectedBindingContext : IBindingContext
{
private ParseOptions parseOptions;
private ServerConnection serverConnection;
/// <summary>
/// Connected binding context constructor
/// </summary>
public ConnectedBindingContext()
{
this.BindingLocked = new ManualResetEvent(initialState: true);
this.BindingTimeout = ConnectedBindingQueue.DefaultBindingTimeout;
this.MetadataDisplayInfoProvider = new MetadataDisplayInfoProvider();
}
/// <summary>
/// Gets or sets a flag indicating if the binder is connected
/// </summary>
public bool IsConnected { get; set; }
/// <summary>
/// Gets or sets the binding server connection
/// </summary>
public ServerConnection ServerConnection
{
get
{
return this.serverConnection;
}
set
{
this.serverConnection = value;
// reset the parse options so the get recreated for the current connection
this.parseOptions = null;
}
}
/// <summary>
/// Gets or sets the metadata display info provider
/// </summary>
public MetadataDisplayInfoProvider MetadataDisplayInfoProvider { get; set; }
/// <summary>
/// Gets or sets the SMO metadata provider
/// </summary>
public SmoMetadataProvider SmoMetadataProvider { get; set; }
/// <summary>
/// Gets or sets the binder
/// </summary>
public IBinder Binder { get; set; }
/// <summary>
/// Gets or sets an event to signal if a binding operation is in progress
/// </summary>
public ManualResetEvent BindingLocked { get; set; }
/// <summary>
/// Gets or sets the binding operation timeout in milliseconds
/// </summary>
public int BindingTimeout { get; set; }
/// <summary>
/// Gets the Language Service ServerVersion
/// </summary>
public ServerVersion ServerVersion
{
get
{
return this.ServerConnection != null
? this.ServerConnection.ServerVersion
: null;
}
}
/// <summary>
/// Gets the current DataEngineType
/// </summary>
public DatabaseEngineType DatabaseEngineType
{
get
{
return this.ServerConnection != null
? this.ServerConnection.DatabaseEngineType
: DatabaseEngineType.Standalone;
}
}
/// <summary>
/// Gets the current connections TransactSqlVersion
/// </summary>
public TransactSqlVersion TransactSqlVersion
{
get
{
return this.IsConnected
? GetTransactSqlVersion(this.ServerVersion)
: TransactSqlVersion.Current;
}
}
/// <summary>
/// Gets the current DatabaseCompatibilityLevel
/// </summary>
public DatabaseCompatibilityLevel DatabaseCompatibilityLevel
{
get
{
return this.IsConnected
? GetDatabaseCompatibilityLevel(this.ServerVersion)
: DatabaseCompatibilityLevel.Current;
}
}
/// <summary>
/// Gets the current ParseOptions
/// </summary>
public ParseOptions ParseOptions
{
get
{
if (this.parseOptions == null)
{
this.parseOptions = new ParseOptions(
batchSeparator: LanguageService.DefaultBatchSeperator,
isQuotedIdentifierSet: true,
compatibilityLevel: DatabaseCompatibilityLevel,
transactSqlVersion: TransactSqlVersion);
}
return this.parseOptions;
}
}
/// <summary>
/// Gets the database compatibility level from a server version
/// </summary>
/// <param name="serverVersion"></param>
private static DatabaseCompatibilityLevel GetDatabaseCompatibilityLevel(ServerVersion serverVersion)
{
int versionMajor = Math.Max(serverVersion.Major, 8);
switch (versionMajor)
{
case 8:
return DatabaseCompatibilityLevel.Version80;
case 9:
return DatabaseCompatibilityLevel.Version90;
case 10:
return DatabaseCompatibilityLevel.Version100;
case 11:
return DatabaseCompatibilityLevel.Version110;
case 12:
return DatabaseCompatibilityLevel.Version120;
case 13:
return DatabaseCompatibilityLevel.Version130;
default:
return DatabaseCompatibilityLevel.Current;
}
}
/// <summary>
/// Gets the transaction sql version from a server version
/// </summary>
/// <param name="serverVersion"></param>
private static TransactSqlVersion GetTransactSqlVersion(ServerVersion serverVersion)
{
int versionMajor = Math.Max(serverVersion.Major, 9);
switch (versionMajor)
{
case 9:
case 10:
// In case of 10.0 we still use Version 10.5 as it is the closest available.
return TransactSqlVersion.Version105;
case 11:
return TransactSqlVersion.Version110;
case 12:
return TransactSqlVersion.Version120;
case 13:
return TransactSqlVersion.Version130;
default:
return TransactSqlVersion.Current;
}
}
}
}

View File

@@ -0,0 +1,109 @@
//
// 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.Data.SqlClient;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.SmoMetadataProvider;
using Microsoft.SqlServer.Management.SqlParser.Binder;
using Microsoft.SqlServer.Management.SqlParser.MetadataProvider;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
using Microsoft.SqlTools.ServiceLayer.SqlContext;
using Microsoft.SqlTools.ServiceLayer.Workspace;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
/// <summary>
/// ConnectedBindingQueue class for processing online binding requests
/// </summary>
public class ConnectedBindingQueue : BindingQueue<ConnectedBindingContext>
{
internal const int DefaultBindingTimeout = 60000;
internal const int DefaultMinimumConnectionTimeout = 30;
/// <summary>
/// Gets the current settings
/// </summary>
internal SqlToolsSettings CurrentSettings
{
get { return WorkspaceService<SqlToolsSettings>.Instance.CurrentSettings; }
}
/// <summary>
/// Generate a unique key based on the ConnectionInfo object
/// </summary>
/// <param name="connInfo"></param>
private string GetConnectionContextKey(ConnectionInfo connInfo)
{
ConnectionDetails details = connInfo.ConnectionDetails;
return string.Format("{0}_{1}_{2}_{3}",
details.ServerName ?? "NULL",
details.DatabaseName ?? "NULL",
details.UserName ?? "NULL",
details.AuthenticationType ?? "NULL"
);
}
/// <summary>
/// Use a ConnectionInfo item to create a connected binding context
/// </summary>
/// <param name="connInfo"></param>
public virtual string AddConnectionContext(ConnectionInfo connInfo)
{
if (connInfo == null)
{
return string.Empty;
}
// lookup the current binding context
string connectionKey = GetConnectionContextKey(connInfo);
IBindingContext bindingContext = this.GetOrCreateBindingContext(connectionKey);
try
{
// increase the connection timeout to at least 30 seconds and and build connection string
// enable PersistSecurityInfo to handle issues in SMO where the connection context is lost in reconnections
int? originalTimeout = connInfo.ConnectionDetails.ConnectTimeout;
bool? originalPersistSecurityInfo = connInfo.ConnectionDetails.PersistSecurityInfo;
connInfo.ConnectionDetails.ConnectTimeout = Math.Max(DefaultMinimumConnectionTimeout, originalTimeout ?? 0);
connInfo.ConnectionDetails.PersistSecurityInfo = true;
string connectionString = ConnectionService.BuildConnectionString(connInfo.ConnectionDetails);
connInfo.ConnectionDetails.ConnectTimeout = originalTimeout;
connInfo.ConnectionDetails.PersistSecurityInfo = originalPersistSecurityInfo;
// open a dedicated binding server connection
SqlConnection sqlConn = new SqlConnection(connectionString);
if (sqlConn != null)
{
sqlConn.Open();
// populate the binding context to work with the SMO metadata provider
ServerConnection serverConn = new ServerConnection(sqlConn);
bindingContext.SmoMetadataProvider = SmoMetadataProvider.CreateConnectedProvider(serverConn);
bindingContext.MetadataDisplayInfoProvider = new MetadataDisplayInfoProvider();
bindingContext.MetadataDisplayInfoProvider.BuiltInCasing =
this.CurrentSettings.SqlTools.IntelliSense.LowerCaseSuggestions.Value
? CasingStyle.Lowercase : CasingStyle.Uppercase;
bindingContext.Binder = BinderProvider.CreateBinder(bindingContext.SmoMetadataProvider);
bindingContext.ServerConnection = serverConn;
bindingContext.BindingTimeout = ConnectedBindingQueue.DefaultBindingTimeout;
bindingContext.IsConnected = true;
}
}
catch (Exception)
{
bindingContext.IsConnected = false;
}
finally
{
bindingContext.BindingLocked.Set();
}
return connectionKey;
}
}
}

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.Threading;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.SmoMetadataProvider;
using Microsoft.SqlServer.Management.SqlParser.Binder;
using Microsoft.SqlServer.Management.SqlParser.Common;
using Microsoft.SqlServer.Management.SqlParser.MetadataProvider;
using Microsoft.SqlServer.Management.SqlParser.Parser;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
/// <summary>
/// The context used for binding requests
/// </summary>
public interface IBindingContext
{
/// <summary>
/// Gets or sets a flag indicating if the context is connected
/// </summary>
bool IsConnected { get; set; }
/// <summary>
/// Gets or sets the binding server connection
/// </summary>
ServerConnection ServerConnection { get; set; }
/// <summary>
/// Gets or sets the metadata display info provider
/// </summary>
MetadataDisplayInfoProvider MetadataDisplayInfoProvider { get; set; }
/// <summary>
/// Gets or sets the SMO metadata provider
/// </summary>
SmoMetadataProvider SmoMetadataProvider { get; set; }
/// <summary>
/// Gets or sets the binder
/// </summary>
IBinder Binder { get; set; }
/// <summary>
/// Gets or sets an event to signal if a binding operation is in progress
/// </summary>
ManualResetEvent BindingLocked { get; set; }
/// <summary>
/// Gets or sets the binding operation timeout in milliseconds
/// </summary>
int BindingTimeout { get; set; }
/// <summary>
/// Gets or sets the current connection parse options
/// </summary>
ParseOptions ParseOptions { get; }
/// <summary>
/// Gets or sets the current connection server version
/// </summary>
ServerVersion ServerVersion { get; }
/// <summary>
/// Gets or sets the database engine type
/// </summary>
DatabaseEngineType DatabaseEngineType { get; }
/// <summary>
/// Gets or sets the T-SQL version
/// </summary>
TransactSqlVersion TransactSqlVersion { get; }
/// <summary>
/// Gets or sets the database compatibility level
/// </summary>
DatabaseCompatibilityLevel DatabaseCompatibilityLevel { get; }
}
}

View File

@@ -11,13 +11,11 @@ using System.Threading.Tasks;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.SqlParser;
using Microsoft.SqlServer.Management.SqlParser.Binder;
using Microsoft.SqlServer.Management.SqlParser.Common;
using Microsoft.SqlServer.Management.SqlParser.Intellisense;
using Microsoft.SqlServer.Management.SqlParser.MetadataProvider;
using Microsoft.SqlServer.Management.SqlParser.Parser;
using Microsoft.SqlServer.Management.SmoMetadataProvider;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection;
using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
@@ -39,31 +37,54 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
internal const int DiagnosticParseDelay = 750;
internal const int FindCompletionsTimeout = 3000;
internal const int HoverTimeout = 3000;
internal const int BindingTimeout = 3000;
internal const int FindCompletionStartTimeout = 50;
internal const int OnConnectionWaitTimeout = 300000;
private static ConnectionService connectionService = null;
private static WorkspaceService<SqlToolsSettings> workspaceServiceInstance;
private object parseMapLock = new object();
private ScriptParseInfo currentCompletionParseInfo;
internal bool ShouldEnableAutocomplete()
{
return true;
}
private ConnectedBindingQueue bindingQueue = new ConnectedBindingQueue();
private ConnectionService connectionService = null;
private ParseOptions defaultParseOptions = new ParseOptions(
batchSeparator: LanguageService.DefaultBatchSeperator,
isQuotedIdentifierSet: true,
compatibilityLevel: DatabaseCompatibilityLevel.Current,
transactSqlVersion: TransactSqlVersion.Current);
/// <summary>
/// Gets or sets the binding queue instance
/// Internal for testing purposes only
/// </summary>
internal ConnectedBindingQueue BindingQueue
{
get
{
return this.bindingQueue;
}
set
{
this.bindingQueue = value;
}
}
/// <summary>
/// Internal for testing purposes only
/// </summary>
internal ConnectionService ConnectionServiceInstance
internal static ConnectionService ConnectionServiceInstance
{
get
{
if(connectionService == null)
if (connectionService == null)
{
connectionService = ConnectionService.Instance;
}
@@ -83,6 +104,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
private Lazy<Dictionary<string, ScriptParseInfo>> scriptParseInfoMap
= new Lazy<Dictionary<string, ScriptParseInfo>>(() => new Dictionary<string, ScriptParseInfo>());
/// <summary>
/// Gets a mapping dictionary for SQL file URIs to ScriptParseInfo objects
/// </summary>
internal Dictionary<string, ScriptParseInfo> ScriptParseInfoMap
{
get
@@ -91,11 +115,22 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
}
/// <summary>
/// Gets the singleton instance object
/// </summary>
public static LanguageService Instance
{
get { return instance.Value; }
}
private ParseOptions DefaultParseOptions
{
get
{
return this.defaultParseOptions;
}
}
/// <summary>
/// Default, parameterless constructor.
/// </summary>
@@ -109,14 +144,40 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
private static CancellationTokenSource ExistingRequestCancellation { get; set; }
/// <summary>
/// Gets the current settings
/// </summary>
internal SqlToolsSettings CurrentSettings
{
get { return WorkspaceService<SqlToolsSettings>.Instance.CurrentSettings; }
}
/// <summary>
/// Gets or sets the current workspace service instance
/// Setter for internal testing purposes only
/// </summary>
internal static WorkspaceService<SqlToolsSettings> WorkspaceServiceInstance
{
get
{
if (LanguageService.workspaceServiceInstance == null)
{
LanguageService.workspaceServiceInstance = WorkspaceService<SqlToolsSettings>.Instance;
}
return LanguageService.workspaceServiceInstance;
}
set
{
LanguageService.workspaceServiceInstance = value;
}
}
/// <summary>
/// Gets the current workspace instance
/// </summary>
internal Workspace.Workspace CurrentWorkspace
{
get { return WorkspaceService<SqlToolsSettings>.Instance.Workspace; }
get { return LanguageService.WorkspaceServiceInstance.Workspace; }
}
/// <summary>
@@ -181,23 +242,31 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// <param name="textDocumentPosition"></param>
/// <param name="requestContext"></param>
/// <returns></returns>
private static async Task HandleCompletionRequest(
internal static async Task HandleCompletionRequest(
TextDocumentPosition textDocumentPosition,
RequestContext<CompletionItem[]> requestContext)
{
// get the current list of completion items and return to client
var scriptFile = WorkspaceService<SqlToolsSettings>.Instance.Workspace.GetFile(
textDocumentPosition.TextDocument.Uri);
// check if Intellisense suggestions are enabled
if (!WorkspaceService<SqlToolsSettings>.Instance.CurrentSettings.IsSuggestionsEnabled)
{
await Task.FromResult(true);
}
else
{
// get the current list of completion items and return to client
var scriptFile = LanguageService.WorkspaceServiceInstance.Workspace.GetFile(
textDocumentPosition.TextDocument.Uri);
ConnectionInfo connInfo;
ConnectionService.Instance.TryFindConnection(
scriptFile.ClientFilePath,
out connInfo);
ConnectionInfo connInfo;
LanguageService.ConnectionServiceInstance.TryFindConnection(
scriptFile.ClientFilePath,
out connInfo);
var completionItems = Instance.GetCompletionItems(
textDocumentPosition, scriptFile, connInfo);
var completionItems = Instance.GetCompletionItems(
textDocumentPosition, scriptFile, connInfo);
await requestContext.SendResult(completionItems);
await requestContext.SendResult(completionItems);
}
}
/// <summary>
@@ -211,8 +280,16 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
CompletionItem completionItem,
RequestContext<CompletionItem> requestContext)
{
completionItem = LanguageService.Instance.ResolveCompletionItem(completionItem);
await requestContext.SendResult(completionItem);
// check if Intellisense suggestions are enabled
if (!WorkspaceService<SqlToolsSettings>.Instance.CurrentSettings.IsSuggestionsEnabled)
{
await Task.FromResult(true);
}
else
{
completionItem = LanguageService.Instance.ResolveCompletionItem(completionItem);
await requestContext.SendResult(completionItem);
}
}
private static async Task HandleDefinitionRequest(
@@ -246,8 +323,21 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
private static async Task HandleHoverRequest(
TextDocumentPosition textDocumentPosition,
RequestContext<Hover> requestContext)
{
await Task.FromResult(true);
{
// check if Quick Info hover tooltips are enabled
if (WorkspaceService<SqlToolsSettings>.Instance.CurrentSettings.IsQuickInfoEnabled)
{
var scriptFile = WorkspaceService<SqlToolsSettings>.Instance.Workspace.GetFile(
textDocumentPosition.TextDocument.Uri);
var hover = LanguageService.Instance.GetHoverItem(textDocumentPosition, scriptFile);
if (hover != null)
{
await requestContext.SendResult(hover);
}
}
await requestContext.SendResult(new Hover());
}
#endregion
@@ -264,7 +354,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
ScriptFile scriptFile,
EventContext eventContext)
{
if (!IsPreviewWindow(scriptFile))
// if not in the preview window and diagnostics are enabled then run diagnostics
if (!IsPreviewWindow(scriptFile)
&& WorkspaceService<SqlToolsSettings>.Instance.CurrentSettings.IsDiagnositicsEnabled)
{
await RunScriptDiagnostics(
new ScriptFile[] { scriptFile },
@@ -278,13 +370,15 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// Handles text document change events
/// </summary>
/// <param name="textChangeParams"></param>
/// <param name="eventContext"></param>
/// <returns></returns>
/// <param name="eventContext"></param>
public async Task HandleDidChangeTextDocumentNotification(ScriptFile[] changedFiles, EventContext eventContext)
{
await this.RunScriptDiagnostics(
changedFiles.ToArray(),
eventContext);
if (WorkspaceService<SqlToolsSettings>.Instance.CurrentSettings.IsDiagnositicsEnabled)
{
await this.RunScriptDiagnostics(
changedFiles.ToArray(),
eventContext);
}
await Task.FromResult(true);
}
@@ -300,13 +394,18 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
SqlToolsSettings oldSettings,
EventContext eventContext)
{
// If script analysis settings have changed we need to clear & possibly update the current diagnostic records.
bool oldScriptAnalysisEnabled = oldSettings.ScriptAnalysis.Enable.HasValue;
if ((oldScriptAnalysisEnabled != newSettings.ScriptAnalysis.Enable))
bool oldEnableIntelliSense = oldSettings.SqlTools.EnableIntellisense;
bool? oldEnableDiagnostics = oldSettings.SqlTools.IntelliSense.EnableDiagnostics;
// update the current settings to reflect any changes
CurrentSettings.Update(newSettings);
// if script analysis settings have changed we need to clear the current diagnostic markers
if (oldEnableIntelliSense != newSettings.SqlTools.EnableIntellisense
|| oldEnableDiagnostics != newSettings.SqlTools.IntelliSense.EnableDiagnostics)
{
// If the user just turned off script analysis or changed the settings path, send a diagnostics
// event to clear the analysis markers that they already have.
if (!newSettings.ScriptAnalysis.Enable.Value)
// if the user just turned off diagnostics then send an event to clear the error markers
if (!newSettings.IsDiagnositicsEnabled)
{
ScriptFileMarker[] emptyAnalysisDiagnostics = new ScriptFileMarker[0];
@@ -315,15 +414,12 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
await DiagnosticsHelper.PublishScriptDiagnostics(scriptFile, emptyAnalysisDiagnostics, eventContext);
}
}
// otherwise rerun diagnostic analysis on all opened SQL files
else
{
await this.RunScriptDiagnostics(CurrentWorkspace.GetOpenedFiles(), eventContext);
}
}
// Update the settings in the current
CurrentSettings.EnableProfileLoading = newSettings.EnableProfileLoading;
CurrentSettings.ScriptAnalysis.Update(newSettings.ScriptAnalysis, CurrentWorkspace.WorkspacePath);
}
#endregion
@@ -350,52 +446,85 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// </summary>
/// <param name="filePath"></param>
/// <param name="sqlText"></param>
/// <returns></returns>
/// <returns>The ParseResult instance returned from SQL Parser</returns>
public ParseResult ParseAndBind(ScriptFile scriptFile, ConnectionInfo connInfo)
{
// get or create the current parse info object
ScriptParseInfo parseInfo = GetScriptParseInfo(scriptFile.ClientFilePath, createIfNotExists: true);
if (parseInfo.BuildingMetadataEvent.WaitOne(LanguageService.FindCompletionsTimeout))
if (parseInfo.BuildingMetadataEvent.WaitOne(LanguageService.BindingTimeout))
{
try
{
parseInfo.BuildingMetadataEvent.Reset();
// parse current SQL file contents to retrieve a list of errors
ParseResult parseResult = Parser.IncrementalParse(
scriptFile.Contents,
parseInfo.ParseResult,
parseInfo.ParseOptions);
parseInfo.ParseResult = parseResult;
if (connInfo != null && parseInfo.IsConnected)
if (connInfo == null || !parseInfo.IsConnected)
{
try
{
List<ParseResult> parseResults = new List<ParseResult>();
parseResults.Add(parseResult);
parseInfo.Binder.Bind(
parseResults,
connInfo.ConnectionDetails.DatabaseName,
BindMode.Batch);
}
catch (ConnectionException)
{
Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object...");
}
catch (SqlParserInternalBinderError)
{
Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object...");
}
// parse current SQL file contents to retrieve a list of errors
ParseResult parseResult = Parser.IncrementalParse(
scriptFile.Contents,
parseInfo.ParseResult,
this.DefaultParseOptions);
parseInfo.ParseResult = parseResult;
}
else
{
QueueItem queueItem = this.BindingQueue.QueueBindingOperation(
key: parseInfo.ConnectionKey,
bindingTimeout: LanguageService.BindingTimeout,
bindOperation: (bindingContext, cancelToken) =>
{
try
{
ParseResult parseResult = Parser.IncrementalParse(
scriptFile.Contents,
parseInfo.ParseResult,
bindingContext.ParseOptions);
parseInfo.ParseResult = parseResult;
List<ParseResult> parseResults = new List<ParseResult>();
parseResults.Add(parseResult);
bindingContext.Binder.Bind(
parseResults,
connInfo.ConnectionDetails.DatabaseName,
BindMode.Batch);
}
catch (ConnectionException)
{
Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object...");
}
catch (SqlParserInternalBinderError)
{
Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object...");
}
catch (Exception ex)
{
Logger.Write(LogLevel.Error, "Unknown exception during parsing " + ex.ToString());
}
return null;
});
queueItem.ItemProcessed.WaitOne();
}
}
catch (Exception ex)
{
// reset the parse result to do a full parse next time
parseInfo.ParseResult = null;
Logger.Write(LogLevel.Error, "Unknown exception during parsing " + ex.ToString());
}
finally
{
parseInfo.BuildingMetadataEvent.Set();
}
}
else
{
Logger.Write(LogLevel.Warning, "Binding metadata lock timeout in ParseAndBind");
}
return parseInfo.ParseResult;
}
@@ -406,42 +535,32 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// <param name="info"></param>
public async Task UpdateLanguageServiceOnConnection(ConnectionInfo info)
{
await Task.Run( () =>
await Task.Run(() =>
{
if (ShouldEnableAutocomplete())
ScriptParseInfo scriptInfo = GetScriptParseInfo(info.OwnerUri, createIfNotExists: true);
if (scriptInfo.BuildingMetadataEvent.WaitOne(LanguageService.OnConnectionWaitTimeout))
{
ScriptParseInfo scriptInfo = GetScriptParseInfo(info.OwnerUri, createIfNotExists: true);
if (scriptInfo.BuildingMetadataEvent.WaitOne(LanguageService.OnConnectionWaitTimeout))
try
{
try
{
scriptInfo.BuildingMetadataEvent.Reset();
var sqlConn = info.SqlConnection as ReliableSqlConnection;
if (sqlConn != null)
{
ServerConnection serverConn = new ServerConnection(sqlConn.GetUnderlyingConnection());
scriptInfo.MetadataDisplayInfoProvider = new MetadataDisplayInfoProvider();
scriptInfo.MetadataProvider = SmoMetadataProvider.CreateConnectedProvider(serverConn);
scriptInfo.Binder = BinderProvider.CreateBinder(scriptInfo.MetadataProvider);
scriptInfo.ServerConnection = new ServerConnection(sqlConn.GetUnderlyingConnection());
scriptInfo.IsConnected = true;
}
}
catch (Exception)
{
scriptInfo.IsConnected = false;
}
finally
{
// Set Metadata Build event to Signal state.
// (Tell Language Service that I am ready with Metadata Provider Object)
scriptInfo.BuildingMetadataEvent.Set();
}
scriptInfo.BuildingMetadataEvent.Reset();
scriptInfo.ConnectionKey = this.BindingQueue.AddConnectionContext(info);
scriptInfo.IsConnected = true;
}
catch (Exception ex)
{
Logger.Write(LogLevel.Error, "Unknown error in OnConnection " + ex.ToString());
scriptInfo.IsConnected = false;
}
finally
{
// Set Metadata Build event to Signal state.
// (Tell Language Service that I am ready with Metadata Provider Object)
scriptInfo.BuildingMetadataEvent.Set();
}
}
// populate SMO metadata provider with most common info
AutoCompleteHelper.PrepopulateCommonMetadata(info, scriptInfo);
}
AutoCompleteHelper.PrepopulateCommonMetadata(info, scriptInfo, this.BindingQueue);
});
}
@@ -469,22 +588,88 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// <param name="completionItem"></param>
internal CompletionItem ResolveCompletionItem(CompletionItem completionItem)
{
var scriptParseInfo = LanguageService.Instance.currentCompletionParseInfo;
if (scriptParseInfo != null && scriptParseInfo.CurrentSuggestions != null)
try
{
foreach (var suggestion in scriptParseInfo.CurrentSuggestions)
var scriptParseInfo = LanguageService.Instance.currentCompletionParseInfo;
if (scriptParseInfo != null && scriptParseInfo.CurrentSuggestions != null)
{
if (string.Equals(suggestion.Title, completionItem.Label))
foreach (var suggestion in scriptParseInfo.CurrentSuggestions)
{
completionItem.Detail = suggestion.DatabaseQualifiedName;
completionItem.Documentation = suggestion.Description;
break;
if (string.Equals(suggestion.Title, completionItem.Label))
{
completionItem.Detail = suggestion.DatabaseQualifiedName;
completionItem.Documentation = suggestion.Description;
break;
}
}
}
}
catch (Exception ex)
{
// if any exceptions are raised looking up extended completion metadata
// then just return the original completion item
Logger.Write(LogLevel.Error, "Exeception in ResolveCompletionItem " + ex.ToString());
}
return completionItem;
}
/// <summary>
/// Get quick info hover tooltips for the current position
/// </summary>
/// <param name="textDocumentPosition"></param>
/// <param name="scriptFile"></param>
internal Hover GetHoverItem(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile)
{
int startLine = textDocumentPosition.Position.Line;
int startColumn = TextUtilities.PositionOfPrevDelimeter(
scriptFile.Contents,
textDocumentPosition.Position.Line,
textDocumentPosition.Position.Character);
int endColumn = textDocumentPosition.Position.Character;
ScriptParseInfo scriptParseInfo = GetScriptParseInfo(textDocumentPosition.TextDocument.Uri);
if (scriptParseInfo != null && scriptParseInfo.ParseResult != null)
{
if (scriptParseInfo.BuildingMetadataEvent.WaitOne(LanguageService.FindCompletionStartTimeout))
{
scriptParseInfo.BuildingMetadataEvent.Reset();
try
{
QueueItem queueItem = this.BindingQueue.QueueBindingOperation(
key: scriptParseInfo.ConnectionKey,
bindingTimeout: LanguageService.HoverTimeout,
bindOperation: (bindingContext, cancelToken) =>
{
// get the current quick info text
Babel.CodeObjectQuickInfo quickInfo = Resolver.GetQuickInfo(
scriptParseInfo.ParseResult,
startLine + 1,
endColumn + 1,
bindingContext.MetadataDisplayInfoProvider);
// convert from the parser format to the VS Code wire format
return AutoCompleteHelper.ConvertQuickInfoToHover(
quickInfo,
startLine,
startColumn,
endColumn);
});
queueItem.ItemProcessed.WaitOne();
return queueItem.GetResultAsT<Hover>();
}
finally
{
scriptParseInfo.BuildingMetadataEvent.Set();
}
}
}
// return null if there isn't a tooltip for the current location
return null;
}
/// <summary>
/// Return the completion item list for the current text position.
/// This method does not await cache builds since it expects to return quickly
@@ -502,6 +687,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
textDocumentPosition.Position.Line,
textDocumentPosition.Position.Character);
int endColumn = textDocumentPosition.Position.Character;
bool useLowerCaseSuggestions = this.CurrentSettings.SqlTools.IntelliSense.LowerCaseSuggestions.Value;
this.currentCompletionParseInfo = null;
@@ -510,7 +696,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
ScriptParseInfo scriptParseInfo = GetScriptParseInfo(textDocumentPosition.TextDocument.Uri);
if (connInfo == null || scriptParseInfo == null)
{
return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn);
return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn, useLowerCaseSuggestions);
}
// reparse and bind the SQL statement if needed
@@ -521,49 +707,60 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
if (scriptParseInfo.ParseResult == null)
{
return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn);
return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn, useLowerCaseSuggestions);
}
if (scriptParseInfo.IsConnected
&& scriptParseInfo.BuildingMetadataEvent.WaitOne(LanguageService.FindCompletionStartTimeout))
{
scriptParseInfo.BuildingMetadataEvent.Reset();
Task<CompletionItem[]> findCompletionsTask = Task.Run(() => {
try
scriptParseInfo.BuildingMetadataEvent.Reset();
QueueItem queueItem = this.BindingQueue.QueueBindingOperation(
key: scriptParseInfo.ConnectionKey,
bindingTimeout: LanguageService.BindingTimeout,
bindOperation: (bindingContext, cancelToken) =>
{
// get the completion list from SQL Parser
scriptParseInfo.CurrentSuggestions = Resolver.FindCompletions(
scriptParseInfo.ParseResult,
textDocumentPosition.Position.Line + 1,
textDocumentPosition.Position.Character + 1,
scriptParseInfo.MetadataDisplayInfoProvider);
CompletionItem[] completions = null;
try
{
// get the completion list from SQL Parser
scriptParseInfo.CurrentSuggestions = Resolver.FindCompletions(
scriptParseInfo.ParseResult,
textDocumentPosition.Position.Line + 1,
textDocumentPosition.Position.Character + 1,
bindingContext.MetadataDisplayInfoProvider);
// cache the current script parse info object to resolve completions later
this.currentCompletionParseInfo = scriptParseInfo;
// cache the current script parse info object to resolve completions later
this.currentCompletionParseInfo = scriptParseInfo;
// convert the suggestion list to the VS Code format
return AutoCompleteHelper.ConvertDeclarationsToCompletionItems(
scriptParseInfo.CurrentSuggestions,
startLine,
startColumn,
endColumn);
}
finally
// convert the suggestion list to the VS Code format
completions = AutoCompleteHelper.ConvertDeclarationsToCompletionItems(
scriptParseInfo.CurrentSuggestions,
startLine,
startColumn,
endColumn);
}
finally
{
scriptParseInfo.BuildingMetadataEvent.Set();
}
return completions;
},
timeoutOperation: (bindingContext) =>
{
scriptParseInfo.BuildingMetadataEvent.Set();
}
});
findCompletionsTask.Wait(LanguageService.FindCompletionsTimeout);
if (findCompletionsTask.IsCompleted
&& findCompletionsTask.Result != null
&& findCompletionsTask.Result.Length > 0)
return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn, useLowerCaseSuggestions);
});
queueItem.ItemProcessed.WaitOne();
var completionItems = queueItem.GetResultAsT<CompletionItem[]>();
if (completionItems != null && completionItems.Length > 0)
{
return findCompletionsTask.Result;
return completionItems;
}
}
return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn);
return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn, useLowerCaseSuggestions);
}
#endregion
@@ -614,7 +811,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// <param name="eventContext"></param>
private Task RunScriptDiagnostics(ScriptFile[] filesToAnalyze, EventContext eventContext)
{
if (!CurrentSettings.ScriptAnalysis.Enable.Value)
if (!CurrentSettings.IsDiagnositicsEnabled)
{
// If the user has disabled script analysis, skip it entirely
return Task.FromResult(true);
@@ -710,7 +907,12 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
#endregion
private void AddOrUpdateScriptParseInfo(string uri, ScriptParseInfo scriptInfo)
/// <summary>
/// Adds a new or updates an existing script parse info instance in local cache
/// </summary>
/// <param name="uri"></param>
/// <param name="scriptInfo"></param>
internal void AddOrUpdateScriptParseInfo(string uri, ScriptParseInfo scriptInfo)
{
lock (this.parseMapLock)
{
@@ -726,7 +928,13 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
}
private ScriptParseInfo GetScriptParseInfo(string uri, bool createIfNotExists = false)
/// <summary>
/// Gets a script parse info object for a file from the local cache
/// Internal for testing purposes only
/// </summary>
/// <param name="uri"></param>
/// <param name="createIfNotExists">Creates a new instance if one doesn't exist</param>
internal ScriptParseInfo GetScriptParseInfo(string uri, bool createIfNotExists = false)
{
lock (this.parseMapLock)
{
@@ -736,6 +944,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
else if (createIfNotExists)
{
// create a new script parse info object and initialize with the current settings
ScriptParseInfo scriptInfo = new ScriptParseInfo();
this.ScriptParseInfoMap.Add(uri, scriptInfo);
return scriptInfo;
@@ -752,10 +961,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
lock (this.parseMapLock)
{
if (this.ScriptParseInfoMap.ContainsKey(uri))
{
var scriptInfo = this.ScriptParseInfoMap[uri];
scriptInfo.ServerConnection.Disconnect();
scriptInfo.ServerConnection = null;
{
return this.ScriptParseInfoMap.Remove(uri);
}
else

View File

@@ -0,0 +1,66 @@
//
// 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.Threading;
using System.Threading.Tasks;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
/// <summary>
/// Class that stores the state of a binding queue request item
/// </summary>
public class QueueItem
{
/// <summary>
/// QueueItem constructor
/// </summary>
public QueueItem()
{
this.ItemProcessed = new ManualResetEvent(initialState: false);
}
/// <summary>
/// Gets or sets the queue item key
/// </summary>
public string Key { get; set; }
/// <summary>
/// Gets or sets the bind operation callback method
/// </summary>
public Func<IBindingContext, CancellationToken, object> BindOperation { get; set; }
/// <summary>
/// Gets or sets the timeout operation to call if the bind operation doesn't finish within timeout period
/// </summary>
public Func<IBindingContext, object> TimeoutOperation { get; set; }
/// <summary>
/// Gets or sets an event to signal when this queue item has been processed
/// </summary>
public ManualResetEvent ItemProcessed { get; set; }
/// <summary>
/// Gets or sets the result of the queued task
/// </summary>
public object Result { get; set; }
/// <summary>
/// Gets or sets the binding operation timeout in milliseconds
/// </summary>
public int? BindingTimeout { get; set; }
/// <summary>
/// Converts the result of the execution to type T
/// </summary>
public T GetResultAsT<T>() where T : class
{
//var task = this.ResultsTask;
return (this.Result != null)
? this.Result as T
: null;
}
}
}

View File

@@ -3,15 +3,9 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.SmoMetadataProvider;
using Microsoft.SqlServer.Management.SqlParser.Binder;
using Microsoft.SqlServer.Management.SqlParser.Common;
using Microsoft.SqlServer.Management.SqlParser.Intellisense;
using Microsoft.SqlServer.Management.SqlParser.MetadataProvider;
using Microsoft.SqlServer.Management.SqlParser.Parser;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
@@ -23,10 +17,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
private ManualResetEvent buildingMetadataEvent = new ManualResetEvent(initialState: true);
private ParseOptions parseOptions = new ParseOptions();
private ServerConnection serverConnection;
/// <summary>
/// Event which tells if MetadataProvider is built fully or not
/// </summary>
@@ -41,163 +31,18 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
public bool IsConnected { get; set; }
/// <summary>
/// Gets or sets the LanguageService SMO ServerConnection
/// Gets or sets the binding queue connection context key
/// </summary>
public ServerConnection ServerConnection
{
get
{
return this.serverConnection;
}
set
{
this.serverConnection = value;
this.parseOptions = new ParseOptions(
batchSeparator: LanguageService.DefaultBatchSeperator,
isQuotedIdentifierSet: true,
compatibilityLevel: DatabaseCompatibilityLevel,
transactSqlVersion: TransactSqlVersion);
}
}
/// <summary>
/// Gets the Language Service ServerVersion
/// </summary>
public ServerVersion ServerVersion
{
get
{
return this.ServerConnection != null
? this.ServerConnection.ServerVersion
: null;
}
}
/// <summary>
/// Gets the current DataEngineType
/// </summary>
public DatabaseEngineType DatabaseEngineType
{
get
{
return this.ServerConnection != null
? this.ServerConnection.DatabaseEngineType
: DatabaseEngineType.Standalone;
}
}
/// <summary>
/// Gets the current connections TransactSqlVersion
/// </summary>
public TransactSqlVersion TransactSqlVersion
{
get
{
return this.IsConnected
? GetTransactSqlVersion(this.ServerVersion)
: TransactSqlVersion.Current;
}
}
/// <summary>
/// Gets the current DatabaseCompatibilityLevel
/// </summary>
public DatabaseCompatibilityLevel DatabaseCompatibilityLevel
{
get
{
return this.IsConnected
? GetDatabaseCompatibilityLevel(this.ServerVersion)
: DatabaseCompatibilityLevel.Current;
}
}
/// <summary>
/// Gets the current ParseOptions
/// </summary>
public ParseOptions ParseOptions
{
get
{
return this.parseOptions;
}
}
/// <summary>
/// Gets or sets the SMO binder for schema-aware intellisense
/// </summary>
public IBinder Binder { get; set; }
public string ConnectionKey { get; set; }
/// <summary>
/// Gets or sets the previous SQL parse result
/// </summary>
public ParseResult ParseResult { get; set; }
/// <summary>
/// Gets or set the SMO metadata provider that's bound to the current connection
/// </summary>
public SmoMetadataProvider MetadataProvider { get; set; }
/// <summary>
/// Gets or sets the SMO metadata display info provider
/// </summary>
public MetadataDisplayInfoProvider MetadataDisplayInfoProvider { get; set; }
/// <summary>
/// Gets or sets the current autocomplete suggestion list
/// </summary>
public IEnumerable<Declaration> CurrentSuggestions { get; set; }
/// <summary>
/// Gets the database compatibility level from a server version
/// </summary>
/// <param name="serverVersion"></param>
private static DatabaseCompatibilityLevel GetDatabaseCompatibilityLevel(ServerVersion serverVersion)
{
int versionMajor = Math.Max(serverVersion.Major, 8);
switch (versionMajor)
{
case 8:
return DatabaseCompatibilityLevel.Version80;
case 9:
return DatabaseCompatibilityLevel.Version90;
case 10:
return DatabaseCompatibilityLevel.Version100;
case 11:
return DatabaseCompatibilityLevel.Version110;
case 12:
return DatabaseCompatibilityLevel.Version120;
case 13:
return DatabaseCompatibilityLevel.Version130;
default:
return DatabaseCompatibilityLevel.Current;
}
}
/// <summary>
/// Gets the transaction sql version from a server version
/// </summary>
/// <param name="serverVersion"></param>
private static TransactSqlVersion GetTransactSqlVersion(ServerVersion serverVersion)
{
int versionMajor = Math.Max(serverVersion.Major, 9);
switch (versionMajor)
{
case 9:
case 10:
// In case of 10.0 we still use Version 10.5 as it is the closest available.
return TransactSqlVersion.Version105;
case 11:
return TransactSqlVersion.Version110;
case 12:
return TransactSqlVersion.Version120;
case 13:
return TransactSqlVersion.Version130;
default:
return TransactSqlVersion.Current;
}
}
}
}