Fixes BindingQueue, GetSignatureHelp and RefreshIntellisenseCache to be truly async (#2175)

This commit is contained in:
Cheena Malhotra
2023-08-23 13:46:13 -07:00
committed by GitHub
parent fcd84f242a
commit cea10c13e6
8 changed files with 433 additions and 414 deletions

View File

@@ -12,9 +12,8 @@ using System.Linq;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Utility;
using Microsoft.SqlTools.Utility;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
@@ -130,7 +129,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
return context.IsConnected;
}
return false;
}
}
}
/// <summary>
@@ -144,7 +143,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
key = "disconnected_binding_context";
}
lock (this.bindingContextLock)
{
if (!this.BindingContextMap.ContainsKey(key))
@@ -155,7 +154,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
return this.BindingContextMap[key];
}
}
}
protected IEnumerable<IBindingContext> GetBindingContexts(string keyPrefix)
@@ -269,10 +268,10 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
this.itemQueuedEvent,
token.WaitHandle
};
while (true)
{
// wait for with an item to be queued or the a cancellation request
// wait for with an item to be queued or the cancellation request
WaitHandle.WaitAny(waitHandles);
if (token.IsCancellationRequested)
{
@@ -283,7 +282,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
// dispatch all pending queue items
while (this.HasPendingQueueItems)
{
{
QueueItem queueItem = GetNextQueueItem();
if (queueItem == null)
{
@@ -291,7 +290,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
IBindingContext bindingContext = GetOrCreateBindingContext(queueItem.Key);
if (bindingContext == null)
if (bindingContext == null)
{
queueItem.ItemProcessed.Set();
continue;
@@ -300,140 +299,16 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
var bindingContextTask = this.BindingContextTasks[bindingContext];
// Run in the binding context task in case this task has to wait for a previous binding operation
this.BindingContextTasks[bindingContext] = bindingContextTask.ContinueWith((task) =>
{
bool lockTaken = false;
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.BindingLock.WaitOne(queueItem.WaitForLockTimeout ?? 0))
{
try
{
Logger.Warning("Binding queue operation timed out waiting for previous operation to finish");
queueItem.Result = queueItem.TimeoutOperation != null
? queueItem.TimeoutOperation(bindingContext)
: null;
}
catch (Exception ex)
{
Logger.Error("Exception running binding queue lock timeout handler: " + ex.ToString());
}
finally
{
queueItem.ItemProcessed.Set();
}
return;
}
bindingContext.BindingLock.Reset();
lockTaken = true;
// execute the binding operation
object result = null;
CancellationTokenSource cancelToken = new CancellationTokenSource();
// run the operation in a separate thread
var bindTask = Task.Run(() =>
{
try
{
result = queueItem.BindOperation(
bindingContext,
cancelToken.Token);
}
catch (Exception ex)
{
Logger.Error("Unexpected exception on the binding queue: " + ex.ToString());
if (queueItem.ErrorHandler != null)
{
try
{
result = queueItem.ErrorHandler(ex);
}
catch (Exception ex2)
{
Logger.Error("Unexpected exception in binding queue error handler: " + ex2.ToString());
}
}
if (IsExceptionOfType(ex, typeof(SqlException)) || IsExceptionOfType(ex, typeof(SocketException)))
{
if (this.OnUnhandledException != null)
{
this.OnUnhandledException(queueItem.Key, ex);
}
RemoveBindingContext(queueItem.Key);
}
}
});
Task.Run(() =>
{
try
{
// check if the binding tasks completed within the binding timeout
if (bindTask.Wait(bindTimeout))
{
queueItem.Result = result;
}
else
{
cancelToken.Cancel();
// if the task didn't complete then call the timeout callback
if (queueItem.TimeoutOperation != null)
{
queueItem.Result = queueItem.TimeoutOperation(bindingContext);
}
bindTask.ContinueWithOnFaulted(t => Logger.Error("Binding queue threw exception " + t.Exception.ToString()));
// Give the task a chance to complete before moving on to the next operation
bindTask.Wait();
}
}
catch (Exception ex)
{
Logger.Error("Binding queue task completion threw exception " + ex.ToString());
}
finally
{
// set item processed to avoid deadlocks
if (lockTaken)
{
bindingContext.BindingLock.Set();
}
queueItem.ItemProcessed.Set();
}
});
}
catch (Exception ex)
{
// catch and log any exceptions raised in the binding calls
// set item processed to avoid deadlocks
Logger.Error("Binding queue threw exception " + ex.ToString());
// set item processed to avoid deadlocks
if (lockTaken)
{
bindingContext.BindingLock.Set();
}
queueItem.ItemProcessed.Set();
}
}, TaskContinuationOptions.None);
this.BindingContextTasks[bindingContext] = bindingContextTask.ContinueWith(
task => DispatchQueueItem(bindingContext, queueItem)
, TaskContinuationOptions.RunContinuationsAsynchronously);
// if a queue processing cancellation was requested then exit the loop
if (token.IsCancellationRequested)
{
break;
}
}
}
}
finally
{
@@ -450,6 +325,127 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
}
private void DispatchQueueItem(IBindingContext bindingContext, QueueItem queueItem)
{
bool lockTaken = false;
try
{
// prefer the queue item binding item, otherwise use the context default timeout - timeout is in milliseconds
int bindTimeoutInMs = queueItem.BindingTimeout ?? bindingContext.BindingTimeout;
// handle the case a previous binding operation is still running
if (!bindingContext.BindingLock.WaitOne(queueItem.WaitForLockTimeout ?? 0))
{
try
{
Logger.Warning("Binding queue operation timed out waiting for previous operation to finish");
queueItem.Result = queueItem.TimeoutOperation != null
? queueItem.TimeoutOperation(bindingContext)
: null;
}
catch (Exception ex)
{
Logger.Error("Exception running binding queue lock timeout handler: " + ex.ToString());
}
finally
{
queueItem.ItemProcessed.Set();
}
}
bindingContext.BindingLock.Reset();
lockTaken = true;
// execute the binding operation
object result = null;
CancellationTokenSource cancelToken = new CancellationTokenSource();
// run the operation in a separate thread
var bindTask = Task.Run(() =>
{
try
{
result = queueItem.BindOperation(
bindingContext,
cancelToken.Token);
}
catch (Exception ex)
{
Logger.Error("Unexpected exception on the binding queue: " + ex.ToString());
if (queueItem.ErrorHandler != null)
{
try
{
result = queueItem.ErrorHandler(ex);
}
catch (Exception ex2)
{
Logger.Error("Unexpected exception in binding queue error handler: " + ex2.ToString());
}
}
if (IsExceptionOfType(ex, typeof(SqlException)) || IsExceptionOfType(ex, typeof(SocketException)))
{
if (this.OnUnhandledException != null)
{
this.OnUnhandledException(queueItem.Key, ex);
}
RemoveBindingContext(queueItem.Key);
}
}
});
Task.Run(() =>
{
try
{
// check if the binding tasks completed within the binding timeout
if (bindTask.Wait(bindTimeoutInMs))
{
queueItem.Result = result;
}
else
{
cancelToken.Cancel();
// if the task didn't complete then call the timeout callback
if (queueItem.TimeoutOperation != null)
{
queueItem.Result = queueItem.TimeoutOperation(bindingContext);
}
bindTask.ContinueWithOnFaulted(t => Logger.Error("Binding queue threw exception " + t.Exception.ToString()));
// Give the task a chance to complete before moving on to the next operation
bindTask.Wait();
}
}
catch (Exception ex)
{
Logger.Error("Binding queue task completion threw exception " + ex.ToString());
}
finally
{
// set item processed to avoid deadlocks
if (lockTaken)
{
bindingContext.BindingLock.Set();
}
queueItem.ItemProcessed.Set();
}
});
}
catch (Exception ex)
{
// catch and log any exceptions raised in the binding calls
// set item processed to avoid deadlocks
Logger.Error("Binding queue threw exception " + ex.ToString());
// set item processed to avoid deadlocks
if (lockTaken)
{
bindingContext.BindingLock.Set();
}
queueItem.ItemProcessed.Set();
}
}
/// <summary>
/// Clear queued items
/// </summary>
@@ -487,7 +483,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
}
}
private bool IsExceptionOfType(Exception ex, Type t)
{
return ex.GetType() == t || (ex.InnerException != null && ex.InnerException.GetType() == t);

View File

@@ -107,18 +107,18 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
compatibilityLevel: DatabaseCompatibilityLevel.Current,
transactSqlVersion: TransactSqlVersion.Current);
private ConcurrentDictionary<string, bool> nonMssqlUriMap = new ConcurrentDictionary<string, bool>();
private ConcurrentDictionary<string, bool> nonMssqlUriMap = new();
private Lazy<Dictionary<string, ScriptParseInfo>> scriptParseInfoMap
= new Lazy<Dictionary<string, ScriptParseInfo>>(() => new Dictionary<string, ScriptParseInfo>());
private Lazy<ConcurrentDictionary<string, ScriptParseInfo>> scriptParseInfoMap
= new Lazy<ConcurrentDictionary<string, ScriptParseInfo>>(() => new());
private readonly ConcurrentDictionary<string, ICompletionExtension> completionExtensions = new ConcurrentDictionary<string, ICompletionExtension>();
private readonly ConcurrentDictionary<string, DateTime> extAssemblyLastUpdateTime = new ConcurrentDictionary<string, DateTime>();
private readonly ConcurrentDictionary<string, ICompletionExtension> completionExtensions = new();
private readonly ConcurrentDictionary<string, DateTime> extAssemblyLastUpdateTime = new();
/// <summary>
/// Gets a mapping dictionary for SQL file URIs to ScriptParseInfo objects
/// </summary>
internal Dictionary<string, ScriptParseInfo> ScriptParseInfoMap
internal ConcurrentDictionary<string, ScriptParseInfo> ScriptParseInfoMap
{
get
{
@@ -289,7 +289,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
WorkspaceServiceInstance.RegisterTextDocCloseCallback(HandleDidCloseTextDocumentNotification);
// Register a callback for when a connection is created
ConnectionServiceInstance.RegisterOnConnectionTask(UpdateLanguageServiceOnConnection);
ConnectionServiceInstance.RegisterOnConnectionTask(StartUpdateLanguageServiceOnConnection);
// Register a callback for when a connection is closed
ConnectionServiceInstance.RegisterOnDisconnectTask(RemoveAutoCompleteCacheUriReference);
@@ -521,23 +521,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
};
}
// turn off this code until needed (10/28/2016)
#if false
private async Task HandleReferencesRequest(
ReferencesParams referencesParams,
RequestContext<Location[]> requestContext)
{
await requestContext.SendResult(null);
}
private async Task HandleDocumentHighlightRequest(
TextDocumentPosition textDocumentPosition,
RequestContext<DocumentHighlight[]> requestContext)
{
await requestContext.SendResult(null);
}
#endif
internal async Task HandleSignatureHelpRequest(
TextDocumentPosition textDocumentPosition,
RequestContext<SignatureHelp> requestContext)
@@ -551,18 +534,26 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
ScriptFile scriptFile = CurrentWorkspace.GetFile(
textDocumentPosition.TextDocument.Uri);
SignatureHelp help = null;
if (scriptFile != null)
{
help = GetSignatureHelp(textDocumentPosition, scriptFile);
}
if (help != null)
{
await requestContext.SendResult(help);
}
else
{
await requestContext.SendResult(new SignatureHelp());
// Start task asynchronously without blocking main thread - this is by design.
// Explanation: STS message queues are single-threaded queues, which should be unblocked as soon as possible.
// All Long-running tasks should be performed in a non-blocking background task, and results should be sent when ready.
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
GetSignatureHelp(textDocumentPosition, scriptFile)
.ContinueWith(async task =>
{
var result = await task;
if (result != null)
{
await requestContext.SendResult(result);
}
else
{
await requestContext.SendResult(new SignatureHelp());
}
});
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
}
}
}
@@ -683,9 +674,33 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// <summary>
/// Handle the rebuild IntelliSense cache notification
/// </summary>
public async Task HandleRebuildIntelliSenseNotification(
/// <param name="rebuildParams">Rebuild params</param>
/// <param name="eventContext">Event context</param>
/// <returns>Async task</returns>
public Task HandleRebuildIntelliSenseNotification(
RebuildIntelliSenseParams rebuildParams,
EventContext eventContext)
{
// Start task asynchronously without blocking main thread - this is by design.
// Explanation: STS message queues are single-threaded queues, which should be unblocked as soon as possible.
// All Long-running tasks should be performed in a non-blocking background task, and results should be sent when ready.
Task.Factory.StartNew(async () =>
{
await DoHandleRebuildIntellisenseNotification(rebuildParams, eventContext);
}, CancellationToken.None,
TaskCreationOptions.None,
TaskScheduler.Default);
return Task.CompletedTask;
}
/// <summary>
/// Internal method to handle rebuild intellisense notification
/// </summary>
/// <param name="rebuildParams">Rebuild params</param>
/// <param name="eventContext">Event context</param>
/// <returns>Async task</returns>
public async Task DoHandleRebuildIntellisenseNotification(RebuildIntelliSenseParams rebuildParams, EventContext eventContext)
{
try
{
@@ -694,7 +709,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
// This URI doesn't come in escaped - so if it's a file path with reserved characters (such as %)
// then we'll fail to find it since GetFile expects the URI to be a fully-escaped URI as that's
// what the document events are sent in as.
var escapedOwnerUri = Uri.EscapeUriString(rebuildParams.OwnerUri);
var escapedOwnerUri = Uri.EscapeDataString(rebuildParams.OwnerUri);
// Skip closing this file if the file doesn't exist
var scriptFile = this.CurrentWorkspace.GetFile(escapedOwnerUri);
if (scriptFile == null)
@@ -710,47 +725,44 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
// check that there is an active connection for the current editor
if (connInfo != null)
{
await Task.Run(() =>
// Get the current ScriptInfo if one exists so we can lock it while we're rebuilding the cache
ScriptParseInfo scriptInfo = GetScriptParseInfo(connInfo.OwnerUri, createIfNotExists: false);
if (scriptInfo != null && scriptInfo.IsConnected &&
Monitor.TryEnter(scriptInfo.BuildingMetadataLock, LanguageService.OnConnectionWaitTimeout))
{
// Get the current ScriptInfo if one exists so we can lock it while we're rebuilding the cache
ScriptParseInfo scriptInfo = GetScriptParseInfo(connInfo.OwnerUri, createIfNotExists: false);
if (scriptInfo != null && scriptInfo.IsConnected &&
Monitor.TryEnter(scriptInfo.BuildingMetadataLock, LanguageService.OnConnectionWaitTimeout))
try
{
try
{
this.BindingQueue.AddConnectionContext(connInfo, featureName: Constants.LanguageServiceFeature, overwrite: true);
RemoveScriptParseInfo(rebuildParams.OwnerUri);
UpdateLanguageServiceOnConnection(connInfo).Wait();
}
catch (Exception ex)
{
Logger.Error("Unknown error " + ex.ToString());
}
finally
{
// Set Metadata Build event to Signal state.
Monitor.Exit(scriptInfo.BuildingMetadataLock);
}
this.BindingQueue.AddConnectionContext(connInfo, featureName: Constants.LanguageServiceFeature, overwrite: true);
RemoveScriptParseInfo(rebuildParams.OwnerUri);
await UpdateLanguageServiceOnConnection(connInfo);
}
catch (Exception ex)
{
Logger.Error("Unknown error " + ex.ToString());
}
finally
{
// Set Metadata Build event to Signal state.
Monitor.Exit(scriptInfo.BuildingMetadataLock);
}
// if not in the preview window and diagnostics are enabled then run diagnostics
if (!IsPreviewWindow(scriptFile)
&& CurrentWorkspaceSettings.IsDiagnosticsEnabled)
{
RunScriptDiagnostics(
new ScriptFile[] { scriptFile },
eventContext);
await RunScriptDiagnostics(
new ScriptFile[] { scriptFile },
eventContext);
}
// Send a notification to signal that autocomplete is ready
ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() { OwnerUri = connInfo.OwnerUri });
});
}
else
{
// Send a notification to signal that autocomplete is ready
await ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() { OwnerUri = rebuildParams.OwnerUri });
await ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() { OwnerUri = connInfo.OwnerUri });
}
else
{
// Send a notification to signal that autocomplete is ready
await ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() { OwnerUri = rebuildParams.OwnerUri });
}
}
}
catch (Exception ex)
@@ -887,121 +899,125 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// <param name="filePath"></param>
/// <param name="sqlText"></param>
/// <returns>The ParseResult instance returned from SQL Parser</returns>
public ParseResult ParseAndBind(ScriptFile scriptFile, ConnectionInfo connInfo)
public Task<ParseResult> ParseAndBind(ScriptFile scriptFile, ConnectionInfo connInfo)
{
Logger.Verbose($"ParseAndBind - {scriptFile}");
// get or create the current parse info object
ScriptParseInfo parseInfo = GetScriptParseInfo(scriptFile.ClientUri, createIfNotExists: true);
if (Monitor.TryEnter(parseInfo.BuildingMetadataLock, LanguageService.BindingTimeout))
return Task.Run(() =>
{
try
if (Monitor.TryEnter(parseInfo.BuildingMetadataLock, LanguageService.BindingTimeout))
{
if (connInfo == null || !parseInfo.IsConnected)
try
{
// parse on separate thread so stack size can be increased
var parseThread = new Thread(() =>
if (connInfo == null || !parseInfo.IsConnected)
{
try
{
// parse current SQL file contents to retrieve a list of errors
ParseResult parseResult = Parser.IncrementalParse(
scriptFile.Contents,
parseInfo.ParseResult,
this.DefaultParseOptions);
parseInfo.ParseResult = parseResult;
}
catch (Exception e)
{
// Log the exception but don't rethrow it to prevent parsing errors from crashing SQL Tools Service
Logger.Error(string.Format("An unexpected error occured while parsing: {0}", e.ToString()));
}
}, ConnectedBindingQueue.QueueThreadStackSize);
parseThread.Start();
parseThread.Join();
}
else
{
QueueItem queueItem = this.BindingQueue.QueueBindingOperation(
key: parseInfo.ConnectionKey,
bindingTimeout: LanguageService.BindingTimeout,
bindOperation: (bindingContext, cancelToken) =>
// parse on separate thread so stack size can be increased
var parseThread = new Thread(() =>
{
try
{
// parse current SQL file contents to retrieve a list of errors
ParseResult parseResult = Parser.IncrementalParse(
scriptFile.Contents,
parseInfo.ParseResult,
bindingContext.ParseOptions);
scriptFile.Contents,
parseInfo.ParseResult,
this.DefaultParseOptions);
parseInfo.ParseResult = parseResult;
List<ParseResult> parseResults = new List<ParseResult>();
parseResults.Add(parseResult);
if (bindingContext.IsConnected && bindingContext.Binder != null)
}
catch (Exception e)
{
// Log the exception but don't rethrow it to prevent parsing errors from crashing SQL Tools Service
Logger.Error(string.Format("An unexpected error occured while parsing: {0}", e.ToString()));
}
}, ConnectedBindingQueue.QueueThreadStackSize);
parseThread.Start();
parseThread.Join();
}
else
{
QueueItem queueItem = this.BindingQueue.QueueBindingOperation(
key: parseInfo.ConnectionKey,
bindingTimeout: LanguageService.BindingTimeout,
bindOperation: (bindingContext, cancelToken) =>
{
try
{
bindingContext.Binder.Bind(
parseResults,
connInfo.ConnectionDetails.DatabaseName,
BindMode.Batch);
ParseResult parseResult = Parser.IncrementalParse(
scriptFile.Contents,
parseInfo.ParseResult,
bindingContext.ParseOptions);
parseInfo.ParseResult = parseResult;
List<ParseResult> parseResults = new List<ParseResult>();
parseResults.Add(parseResult);
if (bindingContext.IsConnected && bindingContext.Binder != null)
{
bindingContext.Binder.Bind(
parseResults,
connInfo.ConnectionDetails.DatabaseName,
BindMode.Batch);
}
}
catch (ConnectionException)
{
Logger.Error("Hit connection exception while binding - disposing binder object...");
}
catch (SqlParserInternalBinderError)
{
Logger.Error("Hit connection exception while binding - disposing binder object...");
}
catch (Exception ex)
{
Logger.Error("Unknown exception during parsing " + ex.ToString());
}
}
catch (ConnectionException)
{
Logger.Error("Hit connection exception while binding - disposing binder object...");
}
catch (SqlParserInternalBinderError)
{
Logger.Error("Hit connection exception while binding - disposing binder object...");
}
catch (Exception ex)
{
Logger.Error("Unknown exception during parsing " + ex.ToString());
}
return null;
});
return null;
});
queueItem.ItemProcessed.WaitOne();
queueItem.ItemProcessed.WaitOne();
}
}
catch (Exception ex)
{
// reset the parse result to do a full parse next time
parseInfo.ParseResult = null;
Logger.Error("Unknown exception during parsing " + ex.ToString());
}
finally
{
Monitor.Exit(parseInfo.BuildingMetadataLock);
}
}
catch (Exception ex)
else
{
// reset the parse result to do a full parse next time
parseInfo.ParseResult = null;
Logger.Error("Unknown exception during parsing " + ex.ToString());
Logger.Warning("Binding metadata lock timeout in ParseAndBind");
}
finally
{
Monitor.Exit(parseInfo.BuildingMetadataLock);
}
}
else
{
Logger.Warning("Binding metadata lock timeout in ParseAndBind");
}
return parseInfo.ParseResult;
return parseInfo.ParseResult;
});
}
/// <summary>
/// Runs UpdateLanguageServiceOnConnection as a background task
/// </summary>
/// <param name="info">Connection Info</param>
/// <returns></returns>
public Task StartUpdateLanguageServiceOnConnection(ConnectionInfo info)
{
// Start task asynchronously without blocking main thread - this is by design.
// Explanation: STS message queues are single-threaded queues, which should be unblocked as soon as possible.
// All Long-running tasks should be performed in a non-blocking background task, and results should be sent when ready.
Task.Factory.StartNew(() => UpdateLanguageServiceOnConnection(info));
return Task.CompletedTask;
}
/// <summary>
/// Starts a Task to update the autocomplete metadata provider when the user connects to a database
/// </summary>
/// <param name="info">Connection info</param>
public Task UpdateLanguageServiceOnConnection(ConnectionInfo info)
{
return Task.Run(() =>
{
DoUpdateLanguageServiceOnConnection(info);
});
}
/// <summary>
/// Update the autocomplete metadata provider when the user connects to a database synchronously
/// </summary>
/// <param name="info">Connection info</param>
public void DoUpdateLanguageServiceOnConnection(ConnectionInfo info)
/// <param name="info"></param>
public async Task UpdateLanguageServiceOnConnection(ConnectionInfo info)
{
if (ConnectionService.IsDedicatedAdminConnection(info.ConnectionDetails))
{
@@ -1028,10 +1044,11 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
Monitor.Exit(scriptInfo.BuildingMetadataLock);
}
}
PrepopulateCommonMetadata(info, scriptInfo, this.BindingQueue);
// Send a notification to signal that autocomplete is ready
ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() { OwnerUri = info.OwnerUri });
await PrepopulateCommonMetadata(info, scriptInfo, this.BindingQueue).ContinueWith(async _ =>
{
// Send a notification to signal that autocomplete is ready
await ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() { OwnerUri = info.OwnerUri });
});
}
@@ -1043,7 +1060,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// </summary>
/// <param name="info"></param>
/// <param name="scriptInfo"></param>
internal void PrepopulateCommonMetadata(
internal async Task PrepopulateCommonMetadata(
ConnectionInfo info,
ScriptParseInfo scriptInfo,
ConnectedBindingQueue bindingQueue)
@@ -1060,71 +1077,73 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
return;
}
ParseAndBind(scriptFile, info);
if (Monitor.TryEnter(scriptInfo.BuildingMetadataLock, LanguageService.OnConnectionWaitTimeout))
await ParseAndBind(scriptFile, info).ContinueWith(t =>
{
try
if (Monitor.TryEnter(scriptInfo.BuildingMetadataLock, LanguageService.OnConnectionWaitTimeout))
{
QueueItem queueItem = bindingQueue.QueueBindingOperation(
key: scriptInfo.ConnectionKey,
bindingTimeout: PrepopulateBindTimeout,
waitForLockTimeout: PrepopulateBindTimeout,
bindOperation: (bindingContext, cancelToken) =>
{
// parse a simple statement that returns common metadata
ParseResult parseResult = Parser.Parse(
"select ",
bindingContext.ParseOptions);
if (bindingContext.IsConnected && bindingContext.Binder != null)
try
{
QueueItem queueItem = bindingQueue.QueueBindingOperation(
key: scriptInfo.ConnectionKey,
bindingTimeout: PrepopulateBindTimeout,
waitForLockTimeout: PrepopulateBindTimeout,
bindOperation: (bindingContext, cancelToken) =>
{
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,
bindingContext.MetadataDisplayInfoProvider);
// this forces lazy evaluation of the suggestion metadata
AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 8, 8);
parseResult = Parser.Parse(
"exec ",
// parse a simple statement that returns common metadata
ParseResult parseResult = Parser.Parse(
"select ",
bindingContext.ParseOptions);
if (bindingContext.IsConnected && bindingContext.Binder != null)
{
List<ParseResult> parseResults = new List<ParseResult>();
parseResults.Add(parseResult);
bindingContext.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
var suggestions = Resolver.FindCompletions(
parseResult, 1, 8,
bindingContext.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, 8, 8);
// this forces lazy evaluation of the suggestion metadata
AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 6, 6);
}
return null;
});
parseResult = Parser.Parse(
"exec ",
bindingContext.ParseOptions);
queueItem.ItemProcessed.WaitOne();
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,
bindingContext.MetadataDisplayInfoProvider);
// this forces lazy evaluation of the suggestion metadata
AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 6, 6);
}
return null;
});
queueItem.ItemProcessed.WaitOne();
}
catch (Exception ex)
{
Logger.Error("Exception in PrepopulateCommonMetadata " + ex.ToString());
}
finally
{
Monitor.Exit(scriptInfo.BuildingMetadataLock);
}
}
catch
{
}
finally
{
Monitor.Exit(scriptInfo.BuildingMetadataLock);
}
}
});
}
}
@@ -1348,7 +1367,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
if (RequiresReparse(scriptParseInfo, scriptFile))
{
scriptParseInfo.ParseResult = ParseAndBind(scriptFile, connInfo);
scriptParseInfo.ParseResult = ParseAndBind(scriptFile, connInfo).GetAwaiter().GetResult();
}
// Get token from selected text
@@ -1499,7 +1518,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// <summary>
/// Get function signature help for the current position
/// </summary>
internal SignatureHelp? GetSignatureHelp(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile)
internal async Task<SignatureHelp?> GetSignatureHelp(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile)
{
Logger.Verbose($"GetSignatureHelp - {scriptFile}");
int startLine = textDocumentPosition.Position.Line;
@@ -1522,8 +1541,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
// reparse and bind the SQL statement if needed
if (RequiresReparse(scriptParseInfo, scriptFile))
{
ParseAndBind(scriptFile, connInfo);
} else
await ParseAndBind(scriptFile, connInfo);
}
else
{
Logger.Verbose($"GetSignatureHelp - No reparse needed for {scriptFile}");
}
@@ -1570,6 +1590,10 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
Logger.Verbose($"GetSignatureHelp - Got result {queueItem.Result}");
return queueItem.GetResultAsT<SignatureHelp>();
}
catch (Exception ex)
{
Logger.Error("Exception in GetSignatureHelp " + ex.ToString());
}
finally
{
Monitor.Exit(scriptParseInfo.BuildingMetadataLock);
@@ -1615,7 +1639,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
// reparse and bind the SQL statement if needed
if (RequiresReparse(scriptParseInfo, scriptFile))
{
ParseAndBind(scriptFile, connInfo);
await ParseAndBind(scriptFile, connInfo);
}
// if the parse failed then return the default list
@@ -1693,14 +1717,14 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// Gets a list of semantic diagnostic marks for the provided script file
/// </summary>
/// <param name="scriptFile"></param>
internal ScriptFileMarker[] GetSemanticMarkers(ScriptFile scriptFile)
internal async Task<ScriptFileMarker[]> GetSemanticMarkers(ScriptFile scriptFile)
{
ConnectionInfo connInfo;
ConnectionServiceInstance.TryFindConnection(
scriptFile.ClientUri,
out connInfo);
var parseResult = ParseAndBind(scriptFile, connInfo);
var parseResult = await ParseAndBind(scriptFile, connInfo);
// build a list of SQL script file markers from the errors
List<ScriptFileMarker> markers = new List<ScriptFileMarker>();
@@ -1835,10 +1859,18 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
Logger.Verbose("Analyzing script file: " + scriptFile.FilePath);
ScriptFileMarker[] semanticMarkers = GetSemanticMarkers(scriptFile);
Logger.Verbose("Analysis complete.");
await DiagnosticsHelper.PublishScriptDiagnostics(scriptFile, semanticMarkers, eventContext);
// Start task asynchronously without blocking main thread - this is by design.
// Explanation: STS message queues are single-threaded queues, which should be unblocked as soon as possible.
// All Long-running tasks should be performed in a non-blocking background task, and results should be sent when ready.
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
GetSemanticMarkers(scriptFile).ContinueWith(async t =>
{
var semanticMarkers = t.GetAwaiter().GetResult();
Logger.Verbose("Analysis complete.");
await DiagnosticsHelper.PublishScriptDiagnostics(scriptFile, semanticMarkers, eventContext);
}, CancellationToken.None);
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
}
}
@@ -1861,7 +1893,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
else
{
Logger.Verbose($"Adding ScriptParseInfo for uri {uri}");
this.ScriptParseInfoMap.Add(uri, scriptInfo);
this.ScriptParseInfoMap.TryAdd(uri, scriptInfo);
}
}
@@ -1887,7 +1919,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
Logger.Verbose($"ScriptParseInfo for uri {uri} did not exist, creating new one");
// create a new script parse info object and initialize with the current settings
ScriptParseInfo scriptInfo = new ScriptParseInfo();
this.ScriptParseInfoMap.Add(uri, scriptInfo);
this.ScriptParseInfoMap.TryAdd(uri, scriptInfo);
return scriptInfo;
}
else
@@ -1902,15 +1934,8 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
lock (this.parseMapLock)
{
if (this.ScriptParseInfoMap.ContainsKey(uri))
{
Logger.Verbose($"Removing ScriptParseInfo for uri {uri}");
return this.ScriptParseInfoMap.Remove(uri);
}
else
{
return false;
}
Logger.Verbose($"Removing ScriptParseInfo for uri {uri}");
return this.ScriptParseInfoMap.TryRemove(uri, out _);
}
}

View File

@@ -1064,22 +1064,19 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
// what the document events are sent in as.
var escapedOwnerUri = Uri.EscapeUriString(request.OwnerUri);
// If it is a document selection, we'll retrieve the text from the document
ExecuteDocumentSelectionParams docRequest = request as ExecuteDocumentSelectionParams;
if (docRequest != null)
if (request is ExecuteDocumentSelectionParams docRequest)
{
return GetSqlTextFromSelectionData(escapedOwnerUri, docRequest.QuerySelection);
}
// If it is a document statement, we'll retrieve the text from the document
ExecuteDocumentStatementParams stmtRequest = request as ExecuteDocumentStatementParams;
if (stmtRequest != null)
if (request is ExecuteDocumentStatementParams stmtRequest)
{
return GetSqlStatementAtPosition(escapedOwnerUri, stmtRequest.Line, stmtRequest.Column);
}
// If it is an ExecuteStringParams, return the text as is
ExecuteStringParams stringRequest = request as ExecuteStringParams;
if (stringRequest != null)
if (request is ExecuteStringParams stringRequest)
{
return stringRequest.Query;
}

View File

@@ -15,6 +15,7 @@ using Microsoft.SqlTools.Utility;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
using System.Runtime.InteropServices;
using Microsoft.SqlTools.ServiceLayer.Utility;
using System.Collections.Concurrent;
namespace Microsoft.SqlTools.ServiceLayer.Workspace
{
@@ -35,7 +36,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace
"vscode-notebook-cell"
};
private Dictionary<string, ScriptFile> workspaceFiles = new Dictionary<string, ScriptFile>();
private ConcurrentDictionary<string, ScriptFile> workspaceFiles = new ConcurrentDictionary<string, ScriptFile>();
#endregion
@@ -121,7 +122,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace
{
scriptFile = new ScriptFile(resolvedFile.FilePath, resolvedFile.ClientUri,streamReader);
this.workspaceFiles.Add(keyName, scriptFile);
this.workspaceFiles.TryAdd(keyName, scriptFile);
}
Logger.Verbose("Opened file on disk: " + resolvedFile.FilePath);
@@ -235,7 +236,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace
{
scriptFile = new ScriptFile(resolvedFile.FilePath, resolvedFile.ClientUri, initialBuffer);
this.workspaceFiles.Add(keyName, scriptFile);
this.workspaceFiles.TryAdd(keyName, scriptFile);
Logger.Verbose("Opened file as in-memory buffer: " + resolvedFile.FilePath);
}
@@ -260,7 +261,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace
{
Validate.IsNotNull("scriptFile", scriptFile);
this.workspaceFiles.Remove(scriptFile.Id);
this.workspaceFiles.TryRemove(scriptFile.Id, out _);
}
internal string GetBaseFilePath(string filePath)

View File

@@ -177,7 +177,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer
requestContext.Verify(x => x.SendError(It.IsAny<string>(), 0, It.IsAny<string>()), Times.Once);
ScriptParseInfo scriptInfo = new ScriptParseInfo { IsConnected = true };
autoCompleteService.ParseAndBind(result.ScriptFile, result.ConnectionInfo);
await autoCompleteService.ParseAndBind(result.ScriptFile, result.ConnectionInfo);
scriptInfo.ConnectionKey = autoCompleteService.BindingQueue.AddConnectionContext(result.ConnectionInfo);
//Invoke auto completion with extension enabled
@@ -219,7 +219,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer
/// provide signature help.
/// </summary>
[Test]
public void GetSignatureHelpReturnsNotNullIfParseInfoInitialized()
public async Task GetSignatureHelpReturnsNotNullIfParseInfoInitialized()
{
// When we make a connection to a live database
Hosting.ServiceHost.SendEventIgnoreExceptions = true;
@@ -243,14 +243,14 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer
// If the SQL has already been parsed
var service = CreateLanguageService(result.ScriptFile);
service.DoUpdateLanguageServiceOnConnection(result.ConnectionInfo);
await service.UpdateLanguageServiceOnConnection(result.ConnectionInfo);
// We should get back a non-null ScriptParseInfo
ScriptParseInfo? parseInfo = service.GetScriptParseInfo(result.ScriptFile.ClientUri);
Assert.That(parseInfo, Is.Not.Null, "ScriptParseInfo");
// And we should get back a non-null SignatureHelp
SignatureHelp? signatureHelp = service.GetSignatureHelp(textDocument, result.ScriptFile);
SignatureHelp? signatureHelp = await service.GetSignatureHelp(textDocument, result.ScriptFile);
Assert.That(signatureHelp, Is.Not.Null, "SignatureHelp");
}
@@ -319,7 +319,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer
testDb.RunQuery("CREATE TABLE dbo.foo(col1 int)");
// And refresh the cache
await langService.HandleRebuildIntelliSenseNotification(
await langService.DoHandleRebuildIntellisenseNotification(
new RebuildIntelliSenseParams() { OwnerUri = connectionInfoResult.ScriptFile.ClientUri },
new TestEventContext());
@@ -446,7 +446,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer
testDb.RunQuery(createTableQueries);
// And refresh the cache
await langService.HandleRebuildIntelliSenseNotification(
await langService.DoHandleRebuildIntellisenseNotification(
new RebuildIntelliSenseParams() { OwnerUri = connectionInfoResult.ScriptFile.ClientUri },
new TestEventContext());

View File

@@ -259,7 +259,7 @@ GO";
scriptFile.Contents = "select * from dbo.func ()";
ScriptParseInfo scriptInfo = new ScriptParseInfo { IsConnected = true };
languageService.ScriptParseInfoMap.Add(scriptFile.ClientUri, scriptInfo);
languageService.ScriptParseInfoMap.TryAdd(scriptFile.ClientUri, scriptInfo);
// Pass in null connection info to force doing a local parse since that hits the BindingQueue timeout
// before we want it to (this is testing the timeout trying to fetch the definitions after the parse)
@@ -729,9 +729,9 @@ GO";
Thread.Sleep(2000);
ScriptParseInfo scriptInfo = new ScriptParseInfo { IsConnected = true };
service.ParseAndBind(scriptFile, connInfo);
await service.ParseAndBind(scriptFile, connInfo);
scriptInfo.ConnectionKey = bindingQueue.AddConnectionContext(connInfo);
service.ScriptParseInfoMap.Add(OwnerUri, scriptInfo);
service.ScriptParseInfoMap.TryAdd(OwnerUri, scriptInfo);
// When I call the language service
var objectResult = service.GetDefinition(objectDocument, scriptFile, connInfo);
@@ -750,7 +750,7 @@ GO";
Cleanup(objectResult.Locations);
Cleanup(sysResult.Locations);
Cleanup(masterResult.Locations);
service.ScriptParseInfoMap.Remove(OwnerUri);
service.ScriptParseInfoMap.TryRemove(OwnerUri, out _);
connInfo.RemoveAllConnections();
}
@@ -786,9 +786,9 @@ GO";
Thread.Sleep(2000);
ScriptParseInfo scriptInfo = new ScriptParseInfo { IsConnected = true };
service.ParseAndBind(scriptFile, connInfo);
await service.ParseAndBind(scriptFile, connInfo);
scriptInfo.ConnectionKey = bindingQueue.AddConnectionContext(connInfo);
service.ScriptParseInfoMap.Add(TestUri, scriptInfo);
service.ScriptParseInfoMap.TryAdd(TestUri, scriptInfo);
// When I call the language service
var fnResult = service.GetDefinition(fnDocument, scriptFile, connInfo);
@@ -807,7 +807,7 @@ GO";
Cleanup(fnResult.Locations);
Cleanup(sysResult.Locations);
Cleanup(masterResult.Locations);
service.ScriptParseInfoMap.Remove(TestUri);
service.ScriptParseInfoMap.TryRemove(TestUri, out _);
connInfo.RemoveAllConnections();
}

View File

@@ -218,7 +218,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
};
ParseResult parseResult = langService.ParseAndBind(scriptFile, null);
ParseResult parseResult = langService.ParseAndBind(scriptFile, null).GetAwaiter().GetResult();
ScriptParseInfo scriptParseInfo = langService.GetScriptParseInfo(scriptFile.ClientUri, true);
return new ScriptDocumentInfo(textDocumentPosition, scriptFile, scriptParseInfo);

View File

@@ -26,17 +26,17 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
public void LatestSqlParserIsUsedByDefault()
{
// This should only parse correctly on SQL server 2016 or newer
const string sql2016Text =
const string sql2016Text =
@"CREATE SECURITY POLICY [FederatedSecurityPolicy]" + "\r\n" +
@"ADD FILTER PREDICATE [rls].[fn_securitypredicate]([CustomerId])" + "\r\n" +
@"ADD FILTER PREDICATE [rls].[fn_securitypredicate]([CustomerId])" + "\r\n" +
@"ON [dbo].[Customer];";
LanguageService service = TestObjects.GetTestLanguageService();
// parse
var scriptFile = new ScriptFile();
scriptFile.SetFileContents(sql2016Text);
ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile);
ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile).GetAwaiter().GetResult();
// verify that no errors are detected
Assert.AreEqual(0, fileMarkers.Length);
@@ -57,7 +57,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
// parse the sql statement
var scriptFile = new ScriptFile();
scriptFile.SetFileContents(sqlWithErrors);
ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile);
ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile).GetAwaiter().GetResult();
// verify there are no errors
Assert.AreEqual(0, fileMarkers.Length);
@@ -78,7 +78,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
// parse sql statement
var scriptFile = new ScriptFile();
scriptFile.SetFileContents(sqlWithErrors);
ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile);
ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile).GetAwaiter().GetResult();
// verify there is one error
Assert.AreEqual(1, fileMarkers.Length);
@@ -97,7 +97,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
public void ParseMultilineSqlWithErrors()
{
// multiline sql with errors
const string sqlWithErrors =
const string sqlWithErrors =
"SELECT *** FROM sys.objects;\n" +
"GO\n" +
"SELECT *** FROM sys.objects;\n";
@@ -108,7 +108,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
// parse sql
var scriptFile = new ScriptFile();
scriptFile.SetFileContents(sqlWithErrors);
ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile);
ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile).GetAwaiter().GetResult();
// verify there are two errors
Assert.AreEqual(2, fileMarkers.Length);
@@ -140,29 +140,29 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
scriptFile.SetFileContents(docContent);
// When requesting SignatureHelp
SignatureHelp signatureHelp = service.GetSignatureHelp(TestObjects.GetTestDocPosition(), scriptFile);
SignatureHelp signatureHelp = service.GetSignatureHelp(TestObjects.GetTestDocPosition(), scriptFile).GetAwaiter().GetResult();
// Then null is returned as no parse info can be used to find the signature
Assert.Null(signatureHelp);
}
[Test]
public void EmptyCompletionListTest()
{
{
Assert.AreEqual(0, AutoCompleteHelper.EmptyCompletionList.Length);
}
internal sealed class TestScriptDocumentInfo : ScriptDocumentInfo
{
public TestScriptDocumentInfo(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile, ScriptParseInfo scriptParseInfo,
public TestScriptDocumentInfo(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile, ScriptParseInfo scriptParseInfo,
string tokenText = null)
:base(textDocumentPosition, scriptFile, scriptParseInfo)
: base(textDocumentPosition, scriptFile, scriptParseInfo)
{
this.tokenText = string.IsNullOrEmpty(tokenText) ? "doesntmatchanythingintheintellisensedefaultlist" : tokenText;
}
private string tokenText;
public override string TokenText
{
get
@@ -174,7 +174,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
[Test]
public void GetDefaultCompletionListWithNoMatchesTest()
{
{
var scriptFile = new ScriptFile();
scriptFile.SetFileContents("koko wants a bananas");
@@ -183,10 +183,10 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
var scriptDocumentInfo = new TestScriptDocumentInfo(
new TextDocumentPosition()
{
TextDocument = new TextDocumentIdentifier() { Uri = TestObjects.ScriptUri },
TextDocument = new TextDocumentIdentifier() { Uri = TestObjects.ScriptUri },
Position = new Position() { Line = 0, Character = 0 }
}, scriptFile, scriptInfo);
AutoCompleteHelper.GetDefaultCompletionItems(scriptDocumentInfo, false);
}