diff --git a/.gitignore b/.gitignore
index c87915eb..ba6a7266 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,7 @@ project.lock.json
*.user
*.userosscache
*.sln.docstates
+*.exe
# Build results
[Dd]ebug/
@@ -30,9 +31,10 @@ msbuild.err
msbuild.wrn
# code coverage artifacts
+coverage.xml
node_modules
packages
-coverage.xml
+reports
# Cross building rootfs
cross/rootfs/
diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs
index be778f92..949206de 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs
@@ -6,10 +6,17 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
+using Microsoft.SqlServer.Management.SmoMetadataProvider;
+using Microsoft.SqlServer.Management.SqlParser.Binder;
+using Microsoft.SqlServer.Management.SqlParser.Intellisense;
+using Microsoft.SqlServer.Management.SqlParser.MetadataProvider;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
using Microsoft.SqlTools.ServiceLayer.Hosting;
+using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
+using Microsoft.SqlTools.ServiceLayer.SqlContext;
+using Microsoft.SqlTools.ServiceLayer.Workspace;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
@@ -40,19 +47,14 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
///
/// Default, parameterless constructor.
- /// TODO: Figure out how to make this truely singleton even with dependency injection for tests
+ /// Internal constructor for use in test cases only
///
- public AutoCompleteService()
+ internal AutoCompleteService()
{
}
#endregion
- // Dictionary of unique intellisense caches for each Connection
- private Dictionary caches =
- new Dictionary(new ConnectionSummaryComparer());
- private Object cachesLock = new Object(); // Used when we insert/remove something from the cache dictionary
-
private ConnectionService connectionService = null;
///
@@ -77,6 +79,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
public void InitializeService(ServiceHost serviceHost)
{
+ // Register auto-complete request handler
+ serviceHost.SetRequestHandler(CompletionRequest.Type, HandleCompletionRequest);
+
// Register a callback for when a connection is created
ConnectionServiceInstance.RegisterOnConnectionTask(UpdateAutoCompleteCache);
@@ -84,12 +89,29 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
ConnectionServiceInstance.RegisterOnDisconnectTask(RemoveAutoCompleteCacheUriReference);
}
- ///
- /// Intellisense cache count access for testing.
- ///
- internal int GetCacheCount()
+ ///
+ /// Auto-complete completion provider request callback
+ ///
+ ///
+ ///
+ ///
+ private static async Task HandleCompletionRequest(
+ TextDocumentPosition textDocumentPosition,
+ RequestContext requestContext)
{
- return caches.Count;
+ // get the current list of completion items and return to client
+ var scriptFile = WorkspaceService.Instance.Workspace.GetFile(
+ textDocumentPosition.TextDocument.Uri);
+
+ ConnectionInfo connInfo;
+ ConnectionService.Instance.TryFindConnection(
+ scriptFile.ClientFilePath,
+ out connInfo);
+
+ var completionItems = Instance.GetCompletionItems(
+ textDocumentPosition, scriptFile, connInfo);
+
+ await requestContext.SendResult(completionItems);
}
///
@@ -99,47 +121,163 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
///
public async Task RemoveAutoCompleteCacheUriReference(ConnectionSummary summary)
{
- await Task.Run( () =>
- {
- lock(cachesLock)
- {
- IntellisenseCache cache;
- if( caches.TryGetValue(summary, out cache) )
- {
- cache.ReferenceCount--;
+ await Task.FromResult(0);
+ // await Task.Run( () =>
+ // {
+ // lock(cachesLock)
+ // {
+ // AutoCompleteCache cache;
+ // if( caches.TryGetValue(summary, out cache) )
+ // {
+ // cache.ReferenceCount--;
- // Remove unused caches
- if( cache.ReferenceCount == 0 )
- {
- caches.Remove(summary);
- }
- }
- }
- });
+ // // Remove unused caches
+ // if( cache.ReferenceCount == 0 )
+ // {
+ // caches.Remove(summary);
+ // }
+ // }
+ // }
+ // });
}
-
///
/// Update the cached autocomplete candidate list when the user connects to a database
///
///
public async Task UpdateAutoCompleteCache(ConnectionInfo info)
{
- if (info != null)
+ await Task.Run( () =>
{
- IntellisenseCache cache;
- lock(cachesLock)
+ if (!LanguageService.Instance.ScriptParseInfoMap.ContainsKey(info.OwnerUri))
{
- if(!caches.TryGetValue(info.ConnectionDetails, out cache))
- {
- cache = new IntellisenseCache(info.Factory, info.ConnectionDetails);
- caches[cache.DatabaseInfo] = cache;
- }
- cache.ReferenceCount++;
+ var srvConn = ConnectionService.GetServerConnection(info);
+ var displayInfoProvider = new MetadataDisplayInfoProvider();
+ var metadataProvider = SmoMetadataProvider.CreateConnectedProvider(srvConn);
+ var binder = BinderProvider.CreateBinder(metadataProvider);
+
+ LanguageService.Instance.ScriptParseInfoMap.Add(info.OwnerUri,
+ new ScriptParseInfo()
+ {
+ Binder = binder,
+ MetadataProvider = metadataProvider,
+ MetadataDisplayInfoProvider = displayInfoProvider
+ });
+
+ var scriptFile = WorkspaceService.Instance.Workspace.GetFile(info.OwnerUri);
+
+ LanguageService.Instance.ParseAndBind(scriptFile, info);
}
-
- await cache.UpdateCache();
+ });
+ }
+
+ ///
+ /// Find the position of the previous delimeter for autocomplete token replacement.
+ /// SQL Parser may have similar functionality in which case we'll delete this method.
+ ///
+ ///
+ ///
+ ///
+ ///
+ private int PositionOfPrevDelimeter(string sql, int startRow, int startColumn)
+ {
+ if (string.IsNullOrWhiteSpace(sql))
+ {
+ return 1;
}
+
+ int prevLineColumns = 0;
+ for (int i = 0; i < startRow; ++i)
+ {
+ while (sql[prevLineColumns] != '\n' && prevLineColumns < sql.Length)
+ {
+ ++prevLineColumns;
+ }
+ ++prevLineColumns;
+ }
+
+ startColumn += prevLineColumns;
+
+ if (startColumn - 1 < sql.Length)
+ {
+ while (--startColumn >= prevLineColumns)
+ {
+ if (sql[startColumn] == ' '
+ || sql[startColumn] == '\t'
+ || sql[startColumn] == '\n'
+ || sql[startColumn] == '.'
+ || sql[startColumn] == '+'
+ || sql[startColumn] == '-'
+ || sql[startColumn] == '*'
+ || sql[startColumn] == '>'
+ || sql[startColumn] == '<'
+ || sql[startColumn] == '='
+ || sql[startColumn] == '/'
+ || sql[startColumn] == '%')
+ {
+ break;
+ }
+ }
+ }
+
+ return startColumn + 1 - prevLineColumns;
+ }
+
+ ///
+ /// Determines whether a reparse and bind is required to provide autocomplete
+ ///
+ ///
+ /// TEMP: Currently hard-coded to false for perf
+ private bool RequiresReparse(ScriptParseInfo info)
+ {
+ return false;
+ }
+
+ ///
+ /// Converts a list of Declaration objects to CompletionItem objects
+ /// since VS Code expects CompletionItems but SQL Parser works with Declarations
+ ///
+ ///
+ ///
+ ///
+ ///
+ private CompletionItem[] ConvertDeclarationsToCompletionItems(
+ IEnumerable suggestions,
+ int row,
+ int startColumn,
+ int endColumn)
+ {
+ List completions = new List();
+ foreach (var autoCompleteItem in suggestions)
+ {
+ // convert the completion item candidates into CompletionItems
+ completions.Add(new CompletionItem()
+ {
+ Label = autoCompleteItem.Title,
+ Kind = CompletionItemKind.Keyword,
+ Detail = autoCompleteItem.Title,
+ Documentation = autoCompleteItem.Description,
+ TextEdit = new TextEdit
+ {
+ NewText = autoCompleteItem.Title,
+ Range = new Range
+ {
+ Start = new Position
+ {
+ Line = row,
+ Character = startColumn
+ },
+ End = new Position
+ {
+ Line = row,
+ Character = endColumn
+ }
+ }
+ }
+ });
+ }
+
+ return completions.ToArray();
}
///
@@ -147,22 +285,48 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// This method does not await cache builds since it expects to return quickly
///
///
- public CompletionItem[] GetCompletionItems(TextDocumentPosition textDocumentPosition)
+ public CompletionItem[] GetCompletionItems(
+ TextDocumentPosition textDocumentPosition,
+ ScriptFile scriptFile,
+ ConnectionInfo connInfo)
{
- // Try to find a cache for the document's backing connection (if available)
- // If we have a connection but no cache, we don't care - assuming the OnConnect and OnDisconnect listeners
- // behave well, there should be a cache for any actively connected document. This also helps skip documents
- // that are not backed by a SQL connection
- ConnectionInfo info;
- IntellisenseCache cache;
- if (ConnectionServiceInstance.TryFindConnection(textDocumentPosition.TextDocument.Uri, out info)
- && caches.TryGetValue((ConnectionSummary)info.ConnectionDetails, out cache))
+ string filePath = textDocumentPosition.TextDocument.Uri;
+
+ // Take a reference to the list at a point in time in case we update and replace the list
+ if (connInfo == null
+ || !LanguageService.Instance.ScriptParseInfoMap.ContainsKey(textDocumentPosition.TextDocument.Uri))
{
- return cache.GetAutoCompleteItems(textDocumentPosition).ToArray();
+ return new CompletionItem[0];
}
-
- return new CompletionItem[0];
+
+ // reparse and bind the SQL statement if needed
+ var scriptParseInfo = LanguageService.Instance.ScriptParseInfoMap[textDocumentPosition.TextDocument.Uri];
+ if (RequiresReparse(scriptParseInfo))
+ {
+ LanguageService.Instance.ParseAndBind(scriptFile, connInfo);
+ }
+
+ if (scriptParseInfo.ParseResult == null)
+ {
+ return new CompletionItem[0];
+ }
+
+ // get the completion list from SQL Parser
+ var suggestions = Resolver.FindCompletions(
+ scriptParseInfo.ParseResult,
+ textDocumentPosition.Position.Line + 1,
+ textDocumentPosition.Position.Character + 1,
+ scriptParseInfo.MetadataDisplayInfoProvider);
+
+ // convert the suggestion list to the VS Code format
+ return ConvertDeclarationsToCompletionItems(
+ suggestions,
+ textDocumentPosition.Position.Line,
+ PositionOfPrevDelimeter(
+ scriptFile.Contents,
+ textDocumentPosition.Position.Line,
+ textDocumentPosition.Position.Character),
+ textDocumentPosition.Position.Character);
}
-
}
}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/IntellisenseCache.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/IntellisenseCache.cs
deleted file mode 100644
index 3fa1ed29..00000000
--- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/IntellisenseCache.cs
+++ /dev/null
@@ -1,135 +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.Data;
-using System.Data.Common;
-using System.Threading.Tasks;
-using Microsoft.SqlServer.Management.SqlParser.Intellisense;
-using Microsoft.SqlTools.ServiceLayer.Connection;
-using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
-using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
-using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
-
-namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
-{
- internal class IntellisenseCache
- {
- ///
- /// connection used to query for intellisense info
- ///
- private DbConnection connection;
-
- ///
- /// Number of documents (URI's) that are using the cache for the same database.
- /// The autocomplete service uses this to remove unreferenced caches.
- ///
- public int ReferenceCount { get; set; }
-
- public IntellisenseCache(ISqlConnectionFactory connectionFactory, ConnectionDetails connectionDetails)
- {
- ReferenceCount = 0;
- DatabaseInfo = connectionDetails.Clone();
-
- // TODO error handling on this. Intellisense should catch or else the service should handle
- connection = connectionFactory.CreateSqlConnection(ConnectionService.BuildConnectionString(connectionDetails));
- connection.Open();
- }
-
- ///
- /// Used to identify a database for which this cache is used
- ///
- public ConnectionSummary DatabaseInfo
- {
- get;
- private set;
- }
- ///
- /// Gets the current autocomplete candidate list
- ///
- public IEnumerable AutoCompleteList { get; private set; }
-
- public async Task UpdateCache()
- {
- DbCommand command = connection.CreateCommand();
- command.CommandText = "SELECT name FROM sys.tables";
- command.CommandTimeout = 15;
- command.CommandType = CommandType.Text;
- var reader = await command.ExecuteReaderAsync();
-
- List results = new List();
- while (await reader.ReadAsync())
- {
- results.Add(reader[0].ToString());
- }
-
- AutoCompleteList = results;
- await Task.FromResult(0);
- }
-
- public List GetAutoCompleteItems(TextDocumentPosition textDocumentPosition)
- {
- List completions = new List();
-
- // Take a reference to the list at a point in time in case we update and replace the list
- //var suggestions = AutoCompleteList;
- if (!LanguageService.Instance.ScriptParseInfoMap.ContainsKey(textDocumentPosition.TextDocument.Uri))
- {
- return completions;
- }
-
- var scriptParseInfo = LanguageService.Instance.ScriptParseInfoMap[textDocumentPosition.TextDocument.Uri];
- var suggestions = Resolver.FindCompletions(
- scriptParseInfo.ParseResult,
- textDocumentPosition.Position.Line + 1,
- textDocumentPosition.Position.Character + 1,
- scriptParseInfo.MetadataDisplayInfoProvider);
-
- int i = 0;
-
- // the completion list will be null is user not connected to server
- if (this.AutoCompleteList != null)
- {
-
- foreach (var autoCompleteItem in suggestions)
- {
- // convert the completion item candidates into CompletionItems
- completions.Add(new CompletionItem()
- {
- Label = autoCompleteItem.Title,
- Kind = CompletionItemKind.Keyword,
- Detail = autoCompleteItem.Title + " details",
- Documentation = autoCompleteItem.Title + " documentation",
- TextEdit = new TextEdit
- {
- NewText = autoCompleteItem.Title,
- 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;
- }
- }
-}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs
index 611e8c81..d414b41e 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
-using System.Data.Common;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlTools.EditorServices.Utility;
@@ -20,9 +19,8 @@ using Microsoft.SqlServer.Management.SqlParser.Parser;
using Location = Microsoft.SqlTools.ServiceLayer.Workspace.Contracts.Location;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlServer.Management.SqlParser.Binder;
-using Microsoft.SqlServer.Management.SmoMetadataProvider;
using Microsoft.SqlServer.Management.Common;
-using Microsoft.SqlServer.Management.SqlParser.MetadataProvider;
+using Microsoft.SqlServer.Management.SqlParser;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
@@ -40,17 +38,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
private Lazy> scriptParseInfoMap
= new Lazy>(() => new Dictionary());
- internal class ScriptParseInfo
- {
- public IBinder Binder { get; set; }
-
- public ParseResult ParseResult { get; set; }
-
- public SmoMetadataProvider MetadataProvider { get; set; }
-
- public MetadataDisplayInfoProvider MetadataDisplayInfoProvider { get; set; }
- }
-
internal Dictionary ScriptParseInfoMap
{
get
@@ -93,21 +80,20 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
///
private SqlToolsContext Context { get; set; }
- ///
- /// The cached parse result from previous incremental parse
- ///
- private ParseResult prevParseResult;
-
#endregion
#region Public Methods
+ ///
+ /// Initializes the Language Service instance
+ ///
+ ///
+ ///
public void InitializeService(ServiceHost serviceHost, SqlToolsContext context)
{
// Register the requests that this service will handle
serviceHost.SetRequestHandler(DefinitionRequest.Type, HandleDefinitionRequest);
serviceHost.SetRequestHandler(ReferencesRequest.Type, HandleReferencesRequest);
- serviceHost.SetRequestHandler(CompletionRequest.Type, HandleCompletionRequest);
serviceHost.SetRequestHandler(CompletionResolveRequest.Type, HandleCompletionResolveRequest);
serviceHost.SetRequestHandler(SignatureHelpRequest.Type, HandleSignatureHelpRequest);
serviceHost.SetRequestHandler(DocumentHighlightRequest.Type, HandleDocumentHighlightRequest);
@@ -135,52 +121,69 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
Context = context;
}
+ ///
+ /// Parses the SQL text and binds it to the SMO metadata provider if connected
+ ///
+ ///
+ ///
+ ///
+ public ParseResult ParseAndBind(ScriptFile scriptFile, ConnectionInfo connInfo)
+ {
+ ScriptParseInfo parseInfo = null;
+ if (this.ScriptParseInfoMap.ContainsKey(scriptFile.ClientFilePath))
+ {
+ parseInfo = this.ScriptParseInfoMap[scriptFile.ClientFilePath];
+ }
+
+ // parse current SQL file contents to retrieve a list of errors
+ ParseOptions parseOptions = new ParseOptions();
+ ParseResult parseResult = Parser.IncrementalParse(
+ scriptFile.Contents,
+ parseInfo != null ? parseInfo.ParseResult : null,
+ parseOptions);
+
+ // save previous result for next incremental parse
+ if (parseInfo != null)
+ {
+ parseInfo.ParseResult = parseResult;
+ }
+
+ if (connInfo != null)
+ {
+ try
+ {
+ List parseResults = new List();
+ parseResults.Add(parseResult);
+ parseInfo.Binder.Bind(
+ parseResults,
+ connInfo.ConnectionDetails.DatabaseName,
+ BindMode.Batch);
+ }
+ catch (ConnectionException)
+ {
+ Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object...");
+ }
+ catch (SqlParserInternalBinderError)
+ {
+ Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object...");
+ }
+ }
+
+ return parseResult;
+ }
+
///
/// 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;
-
ConnectionInfo connInfo;
- bool isConnected = ConnectionService.Instance.TryFindConnection(scriptFile.ClientFilePath, out connInfo);
- if (isConnected)
- {
- if (!this.ScriptParseInfoMap.ContainsKey(scriptFile.ClientFilePath))
- {
- var srvConn = ConnectionService.GetServerConnection(connInfo);
- var metadataProvider = SmoMetadataProvider.CreateConnectedProvider(srvConn);
- var binder = BinderProvider.CreateBinder(metadataProvider);
- var displayInfoProvider = new MetadataDisplayInfoProvider();
-
- this.ScriptParseInfoMap.Add(scriptFile.ClientFilePath,
- new ScriptParseInfo()
- {
- Binder = binder,
- ParseResult = parseResult,
- MetadataProvider = metadataProvider,
- MetadataDisplayInfoProvider = displayInfoProvider
- });
- }
-
- ScriptParseInfo parseInfo = this.ScriptParseInfoMap[scriptFile.ClientFilePath];
- parseInfo.ParseResult = parseResult;
- List parseResults = new List();
- parseResults.Add(parseResult);
- parseInfo.Binder.Bind(
- parseResults,
- connInfo.ConnectionDetails.DatabaseName,
- BindMode.Batch);
- }
+ ConnectionService.Instance.TryFindConnection(
+ scriptFile.ClientFilePath,
+ out connInfo);
+
+ var parseResult = ParseAndBind(scriptFile, connInfo);
// build a list of SQL script file markers from the errors
List markers = new List();
@@ -226,17 +229,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
await Task.FromResult(true);
}
- private static async Task HandleCompletionRequest(
- TextDocumentPosition textDocumentPosition,
- RequestContext requestContext)
- {
- Logger.Write(LogLevel.Verbose, "HandleCompletionRequest");
-
- // get the current list of completion items and return to client
- var completionItems = AutoCompleteService.Instance.GetCompletionItems(textDocumentPosition);
- await requestContext.SendResult(completionItems);
- }
-
private static async Task HandleCompletionResolveRequest(
CompletionItem completionItem,
RequestContext requestContext)
@@ -305,7 +297,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
await Task.FromResult(true);
}
-
///
/// Handles text document change events
@@ -506,7 +497,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
Message = scriptFileMarker.Message,
Range = new Range
{
- // TODO: What offsets should I use?
Start = new Position
{
Line = scriptFileMarker.ScriptRegion.StartLineNumber - 1,
diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs
new file mode 100644
index 00000000..4da2c57e
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.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 Microsoft.SqlServer.Management.SqlParser.Binder;
+using Microsoft.SqlServer.Management.SqlParser.MetadataProvider;
+using Microsoft.SqlServer.Management.SqlParser.Parser;
+using Microsoft.SqlServer.Management.SmoMetadataProvider;
+
+namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
+{
+ ///
+ /// Class for storing cached metadata regarding a parsed SQL file
+ ///
+ internal class ScriptParseInfo
+ {
+ ///
+ /// Gets or sets the SMO binder for schema-aware intellisense
+ ///
+ public IBinder Binder { get; set; }
+
+ ///
+ /// Gets or sets the previous SQL parse result
+ ///
+ public ParseResult ParseResult { get; set; }
+
+ ///
+ /// Gets or set the SMO metadata provider that's bound to the current connection
+ ///
+ ///
+ public SmoMetadataProvider MetadataProvider { get; set; }
+
+ ///
+ /// Gets or sets the SMO metadata display info provider
+ ///
+ ///
+ public MetadataDisplayInfoProvider MetadataDisplayInfoProvider { get; set; }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Workspace/Contracts/ScriptFile.cs b/src/Microsoft.SqlTools.ServiceLayer/Workspace/Contracts/ScriptFile.cs
index f44e2b65..74592ae5 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/Workspace/Contracts/ScriptFile.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/Workspace/Contracts/ScriptFile.cs
@@ -34,9 +34,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace.Contracts
public string FilePath { get; private set; }
///
- /// Gets the path which the editor client uses to identify this file.
+ /// Gets or sets the path which the editor client uses to identify this file.
+ /// Setter for testing purposes only
///
- public string ClientFilePath { get; private set; }
+ public string ClientFilePath { get; internal set; }
///
/// Gets or sets a boolean that determines whether
@@ -52,7 +53,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace.Contracts
public bool IsInMemory { get; private set; }
///
- /// Gets a string containing the full contents of the file.
+ /// Gets or sets a string containing the full contents of the file.
+ /// Setter for testing purposes only
///
public string Contents
{
@@ -60,6 +62,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace.Contracts
{
return string.Join("\r\n", this.FileLines);
}
+ set
+ {
+ this.FileLines = value != null ? value.Split('\n') : null;
+ }
}
///
diff --git a/test/CodeCoverage/codecoverage.bat b/test/CodeCoverage/codecoverage.bat
index 767d68ef..098ec1a1 100644
--- a/test/CodeCoverage/codecoverage.bat
+++ b/test/CodeCoverage/codecoverage.bat
@@ -1,11 +1,25 @@
SET WORKINGDIR=%~dp0
-rmdir %WORKINGDIR%reports\ /S /Q
-del %WORKINGDIR%coverage.xml
-mkdir reports
+
+REM clean-up results from previous run
+RMDIR %WORKINGDIR%reports\ /S /Q
+DEL %WORKINGDIR%coverage.xml
+MKDIR reports
+
+REM backup current project.json
COPY /Y %WORKINGDIR%..\..\src\Microsoft.SqlTools.ServiceLayer\project.json %WORKINGDIR%..\..\src\Microsoft.SqlTools.ServiceLayer\project.json.BAK
+
+REM switch PDB type to Full since that is required by OpenCover for now
+REM we should remove this step on OpenCover supports portable PDB
cscript /nologo ReplaceText.vbs %WORKINGDIR%..\..\src\Microsoft.SqlTools.ServiceLayer\project.json portable full
+
+REM rebuild the SqlToolsService project
dotnet build %WORKINGDIR%..\..\src\Microsoft.SqlTools.ServiceLayer\project.json
+
+REM run the tests through OpenCover and generate a report
"%WORKINGDIR%packages\OpenCover.4.6.519\tools\OpenCover.Console.exe" -register:user -target:dotnet.exe -targetargs:"test %WORKINGDIR%..\Microsoft.SqlTools.ServiceLayer.Test\project.json" -oldstyle -filter:"+[Microsoft.SqlTools.*]* -[xunit*]*" -output:coverage.xml -searchdirs:%WORKINGDIR%..\Microsoft.SqlTools.ServiceLayer.Test\bin\Debug\netcoreapp1.0
"%WORKINGDIR%packages\ReportGenerator.2.4.5.0\tools\ReportGenerator.exe" "-reports:coverage.xml" "-targetdir:%WORKINGDIR%\reports"
+
+REM restore original project.json
COPY /Y %WORKINGDIR%..\..\src\Microsoft.SqlTools.ServiceLayer\project.json.BAK %WORKINGDIR%..\..\src\Microsoft.SqlTools.ServiceLayer\project.json
+DEL %WORKINGDIR%..\..\src\Microsoft.SqlTools.ServiceLayer\project.json.BAK
EXIT
diff --git a/test/CodeCoverage/gulpfile.js b/test/CodeCoverage/gulpfile.js
index 2692900d..38b1576c 100644
--- a/test/CodeCoverage/gulpfile.js
+++ b/test/CodeCoverage/gulpfile.js
@@ -1,5 +1,4 @@
var gulp = require('gulp');
-//var install = require('gulp-install');;
var del = require('del');
var request = require('request');
var fs = require('fs');
diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs
index d89ac3dc..0725c209 100644
--- a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs
+++ b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs
@@ -22,6 +22,7 @@ using Microsoft.SqlServer.Management.SqlParser.Parser;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
using Microsoft.SqlTools.ServiceLayer.LanguageServices;
+using Microsoft.SqlTools.ServiceLayer.Test.QueryExecution;
using Microsoft.SqlTools.ServiceLayer.Test.Utility;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
using Microsoft.SqlTools.Test.Utility;
@@ -38,91 +39,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices
{
#region "Diagnostics tests"
-
- // [Fact]
- // public void TestParseWideWorldImporters()
- // {
- // var sql = File.ReadAllText(@"e:\data\script.sql");
- // //string sql = @"SELECT ";
- // ParseOptions parseOptions = new ParseOptions();
- // ParseResult parseResult = Parser.IncrementalParse(
- // sql,
- // null,
- // parseOptions);
- // }
-
- // [Fact]
- // public void TestSmo()
- // {
- // SqlConnectionStringBuilder connectionBuilder = new SqlConnectionStringBuilder();
- // connectionBuilder["Data Source"] = "sqltools11";
- // connectionBuilder["Integrated Security"] = false;
- // connectionBuilder["User Id"] = "sa";
- // connectionBuilder["Password"] = "Yukon900";
- // connectionBuilder["Initial Catalog"] = "master";
- // string connectionString = connectionBuilder.ToString();
-
- // var conn = new SqlConnection(connectionString);
- // var sqlConn = new ServerConnection(conn);
-
- // var server = new Server(sqlConn);
- // string s = "";
- // foreach (Database db2 in server.Databases)
- // {
- // s += db2.Name;
- // }
-
- // var metadata = SmoMetadataProvider.CreateConnectedProvider(sqlConn);
- // var db = metadata.Server.Databases["master"];
- // }
-
- // [Fact]
- // public void TestSmoMetadataProvider()
- // {
- // SqlConnectionStringBuilder connectionBuilder = new SqlConnectionStringBuilder();
- // //connectionBuilder["Data Source"] = "sqltools11";
- // connectionBuilder["Data Source"] = "localhost";
- // connectionBuilder["Integrated Security"] = false;
- // connectionBuilder["User Id"] = "sa";
- // connectionBuilder["Password"] = "Yukon900";
- // connectionBuilder["Initial Catalog"] = "master";
-
- // try
- // {
- // var sqlConnection = new SqlConnection(connectionBuilder.ToString());
- // var connection = new ServerConnection(sqlConnection);
- // var metadataProvider = SmoMetadataProvider.CreateConnectedProvider(connection);
- // var binder = BinderProvider.CreateBinder(metadataProvider);
- // var displayInfoProvider = new MetadataDisplayInfoProvider();
-
- // //string sql = @"SELECT * FROM sys.objects;";
-
- // string sql = @"SELECT ";
-
- // ParseOptions parseOptions = new ParseOptions();
- // ParseResult parseResult = Parser.IncrementalParse(
- // sql,
- // null,
- // parseOptions);
-
- // List parseResults = new List();
- // parseResults.Add(parseResult);
- // binder.Bind(parseResults, "master", BindMode.Batch);
-
- // var comp = Resolver.FindCompletions(parseResult, 1, 8, displayInfoProvider);
- // comp.Add(null);
- // }
- // finally
- // {
- // // Check if we failed to create a binder object. If so, we temporarely
- // // use a no-op binder which has the effect of turning off binding. We
- // // also set a timer that after the specified timeout expires removes
- // // the no-op timer (object becomes dead) which would give clients of
- // // this class an opportunity to remove it and create a new one.
- // }
- // }
-
-
///
/// Verify that the latest SqlParser (2016 as of this writing) is used by default
///
@@ -234,6 +150,29 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices
#region "Autocomplete Tests"
+ // This test currently requires a live database connection to initialize
+ // SMO connected metadata provider. Since we don't want a live DB dependency
+ // in the CI unit tests this scenario is currently disabled.
+ //[Fact]
+ public void AutoCompleteFindCompletions()
+ {
+ TextDocumentPosition textDocument;
+ ConnectionInfo connInfo;
+ ScriptFile scriptFile;
+ Common.GetAutoCompleteTestObjects(out textDocument, out scriptFile, out connInfo);
+
+ textDocument.Position.Character = 7;
+ scriptFile.Contents = "select ";
+
+ var autoCompleteService = AutoCompleteService.Instance;
+ var completions = autoCompleteService.GetCompletionItems(
+ textDocument,
+ scriptFile,
+ connInfo);
+
+ Assert.True(completions.Length > 0);
+ }
+
///
/// Creates a mock db command that returns a predefined result set
///
@@ -261,166 +200,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices
return connectionMock.Object;
}
-#if false
- ///
- /// Verify that the autocomplete service returns tables for the current connection as suggestions
- ///
- [Fact]
- public void TablesAreReturnedAsAutocompleteSuggestions()
- {
- // Result set for the query of database tables
- Dictionary[] data =
- {
- new Dictionary { {"name", "master" } },
- new Dictionary { {"name", "model" } }
- };
-
- var mockFactory = new Mock();
- mockFactory.Setup(factory => factory.CreateSqlConnection(It.IsAny()))
- .Returns(CreateMockDbConnection(new[] {data}));
-
- var connectionService = new ConnectionService(mockFactory.Object);
- var autocompleteService = new AutoCompleteService();
- autocompleteService.ConnectionServiceInstance = connectionService;
- autocompleteService.InitializeService(Microsoft.SqlTools.ServiceLayer.Hosting.ServiceHost.Instance);
-
- // Open a connection
- // The cache should get updated as part of this
- ConnectParams connectionRequest = TestObjects.GetTestConnectionParams();
- var connectionResult = connectionService.Connect(connectionRequest);
- Assert.NotEmpty(connectionResult.ConnectionId);
-
- // Check that there is one cache created in the auto complete service
- Assert.Equal(1, autocompleteService.GetCacheCount());
-
- // Check that we get table suggestions for an autocomplete request
- TextDocumentPosition position = new TextDocumentPosition();
- position.TextDocument = new TextDocumentIdentifier();
- position.TextDocument.Uri = connectionRequest.OwnerUri;
- position.Position = new Position();
- position.Position.Line = 1;
- position.Position.Character = 1;
- var items = autocompleteService.GetCompletionItems(position);
- Assert.Equal(2, items.Length);
- Assert.Equal("master", items[0].Label);
- Assert.Equal("model", items[1].Label);
- }
-#endif
-
- ///
- /// Verify that only one intellisense cache is created for two documents using
- /// the autocomplete service when they share a common connection.
- ///
- [Fact]
- public void OnlyOneCacheIsCreatedForTwoDocumentsWithSameConnection()
- {
- var connectionService = new ConnectionService(TestObjects.GetTestSqlConnectionFactory());
- var autocompleteService = new AutoCompleteService();
- autocompleteService.ConnectionServiceInstance = connectionService;
- autocompleteService.InitializeService(Microsoft.SqlTools.ServiceLayer.Hosting.ServiceHost.Instance);
-
- // Open two connections
- ConnectParams connectionRequest1 = TestObjects.GetTestConnectionParams();
- connectionRequest1.OwnerUri = "file:///my/first/file.sql";
- ConnectParams connectionRequest2 = TestObjects.GetTestConnectionParams();
- connectionRequest2.OwnerUri = "file:///my/second/file.sql";
- var connectionResult1 = connectionService.Connect(connectionRequest1);
- Assert.NotEmpty(connectionResult1.ConnectionId);
- var connectionResult2 = connectionService.Connect(connectionRequest2);
- Assert.NotEmpty(connectionResult2.ConnectionId);
-
- // Verify that only one intellisense cache is created to service both URI's
- Assert.Equal(1, autocompleteService.GetCacheCount());
- }
-
-#if false
- ///
- /// Verify that two different intellisense caches and corresponding autocomplete
- /// suggestions are provided for two documents with different connections.
- ///
- [Fact]
- public void TwoCachesAreCreatedForTwoDocumentsWithDifferentConnections()
- {
- const string testDb1 = "my_db";
- const string testDb2 = "my_other_db";
-
- // Result set for the query of database tables
- Dictionary[] data1 =
- {
- new Dictionary { {"name", "master" } },
- new Dictionary { {"name", "model" } }
- };
-
- Dictionary[] data2 =
- {
- new Dictionary { {"name", "master" } },
- new Dictionary { {"name", "my_table" } },
- new Dictionary { {"name", "my_other_table" } }
- };
-
- var mockFactory = new Mock();
- mockFactory.Setup(factory => factory.CreateSqlConnection(It.Is(x => x.Contains(testDb1))))
- .Returns(CreateMockDbConnection(new[] {data1}));
- mockFactory.Setup(factory => factory.CreateSqlConnection(It.Is(x => x.Contains(testDb2))))
- .Returns(CreateMockDbConnection(new[] {data2}));
-
- var connectionService = new ConnectionService(mockFactory.Object);
- var autocompleteService = new AutoCompleteService();
- autocompleteService.ConnectionServiceInstance = connectionService;
- autocompleteService.InitializeService(Microsoft.SqlTools.ServiceLayer.Hosting.ServiceHost.Instance);
-
- // Open connections
- // The cache should get updated as part of this
- ConnectParams connectionRequest = TestObjects.GetTestConnectionParams();
- connectionRequest.OwnerUri = "file:///my/first/sql/file.sql";
- connectionRequest.Connection.DatabaseName = testDb1;
- var connectionResult = connectionService.Connect(connectionRequest);
- Assert.NotEmpty(connectionResult.ConnectionId);
-
- // Check that there is one cache created in the auto complete service
- Assert.Equal(1, autocompleteService.GetCacheCount());
-
- // Open second connection
- ConnectParams connectionRequest2 = TestObjects.GetTestConnectionParams();
- connectionRequest2.OwnerUri = "file:///my/second/sql/file.sql";
- connectionRequest2.Connection.DatabaseName = testDb2;
- var connectionResult2 = connectionService.Connect(connectionRequest2);
- Assert.NotEmpty(connectionResult2.ConnectionId);
-
- // Check that there are now two caches in the auto complete service
- Assert.Equal(2, autocompleteService.GetCacheCount());
-
- // Check that we get 2 different table suggestions for autocomplete requests
- TextDocumentPosition position = new TextDocumentPosition();
- position.TextDocument = new TextDocumentIdentifier();
- position.TextDocument.Uri = connectionRequest.OwnerUri;
- position.Position = new Position();
- position.Position.Line = 1;
- position.Position.Character = 1;
-
- var items = autocompleteService.GetCompletionItems(position);
- Assert.Equal(2, items.Length);
- Assert.Equal("master", items[0].Label);
- Assert.Equal("model", items[1].Label);
-
- TextDocumentPosition position2 = new TextDocumentPosition();
- position2.TextDocument = new TextDocumentIdentifier();
- position2.TextDocument.Uri = connectionRequest2.OwnerUri;
- position2.Position = new Position();
- position2.Position.Line = 1;
- position2.Position.Character = 1;
-
- var items2 = autocompleteService.GetCompletionItems(position2);
- Assert.Equal(3, items2.Length);
- Assert.Equal("master", items2[0].Label);
- Assert.Equal("my_table", items2[1].Label);
- Assert.Equal("my_other_table", items2[2].Label);
- }
-#endif
#endregion
}
}
-
-
-
-
diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs
index 6d265de2..878073a0 100644
--- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs
+++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs
@@ -9,14 +9,19 @@ using System.Data;
using System.Data.Common;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.SqlServer.Management.SmoMetadataProvider;
+using Microsoft.SqlServer.Management.SqlParser.Binder;
+using Microsoft.SqlServer.Management.SqlParser.MetadataProvider;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
+using Microsoft.SqlTools.ServiceLayer.LanguageServices;
using Microsoft.SqlTools.ServiceLayer.QueryExecution;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
using Microsoft.SqlTools.ServiceLayer.SqlContext;
using Microsoft.SqlTools.ServiceLayer.Test.Utility;
+using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
using Moq;
using Moq.Protected;
@@ -36,6 +41,16 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
public const int StandardColumns = 5;
+ public static string TestServer { get; set; }
+
+ public static string TestDatabase { get; set; }
+
+ static Common()
+ {
+ TestServer = "sqltools11";
+ TestDatabase = "master";
+ }
+
public static Dictionary[] StandardTestData
{
get { return GetTestData(StandardRows, StandardColumns); }
@@ -122,8 +137,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
{
UserName = "sa",
Password = "Yukon900",
- DatabaseName = "AdventureWorks2016CTP3_2",
- ServerName = "sqltools11"
+ DatabaseName = Common.TestDatabase,
+ ServerName = Common.TestServer
};
return new ConnectionInfo(CreateMockFactory(data, throwOnRead), OwnerUri, connDetails);
@@ -133,6 +148,37 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
#region Service Mocking
+ public static void GetAutoCompleteTestObjects(
+ out TextDocumentPosition textDocument,
+ out ScriptFile scriptFile,
+ out ConnectionInfo connInfo
+ )
+ {
+ textDocument = new TextDocumentPosition();
+ textDocument.TextDocument = new TextDocumentIdentifier();
+ textDocument.TextDocument.Uri = Common.OwnerUri;
+ textDocument.Position = new Position();
+ textDocument.Position.Line = 0;
+ textDocument.Position.Character = 0;
+
+ connInfo = Common.CreateTestConnectionInfo(null, false);
+ var srvConn = ConnectionService.GetServerConnection(connInfo);
+ var displayInfoProvider = new MetadataDisplayInfoProvider();
+ var metadataProvider = SmoMetadataProvider.CreateConnectedProvider(srvConn);
+ var binder = BinderProvider.CreateBinder(metadataProvider);
+
+ LanguageService.Instance.ScriptParseInfoMap.Add(textDocument.TextDocument.Uri,
+ new ScriptParseInfo()
+ {
+ Binder = binder,
+ MetadataProvider = metadataProvider,
+ MetadataDisplayInfoProvider = displayInfoProvider
+ });
+
+ scriptFile = new ScriptFile();
+ scriptFile.ClientFilePath = textDocument.TextDocument.Uri;
+ }
+
public static ConnectionDetails GetTestConnectionDetails()
{
return new ConnectionDetails
diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/project.json b/test/Microsoft.SqlTools.ServiceLayer.Test/project.json
index 3d4d2623..06a46957 100644
--- a/test/Microsoft.SqlTools.ServiceLayer.Test/project.json
+++ b/test/Microsoft.SqlTools.ServiceLayer.Test/project.json
@@ -9,7 +9,7 @@
"System.Runtime.Serialization.Primitives": "4.1.1",
"System.Data.Common": "4.1.0",
"System.Data.SqlClient": "4.1.0",
- "Microsoft.SqlServer.Smo": "140.1.2",
+ "Microsoft.SqlServer.Smo": "140.1.5",
"System.Security.SecureString": "4.0.0",
"System.Collections.Specialized": "4.0.1",
"System.ComponentModel.TypeConverter": "4.1.0",