diff --git a/src/Microsoft.SqlTools.ServiceLayer/Hosting/Protocol/EventContext.cs b/src/Microsoft.SqlTools.ServiceLayer/Hosting/Protocol/EventContext.cs index 4a1bc40e..42202a28 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Hosting/Protocol/EventContext.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Hosting/Protocol/EventContext.cs @@ -14,7 +14,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol /// public class EventContext { - private MessageWriter messageWriter; + private readonly MessageWriter messageWriter; + + /// + /// Parameterless constructor required for mocking + /// + public EventContext() { } public EventContext(MessageWriter messageWriter) { diff --git a/src/Microsoft.SqlTools.ServiceLayer/Workspace/WorkspaceService.cs b/src/Microsoft.SqlTools.ServiceLayer/Workspace/WorkspaceService.cs index d6ad3bf4..dad14f90 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Workspace/WorkspaceService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Workspace/WorkspaceService.cs @@ -44,6 +44,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace ConfigChangeCallbacks = new List(); TextDocChangeCallbacks = new List(); TextDocOpenCallbacks = new List(); + TextDocCloseCallbacks = new List(); CurrentSettings = new TConfig(); } @@ -55,7 +56,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace /// /// Workspace object for the service. Virtual to allow for mocking /// - public virtual Workspace Workspace { get; private set; } + public virtual Workspace Workspace { get; internal set; } /// /// Current settings for the workspace @@ -84,7 +85,14 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace /// File that was opened /// Context of the event raised for the changed files public delegate Task TextDocOpenCallback(ScriptFile openFile, EventContext eventContext); - + + /// + /// Delegate for callbacks that occur when a text document is closed + /// + /// File that was closed + /// Context of the event raised for changed files + public delegate Task TextDocCloseCallback(ScriptFile closedFile, EventContext eventContext); + /// /// List of callbacks to call when the configuration of the workspace changes /// @@ -100,6 +108,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace /// private List TextDocOpenCallbacks { get; set; } + /// + /// List of callbacks to call when a text document is closed + /// + private List TextDocCloseCallbacks { get; set; } #endregion @@ -161,6 +173,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace TextDocChangeCallbacks.Add(task); } + /// + /// Adds a new task to be called when a text document closes. + /// + /// Delegate to call when the document closes + public void RegisterTextDocCloseCallback(TextDocCloseCallback task) + { + TextDocCloseCallbacks.Add(task); + } + /// /// Adds a new task to be called when a file is opened /// @@ -177,7 +198,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace /// /// Handles text document change events /// - protected Task HandleDidChangeTextDocumentNotification( + internal Task HandleDidChangeTextDocumentNotification( DidChangeTextDocumentParams textChangeParams, EventContext eventContext) { @@ -216,7 +237,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace } } - protected async Task HandleDidOpenTextDocumentNotification( + internal async Task HandleDidOpenTextDocumentNotification( DidOpenTextDocumentNotification openParams, EventContext eventContext) { @@ -232,18 +253,31 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace await Task.WhenAll(textDocOpenTasks); } - protected Task HandleDidCloseTextDocumentNotification( + internal async Task HandleDidCloseTextDocumentNotification( DidCloseTextDocumentParams closeParams, EventContext eventContext) { Logger.Write(LogLevel.Verbose, "HandleDidCloseTextDocumentNotification"); - return Task.FromResult(true); + + // Skip closing this file if the file doesn't exist + var closedFile = Workspace.GetFile(closeParams.TextDocument.Uri); + if (closedFile == null) + { + return; + } + + // Trash the existing document from our mapping + Workspace.CloseFile(closedFile); + + // Send out a notification to other services that have subscribed to this event + var textDocClosedTasks = TextDocCloseCallbacks.Select(t => t(closedFile, eventContext)); + await Task.WhenAll(textDocClosedTasks); } /// /// Handles the configuration change event /// - protected async Task HandleDidChangeConfigurationNotification( + internal async Task HandleDidChangeConfigurationNotification( DidChangeConfigurationParams configChangeParams, EventContext eventContext) { diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/Workspace/WorkspaceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/Workspace/WorkspaceTests.cs new file mode 100644 index 00000000..3c11ba58 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/Workspace/WorkspaceTests.cs @@ -0,0 +1,94 @@ +// +// 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.Threading.Tasks; +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.SqlContext; +using Microsoft.SqlTools.ServiceLayer.Workspace; +using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; +using Microsoft.SqlTools.Test.Utility; +using Moq; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.Test.Workspace +{ + public class WorkspaceTests + { + [Fact] + public async Task FileClosedSuccessfully() + { + // Given: + // ... A workspace that has a single file open + var workspace = new ServiceLayer.Workspace.Workspace(); + var workspaceService = new WorkspaceService {Workspace = workspace}; + var openedFile = workspace.GetFileBuffer(TestObjects.ScriptUri, string.Empty); + Assert.NotNull(openedFile); + Assert.NotEmpty(workspace.GetOpenedFiles()); + + // ... And there is a callback registered for the file closed event + ScriptFile closedFile = null; + workspaceService.RegisterTextDocCloseCallback((f, c) => + { + closedFile = f; + return Task.FromResult(true); + }); + + // If: + // ... An event to close the open file occurs + var eventContext = new Mock().Object; + var requestParams = new DidCloseTextDocumentParams + { + TextDocument = new TextDocumentItem {Uri = TestObjects.ScriptUri} + }; + await workspaceService.HandleDidCloseTextDocumentNotification(requestParams, eventContext); + + // Then: + // ... The file should no longer be in the open files + Assert.Empty(workspace.GetOpenedFiles()); + + // ... The callback should have been called + // ... The provided script file should be the one we created + Assert.NotNull(closedFile); + Assert.Equal(openedFile, closedFile); + } + + [Fact] + public async Task FileClosedNotOpen() + { + // Given: + // ... A workspace that has no files open + var workspace = new ServiceLayer.Workspace.Workspace(); + var workspaceService = new WorkspaceService {Workspace = workspace}; + Assert.Empty(workspace.GetOpenedFiles()); + + // ... And there is a callback registered for the file closed event + bool callbackCalled = false; + workspaceService.RegisterTextDocCloseCallback((f, c) => + { + callbackCalled = true; + return Task.FromResult(true); + }); + + // If: + // ... An event to close the a file occurs + var eventContext = new Mock().Object; + var requestParams = new DidCloseTextDocumentParams + { + TextDocument = new TextDocumentItem {Uri = TestObjects.ScriptUri} + }; + // Then: + // ... There should be a file not found exception thrown + // TODO: This logic should be changed to not create the ScriptFile + await Assert.ThrowsAsync( + () => workspaceService.HandleDidCloseTextDocumentNotification(requestParams, eventContext)); + + // ... There should still be no open files + // ... The callback should not have been called + Assert.Empty(workspace.GetOpenedFiles()); + Assert.False(callbackCalled); + } + } +}