diff --git a/global.json b/global.json
new file mode 100644
index 00000000..db6ba19b
--- /dev/null
+++ b/global.json
@@ -0,0 +1,5 @@
+{
+ "projects": [ "src", "test" ]
+}
+
+
diff --git a/src/ServiceHost/LanguageServer/ClientCapabilities.cs b/src/ServiceHost/LanguageServer/ClientCapabilities.cs
new file mode 100644
index 00000000..70e2d068
--- /dev/null
+++ b/src/ServiceHost/LanguageServer/ClientCapabilities.cs
@@ -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.
+//
+
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
+{
+ ///
+ /// Defines a class that describes the capabilities of a language
+ /// client. At this time no specific capabilities are listed for
+ /// clients.
+ ///
+ public class ClientCapabilities
+ {
+ }
+}
+
diff --git a/src/ServiceHost/LanguageServer/Completion.cs b/src/ServiceHost/LanguageServer/Completion.cs
new file mode 100644
index 00000000..5f26ea96
--- /dev/null
+++ b/src/ServiceHost/LanguageServer/Completion.cs
@@ -0,0 +1,85 @@
+//
+// 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.EditorServices.Protocol.MessageProtocol;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
+{
+ public class CompletionRequest
+ {
+ public static readonly
+ RequestType Type =
+ RequestType.Create("textDocument/completion");
+ }
+
+ public class CompletionResolveRequest
+ {
+ public static readonly
+ RequestType Type =
+ RequestType.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; }
+
+ ///
+ /// Gets or sets the documentation string for the completion item.
+ ///
+ 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; }
+
+ ///
+ /// 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.
+ ///
+ public object Data { get; set; }
+ }
+}
diff --git a/src/ServiceHost/LanguageServer/Configuration.cs b/src/ServiceHost/LanguageServer/Configuration.cs
new file mode 100644
index 00000000..b9ad87db
--- /dev/null
+++ b/src/ServiceHost/LanguageServer/Configuration.cs
@@ -0,0 +1,21 @@
+//
+// 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.EditorServices.Protocol.MessageProtocol;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
+{
+ public class DidChangeConfigurationNotification
+ {
+ public static readonly
+ EventType> Type =
+ EventType>.Create("workspace/didChangeConfiguration");
+ }
+
+ public class DidChangeConfigurationParams
+ {
+ public TConfig Settings { get; set; }
+ }
+}
diff --git a/src/ServiceHost/LanguageServer/Definition.cs b/src/ServiceHost/LanguageServer/Definition.cs
new file mode 100644
index 00000000..b18845c3
--- /dev/null
+++ b/src/ServiceHost/LanguageServer/Definition.cs
@@ -0,0 +1,17 @@
+//
+// 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.EditorServices.Protocol.MessageProtocol;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
+{
+ public class DefinitionRequest
+ {
+ public static readonly
+ RequestType Type =
+ RequestType.Create("textDocument/definition");
+ }
+}
+
diff --git a/src/ServiceHost/LanguageServer/Diagnostics.cs b/src/ServiceHost/LanguageServer/Diagnostics.cs
new file mode 100644
index 00000000..a5472607
--- /dev/null
+++ b/src/ServiceHost/LanguageServer/Diagnostics.cs
@@ -0,0 +1,71 @@
+//
+// 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.EditorServices.Protocol.MessageProtocol;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
+{
+ public class PublishDiagnosticsNotification
+ {
+ public static readonly
+ EventType Type =
+ EventType.Create("textDocument/publishDiagnostics");
+
+ ///
+ /// Gets or sets the URI for which diagnostic information is reported.
+ ///
+ public string Uri { get; set; }
+
+ ///
+ /// Gets or sets the array of diagnostic information items.
+ ///
+ public Diagnostic[] Diagnostics { get; set; }
+ }
+
+ public enum DiagnosticSeverity
+ {
+ ///
+ /// Indicates that the diagnostic represents an error.
+ ///
+ Error = 1,
+
+ ///
+ /// Indicates that the diagnostic represents a warning.
+ ///
+ Warning = 2,
+
+ ///
+ /// Indicates that the diagnostic represents an informational message.
+ ///
+ Information = 3,
+
+ ///
+ /// Indicates that the diagnostic represents a hint.
+ ///
+ Hint = 4
+ }
+
+ public class Diagnostic
+ {
+ public Range Range { get; set; }
+
+ ///
+ /// Gets or sets the severity of the diagnostic. If omitted, the
+ /// client should interpret the severity.
+ ///
+ public DiagnosticSeverity? Severity { get; set; }
+
+ ///
+ /// Gets or sets the diagnostic's code (optional).
+ ///
+ public string Code { get; set; }
+
+ ///
+ /// Gets or sets the diagnostic message.
+ ///
+ public string Message { get; set; }
+ }
+}
+
diff --git a/src/ServiceHost/LanguageServer/DocumentHighlight.cs b/src/ServiceHost/LanguageServer/DocumentHighlight.cs
new file mode 100644
index 00000000..6849ddfb
--- /dev/null
+++ b/src/ServiceHost/LanguageServer/DocumentHighlight.cs
@@ -0,0 +1,31 @@
+//
+// 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.EditorServices.Protocol.MessageProtocol;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
+{
+ 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 Type =
+ RequestType.Create("textDocument/documentHighlight");
+ }
+}
+
diff --git a/src/ServiceHost/LanguageServer/ExpandAliasRequest.cs b/src/ServiceHost/LanguageServer/ExpandAliasRequest.cs
new file mode 100644
index 00000000..d7f9fde4
--- /dev/null
+++ b/src/ServiceHost/LanguageServer/ExpandAliasRequest.cs
@@ -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.EditorServices.Protocol.MessageProtocol;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
+{
+ public class ExpandAliasRequest
+ {
+ public static readonly
+ RequestType Type =
+ RequestType.Create("SqlTools/expandAlias");
+ }
+}
diff --git a/src/ServiceHost/LanguageServer/FindModuleRequest.cs b/src/ServiceHost/LanguageServer/FindModuleRequest.cs
new file mode 100644
index 00000000..ab78a158
--- /dev/null
+++ b/src/ServiceHost/LanguageServer/FindModuleRequest.cs
@@ -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 Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
+using System.Collections.Generic;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
+{
+ public class FindModuleRequest
+ {
+ public static readonly
+ RequestType, object> Type =
+ RequestType, object>.Create("SqlTools/findModule");
+ }
+
+
+ public class PSModuleMessage
+ {
+ public string Name { get; set; }
+ public string Description { get; set; }
+ }
+}
diff --git a/src/ServiceHost/LanguageServer/Hover.cs b/src/ServiceHost/LanguageServer/Hover.cs
new file mode 100644
index 00000000..2e196fba
--- /dev/null
+++ b/src/ServiceHost/LanguageServer/Hover.cs
@@ -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.EditorServices.Protocol.MessageProtocol;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
+{
+ 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 Type =
+ RequestType.Create("textDocument/hover");
+
+ }
+}
+
diff --git a/src/ServiceHost/LanguageServer/Initialize.cs b/src/ServiceHost/LanguageServer/Initialize.cs
new file mode 100644
index 00000000..7551835e
--- /dev/null
+++ b/src/ServiceHost/LanguageServer/Initialize.cs
@@ -0,0 +1,46 @@
+//
+// 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.EditorServices.Protocol.MessageProtocol;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
+{
+ public class InitializeRequest
+ {
+ public static readonly
+ RequestType Type =
+ RequestType.Create("initialize");
+
+ ///
+ /// Gets or sets the root path of the editor's open workspace.
+ /// If null it is assumed that a file was opened without having
+ /// a workspace open.
+ ///
+ public string RootPath { get; set; }
+
+ ///
+ /// Gets or sets the capabilities provided by the client (editor).
+ ///
+ public ClientCapabilities Capabilities { get; set; }
+ }
+
+ public class InitializeResult
+ {
+ ///
+ /// Gets or sets the capabilities provided by the language server.
+ ///
+ public ServerCapabilities Capabilities { get; set; }
+ }
+
+ public class InitializeError
+ {
+ ///
+ /// Gets or sets a boolean indicating whether the client should retry
+ /// sending the Initialize request after showing the error to the user.
+ ///
+ public bool Retry { get; set;}
+ }
+}
+
diff --git a/src/ServiceHost/LanguageServer/InstallModuleRequest.cs b/src/ServiceHost/LanguageServer/InstallModuleRequest.cs
new file mode 100644
index 00000000..b03b8864
--- /dev/null
+++ b/src/ServiceHost/LanguageServer/InstallModuleRequest.cs
@@ -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.EditorServices.Protocol.MessageProtocol;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
+{
+ class InstallModuleRequest
+ {
+ public static readonly
+ RequestType Type =
+ RequestType.Create("SqlTools/installModule");
+ }
+}
diff --git a/src/ServiceHost/LanguageServer/References.cs b/src/ServiceHost/LanguageServer/References.cs
new file mode 100644
index 00000000..25a92b12
--- /dev/null
+++ b/src/ServiceHost/LanguageServer/References.cs
@@ -0,0 +1,27 @@
+//
+// 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.EditorServices.Protocol.MessageProtocol;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
+{
+ public class ReferencesRequest
+ {
+ public static readonly
+ RequestType Type =
+ RequestType.Create("textDocument/references");
+ }
+
+ public class ReferencesParams : TextDocumentPosition
+ {
+ public ReferencesContext Context { get; set; }
+ }
+
+ public class ReferencesContext
+ {
+ public bool IncludeDeclaration { get; set; }
+ }
+}
+
diff --git a/src/ServiceHost/LanguageServer/ServerCapabilities.cs b/src/ServiceHost/LanguageServer/ServerCapabilities.cs
new file mode 100644
index 00000000..2f7404d9
--- /dev/null
+++ b/src/ServiceHost/LanguageServer/ServerCapabilities.cs
@@ -0,0 +1,63 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
+{
+ public class ServerCapabilities
+ {
+ public TextDocumentSyncKind? TextDocumentSync { get; set; }
+
+ public bool? HoverProvider { get; set; }
+
+ public CompletionOptions CompletionProvider { get; set; }
+
+ public SignatureHelpOptions SignatureHelpProvider { get; set; }
+
+ public bool? DefinitionProvider { get; set; }
+
+ public bool? ReferencesProvider { get; set; }
+
+ public bool? DocumentHighlightProvider { get; set; }
+
+ public bool? DocumentSymbolProvider { get; set; }
+
+ public bool? WorkspaceSymbolProvider { get; set; }
+ }
+
+ ///
+ /// Defines the document synchronization strategies that a server may support.
+ ///
+ public enum TextDocumentSyncKind
+ {
+ ///
+ /// Indicates that documents should not be synced at all.
+ ///
+ None = 0,
+
+ ///
+ /// Indicates that document changes are always sent with the full content.
+ ///
+ Full,
+
+ ///
+ /// Indicates that document changes are sent as incremental changes after
+ /// the initial document content has been sent.
+ ///
+ Incremental
+ }
+
+ public class CompletionOptions
+ {
+ public bool? ResolveProvider { get; set; }
+
+ public string[] TriggerCharacters { get; set; }
+ }
+
+ public class SignatureHelpOptions
+ {
+ public string[] TriggerCharacters { get; set; }
+ }
+}
+
diff --git a/src/ServiceHost/LanguageServer/ShowOnlineHelpRequest.cs b/src/ServiceHost/LanguageServer/ShowOnlineHelpRequest.cs
new file mode 100644
index 00000000..8f21fb1b
--- /dev/null
+++ b/src/ServiceHost/LanguageServer/ShowOnlineHelpRequest.cs
@@ -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.EditorServices.Protocol.MessageProtocol;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
+{
+ public class ShowOnlineHelpRequest
+ {
+ public static readonly
+ RequestType Type =
+ RequestType.Create("SqlTools/showOnlineHelp");
+ }
+}
diff --git a/src/ServiceHost/LanguageServer/Shutdown.cs b/src/ServiceHost/LanguageServer/Shutdown.cs
new file mode 100644
index 00000000..f0a7bbd2
--- /dev/null
+++ b/src/ServiceHost/LanguageServer/Shutdown.cs
@@ -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.EditorServices.Protocol.MessageProtocol;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
+{
+ ///
+ /// Defines a message that is sent from the client to request
+ /// that the server shut down.
+ ///
+ public class ShutdownRequest
+ {
+ public static readonly
+ RequestType Type =
+ RequestType.Create("shutdown");
+ }
+
+ ///
+ /// Defines an event that is sent from the client to notify that
+ /// the client is exiting and the server should as well.
+ ///
+ public class ExitNotification
+ {
+ public static readonly
+ EventType Type =
+ EventType.Create("exit");
+ }
+}
+
diff --git a/src/ServiceHost/LanguageServer/SignatureHelp.cs b/src/ServiceHost/LanguageServer/SignatureHelp.cs
new file mode 100644
index 00000000..5d4233e3
--- /dev/null
+++ b/src/ServiceHost/LanguageServer/SignatureHelp.cs
@@ -0,0 +1,42 @@
+//
+// 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.EditorServices.Protocol.MessageProtocol;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
+{
+ public class SignatureHelpRequest
+ {
+ public static readonly
+ RequestType Type =
+ RequestType.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; }
+ }
+}
+
diff --git a/src/ServiceHost/LanguageServer/TextDocument.cs b/src/ServiceHost/LanguageServer/TextDocument.cs
new file mode 100644
index 00000000..9f477374
--- /dev/null
+++ b/src/ServiceHost/LanguageServer/TextDocument.cs
@@ -0,0 +1,164 @@
+//
+// 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.EditorServices.Protocol.MessageProtocol;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
+{
+ ///
+ /// Defines a base parameter class for identifying a text document.
+ ///
+ [DebuggerDisplay("TextDocumentIdentifier = {Uri}")]
+ public class TextDocumentIdentifier
+ {
+ ///
+ /// Gets or sets the URI which identifies the path of the
+ /// text document.
+ ///
+ public string Uri { get; set; }
+ }
+
+ ///
+ /// Defines a position in a text document.
+ ///
+ [DebuggerDisplay("TextDocumentPosition = {Position.Line}:{Position.Character}")]
+ public class TextDocumentPosition : TextDocumentIdentifier
+ {
+ ///
+ /// Gets or sets the position in the document.
+ ///
+ public Position Position { get; set; }
+ }
+
+ public class DidOpenTextDocumentNotification : TextDocumentIdentifier
+ {
+ public static readonly
+ EventType Type =
+ EventType.Create("textDocument/didOpen");
+
+ ///
+ /// Gets or sets the full content of the opened document.
+ ///
+ public string Text { get; set; }
+ }
+
+ public class DidCloseTextDocumentNotification
+ {
+ public static readonly
+ EventType Type =
+ EventType.Create("textDocument/didClose");
+ }
+
+ public class DidChangeTextDocumentNotification
+ {
+ public static readonly
+ EventType Type =
+ EventType.Create("textDocument/didChange");
+ }
+
+ public class DidChangeTextDocumentParams : TextDocumentIdentifier
+ {
+ public TextDocumentUriChangeEvent TextDocument { get; set; }
+
+ ///
+ /// Gets or sets the list of changes to the document content.
+ ///
+ public TextDocumentChangeEvent[] ContentChanges { get; set; }
+ }
+
+ public class TextDocumentUriChangeEvent
+ {
+ ///
+ /// Gets or sets the Uri of the changed text document
+ ///
+ public string Uri { get; set; }
+
+ ///
+ /// Gets or sets the Version of the changed text document
+ ///
+ public int Version { get; set; }
+ }
+
+ public class TextDocumentChangeEvent
+ {
+ ///
+ /// Gets or sets the Range where the document was changed. Will
+ /// be null if the server's TextDocumentSyncKind is Full.
+ ///
+ public Range? Range { get; set; }
+
+ ///
+ /// Gets or sets the length of the Range being replaced in the
+ /// document. Will be null if the server's TextDocumentSyncKind is
+ /// Full.
+ ///
+ public int? RangeLength { get; set; }
+
+ ///
+ /// Gets or sets the new text of the document.
+ ///
+ public string Text { get; set; }
+ }
+
+ [DebuggerDisplay("Position = {Line}:{Character}")]
+ public class Position
+ {
+ ///
+ /// Gets or sets the zero-based line number.
+ ///
+ public int Line { get; set; }
+
+ ///
+ /// Gets or sets the zero-based column number.
+ ///
+ public int Character { get; set; }
+ }
+
+ [DebuggerDisplay("Start = {Start.Line}:{Start.Character}, End = {End.Line}:{End.Character}")]
+ public struct Range
+ {
+ ///
+ /// Gets or sets the starting position of the range.
+ ///
+ public Position Start { get; set; }
+
+ ///
+ /// Gets or sets the ending position of the range.
+ ///
+ public Position End { get; set; }
+ }
+
+ [DebuggerDisplay("Range = {Range.Start.Line}:{Range.Start.Character} - {Range.End.Line}:{Range.End.Character}, Uri = {Uri}")]
+ public class Location
+ {
+ ///
+ /// Gets or sets the URI indicating the file in which the location refers.
+ ///
+ public string Uri { get; set; }
+
+ ///
+ /// Gets or sets the Range indicating the range in which location refers.
+ ///
+ public Range Range { get; set; }
+ }
+
+ public enum FileChangeType
+ {
+ Created = 1,
+
+ Changed,
+
+ Deleted
+ }
+
+ public class FileEvent
+ {
+ public string Uri { get; set; }
+
+ public FileChangeType Type { get; set; }
+ }
+}
+
diff --git a/src/ServiceHost/LanguageServer/WorkspaceSymbols.cs b/src/ServiceHost/LanguageServer/WorkspaceSymbols.cs
new file mode 100644
index 00000000..25a554b5
--- /dev/null
+++ b/src/ServiceHost/LanguageServer/WorkspaceSymbols.cs
@@ -0,0 +1,62 @@
+//
+// 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.EditorServices.Protocol.MessageProtocol;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
+{
+ public enum SymbolKind
+ {
+ File = 1,
+ Module = 2,
+ Namespace = 3,
+ Package = 4,
+ Class = 5,
+ Method = 6,
+ Property = 7,
+ Field = 8,
+ Constructor = 9,
+ Enum = 10,
+ Interface = 11,
+ Function = 12,
+ Variable = 13,
+ Constant = 14,
+ String = 15,
+ Number = 16,
+ Boolean = 17,
+ Array = 18,
+ }
+
+ public class SymbolInformation
+ {
+ public string Name { get; set; }
+
+ public SymbolKind Kind { get; set; }
+
+ public Location Location { get; set; }
+
+ public string ContainerName { get; set;}
+ }
+
+ public class DocumentSymbolRequest
+ {
+ public static readonly
+ RequestType Type =
+ RequestType.Create("textDocument/documentSymbol");
+ }
+
+ public class WorkspaceSymbolRequest
+ {
+ public static readonly
+ RequestType Type =
+ RequestType.Create("workspace/symbol");
+ }
+
+ public class WorkspaceSymbolParams
+ {
+ public string Query { get; set;}
+ }
+}
+
diff --git a/src/ServiceHost/LanguageSupport/LanguageService.cs b/src/ServiceHost/LanguageSupport/LanguageService.cs
new file mode 100644
index 00000000..3ab77697
--- /dev/null
+++ b/src/ServiceHost/LanguageSupport/LanguageService.cs
@@ -0,0 +1,57 @@
+//
+// 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.EditorServices;
+using Microsoft.SqlTools.EditorServices.Session;
+
+namespace Microsoft.SqlTools.LanguageSupport
+{
+ ///
+ /// Main class for Language Service functionality
+ ///
+ public class LanguageService
+ {
+ ///
+ /// Gets or sets the current SQL Tools context
+ ///
+ ///
+ private SqlToolsContext Context { get; set; }
+
+ ///
+ /// Constructor for the Language Service class
+ ///
+ ///
+ public LanguageService(SqlToolsContext context)
+ {
+ this.Context = context;
+ }
+
+ ///
+ /// Gets a list of semantic diagnostic marks for the provided script file
+ ///
+ ///
+ public ScriptFileMarker[] GetSemanticMarkers(ScriptFile scriptFile)
+ {
+ // the commented out snippet is an example of how to create a error marker
+ // semanticMarkers = new ScriptFileMarker[1];
+ // semanticMarkers[0] = new ScriptFileMarker()
+ // {
+ // Message = "Error message",
+ // Level = ScriptFileMarkerLevel.Error,
+ // ScriptRegion = new ScriptRegion()
+ // {
+ // File = scriptFile.FilePath,
+ // StartLineNumber = 2,
+ // StartColumnNumber = 2,
+ // StartOffset = 0,
+ // EndLineNumber = 4,
+ // EndColumnNumber = 10,
+ // EndOffset = 0
+ // }
+ // };
+ return new ScriptFileMarker[0];
+ }
+ }
+}
diff --git a/src/ServiceHost/MessageProtocol/Channel/ChannelBase.cs b/src/ServiceHost/MessageProtocol/Channel/ChannelBase.cs
new file mode 100644
index 00000000..848da39f
--- /dev/null
+++ b/src/ServiceHost/MessageProtocol/Channel/ChannelBase.cs
@@ -0,0 +1,81 @@
+//
+// 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.EditorServices.Protocol.MessageProtocol.Serializers;
+using System.Threading.Tasks;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Channel
+{
+ ///
+ /// Defines a base implementation for servers and their clients over a
+ /// single kind of communication channel.
+ ///
+ public abstract class ChannelBase
+ {
+ ///
+ /// Gets a boolean that is true if the channel is connected or false if not.
+ ///
+ public bool IsConnected { get; protected set; }
+
+ ///
+ /// Gets the MessageReader for reading messages from the channel.
+ ///
+ public MessageReader MessageReader { get; protected set; }
+
+ ///
+ /// Gets the MessageWriter for writing messages to the channel.
+ ///
+ public MessageWriter MessageWriter { get; protected set; }
+
+ ///
+ /// Starts the channel and initializes the MessageDispatcher.
+ ///
+ /// The type of message protocol used by the channel.
+ public void Start(MessageProtocolType messageProtocolType)
+ {
+ IMessageSerializer messageSerializer = null;
+ if (messageProtocolType == MessageProtocolType.LanguageServer)
+ {
+ messageSerializer = new JsonRpcMessageSerializer();
+ }
+ else
+ {
+ messageSerializer = new V8MessageSerializer();
+ }
+
+ this.Initialize(messageSerializer);
+ }
+
+ ///
+ /// Returns a Task that allows the consumer of the ChannelBase
+ /// implementation to wait until a connection has been made to
+ /// the opposite endpoint whether it's a client or server.
+ ///
+ /// A Task to be awaited until a connection is made.
+ public abstract Task WaitForConnection();
+
+ ///
+ /// Stops the channel.
+ ///
+ public void Stop()
+ {
+ this.Shutdown();
+ }
+
+ ///
+ /// A method to be implemented by subclasses to handle the
+ /// actual initialization of the channel and the creation and
+ /// assignment of the MessageReader and MessageWriter properties.
+ ///
+ /// The IMessageSerializer to use for message serialization.
+ protected abstract void Initialize(IMessageSerializer messageSerializer);
+
+ ///
+ /// A method to be implemented by subclasses to handle shutdown
+ /// of the channel once Stop is called.
+ ///
+ protected abstract void Shutdown();
+ }
+}
diff --git a/src/ServiceHost/MessageProtocol/Channel/StdioClientChannel.cs b/src/ServiceHost/MessageProtocol/Channel/StdioClientChannel.cs
new file mode 100644
index 00000000..5390f52d
--- /dev/null
+++ b/src/ServiceHost/MessageProtocol/Channel/StdioClientChannel.cs
@@ -0,0 +1,125 @@
+//
+// 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 System.IO;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Channel
+{
+ ///
+ /// Provides a client implementation for the standard I/O channel.
+ /// Launches the server process and then attaches to its console
+ /// streams.
+ ///
+ public class StdioClientChannel : ChannelBase
+ {
+ private string serviceProcessPath;
+ private string serviceProcessArguments;
+
+ private Stream inputStream;
+ private Stream outputStream;
+ private Process serviceProcess;
+
+ ///
+ /// Gets the process ID of the server process.
+ ///
+ public int ProcessId { get; private set; }
+
+ ///
+ /// Initializes an instance of the StdioClient.
+ ///
+ /// The full path to the server process executable.
+ /// Optional arguments to pass to the service process executable.
+ public StdioClientChannel(
+ string serverProcessPath,
+ params string[] serverProcessArguments)
+ {
+ this.serviceProcessPath = serverProcessPath;
+
+ if (serverProcessArguments != null)
+ {
+ this.serviceProcessArguments =
+ string.Join(
+ " ",
+ serverProcessArguments);
+ }
+ }
+
+ protected override void Initialize(IMessageSerializer messageSerializer)
+ {
+ this.serviceProcess = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = this.serviceProcessPath,
+ Arguments = this.serviceProcessArguments,
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ RedirectStandardInput = true,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ StandardOutputEncoding = Encoding.UTF8,
+ },
+ EnableRaisingEvents = true,
+ };
+
+ // Start the process
+ this.serviceProcess.Start();
+ this.ProcessId = this.serviceProcess.Id;
+
+ // Open the standard input/output streams
+ this.inputStream = this.serviceProcess.StandardOutput.BaseStream;
+ this.outputStream = this.serviceProcess.StandardInput.BaseStream;
+
+ // Set up the message reader and writer
+ this.MessageReader =
+ new MessageReader(
+ this.inputStream,
+ messageSerializer);
+
+ this.MessageWriter =
+ new MessageWriter(
+ this.outputStream,
+ messageSerializer);
+
+ this.IsConnected = true;
+ }
+
+ public override Task WaitForConnection()
+ {
+ // We're always connected immediately in the stdio channel
+ return Task.FromResult(true);
+ }
+
+ protected override void Shutdown()
+ {
+ if (this.inputStream != null)
+ {
+ this.inputStream.Dispose();
+ this.inputStream = null;
+ }
+
+ if (this.outputStream != null)
+ {
+ this.outputStream.Dispose();
+ this.outputStream = null;
+ }
+
+ if (this.MessageReader != null)
+ {
+ this.MessageReader = null;
+ }
+
+ if (this.MessageWriter != null)
+ {
+ this.MessageWriter = null;
+ }
+
+ this.serviceProcess.Kill();
+ }
+ }
+}
diff --git a/src/ServiceHost/MessageProtocol/Channel/StdioServerChannel.cs b/src/ServiceHost/MessageProtocol/Channel/StdioServerChannel.cs
new file mode 100644
index 00000000..0b9376d4
--- /dev/null
+++ b/src/ServiceHost/MessageProtocol/Channel/StdioServerChannel.cs
@@ -0,0 +1,60 @@
+//
+// 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.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Channel
+{
+ ///
+ /// Provides a server implementation for the standard I/O channel.
+ /// When started in a process, attaches to the console I/O streams
+ /// to communicate with the client that launched the process.
+ ///
+ public class StdioServerChannel : ChannelBase
+ {
+ private Stream inputStream;
+ private Stream outputStream;
+
+ protected override void Initialize(IMessageSerializer messageSerializer)
+ {
+#if !NanoServer
+ // Ensure that the console is using UTF-8 encoding
+ System.Console.InputEncoding = Encoding.UTF8;
+ System.Console.OutputEncoding = Encoding.UTF8;
+#endif
+
+ // Open the standard input/output streams
+ this.inputStream = System.Console.OpenStandardInput();
+ this.outputStream = System.Console.OpenStandardOutput();
+
+ // Set up the reader and writer
+ this.MessageReader =
+ new MessageReader(
+ this.inputStream,
+ messageSerializer);
+
+ this.MessageWriter =
+ new MessageWriter(
+ this.outputStream,
+ messageSerializer);
+
+ this.IsConnected = true;
+ }
+
+ public override Task WaitForConnection()
+ {
+ // We're always connected immediately in the stdio channel
+ return Task.FromResult(true);
+ }
+
+ protected override void Shutdown()
+ {
+ // No default implementation needed, streams will be
+ // disposed on process shutdown.
+ }
+ }
+}
diff --git a/src/ServiceHost/MessageProtocol/Constants.cs b/src/ServiceHost/MessageProtocol/Constants.cs
new file mode 100644
index 00000000..0fae5d8d
--- /dev/null
+++ b/src/ServiceHost/MessageProtocol/Constants.cs
@@ -0,0 +1,25 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
+{
+ public static class Constants
+ {
+ public const string ContentLengthFormatString = "Content-Length: {0}\r\n\r\n";
+ public static readonly JsonSerializerSettings JsonSerializerSettings;
+
+ static Constants()
+ {
+ JsonSerializerSettings = new JsonSerializerSettings();
+
+ // Camel case all object properties
+ JsonSerializerSettings.ContractResolver =
+ new CamelCasePropertyNamesContractResolver();
+ }
+ }
+}
diff --git a/src/ServiceHost/MessageProtocol/EventContext.cs b/src/ServiceHost/MessageProtocol/EventContext.cs
new file mode 100644
index 00000000..eb42ebbb
--- /dev/null
+++ b/src/ServiceHost/MessageProtocol/EventContext.cs
@@ -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 System.Threading.Tasks;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
+{
+ ///
+ /// Provides context for a received event so that handlers
+ /// can write events back to the channel.
+ ///
+ public class EventContext
+ {
+ private MessageWriter messageWriter;
+
+ public EventContext(MessageWriter messageWriter)
+ {
+ this.messageWriter = messageWriter;
+ }
+
+ public async Task SendEvent(
+ EventType eventType,
+ TParams eventParams)
+ {
+ await this.messageWriter.WriteEvent(
+ eventType,
+ eventParams);
+ }
+ }
+}
+
diff --git a/src/ServiceHost/MessageProtocol/EventType.cs b/src/ServiceHost/MessageProtocol/EventType.cs
new file mode 100644
index 00000000..dd460817
--- /dev/null
+++ b/src/ServiceHost/MessageProtocol/EventType.cs
@@ -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.
+//
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
+{
+ ///
+ /// Defines an event type with a particular method name.
+ ///
+ /// The parameter type for this event.
+ public class EventType
+ {
+ ///
+ /// Gets the method name for the event type.
+ ///
+ public string MethodName { get; private set; }
+
+ ///
+ /// Creates an EventType instance with the given parameter type and method name.
+ ///
+ /// The method name of the event.
+ /// A new EventType instance for the defined type.
+ public static EventType Create(string methodName)
+ {
+ return new EventType()
+ {
+ MethodName = methodName
+ };
+ }
+ }
+}
+
diff --git a/src/ServiceHost/MessageProtocol/IMessageSender.cs b/src/ServiceHost/MessageProtocol/IMessageSender.cs
new file mode 100644
index 00000000..7f331eed
--- /dev/null
+++ b/src/ServiceHost/MessageProtocol/IMessageSender.cs
@@ -0,0 +1,22 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System.Threading.Tasks;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
+{
+ internal interface IMessageSender
+ {
+ Task SendEvent(
+ EventType eventType,
+ TParams eventParams);
+
+ Task SendRequest(
+ RequestType requestType,
+ TParams requestParams,
+ bool waitForResponse);
+ }
+}
+
diff --git a/src/ServiceHost/MessageProtocol/IMessageSerializer.cs b/src/ServiceHost/MessageProtocol/IMessageSerializer.cs
new file mode 100644
index 00000000..81b23fa6
--- /dev/null
+++ b/src/ServiceHost/MessageProtocol/IMessageSerializer.cs
@@ -0,0 +1,30 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
+{
+ ///
+ /// Defines a common interface for message serializers.
+ ///
+ public interface IMessageSerializer
+ {
+ ///
+ /// Serializes a Message to a JObject.
+ ///
+ /// The message to be serialized.
+ /// A JObject which contains the JSON representation of the message.
+ JObject SerializeMessage(Message message);
+
+ ///
+ /// Deserializes a JObject to a Messsage.
+ ///
+ /// The JObject containing the JSON representation of the message.
+ /// The Message that was represented by the JObject.
+ Message DeserializeMessage(JObject messageJson);
+ }
+}
+
diff --git a/src/ServiceHost/MessageProtocol/Message.cs b/src/ServiceHost/MessageProtocol/Message.cs
new file mode 100644
index 00000000..75dab5cd
--- /dev/null
+++ b/src/ServiceHost/MessageProtocol/Message.cs
@@ -0,0 +1,136 @@
+//
+// 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 Newtonsoft.Json.Linq;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
+{
+ ///
+ /// Defines all possible message types.
+ ///
+ public enum MessageType
+ {
+ Unknown,
+ Request,
+ Response,
+ Event
+ }
+
+ ///
+ /// Provides common details for protocol messages of any format.
+ ///
+ [DebuggerDisplay("MessageType = {MessageType.ToString()}, Method = {Method}, Id = {Id}")]
+ public class Message
+ {
+ ///
+ /// Gets or sets the message type.
+ ///
+ public MessageType MessageType { get; set; }
+
+ ///
+ /// Gets or sets the message's sequence ID.
+ ///
+ public string Id { get; set; }
+
+ ///
+ /// Gets or sets the message's method/command name.
+ ///
+ public string Method { get; set; }
+
+ ///
+ /// Gets or sets a JToken containing the contents of the message.
+ ///
+ public JToken Contents { get; set; }
+
+ ///
+ /// Gets or sets a JToken containing error details.
+ ///
+ public JToken Error { get; set; }
+
+ ///
+ /// Creates a message with an Unknown type.
+ ///
+ /// A message with Unknown type.
+ public static Message Unknown()
+ {
+ return new Message
+ {
+ MessageType = MessageType.Unknown
+ };
+ }
+
+ ///
+ /// Creates a message with a Request type.
+ ///
+ /// The sequence ID of the request.
+ /// The method name of the request.
+ /// The contents of the request.
+ /// A message with a Request type.
+ public static Message Request(string id, string method, JToken contents)
+ {
+ return new Message
+ {
+ MessageType = MessageType.Request,
+ Id = id,
+ Method = method,
+ Contents = contents
+ };
+ }
+
+ ///
+ /// Creates a message with a Response type.
+ ///
+ /// The sequence ID of the original request.
+ /// The method name of the original request.
+ /// The contents of the response.
+ /// A message with a Response type.
+ public static Message Response(string id, string method, JToken contents)
+ {
+ return new Message
+ {
+ MessageType = MessageType.Response,
+ Id = id,
+ Method = method,
+ Contents = contents
+ };
+ }
+
+ ///
+ /// Creates a message with a Response type and error details.
+ ///
+ /// The sequence ID of the original request.
+ /// The method name of the original request.
+ /// The error details of the response.
+ /// A message with a Response type and error details.
+ public static Message ResponseError(string id, string method, JToken error)
+ {
+ return new Message
+ {
+ MessageType = MessageType.Response,
+ Id = id,
+ Method = method,
+ Error = error
+ };
+ }
+
+ ///
+ /// Creates a message with an Event type.
+ ///
+ /// The method name of the event.
+ /// The contents of the event.
+ /// A message with an Event type.
+ public static Message Event(string method, JToken contents)
+ {
+ return new Message
+ {
+ MessageType = MessageType.Event,
+ Method = method,
+ Contents = contents
+ };
+ }
+ }
+}
+
diff --git a/src/ServiceHost/MessageProtocol/MessageDispatcher.cs b/src/ServiceHost/MessageProtocol/MessageDispatcher.cs
new file mode 100644
index 00000000..21c179e2
--- /dev/null
+++ b/src/ServiceHost/MessageProtocol/MessageDispatcher.cs
@@ -0,0 +1,325 @@
+//
+// 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.EditorServices.Protocol.MessageProtocol.Channel;
+using Microsoft.SqlTools.EditorServices.Utility;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
+{
+ public class MessageDispatcher
+ {
+ #region Fields
+
+ private ChannelBase protocolChannel;
+
+ private AsyncContextThread messageLoopThread;
+
+ private Dictionary> requestHandlers =
+ new Dictionary>();
+
+ private Dictionary> eventHandlers =
+ new Dictionary>();
+
+ private Action responseHandler;
+
+ private CancellationTokenSource messageLoopCancellationToken =
+ new CancellationTokenSource();
+
+ #endregion
+
+ #region Properties
+
+ public SynchronizationContext SynchronizationContext { get; private set; }
+
+ public bool InMessageLoopThread
+ {
+ get
+ {
+ // We're in the same thread as the message loop if the
+ // current synchronization context equals the one we
+ // know.
+ return SynchronizationContext.Current == this.SynchronizationContext;
+ }
+ }
+
+ protected MessageReader MessageReader { get; private set; }
+
+ protected MessageWriter MessageWriter { get; private set; }
+
+
+ #endregion
+
+ #region Constructors
+
+ public MessageDispatcher(ChannelBase protocolChannel)
+ {
+ this.protocolChannel = protocolChannel;
+ this.MessageReader = protocolChannel.MessageReader;
+ this.MessageWriter = protocolChannel.MessageWriter;
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ public void Start()
+ {
+ // Start the main message loop thread. The Task is
+ // not explicitly awaited because it is running on
+ // an independent background thread.
+ this.messageLoopThread = new AsyncContextThread("Message Dispatcher");
+ this.messageLoopThread
+ .Run(() => this.ListenForMessages(this.messageLoopCancellationToken.Token))
+ .ContinueWith(this.OnListenTaskCompleted);
+ }
+
+ public void Stop()
+ {
+ // Stop the message loop thread
+ if (this.messageLoopThread != null)
+ {
+ this.messageLoopCancellationToken.Cancel();
+ this.messageLoopThread.Stop();
+ }
+ }
+
+ public void SetRequestHandler(
+ RequestType requestType,
+ Func, Task> requestHandler)
+ {
+ this.SetRequestHandler(
+ requestType,
+ requestHandler,
+ false);
+ }
+
+ public void SetRequestHandler(
+ RequestType requestType,
+ Func, Task> requestHandler,
+ bool overrideExisting)
+ {
+ if (overrideExisting)
+ {
+ // Remove the existing handler so a new one can be set
+ this.requestHandlers.Remove(requestType.MethodName);
+ }
+
+ this.requestHandlers.Add(
+ requestType.MethodName,
+ (requestMessage, messageWriter) =>
+ {
+ var requestContext =
+ new RequestContext(
+ requestMessage,
+ messageWriter);
+
+ TParams typedParams = default(TParams);
+ if (requestMessage.Contents != null)
+ {
+ // TODO: Catch parse errors!
+ typedParams = requestMessage.Contents.ToObject();
+ }
+
+ return requestHandler(typedParams, requestContext);
+ });
+ }
+
+ public void SetEventHandler(
+ EventType eventType,
+ Func eventHandler)
+ {
+ this.SetEventHandler(
+ eventType,
+ eventHandler,
+ false);
+ }
+
+ public void SetEventHandler(
+ EventType eventType,
+ Func eventHandler,
+ bool overrideExisting)
+ {
+ if (overrideExisting)
+ {
+ // Remove the existing handler so a new one can be set
+ this.eventHandlers.Remove(eventType.MethodName);
+ }
+
+ this.eventHandlers.Add(
+ eventType.MethodName,
+ (eventMessage, messageWriter) =>
+ {
+ var eventContext = new EventContext(messageWriter);
+
+ TParams typedParams = default(TParams);
+ if (eventMessage.Contents != null)
+ {
+ // TODO: Catch parse errors!
+ typedParams = eventMessage.Contents.ToObject();
+ }
+
+ return eventHandler(typedParams, eventContext);
+ });
+ }
+
+ public void SetResponseHandler(Action responseHandler)
+ {
+ this.responseHandler = responseHandler;
+ }
+
+ #endregion
+
+ #region Events
+
+ public event EventHandler UnhandledException;
+
+ protected void OnUnhandledException(Exception unhandledException)
+ {
+ if (this.UnhandledException != null)
+ {
+ this.UnhandledException(this, unhandledException);
+ }
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ private async Task ListenForMessages(CancellationToken cancellationToken)
+ {
+ this.SynchronizationContext = SynchronizationContext.Current;
+
+ // Run the message loop
+ bool isRunning = true;
+ while (isRunning && !cancellationToken.IsCancellationRequested)
+ {
+ Message newMessage = null;
+
+ try
+ {
+ // Read a message from the channel
+ newMessage = await this.MessageReader.ReadMessage();
+ }
+ catch (MessageParseException e)
+ {
+ // TODO: Write an error response
+
+ Logger.Write(
+ LogLevel.Error,
+ "Could not parse a message that was received:\r\n\r\n" +
+ e.ToString());
+
+ // Continue the loop
+ continue;
+ }
+ catch (EndOfStreamException)
+ {
+ // The stream has ended, end the message loop
+ break;
+ }
+ catch (Exception e)
+ {
+ var b = e.Message;
+ newMessage = null;
+ }
+
+ // The message could be null if there was an error parsing the
+ // previous message. In this case, do not try to dispatch it.
+ if (newMessage != null)
+ {
+ // Process the message
+ await this.DispatchMessage(
+ newMessage,
+ this.MessageWriter);
+ }
+ }
+ }
+
+ protected async Task DispatchMessage(
+ Message messageToDispatch,
+ MessageWriter messageWriter)
+ {
+ Task handlerToAwait = null;
+
+ if (messageToDispatch.MessageType == MessageType.Request)
+ {
+ Func requestHandler = null;
+ if (this.requestHandlers.TryGetValue(messageToDispatch.Method, out requestHandler))
+ {
+ handlerToAwait = requestHandler(messageToDispatch, messageWriter);
+ }
+ else
+ {
+ // TODO: Message not supported error
+ }
+ }
+ else if (messageToDispatch.MessageType == MessageType.Response)
+ {
+ if (this.responseHandler != null)
+ {
+ this.responseHandler(messageToDispatch);
+ }
+ }
+ else if (messageToDispatch.MessageType == MessageType.Event)
+ {
+ Func eventHandler = null;
+ if (this.eventHandlers.TryGetValue(messageToDispatch.Method, out eventHandler))
+ {
+ handlerToAwait = eventHandler(messageToDispatch, messageWriter);
+ }
+ else
+ {
+ // TODO: Message not supported error
+ }
+ }
+ else
+ {
+ // TODO: Return message not supported
+ }
+
+ if (handlerToAwait != null)
+ {
+ try
+ {
+ await handlerToAwait;
+ }
+ catch (TaskCanceledException)
+ {
+ // Some tasks may be cancelled due to legitimate
+ // timeouts so don't let those exceptions go higher.
+ }
+ catch (AggregateException e)
+ {
+ if (!(e.InnerExceptions[0] is TaskCanceledException))
+ {
+ // Cancelled tasks aren't a problem, so rethrow
+ // anything that isn't a TaskCanceledException
+ throw e;
+ }
+ }
+ }
+ }
+
+ private void OnListenTaskCompleted(Task listenTask)
+ {
+ if (listenTask.IsFaulted)
+ {
+ this.OnUnhandledException(listenTask.Exception);
+ }
+ else if (listenTask.IsCompleted || listenTask.IsCanceled)
+ {
+ // TODO: Dispose of anything?
+ }
+ }
+
+ #endregion
+ }
+}
+
diff --git a/src/ServiceHost/MessageProtocol/MessageParseException.cs b/src/ServiceHost/MessageProtocol/MessageParseException.cs
new file mode 100644
index 00000000..98a17c20
--- /dev/null
+++ b/src/ServiceHost/MessageProtocol/MessageParseException.cs
@@ -0,0 +1,23 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
+{
+ public class MessageParseException : Exception
+ {
+ public string OriginalMessageText { get; private set; }
+
+ public MessageParseException(
+ string originalMessageText,
+ string errorMessage,
+ params object[] errorMessageArgs)
+ : base(string.Format(errorMessage, errorMessageArgs))
+ {
+ this.OriginalMessageText = originalMessageText;
+ }
+ }
+}
diff --git a/src/ServiceHost/MessageProtocol/MessageProtocolType.cs b/src/ServiceHost/MessageProtocol/MessageProtocolType.cs
new file mode 100644
index 00000000..5484ae3c
--- /dev/null
+++ b/src/ServiceHost/MessageProtocol/MessageProtocolType.cs
@@ -0,0 +1,23 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
+{
+ ///
+ /// Defines the possible message protocol types.
+ ///
+ public enum MessageProtocolType
+ {
+ ///
+ /// Identifies the language server message protocol.
+ ///
+ LanguageServer,
+
+ ///
+ /// Identifies the debug adapter message protocol.
+ ///
+ DebugAdapter
+ }
+}
diff --git a/src/ServiceHost/MessageProtocol/MessageReader.cs b/src/ServiceHost/MessageProtocol/MessageReader.cs
new file mode 100644
index 00000000..a2df43ba
--- /dev/null
+++ b/src/ServiceHost/MessageProtocol/MessageReader.cs
@@ -0,0 +1,262 @@
+//
+// 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.EditorServices.Utility;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
+{
+ public class MessageReader
+ {
+ #region Private Fields
+
+ public const int DefaultBufferSize = 8192;
+ public const double BufferResizeTrigger = 0.25;
+
+ private const int CR = 0x0D;
+ private const int LF = 0x0A;
+ private static string[] NewLineDelimiters = new string[] { Environment.NewLine };
+
+ private Stream inputStream;
+ private IMessageSerializer messageSerializer;
+ private Encoding messageEncoding;
+
+ private ReadState readState;
+ private bool needsMoreData = true;
+ private int readOffset;
+ private int bufferEndOffset;
+ private byte[] messageBuffer = new byte[DefaultBufferSize];
+
+ private int expectedContentLength;
+ private Dictionary messageHeaders;
+
+ enum ReadState
+ {
+ Headers,
+ Content
+ }
+
+ #endregion
+
+ #region Constructors
+
+ public MessageReader(
+ Stream inputStream,
+ IMessageSerializer messageSerializer,
+ Encoding messageEncoding = null)
+ {
+ Validate.IsNotNull("streamReader", inputStream);
+ Validate.IsNotNull("messageSerializer", messageSerializer);
+
+ this.inputStream = inputStream;
+ this.messageSerializer = messageSerializer;
+
+ this.messageEncoding = messageEncoding;
+ if (messageEncoding == null)
+ {
+ this.messageEncoding = Encoding.UTF8;
+ }
+
+ this.messageBuffer = new byte[DefaultBufferSize];
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ public async Task ReadMessage()
+ {
+ string messageContent = null;
+
+ // Do we need to read more data or can we process the existing buffer?
+ while (!this.needsMoreData || await this.ReadNextChunk())
+ {
+ // Clear the flag since we should have what we need now
+ this.needsMoreData = false;
+
+ // Do we need to look for message headers?
+ if (this.readState == ReadState.Headers &&
+ !this.TryReadMessageHeaders())
+ {
+ // If we don't have enough data to read headers yet, keep reading
+ this.needsMoreData = true;
+ continue;
+ }
+
+ // Do we need to look for message content?
+ if (this.readState == ReadState.Content &&
+ !this.TryReadMessageContent(out messageContent))
+ {
+ // If we don't have enough data yet to construct the content, keep reading
+ this.needsMoreData = true;
+ continue;
+ }
+
+ // We've read a message now, break out of the loop
+ break;
+ }
+
+ // Get the JObject for the JSON content
+ JObject messageObject = JObject.Parse(messageContent);
+
+ // Load the message
+ Logger.Write(
+ LogLevel.Verbose,
+ string.Format(
+ "READ MESSAGE:\r\n\r\n{0}",
+ messageObject.ToString(Formatting.Indented)));
+
+ // Return the parsed message
+ return this.messageSerializer.DeserializeMessage(messageObject);
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ private async Task ReadNextChunk()
+ {
+ // Do we need to resize the buffer? See if less than 1/4 of the space is left.
+ if (((double)(this.messageBuffer.Length - this.bufferEndOffset) / this.messageBuffer.Length) < 0.25)
+ {
+ // Double the size of the buffer
+ Array.Resize(
+ ref this.messageBuffer,
+ this.messageBuffer.Length * 2);
+ }
+
+ // Read the next chunk into the message buffer
+ int readLength =
+ await this.inputStream.ReadAsync(
+ this.messageBuffer,
+ this.bufferEndOffset,
+ this.messageBuffer.Length - this.bufferEndOffset);
+
+ this.bufferEndOffset += readLength;
+
+ if (readLength == 0)
+ {
+ // If ReadAsync returns 0 then it means that the stream was
+ // closed unexpectedly (usually due to the client application
+ // ending suddenly). For now, just terminate the language
+ // server immediately.
+ // TODO: Provide a more graceful shutdown path
+ throw new EndOfStreamException(
+ "MessageReader's input stream ended unexpectedly, terminating.");
+ }
+
+ return true;
+ }
+
+ private bool TryReadMessageHeaders()
+ {
+ int scanOffset = this.readOffset;
+
+ // Scan for the final double-newline that marks the
+ // end of the header lines
+ while (scanOffset + 3 < this.bufferEndOffset &&
+ (this.messageBuffer[scanOffset] != CR ||
+ this.messageBuffer[scanOffset + 1] != LF ||
+ this.messageBuffer[scanOffset + 2] != CR ||
+ this.messageBuffer[scanOffset + 3] != LF))
+ {
+ scanOffset++;
+ }
+
+ // No header or body separator found (e.g CRLFCRLF)
+ if (scanOffset + 3 >= this.bufferEndOffset)
+ {
+ return false;
+ }
+
+ this.messageHeaders = new Dictionary();
+
+ var headers =
+ Encoding.ASCII
+ .GetString(this.messageBuffer, this.readOffset, scanOffset)
+ .Split(NewLineDelimiters, StringSplitOptions.RemoveEmptyEntries);
+
+ // Read each header and store it in the dictionary
+ foreach (var header in headers)
+ {
+ int currentLength = header.IndexOf(':');
+ if (currentLength == -1)
+ {
+ throw new ArgumentException("Message header must separate key and value using :");
+ }
+
+ var key = header.Substring(0, currentLength);
+ var value = header.Substring(currentLength + 1).Trim();
+ this.messageHeaders[key] = value;
+ }
+
+ // Make sure a Content-Length header was present, otherwise it
+ // is a fatal error
+ string contentLengthString = null;
+ if (!this.messageHeaders.TryGetValue("Content-Length", out contentLengthString))
+ {
+ throw new MessageParseException("", "Fatal error: Content-Length header must be provided.");
+ }
+
+ // Parse the content length to an integer
+ if (!int.TryParse(contentLengthString, out this.expectedContentLength))
+ {
+ throw new MessageParseException("", "Fatal error: Content-Length value is not an integer.");
+ }
+
+ // Skip past the headers plus the newline characters
+ this.readOffset += scanOffset + 4;
+
+ // Done reading headers, now read content
+ this.readState = ReadState.Content;
+
+ return true;
+ }
+
+ private bool TryReadMessageContent(out string messageContent)
+ {
+ messageContent = null;
+
+ // Do we have enough bytes to reach the expected length?
+ if ((this.bufferEndOffset - this.readOffset) < this.expectedContentLength)
+ {
+ return false;
+ }
+
+ // Convert the message contents to a string using the specified encoding
+ messageContent =
+ this.messageEncoding.GetString(
+ this.messageBuffer,
+ this.readOffset,
+ this.expectedContentLength);
+
+ // Move the remaining bytes to the front of the buffer for the next message
+ var remainingByteCount = this.bufferEndOffset - (this.expectedContentLength + this.readOffset);
+ Buffer.BlockCopy(
+ this.messageBuffer,
+ this.expectedContentLength + this.readOffset,
+ this.messageBuffer,
+ 0,
+ remainingByteCount);
+
+ // Reset the offsets for the next read
+ this.readOffset = 0;
+ this.bufferEndOffset = remainingByteCount;
+
+ // Done reading content, now look for headers
+ this.readState = ReadState.Headers;
+
+ return true;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/ServiceHost/MessageProtocol/MessageWriter.cs b/src/ServiceHost/MessageProtocol/MessageWriter.cs
new file mode 100644
index 00000000..96e13bcd
--- /dev/null
+++ b/src/ServiceHost/MessageProtocol/MessageWriter.cs
@@ -0,0 +1,140 @@
+//
+// 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.EditorServices.Utility;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
+{
+ public class MessageWriter
+ {
+ #region Private Fields
+
+ private Stream outputStream;
+ private IMessageSerializer messageSerializer;
+ private AsyncLock writeLock = new AsyncLock();
+
+ private JsonSerializer contentSerializer =
+ JsonSerializer.Create(
+ Constants.JsonSerializerSettings);
+
+ #endregion
+
+ #region Constructors
+
+ public MessageWriter(
+ Stream outputStream,
+ IMessageSerializer messageSerializer)
+ {
+ Validate.IsNotNull("streamWriter", outputStream);
+ Validate.IsNotNull("messageSerializer", messageSerializer);
+
+ this.outputStream = outputStream;
+ this.messageSerializer = messageSerializer;
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ // TODO: This method should be made protected or private
+
+ public async Task WriteMessage(Message messageToWrite)
+ {
+ Validate.IsNotNull("messageToWrite", messageToWrite);
+
+ // Serialize the message
+ JObject messageObject =
+ this.messageSerializer.SerializeMessage(
+ messageToWrite);
+
+ // Log the JSON representation of the message
+ Logger.Write(
+ LogLevel.Verbose,
+ string.Format(
+ "WRITE MESSAGE:\r\n\r\n{0}",
+ JsonConvert.SerializeObject(
+ messageObject,
+ Formatting.Indented,
+ Constants.JsonSerializerSettings)));
+
+ string serializedMessage =
+ JsonConvert.SerializeObject(
+ messageObject,
+ Constants.JsonSerializerSettings);
+
+ byte[] messageBytes = Encoding.UTF8.GetBytes(serializedMessage);
+ byte[] headerBytes =
+ Encoding.ASCII.GetBytes(
+ string.Format(
+ Constants.ContentLengthFormatString,
+ messageBytes.Length));
+
+ // Make sure only one call is writing at a time. You might be thinking
+ // "Why not use a normal lock?" We use an AsyncLock here so that the
+ // message loop doesn't get blocked while waiting for I/O to complete.
+ using (await this.writeLock.LockAsync())
+ {
+ // Send the message
+ await this.outputStream.WriteAsync(headerBytes, 0, headerBytes.Length);
+ await this.outputStream.WriteAsync(messageBytes, 0, messageBytes.Length);
+ await this.outputStream.FlushAsync();
+ }
+ }
+
+ public async Task WriteRequest(
+ RequestType requestType,
+ TParams requestParams,
+ int requestId)
+ {
+ // Allow null content
+ JToken contentObject =
+ requestParams != null ?
+ JToken.FromObject(requestParams, contentSerializer) :
+ null;
+
+ await this.WriteMessage(
+ Message.Request(
+ requestId.ToString(),
+ requestType.MethodName,
+ contentObject));
+ }
+
+ public async Task WriteResponse(TResult resultContent, string method, string requestId)
+ {
+ // Allow null content
+ JToken contentObject =
+ resultContent != null ?
+ JToken.FromObject(resultContent, contentSerializer) :
+ null;
+
+ await this.WriteMessage(
+ Message.Response(
+ requestId,
+ method,
+ contentObject));
+ }
+
+ public async Task WriteEvent(EventType eventType, TParams eventParams)
+ {
+ // Allow null content
+ JToken contentObject =
+ eventParams != null ?
+ JToken.FromObject(eventParams, contentSerializer) :
+ null;
+
+ await this.WriteMessage(
+ Message.Event(
+ eventType.MethodName,
+ contentObject));
+ }
+
+ #endregion
+ }
+}
diff --git a/src/ServiceHost/MessageProtocol/ProtocolEndpoint.cs b/src/ServiceHost/MessageProtocol/ProtocolEndpoint.cs
new file mode 100644
index 00000000..daead186
--- /dev/null
+++ b/src/ServiceHost/MessageProtocol/ProtocolEndpoint.cs
@@ -0,0 +1,313 @@
+//
+// 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.EditorServices.Protocol.MessageProtocol.Channel;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
+{
+ ///
+ /// Provides behavior for a client or server endpoint that
+ /// communicates using the specified protocol.
+ ///
+ public class ProtocolEndpoint : IMessageSender
+ {
+ private bool isStarted;
+ private int currentMessageId;
+ private ChannelBase protocolChannel;
+ private MessageProtocolType messageProtocolType;
+ private TaskCompletionSource endpointExitedTask;
+ private SynchronizationContext originalSynchronizationContext;
+
+ private Dictionary> pendingRequests =
+ new Dictionary>();
+
+ ///
+ /// Gets the MessageDispatcher which allows registration of
+ /// handlers for requests, responses, and events that are
+ /// transmitted through the channel.
+ ///
+ protected MessageDispatcher MessageDispatcher { get; set; }
+
+ ///
+ /// Initializes an instance of the protocol server using the
+ /// specified channel for communication.
+ ///
+ ///
+ /// The channel to use for communication with the connected endpoint.
+ ///
+ ///
+ /// The type of message protocol used by the endpoint.
+ ///
+ public ProtocolEndpoint(
+ ChannelBase protocolChannel,
+ MessageProtocolType messageProtocolType)
+ {
+ this.protocolChannel = protocolChannel;
+ this.messageProtocolType = messageProtocolType;
+ this.originalSynchronizationContext = SynchronizationContext.Current;
+ }
+
+ ///
+ /// Starts the language server client and sends the Initialize method.
+ ///
+ /// A Task that can be awaited for initialization to complete.
+ public async Task Start()
+ {
+ if (!this.isStarted)
+ {
+ // Start the provided protocol channel
+ this.protocolChannel.Start(this.messageProtocolType);
+
+ // Start the message dispatcher
+ this.MessageDispatcher = new MessageDispatcher(this.protocolChannel);
+
+ // Set the handler for any message responses that come back
+ this.MessageDispatcher.SetResponseHandler(this.HandleResponse);
+
+ // Listen for unhandled exceptions from the dispatcher
+ this.MessageDispatcher.UnhandledException += MessageDispatcher_UnhandledException;
+
+ // Notify implementation about endpoint start
+ await this.OnStart();
+
+ // Wait for connection and notify the implementor
+ // NOTE: This task is not meant to be awaited.
+ Task waitTask =
+ this.protocolChannel
+ .WaitForConnection()
+ .ContinueWith(
+ async (t) =>
+ {
+ // Start the MessageDispatcher
+ this.MessageDispatcher.Start();
+ await this.OnConnect();
+ });
+
+ // Endpoint is now started
+ this.isStarted = true;
+ }
+ }
+
+ public void WaitForExit()
+ {
+ this.endpointExitedTask = new TaskCompletionSource();
+ this.endpointExitedTask.Task.Wait();
+ }
+
+ public async Task Stop()
+ {
+ if (this.isStarted)
+ {
+ // Make sure no future calls try to stop the endpoint during shutdown
+ this.isStarted = false;
+
+ // Stop the implementation first
+ await this.OnStop();
+
+ // Stop the dispatcher and channel
+ this.MessageDispatcher.Stop();
+ this.protocolChannel.Stop();
+
+ // Notify anyone waiting for exit
+ if (this.endpointExitedTask != null)
+ {
+ this.endpointExitedTask.SetResult(true);
+ }
+ }
+ }
+
+ #region Message Sending
+
+ ///
+ /// Sends a request to the server
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Task SendRequest(
+ RequestType requestType,
+ TParams requestParams)
+ {
+ return this.SendRequest(requestType, requestParams, true);
+ }
+
+ public async Task SendRequest(
+ RequestType requestType,
+ TParams requestParams,
+ bool waitForResponse)
+ {
+ if (!this.protocolChannel.IsConnected)
+ {
+ throw new InvalidOperationException("SendRequest called when ProtocolChannel was not yet connected");
+ }
+
+ this.currentMessageId++;
+
+ TaskCompletionSource responseTask = null;
+
+ if (waitForResponse)
+ {
+ responseTask = new TaskCompletionSource();
+ this.pendingRequests.Add(
+ this.currentMessageId.ToString(),
+ responseTask);
+ }
+
+ await this.protocolChannel.MessageWriter.WriteRequest(
+ requestType,
+ requestParams,
+ this.currentMessageId);
+
+ if (responseTask != null)
+ {
+ var responseMessage = await responseTask.Task;
+
+ return
+ responseMessage.Contents != null ?
+ responseMessage.Contents.ToObject() :
+ default(TResult);
+ }
+ else
+ {
+ // TODO: Better default value here?
+ return default(TResult);
+ }
+ }
+
+ ///
+ /// Sends an event to the channel's endpoint.
+ ///
+ /// The event parameter type.
+ /// The type of event being sent.
+ /// The event parameters being sent.
+ /// A Task that tracks completion of the send operation.
+ public Task SendEvent(
+ EventType eventType,
+ TParams eventParams)
+ {
+ if (!this.protocolChannel.IsConnected)
+ {
+ throw new InvalidOperationException("SendEvent called when ProtocolChannel was not yet connected");
+ }
+
+ // Some events could be raised from a different thread.
+ // To ensure that messages are written serially, dispatch
+ // dispatch the SendEvent call to the message loop thread.
+
+ if (!this.MessageDispatcher.InMessageLoopThread)
+ {
+ TaskCompletionSource writeTask = new TaskCompletionSource();
+
+ this.MessageDispatcher.SynchronizationContext.Post(
+ async (obj) =>
+ {
+ await this.protocolChannel.MessageWriter.WriteEvent(
+ eventType,
+ eventParams);
+
+ writeTask.SetResult(true);
+ }, null);
+
+ return writeTask.Task;
+ }
+ else
+ {
+ return this.protocolChannel.MessageWriter.WriteEvent(
+ eventType,
+ eventParams);
+ }
+ }
+
+ #endregion
+
+ #region Message Handling
+
+ public void SetRequestHandler(
+ RequestType requestType,
+ Func, Task> requestHandler)
+ {
+ this.MessageDispatcher.SetRequestHandler(
+ requestType,
+ requestHandler);
+ }
+
+ public void SetEventHandler(
+ EventType eventType,
+ Func eventHandler)
+ {
+ this.MessageDispatcher.SetEventHandler(
+ eventType,
+ eventHandler,
+ false);
+ }
+
+ public void SetEventHandler(
+ EventType eventType,
+ Func eventHandler,
+ bool overrideExisting)
+ {
+ this.MessageDispatcher.SetEventHandler(
+ eventType,
+ eventHandler,
+ overrideExisting);
+ }
+
+ private void HandleResponse(Message responseMessage)
+ {
+ TaskCompletionSource pendingRequestTask = null;
+
+ if (this.pendingRequests.TryGetValue(responseMessage.Id, out pendingRequestTask))
+ {
+ pendingRequestTask.SetResult(responseMessage);
+ this.pendingRequests.Remove(responseMessage.Id);
+ }
+ }
+
+ #endregion
+
+ #region Subclass Lifetime Methods
+
+ protected virtual Task OnStart()
+ {
+ return Task.FromResult(true);
+ }
+
+ protected virtual Task OnConnect()
+ {
+ return Task.FromResult(true);
+ }
+
+ protected virtual Task OnStop()
+ {
+ return Task.FromResult(true);
+ }
+
+ #endregion
+
+ #region Event Handlers
+
+ private void MessageDispatcher_UnhandledException(object sender, Exception e)
+ {
+ if (this.endpointExitedTask != null)
+ {
+ this.endpointExitedTask.SetException(e);
+ }
+
+ else if (this.originalSynchronizationContext != null)
+ {
+ this.originalSynchronizationContext.Post(o => { throw e; }, null);
+ }
+ }
+
+ #endregion
+ }
+}
+
diff --git a/src/ServiceHost/MessageProtocol/RequestContext.cs b/src/ServiceHost/MessageProtocol/RequestContext.cs
new file mode 100644
index 00000000..a35bb136
--- /dev/null
+++ b/src/ServiceHost/MessageProtocol/RequestContext.cs
@@ -0,0 +1,47 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using Newtonsoft.Json.Linq;
+using System.Threading.Tasks;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
+{
+ public class RequestContext
+ {
+ private Message requestMessage;
+ private MessageWriter messageWriter;
+
+ public RequestContext(Message requestMessage, MessageWriter messageWriter)
+ {
+ this.requestMessage = requestMessage;
+ this.messageWriter = messageWriter;
+ }
+
+ public async Task SendResult(TResult resultDetails)
+ {
+ await this.messageWriter.WriteResponse(
+ resultDetails,
+ requestMessage.Method,
+ requestMessage.Id);
+ }
+
+ public async Task SendEvent(EventType eventType, TParams eventParams)
+ {
+ await this.messageWriter.WriteEvent(
+ eventType,
+ eventParams);
+ }
+
+ public async Task SendError(object errorDetails)
+ {
+ await this.messageWriter.WriteMessage(
+ Message.ResponseError(
+ requestMessage.Id,
+ requestMessage.Method,
+ JToken.FromObject(errorDetails)));
+ }
+ }
+}
+
diff --git a/src/ServiceHost/MessageProtocol/RequestType.cs b/src/ServiceHost/MessageProtocol/RequestType.cs
new file mode 100644
index 00000000..29fc11c5
--- /dev/null
+++ b/src/ServiceHost/MessageProtocol/RequestType.cs
@@ -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.Diagnostics;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
+{
+ [DebuggerDisplay("RequestType MethodName = {MethodName}")]
+ public class RequestType
+ {
+ public string MethodName { get; private set; }
+
+ public static RequestType Create(string typeName)
+ {
+ return new RequestType()
+ {
+ MethodName = typeName
+ };
+ }
+ }
+}
+
diff --git a/src/ServiceHost/MessageProtocol/Serializers/JsonRpcMessageSerializer.cs b/src/ServiceHost/MessageProtocol/Serializers/JsonRpcMessageSerializer.cs
new file mode 100644
index 00000000..fa1d1518
--- /dev/null
+++ b/src/ServiceHost/MessageProtocol/Serializers/JsonRpcMessageSerializer.cs
@@ -0,0 +1,99 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Serializers
+{
+ ///
+ /// Serializes messages in the JSON RPC format. Used primarily
+ /// for language servers.
+ ///
+ public class JsonRpcMessageSerializer : IMessageSerializer
+ {
+ public JObject SerializeMessage(Message message)
+ {
+ JObject messageObject = new JObject();
+
+ messageObject.Add("jsonrpc", JToken.FromObject("2.0"));
+
+ if (message.MessageType == MessageType.Request)
+ {
+ messageObject.Add("id", JToken.FromObject(message.Id));
+ messageObject.Add("method", message.Method);
+ messageObject.Add("params", message.Contents);
+ }
+ else if (message.MessageType == MessageType.Event)
+ {
+ messageObject.Add("method", message.Method);
+ messageObject.Add("params", message.Contents);
+ }
+ else if (message.MessageType == MessageType.Response)
+ {
+ messageObject.Add("id", JToken.FromObject(message.Id));
+
+ if (message.Error != null)
+ {
+ // Write error
+ messageObject.Add("error", message.Error);
+ }
+ else
+ {
+ // Write result
+ messageObject.Add("result", message.Contents);
+ }
+ }
+
+ return messageObject;
+ }
+
+ public Message DeserializeMessage(JObject messageJson)
+ {
+ // TODO: Check for jsonrpc version
+
+ JToken token = null;
+ if (messageJson.TryGetValue("id", out token))
+ {
+ // Message is a Request or Response
+ string messageId = token.ToString();
+
+ if (messageJson.TryGetValue("result", out token))
+ {
+ return Message.Response(messageId, null, token);
+ }
+ else if (messageJson.TryGetValue("error", out token))
+ {
+ return Message.ResponseError(messageId, null, token);
+ }
+ else
+ {
+ JToken messageParams = null;
+ messageJson.TryGetValue("params", out messageParams);
+
+ if (!messageJson.TryGetValue("method", out token))
+ {
+ // TODO: Throw parse error
+ }
+
+ return Message.Request(messageId, token.ToString(), messageParams);
+ }
+ }
+ else
+ {
+ // Messages without an id are events
+ JToken messageParams = token;
+ messageJson.TryGetValue("params", out messageParams);
+
+ if (!messageJson.TryGetValue("method", out token))
+ {
+ // TODO: Throw parse error
+ }
+
+ return Message.Event(token.ToString(), messageParams);
+ }
+ }
+ }
+}
+
diff --git a/src/ServiceHost/MessageProtocol/Serializers/V8MessageSerializer.cs b/src/ServiceHost/MessageProtocol/Serializers/V8MessageSerializer.cs
new file mode 100644
index 00000000..941e249a
--- /dev/null
+++ b/src/ServiceHost/MessageProtocol/Serializers/V8MessageSerializer.cs
@@ -0,0 +1,113 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using Newtonsoft.Json.Linq;
+using System;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Serializers
+{
+ ///
+ /// Serializes messages in the V8 format. Used primarily for debug adapters.
+ ///
+ public class V8MessageSerializer : IMessageSerializer
+ {
+ public JObject SerializeMessage(Message message)
+ {
+ JObject messageObject = new JObject();
+
+ if (message.MessageType == MessageType.Request)
+ {
+ messageObject.Add("type", JToken.FromObject("request"));
+ messageObject.Add("seq", JToken.FromObject(message.Id));
+ messageObject.Add("command", message.Method);
+ messageObject.Add("arguments", message.Contents);
+ }
+ else if (message.MessageType == MessageType.Event)
+ {
+ messageObject.Add("type", JToken.FromObject("event"));
+ messageObject.Add("event", message.Method);
+ messageObject.Add("body", message.Contents);
+ }
+ else if (message.MessageType == MessageType.Response)
+ {
+ messageObject.Add("type", JToken.FromObject("response"));
+ messageObject.Add("request_seq", JToken.FromObject(message.Id));
+ messageObject.Add("command", message.Method);
+
+ if (message.Error != null)
+ {
+ // Write error
+ messageObject.Add("success", JToken.FromObject(false));
+ messageObject.Add("message", message.Error);
+ }
+ else
+ {
+ // Write result
+ messageObject.Add("success", JToken.FromObject(true));
+ messageObject.Add("body", message.Contents);
+ }
+ }
+
+ return messageObject;
+ }
+
+ public Message DeserializeMessage(JObject messageJson)
+ {
+ JToken token = null;
+
+ if (messageJson.TryGetValue("type", out token))
+ {
+ string messageType = token.ToString();
+
+ if (string.Equals("request", messageType, StringComparison.CurrentCultureIgnoreCase))
+ {
+ return Message.Request(
+ messageJson.GetValue("seq").ToString(),
+ messageJson.GetValue("command").ToString(),
+ messageJson.GetValue("arguments"));
+ }
+ else if (string.Equals("response", messageType, StringComparison.CurrentCultureIgnoreCase))
+ {
+ if (messageJson.TryGetValue("success", out token))
+ {
+ // Was the response for a successful request?
+ if (token.ToObject() == true)
+ {
+ return Message.Response(
+ messageJson.GetValue("request_seq").ToString(),
+ messageJson.GetValue("command").ToString(),
+ messageJson.GetValue("body"));
+ }
+ else
+ {
+ return Message.ResponseError(
+ messageJson.GetValue("request_seq").ToString(),
+ messageJson.GetValue("command").ToString(),
+ messageJson.GetValue("message"));
+ }
+ }
+ else
+ {
+ // TODO: Parse error
+ }
+
+ }
+ else if (string.Equals("event", messageType, StringComparison.CurrentCultureIgnoreCase))
+ {
+ return Message.Event(
+ messageJson.GetValue("event").ToString(),
+ messageJson.GetValue("body"));
+ }
+ else
+ {
+ return Message.Unknown();
+ }
+ }
+
+ return Message.Unknown();
+ }
+ }
+}
+
diff --git a/src/ServiceHost/Program.cs b/src/ServiceHost/Program.cs
new file mode 100644
index 00000000..6bfd0f24
--- /dev/null
+++ b/src/ServiceHost/Program.cs
@@ -0,0 +1,40 @@
+//
+// 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.Protocol.Server;
+using Microsoft.SqlTools.EditorServices.Session;
+using Microsoft.SqlTools.EditorServices.Utility;
+
+namespace Microsoft.SqlTools.ServiceHost
+{
+ ///
+ /// Main application class for SQL Tools API Service Host executable
+ ///
+ class Program
+ {
+ ///
+ /// Main entry point into the SQL Tools API Service Host
+ ///
+ static void Main(string[] args)
+ {
+ // turn on Verbose logging during early development
+ // we need to switch to Normal when preparing for public preview
+ Logger.Initialize(minimumLogLevel: LogLevel.Verbose);
+ Logger.Write(LogLevel.Normal, "Starting SQL Tools Service Host");
+
+ const string hostName = "SQL Tools Service Host";
+ const string hostProfileId = "SQLToolsService";
+ 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");
+
+ // create and run the language server
+ var languageServer = new LanguageServer(hostDetails, profilePaths);
+ languageServer.Start().Wait();
+ languageServer.WaitForExit();
+ }
+ }
+}
diff --git a/src/ServiceHost/Properties/AssemblyInfo.cs b/src/ServiceHost/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..27c1daba
--- /dev/null
+++ b/src/ServiceHost/Properties/AssemblyInfo.cs
@@ -0,0 +1,44 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("SqlTools Editor Services Host Protocol Library")]
+[assembly: AssemblyDescription("Provides message types and client/server APIs for the SqlTools Editor Services JSON protocol.")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Microsoft")]
+[assembly: AssemblyProduct("SqlTools Editor Services")]
+[assembly: AssemblyCopyright("� Microsoft Corporation. All rights reserved.")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("78caf6c3-5955-4b15-a302-2bd6b7871d5b")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
+[assembly: AssemblyInformationalVersion("1.0.0.0")]
+
+[assembly: InternalsVisibleTo("Microsoft.SqlTools.EditorServices.Test.Protocol")]
diff --git a/src/ServiceHost/Server/LanguageServer.cs b/src/ServiceHost/Server/LanguageServer.cs
new file mode 100644
index 00000000..d6719141
--- /dev/null
+++ b/src/ServiceHost/Server/LanguageServer.cs
@@ -0,0 +1,517 @@
+//
+// 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.EditorServices.Protocol.LanguageServer;
+using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
+using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Channel;
+using Microsoft.SqlTools.EditorServices.Session;
+using System.Threading.Tasks;
+using Microsoft.SqlTools.EditorServices.Utility;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading;
+using System.Linq;
+using System;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.Server
+{
+ ///
+ /// SQL Tools VS Code Language Server request handler
+ ///
+ public class LanguageServer : LanguageServerBase
+ {
+ private static CancellationTokenSource existingRequestCancellation;
+
+ private LanguageServerSettings currentSettings = new LanguageServerSettings();
+
+ private EditorSession editorSession;
+
+ ///
+ /// Provides details about the host application.
+ ///
+ public LanguageServer(HostDetails hostDetails, ProfilePaths profilePaths)
+ : base(new StdioServerChannel())
+ {
+ this.editorSession = new EditorSession();
+ this.editorSession.StartSession(hostDetails, profilePaths);
+ }
+
+ ///
+ /// Initialize the VS Code request/response callbacks
+ ///
+ protected override void Initialize()
+ {
+ // Register all supported message types
+ this.SetRequestHandler(InitializeRequest.Type, this.HandleInitializeRequest);
+ this.SetEventHandler(DidChangeTextDocumentNotification.Type, this.HandleDidChangeTextDocumentNotification);
+ this.SetEventHandler(DidOpenTextDocumentNotification.Type, this.HandleDidOpenTextDocumentNotification);
+ this.SetEventHandler(DidCloseTextDocumentNotification.Type, this.HandleDidCloseTextDocumentNotification);
+ this.SetEventHandler(DidChangeConfigurationNotification.Type, this.HandleDidChangeConfigurationNotification);
+
+ this.SetRequestHandler(DefinitionRequest.Type, this.HandleDefinitionRequest);
+ this.SetRequestHandler(ReferencesRequest.Type, this.HandleReferencesRequest);
+ this.SetRequestHandler(CompletionRequest.Type, this.HandleCompletionRequest);
+ this.SetRequestHandler(CompletionResolveRequest.Type, this.HandleCompletionResolveRequest);
+ this.SetRequestHandler(SignatureHelpRequest.Type, this.HandleSignatureHelpRequest);
+ this.SetRequestHandler(DocumentHighlightRequest.Type, this.HandleDocumentHighlightRequest);
+ this.SetRequestHandler(HoverRequest.Type, this.HandleHoverRequest);
+ this.SetRequestHandler(DocumentSymbolRequest.Type, this.HandleDocumentSymbolRequest);
+ this.SetRequestHandler(WorkspaceSymbolRequest.Type, this.HandleWorkspaceSymbolRequest);
+ }
+
+ ///
+ /// Handles the shutdown event for the Language Server
+ ///
+ protected override async Task Shutdown()
+ {
+ Logger.Write(LogLevel.Normal, "Language service is shutting down...");
+
+ if (this.editorSession != null)
+ {
+ this.editorSession.Dispose();
+ this.editorSession = null;
+ }
+
+ await Task.FromResult(true);
+ }
+
+ ///
+ /// Handles the initialization request
+ ///
+ ///
+ ///
+ ///
+ protected async Task HandleInitializeRequest(
+ InitializeRequest initializeParams,
+ RequestContext requestContext)
+ {
+ Logger.Write(LogLevel.Verbose, "HandleDidChangeTextDocumentNotification");
+
+ // Grab the workspace path from the parameters
+ editorSession.Workspace.WorkspacePath = initializeParams.RootPath;
+
+ await requestContext.SendResult(
+ new InitializeResult
+ {
+ Capabilities = new ServerCapabilities
+ {
+ TextDocumentSync = TextDocumentSyncKind.Incremental,
+ DefinitionProvider = true,
+ ReferencesProvider = true,
+ DocumentHighlightProvider = true,
+ DocumentSymbolProvider = true,
+ WorkspaceSymbolProvider = true,
+ HoverProvider = true,
+ CompletionProvider = new CompletionOptions
+ {
+ ResolveProvider = true,
+ TriggerCharacters = new string[] { ".", "-", ":", "\\" }
+ },
+ SignatureHelpProvider = new SignatureHelpOptions
+ {
+ TriggerCharacters = new string[] { " " } // TODO: Other characters here?
+ }
+ }
+ });
+ }
+
+ ///
+ /// Handles text document change events
+ ///
+ ///
+ ///
+ ///
+ protected Task HandleDidChangeTextDocumentNotification(
+ DidChangeTextDocumentParams textChangeParams,
+ EventContext eventContext)
+ {
+ StringBuilder msg = new StringBuilder();
+ msg.Append("HandleDidChangeTextDocumentNotification");
+ List changedFiles = new List();
+
+ // 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);
+ }
+
+ ///
+ /// Handles the configuration change event
+ ///
+ ///
+ ///
+ protected async Task HandleDidChangeConfigurationNotification(
+ DidChangeConfigurationParams 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);
+ }
+
+ protected async Task HandleDefinitionRequest(
+ TextDocumentPosition textDocumentPosition,
+ RequestContext requestContext)
+ {
+ Logger.Write(LogLevel.Verbose, "HandleDefinitionRequest");
+ await Task.FromResult(true);
+ }
+
+ protected async Task HandleReferencesRequest(
+ ReferencesParams referencesParams,
+ RequestContext requestContext)
+ {
+ Logger.Write(LogLevel.Verbose, "HandleReferencesRequest");
+ await Task.FromResult(true);
+ }
+
+ protected async Task HandleCompletionRequest(
+ TextDocumentPosition textDocumentPosition,
+ RequestContext requestContext)
+ {
+ Logger.Write(LogLevel.Verbose, "HandleCompletionRequest");
+ await Task.FromResult(true);
+ }
+
+ protected async Task HandleCompletionResolveRequest(
+ CompletionItem completionItem,
+ RequestContext requestContext)
+ {
+ Logger.Write(LogLevel.Verbose, "HandleCompletionResolveRequest");
+ await Task.FromResult(true);
+ }
+
+ protected async Task HandleSignatureHelpRequest(
+ TextDocumentPosition textDocumentPosition,
+ RequestContext requestContext)
+ {
+ Logger.Write(LogLevel.Verbose, "HandleSignatureHelpRequest");
+ await Task.FromResult(true);
+ }
+
+ protected async Task HandleDocumentHighlightRequest(
+ TextDocumentPosition textDocumentPosition,
+ RequestContext requestContext)
+ {
+ Logger.Write(LogLevel.Verbose, "HandleDocumentHighlightRequest");
+ await Task.FromResult(true);
+ }
+
+ protected async Task HandleHoverRequest(
+ TextDocumentPosition textDocumentPosition,
+ RequestContext requestContext)
+ {
+ Logger.Write(LogLevel.Verbose, "HandleHoverRequest");
+ await Task.FromResult(true);
+ }
+
+ protected async Task HandleDocumentSymbolRequest(
+ TextDocumentIdentifier textDocumentIdentifier,
+ RequestContext requestContext)
+ {
+ Logger.Write(LogLevel.Verbose, "HandleDocumentSymbolRequest");
+ await Task.FromResult(true);
+ }
+
+ protected async Task HandleWorkspaceSymbolRequest(
+ WorkspaceSymbolParams workspaceSymbolParams,
+ RequestContext requestContext)
+ {
+ Logger.Write(LogLevel.Verbose, "HandleWorkspaceSymbolRequest");
+ await Task.FromResult(true);
+ }
+
+ ///
+ /// Runs script diagnostics on changed files
+ ///
+ ///
+ ///
+ ///
+ 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 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,
+ editorSession,
+ 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 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);
+ }
+ }
+
+ ///
+ /// 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;
+ }
+ }
+
+ ///
+ /// Switch from 0-based offsets to 1 based offsets
+ ///
+ ///
+ ///
+ 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
+ };
+ }
+ }
+}
diff --git a/src/ServiceHost/Server/LanguageServerBase.cs b/src/ServiceHost/Server/LanguageServerBase.cs
new file mode 100644
index 00000000..0128484b
--- /dev/null
+++ b/src/ServiceHost/Server/LanguageServerBase.cs
@@ -0,0 +1,84 @@
+//
+// 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.EditorServices.Protocol.LanguageServer;
+using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
+using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Channel;
+using System.Threading.Tasks;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.Server
+{
+ public abstract class LanguageServerBase : ProtocolEndpoint
+ {
+ private bool isStarted;
+ private ChannelBase serverChannel;
+ private TaskCompletionSource serverExitedTask;
+
+ public LanguageServerBase(ChannelBase serverChannel) :
+ base(serverChannel, MessageProtocolType.LanguageServer)
+ {
+ this.serverChannel = serverChannel;
+ }
+
+ protected override Task OnStart()
+ {
+ // Register handlers for server lifetime messages
+ this.SetRequestHandler(ShutdownRequest.Type, this.HandleShutdownRequest);
+ this.SetEventHandler(ExitNotification.Type, this.HandleExitNotification);
+
+ // Initialize the implementation class
+ this.Initialize();
+
+ return Task.FromResult(true);
+ }
+
+ protected override async Task OnStop()
+ {
+ await this.Shutdown();
+ }
+
+ ///
+ /// Overridden by the subclass to provide initialization
+ /// logic after the server channel is started.
+ ///
+ protected abstract void Initialize();
+
+ ///
+ /// Can be overridden by the subclass to provide shutdown
+ /// logic before the server exits. Subclasses do not need
+ /// to invoke or return the value of the base implementation.
+ ///
+ protected virtual Task Shutdown()
+ {
+ // No default implementation yet.
+ return Task.FromResult(true);
+ }
+
+ private async Task HandleShutdownRequest(
+ object shutdownParams,
+ RequestContext requestContext)
+ {
+ // Allow the implementor to shut down gracefully
+ await this.Shutdown();
+
+ await requestContext.SendResult(new object());
+ }
+
+ private async Task HandleExitNotification(
+ object exitParams,
+ EventContext eventContext)
+ {
+ // Stop the server channel
+ await this.Stop();
+
+ // Notify any waiter that the server has exited
+ if (this.serverExitedTask != null)
+ {
+ this.serverExitedTask.SetResult(true);
+ }
+ }
+ }
+}
+
diff --git a/src/ServiceHost/Server/LanguageServerEditorOperations.cs b/src/ServiceHost/Server/LanguageServerEditorOperations.cs
new file mode 100644
index 00000000..e5714b19
--- /dev/null
+++ b/src/ServiceHost/Server/LanguageServerEditorOperations.cs
@@ -0,0 +1,114 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+#if false
+using Microsoft.SqlTools.EditorServices.Extensions;
+using Microsoft.SqlTools.EditorServices.Protocol.LanguageServer;
+using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
+using System.Threading.Tasks;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.Server
+{
+ internal class LanguageServerEditorOperations : IEditorOperations
+ {
+ private EditorSession editorSession;
+ private IMessageSender messageSender;
+
+ public LanguageServerEditorOperations(
+ EditorSession editorSession,
+ IMessageSender messageSender)
+ {
+ this.editorSession = editorSession;
+ this.messageSender = messageSender;
+ }
+
+ public async Task GetEditorContext()
+ {
+ ClientEditorContext clientContext =
+ await this.messageSender.SendRequest(
+ GetEditorContextRequest.Type,
+ new GetEditorContextRequest(),
+ true);
+
+ return this.ConvertClientEditorContext(clientContext);
+ }
+
+ public async Task InsertText(string filePath, string text, BufferRange insertRange)
+ {
+ await this.messageSender.SendRequest(
+ InsertTextRequest.Type,
+ new InsertTextRequest
+ {
+ FilePath = filePath,
+ InsertText = text,
+ InsertRange =
+ new Range
+ {
+ Start = new Position
+ {
+ Line = insertRange.Start.Line - 1,
+ Character = insertRange.Start.Column - 1
+ },
+ End = new Position
+ {
+ Line = insertRange.End.Line - 1,
+ Character = insertRange.End.Column - 1
+ }
+ }
+ }, false);
+
+ // TODO: Set the last param back to true!
+ }
+
+ public Task SetSelection(BufferRange selectionRange)
+ {
+ return this.messageSender.SendRequest(
+ SetSelectionRequest.Type,
+ new SetSelectionRequest
+ {
+ SelectionRange =
+ new Range
+ {
+ Start = new Position
+ {
+ Line = selectionRange.Start.Line - 1,
+ Character = selectionRange.Start.Column - 1
+ },
+ End = new Position
+ {
+ Line = selectionRange.End.Line - 1,
+ Character = selectionRange.End.Column - 1
+ }
+ }
+ }, true);
+ }
+
+ public EditorContext ConvertClientEditorContext(
+ ClientEditorContext clientContext)
+ {
+ return
+ new EditorContext(
+ this,
+ this.editorSession.Workspace.GetFile(clientContext.CurrentFilePath),
+ new BufferPosition(
+ clientContext.CursorPosition.Line + 1,
+ clientContext.CursorPosition.Character + 1),
+ new BufferRange(
+ clientContext.SelectionRange.Start.Line + 1,
+ clientContext.SelectionRange.Start.Character + 1,
+ clientContext.SelectionRange.End.Line + 1,
+ clientContext.SelectionRange.End.Character + 1));
+ }
+
+ public Task OpenFile(string filePath)
+ {
+ return
+ this.messageSender.SendRequest(
+ OpenFileRequest.Type,
+ filePath,
+ true);
+ }
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/ServiceHost/Server/LanguageServerSettings.cs b/src/ServiceHost/Server/LanguageServerSettings.cs
new file mode 100644
index 00000000..be09984a
--- /dev/null
+++ b/src/ServiceHost/Server/LanguageServerSettings.cs
@@ -0,0 +1,90 @@
+//
+// 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 Microsoft.SqlTools.EditorServices.Utility;
+
+namespace Microsoft.SqlTools.EditorServices.Protocol.Server
+{
+ public class LanguageServerSettings
+ {
+ public bool EnableProfileLoading { get; set; }
+
+ public ScriptAnalysisSettings ScriptAnalysis { get; set; }
+
+ public LanguageServerSettings()
+ {
+ this.ScriptAnalysis = new ScriptAnalysisSettings();
+ }
+
+ public void Update(LanguageServerSettings settings, string workspaceRootPath)
+ {
+ if (settings != null)
+ {
+ this.EnableProfileLoading = settings.EnableProfileLoading;
+ this.ScriptAnalysis.Update(settings.ScriptAnalysis, workspaceRootPath);
+ }
+ }
+ }
+
+
+ public class ScriptAnalysisSettings
+ {
+ public bool? Enable { get; set; }
+
+ public string SettingsPath { get; set; }
+
+ public ScriptAnalysisSettings()
+ {
+ this.Enable = true;
+ }
+
+ public void Update(ScriptAnalysisSettings settings, string workspaceRootPath)
+ {
+ if (settings != null)
+ {
+ this.Enable = settings.Enable;
+
+ string settingsPath = settings.SettingsPath;
+
+ if (string.IsNullOrWhiteSpace(settingsPath))
+ {
+ settingsPath = null;
+ }
+ else if (!Path.IsPathRooted(settingsPath))
+ {
+ if (string.IsNullOrEmpty(workspaceRootPath))
+ {
+ // The workspace root path could be an empty string
+ // when the user has opened a SqlTools script file
+ // without opening an entire folder (workspace) first.
+ // In this case we should just log an error and let
+ // the specified settings path go through even though
+ // it will fail to load.
+ Logger.Write(
+ LogLevel.Error,
+ "Could not resolve Script Analyzer settings path due to null or empty workspaceRootPath.");
+ }
+ else
+ {
+ settingsPath = Path.GetFullPath(Path.Combine(workspaceRootPath, settingsPath));
+ }
+ }
+
+ this.SettingsPath = settingsPath;
+ }
+ }
+ }
+
+
+ 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 LanguageServerSettings SqlTools { get; set; }
+ }
+}
diff --git a/src/ServiceHost/Session/EditorSession.cs b/src/ServiceHost/Session/EditorSession.cs
new file mode 100644
index 00000000..3c592a8f
--- /dev/null
+++ b/src/ServiceHost/Session/EditorSession.cs
@@ -0,0 +1,75 @@
+//
+// 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.LanguageSupport;
+
+namespace Microsoft.SqlTools.EditorServices
+{
+ ///
+ /// Manages a single session for all editor services. This
+ /// includes managing all open script files for the session.
+ ///
+ public class EditorSession : IDisposable
+ {
+ #region Properties
+
+ ///
+ /// Gets the Workspace instance for this session.
+ ///
+ public Workspace Workspace { get; private set; }
+
+ ///
+ /// Gets or sets the Language Service
+ ///
+ ///
+ public LanguageService LanguageService { get; set; }
+
+ ///
+ /// Gets the SqlToolsContext instance for this session.
+ ///
+ public SqlToolsContext SqlToolsContext { get; private set; }
+
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// Starts the session using the provided IConsoleHost implementation
+ /// for the ConsoleService.
+ ///
+ ///
+ /// Provides details about the host application.
+ ///
+ ///
+ /// An object containing the profile paths for the session.
+ ///
+ 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
+
+ ///
+ /// Disposes of any Runspaces that were created for the
+ /// services used in this session.
+ ///
+ public void Dispose()
+ {
+ }
+
+ #endregion
+
+ }
+}
diff --git a/src/ServiceHost/Session/HostDetails.cs b/src/ServiceHost/Session/HostDetails.cs
new file mode 100644
index 00000000..1a5fc80d
--- /dev/null
+++ b/src/ServiceHost/Session/HostDetails.cs
@@ -0,0 +1,92 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System;
+
+namespace Microsoft.SqlTools.EditorServices.Session
+{
+ ///
+ /// Contains details about the current host application (most
+ /// likely the editor which is using the host process).
+ ///
+ public class HostDetails
+ {
+ #region Constants
+
+ ///
+ /// The default host name for SqlTools Editor Services. Used
+ /// if no host name is specified by the host application.
+ ///
+ public const string DefaultHostName = "SqlTools Editor Services Host";
+
+ ///
+ /// The default host ID for SqlTools Editor Services. Used
+ /// for the host-specific profile path if no host ID is specified.
+ ///
+ public const string DefaultHostProfileId = "Microsoft.SqlToolsEditorServices";
+
+ ///
+ /// The default host version for SqlTools Editor Services. If
+ /// no version is specified by the host application, we use 0.0.0
+ /// to indicate a lack of version.
+ ///
+ public static readonly Version DefaultHostVersion = new Version("0.0.0");
+
+ ///
+ /// The default host details in a HostDetails object.
+ ///
+ public static readonly HostDetails Default = new HostDetails(null, null, null);
+
+ #endregion
+
+ #region Properties
+
+ ///
+ /// Gets the name of the host.
+ ///
+ public string Name { get; private set; }
+
+ ///
+ /// Gets the profile ID of the host, used to determine the
+ /// host-specific profile path.
+ ///
+ public string ProfileId { get; private set; }
+
+ ///
+ /// Gets the version of the host.
+ ///
+ public Version Version { get; private set; }
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Creates an instance of the HostDetails class.
+ ///
+ ///
+ /// The display name for the host, typically in the form of
+ /// "[Application Name] Host".
+ ///
+ ///
+ /// The identifier of the SqlTools host to use for its profile path.
+ /// loaded. Used to resolve a profile path of the form 'X_profile.ps1'
+ /// where 'X' represents the value of hostProfileId. If null, a default
+ /// will be used.
+ ///
+ /// The host application's version.
+ public HostDetails(
+ string name,
+ string profileId,
+ Version version)
+ {
+ this.Name = name ?? DefaultHostName;
+ this.ProfileId = profileId ?? DefaultHostProfileId;
+ this.Version = version ?? DefaultHostVersion;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/ServiceHost/Session/OutputType.cs b/src/ServiceHost/Session/OutputType.cs
new file mode 100644
index 00000000..8ba866d7
--- /dev/null
+++ b/src/ServiceHost/Session/OutputType.cs
@@ -0,0 +1,41 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+namespace Microsoft.SqlTools.EditorServices
+{
+ ///
+ /// Enumerates the types of output lines that will be sent
+ /// to an IConsoleHost implementation.
+ ///
+ public enum OutputType
+ {
+ ///
+ /// A normal output line, usually written with the or Write-Host or
+ /// Write-Output cmdlets.
+ ///
+ Normal,
+
+ ///
+ /// A debug output line, written with the Write-Debug cmdlet.
+ ///
+ Debug,
+
+ ///
+ /// A verbose output line, written with the Write-Verbose cmdlet.
+ ///
+ Verbose,
+
+ ///
+ /// A warning output line, written with the Write-Warning cmdlet.
+ ///
+ Warning,
+
+ ///
+ /// An error output line, written with the Write-Error cmdlet or
+ /// as a result of some error during SqlTools pipeline execution.
+ ///
+ Error
+ }
+}
diff --git a/src/ServiceHost/Session/OutputWrittenEventArgs.cs b/src/ServiceHost/Session/OutputWrittenEventArgs.cs
new file mode 100644
index 00000000..4b1dbbe3
--- /dev/null
+++ b/src/ServiceHost/Session/OutputWrittenEventArgs.cs
@@ -0,0 +1,65 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System;
+
+namespace Microsoft.SqlTools.EditorServices
+{
+ ///
+ /// Provides details about output that has been written to the
+ /// SqlTools host.
+ ///
+ public class OutputWrittenEventArgs
+ {
+ ///
+ /// Gets the text of the output.
+ ///
+ public string OutputText { get; private set; }
+
+ ///
+ /// Gets the type of the output.
+ ///
+ public OutputType OutputType { get; private set; }
+
+ ///
+ /// Gets a boolean which indicates whether a newline
+ /// should be written after the output.
+ ///
+ public bool IncludeNewLine { get; private set; }
+
+ ///
+ /// Gets the foreground color of the output text.
+ ///
+ public ConsoleColor ForegroundColor { get; private set; }
+
+ ///
+ /// Gets the background color of the output text.
+ ///
+ public ConsoleColor BackgroundColor { get; private set; }
+
+ ///
+ /// Creates an instance of the OutputWrittenEventArgs class.
+ ///
+ /// The text of the output.
+ /// A boolean which indicates whether a newline should be written after the output.
+ /// The type of the output.
+ /// The foreground color of the output text.
+ /// The background color of the output text.
+ public OutputWrittenEventArgs(
+ string outputText,
+ bool includeNewLine,
+ OutputType outputType,
+ ConsoleColor foregroundColor,
+ ConsoleColor backgroundColor)
+ {
+ this.OutputText = outputText;
+ this.IncludeNewLine = includeNewLine;
+ this.OutputType = outputType;
+ this.ForegroundColor = foregroundColor;
+ this.BackgroundColor = backgroundColor;
+ }
+ }
+}
+
diff --git a/src/ServiceHost/Session/ProfilePaths.cs b/src/ServiceHost/Session/ProfilePaths.cs
new file mode 100644
index 00000000..4af38521
--- /dev/null
+++ b/src/ServiceHost/Session/ProfilePaths.cs
@@ -0,0 +1,109 @@
+//
+// 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.IO;
+using System.Linq;
+
+namespace Microsoft.SqlTools.EditorServices.Session
+{
+ ///
+ /// Provides profile path resolution behavior relative to the name
+ /// of a particular SqlTools host.
+ ///
+ public class ProfilePaths
+ {
+ #region Constants
+
+ ///
+ /// The file name for the "all hosts" profile. Also used as the
+ /// suffix for the host-specific profile filenames.
+ ///
+ public const string AllHostsProfileName = "profile.ps1";
+
+ #endregion
+
+ #region Properties
+
+ ///
+ /// Gets the profile path for all users, all hosts.
+ ///
+ public string AllUsersAllHosts { get; private set; }
+
+ ///
+ /// Gets the profile path for all users, current host.
+ ///
+ public string AllUsersCurrentHost { get; private set; }
+
+ ///
+ /// Gets the profile path for the current user, all hosts.
+ ///
+ public string CurrentUserAllHosts { get; private set; }
+
+ ///
+ /// Gets the profile path for the current user and host.
+ ///
+ public string CurrentUserCurrentHost { get; private set; }
+
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// Creates a new instance of the ProfilePaths class.
+ ///
+ ///
+ /// The identifier of the host used in the host-specific X_profile.ps1 filename.
+ ///
+ /// The base path to use for constructing AllUsers profile paths.
+ /// The base path to use for constructing CurrentUser profile paths.
+ public ProfilePaths(
+ string hostProfileId,
+ string baseAllUsersPath,
+ string baseCurrentUserPath)
+ {
+ this.Initialize(hostProfileId, baseAllUsersPath, baseCurrentUserPath);
+ }
+
+ private void Initialize(
+ string hostProfileId,
+ string baseAllUsersPath,
+ string baseCurrentUserPath)
+ {
+ string currentHostProfileName =
+ string.Format(
+ "{0}_{1}",
+ hostProfileId,
+ AllHostsProfileName);
+
+ this.AllUsersCurrentHost = Path.Combine(baseAllUsersPath, currentHostProfileName);
+ this.CurrentUserCurrentHost = Path.Combine(baseCurrentUserPath, currentHostProfileName);
+ this.AllUsersAllHosts = Path.Combine(baseAllUsersPath, AllHostsProfileName);
+ this.CurrentUserAllHosts = Path.Combine(baseCurrentUserPath, AllHostsProfileName);
+ }
+
+ ///
+ /// Gets the list of profile paths that exist on the filesystem.
+ ///
+ /// An IEnumerable of profile path strings to be loaded.
+ public IEnumerable GetLoadableProfilePaths()
+ {
+ var profilePaths =
+ new string[]
+ {
+ this.AllUsersAllHosts,
+ this.AllUsersCurrentHost,
+ this.CurrentUserAllHosts,
+ this.CurrentUserCurrentHost
+ };
+
+ return profilePaths.Where(p => File.Exists(p));
+ }
+
+ #endregion
+ }
+}
+
diff --git a/src/ServiceHost/Session/SqlToolsContext.cs b/src/ServiceHost/Session/SqlToolsContext.cs
new file mode 100644
index 00000000..d8016afd
--- /dev/null
+++ b/src/ServiceHost/Session/SqlToolsContext.cs
@@ -0,0 +1,25 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System;
+
+namespace Microsoft.SqlTools.EditorServices.Session
+{
+ public class SqlToolsContext
+ {
+ ///
+ /// Gets the PowerShell version of the current runspace.
+ ///
+ public Version SqlToolsVersion
+ {
+ get; private set;
+ }
+
+ public SqlToolsContext(HostDetails hostDetails, ProfilePaths profilePaths)
+ {
+
+ }
+ }
+}
diff --git a/src/ServiceHost/Utility/AsyncContext.cs b/src/ServiceHost/Utility/AsyncContext.cs
new file mode 100644
index 00000000..c0baa889
--- /dev/null
+++ b/src/ServiceHost/Utility/AsyncContext.cs
@@ -0,0 +1,52 @@
+//
+// 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.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.SqlTools.EditorServices.Utility
+{
+ ///
+ /// Simplifies the setup of a SynchronizationContext for the use
+ /// of async calls in the current thread.
+ ///
+ public static class AsyncContext
+ {
+ ///
+ /// Starts a new ThreadSynchronizationContext, attaches it to
+ /// the thread, and then runs the given async main function.
+ ///
+ ///
+ /// The Task-returning Func which represents the "main" function
+ /// for the thread.
+ ///
+ public static void Start(Func asyncMainFunc)
+ {
+ // Is there already a synchronization context?
+ if (SynchronizationContext.Current != null)
+ {
+ throw new InvalidOperationException(
+ "A SynchronizationContext is already assigned on this thread.");
+ }
+
+ // Create and register a synchronization context for this thread
+ var threadSyncContext = new ThreadSynchronizationContext();
+ SynchronizationContext.SetSynchronizationContext(threadSyncContext);
+
+ // Get the main task and act on its completion
+ Task asyncMainTask = asyncMainFunc();
+ asyncMainTask.ContinueWith(
+ t => threadSyncContext.EndLoop(),
+ TaskScheduler.Default);
+
+ // Start the synchronization context's request loop and
+ // wait for the main task to complete
+ threadSyncContext.RunLoopOnCurrentThread();
+ asyncMainTask.GetAwaiter().GetResult();
+ }
+ }
+}
+
diff --git a/src/ServiceHost/Utility/AsyncContextThread.cs b/src/ServiceHost/Utility/AsyncContextThread.cs
new file mode 100644
index 00000000..7b7947a5
--- /dev/null
+++ b/src/ServiceHost/Utility/AsyncContextThread.cs
@@ -0,0 +1,85 @@
+//
+// 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.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.SqlTools.EditorServices.Utility
+{
+ ///
+ /// Provides a simplified interface for creating a new thread
+ /// and establishing an AsyncContext in it.
+ ///
+ public class AsyncContextThread
+ {
+ #region Private Fields
+
+ private Task threadTask;
+ private string threadName;
+ private CancellationTokenSource threadCancellationToken =
+ new CancellationTokenSource();
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the AsyncContextThread class.
+ ///
+ ///
+ /// The name of the thread for debugging purposes.
+ ///
+ public AsyncContextThread(string threadName)
+ {
+ this.threadName = threadName;
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// Runs a task on the AsyncContextThread.
+ ///
+ ///
+ /// A Func which returns the task to be run on the thread.
+ ///
+ ///
+ /// A Task which can be used to monitor the thread for completion.
+ ///
+ public Task Run(Func taskReturningFunc)
+ {
+ // Start up a long-running task with the action as the
+ // main entry point for the thread
+ this.threadTask =
+ Task.Factory.StartNew(
+ () =>
+ {
+ // Set the thread's name to help with debugging
+ Thread.CurrentThread.Name = "AsyncContextThread: " + this.threadName;
+
+ // Set up an AsyncContext to run the task
+ AsyncContext.Start(taskReturningFunc);
+ },
+ this.threadCancellationToken.Token,
+ TaskCreationOptions.LongRunning,
+ TaskScheduler.Default);
+
+ return this.threadTask;
+ }
+
+ ///
+ /// Stops the thread task.
+ ///
+ public void Stop()
+ {
+ this.threadCancellationToken.Cancel();
+ }
+
+ #endregion
+ }
+}
+
diff --git a/src/ServiceHost/Utility/AsyncLock.cs b/src/ServiceHost/Utility/AsyncLock.cs
new file mode 100644
index 00000000..b12983ce
--- /dev/null
+++ b/src/ServiceHost/Utility/AsyncLock.cs
@@ -0,0 +1,103 @@
+//
+// 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.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.SqlTools.EditorServices.Utility
+{
+ ///
+ /// Provides a simple wrapper over a SemaphoreSlim to allow
+ /// synchronization locking inside of async calls. Cannot be
+ /// used recursively.
+ ///
+ public class AsyncLock
+ {
+ #region Fields
+
+ private Task lockReleaseTask;
+ private SemaphoreSlim lockSemaphore = new SemaphoreSlim(1, 1);
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the AsyncLock class.
+ ///
+ public AsyncLock()
+ {
+ this.lockReleaseTask =
+ Task.FromResult(
+ (IDisposable)new LockReleaser(this));
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// Locks
+ ///
+ /// A task which has an IDisposable
+ public Task LockAsync()
+ {
+ return this.LockAsync(CancellationToken.None);
+ }
+
+ ///
+ /// Obtains or waits for a lock which can be used to synchronize
+ /// access to a resource. The wait may be cancelled with the
+ /// given CancellationToken.
+ ///
+ ///
+ /// A CancellationToken which can be used to cancel the lock.
+ ///
+ ///
+ public Task LockAsync(CancellationToken cancellationToken)
+ {
+ Task waitTask = lockSemaphore.WaitAsync(cancellationToken);
+
+ return waitTask.IsCompleted ?
+ this.lockReleaseTask :
+ waitTask.ContinueWith(
+ (t, releaser) =>
+ {
+ return (IDisposable)releaser;
+ },
+ this.lockReleaseTask.Result,
+ cancellationToken,
+ TaskContinuationOptions.ExecuteSynchronously,
+ TaskScheduler.Default);
+ }
+
+ #endregion
+
+ #region Private Classes
+
+ ///
+ /// Provides an IDisposable wrapper around an AsyncLock so
+ /// that it can easily be used inside of a 'using' block.
+ ///
+ private class LockReleaser : IDisposable
+ {
+ private AsyncLock lockToRelease;
+
+ internal LockReleaser(AsyncLock lockToRelease)
+ {
+ this.lockToRelease = lockToRelease;
+ }
+
+ public void Dispose()
+ {
+ this.lockToRelease.lockSemaphore.Release();
+ }
+ }
+
+ #endregion
+ }
+}
+
diff --git a/src/ServiceHost/Utility/AsyncQueue.cs b/src/ServiceHost/Utility/AsyncQueue.cs
new file mode 100644
index 00000000..c899ee78
--- /dev/null
+++ b/src/ServiceHost/Utility/AsyncQueue.cs
@@ -0,0 +1,155 @@
+//
+// 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 System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.SqlTools.EditorServices.Utility
+{
+ ///
+ /// Provides a synchronized queue which can be used from within async
+ /// operations. This is primarily used for producer/consumer scenarios.
+ ///
+ /// The type of item contained in the queue.
+ public class AsyncQueue
+ {
+ #region Private Fields
+
+ private AsyncLock queueLock = new AsyncLock();
+ private Queue itemQueue;
+ private Queue> requestQueue;
+
+ #endregion
+
+ #region Properties
+
+ ///
+ /// Returns true if the queue is currently empty.
+ ///
+ public bool IsEmpty { get; private set; }
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Initializes an empty instance of the AsyncQueue class.
+ ///
+ public AsyncQueue() : this(Enumerable.Empty())
+ {
+ }
+
+ ///
+ /// Initializes an instance of the AsyncQueue class, pre-populated
+ /// with the given collection of items.
+ ///
+ ///
+ /// An IEnumerable containing the initial items with which the queue will
+ /// be populated.
+ ///
+ public AsyncQueue(IEnumerable initialItems)
+ {
+ this.itemQueue = new Queue(initialItems);
+ this.requestQueue = new Queue>();
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// Enqueues an item onto the end of the queue.
+ ///
+ /// The item to be added to the queue.
+ ///
+ /// A Task which can be awaited until the synchronized enqueue
+ /// operation completes.
+ ///
+ public async Task EnqueueAsync(T item)
+ {
+ using (await queueLock.LockAsync())
+ {
+ TaskCompletionSource requestTaskSource = null;
+
+ // Are any requests waiting?
+ while (this.requestQueue.Count > 0)
+ {
+ // Is the next request cancelled already?
+ requestTaskSource = this.requestQueue.Dequeue();
+ if (!requestTaskSource.Task.IsCanceled)
+ {
+ // Dispatch the item
+ requestTaskSource.SetResult(item);
+ return;
+ }
+ }
+
+ // No more requests waiting, queue the item for a later request
+ this.itemQueue.Enqueue(item);
+ this.IsEmpty = false;
+ }
+ }
+
+ ///
+ /// Dequeues an item from the queue or waits asynchronously
+ /// until an item is available.
+ ///
+ ///
+ /// A Task which can be awaited until a value can be dequeued.
+ ///
+ public Task DequeueAsync()
+ {
+ return this.DequeueAsync(CancellationToken.None);
+ }
+
+ ///
+ /// Dequeues an item from the queue or waits asynchronously
+ /// until an item is available. The wait can be cancelled
+ /// using the given CancellationToken.
+ ///
+ ///
+ /// A CancellationToken with which a dequeue wait can be cancelled.
+ ///
+ ///
+ /// A Task which can be awaited until a value can be dequeued.
+ ///
+ public async Task DequeueAsync(CancellationToken cancellationToken)
+ {
+ Task requestTask;
+
+ using (await queueLock.LockAsync(cancellationToken))
+ {
+ if (this.itemQueue.Count > 0)
+ {
+ // Items are waiting to be taken so take one immediately
+ T item = this.itemQueue.Dequeue();
+ this.IsEmpty = this.itemQueue.Count == 0;
+
+ return item;
+ }
+ else
+ {
+ // Queue the request for the next item
+ var requestTaskSource = new TaskCompletionSource();
+ this.requestQueue.Enqueue(requestTaskSource);
+
+ // Register the wait task for cancel notifications
+ cancellationToken.Register(
+ () => requestTaskSource.TrySetCanceled());
+
+ requestTask = requestTaskSource.Task;
+ }
+ }
+
+ // Wait for the request task to complete outside of the lock
+ return await requestTask;
+ }
+
+ #endregion
+ }
+}
+
diff --git a/src/ServiceHost/Utility/Extensions.cs b/src/ServiceHost/Utility/Extensions.cs
new file mode 100644
index 00000000..c73bc03c
--- /dev/null
+++ b/src/ServiceHost/Utility/Extensions.cs
@@ -0,0 +1,34 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System;
+
+namespace Microsoft.SqlTools.EditorServices.Utility
+{
+ internal static class ObjectExtensions
+ {
+ ///
+ /// Extension to evaluate an object's ToString() method in an exception safe way. This will
+ /// extension method will not throw.
+ ///
+ /// The object on which to call ToString()
+ /// The ToString() return value or a suitable error message is that throws.
+ public static string SafeToString(this object obj)
+ {
+ string str;
+
+ try
+ {
+ str = obj.ToString();
+ }
+ catch (Exception ex)
+ {
+ str = $"";
+ }
+
+ return str;
+ }
+ }
+}
diff --git a/src/ServiceHost/Utility/Logger.cs b/src/ServiceHost/Utility/Logger.cs
new file mode 100644
index 00000000..6bdd39bd
--- /dev/null
+++ b/src/ServiceHost/Utility/Logger.cs
@@ -0,0 +1,222 @@
+//
+// 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.IO;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+namespace Microsoft.SqlTools.EditorServices.Utility
+{
+ ///
+ /// Defines the level indicators for log messages.
+ ///
+ public enum LogLevel
+ {
+ ///
+ /// Indicates a verbose log message.
+ ///
+ Verbose,
+
+ ///
+ /// Indicates a normal, non-verbose log message.
+ ///
+ Normal,
+
+ ///
+ /// Indicates a warning message.
+ ///
+ Warning,
+
+ ///
+ /// Indicates an error message.
+ ///
+ Error
+ }
+
+ ///
+ /// Provides a simple logging interface. May be replaced with a
+ /// more robust solution at a later date.
+ ///
+ public static class Logger
+ {
+ private static LogWriter logWriter;
+
+ ///
+ /// Initializes the Logger for the current session.
+ ///
+ ///
+ /// Optional. Specifies the path at which log messages will be written.
+ ///
+ ///
+ /// Optional. Specifies the minimum log message level to write to the log file.
+ ///
+ public static void Initialize(
+ string logFilePath = "SqlToolsService.log",
+ LogLevel minimumLogLevel = LogLevel.Normal)
+ {
+ if (logWriter != null)
+ {
+ logWriter.Dispose();
+ }
+
+ // TODO: Parameterize this
+ logWriter =
+ new LogWriter(
+ minimumLogLevel,
+ logFilePath,
+ true);
+ }
+
+ ///
+ /// Closes the Logger.
+ ///
+ public static void Close()
+ {
+ if (logWriter != null)
+ {
+ logWriter.Dispose();
+ }
+ }
+
+ ///
+ /// Writes a message to the log file.
+ ///
+ /// The level at which the message will be written.
+ /// The message text to be written.
+ /// The name of the calling method.
+ /// The source file path where the calling method exists.
+ /// The line number of the calling method.
+ public static void Write(
+ LogLevel logLevel,
+ string logMessage,
+ [CallerMemberName] string callerName = null,
+ [CallerFilePath] string callerSourceFile = null,
+ [CallerLineNumber] int callerLineNumber = 0)
+ {
+ if (logWriter != null)
+ {
+ logWriter.Write(
+ logLevel,
+ logMessage,
+ callerName,
+ callerSourceFile,
+ callerLineNumber);
+ }
+ }
+ }
+
+ internal class LogWriter : IDisposable
+ {
+ private TextWriter textWriter;
+ private LogLevel minimumLogLevel = LogLevel.Verbose;
+
+ public LogWriter(LogLevel minimumLogLevel, string logFilePath, bool deleteExisting)
+ {
+ this.minimumLogLevel = minimumLogLevel;
+
+ // Ensure that we have a usable log file path
+ if (!Path.IsPathRooted(logFilePath))
+ {
+ logFilePath =
+ Path.Combine(
+ AppContext.BaseDirectory,
+ logFilePath);
+ }
+
+
+ if (!this.TryOpenLogFile(logFilePath, deleteExisting))
+ {
+ // If the log file couldn't be opened at this location,
+ // try opening it in a more reliable path
+ this.TryOpenLogFile(
+ Path.Combine(
+ Environment.GetEnvironmentVariable("TEMP"),
+ Path.GetFileName(logFilePath)),
+ deleteExisting);
+ }
+ }
+
+ public void Write(
+ LogLevel logLevel,
+ string logMessage,
+ string callerName = null,
+ string callerSourceFile = null,
+ int callerLineNumber = 0)
+ {
+ if (this.textWriter != null &&
+ logLevel >= this.minimumLogLevel)
+ {
+ // Print the timestamp and log level
+ this.textWriter.WriteLine(
+ "{0} [{1}] - Method \"{2}\" at line {3} of {4}\r\n",
+ DateTime.Now,
+ logLevel.ToString().ToUpper(),
+ callerName,
+ callerLineNumber,
+ callerSourceFile);
+
+ // Print out indented message lines
+ foreach (var messageLine in logMessage.Split('\n'))
+ {
+ this.textWriter.WriteLine(" " + messageLine.TrimEnd());
+ }
+
+ // Finish with a newline and flush the writer
+ this.textWriter.WriteLine();
+ this.textWriter.Flush();
+ }
+ }
+
+ public void Dispose()
+ {
+ if (this.textWriter != null)
+ {
+ this.textWriter.Flush();
+ this.textWriter.Dispose();
+ this.textWriter = null;
+ }
+ }
+
+ private bool TryOpenLogFile(
+ string logFilePath,
+ bool deleteExisting)
+ {
+ try
+ {
+ // Make sure the log directory exists
+ Directory.CreateDirectory(
+ Path.GetDirectoryName(
+ logFilePath));
+
+ // Open the log file for writing with UTF8 encoding
+ this.textWriter =
+ new StreamWriter(
+ new FileStream(
+ logFilePath,
+ deleteExisting ?
+ FileMode.Create :
+ FileMode.Append),
+ Encoding.UTF8);
+
+ return true;
+ }
+ catch (Exception e)
+ {
+ if (e is UnauthorizedAccessException ||
+ e is IOException)
+ {
+ // This exception is thrown when we can't open the file
+ // at the path in logFilePath. Return false to indicate
+ // that the log file couldn't be created.
+ return false;
+ }
+
+ // Unexpected exception, rethrow it
+ throw;
+ }
+ }
+ }
+}
diff --git a/src/ServiceHost/Utility/ThreadSynchronizationContext.cs b/src/ServiceHost/Utility/ThreadSynchronizationContext.cs
new file mode 100644
index 00000000..2389bbb0
--- /dev/null
+++ b/src/ServiceHost/Utility/ThreadSynchronizationContext.cs
@@ -0,0 +1,77 @@
+//
+// 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.Concurrent;
+using System.Threading;
+
+namespace Microsoft.SqlTools.EditorServices.Utility
+{
+ ///
+ /// Provides a SynchronizationContext implementation that can be used
+ /// in console applications or any thread which doesn't have its
+ /// own SynchronizationContext.
+ ///
+ public class ThreadSynchronizationContext : SynchronizationContext
+ {
+ #region Private Fields
+
+ private BlockingCollection> requestQueue =
+ new BlockingCollection>();
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Posts a request for execution to the SynchronizationContext.
+ /// This will be executed on the SynchronizationContext's thread.
+ ///
+ ///
+ /// The callback to be invoked on the SynchronizationContext's thread.
+ ///
+ ///
+ /// A state object to pass along to the callback when executed through
+ /// the SynchronizationContext.
+ ///
+ public override void Post(SendOrPostCallback callback, object state)
+ {
+ // Add the request to the queue
+ this.requestQueue.Add(
+ new Tuple(
+ callback, state));
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// Starts the SynchronizationContext message loop on the current thread.
+ ///
+ public void RunLoopOnCurrentThread()
+ {
+ Tuple request;
+
+ while (this.requestQueue.TryTake(out request, Timeout.Infinite))
+ {
+ // Invoke the request's callback
+ request.Item1(request.Item2);
+ }
+ }
+
+ ///
+ /// Ends the SynchronizationContext message loop.
+ ///
+ public void EndLoop()
+ {
+ // Tell the blocking queue that we're done
+ this.requestQueue.CompleteAdding();
+ }
+
+ #endregion
+ }
+}
+
diff --git a/src/ServiceHost/Utility/Validate.cs b/src/ServiceHost/Utility/Validate.cs
new file mode 100644
index 00000000..c788e67b
--- /dev/null
+++ b/src/ServiceHost/Utility/Validate.cs
@@ -0,0 +1,143 @@
+//
+// 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;
+
+namespace Microsoft.SqlTools.EditorServices.Utility
+{
+ ///
+ /// Provides common validation methods to simplify method
+ /// parameter checks.
+ ///
+ public static class Validate
+ {
+ ///
+ /// Throws ArgumentNullException if value is null.
+ ///
+ /// The name of the parameter being validated.
+ /// The value of the parameter being validated.
+ public static void IsNotNull(string parameterName, object valueToCheck)
+ {
+ if (valueToCheck == null)
+ {
+ throw new ArgumentNullException(parameterName);
+ }
+ }
+
+ ///
+ /// Throws ArgumentOutOfRangeException if the value is outside
+ /// of the given lower and upper limits.
+ ///
+ /// The name of the parameter being validated.
+ /// The value of the parameter being validated.
+ /// The lower limit which the value should not be less than.
+ /// The upper limit which the value should not be greater than.
+ public static void IsWithinRange(
+ string parameterName,
+ int valueToCheck,
+ int lowerLimit,
+ int upperLimit)
+ {
+ // TODO: Debug assert here if lowerLimit >= upperLimit
+
+ if (valueToCheck < lowerLimit || valueToCheck > upperLimit)
+ {
+ throw new ArgumentOutOfRangeException(
+ parameterName,
+ valueToCheck,
+ string.Format(
+ "Value is not between {0} and {1}",
+ lowerLimit,
+ upperLimit));
+ }
+ }
+
+ ///
+ /// Throws ArgumentOutOfRangeException if the value is greater than or equal
+ /// to the given upper limit.
+ ///
+ /// The name of the parameter being validated.
+ /// The value of the parameter being validated.
+ /// The upper limit which the value should be less than.
+ public static void IsLessThan(
+ string parameterName,
+ int valueToCheck,
+ int upperLimit)
+ {
+ if (valueToCheck >= upperLimit)
+ {
+ throw new ArgumentOutOfRangeException(
+ parameterName,
+ valueToCheck,
+ string.Format(
+ "Value is greater than or equal to {0}",
+ upperLimit));
+ }
+ }
+
+ ///
+ /// Throws ArgumentOutOfRangeException if the value is less than or equal
+ /// to the given lower limit.
+ ///
+ /// The name of the parameter being validated.
+ /// The value of the parameter being validated.
+ /// The lower limit which the value should be greater than.
+ public static void IsGreaterThan(
+ string parameterName,
+ int valueToCheck,
+ int lowerLimit)
+ {
+ if (valueToCheck < lowerLimit)
+ {
+ throw new ArgumentOutOfRangeException(
+ parameterName,
+ valueToCheck,
+ string.Format(
+ "Value is less than or equal to {0}",
+ lowerLimit));
+ }
+ }
+
+ ///
+ /// Throws ArgumentException if the value is equal to the undesired value.
+ ///
+ /// The type of value to be validated.
+ /// The name of the parameter being validated.
+ /// The value that valueToCheck should not equal.
+ /// The value of the parameter being validated.
+ public static void IsNotEqual(
+ string parameterName,
+ TValue valueToCheck,
+ TValue undesiredValue)
+ {
+ if (EqualityComparer.Default.Equals(valueToCheck, undesiredValue))
+ {
+ throw new ArgumentException(
+ string.Format(
+ "The given value '{0}' should not equal '{1}'",
+ valueToCheck,
+ undesiredValue),
+ parameterName);
+ }
+ }
+
+ ///
+ /// Throws ArgumentException if the value is null, an empty string,
+ /// or a string containing only whitespace.
+ ///
+ /// The name of the parameter being validated.
+ /// The value of the parameter being validated.
+ public static void IsNotNullOrEmptyString(string parameterName, string valueToCheck)
+ {
+ if (string.IsNullOrWhiteSpace(valueToCheck))
+ {
+ throw new ArgumentException(
+ "Parameter contains a null, empty, or whitespace string.",
+ parameterName);
+ }
+ }
+ }
+}
diff --git a/src/ServiceHost/Workspace/BufferPosition.cs b/src/ServiceHost/Workspace/BufferPosition.cs
new file mode 100644
index 00000000..8f790d85
--- /dev/null
+++ b/src/ServiceHost/Workspace/BufferPosition.cs
@@ -0,0 +1,110 @@
+//
+// 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;
+
+namespace Microsoft.SqlTools.EditorServices
+{
+ ///
+ /// Provides details about a position in a file buffer. All
+ /// positions are expressed in 1-based positions (i.e. the
+ /// first line and column in the file is position 1,1).
+ ///
+ [DebuggerDisplay("Position = {Line}:{Column}")]
+ public class BufferPosition
+ {
+ #region Properties
+
+ ///
+ /// Provides an instance that represents a position that has not been set.
+ ///
+ public static readonly BufferPosition None = new BufferPosition(-1, -1);
+
+ ///
+ /// Gets the line number of the position in the buffer.
+ ///
+ public int Line { get; private set; }
+
+ ///
+ /// Gets the column number of the position in the buffer.
+ ///
+ public int Column { get; private set; }
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Creates a new instance of the BufferPosition class.
+ ///
+ /// The line number of the position.
+ /// The column number of the position.
+ public BufferPosition(int line, int column)
+ {
+ this.Line = line;
+ this.Column = column;
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// Compares two instances of the BufferPosition class.
+ ///
+ /// The object to which this instance will be compared.
+ /// True if the positions are equal, false otherwise.
+ public override bool Equals(object obj)
+ {
+ if (!(obj is BufferPosition))
+ {
+ return false;
+ }
+
+ BufferPosition other = (BufferPosition)obj;
+
+ return
+ this.Line == other.Line &&
+ this.Column == other.Column;
+ }
+
+ ///
+ /// Calculates a unique hash code that represents this instance.
+ ///
+ /// A hash code representing this instance.
+ public override int GetHashCode()
+ {
+ return this.Line.GetHashCode() ^ this.Column.GetHashCode();
+ }
+
+ ///
+ /// Compares two positions to check if one is greater than the other.
+ ///
+ /// The first position to compare.
+ /// The second position to compare.
+ /// True if positionOne is greater than positionTwo.
+ public static bool operator >(BufferPosition positionOne, BufferPosition positionTwo)
+ {
+ return
+ (positionOne != null && positionTwo == null) ||
+ (positionOne.Line > positionTwo.Line) ||
+ (positionOne.Line == positionTwo.Line &&
+ positionOne.Column > positionTwo.Column);
+ }
+
+ ///
+ /// Compares two positions to check if one is less than the other.
+ ///
+ /// The first position to compare.
+ /// The second position to compare.
+ /// True if positionOne is less than positionTwo.
+ public static bool operator <(BufferPosition positionOne, BufferPosition positionTwo)
+ {
+ return positionTwo > positionOne;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/ServiceHost/Workspace/BufferRange.cs b/src/ServiceHost/Workspace/BufferRange.cs
new file mode 100644
index 00000000..5d20598f
--- /dev/null
+++ b/src/ServiceHost/Workspace/BufferRange.cs
@@ -0,0 +1,123 @@
+//
+// 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.Diagnostics;
+
+namespace Microsoft.SqlTools.EditorServices
+{
+ ///
+ /// Provides details about a range between two positions in
+ /// a file buffer.
+ ///
+ [DebuggerDisplay("Start = {Start.Line}:{Start.Column}, End = {End.Line}:{End.Column}")]
+ public class BufferRange
+ {
+ #region Properties
+
+ ///
+ /// Provides an instance that represents a range that has not been set.
+ ///
+ public static readonly BufferRange None = new BufferRange(0, 0, 0, 0);
+
+ ///
+ /// Gets the start position of the range in the buffer.
+ ///
+ public BufferPosition Start { get; private set; }
+
+ ///
+ /// Gets the end position of the range in the buffer.
+ ///
+ public BufferPosition End { get; private set; }
+
+ ///
+ /// Returns true if the current range is non-zero, i.e.
+ /// contains valid start and end positions.
+ ///
+ public bool HasRange
+ {
+ get
+ {
+ return this.Equals(BufferRange.None);
+ }
+ }
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Creates a new instance of the BufferRange class.
+ ///
+ /// The start position of the range.
+ /// The end position of the range.
+ public BufferRange(BufferPosition start, BufferPosition end)
+ {
+ if (start > end)
+ {
+ throw new ArgumentException(
+ string.Format(
+ "Start position ({0}, {1}) must come before or be equal to the end position ({2}, {3}).",
+ start.Line, start.Column,
+ end.Line, end.Column));
+ }
+
+ this.Start = start;
+ this.End = end;
+ }
+
+ ///
+ /// Creates a new instance of the BufferRange class.
+ ///
+ /// The 1-based starting line number of the range.
+ /// The 1-based starting column number of the range.
+ /// The 1-based ending line number of the range.
+ /// The 1-based ending column number of the range.
+ public BufferRange(
+ int startLine,
+ int startColumn,
+ int endLine,
+ int endColumn)
+ {
+ this.Start = new BufferPosition(startLine, startColumn);
+ this.End = new BufferPosition(endLine, endColumn);
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// Compares two instances of the BufferRange class.
+ ///
+ /// The object to which this instance will be compared.
+ /// True if the ranges are equal, false otherwise.
+ public override bool Equals(object obj)
+ {
+ if (!(obj is BufferRange))
+ {
+ return false;
+ }
+
+ BufferRange other = (BufferRange)obj;
+
+ return
+ this.Start.Equals(other.Start) &&
+ this.End.Equals(other.End);
+ }
+
+ ///
+ /// Calculates a unique hash code that represents this instance.
+ ///
+ /// A hash code representing this instance.
+ public override int GetHashCode()
+ {
+ return this.Start.GetHashCode() ^ this.End.GetHashCode();
+ }
+
+ #endregion
+ }
+}
+
diff --git a/src/ServiceHost/Workspace/FileChange.cs b/src/ServiceHost/Workspace/FileChange.cs
new file mode 100644
index 00000000..2f6efdf8
--- /dev/null
+++ b/src/ServiceHost/Workspace/FileChange.cs
@@ -0,0 +1,38 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+namespace Microsoft.SqlTools.EditorServices
+{
+ ///
+ /// Contains details relating to a content change in an open file.
+ ///
+ public class FileChange
+ {
+ ///
+ /// The string which is to be inserted in the file.
+ ///
+ public string InsertString { get; set; }
+
+ ///
+ /// The 1-based line number where the change starts.
+ ///
+ public int Line { get; set; }
+
+ ///
+ /// The 1-based column offset where the change starts.
+ ///
+ public int Offset { get; set; }
+
+ ///
+ /// The 1-based line number where the change ends.
+ ///
+ public int EndLine { get; set; }
+
+ ///
+ /// The 1-based column offset where the change ends.
+ ///
+ public int EndOffset { get; set; }
+ }
+}
diff --git a/src/ServiceHost/Workspace/FilePosition.cs b/src/ServiceHost/Workspace/FilePosition.cs
new file mode 100644
index 00000000..2cb58745
--- /dev/null
+++ b/src/ServiceHost/Workspace/FilePosition.cs
@@ -0,0 +1,110 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+namespace Microsoft.SqlTools.EditorServices
+{
+ ///
+ /// Provides details and operations for a buffer position in a
+ /// specific file.
+ ///
+ public class FilePosition : BufferPosition
+ {
+ #region Private Fields
+
+ private ScriptFile scriptFile;
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Creates a new FilePosition instance for the 1-based line and
+ /// column numbers in the specified file.
+ ///
+ /// The ScriptFile in which the position is located.
+ /// The 1-based line number in the file.
+ /// The 1-based column number in the file.
+ public FilePosition(
+ ScriptFile scriptFile,
+ int line,
+ int column)
+ : base(line, column)
+ {
+ this.scriptFile = scriptFile;
+ }
+
+ ///
+ /// Creates a new FilePosition instance for the specified file by
+ /// copying the specified BufferPosition
+ ///
+ /// The ScriptFile in which the position is located.
+ /// The original BufferPosition from which the line and column will be copied.
+ public FilePosition(
+ ScriptFile scriptFile,
+ BufferPosition copiedPosition)
+ : this(scriptFile, copiedPosition.Line, copiedPosition.Column)
+ {
+ scriptFile.ValidatePosition(copiedPosition);
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// Gets a FilePosition relative to this position by adding the
+ /// provided line and column offset relative to the contents of
+ /// the current file.
+ ///
+ /// The line offset to add to this position.
+ /// The column offset to add to this position.
+ /// A new FilePosition instance for the calculated position.
+ public FilePosition AddOffset(int lineOffset, int columnOffset)
+ {
+ return this.scriptFile.CalculatePosition(
+ this,
+ lineOffset,
+ columnOffset);
+ }
+
+ ///
+ /// Gets a FilePosition for the line and column position
+ /// of the beginning of the current line after any initial
+ /// whitespace for indentation.
+ ///
+ /// A new FilePosition instance for the calculated position.
+ public FilePosition GetLineStart()
+ {
+ string scriptLine = scriptFile.FileLines[this.Line - 1];
+
+ int lineStartColumn = 1;
+ for (int i = 0; i < scriptLine.Length; i++)
+ {
+ if (!char.IsWhiteSpace(scriptLine[i]))
+ {
+ lineStartColumn = i + 1;
+ break;
+ }
+ }
+
+ return new FilePosition(this.scriptFile, this.Line, lineStartColumn);
+ }
+
+ ///
+ /// Gets a FilePosition for the line and column position
+ /// of the end of the current line.
+ ///
+ /// A new FilePosition instance for the calculated position.
+ public FilePosition GetLineEnd()
+ {
+ string scriptLine = scriptFile.FileLines[this.Line - 1];
+ return new FilePosition(this.scriptFile, this.Line, scriptLine.Length + 1);
+ }
+
+ #endregion
+
+ }
+}
+
diff --git a/src/ServiceHost/Workspace/ScriptFile.cs b/src/ServiceHost/Workspace/ScriptFile.cs
new file mode 100644
index 00000000..90d66244
--- /dev/null
+++ b/src/ServiceHost/Workspace/ScriptFile.cs
@@ -0,0 +1,538 @@
+//
+// 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.EditorServices.Utility;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Microsoft.SqlTools.EditorServices
+{
+ ///
+ /// Contains the details and contents of an open script file.
+ ///
+ public class ScriptFile
+ {
+ #region Private Fields
+
+ private Version SqlToolsVersion;
+
+ #endregion
+
+ #region Properties
+
+ ///
+ /// Gets a unique string that identifies this file. At this time,
+ /// this property returns a normalized version of the value stored
+ /// in the FilePath property.
+ ///
+ public string Id
+ {
+ get { return this.FilePath.ToLower(); }
+ }
+
+ ///
+ /// Gets the path at which this file resides.
+ ///
+ public string FilePath { get; private set; }
+
+ ///
+ /// Gets the path which the editor client uses to identify this file.
+ ///
+ public string ClientFilePath { get; private set; }
+
+ ///
+ /// Gets or sets a boolean that determines whether
+ /// semantic analysis should be enabled for this file.
+ /// For internal use only.
+ ///
+ internal bool IsAnalysisEnabled { get; set; }
+
+ ///
+ /// Gets a boolean that determines whether this file is
+ /// in-memory or not (either unsaved or non-file content).
+ ///
+ public bool IsInMemory { get; private set; }
+
+ ///
+ /// Gets a string containing the full contents of the file.
+ ///
+ public string Contents
+ {
+ get
+ {
+ return string.Join("\r\n", this.FileLines);
+ }
+ }
+
+ ///
+ /// Gets a BufferRange that represents the entire content
+ /// range of the file.
+ ///
+ public BufferRange FileRange { get; private set; }
+
+ ///
+ /// Gets the list of syntax markers found by parsing this
+ /// file's contents.
+ ///
+ public ScriptFileMarker[] SyntaxMarkers
+ {
+ get;
+ private set;
+ }
+
+ ///
+ /// Gets the list of strings for each line of the file.
+ ///
+ internal IList FileLines
+ {
+ get;
+ private set;
+ }
+
+ ///
+ /// Gets the array of filepaths dot sourced in this ScriptFile
+ ///
+ public string[] ReferencedFiles
+ {
+ get;
+ private set;
+ }
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Creates a new ScriptFile instance by reading file contents from
+ /// the given TextReader.
+ ///
+ /// The path at which the script file resides.
+ /// The path which the client uses to identify the file.
+ /// The TextReader to use for reading the file's contents.
+ /// The version of SqlTools for which the script is being parsed.
+ public ScriptFile(
+ string filePath,
+ string clientFilePath,
+ TextReader textReader,
+ Version SqlToolsVersion)
+ {
+ this.FilePath = filePath;
+ this.ClientFilePath = clientFilePath;
+ this.IsAnalysisEnabled = true;
+ this.IsInMemory = Workspace.IsPathInMemory(filePath);
+ this.SqlToolsVersion = SqlToolsVersion;
+
+ this.SetFileContents(textReader.ReadToEnd());
+ }
+
+ ///
+ /// Creates a new ScriptFile instance with the specified file contents.
+ ///
+ /// The path at which the script file resides.
+ /// The path which the client uses to identify the file.
+ /// The initial contents of the script file.
+ /// The version of SqlTools for which the script is being parsed.
+ public ScriptFile(
+ string filePath,
+ string clientFilePath,
+ string initialBuffer,
+ Version SqlToolsVersion)
+ {
+ this.FilePath = filePath;
+ this.ClientFilePath = clientFilePath;
+ this.IsAnalysisEnabled = true;
+ this.SqlToolsVersion = SqlToolsVersion;
+
+ this.SetFileContents(initialBuffer);
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// Gets a line from the file's contents.
+ ///
+ /// The 1-based line number in the file.
+ /// The complete line at the given line number.
+ public string GetLine(int lineNumber)
+ {
+ Validate.IsWithinRange(
+ "lineNumber", lineNumber,
+ 1, this.FileLines.Count + 1);
+
+ return this.FileLines[lineNumber - 1];
+ }
+
+ ///
+ /// Gets a range of lines from the file's contents.
+ ///
+ /// The buffer range from which lines will be extracted.
+ /// An array of strings from the specified range of the file.
+ public string[] GetLinesInRange(BufferRange bufferRange)
+ {
+ this.ValidatePosition(bufferRange.Start);
+ this.ValidatePosition(bufferRange.End);
+
+ List linesInRange = new List();
+
+ int startLine = bufferRange.Start.Line,
+ endLine = bufferRange.End.Line;
+
+ for (int line = startLine; line <= endLine; line++)
+ {
+ string currentLine = this.FileLines[line - 1];
+ int startColumn =
+ line == startLine
+ ? bufferRange.Start.Column
+ : 1;
+ int endColumn =
+ line == endLine
+ ? bufferRange.End.Column
+ : currentLine.Length + 1;
+
+ currentLine =
+ currentLine.Substring(
+ startColumn - 1,
+ endColumn - startColumn);
+
+ linesInRange.Add(currentLine);
+ }
+
+ return linesInRange.ToArray();
+ }
+
+ ///
+ /// Throws ArgumentOutOfRangeException if the given position is outside
+ /// of the file's buffer extents.
+ ///
+ /// The position in the buffer to be validated.
+ public void ValidatePosition(BufferPosition bufferPosition)
+ {
+ this.ValidatePosition(
+ bufferPosition.Line,
+ bufferPosition.Column);
+ }
+
+ ///
+ /// Throws ArgumentOutOfRangeException if the given position is outside
+ /// of the file's buffer extents.
+ ///
+ /// The 1-based line to be validated.
+ /// The 1-based column to be validated.
+ public void ValidatePosition(int line, int column)
+ {
+ if (line < 1 || line > this.FileLines.Count + 1)
+ {
+ throw new ArgumentOutOfRangeException("Position is outside of file line range.");
+ }
+
+ // The maximum column is either one past the length of the string
+ // or 1 if the string is empty.
+ string lineString = this.FileLines[line - 1];
+ int maxColumn = lineString.Length > 0 ? lineString.Length + 1 : 1;
+
+ if (column < 1 || column > maxColumn)
+ {
+ throw new ArgumentOutOfRangeException(
+ string.Format(
+ "Position is outside of column range for line {0}.",
+ line));
+ }
+ }
+
+ ///
+ /// Applies the provided FileChange to the file's contents
+ ///
+ /// The FileChange to apply to the file's contents.
+ public void ApplyChange(FileChange fileChange)
+ {
+ this.ValidatePosition(fileChange.Line, fileChange.Offset);
+ this.ValidatePosition(fileChange.EndLine, fileChange.EndOffset);
+
+ // Break up the change lines
+ string[] changeLines = fileChange.InsertString.Split('\n');
+
+ // Get the first fragment of the first line
+ string firstLineFragment =
+ this.FileLines[fileChange.Line - 1]
+ .Substring(0, fileChange.Offset - 1);
+
+ // Get the last fragment of the last line
+ string endLine = this.FileLines[fileChange.EndLine - 1];
+ string lastLineFragment =
+ endLine.Substring(
+ fileChange.EndOffset - 1,
+ (this.FileLines[fileChange.EndLine - 1].Length - fileChange.EndOffset) + 1);
+
+ // Remove the old lines
+ for (int i = 0; i <= fileChange.EndLine - fileChange.Line; i++)
+ {
+ this.FileLines.RemoveAt(fileChange.Line - 1);
+ }
+
+ // Build and insert the new lines
+ int currentLineNumber = fileChange.Line;
+ for (int changeIndex = 0; changeIndex < changeLines.Length; changeIndex++)
+ {
+ // Since we split the lines above using \n, make sure to
+ // trim the ending \r's off as well.
+ string finalLine = changeLines[changeIndex].TrimEnd('\r');
+
+ // Should we add first or last line fragments?
+ if (changeIndex == 0)
+ {
+ // Append the first line fragment
+ finalLine = firstLineFragment + finalLine;
+ }
+ if (changeIndex == changeLines.Length - 1)
+ {
+ // Append the last line fragment
+ finalLine = finalLine + lastLineFragment;
+ }
+
+ this.FileLines.Insert(currentLineNumber - 1, finalLine);
+ currentLineNumber++;
+ }
+
+ // Parse the script again to be up-to-date
+ this.ParseFileContents();
+ }
+
+ ///
+ /// Calculates the zero-based character offset of a given
+ /// line and column position in the file.
+ ///
+ /// The 1-based line number from which the offset is calculated.
+ /// The 1-based column number from which the offset is calculated.
+ /// The zero-based offset for the given file position.
+ public int GetOffsetAtPosition(int lineNumber, int columnNumber)
+ {
+ Validate.IsWithinRange("lineNumber", lineNumber, 1, this.FileLines.Count);
+ Validate.IsGreaterThan("columnNumber", columnNumber, 0);
+
+ int offset = 0;
+
+ for(int i = 0; i < lineNumber; i++)
+ {
+ if (i == lineNumber - 1)
+ {
+ // Subtract 1 to account for 1-based column numbering
+ offset += columnNumber - 1;
+ }
+ else
+ {
+ // Add an offset to account for the current platform's newline characters
+ offset += this.FileLines[i].Length + Environment.NewLine.Length;
+ }
+ }
+
+ return offset;
+ }
+
+ ///
+ /// Calculates a FilePosition relative to a starting BufferPosition
+ /// using the given 1-based line and column offset.
+ ///
+ /// The original BufferPosition from which an new position should be calculated.
+ /// The 1-based line offset added to the original position in this file.
+ /// The 1-based column offset added to the original position in this file.
+ /// A new FilePosition instance with the resulting line and column number.
+ public FilePosition CalculatePosition(
+ BufferPosition originalPosition,
+ int lineOffset,
+ int columnOffset)
+ {
+ int newLine = originalPosition.Line + lineOffset,
+ newColumn = originalPosition.Column + columnOffset;
+
+ this.ValidatePosition(newLine, newColumn);
+
+ string scriptLine = this.FileLines[newLine - 1];
+ newColumn = Math.Min(scriptLine.Length + 1, newColumn);
+
+ return new FilePosition(this, newLine, newColumn);
+ }
+
+ ///
+ /// Calculates the 1-based line and column number position based
+ /// on the given buffer offset.
+ ///
+ /// The buffer offset to convert.
+ /// A new BufferPosition containing the position of the offset.
+ public BufferPosition GetPositionAtOffset(int bufferOffset)
+ {
+ BufferRange bufferRange =
+ GetRangeBetweenOffsets(
+ bufferOffset, bufferOffset);
+
+ return bufferRange.Start;
+ }
+
+ ///
+ /// Calculates the 1-based line and column number range based on
+ /// the given start and end buffer offsets.
+ ///
+ /// The start offset of the range.
+ /// The end offset of the range.
+ /// A new BufferRange containing the positions in the offset range.
+ public BufferRange GetRangeBetweenOffsets(int startOffset, int endOffset)
+ {
+ bool foundStart = false;
+ int currentOffset = 0;
+ int searchedOffset = startOffset;
+
+ BufferPosition startPosition = new BufferPosition(0, 0);
+ BufferPosition endPosition = startPosition;
+
+ int line = 0;
+ while (line < this.FileLines.Count)
+ {
+ if (searchedOffset <= currentOffset + this.FileLines[line].Length)
+ {
+ int column = searchedOffset - currentOffset;
+
+ // Have we already found the start position?
+ if (foundStart)
+ {
+ // Assign the end position and end the search
+ endPosition = new BufferPosition(line + 1, column + 1);
+ break;
+ }
+ else
+ {
+ startPosition = new BufferPosition(line + 1, column + 1);
+
+ // Do we only need to find the start position?
+ if (startOffset == endOffset)
+ {
+ endPosition = startPosition;
+ break;
+ }
+ else
+ {
+ // Since the end offset can be on the same line,
+ // skip the line increment and continue searching
+ // for the end position
+ foundStart = true;
+ searchedOffset = endOffset;
+ continue;
+ }
+ }
+ }
+
+ // Increase the current offset and include newline length
+ currentOffset += this.FileLines[line].Length + Environment.NewLine.Length;
+ line++;
+ }
+
+ return new BufferRange(startPosition, endPosition);
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ private void SetFileContents(string fileContents)
+ {
+ // Split the file contents into lines and trim
+ // any carriage returns from the strings.
+ this.FileLines =
+ fileContents
+ .Split('\n')
+ .Select(line => line.TrimEnd('\r'))
+ .ToList();
+
+ // Parse the contents to get syntax tree and errors
+ this.ParseFileContents();
+ }
+
+ ///
+ /// Parses the current file contents to get the AST, tokens,
+ /// and parse errors.
+ ///
+ private void ParseFileContents()
+ {
+#if false
+ ParseError[] parseErrors = null;
+
+ // First, get the updated file range
+ int lineCount = this.FileLines.Count;
+ if (lineCount > 0)
+ {
+ this.FileRange =
+ new BufferRange(
+ new BufferPosition(1, 1),
+ new BufferPosition(
+ lineCount + 1,
+ this.FileLines[lineCount - 1].Length + 1));
+ }
+ else
+ {
+ this.FileRange = BufferRange.None;
+ }
+
+ try
+ {
+#if SqlToolsv5r2
+ // This overload appeared with Windows 10 Update 1
+ if (this.SqlToolsVersion.Major >= 5 &&
+ this.SqlToolsVersion.Build >= 10586)
+ {
+ // Include the file path so that module relative
+ // paths are evaluated correctly
+ this.ScriptAst =
+ Parser.ParseInput(
+ this.Contents,
+ this.FilePath,
+ out this.scriptTokens,
+ out parseErrors);
+ }
+ else
+ {
+ this.ScriptAst =
+ Parser.ParseInput(
+ this.Contents,
+ out this.scriptTokens,
+ out parseErrors);
+ }
+#else
+ this.ScriptAst =
+ Parser.ParseInput(
+ this.Contents,
+ out this.scriptTokens,
+ out parseErrors);
+#endif
+ }
+ catch (RuntimeException ex)
+ {
+ var parseError =
+ new ParseError(
+ null,
+ ex.ErrorRecord.FullyQualifiedErrorId,
+ ex.Message);
+
+ parseErrors = new[] { parseError };
+ this.scriptTokens = new Token[0];
+ this.ScriptAst = null;
+ }
+
+ // Translate parse errors into syntax markers
+ this.SyntaxMarkers =
+ parseErrors
+ .Select(ScriptFileMarker.FromParseError)
+ .ToArray();
+
+ //Get all dot sourced referenced files and store them
+ this.ReferencedFiles =
+ AstOperations.FindDotSourcedIncludes(this.ScriptAst);
+#endif
+ }
+
+#endregion
+ }
+}
diff --git a/src/ServiceHost/Workspace/ScriptFileMarker.cs b/src/ServiceHost/Workspace/ScriptFileMarker.cs
new file mode 100644
index 00000000..87c2576c
--- /dev/null
+++ b/src/ServiceHost/Workspace/ScriptFileMarker.cs
@@ -0,0 +1,56 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+namespace Microsoft.SqlTools.EditorServices
+{
+ ///
+ /// Defines the message level of a script file marker.
+ ///
+ public enum ScriptFileMarkerLevel
+ {
+ ///
+ /// The marker represents an informational message.
+ ///
+ Information = 0,
+
+ ///
+ /// The marker represents a warning message.
+ ///
+ Warning,
+
+ ///
+ /// The marker represents an error message.
+ ///
+ Error
+ };
+
+ ///
+ /// Contains details about a marker that should be displayed
+ /// for the a script file. The marker information could come
+ /// from syntax parsing or semantic analysis of the script.
+ ///
+ public class ScriptFileMarker
+ {
+ #region Properties
+
+ ///
+ /// Gets or sets the marker's message string.
+ ///
+ public string Message { get; set; }
+
+ ///
+ /// Gets or sets the marker's message level.
+ ///
+ public ScriptFileMarkerLevel Level { get; set; }
+
+ ///
+ /// Gets or sets the ScriptRegion where the marker should appear.
+ ///
+ public ScriptRegion ScriptRegion { get; set; }
+
+ #endregion
+ }
+}
+
diff --git a/src/ServiceHost/Workspace/ScriptRegion.cs b/src/ServiceHost/Workspace/ScriptRegion.cs
new file mode 100644
index 00000000..f2fa4ac8
--- /dev/null
+++ b/src/ServiceHost/Workspace/ScriptRegion.cs
@@ -0,0 +1,89 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+//using System.Management.Automation.Language;
+
+namespace Microsoft.SqlTools.EditorServices
+{
+ ///
+ /// Contains details about a specific region of text in script file.
+ ///
+ public sealed class ScriptRegion
+ {
+ #region Properties
+
+ ///
+ /// Gets the file path of the script file in which this region is contained.
+ ///
+ public string File { get; set; }
+
+ ///
+ /// Gets or sets the text that is contained within the region.
+ ///
+ public string Text { get; set; }
+
+ ///
+ /// Gets or sets the starting line number of the region.
+ ///
+ public int StartLineNumber { get; set; }
+
+ ///
+ /// Gets or sets the starting column number of the region.
+ ///
+ public int StartColumnNumber { get; set; }
+
+ ///
+ /// Gets or sets the starting file offset of the region.
+ ///
+ public int StartOffset { get; set; }
+
+ ///
+ /// Gets or sets the ending line number of the region.
+ ///
+ public int EndLineNumber { get; set; }
+
+ ///
+ /// Gets or sets the ending column number of the region.
+ ///
+ public int EndColumnNumber { get; set; }
+
+ ///
+ /// Gets or sets the ending file offset of the region.
+ ///
+ public int EndOffset { get; set; }
+
+ #endregion
+
+ #region Constructors
+
+#if false
+ ///
+ /// Creates a new instance of the ScriptRegion class from an
+ /// instance of an IScriptExtent implementation.
+ ///
+ ///
+ /// The IScriptExtent to copy into the ScriptRegion.
+ ///
+ ///
+ /// A new ScriptRegion instance with the same details as the IScriptExtent.
+ ///
+ public static ScriptRegion Create(IScriptExtent scriptExtent)
+ {
+ return new ScriptRegion
+ {
+ File = scriptExtent.File,
+ Text = scriptExtent.Text,
+ StartLineNumber = scriptExtent.StartLineNumber,
+ StartColumnNumber = scriptExtent.StartColumnNumber,
+ StartOffset = scriptExtent.StartOffset,
+ EndLineNumber = scriptExtent.EndLineNumber,
+ EndColumnNumber = scriptExtent.EndColumnNumber,
+ EndOffset = scriptExtent.EndOffset
+ };
+ }
+#endif
+ #endregion
+ }
+}
diff --git a/src/ServiceHost/Workspace/Workspace.cs b/src/ServiceHost/Workspace/Workspace.cs
new file mode 100644
index 00000000..39e1d70f
--- /dev/null
+++ b/src/ServiceHost/Workspace/Workspace.cs
@@ -0,0 +1,248 @@
+//
+// 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.EditorServices.Utility;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Linq;
+
+namespace Microsoft.SqlTools.EditorServices
+{
+ ///
+ /// Manages a "workspace" of script files that are open for a particular
+ /// editing session. Also helps to navigate references between ScriptFiles.
+ ///
+ public class Workspace
+ {
+ #region Private Fields
+
+ private Version SqlToolsVersion;
+ private Dictionary workspaceFiles = new Dictionary();
+
+ #endregion
+
+ #region Properties
+
+ ///
+ /// Gets or sets the root path of the workspace.
+ ///
+ public string WorkspacePath { get; set; }
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Creates a new instance of the Workspace class.
+ ///
+ /// The version of SqlTools for which scripts will be parsed.
+ public Workspace(Version SqlToolsVersion)
+ {
+ this.SqlToolsVersion = SqlToolsVersion;
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// Gets an open file in the workspace. If the file isn't open but
+ /// exists on the filesystem, load and return it.
+ ///
+ /// The file path at which the script resides.
+ ///
+ /// is not found.
+ ///
+ ///
+ /// contains a null or empty string.
+ ///
+ public ScriptFile GetFile(string filePath)
+ {
+ Validate.IsNotNullOrEmptyString("filePath", filePath);
+
+ // Resolve the full file path
+ string resolvedFilePath = this.ResolveFilePath(filePath);
+ string keyName = resolvedFilePath.ToLower();
+
+ // Make sure the file isn't already loaded into the workspace
+ ScriptFile scriptFile = null;
+ if (!this.workspaceFiles.TryGetValue(keyName, out scriptFile))
+ {
+ // This method allows FileNotFoundException to bubble up
+ // if the file isn't found.
+ 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);
+
+ this.workspaceFiles.Add(keyName, scriptFile);
+ }
+
+ Logger.Write(LogLevel.Verbose, "Opened file on disk: " + resolvedFilePath);
+ }
+
+ return scriptFile;
+ }
+
+ private string ResolveFilePath(string filePath)
+ {
+ if (!IsPathInMemory(filePath))
+ {
+ if (filePath.StartsWith(@"file://"))
+ {
+ // Client sent the path in URI format, extract the local path and trim
+ // any extraneous slashes
+ Uri fileUri = new Uri(filePath);
+ filePath = fileUri.LocalPath.TrimStart('/');
+ }
+
+ // Some clients send paths with UNIX-style slashes, replace those if necessary
+ filePath = filePath.Replace('/', '\\');
+
+ // Clients could specify paths with escaped space, [ and ] characters which .NET APIs
+ // will not handle. These paths will get appropriately escaped just before being passed
+ // into the SqlTools engine.
+ filePath = UnescapePath(filePath);
+
+ // Get the absolute file path
+ filePath = Path.GetFullPath(filePath);
+ }
+
+ Logger.Write(LogLevel.Verbose, "Resolved path: " + filePath);
+
+ return filePath;
+ }
+
+ internal static bool IsPathInMemory(string filePath)
+ {
+ // When viewing SqlTools files in the Git diff viewer, VS Code
+ // sends the contents of the file at HEAD with a URI that starts
+ // with 'inmemory'. Untitled files which have been marked of
+ // type SqlTools have a path starting with 'untitled'.
+ return
+ filePath.StartsWith("inmemory") ||
+ filePath.StartsWith("untitled");
+ }
+
+ ///
+ /// Unescapes any escaped [, ] or space characters. Typically use this before calling a
+ /// .NET API that doesn't understand PowerShell escaped chars.
+ ///
+ /// The path to unescape.
+ /// The path with the ` character before [, ] and spaces removed.
+ public static string UnescapePath(string path)
+ {
+ if (!path.Contains("`"))
+ {
+ return path;
+ }
+
+ return Regex.Replace(path, @"`(?=[ \[\]])", "");
+ }
+
+ ///
+ /// Gets a new ScriptFile instance which is identified by the given file
+ /// path and initially contains the given buffer contents.
+ ///
+ ///
+ ///
+ ///
+ public ScriptFile GetFileBuffer(string filePath, string initialBuffer)
+ {
+ Validate.IsNotNullOrEmptyString("filePath", filePath);
+
+ // Resolve the full file path
+ string resolvedFilePath = this.ResolveFilePath(filePath);
+ string keyName = resolvedFilePath.ToLower();
+
+ // Make sure the file isn't already loaded into the workspace
+ ScriptFile scriptFile = null;
+ if (!this.workspaceFiles.TryGetValue(keyName, out scriptFile))
+ {
+ scriptFile =
+ new ScriptFile(
+ resolvedFilePath,
+ filePath,
+ initialBuffer,
+ this.SqlToolsVersion);
+
+ this.workspaceFiles.Add(keyName, scriptFile);
+
+ Logger.Write(LogLevel.Verbose, "Opened file as in-memory buffer: " + resolvedFilePath);
+ }
+
+ return scriptFile;
+ }
+
+ ///
+ /// Gets an array of all opened ScriptFiles in the workspace.
+ ///
+ /// An array of all opened ScriptFiles in the workspace.
+ public ScriptFile[] GetOpenedFiles()
+ {
+ return workspaceFiles.Values.ToArray();
+ }
+
+ ///
+ /// Closes a currently open script file with the given file path.
+ ///
+ /// The file path at which the script resides.
+ public void CloseFile(ScriptFile scriptFile)
+ {
+ Validate.IsNotNull("scriptFile", scriptFile);
+
+ this.workspaceFiles.Remove(scriptFile.Id);
+ }
+
+ private string GetBaseFilePath(string filePath)
+ {
+ if (IsPathInMemory(filePath))
+ {
+ // If the file is in memory, use the workspace path
+ return this.WorkspacePath;
+ }
+
+ if (!Path.IsPathRooted(filePath))
+ {
+ // TODO: Assert instead?
+ throw new InvalidOperationException(
+ string.Format(
+ "Must provide a full path for originalScriptPath: {0}",
+ filePath));
+ }
+
+ // Get the directory of the file path
+ return Path.GetDirectoryName(filePath);
+ }
+
+ private string ResolveRelativeScriptPath(string baseFilePath, string relativePath)
+ {
+ if (Path.IsPathRooted(relativePath))
+ {
+ return relativePath;
+ }
+
+ // Get the directory of the original script file, combine it
+ // with the given path and then resolve the absolute file path.
+ string combinedPath =
+ Path.GetFullPath(
+ Path.Combine(
+ baseFilePath,
+ relativePath));
+
+ return combinedPath;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/ServiceHost/project.json b/src/ServiceHost/project.json
new file mode 100644
index 00000000..11340892
--- /dev/null
+++ b/src/ServiceHost/project.json
@@ -0,0 +1,21 @@
+{
+ "version": "1.0.0-*",
+ "buildOptions": {
+ "debugType": "portable",
+ "emitEntryPoint": true
+ },
+ "dependencies": {
+ "Newtonsoft.Json": "9.0.1"
+ },
+ "frameworks": {
+ "netcoreapp1.0": {
+ "dependencies": {
+ "Microsoft.NETCore.App": {
+ "type": "platform",
+ "version": "1.0.0"
+ }
+ },
+ "imports": "dnxcore50"
+ }
+ }
+}
diff --git a/test/ServiceHost.Test/App.config b/test/ServiceHost.Test/App.config
new file mode 100644
index 00000000..570b96df
--- /dev/null
+++ b/test/ServiceHost.Test/App.config
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/ServiceHost.Test/LanguageServer/JsonRpcMessageSerializerTests.cs b/test/ServiceHost.Test/LanguageServer/JsonRpcMessageSerializerTests.cs
new file mode 100644
index 00000000..9ec341c5
--- /dev/null
+++ b/test/ServiceHost.Test/LanguageServer/JsonRpcMessageSerializerTests.cs
@@ -0,0 +1,144 @@
+//
+// 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.EditorServices.Protocol.MessageProtocol;
+using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Serializers;
+using Newtonsoft.Json.Linq;
+using Xunit;
+
+namespace Microsoft.SqlTools.EditorServices.Test.Protocol.LanguageServer
+{
+ public class TestMessageContents
+ {
+ public const string SomeFieldValue = "Some value";
+ public const int NumberValue = 42;
+
+ public string SomeField { get; set; }
+
+ public int Number { get; set; }
+
+ public TestMessageContents()
+ {
+ this.SomeField = SomeFieldValue;
+ this.Number = NumberValue;
+ }
+ }
+
+ public class JsonRpcMessageSerializerTests
+ {
+ private IMessageSerializer messageSerializer;
+
+ private const string MessageId = "42";
+ private const string MethodName = "testMethod";
+ private static readonly JToken MessageContent = JToken.FromObject(new TestMessageContents());
+
+ public JsonRpcMessageSerializerTests()
+ {
+ this.messageSerializer = new JsonRpcMessageSerializer();
+ }
+
+ [Fact]
+ public void SerializesRequestMessages()
+ {
+ var messageObj =
+ this.messageSerializer.SerializeMessage(
+ Message.Request(
+ MessageId,
+ MethodName,
+ MessageContent));
+
+ AssertMessageFields(
+ messageObj,
+ checkId: true,
+ checkMethod: true,
+ checkParams: true);
+ }
+
+ [Fact]
+ public void SerializesEventMessages()
+ {
+ var messageObj =
+ this.messageSerializer.SerializeMessage(
+ Message.Event(
+ MethodName,
+ MessageContent));
+
+ AssertMessageFields(
+ messageObj,
+ checkMethod: true,
+ checkParams: true);
+ }
+
+ [Fact]
+ public void SerializesResponseMessages()
+ {
+ var messageObj =
+ this.messageSerializer.SerializeMessage(
+ Message.Response(
+ MessageId,
+ null,
+ MessageContent));
+
+ AssertMessageFields(
+ messageObj,
+ checkId: true,
+ checkResult: true);
+ }
+
+ [Fact]
+ public void SerializesResponseWithErrorMessages()
+ {
+ var messageObj =
+ this.messageSerializer.SerializeMessage(
+ Message.ResponseError(
+ MessageId,
+ null,
+ MessageContent));
+
+ AssertMessageFields(
+ messageObj,
+ checkId: true,
+ checkError: true);
+ }
+
+ private static void AssertMessageFields(
+ JObject messageObj,
+ bool checkId = false,
+ bool checkMethod = false,
+ bool checkParams = false,
+ bool checkResult = false,
+ bool checkError = false)
+ {
+ JToken token = null;
+
+ Assert.True(messageObj.TryGetValue("jsonrpc", out token));
+ Assert.Equal("2.0", token.ToString());
+
+ if (checkId)
+ {
+ Assert.True(messageObj.TryGetValue("id", out token));
+ Assert.Equal(MessageId, token.ToString());
+ }
+
+ if (checkMethod)
+ {
+ Assert.True(messageObj.TryGetValue("method", out token));
+ Assert.Equal(MethodName, token.ToString());
+ }
+
+ if (checkError)
+ {
+ // TODO
+ }
+ else
+ {
+ string contentField = checkParams ? "params" : "result";
+ Assert.True(messageObj.TryGetValue(contentField, out token));
+ Assert.True(JToken.DeepEquals(token, MessageContent));
+ }
+ }
+ }
+}
+
diff --git a/test/ServiceHost.Test/Message/MessageReaderWriterTests.cs b/test/ServiceHost.Test/Message/MessageReaderWriterTests.cs
new file mode 100644
index 00000000..82e619f5
--- /dev/null
+++ b/test/ServiceHost.Test/Message/MessageReaderWriterTests.cs
@@ -0,0 +1,177 @@
+//
+// 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.EditorServices.Protocol.MessageProtocol;
+using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Serializers;
+using System;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.SqlTools.EditorServices.Test.Protocol.MessageProtocol
+{
+ public class MessageReaderWriterTests
+ {
+ const string TestEventString = "{\"type\":\"event\",\"event\":\"testEvent\",\"body\":null}";
+ const string TestEventFormatString = "{{\"event\":\"testEvent\",\"body\":{{\"someString\":\"{0}\"}},\"seq\":0,\"type\":\"event\"}}";
+ readonly int ExpectedMessageByteCount = Encoding.UTF8.GetByteCount(TestEventString);
+
+ private IMessageSerializer messageSerializer;
+
+ public MessageReaderWriterTests()
+ {
+ this.messageSerializer = new V8MessageSerializer();
+ }
+
+ [Fact]
+ public async Task WritesMessage()
+ {
+ MemoryStream outputStream = new MemoryStream();
+
+ MessageWriter messageWriter =
+ new MessageWriter(
+ outputStream,
+ this.messageSerializer);
+
+ // Write the message and then roll back the stream to be read
+ // TODO: This will need to be redone!
+ await messageWriter.WriteMessage(Message.Event("testEvent", null));
+ outputStream.Seek(0, SeekOrigin.Begin);
+
+ string expectedHeaderString =
+ string.Format(
+ Constants.ContentLengthFormatString,
+ ExpectedMessageByteCount);
+
+ byte[] buffer = new byte[128];
+ await outputStream.ReadAsync(buffer, 0, expectedHeaderString.Length);
+
+ Assert.Equal(
+ expectedHeaderString,
+ Encoding.ASCII.GetString(buffer, 0, expectedHeaderString.Length));
+
+ // Read the message
+ await outputStream.ReadAsync(buffer, 0, ExpectedMessageByteCount);
+
+ Assert.Equal(
+ TestEventString,
+ Encoding.UTF8.GetString(buffer, 0, ExpectedMessageByteCount));
+
+ outputStream.Dispose();
+ }
+
+ [Fact]
+ public void ReadsMessage()
+ {
+ MemoryStream inputStream = new MemoryStream();
+ MessageReader messageReader =
+ new MessageReader(
+ inputStream,
+ this.messageSerializer);
+
+ // Write a message to the stream
+ byte[] messageBuffer = this.GetMessageBytes(TestEventString);
+ inputStream.Write(
+ this.GetMessageBytes(TestEventString),
+ 0,
+ messageBuffer.Length);
+
+ inputStream.Flush();
+ inputStream.Seek(0, SeekOrigin.Begin);
+
+ Message messageResult = messageReader.ReadMessage().Result;
+ Assert.Equal("testEvent", messageResult.Method);
+
+ inputStream.Dispose();
+ }
+
+ [Fact]
+ public void ReadsManyBufferedMessages()
+ {
+ MemoryStream inputStream = new MemoryStream();
+ MessageReader messageReader =
+ new MessageReader(
+ inputStream,
+ this.messageSerializer);
+
+ // Get a message to use for writing to the stream
+ byte[] messageBuffer = this.GetMessageBytes(TestEventString);
+
+ // How many messages of this size should we write to overflow the buffer?
+ int overflowMessageCount =
+ (int)Math.Ceiling(
+ (MessageReader.DefaultBufferSize * 1.5) / messageBuffer.Length);
+
+ // Write the necessary number of messages to the stream
+ for (int i = 0; i < overflowMessageCount; i++)
+ {
+ inputStream.Write(messageBuffer, 0, messageBuffer.Length);
+ }
+
+ inputStream.Flush();
+ inputStream.Seek(0, SeekOrigin.Begin);
+
+ // Read the written messages from the stream
+ for (int i = 0; i < overflowMessageCount; i++)
+ {
+ Message messageResult = messageReader.ReadMessage().Result;
+ Assert.Equal("testEvent", messageResult.Method);
+ }
+
+ inputStream.Dispose();
+ }
+
+ [Fact]
+ public void ReaderResizesBufferForLargeMessages()
+ {
+ MemoryStream inputStream = new MemoryStream();
+ MessageReader messageReader =
+ new MessageReader(
+ inputStream,
+ this.messageSerializer);
+
+ // Get a message with content so large that the buffer will need
+ // to be resized to fit it all.
+ byte[] messageBuffer =
+ this.GetMessageBytes(
+ string.Format(
+ TestEventFormatString,
+ new String('X', (int)(MessageReader.DefaultBufferSize * 3))));
+
+ inputStream.Write(messageBuffer, 0, messageBuffer.Length);
+ inputStream.Flush();
+ inputStream.Seek(0, SeekOrigin.Begin);
+
+ Message messageResult = messageReader.ReadMessage().Result;
+ Assert.Equal("testEvent", messageResult.Method);
+
+ inputStream.Dispose();
+ }
+
+ private byte[] GetMessageBytes(string messageString, Encoding encoding = null)
+ {
+ if (encoding == null)
+ {
+ encoding = Encoding.UTF8;
+ }
+
+ byte[] messageBytes = Encoding.UTF8.GetBytes(messageString);
+ byte[] headerBytes =
+ Encoding.ASCII.GetBytes(
+ string.Format(
+ Constants.ContentLengthFormatString,
+ messageBytes.Length));
+
+ // Copy the bytes into a single buffer
+ byte[] finalBytes = new byte[headerBytes.Length + messageBytes.Length];
+ Buffer.BlockCopy(headerBytes, 0, finalBytes, 0, headerBytes.Length);
+ Buffer.BlockCopy(messageBytes, 0, finalBytes, headerBytes.Length, messageBytes.Length);
+
+ return finalBytes;
+ }
+ }
+}
+
diff --git a/test/ServiceHost.Test/Message/TestMessageTypes.cs b/test/ServiceHost.Test/Message/TestMessageTypes.cs
new file mode 100644
index 00000000..cc5981dc
--- /dev/null
+++ b/test/ServiceHost.Test/Message/TestMessageTypes.cs
@@ -0,0 +1,56 @@
+//
+// 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.EditorServices.Protocol.MessageProtocol;
+using System;
+using System.Threading.Tasks;
+
+namespace Microsoft.SqlTools.EditorServices.Test.Protocol.MessageProtocol
+{
+ #region Request Types
+
+ internal class TestRequest
+ {
+ public Task ProcessMessage(
+ EditorSession editorSession,
+ MessageWriter messageWriter)
+ {
+ return Task.FromResult(false);
+ }
+ }
+
+ internal class TestRequestArguments
+ {
+ public string SomeString { get; set; }
+ }
+
+ #endregion
+
+ #region Response Types
+
+ internal class TestResponse
+ {
+ }
+
+ internal class TestResponseBody
+ {
+ public string SomeString { get; set; }
+ }
+
+ #endregion
+
+ #region Event Types
+
+ internal class TestEvent
+ {
+ }
+
+ internal class TestEventBody
+ {
+ public string SomeString { get; set; }
+ }
+
+ #endregion
+}
diff --git a/test/ServiceHost.Test/PowerShellEditorServices.Test.Protocol.csproj b/test/ServiceHost.Test/PowerShellEditorServices.Test.Protocol.csproj
new file mode 100644
index 00000000..54e20896
--- /dev/null
+++ b/test/ServiceHost.Test/PowerShellEditorServices.Test.Protocol.csproj
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+ Debug
+ AnyCPU
+ {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}
+ Library
+ Properties
+ Microsoft.SqlTools.EditorServices.Test.Protocol
+ Microsoft.SqlTools.EditorServices.Test.Protocol
+ v4.6.1
+ 512
+ 69e9ba79
+ ..\..\
+ true
+
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ ..\..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll
+ True
+
+
+
+
+
+
+
+
+
+ ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll
+ True
+
+
+ ..\..\packages\xunit.assert.2.1.0\lib\portable-net45+win8+wp8+wpa81\xunit.assert.dll
+ True
+
+
+ ..\..\packages\xunit.extensibility.core.2.1.0\lib\portable-net45+win8+wp8+wpa81\xunit.core.dll
+ True
+
+
+ ..\..\packages\xunit.extensibility.execution.2.1.0\lib\net45\xunit.execution.desktop.dll
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {f8a0946a-5d25-4651-8079-b8d5776916fb}
+ SqlToolsEditorServices.Protocol
+
+
+ {81e8cbcd-6319-49e7-9662-0475bd0791f4}
+ SqlToolsEditorServices
+
+
+
+
+
+
+
+
+ This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/ServiceHost.Test/Properties/AssemblyInfo.cs b/test/ServiceHost.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..5cf54b90
--- /dev/null
+++ b/test/ServiceHost.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,42 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("SqlToolsEditorServices.Test.Transport.Stdio")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("SqlToolsEditorServices.Test.Transport.Stdio")]
+[assembly: AssemblyCopyright("Copyright � 2015")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("07137FCA-76D0-4CE7-9764-C21DB7A57093")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
+
diff --git a/test/ServiceHost.Test/packages.config b/test/ServiceHost.Test/packages.config
new file mode 100644
index 00000000..d01e8969
--- /dev/null
+++ b/test/ServiceHost.Test/packages.config
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/ServiceHost.Test/project.json b/test/ServiceHost.Test/project.json
new file mode 100644
index 00000000..b7f00724
--- /dev/null
+++ b/test/ServiceHost.Test/project.json
@@ -0,0 +1,30 @@
+{
+ "version": "1.0.0-*",
+ "buildOptions": {
+ "debugType": "portable"
+ },
+ "dependencies": {
+ "Newtonsoft.Json": "9.0.1",
+ "System.Runtime.Serialization.Primitives": "4.1.1",
+ "xunit": "2.1.0",
+ "dotnet-test-xunit": "1.0.0-rc2-192208-24",
+ "ServiceHost": {
+ "target": "project"
+ }
+ },
+ "testRunner": "xunit",
+ "frameworks": {
+ "netcoreapp1.0": {
+ "dependencies": {
+ "Microsoft.NETCore.App": {
+ "type": "platform",
+ "version": "1.0.0"
+ }
+ },
+ "imports": [
+ "dotnet5.4",
+ "portable-net451+win8"
+ ]
+ }
+ }
+}