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);
+ }
+ }
+}