Click here to Skip to main content
15,892,643 members
Articles / Web Development / HTML

AvalonDock [2.0] Tutorial Part 4 - Integrating AvalonEdit Options

Rate me:
Please Sign up or sign in to vote.
5.00/5 (9 votes)
31 Jan 2014CPOL3 min read 60K   1.8K   27  
Integrate AvalonEdit with text editing options into AvalonDock [2.0]
// 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;
		}
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Germany Germany
The Windows Presentation Foundation (WPF) and C# are among my favorites and so I developed Edi

and a few other projects on GitHub. I am normally an algorithms and structure type but WPF has such interesting UI sides that I cannot help myself but get into it.

https://de.linkedin.com/in/dirkbahle

Comments and Discussions