The last 10% of the refactor

This will build successfully
This commit is contained in:
Benjamin Russell
2016-07-22 16:41:23 -07:00
parent 85668cb3de
commit b9f041cdf4
11 changed files with 533 additions and 536 deletions

View File

@@ -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
}
}
/// <summary>
/// Constructor for the Language Service class
/// </summary>
/// <param name="context"></param>
private LanguageService(SqlToolsContext context)
{
this.Context = context;
}
/// <summary>
/// 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<SqlToolsSettings>.Instance.CurrentSettings; }
}
private Workspace CurrentWorkspace
{
get { return WorkspaceService<SqlToolsSettings>.Instance.Workspace; }
}
/// <summary>
/// Gets or sets the current SQL Tools context
/// </summary>
/// <returns></returns>
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<SqlToolsSettings>.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<SqlToolsSettings>.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
/// <summary>
/// Gets a list of semantic diagnostic marks for the provided script file
/// </summary>
/// <param name="scriptFile"></param>
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];
}
/// <summary>
/// Runs script diagnostics on changed files
/// </summary>
/// <param name="filesToAnalyze"></param>
/// <param name="eventContext"></param>
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<bool> cancelTask = new TaskCompletionSource<bool>();
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);
}
/// <summary>
/// Actually run the script diagnostics after waiting for some small delay
/// </summary>
/// <param name="delayMilliseconds"></param>
/// <param name="filesToAnalyze"></param>
/// <param name="eventContext"></param>
/// <param name="cancellationToken"></param>
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);
}
}
/// <summary>
/// Send the diagnostic results back to the host application
/// </summary>
/// <param name="scriptFile"></param>
/// <param name="semanticMarkers"></param>
/// <param name="eventContext"></param>
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()
});
}
/// <summary>
/// Convert a ScriptFileMarker to a Diagnostic that is Language Service compatible
/// </summary>
/// <param name="scriptFileMarker"></param>
/// <returns></returns>
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
}
}
};
}
/// <summary>
/// Map ScriptFileMarker severity to Diagnostic severity
/// </summary>
/// <param name="markerLevel"></param>
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
}
}

View File

@@ -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<SqlToolsSettings>.Instance.InitializeService(serviceHost);
LanguageService.LanguageService.Instance.InitializeService(serviceHost, sqlToolsContext);
// Start the service
serviceHost.Start().Wait();

View File

@@ -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
/// <summary>
/// Creates or retrieves the current instance of the ServiceHost
/// </summary>
/// <param name="hostDetails">Details about the host application</param>
/// <param name="profilePaths">Details about the profile</param>
/// <returns>Instance of the service host</returns>
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.
/// </summary>
/// <param name="hostDetails">Details about the host application</param>
/// <param name="profilePaths">Details about the profile</param>
private ServiceHost(HostDetails hostDetails, ProfilePaths profilePaths)
: base(new StdioServerChannel())
private ServiceHost() : base(new StdioServerChannel())
{
// Initialize the shutdown activities
shutdownActivities = new List<ShutdownHandler>();
// 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<InitializeHandler>();
// 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<object> shutdownRequestContext);
public delegate Task InitializeHandler(InitializeRequest startupParams, RequestContext<InitializeResult> requestContext);
private readonly List<ShutdownHandler> shutdownActivities;
private readonly List<InitializeHandler> initializeActivities;
#endregion
#region Public Methods
@@ -98,24 +78,18 @@ namespace Microsoft.SqlTools.ServiceLayer.ServiceHost
shutdownActivities.Add(activity);
}
/// <summary>
/// Add a new method to be called when the initialize request is submitted
/// </summary>
/// <param name="activity"></param>
public void RegisterInitializeTask(InitializeHandler activity)
{
initializeActivities.Add(activity);
}
#endregion
#region Private Methods
/// <summary>
/// Initialize the VS Code request/response callbacks
/// </summary>
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<LanguageServerSettingsWrapper>.Type, this.HandleDidChangeConfigurationNotification);
}
#region Request Handlers
/// <summary>
/// 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;
}
}
/// <summary>
@@ -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
///////////////////////////////////////////////
/// <summary>
/// Handles text document change events
/// </summary>
/// <param name="textChangeParams"></param>
/// <param name="eventContext"></param>
/// <returns></returns>
protected Task HandleDidChangeTextDocumentNotification(
DidChangeTextDocumentParams textChangeParams,
EventContext eventContext)
{
StringBuilder msg = new StringBuilder();
msg.Append("HandleDidChangeTextDocumentNotification");
List<ScriptFile> changedFiles = new List<ScriptFile>();
// 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);
}
/// <summary>
/// Handles the configuration change event
/// </summary>
/// <param name="configChangeParams"></param>
/// <param name="eventContext"></param>
protected async Task HandleDidChangeConfigurationNotification(
DidChangeConfigurationParams<LanguageServerSettingsWrapper> 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);
}
/// <summary>
/// Runs script diagnostics on changed files
/// </summary>
/// <param name="filesToAnalyze"></param>
/// <param name="editorSession"></param>
/// <param name="eventContext"></param>
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<bool> cancelTask = new TaskCompletionSource<bool>();
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);
}
/// <summary>
/// Actually run the script diagnostics after waiting for some small delay
/// </summary>
/// <param name="delayMilliseconds"></param>
/// <param name="filesToAnalyze"></param>
/// <param name="editorSession"></param>
/// <param name="eventContext"></param>
/// <param name="cancellationToken"></param>
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);
}
}
/// <summary>
/// Send the diagnostic results back to the host application
/// </summary>
/// <param name="scriptFile"></param>
/// <param name="semanticMarkers"></param>
/// <param name="eventContext"></param>
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()
});
}
/// <summary>
/// Convert a ScriptFileMarker to a Diagnostic that is Language Service compatible
/// </summary>
/// <param name="scriptFileMarker"></param>
/// <returns></returns>
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
}
}
};
}
/// <summary>
/// Map ScriptFileMarker severity to Diagnostic severity
/// </summary>
/// <param name="markerLevel"></param>
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;
}
}
/// <summary>
/// Switch from 0-based offsets to 1 based offsets
/// </summary>
/// <param name="changeRange"></param>
/// <param name="insertString"></param>
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
};
}
}
}

View File

@@ -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
{
/// <summary>
/// Manages a single session for all editor services. This
/// includes managing all open script files for the session.
/// </summary>
public class EditorSession : IDisposable
{
#region Properties
/// <summary>
/// Gets the Workspace instance for this session.
/// </summary>
public Workspace Workspace { get; private set; }
/// <summary>
/// Gets or sets the Language Service
/// </summary>
/// <returns></returns>
public LanguageService LanguageService { get; set; }
/// <summary>
/// Gets the SqlToolsContext instance for this session.
/// </summary>
public SqlToolsContext SqlToolsContext { get; private set; }
#endregion
#region Public Methods
/// <summary>
/// Starts the session using the provided IConsoleHost implementation
/// for the ConsoleService.
/// </summary>
/// <param name="hostDetails">
/// Provides details about the host application.
/// </param>
/// <param name="profilePaths">
/// An object containing the profile paths for the session.
/// </param>
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
/// <summary>
/// Disposes of any Runspaces that were created for the
/// services used in this session.
/// </summary>
public void Dispose()
{
}
#endregion
}
}

View File

@@ -5,7 +5,7 @@
using System;
namespace Microsoft.SqlTools.EditorServices.Session
namespace Microsoft.SqlTools.ServiceLayer.SqlContext
{
/// <summary>
/// Contains details about the current host application (most

View File

@@ -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
{
/// <summary>
/// Provides profile path resolution behavior relative to the name

View File

@@ -5,7 +5,7 @@
using System;
namespace Microsoft.SqlTools.EditorServices.Session
namespace Microsoft.SqlTools.ServiceLayer.SqlContext
{
public class SqlToolsContext
{

View File

@@ -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; }
}
}

View File

@@ -16,12 +16,6 @@ namespace Microsoft.SqlTools.ServiceLayer.WorkspaceService.Contracts
/// </summary>
public class ScriptFile
{
#region Private Fields
private Version SqlToolsVersion;
#endregion
#region Properties
/// <summary>
@@ -113,18 +107,15 @@ namespace Microsoft.SqlTools.ServiceLayer.WorkspaceService.Contracts
/// <param name="filePath">The path at which the script file resides.</param>
/// <param name="clientFilePath">The path which the client uses to identify the file.</param>
/// <param name="textReader">The TextReader to use for reading the file's contents.</param>
/// <param name="SqlToolsVersion">The version of SqlTools for which the script is being parsed.</param>
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
/// <param name="filePath">The path at which the script file resides.</param>
/// <param name="clientFilePath">The path which the client uses to identify the file.</param>
/// <param name="initialBuffer">The initial contents of the script file.</param>
/// <param name="SqlToolsVersion">The version of SqlTools for which the script is being parsed.</param>
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);
}

View File

@@ -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
{
/// <summary>
/// Manages a "workspace" of script files that are open for a particular
/// editing session. Also helps to navigate references between ScriptFiles.
/// </summary>
public class Workspace
public class Workspace : IDisposable
{
#region Private Fields
#region Private Fields
private Version SqlToolsVersion;
private Dictionary<string, ScriptFile> workspaceFiles = new Dictionary<string, ScriptFile>();
#endregion
@@ -40,10 +40,8 @@ namespace Microsoft.SqlTools.ServiceLayer.WorkspaceService.Contracts
/// <summary>
/// Creates a new instance of the Workspace class.
/// </summary>
/// <param name="SqlToolsVersion">The version of SqlTools for which scripts will be parsed.</param>
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
/// <summary>
/// Disposes of any Runspaces that were created for the
/// services used in this session.
/// </summary>
public void Dispose()
{
}
#endregion
}
}

View File

@@ -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<TConfig> where TConfig : new()
{
#region Singleton Instance Implementation
private static WorkspaceService<TConfig> instance;
public static WorkspaceService<TConfig> Instance
{
get
{
if (instance == null)
{
instance = new WorkspaceService<TConfig>();
}
return instance;
}
}
private WorkspaceService()
{
ConfigurationNotificationHandlers = new List<DidChangeConfigurationNotificationHandler>();
TextDocumentChangeHandlers = new List<DidChangeTextDocumentNotificationTask>();
}
#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<DidChangeConfigurationNotificationHandler> ConfigurationNotificationHandlers;
public List<DidChangeTextDocumentNotificationTask> 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<TConfig>.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);
});
}
/// <summary>
/// Adds a new task to be called when the configuration has been changed. Use this to
/// handle changing configuration and changing the current configuration.
/// </summary>
/// <param name="task">Task to handle the request</param>
public void RegisterDidChangeConfigurationNotificationTask(DidChangeConfigurationNotificationHandler task)
{
ConfigurationNotificationHandlers.Add(task);
}
/// <summary>
/// Adds a new task to be called when the text of a document changes.
/// </summary>
/// <param name="task">Delegate to call when the document changes</param>
public void RegisterDidChangeTextDocumentNotificationTask(DidChangeTextDocumentNotificationTask task)
{
TextDocumentChangeHandlers.Add(task);
}
#endregion
#region Event Handlers
/// <summary>
/// Handles text document change events
/// </summary>
/// <param name="textChangeParams"></param>
/// <param name="eventContext"></param>
/// <returns></returns>
protected Task HandleDidChangeTextDocumentNotification(
DidChangeTextDocumentParams textChangeParams,
EventContext eventContext)
{
StringBuilder msg = new StringBuilder();
msg.Append("HandleDidChangeTextDocumentNotification");
List<ScriptFile> changedFiles = new List<ScriptFile>();
// 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);
}
/// <summary>
/// Handles the configuration change event
/// </summary>
/// <param name="configChangeParams"></param>
/// <param name="eventContext"></param>
protected async Task HandleDidChangeConfigurationNotification(
DidChangeConfigurationParams<TConfig> 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
/// <summary>
/// Switch from 0-based offsets to 1 based offsets
/// </summary>
/// <param name="changeRange"></param>
/// <param name="insertString"></param>
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
}
}