//
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.SqlTools.EditorServices.Utility;
using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
using Microsoft.SqlTools.ServiceLayer.SqlContext;
using Microsoft.SqlTools.ServiceLayer.WorkspaceServices;
using Microsoft.SqlTools.ServiceLayer.WorkspaceServices.Contracts;
using System.Linq;
using Microsoft.SqlServer.Management.SqlParser.Parser;
using Location = Microsoft.SqlTools.ServiceLayer.WorkspaceServices.Contracts.Location;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
///
/// Main class for Language Service functionality including anything that reqires knowledge of
/// the language to perfom, such as definitions, intellisense, etc.
///
public sealed class LanguageService
{
#region Singleton Instance Implementation
private static readonly Lazy instance = new Lazy(() => new LanguageService());
public static LanguageService Instance
{
get { return instance.Value; }
}
///
/// Default, parameterless constructor.
/// TODO: Figure out how to make this truely singleton even with dependency injection for tests
///
public 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; }
///
/// The cached parse result from previous incremental parse
///
private ParseResult prevParseResult;
#endregion
#region Public Methods
public void InitializeService(ServiceHost serviceHost, SqlToolsContext context)
{
// Register the requests that this service will handle
serviceHost.SetRequestHandler(DefinitionRequest.Type, HandleDefinitionRequest);
serviceHost.SetRequestHandler(ReferencesRequest.Type, HandleReferencesRequest);
serviceHost.SetRequestHandler(CompletionRequest.Type, HandleCompletionRequest);
serviceHost.SetRequestHandler(CompletionResolveRequest.Type, HandleCompletionResolveRequest);
serviceHost.SetRequestHandler(SignatureHelpRequest.Type, HandleSignatureHelpRequest);
serviceHost.SetRequestHandler(DocumentHighlightRequest.Type, HandleDocumentHighlightRequest);
serviceHost.SetRequestHandler(HoverRequest.Type, HandleHoverRequest);
serviceHost.SetRequestHandler(DocumentSymbolRequest.Type, HandleDocumentSymbolRequest);
serviceHost.SetRequestHandler(WorkspaceSymbolRequest.Type, HandleWorkspaceSymbolRequest);
// Register a no-op shutdown task for validation of the shutdown logic
serviceHost.RegisterShutdownTask(async (shutdownParams, shutdownRequestContext) =>
{
Logger.Write(LogLevel.Verbose, "Shutting down language service");
await Task.FromResult(0);
});
// Register the configuration update handler
WorkspaceService.Instance.RegisterConfigChangeCallback(HandleDidChangeConfigurationNotification);
// Store the SqlToolsContext for future use
Context = context;
}
///
/// Gets a list of semantic diagnostic marks for the provided script file
///
///
public ScriptFileMarker[] GetSemanticMarkers(ScriptFile scriptFile)
{
// parse current SQL file contents to retrieve a list of errors
ParseOptions parseOptions = new ParseOptions();
ParseResult parseResult = Parser.IncrementalParse(
scriptFile.Contents,
prevParseResult,
parseOptions);
// save previous result for next incremental parse
this.prevParseResult = parseResult;
// build a list of SQL script file markers from the errors
List markers = new List();
foreach (var error in parseResult.Errors)
{
markers.Add(new ScriptFileMarker()
{
Message = error.Message,
Level = ScriptFileMarkerLevel.Error,
ScriptRegion = new ScriptRegion()
{
File = scriptFile.FilePath,
StartLineNumber = error.Start.LineNumber,
StartColumnNumber = error.Start.ColumnNumber,
StartOffset = 0,
EndLineNumber = error.End.LineNumber,
EndColumnNumber = error.End.ColumnNumber,
EndOffset = 0
}
});
}
return markers.ToArray();
}
#endregion
#region Request Handlers
private static async Task HandleDefinitionRequest(
TextDocumentPosition textDocumentPosition,
RequestContext requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleDefinitionRequest");
await Task.FromResult(true);
}
private static async Task HandleReferencesRequest(
ReferencesParams referencesParams,
RequestContext requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleReferencesRequest");
await Task.FromResult(true);
}
private static async Task HandleCompletionRequest(
TextDocumentPosition textDocumentPosition,
RequestContext requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleCompletionRequest");
await Task.FromResult(true);
}
private static async Task HandleCompletionResolveRequest(
CompletionItem completionItem,
RequestContext requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleCompletionResolveRequest");
await Task.FromResult(true);
}
private static async Task HandleSignatureHelpRequest(
TextDocumentPosition textDocumentPosition,
RequestContext requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleSignatureHelpRequest");
await Task.FromResult(true);
}
private static async Task HandleDocumentHighlightRequest(
TextDocumentPosition textDocumentPosition,
RequestContext requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleDocumentHighlightRequest");
await Task.FromResult(true);
}
private static async Task HandleHoverRequest(
TextDocumentPosition textDocumentPosition,
RequestContext requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleHoverRequest");
await Task.FromResult(true);
}
private static async Task HandleDocumentSymbolRequest(
TextDocumentIdentifier textDocumentIdentifier,
RequestContext requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleDocumentSymbolRequest");
await Task.FromResult(true);
}
private static async Task HandleWorkspaceSymbolRequest(
WorkspaceSymbolParams workspaceSymbolParams,
RequestContext requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleWorkspaceSymbolRequest");
await Task.FromResult(true);
}
#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
///
/// 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
}
}