Skip to content

Commit

Permalink
Add partial parsing for parenthesis
Browse files Browse the repository at this point in the history
- This is part of a fix for #1255 - this change enables signature help in implicit expressions by improving the partial parsing. We're now smart enough about the contents of an implicit expression and attempt to balance parenthesis to determine if we should not full parse.

#1255
  • Loading branch information
Ryan Nowak authored and NTaylorMullen committed Jun 15, 2018
1 parent 1779625 commit af63afd
Show file tree
Hide file tree
Showing 3 changed files with 776 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,21 @@ protected override PartialParseResultInternal CanAcceptChange(Span target, Sourc
return HandleInsertion(target, lastChar.Value, change);
}

if (IsAcceptableInsertionInBalancedParenthesis(target, change))
{
return PartialParseResultInternal.Accepted;
}

if (IsAcceptableDeletion(target, change))
{
return HandleDeletion(target, lastChar.Value, change);
}

if (IsAcceptableDeletionInBalancedParenthesis(target, change))
{
return PartialParseResultInternal.Accepted;
}

return PartialParseResultInternal.Rejected;
}

Expand Down Expand Up @@ -216,8 +226,196 @@ private static bool IsAcceptableDeletion(Span target, SourceChange change)
private static bool IsAcceptableInsertion(Span target, SourceChange change)
{
return change.IsInsert &&
(IsAcceptableEndInsertion(target, change) ||
IsAcceptableInnerInsertion(target, change));
(IsAcceptableEndInsertion(target, change) ||
IsAcceptableInnerInsertion(target, change));
}

// Internal for testing
internal static bool IsAcceptableDeletionInBalancedParenthesis(Span target, SourceChange change)
{
if (!change.IsDelete)
{
return false;
}

var changeStart = change.Span.AbsoluteIndex;
var changeLength = change.Span.Length;
var changeEnd = changeStart + changeLength;
var tokens = target.Symbols.Cast<CSharpSymbol>().ToArray();
if (!IsInsideParenthesis(changeStart, tokens) || !IsInsideParenthesis(changeEnd, tokens))
{
// Either the start or end of the delete does not fall inside of parenthesis, unacceptable inner deletion.
return false;
}

var relativePosition = changeStart - target.Start.AbsoluteIndex;
var deletionContent = target.Content.Substring(relativePosition, changeLength);

if (deletionContent.IndexOfAny(new[] { '(', ')' }) >= 0)
{
// Change deleted some parenthesis
return false;
}

return true;
}

// Internal for testing
internal static bool IsAcceptableInsertionInBalancedParenthesis(Span target, SourceChange change)
{
if (!change.IsInsert)
{
return false;
}

if (change.NewText.IndexOfAny(new[] { '(', ')' }) >= 0)
{
// Insertions of parenthesis aren't handled by us. If someone else wants to accept it, they can.
return false;
}

var tokens = target.Symbols.Cast<CSharpSymbol>().ToArray();
if (IsInsideParenthesis(change.Span.AbsoluteIndex, tokens))
{
return true;
}

return false;
}

// Internal for testing
internal static bool IsInsideParenthesis(int position, IReadOnlyList<CSharpSymbol> tokens)
{
var balanceCount = 0;
var foundInsertionPoint = false;
for (var i = 0; i < tokens.Count; i++)
{
var currentToken = tokens[i];
if (ContainsPosition(position, currentToken))
{
if (balanceCount == 0)
{
// Insertion point is outside of parenthesis, i.e. inserting at the pipe: @Foo|Baz()
return false;
}

foundInsertionPoint = true;
}

if (!TryUpdateBalanceCount(currentToken, ref balanceCount))
{
// Couldn't update the count. This usually occurrs when we run into a ')' outside of any parenthesis.
return false;
}

if (foundInsertionPoint && balanceCount == 0)
{
// Once parenthesis become balanced after the insertion point we return true, no need to go further.
// If they get unbalanced down the line the expression was already unbalanced to begin with and this
// change happens prior to any ambiguity.
return true;
}
}

// Unbalanced parenthesis
return false;
}

// Internal for testing
internal static bool ContainsPosition(int position, CSharpSymbol currentToken)
{
var tokenStart = currentToken.Start.AbsoluteIndex;
if (tokenStart == position)
{
// Token is exactly at the insertion point.
return true;
}

var tokenEnd = tokenStart + currentToken.Content.Length;
if (tokenStart < position && tokenEnd > position)
{
// Insertion point falls in the middle of the current token.
return true;
}

return false;
}

// Internal for testing
internal static bool TryUpdateBalanceCount(CSharpSymbol token, ref int count)
{
var updatedCount = count;
if (token.Type == CSharpSymbolType.LeftParenthesis)
{
updatedCount++;
}
else if (token.Type == CSharpSymbolType.RightParenthesis)
{
if (updatedCount == 0)
{
return false;
}

updatedCount--;
}
else if (token.Type == CSharpSymbolType.StringLiteral)
{
var content = token.Content;
if (content.Length > 0 && content[content.Length - 1] != '"')
{
// Incomplete string literal may have consumed some of our parenthesis and usually occurr during auto-completion of '"' => '""'.
if (!TryUpdateCountFromContent(content, ref updatedCount))
{
return false;
}
}
}
else if (token.Type == CSharpSymbolType.CharacterLiteral)
{
var content = token.Content;
if (content.Length > 0 && content[content.Length - 1] != '\'')
{
// Incomplete character literal may have consumed some of our parenthesis and usually occurr during auto-completion of "'" => "''".
if (!TryUpdateCountFromContent(content, ref updatedCount))
{
return false;
}
}
}

if (updatedCount < 0)
{
return false;
}

count = updatedCount;
return true;
}

// Internal for testing
internal static bool TryUpdateCountFromContent(string content, ref int count)
{
var updatedCount = count;
for (var i = 0; i < content.Length; i++)
{
if (content[i] == '(')
{
updatedCount++;
}
else if (content[i] == ')')
{
if (updatedCount == 0)
{
// Unbalanced parenthesis, i.e. @Foo)
return false;
}

updatedCount--;
}
}

count = updatedCount;
return true;
}

// Accepts character insertions at the end of spans. AKA: '@foo' -> '@fooo' or '@foo' -> '@foo ' etc.
Expand Down Expand Up @@ -291,10 +489,16 @@ private PartialParseResultInternal HandleDeletion(Span target, char previousChar
{
return TryAcceptChange(target, change);
}
else
else if (previousChar == '(')
{
return PartialParseResultInternal.Rejected;
var changeRelativePosition = change.Span.AbsoluteIndex - target.Start.AbsoluteIndex;
if (target.Content[changeRelativePosition] == ')')
{
return PartialParseResultInternal.Accepted | PartialParseResultInternal.Provisional;
}
}

return PartialParseResultInternal.Rejected;
}

private PartialParseResultInternal HandleInsertion(Span target, char previousChar, SourceChange change)
Expand All @@ -308,6 +512,10 @@ private PartialParseResultInternal HandleInsertion(Span target, char previousCha
{
return HandleInsertionAfterIdPart(target, change);
}
else if (previousChar == '(')
{
return HandleInsertionAfterOpenParenthesis(target, change);
}
else
{
return PartialParseResultInternal.Rejected;
Expand All @@ -321,6 +529,12 @@ private PartialParseResultInternal HandleInsertionAfterIdPart(Span target, Sourc
{
return TryAcceptChange(target, change);
}
else if (IsDoubleParenthesisInsertion(change) || IsOpenParenthesisInsertion(change))
{
// Allow inserting parens after an identifier - this is needed to support signature
// help intellisense in VS.
return TryAcceptChange(target, change);
}
else if (EndsWithDot(change.NewText))
{
// Accept it, possibly provisionally
Expand All @@ -337,6 +551,40 @@ private PartialParseResultInternal HandleInsertionAfterIdPart(Span target, Sourc
}
}

private PartialParseResultInternal HandleInsertionAfterOpenParenthesis(Span target, SourceChange change)
{
if (IsCloseParenthesisInsertion(change))
{
return TryAcceptChange(target, change);
}

return PartialParseResultInternal.Rejected;
}

private static bool IsDoubleParenthesisInsertion(SourceChange change)
{
return
change.IsInsert &&
change.NewText.Length == 2 &&
change.NewText == "()";
}

private static bool IsOpenParenthesisInsertion(SourceChange change)
{
return
change.IsInsert &&
change.NewText.Length == 1 &&
change.NewText == "(";
}

private static bool IsCloseParenthesisInsertion(SourceChange change)
{
return
change.IsInsert &&
change.NewText.Length == 1 &&
change.NewText == ")";
}

private static bool EndsWithDot(string content)
{
return (content.Length == 1 && content[0] == '.') ||
Expand Down
Loading

0 comments on commit af63afd

Please sign in to comment.