Introduction

ICSharpCode.AvalonEdit is the WPF-based text editor that I've written for SharpDevelop 4.0. It is meant as a replacement for ICSharpCode.TextEditor, but should be:

Extensible

Easy to use

Better at handling large files

Extensible means that I wanted SharpDevelop add-ins to be able to add features to the text editor. For example, an add-in should be able to allow inserting images into comments – this way, you could put stuff like class diagrams right into the source code!

With, Easy to use, I'm referring to the programming API. It should just work™. For example, this means if you change the document text, the editor should automatically redraw without having to call Invalidate(). And, if you do something wrong, you should get a meaningful exception, not corrupted state and crash later at an unrelated location.

Better at handling large files means that the editor should be able to handle large files (e.g., the mscorlib XML documentation file, 7 MB, 74100 LOC), even when features like folding (code collapsing) are enabled.

Using the Code

The main class of the editor is ICSharpCode.AvalonEdit.TextEditor. You can use it just similar to a normal WPF TextBox:

ICSharpCode.AvalonEdit: TextEditor — the main control that brings it all together

Here is the visual tree of the TextEditor control:

It's important to understand that AvalonEdit is a composite control with three layers: TextEditor (main control), TextArea (editing), TextView (rendering). While the main control provides some convenience methods for common tasks, for most advanced features, you have to work directly with the inner controls. You can access them using textEditor.TextArea or textEditor.TextArea.TextView.

Document (The Text Model)

The main class of the model is ICSharpCode.AvalonEdit.Document.TextDocument. Basically, the document is a StringBuilder with events. However, the Document namespace also contains several features that are useful to applications working with the text editor.

In the text editor, all three controls (TextEditor, TextArea, TextView) have a Document property pointing to the TextDocument instance. You can change the Document property to bind the editor to another document. It is possible to bind two editor instances to the same document; you can use this feature to create a split view.

Offsets usually represent the position between two characters. The first offset at the start of the document is 0; the offset after the first char in the document is 1. The last valid offset is document.TextLength, representing the end of the document. This is exactly the same as the 'index' parameter used by methods in the .NET String or StringBuilder classes.

Offsets are easy to use, but sometimes you need Line / Column pairs instead. AvalonEdit defines a struct called TextLocation for those.

The document provides the methods GetLocation and GetOffset to convert between offsets and TextLocations. Those are convenience methods built on top of the DocumentLine class.

The TextDocument.Lines collection contains a DocumentLine instance for every line in the document. This collection is read-only to user code, and is automatically updated to reflect the current document content.

Rendering

In the whole 'Document' section, there was no mention of extensibility. The text rendering infrastructure now has to compensate for that by being completely extensible.

The ICSharpCode.AvalonEdit.Rendering.TextView class is the heart of AvalonEdit. It takes care of getting the document onto the screen.

To do this in an extensible way, the TextView uses its own kind of model: the VisualLine. Visual lines are created only for the visible part of the document.

The rendering process looks like this:

The last step in the pipeline is the conversion to one or more System.Windows.Media.TextFormatting.TextLine instances. WPF then takes care of the actual text rendering.

The "element generators", "line transformers", and "background renderers" are the extension points; it is possible to add custom implementations of them to the TextView to implement additional features in the editor.

Editing

The TextArea class handles user input and executes the appropriate actions. Both the caret and the selection are controlled by the TextArea.

You can customize the text area by modifying the TextArea.DefaultInputHandler by adding new or replacing existing WPF input bindings in it. You can also set TextArea.ActiveInputHandler to something different than the default, to switch the text area into another mode. You could use this to implement an "incremental search" feature, or even a VI emulator.

The text area has the LeftMargins property – use it to add controls to the left of the text view that look like they're inside the scroll viewer, but don't actually scroll. The AbstractMargin base class contains some useful code to detect when the margin is attached/detached from a text view; or when the active document changes. However, you're not forced to use it; any UIElement can be used as the margin.

Folding

Folding (code collapsing) is implemented as an extension to the editor. It could have been implemented in a separate assembly without having to modify the AvalonEdit code. A VisualLineElementGenerator takes care of the collapsed sections in the text document, and a custom margin draws the plus and minus buttons.

You could use the relevant classes separately; but, to make it a bit easier to use, the static FoldingManager.Install method will create and register the necessary parts automatically.

All that's left for you is to regularly call FoldingManager.UpdateFoldings with the list of foldings you want to provide. You could calculate that list yourself, or you could use a built-in folding strategy to do it for you.

If you want the folding markers to update when the text is changed, you have to repeat the foldingStrategy.UpdateFoldings call regularly.

Currently, only the XmlFoldingStrategy is built into AvalonEdit. The sample application to this article also contains the BraceFoldingStrategy that folds using { and }. However, it is a very simple implementation and does not handle { and } inside strings or comments correctly.

Syntax Highlighting

The highlighting engine in AvalonEdit is implemented in the class DocumentHighlighter. Highlighting is the process of taking a DocumentLine and constructing a HighlightedLine instance for it by assigning colors to different sections of the line.

The HighlightingColorizer class is the only link between highlighting and rendering. It uses a DocumentHighlighter to implement a line transformer that applies the highlighting to the visual lines in the rendering process.

Except for this single call, syntax highlighting is independent from the rendering namespace. To help with other potential uses of the highlighting engine, the HighlightedLine class has the method ToHtml to produce syntax highlighted HTML source code.

The rules for highlighting are defined using an "extensible syntax highlighting definition" (.xshd) file. Here is a complete highlighting definition for a sub-set of C#:

The highlighting engine works with "spans" and "rules" that each have a color assigned to them. In the XSHD format, colors can be both referenced (color="Comment") or directly specified (fontWeight="bold" foreground="Blue").

Spans consist of two Regular Expressions (begin+end); while rules are simply a single regex with a color. The <Keywords> element is just a nice syntax to define a highlighting rule that matches a set of words; internally, a single regex will be used for the whole keyword list.

The highlighting engine works by first analyzing the spans: whenever a begin regex matches some text, that span is pushed onto a stack. Whenever the end regex of the current span matches some text, the span is popped from the stack.

Each span has a nested rule set associated with it, which is empty by default. This is why keywords won't be highlighted inside comments: the span's empty ruleset is active there, so the keyword rule is not applied.

This feature is also used in the string span: the nested span will match when a backslash is encountered, and the character following the backslash will be consumed by the end regex of the nested span (. matches any character). This ensures that \" does not denote the end of the string span; but \\" still does.

What's great about the highlighting engine is that it highlights only on-demand, works incrementally, and yet usually requires only a few KB of memory even for large code files.

On-demand means that when a document is opened, only the lines initially visible will be highlighted. When the user scrolls down, highlighting will continue from the point where it stopped the last time. If the user scrolls quickly, so that the first visible line is far below the last highlighted line, then the highlighting engine still has to process all the lines in between – there might be comment starts in them. However, it will only scan that region for changes in the span stack; highlighting rules will not be tested.

The stack of active spans is stored at the beginning of every line. If the user scrolls back up, the lines getting into view can be highlighted immediately because the necessary context (the span stack) is still available.

Incrementally means that even if the document is changed, the stored span stacks will be reused as far as possible. If the user types /*, that would theoretically cause the whole remainder of the file to become highlighted in the comment color. However, because the engine works on-demand, it will only update the span stacks within the currently visible region and keep a notice 'the highlighting state is not consistent between line X and line X+1', where X is the last line in the visible region. Now, if the user would scroll down, the highlighting state would be updated and the 'not consistent' notice would be moved down. But usually, the user will continue typing and type */ only a few lines later. Now, the highlighting state in the visible region will revert to the normal 'only the main ruleset is on the stack of active spans'. When the user now scrolls down below the line with the 'not consistent' marker, the engine will notice that the old stack and the new stack are identical, and will remove the 'not consistent' marker. This allows reusing the stored span stacks cached from before the user typed /*.

While the stack of active spans might change frequently inside the lines, it rarely changes from the beginning of one line to the beginning of the next line. With most languages, such changes happen only at the start and end of multiline comments. The highlighting engine exploits this property by storing the list of span stacks in a special data structure (ICSharpCode.AvalonEdit.Utils.CompressingTreeList). The memory usage of the highlighting engine is linear to the number of span stack changes; not to the total number of lines. This allows the highlighting engine to store the span stacks for big code files using only a tiny amount of memory, especially in languages like C# where sequences of // or /// are more popular than /* */ comments.

Code Completion

AvalonEdit comes with a code completion drop down window. You only have to handle the text entering events to determine when you want to show the window; all the UI is already done for you.

This code will open the code completion window whenever '.' is pressed. By default, the CompletionWindow only handles key presses like Tab and Enter to insert the currently selected item. To also make it complete when keys like '.' or ';' are pressed, we attach another handler to the TextEntering event and tell the completion window to insert the selected item.

The CompletionWindow will actually never have focus - instead, it hijacks the WPF keyboard input events on the text area and passes them through its ListBox. This allows selecting entries in the completion list using the keyboard and normal typing in the editor at the same time.

For the sake of completeness, here is the implementation of the MyCompletionData class used in the code above:

Both the content and the description shown may be any content acceptable in WPF, including custom UIElements. You may also implement custom logic in the Complete method if you want to do more than simply insert text. The insertionRequestEventArgs can help decide which kind of insertion the user wants - depending on how the insertion was triggered, it is an instance of TextCompositionEventArgs, KeyEventArgs, or MouseEventArgs.

History

August 13, 2008: Work on AvalonEdit started

November 7, 2008: First version of AvalonEdit added to the SharpDevelop 4.0 trunk

June 14, 2009: The SharpDevelop team switches to SharpDevelop 4 as their IDE for working on SharpDevelop; AvalonEdit starts to get used for real work

Comments and Discussions

Great work Daniel, nice to see this Code Project article still has some life in it and just proves how much of a relief AvalonEdit has been for the community.

My question is in regards to the code suggestion. So I currently have something very similar to the sample code (except I also add my own keywords).

So at the moment when I press space and "." the code suggestion appears. What I would like is to have it appear when entering *any* text and disappearing when nothing is found. At the moment it leaves this the listbox, but "flat" with nothing in it.

I also tried using the "show" and "hide" methods (as I also thought it a waste to keep on adding the same components), but it throws an exception that a dialog cannot be shown after it is closed. How do I get around this? As I don't want to initialise the same component over and over again.

Sorry this is quite lengthy, but any suggestions would be appreciated, even a little direction.

I am trying to implement Avalon Editor but I have some problems with controlling height. I want to implement editor the way that it will auto-resize if user resizes the window (by using "minheigh"), but vertical scrollbar doesn't seem to work, so all code lines are displayed and editor gets out of main window. Any idea how to control height without setting it to fixed height (in this case it works ok)?

EDIT: I just had to remove TextEditor out of StackPanel; I didn't notice that one before since it was a few levels higer than TextEditr. I hope that helps someone in the future.

Apologies if this have been covered elsewhere, but I didn't see anything. I am implementing an editor for a genetic programming language (see screenshot) and would love to be able to insert images showing the actual pieces of DNA generated from the code above the relevant lines. That would involve creating additional comment lines that render differently. Is that something I could do with the existing library or would it require an extension? I was blown away by how easy it was to get the editor working in F# and looking nice. Just evaluating now how easy it will be to add other features I need. I'd like to do the intellisense style underlining of error messages in the code for example and show the constructs, or at least allow user to mouse over and see the generated DNA image,

Inserting visual lines that are not present in the underlying document is currently not possible.
However, it is possible to change the rendering in various ways. A VisualLineElementGenerator could detect some form of escape sequence and replace it with an arbitrary WPF UIElement.
I didn't go too much into the details of that in this article (as this is supposed to be an introduction to AvalonEdit). The details of the AvalonEdit rendering are better explained in http://danielgrunwald.de/coding/AvalonEdit/rendering.php[^]. In particular, there's an example VisualLineElementGenerator that turns HTML-like image tags into inline images.

I've not used AvalonEdit yet but would like to for the ability to use the syntax highlighting in a WPF project, but I do have one problem I do not see implemented in the current release and that is the ability to have multiple lines with different background colors. The reason for this is to be able to show different lines of code differently such as adding a background color to show user based modifications and still keep the syntax highlighting (think diff tool mixed with a syntax highlighter). Is AvalonEdit extensible to add this functionality or would it require me to modify the baseline? How would you go about doing this?

I saw you had a ScrollToBottom somewhere, but not implemented in the TextEditor class. Any reason why?

Why: Because it's not in the normal WPF TextBox, either.

The WPF ScrollViewer implements ScrollToEnd() as ScrollToHorizontalOffset(double.NegativeInfinity) followed by ScrollToVerticalOffset(double.PositiveInfinity).

ScrollToBottom() is just ScrollToVerticalOffset(double.PositiveInfinity). TextEditor exposes the ScrollToVerticalOffset() method, so you could use that instead of adding ScrollToBottom().

But, I don't see why it would have any different effect than ScrollToEnd(), given that the ScrollToHorizontalOffset() call is ignored when word-wrapping is enabled. Both methods should act the same way. Yes, there's a bug when scrolling to the end of the document this way: the infinite offset coerced into the valid range and thus set to the document's height; then the last line is rendered which causes word-wrapping and increases the document's height. I've added it to my bug list; it'll be fixed in some future version.
There also might be another bug that causes the performance to be less than optimal; but again, that should be the same with both methods.

What you could use instead is:

textEditor.ScrollToLine(textEditor.Document.LineCount);

This is a completely different method of scrolling that does not involve infinite scroll offsets, and should avoid the bug.

I would like to make a AppendText variant that only appends a single Line, so that when the text contain newline characters, it doesn't create a new Line but instead just wrap the text similar to the WordWrap effect when resizing the window.

I modified your code to show a timestamp instead of Line numbers, so it would be great if the timestamp was only shown once per call to this new AppendText overload.

Also, I have a huge suggestion: you should make a simplified version of this code to create a new control, the "TextViewer", useful for things like a game console, serial monitor, chat box.. Support for line numbers, timestamp, maybe other columns... text formatting (bbcode).. That's what I'm trying to do but I'm beginner and don't really know what I do

Did you see that you can add margins like the LineNumberMargin yourself (via textEditor.TextArea.LeftMargins)? There's usually no reason to modify the AvalonEdit source code, it's pretty extensible.

The easiest solution would be to change the timestamp-showing code to only show a timestamp for some of the lines. Maybe use a List<DateTime?> to store the timestamps for the lines, and add null for every newline within a message.

It is possible to combine multiple lines into a single VisualLine (as done by word-wrapping). I think the easiest way currently is to use \u2028 - AvalonEdit does not recognize this character as a newline, but WPF text rendering does, so it ends up acting like the line break caused by word wrapping.
But this has the downside that long text may cause performance issues - AvalonEdit only renders the lines currently visible; but a long wrapped line counts as a single line for this purpose. This causes major performance issues if a single VisualLine is too long.

For bbcode: if the user copies selected text to the clipboard, do you want to copy the visible text, or the underlying bbcode?
If only the visible text: strip the bbcode-tags yourself (don't append them to the document).
Then use custom data structures to store the formatting, as a rich text editor would do: Using AvalonEdit to implement a rich text editor[^]

I'm beginner, I was searching for a replacement of the slow and poor richtextbox and I almost gave up using WPF to go back to WinForms, when I found this... Installed something called a nuget :/, used the exemple code, it worked on the first try and I f***** love it! I tried writing one million lines without any problems.

I'm attempting to modify it to integrate a simple BBcode parser for few tags I need, and make it show date instead of line number. Then I will have a very nice chat box!

Also I noticed a small bug, when you AppendText (for example with a timer), and then you put the mouse on the window border (like when you want to resize), the cursor blink from Resize cursor to Normal cursor. Is it possible that you fix it? Meanwhile I will take a look but as said I'm beginner...

The UpdateCursor() call is necessary in case a VisualLineElement that customizes the cursor got moved to a different position by the text update. I'll fix the bug by calling InvalidateCursor only if the mouse is over the text area.

I made small modification in MyCompletionData.Complete method. My modification allows replacing the entire word that is under the caret, rather than adding the completion text after the caret, leaving the beginning of the word.

The ICompletionData.Complete() method should always replace the completion segment.

If you want to replace the entire word, the method opening the completion window should set the initial completion segment to the word (set the CompletionWindow.StartOffset/EndOffset properties). The completion window will then keep track of that segment as the user edits the document while the window is open; and pass the final segment to the Complete() method.

To find word boundaries, you could also use TextUtilities.GetNextCaretPosition() method with CaretPositioningMode.WordBorder.

Thanks Daniel! Changing my code now. I've somehow missed the CompletionWindow.EndOffset. Could you please give a little more details (samples, documentation?) for how to use TextUtilities.GetNextCaretPosition go find the current word, or return none if caret is off word? I started with that method but couldn't get it to work when caret was in the middle of a whitespace or end if line/file.

I currently do not know how to insert a glyph into a visual line at the text line ends and starts only if it the visual line content is wrapped.
The text lines inside a visual line are generated after all the element generators have done their job. So how
do I know where to insert the wrap glyphs?

I don't see how this is possible with the WPF TextLine API - the wrapping position is only known after the whole text layout is finished; and the line cannot be changed after that.

I suppose you could render the wrap glyph separately behind the TextLine. (using a BackgroundRenderer)
But you can't reserve space for the glyph this way. I think for that you'd have to change the AvalonEdit source code. And unless the WPF TextFormatter already knows the concept of a wrap glyph, you might have to format the first TextLine of a VisualLine twice if there isn't enough space for the wrap glyph (or alternatively, always reserve that space even if the line doesn't wrap).