Improve autocomplete locking

This commit is contained in:
Karl Burtram
2016-09-18 01:54:32 -07:00
parent 41198e9357
commit e74a392ca9
5 changed files with 272 additions and 103 deletions

View File

@@ -8,6 +8,13 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
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.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;
@@ -15,16 +22,9 @@ using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
using Microsoft.SqlTools.ServiceLayer.SqlContext;
using Microsoft.SqlTools.ServiceLayer.Utility;
using Microsoft.SqlTools.ServiceLayer.Workspace;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.SqlParser;
using Microsoft.SqlTools.ServiceLayer.Utility;
using Microsoft.SqlServer.Management.SqlParser.Binder;
using Microsoft.SqlServer.Management.SqlParser.Intellisense;
using Microsoft.SqlServer.Management.SqlParser.MetadataProvider;
using Microsoft.SqlServer.Management.SqlParser.Parser;
using Microsoft.SqlServer.Management.SmoMetadataProvider;
using Location = Microsoft.SqlTools.ServiceLayer.Workspace.Contracts.Location;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
@@ -35,15 +35,19 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// </summary>
public sealed class LanguageService
{
public const string DefaultBatchSeperator = "GO";
internal const string DefaultBatchSeperator = "GO";
private const int DiagnosticParseDelay = 750;
internal const int DiagnosticParseDelay = 750;
private const int FindCompletionsTimeout = 3000;
internal const int FindCompletionsTimeout = 3000;
private const int FindCompletionStartTimeout = 50;
internal const int FindCompletionStartTimeout = 50;
private const int OnConnectionWaitTimeout = 30000;
internal const int OnConnectionWaitTimeout = 300000;
private object parseMapLock = new object();
private ScriptParseInfo currentCompletionParseInfo;
private bool ShouldEnableAutocomplete()
{
@@ -196,6 +200,21 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
await requestContext.SendResult(completionItems);
}
/// <summary>
/// Handle the resolve completion request event to provide additional
/// autocomplete metadata to the currently select completion item
/// </summary>
/// <param name="completionItem"></param>
/// <param name="requestContext"></param>
/// <returns></returns>
private static async Task HandleCompletionResolveRequest(
CompletionItem completionItem,
RequestContext<CompletionItem> requestContext)
{
completionItem = LanguageService.Instance.ResolveCompletionItem(completionItem);
await requestContext.SendResult(completionItem);
}
private static async Task HandleDefinitionRequest(
TextDocumentPosition textDocumentPosition,
RequestContext<Location[]> requestContext)
@@ -212,14 +231,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
await Task.FromResult(true);
}
private static async Task HandleCompletionResolveRequest(
CompletionItem completionItem,
RequestContext<CompletionItem> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleCompletionResolveRequest");
await Task.FromResult(true);
}
private static async Task HandleSignatureHelpRequest(
TextDocumentPosition textDocumentPosition,
RequestContext<SignatureHelp> requestContext)
@@ -327,8 +338,10 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// it is the last URI connected to a particular connection,
/// then remove the cache.
/// </summary>
public async Task RemoveAutoCompleteCacheUriReference(ConnectionSummary summary)
public async Task RemoveAutoCompleteCacheUriReference(ConnectionSummary summary, string ownerUri)
{
RemoveScriptParseInfo(ownerUri);
// currently this method is disabled, but we need to reimplement now that the
// implementation of the 'cache' has changed.
await Task.FromResult(0);
@@ -342,16 +355,16 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// <returns></returns>
public ParseResult ParseAndBind(ScriptFile scriptFile, ConnectionInfo connInfo)
{
ScriptParseInfo parseInfo = null;
if (this.ScriptParseInfoMap.ContainsKey(scriptFile.ClientFilePath))
{
parseInfo = this.ScriptParseInfoMap[scriptFile.ClientFilePath];
}
else
{
parseInfo = new ScriptParseInfo();
this.ScriptParseInfoMap.Add(scriptFile.ClientFilePath, parseInfo);
}
// get or create the current parse info object
ScriptParseInfo parseInfo = GetScriptParseInfo(scriptFile.ClientFilePath, createIfNotExists: true);
// 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 (parseInfo.BuildingMetadataEvent.WaitOne(LanguageService.FindCompletionsTimeout))
{
@@ -359,14 +372,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
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)
{
try
@@ -398,7 +403,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
/// <summary>
/// Update the cached autocomplete candidate list when the user connects to a database
/// Update the autocomplete metadata provider when the user connects to a database
/// </summary>
/// <param name="info"></param>
public async Task UpdateLanguageServiceOnConnection(ConnectionInfo info)
@@ -407,43 +412,38 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
if (ShouldEnableAutocomplete())
{
ScriptParseInfo scriptInfo =
this.ScriptParseInfoMap.ContainsKey(info.OwnerUri)
? this.ScriptParseInfoMap[info.OwnerUri]
: new ScriptParseInfo();
try
ScriptParseInfo scriptInfo = GetScriptParseInfo(info.OwnerUri, createIfNotExists: true);
if (scriptInfo.BuildingMetadataEvent.WaitOne(LanguageService.OnConnectionWaitTimeout))
{
scriptInfo.BuildingMetadataEvent.WaitOne(LanguageService.OnConnectionWaitTimeout);
scriptInfo.BuildingMetadataEvent.Reset();
var sqlConn = info.SqlConnection as ReliableSqlConnection;
if (sqlConn != null)
try
{
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());
this.ScriptParseInfoMap[info.OwnerUri] = scriptInfo;
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;
AddOrUpdateScriptParseInfo(info.OwnerUri, scriptInfo);
}
}
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();
}
}
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();
}
if (scriptInfo.IsConnected)
{
var scriptFile = WorkspaceService<SqlToolsSettings>.Instance.Workspace.GetFile(info.OwnerUri);
ParseAndBind(scriptFile, info);
}
// populate SMO metadata provider with most common info
AutoCompleteHelper.PrepopulateCommonMetadata(info, scriptInfo);
}
});
}
@@ -466,6 +466,28 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
|| !string.Equals(prevSqlText, currentSqlText);
}
/// <summary>
/// Resolves the details and documentation for a completion item
/// </summary>
/// <param name="completionItem"></param>
internal CompletionItem ResolveCompletionItem(CompletionItem completionItem)
{
var scriptParseInfo = LanguageService.Instance.currentCompletionParseInfo;
if (scriptParseInfo != null && scriptParseInfo.CurrentSuggestions != null)
{
foreach (var suggestion in scriptParseInfo.CurrentSuggestions)
{
if (string.Equals(suggestion.Title, completionItem.Label))
{
completionItem.Detail = suggestion.DatabaseQualifiedName;
completionItem.Documentation = suggestion.Description;
break;
}
}
}
return completionItem;
}
/// <summary>
/// Return the completion item list for the current text position.
/// This method does not await cache builds since it expects to return quickly
@@ -479,20 +501,22 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
string filePath = textDocumentPosition.TextDocument.Uri;
int startLine = textDocumentPosition.Position.Line;
int startColumn = TextUtilities.PositionOfPrevDelimeter(
scriptFile.Contents,
scriptFile.Contents,
textDocumentPosition.Position.Line,
textDocumentPosition.Position.Character);
int endColumn = textDocumentPosition.Position.Character;
this.currentCompletionParseInfo = null;
// Take a reference to the list at a point in time in case we update and replace the list
if (connInfo == null
|| !LanguageService.Instance.ScriptParseInfoMap.ContainsKey(textDocumentPosition.TextDocument.Uri))
ScriptParseInfo scriptParseInfo = GetScriptParseInfo(textDocumentPosition.TextDocument.Uri);
if (connInfo == null || scriptParseInfo == null)
{
return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn);
}
// reparse and bind the SQL statement if needed
var scriptParseInfo = ScriptParseInfoMap[textDocumentPosition.TextDocument.Uri];
if (RequiresReparse(scriptParseInfo, scriptFile))
{
ParseAndBind(scriptFile, connInfo);
@@ -511,15 +535,18 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
try
{
// get the completion list from SQL Parser
var suggestions = Resolver.FindCompletions(
scriptParseInfo.CurrentSuggestions = Resolver.FindCompletions(
scriptParseInfo.ParseResult,
textDocumentPosition.Position.Line + 1,
textDocumentPosition.Position.Character + 1,
scriptParseInfo.MetadataDisplayInfoProvider);
// 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(
suggestions,
scriptParseInfo.CurrentSuggestions,
startLine,
startColumn,
endColumn);
@@ -685,5 +712,60 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
#endregion
private void AddOrUpdateScriptParseInfo(string uri, ScriptParseInfo scriptInfo)
{
lock (this.parseMapLock)
{
if (this.ScriptParseInfoMap.ContainsKey(uri))
{
this.ScriptParseInfoMap[uri] = scriptInfo;
}
else
{
this.ScriptParseInfoMap.Add(uri, scriptInfo);
}
}
}
private ScriptParseInfo GetScriptParseInfo(string uri, bool createIfNotExists = false)
{
lock (this.parseMapLock)
{
if (this.ScriptParseInfoMap.ContainsKey(uri))
{
return this.ScriptParseInfoMap[uri];
}
else if (createIfNotExists)
{
ScriptParseInfo scriptInfo = new ScriptParseInfo();
this.ScriptParseInfoMap.Add(uri, scriptInfo);
return scriptInfo;
}
else
{
return null;
}
}
}
private bool RemoveScriptParseInfo(string uri)
{
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
{
return false;
}
}
}
}
}