From 68999917b24e6350ce30a45620e16c01f7eb8ab6 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Sun, 17 Jul 2016 11:31:31 -0700 Subject: [PATCH 02/12] Setup standard src, test folder structure. Add unit test project. --- global.json | 5 + .../LanguageServer/ClientCapabilities.cs | 18 + src/ServiceHost/LanguageServer/Completion.cs | 85 +++ .../LanguageServer/Configuration.cs | 21 + src/ServiceHost/LanguageServer/Definition.cs | 17 + src/ServiceHost/LanguageServer/Diagnostics.cs | 71 +++ .../LanguageServer/DocumentHighlight.cs | 31 + .../LanguageServer/ExpandAliasRequest.cs | 16 + .../LanguageServer/FindModuleRequest.cs | 24 + src/ServiceHost/LanguageServer/Hover.cs | 32 ++ src/ServiceHost/LanguageServer/Initialize.cs | 46 ++ .../LanguageServer/InstallModuleRequest.cs | 16 + src/ServiceHost/LanguageServer/References.cs | 27 + .../LanguageServer/ServerCapabilities.cs | 63 ++ .../LanguageServer/ShowOnlineHelpRequest.cs | 16 + src/ServiceHost/LanguageServer/Shutdown.cs | 32 ++ .../LanguageServer/SignatureHelp.cs | 42 ++ .../LanguageServer/TextDocument.cs | 164 ++++++ .../LanguageServer/WorkspaceSymbols.cs | 62 ++ .../LanguageSupport/LanguageService.cs | 57 ++ .../MessageProtocol/Channel/ChannelBase.cs | 81 +++ .../Channel/StdioClientChannel.cs | 125 ++++ .../Channel/StdioServerChannel.cs | 60 ++ src/ServiceHost/MessageProtocol/Constants.cs | 25 + .../MessageProtocol/EventContext.cs | 33 ++ src/ServiceHost/MessageProtocol/EventType.cs | 33 ++ .../MessageProtocol/IMessageSender.cs | 22 + .../MessageProtocol/IMessageSerializer.cs | 30 + src/ServiceHost/MessageProtocol/Message.cs | 136 +++++ .../MessageProtocol/MessageDispatcher.cs | 325 +++++++++++ .../MessageProtocol/MessageParseException.cs | 23 + .../MessageProtocol/MessageProtocolType.cs | 23 + .../MessageProtocol/MessageReader.cs | 262 +++++++++ .../MessageProtocol/MessageWriter.cs | 140 +++++ .../MessageProtocol/ProtocolEndpoint.cs | 313 ++++++++++ .../MessageProtocol/RequestContext.cs | 47 ++ .../MessageProtocol/RequestType.cs | 24 + .../Serializers/JsonRpcMessageSerializer.cs | 99 ++++ .../Serializers/V8MessageSerializer.cs | 113 ++++ src/ServiceHost/Program.cs | 40 ++ src/ServiceHost/Properties/AssemblyInfo.cs | 44 ++ src/ServiceHost/Server/LanguageServer.cs | 517 +++++++++++++++++ src/ServiceHost/Server/LanguageServerBase.cs | 84 +++ .../Server/LanguageServerEditorOperations.cs | 114 ++++ .../Server/LanguageServerSettings.cs | 90 +++ src/ServiceHost/Session/EditorSession.cs | 75 +++ src/ServiceHost/Session/HostDetails.cs | 92 +++ src/ServiceHost/Session/OutputType.cs | 41 ++ .../Session/OutputWrittenEventArgs.cs | 65 +++ src/ServiceHost/Session/ProfilePaths.cs | 109 ++++ src/ServiceHost/Session/SqlToolsContext.cs | 25 + src/ServiceHost/Utility/AsyncContext.cs | 52 ++ src/ServiceHost/Utility/AsyncContextThread.cs | 85 +++ src/ServiceHost/Utility/AsyncLock.cs | 103 ++++ src/ServiceHost/Utility/AsyncQueue.cs | 155 +++++ src/ServiceHost/Utility/Extensions.cs | 34 ++ src/ServiceHost/Utility/Logger.cs | 222 ++++++++ .../Utility/ThreadSynchronizationContext.cs | 77 +++ src/ServiceHost/Utility/Validate.cs | 143 +++++ src/ServiceHost/Workspace/BufferPosition.cs | 110 ++++ src/ServiceHost/Workspace/BufferRange.cs | 123 ++++ src/ServiceHost/Workspace/FileChange.cs | 38 ++ src/ServiceHost/Workspace/FilePosition.cs | 110 ++++ src/ServiceHost/Workspace/ScriptFile.cs | 538 ++++++++++++++++++ src/ServiceHost/Workspace/ScriptFileMarker.cs | 56 ++ src/ServiceHost/Workspace/ScriptRegion.cs | 89 +++ src/ServiceHost/Workspace/Workspace.cs | 248 ++++++++ src/ServiceHost/project.json | 21 + test/ServiceHost.Test/App.config | 9 + .../JsonRpcMessageSerializerTests.cs | 144 +++++ .../Message/MessageReaderWriterTests.cs | 177 ++++++ .../Message/TestMessageTypes.cs | 56 ++ ...erShellEditorServices.Test.Protocol.csproj | 109 ++++ .../Properties/AssemblyInfo.cs | 42 ++ test/ServiceHost.Test/packages.config | 11 + test/ServiceHost.Test/project.json | 30 + 76 files changed, 6837 insertions(+) create mode 100644 global.json create mode 100644 src/ServiceHost/LanguageServer/ClientCapabilities.cs create mode 100644 src/ServiceHost/LanguageServer/Completion.cs create mode 100644 src/ServiceHost/LanguageServer/Configuration.cs create mode 100644 src/ServiceHost/LanguageServer/Definition.cs create mode 100644 src/ServiceHost/LanguageServer/Diagnostics.cs create mode 100644 src/ServiceHost/LanguageServer/DocumentHighlight.cs create mode 100644 src/ServiceHost/LanguageServer/ExpandAliasRequest.cs create mode 100644 src/ServiceHost/LanguageServer/FindModuleRequest.cs create mode 100644 src/ServiceHost/LanguageServer/Hover.cs create mode 100644 src/ServiceHost/LanguageServer/Initialize.cs create mode 100644 src/ServiceHost/LanguageServer/InstallModuleRequest.cs create mode 100644 src/ServiceHost/LanguageServer/References.cs create mode 100644 src/ServiceHost/LanguageServer/ServerCapabilities.cs create mode 100644 src/ServiceHost/LanguageServer/ShowOnlineHelpRequest.cs create mode 100644 src/ServiceHost/LanguageServer/Shutdown.cs create mode 100644 src/ServiceHost/LanguageServer/SignatureHelp.cs create mode 100644 src/ServiceHost/LanguageServer/TextDocument.cs create mode 100644 src/ServiceHost/LanguageServer/WorkspaceSymbols.cs create mode 100644 src/ServiceHost/LanguageSupport/LanguageService.cs create mode 100644 src/ServiceHost/MessageProtocol/Channel/ChannelBase.cs create mode 100644 src/ServiceHost/MessageProtocol/Channel/StdioClientChannel.cs create mode 100644 src/ServiceHost/MessageProtocol/Channel/StdioServerChannel.cs create mode 100644 src/ServiceHost/MessageProtocol/Constants.cs create mode 100644 src/ServiceHost/MessageProtocol/EventContext.cs create mode 100644 src/ServiceHost/MessageProtocol/EventType.cs create mode 100644 src/ServiceHost/MessageProtocol/IMessageSender.cs create mode 100644 src/ServiceHost/MessageProtocol/IMessageSerializer.cs create mode 100644 src/ServiceHost/MessageProtocol/Message.cs create mode 100644 src/ServiceHost/MessageProtocol/MessageDispatcher.cs create mode 100644 src/ServiceHost/MessageProtocol/MessageParseException.cs create mode 100644 src/ServiceHost/MessageProtocol/MessageProtocolType.cs create mode 100644 src/ServiceHost/MessageProtocol/MessageReader.cs create mode 100644 src/ServiceHost/MessageProtocol/MessageWriter.cs create mode 100644 src/ServiceHost/MessageProtocol/ProtocolEndpoint.cs create mode 100644 src/ServiceHost/MessageProtocol/RequestContext.cs create mode 100644 src/ServiceHost/MessageProtocol/RequestType.cs create mode 100644 src/ServiceHost/MessageProtocol/Serializers/JsonRpcMessageSerializer.cs create mode 100644 src/ServiceHost/MessageProtocol/Serializers/V8MessageSerializer.cs create mode 100644 src/ServiceHost/Program.cs create mode 100644 src/ServiceHost/Properties/AssemblyInfo.cs create mode 100644 src/ServiceHost/Server/LanguageServer.cs create mode 100644 src/ServiceHost/Server/LanguageServerBase.cs create mode 100644 src/ServiceHost/Server/LanguageServerEditorOperations.cs create mode 100644 src/ServiceHost/Server/LanguageServerSettings.cs create mode 100644 src/ServiceHost/Session/EditorSession.cs create mode 100644 src/ServiceHost/Session/HostDetails.cs create mode 100644 src/ServiceHost/Session/OutputType.cs create mode 100644 src/ServiceHost/Session/OutputWrittenEventArgs.cs create mode 100644 src/ServiceHost/Session/ProfilePaths.cs create mode 100644 src/ServiceHost/Session/SqlToolsContext.cs create mode 100644 src/ServiceHost/Utility/AsyncContext.cs create mode 100644 src/ServiceHost/Utility/AsyncContextThread.cs create mode 100644 src/ServiceHost/Utility/AsyncLock.cs create mode 100644 src/ServiceHost/Utility/AsyncQueue.cs create mode 100644 src/ServiceHost/Utility/Extensions.cs create mode 100644 src/ServiceHost/Utility/Logger.cs create mode 100644 src/ServiceHost/Utility/ThreadSynchronizationContext.cs create mode 100644 src/ServiceHost/Utility/Validate.cs create mode 100644 src/ServiceHost/Workspace/BufferPosition.cs create mode 100644 src/ServiceHost/Workspace/BufferRange.cs create mode 100644 src/ServiceHost/Workspace/FileChange.cs create mode 100644 src/ServiceHost/Workspace/FilePosition.cs create mode 100644 src/ServiceHost/Workspace/ScriptFile.cs create mode 100644 src/ServiceHost/Workspace/ScriptFileMarker.cs create mode 100644 src/ServiceHost/Workspace/ScriptRegion.cs create mode 100644 src/ServiceHost/Workspace/Workspace.cs create mode 100644 src/ServiceHost/project.json create mode 100644 test/ServiceHost.Test/App.config create mode 100644 test/ServiceHost.Test/LanguageServer/JsonRpcMessageSerializerTests.cs create mode 100644 test/ServiceHost.Test/Message/MessageReaderWriterTests.cs create mode 100644 test/ServiceHost.Test/Message/TestMessageTypes.cs create mode 100644 test/ServiceHost.Test/PowerShellEditorServices.Test.Protocol.csproj create mode 100644 test/ServiceHost.Test/Properties/AssemblyInfo.cs create mode 100644 test/ServiceHost.Test/packages.config create mode 100644 test/ServiceHost.Test/project.json 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" + ] + } + } +} From ae54004e999f7caeb425925012da03fcde53a897 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Sun, 17 Jul 2016 11:44:03 -0700 Subject: [PATCH 03/12] Actually stage the deletes. Update .gitignore --- .gitignore | 273 ++++++++- ServiceHost/.vscode/launch.json | 24 - ServiceHost/.vscode/tasks.json | 14 - .../LanguageServer/ClientCapabilities.cs | 18 - ServiceHost/LanguageServer/Completion.cs | 85 --- ServiceHost/LanguageServer/Configuration.cs | 21 - ServiceHost/LanguageServer/Definition.cs | 17 - ServiceHost/LanguageServer/Diagnostics.cs | 71 --- .../LanguageServer/DocumentHighlight.cs | 31 - .../LanguageServer/ExpandAliasRequest.cs | 16 - .../LanguageServer/FindModuleRequest.cs | 24 - ServiceHost/LanguageServer/Hover.cs | 32 -- ServiceHost/LanguageServer/Initialize.cs | 46 -- .../LanguageServer/InstallModuleRequest.cs | 16 - ServiceHost/LanguageServer/References.cs | 27 - .../LanguageServer/ServerCapabilities.cs | 63 -- .../LanguageServer/ShowOnlineHelpRequest.cs | 16 - ServiceHost/LanguageServer/Shutdown.cs | 32 -- ServiceHost/LanguageServer/SignatureHelp.cs | 42 -- ServiceHost/LanguageServer/TextDocument.cs | 164 ------ .../LanguageServer/WorkspaceSymbols.cs | 62 -- .../LanguageSupport/LanguageService.cs | 57 -- .../MessageProtocol/Channel/ChannelBase.cs | 81 --- .../Channel/StdioClientChannel.cs | 125 ---- .../Channel/StdioServerChannel.cs | 60 -- ServiceHost/MessageProtocol/Constants.cs | 25 - ServiceHost/MessageProtocol/EventContext.cs | 33 -- ServiceHost/MessageProtocol/EventType.cs | 33 -- ServiceHost/MessageProtocol/IMessageSender.cs | 22 - .../MessageProtocol/IMessageSerializer.cs | 30 - ServiceHost/MessageProtocol/Message.cs | 136 ----- .../MessageProtocol/MessageDispatcher.cs | 325 ----------- .../MessageProtocol/MessageParseException.cs | 23 - .../MessageProtocol/MessageProtocolType.cs | 23 - ServiceHost/MessageProtocol/MessageReader.cs | 262 --------- ServiceHost/MessageProtocol/MessageWriter.cs | 140 ----- .../MessageProtocol/ProtocolEndpoint.cs | 313 ---------- ServiceHost/MessageProtocol/RequestContext.cs | 47 -- ServiceHost/MessageProtocol/RequestType.cs | 24 - .../Serializers/JsonRpcMessageSerializer.cs | 99 ---- .../Serializers/V8MessageSerializer.cs | 113 ---- ServiceHost/Program.cs | 40 -- ServiceHost/Properties/AssemblyInfo.cs | 44 -- ServiceHost/Server/LanguageServer.cs | 517 ----------------- ServiceHost/Server/LanguageServerBase.cs | 84 --- .../Server/LanguageServerEditorOperations.cs | 114 ---- ServiceHost/Server/LanguageServerSettings.cs | 90 --- ServiceHost/Session/EditorSession.cs | 75 --- ServiceHost/Session/HostDetails.cs | 92 --- ServiceHost/Session/OutputType.cs | 41 -- ServiceHost/Session/OutputWrittenEventArgs.cs | 65 --- ServiceHost/Session/ProfilePaths.cs | 109 ---- ServiceHost/Session/SqlToolsContext.cs | 25 - ServiceHost/Utility/AsyncContext.cs | 52 -- ServiceHost/Utility/AsyncContextThread.cs | 85 --- ServiceHost/Utility/AsyncLock.cs | 103 ---- ServiceHost/Utility/AsyncQueue.cs | 155 ----- ServiceHost/Utility/Extensions.cs | 34 -- ServiceHost/Utility/Logger.cs | 222 -------- .../Utility/ThreadSynchronizationContext.cs | 77 --- ServiceHost/Utility/Validate.cs | 143 ----- ServiceHost/Workspace/BufferPosition.cs | 110 ---- ServiceHost/Workspace/BufferRange.cs | 123 ---- ServiceHost/Workspace/FileChange.cs | 38 -- ServiceHost/Workspace/FilePosition.cs | 110 ---- ServiceHost/Workspace/ScriptFile.cs | 538 ------------------ ServiceHost/Workspace/ScriptFileMarker.cs | 56 -- ServiceHost/Workspace/ScriptRegion.cs | 89 --- ServiceHost/Workspace/Workspace.cs | 248 -------- ServiceHost/project.json | 21 - 70 files changed, 270 insertions(+), 6295 deletions(-) delete mode 100644 ServiceHost/.vscode/launch.json delete mode 100644 ServiceHost/.vscode/tasks.json delete mode 100644 ServiceHost/LanguageServer/ClientCapabilities.cs delete mode 100644 ServiceHost/LanguageServer/Completion.cs delete mode 100644 ServiceHost/LanguageServer/Configuration.cs delete mode 100644 ServiceHost/LanguageServer/Definition.cs delete mode 100644 ServiceHost/LanguageServer/Diagnostics.cs delete mode 100644 ServiceHost/LanguageServer/DocumentHighlight.cs delete mode 100644 ServiceHost/LanguageServer/ExpandAliasRequest.cs delete mode 100644 ServiceHost/LanguageServer/FindModuleRequest.cs delete mode 100644 ServiceHost/LanguageServer/Hover.cs delete mode 100644 ServiceHost/LanguageServer/Initialize.cs delete mode 100644 ServiceHost/LanguageServer/InstallModuleRequest.cs delete mode 100644 ServiceHost/LanguageServer/References.cs delete mode 100644 ServiceHost/LanguageServer/ServerCapabilities.cs delete mode 100644 ServiceHost/LanguageServer/ShowOnlineHelpRequest.cs delete mode 100644 ServiceHost/LanguageServer/Shutdown.cs delete mode 100644 ServiceHost/LanguageServer/SignatureHelp.cs delete mode 100644 ServiceHost/LanguageServer/TextDocument.cs delete mode 100644 ServiceHost/LanguageServer/WorkspaceSymbols.cs delete mode 100644 ServiceHost/LanguageSupport/LanguageService.cs delete mode 100644 ServiceHost/MessageProtocol/Channel/ChannelBase.cs delete mode 100644 ServiceHost/MessageProtocol/Channel/StdioClientChannel.cs delete mode 100644 ServiceHost/MessageProtocol/Channel/StdioServerChannel.cs delete mode 100644 ServiceHost/MessageProtocol/Constants.cs delete mode 100644 ServiceHost/MessageProtocol/EventContext.cs delete mode 100644 ServiceHost/MessageProtocol/EventType.cs delete mode 100644 ServiceHost/MessageProtocol/IMessageSender.cs delete mode 100644 ServiceHost/MessageProtocol/IMessageSerializer.cs delete mode 100644 ServiceHost/MessageProtocol/Message.cs delete mode 100644 ServiceHost/MessageProtocol/MessageDispatcher.cs delete mode 100644 ServiceHost/MessageProtocol/MessageParseException.cs delete mode 100644 ServiceHost/MessageProtocol/MessageProtocolType.cs delete mode 100644 ServiceHost/MessageProtocol/MessageReader.cs delete mode 100644 ServiceHost/MessageProtocol/MessageWriter.cs delete mode 100644 ServiceHost/MessageProtocol/ProtocolEndpoint.cs delete mode 100644 ServiceHost/MessageProtocol/RequestContext.cs delete mode 100644 ServiceHost/MessageProtocol/RequestType.cs delete mode 100644 ServiceHost/MessageProtocol/Serializers/JsonRpcMessageSerializer.cs delete mode 100644 ServiceHost/MessageProtocol/Serializers/V8MessageSerializer.cs delete mode 100644 ServiceHost/Program.cs delete mode 100644 ServiceHost/Properties/AssemblyInfo.cs delete mode 100644 ServiceHost/Server/LanguageServer.cs delete mode 100644 ServiceHost/Server/LanguageServerBase.cs delete mode 100644 ServiceHost/Server/LanguageServerEditorOperations.cs delete mode 100644 ServiceHost/Server/LanguageServerSettings.cs delete mode 100644 ServiceHost/Session/EditorSession.cs delete mode 100644 ServiceHost/Session/HostDetails.cs delete mode 100644 ServiceHost/Session/OutputType.cs delete mode 100644 ServiceHost/Session/OutputWrittenEventArgs.cs delete mode 100644 ServiceHost/Session/ProfilePaths.cs delete mode 100644 ServiceHost/Session/SqlToolsContext.cs delete mode 100644 ServiceHost/Utility/AsyncContext.cs delete mode 100644 ServiceHost/Utility/AsyncContextThread.cs delete mode 100644 ServiceHost/Utility/AsyncLock.cs delete mode 100644 ServiceHost/Utility/AsyncQueue.cs delete mode 100644 ServiceHost/Utility/Extensions.cs delete mode 100644 ServiceHost/Utility/Logger.cs delete mode 100644 ServiceHost/Utility/ThreadSynchronizationContext.cs delete mode 100644 ServiceHost/Utility/Validate.cs delete mode 100644 ServiceHost/Workspace/BufferPosition.cs delete mode 100644 ServiceHost/Workspace/BufferRange.cs delete mode 100644 ServiceHost/Workspace/FileChange.cs delete mode 100644 ServiceHost/Workspace/FilePosition.cs delete mode 100644 ServiceHost/Workspace/ScriptFile.cs delete mode 100644 ServiceHost/Workspace/ScriptFileMarker.cs delete mode 100644 ServiceHost/Workspace/ScriptRegion.cs delete mode 100644 ServiceHost/Workspace/Workspace.cs delete mode 100644 ServiceHost/project.json diff --git a/.gitignore b/.gitignore index d1fcfc4e..4c997e2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,270 @@ -bin -obj -project.lock.json \ No newline at end of file +syntax: glob + +### VisualStudio ### + +# Project.json lock file +project.lock.json + +# Tool Runtime Dir +/[Tt]ools/ + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ +msbuild.log +msbuild.err +msbuild.wrn + + + +# Cross building rootfs +cross/rootfs/ + +# Visual Studio 2015 +.vs/ + +# Visual Studio 2015 Pre-CTP6 +*.sln.ide +*.ide/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +#NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +*.pubxml +*.publishproj + +# NuGet Packages +*.nuget.props +*.nuget.targets +*.nupkg +**/packages/* + +# NuGet package restore lockfiles +project.lock.json + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +*.metaproj +*.metaproj.tmp + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +### MonoDevelop ### + +*.pidb +*.userprefs + +### Windows ### + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### Linux ### + +*~ + +# KDE directory preferences +.directory + +### OSX ### + +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# vim temporary files +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +*.un~ +Session.vim +.netrwhist +*~ + +# Visual Studio Code +.vscode/ diff --git a/ServiceHost/.vscode/launch.json b/ServiceHost/.vscode/launch.json deleted file mode 100644 index 18ebbb27..00000000 --- a/ServiceHost/.vscode/launch.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": ".NET Core Launch (console)", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - "program": "${workspaceRoot}/bin/Debug/netcoreapp1.0/servicehost.dll", - "args": [], - "cwd": "${workspaceRoot}", - "externalConsole": true, - "requireExactSource": false, - "stopAtEntry": false - }, - { - "name": ".NET Core Attach", - "type": "coreclr", - "request": "attach", - "requireExactSource": false, - "processId": 17264 - } - ] -} \ No newline at end of file diff --git a/ServiceHost/.vscode/tasks.json b/ServiceHost/.vscode/tasks.json deleted file mode 100644 index 67d6eb75..00000000 --- a/ServiceHost/.vscode/tasks.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "version": "0.1.0", - "command": "dotnet", - "isShellCommand": true, - "args": [], - "tasks": [ - { - "taskName": "build", - "args": [], - "isBuildCommand": true, - "problemMatcher": "$msCompile" - } - ] -} \ No newline at end of file diff --git a/ServiceHost/LanguageServer/ClientCapabilities.cs b/ServiceHost/LanguageServer/ClientCapabilities.cs deleted file mode 100644 index 70e2d068..00000000 --- a/ServiceHost/LanguageServer/ClientCapabilities.cs +++ /dev/null @@ -1,18 +0,0 @@ -// -// 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/ServiceHost/LanguageServer/Completion.cs b/ServiceHost/LanguageServer/Completion.cs deleted file mode 100644 index 5f26ea96..00000000 --- a/ServiceHost/LanguageServer/Completion.cs +++ /dev/null @@ -1,85 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System.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/ServiceHost/LanguageServer/Configuration.cs b/ServiceHost/LanguageServer/Configuration.cs deleted file mode 100644 index b9ad87db..00000000 --- a/ServiceHost/LanguageServer/Configuration.cs +++ /dev/null @@ -1,21 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/LanguageServer/Definition.cs b/ServiceHost/LanguageServer/Definition.cs deleted file mode 100644 index b18845c3..00000000 --- a/ServiceHost/LanguageServer/Definition.cs +++ /dev/null @@ -1,17 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/LanguageServer/Diagnostics.cs b/ServiceHost/LanguageServer/Diagnostics.cs deleted file mode 100644 index a5472607..00000000 --- a/ServiceHost/LanguageServer/Diagnostics.cs +++ /dev/null @@ -1,71 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/LanguageServer/DocumentHighlight.cs b/ServiceHost/LanguageServer/DocumentHighlight.cs deleted file mode 100644 index 6849ddfb..00000000 --- a/ServiceHost/LanguageServer/DocumentHighlight.cs +++ /dev/null @@ -1,31 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/LanguageServer/ExpandAliasRequest.cs b/ServiceHost/LanguageServer/ExpandAliasRequest.cs deleted file mode 100644 index d7f9fde4..00000000 --- a/ServiceHost/LanguageServer/ExpandAliasRequest.cs +++ /dev/null @@ -1,16 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/LanguageServer/FindModuleRequest.cs b/ServiceHost/LanguageServer/FindModuleRequest.cs deleted file mode 100644 index ab78a158..00000000 --- a/ServiceHost/LanguageServer/FindModuleRequest.cs +++ /dev/null @@ -1,24 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/LanguageServer/Hover.cs b/ServiceHost/LanguageServer/Hover.cs deleted file mode 100644 index 2e196fba..00000000 --- a/ServiceHost/LanguageServer/Hover.cs +++ /dev/null @@ -1,32 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/LanguageServer/Initialize.cs b/ServiceHost/LanguageServer/Initialize.cs deleted file mode 100644 index 7551835e..00000000 --- a/ServiceHost/LanguageServer/Initialize.cs +++ /dev/null @@ -1,46 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/LanguageServer/InstallModuleRequest.cs b/ServiceHost/LanguageServer/InstallModuleRequest.cs deleted file mode 100644 index b03b8864..00000000 --- a/ServiceHost/LanguageServer/InstallModuleRequest.cs +++ /dev/null @@ -1,16 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/LanguageServer/References.cs b/ServiceHost/LanguageServer/References.cs deleted file mode 100644 index 25a92b12..00000000 --- a/ServiceHost/LanguageServer/References.cs +++ /dev/null @@ -1,27 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/LanguageServer/ServerCapabilities.cs b/ServiceHost/LanguageServer/ServerCapabilities.cs deleted file mode 100644 index 2f7404d9..00000000 --- a/ServiceHost/LanguageServer/ServerCapabilities.cs +++ /dev/null @@ -1,63 +0,0 @@ -// -// 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/ServiceHost/LanguageServer/ShowOnlineHelpRequest.cs b/ServiceHost/LanguageServer/ShowOnlineHelpRequest.cs deleted file mode 100644 index 8f21fb1b..00000000 --- a/ServiceHost/LanguageServer/ShowOnlineHelpRequest.cs +++ /dev/null @@ -1,16 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/LanguageServer/Shutdown.cs b/ServiceHost/LanguageServer/Shutdown.cs deleted file mode 100644 index f0a7bbd2..00000000 --- a/ServiceHost/LanguageServer/Shutdown.cs +++ /dev/null @@ -1,32 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/LanguageServer/SignatureHelp.cs b/ServiceHost/LanguageServer/SignatureHelp.cs deleted file mode 100644 index 5d4233e3..00000000 --- a/ServiceHost/LanguageServer/SignatureHelp.cs +++ /dev/null @@ -1,42 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/LanguageServer/TextDocument.cs b/ServiceHost/LanguageServer/TextDocument.cs deleted file mode 100644 index 9f477374..00000000 --- a/ServiceHost/LanguageServer/TextDocument.cs +++ /dev/null @@ -1,164 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System.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/ServiceHost/LanguageServer/WorkspaceSymbols.cs b/ServiceHost/LanguageServer/WorkspaceSymbols.cs deleted file mode 100644 index 25a554b5..00000000 --- a/ServiceHost/LanguageServer/WorkspaceSymbols.cs +++ /dev/null @@ -1,62 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/LanguageSupport/LanguageService.cs b/ServiceHost/LanguageSupport/LanguageService.cs deleted file mode 100644 index 3ab77697..00000000 --- a/ServiceHost/LanguageSupport/LanguageService.cs +++ /dev/null @@ -1,57 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/MessageProtocol/Channel/ChannelBase.cs b/ServiceHost/MessageProtocol/Channel/ChannelBase.cs deleted file mode 100644 index 848da39f..00000000 --- a/ServiceHost/MessageProtocol/Channel/ChannelBase.cs +++ /dev/null @@ -1,81 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/MessageProtocol/Channel/StdioClientChannel.cs b/ServiceHost/MessageProtocol/Channel/StdioClientChannel.cs deleted file mode 100644 index 5390f52d..00000000 --- a/ServiceHost/MessageProtocol/Channel/StdioClientChannel.cs +++ /dev/null @@ -1,125 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System.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/ServiceHost/MessageProtocol/Channel/StdioServerChannel.cs b/ServiceHost/MessageProtocol/Channel/StdioServerChannel.cs deleted file mode 100644 index 0b9376d4..00000000 --- a/ServiceHost/MessageProtocol/Channel/StdioServerChannel.cs +++ /dev/null @@ -1,60 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System.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/ServiceHost/MessageProtocol/Constants.cs b/ServiceHost/MessageProtocol/Constants.cs deleted file mode 100644 index 0fae5d8d..00000000 --- a/ServiceHost/MessageProtocol/Constants.cs +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/MessageProtocol/EventContext.cs b/ServiceHost/MessageProtocol/EventContext.cs deleted file mode 100644 index eb42ebbb..00000000 --- a/ServiceHost/MessageProtocol/EventContext.cs +++ /dev/null @@ -1,33 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System.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/ServiceHost/MessageProtocol/EventType.cs b/ServiceHost/MessageProtocol/EventType.cs deleted file mode 100644 index dd460817..00000000 --- a/ServiceHost/MessageProtocol/EventType.cs +++ /dev/null @@ -1,33 +0,0 @@ -// -// 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/ServiceHost/MessageProtocol/IMessageSender.cs b/ServiceHost/MessageProtocol/IMessageSender.cs deleted file mode 100644 index 7f331eed..00000000 --- a/ServiceHost/MessageProtocol/IMessageSender.cs +++ /dev/null @@ -1,22 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System.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/ServiceHost/MessageProtocol/IMessageSerializer.cs b/ServiceHost/MessageProtocol/IMessageSerializer.cs deleted file mode 100644 index 81b23fa6..00000000 --- a/ServiceHost/MessageProtocol/IMessageSerializer.cs +++ /dev/null @@ -1,30 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/MessageProtocol/Message.cs b/ServiceHost/MessageProtocol/Message.cs deleted file mode 100644 index 75dab5cd..00000000 --- a/ServiceHost/MessageProtocol/Message.cs +++ /dev/null @@ -1,136 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System.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/ServiceHost/MessageProtocol/MessageDispatcher.cs b/ServiceHost/MessageProtocol/MessageDispatcher.cs deleted file mode 100644 index 21c179e2..00000000 --- a/ServiceHost/MessageProtocol/MessageDispatcher.cs +++ /dev/null @@ -1,325 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/MessageProtocol/MessageParseException.cs b/ServiceHost/MessageProtocol/MessageParseException.cs deleted file mode 100644 index 98a17c20..00000000 --- a/ServiceHost/MessageProtocol/MessageParseException.cs +++ /dev/null @@ -1,23 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System; - -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/ServiceHost/MessageProtocol/MessageProtocolType.cs b/ServiceHost/MessageProtocol/MessageProtocolType.cs deleted file mode 100644 index 5484ae3c..00000000 --- a/ServiceHost/MessageProtocol/MessageProtocolType.cs +++ /dev/null @@ -1,23 +0,0 @@ -// -// 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/ServiceHost/MessageProtocol/MessageReader.cs b/ServiceHost/MessageProtocol/MessageReader.cs deleted file mode 100644 index a2df43ba..00000000 --- a/ServiceHost/MessageProtocol/MessageReader.cs +++ /dev/null @@ -1,262 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/MessageProtocol/MessageWriter.cs b/ServiceHost/MessageProtocol/MessageWriter.cs deleted file mode 100644 index 96e13bcd..00000000 --- a/ServiceHost/MessageProtocol/MessageWriter.cs +++ /dev/null @@ -1,140 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/MessageProtocol/ProtocolEndpoint.cs b/ServiceHost/MessageProtocol/ProtocolEndpoint.cs deleted file mode 100644 index daead186..00000000 --- a/ServiceHost/MessageProtocol/ProtocolEndpoint.cs +++ /dev/null @@ -1,313 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/MessageProtocol/RequestContext.cs b/ServiceHost/MessageProtocol/RequestContext.cs deleted file mode 100644 index a35bb136..00000000 --- a/ServiceHost/MessageProtocol/RequestContext.cs +++ /dev/null @@ -1,47 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/MessageProtocol/RequestType.cs b/ServiceHost/MessageProtocol/RequestType.cs deleted file mode 100644 index 29fc11c5..00000000 --- a/ServiceHost/MessageProtocol/RequestType.cs +++ /dev/null @@ -1,24 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System.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/ServiceHost/MessageProtocol/Serializers/JsonRpcMessageSerializer.cs b/ServiceHost/MessageProtocol/Serializers/JsonRpcMessageSerializer.cs deleted file mode 100644 index fa1d1518..00000000 --- a/ServiceHost/MessageProtocol/Serializers/JsonRpcMessageSerializer.cs +++ /dev/null @@ -1,99 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/MessageProtocol/Serializers/V8MessageSerializer.cs b/ServiceHost/MessageProtocol/Serializers/V8MessageSerializer.cs deleted file mode 100644 index 941e249a..00000000 --- a/ServiceHost/MessageProtocol/Serializers/V8MessageSerializer.cs +++ /dev/null @@ -1,113 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/Program.cs b/ServiceHost/Program.cs deleted file mode 100644 index 6bfd0f24..00000000 --- a/ServiceHost/Program.cs +++ /dev/null @@ -1,40 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; -using Microsoft.SqlTools.EditorServices.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/ServiceHost/Properties/AssemblyInfo.cs b/ServiceHost/Properties/AssemblyInfo.cs deleted file mode 100644 index 27c1daba..00000000 --- a/ServiceHost/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,44 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System.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/ServiceHost/Server/LanguageServer.cs b/ServiceHost/Server/LanguageServer.cs deleted file mode 100644 index d6719141..00000000 --- a/ServiceHost/Server/LanguageServer.cs +++ /dev/null @@ -1,517 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// -using 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/ServiceHost/Server/LanguageServerBase.cs b/ServiceHost/Server/LanguageServerBase.cs deleted file mode 100644 index 0128484b..00000000 --- a/ServiceHost/Server/LanguageServerBase.cs +++ /dev/null @@ -1,84 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/Server/LanguageServerEditorOperations.cs b/ServiceHost/Server/LanguageServerEditorOperations.cs deleted file mode 100644 index e5714b19..00000000 --- a/ServiceHost/Server/LanguageServerEditorOperations.cs +++ /dev/null @@ -1,114 +0,0 @@ -// -// 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/ServiceHost/Server/LanguageServerSettings.cs b/ServiceHost/Server/LanguageServerSettings.cs deleted file mode 100644 index be09984a..00000000 --- a/ServiceHost/Server/LanguageServerSettings.cs +++ /dev/null @@ -1,90 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System.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/ServiceHost/Session/EditorSession.cs b/ServiceHost/Session/EditorSession.cs deleted file mode 100644 index 3c592a8f..00000000 --- a/ServiceHost/Session/EditorSession.cs +++ /dev/null @@ -1,75 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System; -using Microsoft.SqlTools.EditorServices.Session; -using Microsoft.SqlTools.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/ServiceHost/Session/HostDetails.cs b/ServiceHost/Session/HostDetails.cs deleted file mode 100644 index 1a5fc80d..00000000 --- a/ServiceHost/Session/HostDetails.cs +++ /dev/null @@ -1,92 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System; - -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/ServiceHost/Session/OutputType.cs b/ServiceHost/Session/OutputType.cs deleted file mode 100644 index 8ba866d7..00000000 --- a/ServiceHost/Session/OutputType.cs +++ /dev/null @@ -1,41 +0,0 @@ -// -// 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/ServiceHost/Session/OutputWrittenEventArgs.cs b/ServiceHost/Session/OutputWrittenEventArgs.cs deleted file mode 100644 index 4b1dbbe3..00000000 --- a/ServiceHost/Session/OutputWrittenEventArgs.cs +++ /dev/null @@ -1,65 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System; - -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/ServiceHost/Session/ProfilePaths.cs b/ServiceHost/Session/ProfilePaths.cs deleted file mode 100644 index 4af38521..00000000 --- a/ServiceHost/Session/ProfilePaths.cs +++ /dev/null @@ -1,109 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System; -using 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/ServiceHost/Session/SqlToolsContext.cs b/ServiceHost/Session/SqlToolsContext.cs deleted file mode 100644 index d8016afd..00000000 --- a/ServiceHost/Session/SqlToolsContext.cs +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System; - -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/ServiceHost/Utility/AsyncContext.cs b/ServiceHost/Utility/AsyncContext.cs deleted file mode 100644 index c0baa889..00000000 --- a/ServiceHost/Utility/AsyncContext.cs +++ /dev/null @@ -1,52 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System; -using 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/ServiceHost/Utility/AsyncContextThread.cs b/ServiceHost/Utility/AsyncContextThread.cs deleted file mode 100644 index 7b7947a5..00000000 --- a/ServiceHost/Utility/AsyncContextThread.cs +++ /dev/null @@ -1,85 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System; -using 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/ServiceHost/Utility/AsyncLock.cs b/ServiceHost/Utility/AsyncLock.cs deleted file mode 100644 index b12983ce..00000000 --- a/ServiceHost/Utility/AsyncLock.cs +++ /dev/null @@ -1,103 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System; -using 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/ServiceHost/Utility/AsyncQueue.cs b/ServiceHost/Utility/AsyncQueue.cs deleted file mode 100644 index c899ee78..00000000 --- a/ServiceHost/Utility/AsyncQueue.cs +++ /dev/null @@ -1,155 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System.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/ServiceHost/Utility/Extensions.cs b/ServiceHost/Utility/Extensions.cs deleted file mode 100644 index c73bc03c..00000000 --- a/ServiceHost/Utility/Extensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System; - -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/ServiceHost/Utility/Logger.cs b/ServiceHost/Utility/Logger.cs deleted file mode 100644 index 6bdd39bd..00000000 --- a/ServiceHost/Utility/Logger.cs +++ /dev/null @@ -1,222 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System; -using 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/ServiceHost/Utility/ThreadSynchronizationContext.cs b/ServiceHost/Utility/ThreadSynchronizationContext.cs deleted file mode 100644 index 2389bbb0..00000000 --- a/ServiceHost/Utility/ThreadSynchronizationContext.cs +++ /dev/null @@ -1,77 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System; -using 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/ServiceHost/Utility/Validate.cs b/ServiceHost/Utility/Validate.cs deleted file mode 100644 index c788e67b..00000000 --- a/ServiceHost/Utility/Validate.cs +++ /dev/null @@ -1,143 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System; -using 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/ServiceHost/Workspace/BufferPosition.cs b/ServiceHost/Workspace/BufferPosition.cs deleted file mode 100644 index 8f790d85..00000000 --- a/ServiceHost/Workspace/BufferPosition.cs +++ /dev/null @@ -1,110 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System.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/ServiceHost/Workspace/BufferRange.cs b/ServiceHost/Workspace/BufferRange.cs deleted file mode 100644 index 5d20598f..00000000 --- a/ServiceHost/Workspace/BufferRange.cs +++ /dev/null @@ -1,123 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System; -using 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/ServiceHost/Workspace/FileChange.cs b/ServiceHost/Workspace/FileChange.cs deleted file mode 100644 index 2f6efdf8..00000000 --- a/ServiceHost/Workspace/FileChange.cs +++ /dev/null @@ -1,38 +0,0 @@ -// -// 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/ServiceHost/Workspace/FilePosition.cs b/ServiceHost/Workspace/FilePosition.cs deleted file mode 100644 index 2cb58745..00000000 --- a/ServiceHost/Workspace/FilePosition.cs +++ /dev/null @@ -1,110 +0,0 @@ -// -// 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/ServiceHost/Workspace/ScriptFile.cs b/ServiceHost/Workspace/ScriptFile.cs deleted file mode 100644 index 90d66244..00000000 --- a/ServiceHost/Workspace/ScriptFile.cs +++ /dev/null @@ -1,538 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/Workspace/ScriptFileMarker.cs b/ServiceHost/Workspace/ScriptFileMarker.cs deleted file mode 100644 index 87c2576c..00000000 --- a/ServiceHost/Workspace/ScriptFileMarker.cs +++ /dev/null @@ -1,56 +0,0 @@ -// -// 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/ServiceHost/Workspace/ScriptRegion.cs b/ServiceHost/Workspace/ScriptRegion.cs deleted file mode 100644 index f2fa4ac8..00000000 --- a/ServiceHost/Workspace/ScriptRegion.cs +++ /dev/null @@ -1,89 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -//using System.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/ServiceHost/Workspace/Workspace.cs b/ServiceHost/Workspace/Workspace.cs deleted file mode 100644 index 39e1d70f..00000000 --- a/ServiceHost/Workspace/Workspace.cs +++ /dev/null @@ -1,248 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using 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/ServiceHost/project.json b/ServiceHost/project.json deleted file mode 100644 index 28565355..00000000 --- a/ServiceHost/project.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "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" - } - } -} From 7119db3bcd1dd5ccbede9427605efbbaea35b1f1 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Sat, 23 Jul 2016 15:05:02 -0700 Subject: [PATCH 04/12] Integrate SqlParser into the onchange diagnostics to provide error messages. --- .../LanguageSupport/LanguageService.cs | 57 +++++++++++++------ src/ServiceHost/Server/LanguageServer.cs | 8 +-- src/ServiceHost/project.json | 3 +- 3 files changed, 45 insertions(+), 23 deletions(-) diff --git a/src/ServiceHost/LanguageSupport/LanguageService.cs b/src/ServiceHost/LanguageSupport/LanguageService.cs index 3ab77697..ff4f4a65 100644 --- a/src/ServiceHost/LanguageSupport/LanguageService.cs +++ b/src/ServiceHost/LanguageSupport/LanguageService.cs @@ -5,6 +5,8 @@ using Microsoft.SqlTools.EditorServices; using Microsoft.SqlTools.EditorServices.Session; +using Microsoft.SqlServer.Management.SqlParser.Parser; +using System.Collections.Generic; namespace Microsoft.SqlTools.LanguageSupport { @@ -13,6 +15,11 @@ namespace Microsoft.SqlTools.LanguageSupport /// public class LanguageService { + /// + /// The cached parse result from previous incremental parse + /// + private ParseResult prevParseResult; + /// /// Gets or sets the current SQL Tools context /// @@ -34,24 +41,38 @@ namespace Microsoft.SqlTools.LanguageSupport /// 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]; + // parse current SQL file contents to retrieve a list of errors + ParseOptions parseOptions = new ParseOptions(); + ParseResult parseResult = Parser.IncrementalParse( + scriptFile.Contents, + prevParseResult, + parseOptions); + + // save previous result for next incremental parse + this.prevParseResult = parseResult; + + // build a list of SQL script file markers from the errors + List markers = new List(); + foreach (var error in parseResult.Errors) + { + markers.Add(new ScriptFileMarker() + { + Message = error.Message, + Level = ScriptFileMarkerLevel.Error, + ScriptRegion = new ScriptRegion() + { + File = scriptFile.FilePath, + StartLineNumber = error.Start.LineNumber, + StartColumnNumber = error.Start.ColumnNumber, + StartOffset = 0, + EndLineNumber = error.End.LineNumber, + EndColumnNumber = error.End.ColumnNumber, + EndOffset = 0 + } + }); + } + + return markers.ToArray(); } } } diff --git a/src/ServiceHost/Server/LanguageServer.cs b/src/ServiceHost/Server/LanguageServer.cs index d6719141..5dbbc310 100644 --- a/src/ServiceHost/Server/LanguageServer.cs +++ b/src/ServiceHost/Server/LanguageServer.cs @@ -122,7 +122,7 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server /// /// /// - protected Task HandleDidChangeTextDocumentNotification( + protected async Task HandleDidChangeTextDocumentNotification( DidChangeTextDocumentParams textChangeParams, EventContext eventContext) { @@ -133,7 +133,7 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server // A text change notification can batch multiple change requests foreach (var textChange in textChangeParams.ContentChanges) { - string fileUri = textChangeParams.TextDocument.Uri; + string fileUri = textChangeParams.Uri ?? textChangeParams.TextDocument.Uri; msg.AppendLine(); msg.Append(" File: "); msg.Append(fileUri); @@ -150,12 +150,12 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server Logger.Write(LogLevel.Verbose, msg.ToString()); - this.RunScriptDiagnostics( + await this.RunScriptDiagnostics( changedFiles.ToArray(), editorSession, eventContext); - return Task.FromResult(true); + await Task.FromResult(true); } protected Task HandleDidOpenTextDocumentNotification( diff --git a/src/ServiceHost/project.json b/src/ServiceHost/project.json index 11340892..91353567 100644 --- a/src/ServiceHost/project.json +++ b/src/ServiceHost/project.json @@ -5,7 +5,8 @@ "emitEntryPoint": true }, "dependencies": { - "Newtonsoft.Json": "9.0.1" + "Newtonsoft.Json": "9.0.1", + "Microsoft.SqlServer.SqlParser": "140.1.3" }, "frameworks": { "netcoreapp1.0": { From 3a55333598e402de85d265eed02f8b424bca2e9c Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Sat, 23 Jul 2016 17:05:49 -0700 Subject: [PATCH 05/12] Add tests for the language service diagnostics --- src/ServiceHost/Workspace/ScriptFile.cs | 21 +++- .../LanguageServer/LanguageServiceTests.cs | 116 ++++++++++++++++++ 2 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 test/ServiceHost.Test/LanguageServer/LanguageServiceTests.cs diff --git a/src/ServiceHost/Workspace/ScriptFile.cs b/src/ServiceHost/Workspace/ScriptFile.cs index 90d66244..166b50ea 100644 --- a/src/ServiceHost/Workspace/ScriptFile.cs +++ b/src/ServiceHost/Workspace/ScriptFile.cs @@ -106,6 +106,13 @@ namespace Microsoft.SqlTools.EditorServices #region Constructors + /// + /// Add a default constructor for testing + /// + public ScriptFile() + { + } + /// /// Creates a new ScriptFile instance by reading file contents from /// the given TextReader. @@ -433,11 +440,11 @@ namespace Microsoft.SqlTools.EditorServices return new BufferRange(startPosition, endPosition); } - #endregion - - #region Private Methods - - private void SetFileContents(string fileContents) + /// + /// Set the script files contents + /// + /// + public void SetFileContents(string fileContents) { // Split the file contents into lines and trim // any carriage returns from the strings. @@ -451,6 +458,10 @@ namespace Microsoft.SqlTools.EditorServices this.ParseFileContents(); } + #endregion + + #region Private Methods + /// /// Parses the current file contents to get the AST, tokens, /// and parse errors. diff --git a/test/ServiceHost.Test/LanguageServer/LanguageServiceTests.cs b/test/ServiceHost.Test/LanguageServer/LanguageServiceTests.cs new file mode 100644 index 00000000..70fd2acd --- /dev/null +++ b/test/ServiceHost.Test/LanguageServer/LanguageServiceTests.cs @@ -0,0 +1,116 @@ +// +// 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; +using Microsoft.SqlTools.LanguageSupport; +using Xunit; + +namespace Microsoft.SqlTools.Test.LanguageServer +{ + /// + /// Tests for the ServiceHost Language Service tests + /// + public class LanguageServiceTests + { + /// + /// Create a test language service instance + /// + /// + private LanguageService CreateTestService() + { + return new LanguageService(new SqlToolsContext(null, null)); + } + + #region "Diagnostics tests" + + /// + /// Verify that the SQL parser correctly detects errors in text + /// + [Fact] + public void ParseSelectStatementWithoutErrors() + { + // sql statement with no errors + const string sqlWithErrors = "SELECT * FROM sys.objects"; + + // get the test service + LanguageService service = CreateTestService(); + + // parse the sql statement + var scriptFile = new ScriptFile(); + scriptFile.SetFileContents(sqlWithErrors); + ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile); + + // verify there are no errors + Assert.Equal(0, fileMarkers.Length); + } + + /// + /// Verify that the SQL parser correctly detects errors in text + /// + [Fact] + public void ParseSelectStatementWithError() + { + // sql statement with errors + const string sqlWithErrors = "SELECT *** FROM sys.objects"; + + // get test service + LanguageService service = CreateTestService(); + + // parse sql statement + var scriptFile = new ScriptFile(); + scriptFile.SetFileContents(sqlWithErrors); + ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile); + + // verify there is one error + Assert.Equal(1, fileMarkers.Length); + + // verify the position of the error + Assert.Equal(9, fileMarkers[0].ScriptRegion.StartColumnNumber); + Assert.Equal(1, fileMarkers[0].ScriptRegion.StartLineNumber); + Assert.Equal(10, fileMarkers[0].ScriptRegion.EndColumnNumber); + Assert.Equal(1, fileMarkers[0].ScriptRegion.EndLineNumber); + } + + /// + /// Verify that the SQL parser correctly detects errors in text + /// + [Fact] + public void ParseMultilineSqlWithErrors() + { + // multiline sql with errors + const string sqlWithErrors = + "SELECT *** FROM sys.objects;\n" + + "GO\n" + + "SELECT *** FROM sys.objects;\n"; + + // get test service + LanguageService service = CreateTestService(); + + // parse sql + var scriptFile = new ScriptFile(); + scriptFile.SetFileContents(sqlWithErrors); + ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile); + + // verify there are two errors + Assert.Equal(2, fileMarkers.Length); + + // check position of first error + Assert.Equal(9, fileMarkers[0].ScriptRegion.StartColumnNumber); + Assert.Equal(1, fileMarkers[0].ScriptRegion.StartLineNumber); + Assert.Equal(10, fileMarkers[0].ScriptRegion.EndColumnNumber); + Assert.Equal(1, fileMarkers[0].ScriptRegion.EndLineNumber); + + // check position of second error + Assert.Equal(9, fileMarkers[1].ScriptRegion.StartColumnNumber); + Assert.Equal(3, fileMarkers[1].ScriptRegion.StartLineNumber); + Assert.Equal(10, fileMarkers[1].ScriptRegion.EndColumnNumber); + Assert.Equal(3, fileMarkers[1].ScriptRegion.EndLineNumber); + } + + #endregion + } +} + From 4484c8d8cf4a61cabfef2cc3eb80bd5e792335ce Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Sun, 24 Jul 2016 02:47:45 -0700 Subject: [PATCH 06/12] Initial implementation for autocomplete. --- .../Connection/ConnectionMessages.cs | 63 +++++++ .../Connection/ConnectionService.cs | 160 ++++++++++++++++++ src/ServiceHost/Connection/ISqlConnection.cs | 34 ++++ src/ServiceHost/Connection/SqlConnection.cs | 70 ++++++++ .../LanguageSupport/AutoCompleteService.cs | 49 ++++++ src/ServiceHost/Server/LanguageServer.cs | 89 +++++++++- src/ServiceHost/project.json | 4 +- .../Connection/ConnectionServiceTests.cs | 63 +++++++ .../LanguageServer/LanguageServiceTests.cs | 34 ++-- .../Message/TestMessageTypes.cs | 1 - test/ServiceHost.Test/Utility/TestObjects.cs | 108 ++++++++++++ test/ServiceHost.Test/project.json | 8 +- 12 files changed, 664 insertions(+), 19 deletions(-) create mode 100644 src/ServiceHost/Connection/ConnectionMessages.cs create mode 100644 src/ServiceHost/Connection/ConnectionService.cs create mode 100644 src/ServiceHost/Connection/ISqlConnection.cs create mode 100644 src/ServiceHost/Connection/SqlConnection.cs create mode 100644 src/ServiceHost/LanguageSupport/AutoCompleteService.cs create mode 100644 test/ServiceHost.Test/Connection/ConnectionServiceTests.cs create mode 100644 test/ServiceHost.Test/Utility/TestObjects.cs diff --git a/src/ServiceHost/Connection/ConnectionMessages.cs b/src/ServiceHost/Connection/ConnectionMessages.cs new file mode 100644 index 00000000..814e55f0 --- /dev/null +++ b/src/ServiceHost/Connection/ConnectionMessages.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. +// + +using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol; + +namespace Microsoft.SqlTools.EditorServices.Connection +{ + /// + /// Message format for the initial connection request + /// + public class ConnectionDetails + { + /// + /// Gets or sets the connection server name + /// + public string ServerName { get; set; } + + /// + /// Gets or sets the connection database name + /// + public string DatabaseName { get; set; } + + /// + /// Gets or sets the connection user name + /// + public string UserName { get; set; } + + /// + /// Gets or sets the connection password + /// + /// + public string Password { get; set; } + } + + /// + /// Message format for the connection result response + /// + public class ConnectionResult + { + /// + /// Gets or sets the connection id + /// + public int ConnectionId { get; set; } + + /// + /// Gets or sets any connection error messages + /// + public string Messages { get; set; } + } + + /// + /// Connect request mapping entry + /// + public class ConnectionRequest + { + public static readonly + RequestType Type = + RequestType.Create("connection/connect"); + } + +} diff --git a/src/ServiceHost/Connection/ConnectionService.cs b/src/ServiceHost/Connection/ConnectionService.cs new file mode 100644 index 00000000..8f0634a7 --- /dev/null +++ b/src/ServiceHost/Connection/ConnectionService.cs @@ -0,0 +1,160 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Data.SqlClient; +using System.Threading.Tasks; + +namespace Microsoft.SqlTools.EditorServices.Connection +{ + /// + /// Main class for the Connection Management services + /// + public class ConnectionService + { + /// + /// Singleton service instance + /// + private static Lazy instance + = new Lazy(() => new ConnectionService()); + + /// + /// The SQL connection factory object + /// + private ISqlConnectionFactory connectionFactory; + + /// + /// The current connection id that was previously used + /// + private int maxConnectionId = 0; + + /// + /// Active connections lazy dictionary instance + /// + private Lazy> activeConnections + = new Lazy>(() + => new Dictionary()); + + /// + /// Callback for onconnection handler + /// + /// + public delegate Task OnConnectionHandler(ISqlConnection sqlConnection); + + /// + /// List of onconnection handlers + /// + private readonly List onConnectionActivities = new List(); + + /// + /// Gets the active connection map + /// + public Dictionary ActiveConnections + { + get + { + return activeConnections.Value; + } + } + + /// + /// Gets the singleton service instance + /// + public static ConnectionService Instance + { + get + { + return instance.Value; + } + } + + /// + /// Gets the SQL connection factory instance + /// + public ISqlConnectionFactory ConnectionFactory + { + get + { + if (this.connectionFactory == null) + { + this.connectionFactory = new SqlConnectionFactory(); + } + return this.connectionFactory; + } + } + + /// + /// Default constructor is private since it's a singleton class + /// + private ConnectionService() + { + } + + /// + /// Test constructor that injects dependency interfaces + /// + /// + public ConnectionService(ISqlConnectionFactory testFactory) + { + this.connectionFactory = testFactory; + } + + /// + /// Open a connection with the specified connection details + /// + /// + public ConnectionResult Connect(ConnectionDetails connectionDetails) + { + // build the connection string from the input parameters + string connectionString = BuildConnectionString(connectionDetails); + + // create a sql connection instance + ISqlConnection connection = this.ConnectionFactory.CreateSqlConnection(); + + // open the database + connection.OpenDatabaseConnection(connectionString); + + // map the connection id to the connection object for future lookups + this.ActiveConnections.Add(++maxConnectionId, connection); + + // invoke callback notifications + foreach (var activity in this.onConnectionActivities) + { + activity(connection); + } + + // return the connection result + return new ConnectionResult() + { + ConnectionId = maxConnectionId + }; + } + + /// + /// Add a new method to be called when the onconnection request is submitted + /// + /// + public void RegisterOnConnectionTask(OnConnectionHandler activity) + { + onConnectionActivities.Add(activity); + } + + /// + /// Build a connection string from a connection details instance + /// + /// + private string BuildConnectionString(ConnectionDetails connectionDetails) + { + SqlConnectionStringBuilder connectionBuilder = new SqlConnectionStringBuilder(); + connectionBuilder["Data Source"] = connectionDetails.ServerName; + connectionBuilder["Integrated Security"] = false; + connectionBuilder["User Id"] = connectionDetails.UserName; + connectionBuilder["Password"] = connectionDetails.Password; + connectionBuilder["Initial Catalog"] = connectionDetails.DatabaseName; + return connectionBuilder.ToString(); + } + } +} diff --git a/src/ServiceHost/Connection/ISqlConnection.cs b/src/ServiceHost/Connection/ISqlConnection.cs new file mode 100644 index 00000000..4ef80c5e --- /dev/null +++ b/src/ServiceHost/Connection/ISqlConnection.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.Collections.Generic; + +namespace Microsoft.SqlTools.EditorServices.Connection +{ + /// + /// Interface for the SQL Connection factory + /// + public interface ISqlConnectionFactory + { + /// + /// Create a new SQL Connection object + /// + ISqlConnection CreateSqlConnection(); + } + + /// + /// Interface for the SQL Connection wrapper + /// + public interface ISqlConnection + { + /// + /// Open a connection to the provided connection string + /// + /// + void OpenDatabaseConnection(string connectionString); + + IEnumerable GetServerObjects(); + } +} diff --git a/src/ServiceHost/Connection/SqlConnection.cs b/src/ServiceHost/Connection/SqlConnection.cs new file mode 100644 index 00000000..74104fb9 --- /dev/null +++ b/src/ServiceHost/Connection/SqlConnection.cs @@ -0,0 +1,70 @@ +// +// 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.Data; +using System.Data.SqlClient; + +namespace Microsoft.SqlTools.EditorServices.Connection +{ + /// + /// Factory class to create SqlClientConnections + /// The purpose of the factory is to make it easier to mock out the database + /// in 'offline' unit test scenarios. + /// + public class SqlConnectionFactory : ISqlConnectionFactory + { + /// + /// Creates a new SqlClientConnection object + /// + public ISqlConnection CreateSqlConnection() + { + return new SqlClientConnection(); + } + } + + /// + /// Wrapper class that implements ISqlConnection and hosts a SqlConnection. + /// This wrapper exists primarily for decoupling to support unit testing. + /// + public class SqlClientConnection : ISqlConnection + { + /// + /// the underlying SQL connection + /// + private SqlConnection connection; + + /// + /// Opens a SqlConnection using provided connection string + /// + /// + public void OpenDatabaseConnection(string connectionString) + { + this.connection = new SqlConnection(connectionString); + this.connection.Open(); + } + + /// + /// Gets a list of database server schema objects + /// + /// + public IEnumerable GetServerObjects() + { + SqlCommand command = connection.CreateCommand(); + command.CommandText = "SELECT name FROM sys.objects"; + command.CommandTimeout = 15; + command.CommandType = CommandType.Text; + var reader = command.ExecuteReader(); + + List results = new List(); + while (reader.Read()) + { + results.Add(reader[0].ToString()); + } + + return results; + } + } +} diff --git a/src/ServiceHost/LanguageSupport/AutoCompleteService.cs b/src/ServiceHost/LanguageSupport/AutoCompleteService.cs new file mode 100644 index 00000000..48442b64 --- /dev/null +++ b/src/ServiceHost/LanguageSupport/AutoCompleteService.cs @@ -0,0 +1,49 @@ +// +// 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.Connection; +using System; +using System.Collections.Generic; + +namespace Microsoft.SqlTools.LanguageSupport +{ + /// + /// Main class for Autocomplete functionality + /// + public class AutoCompleteService + { + /// + /// Singleton service instance + /// + private static Lazy instance + = new Lazy(() => new AutoCompleteService()); + + private IEnumerable autoCompleteList; + + public IEnumerable AutoCompleteList + { + get + { + return this.autoCompleteList; + } + } + + /// + /// Gets the singleton service instance + /// + public static AutoCompleteService Instance + { + get + { + return instance.Value; + } + } + + public void UpdateAutoCompleteCache(ISqlConnection connection) + { + this.autoCompleteList = connection.GetServerObjects(); + } + } +} diff --git a/src/ServiceHost/Server/LanguageServer.cs b/src/ServiceHost/Server/LanguageServer.cs index 5dbbc310..eb054b45 100644 --- a/src/ServiceHost/Server/LanguageServer.cs +++ b/src/ServiceHost/Server/LanguageServer.cs @@ -13,6 +13,8 @@ using System.Text; using System.Threading; using System.Linq; using System; +using Microsoft.SqlTools.EditorServices.Connection; +using Microsoft.SqlTools.LanguageSupport; namespace Microsoft.SqlTools.EditorServices.Protocol.Server { @@ -57,7 +59,22 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server this.SetRequestHandler(DocumentHighlightRequest.Type, this.HandleDocumentHighlightRequest); this.SetRequestHandler(HoverRequest.Type, this.HandleHoverRequest); this.SetRequestHandler(DocumentSymbolRequest.Type, this.HandleDocumentSymbolRequest); - this.SetRequestHandler(WorkspaceSymbolRequest.Type, this.HandleWorkspaceSymbolRequest); + this.SetRequestHandler(WorkspaceSymbolRequest.Type, this.HandleWorkspaceSymbolRequest); + + this.SetRequestHandler(ConnectionRequest.Type, this.HandleConnectRequest); + + // register an OnConnection callback + ConnectionService.Instance.RegisterOnConnectionTask(OnConnection); + } + + /// + /// Callback for when a user connection is done processing + /// + /// + public Task OnConnection(ISqlConnection sqlConnection) + { + AutoCompleteService.Instance.UpdateAutoCompleteCache(sqlConnection); + return Task.FromResult(true); } /// @@ -245,7 +262,57 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server RequestContext requestContext) { Logger.Write(LogLevel.Verbose, "HandleCompletionRequest"); - await Task.FromResult(true); + + var connectionService = ConnectionService.Instance; + if (connectionService.ActiveConnections.Count > 0) + { + AutoCompleteService.Instance.UpdateAutoCompleteCache( + connectionService.ActiveConnections.First().Value); + } + + var autoCompleteList = AutoCompleteService.Instance.AutoCompleteList; + var completions = new List(); + + int i = 0; + if (autoCompleteList != null) + { + foreach (var autoCompleteItem in autoCompleteList) + { + completions.Add(new CompletionItem() + { + Label = autoCompleteItem, + Kind = CompletionItemKind.Keyword, + Detail = autoCompleteItem + " details", + Documentation = autoCompleteItem + " documentation", + //SortText = "SortText", + TextEdit = new TextEdit + { + NewText = "New Text", + Range = new Range + { + Start = new Position + { + Line = textDocumentPosition.Position.Line, + Character = textDocumentPosition.Position.Character + }, + End = new Position + { + Line = textDocumentPosition.Position.Line, + Character = textDocumentPosition.Position.Character + 5 + } + } + } + }); + + // only show 50 items + if (++i == 50) + { + break; + } + } + } + + await requestContext.SendResult(completions.ToArray()); } protected async Task HandleCompletionResolveRequest( @@ -296,6 +363,24 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server await Task.FromResult(true); } + /// + /// Handle new connection requests + /// + /// + /// + /// + protected async Task HandleConnectRequest( + ConnectionDetails connectionDetails, + RequestContext requestContext) + { + Logger.Write(LogLevel.Verbose, "HandleConnectRequest"); + + // open connection base on request details + ConnectionResult result = ConnectionService.Instance.Connect(connectionDetails); + + await requestContext.SendResult(result); + } + /// /// Runs script diagnostics on changed files /// diff --git a/src/ServiceHost/project.json b/src/ServiceHost/project.json index 91353567..31dac66d 100644 --- a/src/ServiceHost/project.json +++ b/src/ServiceHost/project.json @@ -6,7 +6,9 @@ }, "dependencies": { "Newtonsoft.Json": "9.0.1", - "Microsoft.SqlServer.SqlParser": "140.1.3" + "Microsoft.SqlServer.SqlParser": "140.1.3", + "System.Data.Common": "4.1.0", + "System.Data.SqlClient": "4.1.0" }, "frameworks": { "netcoreapp1.0": { diff --git a/test/ServiceHost.Test/Connection/ConnectionServiceTests.cs b/test/ServiceHost.Test/Connection/ConnectionServiceTests.cs new file mode 100644 index 00000000..b7356b28 --- /dev/null +++ b/test/ServiceHost.Test/Connection/ConnectionServiceTests.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. +// + +#define USE_LIVE_CONNECTION + +using System.Threading.Tasks; +using Microsoft.SqlTools.Test.Utility; +using Xunit; + +namespace Microsoft.SqlTools.Test.Connection +{ + /// + /// Tests for the ServiceHost Connection Service tests + /// + public class ConnectionServiceTests + { + #region "Connection tests" + + /// + /// Verify that the SQL parser correctly detects errors in text + /// + [Fact] + public void ConnectToDatabaseTest() + { + // connect to a database instance + var connectionResult = + TestObjects.GetTestConnectionService() + .Connect(TestObjects.GetTestConnectionDetails()); + + // verify that a valid connection id was returned + Assert.True(connectionResult.ConnectionId > 0); + } + + /// + /// Verify that the SQL parser correctly detects errors in text + /// + [Fact] + public void OnConnectionCallbackHandlerTest() + { + bool callbackInvoked = false; + + // setup connection service with callback + var connectionService = TestObjects.GetTestConnectionService(); + connectionService.RegisterOnConnectionTask( + (sqlConnection) => { + callbackInvoked = true; + return Task.FromResult(true); + } + ); + + // connect to a database instance + var connectionResult = TestObjects.GetTestConnectionService() + .Connect(TestObjects.GetTestConnectionDetails()); + + // verify that a valid connection id was returned + Assert.True(callbackInvoked); + } + + #endregion + } +} diff --git a/test/ServiceHost.Test/LanguageServer/LanguageServiceTests.cs b/test/ServiceHost.Test/LanguageServer/LanguageServiceTests.cs index 70fd2acd..36aac6b4 100644 --- a/test/ServiceHost.Test/LanguageServer/LanguageServiceTests.cs +++ b/test/ServiceHost.Test/LanguageServer/LanguageServiceTests.cs @@ -6,6 +6,8 @@ using Microsoft.SqlTools.EditorServices; using Microsoft.SqlTools.EditorServices.Session; using Microsoft.SqlTools.LanguageSupport; +using Microsoft.SqlTools.Test.Connection; +using Microsoft.SqlTools.Test.Utility; using Xunit; namespace Microsoft.SqlTools.Test.LanguageServer @@ -15,15 +17,6 @@ namespace Microsoft.SqlTools.Test.LanguageServer /// public class LanguageServiceTests { - /// - /// Create a test language service instance - /// - /// - private LanguageService CreateTestService() - { - return new LanguageService(new SqlToolsContext(null, null)); - } - #region "Diagnostics tests" /// @@ -36,7 +29,7 @@ namespace Microsoft.SqlTools.Test.LanguageServer const string sqlWithErrors = "SELECT * FROM sys.objects"; // get the test service - LanguageService service = CreateTestService(); + LanguageService service = TestObjects.GetTestLanguageService(); // parse the sql statement var scriptFile = new ScriptFile(); @@ -57,7 +50,7 @@ namespace Microsoft.SqlTools.Test.LanguageServer const string sqlWithErrors = "SELECT *** FROM sys.objects"; // get test service - LanguageService service = CreateTestService(); + LanguageService service = TestObjects.GetTestLanguageService(); // parse sql statement var scriptFile = new ScriptFile(); @@ -87,7 +80,7 @@ namespace Microsoft.SqlTools.Test.LanguageServer "SELECT *** FROM sys.objects;\n"; // get test service - LanguageService service = CreateTestService(); + LanguageService service = TestObjects.GetTestLanguageService(); // parse sql var scriptFile = new ScriptFile(); @@ -111,6 +104,23 @@ namespace Microsoft.SqlTools.Test.LanguageServer } #endregion + + #region "Autocomplete Tests" + + /// + /// Verify that the SQL parser correctly detects errors in text + /// + [Fact] + public void AutocompleteTest() + { + var autocompleteService = TestObjects.GetAutoCompleteService(); + var connectionService = TestObjects.GetTestConnectionService(); + var connectionResult = connectionService.Connect(TestObjects.GetTestConnectionDetails()); + var sqlConnection = connectionService.ActiveConnections[connectionResult.ConnectionId]; + autocompleteService.UpdateAutoCompleteCache(sqlConnection); + } + + #endregion } } diff --git a/test/ServiceHost.Test/Message/TestMessageTypes.cs b/test/ServiceHost.Test/Message/TestMessageTypes.cs index cc5981dc..75cf1291 100644 --- a/test/ServiceHost.Test/Message/TestMessageTypes.cs +++ b/test/ServiceHost.Test/Message/TestMessageTypes.cs @@ -4,7 +4,6 @@ // using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol; -using System; using System.Threading.Tasks; namespace Microsoft.SqlTools.EditorServices.Test.Protocol.MessageProtocol diff --git a/test/ServiceHost.Test/Utility/TestObjects.cs b/test/ServiceHost.Test/Utility/TestObjects.cs new file mode 100644 index 00000000..22fc3f4d --- /dev/null +++ b/test/ServiceHost.Test/Utility/TestObjects.cs @@ -0,0 +1,108 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +//#define USE_LIVE_CONNECTION + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.SqlTools.EditorServices.Connection; +using Microsoft.SqlTools.EditorServices.Session; +using Microsoft.SqlTools.LanguageSupport; +using Xunit; + +namespace Microsoft.SqlTools.Test.Utility +{ + /// + /// Tests for the ServiceHost Connection Service tests + /// + public class TestObjects + { + /// + /// Creates a test connection service + /// + public static ConnectionService GetTestConnectionService() + { +#if !USE_LIVE_CONNECTION + // use mock database connection + return new ConnectionService(new TestSqlConnectionFactory()); +#else + // connect to a real server instance + return ConnectionService.Instance; +#endif + } + + /// + /// Creates a test connection details object + /// + public static ConnectionDetails GetTestConnectionDetails() + { + return new ConnectionDetails() + { + UserName = "sa", + Password = "Yukon900", + DatabaseName = "AdventureWorks2016CTP3_2", + ServerName = "sqltools11" + }; + } + + /// + /// Create a test language service instance + /// + /// + public static LanguageService GetTestLanguageService() + { + return new LanguageService(new SqlToolsContext(null, null)); + } + + /// + /// Creates a test autocomplete service instance + /// + public static AutoCompleteService GetAutoCompleteService() + { + return AutoCompleteService.Instance; + } + + /// + /// Creates a test sql connection factory instance + /// + public static ISqlConnectionFactory GetTestSqlConnectionFactory() + { +#if !USE_LIVE_CONNECTION + // use mock database connection + return new TestSqlConnectionFactory(); +#else + // connect to a real server instance + return ConnectionService.Instance.ConnectionFactory; +#endif + + } + } + + /// + /// Test mock class for SqlConnection wrapper + /// + public class TestSqlConnection : ISqlConnection + { + public void OpenDatabaseConnection(string connectionString) + { + } + + public IEnumerable GetServerObjects() + { + return null; + } + } + + /// + /// Test mock class for SqlConnection factory + /// + public class TestSqlConnectionFactory : ISqlConnectionFactory + { + public ISqlConnection CreateSqlConnection() + { + return new TestSqlConnection(); + } + } +} diff --git a/test/ServiceHost.Test/project.json b/test/ServiceHost.Test/project.json index b7f00724..947660d5 100644 --- a/test/ServiceHost.Test/project.json +++ b/test/ServiceHost.Test/project.json @@ -6,11 +6,13 @@ "dependencies": { "Newtonsoft.Json": "9.0.1", "System.Runtime.Serialization.Primitives": "4.1.1", + "System.Data.Common": "4.1.0", + "System.Data.SqlClient": "4.1.0", "xunit": "2.1.0", "dotnet-test-xunit": "1.0.0-rc2-192208-24", - "ServiceHost": { - "target": "project" - } + "ServiceHost": { + "target": "project" + } }, "testRunner": "xunit", "frameworks": { From 4cebc196ff40ad509d87abd49ebde533fde5ff5a Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Sun, 24 Jul 2016 13:37:10 -0700 Subject: [PATCH 07/12] Switch to using sys.tables for autocomplete Move some code into a better class --- src/ServiceHost/Connection/SqlConnection.cs | 4 +- .../LanguageSupport/AutoCompleteService.cs | 63 ++++++++++++++ src/ServiceHost/Server/LanguageServer.cs | 83 +++++++------------ 3 files changed, 98 insertions(+), 52 deletions(-) diff --git a/src/ServiceHost/Connection/SqlConnection.cs b/src/ServiceHost/Connection/SqlConnection.cs index 74104fb9..5bb92553 100644 --- a/src/ServiceHost/Connection/SqlConnection.cs +++ b/src/ServiceHost/Connection/SqlConnection.cs @@ -52,8 +52,10 @@ namespace Microsoft.SqlTools.EditorServices.Connection /// public IEnumerable GetServerObjects() { + // Select the values from sys.tables to give a super basic + // autocomplete experience. This will be replaced by SMO. SqlCommand command = connection.CreateCommand(); - command.CommandText = "SELECT name FROM sys.objects"; + command.CommandText = "SELECT name FROM sys.tables"; command.CommandTimeout = 15; command.CommandType = CommandType.Text; var reader = command.ExecuteReader(); diff --git a/src/ServiceHost/LanguageSupport/AutoCompleteService.cs b/src/ServiceHost/LanguageSupport/AutoCompleteService.cs index 48442b64..2cf98484 100644 --- a/src/ServiceHost/LanguageSupport/AutoCompleteService.cs +++ b/src/ServiceHost/LanguageSupport/AutoCompleteService.cs @@ -4,6 +4,7 @@ // using Microsoft.SqlTools.EditorServices.Connection; +using Microsoft.SqlTools.EditorServices.Protocol.LanguageServer; using System; using System.Collections.Generic; @@ -20,8 +21,14 @@ namespace Microsoft.SqlTools.LanguageSupport private static Lazy instance = new Lazy(() => new AutoCompleteService()); + /// + /// The current autocomplete candidate list + /// private IEnumerable autoCompleteList; + /// + /// Gets the current autocomplete candidate list + /// public IEnumerable AutoCompleteList { get @@ -41,9 +48,65 @@ namespace Microsoft.SqlTools.LanguageSupport } } + /// + /// Update the cached autocomplete candidate list when the user connects to a database + /// + /// public void UpdateAutoCompleteCache(ISqlConnection connection) { this.autoCompleteList = connection.GetServerObjects(); } + + /// + /// Return the completion item list for the current text position + /// + /// + public CompletionItem[] GetCompletionItems(TextDocumentPosition textDocumentPosition) + { + var completions = new List(); + + int i = 0; + + // the completion list will be null is user not connected to server + if (this.AutoCompleteList != null) + { + foreach (var autoCompleteItem in this.AutoCompleteList) + { + // convert the completion item candidates into CompletionItems + completions.Add(new CompletionItem() + { + Label = autoCompleteItem, + Kind = CompletionItemKind.Keyword, + Detail = autoCompleteItem + " details", + Documentation = autoCompleteItem + " documentation", + TextEdit = new TextEdit + { + NewText = autoCompleteItem, + Range = new Range + { + Start = new Position + { + Line = textDocumentPosition.Position.Line, + Character = textDocumentPosition.Position.Character + }, + End = new Position + { + Line = textDocumentPosition.Position.Line, + Character = textDocumentPosition.Position.Character + 5 + } + } + } + }); + + // only show 50 items + if (++i == 50) + { + break; + } + } + } + return completions.ToArray(); + } + } } diff --git a/src/ServiceHost/Server/LanguageServer.cs b/src/ServiceHost/Server/LanguageServer.cs index eb054b45..c2952f92 100644 --- a/src/ServiceHost/Server/LanguageServer.cs +++ b/src/ServiceHost/Server/LanguageServer.cs @@ -175,15 +175,38 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server await Task.FromResult(true); } + /// + /// Handle the file open notification + /// + /// + /// protected Task HandleDidOpenTextDocumentNotification( DidOpenTextDocumentNotification openParams, EventContext eventContext) { Logger.Write(LogLevel.Verbose, "HandleDidOpenTextDocumentNotification"); + + // read the SQL file contents into the ScriptFile + ScriptFile openedFile = + editorSession.Workspace.GetFileBuffer( + openParams.Uri, + openParams.Text); + + // run diagnostics on the opened file + this.RunScriptDiagnostics( + new ScriptFile[] { openedFile }, + editorSession, + eventContext); + return Task.FromResult(true); } - protected Task HandleDidCloseTextDocumentNotification( + /// + /// Handle the close document notication + /// + /// + /// + protected Task HandleDidCloseTextDocumentNotification( TextDocumentIdentifier closeParams, EventContext eventContext) { @@ -257,62 +280,20 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server await Task.FromResult(true); } + /// + /// Handles the completion list request + /// + /// + /// protected async Task HandleCompletionRequest( TextDocumentPosition textDocumentPosition, RequestContext requestContext) { Logger.Write(LogLevel.Verbose, "HandleCompletionRequest"); - var connectionService = ConnectionService.Instance; - if (connectionService.ActiveConnections.Count > 0) - { - AutoCompleteService.Instance.UpdateAutoCompleteCache( - connectionService.ActiveConnections.First().Value); - } - - var autoCompleteList = AutoCompleteService.Instance.AutoCompleteList; - var completions = new List(); - - int i = 0; - if (autoCompleteList != null) - { - foreach (var autoCompleteItem in autoCompleteList) - { - completions.Add(new CompletionItem() - { - Label = autoCompleteItem, - Kind = CompletionItemKind.Keyword, - Detail = autoCompleteItem + " details", - Documentation = autoCompleteItem + " documentation", - //SortText = "SortText", - TextEdit = new TextEdit - { - NewText = "New Text", - Range = new Range - { - Start = new Position - { - Line = textDocumentPosition.Position.Line, - Character = textDocumentPosition.Position.Character - }, - End = new Position - { - Line = textDocumentPosition.Position.Line, - Character = textDocumentPosition.Position.Character + 5 - } - } - } - }); - - // only show 50 items - if (++i == 50) - { - break; - } - } - } - - await requestContext.SendResult(completions.ToArray()); + // get teh current list of completion items and return to client + var completionItems = AutoCompleteService.Instance.GetCompletionItems(textDocumentPosition); + await requestContext.SendResult(completionItems); } protected async Task HandleCompletionResolveRequest( From ffc89231f4c0ec9a5180625a3c728692c65b42ce Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Sun, 24 Jul 2016 13:38:44 -0700 Subject: [PATCH 08/12] Delete unused csproj file. --- ...erShellEditorServices.Test.Protocol.csproj | 109 ------------------ 1 file changed, 109 deletions(-) delete mode 100644 test/ServiceHost.Test/PowerShellEditorServices.Test.Protocol.csproj diff --git a/test/ServiceHost.Test/PowerShellEditorServices.Test.Protocol.csproj b/test/ServiceHost.Test/PowerShellEditorServices.Test.Protocol.csproj deleted file mode 100644 index 54e20896..00000000 --- a/test/ServiceHost.Test/PowerShellEditorServices.Test.Protocol.csproj +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - - 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 From 6464240f0b73ab05be5a5ec76d97eefcbb661314 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Mon, 25 Jul 2016 12:58:44 -0700 Subject: [PATCH 09/12] Add nuget.config to pickup SQL Parser nuget package --- nuget.config | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 nuget.config diff --git a/nuget.config b/nuget.config new file mode 100644 index 00000000..33539216 --- /dev/null +++ b/nuget.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + From ee2dc04e398407c63000f72014026d7a3efe9abe Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Mon, 25 Jul 2016 18:10:42 -0700 Subject: [PATCH 10/12] Fix merge build breaks --- .../LanguageSupport/AutoCompleteService.cs | 112 ---- .../LanguageSupport/LanguageService.cs | 78 --- src/ServiceHost/Server/LanguageServer.cs | 583 ------------------ test/ServiceHost.Test/Utility/TestObjects.cs | 2 +- 4 files changed, 1 insertion(+), 774 deletions(-) delete mode 100644 src/ServiceHost/LanguageSupport/AutoCompleteService.cs delete mode 100644 src/ServiceHost/LanguageSupport/LanguageService.cs delete mode 100644 src/ServiceHost/Server/LanguageServer.cs diff --git a/src/ServiceHost/LanguageSupport/AutoCompleteService.cs b/src/ServiceHost/LanguageSupport/AutoCompleteService.cs deleted file mode 100644 index 2cf98484..00000000 --- a/src/ServiceHost/LanguageSupport/AutoCompleteService.cs +++ /dev/null @@ -1,112 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using Microsoft.SqlTools.EditorServices.Connection; -using Microsoft.SqlTools.EditorServices.Protocol.LanguageServer; -using System; -using System.Collections.Generic; - -namespace Microsoft.SqlTools.LanguageSupport -{ - /// - /// Main class for Autocomplete functionality - /// - public class AutoCompleteService - { - /// - /// Singleton service instance - /// - private static Lazy instance - = new Lazy(() => new AutoCompleteService()); - - /// - /// The current autocomplete candidate list - /// - private IEnumerable autoCompleteList; - - /// - /// Gets the current autocomplete candidate list - /// - public IEnumerable AutoCompleteList - { - get - { - return this.autoCompleteList; - } - } - - /// - /// Gets the singleton service instance - /// - public static AutoCompleteService Instance - { - get - { - return instance.Value; - } - } - - /// - /// Update the cached autocomplete candidate list when the user connects to a database - /// - /// - public void UpdateAutoCompleteCache(ISqlConnection connection) - { - this.autoCompleteList = connection.GetServerObjects(); - } - - /// - /// Return the completion item list for the current text position - /// - /// - public CompletionItem[] GetCompletionItems(TextDocumentPosition textDocumentPosition) - { - var completions = new List(); - - int i = 0; - - // the completion list will be null is user not connected to server - if (this.AutoCompleteList != null) - { - foreach (var autoCompleteItem in this.AutoCompleteList) - { - // convert the completion item candidates into CompletionItems - completions.Add(new CompletionItem() - { - Label = autoCompleteItem, - Kind = CompletionItemKind.Keyword, - Detail = autoCompleteItem + " details", - Documentation = autoCompleteItem + " documentation", - TextEdit = new TextEdit - { - NewText = autoCompleteItem, - Range = new Range - { - Start = new Position - { - Line = textDocumentPosition.Position.Line, - Character = textDocumentPosition.Position.Character - }, - End = new Position - { - Line = textDocumentPosition.Position.Line, - Character = textDocumentPosition.Position.Character + 5 - } - } - } - }); - - // only show 50 items - if (++i == 50) - { - break; - } - } - } - return completions.ToArray(); - } - - } -} diff --git a/src/ServiceHost/LanguageSupport/LanguageService.cs b/src/ServiceHost/LanguageSupport/LanguageService.cs deleted file mode 100644 index ff4f4a65..00000000 --- a/src/ServiceHost/LanguageSupport/LanguageService.cs +++ /dev/null @@ -1,78 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using Microsoft.SqlTools.EditorServices; -using Microsoft.SqlTools.EditorServices.Session; -using Microsoft.SqlServer.Management.SqlParser.Parser; -using System.Collections.Generic; - -namespace Microsoft.SqlTools.LanguageSupport -{ - /// - /// Main class for Language Service functionality - /// - public class LanguageService - { - /// - /// The cached parse result from previous incremental parse - /// - private ParseResult prevParseResult; - - /// - /// 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) - { - // parse current SQL file contents to retrieve a list of errors - ParseOptions parseOptions = new ParseOptions(); - ParseResult parseResult = Parser.IncrementalParse( - scriptFile.Contents, - prevParseResult, - parseOptions); - - // save previous result for next incremental parse - this.prevParseResult = parseResult; - - // build a list of SQL script file markers from the errors - List markers = new List(); - foreach (var error in parseResult.Errors) - { - markers.Add(new ScriptFileMarker() - { - Message = error.Message, - Level = ScriptFileMarkerLevel.Error, - ScriptRegion = new ScriptRegion() - { - File = scriptFile.FilePath, - StartLineNumber = error.Start.LineNumber, - StartColumnNumber = error.Start.ColumnNumber, - StartOffset = 0, - EndLineNumber = error.End.LineNumber, - EndColumnNumber = error.End.ColumnNumber, - EndOffset = 0 - } - }); - } - - return markers.ToArray(); - } - } -} diff --git a/src/ServiceHost/Server/LanguageServer.cs b/src/ServiceHost/Server/LanguageServer.cs deleted file mode 100644 index c2952f92..00000000 --- a/src/ServiceHost/Server/LanguageServer.cs +++ /dev/null @@ -1,583 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// -using 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; -using Microsoft.SqlTools.EditorServices.Connection; -using Microsoft.SqlTools.LanguageSupport; - -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); - - this.SetRequestHandler(ConnectionRequest.Type, this.HandleConnectRequest); - - // register an OnConnection callback - ConnectionService.Instance.RegisterOnConnectionTask(OnConnection); - } - - /// - /// Callback for when a user connection is done processing - /// - /// - public Task OnConnection(ISqlConnection sqlConnection) - { - AutoCompleteService.Instance.UpdateAutoCompleteCache(sqlConnection); - return Task.FromResult(true); - } - - /// - /// 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 async 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.Uri ?? 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()); - - await this.RunScriptDiagnostics( - changedFiles.ToArray(), - editorSession, - eventContext); - - await Task.FromResult(true); - } - - /// - /// Handle the file open notification - /// - /// - /// - protected Task HandleDidOpenTextDocumentNotification( - DidOpenTextDocumentNotification openParams, - EventContext eventContext) - { - Logger.Write(LogLevel.Verbose, "HandleDidOpenTextDocumentNotification"); - - // read the SQL file contents into the ScriptFile - ScriptFile openedFile = - editorSession.Workspace.GetFileBuffer( - openParams.Uri, - openParams.Text); - - // run diagnostics on the opened file - this.RunScriptDiagnostics( - new ScriptFile[] { openedFile }, - editorSession, - eventContext); - - return Task.FromResult(true); - } - - /// - /// Handle the close document notication - /// - /// - /// - 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); - } - - /// - /// Handles the completion list request - /// - /// - /// - protected async Task HandleCompletionRequest( - TextDocumentPosition textDocumentPosition, - RequestContext requestContext) - { - Logger.Write(LogLevel.Verbose, "HandleCompletionRequest"); - - // get teh current list of completion items and return to client - var completionItems = AutoCompleteService.Instance.GetCompletionItems(textDocumentPosition); - await requestContext.SendResult(completionItems); - } - - 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); - } - - /// - /// Handle new connection requests - /// - /// - /// - /// - protected async Task HandleConnectRequest( - ConnectionDetails connectionDetails, - RequestContext requestContext) - { - Logger.Write(LogLevel.Verbose, "HandleConnectRequest"); - - // open connection base on request details - ConnectionResult result = ConnectionService.Instance.Connect(connectionDetails); - - await requestContext.SendResult(result); - } - - /// - /// 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/test/ServiceHost.Test/Utility/TestObjects.cs b/test/ServiceHost.Test/Utility/TestObjects.cs index 4e086279..c506d600 100644 --- a/test/ServiceHost.Test/Utility/TestObjects.cs +++ b/test/ServiceHost.Test/Utility/TestObjects.cs @@ -53,7 +53,7 @@ namespace Microsoft.SqlTools.Test.Utility /// public static LanguageService GetTestLanguageService() { - return new LanguageService(new SqlToolsContext(null, null)); + return new LanguageService(); } /// From 7ff6cc34a03c22b3b82198069179630dd2a8efe3 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Mon, 25 Jul 2016 18:19:20 -0700 Subject: [PATCH 11/12] Fix binary names and add internalsvisible flag for tests --- src/ServiceHost/LanguageServices/LanguageService.cs | 3 +-- src/ServiceHost/Properties/AssemblyInfo.cs | 2 +- src/ServiceHost/project.json | 1 + test/ServiceHost.Test/project.json | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ServiceHost/LanguageServices/LanguageService.cs b/src/ServiceHost/LanguageServices/LanguageService.cs index d40aa5fc..21b89c19 100644 --- a/src/ServiceHost/LanguageServices/LanguageService.cs +++ b/src/ServiceHost/LanguageServices/LanguageService.cs @@ -38,9 +38,8 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// Default, parameterless constructor. - /// TODO: Figure out how to make this truely singleton even with dependency injection for tests /// - public LanguageService() + internal LanguageService() { } diff --git a/src/ServiceHost/Properties/AssemblyInfo.cs b/src/ServiceHost/Properties/AssemblyInfo.cs index 27c1daba..ee34cfc1 100644 --- a/src/ServiceHost/Properties/AssemblyInfo.cs +++ b/src/ServiceHost/Properties/AssemblyInfo.cs @@ -41,4 +41,4 @@ using System.Runtime.InteropServices; [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0.0")] -[assembly: InternalsVisibleTo("Microsoft.SqlTools.EditorServices.Test.Protocol")] +[assembly: InternalsVisibleTo("Microsoft.SqlTools.ServiceHost.Test")] diff --git a/src/ServiceHost/project.json b/src/ServiceHost/project.json index 31dac66d..bd889b55 100644 --- a/src/ServiceHost/project.json +++ b/src/ServiceHost/project.json @@ -1,4 +1,5 @@ { + "name": "Microsoft.SqlTools.ServiceHost", "version": "1.0.0-*", "buildOptions": { "debugType": "portable", diff --git a/test/ServiceHost.Test/project.json b/test/ServiceHost.Test/project.json index a248b200..a32fe523 100644 --- a/test/ServiceHost.Test/project.json +++ b/test/ServiceHost.Test/project.json @@ -1,4 +1,5 @@ { + "name": "Microsoft.SqlTools.ServiceHost.Test", "version": "1.0.0-*", "buildOptions": { "debugType": "portable" From 818c9fa47f566d62a62f886fcb8bdf22e903be9c Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Tue, 26 Jul 2016 11:43:55 -0700 Subject: [PATCH 12/12] Fix breaks introduced in the refactoring merge. --- src/ServiceHost/Hosting/ServiceHost.cs | 6 ++ .../LanguageServices/LanguageService.cs | 68 ++++++++++++++++++- src/ServiceHost/Program.cs | 8 ++- .../WorkspaceServices/WorkspaceService.cs | 37 +++++++++- 4 files changed, 111 insertions(+), 8 deletions(-) diff --git a/src/ServiceHost/Hosting/ServiceHost.cs b/src/ServiceHost/Hosting/ServiceHost.cs index dbc561fe..0f2a0d9a 100644 --- a/src/ServiceHost/Hosting/ServiceHost.cs +++ b/src/ServiceHost/Hosting/ServiceHost.cs @@ -45,7 +45,13 @@ namespace Microsoft.SqlTools.ServiceLayer.Hosting // Initialize the shutdown activities shutdownCallbacks = new List(); initializeCallbacks = new List(); + } + /// + /// Provide initialization that must occur after the service host is started + /// + public void Initialize() + { // Register the requests that this service host will handle this.SetRequestHandler(InitializeRequest.Type, this.HandleInitializeRequest); this.SetRequestHandler(ShutdownRequest.Type, this.HandleShutdownRequest); diff --git a/src/ServiceHost/LanguageServices/LanguageService.cs b/src/ServiceHost/LanguageServices/LanguageService.cs index 21b89c19..eb643c0c 100644 --- a/src/ServiceHost/LanguageServices/LanguageService.cs +++ b/src/ServiceHost/LanguageServices/LanguageService.cs @@ -17,6 +17,7 @@ using Microsoft.SqlTools.ServiceLayer.WorkspaceServices.Contracts; using System.Linq; using Microsoft.SqlServer.Management.SqlParser.Parser; using Location = Microsoft.SqlTools.ServiceLayer.WorkspaceServices.Contracts.Location; +using Microsoft.SqlTools.ServiceLayer.Connection; namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { @@ -97,6 +98,15 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices // Register the configuration update handler WorkspaceService.Instance.RegisterConfigChangeCallback(HandleDidChangeConfigurationNotification); + // Register the file change update handler + WorkspaceService.Instance.RegisterTextDocChangeCallback(HandleDidChangeTextDocumentNotification); + + // Register the file open update handler + WorkspaceService.Instance.RegisterTextDocOpenCallback(HandleDidOpenTextDocumentNotification); + + // register an OnConnection callback + ConnectionService.Instance.RegisterOnConnectionTask(OnConnection); + // Store the SqlToolsContext for future use Context = context; } @@ -165,8 +175,11 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices TextDocumentPosition textDocumentPosition, RequestContext requestContext) { - Logger.Write(LogLevel.Verbose, "HandleCompletionRequest"); - await Task.FromResult(true); + Logger.Write(LogLevel.Verbose, "HandleCompletionRequest"); + + // get the current list of completion items and return to client + var completionItems = AutoCompleteService.Instance.GetCompletionItems(textDocumentPosition); + await requestContext.SendResult(completionItems); } private static async Task HandleCompletionResolveRequest( @@ -221,6 +234,45 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices #region Handlers for Events from Other Services + /// + /// Handle the file open notification + /// + /// + /// + /// + public async Task HandleDidOpenTextDocumentNotification( + ScriptFile scriptFile, + EventContext eventContext) + { + await this.RunScriptDiagnostics( + new ScriptFile[] { scriptFile }, + eventContext); + + await Task.FromResult(true); + } + + + /// + /// Handles text document change events + /// + /// + /// + /// + public async Task HandleDidChangeTextDocumentNotification(ScriptFile[] changedFiles, EventContext eventContext) + { + await this.RunScriptDiagnostics( + changedFiles.ToArray(), + eventContext); + + await Task.FromResult(true); + } + + /// + /// Handle the file configuration change notification + /// + /// + /// + /// public async Task HandleDidChangeConfigurationNotification( SqlToolsSettings newSettings, SqlToolsSettings oldSettings, @@ -251,7 +303,17 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices CurrentSettings.EnableProfileLoading = newSettings.EnableProfileLoading; CurrentSettings.ScriptAnalysis.Update(newSettings.ScriptAnalysis, CurrentWorkspace.WorkspacePath); } - + + /// + /// Callback for when a user connection is done processing + /// + /// + public async Task OnConnection(ISqlConnection sqlConnection) + { + await AutoCompleteService.Instance.UpdateAutoCompleteCache(sqlConnection); + await Task.FromResult(true); + } + #endregion #region Private Helpers diff --git a/src/ServiceHost/Program.cs b/src/ServiceHost/Program.cs index 02369811..a3f9cf8b 100644 --- a/src/ServiceHost/Program.cs +++ b/src/ServiceHost/Program.cs @@ -7,6 +7,7 @@ using Microsoft.SqlTools.ServiceLayer.Hosting; using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.ServiceLayer.WorkspaceServices; using Microsoft.SqlTools.ServiceLayer.LanguageServices; +using Microsoft.SqlTools.ServiceLayer.Connection; namespace Microsoft.SqlTools.ServiceLayer { @@ -37,13 +38,16 @@ namespace Microsoft.SqlTools.ServiceLayer // Grab the instance of the service host ServiceHost serviceHost = ServiceHost.Instance; + // Start the service + serviceHost.Start().Wait(); + // Initialize the services that will be hosted here WorkspaceService.Instance.InitializeService(serviceHost); AutoCompleteService.Instance.InitializeService(serviceHost); LanguageService.Instance.InitializeService(serviceHost, sqlToolsContext); + ConnectionService.Instance.Initialize(serviceHost); - // Start the service - serviceHost.Start().Wait(); + serviceHost.Initialize(); serviceHost.WaitForExit(); } } diff --git a/src/ServiceHost/WorkspaceServices/WorkspaceService.cs b/src/ServiceHost/WorkspaceServices/WorkspaceService.cs index 13575121..96878f45 100644 --- a/src/ServiceHost/WorkspaceServices/WorkspaceService.cs +++ b/src/ServiceHost/WorkspaceServices/WorkspaceService.cs @@ -43,6 +43,7 @@ namespace Microsoft.SqlTools.ServiceLayer.WorkspaceServices { ConfigChangeCallbacks = new List(); TextDocChangeCallbacks = new List(); + TextDocOpenCallbacks = new List(); } #endregion @@ -69,6 +70,13 @@ namespace Microsoft.SqlTools.ServiceLayer.WorkspaceServices /// Context of the event raised for the changed files public delegate Task TextDocChangeCallback(ScriptFile[] changedFiles, EventContext eventContext); + /// + /// Delegate for callbacks that occur when a text document is opened + /// + /// File that was opened + /// Context of the event raised for the changed files + public delegate Task TextDocOpenCallback(ScriptFile openFile, EventContext eventContext); + /// /// List of callbacks to call when the configuration of the workspace changes /// @@ -79,6 +87,12 @@ namespace Microsoft.SqlTools.ServiceLayer.WorkspaceServices /// private List TextDocChangeCallbacks { get; set; } + /// + /// List of callbacks to call when a text document is opened + /// + private List TextDocOpenCallbacks { get; set; } + + #endregion #region Public Methods @@ -140,6 +154,15 @@ namespace Microsoft.SqlTools.ServiceLayer.WorkspaceServices TextDocChangeCallbacks.Add(task); } + /// + /// Adds a new task to be called when a file is opened + /// + /// Delegate to call when a document is opened + public void RegisterTextDocOpenCallback(TextDocOpenCallback task) + { + TextDocOpenCallbacks.Add(task); + } + #endregion #region Event Handlers @@ -158,7 +181,7 @@ namespace Microsoft.SqlTools.ServiceLayer.WorkspaceServices // A text change notification can batch multiple change requests foreach (var textChange in textChangeParams.ContentChanges) { - string fileUri = textChangeParams.TextDocument.Uri; + string fileUri = textChangeParams.Uri ?? textChangeParams.TextDocument.Uri; msg.AppendLine(String.Format(" File: {0}", fileUri)); ScriptFile changedFile = Workspace.GetFile(fileUri); @@ -177,12 +200,20 @@ namespace Microsoft.SqlTools.ServiceLayer.WorkspaceServices return Task.WhenAll(handlers); } - protected Task HandleDidOpenTextDocumentNotification( + protected async Task HandleDidOpenTextDocumentNotification( DidOpenTextDocumentNotification openParams, EventContext eventContext) { Logger.Write(LogLevel.Verbose, "HandleDidOpenTextDocumentNotification"); - return Task.FromResult(true); + + // read the SQL file contents into the ScriptFile + ScriptFile openedFile = Workspace.GetFileBuffer(openParams.Uri, openParams.Text); + + // Propagate the changes to the event handlers + var textDocOpenTasks = TextDocOpenCallbacks.Select( + t => t(openedFile, eventContext)); + + await Task.WhenAll(textDocOpenTasks); } protected Task HandleDidCloseTextDocumentNotification(