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