diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs
index a2c51c9a..9667c0e3 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs
@@ -15,6 +15,7 @@ using Microsoft.SqlServer.Management.SqlParser.Parser;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
using Microsoft.SqlTools.ServiceLayer.SqlContext;
+using Microsoft.SqlTools.ServiceLayer.Utility;
using Microsoft.SqlTools.ServiceLayer.Workspace;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
@@ -679,5 +680,77 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
return null;
}
}
+
+ ///
+ /// Converts a SQL Parser List of MethodHelpText objects into a VS Code SignatureHelp object
+ ///
+ internal static SignatureHelp ConvertMethodHelpTextListToSignatureHelp(List methods, Babel.MethodNameAndParamLocations locations, int line, int column)
+ {
+ Validate.IsNotNull(nameof(methods), methods);
+ Validate.IsNotNull(nameof(locations), locations);
+ Validate.IsGreaterThan(nameof(line), line, 0);
+ Validate.IsGreaterThan(nameof(column), column, 0);
+
+ SignatureHelp help = new SignatureHelp();
+
+ help.Signatures = methods.Select(method =>
+ {
+ return new SignatureInformation()
+ {
+ // Signature label format: param1, param2, ..., paramn RETURNS
+ Label = method.Name + " " + method.Parameters.Select(parameter => parameter.Display).Aggregate((l, r) => l + "," + r) + " " + method.Type,
+ Documentation = method.Description,
+ Parameters = method.Parameters.Select(parameter =>
+ {
+ return new ParameterInformation()
+ {
+ Label = parameter.Display,
+ Documentation = parameter.Description
+ };
+ }).ToArray()
+ };
+ }).Where(method => method.Label.Contains(locations.Name)).ToArray();
+
+ if (help.Signatures.Length == 0)
+ {
+ return null;
+ }
+
+ // Find the matching method signature at the cursor's location
+ // For now, take the first match (since we've already filtered by name above)
+ help.ActiveSignature = 0;
+
+ // Determine the current parameter at the cursor
+ int currentParameter = -1; // Default case: not on any particular parameter
+ if (locations.ParamStartLocation != null)
+ {
+ // Is the cursor past the function name?
+ var location = locations.ParamStartLocation.Value;
+ if (line > location.LineNumber || (line == location.LineNumber && line == location.LineNumber && column >= location.ColumnNumber))
+ {
+ currentParameter = 0;
+ }
+ }
+ foreach (var location in locations.ParamSeperatorLocations)
+ {
+ // Is the cursor past a comma ',' and at least on the next parameter?
+ if (line > location.LineNumber || (line == location.LineNumber && column > location.ColumnNumber))
+ {
+ currentParameter++;
+ }
+ }
+ if (locations.ParamEndLocation != null)
+ {
+ // Is the cursor past the end of the parameter list on a different token?
+ var location = locations.ParamEndLocation.Value;
+ if (line > location.LineNumber || (line == location.LineNumber && line == location.LineNumber && column > location.ColumnNumber))
+ {
+ currentParameter = -1;
+ }
+ }
+ help.ActiveParameter = currentParameter;
+
+ return help;
+ }
}
}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs
index eb2713ef..8d164861 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs
@@ -200,9 +200,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
// turn off until needed (10/28/2016)
// serviceHost.SetRequestHandler(DefinitionRequest.Type, HandleDefinitionRequest);
// serviceHost.SetRequestHandler(ReferencesRequest.Type, HandleReferencesRequest);
- // serviceHost.SetRequestHandler(SignatureHelpRequest.Type, HandleSignatureHelpRequest);
// serviceHost.SetRequestHandler(DocumentHighlightRequest.Type, HandleDocumentHighlightRequest);
+ serviceHost.SetRequestHandler(SignatureHelpRequest.Type, HandleSignatureHelpRequest);
serviceHost.SetRequestHandler(CompletionResolveRequest.Type, HandleCompletionResolveRequest);
serviceHost.SetRequestHandler(HoverRequest.Type, HandleHoverRequest);
serviceHost.SetRequestHandler(CompletionRequest.Type, HandleCompletionRequest);
@@ -309,13 +309,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
await Task.FromResult(true);
}
- private static async Task HandleSignatureHelpRequest(
- TextDocumentPosition textDocumentPosition,
- RequestContext requestContext)
- {
- await Task.FromResult(true);
- }
-
private static async Task HandleDocumentHighlightRequest(
TextDocumentPosition textDocumentPosition,
RequestContext requestContext)
@@ -324,6 +317,32 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
#endif
+ private static async Task HandleSignatureHelpRequest(
+ TextDocumentPosition textDocumentPosition,
+ RequestContext requestContext)
+ {
+ // check if Intellisense suggestions are enabled
+ if (!WorkspaceService.Instance.CurrentSettings.IsSuggestionsEnabled)
+ {
+ await Task.FromResult(true);
+ }
+ else
+ {
+ ScriptFile scriptFile = WorkspaceService.Instance.Workspace.GetFile(
+ textDocumentPosition.TextDocument.Uri);
+
+ SignatureHelp help = LanguageService.Instance.GetSignatureHelp(textDocumentPosition, scriptFile);
+ if (help != null)
+ {
+ await requestContext.SendResult(help);
+ }
+ else
+ {
+ await requestContext.SendResult(new SignatureHelp());
+ }
+ }
+ }
+
private static async Task HandleHoverRequest(
TextDocumentPosition textDocumentPosition,
RequestContext requestContext)
@@ -690,6 +709,76 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
return null;
}
+ ///
+ /// Get function signature help for the current position
+ ///
+ internal SignatureHelp GetSignatureHelp(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile)
+ {
+ int startLine = textDocumentPosition.Position.Line;
+ int startColumn = TextUtilities.PositionOfPrevDelimeter(
+ scriptFile.Contents,
+ textDocumentPosition.Position.Line,
+ textDocumentPosition.Position.Character);
+ int endColumn = textDocumentPosition.Position.Character;
+
+ ScriptParseInfo scriptParseInfo = GetScriptParseInfo(textDocumentPosition.TextDocument.Uri);
+
+ ConnectionInfo connInfo;
+ LanguageService.ConnectionServiceInstance.TryFindConnection(
+ scriptFile.ClientFilePath,
+ out connInfo);
+
+ // reparse and bind the SQL statement if needed
+ if (RequiresReparse(scriptParseInfo, scriptFile))
+ {
+ ParseAndBind(scriptFile, connInfo);
+ }
+
+ if (scriptParseInfo != null && scriptParseInfo.ParseResult != null)
+ {
+ if (Monitor.TryEnter(scriptParseInfo.BuildingMetadataLock))
+ {
+ try
+ {
+ QueueItem queueItem = this.BindingQueue.QueueBindingOperation(
+ key: scriptParseInfo.ConnectionKey,
+ bindingTimeout: LanguageService.BindingTimeout,
+ bindOperation: (bindingContext, cancelToken) =>
+ {
+ // get the list of possible current methods for signature help
+ var methods = Resolver.FindMethods(
+ scriptParseInfo.ParseResult,
+ startLine + 1,
+ endColumn + 1,
+ bindingContext.MetadataDisplayInfoProvider);
+
+ // get positional information on the current method
+ var methodLocations = Resolver.GetMethodNameAndParams(scriptParseInfo.ParseResult,
+ startLine + 1,
+ endColumn + 1,
+ bindingContext.MetadataDisplayInfoProvider);
+
+ // convert from the parser format to the VS Code wire format
+ return AutoCompleteHelper.ConvertMethodHelpTextListToSignatureHelp(methods,
+ methodLocations,
+ startLine + 1,
+ endColumn + 1);
+ });
+
+ queueItem.ItemProcessed.WaitOne();
+ return queueItem.GetResultAsT();
+ }
+ finally
+ {
+ Monitor.Exit(scriptParseInfo.BuildingMetadataLock);
+ }
+ }
+ }
+
+ // return null if there isn't a tooltip for the current location
+ return null;
+ }
+
///
/// Return the completion item list for the current text position.
/// This method does not await cache builds since it expects to return quickly
diff --git a/test/Microsoft.SqlTools.ServiceLayer.TestDriver/Tests/LanguageServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.TestDriver/Tests/LanguageServiceTests.cs
index 039b0b6f..dd93a8e6 100644
--- a/test/Microsoft.SqlTools.ServiceLayer.TestDriver/Tests/LanguageServiceTests.cs
+++ b/test/Microsoft.SqlTools.ServiceLayer.TestDriver/Tests/LanguageServiceTests.cs
@@ -3,6 +3,8 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
+using System.IO;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
@@ -255,5 +257,172 @@ namespace Microsoft.SqlTools.ServiceLayer.TestDriver.Tests
await testHelper.Disconnect(queryTempFile.FilePath);
}
}
+
+ [Fact]
+ public async Task FunctionSignatureCompletionReturnsEmptySignatureHelpObjectWhenThereAreNoMatches()
+ {
+ string sqlText = "EXEC sys.fn_not_a_real_function ";
+
+ using (SelfCleaningTempFile tempFile = new SelfCleaningTempFile())
+ using (TestHelper testHelper = new TestHelper())
+ {
+ string ownerUri = tempFile.FilePath;
+ File.WriteAllText(ownerUri, sqlText);
+
+ // Connect
+ await testHelper.Connect(ownerUri, ConnectionTestUtils.LocalhostConnection);
+
+ // Wait for intellisense to be ready
+ var readyParams = await testHelper.Driver.WaitForEvent(IntelliSenseReadyNotification.Type, 30000);
+ Assert.NotNull(readyParams);
+ Assert.Equal(ownerUri, readyParams.OwnerUri);
+
+ // Send a function signature help Request
+ var position = new TextDocumentPosition()
+ {
+ TextDocument = new TextDocumentIdentifier()
+ {
+ Uri = ownerUri
+ },
+ Position = new Position()
+ {
+ Line = 0,
+ Character = sqlText.Length
+ }
+ };
+ var signatureHelp = await testHelper.Driver.SendRequest(SignatureHelpRequest.Type, position);
+
+ Assert.NotNull(signatureHelp);
+ Assert.False(signatureHelp.ActiveSignature.HasValue);
+ Assert.Null(signatureHelp.Signatures);
+
+ await testHelper.Disconnect(ownerUri);
+ }
+ }
+
+ [Fact]
+ public async Task FunctionSignatureCompletionReturnsCorrectFunction()
+ {
+ string sqlText = "EXEC sys.fn_isrolemember ";
+
+ using (SelfCleaningTempFile tempFile = new SelfCleaningTempFile())
+ using (TestHelper testHelper = new TestHelper())
+ {
+ string ownerUri = tempFile.FilePath;
+ File.WriteAllText(ownerUri, sqlText);
+
+ // Connect
+ await testHelper.Connect(ownerUri, ConnectionTestUtils.LocalhostConnection);
+
+ // Wait for intellisense to be ready
+ var readyParams = await testHelper.Driver.WaitForEvent(IntelliSenseReadyNotification.Type, 30000);
+ Assert.NotNull(readyParams);
+ Assert.Equal(ownerUri, readyParams.OwnerUri);
+
+ // Send a function signature help Request
+ var position = new TextDocumentPosition()
+ {
+ TextDocument = new TextDocumentIdentifier()
+ {
+ Uri = ownerUri
+ },
+ Position = new Position()
+ {
+ Line = 0,
+ Character = sqlText.Length
+ }
+ };
+ var signatureHelp = await testHelper.Driver.SendRequest(SignatureHelpRequest.Type, position);
+
+ Assert.NotNull(signatureHelp);
+ Assert.True(signatureHelp.ActiveSignature.HasValue);
+ Assert.NotEmpty(signatureHelp.Signatures);
+
+ var label = signatureHelp.Signatures[signatureHelp.ActiveSignature.Value].Label;
+ Assert.NotNull(label);
+ Assert.NotEmpty(label);
+ Assert.True(label.Contains("fn_isrolemember"));
+
+ await testHelper.Disconnect(ownerUri);
+ }
+ }
+
+ [Fact]
+ public async Task FunctionSignatureCompletionReturnsCorrectParametersAtEachPosition()
+ {
+ string sqlText = "EXEC sys.fn_isrolemember 1, 'testing', 2";
+
+ using (SelfCleaningTempFile tempFile = new SelfCleaningTempFile())
+ using (TestHelper testHelper = new TestHelper())
+ {
+ string ownerUri = tempFile.FilePath;
+ File.WriteAllText(ownerUri, sqlText);
+
+ // Connect
+ await testHelper.Connect(ownerUri, ConnectionTestUtils.LocalhostConnection);
+
+ // Wait for intellisense to be ready
+ var readyParams = await testHelper.Driver.WaitForEvent(IntelliSenseReadyNotification.Type, 30000);
+ Assert.NotNull(readyParams);
+ Assert.Equal(ownerUri, readyParams.OwnerUri);
+
+ // Verify all parameters when the cursor is inside of parameters and at separator boundaries (,)
+ await VerifyFunctionSignatureHelpParameter(testHelper, ownerUri, 25, "fn_isrolemember", 0, "@mode int");
+ await VerifyFunctionSignatureHelpParameter(testHelper, ownerUri, 26, "fn_isrolemember", 0, "@mode int");
+ await VerifyFunctionSignatureHelpParameter(testHelper, ownerUri, 27, "fn_isrolemember", 1, "@login sysname");
+ await VerifyFunctionSignatureHelpParameter(testHelper, ownerUri, 30, "fn_isrolemember", 1, "@login sysname");
+ await VerifyFunctionSignatureHelpParameter(testHelper, ownerUri, 37, "fn_isrolemember", 1, "@login sysname");
+ await VerifyFunctionSignatureHelpParameter(testHelper, ownerUri, 38, "fn_isrolemember", 2, "@tranpubid int");
+ await VerifyFunctionSignatureHelpParameter(testHelper, ownerUri, 39, "fn_isrolemember", 2, "@tranpubid int");
+
+ await testHelper.Disconnect(ownerUri);
+ }
+ }
+
+ public async Task VerifyFunctionSignatureHelpParameter(
+ TestHelper testHelper,
+ string ownerUri,
+ int character,
+ string expectedFunctionName,
+ int expectedParameterIndex,
+ string expectedParameterName)
+ {
+ var position = new TextDocumentPosition()
+ {
+ TextDocument = new TextDocumentIdentifier()
+ {
+ Uri = ownerUri
+ },
+ Position = new Position()
+ {
+ Line = 0,
+ Character = character
+ }
+ };
+ var signatureHelp = await testHelper.Driver.SendRequest(SignatureHelpRequest.Type, position);
+
+ Assert.NotNull(signatureHelp);
+ Assert.NotNull(signatureHelp.ActiveSignature);
+ Assert.True(signatureHelp.ActiveSignature.HasValue);
+ Assert.NotEmpty(signatureHelp.Signatures);
+
+ var activeSignature = signatureHelp.Signatures[signatureHelp.ActiveSignature.Value];
+ Assert.NotNull(activeSignature);
+
+ var label = activeSignature.Label;
+ Assert.NotNull(label);
+ Assert.NotEmpty(label);
+ Assert.True(label.Contains(expectedFunctionName));
+
+ Assert.NotNull(signatureHelp.ActiveParameter);
+ Assert.True(signatureHelp.ActiveParameter.HasValue);
+ Assert.Equal(expectedParameterIndex, signatureHelp.ActiveParameter.Value);
+
+ var parameter = activeSignature.Parameters[signatureHelp.ActiveParameter.Value];
+ Assert.NotNull(parameter);
+ Assert.NotNull(parameter.Label);
+ Assert.NotEmpty(parameter.Label);
+ Assert.Equal(expectedParameterName, parameter.Label);
+ }
}
}