Merge branch 'dev' into feature/queryExecutionV1

Also adding a fancy new mocked out reader for mocking db calls.
This commit is contained in:
Benjamin Russell
2016-08-01 14:27:57 -07:00
86 changed files with 475 additions and 181 deletions

View File

@@ -0,0 +1,139 @@
//
// 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.Data;
using System.Data.SqlClient;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.ConnectionServices;
using Microsoft.SqlTools.ServiceLayer.ConnectionServices.Contracts;
using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
using Microsoft.SqlTools.ServiceLayer.WorkspaceServices.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
/// <summary>
/// Main class for Autocomplete functionality
/// </summary>
public class AutoCompleteService
{
#region Singleton Instance Implementation
/// <summary>
/// Singleton service instance
/// </summary>
private static Lazy<AutoCompleteService> instance
= new Lazy<AutoCompleteService>(() => new AutoCompleteService());
/// <summary>
/// Gets the singleton service instance
/// </summary>
public static AutoCompleteService Instance
{
get
{
return instance.Value;
}
}
/// <summary>
/// Default, parameterless constructor.
/// TODO: Figure out how to make this truely singleton even with dependency injection for tests
/// </summary>
public AutoCompleteService()
{
}
#endregion
/// <summary>
/// Gets the current autocomplete candidate list
/// </summary>
public IEnumerable<string> AutoCompleteList { get; private set; }
public void InitializeService(ServiceHost serviceHost)
{
// Register a callback for when a connection is created
ConnectionService.Instance.RegisterOnConnectionTask(UpdateAutoCompleteCache);
}
/// <summary>
/// Update the cached autocomplete candidate list when the user connects to a database
/// TODO: Update with refactoring/async
/// </summary>
/// <param name="connection"></param>
public async Task UpdateAutoCompleteCache(ISqlConnection connection)
{
IDbCommand command = connection.CreateCommand();
command.CommandText = "SELECT name FROM sys.tables";
command.CommandTimeout = 15;
command.CommandType = CommandType.Text;
var reader = command.ExecuteReader();
List<string> results = new List<string>();
while (reader.Read())
{
results.Add(reader[0].ToString());
}
AutoCompleteList = results;
await Task.FromResult(0);
}
/// <summary>
/// Return the completion item list for the current text position
/// </summary>
/// <param name="textDocumentPosition"></param>
public CompletionItem[] GetCompletionItems(TextDocumentPosition textDocumentPosition)
{
var completions = new List<CompletionItem>();
int i = 0;
// the completion list will be null is user not connected to server
if (this.AutoCompleteList != null)
{
foreach (var autoCompleteItem in this.AutoCompleteList)
{
// convert the completion item candidates into CompletionItems
completions.Add(new CompletionItem()
{
Label = autoCompleteItem,
Kind = CompletionItemKind.Keyword,
Detail = autoCompleteItem + " details",
Documentation = autoCompleteItem + " documentation",
TextEdit = new TextEdit
{
NewText = autoCompleteItem,
Range = new Range
{
Start = new Position
{
Line = textDocumentPosition.Position.Line,
Character = textDocumentPosition.Position.Character
},
End = new Position
{
Line = textDocumentPosition.Position.Line,
Character = textDocumentPosition.Position.Character + 5
}
}
}
});
// only show 50 items
if (++i == 50)
{
break;
}
}
}
return completions.ToArray();
}
}
}

View File

@@ -0,0 +1,86 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Diagnostics;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.WorkspaceServices.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
{
public class CompletionRequest
{
public static readonly
RequestType<TextDocumentPosition, CompletionItem[]> Type =
RequestType<TextDocumentPosition, CompletionItem[]>.Create("textDocument/completion");
}
public class CompletionResolveRequest
{
public static readonly
RequestType<CompletionItem, CompletionItem> Type =
RequestType<CompletionItem, CompletionItem>.Create("completionItem/resolve");
}
public enum CompletionItemKind
{
Text = 1,
Method = 2,
Function = 3,
Constructor = 4,
Field = 5,
Variable = 6,
Class = 7,
Interface = 8,
Module = 9,
Property = 10,
Unit = 11,
Value = 12,
Enum = 13,
Keyword = 14,
Snippet = 15,
Color = 16,
File = 17,
Reference = 18
}
[DebuggerDisplay("NewText = {NewText}, Range = {Range.Start.Line}:{Range.Start.Character} - {Range.End.Line}:{Range.End.Character}")]
public class TextEdit
{
public Range Range { get; set; }
public string NewText { get; set; }
}
[DebuggerDisplay("Kind = {Kind.ToString()}, Label = {Label}, Detail = {Detail}")]
public class CompletionItem
{
public string Label { get; set; }
public CompletionItemKind? Kind { get; set; }
public string Detail { get; set; }
/// <summary>
/// Gets or sets the documentation string for the completion item.
/// </summary>
public string Documentation { get; set; }
public string SortText { get; set; }
public string FilterText { get; set; }
public string InsertText { get; set; }
public TextEdit TextEdit { get; set; }
/// <summary>
/// Gets or sets a custom data field that allows the server to mark
/// each completion item with an identifier that will help correlate
/// the item to the previous completion request during a completion
/// resolve request.
/// </summary>
public object Data { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.WorkspaceServices.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
{
public class DefinitionRequest
{
public static readonly
RequestType<TextDocumentPosition, Location[]> Type =
RequestType<TextDocumentPosition, Location[]>.Create("textDocument/definition");
}
}

View File

@@ -0,0 +1,72 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.WorkspaceServices.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
{
public class PublishDiagnosticsNotification
{
public static readonly
EventType<PublishDiagnosticsNotification> Type =
EventType<PublishDiagnosticsNotification>.Create("textDocument/publishDiagnostics");
/// <summary>
/// Gets or sets the URI for which diagnostic information is reported.
/// </summary>
public string Uri { get; set; }
/// <summary>
/// Gets or sets the array of diagnostic information items.
/// </summary>
public Diagnostic[] Diagnostics { get; set; }
}
public enum DiagnosticSeverity
{
/// <summary>
/// Indicates that the diagnostic represents an error.
/// </summary>
Error = 1,
/// <summary>
/// Indicates that the diagnostic represents a warning.
/// </summary>
Warning = 2,
/// <summary>
/// Indicates that the diagnostic represents an informational message.
/// </summary>
Information = 3,
/// <summary>
/// Indicates that the diagnostic represents a hint.
/// </summary>
Hint = 4
}
public class Diagnostic
{
public Range Range { get; set; }
/// <summary>
/// Gets or sets the severity of the diagnostic. If omitted, the
/// client should interpret the severity.
/// </summary>
public DiagnosticSeverity? Severity { get; set; }
/// <summary>
/// Gets or sets the diagnostic's code (optional).
/// </summary>
public string Code { get; set; }
/// <summary>
/// Gets or sets the diagnostic message.
/// </summary>
public string Message { get; set; }
}
}

View File

@@ -0,0 +1,32 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.WorkspaceServices.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
{
public enum DocumentHighlightKind
{
Text = 1,
Read = 2,
Write = 3
}
public class DocumentHighlight
{
public Range Range { get; set; }
public DocumentHighlightKind Kind { get; set; }
}
public class DocumentHighlightRequest
{
public static readonly
RequestType<TextDocumentPosition, DocumentHighlight[]> Type =
RequestType<TextDocumentPosition, DocumentHighlight[]>.Create("textDocument/documentHighlight");
}
}

View File

@@ -0,0 +1,16 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
{
public class ExpandAliasRequest
{
public static readonly
RequestType<string, string> Type =
RequestType<string, string>.Create("SqlTools/expandAlias");
}
}

View File

@@ -0,0 +1,24 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Collections.Generic;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
{
public class FindModuleRequest
{
public static readonly
RequestType<List<PSModuleMessage>, object> Type =
RequestType<List<PSModuleMessage>, object>.Create("SqlTools/findModule");
}
public class PSModuleMessage
{
public string Name { get; set; }
public string Description { get; set; }
}
}

View File

@@ -0,0 +1,33 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.WorkspaceServices.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
{
public class MarkedString
{
public string Language { get; set; }
public string Value { get; set; }
}
public class Hover
{
public MarkedString[] Contents { get; set; }
public Range? Range { get; set; }
}
public class HoverRequest
{
public static readonly
RequestType<TextDocumentPosition, Hover> Type =
RequestType<TextDocumentPosition, Hover>.Create("textDocument/hover");
}
}

View File

@@ -0,0 +1,16 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
{
class InstallModuleRequest
{
public static readonly
RequestType<string, object> Type =
RequestType<string, object>.Create("SqlTools/installModule");
}
}

View File

@@ -0,0 +1,28 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.WorkspaceServices.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
{
public class ReferencesRequest
{
public static readonly
RequestType<ReferencesParams, Location[]> Type =
RequestType<ReferencesParams, Location[]>.Create("textDocument/references");
}
public class ReferencesParams : TextDocumentPosition
{
public ReferencesContext Context { get; set; }
}
public class ReferencesContext
{
public bool IncludeDeclaration { get; set; }
}
}

View File

@@ -0,0 +1,16 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
{
public class ShowOnlineHelpRequest
{
public static readonly
RequestType<string, object> Type =
RequestType<string, object>.Create("SqlTools/showOnlineHelp");
}
}

View File

@@ -0,0 +1,43 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.WorkspaceServices.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
{
public class SignatureHelpRequest
{
public static readonly
RequestType<TextDocumentPosition, SignatureHelp> Type =
RequestType<TextDocumentPosition, SignatureHelp>.Create("textDocument/signatureHelp");
}
public class ParameterInformation
{
public string Label { get; set; }
public string Documentation { get; set; }
}
public class SignatureInformation
{
public string Label { get; set; }
public string Documentation { get; set; }
public ParameterInformation[] Parameters { get; set; }
}
public class SignatureHelp
{
public SignatureInformation[] Signatures { get; set; }
public int? ActiveSignature { get; set; }
public int? ActiveParameter { get; set; }
}
}

View File

@@ -0,0 +1,504 @@
//
// 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;
using Microsoft.SqlTools.ServiceLayer.ConnectionServices;
using Microsoft.SqlTools.ServiceLayer.ConnectionServices.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
/// <summary>
/// Main class for Language Service functionality including anything that reqires knowledge of
/// the language to perfom, such as definitions, intellisense, etc.
/// </summary>
public sealed class LanguageService
{
#region Singleton Instance Implementation
private static readonly Lazy<LanguageService> instance = new Lazy<LanguageService>(() => new LanguageService());
public static LanguageService Instance
{
get { return instance.Value; }
}
/// <summary>
/// Default, parameterless constructor.
/// </summary>
internal 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; }
/// <summary>
/// The cached parse result from previous incremental parse
/// </summary>
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<SqlToolsSettings>.Instance.RegisterConfigChangeCallback(HandleDidChangeConfigurationNotification);
// Register the file change update handler
WorkspaceService<SqlToolsSettings>.Instance.RegisterTextDocChangeCallback(HandleDidChangeTextDocumentNotification);
// Register the file open update handler
WorkspaceService<SqlToolsSettings>.Instance.RegisterTextDocOpenCallback(HandleDidOpenTextDocumentNotification);
// register an OnConnection callback
ConnectionService.Instance.RegisterOnConnectionTask(OnConnection);
// Store the SqlToolsContext for future use
Context = context;
}
/// <summary>
/// Gets a list of semantic diagnostic marks for the provided script file
/// </summary>
/// <param name="scriptFile"></param>
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<ScriptFileMarker> markers = new List<ScriptFileMarker>();
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<Location[]> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleDefinitionRequest");
await Task.FromResult(true);
}
private static async Task HandleReferencesRequest(
ReferencesParams referencesParams,
RequestContext<Location[]> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleReferencesRequest");
await Task.FromResult(true);
}
private static async Task HandleCompletionRequest(
TextDocumentPosition textDocumentPosition,
RequestContext<CompletionItem[]> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleCompletionRequest");
// get the current list of completion items and return to client
var completionItems = AutoCompleteService.Instance.GetCompletionItems(textDocumentPosition);
await requestContext.SendResult(completionItems);
}
private static async Task HandleCompletionResolveRequest(
CompletionItem completionItem,
RequestContext<CompletionItem> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleCompletionResolveRequest");
await Task.FromResult(true);
}
private static async Task HandleSignatureHelpRequest(
TextDocumentPosition textDocumentPosition,
RequestContext<SignatureHelp> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleSignatureHelpRequest");
await Task.FromResult(true);
}
private static async Task HandleDocumentHighlightRequest(
TextDocumentPosition textDocumentPosition,
RequestContext<DocumentHighlight[]> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleDocumentHighlightRequest");
await Task.FromResult(true);
}
private static async Task HandleHoverRequest(
TextDocumentPosition textDocumentPosition,
RequestContext<Hover> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleHoverRequest");
await Task.FromResult(true);
}
private static async Task HandleDocumentSymbolRequest(
TextDocumentIdentifier textDocumentIdentifier,
RequestContext<SymbolInformation[]> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleDocumentSymbolRequest");
await Task.FromResult(true);
}
private static async Task HandleWorkspaceSymbolRequest(
WorkspaceSymbolParams workspaceSymbolParams,
RequestContext<SymbolInformation[]> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleWorkspaceSymbolRequest");
await Task.FromResult(true);
}
#endregion
#region Handlers for Events from Other Services
/// <summary>
/// Handle the file open notification
/// </summary>
/// <param name="scriptFile"></param>
/// <param name="eventContext"></param>
/// <returns></returns>
public async Task HandleDidOpenTextDocumentNotification(
ScriptFile scriptFile,
EventContext eventContext)
{
await this.RunScriptDiagnostics(
new ScriptFile[] { scriptFile },
eventContext);
await Task.FromResult(true);
}
/// <summary>
/// Handles text document change events
/// </summary>
/// <param name="textChangeParams"></param>
/// <param name="eventContext"></param>
/// <returns></returns>
public async Task HandleDidChangeTextDocumentNotification(ScriptFile[] changedFiles, EventContext eventContext)
{
await this.RunScriptDiagnostics(
changedFiles.ToArray(),
eventContext);
await Task.FromResult(true);
}
/// <summary>
/// Handle the file configuration change notification
/// </summary>
/// <param name="newSettings"></param>
/// <param name="oldSettings"></param>
/// <param name="eventContext"></param>
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);
}
/// <summary>
/// Callback for when a user connection is done processing
/// </summary>
/// <param name="sqlConnection"></param>
public async Task OnConnection(ISqlConnection sqlConnection)
{
await AutoCompleteService.Instance.UpdateAutoCompleteCache(sqlConnection);
await Task.FromResult(true);
}
#endregion
#region Private Helpers
/// <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
}
}