diff --git a/nuget.config b/nuget.config
new file mode 100644
index 00000000..33539216
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..5bb92553
--- /dev/null
+++ b/src/ServiceHost/Connection/SqlConnection.cs
@@ -0,0 +1,72 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using 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()
+ {
+ // 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.tables";
+ 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..2cf98484
--- /dev/null
+++ b/src/ServiceHost/LanguageSupport/AutoCompleteService.cs
@@ -0,0 +1,112 @@
+//
+// 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
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..c2952f92 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);
}
///
@@ -122,7 +139,7 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server
///
///
///
- protected Task HandleDidChangeTextDocumentNotification(
+ protected async Task HandleDidChangeTextDocumentNotification(
DidChangeTextDocumentParams textChangeParams,
EventContext eventContext)
{
@@ -133,7 +150,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,23 +167,46 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server
Logger.Write(LogLevel.Verbose, msg.ToString());
- this.RunScriptDiagnostics(
+ 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);
}
- protected Task HandleDidOpenTextDocumentNotification(
- DidOpenTextDocumentNotification openParams,
- EventContext eventContext)
- {
- Logger.Write(LogLevel.Verbose, "HandleDidOpenTextDocumentNotification");
- return Task.FromResult(true);
- }
-
- protected Task HandleDidCloseTextDocumentNotification(
+ ///
+ /// Handle the close document notication
+ ///
+ ///
+ ///
+ protected Task HandleDidCloseTextDocumentNotification(
TextDocumentIdentifier closeParams,
EventContext eventContext)
{
@@ -240,12 +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");
- await Task.FromResult(true);
+
+ // 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(
@@ -296,6 +344,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/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/src/ServiceHost/project.json b/src/ServiceHost/project.json
index 11340892..31dac66d 100644
--- a/src/ServiceHost/project.json
+++ b/src/ServiceHost/project.json
@@ -5,7 +5,10 @@
"emitEntryPoint": true
},
"dependencies": {
- "Newtonsoft.Json": "9.0.1"
+ "Newtonsoft.Json": "9.0.1",
+ "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..6796183c
--- /dev/null
+++ b/test/ServiceHost.Test/Connection/ConnectionServiceTests.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.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 = connectionService.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
new file mode 100644
index 00000000..36aac6b4
--- /dev/null
+++ b/test/ServiceHost.Test/LanguageServer/LanguageServiceTests.cs
@@ -0,0 +1,126 @@
+//
+// 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 Microsoft.SqlTools.Test.Connection;
+using Microsoft.SqlTools.Test.Utility;
+using Xunit;
+
+namespace Microsoft.SqlTools.Test.LanguageServer
+{
+ ///
+ /// Tests for the ServiceHost Language Service tests
+ ///
+ public class LanguageServiceTests
+ {
+ #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 = TestObjects.GetTestLanguageService();
+
+ // 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 = TestObjects.GetTestLanguageService();
+
+ // 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 = TestObjects.GetTestLanguageService();
+
+ // 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
+
+ #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/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..a248b200 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": {