From f2a5ebd605beeda046772704941c5f039233ac39 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Sat, 16 Jul 2016 10:45:27 -0700 Subject: [PATCH] Add configuration change notification event handler. Clean up code a bit more to remove unneeded code. Better integrate the diagnostic callback code. --- .../LanguageSupport/LanguageService.cs | 57 +++++ ServiceHost/Program.cs | 4 +- ServiceHost/Server/LanguageServer.cs | 219 ++++++++++++------ ServiceHost/Session/EditorSession.cs | 144 +----------- 4 files changed, 212 insertions(+), 212 deletions(-) create mode 100644 ServiceHost/LanguageSupport/LanguageService.cs diff --git a/ServiceHost/LanguageSupport/LanguageService.cs b/ServiceHost/LanguageSupport/LanguageService.cs new file mode 100644 index 00000000..3ab77697 --- /dev/null +++ b/ServiceHost/LanguageSupport/LanguageService.cs @@ -0,0 +1,57 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.EditorServices; +using Microsoft.SqlTools.EditorServices.Session; + +namespace Microsoft.SqlTools.LanguageSupport +{ + /// + /// Main class for Language Service functionality + /// + public class LanguageService + { + /// + /// Gets or sets the current SQL Tools context + /// + /// + private SqlToolsContext Context { get; set; } + + /// + /// Constructor for the Language Service class + /// + /// + public LanguageService(SqlToolsContext context) + { + this.Context = context; + } + + /// + /// Gets a list of semantic diagnostic marks for the provided script file + /// + /// + public ScriptFileMarker[] GetSemanticMarkers(ScriptFile scriptFile) + { + // the commented out snippet is an example of how to create a error marker + // semanticMarkers = new ScriptFileMarker[1]; + // semanticMarkers[0] = new ScriptFileMarker() + // { + // Message = "Error message", + // Level = ScriptFileMarkerLevel.Error, + // ScriptRegion = new ScriptRegion() + // { + // File = scriptFile.FilePath, + // StartLineNumber = 2, + // StartColumnNumber = 2, + // StartOffset = 0, + // EndLineNumber = 4, + // EndColumnNumber = 10, + // EndOffset = 0 + // } + // }; + return new ScriptFileMarker[0]; + } + } +} diff --git a/ServiceHost/Program.cs b/ServiceHost/Program.cs index 622a026c..6bfd0f24 100644 --- a/ServiceHost/Program.cs +++ b/ServiceHost/Program.cs @@ -18,7 +18,9 @@ namespace Microsoft.SqlTools.ServiceHost /// static void Main(string[] args) { - Logger.Initialize(); + // turn on Verbose logging during early development + // we need to switch to Normal when preparing for public preview + Logger.Initialize(minimumLogLevel: LogLevel.Verbose); Logger.Write(LogLevel.Normal, "Starting SQL Tools Service Host"); const string hostName = "SQL Tools Service Host"; diff --git a/ServiceHost/Server/LanguageServer.cs b/ServiceHost/Server/LanguageServer.cs index 7386d71f..d6719141 100644 --- a/ServiceHost/Server/LanguageServer.cs +++ b/ServiceHost/Server/LanguageServer.cs @@ -12,6 +12,7 @@ using System.Collections.Generic; using System.Text; using System.Threading; using System.Linq; +using System; namespace Microsoft.SqlTools.EditorServices.Protocol.Server { @@ -21,6 +22,8 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server public class LanguageServer : LanguageServerBase { private static CancellationTokenSource existingRequestCancellation; + + private LanguageServerSettings currentSettings = new LanguageServerSettings(); private EditorSession editorSession; @@ -34,6 +37,9 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server this.editorSession.StartSession(hostDetails, profilePaths); } + /// + /// Initialize the VS Code request/response callbacks + /// protected override void Initialize() { // Register all supported message types @@ -54,6 +60,9 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server this.SetRequestHandler(WorkspaceSymbolRequest.Type, this.HandleWorkspaceSymbolRequest); } + /// + /// Handles the shutdown event for the Language Server + /// protected override async Task Shutdown() { Logger.Write(LogLevel.Normal, "Language service is shutting down..."); @@ -67,14 +76,20 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server await Task.FromResult(true); } + /// + /// Handles the initialization request + /// + /// + /// + /// protected async Task HandleInitializeRequest( InitializeRequest initializeParams, RequestContext requestContext) { - Logger.Write(LogLevel.Normal, "HandleDidChangeTextDocumentNotification"); + Logger.Write(LogLevel.Verbose, "HandleDidChangeTextDocumentNotification"); // Grab the workspace path from the parameters - //editorSession.Workspace.WorkspacePath = initializeParams.RootPath; + editorSession.Workspace.WorkspacePath = initializeParams.RootPath; await requestContext.SendResult( new InitializeResult @@ -133,7 +148,7 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server changedFiles.Add(changedFile); } - Logger.Write(LogLevel.Normal, msg.ToString()); + Logger.Write(LogLevel.Verbose, msg.ToString()); this.RunScriptDiagnostics( changedFiles.ToArray(), @@ -147,7 +162,7 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server DidOpenTextDocumentNotification openParams, EventContext eventContext) { - Logger.Write(LogLevel.Normal, "HandleDidOpenTextDocumentNotification"); + Logger.Write(LogLevel.Verbose, "HandleDidOpenTextDocumentNotification"); return Task.FromResult(true); } @@ -155,15 +170,57 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server TextDocumentIdentifier closeParams, EventContext eventContext) { - Logger.Write(LogLevel.Normal, "HandleDidCloseTextDocumentNotification"); + Logger.Write(LogLevel.Verbose, "HandleDidCloseTextDocumentNotification"); return Task.FromResult(true); } + /// + /// Handles the configuration change event + /// + /// + /// protected async Task HandleDidChangeConfigurationNotification( DidChangeConfigurationParams configChangeParams, EventContext eventContext) { - Logger.Write(LogLevel.Normal, "HandleDidChangeConfigurationNotification"); + Logger.Write(LogLevel.Verbose, "HandleDidChangeConfigurationNotification"); + + bool oldLoadProfiles = this.currentSettings.EnableProfileLoading; + bool oldScriptAnalysisEnabled = + this.currentSettings.ScriptAnalysis.Enable.HasValue; + string oldScriptAnalysisSettingsPath = + this.currentSettings.ScriptAnalysis.SettingsPath; + + this.currentSettings.Update( + configChangeParams.Settings.SqlTools, + this.editorSession.Workspace.WorkspacePath); + + // If script analysis settings have changed we need to clear & possibly update the current diagnostic records. + if ((oldScriptAnalysisEnabled != this.currentSettings.ScriptAnalysis.Enable)) + { + // 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 (!this.currentSettings.ScriptAnalysis.Enable.Value) + { + ScriptFileMarker[] emptyAnalysisDiagnostics = new ScriptFileMarker[0]; + + foreach (var scriptFile in editorSession.Workspace.GetOpenedFiles()) + { + await PublishScriptDiagnostics( + scriptFile, + emptyAnalysisDiagnostics, + eventContext); + } + } + else + { + await this.RunScriptDiagnostics( + this.editorSession.Workspace.GetOpenedFiles(), + this.editorSession, + eventContext); + } + } + await Task.FromResult(true); } @@ -171,7 +228,7 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server TextDocumentPosition textDocumentPosition, RequestContext requestContext) { - Logger.Write(LogLevel.Normal, "HandleDefinitionRequest"); + Logger.Write(LogLevel.Verbose, "HandleDefinitionRequest"); await Task.FromResult(true); } @@ -179,7 +236,7 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server ReferencesParams referencesParams, RequestContext requestContext) { - Logger.Write(LogLevel.Normal, "HandleReferencesRequest"); + Logger.Write(LogLevel.Verbose, "HandleReferencesRequest"); await Task.FromResult(true); } @@ -187,7 +244,7 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server TextDocumentPosition textDocumentPosition, RequestContext requestContext) { - Logger.Write(LogLevel.Normal, "HandleCompletionRequest"); + Logger.Write(LogLevel.Verbose, "HandleCompletionRequest"); await Task.FromResult(true); } @@ -195,7 +252,7 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server CompletionItem completionItem, RequestContext requestContext) { - Logger.Write(LogLevel.Normal, "HandleCompletionResolveRequest"); + Logger.Write(LogLevel.Verbose, "HandleCompletionResolveRequest"); await Task.FromResult(true); } @@ -203,7 +260,7 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server TextDocumentPosition textDocumentPosition, RequestContext requestContext) { - Logger.Write(LogLevel.Normal, "HandleSignatureHelpRequest"); + Logger.Write(LogLevel.Verbose, "HandleSignatureHelpRequest"); await Task.FromResult(true); } @@ -211,7 +268,7 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server TextDocumentPosition textDocumentPosition, RequestContext requestContext) { - Logger.Write(LogLevel.Normal, "HandleDocumentHighlightRequest"); + Logger.Write(LogLevel.Verbose, "HandleDocumentHighlightRequest"); await Task.FromResult(true); } @@ -219,7 +276,7 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server TextDocumentPosition textDocumentPosition, RequestContext requestContext) { - Logger.Write(LogLevel.Normal, "HandleHoverRequest"); + Logger.Write(LogLevel.Verbose, "HandleHoverRequest"); await Task.FromResult(true); } @@ -227,7 +284,7 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server TextDocumentIdentifier textDocumentIdentifier, RequestContext requestContext) { - Logger.Write(LogLevel.Normal, "HandleDocumentSymbolRequest"); + Logger.Write(LogLevel.Verbose, "HandleDocumentSymbolRequest"); await Task.FromResult(true); } @@ -235,53 +292,57 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server WorkspaceSymbolParams workspaceSymbolParams, RequestContext requestContext) { - Logger.Write(LogLevel.Normal, "HandleWorkspaceSymbolRequest"); + Logger.Write(LogLevel.Verbose, "HandleWorkspaceSymbolRequest"); await Task.FromResult(true); } + /// + /// Runs script diagnostics on changed files + /// + /// + /// + /// private Task RunScriptDiagnostics( ScriptFile[] filesToAnalyze, EditorSession editorSession, EventContext eventContext) { - // if (!this.currentSettings.ScriptAnalysis.Enable.Value) - // { - // // If the user has disabled script analysis, skip it entirely - // return Task.FromResult(true); - // } + if (!this.currentSettings.ScriptAnalysis.Enable.Value) + { + // 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 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) - // { - // // TODO: Catch a more specific exception! - // Logger.Write( - // LogLevel.Error, - // string.Format( - // "Exception while cancelling analysis task:\n\n{0}", - // e.ToString())); + // 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; - // } + 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. - // TODO: Is there a better way to do this? existingRequestCancellation = new CancellationTokenSource(); Task.Factory.StartNew( () => @@ -298,7 +359,14 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server return Task.FromResult(true); } - + /// + /// Actually run the script diagnostics after waiting for some small delay + /// + /// + /// + /// + /// + /// private static async Task DelayThenInvokeDiagnostics( int delayMilliseconds, ScriptFile[] filesToAnalyze, @@ -329,37 +397,17 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server foreach (ScriptFile scriptFile in filesToAnalyze) { ScriptFileMarker[] semanticMarkers = null; - // if (editorSession.AnalysisService != null) - // { - // Logger.Write(LogLevel.Verbose, "Analyzing script file: " + scriptFile.FilePath); - - // semanticMarkers = - // editorSession.AnalysisService.GetSemanticMarkers( - // scriptFile); - - // Logger.Write(LogLevel.Verbose, "Analysis complete."); - // } - // else + if (editorSession.LanguageService != null) + { + Logger.Write(LogLevel.Verbose, "Analyzing script file: " + scriptFile.FilePath); + semanticMarkers = editorSession.LanguageService.GetSemanticMarkers(scriptFile); + Logger.Write(LogLevel.Verbose, "Analysis complete."); + } + else { // Semantic markers aren't available if the AnalysisService // isn't available - semanticMarkers = new ScriptFileMarker[0]; - // semanticMarkers = new ScriptFileMarker[1]; - // semanticMarkers[0] = new ScriptFileMarker() - // { - // Message = "Error message", - // Level = ScriptFileMarkerLevel.Error, - // ScriptRegion = new ScriptRegion() - // { - // File = scriptFile.FilePath, - // StartLineNumber = 2, - // StartColumnNumber = 2, - // StartOffset = 0, - // EndLineNumber = 4, - // EndColumnNumber = 10, - // EndOffset = 0 - // } - // }; + semanticMarkers = new ScriptFileMarker[0]; } await PublishScriptDiagnostics( @@ -369,6 +417,12 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server } } + /// + /// Send the diagnostic results back to the host application + /// + /// + /// + /// private static async Task PublishScriptDiagnostics( ScriptFile scriptFile, ScriptFileMarker[] semanticMarkers, @@ -392,6 +446,11 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server }); } + /// + /// Convert a ScriptFileMarker to a Diagnostic that is Language Service compatible + /// + /// + /// private static Diagnostic GetDiagnosticFromMarker(ScriptFileMarker scriptFileMarker) { return new Diagnostic @@ -415,6 +474,10 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server }; } + /// + /// Map ScriptFileMarker severity to Diagnostic severity + /// + /// private static DiagnosticSeverity MapDiagnosticSeverity(ScriptFileMarkerLevel markerLevel) { switch (markerLevel) @@ -433,10 +496,14 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server } } + /// + /// Switch from 0-based offsets to 1 based offsets + /// + /// + /// private static FileChange GetFileChangeDetails(Range changeRange, string insertString) { // The protocol's positions are zero-based so add 1 to all offsets - return new FileChange { InsertString = insertString, diff --git a/ServiceHost/Session/EditorSession.cs b/ServiceHost/Session/EditorSession.cs index 310a44c8..3c592a8f 100644 --- a/ServiceHost/Session/EditorSession.cs +++ b/ServiceHost/Session/EditorSession.cs @@ -5,6 +5,7 @@ using System; using Microsoft.SqlTools.EditorServices.Session; +using Microsoft.SqlTools.LanguageSupport; namespace Microsoft.SqlTools.EditorServices { @@ -21,6 +22,12 @@ namespace Microsoft.SqlTools.EditorServices /// public Workspace Workspace { get; private set; } + /// + /// Gets or sets the Language Service + /// + /// + public LanguageService LanguageService { get; set; } + /// /// Gets the SqlToolsContext instance for this session. /// @@ -30,80 +37,6 @@ namespace Microsoft.SqlTools.EditorServices #region Public Methods - /// - /// Starts the session using the provided IConsoleHost implementation - /// for the ConsoleService. - /// - /// - /// Provides details about the host application. - /// - /// - /// An object containing the profile paths for the session. - /// - public void StartSession(HostDetails hostDetails, ProfilePaths profilePaths) - { - // Initialize all services - this.SqlToolsContext = new SqlToolsContext(hostDetails, profilePaths); - - - // this.LanguageService = new LanguageService(this.SqlToolsContext); - // this.DebugService = new DebugService(this.SqlToolsContext); - // this.ConsoleService = new ConsoleService(this.SqlToolsContext); - // this.ExtensionService = new ExtensionService(this.SqlToolsContext); - - // this.InstantiateAnalysisService(); - - // Create a workspace to contain open files - this.Workspace = new Workspace(this.SqlToolsContext.SqlToolsVersion); - } - - #endregion - - #region IDisposable Implementation - - /// - /// Disposes of any Runspaces that were created for the - /// services used in this session. - /// - public void Dispose() - { - } - - #endregion - - -#if false - #region Properties - - /// - /// Gets the LanguageService instance for this session. - /// - public LanguageService LanguageService { get; private set; } - - /// - /// Gets the AnalysisService instance for this session. - /// - public AnalysisService AnalysisService { get; private set; } - - /// - /// Gets the DebugService instance for this session. - /// - public DebugService DebugService { get; private set; } - - /// - /// Gets the ConsoleService instance for this session. - /// - public ConsoleService ConsoleService { get; private set; } - - /// - /// Gets the ExtensionService instance for this session. - /// - public ExtensionService ExtensionService { get; private set; } - - #endregion - - #region Public Methods - /// /// Starts the session using the provided IConsoleHost implementation /// for the ConsoleService. @@ -119,59 +52,11 @@ namespace Microsoft.SqlTools.EditorServices // Initialize all services this.SqlToolsContext = new SqlToolsContext(hostDetails, profilePaths); this.LanguageService = new LanguageService(this.SqlToolsContext); - this.DebugService = new DebugService(this.SqlToolsContext); - this.ConsoleService = new ConsoleService(this.SqlToolsContext); - this.ExtensionService = new ExtensionService(this.SqlToolsContext); - - this.InstantiateAnalysisService(); // Create a workspace to contain open files this.Workspace = new Workspace(this.SqlToolsContext.SqlToolsVersion); } - /// - /// Restarts the AnalysisService so it can be configured with a new settings file. - /// - /// Path to the settings file. - public void RestartAnalysisService(string settingsPath) - { - this.AnalysisService?.Dispose(); - InstantiateAnalysisService(settingsPath); - } - - internal void InstantiateAnalysisService(string settingsPath = null) - { - // Only enable the AnalysisService if the machine has SqlTools - // v5 installed. Script Analyzer works on earlier SqlTools - // versions but our hard dependency on their binaries complicates - // the deployment and assembly loading since we would have to - // conditionally load the binaries for v3/v4 support. This problem - // will be solved in the future by using Script Analyzer as a - // module rather than an assembly dependency. - if (this.SqlToolsContext.SqlToolsVersion.Major >= 5) - { - // AnalysisService will throw FileNotFoundException if - // Script Analyzer binaries are not included. - try - { - this.AnalysisService = new AnalysisService(this.SqlToolsContext.ConsoleHost, settingsPath); - } - catch (FileNotFoundException) - { - Logger.Write( - LogLevel.Warning, - "Script Analyzer binaries not found, AnalysisService will be disabled."); - } - } - else - { - Logger.Write( - LogLevel.Normal, - "Script Analyzer cannot be loaded due to unsupported SqlTools version " + - this.SqlToolsContext.SqlToolsVersion.ToString()); - } - } - #endregion #region IDisposable Implementation @@ -181,21 +66,10 @@ namespace Microsoft.SqlTools.EditorServices /// services used in this session. /// public void Dispose() - { - if (this.AnalysisService != null) - { - this.AnalysisService.Dispose(); - this.AnalysisService = null; - } - - if (this.SqlToolsContext != null) - { - this.SqlToolsContext.Dispose(); - this.SqlToolsContext = null; - } + { } #endregion -#endif + } }