mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-02-16 10:58:30 -05:00
Implemented function signature help, and added tests (#153)
* Implemented function signature help, and added tests * Incremental commit of changes from code review feedback * Rename test * Added check to make sure intellisense is enabled * Use HoverTimeout instead of BindingTimeout
This commit is contained in:
@@ -15,6 +15,7 @@ using Microsoft.SqlServer.Management.SqlParser.Parser;
|
|||||||
using Microsoft.SqlTools.ServiceLayer.Connection;
|
using Microsoft.SqlTools.ServiceLayer.Connection;
|
||||||
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
|
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
|
||||||
using Microsoft.SqlTools.ServiceLayer.SqlContext;
|
using Microsoft.SqlTools.ServiceLayer.SqlContext;
|
||||||
|
using Microsoft.SqlTools.ServiceLayer.Utility;
|
||||||
using Microsoft.SqlTools.ServiceLayer.Workspace;
|
using Microsoft.SqlTools.ServiceLayer.Workspace;
|
||||||
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
|
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
|
||||||
|
|
||||||
@@ -679,5 +680,77 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a SQL Parser List of MethodHelpText objects into a VS Code SignatureHelp object
|
||||||
|
/// </summary>
|
||||||
|
internal static SignatureHelp ConvertMethodHelpTextListToSignatureHelp(List<Babel.MethodHelpText> 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: <name> param1, param2, ..., paramn RETURNS <type>
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,9 +200,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
|
|||||||
// turn off until needed (10/28/2016)
|
// turn off until needed (10/28/2016)
|
||||||
// serviceHost.SetRequestHandler(DefinitionRequest.Type, HandleDefinitionRequest);
|
// serviceHost.SetRequestHandler(DefinitionRequest.Type, HandleDefinitionRequest);
|
||||||
// serviceHost.SetRequestHandler(ReferencesRequest.Type, HandleReferencesRequest);
|
// serviceHost.SetRequestHandler(ReferencesRequest.Type, HandleReferencesRequest);
|
||||||
// serviceHost.SetRequestHandler(SignatureHelpRequest.Type, HandleSignatureHelpRequest);
|
|
||||||
// serviceHost.SetRequestHandler(DocumentHighlightRequest.Type, HandleDocumentHighlightRequest);
|
// serviceHost.SetRequestHandler(DocumentHighlightRequest.Type, HandleDocumentHighlightRequest);
|
||||||
|
|
||||||
|
serviceHost.SetRequestHandler(SignatureHelpRequest.Type, HandleSignatureHelpRequest);
|
||||||
serviceHost.SetRequestHandler(CompletionResolveRequest.Type, HandleCompletionResolveRequest);
|
serviceHost.SetRequestHandler(CompletionResolveRequest.Type, HandleCompletionResolveRequest);
|
||||||
serviceHost.SetRequestHandler(HoverRequest.Type, HandleHoverRequest);
|
serviceHost.SetRequestHandler(HoverRequest.Type, HandleHoverRequest);
|
||||||
serviceHost.SetRequestHandler(CompletionRequest.Type, HandleCompletionRequest);
|
serviceHost.SetRequestHandler(CompletionRequest.Type, HandleCompletionRequest);
|
||||||
@@ -309,13 +309,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
|
|||||||
await Task.FromResult(true);
|
await Task.FromResult(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task HandleSignatureHelpRequest(
|
|
||||||
TextDocumentPosition textDocumentPosition,
|
|
||||||
RequestContext<SignatureHelp> requestContext)
|
|
||||||
{
|
|
||||||
await Task.FromResult(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task HandleDocumentHighlightRequest(
|
private static async Task HandleDocumentHighlightRequest(
|
||||||
TextDocumentPosition textDocumentPosition,
|
TextDocumentPosition textDocumentPosition,
|
||||||
RequestContext<DocumentHighlight[]> requestContext)
|
RequestContext<DocumentHighlight[]> requestContext)
|
||||||
@@ -324,6 +317,32 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
private static async Task HandleSignatureHelpRequest(
|
||||||
|
TextDocumentPosition textDocumentPosition,
|
||||||
|
RequestContext<SignatureHelp> requestContext)
|
||||||
|
{
|
||||||
|
// check if Intellisense suggestions are enabled
|
||||||
|
if (!WorkspaceService<SqlToolsSettings>.Instance.CurrentSettings.IsSuggestionsEnabled)
|
||||||
|
{
|
||||||
|
await Task.FromResult(true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ScriptFile scriptFile = WorkspaceService<SqlToolsSettings>.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(
|
private static async Task HandleHoverRequest(
|
||||||
TextDocumentPosition textDocumentPosition,
|
TextDocumentPosition textDocumentPosition,
|
||||||
RequestContext<Hover> requestContext)
|
RequestContext<Hover> requestContext)
|
||||||
@@ -690,6 +709,76 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get function signature help for the current position
|
||||||
|
/// </summary>
|
||||||
|
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<SignatureHelp>();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Monitor.Exit(scriptParseInfo.BuildingMetadataLock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return null if there isn't a tooltip for the current location
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Return the completion item list for the current text position.
|
/// Return the completion item list for the current text position.
|
||||||
/// This method does not await cache builds since it expects to return quickly
|
/// This method does not await cache builds since it expects to return quickly
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
// 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;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
|
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
|
||||||
@@ -255,5 +257,172 @@ namespace Microsoft.SqlTools.ServiceLayer.TestDriver.Tests
|
|||||||
await testHelper.Disconnect(queryTempFile.FilePath);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user