fixed peek definition tokenizing (#281)

* fixed peek definition tokenizing

* fixed signature help test

* added new heuristic for PeekDefinition behaviour

* added tests for new heuristic

* fixed code according to Kevin's CR

* fixed failing test due to shared connection

* changed uri for procedure test
This commit is contained in:
Aditya Bist
2017-03-23 13:19:59 -07:00
committed by GitHub
parent d2fd8f8f41
commit 9e8e1df95e
5 changed files with 1293 additions and 917 deletions

View File

@@ -774,6 +774,100 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
return completionItem; return completionItem;
} }
/// <summary>
/// Queue a task to the binding queue
/// </summary>
/// <param name="textDocumentPosition"></param>
/// <param name="scriptParseInfo"></param>
/// <param name="connectionInfo"></param>
/// <param name="scriptFile"></param>
/// <param name="tokenText"></param>
/// <returns> Returns the result of the task as a DefinitionResult </returns>
private DefinitionResult QueueTask(TextDocumentPosition textDocumentPosition, ScriptParseInfo scriptParseInfo,
ConnectionInfo connInfo, ScriptFile scriptFile, string tokenText)
{
// Queue the task with the binding queue
QueueItem queueItem = this.BindingQueue.QueueBindingOperation(
key: scriptParseInfo.ConnectionKey,
bindingTimeout: LanguageService.PeekDefinitionTimeout,
bindOperation: (bindingContext, cancelToken) =>
{
string schemaName = this.GetSchemaName(scriptParseInfo, textDocumentPosition.Position, scriptFile);
// Script object using SMO
Scripter scripter = new Scripter(bindingContext.ServerConnection, connInfo);
return scripter.GetScript(
scriptParseInfo.ParseResult,
textDocumentPosition.Position,
bindingContext.MetadataDisplayInfoProvider,
tokenText,
schemaName);
},
timeoutOperation: (bindingContext) =>
{
// return error result
return new DefinitionResult
{
IsErrorResult = true,
Message = SR.PeekDefinitionTimedoutError,
Locations = null
};
});
// wait for the queue item
queueItem.ItemProcessed.WaitOne();
var result = queueItem.GetResultAsT<DefinitionResult>();
return result;
}
private DefinitionResult GetDefinitionFromTokenList(TextDocumentPosition textDocumentPosition, List<Token> tokenList,
ScriptParseInfo scriptParseInfo, ScriptFile scriptFile, ConnectionInfo connInfo)
{
DefinitionResult lastResult = null;
foreach (var token in tokenList)
{
// Strip "[" and "]"(if present) from the token text to enable matching with the suggestions.
// The suggestion title does not contain any sql punctuation
string tokenText = TextUtilities.RemoveSquareBracketSyntax(token.Text);
textDocumentPosition.Position.Line = token.StartLocation.LineNumber;
textDocumentPosition.Position.Character = token.StartLocation.ColumnNumber;
if (Monitor.TryEnter(scriptParseInfo.BuildingMetadataLock))
{
try
{
var result = QueueTask(textDocumentPosition, scriptParseInfo, connInfo, scriptFile, tokenText);
lastResult = result;
if (!result.IsErrorResult)
{
return result;
}
}
catch (Exception ex)
{
// if any exceptions are raised return error result with message
Logger.Write(LogLevel.Error, "Exception in GetDefinition " + ex.ToString());
return new DefinitionResult
{
IsErrorResult = true,
Message = SR.PeekDefinitionError(ex.Message),
Locations = null
};
}
finally
{
Monitor.Exit(scriptParseInfo.BuildingMetadataLock);
}
}
else
{
Logger.Write(LogLevel.Error, "Timeout waiting to query metadata from server");
}
}
return (lastResult != null) ? lastResult : null;
}
/// <summary> /// <summary>
/// Get definition for a selected sql object using SMO Scripting /// Get definition for a selected sql object using SMO Scripting
/// </summary> /// </summary>
@@ -796,76 +890,63 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
} }
// Get token from selected text // Get token from selected text
Token selectedToken = ScriptDocumentInfo.GetToken(scriptParseInfo, textDocumentPosition.Position.Line + 1, textDocumentPosition.Position.Character); Tuple<Stack<Token>, Queue<Token>> selectedToken = ScriptDocumentInfo.GetPeekDefinitionTokens(scriptParseInfo,
textDocumentPosition.Position.Line + 1, textDocumentPosition.Position.Character + 1);
if (selectedToken == null) if (selectedToken == null)
{ {
return null; return null;
} }
// Strip "[" and "]"(if present) from the token text to enable matching with the suggestions.
// The suggestion title does not contain any sql punctuation
string tokenText = TextUtilities.RemoveSquareBracketSyntax(selectedToken.Text);
if (scriptParseInfo.IsConnected && Monitor.TryEnter(scriptParseInfo.BuildingMetadataLock)) if (scriptParseInfo.IsConnected)
{ {
try //try children tokens first
{ Stack<Token> childrenTokens = selectedToken.Item1;
// Queue the task with the binding queue List<Token> tokenList = childrenTokens.ToList();
QueueItem queueItem = this.BindingQueue.QueueBindingOperation( DefinitionResult childrenResult = GetDefinitionFromTokenList(textDocumentPosition, tokenList, scriptParseInfo, scriptFile, connInfo);
key: scriptParseInfo.ConnectionKey,
bindingTimeout: LanguageService.PeekDefinitionTimeout, // if the children peak definition returned null then
bindOperation: (bindingContext, cancelToken) => // try the parents
{ if (childrenResult == null || childrenResult.IsErrorResult)
string schemaName = this.GetSchemaName(scriptParseInfo, textDocumentPosition.Position, scriptFile); {
// Script object using SMO Queue<Token> parentTokens = selectedToken.Item2;
Scripter scripter = new Scripter(bindingContext.ServerConnection, connInfo); tokenList = parentTokens.ToList();
return scripter.GetScript( DefinitionResult parentResult = GetDefinitionFromTokenList(textDocumentPosition, tokenList, scriptParseInfo, scriptFile, connInfo);
scriptParseInfo.ParseResult, return (parentResult == null) ? null : parentResult;
textDocumentPosition.Position, }
bindingContext.MetadataDisplayInfoProvider, else
tokenText, {
schemaName); return childrenResult;
}, }
timeoutOperation: (bindingContext) => }
{ else
// return error result {
return new DefinitionResult // User is not connected.
{ return new DefinitionResult
IsErrorResult = true, {
Message = SR.PeekDefinitionTimedoutError, IsErrorResult = true,
Locations = null Message = SR.PeekDefinitionNotConnectedError,
}; Locations = null
}); };
// wait for the queue item
queueItem.ItemProcessed.WaitOne();
return queueItem.GetResultAsT<DefinitionResult>();
}
catch (Exception ex)
{
// if any exceptions are raised return error result with message
Logger.Write(LogLevel.Error, "Exception in GetDefinition " + ex.ToString());
return new DefinitionResult
{
IsErrorResult = true,
Message = SR.PeekDefinitionError(ex.Message),
Locations = null
};
}
finally
{
Monitor.Exit(scriptParseInfo.BuildingMetadataLock);
}
} }
else }
/// <summary>
/// Wrapper around find token method
/// </summary>
/// <param name="scriptParseInfo"></param>
/// <param name="startLine"></param>
/// <param name="startColumn"></param>
/// <returns> token index</returns>
private int FindTokenWithCorrectOffset(ScriptParseInfo scriptParseInfo, int startLine, int startColumn)
{
var tokenIndex = scriptParseInfo.ParseResult.Script.TokenManager.FindToken(startLine, startColumn);
var end = scriptParseInfo.ParseResult.Script.TokenManager.GetToken(tokenIndex).EndLocation;
if (end.LineNumber == startLine && end.ColumnNumber == startColumn)
{ {
// User is not connected. return tokenIndex + 1;
return new DefinitionResult
{
IsErrorResult = true,
Message = SR.PeekDefinitionNotConnectedError,
Locations = null
};
} }
return tokenIndex;
} }
/// <summary> /// <summary>
@@ -878,13 +959,13 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
private string GetSchemaName(ScriptParseInfo scriptParseInfo, Position position, ScriptFile scriptFile) private string GetSchemaName(ScriptParseInfo scriptParseInfo, Position position, ScriptFile scriptFile)
{ {
// Offset index by 1 for sql parser // Offset index by 1 for sql parser
int startLine = position.Line + 1; int startLine = position.Line;
int startColumn = position.Character + 1; int startColumn = position.Character;
// Get schema name // Get schema name
if (scriptParseInfo != null && scriptParseInfo.ParseResult != null && scriptParseInfo.ParseResult.Script != null && scriptParseInfo.ParseResult.Script.Tokens != null) if (scriptParseInfo != null && scriptParseInfo.ParseResult != null && scriptParseInfo.ParseResult.Script != null && scriptParseInfo.ParseResult.Script.Tokens != null)
{ {
var tokenIndex = scriptParseInfo.ParseResult.Script.TokenManager.FindToken(startLine, startColumn); var tokenIndex = FindTokenWithCorrectOffset(scriptParseInfo, startLine, startColumn);
var prevTokenIndex = scriptParseInfo.ParseResult.Script.TokenManager.GetPreviousSignificantTokenIndex(tokenIndex); var prevTokenIndex = scriptParseInfo.ParseResult.Script.TokenManager.GetPreviousSignificantTokenIndex(tokenIndex);
var prevTokenText = scriptParseInfo.ParseResult.Script.TokenManager.GetText(prevTokenIndex); var prevTokenText = scriptParseInfo.ParseResult.Script.TokenManager.GetText(prevTokenIndex);
if (prevTokenText != null && prevTokenText.Equals(".")) if (prevTokenText != null && prevTokenText.Equals("."))

View File

@@ -1,133 +1,192 @@
// //
// Copyright (c) Microsoft. All rights reserved. // Copyright (c) Microsoft. All rights reserved.
// 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 Microsoft.SqlServer.Management.SqlParser.Parser; using System;
using Microsoft.SqlTools.Utility; using Microsoft.SqlServer.Management.SqlParser.Parser;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; using Microsoft.SqlTools.Utility;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion using System.Collections.Generic;
{
/// <summary> namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion
/// A class to calculate the numbers used by SQL parser using the text positions and content {
/// </summary> /// <summary>
internal class ScriptDocumentInfo /// A class to calculate the numbers used by SQL parser using the text positions and content
{ /// </summary>
/// <summary> internal class ScriptDocumentInfo
/// Create new instance {
/// </summary> /// <summary>
public ScriptDocumentInfo(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile, ScriptParseInfo scriptParseInfo) /// Create new instance
: this(textDocumentPosition, scriptFile) /// </summary>
{ public ScriptDocumentInfo(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile, ScriptParseInfo scriptParseInfo)
Validate.IsNotNull(nameof(scriptParseInfo), scriptParseInfo); : this(textDocumentPosition, scriptFile)
{
ScriptParseInfo = scriptParseInfo; Validate.IsNotNull(nameof(scriptParseInfo), scriptParseInfo);
// need to adjust line & column for base-1 parser indices
Token = GetToken(scriptParseInfo, ParserLine, ParserColumn); ScriptParseInfo = scriptParseInfo;
} // need to adjust line & column for base-1 parser indices
Token = GetToken(scriptParseInfo, ParserLine, ParserColumn);
private ScriptDocumentInfo(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile) }
{
StartLine = textDocumentPosition.Position.Line; private ScriptDocumentInfo(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile)
ParserLine = textDocumentPosition.Position.Line + 1; {
StartColumn = TextUtilities.PositionOfPrevDelimeter( StartLine = textDocumentPosition.Position.Line;
scriptFile.Contents, ParserLine = textDocumentPosition.Position.Line + 1;
textDocumentPosition.Position.Line, StartColumn = TextUtilities.PositionOfPrevDelimeter(
textDocumentPosition.Position.Character); scriptFile.Contents,
EndColumn = TextUtilities.PositionOfNextDelimeter( textDocumentPosition.Position.Line,
scriptFile.Contents, textDocumentPosition.Position.Character);
textDocumentPosition.Position.Line, EndColumn = TextUtilities.PositionOfNextDelimeter(
textDocumentPosition.Position.Character); scriptFile.Contents,
ParserColumn = textDocumentPosition.Position.Character + 1; textDocumentPosition.Position.Line,
Contents = scriptFile.Contents; textDocumentPosition.Position.Character);
} ParserColumn = textDocumentPosition.Position.Character + 1;
Contents = scriptFile.Contents;
/// <summary> }
/// Creates a new <see cref="ScriptDocumentInfo"/> with no backing <see cref="ScriptParseInfo"/> defined
/// </summary> /// <summary>
/// <param name="textDocumentPosition">A <see cref="TextDocumentPosition"/></param> /// Creates a new <see cref="ScriptDocumentInfo"/> with no backing <see cref="ScriptParseInfo"/> defined
/// <param name="scriptFile">A <see cref="ScriptFile"/> to process</param> /// </summary>
/// <returns></returns> /// <param name="textDocumentPosition">A <see cref="TextDocumentPosition"/></param>
public static ScriptDocumentInfo CreateDefaultDocumentInfo(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile) /// <param name="scriptFile">A <see cref="ScriptFile"/> to process</param>
{ /// <returns></returns>
return new ScriptDocumentInfo(textDocumentPosition, scriptFile); public static ScriptDocumentInfo CreateDefaultDocumentInfo(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile)
} {
return new ScriptDocumentInfo(textDocumentPosition, scriptFile);
/// <summary> }
/// Gets a string containing the full contents of the file.
/// </summary> /// <summary>
public string Contents { get; private set; } /// Gets a string containing the full contents of the file.
/// </summary>
/// <summary> public string Contents { get; private set; }
/// Script Parse Info Instance
/// </summary> /// <summary>
public ScriptParseInfo ScriptParseInfo { get; private set; } /// Script Parse Info Instance
/// </summary>
/// <summary> public ScriptParseInfo ScriptParseInfo { get; private set; }
/// Start Line
/// </summary> /// <summary>
public int StartLine { get; private set; } /// Start Line
/// </summary>
/// <summary> public int StartLine { get; private set; }
/// Parser Line
/// </summary> /// <summary>
public int ParserLine { get; private set; } /// Parser Line
/// </summary>
/// <summary> public int ParserLine { get; private set; }
/// Start Column
/// </summary> /// <summary>
public int StartColumn { get; private set; } /// Start Column
/// </summary>
/// <summary> public int StartColumn { get; private set; }
/// end Column
/// </summary> /// <summary>
public int EndColumn { get; private set; } /// end Column
/// </summary>
/// <summary> public int EndColumn { get; private set; }
/// Parser Column
/// </summary> /// <summary>
public int ParserColumn { get; private set; } /// Parser Column
/// </summary>
/// <summary> public int ParserColumn { get; private set; }
/// The token text in the file content used for completion list
/// </summary> /// <summary>
public virtual string TokenText /// The token text in the file content used for completion list
{ /// </summary>
get public virtual string TokenText
{ {
return Token != null ? Token.Text : null; get
} {
} return Token != null ? Token.Text : null;
}
/// <summary> }
/// The token in the file content used for completion list
/// </summary> /// <summary>
public Token Token { get; private set; } /// The token in the file content used for completion list
/// </summary>
/// <summary> public Token Token { get; private set; }
/// Returns the token that will be used by SQL parser for creating the completion list
/// </summary> /// <summary>
internal static Token GetToken(ScriptParseInfo scriptParseInfo, int startLine, int startColumn) /// Returns the token that will be used by SQL parser for creating the completion list
{ /// </summary>
if (scriptParseInfo != null && scriptParseInfo.ParseResult != null && scriptParseInfo.ParseResult.Script != null && scriptParseInfo.ParseResult.Script.Tokens != null) internal static Token GetToken(ScriptParseInfo scriptParseInfo, int startLine, int startColumn)
{ {
var tokenIndex = scriptParseInfo.ParseResult.Script.TokenManager.FindToken(startLine, startColumn); if (scriptParseInfo != null && scriptParseInfo.ParseResult != null && scriptParseInfo.ParseResult.Script != null && scriptParseInfo.ParseResult.Script.Tokens != null)
if (tokenIndex >= 0) {
{ var tokenIndex = scriptParseInfo.ParseResult.Script.TokenManager.FindToken(startLine, startColumn);
// return the current token if (tokenIndex >= 0)
int currentIndex = 0; {
foreach (var token in scriptParseInfo.ParseResult.Script.Tokens) // return the current token
{ int currentIndex = 0;
if (currentIndex == tokenIndex) foreach (var token in scriptParseInfo.ParseResult.Script.Tokens)
{ {
return token; if (currentIndex == tokenIndex)
} {
++currentIndex; return token;
} }
} ++currentIndex;
} }
return null; }
} }
} return null;
} }
/// <summary>
/// Returns the token that is used for Peek Definition objects
/// </summary>
internal static Tuple<Stack<Token>, Queue<Token>> GetPeekDefinitionTokens(ScriptParseInfo scriptParseInfo, int startLine, int startColumn)
{
Stack<Token> childrenTokens = new Stack<Token>();
Queue<Token> parentTokens = new Queue<Token>();
if (scriptParseInfo != null
&& scriptParseInfo.ParseResult != null
&& scriptParseInfo.ParseResult.Script != null
&& scriptParseInfo.ParseResult.Script.Tokens != null)
{
var tokenIndex = scriptParseInfo.ParseResult.Script.TokenManager.FindToken(startLine, startColumn);
if (tokenIndex >= 0)
{
// return the current token and the ones to its right
// until we hit a whitespace token
int currentIndex = 0;
foreach (var token in scriptParseInfo.ParseResult.Script.Tokens)
{
if (currentIndex == tokenIndex)
{
// push all parent tokens until we hit whitespace
int parentIndex = currentIndex;
while (true)
{
if (scriptParseInfo.ParseResult.Script.TokenManager.GetToken(parentIndex).Type != "LEX_WHITE")
{
parentTokens.Enqueue(scriptParseInfo.ParseResult.Script.TokenManager.GetToken(parentIndex));
parentIndex--;
}
else
{
break;
}
}
}
else if (currentIndex > tokenIndex)
{
// push all children tokens until we hit whitespace
if (scriptParseInfo.ParseResult.Script.TokenManager.GetToken(currentIndex).Type != "LEX_WHITE")
{
childrenTokens.Push(token);
}
else
{
break;
}
}
++currentIndex;
}
return Tuple.Create(childrenTokens, parentTokens);
}
}
return null;
}
}
}

View File

@@ -133,8 +133,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting
/// <returns>Location object of the script file</returns> /// <returns>Location object of the script file</returns>
internal DefinitionResult GetScript(ParseResult parseResult, Position position, IMetadataDisplayInfoProvider metadataDisplayInfoProvider, string tokenText, string schemaName) internal DefinitionResult GetScript(ParseResult parseResult, Position position, IMetadataDisplayInfoProvider metadataDisplayInfoProvider, string tokenText, string schemaName)
{ {
int parserLine = position.Line + 1; int parserLine = position.Line;
int parserColumn = position.Character + 1; int parserColumn = position.Character;
// Get DeclarationItems from The Intellisense Resolver for the selected token. The type of the selected token is extracted from the declarationItem. // Get DeclarationItems from The Intellisense Resolver for the selected token. The type of the selected token is extracted from the declarationItem.
IEnumerable<Declaration> declarationItems = GetCompletionsForToken(parseResult, parserLine, parserColumn, metadataDisplayInfoProvider); IEnumerable<Declaration> declarationItems = GetCompletionsForToken(parseResult, parserLine, parserColumn, metadataDisplayInfoProvider);
if (declarationItems != null && declarationItems.Count() > 0) if (declarationItems != null && declarationItems.Count() > 0)

View File

@@ -156,7 +156,36 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace.Contracts
/// <summary> /// <summary>
/// Gets or sets the zero-based column number. /// Gets or sets the zero-based column number.
/// </summary> /// </summary>
public int Character { get; set; } public int Character { get; set; }
/// <summary>
/// Overrides the base equality method
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public override bool Equals(object obj)
{
if (obj == null || (obj as Position == null))
{
return false;
}
Position p = (Position) obj;
bool result = (Line == p.Line) && (Character == p.Character);
return result;
}
/// <summary>
/// Overrides the base GetHashCode method
/// </summary>
/// <returns></returns>
public override int GetHashCode()
{
int hash = 17;
hash = hash * 23 + Line.GetHashCode();
hash = hash * 23 + Character.GetHashCode();
return hash;
}
} }
[DebuggerDisplay("Start = {Start.Line}:{Start.Character}, End = {End.Line}:{End.Character}")] [DebuggerDisplay("Start = {Start.Line}:{Start.Character}, End = {End.Line}:{End.Character}")]
@@ -171,6 +200,37 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace.Contracts
/// Gets or sets the ending position of the range. /// Gets or sets the ending position of the range.
/// </summary> /// </summary>
public Position End { get; set; } public Position End { get; set; }
/// <summary>
/// Overrides the base equality method
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public override bool Equals(object obj)
{
if (obj == null || !(obj is Range))
{
return false;
}
Range range = (Range) obj;
bool sameStart = range.Start.Equals(Start);
bool sameEnd = range.End.Equals(End);
return (sameStart && sameEnd);
}
/// <summary>
/// Overrides the base GetHashCode method
/// </summary>
/// <returns></returns>
public override int GetHashCode()
{
int hash = 17;
hash = hash * 23 + Start.GetHashCode();
hash = hash * 23 + End.GetHashCode();
return hash;
}
} }
[DebuggerDisplay("Range = {Range.Start.Line}:{Range.Start.Character} - {Range.End.Line}:{Range.End.Character}, Uri = {Uri}")] [DebuggerDisplay("Range = {Range.Start.Line}:{Range.Start.Character} - {Range.End.Line}:{Range.End.Character}, Uri = {Uri}")]
@@ -185,6 +245,35 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace.Contracts
/// Gets or sets the Range indicating the range in which location refers. /// Gets or sets the Range indicating the range in which location refers.
/// </summary> /// </summary>
public Range Range { get; set; } public Range Range { get; set; }
/// <summary>
/// Overrides the base equality method
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public override bool Equals(object obj)
{
if (obj == null || (obj as Location == null))
{
return false;
}
Location loc = (Location)obj;
bool sameUri = string.Equals(loc.Uri, Uri);
bool sameRange = loc.Range.Equals(Range);
return (sameUri && sameRange);
}
/// <summary>
/// Overrides the base GetHashCode method
/// </summary>
/// <returns></returns>
public override int GetHashCode()
{
int hash = 17;
hash = hash * 23 + Uri.GetHashCode();
hash = hash * 23 + Range.GetHashCode();
return hash;
}
} }
public enum FileChangeType public enum FileChangeType