Browse Source
Added (unfinished) CP article about AvalonEdit. git-svn-id: svn://svn.sharpdevelop.net/sharpdevelop/trunk@4973 1ccf3a8d-04fe-1044-b7c0-cef0b8235c61shortcuts
26 changed files with 393 additions and 48 deletions
Binary file not shown.
@ -0,0 +1,290 @@
@@ -0,0 +1,290 @@
|
||||
<!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>The Code Project</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> |
||||
<!---------------------------------------------------------------------------> |
||||
|
||||
|
||||
<!------------------------------- STEP 1 ---------------------------> |
||||
<!-- Fill in the details (CodeProject will reformat this section for you) --> |
||||
|
||||
|
||||
<!------------------------------- STEP 2 ---------------------------> |
||||
<!-- Include download and sample image information. --> |
||||
|
||||
<ul class=download> |
||||
<li><a href="Article_demo.zip">Download demo project - XXX Kb </a></li> |
||||
<li><a href="Article_src.zip">Download source - XXX Kb</a></li> |
||||
</ul> |
||||
|
||||
<p><img src="Article.gif" alt="Sample Image - maximum width is 600 pixels" width=400 height=200></p> |
||||
|
||||
|
||||
<!------------------------------- STEP 3 ---------------------------> |
||||
|
||||
<!-- 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 Invalidate(). |
||||
|
||||
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 TextBox: |
||||
<pre lang="cs"><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.GetDefinitionByExtension(".cs");</pre> |
||||
AvalonEdit has syntax highlighting definitions built in for: |
||||
ASP.NET, Batch files, Boo, Coco/R grammars, C++, C#, HTML, Java, JavaScript, Patch files, PHP, TeX, VB.NET, 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</h2> |
||||
TODO: overview of the namespaces, insert graph from NDepend |
||||
|
||||
<h2>Document (The Model)</h2> |
||||
|
||||
<p>So, what is the model of a text editor that has support for complex features like syntax highlighting and folding?<br> |
||||
Would you expect to be able to access collapsed text using the document model, given that the text is folded away?<br> |
||||
Is the syntax highlighting part of the model? |
||||
|
||||
<p>In my quest for a good representation of the model, I decided on a radical strategy: |
||||
<b>if it's not a <code>char</code>, it's not in the model</b>! |
||||
|
||||
<p>The main class of the model is <code>ICSharpCode.AvalonEdit.Document.TextDocument</code>, |
||||
and it's sort of a <code>StringBuilder</code> with events. |
||||
|
||||
<p><i>Simplified</i> definition of TextDocument: |
||||
<pre lang="cs">public sealed class TextDocument : ITextSource |
||||
{ |
||||
public event EventHandler UpdateStarted; |
||||
public event EventHandler<DocumentChangeEventArgs> Changing; |
||||
public event EventHandler<DocumentChangeEventArgs> Changed; |
||||
public event EventHandler TextChanged; |
||||
public event EventHandler UpdateFinished; |
||||
|
||||
public TextAnchor CreateAnchor(int offset); |
||||
public ITextSource CreateSnapshot(); |
||||
|
||||
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 BeginUpdate(); |
||||
public bool IsInUpdate { get; } |
||||
public void EndUpdate(); |
||||
|
||||
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> |
||||
|
||||
<h3>Offsets</h3> |
||||
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. |
||||
|
||||
<p>This is exactly the same as the 'index' parameter used by methods in the .NET <code>String</code> or <code>StringBuilder</code> classes. |
||||
Offsets are used because they are dead simple. Want to get all text between offset 10 and offset 30? |
||||
Simply call <code>document.GetText(10, 20)</code> - just like <code>String.Substring</code>, AvalonEdit usually uses <code>Offset / Length</code> pairs to refer to text segments. |
||||
|
||||
<p>To easily pass such segments around, AvalonEdit defines the <code>ISegment</code> interface: |
||||
<pre lang="cs">public interface ISegment |
||||
{ |
||||
int Offset { get; } |
||||
int Length { get; } // must be non-negative |
||||
int EndOffset { get; } // must return Offset+Length |
||||
}</pre> |
||||
All TextDocument methods taking Offset/Length parameters also have an overload taking an <code>ISegment</code> instance - I have just removed those from the code listing above to make it easier to read. |
||||
|
||||
<h3>Lines</h3> |
||||
Offsets are easy to use, but sometimes you need Line / Column pairs instead. |
||||
AvalonEdit defines a struct called <code>TextLocation</code> for those. |
||||
|
||||
<p>The TextDocument 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>. |
||||
|
||||
<p>The TextDocument.Lines collection contains one DocumentLine instance for every line in the document. |
||||
This collection is read-only to user code and is automatically updated to always<sup><small>*</small></sup> reflect the current document content. |
||||
|
||||
<p>Internally, the DocumentLine instances build a binary tree that allows for both efficient updates and lookup. |
||||
Looking up the start offset from a line number is possible in O(lg N) time, and the data structure also updates all offsets in O(lg N) time whenever text is inserted/removed. |
||||
|
||||
|
||||
<p><small>* tiny exception: it is possible to see the line collection in an inconsistent state inside <code>ILineTracker</code> callbacks. Don't use <code>ILineTracker</code> |
||||
unless you know what you are doing!</small> |
||||
|
||||
<h3>Change Events</h3> |
||||
|
||||
Here is the order in which events are raised during a document update: |
||||
|
||||
<p><b>BeginUpdate()</b> |
||||
<ul><li><code>UpdateStarted</code> event is raised</li></ul> |
||||
|
||||
<p><b>Insert() / Remove() / Replace()</b> |
||||
<ul> |
||||
<li><code>Changing</code> event is raised</li> |
||||
<li>The document is changed</li> |
||||
<li><code>TextAnchor.Deleted</code> events are raised if anchors were in the deleted text portion</li> |
||||
<li><code>Changed</code> event is raised</li> |
||||
</ul> |
||||
|
||||
<p><b>EndUpdate()</b> |
||||
<ul><li><code>TextChanged</code> event is raised</li> |
||||
<li><code>TextLengthChanged</code> event is raised</li> |
||||
<li><code>LineCountChanged</code> event is raised</li> |
||||
<li><code>UpdateFinished</code> event is raised</li></ul> |
||||
|
||||
<p>If the insert/remove/replace methods are called without a call to <code>BeginUpdate()</code>, they will call |
||||
<code>BeginUpdate()</code> and <code>EndUpdate()</code> to ensure no change happens outside of UpdateStarted/UpdateFinished. |
||||
|
||||
<p>There can be multiple document changes between the <code>BeginUpdate()</code> and <code>EndUpdate()</code> calls. |
||||
In this case, the events associated with EndUpdate will be raised only once after the whole document update is done. |
||||
|
||||
<p>The <code>UndoStack</code> listens to the <code>UpdateStarted</code> and <code>UpdateFinished</code> events to group |
||||
all changes into a single undo step. |
||||
|
||||
<h3>TextAnchor</h3> |
||||
If you are working with the text editor, you will likely run into the problem that you need to store an offset, but want it to adjust |
||||
automatically whenever text is inserted prior to that offset. |
||||
|
||||
<p>Sure, you could listen to the <code>TextDocument.Changed</code> event and call <code>GetNewOffset</code> on the <code>DocumentChangeEventArgs</code> to translate |
||||
the offset, but that gets tedious; especially when your object is short-lived and you have to deal with deregistering the event handler at the correct point of time.<br> |
||||
|
||||
<p>A much simpler solution is to use the <code>TextAnchor</code> class. Usage: |
||||
<pre lang="cs">TextAnchor anchor = document.CreateAnchor(offset); |
||||
ChangeMyDocument(); |
||||
int newOffset = anchor.Offset;</pre> |
||||
|
||||
<p>The document will automatically update all text anchors; and because it uses weak references to do so, the GC can simply collect the anchor object when you don't need it anymore. |
||||
|
||||
<p>Moreover, the document is able to efficiently update a large number of anchors without having to look at each anchor object individually. Updating the offsets of all anchors |
||||
usually only takes time logarithmic to the number of anchors. Retrieving the <code>TextAnchor.Offset</code> property also runs in O(lg N). |
||||
|
||||
<p>When a piece of text containing an anchor is removed; that anchor will be deleted. First, the <code>TextAnchor.IsDeleted</code> property is set to true on all deleted anchors, then the |
||||
<code>TextAnchor.Deleted</code> events are raised. You cannot retrieve the offset from an anchor that has been deleted. |
||||
|
||||
<p>This deletion behavior might be useful when using anchors for building a bookmark feature, but in other cases you want to still be able to use the anchor. For those cases, set <code>TextAnchor.SurviveDeletion = true</code>. |
||||
|
||||
<p>Note that anchor movement is ambiguous if text is inserted exactly at the anchor's location. Does the anchor stay before the inserted text, or does it move after it? |
||||
The property <code>TextAnchor.MovementType</code> will be used to determine which of these two options the anchor will choose. The default value is <code>AnchorMovementType.BeforeInsertion</code>. |
||||
|
||||
<p>If you want to track a segment, you can use the <code>AnchorSegment</code> class which implements <code>ISegment</code> using two text anchors. |
||||
|
||||
<h3>TextSegmentCollection</h3> |
||||
<p>Sometimes it is useful to store a list of segments and be able to efficiently find all segments overlapping with some other segment.<br> |
||||
Example: you might want to store a large number compiler warnings and render squiggly underlines only those that are in the visible region of the document. |
||||
|
||||
<p>The <code>TextSegmentCollection</code> serves this purpose. Connected to a document, it will automatically update the offsets of all TextSegment instances inside the collection; |
||||
but it also has the useful methods <code>FindOverlappingSegments</code> and <code>FindFirstSegmentWithStartAfter</code>. |
||||
The underlying data structure is a hybrid between the one used for text anchors and an <a href="http://en.wikipedia.org/wiki/Interval_tree#Augmented_tree">interval tree</a>, so it is able to do both jobs quite fast. |
||||
|
||||
<h3>Thread Safety</h3> |
||||
<p>The <code>TextDocument</code> class is not thread-safe. It expects to have a single owner thread and will throw an <code>InvalidOperationException</code> when accessed from another thread. |
||||
|
||||
<p>However, there is a single method that is thread-safe: <code>CreateSnapshot()</code><br> |
||||
It returns an immutable snapshot of the document. The snapshot is thread-safe, so it is very useful for features like a background parser that is running on its own thread. |
||||
The overload <code>CreateSnapshot(out ChangeTrackingCheckpoint)</code> also returns a <code>ChangeTrackingCheckpoint</code> for the document snapshot. |
||||
Once you have two checkpoints, you can call <code>GetChangesTo</code> to retrieve the complete list of document changes that happened between those versions of the document. |
||||
|
||||
<h2>Rendering</h2> |
||||
|
||||
Noticed how through 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 TextView uses its own kind of model: the <code>VisualLine</code>. |
||||
VisualLines are created only for the visible part of the document. |
||||
<p>The creation process looks like this:<br> |
||||
<img src="renderingPipeline.png" alt="rendering pipeline"> |
||||
|
||||
<h2>Editing</h2> |
||||
|
||||
TODO: write this section |
||||
|
||||
<h2>Points of Interest</h2> |
||||
|
||||
<p>Did you learn anything interesting/fun/annoying while writing the code? Did you |
||||
do anything particularly clever or wild or zany? |
||||
|
||||
<h2>History</h2> |
||||
|
||||
<p>Keep a running update of any changes or improvements you've made here. |
||||
|
||||
<p><b>Note: although my sample code is provided under the BSD license, ICSharpCode.AvalonEdit itself is provided under the terms of the GNU LGPL.</b> |
||||
|
||||
<!------------------------------- That's it! ---------------------------> |
||||
</body> |
||||
|
||||
</html> |
After Width: | Height: | Size: 28 KiB |
Loading…
Reference in new issue