475 lines
13 KiB
C#
475 lines
13 KiB
C#
// 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;
|
|
}
|
|
}
|
|
}
|