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.
470 lines
14 KiB
470 lines
14 KiB
// <file> |
|
// <copyright see="prj:///doc/copyright.txt">2002-2005 AlphaSierraPapa</copyright> |
|
// <license see="prj:///doc/license.txt">GNU General Public License</license> |
|
// <owner name="Daniel Grunwald" email="daniel@danielgrunwald.de"/> |
|
// <version>$Revision$</version> |
|
// </file> |
|
|
|
using System; |
|
using System.Collections.Generic; |
|
using System.Text; |
|
using ICSharpCode.Core; |
|
using ICSharpCode.SharpDevelop.Dom; |
|
|
|
namespace Grunwald.BooBinding.CodeCompletion |
|
{ |
|
// TODO: We could need some unit tests for this. |
|
public class ExpressionFinder : IExpressionFinder |
|
{ |
|
string fileName; |
|
|
|
public ExpressionFinder(string fileName) |
|
{ |
|
this.fileName = fileName; |
|
} |
|
|
|
#region RemoveLastPart |
|
/// <summary> |
|
/// Removes the last part of the expression. |
|
/// </summary> |
|
/// <example> |
|
/// "arr[i]" => "arr" |
|
/// "obj.Field" => "obj" |
|
/// "obj.Method(args,...)" => "obj.Method" |
|
/// </example> |
|
public string RemoveLastPart(string expression) |
|
{ |
|
int state = 0; |
|
int pos = 0; |
|
int lastFinishPos = 0; |
|
int brackets = 0; |
|
while (state >= 0) { |
|
state = FindNextCodeCharacter(state, expression, ref pos); |
|
if (pos >= expression.Length) |
|
break; |
|
char c = expression[pos]; |
|
if (c == '[' || c == '(' || c == '{') { |
|
if (brackets == 0) |
|
lastFinishPos = pos; |
|
brackets += 1; |
|
} |
|
if (brackets == 0 && c == '.') { |
|
lastFinishPos = pos; |
|
} |
|
if (brackets > 0 && (c == ']' || c == ')' || c == '}')) { |
|
brackets -= 1; |
|
} |
|
} |
|
return expression.Substring(0, lastFinishPos); |
|
} |
|
#endregion |
|
|
|
#region Find Expression |
|
// The expression finder can find an expression in a text |
|
// inText is the full source code, offset the cursor position |
|
|
|
// example: "_var = 'bla'\n_var^\nprint _var" |
|
// where ^ is the cursor position |
|
// in that simple case the expression finder should return 'n_var'. |
|
|
|
// but also complex expressions like |
|
// 'filename.Substring(filename.IndexOf("var="))' |
|
// should be returned if the cursor is after the last ). |
|
|
|
// implementation note: the text after offset is irrelevant, so |
|
// every operation on the string aborts after reaching offset |
|
|
|
const string _closingBrackets = "}])"; |
|
const string _openingBrackets = "{[("; |
|
|
|
public ExpressionResult FindExpression(string inText, int offset) |
|
{ |
|
if (inText == null || offset >= inText.Length) |
|
return new ExpressionResult(null); |
|
// OK, first try a kind of "quick find" |
|
int i = offset + 1; |
|
const string forbidden = "\"\'/#)]}"; |
|
const string finish = "([{=+*<,:"; |
|
int start = -1; |
|
while (i > 0) { |
|
i -= 1; |
|
char c = inText[i]; |
|
if (finish.IndexOf(c) >= 0) { |
|
start = i + 1; |
|
break; |
|
} |
|
if (forbidden.IndexOf(c) >= 0) { |
|
//LoggingService.Debug("Quickfind failed: got " + c); |
|
break; |
|
} |
|
if (char.IsWhiteSpace(c)) { |
|
break; |
|
} |
|
if (start >= 0) { |
|
if (CheckString(inText, start, "/#\"\'", "\r\n")) { |
|
return GetExpression(inText, start, offset + 1); |
|
} |
|
} |
|
} |
|
|
|
inText = SimplifyCode(inText, offset); |
|
if (inText == null) { |
|
return new ExpressionResult(null); |
|
} |
|
// inText now has no comments or string literals, but the same meaning in |
|
// terms of the type system |
|
// Now go back until a finish-character or a whitespace character |
|
Stack<int> bracketStack = new Stack<int>(); |
|
i = inText.Length; |
|
while (i > 0) { |
|
i -= 1; |
|
char c = inText[i]; |
|
if (bracketStack.Count == 0 && (finish.IndexOf(c) >= 0 || Char.IsWhiteSpace(c))) { |
|
// SUCCESS! |
|
return GetExpression(inText, i + 1, inText.Length); |
|
} |
|
int bracket = _closingBrackets.IndexOf(c); |
|
if (bracket >= 0) { |
|
bracketStack.Push(bracket); |
|
} |
|
bracket = _openingBrackets.IndexOf(c); |
|
if (bracket >= 0) { |
|
while (bracketStack.Count > 0 && bracketStack.Pop() > bracket); |
|
} |
|
} |
|
return new ExpressionResult(null); |
|
} |
|
|
|
bool CheckString(string text, int offset, string forbidden, string finish) |
|
{ |
|
int i = offset; |
|
while (i > 0) { |
|
i -= 1; |
|
char c = text[i]; |
|
if (forbidden.IndexOf(c) >= 0) return false; |
|
if (finish.IndexOf(c) >= 0) return true; |
|
} |
|
return true; |
|
} |
|
|
|
ExpressionResult GetExpression(string inText, int start, int end) |
|
{ |
|
if (start == end) return new ExpressionResult(null); |
|
StringBuilder b = new StringBuilder(); |
|
bool wasSpace = true; |
|
int i = start; |
|
while (i < end) { |
|
char c = inText[i]; |
|
if (Char.IsWhiteSpace(c)) { |
|
if (!wasSpace) b.Append(' '); |
|
wasSpace = true; |
|
} else { |
|
wasSpace = false; |
|
b.Append(c); |
|
} |
|
i += 1; |
|
} |
|
ExpressionResult result = new ExpressionResult(b.ToString()); |
|
// Now try to find the context of the expression |
|
while (--start > 0 && char.IsWhiteSpace(inText, start)); |
|
if (start > 2 && char.IsWhiteSpace(inText, start - 2) |
|
&& inText[start - 1] == 'a' && inText[start] == 's') |
|
{ |
|
result.Context = ExpressionContext.Type; |
|
} else if (start > 6 && char.IsWhiteSpace(inText, start - 6) |
|
&& inText[start - 5] == 'i' && inText[start - 4] == 'm' |
|
&& inText[start - 3] == 'p' && inText[start - 2] == 'o' |
|
&& inText[start - 1] == 'r' && inText[start] == 't') |
|
{ |
|
result.Context = ExpressionContext.Importable; |
|
} else { |
|
bool wasSquareBracket = false; |
|
int brackets = 0; |
|
while (start > 0) { |
|
char c = inText[start]; |
|
if (c == '\n') break; |
|
if (brackets == 0) { |
|
if (c == '(' || c == ',') |
|
break; |
|
if (!char.IsWhiteSpace(inText, start)) |
|
wasSquareBracket = inText[start] == '['; |
|
} else { |
|
if (c == '[' || c == '(') |
|
brackets -= 1; |
|
} |
|
if (c == ')' || c == ']') |
|
brackets += 1; |
|
start -= 1; |
|
} |
|
if (wasSquareBracket) { |
|
result.Context = BooAttributeContext.Instance; |
|
} |
|
} |
|
|
|
return result; |
|
} |
|
|
|
internal class BooAttributeContext : ExpressionContext |
|
{ |
|
public static BooAttributeContext Instance = new BooAttributeContext(); |
|
|
|
public override bool ShowEntry(object o) |
|
{ |
|
IClass c = o as IClass; |
|
if (c != null && c.IsAbstract) |
|
return false; |
|
if (ExpressionContext.Attribute.ShowEntry(o)) |
|
return true; |
|
if (c == null) |
|
return false; |
|
if (BooProject.BooCompilerPC != null) { |
|
return c.IsTypeInInheritanceTree(BooProject.BooCompilerPC.GetClass("Boo.Lang.Compiler.AbstractAstAttribute")); |
|
} else { |
|
foreach (IReturnType baseType in c.BaseTypes) { |
|
if (baseType.FullyQualifiedName == "Boo.Lang.Compiler.AbstractAstAttribute") |
|
return true; |
|
} |
|
return false; |
|
} |
|
} |
|
} |
|
#endregion |
|
|
|
#region Find Full Expression |
|
public ExpressionResult FindFullExpression(string inText, int offset) |
|
{ |
|
ExpressionResult result = FindExpression(inText, offset); |
|
if (result.Expression == null) |
|
return result; |
|
StringBuilder b = new StringBuilder(result.Expression); |
|
// accepting current identifier |
|
int i; |
|
for (i = offset + 1; i < inText.Length; i++) { |
|
char c = inText[i]; |
|
if (!char.IsLetterOrDigit(c) && c != '_') { |
|
break; |
|
} |
|
} |
|
i -= 1; |
|
// accepting brackets/parenthesis |
|
int state = 0; |
|
ResetStateMachine(); |
|
Stack<int> bracketStack = new Stack<int>(); |
|
while (state >= 0) { |
|
state = FindNextCodeCharacter(state, inText, ref i); |
|
if (state < 0) break; |
|
char c = (i < inText.Length) ? inText[i] : '\0'; |
|
int bracket = _openingBrackets.IndexOf(c); |
|
if (bracket >= 0) { |
|
bracketStack.Push(bracket); |
|
} else { |
|
if (bracketStack.Count == 0) { |
|
b.Append(inText, offset + 1, i - offset - 1); |
|
result.Expression = b.ToString(); |
|
return result; |
|
} else if (c == '\0') { |
|
// end of document |
|
break; |
|
} |
|
} |
|
bracket = _closingBrackets.IndexOf(c); |
|
if (bracket >= 0) { |
|
while (bracketStack.Count > 0 && bracketStack.Pop() > bracket); |
|
} |
|
} |
|
return new ExpressionResult(null); |
|
} |
|
#endregion |
|
|
|
#region State Machine / SimplifyCode |
|
static readonly int[] inputTable; |
|
|
|
static ExpressionFinder() |
|
{ |
|
inputTable = new int[128]; |
|
for (int i = 0; i < inputTable.Length; i++) { |
|
inputTable[i] = _elseIndex; |
|
} |
|
inputTable[ 34] = 0; // " |
|
inputTable[ 39] = 1; // ' |
|
inputTable[ 92] = 2; // \ |
|
inputTable[ 10] = 3; // \n |
|
inputTable[ 13] = 3; // \r |
|
inputTable[ 36] = 4; // $ |
|
inputTable[123] = 5; // { |
|
inputTable[125] = 6; // } |
|
inputTable[ 35] = 7; // # |
|
inputTable[ 47] = 8; // / |
|
inputTable[ 42] = 9; // * |
|
} |
|
|
|
const int _elseIndex = 10; |
|
public const int PossibleRegexStart = 12; |
|
public const int LineCommentState = 13; |
|
|
|
static readonly |
|
int[][] _stateTable = { // " ' \ \n $ { } # / * else |
|
/* 0: in Code */ new int[] { 1 , 7 , 0 , 0 , 0 , 0 , 0 , 13 , 12 , 0 , 0 }, |
|
/* 1: after " */ new int[] { 2 , 6 , 10 , 0 , 8 , 6 , 6 , 6 , 6 , 6 , 6 }, |
|
/* 2: after "" */ new int[] { 3 , 7 , 0 , 0 , 0 , 0 , 0 , 13 , 12 , 0 , 0 }, |
|
/* 3: in """ */ new int[] { 4 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 }, |
|
/* 4: in """, " */ new int[] { 5 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 }, |
|
/* 5: in """, "" */ new int[] { 0 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 }, |
|
/* 6: in "-string */ new int[] { 0 , 6 , 10 , 0 , 8 , 6 , 6 , 6 , 6 , 6 , 6 }, |
|
/* 7: in '-string */ new int[] { 7 , 0 , 11 , 0 , 7 , 7 , 7 , 7 , 7 , 7 , 7 }, |
|
/* 8: after $ in " */ new int[] { 0 , 6 , 10 , 0 , 8 , 9 , 6 , 6 , 6 , 6 , 6 }, |
|
/* 9: in "{ */ new int[] { 9 , 9 , 9 , 9 , 9 , 9 , 6 , 9 , 9 , 9 , 9 }, |
|
/* 10: after \ in " */ new int[] { 6 , 6 , 6 , 0 , 6 , 6 , 6 , 6 , 6 , 6 , 6 }, |
|
/* 11: after \ in ' */ new int[] { 7 , 7 , 7 , 0 , 7 , 7 , 7 , 7 , 7 , 7 , 7 }, |
|
/* 12: after / */ new int[] { 1 , 7 , 0 , 0 , 0 , 0 , 0 , 0 , 13 ,-14 , 0 }, |
|
/* 13: line comment */ new int[] { 13 , 13 , 13 , 0 , 13 , 13 , 13 , 13 , 13 , 13 , 13 }, |
|
/* 14: block comment*/ new int[] { 14 , 14 , 14 , 14 , 14 , 14 , 14 , 14 , 14 , 15 , 14 }, |
|
/* 15: after * in bc*/ new int[] { 14 , 14 , 14 , 14 , 14 , 14 , 14 , 14 ,-15 , 15 , 14 } |
|
}; |
|
|
|
static bool IsInNormalCode(int state) |
|
{ |
|
return state == 0 || state == 2 || state == 12; |
|
} |
|
|
|
int commentblocks; |
|
|
|
public void ResetStateMachine() |
|
{ |
|
commentblocks = 0; |
|
} |
|
|
|
public int FeedStateMachine(int oldState, char c) |
|
{ |
|
int charNum = (int)c; |
|
int input; |
|
if (charNum < inputTable.Length) { |
|
input = inputTable[charNum]; |
|
} else { |
|
input = _elseIndex; |
|
} |
|
int action = _stateTable[oldState][input]; |
|
if (action == -14) { |
|
// enter block comment |
|
commentblocks += 1; |
|
return 14; |
|
} else if (action == -15) { |
|
// leave block comment |
|
commentblocks -= 1; |
|
if (commentblocks == 0) |
|
return 0; |
|
else |
|
return 14; |
|
} |
|
return action; |
|
} |
|
|
|
/// <summary> |
|
/// Goes to the next position in "text" that is code (not comment, string etc.). |
|
/// Returns a state that has be passed in as <paramref name="state"/> on the |
|
/// next call. |
|
/// </summary> |
|
public int FindNextCodeCharacter(int state, string text, ref int pos) |
|
{ |
|
ResetStateMachine(); |
|
do { |
|
pos += 1; |
|
if (pos >= text.Length) |
|
break; |
|
char c = text[pos]; |
|
state = FeedStateMachine(state, c); |
|
if (state == PossibleRegexStart) { |
|
// after / could be a regular expression, do a special check for that |
|
int regexEnd = SkipRegularExpression(text, pos, text.Length - 1); |
|
if (regexEnd > 0) { |
|
pos = regexEnd; |
|
} else if (regexEnd == -1) { |
|
// cursor is in regex |
|
return -1; |
|
} // else: regexEnd is 0 if its not a regex |
|
} |
|
} while (!IsInNormalCode(state)); |
|
return state; |
|
} |
|
|
|
/// <summary>This method makes boo source code "simpler" by removing all comments |
|
/// and replacing all string litarals through string.Empty. |
|
/// Regular expressions literals are replaced with the simple regex /a/</summary> |
|
public string SimplifyCode(string inText, int offset) |
|
{ |
|
StringBuilder result = new StringBuilder(); |
|
StringBuilder inStringResult = new StringBuilder(" "); |
|
int state = 0; |
|
ResetStateMachine(); |
|
int i = -1; |
|
while (i < offset) { |
|
i += 1; |
|
char c = inText[i]; |
|
int action = FeedStateMachine(state, c); |
|
if (action == 9) { |
|
// enter inner string expression (${...}) |
|
if (state == 9) |
|
inStringResult.Append(c); |
|
else |
|
inStringResult.Length = 1; |
|
state = action; |
|
} else if (action == 0 || action == PossibleRegexStart) { |
|
// go to normal code |
|
if (action == PossibleRegexStart) { |
|
// after / could be a regular expression, do a special check for that |
|
int regexEnd = SkipRegularExpression(inText, i, offset); |
|
if (regexEnd > 0) { |
|
i = regexEnd; |
|
result.Append("/a"); |
|
} else if (regexEnd == -1) { |
|
// cursor is in regex |
|
return null; |
|
} |
|
} |
|
if (state == 2 || (state >= 6 && state <= 11)) |
|
result.Append("''"); |
|
if (IsInNormalCode(state)) |
|
result.Append(c); |
|
state = action; |
|
} else { |
|
state = action; |
|
} |
|
} |
|
if (IsInNormalCode(state)) { |
|
// cursor is in normal code |
|
return result.ToString(); |
|
} else if (state == 9) { |
|
// cursor is in inner string expression (${...}) |
|
return inStringResult.ToString(); |
|
} else { |
|
// cursor is in comment/string |
|
return null; |
|
} |
|
} |
|
|
|
/// <summary>Skips the regular expression in inText at position pos. Returns end position of the ending / if |
|
/// successful or 0 is no regular expression was found at the location. |
|
/// Return -1 if maxOffset is inside the regular expression.</summary> |
|
public int SkipRegularExpression(string inText, int pos, int maxOffset) |
|
{ |
|
bool containsWhitespace; |
|
if (pos > 0) { |
|
containsWhitespace = (inText[pos - 1] == '@'); |
|
} else { |
|
containsWhitespace = false; |
|
} |
|
if (pos == maxOffset) return -1; // cursor is after / -> cursor inside regex |
|
if (inText[pos + 1] == '/') return 0; // double // is comment, no regex |
|
int i = pos; |
|
while (i < maxOffset) { |
|
i += 1; |
|
if (!containsWhitespace && Char.IsWhiteSpace(inText, i)) |
|
return 0; // this is no regex |
|
if (inText[i] == '/') |
|
return i; |
|
} |
|
return -1; // maxOffset inside regex |
|
} |
|
#endregion |
|
} |
|
}
|
|
|