Click here to Skip to main content
Click here to Skip to main content
Add your own
alternative version

Using AvalonEdit (WPF Text Editor)

, 1 Apr 2013 LGPL3
AvalonEdit is an extensible Open-Source text editor with support for syntax highlighting and folding.
AvalonEdit_4.3.1_Binaries-noexe.zip
AvalonEdit_4.3.1_Binaries.zip
AvalonEdit.Sample.exe
ICSharpCode.AvalonEdit.dll
AvalonEdit_4.3.1_Source-noexe.zip
AvalonEdit.Sample
AvalonEdit
dependencies.png
folding.png
renderingPipeline.png
screenshot.png
snoop.png
CustomHighlighting.xshd
Images
Copy.png
Cut.png
Delete.png
Open.png
Paste.png
Redo.png
Save.png
Undo.png
WordWrap.png
Properties
RenderingPipeline.pptx
Documentation
Architecture.aml
Code Completion.aml
Coordinate Systems.aml
Folding.aml
ICSharpCode.AvalonEdit.content
ICSharpCode.AvalonEdit.shfbproj
Media
NamespaceDependencies.png
RenderingPipeline.png
VisualTree.png
WelcomeScreenshot.png
Sample Application.aml
Syntax Highlighting.aml
Text Rendering.aml
Welcome.aml
ICSharpCode.AvalonEdit.Tests
Document
Editing
Highlighting
ICSharpCode.AvalonEdit.Tests.PartCover.Settings
Properties
Search
Utils
ICSharpCode.AvalonEdit
CodeCompletion
Document
Editing
Folding
Highlighting
Resources
ASPX.xshd
Boo.xshd
Coco-Mode.xshd
CPP-Mode.xshd
CSharp-Mode.xshd
CSS-Mode.xshd
HTML-Mode.xshd
Java-Mode.xshd
JavaScript-Mode.xshd
MarkDown-Mode.xshd
Patch-Mode.xshd
PHP-Mode.xshd
PowerShell.xshd
Tex-Mode.xshd
VBNET-Mode.xshd
XML-Mode.xshd
XmlDoc.xshd
Xshd
ICSharpCode.AvalonEdit.snk
Indentation
CSharp
Properties
Rendering
Search
next.png
prev.png
Snippets
themes
RightArrow.cur
Utils
Xml
AvalonEdit_4.3.1_Source.zip
dependencies.png
folding.png
renderingPipeline.png
screenshot.png
snoop.png
CustomHighlighting.xshd
Copy.png
Cut.png
Delete.png
Open.png
Paste.png
Redo.png
Save.png
Undo.png
WordWrap.png
RenderingPipeline.pptx
Architecture.aml
Code Completion.aml
Coordinate Systems.aml
Folding.aml
ICSharpCode.AvalonEdit.content
ICSharpCode.AvalonEdit.shfbproj
NamespaceDependencies.png
RenderingPipeline.png
VisualTree.png
WelcomeScreenshot.png
Sample Application.aml
Syntax Highlighting.aml
Text Rendering.aml
Welcome.aml
ICSharpCode.AvalonEdit.Tests.PartCover.Settings
nunit.framework.dll
ASPX.xshd
Boo.xshd
Coco-Mode.xshd
CPP-Mode.xshd
CSharp-Mode.xshd
CSS-Mode.xshd
HTML-Mode.xshd
Java-Mode.xshd
JavaScript-Mode.xshd
MarkDown-Mode.xshd
Patch-Mode.xshd
PHP-Mode.xshd
PowerShell.xshd
Tex-Mode.xshd
VBNET-Mode.xshd
XML-Mode.xshd
XmlDoc.xshd
ICSharpCode.AvalonEdit.snk
next.png
prev.png
RightArrow.cur
AvalonEdit_Binaries-noexe.zip
AvalonEdit_Binaries.zip
AvalonEdit.Sample.exe
ICSharpCode.AvalonEdit.dll
AvalonEdit_CHM_Documentation.zip
AvalonEdit Documentation.chm
AvalonEdit_Source.zip
dependencies.png
folding.png
renderingPipeline.png
screenshot.png
snoop.png
CustomHighlighting.xshd
Copy.png
Cut.png
Delete.png
Open.png
Paste.png
Redo.png
Save.png
Undo.png
WordWrap.png
RenderingPipeline.pptx
Architecture.aml
Code Completion.aml
Coordinate Systems.aml
Folding.aml
Help
ICSharpCode.AvalonEdit.content
ICSharpCode.AvalonEdit.shfbproj
NamespaceDependencies.png
RenderingPipeline.png
VisualTree.png
WelcomeScreenshot.png
Sample Application.aml
Syntax Highlighting.aml
Text Rendering.aml
Welcome.aml
ICSharpCode.AvalonEdit.Tests.PartCover.Settings
nunit.framework.dll
ASPX.xshd
Boo.xshd
Coco-Mode.xshd
CPP-Mode.xshd
CSharp-Mode.xshd
CSS-Mode.xshd
HTML-Mode.xshd
Java-Mode.xshd
JavaScript-Mode.xshd
Patch-Mode.xshd
PHP-Mode.xshd
PowerShell.xshd
Tex-Mode.xshd
VBNET-Mode.xshd
XML-Mode.xshd
XmlDoc.xshd
ICSharpCode.AvalonEdit.snk
next.png
prev.png
RightArrow.cur
// 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 GNU Lesser General Public License (LGPLv3)

Share

About the Author

Daniel Grunwald

Germany Germany
I am the lead developer on the SharpDevelop open source project.
Follow on   Twitter   Google+   LinkedIn

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.150129.1 | Last Updated 1 Apr 2013
Article Copyright 2009 by Daniel Grunwald
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid