diff --git a/src/ServiceHost/LanguageService/LanguageService.cs b/src/ServiceHost/LanguageService/LanguageService.cs index 51665fbd..05cc90c5 100644 --- a/src/ServiceHost/LanguageService/LanguageService.cs +++ b/src/ServiceHost/LanguageService/LanguageService.cs @@ -3,12 +3,16 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System; +using System.Threading; using System.Threading.Tasks; -using Microsoft.SqlTools.EditorServices.Session; using Microsoft.SqlTools.EditorServices.Utility; using Microsoft.SqlTools.ServiceLayer.LanguageService.Contracts; using Microsoft.SqlTools.ServiceLayer.ServiceHost.Protocol; +using Microsoft.SqlTools.ServiceLayer.SqlContext; +using Microsoft.SqlTools.ServiceLayer.WorkspaceService; using Microsoft.SqlTools.ServiceLayer.WorkspaceService.Contracts; +using System.Linq; namespace Microsoft.SqlTools.ServiceLayer.LanguageService { @@ -34,15 +38,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageService } } - /// - /// Constructor for the Language Service class - /// - /// - private LanguageService(SqlToolsContext context) - { - this.Context = context; - } - /// /// Default, parameterless contstructor. /// TODO: Remove once the SqlToolsContext stuff is sorted out @@ -54,13 +49,29 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageService #endregion + #region Properties + + private static CancellationTokenSource ExistingRequestCancellation { get; set; } + + private SqlToolsSettings CurrentSettings + { + get { return WorkspaceService.Instance.CurrentSettings; } + } + + private Workspace CurrentWorkspace + { + get { return WorkspaceService.Instance.Workspace; } + } + /// /// Gets or sets the current SQL Tools context /// /// private SqlToolsContext Context { get; set; } - public void InitializeService(ServiceHost.ServiceHost serviceHost) + #endregion + + public void InitializeService(ServiceHost.ServiceHost serviceHost, SqlToolsContext context) { // Register the requests that this service will handle serviceHost.SetRequestHandler(DefinitionRequest.Type, HandleDefinitionRequest); @@ -79,6 +90,12 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageService Logger.Write(LogLevel.Verbose, "Shutting down language service"); await Task.FromResult(0); }); + + // Register the configuration update handler + WorkspaceService.Instance.RegisterDidChangeConfigurationNotificationTask(HandleDidChangeConfigurationNotification); + + // Store the SqlToolsContext for future use + Context = context; } #region Request Handlers @@ -157,11 +174,48 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageService #endregion + #region Handlers for Events from Other Services + + public async Task HandleDidChangeConfigurationNotification( + SqlToolsSettings newSettings, + SqlToolsSettings oldSettings, + EventContext eventContext) + { + // If script analysis settings have changed we need to clear & possibly update the current diagnostic records. + bool oldScriptAnalysisEnabled = oldSettings.ScriptAnalysis.Enable.HasValue; + if ((oldScriptAnalysisEnabled != newSettings.ScriptAnalysis.Enable)) + { + // If the user just turned off script analysis or changed the settings path, send a diagnostics + // event to clear the analysis markers that they already have. + if (!newSettings.ScriptAnalysis.Enable.Value) + { + ScriptFileMarker[] emptyAnalysisDiagnostics = new ScriptFileMarker[0]; + + foreach (var scriptFile in WorkspaceService.Instance.Workspace.GetOpenedFiles()) + { + await PublishScriptDiagnostics(scriptFile, emptyAnalysisDiagnostics, eventContext); + } + } + else + { + await this.RunScriptDiagnostics(CurrentWorkspace.GetOpenedFiles(), eventContext); + } + } + + // Update the settings in the current + CurrentSettings.EnableProfileLoading = newSettings.EnableProfileLoading; + CurrentSettings.ScriptAnalysis.Update(newSettings.ScriptAnalysis, CurrentWorkspace.WorkspacePath); + } + + #endregion + + #region Private Helpers + /// /// Gets a list of semantic diagnostic marks for the provided script file /// /// - public ScriptFileMarker[] GetSemanticMarkers(ScriptFile scriptFile) + private ScriptFileMarker[] GetSemanticMarkers(ScriptFile scriptFile) { // the commented out snippet is an example of how to create a error marker // semanticMarkers = new ScriptFileMarker[1]; @@ -182,5 +236,187 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageService // }; return new ScriptFileMarker[0]; } + + /// + /// Runs script diagnostics on changed files + /// + /// + /// + private Task RunScriptDiagnostics(ScriptFile[] filesToAnalyze, EventContext eventContext) + { + if (!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 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( + 750, + 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) + { + Logger.Write(LogLevel.Verbose, "Analyzing script file: " + scriptFile.FilePath); + ScriptFileMarker[] semanticMarkers = GetSemanticMarkers(scriptFile); + Logger.Write(LogLevel.Verbose, "Analysis complete."); + + await PublishScriptDiagnostics(scriptFile, semanticMarkers, eventContext); + } + } + + /// + /// Send the diagnostic results back to the host application + /// + /// + /// + /// + private static async Task PublishScriptDiagnostics( + ScriptFile scriptFile, + ScriptFileMarker[] semanticMarkers, + EventContext eventContext) + { + var allMarkers = scriptFile.SyntaxMarkers != null + ? scriptFile.SyntaxMarkers.Concat(semanticMarkers) + : semanticMarkers; + + // Always send syntax and semantic errors. We want to + // make sure no out-of-date markers are being displayed. + await eventContext.SendEvent( + PublishDiagnosticsNotification.Type, + new PublishDiagnosticsNotification + { + Uri = scriptFile.ClientFilePath, + Diagnostics = + allMarkers + .Select(GetDiagnosticFromMarker) + .ToArray() + }); + } + + /// + /// Convert a ScriptFileMarker to a Diagnostic that is Language Service compatible + /// + /// + /// + private static Diagnostic GetDiagnosticFromMarker(ScriptFileMarker scriptFileMarker) + { + return new Diagnostic + { + Severity = MapDiagnosticSeverity(scriptFileMarker.Level), + Message = scriptFileMarker.Message, + Range = new Range + { + // TODO: What offsets should I use? + Start = new Position + { + Line = scriptFileMarker.ScriptRegion.StartLineNumber - 1, + Character = scriptFileMarker.ScriptRegion.StartColumnNumber - 1 + }, + End = new Position + { + Line = scriptFileMarker.ScriptRegion.EndLineNumber - 1, + Character = scriptFileMarker.ScriptRegion.EndColumnNumber - 1 + } + } + }; + } + + /// + /// Map ScriptFileMarker severity to Diagnostic severity + /// + /// + private static DiagnosticSeverity MapDiagnosticSeverity(ScriptFileMarkerLevel markerLevel) + { + switch (markerLevel) + { + case ScriptFileMarkerLevel.Error: + return DiagnosticSeverity.Error; + + case ScriptFileMarkerLevel.Warning: + return DiagnosticSeverity.Warning; + + case ScriptFileMarkerLevel.Information: + return DiagnosticSeverity.Information; + + default: + return DiagnosticSeverity.Error; + } + } + + #endregion } } diff --git a/src/ServiceHost/Program.cs b/src/ServiceHost/Program.cs index fda85c74..9717feb3 100644 --- a/src/ServiceHost/Program.cs +++ b/src/ServiceHost/Program.cs @@ -2,8 +2,8 @@ // 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 Microsoft.SqlTools.EditorServices.Session; using Microsoft.SqlTools.EditorServices.Utility; +using Microsoft.SqlTools.ServiceLayer.SqlContext; namespace Microsoft.SqlTools.ServiceLayer { @@ -24,17 +24,19 @@ namespace Microsoft.SqlTools.ServiceLayer const string hostName = "SQL Tools Service Host"; const string hostProfileId = "SQLToolsService"; - Version hostVersion = new Version(1,0); + Version hostVersion = new Version(1,0); // set up the host details and profile paths var hostDetails = new HostDetails(hostName, hostProfileId, hostVersion); var profilePaths = new ProfilePaths(hostProfileId, "baseAllUsersPath", "baseCurrentUserPath"); + SqlToolsContext sqlToolsContext = new SqlToolsContext(hostDetails, profilePaths); // Create the service host - ServiceHost.ServiceHost serviceHost = ServiceHost.ServiceHost.Create(hostDetails, profilePaths); + ServiceHost.ServiceHost serviceHost = ServiceHost.ServiceHost.Create(); // Initialize the services that will be hosted here - LanguageService.LanguageService.Instance.InitializeService(serviceHost); + WorkspaceService.WorkspaceService.Instance.InitializeService(serviceHost); + LanguageService.LanguageService.Instance.InitializeService(serviceHost, sqlToolsContext); // Start the service serviceHost.Start().Wait(); diff --git a/src/ServiceHost/ServiceHost/ServiceHost.cs b/src/ServiceHost/ServiceHost/ServiceHost.cs index 0b393e3f..1e433b91 100644 --- a/src/ServiceHost/ServiceHost/ServiceHost.cs +++ b/src/ServiceHost/ServiceHost/ServiceHost.cs @@ -5,18 +5,11 @@ using System.Threading.Tasks; using System.Collections.Generic; -using System.Text; -using System.Threading; using System.Linq; -using System; -using Microsoft.SqlTools.EditorServices; -using Microsoft.SqlTools.EditorServices.Session; using Microsoft.SqlTools.EditorServices.Utility; -using Microsoft.SqlTools.ServiceLayer.LanguageService.Contracts; using Microsoft.SqlTools.ServiceLayer.ServiceHost.Contracts; using Microsoft.SqlTools.ServiceLayer.ServiceHost.Protocol; using Microsoft.SqlTools.ServiceLayer.ServiceHost.Protocol.Channel; -using Microsoft.SqlTools.ServiceLayer.WorkspaceService.Contracts; namespace Microsoft.SqlTools.ServiceLayer.ServiceHost { @@ -35,18 +28,13 @@ namespace Microsoft.SqlTools.ServiceLayer.ServiceHost /// /// Creates or retrieves the current instance of the ServiceHost /// - /// Details about the host application - /// Details about the profile /// Instance of the service host - public static ServiceHost Create(HostDetails hostDetails, ProfilePaths profilePaths) + public static ServiceHost Create() { if (instance == null) { - instance = new ServiceHost(hostDetails, profilePaths); + instance = new ServiceHost(); } - // TODO: hostDetails and profilePaths are thrown out in SqlDataToolsContext, - // so we don't need to keep track of whether these have changed for now. - return instance; } @@ -54,17 +42,11 @@ namespace Microsoft.SqlTools.ServiceLayer.ServiceHost /// Constructs new instance of ServiceHost using the host and profile details provided. /// Access is private to ensure only one instance exists at a time. /// - /// Details about the host application - /// Details about the profile - private ServiceHost(HostDetails hostDetails, ProfilePaths profilePaths) - : base(new StdioServerChannel()) + private ServiceHost() : base(new StdioServerChannel()) { // Initialize the shutdown activities shutdownActivities = new List(); - - // Create an editor session that we'll use for keeping track of state - this.editorSession = new EditorSession(); - this.editorSession.StartSession(hostDetails, profilePaths); + initializeActivities = new List(); // Register the requests that this service host will handle this.SetRequestHandler(InitializeRequest.Type, this.HandleInitializeRequest); @@ -75,16 +57,14 @@ namespace Microsoft.SqlTools.ServiceLayer.ServiceHost #region Member Variables - private static CancellationTokenSource existingRequestCancellation; - - private ServiceHostSettings currentSettings = new ServiceHostSettings(); - - private EditorSession editorSession; - public delegate Task ShutdownHandler(object shutdownParams, RequestContext shutdownRequestContext); + public delegate Task InitializeHandler(InitializeRequest startupParams, RequestContext requestContext); + private readonly List shutdownActivities; + private readonly List initializeActivities; + #endregion #region Public Methods @@ -98,24 +78,18 @@ namespace Microsoft.SqlTools.ServiceLayer.ServiceHost shutdownActivities.Add(activity); } + /// + /// Add a new method to be called when the initialize request is submitted + /// + /// + public void RegisterInitializeTask(InitializeHandler activity) + { + initializeActivities.Add(activity); + } + #endregion - #region Private Methods - - /// - /// Initialize the VS Code request/response callbacks - /// - private void Initialize() - { - // Register all supported message types - - this.SetEventHandler(DidChangeTextDocumentNotification.Type, this.HandleDidChangeTextDocumentNotification); - this.SetEventHandler(DidOpenTextDocumentNotification.Type, this.HandleDidOpenTextDocumentNotification); - this.SetEventHandler(DidCloseTextDocumentNotification.Type, this.HandleDidCloseTextDocumentNotification); - this.SetEventHandler(DidChangeConfigurationNotification.Type, this.HandleDidChangeConfigurationNotification); - - - } + #region Request Handlers /// /// Handles the shutdown event for the Language Server @@ -127,13 +101,6 @@ namespace Microsoft.SqlTools.ServiceLayer.ServiceHost // Call all the shutdown methods provided by the service components Task[] shutdownTasks = shutdownActivities.Select(t => t(shutdownParams, requestContext)).ToArray(); await Task.WhenAll(shutdownTasks); - - // Shutdown the editor session - if (this.editorSession != null) - { - this.editorSession.Dispose(); - this.editorSession = null; - } } /// @@ -146,9 +113,13 @@ namespace Microsoft.SqlTools.ServiceLayer.ServiceHost { Logger.Write(LogLevel.Verbose, "HandleInitializationRequest"); - // Grab the workspace path from the parameters - editorSession.Workspace.WorkspacePath = initializeParams.RootPath; + // Call all tasks that registered on the initialize request + var initializeTasks = initializeActivities.Select(t => t(initializeParams, requestContext)); + await Task.WhenAll(initializeTasks); + // TODO: Figure out where this needs to go to be agnostic of the language + + // Send back what this server can do await requestContext.SendResult( new InitializeResult { @@ -175,334 +146,5 @@ namespace Microsoft.SqlTools.ServiceLayer.ServiceHost } #endregion - - /////////////////////////////////////////////// - - - /// - /// Handles text document change events - /// - /// - /// - /// - protected Task HandleDidChangeTextDocumentNotification( - DidChangeTextDocumentParams textChangeParams, - EventContext eventContext) - { - StringBuilder msg = new StringBuilder(); - msg.Append("HandleDidChangeTextDocumentNotification"); - List changedFiles = new List(); - - // A text change notification can batch multiple change requests - foreach (var textChange in textChangeParams.ContentChanges) - { - string fileUri = textChangeParams.TextDocument.Uri; - msg.AppendLine(); - msg.Append(" File: "); - msg.Append(fileUri); - - ScriptFile changedFile = editorSession.Workspace.GetFile(fileUri); - - changedFile.ApplyChange( - GetFileChangeDetails( - textChange.Range.Value, - textChange.Text)); - - changedFiles.Add(changedFile); - } - - Logger.Write(LogLevel.Verbose, msg.ToString()); - - this.RunScriptDiagnostics( - changedFiles.ToArray(), - editorSession, - eventContext); - - return Task.FromResult(true); - } - - protected Task HandleDidOpenTextDocumentNotification( - DidOpenTextDocumentNotification openParams, - EventContext eventContext) - { - Logger.Write(LogLevel.Verbose, "HandleDidOpenTextDocumentNotification"); - return Task.FromResult(true); - } - - protected Task HandleDidCloseTextDocumentNotification( - TextDocumentIdentifier closeParams, - EventContext eventContext) - { - 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.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); - } - - /// - /// 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 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( - 750, - filesToAnalyze, - editorSession, - 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 static async Task DelayThenInvokeDiagnostics( - int delayMilliseconds, - ScriptFile[] filesToAnalyze, - EditorSession editorSession, - 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) - { - ScriptFileMarker[] semanticMarkers = null; - 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]; - } - - await PublishScriptDiagnostics( - scriptFile, - semanticMarkers, - eventContext); - } - } - - /// - /// Send the diagnostic results back to the host application - /// - /// - /// - /// - private static async Task PublishScriptDiagnostics( - ScriptFile scriptFile, - ScriptFileMarker[] semanticMarkers, - EventContext eventContext) - { - var allMarkers = scriptFile.SyntaxMarkers != null - ? scriptFile.SyntaxMarkers.Concat(semanticMarkers) - : semanticMarkers; - - // Always send syntax and semantic errors. We want to - // make sure no out-of-date markers are being displayed. - await eventContext.SendEvent( - PublishDiagnosticsNotification.Type, - new PublishDiagnosticsNotification - { - Uri = scriptFile.ClientFilePath, - Diagnostics = - allMarkers - .Select(GetDiagnosticFromMarker) - .ToArray() - }); - } - - /// - /// Convert a ScriptFileMarker to a Diagnostic that is Language Service compatible - /// - /// - /// - private static Diagnostic GetDiagnosticFromMarker(ScriptFileMarker scriptFileMarker) - { - return new Diagnostic - { - Severity = MapDiagnosticSeverity(scriptFileMarker.Level), - Message = scriptFileMarker.Message, - Range = new Range - { - // TODO: What offsets should I use? - Start = new Position - { - Line = scriptFileMarker.ScriptRegion.StartLineNumber - 1, - Character = scriptFileMarker.ScriptRegion.StartColumnNumber - 1 - }, - End = new Position - { - Line = scriptFileMarker.ScriptRegion.EndLineNumber - 1, - Character = scriptFileMarker.ScriptRegion.EndColumnNumber - 1 - } - } - }; - } - - /// - /// Map ScriptFileMarker severity to Diagnostic severity - /// - /// - private static DiagnosticSeverity MapDiagnosticSeverity(ScriptFileMarkerLevel markerLevel) - { - switch (markerLevel) - { - case ScriptFileMarkerLevel.Error: - return DiagnosticSeverity.Error; - - case ScriptFileMarkerLevel.Warning: - return DiagnosticSeverity.Warning; - - case ScriptFileMarkerLevel.Information: - return DiagnosticSeverity.Information; - - default: - return DiagnosticSeverity.Error; - } - } - - /// - /// 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, - Line = changeRange.Start.Line + 1, - Offset = changeRange.Start.Character + 1, - EndLine = changeRange.End.Line + 1, - EndOffset = changeRange.End.Character + 1 - }; - } } } diff --git a/src/ServiceHost/Session/EditorSession.cs b/src/ServiceHost/Session/EditorSession.cs deleted file mode 100644 index 7b3dcc46..00000000 --- a/src/ServiceHost/Session/EditorSession.cs +++ /dev/null @@ -1,76 +0,0 @@ -// -// 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 Microsoft.SqlTools.EditorServices.Session; -using Microsoft.SqlTools.ServiceLayer.LanguageService; -using Microsoft.SqlTools.ServiceLayer.WorkspaceService.Contracts; - -namespace Microsoft.SqlTools.EditorServices -{ - /// - /// Manages a single session for all editor services. This - /// includes managing all open script files for the session. - /// - public class EditorSession : IDisposable - { - #region Properties - - /// - /// Gets the Workspace instance for this session. - /// - public Workspace Workspace { get; private set; } - - /// - /// Gets or sets the Language Service - /// - /// - public LanguageService LanguageService { get; set; } - - /// - /// Gets the SqlToolsContext instance for this session. - /// - public SqlToolsContext SqlToolsContext { get; private set; } - - #endregion - - #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); - - // 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 - - } -} diff --git a/src/ServiceHost/Session/HostDetails.cs b/src/ServiceHost/SqlContext/HostDetails.cs similarity index 98% rename from src/ServiceHost/Session/HostDetails.cs rename to src/ServiceHost/SqlContext/HostDetails.cs index 1a5fc80d..1b78faa4 100644 --- a/src/ServiceHost/Session/HostDetails.cs +++ b/src/ServiceHost/SqlContext/HostDetails.cs @@ -5,7 +5,7 @@ using System; -namespace Microsoft.SqlTools.EditorServices.Session +namespace Microsoft.SqlTools.ServiceLayer.SqlContext { /// /// Contains details about the current host application (most diff --git a/src/ServiceHost/Session/ProfilePaths.cs b/src/ServiceHost/SqlContext/ProfilePaths.cs similarity index 98% rename from src/ServiceHost/Session/ProfilePaths.cs rename to src/ServiceHost/SqlContext/ProfilePaths.cs index 4af38521..f841970d 100644 --- a/src/ServiceHost/Session/ProfilePaths.cs +++ b/src/ServiceHost/SqlContext/ProfilePaths.cs @@ -3,12 +3,11 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using System; using System.Collections.Generic; using System.IO; using System.Linq; -namespace Microsoft.SqlTools.EditorServices.Session +namespace Microsoft.SqlTools.ServiceLayer.SqlContext { /// /// Provides profile path resolution behavior relative to the name diff --git a/src/ServiceHost/Session/SqlToolsContext.cs b/src/ServiceHost/SqlContext/SqlToolsContext.cs similarity index 91% rename from src/ServiceHost/Session/SqlToolsContext.cs rename to src/ServiceHost/SqlContext/SqlToolsContext.cs index d8016afd..bf4d67c9 100644 --- a/src/ServiceHost/Session/SqlToolsContext.cs +++ b/src/ServiceHost/SqlContext/SqlToolsContext.cs @@ -5,7 +5,7 @@ using System; -namespace Microsoft.SqlTools.EditorServices.Session +namespace Microsoft.SqlTools.ServiceLayer.SqlContext { public class SqlToolsContext { diff --git a/src/ServiceHost/ServiceHost/ServiceHostSettings.cs b/src/ServiceHost/SqlContext/SqlToolsSettings.cs similarity index 82% rename from src/ServiceHost/ServiceHost/ServiceHostSettings.cs rename to src/ServiceHost/SqlContext/SqlToolsSettings.cs index 53d99647..a6f242ed 100644 --- a/src/ServiceHost/ServiceHost/ServiceHostSettings.cs +++ b/src/ServiceHost/SqlContext/SqlToolsSettings.cs @@ -1,25 +1,26 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System.IO; +using System.IO; using Microsoft.SqlTools.EditorServices.Utility; -namespace Microsoft.SqlTools.ServiceLayer.ServiceHost +namespace Microsoft.SqlTools.ServiceLayer.SqlContext { - public class ServiceHostSettings + public class SqlToolsSettings { - public bool EnableProfileLoading { get; set; } + // TODO: Is this needed? I can't make sense of this comment. + // NOTE: This property is capitalized as 'SqlTools' because the + // mode name sent from the client is written as 'SqlTools' and + // JSON.net is using camelCasing. + //public ServiceHostSettings SqlTools { get; set; } - public ScriptAnalysisSettings ScriptAnalysis { get; set; } - - public ServiceHostSettings() + public SqlToolsSettings() { this.ScriptAnalysis = new ScriptAnalysisSettings(); } - public void Update(ServiceHostSettings settings, string workspaceRootPath) + public bool EnableProfileLoading { get; set; } + + public ScriptAnalysisSettings ScriptAnalysis { get; set; } + + public void Update(SqlToolsSettings settings, string workspaceRootPath) { if (settings != null) { @@ -28,7 +29,6 @@ namespace Microsoft.SqlTools.ServiceLayer.ServiceHost } } } - public class ScriptAnalysisSettings { @@ -77,14 +77,4 @@ namespace Microsoft.SqlTools.ServiceLayer.ServiceHost } } } - - - public class LanguageServerSettingsWrapper - { - // NOTE: This property is capitalized as 'SqlTools' because the - // mode name sent from the client is written as 'SqlTools' and - // JSON.net is using camelCasing. - - public ServiceHostSettings SqlTools { get; set; } - } } diff --git a/src/ServiceHost/WorkspaceService/Contracts/ScriptFile.cs b/src/ServiceHost/WorkspaceService/Contracts/ScriptFile.cs index 4e30b840..134cdb9b 100644 --- a/src/ServiceHost/WorkspaceService/Contracts/ScriptFile.cs +++ b/src/ServiceHost/WorkspaceService/Contracts/ScriptFile.cs @@ -16,12 +16,6 @@ namespace Microsoft.SqlTools.ServiceLayer.WorkspaceService.Contracts /// public class ScriptFile { - #region Private Fields - - private Version SqlToolsVersion; - - #endregion - #region Properties /// @@ -113,18 +107,15 @@ namespace Microsoft.SqlTools.ServiceLayer.WorkspaceService.Contracts /// The path at which the script file resides. /// The path which the client uses to identify the file. /// The TextReader to use for reading the file's contents. - /// The version of SqlTools for which the script is being parsed. public ScriptFile( string filePath, string clientFilePath, - TextReader textReader, - Version SqlToolsVersion) + TextReader textReader) { this.FilePath = filePath; this.ClientFilePath = clientFilePath; this.IsAnalysisEnabled = true; this.IsInMemory = Workspace.IsPathInMemory(filePath); - this.SqlToolsVersion = SqlToolsVersion; this.SetFileContents(textReader.ReadToEnd()); } @@ -135,17 +126,14 @@ namespace Microsoft.SqlTools.ServiceLayer.WorkspaceService.Contracts /// The path at which the script file resides. /// The path which the client uses to identify the file. /// The initial contents of the script file. - /// The version of SqlTools for which the script is being parsed. public ScriptFile( string filePath, string clientFilePath, - string initialBuffer, - Version SqlToolsVersion) + string initialBuffer) { this.FilePath = filePath; this.ClientFilePath = clientFilePath; this.IsAnalysisEnabled = true; - this.SqlToolsVersion = SqlToolsVersion; this.SetFileContents(initialBuffer); } diff --git a/src/ServiceHost/WorkspaceService/Contracts/Workspace.cs b/src/ServiceHost/WorkspaceService/Workspace.cs similarity index 90% rename from src/ServiceHost/WorkspaceService/Contracts/Workspace.cs rename to src/ServiceHost/WorkspaceService/Workspace.cs index 83b655fd..6aa8f479 100644 --- a/src/ServiceHost/WorkspaceService/Contracts/Workspace.cs +++ b/src/ServiceHost/WorkspaceService/Workspace.cs @@ -3,25 +3,25 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.SqlTools.EditorServices.Utility; using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Text.RegularExpressions; using System.Linq; +using Microsoft.SqlTools.EditorServices.Utility; +using Microsoft.SqlTools.ServiceLayer.WorkspaceService.Contracts; -namespace Microsoft.SqlTools.ServiceLayer.WorkspaceService.Contracts +namespace Microsoft.SqlTools.ServiceLayer.WorkspaceService { /// /// Manages a "workspace" of script files that are open for a particular /// editing session. Also helps to navigate references between ScriptFiles. /// - public class Workspace + public class Workspace : IDisposable { - #region Private Fields + #region Private Fields - private Version SqlToolsVersion; private Dictionary workspaceFiles = new Dictionary(); #endregion @@ -40,10 +40,8 @@ namespace Microsoft.SqlTools.ServiceLayer.WorkspaceService.Contracts /// /// Creates a new instance of the Workspace class. /// - /// The version of SqlTools for which scripts will be parsed. - public Workspace(Version SqlToolsVersion) + public Workspace() { - this.SqlToolsVersion = SqlToolsVersion; } #endregion @@ -78,12 +76,7 @@ namespace Microsoft.SqlTools.ServiceLayer.WorkspaceService.Contracts using (FileStream fileStream = new FileStream(resolvedFilePath, FileMode.Open, FileAccess.Read)) using (StreamReader streamReader = new StreamReader(fileStream, Encoding.UTF8)) { - scriptFile = - new ScriptFile( - resolvedFilePath, - filePath, - streamReader, - this.SqlToolsVersion); + scriptFile = new ScriptFile(resolvedFilePath, filePath,streamReader); this.workspaceFiles.Add(keyName, scriptFile); } @@ -169,12 +162,7 @@ namespace Microsoft.SqlTools.ServiceLayer.WorkspaceService.Contracts ScriptFile scriptFile = null; if (!this.workspaceFiles.TryGetValue(keyName, out scriptFile)) { - scriptFile = - new ScriptFile( - resolvedFilePath, - filePath, - initialBuffer, - this.SqlToolsVersion); + scriptFile = new ScriptFile(resolvedFilePath, filePath, initialBuffer); this.workspaceFiles.Add(keyName, scriptFile); @@ -244,5 +232,17 @@ namespace Microsoft.SqlTools.ServiceLayer.WorkspaceService.Contracts } #endregion + + #region IDisposable Implementation + + /// + /// Disposes of any Runspaces that were created for the + /// services used in this session. + /// + public void Dispose() + { + } + + #endregion } } diff --git a/src/ServiceHost/WorkspaceService/WorkspaceService.cs b/src/ServiceHost/WorkspaceService/WorkspaceService.cs new file mode 100644 index 00000000..fa92997f --- /dev/null +++ b/src/ServiceHost/WorkspaceService/WorkspaceService.cs @@ -0,0 +1,216 @@ +// +// 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.Text; +using System.Threading.Tasks; +using Microsoft.SqlTools.EditorServices.Utility; +using Microsoft.SqlTools.ServiceLayer.ServiceHost.Protocol; +using Microsoft.SqlTools.ServiceLayer.WorkspaceService.Contracts; +using System.Linq; + +namespace Microsoft.SqlTools.ServiceLayer.WorkspaceService +{ + public class WorkspaceService where TConfig : new() + { + + #region Singleton Instance Implementation + + private static WorkspaceService instance; + + public static WorkspaceService Instance + { + get + { + if (instance == null) + { + instance = new WorkspaceService(); + } + return instance; + } + } + + private WorkspaceService() + { + ConfigurationNotificationHandlers = new List(); + TextDocumentChangeHandlers = new List(); + } + + #endregion + + #region Properties + + public Workspace Workspace { get; private set; } + + public TConfig CurrentSettings { get; private set; } + + public delegate Task DidChangeConfigurationNotificationHandler(TConfig newSettings, TConfig oldSettings, EventContext eventContext); + + public delegate Task DidChangeTextDocumentNotificationTask(ScriptFile[] changedFiles, EventContext eventContext); + + public List ConfigurationNotificationHandlers; + public List TextDocumentChangeHandlers; + + + #endregion + + #region Public Methods + + public void InitializeService(ServiceHost.ServiceHost serviceHost) + { + // Create a workspace that will handle state for the session + Workspace = new Workspace(); + CurrentSettings = new TConfig(); + + // Register the handlers for when changes to the workspae occur + serviceHost.SetEventHandler(DidChangeTextDocumentNotification.Type, HandleDidChangeTextDocumentNotification); + serviceHost.SetEventHandler(DidOpenTextDocumentNotification.Type, HandleDidOpenTextDocumentNotification); + serviceHost.SetEventHandler(DidCloseTextDocumentNotification.Type, HandleDidCloseTextDocumentNotification); + serviceHost.SetEventHandler(DidChangeConfigurationNotification.Type, HandleDidChangeConfigurationNotification); + + // Register an initialization handler that sets the workspace path + serviceHost.RegisterInitializeTask(async (parameters, contect) => + { + Logger.Write(LogLevel.Verbose, "Initializing workspace service"); + + if (Workspace != null) + { + Workspace.WorkspacePath = parameters.RootPath; + } + await Task.FromResult(0); + }); + + // Register a shutdown request that disposes the workspace + serviceHost.RegisterShutdownTask(async (parameters, context) => + { + Logger.Write(LogLevel.Verbose, "Shutting down workspace service"); + + if (Workspace != null) + { + Workspace.Dispose(); + Workspace = null; + } + await Task.FromResult(0); + }); + } + + /// + /// Adds a new task to be called when the configuration has been changed. Use this to + /// handle changing configuration and changing the current configuration. + /// + /// Task to handle the request + public void RegisterDidChangeConfigurationNotificationTask(DidChangeConfigurationNotificationHandler task) + { + ConfigurationNotificationHandlers.Add(task); + } + + /// + /// Adds a new task to be called when the text of a document changes. + /// + /// Delegate to call when the document changes + public void RegisterDidChangeTextDocumentNotificationTask(DidChangeTextDocumentNotificationTask task) + { + TextDocumentChangeHandlers.Add(task); + } + + #endregion + + #region Event Handlers + + /// + /// Handles text document change events + /// + /// + /// + /// + protected Task HandleDidChangeTextDocumentNotification( + DidChangeTextDocumentParams textChangeParams, + EventContext eventContext) + { + StringBuilder msg = new StringBuilder(); + msg.Append("HandleDidChangeTextDocumentNotification"); + List changedFiles = new List(); + + // A text change notification can batch multiple change requests + foreach (var textChange in textChangeParams.ContentChanges) + { + string fileUri = textChangeParams.TextDocument.Uri; + msg.AppendLine(String.Format(" File: {0}", fileUri)); + + ScriptFile changedFile = Workspace.GetFile(fileUri); + + changedFile.ApplyChange( + GetFileChangeDetails( + textChange.Range.Value, + textChange.Text)); + + changedFiles.Add(changedFile); + } + + Logger.Write(LogLevel.Verbose, msg.ToString()); + + var handlers = TextDocumentChangeHandlers.Select(t => t(changedFiles.ToArray(), eventContext)).ToArray(); + return Task.WhenAll(handlers); + } + + protected Task HandleDidOpenTextDocumentNotification( + DidOpenTextDocumentNotification openParams, + EventContext eventContext) + { + Logger.Write(LogLevel.Verbose, "HandleDidOpenTextDocumentNotification"); + return Task.FromResult(true); + } + + protected Task HandleDidCloseTextDocumentNotification( + TextDocumentIdentifier closeParams, + EventContext eventContext) + { + 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.Verbose, "HandleDidChangeConfigurationNotification"); + + // Propagate the changes to the event handlers + var configUpdateTasks = ConfigurationNotificationHandlers.Select( + t => t(configChangeParams.Settings, CurrentSettings, eventContext)).ToArray(); + await Task.WhenAll(configUpdateTasks); + } + + #endregion + + #region Private Helpers + + /// + /// 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, + Line = changeRange.Start.Line + 1, + Offset = changeRange.Start.Character + 1, + EndLine = changeRange.End.Line + 1, + EndOffset = changeRange.End.Character + 1 + }; + } + + #endregion + } +}