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.
429 lines
24 KiB
429 lines
24 KiB
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> |
|
<!------------------------------------------------------------> |
|
<!-- INTRODUCTION |
|
|
|
The Code Project article submission template (HTML version) |
|
|
|
Using this template will help us post your article sooner. To use, just |
|
follow the 3 easy steps below: |
|
|
|
1. Fill in the article description details |
|
2. Add links to your images and downloads |
|
3. Include the main article text |
|
|
|
That's all there is to it! All formatting will be done by our submission |
|
scripts and style sheets. |
|
|
|
--> |
|
<!------------------------------------------------------------> |
|
<!-- IGNORE THIS SECTION --> |
|
<html> |
|
<head> |
|
<title>AvalonEdit</title> |
|
<Style> |
|
BODY, P, TD { font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 10pt } |
|
H2,H3,H4,H5 { color: #ff9900; font-weight: bold; } |
|
H2 { font-size: 13pt; } |
|
H3 { font-size: 12pt; } |
|
H4 { font-size: 10pt; color: black; } |
|
PRE { BACKGROUND-COLOR: #FBEDBB; FONT-FAMILY: "Courier New", Courier, mono; WHITE-SPACE: pre; } |
|
CODE { COLOR: #990000; FONT-FAMILY: "Courier New", Courier, mono; } |
|
</style> |
|
<link rel="stylesheet" type="text/css" href="http://www.codeproject.com/App_Themes/NetCommunity/CodeProject.css"> |
|
</head> |
|
<body bgcolor="#FFFFFF" color=#000000> |
|
<div style="width:600px; margin-left: 24px;"> |
|
<!------------------------------------------------------------> |
|
|
|
|
|
<!------------------------------------------------------------> |
|
<!-- Fill in the details (CodeProject will reformat this section for you) --> |
|
|
|
|
|
<!------------------------------------------------------------> |
|
<!-- Include download and sample image information. --> |
|
|
|
<ul class=download> |
|
<li><a href="AvalonEdit/AvalonEdit_Binaries.zip">Download binaries - 206.5 KB</a></li> |
|
<li><a href="AvalonEdit/AvalonEdit_Source.zip">Download source code - 391.3 KB</a></li> |
|
<li><a href="AvalonEdit/AvalonEdit_CHM_Documentation.zip">Download .chm documentation file - 1.88 MB</a></li> |
|
</ul> |
|
<p>The latest version of AvalonEdit can be found as part of the <a href="http://www.icsharpcode.net/OpenSource/SD/">SharpDevelop</a> project. |
|
For details on AvalonEdit, please see <a href="http://www.avalonedit.net/">www.avalonedit.net</a>.</p> |
|
|
|
<p><img src="AvalonEdit/screenshot.png" width="611" height="441" alt="Sample Image" /></p> |
|
|
|
|
|
<!------------------------------------------------------------> |
|
|
|
<!-- Add the article text. Please use simple formatting (<h2>, <p> etc) --> |
|
|
|
<h2>Introduction</h2> |
|
|
|
<p>ICSharpCode.AvalonEdit is the WPF-based text editor that I've written for SharpDevelop 4.0. It is meant as a replacement |
|
for <a href="http://www.codeproject.com/KB/edit/TextEditorControl.aspx">ICSharpCode.TextEditor</a>, but should be: |
|
<ul> |
|
<li>Extensible</li> |
|
<li>Easy to use</li> |
|
<li>Better at handling large files</li> |
|
</ul> |
|
<p> |
|
<b>Extensible</b> means that I wanted SharpDevelop AddIns to be able to add features to the text editor. |
|
For example, an AddIn should be able to allow inserting images into comments – this way you could put |
|
stuff like class diagrams right into the source code! |
|
<p> |
|
With, <b>Easy to use</b>, 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 <code>Invalidate()</code>. |
|
|
|
And if you do something wrong, you should get a meaningful exception, not corrupted state and crash later at an unrelated location. |
|
|
|
<p> |
|
<b>Better at handling large files</b> 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. |
|
|
|
<h2>Using the Code</h2> |
|
|
|
<p>The main class of the editor is <code>ICSharpCode.AvalonEdit.TextEditor</code>. |
|
You can use it just similar to a normal WPF <code>TextBox</code>: |
|
<pre lang="xml"><avalonEdit:TextEditor |
|
xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit" |
|
Name="textEditor" |
|
FontFamily="Consolas" |
|
FontSize="10pt"/></pre> |
|
|
|
<p>To enable syntax highlighting, use: |
|
|
|
<pre lang="cs">textEditor.SyntaxHighlighting = HighlightingManager.Instance.GetDefinition("C#");</pre> |
|
AvalonEdit has syntax highlighting definitions built in for: |
|
ASP.NET, Boo, Coco/R grammars, C++, C#, HTML, Java, JavaScript, Patch files, PHP, TeX, VB, XML |
|
|
|
<p>If you need more of AvalonEdit than a simple text box with syntax highlighting, you will first have to learn more about the architecture of AvalonEdit. |
|
|
|
<!------------------------------------------------------------> |
|
<h2>Architecture of AvalonEdit</h2> |
|
<img src="AvalonEdit/dependencies.png" width="583" height="439" alt="Namespace Dependency Graph"/> |
|
<p> |
|
As you can see in this dependency graph, AvalonEdit consists of a few sub-namespaces that have cleanly separated jobs. |
|
Most of the namespaces have a kind of 'main' class. |
|
<ul> |
|
<li>ICSharpCode.AvalonEdit.Utils: Various utility classes</li> |
|
<li>ICSharpCode.AvalonEdit.Document: <code>TextDocument</code> — text model</li> |
|
<li>ICSharpCode.AvalonEdit.Rendering: <code>TextView</code> — extensible view onto the document</li> |
|
<li>ICSharpCode.AvalonEdit.Editing: <code>TextArea</code> — controls text editing (e.g. caret, selection, handles user input)</li> |
|
<li>ICSharpCode.AvalonEdit.Folding: <code>FoldingManager</code> — enables code collapsing</li> |
|
<li>ICSharpCode.AvalonEdit.Highlighting: <code>HighlightingManager</code> — highlighting engine</li> |
|
<li>ICSharpCode.AvalonEdit.Highlighting.Xshd: <code>HighlightingLoader</code> — XML syntax highlighting definition support (.xshd files)</li> |
|
<li>ICSharpCode.AvalonEdit.CodeCompletion: <code>CompletionWindow</code> — shows a drop-down list for code completion</li> |
|
<li>ICSharpCode.AvalonEdit: <code>TextEditor</code> — the main control that brings it all together</li> |
|
</ul> |
|
|
|
<p> |
|
Here is the visual tree of the <code>TextEditor</code> control:<br> |
|
<img src="AvalonEdit/snoop.png" width="272" height="351" alt="Visual Tree"/> |
|
<p> |
|
It's important to understand that AvalonEdit is a composite control with the three layers: <code>TextEditor</code> (main control), <code>TextArea</code> (editing), <code>TextView</code> (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 <code>textEditor.TextArea</code> |
|
or <code>textEditor.TextArea.TextView</code>. |
|
|
|
<!------------------------------------------------------------> |
|
<h2>The Text Model: Document</h2> |
|
|
|
<p>The main class of the model is <code>ICSharpCode.AvalonEdit.Document.TextDocument</code>. |
|
Basically, the document is a <code>StringBuilder</code> with events. |
|
However, the <code>Document</code> namespace also contains several features that are useful to applications working with the text editor. |
|
|
|
<p>In the text editor, all three controls (<code>TextEditor</code>, <code>TextArea</code>, <code>TextView</code>) have a <code>Document</code> property pointing to the <code>TextDocument</code> instance. |
|
You can change the <code>Document</code> 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. |
|
|
|
<p><i>Simplified</i> definition of <code>TextDocument</code>: |
|
<pre lang="cs">public sealed class TextDocument : ITextSource |
|
{ |
|
public event EventHandler<DocumentChangeEventArgs> Changing; |
|
public event EventHandler<DocumentChangeEventArgs> Changed; |
|
public event EventHandler TextChanged; |
|
|
|
public IList<DocumentLine> Lines { get; } |
|
public DocumentLine GetLineByNumber(int number); |
|
public DocumentLine GetLineByOffset(int offset); |
|
public TextLocation GetLocation(int offset); |
|
public int GetOffset(int line, int column); |
|
|
|
public char GetCharAt(int offset); |
|
public string GetText(int offset, int length); |
|
|
|
public void Insert(int offset, string text); |
|
public void Remove(int offset, int length); |
|
public void Replace(int offset, int length, string text); |
|
|
|
public string Text { get; set; } |
|
public int LineCount { get; } |
|
public int TextLength { get; } |
|
public UndoStack UndoStack { get; } |
|
}</pre> |
|
|
|
In AvalonEdit, an index into the document is called an <b>offset</b>. |
|
|
|
<p>Offsets usually represent the position between two characters. |
|
The first offset at the start of the document is 0; the offset after the first <code>char</code> in the document is 1. |
|
The last valid offset is <code>document.TextLength</code>, representing the end of the document. |
|
This is exactly the same as the 'index' parameter used by methods in the .NET <code>String</code> or <code>StringBuilder</code> classes. |
|
<p> |
|
Offsets are easy to use, but sometimes you need Line / Column pairs instead. |
|
AvalonEdit defines a <code>struct</code> called <code>TextLocation</code> for those. |
|
|
|
<p>The document provides the methods <code>GetLocation</code> and <code>GetOffset</code> to convert between offsets and <code>TextLocation</code>s. |
|
Those are convenience methods built on top of the <code>DocumentLine</code> class. |
|
|
|
<p>The <code>TextDocument.Lines</code> collection contains one <code>DocumentLine</code> 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. |
|
|
|
<!------------------------------------------------------------> |
|
<h2>Rendering: TextView</h2> |
|
|
|
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. |
|
|
|
<p>The <code>ICSharpCode.AvalonEdit.Rendering.TextView</code> class is the heart of AvalonEdit. |
|
It takes care of getting the document onto the screen. |
|
|
|
<p>To do this in an extensible way, the <code>TextView</code> uses its own kind of model: the <code>VisualLine</code>. |
|
Visual lines are created only for the visible part of the document. |
|
<p>The rendering process looks like this:<br> |
|
<img src="AvalonEdit/renderingPipeline.png" width="443" height="570" alt="rendering pipeline"/><br> |
|
The last step in the pipeline is the conversion to one or more <code>System.Windows.Media.TextFormatting.TextLine</code> instances. WPF then takes care of the actual text rendering. |
|
<p> |
|
The "element generators", "line transformers" and "background renderers" are the extension points; it is possible to add custom implementations of |
|
them to the <code>TextView</code> to implement additional features in the editor. |
|
<!-- |
|
<p> |
|
The extensibility features of the rendering namespace are discussed in detail in the article "AvalonEdit Rendering". (to be published soon) |
|
--> |
|
|
|
<h2>Editing: TextArea</h2> |
|
|
|
The <code>TextArea</code> class is handling user input and executing the appropriate actions. |
|
Both the caret and the selection are controlled by the <code>TextArea</code>. |
|
<p> |
|
You can customize the text area by modifying the <code>TextArea.DefaultInputHandler</code> by adding new or replacing existing |
|
WPF input bindings in it. You can also set <code>TextArea.ActiveInputHandler</code> 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. |
|
<p> |
|
The text area has the <code>LeftMargins</code> 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 <code>AbstractMargin</code> base class contains some useful code |
|
to detect when the margin is attached/detaching from a text view; or when the active document changes. However, you're not forced to use it; |
|
any <code>UIElement</code> can be used as margin. |
|
|
|
<h2>Folding</h2> |
|
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 <code>VisualLineElementGenerator</code> takes care of the collapsed sections in the text document; and a custom margin draws the plus and minus |
|
buttons. |
|
<p> |
|
You could use the relevant classes separately; but, to make it a bit easier to use, the static <code>FoldingManager.Install</code> |
|
method will create and register the necessary parts automatically. |
|
<p> |
|
All that's left for you is to regularly call <code>FoldingManager.UpdateFoldings</code> 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. |
|
<p> |
|
Here is the full code required to enable folding: |
|
<pre lang="cs">foldingManager = FoldingManager.Install(textEditor.TextArea); |
|
foldingStrategy = new XmlFoldingStrategy(); |
|
foldingStrategy.UpdateFoldings(foldingManager, textEditor.Document);</pre> |
|
If you want the folding markers to update when the text is changed, you have to repeat the <code>foldingStrategy.UpdateFoldings</code> call regularly. |
|
<p> |
|
Currently, only the <code>XmlFoldingStrategy</code> is built into AvalonEdit. |
|
The sample application to this article also contains the <code>BraceFoldingStrategy</code> that folds using { and }. |
|
However, it is a very simple implementation and does not handle { and } inside strings or comments correctly. |
|
|
|
<h2>Syntax Highlighting</h2> |
|
The highlighting engine in AvalonEdit is implemented in the class <code>DocumentHighlighter</code>. |
|
Highlighting is the process of taking a <code>DocumentLine</code> and constructing a <code>HighlightedLine</code> instance for it |
|
by assigning colors to different sections of the line. |
|
<p> |
|
The <code>HighlightingColorizer</code> class is the only link between highlighting and rendering. It uses a <code>DocumentHighlighter</code> |
|
to implement a line transformer that applies the highlighting to the visual lines in the rendering process. |
|
<p> |
|
Except for this single call, syntax highlighting is independent from the rendering namespace. |
|
To help with other potential uses of the highlighting engine, the <code>HighlightedLine</code> class has the method |
|
<code>ToHtml</code> to produces syntax highlighted HTML source code. |
|
<p> |
|
The rules for the highlighting are defined using an "extensible syntax highlighting definition" (.xshd) file. |
|
Here is a complete highlighting definition for a sub-set of C#: |
|
<pre lang="xml"><SyntaxDefinition name="C#" |
|
xmlns="http://icsharpcode.net/sharpdevelop/syntaxdefinition/2008"> |
|
<Color name="Comment" foreground="Green" /> |
|
<Color name="String" foreground="Blue" /> |
|
|
|
<!-- This is the main ruleset. --> |
|
<RuleSet> |
|
<Span color="Comment" begin="//" /> |
|
<Span color="Comment" multiline="true" begin="/\*" end="\*/" /> |
|
|
|
<Span color="String"> |
|
<Begin>"</Begin> |
|
<End>"</End> |
|
<RuleSet> |
|
<!-- nested span for escape sequences --> |
|
<Span begin="\\" end="." /> |
|
</RuleSet> |
|
</Span> |
|
|
|
<Keywords fontWeight="bold" foreground="Blue"> |
|
<Word>if</Word> |
|
<Word>else</Word> |
|
<!-- ... --> |
|
</Keywords> |
|
|
|
<!-- Digits --> |
|
<Rule foreground="DarkBlue"> |
|
\b0[xX][0-9a-fA-F]+ # hex number |
|
| \b |
|
( \d+(\.[0-9]+)? #number with optional floating point |
|
| \.[0-9]+ #or just starting with floating point |
|
) |
|
([eE][+-]?[0-9]+)? # optional exponent |
|
</Rule> |
|
</RuleSet> |
|
</SyntaxDefinition></pre> |
|
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 (<code>color="Comment"</code>) or directly specified (<code>fontWeight="bold" foreground="Blue"</code>). |
|
<p> |
|
Spans consist of two regular expressions (begin+end), while rules are simply a single RegEx with a color. The <code><Keywords></code> 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. |
|
<p> |
|
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. |
|
<p> |
|
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. |
|
<p> |
|
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 (<code>.</code> matches any character). |
|
This ensures that <code>\"</code> does not denote the end of the string span; but <code>\\"</code> still does. |
|
<p> |
|
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. |
|
|
|
<p><i>On-demand</i> 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. |
|
<p>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. |
|
<p><i>Incrementally</i> means that even if the document is changed, the stored span stacks will be reused as far as possible. If the user types <code>/*</code>, 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 <code>*/</code> 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 <code>/*</code>. |
|
|
|
<p>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 (<code>ICSharpCode.AvalonEdit.Utils.CompressingTreeList</code>). |
|
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 <code>//</code> or <code>///</code> are more popular than <code>/* */</code> comments. |
|
|
|
<h2>Code Completion</h2> |
|
<p>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. |
|
<p> |
|
Here's how you can use it: |
|
<pre lang="cs"> // in the constructor: |
|
textEditor.TextArea.TextEntering += textEditor_TextArea_TextEntering; |
|
textEditor.TextArea.TextEntered += textEditor_TextArea_TextEntered; |
|
} |
|
|
|
CompletionWindow completionWindow; |
|
|
|
void textEditor_TextArea_TextEntered(object sender, TextCompositionEventArgs e) |
|
{ |
|
if (e.Text == ".") { |
|
// Open code completion after the user has pressed dot: |
|
completionWindow = new CompletionWindow(textEditor.TextArea); |
|
IList<ICompletionData> data = completionWindow.CompletionList.CompletionData; |
|
data.Add(new MyCompletionData("Item1")); |
|
data.Add(new MyCompletionData("Item2")); |
|
data.Add(new MyCompletionData("Item3")); |
|
completionWindow.Show(); |
|
completionWindow.Closed += delegate { |
|
completionWindow = null; |
|
}; |
|
} |
|
} |
|
|
|
void textEditor_TextArea_TextEntering(object sender, TextCompositionEventArgs e) |
|
{ |
|
if (e.Text.Length > 0 && completionWindow != null) { |
|
if (!char.IsLetterOrDigit(e.Text[0])) { |
|
// Whenever a non-letter is typed while the completion window is open, |
|
// insert the currently selected element. |
|
completionWindow.CompletionList.RequestInsertion(e); |
|
} |
|
} |
|
// Do not set e.Handled=true. |
|
// We still want to insert the character that was typed. |
|
}</pre> |
|
This code will open the code completion window whenever '.' is pressed. |
|
By default, the <code>CompletionWindow</code> 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 <code>TextEntering</code> event and tell the |
|
completion window to insert the selected item. |
|
<p> |
|
The <code>CompletionWindow</code> will actually never have focus - instead, it hijacks the WPF keyboard input events |
|
on the text area and passes them through its <code>ListBox</code>. |
|
This allows selecting entries in the completion list using the keyboard and normal typing in the editor at the same time. |
|
<p>For the sake of completeness, here is the implementation of the <code>MyCompletionData</code> class used in the code above: |
|
<pre lang="cs">/// Implements AvalonEdit ICompletionData interface to provide the entries in the |
|
/// completion drop down. |
|
public class MyCompletionData : ICompletionData |
|
{ |
|
public MyCompletionData(string text) |
|
{ |
|
this.Text = text; |
|
} |
|
|
|
public System.Windows.Media.ImageSource Image { |
|
get { return null; } |
|
} |
|
|
|
public string Text { get; private set; } |
|
|
|
// Use this property if you want to show a fancy UIElement in the list. |
|
public object Content { |
|
get { return this.Text; } |
|
} |
|
|
|
public object Description { |
|
get { return "Description for " + this.Text; } |
|
} |
|
|
|
public void Complete(TextArea textArea, ISegment completionSegment, |
|
EventArgs insertionRequestEventArgs) |
|
{ |
|
textArea.Document.Replace(completionSegment, this.Text); |
|
} |
|
}</pre> |
|
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 <code>Complete</code> method if you want to do more than simply inserting the text. |
|
The <code>insertionRequestEventArgs</code> can help decide which kind of insertion the user wants - depending on how the insertion |
|
was triggered, it is an instance of <code>TextCompositionEventArgs</code>, <code>KeyEventArgs</code> or <code>MouseEventArgs</code>. |
|
|
|
<h2>History</h2> |
|
|
|
<ul> |
|
<li>August 13, 2008: Work on AvalonEdit started</li> |
|
<li>November 7, 2008: First version of AvalonEdit added to SharpDevelop 4.0 trunk</li> |
|
<li>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</li> |
|
<li>October 4, 2009: This article first published on The Code Project</li> |
|
</ul> |
|
|
|
<p><b>Note: although my sample code is provided under the MIT license, ICSharpCode.AvalonEdit itself is provided under the terms of the GNU LGPL.</b> |
|
|
|
<!------------------------------- That's it! ---------------------------> |
|
</div></body> |
|
|
|
</html>
|
|
|