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.
474 lines
13 KiB
474 lines
13 KiB
// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) |
|
// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) |
|
|
|
using System; |
|
using System.Collections.Generic; |
|
using System.Globalization; |
|
using System.Text; |
|
|
|
namespace ICSharpCode.AvalonEdit.Indentation.CSharp |
|
{ |
|
sealed class IndentationSettings |
|
{ |
|
public string IndentString = "\t"; |
|
/// <summary>Leave empty lines empty.</summary> |
|
public bool LeaveEmptyLines = true; |
|
} |
|
|
|
sealed class IndentationReformatter |
|
{ |
|
/// <summary> |
|
/// An indentation block. Tracks the state of the indentation. |
|
/// </summary> |
|
struct Block |
|
{ |
|
/// <summary> |
|
/// The indentation outside of the block. |
|
/// </summary> |
|
public string OuterIndent; |
|
|
|
/// <summary> |
|
/// The indentation inside the block. |
|
/// </summary> |
|
public string InnerIndent; |
|
|
|
/// <summary> |
|
/// The last word that was seen inside this block. |
|
/// Because parenthesis open a sub-block and thus don't change their parent's LastWord, |
|
/// this property can be used to identify the type of block statement (if, while, switch) |
|
/// at the position of the '{'. |
|
/// </summary> |
|
public string LastWord; |
|
|
|
/// <summary> |
|
/// The type of bracket that opened this block (, [ or { |
|
/// </summary> |
|
public char Bracket; |
|
|
|
/// <summary> |
|
/// Gets whether there's currently a line continuation going on inside this block. |
|
/// </summary> |
|
public bool Continuation; |
|
|
|
/// <summary> |
|
/// Gets whether there's currently a 'one-line-block' going on. 'one-line-blocks' occur |
|
/// with if statements that don't use '{}'. They are not represented by a Block instance on |
|
/// the stack, but are instead handled similar to line continuations. |
|
/// This property is an integer because there might be multiple nested one-line-blocks. |
|
/// As soon as there is a finished statement, OneLineBlock is reset to 0. |
|
/// </summary> |
|
public int OneLineBlock; |
|
|
|
/// <summary> |
|
/// The previous value of one-line-block before it was reset. |
|
/// Used to restore the indentation of 'else' to the correct level. |
|
/// </summary> |
|
public int PreviousOneLineBlock; |
|
|
|
public void ResetOneLineBlock() |
|
{ |
|
PreviousOneLineBlock = OneLineBlock; |
|
OneLineBlock = 0; |
|
} |
|
|
|
/// <summary> |
|
/// Gets the line number where this block started. |
|
/// </summary> |
|
public int StartLine; |
|
|
|
public void Indent(IndentationSettings set) |
|
{ |
|
Indent(set.IndentString); |
|
} |
|
|
|
public void Indent(string indentationString) |
|
{ |
|
OuterIndent = InnerIndent; |
|
InnerIndent += indentationString; |
|
Continuation = false; |
|
ResetOneLineBlock(); |
|
LastWord = ""; |
|
} |
|
|
|
public override string ToString() |
|
{ |
|
return string.Format( |
|
CultureInfo.InvariantCulture, |
|
"[Block StartLine={0}, LastWord='{1}', Continuation={2}, OneLineBlock={3}, PreviousOneLineBlock={4}]", |
|
this.StartLine, this.LastWord, this.Continuation, this.OneLineBlock, this.PreviousOneLineBlock); |
|
} |
|
} |
|
|
|
StringBuilder wordBuilder; |
|
Stack<Block> blocks; // blocks contains all blocks outside of the current |
|
Block block; // block is the current block |
|
|
|
bool inString; |
|
bool inChar; |
|
bool verbatim; |
|
bool escape; |
|
|
|
bool lineComment; |
|
bool blockComment; |
|
|
|
char lastRealChar; // last non-comment char |
|
|
|
public void Reformat(IDocumentAccessor doc, IndentationSettings set) |
|
{ |
|
Init(); |
|
|
|
while (doc.MoveNext()) { |
|
Step(doc, set); |
|
} |
|
} |
|
|
|
public void Init() |
|
{ |
|
wordBuilder = new StringBuilder(); |
|
blocks = new Stack<Block>(); |
|
block = new Block(); |
|
block.InnerIndent = ""; |
|
block.OuterIndent = ""; |
|
block.Bracket = '{'; |
|
block.Continuation = false; |
|
block.LastWord = ""; |
|
block.OneLineBlock = 0; |
|
block.PreviousOneLineBlock = 0; |
|
block.StartLine = 0; |
|
|
|
inString = false; |
|
inChar = false; |
|
verbatim = false; |
|
escape = false; |
|
|
|
lineComment = false; |
|
blockComment = false; |
|
|
|
lastRealChar = ' '; // last non-comment char |
|
} |
|
|
|
public void Step(IDocumentAccessor doc, IndentationSettings set) |
|
{ |
|
string line = doc.Text; |
|
if (set.LeaveEmptyLines && line.Length == 0) return; // leave empty lines empty |
|
line = line.TrimStart(); |
|
|
|
StringBuilder indent = new StringBuilder(); |
|
if (line.Length == 0) { |
|
// Special treatment for empty lines: |
|
if (blockComment || (inString && verbatim)) |
|
return; |
|
indent.Append(block.InnerIndent); |
|
indent.Append(Repeat(set.IndentString, block.OneLineBlock)); |
|
if (block.Continuation) |
|
indent.Append(set.IndentString); |
|
if (doc.Text != indent.ToString()) |
|
doc.Text = indent.ToString(); |
|
return; |
|
} |
|
|
|
if (TrimEnd(doc)) |
|
line = doc.Text.TrimStart(); |
|
|
|
Block oldBlock = block; |
|
bool startInComment = blockComment; |
|
bool startInString = (inString && verbatim); |
|
|
|
#region Parse char by char |
|
lineComment = false; |
|
inChar = false; |
|
escape = false; |
|
if (!verbatim) inString = false; |
|
|
|
lastRealChar = '\n'; |
|
|
|
char lastchar = ' '; |
|
char c = ' '; |
|
char nextchar = line[0]; |
|
for (int i = 0; i < line.Length; i++) { |
|
if (lineComment) break; // cancel parsing current line |
|
|
|
lastchar = c; |
|
c = nextchar; |
|
if (i + 1 < line.Length) |
|
nextchar = line[i + 1]; |
|
else |
|
nextchar = '\n'; |
|
|
|
if (escape) { |
|
escape = false; |
|
continue; |
|
} |
|
|
|
#region Check for comment/string chars |
|
switch (c) { |
|
case '/': |
|
if (blockComment && lastchar == '*') |
|
blockComment = false; |
|
if (!inString && !inChar) { |
|
if (!blockComment && nextchar == '/') |
|
lineComment = true; |
|
if (!lineComment && nextchar == '*') |
|
blockComment = true; |
|
} |
|
break; |
|
case '#': |
|
if (!(inChar || blockComment || inString)) |
|
lineComment = true; |
|
break; |
|
case '"': |
|
if (!(inChar || lineComment || blockComment)) { |
|
inString = !inString; |
|
if (!inString && verbatim) { |
|
if (nextchar == '"') { |
|
escape = true; // skip escaped quote |
|
inString = true; |
|
} else { |
|
verbatim = false; |
|
} |
|
} else if (inString && lastchar == '@') { |
|
verbatim = true; |
|
} |
|
} |
|
break; |
|
case '\'': |
|
if (!(inString || lineComment || blockComment)) { |
|
inChar = !inChar; |
|
} |
|
break; |
|
case '\\': |
|
if ((inString && !verbatim) || inChar) |
|
escape = true; // skip next character |
|
break; |
|
} |
|
#endregion |
|
|
|
if (lineComment || blockComment || inString || inChar) { |
|
if (wordBuilder.Length > 0) |
|
block.LastWord = wordBuilder.ToString(); |
|
wordBuilder.Length = 0; |
|
continue; |
|
} |
|
|
|
if (!Char.IsWhiteSpace(c) && c != '[' && c != '/') { |
|
if (block.Bracket == '{') |
|
block.Continuation = true; |
|
} |
|
|
|
if (Char.IsLetterOrDigit(c)) { |
|
wordBuilder.Append(c); |
|
} else { |
|
if (wordBuilder.Length > 0) |
|
block.LastWord = wordBuilder.ToString(); |
|
wordBuilder.Length = 0; |
|
} |
|
|
|
#region Push/Pop the blocks |
|
switch (c) { |
|
case '{': |
|
block.ResetOneLineBlock(); |
|
blocks.Push(block); |
|
block.StartLine = doc.LineNumber; |
|
if (block.LastWord == "switch") { |
|
block.Indent(set.IndentString + set.IndentString); |
|
/* oldBlock refers to the previous line, not the previous block |
|
* The block we want is not available anymore because it was never pushed. |
|
* } else if (oldBlock.OneLineBlock) { |
|
// Inside a one-line-block is another statement |
|
// with a full block: indent the inner full block |
|
// by one additional level |
|
block.Indent(set, set.IndentString + set.IndentString); |
|
block.OuterIndent += set.IndentString; |
|
// Indent current line if it starts with the '{' character |
|
if (i == 0) { |
|
oldBlock.InnerIndent += set.IndentString; |
|
}*/ |
|
} else { |
|
block.Indent(set); |
|
} |
|
block.Bracket = '{'; |
|
break; |
|
case '}': |
|
while (block.Bracket != '{') { |
|
if (blocks.Count == 0) break; |
|
block = blocks.Pop(); |
|
} |
|
if (blocks.Count == 0) break; |
|
block = blocks.Pop(); |
|
block.Continuation = false; |
|
block.ResetOneLineBlock(); |
|
break; |
|
case '(': |
|
case '[': |
|
blocks.Push(block); |
|
if (block.StartLine == doc.LineNumber) |
|
block.InnerIndent = block.OuterIndent; |
|
else |
|
block.StartLine = doc.LineNumber; |
|
block.Indent(Repeat(set.IndentString, oldBlock.OneLineBlock) + |
|
(oldBlock.Continuation ? set.IndentString : "") + |
|
(i == line.Length - 1 ? set.IndentString : new String(' ', i + 1))); |
|
block.Bracket = c; |
|
break; |
|
case ')': |
|
if (blocks.Count == 0) break; |
|
if (block.Bracket == '(') { |
|
block = blocks.Pop(); |
|
if (IsSingleStatementKeyword(block.LastWord)) |
|
block.Continuation = false; |
|
} |
|
break; |
|
case ']': |
|
if (blocks.Count == 0) break; |
|
if (block.Bracket == '[') |
|
block = blocks.Pop(); |
|
break; |
|
case ';': |
|
case ',': |
|
block.Continuation = false; |
|
block.ResetOneLineBlock(); |
|
break; |
|
case ':': |
|
if (block.LastWord == "case" |
|
|| line.StartsWith("case ", StringComparison.Ordinal) |
|
|| line.StartsWith(block.LastWord + ":", StringComparison.Ordinal)) |
|
{ |
|
block.Continuation = false; |
|
block.ResetOneLineBlock(); |
|
} |
|
break; |
|
} |
|
|
|
if (!Char.IsWhiteSpace(c)) { |
|
// register this char as last char |
|
lastRealChar = c; |
|
} |
|
#endregion |
|
} |
|
#endregion |
|
|
|
if (wordBuilder.Length > 0) |
|
block.LastWord = wordBuilder.ToString(); |
|
wordBuilder.Length = 0; |
|
|
|
if (startInString) return; |
|
if (startInComment && line[0] != '*') return; |
|
if (doc.Text.StartsWith("//\t", StringComparison.Ordinal) || doc.Text == "//") |
|
return; |
|
|
|
if (line[0] == '}') { |
|
indent.Append(oldBlock.OuterIndent); |
|
oldBlock.ResetOneLineBlock(); |
|
oldBlock.Continuation = false; |
|
} else { |
|
indent.Append(oldBlock.InnerIndent); |
|
} |
|
|
|
if (indent.Length > 0 && oldBlock.Bracket == '(' && line[0] == ')') { |
|
indent.Remove(indent.Length - 1, 1); |
|
} else if (indent.Length > 0 && oldBlock.Bracket == '[' && line[0] == ']') { |
|
indent.Remove(indent.Length - 1, 1); |
|
} |
|
|
|
if (line[0] == ':') { |
|
oldBlock.Continuation = true; |
|
} else if (lastRealChar == ':' && indent.Length >= set.IndentString.Length) { |
|
if (block.LastWord == "case" || line.StartsWith("case ", StringComparison.Ordinal) || line.StartsWith(block.LastWord + ":", StringComparison.Ordinal)) |
|
indent.Remove(indent.Length - set.IndentString.Length, set.IndentString.Length); |
|
} else if (lastRealChar == ')') { |
|
if (IsSingleStatementKeyword(block.LastWord)) { |
|
block.OneLineBlock++; |
|
} |
|
} else if (lastRealChar == 'e' && block.LastWord == "else") { |
|
block.OneLineBlock = Math.Max(1, block.PreviousOneLineBlock); |
|
block.Continuation = false; |
|
oldBlock.OneLineBlock = block.OneLineBlock - 1; |
|
} |
|
|
|
if (doc.IsReadOnly) { |
|
// We can't change the current line, but we should accept the existing |
|
// indentation if possible (=if the current statement is not a multiline |
|
// statement). |
|
if (!oldBlock.Continuation && oldBlock.OneLineBlock == 0 && |
|
oldBlock.StartLine == block.StartLine && |
|
block.StartLine < doc.LineNumber && lastRealChar != ':') |
|
{ |
|
// use indent StringBuilder to get the indentation of the current line |
|
indent.Length = 0; |
|
line = doc.Text; // get untrimmed line |
|
for (int i = 0; i < line.Length; ++i) { |
|
if (!Char.IsWhiteSpace(line[i])) |
|
break; |
|
indent.Append(line[i]); |
|
} |
|
// /* */ multiline comments have an extra space - do not count it |
|
// for the block's indentation. |
|
if (startInComment && indent.Length > 0 && indent[indent.Length - 1] == ' ') { |
|
indent.Length -= 1; |
|
} |
|
block.InnerIndent = indent.ToString(); |
|
} |
|
return; |
|
} |
|
|
|
if (line[0] != '{') { |
|
if (line[0] != ')' && oldBlock.Continuation && oldBlock.Bracket == '{') |
|
indent.Append(set.IndentString); |
|
indent.Append(Repeat(set.IndentString, oldBlock.OneLineBlock)); |
|
} |
|
|
|
// this is only for blockcomment lines starting with *, |
|
// all others keep their old indentation |
|
if (startInComment) |
|
indent.Append(' '); |
|
|
|
if (indent.Length != (doc.Text.Length - line.Length) || |
|
!doc.Text.StartsWith(indent.ToString(), StringComparison.Ordinal) || |
|
Char.IsWhiteSpace(doc.Text[indent.Length])) |
|
{ |
|
doc.Text = indent.ToString() + line; |
|
} |
|
} |
|
|
|
static string Repeat(string text, int count) |
|
{ |
|
if (count == 0) |
|
return string.Empty; |
|
if (count == 1) |
|
return text; |
|
StringBuilder b = new StringBuilder(text.Length * count); |
|
for (int i = 0; i < count; i++) |
|
b.Append(text); |
|
return b.ToString(); |
|
} |
|
|
|
static bool IsSingleStatementKeyword(string keyword) |
|
{ |
|
switch (keyword) { |
|
case "if": |
|
case "for": |
|
case "while": |
|
case "do": |
|
case "foreach": |
|
case "using": |
|
case "lock": |
|
return true; |
|
default: |
|
return false; |
|
} |
|
} |
|
|
|
static bool TrimEnd(IDocumentAccessor doc) |
|
{ |
|
string line = doc.Text; |
|
if (!Char.IsWhiteSpace(line[line.Length - 1])) return false; |
|
|
|
// one space after an empty comment is allowed |
|
if (line.EndsWith("// ", StringComparison.Ordinal) || line.EndsWith("* ", StringComparison.Ordinal)) |
|
return false; |
|
|
|
doc.Text = line.TrimEnd(); |
|
return true; |
|
} |
|
} |
|
}
|
|
|