// // 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.Text; using System.Threading.Tasks; using Microsoft.SqlTools.EditorServices.Utility; using Microsoft.SqlTools.ServiceLayer.Hosting; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; namespace Microsoft.SqlTools.ServiceLayer.Workspace { /// /// Class for handling requests/events that deal with the state of the workspace, including the /// opening and closing of files, the changing of configuration, etc. /// /// /// The type of the class used for serializing and deserializing the configuration. Must be the /// actual type of the instance otherwise deserialization will be incomplete. /// public class WorkspaceService where TConfig : class, new() { #region Singleton Instance Implementation private static readonly Lazy> instance = new Lazy>(() => new WorkspaceService()); public static WorkspaceService Instance { get { return instance.Value; } } /// /// Default, parameterless constructor. /// TODO: Figure out how to make this truely singleton even with dependency injection for tests /// public WorkspaceService() { ConfigChangeCallbacks = new List(); TextDocChangeCallbacks = new List(); TextDocOpenCallbacks = new List(); } #endregion #region Properties public Workspace Workspace { get; private set; } public TConfig CurrentSettings { get; private set; } /// /// Delegate for callbacks that occur when the configuration for the workspace changes /// /// The settings that were just set /// The settings before they were changed /// Context of the event that triggered the callback /// public delegate Task ConfigChangeCallback(TConfig newSettings, TConfig oldSettings, EventContext eventContext); /// /// Delegate for callbacks that occur when the current text document changes /// /// Array of files that changed /// Context of the event raised for the changed files public delegate Task TextDocChangeCallback(ScriptFile[] changedFiles, EventContext eventContext); /// /// Delegate for callbacks that occur when a text document is opened /// /// File that was opened /// Context of the event raised for the changed files public delegate Task TextDocOpenCallback(ScriptFile openFile, EventContext eventContext); /// /// List of callbacks to call when the configuration of the workspace changes /// private List ConfigChangeCallbacks { get; set; } /// /// List of callbacks to call when the current text document changes /// private List TextDocChangeCallbacks { get; set; } /// /// List of callbacks to call when a text document is opened /// private List TextDocOpenCallbacks { get; set; } #endregion #region Public Methods public void InitializeService(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 RegisterConfigChangeCallback(ConfigChangeCallback task) { ConfigChangeCallbacks.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 RegisterTextDocChangeCallback(TextDocChangeCallback task) { TextDocChangeCallbacks.Add(task); } /// /// Adds a new task to be called when a file is opened /// /// Delegate to call when a document is opened public void RegisterTextDocOpenCallback(TextDocOpenCallback task) { TextDocOpenCallbacks.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.Uri ?? 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 = TextDocChangeCallbacks.Select(t => t(changedFiles.ToArray(), eventContext)); return Task.WhenAll(handlers); } protected async Task HandleDidOpenTextDocumentNotification( DidOpenTextDocumentNotification openParams, EventContext eventContext) { Logger.Write(LogLevel.Verbose, "HandleDidOpenTextDocumentNotification"); // read the SQL file contents into the ScriptFile ScriptFile openedFile = Workspace.GetFileBuffer(openParams.Uri, openParams.Text); // Propagate the changes to the event handlers var textDocOpenTasks = TextDocOpenCallbacks.Select( t => t(openedFile, eventContext)); await Task.WhenAll(textDocOpenTasks); } 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 = ConfigChangeCallbacks.Select( t => t(configChangeParams.Settings, CurrentSettings, eventContext)); 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 } }