// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // using System; using System.Collections.Generic; using System.Linq; using 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.Common; using Microsoft.SqlServer.Management.SqlParser.Intellisense; using Microsoft.SqlServer.Management.SqlParser.Parser; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; 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 Location = Microsoft.SqlTools.ServiceLayer.Workspace.Contracts.Location; namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { /// /// Main class for Language Service functionality including anything that requires knowledge of /// the language to perform, such as definitions, intellisense, etc. /// public sealed class LanguageService { private const int OneSecond = 1000; internal const string DefaultBatchSeperator = "GO"; internal const int DiagnosticParseDelay = 750; internal const int HoverTimeout = 500; internal const int BindingTimeout = 500; internal const int OnConnectionWaitTimeout = 300 * OneSecond; internal const int PeekDefinitionTimeout = 10 * OneSecond; private static ConnectionService connectionService = null; private static WorkspaceService workspaceServiceInstance; private object parseMapLock = new object(); private ScriptParseInfo currentCompletionParseInfo; private ConnectedBindingQueue bindingQueue = new ConnectedBindingQueue(); private ParseOptions defaultParseOptions = new ParseOptions( batchSeparator: LanguageService.DefaultBatchSeperator, isQuotedIdentifierSet: true, compatibilityLevel: DatabaseCompatibilityLevel.Current, transactSqlVersion: TransactSqlVersion.Current); /// /// Gets or sets the binding queue instance /// Internal for testing purposes only /// internal ConnectedBindingQueue BindingQueue { get { return this.bindingQueue; } set { this.bindingQueue = value; } } /// /// Internal for testing purposes only /// internal static ConnectionService ConnectionServiceInstance { get { if (connectionService == null) { connectionService = ConnectionService.Instance; } return connectionService; } set { connectionService = value; } } #region Singleton Instance Implementation private static readonly Lazy instance = new Lazy(() => new LanguageService()); private Lazy> scriptParseInfoMap = new Lazy>(() => new Dictionary()); /// /// Gets a mapping dictionary for SQL file URIs to ScriptParseInfo objects /// internal Dictionary ScriptParseInfoMap { get { return this.scriptParseInfoMap.Value; } } /// /// Gets the singleton instance object /// public static LanguageService Instance { get { return instance.Value; } } private ParseOptions DefaultParseOptions { get { return this.defaultParseOptions; } } /// /// Default, parameterless constructor. /// internal LanguageService() { } #endregion #region Properties private static CancellationTokenSource ExistingRequestCancellation { get; set; } /// /// Gets the current settings /// internal SqlToolsSettings CurrentSettings { get { return WorkspaceService.Instance.CurrentSettings; } } /// /// Gets or sets the current workspace service instance /// Setter for internal testing purposes only /// internal static WorkspaceService WorkspaceServiceInstance { get { if (LanguageService.workspaceServiceInstance == null) { LanguageService.workspaceServiceInstance = WorkspaceService.Instance; } return LanguageService.workspaceServiceInstance; } set { LanguageService.workspaceServiceInstance = value; } } /// /// Gets the current workspace instance /// internal Workspace.Workspace CurrentWorkspace { get { return LanguageService.WorkspaceServiceInstance.Workspace; } } /// /// Gets or sets the current SQL Tools context /// /// internal SqlToolsContext Context { get; set; } #endregion #region Public Methods /// /// Initializes the Language Service instance /// /// /// public void InitializeService(ServiceHost serviceHost, SqlToolsContext context) { // Register the requests that this service will handle // turn off until needed (10/28/2016) // serviceHost.SetRequestHandler(ReferencesRequest.Type, HandleReferencesRequest); // serviceHost.SetRequestHandler(DocumentHighlightRequest.Type, HandleDocumentHighlightRequest); serviceHost.SetRequestHandler(SignatureHelpRequest.Type, HandleSignatureHelpRequest); serviceHost.SetRequestHandler(CompletionResolveRequest.Type, HandleCompletionResolveRequest); serviceHost.SetRequestHandler(HoverRequest.Type, HandleHoverRequest); serviceHost.SetRequestHandler(CompletionRequest.Type, HandleCompletionRequest); serviceHost.SetRequestHandler(DefinitionRequest.Type, HandleDefinitionRequest); // Register a no-op shutdown task for validation of the shutdown logic serviceHost.RegisterShutdownTask(async (shutdownParams, shutdownRequestContext) => { Logger.Write(LogLevel.Verbose, "Shutting down language service"); await Task.FromResult(0); }); // Register the configuration update handler WorkspaceService.Instance.RegisterConfigChangeCallback(HandleDidChangeConfigurationNotification); // Register the file change update handler WorkspaceService.Instance.RegisterTextDocChangeCallback(HandleDidChangeTextDocumentNotification); // Register the file open update handler WorkspaceService.Instance.RegisterTextDocOpenCallback(HandleDidOpenTextDocumentNotification); // Register a callback for when a connection is created ConnectionServiceInstance.RegisterOnConnectionTask(UpdateLanguageServiceOnConnection); // Register a callback for when a connection is closed ConnectionServiceInstance.RegisterOnDisconnectTask(RemoveAutoCompleteCacheUriReference); // Store the SqlToolsContext for future use Context = context; } #endregion #region Request Handlers /// /// Auto-complete completion provider request callback /// /// /// /// internal static async Task HandleCompletionRequest( TextDocumentPosition textDocumentPosition, RequestContext requestContext) { // check if Intellisense suggestions are enabled if (!WorkspaceService.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; LanguageService.ConnectionServiceInstance.TryFindConnection( scriptFile.ClientFilePath, out connInfo); var completionItems = Instance.GetCompletionItems( textDocumentPosition, scriptFile, connInfo); await requestContext.SendResult(completionItems); } } /// /// Handle the resolve completion request event to provide additional /// autocomplete metadata to the currently select completion item /// /// /// /// private static async Task HandleCompletionResolveRequest( CompletionItem completionItem, RequestContext requestContext) { // check if Intellisense suggestions are enabled if (!WorkspaceService.Instance.CurrentSettings.IsSuggestionsEnabled) { await Task.FromResult(true); } else { completionItem = LanguageService.Instance.ResolveCompletionItem(completionItem); await requestContext.SendResult(completionItem); } } internal static async Task HandleDefinitionRequest(TextDocumentPosition textDocumentPosition, RequestContext requestContext) { if (WorkspaceService.Instance.CurrentSettings.IsIntelliSenseEnabled) { // Retrieve document and connection ConnectionInfo connInfo; var scriptFile = LanguageService.WorkspaceServiceInstance.Workspace.GetFile(textDocumentPosition.TextDocument.Uri); LanguageService.ConnectionServiceInstance.TryFindConnection(scriptFile.ClientFilePath, out connInfo); Location[] locations = LanguageService.Instance.GetDefinition(textDocumentPosition, scriptFile, connInfo); if (locations != null) { await requestContext.SendResult(locations); } } } // turn off this code until needed (10/28/2016) #if false private static async Task HandleReferencesRequest( ReferencesParams referencesParams, RequestContext requestContext) { await Task.FromResult(true); } private static async Task HandleDocumentHighlightRequest( TextDocumentPosition textDocumentPosition, RequestContext requestContext) { await Task.FromResult(true); } #endif private static async Task HandleSignatureHelpRequest( TextDocumentPosition textDocumentPosition, RequestContext requestContext) { // check if Intellisense suggestions are enabled if (!WorkspaceService.Instance.CurrentSettings.IsSuggestionsEnabled) { await Task.FromResult(true); } else { ScriptFile scriptFile = WorkspaceService.Instance.Workspace.GetFile( textDocumentPosition.TextDocument.Uri); SignatureHelp help = LanguageService.Instance.GetSignatureHelp(textDocumentPosition, scriptFile); if (help != null) { await requestContext.SendResult(help); } else { await requestContext.SendResult(new SignatureHelp()); } } } private static async Task HandleHoverRequest( TextDocumentPosition textDocumentPosition, RequestContext requestContext) { // check if Quick Info hover tooltips are enabled if (WorkspaceService.Instance.CurrentSettings.IsQuickInfoEnabled) { var scriptFile = WorkspaceService.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 #region Handlers for Events from Other Services /// /// Handle the file open notification /// /// /// /// public async Task HandleDidOpenTextDocumentNotification( ScriptFile scriptFile, EventContext eventContext) { // if not in the preview window and diagnostics are enabled then run diagnostics if (!IsPreviewWindow(scriptFile) && WorkspaceService.Instance.CurrentSettings.IsDiagnositicsEnabled) { await RunScriptDiagnostics( new ScriptFile[] { scriptFile }, eventContext); } await Task.FromResult(true); } /// /// Handles text document change events /// /// /// public async Task HandleDidChangeTextDocumentNotification(ScriptFile[] changedFiles, EventContext eventContext) { if (WorkspaceService.Instance.CurrentSettings.IsDiagnositicsEnabled) { await this.RunScriptDiagnostics( changedFiles.ToArray(), eventContext); } await Task.FromResult(true); } /// /// Handle the file configuration change notification /// /// /// /// public async Task HandleDidChangeConfigurationNotification( SqlToolsSettings newSettings, SqlToolsSettings oldSettings, EventContext eventContext) { bool oldEnableIntelliSense = oldSettings.SqlTools.IntelliSense.EnableIntellisense; bool? oldEnableDiagnostics = oldSettings.SqlTools.IntelliSense.EnableErrorChecking; // 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.IntelliSense.EnableIntellisense || oldEnableDiagnostics != newSettings.SqlTools.IntelliSense.EnableErrorChecking) { // if the user just turned off diagnostics then send an event to clear the error markers if (!newSettings.IsDiagnositicsEnabled) { ScriptFileMarker[] emptyAnalysisDiagnostics = new ScriptFileMarker[0]; foreach (var scriptFile in WorkspaceService.Instance.Workspace.GetOpenedFiles()) { await DiagnosticsHelper.PublishScriptDiagnostics(scriptFile, emptyAnalysisDiagnostics, eventContext); } } // otherwise rerun diagnostic analysis on all opened SQL files else { await this.RunScriptDiagnostics(CurrentWorkspace.GetOpenedFiles(), eventContext); } } } #endregion #region "AutoComplete Provider methods" /// /// Remove a reference to an autocomplete cache from a URI. If /// it is the last URI connected to a particular connection, /// then remove the cache. /// 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); } /// /// Parses the SQL text and binds it to the SMO metadata provider if connected /// /// /// /// The ParseResult instance returned from SQL Parser public ParseResult ParseAndBind(ScriptFile scriptFile, ConnectionInfo connInfo) { // get or create the current parse info object ScriptParseInfo parseInfo = GetScriptParseInfo(scriptFile.ClientFilePath, createIfNotExists: true); if (Monitor.TryEnter(parseInfo.BuildingMetadataLock, LanguageService.BindingTimeout)) { try { if (connInfo == null || !parseInfo.IsConnected) { // 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 parseResults = new List(); 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 { Monitor.Exit(parseInfo.BuildingMetadataLock); } } else { Logger.Write(LogLevel.Warning, "Binding metadata lock timeout in ParseAndBind"); } return parseInfo.ParseResult; } /// /// Update the autocomplete metadata provider when the user connects to a database /// /// public async Task UpdateLanguageServiceOnConnection(ConnectionInfo info) { await Task.Run(() => { ScriptParseInfo scriptInfo = GetScriptParseInfo(info.OwnerUri, createIfNotExists: true); if (Monitor.TryEnter(scriptInfo.BuildingMetadataLock, LanguageService.OnConnectionWaitTimeout)) { try { 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) Monitor.Exit(scriptInfo.BuildingMetadataLock); } } AutoCompleteHelper.PrepopulateCommonMetadata(info, scriptInfo, this.BindingQueue); // Send a notification to signal that autocomplete is ready ServiceHost.Instance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() {OwnerUri = info.OwnerUri}); }); } /// /// Determines whether a reparse and bind is required to provide autocomplete /// /// private bool RequiresReparse(ScriptParseInfo info, ScriptFile scriptFile) { if (info.ParseResult == null) { return true; } string prevSqlText = info.ParseResult.Script.Sql; string currentSqlText = scriptFile.Contents; return prevSqlText.Length != currentSqlText.Length || !string.Equals(prevSqlText, currentSqlText); } /// /// Resolves the details and documentation for a completion item /// /// internal CompletionItem ResolveCompletionItem(CompletionItem completionItem) { var scriptParseInfo = LanguageService.Instance.currentCompletionParseInfo; if (scriptParseInfo != null && scriptParseInfo.CurrentSuggestions != null) { if (Monitor.TryEnter(scriptParseInfo.BuildingMetadataLock)) { try { QueueItem queueItem = this.BindingQueue.QueueBindingOperation( key: scriptParseInfo.ConnectionKey, bindingTimeout: LanguageService.BindingTimeout, bindOperation: (bindingContext, cancelToken) => { foreach (var suggestion in scriptParseInfo.CurrentSuggestions) { if (string.Equals(suggestion.Title, completionItem.Label)) { completionItem.Detail = suggestion.DatabaseQualifiedName; completionItem.Documentation = suggestion.Description; break; } } return completionItem; }); queueItem.ItemProcessed.WaitOne(); } 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()); } finally { Monitor.Exit(scriptParseInfo.BuildingMetadataLock); } } } return completionItem; } /// /// Get definition for a selected sql object using SMO Scripting /// /// /// /// /// Location with the URI of the script file internal Location[] GetDefinition(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile, ConnectionInfo connInfo) { // Parse sql ScriptParseInfo scriptParseInfo = GetScriptParseInfo(textDocumentPosition.TextDocument.Uri); if (scriptParseInfo == null) { return null; } if (RequiresReparse(scriptParseInfo, scriptFile)) { scriptParseInfo.ParseResult = ParseAndBind(scriptFile, connInfo); } // Get token from selected text Token selectedToken = GetToken(scriptParseInfo, textDocumentPosition.Position.Line + 1, textDocumentPosition.Position.Character); if (selectedToken == null) { return null; } // Strip "[" and "]"(if present) from the token text to enable matching with the suggestions. // The suggestion title does not contain any sql punctuation string tokenText = TextUtilities.RemoveSquareBracketSyntax(selectedToken.Text); if (scriptParseInfo.IsConnected && Monitor.TryEnter(scriptParseInfo.BuildingMetadataLock)) { try { // Queue the task with the binding queue QueueItem queueItem = this.BindingQueue.QueueBindingOperation( key: scriptParseInfo.ConnectionKey, bindingTimeout: LanguageService.PeekDefinitionTimeout, bindOperation: (bindingContext, cancelToken) => { // Get suggestions for the token int parserLine = textDocumentPosition.Position.Line + 1; int parserColumn = textDocumentPosition.Position.Character + 1; IEnumerable declarationItems = Resolver.FindCompletions( scriptParseInfo.ParseResult, parserLine, parserColumn, bindingContext.MetadataDisplayInfoProvider); // Match token with the suggestions(declaration items) returned string schemaName = this.GetSchemaName(scriptParseInfo, textDocumentPosition.Position, scriptFile); PeekDefinition peekDefinition = new PeekDefinition(connInfo); return peekDefinition.GetScript(declarationItems, tokenText, schemaName); }); // wait for the queue item queueItem.ItemProcessed.WaitOne(); return queueItem.GetResultAsT(); } finally { Monitor.Exit(scriptParseInfo.BuildingMetadataLock); } } return null; } /// /// Extract schema name for a token, if present /// /// /// /// /// schema nama private string GetSchemaName(ScriptParseInfo scriptParseInfo, Position position, ScriptFile scriptFile) { // Offset index by 1 for sql parser int startLine = position.Line + 1; int startColumn = position.Character + 1; // Get schema name if (scriptParseInfo != null && scriptParseInfo.ParseResult != null && scriptParseInfo.ParseResult.Script != null && scriptParseInfo.ParseResult.Script.Tokens != null) { var tokenIndex = scriptParseInfo.ParseResult.Script.TokenManager.FindToken(startLine, startColumn); var prevTokenIndex = scriptParseInfo.ParseResult.Script.TokenManager.GetPreviousSignificantTokenIndex(tokenIndex); var prevTokenText = scriptParseInfo.ParseResult.Script.TokenManager.GetText(prevTokenIndex); if (prevTokenText != null && prevTokenText.Equals(".")) { var schemaTokenIndex = scriptParseInfo.ParseResult.Script.TokenManager.GetPreviousSignificantTokenIndex(prevTokenIndex); Token schemaToken = scriptParseInfo.ParseResult.Script.TokenManager.GetToken(schemaTokenIndex); return TextUtilities.RemoveSquareBracketSyntax(schemaToken.Text); } } // if no schema name, returns null return null; } /// /// Get quick info hover tooltips for the current position /// /// /// 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 (Monitor.TryEnter(scriptParseInfo.BuildingMetadataLock)) { 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(); } finally { Monitor.Exit(scriptParseInfo.BuildingMetadataLock); } } } // return null if there isn't a tooltip for the current location return null; } /// /// Get function signature help for the current position /// internal SignatureHelp GetSignatureHelp(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); ConnectionInfo connInfo; LanguageService.ConnectionServiceInstance.TryFindConnection( scriptFile.ClientFilePath, out connInfo); // reparse and bind the SQL statement if needed if (RequiresReparse(scriptParseInfo, scriptFile)) { ParseAndBind(scriptFile, connInfo); } if (scriptParseInfo != null && scriptParseInfo.ParseResult != null) { if (Monitor.TryEnter(scriptParseInfo.BuildingMetadataLock)) { try { QueueItem queueItem = this.BindingQueue.QueueBindingOperation( key: scriptParseInfo.ConnectionKey, bindingTimeout: LanguageService.BindingTimeout, bindOperation: (bindingContext, cancelToken) => { // get the list of possible current methods for signature help var methods = Resolver.FindMethods( scriptParseInfo.ParseResult, startLine + 1, endColumn + 1, bindingContext.MetadataDisplayInfoProvider); // get positional information on the current method var methodLocations = Resolver.GetMethodNameAndParams(scriptParseInfo.ParseResult, startLine + 1, endColumn + 1, bindingContext.MetadataDisplayInfoProvider); // convert from the parser format to the VS Code wire format return AutoCompleteHelper.ConvertMethodHelpTextListToSignatureHelp(methods, methodLocations, startLine + 1, endColumn + 1); }); queueItem.ItemProcessed.WaitOne(); return queueItem.GetResultAsT(); } finally { Monitor.Exit(scriptParseInfo.BuildingMetadataLock); } } } // return null if there isn't a tooltip for the current location return null; } /// /// Return the completion item list for the current text position. /// This method does not await cache builds since it expects to return quickly /// /// public CompletionItem[] GetCompletionItems( TextDocumentPosition textDocumentPosition, ScriptFile scriptFile, ConnectionInfo connInfo) { // initialize some state to parse and bind the current script file this.currentCompletionParseInfo = null; CompletionItem[] resultCompletionItems = null; string filePath = textDocumentPosition.TextDocument.Uri; int startLine = textDocumentPosition.Position.Line; int parserLine = textDocumentPosition.Position.Line + 1; int startColumn = TextUtilities.PositionOfPrevDelimeter( scriptFile.Contents, textDocumentPosition.Position.Line, textDocumentPosition.Position.Character); int endColumn = TextUtilities.PositionOfNextDelimeter( scriptFile.Contents, textDocumentPosition.Position.Line, textDocumentPosition.Position.Character); int parserColumn = textDocumentPosition.Position.Character + 1; bool useLowerCaseSuggestions = this.CurrentSettings.SqlTools.IntelliSense.LowerCaseSuggestions.Value; // get the current script parse info object ScriptParseInfo scriptParseInfo = GetScriptParseInfo(textDocumentPosition.TextDocument.Uri); if (scriptParseInfo == null) { return AutoCompleteHelper.GetDefaultCompletionItems( startLine, startColumn, endColumn, useLowerCaseSuggestions); } // reparse and bind the SQL statement if needed if (RequiresReparse(scriptParseInfo, scriptFile)) { ParseAndBind(scriptFile, connInfo); } // if the parse failed then return the default list if (scriptParseInfo.ParseResult == null) { return AutoCompleteHelper.GetDefaultCompletionItems( startLine, startColumn, endColumn, useLowerCaseSuggestions); } // need to adjust line & column for base-1 parser indices Token token = GetToken(scriptParseInfo, parserLine, parserColumn); string tokenText = token != null ? token.Text : null; // check if the file is connected and the file lock is available if (scriptParseInfo.IsConnected && Monitor.TryEnter(scriptParseInfo.BuildingMetadataLock)) { try { // queue the completion task with the binding queue 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, parserLine, parserColumn, bindingContext.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( scriptParseInfo.CurrentSuggestions, startLine, startColumn, endColumn, tokenText); }, timeoutOperation: (bindingContext) => { // return the default list if the connected bind fails return AutoCompleteHelper.GetDefaultCompletionItems( startLine, startColumn, endColumn, useLowerCaseSuggestions, tokenText); }); // wait for the queue item queueItem.ItemProcessed.WaitOne(); var completionItems = queueItem.GetResultAsT(); if (completionItems != null && completionItems.Length > 0) { resultCompletionItems = completionItems; } else if (!ShouldShowCompletionList(token)) { resultCompletionItems = AutoCompleteHelper.EmptyCompletionList; } } finally { Monitor.Exit(scriptParseInfo.BuildingMetadataLock); } } // if there are no completions then provide the default list if (resultCompletionItems == null) { resultCompletionItems = AutoCompleteHelper.GetDefaultCompletionItems( startLine, startColumn, endColumn, useLowerCaseSuggestions, tokenText); } return resultCompletionItems; } private static Token GetToken(ScriptParseInfo scriptParseInfo, int startLine, int startColumn) { if (scriptParseInfo != null && scriptParseInfo.ParseResult != null && scriptParseInfo.ParseResult.Script != null && scriptParseInfo.ParseResult.Script.Tokens != null) { var tokenIndex = scriptParseInfo.ParseResult.Script.TokenManager.FindToken(startLine, startColumn); if (tokenIndex >= 0) { // return the current token int currentIndex = 0; foreach (var token in scriptParseInfo.ParseResult.Script.Tokens) { if (currentIndex == tokenIndex) { return token; } ++currentIndex; } } } return null; } private static bool ShouldShowCompletionList(Token token) { bool result = true; if (token != null) { switch (token.Id) { case (int)Tokens.LEX_MULTILINE_COMMENT: case (int)Tokens.LEX_END_OF_LINE_COMMENT: result = false; break; } } return result; } #endregion #region Diagnostic Provider methods /// /// Gets a list of semantic diagnostic marks for the provided script file /// /// internal ScriptFileMarker[] GetSemanticMarkers(ScriptFile scriptFile) { ConnectionInfo connInfo; ConnectionService.Instance.TryFindConnection( scriptFile.ClientFilePath, out connInfo); var parseResult = ParseAndBind(scriptFile, connInfo); // build a list of SQL script file markers from the errors List markers = new List(); if (parseResult != null && parseResult.Errors != null) { foreach (var error in parseResult.Errors) { markers.Add(new ScriptFileMarker() { Message = error.Message, Level = ScriptFileMarkerLevel.Error, ScriptRegion = new ScriptRegion() { File = scriptFile.FilePath, StartLineNumber = error.Start.LineNumber, StartColumnNumber = error.Start.ColumnNumber, StartOffset = 0, EndLineNumber = error.End.LineNumber, EndColumnNumber = error.End.ColumnNumber, EndOffset = 0 } }); } } return markers.ToArray(); } /// /// Runs script diagnostics on changed files /// /// /// private Task RunScriptDiagnostics(ScriptFile[] filesToAnalyze, EventContext eventContext) { if (!CurrentSettings.IsDiagnositicsEnabled) { // If the user has disabled script analysis, skip it entirely return Task.FromResult(true); } // If there's an existing task, attempt to cancel it try { if (ExistingRequestCancellation != null) { // Try to cancel the request ExistingRequestCancellation.Cancel(); // If cancellation didn't throw an exception, // clean up the existing token ExistingRequestCancellation.Dispose(); ExistingRequestCancellation = null; } } catch (Exception e) { Logger.Write( LogLevel.Error, string.Format( "Exception while cancelling analysis task:\n\n{0}", e.ToString())); TaskCompletionSource cancelTask = new TaskCompletionSource(); cancelTask.SetCanceled(); return cancelTask.Task; } // Create a fresh cancellation token and then start the task. // We create this on a different TaskScheduler so that we // don't block the main message loop thread. ExistingRequestCancellation = new CancellationTokenSource(); Task.Factory.StartNew( () => DelayThenInvokeDiagnostics( LanguageService.DiagnosticParseDelay, filesToAnalyze, eventContext, ExistingRequestCancellation.Token), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); return Task.FromResult(true); } /// /// Actually run the script diagnostics after waiting for some small delay /// /// /// /// /// private async Task DelayThenInvokeDiagnostics( int delayMilliseconds, ScriptFile[] filesToAnalyze, EventContext eventContext, CancellationToken cancellationToken) { // First of all, wait for the desired delay period before // analyzing the provided list of files try { await Task.Delay(delayMilliseconds, cancellationToken); } catch (TaskCanceledException) { // If the task is cancelled, exit directly return; } // If we've made it past the delay period then we don't care // about the cancellation token anymore. This could happen // when the user stops typing for long enough that the delay // period ends but then starts typing while analysis is going // on. It makes sense to send back the results from the first // delay period while the second one is ticking away. // Get the requested files foreach (ScriptFile scriptFile in filesToAnalyze) { if (IsPreviewWindow(scriptFile)) { continue; } Logger.Write(LogLevel.Verbose, "Analyzing script file: " + scriptFile.FilePath); ScriptFileMarker[] semanticMarkers = GetSemanticMarkers(scriptFile); Logger.Write(LogLevel.Verbose, "Analysis complete."); await DiagnosticsHelper.PublishScriptDiagnostics(scriptFile, semanticMarkers, eventContext); } } #endregion /// /// Adds a new or updates an existing script parse info instance in local cache /// /// /// internal void AddOrUpdateScriptParseInfo(string uri, ScriptParseInfo scriptInfo) { lock (this.parseMapLock) { if (this.ScriptParseInfoMap.ContainsKey(uri)) { this.ScriptParseInfoMap[uri] = scriptInfo; } else { this.ScriptParseInfoMap.Add(uri, scriptInfo); } } } /// /// Gets a script parse info object for a file from the local cache /// Internal for testing purposes only /// /// /// Creates a new instance if one doesn't exist internal ScriptParseInfo GetScriptParseInfo(string uri, bool createIfNotExists = false) { lock (this.parseMapLock) { if (this.ScriptParseInfoMap.ContainsKey(uri)) { return this.ScriptParseInfoMap[uri]; } 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; } else { return null; } } } private bool RemoveScriptParseInfo(string uri) { lock (this.parseMapLock) { if (this.ScriptParseInfoMap.ContainsKey(uri)) { return this.ScriptParseInfoMap.Remove(uri); } else { return false; } } } /// /// Returns a flag indicating if the ScriptFile refers to the output window. /// /// private bool IsPreviewWindow(ScriptFile scriptFile) { if (scriptFile != null && !string.IsNullOrWhiteSpace(scriptFile.ClientFilePath)) { return scriptFile.ClientFilePath.StartsWith("tsqloutput:"); } else { return false; } } } }