mirror of https://github.com/icsharpcode/ILSpy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
645 lines
19 KiB
645 lines
19 KiB
// |
|
// CSharpCompletionEngineBase.cs |
|
// |
|
// Author: |
|
// Mike Krüger <mkrueger@xamarin.com> |
|
// |
|
// Copyright (c) 2011 Xamarin Inc. (http://xamarin.com) |
|
// |
|
// Permission is hereby granted, free of charge, to any person obtaining a copy |
|
// of this software and associated documentation files (the "Software"), to deal |
|
// in the Software without restriction, including without limitation the rights |
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
// copies of the Software, and to permit persons to whom the Software is |
|
// furnished to do so, subject to the following conditions: |
|
// |
|
// The above copyright notice and this permission notice shall be included in |
|
// all copies or substantial portions of the Software. |
|
// |
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|
// THE SOFTWARE. |
|
using System; |
|
using System.Collections.Generic; |
|
using System.Linq; |
|
using System.Text; |
|
|
|
using ICSharpCode.NRefactory.CSharp.Resolver; |
|
using ICSharpCode.NRefactory.Editor; |
|
using ICSharpCode.NRefactory.TypeSystem; |
|
using ICSharpCode.NRefactory.Semantics; |
|
using ICSharpCode.NRefactory.TypeSystem.Implementation; |
|
using ICSharpCode.NRefactory.CSharp.TypeSystem; |
|
|
|
namespace ICSharpCode.NRefactory.CSharp.Completion |
|
{ |
|
/// <summary> |
|
/// Acts as a common base between code completion and parameter completion. |
|
/// </summary> |
|
public class CSharpCompletionEngineBase |
|
{ |
|
protected IDocument document; |
|
protected int offset; |
|
protected TextLocation location; |
|
protected IUnresolvedTypeDefinition currentType; |
|
protected IUnresolvedMember currentMember; |
|
|
|
#region Input properties |
|
public CSharpTypeResolveContext ctx { get; private set; } |
|
|
|
public CompilationUnit Unit { get; private set; } |
|
|
|
public CSharpParsedFile CSharpParsedFile { get; private set; } |
|
|
|
public IProjectContent ProjectContent { get; private set; } |
|
|
|
ICompilation compilation; |
|
|
|
protected ICompilation Compilation { |
|
get { |
|
if (compilation == null) |
|
compilation = ProjectContent.Resolve (ctx).Compilation; |
|
return compilation; |
|
} |
|
} |
|
#endregion |
|
|
|
protected CSharpCompletionEngineBase (IProjectContent content, CSharpTypeResolveContext ctx, CompilationUnit unit, CSharpParsedFile parsedFile) |
|
{ |
|
if (content == null) |
|
throw new ArgumentNullException ("content"); |
|
if (ctx == null) |
|
throw new ArgumentNullException ("ctx"); |
|
if (unit == null) |
|
throw new ArgumentNullException ("unit"); |
|
if (parsedFile == null) |
|
throw new ArgumentNullException ("parsedFile"); |
|
|
|
this.ProjectContent = content; |
|
this.ctx = ctx; |
|
this.Unit = unit; |
|
this.CSharpParsedFile = parsedFile; |
|
} |
|
|
|
|
|
public IMemberProvider MemberProvider { |
|
get; |
|
set; |
|
} |
|
|
|
protected void SetOffset (int offset) |
|
{ |
|
Reset (); |
|
|
|
this.offset = offset; |
|
this.location = document.GetLocation (offset); |
|
var provider = MemberProvider ?? new DefaultMemberProvider (this); |
|
provider.GetCurrentMembers (offset, out currentType, out currentMember); |
|
} |
|
|
|
#region Context helper methods |
|
protected bool IsInsideCommentOrString () |
|
{ |
|
var text = GetMemberTextToCaret (); |
|
bool inSingleComment = false, inString = false, inVerbatimString = false, inChar = false, inMultiLineComment = false; |
|
|
|
for (int i = 0; i < text.Item1.Length - 1; i++) { |
|
char ch = text.Item1 [i]; |
|
char nextCh = text.Item1 [i + 1]; |
|
|
|
switch (ch) { |
|
case '/': |
|
if (inString || inChar || inVerbatimString) |
|
break; |
|
if (nextCh == '/') { |
|
i++; |
|
inSingleComment = true; |
|
} |
|
if (nextCh == '*') |
|
inMultiLineComment = true; |
|
break; |
|
case '*': |
|
if (inString || inChar || inVerbatimString || inSingleComment) |
|
break; |
|
if (nextCh == '/') { |
|
i++; |
|
inMultiLineComment = false; |
|
} |
|
break; |
|
case '@': |
|
if (inString || inChar || inVerbatimString || inSingleComment || inMultiLineComment) |
|
break; |
|
if (nextCh == '"') { |
|
i++; |
|
inVerbatimString = true; |
|
} |
|
break; |
|
case '\n': |
|
case '\r': |
|
inSingleComment = false; |
|
inString = false; |
|
inChar = false; |
|
break; |
|
case '\\': |
|
if (inString || inChar) |
|
i++; |
|
break; |
|
case '"': |
|
if (inSingleComment || inMultiLineComment || inChar) |
|
break; |
|
if (inVerbatimString) { |
|
if (nextCh == '"') { |
|
i++; |
|
break; |
|
} |
|
inVerbatimString = false; |
|
break; |
|
} |
|
inString = !inString; |
|
break; |
|
case '\'': |
|
if (inSingleComment || inMultiLineComment || inString || inVerbatimString) |
|
break; |
|
inChar = !inChar; |
|
break; |
|
} |
|
} |
|
|
|
return inSingleComment || inString || inVerbatimString || inChar || inMultiLineComment; |
|
} |
|
|
|
protected bool IsInsideComment (int offset) |
|
{ |
|
var loc = document.GetLocation (offset); |
|
return Unit.GetNodeAt<ICSharpCode.NRefactory.CSharp.Comment> (loc.Line, loc.Column) != null; |
|
} |
|
|
|
protected bool IsInsideDocComment () |
|
{ |
|
var loc = document.GetLocation (offset); |
|
var cmt = Unit.GetNodeAt<ICSharpCode.NRefactory.CSharp.Comment> (loc.Line, loc.Column - 1); |
|
return cmt != null && cmt.CommentType == CommentType.Documentation; |
|
} |
|
|
|
protected bool IsInsideString (int offset) |
|
{ |
|
|
|
var loc = document.GetLocation (offset); |
|
var expr = Unit.GetNodeAt<PrimitiveExpression> (loc.Line, loc.Column); |
|
return expr != null && expr.Value is string; |
|
} |
|
|
|
protected CSharpResolver GetState () |
|
{ |
|
return new CSharpResolver (ctx); |
|
/*var state = new CSharpResolver (ctx); |
|
|
|
state.CurrentMember = currentMember; |
|
state.CurrentTypeDefinition = currentType; |
|
state.CurrentUsingScope = CSharpParsedFile.GetUsingScope (location); |
|
if (state.CurrentMember != null) { |
|
var node = Unit.GetNodeAt (location); |
|
if (node == null) |
|
return state; |
|
var navigator = new NodeListResolveVisitorNavigator (new[] { node }); |
|
var visitor = new ResolveVisitor (state, CSharpParsedFile, navigator); |
|
Unit.AcceptVisitor (visitor, null); |
|
try { |
|
var newState = visitor.GetResolverStateBefore (node); |
|
if (newState != null) |
|
state = newState; |
|
} catch (Exception) { |
|
} |
|
} |
|
|
|
return state;*/ |
|
} |
|
#endregion |
|
|
|
#region Basic parsing/resolving functions |
|
Stack<Tuple<char, int>> GetBracketStack (string memberText) |
|
{ |
|
var bracketStack = new Stack<Tuple<char, int>> (); |
|
|
|
bool inSingleComment = false, inString = false, inVerbatimString = false, inChar = false, inMultiLineComment = false; |
|
|
|
for (int i = 0; i < memberText.Length; i++) { |
|
char ch = memberText [i]; |
|
char nextCh = i + 1 < memberText.Length ? memberText [i + 1] : '\0'; |
|
switch (ch) { |
|
case '(': |
|
case '[': |
|
case '{': |
|
if (inString || inChar || inVerbatimString || inSingleComment || inMultiLineComment) |
|
break; |
|
bracketStack.Push (Tuple.Create (ch, i)); |
|
break; |
|
case ')': |
|
case ']': |
|
case '}': |
|
if (inString || inChar || inVerbatimString || inSingleComment || inMultiLineComment) |
|
break; |
|
if (bracketStack.Count > 0) |
|
bracketStack.Pop (); |
|
break; |
|
case '/': |
|
if (inString || inChar || inVerbatimString) |
|
break; |
|
if (nextCh == '/') { |
|
i++; |
|
inSingleComment = true; |
|
} |
|
if (nextCh == '*') |
|
inMultiLineComment = true; |
|
break; |
|
case '*': |
|
if (inString || inChar || inVerbatimString || inSingleComment) |
|
break; |
|
if (nextCh == '/') { |
|
i++; |
|
inMultiLineComment = false; |
|
} |
|
break; |
|
case '@': |
|
if (inString || inChar || inVerbatimString || inSingleComment || inMultiLineComment) |
|
break; |
|
if (nextCh == '"') { |
|
i++; |
|
inVerbatimString = true; |
|
} |
|
break; |
|
case '\n': |
|
case '\r': |
|
inSingleComment = false; |
|
inString = false; |
|
inChar = false; |
|
break; |
|
case '\\': |
|
if (inString || inChar) |
|
i++; |
|
break; |
|
case '"': |
|
if (inSingleComment || inMultiLineComment || inChar) |
|
break; |
|
if (inVerbatimString) { |
|
if (nextCh == '"') { |
|
i++; |
|
break; |
|
} |
|
inVerbatimString = false; |
|
break; |
|
} |
|
inString = !inString; |
|
break; |
|
case '\'': |
|
if (inSingleComment || inMultiLineComment || inString || inVerbatimString) |
|
break; |
|
inChar = !inChar; |
|
break; |
|
default : |
|
break; |
|
} |
|
} |
|
return bracketStack; |
|
} |
|
|
|
protected void AppendMissingClosingBrackets (StringBuilder wrapper, string memberText, bool appendSemicolon) |
|
{ |
|
var bracketStack = GetBracketStack (memberText); |
|
bool didAppendSemicolon = !appendSemicolon; |
|
char lastBracket = '\0'; |
|
while (bracketStack.Count > 0) { |
|
var t = bracketStack.Pop (); |
|
switch (t.Item1) { |
|
case '(': |
|
wrapper.Append (')'); |
|
didAppendSemicolon = false; |
|
lastBracket = ')'; |
|
break; |
|
case '[': |
|
wrapper.Append (']'); |
|
didAppendSemicolon = false; |
|
lastBracket = ']'; |
|
break; |
|
case '<': |
|
wrapper.Append ('>'); |
|
didAppendSemicolon = false; |
|
lastBracket = '>'; |
|
break; |
|
case '{': |
|
int o = t.Item2 - 1; |
|
if (!didAppendSemicolon) { |
|
didAppendSemicolon = true; |
|
wrapper.Append (';'); |
|
} |
|
|
|
bool didAppendCatch = false; |
|
while (o >= "try".Length) { |
|
char ch = memberText [o]; |
|
if (!char.IsWhiteSpace (ch)) { |
|
if (ch == 'y' && memberText [o - 1] == 'r' && memberText [o - 2] == 't') { |
|
wrapper.Append ("} catch {}"); |
|
didAppendCatch = true; |
|
} |
|
break; |
|
} |
|
o--; |
|
} |
|
if (!didAppendCatch) |
|
wrapper.Append ('}'); |
|
break; |
|
} |
|
} |
|
if (currentMember == null && lastBracket == ']') { |
|
// attribute context |
|
wrapper.Append ("class GenAttr {}"); |
|
} else { |
|
if (!didAppendSemicolon) |
|
wrapper.Append (';'); |
|
} |
|
} |
|
|
|
protected CompilationUnit ParseStub (string continuation, bool appendSemicolon = true, string afterContinuation = null) |
|
{ |
|
var mt = GetMemberTextToCaret (); |
|
if (mt == null) |
|
return null; |
|
|
|
string memberText = mt.Item1; |
|
bool wrapInClass = mt.Item2; |
|
|
|
var wrapper = new StringBuilder (); |
|
|
|
if (wrapInClass) { |
|
/* foreach (var child in Unit.Children) { |
|
if (child is UsingDeclaration) { |
|
var offset = document.GetOffset (child.StartLocation); |
|
wrapper.Append (document.GetText (offset, document.GetOffset (child.EndLocation) - offset)); |
|
} |
|
}*/ |
|
wrapper.Append ("class Stub {"); |
|
wrapper.AppendLine (); |
|
} |
|
|
|
wrapper.Append (memberText); |
|
wrapper.Append (continuation); |
|
AppendMissingClosingBrackets (wrapper, memberText, appendSemicolon); |
|
wrapper.Append (afterContinuation); |
|
|
|
if (wrapInClass) |
|
wrapper.Append ('}'); |
|
|
|
TextLocation memberLocation; |
|
if (currentMember != null && currentType != null && currentType.Kind != TypeKind.Enum) { |
|
memberLocation = currentMember.Region.Begin; |
|
} else if (currentType != null) { |
|
memberLocation = currentType.Region.Begin; |
|
} else { |
|
memberLocation = new TextLocation (1, 1); |
|
} |
|
|
|
using (var stream = new System.IO.StringReader (wrapper.ToString ())) { |
|
try { |
|
var parser = new CSharpParser (); |
|
return parser.Parse (stream, "stub.cs", wrapInClass ? memberLocation.Line - 2 : 0); |
|
} catch (Exception) { |
|
Console.WriteLine ("------"); |
|
Console.WriteLine (wrapper); |
|
throw; |
|
} |
|
} |
|
} |
|
|
|
string cachedText = null; |
|
|
|
protected virtual void Reset () |
|
{ |
|
cachedText = null; |
|
} |
|
|
|
protected Tuple<string, bool> GetMemberTextToCaret () |
|
{ |
|
int startOffset; |
|
if (currentMember != null && currentType != null && currentType.Kind != TypeKind.Enum) { |
|
startOffset = document.GetOffset (currentMember.Region.BeginLine, currentMember.Region.BeginColumn); |
|
} else if (currentType != null) { |
|
startOffset = document.GetOffset (currentType.Region.BeginLine, currentType.Region.BeginColumn); |
|
} else { |
|
startOffset = 0; |
|
} |
|
while (startOffset > 0) { |
|
char ch = document.GetCharAt (startOffset - 1); |
|
if (ch != ' ' && ch != '\t') |
|
break; |
|
--startOffset; |
|
} |
|
if (cachedText == null) |
|
cachedText = document.GetText (startOffset, offset - startOffset); |
|
|
|
return Tuple.Create (cachedText, startOffset != 0); |
|
} |
|
|
|
protected Tuple<CSharpParsedFile, AstNode, CompilationUnit> GetInvocationBeforeCursor (bool afterBracket) |
|
{ |
|
CompilationUnit baseUnit; |
|
if (currentMember == null) { |
|
baseUnit = ParseStub ("", false); |
|
var section = baseUnit.GetNodeAt<AttributeSection> (location.Line, location.Column - 2); |
|
var attr = section != null ? section.Attributes.LastOrDefault () : null; |
|
if (attr != null) { |
|
// insert target type into compilation unit, to respect the |
|
attr.Remove (); |
|
var node = Unit.GetNodeAt (location) ?? Unit; |
|
node.AddChild (attr, AttributeSection.AttributeRole); |
|
return Tuple.Create (CSharpParsedFile, (AstNode)attr, Unit); |
|
} |
|
} |
|
if (currentMember == null && currentType == null) { |
|
return null; |
|
} |
|
baseUnit = ParseStub (afterBracket ? "" : "x"); |
|
|
|
var memberLocation = currentMember != null ? currentMember.Region.Begin : currentType.Region.Begin; |
|
var mref = baseUnit.GetNodeAt (location.Line, location.Column - 1, n => n is InvocationExpression || n is ObjectCreateExpression); |
|
AstNode expr = null; |
|
if (mref is InvocationExpression) { |
|
expr = ((InvocationExpression)mref).Target; |
|
} else if (mref is ObjectCreateExpression) { |
|
expr = mref; |
|
} else { |
|
baseUnit = ParseStub (")};", false); |
|
mref = baseUnit.GetNodeAt (location.Line, location.Column - 1, n => n is InvocationExpression || n is ObjectCreateExpression); |
|
if (mref is InvocationExpression) { |
|
expr = ((InvocationExpression)mref).Target; |
|
} else if (mref is ObjectCreateExpression) { |
|
expr = mref; |
|
} |
|
if (expr == null) |
|
return null; |
|
} |
|
var member = Unit.GetNodeAt<AttributedNode> (memberLocation); |
|
var member2 = baseUnit.GetNodeAt<AttributedNode> (memberLocation); |
|
member2.Remove (); |
|
member.ReplaceWith (member2); |
|
var tsvisitor = new TypeSystemConvertVisitor (CSharpParsedFile.FileName); |
|
Unit.AcceptVisitor (tsvisitor, null); |
|
return Tuple.Create (tsvisitor.ParsedFile, (AstNode)expr, Unit); |
|
} |
|
|
|
protected Tuple<ResolveResult, CSharpResolver> ResolveExpression (CSharpParsedFile file, AstNode expr, CompilationUnit unit) |
|
{ |
|
if (expr == null) |
|
return null; |
|
AstNode resolveNode; |
|
if (expr is Expression || expr is AstType) { |
|
resolveNode = expr; |
|
} else if (expr is VariableDeclarationStatement) { |
|
resolveNode = ((VariableDeclarationStatement)expr).Type; |
|
} else { |
|
resolveNode = expr; |
|
} |
|
|
|
// var newContent = ProjectContent.UpdateProjectContent (CSharpParsedFile, file); |
|
|
|
var csResolver = new CSharpAstResolver(new CSharpResolver (ctx), unit, CSharpParsedFile); |
|
|
|
var result = csResolver.Resolve (resolveNode); |
|
var state = csResolver.GetResolverStateBefore (resolveNode); |
|
return Tuple.Create (result, state); |
|
} |
|
|
|
protected static void Print (AstNode node) |
|
{ |
|
var v = new CSharpOutputVisitor (Console.Out, new CSharpFormattingOptions ()); |
|
node.AcceptVisitor (v, null); |
|
} |
|
|
|
#endregion |
|
|
|
class DefaultMemberProvider : IMemberProvider |
|
{ |
|
CSharpCompletionEngineBase engine; |
|
|
|
|
|
public DefaultMemberProvider (CSharpCompletionEngineBase engine) |
|
{ |
|
this.engine = engine; |
|
} |
|
|
|
public void GetCurrentMembers (int offset, out IUnresolvedTypeDefinition currentType, out IUnresolvedMember currentMember) |
|
{ |
|
var document = engine.document; |
|
var location = engine.location; |
|
|
|
currentType = null; |
|
|
|
foreach (var type in engine.CSharpParsedFile.TopLevelTypeDefinitions) { |
|
if (type.Region.Begin < location) |
|
currentType = type; |
|
} |
|
currentType = FindInnerType (currentType, location); |
|
|
|
// location is beyond last reported end region, now we need to check, if the end region changed |
|
if (currentType != null && currentType.Region.End < location) { |
|
if (!IsInsideType (currentType, location)) |
|
currentType = null; |
|
} |
|
currentMember = null; |
|
if (currentType != null) { |
|
foreach (var member in currentType.Members) { |
|
if (member.Region.Begin < location && (currentMember == null || currentMember.Region.Begin < member.Region.Begin)) |
|
currentMember = member; |
|
} |
|
} |
|
|
|
// location is beyond last reported end region, now we need to check, if the end region changed |
|
// NOTE: Enums are a special case, there the "last" field needs to be treated as current member |
|
if (currentMember != null && currentMember.Region.End < location && currentType.Kind != TypeKind.Enum) { |
|
if (!IsInsideType (currentMember, location)) |
|
currentMember = null; |
|
} |
|
var stack = engine.GetBracketStack (engine.GetMemberTextToCaret ().Item1); |
|
if (stack.Count == 0) |
|
currentMember = null; |
|
} |
|
|
|
IUnresolvedTypeDefinition FindInnerType (IUnresolvedTypeDefinition parent, TextLocation location) |
|
{ |
|
if (parent == null) |
|
return null; |
|
var currentType = parent; |
|
foreach (var type in parent.NestedTypes) { |
|
if (type.Region.Begin < location && location < type.Region.End) |
|
currentType = FindInnerType (type, location); |
|
} |
|
|
|
return currentType; |
|
} |
|
|
|
bool IsInsideType (IUnresolvedEntity currentType, TextLocation location) |
|
{ |
|
var document = engine.document; |
|
|
|
int startOffset = document.GetOffset (currentType.Region.Begin); |
|
int endOffset = document.GetOffset (location); |
|
bool foundEndBracket = false; |
|
|
|
var bracketStack = new Stack<char> (); |
|
|
|
bool isInString = false, isInChar = false; |
|
bool isInLineComment = false, isInBlockComment = false; |
|
|
|
for (int i = startOffset; i < endOffset; i++) { |
|
char ch = document.GetCharAt (i); |
|
switch (ch) { |
|
case '(': |
|
case '[': |
|
case '{': |
|
if (!isInString && !isInChar && !isInLineComment && !isInBlockComment) |
|
bracketStack.Push (ch); |
|
break; |
|
case ')': |
|
case ']': |
|
case '}': |
|
if (!isInString && !isInChar && !isInLineComment && !isInBlockComment) |
|
if (bracketStack.Count > 0) |
|
bracketStack.Pop (); |
|
break; |
|
case '\r': |
|
case '\n': |
|
isInLineComment = false; |
|
break; |
|
case '/': |
|
if (isInBlockComment) { |
|
if (i > 0 && document.GetCharAt (i - 1) == '*') |
|
isInBlockComment = false; |
|
} else if (!isInString && !isInChar && i + 1 < document.TextLength) { |
|
char nextChar = document.GetCharAt (i + 1); |
|
if (nextChar == '/') |
|
isInLineComment = true; |
|
if (!isInLineComment && nextChar == '*') |
|
isInBlockComment = true; |
|
} |
|
break; |
|
case '"': |
|
if (!(isInChar || isInLineComment || isInBlockComment)) |
|
isInString = !isInString; |
|
break; |
|
case '\'': |
|
if (!(isInString || isInLineComment || isInBlockComment)) |
|
isInChar = !isInChar; |
|
break; |
|
default : |
|
break; |
|
} |
|
} |
|
return bracketStack.Any (t => t == '{'); |
|
} |
|
} |
|
|
|
} |
|
} |