Browse Source

Squashed 'AvalonEdit/' content from commit 58e2044

git-subtree-dir: AvalonEdit
git-subtree-split: 58e2044b109cadb1ec7eacce07839f17de8de4bf
pull/1/head
Daniel Grunwald 15 years ago
commit
4bbae335f7
  1. 33
      Documentation/Architecture.aml
  2. 133
      Documentation/Code Completion.aml
  3. 175
      Documentation/Coordinate Systems.aml
  4. 58
      Documentation/Folding.aml
  5. 11
      Documentation/ICSharpCode.AvalonEdit.content
  6. 188
      Documentation/ICSharpCode.AvalonEdit.shfbproj
  7. 566
      Documentation/License.html
  8. BIN
      Documentation/Media/NamespaceDependencies.png
  9. BIN
      Documentation/Media/RenderingPipeline.png
  10. BIN
      Documentation/Media/VisualTree.png
  11. BIN
      Documentation/Media/WelcomeScreenshot.png
  12. 78
      Documentation/Sample Application.aml
  13. 234
      Documentation/Syntax Highlighting.aml
  14. 171
      Documentation/Text Rendering.aml
  15. 70
      Documentation/Welcome.aml
  16. 64
      ICSharpCode.AvalonEdit.Tests/Document/ChangeTrackingTest.cs
  17. 154
      ICSharpCode.AvalonEdit.Tests/Document/CollapsingTests.cs
  18. 76
      ICSharpCode.AvalonEdit.Tests/Document/HeightTests.cs
  19. 531
      ICSharpCode.AvalonEdit.Tests/Document/LineManagerTests.cs
  20. 168
      ICSharpCode.AvalonEdit.Tests/Document/RandomizedLineManagerTest.cs
  21. 313
      ICSharpCode.AvalonEdit.Tests/Document/TextAnchorTest.cs
  22. 350
      ICSharpCode.AvalonEdit.Tests/Document/TextSegmentTreeTest.cs
  23. 76
      ICSharpCode.AvalonEdit.Tests/Document/TextUtilitiesTests.cs
  24. 60
      ICSharpCode.AvalonEdit.Tests/Editing/ChangeDocumentTests.cs
  25. 131
      ICSharpCode.AvalonEdit.Tests/Editing/TextSegmentReadOnlySectionTests.cs
  26. 42
      ICSharpCode.AvalonEdit.Tests/Highlighting/HtmlClipboardTests.cs
  27. 3
      ICSharpCode.AvalonEdit.Tests/ICSharpCode.AvalonEdit.Tests.PartCover.Settings
  28. 107
      ICSharpCode.AvalonEdit.Tests/ICSharpCode.AvalonEdit.Tests.csproj
  29. 33
      ICSharpCode.AvalonEdit.Tests/Properties/AssemblyInfo.cs
  30. 86
      ICSharpCode.AvalonEdit.Tests/Utils/CaretNavigationTests.cs
  31. 107
      ICSharpCode.AvalonEdit.Tests/Utils/CompressingTreeListTests.cs
  32. 36
      ICSharpCode.AvalonEdit.Tests/Utils/ExtensionMethodsTests.cs
  33. 36
      ICSharpCode.AvalonEdit.Tests/Utils/IndentationStringTests.cs
  34. 180
      ICSharpCode.AvalonEdit.Tests/Utils/RopeTests.cs
  35. 110
      ICSharpCode.AvalonEdit.Tests/WeakReferenceTests.cs
  36. 223
      ICSharpCode.AvalonEdit.Tests/XmlParser/ParserTests.cs
  37. 115
      ICSharpCode.AvalonEdit.Tests/XmlParser/TextReplacementTests.cs
  38. BIN
      ICSharpCode.AvalonEdit.Tests/XmlParser/W3C.zip
  39. 15
      ICSharpCode.AvalonEdit.Tests/app.config
  40. 87
      ICSharpCode.AvalonEdit/AvalonEditCommands.cs
  41. 374
      ICSharpCode.AvalonEdit/CodeCompletion/CompletionList.cs
  42. 56
      ICSharpCode.AvalonEdit/CodeCompletion/CompletionList.xaml
  43. 96
      ICSharpCode.AvalonEdit/CodeCompletion/CompletionListBox.cs
  44. 198
      ICSharpCode.AvalonEdit/CodeCompletion/CompletionWindow.cs
  45. 376
      ICSharpCode.AvalonEdit/CodeCompletion/CompletionWindowBase.cs
  46. 54
      ICSharpCode.AvalonEdit/CodeCompletion/ICompletionData.cs
  47. 42
      ICSharpCode.AvalonEdit/CodeCompletion/IOverloadProvider.cs
  48. 66
      ICSharpCode.AvalonEdit/CodeCompletion/InsightWindow.cs
  49. 112
      ICSharpCode.AvalonEdit/CodeCompletion/InsightWindow.xaml
  50. 54
      ICSharpCode.AvalonEdit/CodeCompletion/OverloadInsightWindow.cs
  51. 101
      ICSharpCode.AvalonEdit/CodeCompletion/OverloadViewer.cs
  52. 140
      ICSharpCode.AvalonEdit/Document/ChangeTrackingCheckpoint.cs
  53. 131
      ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs
  54. 52
      ICSharpCode.AvalonEdit/Document/DocumentChangeOperation.cs
  55. 242
      ICSharpCode.AvalonEdit/Document/DocumentLine.cs
  56. 712
      ICSharpCode.AvalonEdit/Document/DocumentLineTree.cs
  57. 192
      ICSharpCode.AvalonEdit/Document/GapTextBuffer.cs
  58. 55
      ICSharpCode.AvalonEdit/Document/ILineTracker.cs
  59. 220
      ICSharpCode.AvalonEdit/Document/ISegment.cs
  60. 320
      ICSharpCode.AvalonEdit/Document/ITextSource.cs
  61. 30
      ICSharpCode.AvalonEdit/Document/IUndoableOperation.cs
  62. 288
      ICSharpCode.AvalonEdit/Document/LineManager.cs
  63. 83
      ICSharpCode.AvalonEdit/Document/LineNode.cs
  64. 132
      ICSharpCode.AvalonEdit/Document/NewLineFinder.cs
  65. 347
      ICSharpCode.AvalonEdit/Document/OffsetChangeMap.cs
  66. 194
      ICSharpCode.AvalonEdit/Document/TextAnchor.cs
  67. 87
      ICSharpCode.AvalonEdit/Document/TextAnchorNode.cs
  68. 753
      ICSharpCode.AvalonEdit/Document/TextAnchorTree.cs
  69. 817
      ICSharpCode.AvalonEdit/Document/TextDocument.cs
  70. 147
      ICSharpCode.AvalonEdit/Document/TextDocumentWeakEventManager.cs
  71. 166
      ICSharpCode.AvalonEdit/Document/TextLocation.cs
  72. 236
      ICSharpCode.AvalonEdit/Document/TextSegment.cs
  73. 951
      ICSharpCode.AvalonEdit/Document/TextSegmentCollection.cs
  74. 332
      ICSharpCode.AvalonEdit/Document/TextUtilities.cs
  75. 61
      ICSharpCode.AvalonEdit/Document/UndoOperationGroup.cs
  76. 442
      ICSharpCode.AvalonEdit/Document/UndoStack.cs
  77. 85
      ICSharpCode.AvalonEdit/Document/WeakLineTracker.cs
  78. 102
      ICSharpCode.AvalonEdit/Editing/AbstractMargin.cs
  79. 469
      ICSharpCode.AvalonEdit/Editing/Caret.cs
  80. 86
      ICSharpCode.AvalonEdit/Editing/CaretLayer.cs
  81. 335
      ICSharpCode.AvalonEdit/Editing/CaretNavigationCommandHandler.cs
  82. 33
      ICSharpCode.AvalonEdit/Editing/CaretWeakEventHandler.cs
  83. 63
      ICSharpCode.AvalonEdit/Editing/DottedLineMargin.cs
  84. 46
      ICSharpCode.AvalonEdit/Editing/DragDropException.cs
  85. 554
      ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.cs
  86. 32
      ICSharpCode.AvalonEdit/Editing/IReadOnlySectionProvider.cs
  87. 232
      ICSharpCode.AvalonEdit/Editing/LineNumberMargin.cs
  88. 50
      ICSharpCode.AvalonEdit/Editing/NoReadOnlySections.cs
  89. 279
      ICSharpCode.AvalonEdit/Editing/RectangleSelection.cs
  90. 181
      ICSharpCode.AvalonEdit/Editing/Selection.cs
  91. 48
      ICSharpCode.AvalonEdit/Editing/SelectionColorizer.cs
  92. 52
      ICSharpCode.AvalonEdit/Editing/SelectionLayer.cs
  93. 580
      ICSharpCode.AvalonEdit/Editing/SelectionMouseHandler.cs
  94. 170
      ICSharpCode.AvalonEdit/Editing/SimpleSelection.cs
  95. 1025
      ICSharpCode.AvalonEdit/Editing/TextArea.cs
  96. 109
      ICSharpCode.AvalonEdit/Editing/TextAreaDefaultInputHandlers.cs
  97. 242
      ICSharpCode.AvalonEdit/Editing/TextAreaInputHandler.cs
  98. 80
      ICSharpCode.AvalonEdit/Editing/TextSegmentReadOnlySectionProvider.cs
  99. 30
      ICSharpCode.AvalonEdit/Folding/AbstractFoldingStrategy.cs
  100. 179
      ICSharpCode.AvalonEdit/Folding/FoldingElementGenerator.cs
  101. Some files were not shown because too many files have changed in this diff Show More

33
Documentation/Architecture.aml

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<topic id="5d1af8a2-fc1b-4a1b-b6c1-f33fb14bec1f" revisionNumber="1">
<developerConceptualDocument xmlns="http://ddue.schemas.microsoft.com/authoring/2003/5" xmlns:xlink="http://www.w3.org/1999/xlink">
<!--
<summary>
<para>Optional summary abstract</para>
</summary>
-->
<introduction>
<!-- Uncomment this to generate an outline of the section and sub-section
titles. Specify a numeric value as the inner text to limit it to
a specific number of sub-topics when creating the outline. Specify
zero (0) to limit it to top-level sections only. -->
<!-- <autoOutline /> -->
<mediaLink><image xlink:href="NamespaceDependencies" placement="center"/></mediaLink>
<para>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.</para>
<para>Here is the visual tree of the TextEditor control:</para>
<mediaLink><image xlink:href="VisualTree" placement="center"/></mediaLink>
<para>It's important to understand that AvalonEdit is a composite control
with the three layers:
<codeEntityReference>T:ICSharpCode.AvalonEdit.TextEditor</codeEntityReference> (main control),
<codeEntityReference>T:ICSharpCode.AvalonEdit.Editing.TextArea</codeEntityReference> (editing),
<codeEntityReference>T:ICSharpCode.AvalonEdit.Rendering.TextView</codeEntityReference> (rendering).
</para><para>
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 <codeInline>textEditor.TextArea</codeInline> or
<codeInline>textEditor.TextArea.TextView</codeInline>.</para>
</introduction>
</developerConceptualDocument>
</topic>

133
Documentation/Code Completion.aml

@ -0,0 +1,133 @@ @@ -0,0 +1,133 @@
<?xml version="1.0" encoding="utf-8"?>
<topic id="47c58b63-f30c-4290-a2f2-881d21227446" revisionNumber="1">
<developerConceptualDocument xmlns="http://ddue.schemas.microsoft.com/authoring/2003/5" xmlns:xlink="http://www.w3.org/1999/xlink">
<introduction>
<para>
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.
</para>
</introduction>
<section>
<title>Usage of the Code Completion Window</title>
<content>
<code language="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&lt;ICompletionData&gt; 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 &amp;&amp; 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.
}
</code>
<para>
This code will open the code completion window whenever '.' is pressed.
By default, the
<codeEntityReference>T:ICSharpCode.AvalonEdit.CodeCompletion.CompletionWindow</codeEntityReference>
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 <codeInline>TextEntering</codeInline> event
and tell the completion window to insert the selected item.
</para>
<para>
The <codeInline>CompletionWindow</codeInline> will actually never have
focus - instead, it hijacks
the WPF keyboard input events on the text area and passes them through its
<codeInline>ListBox</codeInline>.
This allows selecting entries in the completion list using the
keyboard and normal typing in the editor at the same time.
</para>
<para>
Here is the implementation of the MyCompletionData class used in the code above:
<code language="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);
}
}
</code>
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 <codeInline>Complete</codeInline>
method if you want to do more than simply inserting the text.
The <codeInline>insertionRequestEventArgs</codeInline> can help decide which
kind of insertion the user wants - depending on how the insertion was triggered,
it is an instance of <codeInline>TextCompositionEventArgs</codeInline>,
<codeInline>KeyEventArgs</codeInline> or <codeInline>MouseEventArgs</codeInline>.
</para>
</content>
</section>
<section>
<title>Code Completion for C#</title>
<content>
<para>
Full C# code completion is not in the scope of AvalonEdit.
You will need a C# parser, a C# type system, and the ability
to resolve C# expressions in your type system.
</para>
<para>
If you want to learn how this is handled in SharpDevelop, please
see:
<externalLink>
<linkText>Code Completion in SharpDevelop</linkText>
<linkUri>http://wiki.sharpdevelop.net/CodeCompletion.ashx</linkUri>
<linkTarget>_blank</linkTarget>
</externalLink>
</para>
</content>
</section>
</developerConceptualDocument>
</topic>

175
Documentation/Coordinate Systems.aml

@ -0,0 +1,175 @@ @@ -0,0 +1,175 @@
<?xml version="1.0" encoding="utf-8"?>
<topic id="5b1854b4-884c-4713-b921-b28e96a1b43e" revisionNumber="1">
<developerConceptualDocument xmlns="http://ddue.schemas.microsoft.com/authoring/2003/5" xmlns:xlink="http://www.w3.org/1999/xlink">
<!--
<summary>
<para>Optional summary abstract</para>
</summary>
-->
<introduction>
<!-- Uncomment this to generate an outline of the section and sub-section
titles. Specify a numeric value as the inner text to limit it to
a specific number of sub-topics when creating the outline. Specify
zero (0) to limit it to top-level sections only. -->
<!-- <autoOutline /> -->
<para>The text editor makes use of several different coordinate systems.
Here's an explanation of them.</para>
</introduction>
<!-- Add one or more top-level section elements. These are collapsible.
If using <autoOutline /> tag, add an address attribute to identify
it so that it can be jumped to with a hyperlink. -->
<section>
<title>Offset</title>
<content>
<para>In AvalonEdit, an index into the document is called an <newTerm>offset</newTerm>.</para>
<para>
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 <codeInline>document.TextLength</codeInline>,
representing the end of the document.
This is exactly the same as the <codeInline>index</codeInline> parameter
used by methods in the .NET String or StringBuilder classes.
</para>
</content>
</section>
<section>
<title>TextLocation</title>
<content>
<para>The
<codeEntityReference qualifyHint="true">T:ICSharpCode.AvalonEdit.Document.TextLocation</codeEntityReference>
struct represents a Line/Column pair. Line and column are counted from 1.</para>
<para>The document provides the methods
<codeEntityReference qualifyHint="true">M:ICSharpCode.AvalonEdit.Document.TextDocument.GetLocation(System.Int32)</codeEntityReference>
and
<codeEntityReference qualifyHint="true">M:ICSharpCode.AvalonEdit.Document.TextDocument.GetOffset(ICSharpCode.AvalonEdit.Document.TextLocation)</codeEntityReference>
to convert between offsets and <codeInline>TextLocation</codeInline>s.</para>
</content>
</section>
<section>
<title>TextAnchor</title>
<content>
<para>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. </para>
<para>Sure, you could listen to the TextDocument.Changed event and call
GetNewOffset on the DocumentChangeEventArgs 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.</para>
<para>A text anchor object stores an Offset, but automatically
updates the offset when text is inserted/removed before the offset.
</para>
<para>
A much simpler solution is to use the
<codeEntityReference qualifyHint="true">T:ICSharpCode.AvalonEdit.Document.TextAnchor</codeEntityReference>
class.
Please take a look at the documentation for that class for more details.
</para>
</content>
</section>
<section>
<title>RelativeTextOffset</title>
<content>
<para>An offset in the document, but relative to the start offset of a <codeEntityReference>T:ICSharpCode.AvalonEdit.Rendering.VisualLine</codeEntityReference>.</para>
<para>Relative text offsets are used to store document offsets in visual lines.</para>
<para>You can convert between relative text offsets and document offsets
by adding/subtracting
<codeEntityReference qualifyHint="true">P:ICSharpCode.AvalonEdit.Rendering.VisualLine.FirstDocumentLine</codeEntityReference>.<codeEntityReference>P:ICSharpCode.AvalonEdit.Document.DocumentLine.Offset</codeEntityReference>.
</para>
</content>
</section>
<section>
<title>VisualColumn</title>
<content>
<para>An integer value that specifies a position inside a VisualLine.</para>
<para>
Not only text has a length in the visual line, but also other VisualLineElements.
VisualColumn is counting from 0 for each visual line.
</para>
<para>For example, tab markers take 2 visual columns (the marker and the tab space),
newline markers take 1 visual column; folding markers take just 1 visual column
even though they are longer in the document text.</para>
<para>Use the
<codeEntityReference qualifyHint="true">M:ICSharpCode.AvalonEdit.Rendering.VisualLine.GetVisualColumn(System.Int32)</codeEntityReference>
and
<codeEntityReference qualifyHint="true">M:ICSharpCode.AvalonEdit.Rendering.VisualLine.GetRelativeOffset(System.Int32)</codeEntityReference>
methods to convert between
visual columns and relative text offsets.</para>
<alert class="note">
<para>Do not confuse VisualColumn with text columns.
VisualColumn starts at 0, text column at 1. Text may have different length
in the two coordinate systems (e.g. tab markers, foldings).</para>
</alert>
</content>
</section>
<section>
<title>TextViewPosition</title>
<content>
<para>A Line,Column,VisualColumn triple.</para>
<para>The <codeEntityReference qualifyHint="true">T:ICSharpCode.AvalonEdit.TextViewPosition</codeEntityReference>
struct can be implicitly converted
to <codeEntityReference qualifyHint="false">T:ICSharpCode.AvalonEdit.Document.TextLocation</codeEntityReference>,
but has the additional VisualColumn information
that is necessary to accurately hold the caret position when
VisualLineElements with DocumentLength 0 are in use.</para>
</content>
</section>
<section>
<title>VisualTop</title>
<content>
<para>A double value that specifies the distance from the top of
the document to the top of a line measured in device-independent pixels.</para>
<para>VisualTop is equivalent to the Y component of a VisualPosition.</para>
</content>
</section>
<section>
<title>VisualPosition</title>
<content>
<para>A Point value (double X,Y) that specifies the position of an
element from the top left document corner measured in device-independent pixels.</para>
<para>To convert a VisualPosition to or from a (mouse) position inside
the TextView, simply subtract or add
<codeEntityReference qualifyHint="true">P:ICSharpCode.AvalonEdit.Rendering.TextView.ScrollOffset</codeEntityReference>
to it.
</para>
</content>
</section>
<relatedTopics>
<!-- One or more of the following:
- A local link
- An external link
- A code entity reference
<link xlink:href="Other Topic's ID"/>
<link xlink:href="Other Topic's ID">Link inner text</link>
<externalLink>
<linkText>Link text</linkText>
<linkAlternateText>Optional alternate link text</linkAlternateText>
<linkUri>URI</linkUri>
</externalLink>
<codeEntityReference>API member ID</codeEntityReference>
Examples:
<link xlink:href="00e97994-e9e6-46e0-b420-5be86b2f8270" />
<link xlink:href="00e97994-e9e6-46e0-b420-5be86b2f8278">Some other topic</link>
<externalLink>
<linkText>SHFB on CodePlex</linkText>
<linkAlternateText>Go to CodePlex</linkAlternateText>
<linkUri>http://www.codeplex.com/SHFB</linkUri>
</externalLink>
<codeEntityReference>T:TestDoc.TestClass</codeEntityReference>
<codeEntityReference>P:TestDoc.TestClass.SomeProperty</codeEntityReference>
<codeEntityReference>M:TestDoc.TestClass.#ctor</codeEntityReference>
<codeEntityReference>M:TestDoc.TestClass.#ctor(System.String,System.Int32)</codeEntityReference>
<codeEntityReference>M:TestDoc.TestClass.ToString</codeEntityReference>
<codeEntityReference>M:TestDoc.TestClass.FirstMethod</codeEntityReference>
<codeEntityReference>M:TestDoc.TestClass.SecondMethod(System.Int32,System.String)</codeEntityReference>
-->
</relatedTopics>
</developerConceptualDocument>
</topic>

58
Documentation/Folding.aml

@ -0,0 +1,58 @@ @@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<topic id="440df648-413e-4f42-a28b-6b2b0e9b1084" revisionNumber="1">
<developerConceptualDocument xmlns="http://ddue.schemas.microsoft.com/authoring/2003/5" xmlns:xlink="http://www.w3.org/1999/xlink">
<!--<introduction>
<para>
Introduction for 'Folding'.
</para>
</introduction>-->
<section>
<title>How to use Folding in your application</title>
<content>
<para>
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 <codeEntityReference>T:ICSharpCode.AvalonEdit.Rendering.VisualLineElementGenerator</codeEntityReference>
takes care of the collapsed sections in the text document; and a custom
margin draws the plus and minus buttons.
</para>
<para>You could use the relevant classes separately;
but, to make it a bit easier to use, the static
<codeEntityReference qualifyHint="true">M:ICSharpCode.AvalonEdit.Folding.FoldingManager.Install(ICSharpCode.AvalonEdit.Editing.TextArea)</codeEntityReference>
method will create and register the necessary parts automatically.</para>
<para>All that's left for you is to regularly call
<codeEntityReference qualifyHint="true">M:ICSharpCode.AvalonEdit.Folding.FoldingManager.UpdateFoldings(System.Collections.Generic.IEnumerable{ICSharpCode.AvalonEdit.Folding.NewFolding},System.Int32)</codeEntityReference>
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.</para>
<para>Here is the full code required to enable folding:
<code language="cs">foldingManager = FoldingManager.Install(textEditor.TextArea);
foldingStrategy = new XmlFoldingStrategy();
foldingStrategy.UpdateFoldings(foldingManager, textEditor.Document);</code>
If you want the folding markers to update when the text is changed,
you have to repeat the <codeInline>foldingStrategy.UpdateFoldings</codeInline> call regularly.
</para>
</content>
</section>
<section>
<title>How the FoldingManager works</title>
<content>
<para>
The FoldingManager maintains a list of foldings. The FoldMargin displays those foldings and provides
the UI for collapsing/expanding.</para><para>
Folded foldings cause the FoldingElementGenerator to produce a line element that spans the whole folded
text section, causing the text generation for the visual line that contains the folding start to
continue after the folding end in another line.</para><para>
To ensure scrolling works correctly in the presence of foldings, lines inside folded regions must not
be used as start lines for the visual line generation. This is done by setting the line height of all
such lines to 0. To efficiently set the height on a large number of lines and support reverting to the
old height when the folding is uncollapsed, a CollapsedLineSection is used.
</para>
</content>
</section>
<relatedTopics>
<codeEntityReference>T:ICSharpCode.AvalonEdit.Folding.FoldingManager</codeEntityReference>
</relatedTopics>
</developerConceptualDocument>
</topic>

11
Documentation/ICSharpCode.AvalonEdit.content

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Topics defaultTopic="c52241ea-3eba-4ddf-b463-6349cbff38fd">
<Topic id="c52241ea-3eba-4ddf-b463-6349cbff38fd" visible="True" />
<Topic id="70c4df51-5ecb-4e24-a574-8c5a84306bd1" visible="True" />
<Topic id="5d1af8a2-fc1b-4a1b-b6c1-f33fb14bec1f" visible="True" />
<Topic id="5b1854b4-884c-4713-b921-b28e96a1b43e" visible="True" />
<Topic id="c06e9832-9ef0-4d65-ac2e-11f7ce9c7774" visible="True" />
<Topic id="4d4ceb51-154d-43f0-b876-ad9640c5d2d8" visible="True" />
<Topic id="440df648-413e-4f42-a28b-6b2b0e9b1084" visible="True" />
<Topic id="47c58b63-f30c-4290-a2f2-881d21227446" visible="True" />
</Topics>

188
Documentation/ICSharpCode.AvalonEdit.shfbproj

@ -0,0 +1,188 @@ @@ -0,0 +1,188 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
<PropertyGroup>
<!-- The configuration and platform will be used to determine which
assemblies to include from solution and project documentation
sources -->
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{850b6602-0a7f-413a-864a-e816b98d7407}</ProjectGuid>
<SHFBSchemaVersion>1.8.0.0</SHFBSchemaVersion>
<!-- AssemblyName, Name, and RootNamespace are not used by SHFB but Visual
Studio adds them anyway -->
<AssemblyName>Documentation</AssemblyName>
<RootNamespace>Documentation</RootNamespace>
<Name>Documentation</Name>
<!-- SHFB properties -->
<OutputPath>.\Help\</OutputPath>
<HtmlHelpName>AvalonEdit Documentation</HtmlHelpName>
<ProjectSummary>
</ProjectSummary>
<MissingTags>Summary, AutoDocumentCtors, Namespace</MissingTags>
<VisibleItems>InheritedMembers, InheritedFrameworkMembers, Protected, ProtectedInternalAsProtected</VisibleItems>
<HtmlHelp1xCompilerPath>
</HtmlHelp1xCompilerPath>
<HtmlHelp2xCompilerPath>
</HtmlHelp2xCompilerPath>
<SandcastlePath>
</SandcastlePath>
<WorkingPath>
</WorkingPath>
<BuildLogFile>
</BuildLogFile>
<FrameworkVersion>3.5</FrameworkVersion>
<HelpTitle>AvalonEdit</HelpTitle>
<CopyrightText>Copyright 2008-2010, Daniel Grunwald</CopyrightText>
<PresentationStyle>Prototype</PresentationStyle>
<HelpFileVersion>4.0.0.0</HelpFileVersion>
<ComponentConfigurations>
<ComponentConfig id="Cached Reflection Index Data" enabled="True">
<component id="Cached Reflection Index Data" type="SandcastleBuilder.Components.CachedCopyFromIndexComponent" assembly="{@SHFBFolder}SandcastleBuilder.Components.dll">
<index name="reflection" value="/reflection/apis/api" key="@id" cache="10">
<cache base="{@SandcastlePath}Data\Reflection" recurse="true" files="*.xml" cacheFile="{@AppDataFolder}Cache\Reflection.cache" />
<data files="reflection.xml" />
</index>
<copy name="reflection" source="*" target="/document/reference" />
</component>
</ComponentConfig>
<ComponentConfig id="Cached MSDN URL References" enabled="True">
<component id="Cached MSDN URL References" type="SandcastleBuilder.Components.CachedResolveReferenceLinksComponent" assembly="{@SHFBFolder}SandcastleBuilder.Components.dll">
<cache filename="{@AppDataFolder}Cache\MsdnUrl.cache" />
<targets base="{@SandcastlePath}Data\Reflection" recurse="true" files="*.xml" type="{@SDKLinks}" />
<targets files="reflection.xml" type="{@ProjectLinks}" />
</component>
</ComponentConfig>
<ComponentConfig id="IntelliSense Component" enabled="True">
<component id="IntelliSense Component" type="SandcastleBuilder.Components.IntelliSenseComponent" assembly="{@SHFBFolder}SandcastleBuilder.Components.dll">
<!-- Output options (optional)
Attributes:
Include Namespaces (false by default)
Namespaces filename ("Namespaces" if not specified or empty)
Directory (current folder if not specified or empty) -->
<output includeNamespaces="false" namespacesFile="Namespaces" folder="{@OutputFolder}" />
</component>
</ComponentConfig>
<ComponentConfig id="Cached Framework Comments Index Data" enabled="True">
<component id="Cached Framework Comments Index Data" type="SandcastleBuilder.Components.CachedCopyFromIndexComponent" assembly="{@SHFBFolder}SandcastleBuilder.Components.dll">
<index name="comments" value="/doc/members/member" key="@name" cache="100">
{@CachedFrameworkCommentList}
{@CommentFileList}
</index>
<copy name="comments" source="*" target="/document/comments" />
</component>
</ComponentConfig>
<ComponentConfig id="Code Block Component" enabled="True">
<component id="Code Block Component" type="SandcastleBuilder.Components.CodeBlockComponent" assembly="{@SHFBFolder}SandcastleBuilder.Components.dll">
<!-- Base path for relative filenames in source attributes
(optional) -->
<basePath value="{@HtmlEncProjectFolder}" />
<!-- Connect to language filter (optional). If omitted,
language filtering is enabled by default. -->
<languageFilter value="true" />
<!-- Allow missing source files (Optional). If omitted,
it will generate errors if referenced source files
are missing. -->
<allowMissingSource value="false" />
<!-- Remove region markers from imported code blocks. If omitted,
region markers in imported code blocks are left alone. -->
<removeRegionMarkers value="false" />
<!-- Code colorizer options (required).
Attributes:
Language syntax configuration file (required)
XSLT style file (required)
"Copy" image file URL (required)
Default language (optional)
Enable line numbering (optional)
Enable outlining (optional)
Keep XML comment "see" tags within the code (optional)
Tab size override (optional, 0 = Use syntax file setting)
Use language name as default title (optional) -->
<colorizer syntaxFile="{@SHFBFolder}Colorizer\highlight.xml" styleFile="{@SHFBFolder}Colorizer\highlight.xsl" copyImageUrl="../icons/CopyCode.gif" language="cs" numberLines="false" outlining="false" keepSeeTags="false" tabSize="0" defaultTitle="true" />
</component>
</ComponentConfig>
<ComponentConfig id="Post-transform Component" enabled="True">
<component id="Post-transform Component" type="SandcastleBuilder.Components.PostTransformComponent" assembly="{@SHFBFolder}SandcastleBuilder.Components.dll">
<!-- Code colorizer files (required).
Attributes:
Stylesheet file (required)
Script file (required)
"Copy" image file (required) -->
<colorizer stylesheet="{@SHFBFolder}Colorizer\highlight.css" scriptFile="{@SHFBFolder}Colorizer\highlight.js" copyImage="{@SHFBFolder}Colorizer\CopyCode.gif" />
<!-- Base output path for the files (required). This should match
the parent folder of the output path of the HTML files (see
SaveComponent). -->
<outputPath value="Output\" />
<!-- Logo image file (optional). Filename is required. The height,
width, altText, placement, and alignment attributes are
optional. -->
<logoFile filename="" height="0" width="0" altText="" placement="left" alignment="left" />
</component>
</ComponentConfig>
</ComponentConfigurations>
<DocumentationSources>
<DocumentationSource sourceFile="..\ICSharpCode.AvalonEdit\ICSharpCode.AvalonEdit.csproj" />
</DocumentationSources>
<NamespaceSummaries>
<NamespaceSummaryItem name="(global)" isDocumented="False" />
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit" isDocumented="True">This is the main AvalonEdit namespace.</NamespaceSummaryItem>
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit.CodeCompletion" isDocumented="True">This namespace contains classes to show the code completion window.</NamespaceSummaryItem>
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit.Document" isDocumented="True">This namespace contains the document model.
The most important class here is TextDocument, which represents document that can be displayed and edited in the text editor.</NamespaceSummaryItem>
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit.Editing" isDocumented="True">This namespace is the home of the TextArea class. It manages user input and handles the caret and the selection.</NamespaceSummaryItem>
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit.Folding" isDocumented="True">This namespace contains the folding (code collapsing) implementation.</NamespaceSummaryItem>
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit.Highlighting" isDocumented="True">This namespace contains the engine for highlighting text documents (DocumentHighlighter).
Additionally, the class HighlightingColorizer provides integration of the highlighting engine into the text editor GUI.</NamespaceSummaryItem>
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit.Highlighting.Xshd" isDocumented="True">This namespace contains a document model for syntax highlighting definitions (.xshd files).</NamespaceSummaryItem>
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit.Indentation" isDocumented="True">This namespace contains the logic for automatic indentation.</NamespaceSummaryItem>
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit.Rendering" isDocumented="True">This namespace contains the text rendering infrastructure.</NamespaceSummaryItem>
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit.Utils" isDocumented="True">This namespace contains various utility classes.</NamespaceSummaryItem>
<NamespaceSummaryItem name="XamlGeneratedNamespace" isDocumented="False" />
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit.Xml" isDocumented="True">This namespace contains an error-tolerant XML parser with support for incremental parsing, only reparsing the changed regions of a TextDocument.</NamespaceSummaryItem>
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit.Snippets" isDocumented="True">Snippets perform automatic text insertion. Snippets can be interactive after they were expanded, for example to allow the user to easily replace fields in the expanded snippet.</NamespaceSummaryItem></NamespaceSummaries>
<CleanIntermediates>True</CleanIntermediates>
<SyntaxFilters>Standard</SyntaxFilters>
</PropertyGroup>
<!-- There are no properties for these two groups but they need to appear in
order for Visual Studio to perform the build. -->
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
</PropertyGroup>
<ItemGroup>
<None Include="Coordinate Systems.aml" />
<None Include="Architecture.aml" />
<None Include="Code Completion.aml" />
<None Include="Sample Application.aml" />
<None Include="Folding.aml" />
<None Include="Syntax Highlighting.aml" />
<None Include="Text Rendering.aml" />
<None Include="Welcome.aml" />
</ItemGroup>
<ItemGroup>
<ContentLayout Include="ICSharpCode.AvalonEdit.content" />
</ItemGroup>
<ItemGroup>
<Image Include="Media\WelcomeScreenshot.png">
<ImageId>WelcomeScreenshot</ImageId>
</Image>
<Image Include="Media\VisualTree.png">
<ImageId>VisualTree</ImageId>
<AlternateText>Visual Tree</AlternateText>
</Image>
<Image Include="Media\RenderingPipeline.png">
<ImageId>RenderingPipeline</ImageId>
</Image>
<Image Include="Media\NamespaceDependencies.png">
<ImageId>NamespaceDependencies</ImageId>
<AlternateText>Namespace Dependency Graph</AlternateText>
</Image>
<Content Include="License.html">
<ExcludeFromToc>True</ExcludeFromToc>
</Content>
</ItemGroup>
<ItemGroup>
<Folder Include="Media\" />
</ItemGroup>
<!-- Import the SHFB build targets -->
<Import Project="$(SHFBROOT)\SandcastleHelpFileBuilder.targets" />
</Project>

566
Documentation/License.html

@ -0,0 +1,566 @@ @@ -0,0 +1,566 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>License</title>
<meta name="id" content="e41769b2-38f7-4605-b1d8-9cd22a50a685">
<meta name="revisionNumber" content="1"></meta>
<link rel="stylesheet" type="text/css" href="../styles/presentation.css" />
</head>
<body>
<h3>GNU LESSER GENERAL PUBLIC LICENSE</h3>
<p>
Version 2.1, February 1999
</p>
<pre>Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
</pre>
<h3>Preamble</h3>
<p>
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
</p>
<p>
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
</p>
<p>
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
</p>
<p>
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
</p>
<p>
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
</p>
<p>
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
</p>
<p>
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
</p>
<p>
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
</p>
<p>
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
</p>
<p>
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
</p>
<p>
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
</p>
<p>
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
</p>
<p>
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
</p>
<p>
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
</p>
<p>
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
</p>
<h3>TERMS AND CONDITIONS FOR COPYING,
DISTRIBUTION AND MODIFICATION</h3>
<p>
<strong>0.</strong>
This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
</p>
<p>
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
</p>
<p>
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
</p>
<p>
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
</p>
<p>
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
</p>
<p>
<strong>1.</strong>
You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
</p>
<p>
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
</p>
<p>
<strong>2.</strong>
You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
</p>
<ul>
<li><strong>a)</strong>
The modified work must itself be a software library.</li>
<li><strong>b)</strong>
You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.</li>
<li><strong>c)</strong>
You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.</li>
<li><strong>d)</strong>
If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
<p>
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)</p></li>
</ul>
<p>
These requirements apply to the modified work as a whole. If identifiable
sections of that work are not derived from the Library, and can be
reasonably considered independent and separate works in themselves, then
this License, and its terms, do not apply to those sections when you
distribute them as separate works. But when you distribute the same
sections as part of a whole which is a work based on the Library, the
distribution of the whole must be on the terms of this License, whose
permissions for other licensees extend to the entire whole, and thus to
each and every part regardless of who wrote it.
</p>
<p>
Thus, it is not the intent of this section to claim rights or contest your
rights to work written entirely by you; rather, the intent is to exercise
the right to control the distribution of derivative or collective works
based on the Library.
</p>
<p>
In addition, mere aggregation of another work not based on the Library with
the Library (or with a work based on the Library) on a volume of a storage
or distribution medium does not bring the other work under the scope of
this License.
</p>
<p>
<strong>3.</strong>
You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
</p>
<p>
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
</p>
<p>
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
</p>
<p>
<strong>4.</strong>
You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
</p>
<p>
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
</p>
<p>
<strong>5.</strong>
A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
</p>
<p>
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
</p>
<p>
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
</p>
<p>
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
</p>
<p>
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
</p>
<p>
<strong>6.</strong>
As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
</p>
<p>
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
</p>
<ul>
<li><strong>a)</strong> Accompany the work with the complete
corresponding machine-readable source code for the Library
including whatever changes were used in the work (which must be
distributed under Sections 1 and 2 above); and, if the work is an
executable linked with the Library, with the complete
machine-readable "work that uses the Library", as object code
and/or source code, so that the user can modify the Library and
then relink to produce a modified executable containing the
modified Library. (It is understood that the user who changes the
contents of definitions files in the Library will not necessarily
be able to recompile the application to use the modified
definitions.)</li>
<li><strong>b)</strong> Use a suitable shared library mechanism
for linking with the Library. A suitable mechanism is one that
(1) uses at run time a copy of the library already present on the
user's computer system, rather than copying library functions into
the executable, and (2) will operate properly with a modified
version of the library, if the user installs one, as long as the
modified version is interface-compatible with the version that the
work was made with.</li>
<li><strong>c)</strong> Accompany the work with a written offer,
valid for at least three years, to give the same user the
materials specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.</li>
<li><strong>d)</strong> If distribution of the work is made by
offering access to copy from a designated place, offer equivalent
access to copy the above specified materials from the same
place.</li>
<li><strong>e)</strong> Verify that the user has already received
a copy of these materials or that you have already sent this user
a copy.</li>
</ul>
<p>
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
</p>
<p>
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
</p>
<p>
<strong>7.</strong> You may place library facilities that are a work
based on the Library side-by-side in a single library together with
other library facilities not covered by this License, and distribute
such a combined library, provided that the separate distribution of
the work based on the Library and of the other library facilities is
otherwise permitted, and provided that you do these two things:
</p>
<ul>
<li><strong>a)</strong> Accompany the combined library with a copy
of the same work based on the Library, uncombined with any other
library facilities. This must be distributed under the terms of
the Sections above.</li>
<li><strong>b)</strong> Give prominent notice with the combined
library of the fact that part of it is a work based on the
Library, and explaining where to find the accompanying uncombined
form of the same work.</li>
</ul>
<p>
<strong>8.</strong> You may not copy, modify, sublicense, link with,
or distribute the Library except as expressly provided under this
License. Any attempt otherwise to copy, modify, sublicense, link
with, or distribute the Library is void, and will automatically
terminate your rights under this License. However, parties who have
received copies, or rights, from you under this License will not have
their licenses terminated so long as such parties remain in full
compliance.
</p>
<p>
<strong>9.</strong>
You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
</p>
<p>
<strong>10.</strong>
Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
</p>
<p>
<strong>11.</strong>
If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
</p>
<p>
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
</p>
<p>
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
</p>
<p>
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
</p>
<p>
<strong>12.</strong>
If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
</p>
<p>
<strong>13.</strong>
The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
</p>
<p>
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
</p>
<p>
<strong>14.</strong>
If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
</p>
<p>
<strong>NO WARRANTY</strong>
</p>
<p>
<strong>15.</strong>
BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
</p>
<p>
<strong>16.</strong>
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
</p>
<h3>END OF TERMS AND CONDITIONS</h3>
</body>
</html>

BIN
Documentation/Media/NamespaceDependencies.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
Documentation/Media/RenderingPipeline.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
Documentation/Media/VisualTree.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
Documentation/Media/WelcomeScreenshot.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

78
Documentation/Sample Application.aml

@ -0,0 +1,78 @@ @@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<topic id="70c4df51-5ecb-4e24-a574-8c5a84306bd1" revisionNumber="1">
<developerSampleDocument xmlns="http://ddue.schemas.microsoft.com/authoring/2003/5" xmlns:xlink="http://www.w3.org/1999/xlink">
<!--
<summary>
<para>Optional summary abstract</para>
</summary>
-->
<introduction>
<!-- Uncomment this to generate an outline of the section and sub-section
titles. Specify a numeric value as the inner text to limit it to
a specific number of sub-topics when creating the outline. Specify
zero (0) to limit it to top-level sections only. -->
<!-- <autoOutline /> -->
<para>In the SharpDevelop source code download, you will find a small sample
application in SharpDevelop\samples\AvalonEdit.Sample.</para>
</introduction>
<mediaLink><image xlink:href="WelcomeScreenshot" placement="center"/></mediaLink>
<!-- <procedure>Optional procedures. See How To document for procedure layout example.</procedure> -->
<!-- <requirements>Optional requirements section</requirements> -->
<!-- <demonstrates>Optional info about what is demonstrated</demonstrates> -->
<!-- <codeExample>Optional code example</codeExample> -->
<!-- Add one or more top-level section elements. These are collapsible.
If using <autoOutline />, add an address attribute to identify it
and specify a title so that it can be jumped to with a hyperlink. -->
<section>
<title>The Code Project article</title>
<content>
<para>
There is a Code Project article based on the sample application:
<externalLink>
<linkText>http://www.codeproject.com/KB/edit/AvalonEdit.aspx</linkText>
<linkUri>http://www.codeproject.com/KB/edit/AvalonEdit.aspx</linkUri>
<linkTarget>_blank</linkTarget>
</externalLink>
</para>
<para>
However, most of the material from that article has been included in this help file.
</para>
</content>
</section>
<relatedTopics>
<!-- One or more of the following:
- A local link
- An external link
- A code entity reference
<link xlink:href="Other Topic's ID">Link text</link>
<externalLink>
<linkText>Link text</linkText>
<linkAlternateText>Optional alternate link text</linkAlternateText>
<linkUri>URI</linkUri>
</externalLink>
<codeEntityReference>API member ID</codeEntityReference>
Examples:
<link xlink:href="00e97994-e9e6-46e0-b420-5be86b2f8278">Some other topic</link>
<externalLink>
<linkText>SHFB on CodePlex</linkText>
<linkAlternateText>Go to CodePlex</linkAlternateText>
<linkUri>http://shfb.codeplex.com</linkUri>
</externalLink>
<codeEntityReference>T:TestDoc.TestClass</codeEntityReference>
<codeEntityReference>P:TestDoc.TestClass.SomeProperty</codeEntityReference>
<codeEntityReference>M:TestDoc.TestClass.#ctor</codeEntityReference>
<codeEntityReference>M:TestDoc.TestClass.#ctor(System.String,System.Int32)</codeEntityReference>
<codeEntityReference>M:TestDoc.TestClass.ToString</codeEntityReference>
<codeEntityReference>M:TestDoc.TestClass.FirstMethod</codeEntityReference>
<codeEntityReference>M:TestDoc.TestClass.SecondMethod(System.Int32,System.String)</codeEntityReference>
-->
</relatedTopics>
</developerSampleDocument>
</topic>

234
Documentation/Syntax Highlighting.aml

@ -0,0 +1,234 @@ @@ -0,0 +1,234 @@
<?xml version="1.0" encoding="utf-8"?>
<topic id="4d4ceb51-154d-43f0-b876-ad9640c5d2d8" revisionNumber="1">
<developerConceptualDocument xmlns="http://ddue.schemas.microsoft.com/authoring/2003/5" xmlns:xlink="http://www.w3.org/1999/xlink">
<introduction>
<para>Probably the most important feature for any text editor is syntax highlighting.</para>
<para>AvalonEdit has a flexible text rendering model, see
<link xlink:href="c06e9832-9ef0-4d65-ac2e-11f7ce9c7774" />. Among the
text rendering extension points is the support for "visual line transformers" that
can change the display of a visual line after it has been constructed by the "visual element generators".
A useful base class implementing IVisualLineTransformer for the purpose of syntax highlighting
is <codeEntityReference>T:ICSharpCode.AvalonEdit.Rendering.DocumentColorizingTransformer</codeEntityReference>.
Take a look at that class' documentation to see
how to write fully custom syntax highlighters. This article only discusses the XML-driven built-in
highlighting engine.
</para>
</introduction>
<section>
<title>The highlighting engine</title>
<content>
<para>
The highlighting engine in AvalonEdit is implemented in the class
<codeEntityReference>T:ICSharpCode.AvalonEdit.Highlighting.DocumentHighlighter</codeEntityReference>.
Highlighting is the process of taking a DocumentLine and constructing
a <codeEntityReference>T:ICSharpCode.AvalonEdit.Highlighting.HighlightedLine</codeEntityReference>
instance for it by assigning colors to different sections of the line.
A <codeInline>HighlightedLine</codeInline> is simply a list of
(possibly nested) highlighted text sections.
</para><para>
The <codeInline>HighlightingColorizer</codeInline> class is the only
link between highlighting and rendering.
It uses a <codeInline>DocumentHighlighter</codeInline> to implement
a line transformer that applies the
highlighting to the visual lines in the rendering process.
</para><para>
Except for this single call, syntax highlighting is independent from the
rendering namespace. To help with other potential uses of the highlighting
engine, the <codeInline>HighlightedLine</codeInline> class has the
method <codeInline>ToHtml()</codeInline>
to produce syntax highlighted HTML source code.
</para>
<para>The highlighting rules used by the highlighting engine to highlight
the document are described by the following classes:
</para>
<definitionTable>
<definedTerm>HighlightingRuleSet</definedTerm>
<definition>Describes a set of highlighting spans and rules.</definition>
<definedTerm>HighlightingSpan</definedTerm>
<definition>A span consists of two regular expressions (Start and End), a color,
and a child ruleset.
The region between Start and End expressions will be assigned the
given color, and inside that span, the rules of the child
ruleset apply.
If the child ruleset also has <codeInline>HighlightingSpan</codeInline>s,
they can be nested, allowing highlighting constructs like nested comments or one language
embedded in another.</definition>
<definedTerm>HighlightingRule</definedTerm>
<definition>A highlighting rule is a regular expression with a color.
It will highlight matches of the regular expression using that color.</definition>
<definedTerm>HighlightingColor</definedTerm>
<definition>A highlighting color isn't just a color: it consists of a foreground
color, font weight and font style.</definition>
</definitionTable>
<para>
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.
</para><para>
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.
</para><para>
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
(<codeInline>.</codeInline> matches any character).
This ensures that <codeInline>\"</codeInline> does not denote the end of the string span;
but <codeInline>\\"</codeInline> still does.
</para><para>
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.
</para><para>
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.
</para><para>
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.
</para><para>
Incrementally means that even if the document is changed, the stored span stacks
will be reused as far as possible. If the user types <codeInline>/*</codeInline>,
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 <codeInline>*/</codeInline> 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
<codeInline>/*</codeInline>.
</para><para>
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
(<codeEntityReference>T:ICSharpCode.AvalonEdit.Utils.CompressingTreeList`1</codeEntityReference>).
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 <codeInline>//</codeInline> or <codeInline>///</codeInline>
are more popular than <codeInline>/* */</codeInline> comments.
</para>
</content>
</section>
<section>
<title>XML highlighting definitions</title>
<content>
<para>AvalonEdit supports XML syntax highlighting definitions (.xshd files).</para>
<para>In the AvalonEdit source code, you can find the file
<codeInline>ICSharpCode.AvalonEdit\Highlighting\Resources\ModeV2.xsd</codeInline>.
This is an XML schema for the .xshd file format; you can use it to
code completion for .xshd files in XML editors.
</para>
<para>Here is an example highlighting definition for a sub-set of C#:
<code language="xml"><![CDATA[
<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>
]]></code>
</para>
</content>
</section>
<section>
<title>ICSharpCode.TextEditor XML highlighting definitions</title>
<content>
<para>ICSharpCode.TextEditor (the predecessor of AvalonEdit) used
a different version of the XSHD file format.
AvalonEdit detects the difference between the formats using the XML namespace:
The new format uses <codeInline>xmlns="http://icsharpcode.net/sharpdevelop/syntaxdefinition/2008"</codeInline>,
the old format does not use any XML namespace.
</para><para>
AvalonEdit can load .xshd files written in that old format, and even
automatically convert them to the new format. However, not all
constructs of the old file format are supported by AvalonEdit.
</para>
<code language="cs"><![CDATA[// convert from old .xshd format to new format
XshdSyntaxDefinition xshd;
using (XmlTextReader reader = new XmlTextReader("input.xshd")) {
xshd = HighlightingLoader.LoadXshd(reader);
}
using (XmlTextWriter writer = new XmlTextWriter("output.xshd", System.Text.Encoding.UTF8)) {
writer.Formatting = Formatting.Indented;
new SaveXshdVisitor(writer).WriteDefinition(xshd);
}
]]></code>
</content>
</section>
<section>
<title>Programmatically accessing highlighting information</title>
<content>
<para>As described above, the highlighting engine only stores the "span stack"
at the start of each line. This information can be retrieved using the
<codeEntityReference>M:ICSharpCode.AvalonEdit.Highlighting.DocumentHighlighter.GetSpanStack(System.Int32)</codeEntityReference>
method:
<code language="cs"><![CDATA[bool isInComment = documentHighlighter.GetSpanStack(1).Any(
s => s.SpanColor != null && s.SpanColor.Name == "Comment");
// returns true if the end of line 1 (=start of line 2) is inside a multiline comment]]></code>
Spans can be identified using their color. For this purpose, named colors should be used in the syntax definition.
</para>
<para>For more detailed results inside lines, the highlighting algorithm
must be executed for that line:
<code language="cs"><![CDATA[int off = document.GetOffset(7, 22);
HighlightedLine result = documentHighlighter.HighlightLine(document.GetLineByNumber(7));
bool isInComment = result.Sections.Any(
s => s.Offset <= off && s.Offset+s.Length >= off
&& s.Color.Name == "Comment");]]></code>
</para>
</content>
</section>
<relatedTopics>
<codeEntityReference>N:ICSharpCode.AvalonEdit.Highlighting</codeEntityReference>
</relatedTopics>
</developerConceptualDocument>
</topic>

171
Documentation/Text Rendering.aml

@ -0,0 +1,171 @@ @@ -0,0 +1,171 @@
<?xml version="1.0" encoding="utf-8"?>
<topic id="c06e9832-9ef0-4d65-ac2e-11f7ce9c7774" revisionNumber="1">
<developerConceptualDocument xmlns="http://ddue.schemas.microsoft.com/authoring/2003/5" xmlns:xlink="http://www.w3.org/1999/xlink">
<summary>
<para>This document describes how the TextView class renders the text, and
how you can extend the text rendering process to add new features to the text editor.
</para>
</summary>
<introduction>
<para>The <codeEntityReference qualifyHint="true">T:ICSharpCode.AvalonEdit.Rendering.TextView</codeEntityReference>
class is the heart of AvalonEdit.
It takes care of getting the document onto the screen.</para>
<para>To do this in an extensible way, the TextView uses its own kind of model:
the <codeEntityReference>T:ICSharpCode.AvalonEdit.Rendering.VisualLine</codeEntityReference>.
Visual lines are created only for the visible part of the document.</para>
<para>
The rendering process looks like this:
<mediaLink><image xlink:href="RenderingPipeline" placement="center"/></mediaLink>
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.
</para>
</introduction>
<!-- Add one or more top-level section elements. These are collapsible.
If using <autoOutline /> tag, add an address attribute to identify
it so that it can be jumped to with a hyperlink. -->
<section>
<title>Lifetime of VisualLines</title>
<content>
<para>
VisualLines are only created for the visible part of the document.
Lots of actions can trigger their creation, but most commonly the creation will be
caused by the MeasureOverride method of TextView.
When the TextView is measured, it uses the height tree to determine the first
document line in the visible region. Then, it constructs and measures a VisualLine
for that first line, and repeats that with the following lines
until the visible region is filled.
</para>
<para>
The TextView caches VisualLines - when the user scrolls down, only the VisualLines
coming into view are created, the rest is reused.
The VisualLine cache can be manually invalidated using the Redraw method family;
moreover, lots of actions cause automatic invalidation:
<list class="bullet"><listItem>any change in the document will invalidate the affected VisualLines</listItem><listItem>changing the width of the TextView invalidates all VisualLines if word-wrap is enabled</listItem><listItem>changing any text editor settings (word-wrap, font size, etc.) will invalidate all VisualLines</listItem><listItem>VisualLines leaving the visible area after scrolling will be disposed</listItem></list>
In general, manual invalidation is required only if you have written a text editor extension
(BackgroundRenderer, VisualLineElementGenerator or VisualLineTransformer) that also consumes
external data - in that case, you'll have to notify the text editor that VisualLines need
to be recreated when your external data changes.
</para>
<alert class="note">
<para>If external data used by your text editor extension changes, call
<codeEntityReference qualifyHint="true">M:ICSharpCode.AvalonEdit.Rendering.TextView.Redraw</codeEntityReference>
to invalidate the VisualLine.
</para>
</alert>
<para>
Invalidating VisualLines does not cause immediate recreation of the lines!
Rather, the VisualLines are recreated when the text view is next re-measured.
While measurement in WPF normally happens with DispatcherPriority.Render,
the TextView also supports redrawing with a lower priority than that.
For example, normal text insertion causes a redraw at background priority, so that
in case the user types faster than the text view can redraw, there will be only
one redraw for multiple input actions.
</para>
<alert class="note">
<para>
The TextView will never return invalid lines to you, but you
may run into the case that the valid visual lines are not available.
</para>
<para>
What happens in this case depends on the method you are calling -
the new visual line might get created automatically,
null could be returned, or you may get a
<codeEntityReference>T:ICSharpCode.AvalonEdit.Rendering.VisualLinesInvalidException</codeEntityReference>.
</para>
<para>
You can call
<codeEntityReference qualifyHint="true">M:ICSharpCode.AvalonEdit.Rendering.TextView.EnsureVisualLines</codeEntityReference>
to make the text view create all VisualLines in the visible region.
</para>
</alert>
</content>
</section>
<section>
<title>Building visual line elements</title>
<content>
<para>
As a first step, the VisualLineElementGenerators are used to produce elements. The
room in between the elements returned from the generators is filled with text elements.
Then, the VisualLine assigns the VisualColumn and RelativeTextOffset properties of the line elements.
</para>
<para>
For example, a line contains the text "Hello, World".
The user has enabled "ShowSpaces", so the text editor should show a little dot instead of the space.
In this case, the SingleCharacterElementGenerator, which is responsible for ShowSpaces, will produce
a "SpaceTextElement" for the space character. Because no other generators are interested in the line,
the remaining strings "Hello," and "World" will be represented by VisualLineText elements.
</para>
</content>
</section>
<section>
<title>Transforming visual line elements</title>
<content>
<para>
After that, the IVisualLineTransformers are used to modify the produced line elements. Transformers
must not add elements, but they may split existing elements, e.g. to colorize only parts of an
element. When splitting elements (or somehow modifying the elements collection), care must be taken
that the VisualColumn,VisualLine,RelativeTextOffset and DocumentLength properties stay correct.
</para>
<para>
The ColorizingTransformer base class provides helper methods for splitting, so the derived class
can simply say "color this section in that color".
</para>
<para>
The DocumentColorizingTransformer extends the ColorizingTransformer and additionally
allows highlighting on per DocumentLine, coloring text segments (instead of directly
working with visual line elements).
</para>
</content>
</section>
<section>
<title>Constructing TextLines</title>
<content>
<para>
After building the visual line elements, the TextLines for the visual line are constructed.
A visual line may result in multiple text lines when word wrapping is active or when special visual
line elements force a line break.
Building text lines:
The text line construction is done by a WPF TextFormatter.
The VisualLineTextSource will take the visual line elements and build WPF TextRuns from it,
while the WPF TextFormatter takes care of word wrapping etc.
VisualLineElements are requested to produce TextRuns for their full or a partial length.
The TextView will take care to measure any inline UI elements in the visual lines.
</para>
</content>
</section>
<section>
<title>Rest of the Rendering</title>
<content>
<para>
After the visible region is filled, the TextView updates the heights stored in the document lines to
the measured heights. This way, scrolling takes account for word-wrapping.
The constructed text lines are stored for the arrange and render steps.
Now, finally, the measure step is complete.
</para>
<para>
The WPF arrange step doesn't have much work to do:
It just arranges inline UI elements at their position inside the text.
</para>
<para>
The actual rendering does not happen directly in the TextView, but in its
various layers.
</para>
<para>
These are the predefined layers:
<list class="bullet"><listItem>Background layer: renders the background colors associated with the visual elements</listItem><listItem>Selection layer: renders the background of the selection</listItem><listItem>Text layer: renders the TextLines that were constructed during the Measure step.
The text layer also serves as container for any inline UI elements.
</listItem><listItem>Caret layer: renders a blinking caret</listItem></list>
It's also possible to insert new layers into the TextView using the
<codeEntityReference qualifyHint="true">M:ICSharpCode.AvalonEdit.Rendering.TextView.InsertLayer(System.Windows.UIElement,ICSharpCode.AvalonEdit.Rendering.KnownLayer,ICSharpCode.AvalonEdit.Rendering.LayerInsertionPosition)</codeEntityReference>
method.
This allows adding custom interactive components to the editor
while being in full control of the Z-Order.
</para>
</content>
</section>
<relatedTopics>
<codeEntityReference>T:ICSharpCode.AvalonEdit.Rendering.TextView</codeEntityReference>
</relatedTopics>
</developerConceptualDocument>
</topic>

70
Documentation/Welcome.aml

@ -0,0 +1,70 @@ @@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<topic id="c52241ea-3eba-4ddf-b463-6349cbff38fd" revisionNumber="1">
<developerConceptualDocument xmlns="http://ddue.schemas.microsoft.com/authoring/2003/5" xmlns:xlink="http://www.w3.org/1999/xlink">
<summary>
<para>AvalonEdit is a WPF-based extensible text editor.</para>
</summary>
<introduction>
<para>While the WPF RichTextBox is quite powerful, you quickly run into its limits
when trying to use it as a code editor: it's hard to write efficient syntax highlighting for it,
and you cannot really implement features like code folding with the standard RichTextBox.</para>
<para>The problem is: the RichTextBox edits a rich document.
In contrast, AvalonEdit simply edits text.</para>
<para>However, AvalonEdit offers lots of possibilities on how the text document is
displayed - so it is much more suitable for a code editor where things like the text color
are not controlled by the user, but instead depend on the text (syntax highlighting).
</para>
<para>
AvalonEdit was written for the SharpDevelop IDE. It replaces our old
Windows Forms-based text editor (ICSharpCode.TextEditor).
</para>
</introduction>
<mediaLink><image xlink:href="WelcomeScreenshot" placement="center"/></mediaLink>
<section>
<title>Usage</title>
<content>
<para>The main class of the editor is <codeEntityReference qualifyHint="true">T:ICSharpCode.AvalonEdit.TextEditor</codeEntityReference>.
You can use it similar to a normal WPF TextBox:</para>
<code language="xml"><![CDATA[
<avalonEdit:TextEditor
xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit"
Name="textEditor"
SyntaxHighlighting="C#"
FontFamily="Consolas"
FontSize="10pt"/>
]]></code>
</content>
</section>
<section>
<title>System requirements</title>
<content>
<para>AvalonEdit requires the
<externalLink>
<linkText>.NET Framework 3.5 SP1</linkText>
<linkUri>http://www.microsoft.com/downloads/details.aspx?FamilyID=ab99342f-5d1a-413d-8319-81da479ab0d7&amp;DisplayLang=en</linkUri>
<linkTarget>_blank</linkTarget>
</externalLink>.
For compiling AvalonEdit inside Visual Studio 2008, VS08 SP1 is required.
</para>
<para>AvalonEdit requires FullTrust and will not run as XBAP.</para>
</content>
</section>
<relatedTopics>
<codeEntityReference qualifyHint="true">T:ICSharpCode.AvalonEdit.TextEditor</codeEntityReference>
<externalLink>
<linkText>www.avalonedit.net</linkText>
<linkUri>http://www.avalonedit.net</linkUri>
<linkTarget>_blank</linkTarget>
</externalLink>
<externalLink>
<linkText>www.icsharpcode.net</linkText>
<linkUri>http://www.icsharpcode.net</linkUri>
<linkTarget>_blank</linkTarget>
</externalLink>
</relatedTopics>
</developerConceptualDocument>
</topic>

64
ICSharpCode.AvalonEdit.Tests/Document/ChangeTrackingTest.cs

@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
// 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.Linq;
using NUnit.Framework;
namespace ICSharpCode.AvalonEdit.Document
{
[TestFixture]
public class ChangeTrackingTest
{
[Test]
public void NoChanges()
{
TextDocument document = new TextDocument("initial text");
ChangeTrackingCheckpoint checkpoint1, checkpoint2;
ITextSource snapshot1 = document.CreateSnapshot(out checkpoint1);
ITextSource snapshot2 = document.CreateSnapshot(out checkpoint2);
Assert.AreEqual(0, checkpoint1.CompareAge(checkpoint2));
Assert.AreEqual(0, checkpoint1.GetChangesTo(checkpoint2).Count());
Assert.AreEqual(document.Text, snapshot1.Text);
Assert.AreEqual(document.Text, snapshot2.Text);
}
[Test]
public void ForwardChanges()
{
TextDocument document = new TextDocument("initial text");
ChangeTrackingCheckpoint checkpoint1, checkpoint2;
ITextSource snapshot1 = document.CreateSnapshot(out checkpoint1);
document.Replace(0, 7, "nw");
document.Insert(1, "e");
ITextSource snapshot2 = document.CreateSnapshot(out checkpoint2);
Assert.AreEqual(-1, checkpoint1.CompareAge(checkpoint2));
DocumentChangeEventArgs[] arr = checkpoint1.GetChangesTo(checkpoint2).ToArray();
Assert.AreEqual(2, arr.Length);
Assert.AreEqual("nw", arr[0].InsertedText);
Assert.AreEqual("e", arr[1].InsertedText);
Assert.AreEqual("initial text", snapshot1.Text);
Assert.AreEqual("new text", snapshot2.Text);
}
[Test]
public void BackwardChanges()
{
TextDocument document = new TextDocument("initial text");
ChangeTrackingCheckpoint checkpoint1, checkpoint2;
ITextSource snapshot1 = document.CreateSnapshot(out checkpoint1);
document.Replace(0, 7, "nw");
document.Insert(1, "e");
ITextSource snapshot2 = document.CreateSnapshot(out checkpoint2);
Assert.AreEqual(1, checkpoint2.CompareAge(checkpoint1));
DocumentChangeEventArgs[] arr = checkpoint2.GetChangesTo(checkpoint1).ToArray();
Assert.AreEqual(2, arr.Length);
Assert.AreEqual("", arr[0].InsertedText);
Assert.AreEqual("initial", arr[1].InsertedText);
Assert.AreEqual("initial text", snapshot1.Text);
Assert.AreEqual("new text", snapshot2.Text);
}
}
}

154
ICSharpCode.AvalonEdit.Tests/Document/CollapsingTests.cs

@ -0,0 +1,154 @@ @@ -0,0 +1,154 @@
// 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 ICSharpCode.AvalonEdit.Rendering;
using NUnit.Framework;
namespace ICSharpCode.AvalonEdit.Document
{
[TestFixture]
public class CollapsingTests
{
TextDocument document;
HeightTree heightTree;
[SetUp]
public void Setup()
{
document = new TextDocument();
document.Text = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10";
heightTree = new HeightTree(document, 10);
foreach (DocumentLine line in document.Lines) {
heightTree.SetHeight(line, line.LineNumber);
}
}
CollapsedLineSection SimpleCheck(int from, int to)
{
CollapsedLineSection sec1 = heightTree.CollapseText(document.GetLineByNumber(from), document.GetLineByNumber(to));
for (int i = 1; i < from; i++) {
Assert.IsFalse(heightTree.GetIsCollapsed(i));
}
for (int i = from; i <= to; i++) {
Assert.IsTrue(heightTree.GetIsCollapsed(i));
}
for (int i = to + 1; i <= 10; i++) {
Assert.IsFalse(heightTree.GetIsCollapsed(i));
}
CheckHeights();
return sec1;
}
[Test]
public void SimpleCheck()
{
SimpleCheck(4, 6);
}
[Test]
public void SimpleUncollapse()
{
CollapsedLineSection sec1 = heightTree.CollapseText(document.GetLineByNumber(4), document.GetLineByNumber(6));
sec1.Uncollapse();
for (int i = 1; i <= 10; i++) {
Assert.IsFalse(heightTree.GetIsCollapsed(i));
}
CheckHeights();
}
[Test]
public void FullCheck()
{
for (int from = 1; from <= 10; from++) {
for (int to = from; to <= 10; to++) {
try {
SimpleCheck(from, to).Uncollapse();
for (int i = 1; i <= 10; i++) {
Assert.IsFalse(heightTree.GetIsCollapsed(i));
}
CheckHeights();
} catch {
Console.WriteLine("from = " + from + ", to = " + to);
throw;
}
}
}
}
[Test]
public void InsertInCollapsedSection()
{
CollapsedLineSection sec1 = heightTree.CollapseText(document.GetLineByNumber(4), document.GetLineByNumber(6));
document.Insert(document.GetLineByNumber(5).Offset, "a\nb\nc");
for (int i = 1; i < 4; i++) {
Assert.IsFalse(heightTree.GetIsCollapsed(i));
}
for (int i = 4; i <= 8; i++) {
Assert.IsTrue(heightTree.GetIsCollapsed(i));
}
for (int i = 9; i <= 12; i++) {
Assert.IsFalse(heightTree.GetIsCollapsed(i));
}
CheckHeights();
}
[Test]
public void RemoveInCollapsedSection()
{
CollapsedLineSection sec1 = heightTree.CollapseText(document.GetLineByNumber(3), document.GetLineByNumber(7));
int line4Offset = document.GetLineByNumber(4).Offset;
int line6Offset = document.GetLineByNumber(6).Offset;
document.Remove(line4Offset, line6Offset - line4Offset);
for (int i = 1; i < 3; i++) {
Assert.IsFalse(heightTree.GetIsCollapsed(i));
}
for (int i = 3; i <= 5; i++) {
Assert.IsTrue(heightTree.GetIsCollapsed(i));
}
for (int i = 6; i <= 8; i++) {
Assert.IsFalse(heightTree.GetIsCollapsed(i));
}
CheckHeights();
}
[Test]
public void RemoveEndOfCollapsedSection()
{
CollapsedLineSection sec1 = heightTree.CollapseText(document.GetLineByNumber(3), document.GetLineByNumber(6));
int line5Offset = document.GetLineByNumber(5).Offset;
int line8Offset = document.GetLineByNumber(8).Offset;
document.Remove(line5Offset, line8Offset - line5Offset);
for (int i = 1; i < 3; i++) {
Assert.IsFalse(heightTree.GetIsCollapsed(i));
}
for (int i = 3; i <= 5; i++) {
Assert.IsTrue(heightTree.GetIsCollapsed(i));
}
for (int i = 6; i <= 7; i++) {
Assert.IsFalse(heightTree.GetIsCollapsed(i));
}
CheckHeights();
}
[Test]
public void RemoveCollapsedSection()
{
CollapsedLineSection sec1 = heightTree.CollapseText(document.GetLineByNumber(3), document.GetLineByNumber(3));
int line3Offset = document.GetLineByNumber(3).Offset;
document.Remove(line3Offset - 1, 1);
for (int i = 1; i <= 9; i++) {
Assert.IsFalse(heightTree.GetIsCollapsed(i));
}
CheckHeights();
Assert.AreSame(null, sec1.Start);
Assert.AreSame(null, sec1.End);
Assert.IsTrue(sec1.IsCollapsed);
}
void CheckHeights()
{
HeightTests.CheckHeights(document, heightTree);
}
}
}

76
ICSharpCode.AvalonEdit.Tests/Document/HeightTests.cs

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
// 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.Linq;
using ICSharpCode.AvalonEdit.Rendering;
using NUnit.Framework;
namespace ICSharpCode.AvalonEdit.Document
{
[TestFixture]
public class HeightTests
{
TextDocument document;
HeightTree heightTree;
[SetUp]
public void Setup()
{
document = new TextDocument();
document.Text = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10";
heightTree = new HeightTree(document, 10);
foreach (DocumentLine line in document.Lines) {
heightTree.SetHeight(line, line.LineNumber);
}
}
[Test]
public void SimpleCheck()
{
CheckHeights();
}
[Test]
public void TestLinesRemoved()
{
document.Remove(5, 4);
CheckHeights();
}
[Test]
public void TestHeightChanged()
{
heightTree.SetHeight(document.GetLineByNumber(4), 100);
CheckHeights();
}
[Test]
public void TestLinesInserted()
{
document.Insert(0, "x\ny\n");
heightTree.SetHeight(document.Lines[0], 100);
heightTree.SetHeight(document.Lines[1], 1000);
heightTree.SetHeight(document.Lines[2], 10000);
CheckHeights();
}
void CheckHeights()
{
CheckHeights(document, heightTree);
}
internal static void CheckHeights(TextDocument document, HeightTree heightTree)
{
double[] heights = document.Lines.Select(l => heightTree.GetIsCollapsed(l.LineNumber) ? 0 : heightTree.GetHeight(l)).ToArray();
double[] visualPositions = new double[document.LineCount+1];
for (int i = 0; i < heights.Length; i++) {
visualPositions[i+1]=visualPositions[i]+heights[i];
}
foreach (DocumentLine ls in document.Lines) {
Assert.AreEqual(visualPositions[ls.LineNumber-1], heightTree.GetVisualPosition(ls));
}
Assert.AreEqual(visualPositions[document.LineCount], heightTree.TotalHeight);
}
}
}

531
ICSharpCode.AvalonEdit.Tests/Document/LineManagerTests.cs

@ -0,0 +1,531 @@ @@ -0,0 +1,531 @@
// 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 NUnit.Framework;
namespace ICSharpCode.AvalonEdit.Document
{
[TestFixture]
public class LineManagerTests
{
TextDocument document;
[SetUp]
public void SetUp()
{
document = new TextDocument();
}
[Test]
public void CheckEmptyDocument()
{
Assert.AreEqual("", document.Text);
Assert.AreEqual(0, document.TextLength);
Assert.AreEqual(1, document.LineCount);
}
[Test]
public void CheckClearingDocument()
{
document.Text = "Hello,\nWorld!";
Assert.AreEqual(2, document.LineCount);
document.Text = "";
Assert.AreEqual("", document.Text);
Assert.AreEqual(0, document.TextLength);
Assert.AreEqual(1, document.LineCount);
}
[Test]
public void CheckGetLineInEmptyDocument()
{
Assert.AreEqual(1, document.Lines.Count);
List<DocumentLine> lines = new List<DocumentLine>(document.Lines);
Assert.AreEqual(1, lines.Count);
DocumentLine line = document.Lines[0];
Assert.AreSame(line, lines[0]);
Assert.AreSame(line, document.GetLineByNumber(1));
Assert.AreSame(line, document.GetLineByOffset(0));
}
[Test]
public void CheckLineSegmentInEmptyDocument()
{
DocumentLine line = document.GetLineByNumber(1);
Assert.AreEqual(1, line.LineNumber);
Assert.AreEqual(0, line.Offset);
Assert.IsFalse(line.IsDeleted);
Assert.AreEqual(0, line.Length);
Assert.AreEqual(0, line.TotalLength);
Assert.AreEqual(0, line.DelimiterLength);
}
[Test]
public void LineIndexOfTest()
{
DocumentLine line = document.GetLineByNumber(1);
Assert.AreEqual(0, document.Lines.IndexOf(line));
DocumentLine lineFromOtherDocument = new TextDocument().GetLineByNumber(1);
Assert.AreEqual(-1, document.Lines.IndexOf(lineFromOtherDocument));
document.Text = "a\nb\nc";
DocumentLine middleLine = document.GetLineByNumber(2);
Assert.AreEqual(1, document.Lines.IndexOf(middleLine));
document.Remove(1, 3);
Assert.IsTrue(middleLine.IsDeleted);
Assert.AreEqual(-1, document.Lines.IndexOf(middleLine));
}
[Test]
public void InsertInEmptyDocument()
{
document.Insert(0, "a");
Assert.AreEqual(document.LineCount, 1);
DocumentLine line = document.GetLineByNumber(1);
Assert.AreEqual("a", document.GetText(line));
}
[Test]
public void SetText()
{
document.Text = "a";
Assert.AreEqual(document.LineCount, 1);
DocumentLine line = document.GetLineByNumber(1);
Assert.AreEqual("a", document.GetText(line));
}
[Test]
public void InsertNothing()
{
document.Insert(0, "");
Assert.AreEqual(document.LineCount, 1);
Assert.AreEqual(document.TextLength, 0);
}
[Test, ExpectedException(typeof(ArgumentNullException))]
public void InsertNull()
{
document.Insert(0, null);
}
[Test, ExpectedException(typeof(ArgumentNullException))]
public void SetTextNull()
{
document.Text = null;
}
[Test]
public void RemoveNothing()
{
document.Remove(0, 0);
Assert.AreEqual(document.LineCount, 1);
Assert.AreEqual(document.TextLength, 0);
}
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void GetCharAt0EmptyDocument()
{
document.GetCharAt(0);
}
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void GetCharAtNegativeOffset()
{
document.Text = "a\nb";
document.GetCharAt(-1);
}
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void GetCharAtEndOffset()
{
document.Text = "a\nb";
document.GetCharAt(document.TextLength);
}
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void InsertAtNegativeOffset()
{
document.Text = "a\nb";
document.Insert(-1, "text");
}
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void InsertAfterEndOffset()
{
document.Text = "a\nb";
document.Insert(4, "text");
}
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void RemoveNegativeAmount()
{
document.Text = "abcd";
document.Remove(2, -1);
}
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void RemoveTooMuch()
{
document.Text = "abcd";
document.Remove(2, 10);
}
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void GetLineByNumberNegative()
{
document.Text = "a\nb";
document.GetLineByNumber(-1);
}
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void GetLineByNumberTooHigh()
{
document.Text = "a\nb";
document.GetLineByNumber(3);
}
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void GetLineByOffsetNegative()
{
document.Text = "a\nb";
document.GetLineByOffset(-1);
}
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void GetLineByOffsetToHigh()
{
document.Text = "a\nb";
document.GetLineByOffset(10);
}
[Test]
public void InsertAtEndOffset()
{
document.Text = "a\nb";
CheckDocumentLines("a",
"b");
document.Insert(3, "text");
CheckDocumentLines("a",
"btext");
}
[Test]
public void GetCharAt()
{
document.Text = "a\r\nb";
Assert.AreEqual('a', document.GetCharAt(0));
Assert.AreEqual('\r', document.GetCharAt(1));
Assert.AreEqual('\n', document.GetCharAt(2));
Assert.AreEqual('b', document.GetCharAt(3));
}
[Test]
public void CheckMixedNewLineTest()
{
const string mixedNewlineText = "line 1\nline 2\r\nline 3\rline 4";
document.Text = mixedNewlineText;
Assert.AreEqual(mixedNewlineText, document.Text);
Assert.AreEqual(4, document.LineCount);
for (int i = 1; i < 4; i++) {
DocumentLine line = document.GetLineByNumber(i);
Assert.AreEqual(i, line.LineNumber);
Assert.AreEqual("line " + i, document.GetText(line));
}
Assert.AreEqual(1, document.GetLineByNumber(1).DelimiterLength);
Assert.AreEqual(2, document.GetLineByNumber(2).DelimiterLength);
Assert.AreEqual(1, document.GetLineByNumber(3).DelimiterLength);
Assert.AreEqual(0, document.GetLineByNumber(4).DelimiterLength);
}
[Test]
public void LfCrIsTwoNewLinesTest()
{
document.Text = "a\n\rb";
Assert.AreEqual("a\n\rb", document.Text);
CheckDocumentLines("a",
"",
"b");
}
[Test]
public void RemoveFirstPartOfDelimiter()
{
document.Text = "a\r\nb";
document.Remove(1, 1);
Assert.AreEqual("a\nb", document.Text);
CheckDocumentLines("a",
"b");
}
[Test]
public void RemoveLineContentAndJoinDelimiters()
{
document.Text = "a\rb\nc";
document.Remove(2, 1);
Assert.AreEqual("a\r\nc", document.Text);
CheckDocumentLines("a",
"c");
}
[Test]
public void RemoveLineContentAndJoinDelimiters2()
{
document.Text = "a\rb\nc\nd";
document.Remove(2, 3);
Assert.AreEqual("a\r\nd", document.Text);
CheckDocumentLines("a",
"d");
}
[Test]
public void RemoveLineContentAndJoinDelimiters3()
{
document.Text = "a\rb\r\nc";
document.Remove(2, 2);
Assert.AreEqual("a\r\nc", document.Text);
CheckDocumentLines("a",
"c");
}
[Test]
public void RemoveLineContentAndJoinNonMatchingDelimiters()
{
document.Text = "a\nb\nc";
document.Remove(2, 1);
Assert.AreEqual("a\n\nc", document.Text);
CheckDocumentLines("a",
"",
"c");
}
[Test]
public void RemoveLineContentAndJoinNonMatchingDelimiters2()
{
document.Text = "a\nb\rc";
document.Remove(2, 1);
Assert.AreEqual("a\n\rc", document.Text);
CheckDocumentLines("a",
"",
"c");
}
[Test]
public void RemoveMultilineUpToFirstPartOfDelimiter()
{
document.Text = "0\n1\r\n2";
document.Remove(1, 3);
Assert.AreEqual("0\n2", document.Text);
CheckDocumentLines("0",
"2");
}
[Test]
public void RemoveSecondPartOfDelimiter()
{
document.Text = "a\r\nb";
document.Remove(2, 1);
Assert.AreEqual("a\rb", document.Text);
CheckDocumentLines("a",
"b");
}
[Test]
public void RemoveFromSecondPartOfDelimiter()
{
document.Text = "a\r\nb\nc";
document.Remove(2, 3);
Assert.AreEqual("a\rc", document.Text);
CheckDocumentLines("a",
"c");
}
[Test]
public void RemoveFromSecondPartOfDelimiterToDocumentEnd()
{
document.Text = "a\r\nb";
document.Remove(2, 2);
Assert.AreEqual("a\r", document.Text);
CheckDocumentLines("a",
"");
}
[Test]
public void RemoveUpToMatchingDelimiter1()
{
document.Text = "a\r\nb\nc";
document.Remove(2, 2);
Assert.AreEqual("a\r\nc", document.Text);
CheckDocumentLines("a",
"c");
}
[Test]
public void RemoveUpToMatchingDelimiter2()
{
document.Text = "a\r\nb\r\nc";
document.Remove(2, 3);
Assert.AreEqual("a\r\nc", document.Text);
CheckDocumentLines("a",
"c");
}
[Test]
public void RemoveUpToNonMatchingDelimiter()
{
document.Text = "a\r\nb\rc";
document.Remove(2, 2);
Assert.AreEqual("a\r\rc", document.Text);
CheckDocumentLines("a",
"",
"c");
}
[Test]
public void RemoveTwoCharDelimiter()
{
document.Text = "a\r\nb";
document.Remove(1, 2);
Assert.AreEqual("ab", document.Text);
CheckDocumentLines("ab");
}
[Test]
public void RemoveOneCharDelimiter()
{
document.Text = "a\nb";
document.Remove(1, 1);
Assert.AreEqual("ab", document.Text);
CheckDocumentLines("ab");
}
void CheckDocumentLines(params string[] lines)
{
Assert.AreEqual(lines.Length, document.LineCount, "LineCount");
for (int i = 0; i < lines.Length; i++) {
Assert.AreEqual(lines[i], document.GetText(document.Lines[i]), "Text of line " + (i + 1));
}
}
[Test]
public void FixUpFirstPartOfDelimiter()
{
document.Text = "a\n\nb";
document.Replace(1, 1, "\r");
Assert.AreEqual("a\r\nb", document.Text);
CheckDocumentLines("a",
"b");
}
[Test]
public void FixUpSecondPartOfDelimiter()
{
document.Text = "a\r\rb";
document.Replace(2, 1, "\n");
Assert.AreEqual("a\r\nb", document.Text);
CheckDocumentLines("a",
"b");
}
[Test]
public void InsertInsideDelimiter()
{
document.Text = "a\r\nc";
document.Insert(2, "b");
Assert.AreEqual("a\rb\nc", document.Text);
CheckDocumentLines("a",
"b",
"c");
}
[Test]
public void InsertInsideDelimiter2()
{
document.Text = "a\r\nd";
document.Insert(2, "b\nc");
Assert.AreEqual("a\rb\nc\nd", document.Text);
CheckDocumentLines("a",
"b",
"c",
"d");
}
[Test]
public void InsertInsideDelimiter3()
{
document.Text = "a\r\nc";
document.Insert(2, "b\r");
Assert.AreEqual("a\rb\r\nc", document.Text);
CheckDocumentLines("a",
"b",
"c");
}
[Test]
public void ExtendDelimiter1()
{
document.Text = "a\nc";
document.Insert(1, "b\r");
Assert.AreEqual("ab\r\nc", document.Text);
CheckDocumentLines("ab",
"c");
}
[Test]
public void ExtendDelimiter2()
{
document.Text = "a\rc";
document.Insert(2, "\nb");
Assert.AreEqual("a\r\nbc", document.Text);
CheckDocumentLines("a",
"bc");
}
[Test]
public void ReplaceLineContentBetweenMatchingDelimiters()
{
document.Text = "a\rb\nc";
document.Replace(2, 1, "x");
Assert.AreEqual("a\rx\nc", document.Text);
CheckDocumentLines("a",
"x",
"c");
}
[Test]
public void GetOffset()
{
document.Text = "Hello,\nWorld!";
Assert.AreEqual(0, document.GetOffset(1, 1));
Assert.AreEqual(1, document.GetOffset(1, 2));
Assert.AreEqual(5, document.GetOffset(1, 6));
Assert.AreEqual(6, document.GetOffset(1, 7));
Assert.AreEqual(7, document.GetOffset(2, 1));
Assert.AreEqual(8, document.GetOffset(2, 2));
Assert.AreEqual(12, document.GetOffset(2, 6));
Assert.AreEqual(13, document.GetOffset(2, 7));
}
[Test]
public void GetOffsetIgnoreNegativeColumns()
{
document.Text = "Hello,\nWorld!";
Assert.AreEqual(0, document.GetOffset(1, -1));
Assert.AreEqual(0, document.GetOffset(1, -100));
Assert.AreEqual(0, document.GetOffset(1, 0));
Assert.AreEqual(7, document.GetOffset(2, -1));
Assert.AreEqual(7, document.GetOffset(2, -100));
Assert.AreEqual(7, document.GetOffset(2, 0));
}
[Test]
public void GetOffsetIgnoreTooHighColumns()
{
document.Text = "Hello,\nWorld!";
Assert.AreEqual(6, document.GetOffset(1, 8));
Assert.AreEqual(6, document.GetOffset(1, 100));
Assert.AreEqual(13, document.GetOffset(2, 8));
Assert.AreEqual(13, document.GetOffset(2, 100));
}
}
}

168
ICSharpCode.AvalonEdit.Tests/Document/RandomizedLineManagerTest.cs

@ -0,0 +1,168 @@ @@ -0,0 +1,168 @@
// 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 ICSharpCode.AvalonEdit.Rendering;
using NUnit.Framework;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// A randomized test for the line manager.
/// </summary>
[TestFixture]
public class RandomizedLineManagerTest
{
TextDocument document;
Random rnd;
[TestFixtureSetUp]
public void FixtureSetup()
{
int seed = Environment.TickCount;
Console.WriteLine("RandomizedLineManagerTest Seed: " + seed);
rnd = new Random(seed);
}
[SetUp]
public void Setup()
{
document = new TextDocument();
}
[Test]
public void ShortReplacements()
{
char[] chars = { 'a', 'b', '\r', '\n' };
char[] buffer = new char[20];
for (int i = 0; i < 2500; i++) {
int offset = rnd.Next(0, document.TextLength);
int length = rnd.Next(0, document.TextLength - offset);
int newTextLength = rnd.Next(0, 20);
for (int j = 0; j < newTextLength; j++) {
buffer[j] = chars[rnd.Next(0, chars.Length)];
}
document.Replace(offset, length, new string(buffer, 0, newTextLength));
CheckLines();
}
}
[Test]
public void LargeReplacements()
{
char[] chars = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', '\r', '\n' };
char[] buffer = new char[1000];
for (int i = 0; i < 20; i++) {
int offset = rnd.Next(0, document.TextLength);
int length = rnd.Next(0, (document.TextLength - offset) / 4);
int newTextLength = rnd.Next(0, 1000);
for (int j = 0; j < newTextLength; j++) {
buffer[j] = chars[rnd.Next(0, chars.Length)];
}
string newText = new string(buffer, 0, newTextLength);
string expectedText = document.Text.Remove(offset, length).Insert(offset, newText);
document.Replace(offset, length, newText);
Assert.AreEqual(expectedText, document.Text);
CheckLines();
}
}
void CheckLines()
{
string text = document.Text;
int lineNumber = 1;
int lineStart = 0;
for (int i = 0; i < text.Length; i++) {
char c = text[i];
if (c == '\r' && i + 1 < text.Length && text[i + 1] == '\n') {
DocumentLine line = document.GetLineByNumber(lineNumber);
Assert.AreEqual(lineNumber, line.LineNumber);
Assert.AreEqual(2, line.DelimiterLength);
Assert.AreEqual(lineStart, line.Offset);
Assert.AreEqual(i - lineStart, line.Length);
i++; // consume \n
lineNumber++;
lineStart = i+1;
} else if (c == '\r' || c == '\n') {
DocumentLine line = document.GetLineByNumber(lineNumber);
Assert.AreEqual(lineNumber, line.LineNumber);
Assert.AreEqual(1, line.DelimiterLength);
Assert.AreEqual(lineStart, line.Offset);
Assert.AreEqual(i - lineStart, line.Length);
lineNumber++;
lineStart = i+1;
}
}
Assert.AreEqual(lineNumber, document.LineCount);
}
[Test]
public void CollapsingTest()
{
char[] chars = { 'a', 'b', '\r', '\n' };
char[] buffer = new char[20];
HeightTree heightTree = new HeightTree(document, 10);
List<CollapsedLineSection> collapsedSections = new List<CollapsedLineSection>();
for (int i = 0; i < 2500; i++) {
// Console.WriteLine("Iteration " + i);
// Console.WriteLine(heightTree.GetTreeAsString());
// foreach (CollapsedLineSection cs in collapsedSections) {
// Console.WriteLine(cs);
// }
switch (rnd.Next(0, 10)) {
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
int offset = rnd.Next(0, document.TextLength);
int length = rnd.Next(0, document.TextLength - offset);
int newTextLength = rnd.Next(0, 20);
for (int j = 0; j < newTextLength; j++) {
buffer[j] = chars[rnd.Next(0, chars.Length)];
}
document.Replace(offset, length, new string(buffer, 0, newTextLength));
break;
case 6:
case 7:
int startLine = rnd.Next(1, document.LineCount + 1);
int endLine = rnd.Next(startLine, document.LineCount + 1);
collapsedSections.Add(heightTree.CollapseText(document.GetLineByNumber(startLine), document.GetLineByNumber(endLine)));
break;
case 8:
if (collapsedSections.Count > 0) {
CollapsedLineSection cs = collapsedSections[rnd.Next(0, collapsedSections.Count)];
// unless the text section containing the CollapsedSection was deleted:
if (cs.Start != null) {
cs.Uncollapse();
}
collapsedSections.Remove(cs);
}
break;
case 9:
foreach (DocumentLine ls in document.Lines) {
heightTree.SetHeight(ls, ls.LineNumber);
}
break;
}
var treeSections = new HashSet<CollapsedLineSection>(heightTree.GetAllCollapsedSections());
int expectedCount = 0;
foreach (CollapsedLineSection cs in collapsedSections) {
if (cs.Start != null) {
expectedCount++;
Assert.IsTrue(treeSections.Contains(cs));
}
}
Assert.AreEqual(expectedCount, treeSections.Count);
CheckLines();
HeightTests.CheckHeights(document, heightTree);
}
}
}
}

313
ICSharpCode.AvalonEdit.Tests/Document/TextAnchorTest.cs

@ -0,0 +1,313 @@ @@ -0,0 +1,313 @@
// 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 NUnit.Framework;
using System.Collections.Generic;
namespace ICSharpCode.AvalonEdit.Document
{
[TestFixture]
public class TextAnchorTest
{
TextDocument document;
[SetUp]
public void SetUp()
{
document = new TextDocument();
}
[Test]
public void AnchorInEmptyDocument()
{
TextAnchor a1 = document.CreateAnchor(0);
TextAnchor a2 = document.CreateAnchor(0);
a1.MovementType = AnchorMovementType.BeforeInsertion;
a2.MovementType = AnchorMovementType.AfterInsertion;
Assert.AreEqual(0, a1.Offset);
Assert.AreEqual(0, a2.Offset);
document.Insert(0, "x");
Assert.AreEqual(0, a1.Offset);
Assert.AreEqual(1, a2.Offset);
}
[Test]
public void AnchorsSurviveDeletion()
{
document.Text = new string(' ', 10);
TextAnchor[] a1 = new TextAnchor[11];
TextAnchor[] a2 = new TextAnchor[11];
for (int i = 0; i < 11; i++) {
//Console.WriteLine("Insert first at i = " + i);
a1[i] = document.CreateAnchor(i);
a1[i].SurviveDeletion = true;
//Console.WriteLine(document.GetTextAnchorTreeAsString());
//Console.WriteLine("Insert second at i = " + i);
a2[i] = document.CreateAnchor(i);
a2[i].SurviveDeletion = false;
//Console.WriteLine(document.GetTextAnchorTreeAsString());
}
for (int i = 0; i < 11; i++) {
Assert.AreEqual(i, a1[i].Offset);
Assert.AreEqual(i, a2[i].Offset);
}
document.Remove(1, 8);
for (int i = 0; i < 11; i++) {
if (i <= 1) {
Assert.IsFalse(a1[i].IsDeleted);
Assert.IsFalse(a2[i].IsDeleted);
Assert.AreEqual(i, a1[i].Offset);
Assert.AreEqual(i, a2[i].Offset);
} else if (i <= 8) {
Assert.IsFalse(a1[i].IsDeleted);
Assert.IsTrue(a2[i].IsDeleted);
Assert.AreEqual(1, a1[i].Offset);
} else {
Assert.IsFalse(a1[i].IsDeleted);
Assert.IsFalse(a2[i].IsDeleted);
Assert.AreEqual(i - 8, a1[i].Offset);
Assert.AreEqual(i - 8, a2[i].Offset);
}
}
}
Random rnd;
[TestFixtureSetUp]
public void FixtureSetup()
{
int seed = Environment.TickCount;
Console.WriteLine("TextAnchorTest Seed: " + seed);
rnd = new Random(seed);
}
[Test]
public void CreateAnchors()
{
List<TextAnchor> anchors = new List<TextAnchor>();
List<int> expectedOffsets = new List<int>();
document.Text = new string(' ', 1000);
for (int i = 0; i < 1000; i++) {
int offset = rnd.Next(1000);
anchors.Add(document.CreateAnchor(offset));
expectedOffsets.Add(offset);
}
for (int i = 0; i < anchors.Count; i++) {
Assert.AreEqual(expectedOffsets[i], anchors[i].Offset);
}
GC.KeepAlive(anchors);
}
[Test]
public void CreateAndGCAnchors()
{
List<TextAnchor> anchors = new List<TextAnchor>();
List<int> expectedOffsets = new List<int>();
document.Text = new string(' ', 1000);
for (int t = 0; t < 250; t++) {
int c = rnd.Next(50);
if (rnd.Next(2) == 0) {
for (int i = 0; i < c; i++) {
int offset = rnd.Next(1000);
anchors.Add(document.CreateAnchor(offset));
expectedOffsets.Add(offset);
}
} else if (c <= anchors.Count) {
anchors.RemoveRange(0, c);
expectedOffsets.RemoveRange(0, c);
GC.Collect();
}
for (int j = 0; j < anchors.Count; j++) {
Assert.AreEqual(expectedOffsets[j], anchors[j].Offset);
}
}
GC.KeepAlive(anchors);
}
[Test]
public void MoveAnchorsDuringReplace()
{
document.Text = "abcd";
TextAnchor start = document.CreateAnchor(1);
TextAnchor middleDeletable = document.CreateAnchor(2);
TextAnchor middleSurvivorLeft = document.CreateAnchor(2);
middleSurvivorLeft.SurviveDeletion = true;
middleSurvivorLeft.MovementType = AnchorMovementType.BeforeInsertion;
TextAnchor middleSurvivorRight = document.CreateAnchor(2);
middleSurvivorRight.SurviveDeletion = true;
middleSurvivorRight.MovementType = AnchorMovementType.AfterInsertion;
TextAnchor end = document.CreateAnchor(3);
document.Replace(1, 2, "BxC");
Assert.AreEqual(1, start.Offset);
Assert.IsTrue(middleDeletable.IsDeleted);
Assert.AreEqual(1, middleSurvivorLeft.Offset);
Assert.AreEqual(4, middleSurvivorRight.Offset);
Assert.AreEqual(4, end.Offset);
}
[Test]
public void CreateAndMoveAnchors()
{
List<TextAnchor> anchors = new List<TextAnchor>();
List<int> expectedOffsets = new List<int>();
document.Text = new string(' ', 1000);
for (int t = 0; t < 250; t++) {
//Console.Write("t = " + t + " ");
int c = rnd.Next(50);
switch (rnd.Next(5)) {
case 0:
//Console.WriteLine("Add c=" + c + " anchors");
for (int i = 0; i < c; i++) {
int offset = rnd.Next(document.TextLength);
TextAnchor anchor = document.CreateAnchor(offset);
if (rnd.Next(2) == 0)
anchor.MovementType = AnchorMovementType.BeforeInsertion;
else
anchor.MovementType = AnchorMovementType.AfterInsertion;
anchor.SurviveDeletion = rnd.Next(2) == 0;
anchors.Add(anchor);
expectedOffsets.Add(offset);
}
break;
case 1:
if (c <= anchors.Count) {
//Console.WriteLine("Remove c=" + c + " anchors");
anchors.RemoveRange(0, c);
expectedOffsets.RemoveRange(0, c);
GC.Collect();
}
break;
case 2:
int insertOffset = rnd.Next(document.TextLength);
int insertLength = rnd.Next(1000);
//Console.WriteLine("insertOffset=" + insertOffset + " insertLength="+insertLength);
document.Insert(insertOffset, new string(' ', insertLength));
for (int i = 0; i < anchors.Count; i++) {
if (anchors[i].MovementType == AnchorMovementType.BeforeInsertion) {
if (expectedOffsets[i] > insertOffset)
expectedOffsets[i] += insertLength;
} else {
if (expectedOffsets[i] >= insertOffset)
expectedOffsets[i] += insertLength;
}
}
break;
case 3:
int removalOffset = rnd.Next(document.TextLength);
int removalLength = rnd.Next(document.TextLength - removalOffset);
//Console.WriteLine("RemovalOffset=" + removalOffset + " RemovalLength="+removalLength);
document.Remove(removalOffset, removalLength);
for (int i = anchors.Count - 1; i >= 0; i--) {
if (expectedOffsets[i] > removalOffset && expectedOffsets[i] < removalOffset + removalLength) {
if (anchors[i].SurviveDeletion) {
expectedOffsets[i] = removalOffset;
} else {
Assert.IsTrue(anchors[i].IsDeleted);
anchors.RemoveAt(i);
expectedOffsets.RemoveAt(i);
}
} else if (expectedOffsets[i] > removalOffset) {
expectedOffsets[i] -= removalLength;
}
}
break;
case 4:
int replaceOffset = rnd.Next(document.TextLength);
int replaceRemovalLength = rnd.Next(document.TextLength - replaceOffset);
int replaceInsertLength = rnd.Next(1000);
//Console.WriteLine("ReplaceOffset=" + replaceOffset + " RemovalLength="+replaceRemovalLength + " InsertLength=" + replaceInsertLength);
document.Replace(replaceOffset, replaceRemovalLength, new string(' ', replaceInsertLength));
for (int i = anchors.Count - 1; i >= 0; i--) {
if (expectedOffsets[i] > replaceOffset && expectedOffsets[i] < replaceOffset + replaceRemovalLength) {
if (anchors[i].SurviveDeletion) {
if (anchors[i].MovementType == AnchorMovementType.AfterInsertion)
expectedOffsets[i] = replaceOffset + replaceInsertLength;
else
expectedOffsets[i] = replaceOffset;
} else {
Assert.IsTrue(anchors[i].IsDeleted);
anchors.RemoveAt(i);
expectedOffsets.RemoveAt(i);
}
} else if (expectedOffsets[i] > replaceOffset) {
expectedOffsets[i] += replaceInsertLength - replaceRemovalLength;
} else if (expectedOffsets[i] == replaceOffset && replaceRemovalLength == 0 && anchors[i].MovementType == AnchorMovementType.AfterInsertion) {
expectedOffsets[i] += replaceInsertLength - replaceRemovalLength;
}
}
break;
}
Assert.AreEqual(anchors.Count, expectedOffsets.Count);
for (int j = 0; j < anchors.Count; j++) {
Assert.AreEqual(expectedOffsets[j], anchors[j].Offset);
}
}
GC.KeepAlive(anchors);
}
[Test]
public void RepeatedTextDragDrop()
{
document.Text = new string(' ', 1000);
for (int i = 0; i < 20; i++) {
TextAnchor a = document.CreateAnchor(144);
TextAnchor b = document.CreateAnchor(157);
document.Insert(128, new string('a', 13));
document.Remove(157, 13);
a = document.CreateAnchor(128);
b = document.CreateAnchor(141);
document.Insert(157, new string('b', 13));
document.Remove(128, 13);
a = null;
b = null;
if ((i % 5) == 0)
GC.Collect();
}
}
[Test]
public void ReplaceSpacesWithTab()
{
document.Text = "a b";
TextAnchor before = document.CreateAnchor(1);
before.MovementType = AnchorMovementType.AfterInsertion;
TextAnchor after = document.CreateAnchor(5);
TextAnchor survivingMiddle = document.CreateAnchor(2);
TextAnchor deletedMiddle = document.CreateAnchor(3);
document.Replace(1, 4, "\t", OffsetChangeMappingType.CharacterReplace);
Assert.AreEqual("a\tb", document.Text);
// yes, the movement is a bit strange; but that's how CharacterReplace works when the text gets shorter
Assert.AreEqual(1, before.Offset);
Assert.AreEqual(2, after.Offset);
Assert.AreEqual(2, survivingMiddle.Offset);
Assert.AreEqual(2, deletedMiddle.Offset);
}
[Test]
public void ReplaceTwoCharactersWithThree()
{
document.Text = "a12b";
TextAnchor before = document.CreateAnchor(1);
before.MovementType = AnchorMovementType.AfterInsertion;
TextAnchor after = document.CreateAnchor(3);
before.MovementType = AnchorMovementType.BeforeInsertion;
TextAnchor middleB = document.CreateAnchor(2);
before.MovementType = AnchorMovementType.BeforeInsertion;
TextAnchor middleA = document.CreateAnchor(2);
before.MovementType = AnchorMovementType.AfterInsertion;
document.Replace(1, 2, "123", OffsetChangeMappingType.CharacterReplace);
Assert.AreEqual("a123b", document.Text);
Assert.AreEqual(1, before.Offset);
Assert.AreEqual(4, after.Offset);
Assert.AreEqual(2, middleA.Offset);
Assert.AreEqual(2, middleB.Offset);
}
}
}

350
ICSharpCode.AvalonEdit.Tests/Document/TextSegmentTreeTest.cs

@ -0,0 +1,350 @@ @@ -0,0 +1,350 @@
// 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 NUnit.Framework;
using System.Collections.Generic;
namespace ICSharpCode.AvalonEdit.Document
{
[TestFixture]
public class TextSegmentTreeTest
{
Random rnd;
[TestFixtureSetUp]
public void FixtureSetup()
{
int seed = Environment.TickCount;
Console.WriteLine("TextSegmentTreeTest Seed: " + seed);
rnd = new Random(seed);
}
class TestTextSegment : TextSegment
{
internal int ExpectedOffset, ExpectedLength;
public TestTextSegment(int expectedOffset, int expectedLength)
{
this.ExpectedOffset = expectedOffset;
this.ExpectedLength = expectedLength;
this.StartOffset = expectedOffset;
this.Length = expectedLength;
}
}
TextSegmentCollection<TestTextSegment> tree;
List<TestTextSegment> expectedSegments;
[SetUp]
public void SetUp()
{
tree = new TextSegmentCollection<TestTextSegment>();
expectedSegments = new List<TestTextSegment>();
}
[Test]
public void FindInEmptyTree()
{
Assert.AreSame(null, tree.FindFirstSegmentWithStartAfter(0));
Assert.AreEqual(0, tree.FindSegmentsContaining(0).Count);
Assert.AreEqual(0, tree.FindOverlappingSegments(10, 20).Count);
}
[Test]
public void FindFirstSegmentWithStartAfter()
{
var s1 = new TestTextSegment(5, 10);
var s2 = new TestTextSegment(10, 10);
tree.Add(s1);
tree.Add(s2);
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(-100));
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(0));
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(4));
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(5));
Assert.AreSame(s2, tree.FindFirstSegmentWithStartAfter(6));
Assert.AreSame(s2, tree.FindFirstSegmentWithStartAfter(9));
Assert.AreSame(s2, tree.FindFirstSegmentWithStartAfter(10));
Assert.AreSame(null, tree.FindFirstSegmentWithStartAfter(11));
Assert.AreSame(null, tree.FindFirstSegmentWithStartAfter(100));
}
[Test]
public void FindFirstSegmentWithStartAfterWithDuplicates()
{
var s1 = new TestTextSegment(5, 10);
var s1b = new TestTextSegment(5, 7);
var s2 = new TestTextSegment(10, 10);
var s2b = new TestTextSegment(10, 7);
tree.Add(s1);
tree.Add(s1b);
tree.Add(s2);
tree.Add(s2b);
Assert.AreSame(s1b, tree.GetNextSegment(s1));
Assert.AreSame(s2b, tree.GetNextSegment(s2));
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(-100));
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(0));
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(4));
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(5));
Assert.AreSame(s2, tree.FindFirstSegmentWithStartAfter(6));
Assert.AreSame(s2, tree.FindFirstSegmentWithStartAfter(9));
Assert.AreSame(s2, tree.FindFirstSegmentWithStartAfter(10));
Assert.AreSame(null, tree.FindFirstSegmentWithStartAfter(11));
Assert.AreSame(null, tree.FindFirstSegmentWithStartAfter(100));
}
[Test]
public void FindFirstSegmentWithStartAfterWithDuplicates2()
{
var s1 = new TestTextSegment(5, 1);
var s2 = new TestTextSegment(5, 2);
var s3 = new TestTextSegment(5, 3);
var s4 = new TestTextSegment(5, 4);
tree.Add(s1);
tree.Add(s2);
tree.Add(s3);
tree.Add(s4);
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(0));
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(1));
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(4));
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(5));
Assert.AreSame(null, tree.FindFirstSegmentWithStartAfter(6));
}
TestTextSegment AddSegment(int offset, int length)
{
// Console.WriteLine("Add " + offset + ", " + length);
TestTextSegment s = new TestTextSegment(offset, length);
tree.Add(s);
expectedSegments.Add(s);
return s;
}
void RemoveSegment(TestTextSegment s)
{
// Console.WriteLine("Remove " + s);
expectedSegments.Remove(s);
tree.Remove(s);
}
void TestRetrieval(int offset, int length)
{
HashSet<TestTextSegment> actual = new HashSet<TestTextSegment>(tree.FindOverlappingSegments(offset, length));
HashSet<TestTextSegment> expected = new HashSet<TestTextSegment>();
foreach (TestTextSegment e in expectedSegments) {
if (e.ExpectedOffset + e.ExpectedLength < offset)
continue;
if (e.ExpectedOffset > offset + length)
continue;
expected.Add(e);
}
Assert.IsTrue(actual.IsSubsetOf(expected));
Assert.IsTrue(expected.IsSubsetOf(actual));
}
void CheckSegments()
{
Assert.AreEqual(expectedSegments.Count, tree.Count);
foreach (TestTextSegment s in expectedSegments) {
Assert.AreEqual(s.ExpectedOffset, s.StartOffset /*, "startoffset for " + s*/);
Assert.AreEqual(s.ExpectedLength, s.Length /*, "length for " + s*/);
}
}
[Test]
public void AddSegments()
{
TestTextSegment s1 = AddSegment(10, 20);
TestTextSegment s2 = AddSegment(15, 10);
CheckSegments();
}
void ChangeDocument(OffsetChangeMapEntry change)
{
tree.UpdateOffsets(change);
foreach (TestTextSegment s in expectedSegments) {
int endOffset = s.ExpectedOffset + s.ExpectedLength;
s.ExpectedOffset = change.GetNewOffset(s.ExpectedOffset, AnchorMovementType.AfterInsertion);
s.ExpectedLength = Math.Max(0, change.GetNewOffset(endOffset, AnchorMovementType.BeforeInsertion) - s.ExpectedOffset);
}
}
[Test]
public void InsertionBeforeAllSegments()
{
TestTextSegment s1 = AddSegment(10, 20);
TestTextSegment s2 = AddSegment(15, 10);
ChangeDocument(new OffsetChangeMapEntry(5, 0, 2));
CheckSegments();
}
[Test]
public void ReplacementBeforeAllSegmentsTouchingFirstSegment()
{
TestTextSegment s1 = AddSegment(10, 20);
TestTextSegment s2 = AddSegment(15, 10);
ChangeDocument(new OffsetChangeMapEntry(5, 5, 2));
CheckSegments();
}
[Test]
public void InsertionAfterAllSegments()
{
TestTextSegment s1 = AddSegment(10, 20);
TestTextSegment s2 = AddSegment(15, 10);
ChangeDocument(new OffsetChangeMapEntry(45, 0, 2));
CheckSegments();
}
[Test]
public void ReplacementOverlappingWithStartOfSegment()
{
TestTextSegment s1 = AddSegment(10, 20);
TestTextSegment s2 = AddSegment(15, 10);
ChangeDocument(new OffsetChangeMapEntry(9, 7, 2));
CheckSegments();
}
[Test]
public void ReplacementOfWholeSegment()
{
TestTextSegment s1 = AddSegment(10, 20);
TestTextSegment s2 = AddSegment(15, 10);
ChangeDocument(new OffsetChangeMapEntry(10, 20, 30));
CheckSegments();
}
[Test]
public void ReplacementAtEndOfSegment()
{
TestTextSegment s1 = AddSegment(10, 20);
TestTextSegment s2 = AddSegment(15, 10);
ChangeDocument(new OffsetChangeMapEntry(24, 6, 10));
CheckSegments();
}
[Test]
public void RandomizedNoDocumentChanges()
{
for (int i = 0; i < 1000; i++) {
// Console.WriteLine(tree.GetTreeAsString());
// Console.WriteLine("Iteration " + i);
switch (rnd.Next(3)) {
case 0:
AddSegment(rnd.Next(500), rnd.Next(30));
break;
case 1:
AddSegment(rnd.Next(500), rnd.Next(300));
break;
case 2:
if (tree.Count > 0) {
RemoveSegment(expectedSegments[rnd.Next(tree.Count)]);
}
break;
}
CheckSegments();
}
}
[Test]
public void RandomizedCloseNoDocumentChanges()
{
// Lots of segments in a short document. Tests how the tree copes with multiple identical segments.
for (int i = 0; i < 1000; i++) {
switch (rnd.Next(3)) {
case 0:
AddSegment(rnd.Next(20), rnd.Next(10));
break;
case 1:
AddSegment(rnd.Next(20), rnd.Next(20));
break;
case 2:
if (tree.Count > 0) {
RemoveSegment(expectedSegments[rnd.Next(tree.Count)]);
}
break;
}
CheckSegments();
}
}
[Test]
public void RandomizedRetrievalTest()
{
for (int i = 0; i < 1000; i++) {
AddSegment(rnd.Next(500), rnd.Next(300));
}
CheckSegments();
for (int i = 0; i < 1000; i++) {
TestRetrieval(rnd.Next(1000) - 100, rnd.Next(500));
}
}
[Test]
public void RandomizedWithDocumentChanges()
{
for (int i = 0; i < 500; i++) {
// Console.WriteLine(tree.GetTreeAsString());
// Console.WriteLine("Iteration " + i);
switch (rnd.Next(6)) {
case 0:
AddSegment(rnd.Next(500), rnd.Next(30));
break;
case 1:
AddSegment(rnd.Next(500), rnd.Next(300));
break;
case 2:
if (tree.Count > 0) {
RemoveSegment(expectedSegments[rnd.Next(tree.Count)]);
}
break;
case 3:
ChangeDocument(new OffsetChangeMapEntry(rnd.Next(800), rnd.Next(50), rnd.Next(50)));
break;
case 4:
ChangeDocument(new OffsetChangeMapEntry(rnd.Next(800), 0, rnd.Next(50)));
break;
case 5:
ChangeDocument(new OffsetChangeMapEntry(rnd.Next(800), rnd.Next(50), 0));
break;
}
CheckSegments();
}
}
[Test]
public void RandomizedWithDocumentChangesClose()
{
for (int i = 0; i < 500; i++) {
// Console.WriteLine(tree.GetTreeAsString());
// Console.WriteLine("Iteration " + i);
switch (rnd.Next(6)) {
case 0:
AddSegment(rnd.Next(50), rnd.Next(30));
break;
case 1:
AddSegment(rnd.Next(50), rnd.Next(3));
break;
case 2:
if (tree.Count > 0) {
RemoveSegment(expectedSegments[rnd.Next(tree.Count)]);
}
break;
case 3:
ChangeDocument(new OffsetChangeMapEntry(rnd.Next(80), rnd.Next(10), rnd.Next(10)));
break;
case 4:
ChangeDocument(new OffsetChangeMapEntry(rnd.Next(80), 0, rnd.Next(10)));
break;
case 5:
ChangeDocument(new OffsetChangeMapEntry(rnd.Next(80), rnd.Next(10), 0));
break;
}
CheckSegments();
}
}
}
}

76
ICSharpCode.AvalonEdit.Tests/Document/TextUtilitiesTests.cs

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
// 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 NUnit.Framework;
namespace ICSharpCode.AvalonEdit.Document
{
[TestFixture]
public class TextUtilitiesTests
{
#region GetWhitespaceAfter
[Test]
public void TestGetWhitespaceAfter()
{
Assert.AreEqual(new SimpleSegment(2, 3), TextUtilities.GetWhitespaceAfter(new StringTextSource("a \t \tb"), 2));
}
[Test]
public void TestGetWhitespaceAfterDoesNotSkipNewLine()
{
Assert.AreEqual(new SimpleSegment(2, 3), TextUtilities.GetWhitespaceAfter(new StringTextSource("a \t \tb"), 2));
}
[Test]
public void TestGetWhitespaceAfterEmptyResult()
{
Assert.AreEqual(new SimpleSegment(2, 0), TextUtilities.GetWhitespaceAfter(new StringTextSource("a b"), 2));
}
[Test]
public void TestGetWhitespaceAfterEndOfString()
{
Assert.AreEqual(new SimpleSegment(2, 0), TextUtilities.GetWhitespaceAfter(new StringTextSource("a "), 2));
}
[Test]
public void TestGetWhitespaceAfterUntilEndOfString()
{
Assert.AreEqual(new SimpleSegment(2, 3), TextUtilities.GetWhitespaceAfter(new StringTextSource("a \t \t"), 2));
}
#endregion
#region GetWhitespaceBefore
[Test]
public void TestGetWhitespaceBefore()
{
Assert.AreEqual(new SimpleSegment(1, 3), TextUtilities.GetWhitespaceBefore(new StringTextSource("a\t \t b"), 4));
}
[Test]
public void TestGetWhitespaceBeforeDoesNotSkipNewLine()
{
Assert.AreEqual(new SimpleSegment(2, 1), TextUtilities.GetWhitespaceBefore(new StringTextSource("a\n b"), 3));
}
[Test]
public void TestGetWhitespaceBeforeEmptyResult()
{
Assert.AreEqual(new SimpleSegment(2, 0), TextUtilities.GetWhitespaceBefore(new StringTextSource(" a b"), 2));
}
[Test]
public void TestGetWhitespaceBeforeStartOfString()
{
Assert.AreEqual(new SimpleSegment(0, 0), TextUtilities.GetWhitespaceBefore(new StringTextSource(" a"), 0));
}
[Test]
public void TestGetWhitespaceBeforeUntilStartOfString()
{
Assert.AreEqual(new SimpleSegment(0, 2), TextUtilities.GetWhitespaceBefore(new StringTextSource(" \t a"), 2));
}
#endregion
}
}

60
ICSharpCode.AvalonEdit.Tests/Editing/ChangeDocumentTests.cs

@ -0,0 +1,60 @@ @@ -0,0 +1,60 @@
// 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.Text;
using ICSharpCode.AvalonEdit.Document;
using NUnit.Framework;
namespace ICSharpCode.AvalonEdit.Editing
{
[TestFixture]
public class ChangeDocumentTests
{
[Test]
public void ClearCaretAndSelectionOnDocumentChange()
{
TextArea textArea = new TextArea();
textArea.Document = new TextDocument("1\n2\n3\n4th line");
textArea.Caret.Offset = 6;
textArea.Selection = new SimpleSelection(3, 6);
textArea.Document = new TextDocument("1\n2nd");
Assert.AreEqual(0, textArea.Caret.Offset);
Assert.AreEqual(new TextLocation(1, 1), textArea.Caret.Location);
Assert.AreSame(Selection.Empty, textArea.Selection);
}
[Test]
public void SetDocumentToNull()
{
TextArea textArea = new TextArea();
textArea.Document = new TextDocument("1\n2\n3\n4th line");
textArea.Caret.Offset = 6;
textArea.Selection = new SimpleSelection(3, 6);
textArea.Document = null;
Assert.AreEqual(0, textArea.Caret.Offset);
Assert.AreEqual(new TextLocation(1, 1), textArea.Caret.Location);
Assert.AreSame(Selection.Empty, textArea.Selection);
}
[Test]
public void CheckEventOrderOnDocumentChange()
{
TextArea textArea = new TextArea();
TextDocument newDocument = new TextDocument();
StringBuilder b = new StringBuilder();
textArea.TextView.DocumentChanged += delegate {
b.Append("TextView.DocumentChanged;");
Assert.AreSame(newDocument, textArea.TextView.Document);
Assert.AreSame(newDocument, textArea.Document);
};
textArea.DocumentChanged += delegate {
b.Append("TextArea.DocumentChanged;");
Assert.AreSame(newDocument, textArea.TextView.Document);
Assert.AreSame(newDocument, textArea.Document);
};
textArea.Document = newDocument;
Assert.AreEqual("TextView.DocumentChanged;TextArea.DocumentChanged;", b.ToString());
}
}
}

131
ICSharpCode.AvalonEdit.Tests/Editing/TextSegmentReadOnlySectionTests.cs

@ -0,0 +1,131 @@ @@ -0,0 +1,131 @@
// 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 ICSharpCode.AvalonEdit.Document;
using System;
using System.Linq;
using NUnit.Framework;
namespace ICSharpCode.AvalonEdit.Editing
{
[TestFixture]
public class TextSegmentReadOnlySectionTests
{
TextSegmentCollection<TextSegment> segments;
TextSegmentReadOnlySectionProvider<TextSegment> provider;
[SetUp]
public void SetUp()
{
segments = new TextSegmentCollection<TextSegment>();
provider = new TextSegmentReadOnlySectionProvider<TextSegment>(segments);
}
[Test]
public void InsertionPossibleWhenNothingIsReadOnly()
{
Assert.IsTrue(provider.CanInsert(0));
Assert.IsTrue(provider.CanInsert(100));
}
[Test]
public void DeletionPossibleWhenNothingIsReadOnly()
{
var result = provider.GetDeletableSegments(new SimpleSegment(10, 20)).ToList();
Assert.AreEqual(1, result.Count);
Assert.AreEqual(10, result[0].Offset);
Assert.AreEqual(20, result[0].Length);
}
[Test]
public void InsertionPossibleBeforeReadOnlySegment()
{
segments.Add(new TextSegment { StartOffset = 10, EndOffset = 15 });
Assert.IsTrue(provider.CanInsert(5));
}
[Test]
public void InsertionPossibleAtStartOfReadOnlySegment()
{
segments.Add(new TextSegment { StartOffset = 10, EndOffset = 15 });
Assert.IsTrue(provider.CanInsert(10));
}
[Test]
public void InsertionImpossibleInsideReadOnlySegment()
{
segments.Add(new TextSegment { StartOffset = 10, EndOffset = 15 });
Assert.IsFalse(provider.CanInsert(11));
Assert.IsFalse(provider.CanInsert(12));
Assert.IsFalse(provider.CanInsert(13));
Assert.IsFalse(provider.CanInsert(14));
}
[Test]
public void InsertionPossibleAtEndOfReadOnlySegment()
{
segments.Add(new TextSegment { StartOffset = 10, EndOffset = 15 });
Assert.IsTrue(provider.CanInsert(15));
}
[Test]
public void InsertionPossibleBetweenReadOnlySegments()
{
segments.Add(new TextSegment { StartOffset = 10, EndOffset = 15 });
segments.Add(new TextSegment { StartOffset = 15, EndOffset = 20 });
Assert.IsTrue(provider.CanInsert(15));
}
[Test]
public void DeletionImpossibleInReadOnlySegment()
{
segments.Add(new TextSegment { StartOffset = 10, Length = 5 });
var result = provider.GetDeletableSegments(new SimpleSegment(11, 2)).ToList();
Assert.AreEqual(0, result.Count);
}
[Test]
public void DeletionAroundReadOnlySegment()
{
segments.Add(new TextSegment { StartOffset = 20, Length = 5 });
var result = provider.GetDeletableSegments(new SimpleSegment(15, 16)).ToList();
Assert.AreEqual(2, result.Count);
Assert.AreEqual(15, result[0].Offset);
Assert.AreEqual(5, result[0].Length);
Assert.AreEqual(25, result[1].Offset);
Assert.AreEqual(6, result[1].Length);
}
[Test]
public void DeleteLastCharacterInReadOnlySegment()
{
segments.Add(new TextSegment { StartOffset = 20, Length = 5 });
var result = provider.GetDeletableSegments(new SimpleSegment(24, 1)).ToList();
Assert.AreEqual(0, result.Count);
/* // we would need this result for the old Backspace code so that the last character doesn't get selected:
Assert.AreEqual(1, result.Count);
Assert.AreEqual(25, result[0].Offset);
Assert.AreEqual(0, result[0].Length);*/
}
[Test]
public void DeleteFirstCharacterInReadOnlySegment()
{
segments.Add(new TextSegment { StartOffset = 20, Length = 5 });
var result = provider.GetDeletableSegments(new SimpleSegment(20, 1)).ToList();
Assert.AreEqual(0, result.Count);
/* // we would need this result for the old Delete code so that the first character doesn't get selected:
Assert.AreEqual(1, result.Count);
Assert.AreEqual(2, result[0].Offset);
Assert.AreEqual(0, result[0].Length);*/
}
[Test]
public void DeleteWholeReadOnlySegment()
{
segments.Add(new TextSegment { StartOffset = 20, Length = 5 });
var result = provider.GetDeletableSegments(new SimpleSegment(20, 5)).ToList();
Assert.AreEqual(0, result.Count);
}
}
}

42
ICSharpCode.AvalonEdit.Tests/Highlighting/HtmlClipboardTests.cs

@ -0,0 +1,42 @@ @@ -0,0 +1,42 @@
// 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.Windows;
using ICSharpCode.AvalonEdit.Document;
using NUnit.Framework;
namespace ICSharpCode.AvalonEdit.Highlighting
{
[TestFixture]
public class HtmlClipboardTests
{
TextDocument document;
DocumentHighlighter highlighter;
public HtmlClipboardTests()
{
document = new TextDocument("using System.Text;\n\tstring text = SomeMethod();");
highlighter = new DocumentHighlighter(document, HighlightingManager.Instance.GetDefinition("C#").MainRuleSet);
}
[Test]
public void FullDocumentTest()
{
var segment = new TextSegment { StartOffset = 0, Length = document.TextLength };
string html = HtmlClipboard.CreateHtmlFragment(document, highlighter, segment, new HtmlOptions());
Assert.AreEqual("<span style=\"color: #008000; font-weight: bold; \">using</span>&nbsp;" +
"System<span style=\"color: #006400; \">.</span>Text<span style=\"color: #006400; \">;</span><br>" + Environment.NewLine +
"&nbsp;&nbsp;&nbsp;&nbsp;<span style=\"color: #ff0000; \">string</span>&nbsp;" +
"text =&nbsp;<span style=\"color: #191970; font-weight: bold; \">SomeMethod</span><span style=\"color: #006400; \">();</span>", html);
}
[Test]
public void PartOfHighlightedWordTest()
{
var segment = new TextSegment { StartOffset = 1, Length = 3 };
string html = HtmlClipboard.CreateHtmlFragment(document, highlighter, segment, new HtmlOptions());
Assert.AreEqual("<span style=\"color: #008000; font-weight: bold; \">sin</span>", html);
}
}
}

3
ICSharpCode.AvalonEdit.Tests/ICSharpCode.AvalonEdit.Tests.PartCover.Settings

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
<PartCoverSettings>
<Rule>+[ICSharpCode.AvalonEdit]*</Rule>
</PartCoverSettings>

107
ICSharpCode.AvalonEdit.Tests/ICSharpCode.AvalonEdit.Tests.csproj

@ -0,0 +1,107 @@ @@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{6222A3A1-83CE-47A3-A4E4-A018F82D44D8}</ProjectGuid>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<OutputType>Library</OutputType>
<RootNamespace>ICSharpCode.AvalonEdit</RootNamespace>
<AssemblyName>ICSharpCode.AvalonEdit.Tests</AssemblyName>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<AppDesignerFolder>Properties</AppDesignerFolder>
<SourceAnalysisOverrideSettingsFile>"C:\Program Files\SharpDevelop\3.0\bin\..\AddIns\AddIns\Misc\SourceAnalysis\Settings.SourceAnalysis"</SourceAnalysisOverrideSettingsFile>
<SignAssembly>True</SignAssembly>
<AssemblyOriginatorKeyFile>..\ICSharpCode.AvalonEdit\ICSharpCode.AvalonEdit.snk</AssemblyOriginatorKeyFile>
<DelaySign>False</DelaySign>
<AssemblyOriginatorKeyMode>File</AssemblyOriginatorKeyMode>
<AllowUnsafeBlocks>False</AllowUnsafeBlocks>
<NoStdLib>False</NoStdLib>
<WarningLevel>4</WarningLevel>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<OutputPath>..\..\..\..\bin\UnitTests\</OutputPath>
<RunPostBuildEvent>OnBuildSuccess</RunPostBuildEvent>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>Full</DebugType>
<Optimize>False</Optimize>
<CheckForOverflowUnderflow>True</CheckForOverflowUnderflow>
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>false</DebugSymbols>
<DebugType>None</DebugType>
<Optimize>True</Optimize>
<CheckForOverflowUnderflow>False</CheckForOverflowUnderflow>
<DefineConstants>TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Platform)' == 'AnyCPU' ">
<RegisterForComInterop>False</RegisterForComInterop>
<GenerateSerializationAssemblies>Auto</GenerateSerializationAssemblies>
<BaseAddress>4194304</BaseAddress>
<PlatformTarget>x86</PlatformTarget>
<FileAlignment>4096</FileAlignment>
</PropertyGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.Targets" />
<ItemGroup>
<Reference Include="ICSharpCode.SharpZipLib">
<HintPath>..\..\..\AddIns\Misc\AddInManager\RequiredLibraries\ICSharpCode.SharpZipLib.dll</HintPath>
</Reference>
<Reference Include="nunit.framework">
<HintPath>..\..\..\Tools\NUnit\nunit.framework.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="PresentationCore">
<RequiredTargetFramework>3.0</RequiredTargetFramework>
</Reference>
<Reference Include="PresentationFramework">
<RequiredTargetFramework>3.0</RequiredTargetFramework>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
<Reference Include="System.Xaml">
<RequiredTargetFramework>4.0</RequiredTargetFramework>
</Reference>
<Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
<Reference Include="WindowsBase">
<RequiredTargetFramework>3.0</RequiredTargetFramework>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Document\ChangeTrackingTest.cs" />
<Compile Include="Document\TextAnchorTest.cs" />
<Compile Include="Document\TextSegmentTreeTest.cs" />
<Compile Include="Document\TextUtilitiesTests.cs" />
<Compile Include="Editing\ChangeDocumentTests.cs" />
<Compile Include="Editing\TextSegmentReadOnlySectionTests.cs" />
<Compile Include="Highlighting\HtmlClipboardTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Document\CollapsingTests.cs" />
<Compile Include="Document\HeightTests.cs" />
<Compile Include="Document\RandomizedLineManagerTest.cs" />
<Compile Include="Document\LineManagerTests.cs" />
<Compile Include="Utils\CaretNavigationTests.cs" />
<Compile Include="Utils\CompressingTreeListTests.cs" />
<Compile Include="Utils\ExtensionMethodsTests.cs" />
<Compile Include="Utils\IndentationStringTests.cs" />
<Compile Include="Utils\RopeTests.cs" />
<Compile Include="WeakReferenceTests.cs" />
<Compile Include="XmlParser\ParserTests.cs" />
<Compile Include="XmlParser\TextReplacementTests.cs" />
<None Include="app.config" />
<None Include="XmlParser\W3C.zip">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ICSharpCode.AvalonEdit\ICSharpCode.AvalonEdit.csproj">
<Project>{6C55B776-26D4-4DB3-A6AB-87E783B2F3D1}</Project>
<Name>ICSharpCode.AvalonEdit</Name>
</ProjectReference>
</ItemGroup>
</Project>

33
ICSharpCode.AvalonEdit.Tests/Properties/AssemblyInfo.cs

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
// 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)
#region Using directives
using System;
using System.Reflection;
using System.Runtime.InteropServices;
#endregion
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("ICSharpCode.AvalonEdit.Tests")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("ICSharpCode.AvalonEdit.Tests")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// This sets the default COM visibility of types in the assembly to invisible.
// If you need to expose a type to COM, use [ComVisible(true)] on that type.
[assembly: ComVisible(false)]
// The assembly version has following format :
//
// Major.Minor.Build.Revision
//
// You can specify all the values or you can use the default the Revision and
// Build Numbers by using the '*' as shown below:
[assembly: AssemblyVersion("1.0.*")]

86
ICSharpCode.AvalonEdit.Tests/Utils/CaretNavigationTests.cs

@ -0,0 +1,86 @@ @@ -0,0 +1,86 @@
// 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.Windows.Documents;
using ICSharpCode.AvalonEdit.Document;
using NUnit.Framework;
namespace ICSharpCode.AvalonEdit.Utils
{
[TestFixture]
public class CaretNavigationTests
{
int GetNextCaretStop(string text, int offset, CaretPositioningMode mode)
{
return TextUtilities.GetNextCaretPosition(new StringTextSource(text), offset, LogicalDirection.Forward, mode);
}
int GetPrevCaretStop(string text, int offset, CaretPositioningMode mode)
{
return TextUtilities.GetNextCaretPosition(new StringTextSource(text), offset, LogicalDirection.Backward, mode);
}
[Test]
public void CaretStopInEmptyString()
{
Assert.AreEqual(0, GetNextCaretStop("", -1, CaretPositioningMode.Normal));
Assert.AreEqual(-1, GetNextCaretStop("", 0, CaretPositioningMode.Normal));
Assert.AreEqual(-1, GetPrevCaretStop("", 0, CaretPositioningMode.Normal));
Assert.AreEqual(0, GetPrevCaretStop("", 1, CaretPositioningMode.Normal));
Assert.AreEqual(-1, GetNextCaretStop("", -1, CaretPositioningMode.WordStart));
Assert.AreEqual(-1, GetNextCaretStop("", -1, CaretPositioningMode.WordBorder));
Assert.AreEqual(-1, GetPrevCaretStop("", 1, CaretPositioningMode.WordStart));
Assert.AreEqual(-1, GetPrevCaretStop("", 1, CaretPositioningMode.WordBorder));
}
[Test]
public void StartOfDocumentWithWordStart()
{
Assert.AreEqual(0, GetNextCaretStop("word", -1, CaretPositioningMode.Normal));
Assert.AreEqual(0, GetNextCaretStop("word", -1, CaretPositioningMode.WordStart));
Assert.AreEqual(0, GetNextCaretStop("word", -1, CaretPositioningMode.WordBorder));
Assert.AreEqual(0, GetPrevCaretStop("word", 1, CaretPositioningMode.Normal));
Assert.AreEqual(0, GetPrevCaretStop("word", 1, CaretPositioningMode.WordStart));
Assert.AreEqual(0, GetPrevCaretStop("word", 1, CaretPositioningMode.WordBorder));
}
[Test]
public void StartOfDocumentNoWordStart()
{
Assert.AreEqual(0, GetNextCaretStop(" word", -1, CaretPositioningMode.Normal));
Assert.AreEqual(1, GetNextCaretStop(" word", -1, CaretPositioningMode.WordStart));
Assert.AreEqual(1, GetNextCaretStop(" word", -1, CaretPositioningMode.WordBorder));
Assert.AreEqual(0, GetPrevCaretStop(" word", 1, CaretPositioningMode.Normal));
Assert.AreEqual(-1, GetPrevCaretStop(" word", 1, CaretPositioningMode.WordStart));
Assert.AreEqual(-1, GetPrevCaretStop(" word", 1, CaretPositioningMode.WordBorder));
}
[Test]
public void EndOfDocumentWordBorder()
{
Assert.AreEqual(4, GetNextCaretStop("word", 3, CaretPositioningMode.Normal));
Assert.AreEqual(-1, GetNextCaretStop("word", 3, CaretPositioningMode.WordStart));
Assert.AreEqual(4, GetNextCaretStop("word", 3, CaretPositioningMode.WordBorder));
Assert.AreEqual(4, GetPrevCaretStop("word", 5, CaretPositioningMode.Normal));
Assert.AreEqual(0, GetPrevCaretStop("word", 5, CaretPositioningMode.WordStart));
Assert.AreEqual(4, GetPrevCaretStop("word", 5, CaretPositioningMode.WordBorder));
}
[Test]
public void EndOfDocumentNoWordBorder()
{
Assert.AreEqual(4, GetNextCaretStop("txt ", 3, CaretPositioningMode.Normal));
Assert.AreEqual(-1, GetNextCaretStop("txt ", 3, CaretPositioningMode.WordStart));
Assert.AreEqual(-1, GetNextCaretStop("txt ", 3, CaretPositioningMode.WordBorder));
Assert.AreEqual(4, GetPrevCaretStop("txt ", 5, CaretPositioningMode.Normal));
Assert.AreEqual(0, GetPrevCaretStop("txt ", 5, CaretPositioningMode.WordStart));
Assert.AreEqual(3, GetPrevCaretStop("txt ", 5, CaretPositioningMode.WordBorder));
}
}
}

107
ICSharpCode.AvalonEdit.Tests/Utils/CompressingTreeListTests.cs

@ -0,0 +1,107 @@ @@ -0,0 +1,107 @@
// 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.Linq;
using NUnit.Framework;
namespace ICSharpCode.AvalonEdit.Utils
{
[TestFixture]
public class CompressingTreeListTests
{
[Test]
public void EmptyTreeList()
{
CompressingTreeList<string> list = new CompressingTreeList<string>(string.Equals);
Assert.AreEqual(0, list.Count);
foreach (string v in list) {
Assert.Fail();
}
string[] arr = new string[0];
list.CopyTo(arr, 0);
}
[Test]
public void CheckAdd10BillionElements()
{
const int billion = 1000000000;
CompressingTreeList<string> list = new CompressingTreeList<string>(string.Equals);
list.InsertRange(0, billion, "A");
list.InsertRange(1, billion, "B");
Assert.AreEqual(2 * billion, list.Count);
try {
list.InsertRange(2, billion, "C");
Assert.Fail("Expected OverflowException");
} catch (OverflowException) {
// expected
}
}
[Test]
public void AddRepeated()
{
CompressingTreeList<int> list = new CompressingTreeList<int>((a, b) => a == b);
list.Add(42);
list.Add(42);
list.Add(42);
list.Insert(0, 42);
list.Insert(1, 42);
Assert.AreEqual(new[] { 42, 42, 42, 42, 42 }, list.ToArray());
}
[Test]
public void RemoveRange()
{
CompressingTreeList<int> list = new CompressingTreeList<int>((a, b) => a == b);
for (int i = 1; i <= 3; i++) {
list.InsertRange(list.Count, 2, i);
}
Assert.AreEqual(new[] { 1, 1, 2, 2, 3, 3 }, list.ToArray());
list.RemoveRange(1, 4);
Assert.AreEqual(new[] { 1, 3 }, list.ToArray());
list.Insert(1, 1);
list.InsertRange(2, 2, 2);
list.Insert(4, 1);
Assert.AreEqual(new[] { 1, 1, 2, 2, 1, 3 }, list.ToArray());
list.RemoveRange(2, 2);
Assert.AreEqual(new[] { 1, 1, 1, 3 }, list.ToArray());
}
[Test]
public void RemoveAtEnd()
{
CompressingTreeList<int> list = new CompressingTreeList<int>((a, b) => a == b);
for (int i = 1; i <= 3; i++) {
list.InsertRange(list.Count, 2, i);
}
Assert.AreEqual(new[] { 1, 1, 2, 2, 3, 3 }, list.ToArray());
list.RemoveRange(3, 3);
Assert.AreEqual(new[] { 1, 1, 2 }, list.ToArray());
}
[Test]
public void RemoveAtStart()
{
CompressingTreeList<int> list = new CompressingTreeList<int>((a, b) => a == b);
for (int i = 1; i <= 3; i++) {
list.InsertRange(list.Count, 2, i);
}
Assert.AreEqual(new[] { 1, 1, 2, 2, 3, 3 }, list.ToArray());
list.RemoveRange(0, 1);
Assert.AreEqual(new[] { 1, 2, 2, 3, 3 }, list.ToArray());
}
[Test]
public void RemoveAtStart2()
{
CompressingTreeList<int> list = new CompressingTreeList<int>((a, b) => a == b);
for (int i = 1; i <= 3; i++) {
list.InsertRange(list.Count, 2, i);
}
Assert.AreEqual(new[] { 1, 1, 2, 2, 3, 3 }, list.ToArray());
list.RemoveRange(0, 3);
Assert.AreEqual(new[] { 2, 3, 3 }, list.ToArray());
}
}
}

36
ICSharpCode.AvalonEdit.Tests/Utils/ExtensionMethodsTests.cs

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
// 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 NUnit.Framework;
namespace ICSharpCode.AvalonEdit.Utils
{
[TestFixture]
public class ExtensionMethodsTests
{
[Test]
public void ZeroIsNotCloseToOne()
{
Assert.IsFalse(0.0.IsClose(1));
}
[Test]
public void ZeroIsCloseToZero()
{
Assert.IsTrue(0.0.IsClose(0));
}
[Test]
public void InfinityIsCloseToInfinity()
{
Assert.IsTrue(double.PositiveInfinity.IsClose(double.PositiveInfinity));
}
[Test]
public void NaNIsNotCloseToNaN()
{
Assert.IsFalse(double.NaN.IsClose(double.NaN));
}
}
}

36
ICSharpCode.AvalonEdit.Tests/Utils/IndentationStringTests.cs

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
// 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 NUnit.Framework;
namespace ICSharpCode.AvalonEdit.Utils
{
[TestFixture]
public class IndentationStringTests
{
[Test]
public void IndentWithSingleTab()
{
var options = new TextEditorOptions { IndentationSize = 4, ConvertTabsToSpaces = false };
Assert.AreEqual("\t", options.IndentationString);
Assert.AreEqual("\t", options.GetIndentationString(2));
Assert.AreEqual("\t", options.GetIndentationString(3));
Assert.AreEqual("\t", options.GetIndentationString(4));
Assert.AreEqual("\t", options.GetIndentationString(5));
Assert.AreEqual("\t", options.GetIndentationString(6));
}
[Test]
public void IndentWith4Spaces()
{
var options = new TextEditorOptions { IndentationSize = 4, ConvertTabsToSpaces = true };
Assert.AreEqual(" ", options.IndentationString);
Assert.AreEqual(" ", options.GetIndentationString(2));
Assert.AreEqual(" ", options.GetIndentationString(3));
Assert.AreEqual(" ", options.GetIndentationString(4));
Assert.AreEqual(" ", options.GetIndentationString(5));
Assert.AreEqual(" ", options.GetIndentationString(6));
}
}
}

180
ICSharpCode.AvalonEdit.Tests/Utils/RopeTests.cs

@ -0,0 +1,180 @@ @@ -0,0 +1,180 @@
// 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.IO;
using NUnit.Framework;
using System.Text;
namespace ICSharpCode.AvalonEdit.Utils
{
[TestFixture]
public class RopeTests
{
[Test]
public void EmptyRope()
{
Rope<char> empty = new Rope<char>();
Assert.AreEqual(0, empty.Length);
Assert.AreEqual("", empty.ToString());
}
[Test]
public void EmptyRopeFromString()
{
Rope<char> empty = new Rope<char>(string.Empty);
Assert.AreEqual(0, empty.Length);
Assert.AreEqual("", empty.ToString());
}
[Test]
public void InitializeRopeFromShortString()
{
Rope<char> rope = new Rope<char>("Hello, World");
Assert.AreEqual(12, rope.Length);
Assert.AreEqual("Hello, World", rope.ToString());
}
string BuildLongString(int lines)
{
StringWriter w = new StringWriter();
w.NewLine = "\n";
for (int i = 1; i <= lines; i++) {
w.WriteLine(i.ToString());
}
return w.ToString();
}
[Test]
public void InitializeRopeFromLongString()
{
string text = BuildLongString(1000);
Rope<char> rope = new Rope<char>(text);
Assert.AreEqual(text.Length, rope.Length);
Assert.AreEqual(text, rope.ToString());
Assert.AreEqual(text.ToCharArray(), rope.ToArray());
}
[Test]
public void TestToArrayAndToStringWithParts()
{
string text = BuildLongString(1000);
Rope<char> rope = new Rope<char>(text);
string textPart = text.Substring(1200, 600);
char[] arrayPart = textPart.ToCharArray();
Assert.AreEqual(textPart, rope.ToString(1200, 600));
Assert.AreEqual(arrayPart, rope.ToArray(1200, 600));
Rope<char> partialRope = rope.GetRange(1200, 600);
Assert.AreEqual(textPart, partialRope.ToString());
Assert.AreEqual(arrayPart, partialRope.ToArray());
}
[Test]
public void ConcatenateStringToRope()
{
StringBuilder b = new StringBuilder();
Rope<char> rope = new Rope<char>();
for (int i = 1; i <= 1000; i++) {
b.Append(i.ToString());
rope.AddText(i.ToString());
b.Append(' ');
rope.Add(' ');
}
Assert.AreEqual(b.ToString(), rope.ToString());
}
[Test]
public void ConcatenateSmallRopesToRope()
{
StringBuilder b = new StringBuilder();
Rope<char> rope = new Rope<char>();
for (int i = 1; i <= 1000; i++) {
b.Append(i.ToString());
b.Append(' ');
rope.AddRange(CharRope.Create(i.ToString() + " "));
}
Assert.AreEqual(b.ToString(), rope.ToString());
}
[Test]
public void AppendLongTextToEmptyRope()
{
string text = BuildLongString(1000);
Rope<char> rope = new Rope<char>();
rope.AddText(text);
Assert.AreEqual(text, rope.ToString());
}
[Test]
public void ConcatenateStringToRopeBackwards()
{
StringBuilder b = new StringBuilder();
Rope<char> rope = new Rope<char>();
for (int i = 1; i <= 1000; i++) {
b.Append(i.ToString());
b.Append(' ');
}
for (int i = 1000; i >= 1; i--) {
rope.Insert(0, ' ');
rope.InsertText(0, i.ToString());
}
Assert.AreEqual(b.ToString(), rope.ToString());
}
[Test]
public void ConcatenateSmallRopesToRopeBackwards()
{
StringBuilder b = new StringBuilder();
Rope<char> rope = new Rope<char>();
for (int i = 1; i <= 1000; i++) {
b.Append(i.ToString());
b.Append(' ');
}
for (int i = 1000; i >= 1; i--) {
rope.InsertRange(0, CharRope.Create(i.ToString() + " "));
}
Assert.AreEqual(b.ToString(), rope.ToString());
}
[Test]
public void ConcatenateStringToRopeByInsertionInMiddle()
{
StringBuilder b = new StringBuilder();
Rope<char> rope = new Rope<char>();
for (int i = 1; i <= 998; i++) {
b.Append(i.ToString("d3"));
b.Append(' ');
}
int middle = 0;
for (int i = 1; i <= 499; i++) {
rope.InsertText(middle, i.ToString("d3"));
middle += 3;
rope.Insert(middle, ' ');
middle++;
rope.InsertText(middle, (999-i).ToString("d3"));
rope.Insert(middle + 3, ' ');
}
Assert.AreEqual(b.ToString(), rope.ToString());
}
[Test]
public void ConcatenateSmallRopesByInsertionInMiddle()
{
StringBuilder b = new StringBuilder();
Rope<char> rope = new Rope<char>();
for (int i = 1; i <= 1000; i++) {
b.Append(i.ToString("d3"));
b.Append(' ');
}
int middle = 0;
for (int i = 1; i <= 500; i++) {
rope.InsertRange(middle, CharRope.Create(i.ToString("d3") + " "));
middle += 4;
rope.InsertRange(middle, CharRope.Create((1001-i).ToString("d3") + " "));
}
Assert.AreEqual(b.ToString(), rope.ToString());
}
}
}

110
ICSharpCode.AvalonEdit.Tests/WeakReferenceTests.cs

@ -0,0 +1,110 @@ @@ -0,0 +1,110 @@
// 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 ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Editing;
using ICSharpCode.AvalonEdit.Rendering;
using NUnit.Framework;
namespace ICSharpCode.AvalonEdit
{
[TestFixture]
public class WeakReferenceTests
{
[Test]
public void TextViewCanBeCollectedTest()
{
TextView textView = new TextView();
WeakReference wr = new WeakReference(textView);
textView = null;
GarbageCollect();
Assert.IsFalse(wr.IsAlive);
}
[Test]
public void DocumentDoesNotHoldReferenceToTextView()
{
TextDocument textDocument = new TextDocument();
Assert.AreEqual(0, textDocument.LineTrackers.Count);
TextView textView = new TextView();
WeakReference wr = new WeakReference(textView);
textView.Document = textDocument;
Assert.AreEqual(1, textDocument.LineTrackers.Count);
textView = null;
GarbageCollect();
Assert.IsFalse(wr.IsAlive);
// document cannot immediately clear the line tracker
Assert.AreEqual(1, textDocument.LineTrackers.Count);
// but it should clear it on the next change
textDocument.Insert(0, "a");
Assert.AreEqual(0, textDocument.LineTrackers.Count);
}
[Test]
[Ignore]
public void DocumentDoesNotHoldReferenceToTextArea()
{
TextDocument textDocument = new TextDocument();
TextArea textArea = new TextArea();
WeakReference wr = new WeakReference(textArea);
textArea.Document = textDocument;
textArea = null;
GarbageCollect();
Assert.IsFalse(wr.IsAlive);
GC.KeepAlive(textDocument);
}
[Test]
[Ignore]
public void DocumentDoesNotHoldReferenceToTextEditor()
{
TextDocument textDocument = new TextDocument();
TextEditor textEditor = new TextEditor();
WeakReference wr = new WeakReference(textEditor);
textEditor.Document = textDocument;
textEditor = null;
GarbageCollect();
Assert.IsFalse(wr.IsAlive);
GC.KeepAlive(textDocument);
}
[Test]
public void DocumentDoesNotHoldReferenceToLineMargin()
{
TextDocument textDocument = new TextDocument();
WeakReference wr = DocumentDoesNotHoldReferenceToLineMargin_CreateMargin(textDocument);
GarbageCollect();
Assert.IsFalse(wr.IsAlive);
GC.KeepAlive(textDocument);
}
// using a method to ensure the local variables can be garbage collected after the method returns
WeakReference DocumentDoesNotHoldReferenceToLineMargin_CreateMargin(TextDocument textDocument)
{
TextView textView = new TextView() {
Document = textDocument
};
LineNumberMargin margin = new LineNumberMargin() {
TextView = textView
};
return new WeakReference(textView);
}
static void GarbageCollect()
{
GC.WaitForPendingFinalizers();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
}
}
}

223
ICSharpCode.AvalonEdit.Tests/XmlParser/ParserTests.cs

@ -0,0 +1,223 @@ @@ -0,0 +1,223 @@
// 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.Linq;
using System.Diagnostics;
using System.IO;
using System.Text;
using ICSharpCode.AvalonEdit.Xml;
using ICSharpCode.SharpZipLib.Zip;
using NUnit.Framework;
namespace ICSharpCode.AvalonEdit.Xml
{
class TestFile
{
public string Name { get; set; }
public string Content { get; set; }
public string Canonical { get; set; }
public string Description { get; set; }
}
[TestFixture]
public class ParserTests
{
readonly string zipFileName = @"XmlParser\W3C.zip";
List<TestFile> xmlFiles = new List<TestFile>();
[TestFixtureSetUp]
public void OpenZipFile()
{
ZipFile zipFile = new ZipFile(zipFileName);
Dictionary<string, TestFile> xmlFiles = new Dictionary<string, TestFile>();
// Decompress XML files
foreach(ZipEntry zipEntry in zipFile.Cast<ZipEntry>().Where(zip => zip.IsFile && zip.Name.EndsWith(".xml"))) {
Stream stream = zipFile.GetInputStream(zipEntry);
string content = new StreamReader(stream).ReadToEnd();
xmlFiles.Add(zipEntry.Name, new TestFile { Name = zipEntry.Name, Content = content });
}
// Add descriptions
foreach(TestFile metaData in xmlFiles.Values.Where(f => f.Name.StartsWith("ibm/ibm_oasis"))) {
var doc = System.Xml.Linq.XDocument.Parse(metaData.Content);
foreach(var testElem in doc.Descendants("TEST")) {
string uri = "ibm/" + testElem.Attribute("URI").Value;
string description = testElem.Value.Replace("\n ", "\n").TrimStart('\n');
if (xmlFiles.ContainsKey(uri))
xmlFiles[uri].Description = description;
}
}
// Copy canonical forms
foreach(TestFile canonical in xmlFiles.Values.Where(f => f.Name.Contains("/out/"))) {
string uri = canonical.Name.Replace("/out/", "/");
if (xmlFiles.ContainsKey(uri))
xmlFiles[uri].Canonical = canonical.Content;
}
// Copy resuts to field
this.xmlFiles.AddRange(xmlFiles.Values.Where(f => !f.Name.Contains("/out/")));
}
IEnumerable<TestFile> GetXmlFilesStartingWith(string directory)
{
return xmlFiles.Where(f => f.Name.StartsWith(directory));
}
[Test]
public void W3C_Valid()
{
string[] exclude = {
// NAME in DTD infoset
"ibm02v01", "ibm03v01", "ibm85v01", "ibm86v01", "ibm87v01", "ibm88v01", "ibm89v01",
};
TestFiles(GetXmlFilesStartingWith("ibm/valid/"), true, exclude);
}
[Test]
public void W3C_Invalid()
{
string[] exclude = {
// Default attribute value
"ibm56i03",
};
TestFiles(GetXmlFilesStartingWith("ibm/invalid/"), true, exclude);
}
[Test]
public void W3C_NotWellformed()
{
string[] exclude = {
// XML declaration well formed
"ibm23n", "ibm24n", "ibm26n01", "ibm32n", "ibm80n06", "ibm81n01", "ibm81n02", "ibm81n03", "ibm81n04", "ibm81n05", "ibm81n06", "ibm81n07", "ibm81n08", "ibm81n09",
// Invalid chars in a comment - do we care?
"ibm02n",
// Invalid char ref - do we care?
"ibm66n12", "ibm66n13", "ibm66n14", "ibm66n15",
// DTD in wrong location
"ibm27n01", "ibm43n",
// Entity refs depending on DTD
"ibm41n10", "ibm41n11", "ibm41n12", "ibm41n13", "ibm41n14", "ibm68n04", "ibm68n06", "ibm68n07", "ibm68n08", "ibm68n09", "ibm68n10",
// DTD Related tests
"ibm09n01", "ibm09n02", "ibm13n01", "ibm13n02", "ibm13n03", "ibm28n01", "ibm28n02", "ibm28n03", "ibm29n01", "ibm29n03", "ibm29n04", "ibm29n07", "ibm30n01", "ibm31n01", "ibm45n01", "ibm45n02", "ibm45n03", "ibm45n04", "ibm45n05", "ibm45n06", "ibm46n01", "ibm46n02", "ibm46n03", "ibm46n04",
"ibm46n05", "ibm47n01", "ibm47n02", "ibm47n03", "ibm47n04", "ibm47n05", "ibm47n06", "ibm48n01", "ibm48n02", "ibm48n03", "ibm48n04", "ibm48n05", "ibm48n06", "ibm48n07", "ibm49n01", "ibm49n02", "ibm49n03", "ibm49n04", "ibm49n05", "ibm49n06", "ibm50n01", "ibm50n02", "ibm50n03", "ibm50n04",
"ibm50n05", "ibm50n06", "ibm50n07", "ibm51n01", "ibm51n02", "ibm51n03", "ibm51n04", "ibm51n05", "ibm51n06", "ibm51n07", "ibm52n01", "ibm52n02", "ibm52n03", "ibm53n01", "ibm53n02", "ibm53n03", "ibm53n04", "ibm53n05", "ibm53n06", "ibm53n07", "ibm53n08", "ibm54n01", "ibm54n02", "ibm55n01",
"ibm55n02", "ibm55n03", "ibm56n01", "ibm56n02", "ibm56n03", "ibm56n04", "ibm56n05", "ibm56n06", "ibm56n07", "ibm57n01", "ibm58n01", "ibm58n02", "ibm58n03", "ibm58n04", "ibm58n05", "ibm58n06", "ibm58n07", "ibm58n08", "ibm59n01", "ibm59n02", "ibm59n03", "ibm59n04", "ibm59n05", "ibm59n06",
"ibm60n01", "ibm60n02", "ibm60n03", "ibm60n04", "ibm60n05", "ibm60n06", "ibm60n07", "ibm60n08", "ibm61n01", "ibm62n01", "ibm62n02", "ibm62n03", "ibm62n04", "ibm62n05", "ibm62n06", "ibm62n07", "ibm62n08", "ibm63n01", "ibm63n02", "ibm63n03", "ibm63n04", "ibm63n05", "ibm63n06", "ibm63n07",
"ibm64n01", "ibm64n02", "ibm64n03", "ibm65n01", "ibm65n02", "ibm66n01", "ibm66n03", "ibm66n05", "ibm66n07", "ibm66n09", "ibm66n11", "ibm69n01", "ibm69n02", "ibm69n03", "ibm69n04", "ibm69n05", "ibm69n06", "ibm69n07", "ibm70n01", "ibm71n01", "ibm71n02", "ibm71n03", "ibm71n04", "ibm71n05",
"ibm72n01", "ibm72n02", "ibm72n03", "ibm72n04", "ibm72n05", "ibm72n06", "ibm72n09", "ibm73n01", "ibm73n03", "ibm74n01", "ibm75n01", "ibm75n02", "ibm75n03", "ibm75n04", "ibm75n05", "ibm75n06", "ibm75n07", "ibm75n08", "ibm75n09", "ibm75n10", "ibm75n11", "ibm75n12", "ibm75n13", "ibm76n01",
"ibm76n02", "ibm76n03", "ibm76n04", "ibm76n05", "ibm76n06", "ibm76n07", "ibm77n01", "ibm77n02", "ibm77n03", "ibm77n04", "ibm78n01", "ibm78n02", "ibm79n01", "ibm79n02", "ibm82n01", "ibm82n02", "ibm82n03", "ibm82n04", "ibm82n08", "ibm83n01", "ibm83n03", "ibm83n04", "ibm83n05", "ibm83n06",
// No idea what this is
"misc/432gewf", "ibm28an01",
};
TestFiles(GetXmlFilesStartingWith("ibm/not-wf/"), false, exclude);
}
StringBuilder errorOutput;
void TestFiles(IEnumerable<TestFile> files, bool areWellFormed, string[] exclude)
{
errorOutput = new StringBuilder();
int testsRun = 0;
int ignored = 0;
foreach (TestFile file in files) {
if (exclude.Any(exc => file.Name.Contains(exc))) {
ignored++;
} else {
testsRun++;
TestFile(file, areWellFormed);
}
}
if (testsRun == 0) {
Assert.Fail("Test files not found");
}
if (errorOutput.Length > 0) {
// Can not output ]]> otherwise nuint will crash
Assert.Fail(errorOutput.Replace("]]>", "]]~NUNIT~>").ToString());
}
}
/// <remarks>
/// If using DTD, canonical representation is not checked
/// If using DTD, uknown entiry references are not error
/// </remarks>
bool TestFile(TestFile testFile, bool isWellFormed)
{
bool passed = true;
string content = testFile.Content;
Debug.WriteLine("Testing " + testFile.Name + "...");
AXmlParser parser = new AXmlParser();
bool usingDTD = content.Contains("<!DOCTYPE") && (content.Contains("<!ENTITY") || content.Contains(" SYSTEM "));
if (usingDTD)
parser.UnknownEntityReferenceIsError = false;
AXmlDocument document;
parser.Lock.EnterWriteLock();
try {
document = parser.Parse(content, null);
} finally {
parser.Lock.ExitWriteLock();
}
string printed = PrettyPrintAXmlVisitor.PrettyPrint(document);
if (content != printed) {
errorOutput.AppendFormat("Output of pretty printed XML for \"{0}\" does not match the original.\n", testFile.Name);
errorOutput.AppendFormat("Pretty printed:\n{0}\n", Indent(printed));
passed = false;
}
if (isWellFormed && !usingDTD) {
string canonicalPrint = CanonicalPrintAXmlVisitor.Print(document);
if (testFile.Canonical != null) {
if (testFile.Canonical != canonicalPrint) {
errorOutput.AppendFormat("Canonical XML for \"{0}\" does not match the excpected.\n", testFile.Name);
errorOutput.AppendFormat("Expected:\n{0}\n", Indent(testFile.Canonical));
errorOutput.AppendFormat("Seen:\n{0}\n", Indent(canonicalPrint));
passed = false;
}
} else {
errorOutput.AppendFormat("Can not find canonical output for \"{0}\"", testFile.Name);
errorOutput.AppendFormat("Suggested canonical output:\n{0}\n", Indent(canonicalPrint));
passed = false;
}
}
bool hasErrors = document.SyntaxErrors.FirstOrDefault() != null;
if (isWellFormed && hasErrors) {
errorOutput.AppendFormat("Syntax error(s) in well formed file \"{0}\":\n", testFile.Name);
foreach (var error in document.SyntaxErrors) {
string followingText = content.Substring(error.StartOffset, Math.Min(10, content.Length - error.StartOffset));
errorOutput.AppendFormat("Error ({0}-{1}): {2} (followed by \"{3}\")\n", error.StartOffset, error.EndOffset, error.Message, followingText);
}
passed = false;
}
if (!isWellFormed && !hasErrors) {
errorOutput.AppendFormat("No syntax errors reported for mallformed file \"{0}\"\n", testFile.Name);
passed = false;
}
// Epilog
if (!passed) {
if (testFile.Description != null) {
errorOutput.AppendFormat("Test description:\n{0}\n", Indent(testFile.Description));
}
errorOutput.AppendFormat("File content:\n{0}\n", Indent(content));
errorOutput.AppendLine();
}
return passed;
}
string Indent(string text)
{
return " " + text.TrimEnd().Replace("\n", "\n ");
}
}
}

115
ICSharpCode.AvalonEdit.Tests/XmlParser/TextReplacementTests.cs

@ -0,0 +1,115 @@ @@ -0,0 +1,115 @@
// 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 ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Xml;
using NUnit.Framework;
namespace ICSharpCode.AvalonEdit.Xml
{
[TestFixture]
public class TextReplacementTests
{
#region Test Data
string initialDocumentText = @"<UserControl x:Class='ICSharpCode.Profiler.Controls.TimeLineCell'
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Grid>
</Grid>
</UserControl>";
string finalDocumentText = @"<UserControl x:Class='ICSharpCode.Profiler.Controls.TimeLineCell'
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height='20' />
<RowDefinition Height='20' />
<RowDefinition Height='Auto' />
</Grid.RowDefinitions>
<StackPanel Orientation='Horizontal'>
<TextBlock Text='Test' />
</StackPanel>
<local:TimeLineControl x:Name='t1' Grid.Row='1' />
<TextBlock Grid.Row='2' Text='Test' />
</Grid>
</UserControl>";
int offset = @"<UserControl x:Class='ICSharpCode.Profiler.Controls.TimeLineCell'
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
".Length;
string original = @" <Grid>
</Grid>";
string replacement = @" <Grid>
<Grid.RowDefinitions>
<RowDefinition Height='20' />
<RowDefinition Height='20' />
<RowDefinition Height='Auto' />
</Grid.RowDefinitions>
<StackPanel Orientation='Horizontal'>
<TextBlock Text='Test' />
</StackPanel>
<local:TimeLineControl x:Name='t1' Grid.Row='1' />
<TextBlock Grid.Row='2' Text='Test' />
</Grid>";
#endregion
[Test]
public void ReplacementTest1()
{
/*
* REPRODUCTION STEPS
*
* 1. Run XmlDOM project
* 2. paste text from initialDocumentText (see Test Data region)
* 3. select lines 4 to 6
* 4. replace with replacement (see Test Data region)
* 5. exception thrown:
* ICSharpCode.AvalonEdit.Xml.InternalException : Assertion failed: cached elements must not have zero length
* at ICSharpCode.AvalonEdit.Xml.AXmlParser.Assert(Boolean condition, String message)
* in c:\Projects\SharpDevelop\4.0\SharpDevelop\src\Libraries\AvalonEdit\ICSharpCode.AvalonEdit\Xml\AXmlParser.cs:line 121
* at ICSharpCode.AvalonEdit.Xml.TagReader.TryReadFromCacheOrNew[T](T& res, Predicate`1 condition)
* in c:\Projects\SharpDevelop\4.0\SharpDevelop\src\Libraries\AvalonEdit\ICSharpCode.AvalonEdit\Xml\TagReader.cs:line 39
* at ICSharpCode.AvalonEdit.Xml.TagReader.<ReadText>d__12.MoveNext()
* in c:\Projects\SharpDevelop\4.0\SharpDevelop\src\Libraries\AvalonEdit\ICSharpCode.AvalonEdit\Xml\TagReader.cs:line 456
* at System.Collections.Generic.List`1.InsertRange(Int32 index, IEnumerable`1 collection)
* at System.Collections.Generic.List`1.AddRange(IEnumerable`1 collection)
* at ICSharpCode.AvalonEdit.Xml.TagReader.ReadAllTags()
* in c:\Projects\SharpDevelop\4.0\SharpDevelop\src\Libraries\AvalonEdit\ICSharpCode.AvalonEdit\Xml\TagReader.cs:line 73
* at ICSharpCode.AvalonEdit.Xml.AXmlParser.Parse(String input, IEnumerable`1 changesSinceLastParse)
* in c:\Projects\SharpDevelop\4.0\SharpDevelop\src\Libraries\AvalonEdit\ICSharpCode.AvalonEdit\Xml\AXmlParser.cs:line 161
* at ICSharpCode.AvalonEdit.Tests.XmlParser.TextReplacementTests.RunTest()
* in c:\Projects\SharpDevelop\4.0\SharpDevelop\src\Libraries\AvalonEdit\ICSharpCode.AvalonEdit.Tests\XmlParser\TextReplacementTests.cs:line 114
* at ICSharpCode.AvalonEdit.Tests.XmlParser.TextReplacementTests.TestMethod(
* ) in c:\Projects\SharpDevelop\4.0\SharpDevelop\src\Libraries\AvalonEdit\ICSharpCode.AvalonEdit.Tests\XmlParser\TextReplacementTests.cs:line 97
* */
Assert.DoesNotThrow(RunTest1);
}
void RunTest1()
{
AXmlParser parser = new AXmlParser();
try {
parser.Lock.EnterWriteLock();
parser.Parse(initialDocumentText, null); // full reparse
IList<DocumentChangeEventArgs> changes = new List<DocumentChangeEventArgs>();
changes.Add(new DocumentChangeEventArgs(offset, original, replacement));
parser.Parse(finalDocumentText, changes);
} finally {
parser.Lock.ExitWriteLock();
}
}
}
}

BIN
ICSharpCode.AvalonEdit.Tests/XmlParser/W3C.zip

Binary file not shown.

15
ICSharpCode.AvalonEdit.Tests/app.config

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="NUnit">
<section name="TestRunner"
type="System.Configuration.NameValueSectionHandler" />
</sectionGroup>
</configSections>
<NUnit>
<TestRunner>
<!-- Valid values are STA,MTA. Others ignored. -->
<add key="ApartmentState" value="STA" />
</TestRunner>
</NUnit>
</configuration>

87
ICSharpCode.AvalonEdit/AvalonEditCommands.cs

@ -0,0 +1,87 @@ @@ -0,0 +1,87 @@
// 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.Windows.Input;
namespace ICSharpCode.AvalonEdit
{
/// <summary>
/// Custom commands for AvalonEdit.
/// </summary>
public static class AvalonEditCommands
{
/// <summary>
/// Deletes the current line.
/// The default shortcut is Ctrl+D.
/// </summary>
public static readonly RoutedCommand DeleteLine = new RoutedCommand(
"DeleteLine", typeof(TextEditor),
new InputGestureCollection {
new KeyGesture(Key.D, ModifierKeys.Control)
});
/// <summary>
/// Removes leading whitespace from the selected lines (or the whole document if the selection is empty).
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace",
Justification = "WPF uses 'Whitespace'")]
public static readonly RoutedCommand RemoveLeadingWhitespace = new RoutedCommand("RemoveLeadingWhitespace", typeof(TextEditor));
/// <summary>
/// Removes trailing whitespace from the selected lines (or the whole document if the selection is empty).
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace",
Justification = "WPF uses 'Whitespace'")]
public static readonly RoutedCommand RemoveTrailingWhitespace = new RoutedCommand("RemoveTrailingWhitespace", typeof(TextEditor));
/// <summary>
/// Converts the selected text to upper case.
/// </summary>
public static readonly RoutedCommand ConvertToUppercase = new RoutedCommand("ConvertToUppercase", typeof(TextEditor));
/// <summary>
/// Converts the selected text to lower case.
/// </summary>
public static readonly RoutedCommand ConvertToLowercase = new RoutedCommand("ConvertToLowercase", typeof(TextEditor));
/// <summary>
/// Converts the selected text to title case.
/// </summary>
public static readonly RoutedCommand ConvertToTitleCase = new RoutedCommand("ConvertToTitleCase", typeof(TextEditor));
/// <summary>
/// Inverts the case of the selected text.
/// </summary>
public static readonly RoutedCommand InvertCase = new RoutedCommand("InvertCase", typeof(TextEditor));
/// <summary>
/// Converts tabs to spaces in the selected text.
/// </summary>
public static readonly RoutedCommand ConvertTabsToSpaces = new RoutedCommand("ConvertTabsToSpaces", typeof(TextEditor));
/// <summary>
/// Converts spaces to tabs in the selected text.
/// </summary>
public static readonly RoutedCommand ConvertSpacesToTabs = new RoutedCommand("ConvertSpacesToTabs", typeof(TextEditor));
/// <summary>
/// Converts leading tabs to spaces in the selected lines (or the whole document if the selection is empty).
/// </summary>
public static readonly RoutedCommand ConvertLeadingTabsToSpaces = new RoutedCommand("ConvertLeadingTabsToSpaces", typeof(TextEditor));
/// <summary>
/// Converts leading spaces to tabs in the selected lines (or the whole document if the selection is empty).
/// </summary>
public static readonly RoutedCommand ConvertLeadingSpacesToTabs = new RoutedCommand("ConvertLeadingSpacesToTabs", typeof(TextEditor));
/// <summary>
/// Runs the IIndentationStrategy on the selected lines (or the whole document if the selection is empty).
/// </summary>
public static readonly RoutedCommand IndentSelection = new RoutedCommand(
"IndentSelection", typeof(TextEditor),
new InputGestureCollection {
new KeyGesture(Key.I, ModifierKeys.Control)
});
}
}

374
ICSharpCode.AvalonEdit/CodeCompletion/CompletionList.cs

@ -0,0 +1,374 @@ @@ -0,0 +1,374 @@
// 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.Collections.ObjectModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Linq;
namespace ICSharpCode.AvalonEdit.CodeCompletion
{
/// <summary>
/// The listbox used inside the CompletionWindow, contains CompletionListBox.
/// </summary>
public class CompletionList : Control
{
static CompletionList()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CompletionList),
new FrameworkPropertyMetadata(typeof(CompletionList)));
}
bool isFiltering = true;
/// <summary>
/// If true, the CompletionList is filtered to show only matching items. Also enables search by substring.
/// If false, enables the old behavior: no filtering, search by string.StartsWith.
/// </summary>
public bool IsFiltering {
get { return isFiltering; }
set { isFiltering = value; }
}
/// <summary>
/// Dependency property for <see cref="EmptyTemplate" />.
/// </summary>
public static readonly DependencyProperty EmptyTemplateProperty =
DependencyProperty.Register("EmptyTemplate", typeof(ControlTemplate), typeof(CompletionList),
new FrameworkPropertyMetadata());
/// <summary>
/// Content of EmptyTemplate will be shown when CompletionList contains no items.
/// If EmptyTemplate is null, nothing will be shown.
/// </summary>
public ControlTemplate EmptyTemplate {
get { return (ControlTemplate)GetValue(EmptyTemplateProperty); }
set { SetValue(EmptyTemplateProperty, value); }
}
/// <summary>
/// Is raised when the completion list indicates that the user has chosen
/// an entry to be completed.
/// </summary>
public event EventHandler InsertionRequested;
/// <summary>
/// Raises the InsertionRequested event.
/// </summary>
public void RequestInsertion(EventArgs e)
{
if (InsertionRequested != null)
InsertionRequested(this, e);
}
CompletionListBox listBox;
/// <inheritdoc/>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
listBox = GetTemplateChild("PART_ListBox") as CompletionListBox;
if (listBox != null) {
listBox.ItemsSource = completionData;
}
}
/// <summary>
/// Gets the list box.
/// </summary>
public CompletionListBox ListBox {
get {
if (listBox == null)
ApplyTemplate();
return listBox;
}
}
/// <summary>
/// Gets the scroll viewer used in this list box.
/// </summary>
public ScrollViewer ScrollViewer {
get { return listBox != null ? listBox.scrollViewer : null; }
}
ObservableCollection<ICompletionData> completionData = new ObservableCollection<ICompletionData>();
/// <summary>
/// Gets the list to which completion data can be added.
/// </summary>
public IList<ICompletionData> CompletionData {
get { return completionData; }
}
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (!e.Handled) {
HandleKey(e);
}
}
/// <summary>
/// Handles a key press. Used to let the completion list handle key presses while the
/// focus is still on the text editor.
/// </summary>
public void HandleKey(KeyEventArgs e)
{
if (listBox == null)
return;
// We have to do some key handling manually, because the default doesn't work with
// our simulated events.
// Also, the default PageUp/PageDown implementation changes the focus, so we avoid it.
switch (e.Key) {
case Key.Down:
e.Handled = true;
listBox.SelectIndex(listBox.SelectedIndex + 1);
break;
case Key.Up:
e.Handled = true;
listBox.SelectIndex(listBox.SelectedIndex - 1);
break;
case Key.PageDown:
e.Handled = true;
listBox.SelectIndex(listBox.SelectedIndex + listBox.VisibleItemCount);
break;
case Key.PageUp:
e.Handled = true;
listBox.SelectIndex(listBox.SelectedIndex - listBox.VisibleItemCount);
break;
case Key.Home:
e.Handled = true;
listBox.SelectIndex(0);
break;
case Key.End:
e.Handled = true;
listBox.SelectIndex(listBox.Items.Count - 1);
break;
case Key.Tab:
case Key.Enter:
e.Handled = true;
RequestInsertion(e);
break;
}
}
/// <inheritdoc/>
protected override void OnMouseDoubleClick(MouseButtonEventArgs e)
{
base.OnMouseDoubleClick(e);
if (e.ChangedButton == MouseButton.Left) {
e.Handled = true;
RequestInsertion(e);
}
}
/// <summary>
/// Gets/Sets the selected item.
/// </summary>
public ICompletionData SelectedItem {
get {
return (listBox != null ? listBox.SelectedItem : null) as ICompletionData;
}
set {
if (listBox == null && value != null)
ApplyTemplate();
listBox.SelectedItem = value;
}
}
/// <summary>
/// Occurs when the SelectedItem property changes.
/// </summary>
public event SelectionChangedEventHandler SelectionChanged {
add { AddHandler(Selector.SelectionChangedEvent, value); }
remove { RemoveHandler(Selector.SelectionChangedEvent, value); }
}
// SelectItem gets called twice for every typed character (once from FormatLine), this helps execute SelectItem only once
string currentText;
ObservableCollection<ICompletionData> currentList;
/// <summary>
/// Selects the best match, and filter the items if turned on using <see cref="IsFiltering" />.
/// </summary>
public void SelectItem(string text)
{
if (text == currentText)
return;
if (listBox == null)
ApplyTemplate();
if (this.IsFiltering) {
SelectItemFiltering(text);
}
else {
SelectItemWithStart(text);
}
currentText = text;
}
/// <summary>
/// Filters CompletionList items to show only those matching given query, and selects the best match.
/// </summary>
void SelectItemFiltering(string query)
{
// if the user just typed one more character, don't filter all data but just filter what we are already displaying
var listToFilter = (this.currentList != null && (!string.IsNullOrEmpty(this.currentText)) && (!string.IsNullOrEmpty(query)) &&
query.StartsWith(this.currentText, StringComparison.Ordinal)) ?
this.currentList : this.completionData;
var matchingItems =
from item in listToFilter
let quality = GetMatchQuality(item.Text, query)
where quality > 0
select new { Item = item, Quality = quality };
// e.g. "DateTimeKind k = (*cc here suggests DateTimeKind*)"
ICompletionData suggestedItem = listBox.SelectedIndex != -1 ? (ICompletionData)(listBox.Items[listBox.SelectedIndex]) : null;
var listBoxItems = new ObservableCollection<ICompletionData>();
int bestIndex = -1;
int bestQuality = -1;
double bestPriority = 0;
int i = 0;
foreach (var matchingItem in matchingItems) {
double priority = matchingItem.Item == suggestedItem ? double.PositiveInfinity : matchingItem.Item.Priority;
int quality = matchingItem.Quality;
if (quality > bestQuality || (quality == bestQuality && (priority > bestPriority))) {
bestIndex = i;
bestPriority = priority;
bestQuality = quality;
}
listBoxItems.Add(matchingItem.Item);
i++;
}
this.currentList = listBoxItems;
listBox.ItemsSource = listBoxItems;
SelectIndexCentered(bestIndex);
}
/// <summary>
/// Selects the item that starts with the specified query.
/// </summary>
void SelectItemWithStart(string query)
{
if (string.IsNullOrEmpty(query))
return;
int suggestedIndex = listBox.SelectedIndex;
int bestIndex = -1;
int bestQuality = -1;
double bestPriority = 0;
for (int i = 0; i < completionData.Count; ++i) {
int quality = GetMatchQuality(completionData[i].Text, query);
if (quality < 0)
continue;
double priority = completionData[i].Priority;
bool useThisItem;
if (bestQuality < quality) {
useThisItem = true;
} else {
if (bestIndex == suggestedIndex) {
useThisItem = false;
} else if (i == suggestedIndex) {
// prefer recommendedItem, regardless of its priority
useThisItem = bestQuality == quality;
} else {
useThisItem = bestQuality == quality && bestPriority < priority;
}
}
if (useThisItem) {
bestIndex = i;
bestPriority = priority;
bestQuality = quality;
}
}
SelectIndexCentered(bestIndex);
}
void SelectIndexCentered(int bestIndex)
{
if (bestIndex < 0) {
listBox.ClearSelection();
} else {
int firstItem = listBox.FirstVisibleItem;
if (bestIndex < firstItem || firstItem + listBox.VisibleItemCount <= bestIndex) {
// CenterViewOn does nothing as CompletionListBox.ScrollViewer is null
listBox.CenterViewOn(bestIndex);
listBox.SelectIndex(bestIndex);
} else {
listBox.SelectIndex(bestIndex);
}
}
}
int GetMatchQuality(string itemText, string query)
{
// Qualities:
// 8 = full match case sensitive
// 7 = full match
// 6 = match start case sensitive
// 5 = match start
// 4 = match CamelCase when length of query is 1 or 2 characters
// 3 = match substring case sensitive
// 2 = match sustring
// 1 = match CamelCase
// -1 = no match
if (query == itemText)
return 8;
if (string.Equals(itemText, query, StringComparison.InvariantCultureIgnoreCase))
return 7;
if (itemText.StartsWith(query, StringComparison.InvariantCulture))
return 6;
if (itemText.StartsWith(query, StringComparison.InvariantCultureIgnoreCase))
return 5;
bool? camelCaseMatch = null;
if (query.Length <= 2) {
camelCaseMatch = CamelCaseMatch(itemText, query);
if (camelCaseMatch == true) return 4;
}
// search by substring, if filtering (i.e. new behavior) turned on
if (IsFiltering) {
if (itemText.IndexOf(query, StringComparison.InvariantCulture) >= 0)
return 3;
if (itemText.IndexOf(query, StringComparison.InvariantCultureIgnoreCase) >= 0)
return 2;
}
if (!camelCaseMatch.HasValue)
camelCaseMatch = CamelCaseMatch(itemText, query);
if (camelCaseMatch == true)
return 1;
return -1;
}
static bool CamelCaseMatch(string text, string query)
{
int i = 0;
foreach (char upper in text.Where(c => char.IsUpper(c))) {
if (i > query.Length - 1)
return true; // return true here for CamelCase partial match ("CQ" matches "CodeQualityAnalysis")
if (char.ToUpper(query[i], CultureInfo.InvariantCulture) != upper)
return false;
i++;
}
if (i >= query.Length)
return true;
return false;
}
}
}

56
ICSharpCode.AvalonEdit/CodeCompletion/CompletionList.xaml

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:AvalonEdit="clr-namespace:ICSharpCode.AvalonEdit"
xmlns:cc="clr-namespace:ICSharpCode.AvalonEdit.CodeCompletion"
>
<Style TargetType="{x:Type ListBoxItem}" x:Key="CompletionListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border Name="Bd"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="true">
<ContentPresenter
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>
<!-- Simplified triggers:
we don't want a gray selection background when the ListBox doesn't have focus
-->
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="true">
<Setter Property="Background"
Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
<Setter Property="Foreground"
Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type cc:CompletionList}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type cc:CompletionList}">
<cc:CompletionListBox x:Name="PART_ListBox"
ItemContainerStyle="{StaticResource CompletionListBoxItem}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Image}" Width="16" Height="16" Margin="0,0,2,0"/>
<ContentControl Content="{Binding Content}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</cc:CompletionListBox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

96
ICSharpCode.AvalonEdit/CodeCompletion/CompletionListBox.cs

@ -0,0 +1,96 @@ @@ -0,0 +1,96 @@
// 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.Windows;
using System.Windows.Controls;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.CodeCompletion
{
/// <summary>
/// The list box used inside the CompletionList.
/// </summary>
public class CompletionListBox : ListBox
{
internal ScrollViewer scrollViewer;
/// <inheritdoc/>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
// Find the scroll viewer:
scrollViewer = null;
if (this.VisualChildrenCount > 0) {
Border border = this.GetVisualChild(0) as Border;
if (border != null)
scrollViewer = border.Child as ScrollViewer;
}
}
/// <summary>
/// Gets the number of the first visible item.
/// </summary>
public int FirstVisibleItem {
get {
if (scrollViewer == null || scrollViewer.ExtentHeight == 0) {
return 0;
} else {
return (int)(this.Items.Count * scrollViewer.VerticalOffset / scrollViewer.ExtentHeight);
}
}
set {
value = value.CoerceValue(0, this.Items.Count - this.VisibleItemCount);
if (scrollViewer != null) {
scrollViewer.ScrollToVerticalOffset((double)value / this.Items.Count * scrollViewer.ExtentHeight);
}
}
}
/// <summary>
/// Gets the number of visible items.
/// </summary>
public int VisibleItemCount {
get {
if (scrollViewer == null || scrollViewer.ExtentHeight == 0) {
return 10;
} else {
return Math.Max(
3,
(int)Math.Ceiling(this.Items.Count * scrollViewer.ViewportHeight
/ scrollViewer.ExtentHeight));
}
}
}
/// <summary>
/// Removes the selection.
/// </summary>
public void ClearSelection()
{
this.SelectedIndex = -1;
}
/// <summary>
/// Selects the item with the specified index and scrolls it into view.
/// </summary>
public void SelectIndex(int index)
{
if (index >= this.Items.Count)
index = this.Items.Count - 1;
if (index < 0)
index = 0;
this.SelectedIndex = index;
this.ScrollIntoView(this.SelectedItem);
}
/// <summary>
/// Centers the view on the item with the specified index.
/// </summary>
public void CenterViewOn(int index)
{
this.FirstVisibleItem = index - VisibleItemCount / 2;
}
}
}

198
ICSharpCode.AvalonEdit/CodeCompletion/CompletionWindow.cs

@ -0,0 +1,198 @@ @@ -0,0 +1,198 @@
// 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.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Threading;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Editing;
namespace ICSharpCode.AvalonEdit.CodeCompletion
{
/// <summary>
/// The code completion window.
/// </summary>
public class CompletionWindow : CompletionWindowBase
{
readonly CompletionList completionList = new CompletionList();
ToolTip toolTip = new ToolTip();
/// <summary>
/// Gets the completion list used in this completion window.
/// </summary>
public CompletionList CompletionList {
get { return completionList; }
}
/// <summary>
/// Creates a new code completion window.
/// </summary>
public CompletionWindow(TextArea textArea) : base(textArea)
{
// keep height automatic
this.CloseAutomatically = true;
this.SizeToContent = SizeToContent.Height;
this.MaxHeight = 300;
this.Width = 175;
this.Content = completionList;
// prevent user from resizing window to 0x0
this.MinHeight = 15;
this.MinWidth = 30;
toolTip.PlacementTarget = this;
toolTip.Placement = PlacementMode.Right;
toolTip.Closed += toolTip_Closed;
AttachEvents();
}
#region ToolTip handling
void toolTip_Closed(object sender, RoutedEventArgs e)
{
// Clear content after tooltip is closed.
// We cannot clear is immediately when setting IsOpen=false
// because the tooltip uses an animation for closing.
if (toolTip != null)
toolTip.Content = null;
}
void completionList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var item = completionList.SelectedItem;
if (item == null)
return;
object description = item.Description;
if (description != null) {
string descriptionText = description as string;
if (descriptionText != null) {
toolTip.Content = new TextBlock {
Text = descriptionText,
TextWrapping = TextWrapping.Wrap
};
} else {
toolTip.Content = description;
}
toolTip.IsOpen = true;
} else {
toolTip.IsOpen = false;
}
}
#endregion
void completionList_InsertionRequested(object sender, EventArgs e)
{
Close();
// The window must close before Complete() is called.
// If the Complete callback pushes stacked input handlers, we don't want to pop those when the CC window closes.
var item = completionList.SelectedItem;
if (item != null)
item.Complete(this.TextArea, new AnchorSegment(this.TextArea.Document, this.StartOffset, this.EndOffset - this.StartOffset), e);
}
void AttachEvents()
{
this.completionList.InsertionRequested += completionList_InsertionRequested;
this.completionList.SelectionChanged += completionList_SelectionChanged;
this.TextArea.Caret.PositionChanged += CaretPositionChanged;
this.TextArea.MouseWheel += textArea_MouseWheel;
this.TextArea.PreviewTextInput += textArea_PreviewTextInput;
}
/// <inheritdoc/>
protected override void DetachEvents()
{
this.completionList.InsertionRequested -= completionList_InsertionRequested;
this.completionList.SelectionChanged -= completionList_SelectionChanged;
this.TextArea.Caret.PositionChanged -= CaretPositionChanged;
this.TextArea.MouseWheel -= textArea_MouseWheel;
this.TextArea.PreviewTextInput -= textArea_PreviewTextInput;
base.DetachEvents();
}
/// <inheritdoc/>
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
if (toolTip != null) {
toolTip.IsOpen = false;
toolTip = null;
}
}
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (!e.Handled) {
completionList.HandleKey(e);
}
}
void textArea_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
e.Handled = RaiseEventPair(this, PreviewTextInputEvent, TextInputEvent,
new TextCompositionEventArgs(e.Device, e.TextComposition));
}
void textArea_MouseWheel(object sender, MouseWheelEventArgs e)
{
e.Handled = RaiseEventPair(GetScrollEventTarget(),
PreviewMouseWheelEvent, MouseWheelEvent,
new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta));
}
UIElement GetScrollEventTarget()
{
if (completionList == null)
return this;
return completionList.ScrollViewer ?? completionList.ListBox ?? (UIElement)completionList;
}
/// <summary>
/// Gets/Sets whether the completion window should close automatically.
/// The default value is true.
/// </summary>
public bool CloseAutomatically { get; set; }
/// <inheritdoc/>
protected override bool CloseOnFocusLost {
get { return this.CloseAutomatically; }
}
/// <summary>
/// When this flag is set, code completion closes if the caret moves to the
/// beginning of the allowed range. This is useful in Ctrl+Space and "complete when typing",
/// but not in dot-completion.
/// Has no effect if CloseAutomatically is false.
/// </summary>
public bool CloseWhenCaretAtBeginning { get; set; }
void CaretPositionChanged(object sender, EventArgs e)
{
int offset = this.TextArea.Caret.Offset;
if (offset == this.StartOffset) {
if (CloseAutomatically && CloseWhenCaretAtBeginning) {
Close();
} else {
completionList.SelectItem(string.Empty);
}
return;
}
if (offset < this.StartOffset || offset > this.EndOffset) {
if (CloseAutomatically) {
Close();
}
} else {
TextDocument document = this.TextArea.Document;
if (document != null) {
completionList.SelectItem(document.GetText(this.StartOffset, offset - this.StartOffset));
}
}
}
}
}

376
ICSharpCode.AvalonEdit/CodeCompletion/CompletionWindowBase.cs

@ -0,0 +1,376 @@ @@ -0,0 +1,376 @@
// 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.Linq;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Threading;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Editing;
using ICSharpCode.AvalonEdit.Rendering;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.CodeCompletion
{
/// <summary>
/// Base class for completion windows. Handles positioning the window at the caret.
/// </summary>
public class CompletionWindowBase : Window
{
static CompletionWindowBase()
{
WindowStyleProperty.OverrideMetadata(typeof(CompletionWindowBase), new FrameworkPropertyMetadata(WindowStyle.None));
ShowActivatedProperty.OverrideMetadata(typeof(CompletionWindowBase), new FrameworkPropertyMetadata(Boxes.False));
ShowInTaskbarProperty.OverrideMetadata(typeof(CompletionWindowBase), new FrameworkPropertyMetadata(Boxes.False));
}
/// <summary>
/// Gets the parent TextArea.
/// </summary>
public TextArea TextArea { get; private set; }
Window parentWindow;
TextDocument document;
/// <summary>
/// Gets/Sets the start of the text range in which the completion window stays open.
/// This text portion is used to determine the text used to select an entry in the completion list by typing.
/// </summary>
public int StartOffset { get; set; }
/// <summary>
/// Gets/Sets the end of the text range in which the completion window stays open.
/// This text portion is used to determine the text used to select an entry in the completion list by typing.
/// </summary>
public int EndOffset { get; set; }
/// <summary>
/// Gets whether the window was opened above the current line.
/// </summary>
protected bool IsUp { get; private set; }
/// <summary>
/// Creates a new CompletionWindowBase.
/// </summary>
public CompletionWindowBase(TextArea textArea)
{
if (textArea == null)
throw new ArgumentNullException("textArea");
this.TextArea = textArea;
parentWindow = Window.GetWindow(textArea);
this.Owner = parentWindow;
this.AddHandler(MouseUpEvent, new MouseButtonEventHandler(OnMouseUp), true);
StartOffset = EndOffset = this.TextArea.Caret.Offset;
AttachEvents();
}
#region Event Handlers
void AttachEvents()
{
document = this.TextArea.Document;
if (document != null) {
document.Changing += textArea_Document_Changing;
}
// LostKeyboardFocus seems to be more reliable than PreviewLostKeyboardFocus - see SD-1729
this.TextArea.LostKeyboardFocus += TextAreaLostFocus;
this.TextArea.TextView.ScrollOffsetChanged += TextViewScrollOffsetChanged;
this.TextArea.DocumentChanged += TextAreaDocumentChanged;
if (parentWindow != null) {
parentWindow.LocationChanged += parentWindow_LocationChanged;
}
// close previous completion windows of same type
foreach (InputHandler x in this.TextArea.StackedInputHandlers.OfType<InputHandler>()) {
if (x.window.GetType() == this.GetType())
this.TextArea.PopStackedInputHandler(x);
}
myInputHandler = new InputHandler(this);
this.TextArea.PushStackedInputHandler(myInputHandler);
}
/// <summary>
/// Detaches events from the text area.
/// </summary>
protected virtual void DetachEvents()
{
if (document != null) {
document.Changing -= textArea_Document_Changing;
}
this.TextArea.LostKeyboardFocus -= TextAreaLostFocus;
this.TextArea.TextView.ScrollOffsetChanged -= TextViewScrollOffsetChanged;
this.TextArea.DocumentChanged -= TextAreaDocumentChanged;
if (parentWindow != null) {
parentWindow.LocationChanged -= parentWindow_LocationChanged;
}
this.TextArea.PopStackedInputHandler(myInputHandler);
}
#region InputHandler
InputHandler myInputHandler;
/// <summary>
/// A dummy input handler (that justs invokes the default input handler).
/// This is used to ensure the completion window closes when any other input handler
/// becomes active.
/// </summary>
sealed class InputHandler : TextAreaStackedInputHandler
{
internal readonly CompletionWindowBase window;
public InputHandler(CompletionWindowBase window)
: base(window.TextArea)
{
Debug.Assert(window != null);
this.window = window;
}
public override void Detach()
{
base.Detach();
window.Close();
}
const Key KeyDeadCharProcessed = (Key)0xac; // Key.DeadCharProcessed; // new in .NET 4
public override void OnPreviewKeyDown(KeyEventArgs e)
{
// prevents crash when typing deadchar while CC window is open
if (e.Key == KeyDeadCharProcessed)
return;
e.Handled = RaiseEventPair(window, PreviewKeyDownEvent, KeyDownEvent,
new KeyEventArgs(e.KeyboardDevice, e.InputSource, e.Timestamp, e.Key));
}
public override void OnPreviewKeyUp(KeyEventArgs e)
{
if (e.Key == KeyDeadCharProcessed)
return;
e.Handled = RaiseEventPair(window, PreviewKeyUpEvent, KeyUpEvent,
new KeyEventArgs(e.KeyboardDevice, e.InputSource, e.Timestamp, e.Key));
}
}
#endregion
void TextViewScrollOffsetChanged(object sender, EventArgs e)
{
// Workaround for crash #1580 (reproduction steps unknown):
// NullReferenceException in System.Windows.Window.CreateSourceWindow()
if (!sourceIsInitialized)
return;
IScrollInfo scrollInfo = this.TextArea.TextView;
Rect visibleRect = new Rect(scrollInfo.HorizontalOffset, scrollInfo.VerticalOffset, scrollInfo.ViewportWidth, scrollInfo.ViewportHeight);
// close completion window when the user scrolls so far that the anchor position is leaving the visible area
if (visibleRect.Contains(visualLocation) || visibleRect.Contains(visualLocationTop))
UpdatePosition();
else
Close();
}
void TextAreaDocumentChanged(object sender, EventArgs e)
{
Close();
}
void TextAreaLostFocus(object sender, RoutedEventArgs e)
{
Dispatcher.BeginInvoke(new Action(CloseIfFocusLost), DispatcherPriority.Background);
}
void parentWindow_LocationChanged(object sender, EventArgs e)
{
UpdatePosition();
}
/// <inheritdoc/>
protected override void OnDeactivated(EventArgs e)
{
base.OnDeactivated(e);
Dispatcher.BeginInvoke(new Action(CloseIfFocusLost), DispatcherPriority.Background);
}
#endregion
/// <summary>
/// Raises a tunnel/bubble event pair for a WPF control.
/// </summary>
/// <param name="target">The WPF control for which the event should be raised.</param>
/// <param name="previewEvent">The tunneling event.</param>
/// <param name="event">The bubbling event.</param>
/// <param name="args">The event args to use.</param>
/// <returns>The <see cref="RoutedEventArgs.Handled"/> value of the event args.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate")]
protected static bool RaiseEventPair(UIElement target, RoutedEvent previewEvent, RoutedEvent @event, RoutedEventArgs args)
{
if (target == null)
throw new ArgumentNullException("target");
if (previewEvent == null)
throw new ArgumentNullException("previewEvent");
if (@event == null)
throw new ArgumentNullException("event");
if (args == null)
throw new ArgumentNullException("args");
args.RoutedEvent = previewEvent;
target.RaiseEvent(args);
args.RoutedEvent = @event;
target.RaiseEvent(args);
return args.Handled;
}
// Special handler: handledEventsToo
void OnMouseUp(object sender, MouseButtonEventArgs e)
{
ActivateParentWindow();
}
/// <summary>
/// Activates the parent window.
/// </summary>
protected virtual void ActivateParentWindow()
{
if (parentWindow != null)
parentWindow.Activate();
}
void CloseIfFocusLost()
{
if (CloseOnFocusLost) {
Debug.WriteLine("CloseIfFocusLost: this.IsActive=" + this.IsActive + " IsTextAreaFocused=" + IsTextAreaFocused);
if (!this.IsActive && !IsTextAreaFocused) {
Close();
}
}
}
/// <summary>
/// Gets whether the completion window should automatically close when the text editor looses focus.
/// </summary>
protected virtual bool CloseOnFocusLost {
get { return true; }
}
bool IsTextAreaFocused {
get {
if (parentWindow != null && !parentWindow.IsActive)
return false;
return this.TextArea.IsKeyboardFocused;
}
}
bool sourceIsInitialized;
/// <inheritdoc/>
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
if (document != null && this.StartOffset != this.TextArea.Caret.Offset) {
SetPosition(new TextViewPosition(document.GetLocation(this.StartOffset)));
} else {
SetPosition(this.TextArea.Caret.Position);
}
sourceIsInitialized = true;
}
/// <inheritdoc/>
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
DetachEvents();
}
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (!e.Handled && e.Key == Key.Escape) {
e.Handled = true;
Close();
}
}
Point visualLocation, visualLocationTop;
/// <summary>
/// Positions the completion window at the specified position.
/// </summary>
protected void SetPosition(TextViewPosition position)
{
TextView textView = this.TextArea.TextView;
visualLocation = textView.GetVisualPosition(position, VisualYPosition.LineBottom);
visualLocationTop = textView.GetVisualPosition(position, VisualYPosition.LineTop);
UpdatePosition();
}
void UpdatePosition()
{
TextView textView = this.TextArea.TextView;
// PointToScreen returns device dependent units (physical pixels)
Point location = textView.PointToScreen(visualLocation - textView.ScrollOffset);
Point locationTop = textView.PointToScreen(visualLocationTop - textView.ScrollOffset);
// Let's use device dependent units for everything
Size completionWindowSize = new Size(this.ActualWidth, this.ActualHeight).TransformToDevice(textView);
Rect bounds = new Rect(location, completionWindowSize);
Rect workingScreen = System.Windows.Forms.Screen.GetWorkingArea(location.ToSystemDrawing()).ToWpf();
if (!workingScreen.Contains(bounds)) {
if (bounds.Left < workingScreen.Left) {
bounds.X = workingScreen.Left;
} else if (bounds.Right > workingScreen.Right) {
bounds.X = workingScreen.Right - bounds.Width;
}
if (bounds.Bottom > workingScreen.Bottom) {
bounds.Y = locationTop.Y - bounds.Height;
IsUp = true;
} else {
IsUp = false;
}
if (bounds.Y < workingScreen.Top) {
bounds.Y = workingScreen.Top;
}
}
// Convert the window bounds to device independent units
bounds = bounds.TransformFromDevice(textView);
this.Left = bounds.X;
this.Top = bounds.Y;
}
/// <inheritdoc/>
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
base.OnRenderSizeChanged(sizeInfo);
if (sizeInfo.HeightChanged && IsUp) {
this.Top += sizeInfo.PreviousSize.Height - sizeInfo.NewSize.Height;
}
}
/// <summary>
/// Gets/sets whether the completion window should expect text insertion at the start offset,
/// which not go into the completion region, but before it.
/// </summary>
/// <remarks>This property allows only a single insertion, it is reset to false
/// when that insertion has occurred.</remarks>
public bool ExpectInsertionBeforeStart { get; set; }
void textArea_Document_Changing(object sender, DocumentChangeEventArgs e)
{
if (e.Offset + e.RemovalLength == this.StartOffset && e.RemovalLength > 0) {
Close(); // removal immediately in front of completion segment: close the window
// this is necessary when pressing backspace after dot-completion
}
if (e.Offset == StartOffset && e.RemovalLength == 0 && ExpectInsertionBeforeStart) {
StartOffset = e.GetNewOffset(StartOffset, AnchorMovementType.AfterInsertion);
this.ExpectInsertionBeforeStart = false;
} else {
StartOffset = e.GetNewOffset(StartOffset, AnchorMovementType.BeforeInsertion);
}
EndOffset = e.GetNewOffset(EndOffset, AnchorMovementType.AfterInsertion);
}
}
}

54
ICSharpCode.AvalonEdit/CodeCompletion/ICompletionData.cs

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
// 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.Windows.Media;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Editing;
namespace ICSharpCode.AvalonEdit.CodeCompletion
{
/// <summary>
/// Describes an entry in the <see cref="CompletionList"/>.
/// </summary>
public interface ICompletionData
{
/// <summary>
/// Gets the image.
/// </summary>
ImageSource Image { get; }
/// <summary>
/// Gets the text. This property is used to filter the list of visible elements.
/// </summary>
string Text { get; }
/// <summary>
/// The displayed content. This can be the same as 'Text', or a WPF UIElement if
/// you want to display rich content.
/// </summary>
object Content { get; }
/// <summary>
/// Gets the description.
/// </summary>
object Description { get; }
/// <summary>
/// Gets the priority. This property is used in the selection logic. You can use it to prefer selecting those items
/// which the user is accessing most frequently.
/// </summary>
double Priority { get; }
/// <summary>
/// Perform the completion.
/// </summary>
/// <param name="textArea">The text area on which completion is performed.</param>
/// <param name="completionSegment">The text segment that was used by the completion window if
/// the user types (segment between CompletionWindow.StartOffset and CompletionWindow.EndOffset).</param>
/// <param name="insertionRequestEventArgs">The EventArgs used for the insertion request.
/// These can be TextCompositionEventArgs, KeyEventArgs, MouseEventArgs, depending on how
/// the insertion was triggered.</param>
void Complete(TextArea textArea, ISegment completionSegment, EventArgs insertionRequestEventArgs);
}
}

42
ICSharpCode.AvalonEdit/CodeCompletion/IOverloadProvider.cs

@ -0,0 +1,42 @@ @@ -0,0 +1,42 @@
// 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.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
namespace ICSharpCode.AvalonEdit.CodeCompletion
{
/// <summary>
/// Provides the items for the OverloadViewer.
/// </summary>
public interface IOverloadProvider : INotifyPropertyChanged
{
/// <summary>
/// Gets/Sets the selected index.
/// </summary>
int SelectedIndex { get; set; }
/// <summary>
/// Gets the number of overloads.
/// </summary>
int Count { get; }
/// <summary>
/// Gets the text 'SelectedIndex of Count'.
/// </summary>
string CurrentIndexText { get; }
/// <summary>
/// Gets the current header.
/// </summary>
object CurrentHeader { get; }
/// <summary>
/// Gets the current content.
/// </summary>
object CurrentContent { get; }
}
}

66
ICSharpCode.AvalonEdit/CodeCompletion/InsightWindow.cs

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
// 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.Windows;
using ICSharpCode.AvalonEdit.Editing;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.CodeCompletion
{
/// <summary>
/// A popup-like window that is attached to a text segment.
/// </summary>
public class InsightWindow : CompletionWindowBase
{
static InsightWindow()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(InsightWindow),
new FrameworkPropertyMetadata(typeof(InsightWindow)));
AllowsTransparencyProperty.OverrideMetadata(typeof(InsightWindow),
new FrameworkPropertyMetadata(Boxes.True));
}
/// <summary>
/// Creates a new InsightWindow.
/// </summary>
public InsightWindow(TextArea textArea) : base(textArea)
{
this.CloseAutomatically = true;
AttachEvents();
}
/// <summary>
/// Gets/Sets whether the insight window should close automatically.
/// The default value is true.
/// </summary>
public bool CloseAutomatically { get; set; }
/// <inheritdoc/>
protected override bool CloseOnFocusLost {
get { return this.CloseAutomatically; }
}
void AttachEvents()
{
this.TextArea.Caret.PositionChanged += CaretPositionChanged;
}
/// <inheritdoc/>
protected override void DetachEvents()
{
this.TextArea.Caret.PositionChanged -= CaretPositionChanged;
base.DetachEvents();
}
void CaretPositionChanged(object sender, EventArgs e)
{
if (this.CloseAutomatically) {
int offset = this.TextArea.Caret.Offset;
if (offset < this.StartOffset || offset > this.EndOffset) {
Close();
}
}
}
}
}

112
ICSharpCode.AvalonEdit/CodeCompletion/InsightWindow.xaml

@ -0,0 +1,112 @@ @@ -0,0 +1,112 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="clr-namespace:ICSharpCode.AvalonEdit.CodeCompletion"
>
<!-- Template for InsightWindow. Based on the template for ToolTip. -->
<Style TargetType="{x:Type cc:InsightWindow}">
<Setter Property="SizeToContent" Value="WidthAndHeight" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="{DynamicResource {x:Static SystemColors.WindowFrameBrushKey}}" />
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.InfoBrushKey}}" />
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.InfoTextBrushKey}}" />
<Setter Property="FontFamily" Value="{DynamicResource {x:Static SystemFonts.StatusFontFamilyKey}}" />
<Setter Property="FontSize" Value="{DynamicResource {x:Static SystemFonts.StatusFontSizeKey}}" />
<Setter Property="FontStyle" Value="{DynamicResource {x:Static SystemFonts.StatusFontStyleKey}}" />
<Setter Property="FontWeight" Value="{DynamicResource {x:Static SystemFonts.StatusFontWeightKey}}" />
<Setter Property="Padding" Value="1,1,3,1" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type cc:InsightWindow}">
<Border BorderThickness="{TemplateBinding Border.BorderThickness}"
Padding="{TemplateBinding Control.Padding}"
CornerRadius="2,2,2,2"
BorderBrush="{TemplateBinding Border.BorderBrush}"
Background="{TemplateBinding Panel.Background}">
<AdornerDecorator>
<ContentPresenter
Content="{TemplateBinding ContentControl.Content}"
ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}"
ContentStringFormat="{TemplateBinding ContentControl.ContentStringFormat}"
HorizontalAlignment="{TemplateBinding Control.HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding Control.VerticalContentAlignment}"
SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
</AdornerDecorator>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Template for OverloadViewer. -->
<Style TargetType="{x:Type cc:OverloadViewer}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type cc:OverloadViewer}">
<Grid>
<Grid.Resources>
<cc:CollapseIfSingleOverloadConverter x:Key="collapseIfSingleOverloadConverter"/>
<!-- Style of the UpDownButton -->
<Style TargetType="{x:Type Button}" x:Key="upDownButtonStyle">
<Style.Setters>
<Setter Property="Background" Value="LightGray"/>
<Setter Property="Padding" Value="2,2,2,2"/>
<Setter Property="Width" Value="9"/>
<Setter Property="Height" Value="9"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Name="bd"
Background="{TemplateBinding Background}" CornerRadius="2">
<ContentControl Margin="{TemplateBinding Padding}"
Content="{TemplateBinding Content}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter TargetName="bd" Property="Background" Value="LightBlue"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Grid.Column="0"
Margin="0,0,4,0"
Orientation="Horizontal"
Visibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Provider.Count, Converter={StaticResource collapseIfSingleOverloadConverter}}">
<Button Name="PART_UP" Style="{StaticResource upDownButtonStyle}">
<Path Stroke="Black" Fill="Black" Data="M 0,0.866 L 1,0.866 L 0.5,0 Z" Stretch="UniformToFill" />
</Button>
<TextBlock Margin="2,0,2,0"
Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Provider.CurrentIndexText}"/>
<Button Name="PART_DOWN" Style="{StaticResource upDownButtonStyle}">
<Path Stroke="Black" Fill="Black" Data="M 0,0 L 1,0 L 0.5,0.866 Z" Stretch="UniformToFill" />
</Button>
</StackPanel>
<ContentControl Grid.Row="0" Grid.Column="1"
Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Provider.CurrentHeader}"/>
<ContentControl Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Provider.CurrentContent}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

54
ICSharpCode.AvalonEdit/CodeCompletion/OverloadInsightWindow.cs

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
// 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.Windows;
using System.Windows.Input;
using ICSharpCode.AvalonEdit.Editing;
namespace ICSharpCode.AvalonEdit.CodeCompletion
{
/// <summary>
/// Insight window that shows an OverloadViewer.
/// </summary>
public class OverloadInsightWindow : InsightWindow
{
OverloadViewer overloadViewer = new OverloadViewer();
/// <summary>
/// Creates a new OverloadInsightWindow.
/// </summary>
public OverloadInsightWindow(TextArea textArea) : base(textArea)
{
overloadViewer.Margin = new Thickness(2,0,0,0);
this.Content = overloadViewer;
}
/// <summary>
/// Gets/Sets the item provider.
/// </summary>
public IOverloadProvider Provider {
get { return overloadViewer.Provider; }
set { overloadViewer.Provider = value; }
}
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (!e.Handled && this.Provider.Count > 1) {
switch (e.Key) {
case Key.Up:
e.Handled = true;
overloadViewer.ChangeIndex(-1);
break;
case Key.Down:
e.Handled = true;
overloadViewer.ChangeIndex(+1);
break;
}
}
}
}
}

101
ICSharpCode.AvalonEdit/CodeCompletion/OverloadViewer.cs

@ -0,0 +1,101 @@ @@ -0,0 +1,101 @@
// 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.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace ICSharpCode.AvalonEdit.CodeCompletion
{
/// <summary>
/// Represents a text between "Up" and "Down" buttons.
/// </summary>
public class OverloadViewer : Control
{
static OverloadViewer()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(OverloadViewer),
new FrameworkPropertyMetadata(typeof(OverloadViewer)));
}
/// <summary>
/// The text property.
/// </summary>
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(OverloadViewer));
/// <summary>
/// Gets/Sets the text between the Up and Down buttons.
/// </summary>
public string Text {
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
/// <inheritdoc/>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
Button upButton = (Button)this.Template.FindName("PART_UP", this);
upButton.Click += (sender, e) => {
e.Handled = true;
ChangeIndex(-1);
};
Button downButton = (Button)this.Template.FindName("PART_DOWN", this);
downButton.Click += (sender, e) => {
e.Handled = true;
ChangeIndex(+1);
};
}
/// <summary>
/// The ItemProvider property.
/// </summary>
public static readonly DependencyProperty ProviderProperty =
DependencyProperty.Register("Provider", typeof(IOverloadProvider), typeof(OverloadViewer));
/// <summary>
/// Gets/Sets the item provider.
/// </summary>
public IOverloadProvider Provider {
get { return (IOverloadProvider)GetValue(ProviderProperty); }
set { SetValue(ProviderProperty, value); }
}
/// <summary>
/// Changes the selected index.
/// </summary>
/// <param name="relativeIndexChange">The relative index change - usual values are +1 or -1.</param>
public void ChangeIndex(int relativeIndexChange)
{
IOverloadProvider p = this.Provider;
if (p != null) {
int newIndex = p.SelectedIndex + relativeIndexChange;
if (newIndex < 0)
newIndex = p.Count - 1;
if (newIndex >= p.Count)
newIndex = 0;
p.SelectedIndex = newIndex;
}
}
}
sealed class CollapseIfSingleOverloadConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return ((int)value < 2) ? Visibility.Collapsed : Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

140
ICSharpCode.AvalonEdit/Document/ChangeTrackingCheckpoint.cs

@ -0,0 +1,140 @@ @@ -0,0 +1,140 @@
// 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 ICSharpCode.AvalonEdit.Utils;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// <para>A checkpoint that allows tracking changes to a TextDocument.</para>
/// <para>
/// Use <see cref="TextDocument.CreateSnapshot(out ChangeTrackingCheckpoint)"/> to create a checkpoint.
/// </para>
/// </summary>
/// <remarks>
/// <para>The <see cref="ChangeTrackingCheckpoint"/> class allows tracking document changes, even from background threads.</para>
/// <para>Once you have two checkpoints, you can call <see cref="ChangeTrackingCheckpoint.GetChangesTo"/> to retrieve the complete list
/// of document changes that happened between those versions of the document.</para>
/// </remarks>
public sealed class ChangeTrackingCheckpoint
{
// Object that is unique per document.
// Used to determine if two checkpoints belong to the same document.
// We don't use a reference to the document itself to allow the GC to reclaim the document memory
// even if there are still references to checkpoints.
readonly object documentIdentifier;
// 'value' is the change from the previous checkpoint to this checkpoint
// TODO: store the change in the older checkpoint instead - if only a reference to the
// newest document version exists, the GC should be able to collect all DocumentChangeEventArgs.
readonly DocumentChangeEventArgs value;
readonly int id;
ChangeTrackingCheckpoint next;
internal ChangeTrackingCheckpoint(object documentIdentifier)
{
this.documentIdentifier = documentIdentifier;
}
internal ChangeTrackingCheckpoint(object documentIdentifier, DocumentChangeEventArgs value, int id)
{
this.documentIdentifier = documentIdentifier;
this.value = value;
this.id = id;
}
internal ChangeTrackingCheckpoint Append(DocumentChangeEventArgs change)
{
Debug.Assert(this.next == null);
this.next = new ChangeTrackingCheckpoint(this.documentIdentifier, change, unchecked( this.id + 1 ));
return this.next;
}
/// <summary>
/// Creates a change tracking checkpoint for the specified document.
/// This method is thread-safe.
/// If you need a ChangeTrackingCheckpoint that's consistent with a snapshot of the document,
/// use <see cref="TextDocument.CreateSnapshot(out ChangeTrackingCheckpoint)"/>.
/// </summary>
public static ChangeTrackingCheckpoint Create(TextDocument document)
{
if (document == null)
throw new ArgumentNullException("document");
return document.CreateChangeTrackingCheckpoint();
}
/// <summary>
/// Gets whether this checkpoint belongs to the same document as the other checkpoint.
/// </summary>
public bool BelongsToSameDocumentAs(ChangeTrackingCheckpoint other)
{
if (other == null)
throw new ArgumentNullException("other");
return documentIdentifier == other.documentIdentifier;
}
/// <summary>
/// Compares the age of this checkpoint to the other checkpoint.
/// </summary>
/// <remarks>This method is thread-safe.</remarks>
/// <exception cref="ArgumentException">Raised if 'other' belongs to a different document than this checkpoint.</exception>
/// <returns>-1 if this checkpoint is older than <paramref name="other"/>.
/// 0 if <c>this</c>==<paramref name="other"/>.
/// 1 if this checkpoint is newer than <paramref name="other"/>.</returns>
public int CompareAge(ChangeTrackingCheckpoint other)
{
if (other == null)
throw new ArgumentNullException("other");
if (other.documentIdentifier != this.documentIdentifier)
throw new ArgumentException("Checkpoints do not belong to the same document.");
// We will allow overflows, but assume that the maximum distance between checkpoints is 2^31-1.
// This is guaranteed on x86 because so many checkpoints don't fit into memory.
return Math.Sign(unchecked( this.id - other.id ));
}
/// <summary>
/// Gets the changes from this checkpoint to the other checkpoint.
/// If 'other' is older than this checkpoint, reverse changes are calculated.
/// </summary>
/// <remarks>This method is thread-safe.</remarks>
/// <exception cref="ArgumentException">Raised if 'other' belongs to a different document than this checkpoint.</exception>
public IEnumerable<DocumentChangeEventArgs> GetChangesTo(ChangeTrackingCheckpoint other)
{
int result = CompareAge(other);
if (result < 0)
return GetForwardChanges(other);
else if (result > 0)
return other.GetForwardChanges(this).Reverse().Select(change => change.Invert());
else
return Empty<DocumentChangeEventArgs>.Array;
}
IEnumerable<DocumentChangeEventArgs> GetForwardChanges(ChangeTrackingCheckpoint other)
{
// Return changes from this(exclusive) to other(inclusive).
ChangeTrackingCheckpoint node = this;
do {
node = node.next;
yield return node.value;
} while (node != other);
}
/// <summary>
/// Calculates where the offset has moved in the other buffer version.
/// </summary>
/// <remarks>This method is thread-safe.</remarks>
/// <exception cref="ArgumentException">Raised if 'other' belongs to a different document than this checkpoint.</exception>
public int MoveOffsetTo(ChangeTrackingCheckpoint other, int oldOffset, AnchorMovementType movement)
{
int offset = oldOffset;
foreach (DocumentChangeEventArgs e in GetChangesTo(other)) {
offset = e.GetNewOffset(offset, movement);
}
return offset;
}
}
}

131
ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs

@ -0,0 +1,131 @@ @@ -0,0 +1,131 @@
// 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 ICSharpCode.AvalonEdit.Utils;
using System;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// Describes a change of the document text.
/// This class is thread-safe.
/// </summary>
[Serializable]
public class DocumentChangeEventArgs : EventArgs
{
/// <summary>
/// The offset at which the change occurs.
/// </summary>
public int Offset { get; private set; }
/// <summary>
/// The text that was inserted.
/// </summary>
public string RemovedText { get; private set; }
/// <summary>
/// The number of characters removed.
/// </summary>
public int RemovalLength {
get { return RemovedText.Length; }
}
/// <summary>
/// The text that was inserted.
/// </summary>
public string InsertedText { get; private set; }
/// <summary>
/// The number of characters inserted.
/// </summary>
public int InsertionLength {
get { return InsertedText.Length; }
}
volatile OffsetChangeMap offsetChangeMap;
/// <summary>
/// Gets the OffsetChangeMap associated with this document change.
/// </summary>
/// <remarks>The OffsetChangeMap instance is guaranteed to be frozen and thus thread-safe.</remarks>
public OffsetChangeMap OffsetChangeMap {
get {
OffsetChangeMap map = offsetChangeMap;
if (map == null) {
// create OffsetChangeMap on demand
map = OffsetChangeMap.FromSingleElement(CreateSingleChangeMapEntry());
offsetChangeMap = map;
}
return map;
}
}
internal OffsetChangeMapEntry CreateSingleChangeMapEntry()
{
return new OffsetChangeMapEntry(this.Offset, this.RemovalLength, this.InsertionLength);
}
/// <summary>
/// Gets the OffsetChangeMap, or null if the default offset map (=single replacement) is being used.
/// </summary>
internal OffsetChangeMap OffsetChangeMapOrNull {
get {
return offsetChangeMap;
}
}
/// <summary>
/// Gets the new offset where the specified offset moves after this document change.
/// </summary>
public int GetNewOffset(int offset, AnchorMovementType movementType)
{
if (offsetChangeMap != null)
return offsetChangeMap.GetNewOffset(offset, movementType);
else
return CreateSingleChangeMapEntry().GetNewOffset(offset, movementType);
}
/// <summary>
/// Creates a new DocumentChangeEventArgs object.
/// </summary>
public DocumentChangeEventArgs(int offset, string removedText, string insertedText)
: this(offset, removedText, insertedText, null)
{
}
/// <summary>
/// Creates a new DocumentChangeEventArgs object.
/// </summary>
public DocumentChangeEventArgs(int offset, string removedText, string insertedText, OffsetChangeMap offsetChangeMap)
{
ThrowUtil.CheckNotNegative(offset, "offset");
ThrowUtil.CheckNotNull(removedText, "removedText");
ThrowUtil.CheckNotNull(insertedText, "insertedText");
this.Offset = offset;
this.RemovedText = removedText;
this.InsertedText = insertedText;
if (offsetChangeMap != null) {
if (!offsetChangeMap.IsFrozen)
throw new ArgumentException("The OffsetChangeMap must be frozen before it can be used in DocumentChangeEventArgs");
if (!offsetChangeMap.IsValidForDocumentChange(offset, removedText.Length, insertedText.Length))
throw new ArgumentException("OffsetChangeMap is not valid for this document change", "offsetChangeMap");
this.offsetChangeMap = offsetChangeMap;
}
}
/// <summary>
/// Creates DocumentChangeEventArgs for the reverse change.
/// </summary>
public DocumentChangeEventArgs Invert()
{
OffsetChangeMap map = this.OffsetChangeMapOrNull;
if (map != null) {
map = map.Invert();
map.Freeze();
}
return new DocumentChangeEventArgs(this.Offset, this.InsertedText, this.RemovedText, map);
}
}
}

52
ICSharpCode.AvalonEdit/Document/DocumentChangeOperation.cs

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
// 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.Diagnostics;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// Describes a change to a TextDocument.
/// </summary>
sealed class DocumentChangeOperation : IUndoableOperationWithContext
{
TextDocument document;
DocumentChangeEventArgs change;
public DocumentChangeOperation(TextDocument document, DocumentChangeEventArgs change)
{
this.document = document;
this.change = change;
}
public void Undo(UndoStack stack)
{
Debug.Assert(stack.state == UndoStack.StatePlayback);
stack.RegisterAffectedDocument(document);
stack.state = UndoStack.StatePlaybackModifyDocument;
this.Undo();
stack.state = UndoStack.StatePlayback;
}
public void Redo(UndoStack stack)
{
Debug.Assert(stack.state == UndoStack.StatePlayback);
stack.RegisterAffectedDocument(document);
stack.state = UndoStack.StatePlaybackModifyDocument;
this.Redo();
stack.state = UndoStack.StatePlayback;
}
public void Undo()
{
OffsetChangeMap map = change.OffsetChangeMapOrNull;
document.Replace(change.Offset, change.InsertionLength, change.RemovedText, map != null ? map.Invert() : null);
}
public void Redo()
{
document.Replace(change.Offset, change.RemovalLength, change.InsertedText, change.OffsetChangeMapOrNull);
}
}
}

242
ICSharpCode.AvalonEdit/Document/DocumentLine.cs

@ -0,0 +1,242 @@ @@ -0,0 +1,242 @@
// 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.Diagnostics;
using System.Globalization;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// Represents a line inside a <see cref="TextDocument"/>.
/// </summary>
/// <remarks>
/// <para>
/// The <see cref="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 reflect the current document content.
/// </para>
/// <para>
/// Internally, the DocumentLine instances are arranged in a binary tree that allows for both efficient updates and lookup.
/// Converting between offset and line number is possible in O(lg N) time,
/// and the data structure also updates all offsets in O(lg N) whenever a line is inserted or removed.
/// </para>
/// </remarks>
public sealed partial class DocumentLine : ISegment
{
#region Constructor
#if DEBUG
// Required for thread safety check which is done only in debug builds.
// To save space, we don't store the document reference in release builds as we don't need it there.
readonly TextDocument document;
#endif
internal bool isDeleted;
internal DocumentLine(TextDocument document)
{
#if DEBUG
Debug.Assert(document != null);
this.document = document;
#endif
}
[Conditional("DEBUG")]
void DebugVerifyAccess()
{
#if DEBUG
document.DebugVerifyAccess();
#endif
}
#endregion
#region Events
// /// <summary>
// /// Is raised when the line is deleted.
// /// </summary>
// public event EventHandler Deleted;
//
// /// <summary>
// /// Is raised when the line's text changes.
// /// </summary>
// public event EventHandler TextChanged;
//
// /// <summary>
// /// Raises the Deleted or TextChanged event.
// /// </summary>
// internal void RaiseChanged()
// {
// if (IsDeleted) {
// if (Deleted != null)
// Deleted(this, EventArgs.Empty);
// } else {
// if (TextChanged != null)
// TextChanged(this, EventArgs.Empty);
// }
// }
#endregion
#region Properties stored in tree
/// <summary>
/// Gets if this line was deleted from the document.
/// </summary>
public bool IsDeleted {
get {
DebugVerifyAccess();
return isDeleted;
}
}
/// <summary>
/// Gets the number of this line.
/// Runtime: O(log n)
/// </summary>
/// <exception cref="InvalidOperationException">The line was deleted.</exception>
public int LineNumber {
get {
if (IsDeleted)
throw new InvalidOperationException();
return DocumentLineTree.GetIndexFromNode(this) + 1;
}
}
/// <summary>
/// Gets the starting offset of the line in the document's text.
/// Runtime: O(log n)
/// </summary>
/// <exception cref="InvalidOperationException">The line was deleted.</exception>
public int Offset {
get {
if (IsDeleted)
throw new InvalidOperationException();
return DocumentLineTree.GetOffsetFromNode(this);
}
}
/// <summary>
/// Gets the end offset of the line in the document's text (the offset before the line delimiter).
/// Runtime: O(log n)
/// </summary>
/// <exception cref="InvalidOperationException">The line was deleted.</exception>
/// <remarks>EndOffset = <see cref="Offset"/> + <see cref="Length"/>.</remarks>
public int EndOffset {
get { return this.Offset + this.Length; }
}
#endregion
#region Length
int totalLength;
byte delimiterLength;
/// <summary>
/// Gets the length of this line. The length does not include the line delimiter. O(1)
/// </summary>
/// <remarks>This property is still available even if the line was deleted;
/// in that case, it contains the line's length before the deletion.</remarks>
public int Length {
get {
DebugVerifyAccess();
return totalLength - delimiterLength;
}
}
/// <summary>
/// Gets the length of this line, including the line delimiter. O(1)
/// </summary>
/// <remarks>This property is still available even if the line was deleted;
/// in that case, it contains the line's length before the deletion.</remarks>
public int TotalLength {
get {
DebugVerifyAccess();
return totalLength;
}
internal set {
// this is set by DocumentLineTree
totalLength = value;
}
}
/// <summary>
/// <para>Gets the length of the line delimiter.</para>
/// <para>The value is 1 for single <c>"\r"</c> or <c>"\n"</c>, 2 for the <c>"\r\n"</c> sequence;
/// and 0 for the last line in the document.</para>
/// </summary>
/// <remarks>This property is still available even if the line was deleted;
/// in that case, it contains the line delimiter's length before the deletion.</remarks>
public int DelimiterLength {
get {
DebugVerifyAccess();
return delimiterLength;
}
internal set {
Debug.Assert(value >= 0 && value <= 2);
delimiterLength = (byte)value;
}
}
#endregion
#region Previous / Next Line
/// <summary>
/// Gets the next line in the document.
/// </summary>
/// <returns>The line following this line, or null if this is the last line.</returns>
public DocumentLine NextLine {
get {
DebugVerifyAccess();
if (right != null) {
return right.LeftMost;
} else {
DocumentLine node = this;
DocumentLine oldNode;
do {
oldNode = node;
node = node.parent;
// we are on the way up from the right part, don't output node again
} while (node != null && node.right == oldNode);
return node;
}
}
}
/// <summary>
/// Gets the previous line in the document.
/// </summary>
/// <returns>The line before this line, or null if this is the first line.</returns>
public DocumentLine PreviousLine {
get {
DebugVerifyAccess();
if (left != null) {
return left.RightMost;
} else {
DocumentLine node = this;
DocumentLine oldNode;
do {
oldNode = node;
node = node.parent;
// we are on the way up from the left part, don't output node again
} while (node != null && node.left == oldNode);
return node;
}
}
}
#endregion
#region ToString
/// <summary>
/// Gets a string with debug output showing the line number and offset.
/// Does not include the line's text.
/// </summary>
public override string ToString()
{
if (IsDeleted)
return "[DocumentLine deleted]";
else
return string.Format(
CultureInfo.InvariantCulture,
"[DocumentLine Number={0} Offset={1} Length={2}]", LineNumber, Offset, Length);
}
#endregion
}
}

712
ICSharpCode.AvalonEdit/Document/DocumentLineTree.cs

@ -0,0 +1,712 @@ @@ -0,0 +1,712 @@
// 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.Diagnostics;
using System.Text;
namespace ICSharpCode.AvalonEdit.Document
{
using LineNode = DocumentLine;
/// <summary>
/// Data structure for efficient management of the document lines (most operations are O(lg n)).
/// This implements an augmented red-black tree.
/// See <see cref="LineNode"/> for the augmented data.
///
/// NOTE: The tree is never empty, initially it contains an empty line.
/// </summary>
sealed class DocumentLineTree : IList<DocumentLine>
{
#region Constructor
readonly TextDocument document;
LineNode root;
public DocumentLineTree(TextDocument document)
{
this.document = document;
DocumentLine emptyLine = new DocumentLine(document);
root = emptyLine.InitLineNode();
}
#endregion
#region Rotation callbacks
internal static void UpdateAfterChildrenChange(LineNode node)
{
int totalCount = 1;
int totalLength = node.TotalLength;
if (node.left != null) {
totalCount += node.left.nodeTotalCount;
totalLength += node.left.nodeTotalLength;
}
if (node.right != null) {
totalCount += node.right.nodeTotalCount;
totalLength += node.right.nodeTotalLength;
}
if (totalCount != node.nodeTotalCount
|| totalLength != node.nodeTotalLength)
{
node.nodeTotalCount = totalCount;
node.nodeTotalLength = totalLength;
if (node.parent != null) UpdateAfterChildrenChange(node.parent);
}
}
static void UpdateAfterRotateLeft(LineNode node)
{
UpdateAfterChildrenChange(node);
// not required: rotations only happen on insertions/deletions
// -> totalCount changes -> the parent is always updated
//UpdateAfterChildrenChange(node.parent);
}
static void UpdateAfterRotateRight(LineNode node)
{
UpdateAfterChildrenChange(node);
// not required: rotations only happen on insertions/deletions
// -> totalCount changes -> the parent is always updated
//UpdateAfterChildrenChange(node.parent);
}
#endregion
#region RebuildDocument
/// <summary>
/// Rebuild the tree, in O(n).
/// </summary>
public void RebuildTree(List<DocumentLine> documentLines)
{
LineNode[] nodes = new LineNode[documentLines.Count];
for (int i = 0; i < documentLines.Count; i++) {
DocumentLine ls = documentLines[i];
LineNode node = ls.InitLineNode();
nodes[i] = node;
}
Debug.Assert(nodes.Length > 0);
// now build the corresponding balanced tree
int height = GetTreeHeight(nodes.Length);
Debug.WriteLine("DocumentLineTree will have height: " + height);
root = BuildTree(nodes, 0, nodes.Length, height);
root.color = BLACK;
#if DEBUG
CheckProperties();
#endif
}
internal static int GetTreeHeight(int size)
{
if (size == 0)
return 0;
else
return GetTreeHeight(size / 2) + 1;
}
/// <summary>
/// build a tree from a list of nodes
/// </summary>
LineNode BuildTree(LineNode[] nodes, int start, int end, int subtreeHeight)
{
Debug.Assert(start <= end);
if (start == end) {
return null;
}
int middle = (start + end) / 2;
LineNode node = nodes[middle];
node.left = BuildTree(nodes, start, middle, subtreeHeight - 1);
node.right = BuildTree(nodes, middle + 1, end, subtreeHeight - 1);
if (node.left != null) node.left.parent = node;
if (node.right != null) node.right.parent = node;
if (subtreeHeight == 1)
node.color = RED;
UpdateAfterChildrenChange(node);
return node;
}
#endregion
#region GetNodeBy... / Get...FromNode
LineNode GetNodeByIndex(int index)
{
Debug.Assert(index >= 0);
Debug.Assert(index < root.nodeTotalCount);
LineNode node = root;
while (true) {
if (node.left != null && index < node.left.nodeTotalCount) {
node = node.left;
} else {
if (node.left != null) {
index -= node.left.nodeTotalCount;
}
if (index == 0)
return node;
index--;
node = node.right;
}
}
}
internal static int GetIndexFromNode(LineNode node)
{
int index = (node.left != null) ? node.left.nodeTotalCount : 0;
while (node.parent != null) {
if (node == node.parent.right) {
if (node.parent.left != null)
index += node.parent.left.nodeTotalCount;
index++;
}
node = node.parent;
}
return index;
}
LineNode GetNodeByOffset(int offset)
{
Debug.Assert(offset >= 0);
Debug.Assert(offset <= root.nodeTotalLength);
if (offset == root.nodeTotalLength) {
return root.RightMost;
}
LineNode node = root;
while (true) {
if (node.left != null && offset < node.left.nodeTotalLength) {
node = node.left;
} else {
if (node.left != null) {
offset -= node.left.nodeTotalLength;
}
offset -= node.TotalLength;
if (offset < 0)
return node;
node = node.right;
}
}
}
internal static int GetOffsetFromNode(LineNode node)
{
int offset = (node.left != null) ? node.left.nodeTotalLength : 0;
while (node.parent != null) {
if (node == node.parent.right) {
if (node.parent.left != null)
offset += node.parent.left.nodeTotalLength;
offset += node.parent.TotalLength;
}
node = node.parent;
}
return offset;
}
#endregion
#region GetLineBy
public DocumentLine GetByNumber(int number)
{
return GetNodeByIndex(number - 1);
}
public DocumentLine GetByOffset(int offset)
{
return GetNodeByOffset(offset);
}
#endregion
#region LineCount
public int LineCount {
get {
return root.nodeTotalCount;
}
}
#endregion
#region CheckProperties
#if DEBUG
[Conditional("DATACONSISTENCYTEST")]
internal void CheckProperties()
{
Debug.Assert(root.nodeTotalLength == document.TextLength);
CheckProperties(root);
// check red-black property:
int blackCount = -1;
CheckNodeProperties(root, null, RED, 0, ref blackCount);
}
void CheckProperties(LineNode node)
{
int totalCount = 1;
int totalLength = node.TotalLength;
if (node.left != null) {
CheckProperties(node.left);
totalCount += node.left.nodeTotalCount;
totalLength += node.left.nodeTotalLength;
}
if (node.right != null) {
CheckProperties(node.right);
totalCount += node.right.nodeTotalCount;
totalLength += node.right.nodeTotalLength;
}
Debug.Assert(node.nodeTotalCount == totalCount);
Debug.Assert(node.nodeTotalLength == totalLength);
}
/*
1. A node is either red or black.
2. The root is black.
3. All leaves are black. (The leaves are the NIL children.)
4. Both children of every red node are black. (So every red node must have a black parent.)
5. Every simple path from a node to a descendant leaf contains the same number of black nodes. (Not counting the leaf node.)
*/
void CheckNodeProperties(LineNode node, LineNode parentNode, bool parentColor, int blackCount, ref int expectedBlackCount)
{
if (node == null) return;
Debug.Assert(node.parent == parentNode);
if (parentColor == RED) {
Debug.Assert(node.color == BLACK);
}
if (node.color == BLACK) {
blackCount++;
}
if (node.left == null && node.right == null) {
// node is a leaf node:
if (expectedBlackCount == -1)
expectedBlackCount = blackCount;
else
Debug.Assert(expectedBlackCount == blackCount);
}
CheckNodeProperties(node.left, node, node.color, blackCount, ref expectedBlackCount);
CheckNodeProperties(node.right, node, node.color, blackCount, ref expectedBlackCount);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
public string GetTreeAsString()
{
StringBuilder b = new StringBuilder();
AppendTreeToString(root, b, 0);
return b.ToString();
}
static void AppendTreeToString(LineNode node, StringBuilder b, int indent)
{
if (node.color == RED)
b.Append("RED ");
else
b.Append("BLACK ");
b.AppendLine(node.ToString());
indent += 2;
if (node.left != null) {
b.Append(' ', indent);
b.Append("L: ");
AppendTreeToString(node.left, b, indent);
}
if (node.right != null) {
b.Append(' ', indent);
b.Append("R: ");
AppendTreeToString(node.right, b, indent);
}
}
#endif
#endregion
#region Insert/Remove lines
public void RemoveLine(DocumentLine line)
{
RemoveNode(line);
line.isDeleted = true;
}
public DocumentLine InsertLineAfter(DocumentLine line, int totalLength)
{
DocumentLine newLine = new DocumentLine(document);
newLine.TotalLength = totalLength;
InsertAfter(line, newLine);
return newLine;
}
void InsertAfter(LineNode node, DocumentLine newLine)
{
LineNode newNode = newLine.InitLineNode();
if (node.right == null) {
InsertAsRight(node, newNode);
} else {
InsertAsLeft(node.right.LeftMost, newNode);
}
}
#endregion
#region Red/Black Tree
internal const bool RED = true;
internal const bool BLACK = false;
void InsertAsLeft(LineNode parentNode, LineNode newNode)
{
Debug.Assert(parentNode.left == null);
parentNode.left = newNode;
newNode.parent = parentNode;
newNode.color = RED;
UpdateAfterChildrenChange(parentNode);
FixTreeOnInsert(newNode);
}
void InsertAsRight(LineNode parentNode, LineNode newNode)
{
Debug.Assert(parentNode.right == null);
parentNode.right = newNode;
newNode.parent = parentNode;
newNode.color = RED;
UpdateAfterChildrenChange(parentNode);
FixTreeOnInsert(newNode);
}
void FixTreeOnInsert(LineNode node)
{
Debug.Assert(node != null);
Debug.Assert(node.color == RED);
Debug.Assert(node.left == null || node.left.color == BLACK);
Debug.Assert(node.right == null || node.right.color == BLACK);
LineNode parentNode = node.parent;
if (parentNode == null) {
// we inserted in the root -> the node must be black
// since this is a root node, making the node black increments the number of black nodes
// on all paths by one, so it is still the same for all paths.
node.color = BLACK;
return;
}
if (parentNode.color == BLACK) {
// if the parent node where we inserted was black, our red node is placed correctly.
// since we inserted a red node, the number of black nodes on each path is unchanged
// -> the tree is still balanced
return;
}
// parentNode is red, so there is a conflict here!
// because the root is black, parentNode is not the root -> there is a grandparent node
LineNode grandparentNode = parentNode.parent;
LineNode uncleNode = Sibling(parentNode);
if (uncleNode != null && uncleNode.color == RED) {
parentNode.color = BLACK;
uncleNode.color = BLACK;
grandparentNode.color = RED;
FixTreeOnInsert(grandparentNode);
return;
}
// now we know: parent is red but uncle is black
// First rotation:
if (node == parentNode.right && parentNode == grandparentNode.left) {
RotateLeft(parentNode);
node = node.left;
} else if (node == parentNode.left && parentNode == grandparentNode.right) {
RotateRight(parentNode);
node = node.right;
}
// because node might have changed, reassign variables:
parentNode = node.parent;
grandparentNode = parentNode.parent;
// Now recolor a bit:
parentNode.color = BLACK;
grandparentNode.color = RED;
// Second rotation:
if (node == parentNode.left && parentNode == grandparentNode.left) {
RotateRight(grandparentNode);
} else {
// because of the first rotation, this is guaranteed:
Debug.Assert(node == parentNode.right && parentNode == grandparentNode.right);
RotateLeft(grandparentNode);
}
}
void RemoveNode(LineNode removedNode)
{
if (removedNode.left != null && removedNode.right != null) {
// replace removedNode with it's in-order successor
LineNode leftMost = removedNode.right.LeftMost;
RemoveNode(leftMost); // remove leftMost from its current location
// and overwrite the removedNode with it
ReplaceNode(removedNode, leftMost);
leftMost.left = removedNode.left;
if (leftMost.left != null) leftMost.left.parent = leftMost;
leftMost.right = removedNode.right;
if (leftMost.right != null) leftMost.right.parent = leftMost;
leftMost.color = removedNode.color;
UpdateAfterChildrenChange(leftMost);
if (leftMost.parent != null) UpdateAfterChildrenChange(leftMost.parent);
return;
}
// now either removedNode.left or removedNode.right is null
// get the remaining child
LineNode parentNode = removedNode.parent;
LineNode childNode = removedNode.left ?? removedNode.right;
ReplaceNode(removedNode, childNode);
if (parentNode != null) UpdateAfterChildrenChange(parentNode);
if (removedNode.color == BLACK) {
if (childNode != null && childNode.color == RED) {
childNode.color = BLACK;
} else {
FixTreeOnDelete(childNode, parentNode);
}
}
}
void FixTreeOnDelete(LineNode node, LineNode parentNode)
{
Debug.Assert(node == null || node.parent == parentNode);
if (parentNode == null)
return;
// warning: node may be null
LineNode sibling = Sibling(node, parentNode);
if (sibling.color == RED) {
parentNode.color = RED;
sibling.color = BLACK;
if (node == parentNode.left) {
RotateLeft(parentNode);
} else {
RotateRight(parentNode);
}
sibling = Sibling(node, parentNode); // update value of sibling after rotation
}
if (parentNode.color == BLACK
&& sibling.color == BLACK
&& GetColor(sibling.left) == BLACK
&& GetColor(sibling.right) == BLACK)
{
sibling.color = RED;
FixTreeOnDelete(parentNode, parentNode.parent);
return;
}
if (parentNode.color == RED
&& sibling.color == BLACK
&& GetColor(sibling.left) == BLACK
&& GetColor(sibling.right) == BLACK)
{
sibling.color = RED;
parentNode.color = BLACK;
return;
}
if (node == parentNode.left &&
sibling.color == BLACK &&
GetColor(sibling.left) == RED &&
GetColor(sibling.right) == BLACK)
{
sibling.color = RED;
sibling.left.color = BLACK;
RotateRight(sibling);
}
else if (node == parentNode.right &&
sibling.color == BLACK &&
GetColor(sibling.right) == RED &&
GetColor(sibling.left) == BLACK)
{
sibling.color = RED;
sibling.right.color = BLACK;
RotateLeft(sibling);
}
sibling = Sibling(node, parentNode); // update value of sibling after rotation
sibling.color = parentNode.color;
parentNode.color = BLACK;
if (node == parentNode.left) {
if (sibling.right != null) {
Debug.Assert(sibling.right.color == RED);
sibling.right.color = BLACK;
}
RotateLeft(parentNode);
} else {
if (sibling.left != null) {
Debug.Assert(sibling.left.color == RED);
sibling.left.color = BLACK;
}
RotateRight(parentNode);
}
}
void ReplaceNode(LineNode replacedNode, LineNode newNode)
{
if (replacedNode.parent == null) {
Debug.Assert(replacedNode == root);
root = newNode;
} else {
if (replacedNode.parent.left == replacedNode)
replacedNode.parent.left = newNode;
else
replacedNode.parent.right = newNode;
}
if (newNode != null) {
newNode.parent = replacedNode.parent;
}
replacedNode.parent = null;
}
void RotateLeft(LineNode p)
{
// let q be p's right child
LineNode q = p.right;
Debug.Assert(q != null);
Debug.Assert(q.parent == p);
// set q to be the new root
ReplaceNode(p, q);
// set p's right child to be q's left child
p.right = q.left;
if (p.right != null) p.right.parent = p;
// set q's left child to be p
q.left = p;
p.parent = q;
UpdateAfterRotateLeft(p);
}
void RotateRight(LineNode p)
{
// let q be p's left child
LineNode q = p.left;
Debug.Assert(q != null);
Debug.Assert(q.parent == p);
// set q to be the new root
ReplaceNode(p, q);
// set p's left child to be q's right child
p.left = q.right;
if (p.left != null) p.left.parent = p;
// set q's right child to be p
q.right = p;
p.parent = q;
UpdateAfterRotateRight(p);
}
static LineNode Sibling(LineNode node)
{
if (node == node.parent.left)
return node.parent.right;
else
return node.parent.left;
}
static LineNode Sibling(LineNode node, LineNode parentNode)
{
Debug.Assert(node == null || node.parent == parentNode);
if (node == parentNode.left)
return parentNode.right;
else
return parentNode.left;
}
static bool GetColor(LineNode node)
{
return node != null ? node.color : BLACK;
}
#endregion
#region IList implementation
DocumentLine IList<DocumentLine>.this[int index] {
get {
document.VerifyAccess();
return GetByNumber(1 + index);
}
set {
throw new NotSupportedException();
}
}
int ICollection<DocumentLine>.Count {
get {
document.VerifyAccess();
return LineCount;
}
}
bool ICollection<DocumentLine>.IsReadOnly {
get { return true; }
}
int IList<DocumentLine>.IndexOf(DocumentLine item)
{
document.VerifyAccess();
if (item == null || item.IsDeleted)
return -1;
int index = item.LineNumber - 1;
if (index < LineCount && GetNodeByIndex(index) == item)
return index;
else
return -1;
}
void IList<DocumentLine>.Insert(int index, DocumentLine item)
{
throw new NotSupportedException();
}
void IList<DocumentLine>.RemoveAt(int index)
{
throw new NotSupportedException();
}
void ICollection<DocumentLine>.Add(DocumentLine item)
{
throw new NotSupportedException();
}
void ICollection<DocumentLine>.Clear()
{
throw new NotSupportedException();
}
bool ICollection<DocumentLine>.Contains(DocumentLine item)
{
IList<DocumentLine> self = this;
return self.IndexOf(item) >= 0;
}
void ICollection<DocumentLine>.CopyTo(DocumentLine[] array, int arrayIndex)
{
if (array == null)
throw new ArgumentNullException("array");
if (array.Length < LineCount)
throw new ArgumentException("The array is too small", "array");
if (arrayIndex < 0 || arrayIndex + LineCount > array.Length)
throw new ArgumentOutOfRangeException("arrayIndex", arrayIndex, "Value must be between 0 and " + (array.Length - LineCount));
foreach (DocumentLine ls in this) {
array[arrayIndex++] = ls;
}
}
bool ICollection<DocumentLine>.Remove(DocumentLine item)
{
throw new NotSupportedException();
}
public IEnumerator<DocumentLine> GetEnumerator()
{
document.VerifyAccess();
return Enumerate();
}
IEnumerator<DocumentLine> Enumerate()
{
document.VerifyAccess();
DocumentLine line = root.LeftMost;
while (line != null) {
yield return line;
line = line.NextLine;
}
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
#endregion
}
}

192
ICSharpCode.AvalonEdit/Document/GapTextBuffer.cs

@ -0,0 +1,192 @@ @@ -0,0 +1,192 @@
// 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.Diagnostics;
using System.Text;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Document
{
/*
/// <summary>
/// Implementation of a gap text buffer.
/// </summary>
sealed class GapTextBuffer
{
char[] buffer = Empty<char>.Array;
/// <summary>
/// The current text content.
/// Is set to null whenever the buffer changes, and gets a value only when the
/// full text content is requested.
/// </summary>
string textContent;
/// <summary>
/// last GetText result
/// </summary>
string lastGetTextResult;
int lastGetTextRequestOffset;
int gapBeginOffset;
int gapEndOffset;
int gapLength; // gapLength == gapEndOffset - gapBeginOffset
/// <summary>
/// when gap is too small for inserted text or gap is too large (exceeds maxGapLength),
/// a new buffer is reallocated with a new gap of at least this size.
/// </summary>
const int minGapLength = 128;
/// <summary>
/// when the gap exceeds this size, reallocate a smaller buffer
/// </summary>
const int maxGapLength = 4096;
public int Length {
get {
return buffer.Length - gapLength;
}
}
/// <summary>
/// Gets the buffer content.
/// </summary>
public string Text {
get {
if (textContent == null)
textContent = GetText(0, Length);
return textContent;
}
set {
Debug.Assert(value != null);
textContent = value; lastGetTextResult = null;
buffer = new char[value.Length + minGapLength];
value.CopyTo(0, buffer, 0, value.Length);
gapBeginOffset = value.Length;
gapEndOffset = buffer.Length;
gapLength = gapEndOffset - gapBeginOffset;
}
}
public char GetCharAt(int offset)
{
return offset < gapBeginOffset ? buffer[offset] : buffer[offset + gapLength];
}
public string GetText(int offset, int length)
{
if (length == 0)
return string.Empty;
if (lastGetTextRequestOffset == offset && lastGetTextResult != null && length == lastGetTextResult.Length)
return lastGetTextResult;
int end = offset + length;
string result;
if (end < gapBeginOffset) {
result = new string(buffer, offset, length);
} else if (offset > gapBeginOffset) {
result = new string(buffer, offset + gapLength, length);
} else {
int block1Size = gapBeginOffset - offset;
int block2Size = end - gapBeginOffset;
StringBuilder buf = new StringBuilder(block1Size + block2Size);
buf.Append(buffer, offset, block1Size);
buf.Append(buffer, gapEndOffset, block2Size);
result = buf.ToString();
}
lastGetTextRequestOffset = offset;
lastGetTextResult = result;
return result;
}
/// <summary>
/// Inserts text at the specified offset.
/// </summary>
public void Insert(int offset, string text)
{
Debug.Assert(offset >= 0 && offset <= Length);
if (text.Length == 0)
return;
textContent = null; lastGetTextResult = null;
PlaceGap(offset, text.Length);
text.CopyTo(0, buffer, gapBeginOffset, text.Length);
gapBeginOffset += text.Length;
gapLength = gapEndOffset - gapBeginOffset;
}
/// <summary>
/// Remove <paramref name="length"/> characters at <paramref name="offset"/>.
/// Leave a gap of at least <paramref name="reserveGapSize"/>.
/// </summary>
public void Remove(int offset, int length, int reserveGapSize)
{
Debug.Assert(offset >= 0 && offset <= Length);
Debug.Assert(length >= 0 && offset + length <= Length);
Debug.Assert(reserveGapSize >= 0);
if (length == 0)
return;
textContent = null; lastGetTextResult = null;
PlaceGap(offset, reserveGapSize - length);
gapEndOffset += length; // delete removed text
gapLength = gapEndOffset - gapBeginOffset;
if (gapLength - reserveGapSize > maxGapLength && gapLength - reserveGapSize > buffer.Length / 4) {
// shrink gap
MakeNewBuffer(gapBeginOffset, reserveGapSize + minGapLength);
}
}
void PlaceGap(int newGapOffset, int minRequiredGapLength)
{
if (gapLength < minRequiredGapLength) {
// enlarge gap
MakeNewBuffer(newGapOffset, minRequiredGapLength + Math.Max(minGapLength, buffer.Length / 8));
} else {
while (newGapOffset < gapBeginOffset) {
buffer[--gapEndOffset] = buffer[--gapBeginOffset];
}
while (newGapOffset > gapBeginOffset) {
buffer[gapBeginOffset++] = buffer[gapEndOffset++];
}
}
}
void MakeNewBuffer(int newGapOffset, int newGapLength)
{
char[] newBuffer = new char[Length + newGapLength];
Debug.WriteLine("GapTextBuffer was reallocated, new size=" + newBuffer.Length);
if (newGapOffset < gapBeginOffset) {
// gap is moving backwards
// first part:
Array.Copy(buffer, 0, newBuffer, 0, newGapOffset);
// moving middle part:
Array.Copy(buffer, newGapOffset, newBuffer, newGapOffset + newGapLength, gapBeginOffset - newGapOffset);
// last part:
Array.Copy(buffer, gapEndOffset, newBuffer, newBuffer.Length - (buffer.Length - gapEndOffset), buffer.Length - gapEndOffset);
} else {
// gap is moving forwards
// first part:
Array.Copy(buffer, 0, newBuffer, 0, gapBeginOffset);
// moving middle part:
Array.Copy(buffer, gapEndOffset, newBuffer, gapBeginOffset, newGapOffset - gapBeginOffset);
// last part:
int lastPartLength = newBuffer.Length - (newGapOffset + newGapLength);
Array.Copy(buffer, buffer.Length - lastPartLength, newBuffer, newGapOffset + newGapLength, lastPartLength);
}
gapBeginOffset = newGapOffset;
gapEndOffset = newGapOffset + newGapLength;
gapLength = newGapLength;
buffer = newBuffer;
}
}
*/
}

55
ICSharpCode.AvalonEdit/Document/ILineTracker.cs

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
// 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;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// Allows for low-level line tracking.
/// </summary>
/// <remarks>
/// The methods on this interface are called by the TextDocument's LineManager immediately after the document
/// has changed, *while* the DocumentLineTree is updating.
/// Thus, the DocumentLineTree may be in an invalid state when these methods are called.
/// This interface should only be used to update per-line data structures like the HeightTree.
/// Line trackers must not cause any events to be raised during an update to prevent other code from seeing
/// the invalid state.
/// Line trackers may be called while the TextDocument has taken a lock.
/// You must be careful not to dead-lock inside ILineTracker callbacks.
/// </remarks>
public interface ILineTracker
{
/// <summary>
/// Is called immediately before a document line is removed.
/// </summary>
void BeforeRemoveLine(DocumentLine line);
// /// <summary>
// /// Is called immediately after a document line is removed.
// /// </summary>
// void AfterRemoveLine(DocumentLine line);
/// <summary>
/// Is called immediately before a document line changes length.
/// This method will be called whenever the line is changed, even when the length stays as it is.
/// The method might be called multiple times for a single line because
/// a replacement is internally handled as removal followed by insertion.
/// </summary>
void SetLineLength(DocumentLine line, int newTotalLength);
/// <summary>
/// Is called immediately after a line was inserted.
/// </summary>
/// <param name="newLine">The new line</param>
/// <param name="insertionPos">The existing line before the new line</param>
void LineInserted(DocumentLine insertionPos, DocumentLine newLine);
/// <summary>
/// Indicates that there were changes to the document that the line tracker was not notified of.
/// The document is in a consistent state (but the line trackers aren't), and line trackers should
/// throw away their data and rebuild the document.
/// </summary>
void RebuildDocument();
}
}

220
ICSharpCode.AvalonEdit/Document/ISegment.cs

@ -0,0 +1,220 @@ @@ -0,0 +1,220 @@
// 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.Diagnostics;
using ICSharpCode.AvalonEdit.Utils;
using System.Globalization;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// An (Offset,Length)-pair.
/// </summary>
/// <seealso cref="TextSegment"/>
/// <seealso cref="AnchorSegment"/>
public interface ISegment
{
/// <summary>
/// Gets the start offset of the segment.
/// </summary>
int Offset { get; }
/// <summary>
/// Gets the length of the segment.
/// </summary>
/// <remarks>Must not be negative.</remarks>
int Length { get; }
/// <summary>
/// Gets the end offset of the segment.
/// </summary>
/// <remarks>EndOffset = Offset + Length;</remarks>
int EndOffset { get; }
}
static class SegmentExtensions
{
/// <summary>
/// Gets whether the segment contains the offset.
/// </summary>
/// <returns>
/// True, if offset is between segment.Start and segment.End (inclusive); otherwise, false.
/// </returns>
public static bool Contains(this ISegment segment, int offset)
{
int start = segment.Offset;
int end = start + segment.Length;
return offset >= start && offset <= end;
}
/// <summary>
/// Gets the overlapping portion of the segments.
/// Returns SimpleSegment.Invalid if the segments don't overlap.
/// </summary>
public static SimpleSegment GetOverlap(this ISegment segment, ISegment other)
{
int start = Math.Max(segment.Offset, other.Offset);
int end = Math.Min(segment.EndOffset, other.EndOffset);
if (end < start)
return SimpleSegment.Invalid;
else
return new SimpleSegment(start, end - start);
}
}
/// <summary>
/// Represents a simple segment (Offset,Length pair) that is not automatically updated
/// on document changes.
/// </summary>
struct SimpleSegment : IEquatable<SimpleSegment>, ISegment
{
public static readonly SimpleSegment Invalid = new SimpleSegment(-1, -1);
public readonly int Offset, Length;
int ISegment.Offset {
get { return Offset; }
}
int ISegment.Length {
get { return Length; }
}
public int EndOffset {
get {
return Offset + Length;
}
}
public SimpleSegment(int offset, int length)
{
this.Offset = offset;
this.Length = length;
}
public SimpleSegment(ISegment segment)
{
Debug.Assert(segment != null);
this.Offset = segment.Offset;
this.Length = segment.Length;
}
public override int GetHashCode()
{
unchecked {
return Offset + 10301 * Length;
}
}
public override bool Equals(object obj)
{
return (obj is SimpleSegment) && Equals((SimpleSegment)obj);
}
public bool Equals(SimpleSegment other)
{
return this.Offset == other.Offset && this.Length == other.Length;
}
public static bool operator ==(SimpleSegment left, SimpleSegment right)
{
return left.Equals(right);
}
public static bool operator !=(SimpleSegment left, SimpleSegment right)
{
return !left.Equals(right);
}
/// <inheritdoc/>
public override string ToString()
{
return "[Offset=" + Offset.ToString(CultureInfo.InvariantCulture) + ", Length=" + Length.ToString(CultureInfo.InvariantCulture) + "]";
}
}
/// <summary>
/// A segment using <see cref="TextAnchor"/>s as start and end positions.
/// </summary>
/// <remarks>
/// <para>
/// For the constructors creating new anchors, the start position will be AfterInsertion and the end position will be BeforeInsertion.
/// Should the end position move before the start position, the segment will have length 0.
/// </para>
/// </remarks>
/// <seealso cref="ISegment"/>
/// <seealso cref="TextSegment"/>
public sealed class AnchorSegment : ISegment
{
readonly TextAnchor start, end;
/// <inheritdoc/>
public int Offset {
get { return start.Offset; }
}
/// <inheritdoc/>
public int Length {
get {
// Math.Max takes care of the fact that end.Offset might move before start.Offset.
return Math.Max(0, end.Offset - start.Offset);
}
}
/// <inheritdoc/>
public int EndOffset {
get {
// Math.Max takes care of the fact that end.Offset might move before start.Offset.
return Math.Max(start.Offset, end.Offset);
}
}
/// <summary>
/// Creates a new AnchorSegment using the specified anchors.
/// The anchors must have <see cref="TextAnchor.SurviveDeletion"/> set to true.
/// </summary>
public AnchorSegment(TextAnchor start, TextAnchor end)
{
if (start == null)
throw new ArgumentNullException("start");
if (end == null)
throw new ArgumentNullException("end");
if (!start.SurviveDeletion)
throw new ArgumentException("Anchors for AnchorSegment must use SurviveDeletion", "start");
if (!end.SurviveDeletion)
throw new ArgumentException("Anchors for AnchorSegment must use SurviveDeletion", "end");
this.start = start;
this.end = end;
}
/// <summary>
/// Creates a new AnchorSegment that creates new anchors.
/// </summary>
public AnchorSegment(TextDocument document, ISegment segment)
: this(document, ThrowUtil.CheckNotNull(segment, "segment").Offset, segment.Length)
{
}
/// <summary>
/// Creates a new AnchorSegment that creates new anchors.
/// </summary>
public AnchorSegment(TextDocument document, int offset, int length)
{
if (document == null)
throw new ArgumentNullException("document");
this.start = document.CreateAnchor(offset);
this.start.SurviveDeletion = true;
this.start.MovementType = AnchorMovementType.AfterInsertion;
this.end = document.CreateAnchor(offset + length);
this.end.SurviveDeletion = true;
this.end.MovementType = AnchorMovementType.BeforeInsertion;
}
/// <inheritdoc/>
public override string ToString()
{
return "[Offset=" + Offset.ToString(CultureInfo.InvariantCulture) + ", EndOffset=" + EndOffset.ToString(CultureInfo.InvariantCulture) + "]";
}
}
}

320
ICSharpCode.AvalonEdit/Document/ITextSource.cs

@ -0,0 +1,320 @@ @@ -0,0 +1,320 @@
// 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 ICSharpCode.AvalonEdit.Utils;
using System;
using System.IO;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// Interface for read-only access to a text source.
/// </summary>
/// <seealso cref="TextDocument"/>
/// <seealso cref="StringTextSource"/>
public interface ITextSource
{
/// <summary>
/// Gets the whole text as string.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods")]
string Text { get; }
/// <summary>
/// Is raised when the Text property changes.
/// </summary>
event EventHandler TextChanged;
/// <summary>
/// Gets the total text length.
/// </summary>
/// <returns>The length of the text, in characters.</returns>
/// <remarks>This is the same as Text.Length, but is more efficient because
/// it doesn't require creating a String object.</remarks>
int TextLength { get; }
/// <summary>
/// Gets a character at the specified position in the document.
/// </summary>
/// <paramref name="offset">The index of the character to get.</paramref>
/// <exception cref="ArgumentOutOfRangeException">Offset is outside the valid range (0 to TextLength-1).</exception>
/// <returns>The character at the specified position.</returns>
/// <remarks>This is the same as Text[offset], but is more efficient because
/// it doesn't require creating a String object.</remarks>
char GetCharAt(int offset);
/// <summary>
/// Gets the index of the first occurrence of any character in the specified array.
/// </summary>
/// <param name="anyOf"></param>
/// <param name="startIndex">Start index of the search.</param>
/// <param name="count">Length of the area to search.</param>
/// <returns>The first index where any character was found; or -1 if no occurrence was found.</returns>
int IndexOfAny(char[] anyOf, int startIndex, int count);
/// <summary>
/// Retrieves the text for a portion of the document.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">offset or length is outside the valid range.</exception>
/// <remarks>This is the same as Text.Substring, but is more efficient because
/// it doesn't require creating a String object for the whole document.</remarks>
string GetText(int offset, int length);
/// <summary>
/// Creates a snapshot of the current text.
/// </summary>
/// <remarks>
/// This method is generally not thread-safe when called on a mutable text buffer, but the resulting text buffer is immutable and thread-safe.
/// However, some implementing classes may provide additional thread-safety guarantees, see <see cref="TextDocument.CreateSnapshot()">TextDocument.CreateSnapshot</see>.
/// </remarks>
ITextSource CreateSnapshot();
/// <summary>
/// Creates a snapshot of a part of the current text.
/// </summary>
/// <remarks>
/// This method is generally not thread-safe when called on a mutable text buffer, but the resulting text buffer is immutable and thread-safe.
/// However, some implementing classes may provide additional thread-safety guarantees, see <see cref="TextDocument.CreateSnapshot()">TextDocument.CreateSnapshot</see>.
/// </remarks>
ITextSource CreateSnapshot(int offset, int length);
/// <summary>
/// Creates a text reader.
/// If the text is changed while a reader is active, the reader will continue to read from the old text version.
/// </summary>
TextReader CreateReader();
}
/// <summary>
/// Implements the ITextSource interface by wrapping another TextSource
/// and viewing only a part of the text.
/// </summary>
[Obsolete("This class will be removed in a future version of AvalonEdit")]
public sealed class TextSourceView : ITextSource
{
readonly ITextSource baseTextSource;
readonly ISegment viewedSegment;
/// <summary>
/// Creates a new TextSourceView object.
/// </summary>
/// <param name="baseTextSource">The base text source.</param>
/// <param name="viewedSegment">A text segment from the base text source</param>
public TextSourceView(ITextSource baseTextSource, ISegment viewedSegment)
{
if (baseTextSource == null)
throw new ArgumentNullException("baseTextSource");
if (viewedSegment == null)
throw new ArgumentNullException("viewedSegment");
this.baseTextSource = baseTextSource;
this.viewedSegment = viewedSegment;
}
/// <inheritdoc/>
public event EventHandler TextChanged {
add { baseTextSource.TextChanged += value; }
remove { baseTextSource.TextChanged -= value; }
}
/// <inheritdoc/>
public string Text {
get {
return baseTextSource.GetText(viewedSegment.Offset, viewedSegment.Length);
}
}
/// <inheritdoc/>
public int TextLength {
get { return viewedSegment.Length; }
}
/// <inheritdoc/>
public char GetCharAt(int offset)
{
return baseTextSource.GetCharAt(viewedSegment.Offset + offset);
}
/// <inheritdoc/>
public string GetText(int offset, int length)
{
return baseTextSource.GetText(viewedSegment.Offset + offset, length);
}
/// <inheritdoc/>
public ITextSource CreateSnapshot()
{
return baseTextSource.CreateSnapshot(viewedSegment.Offset, viewedSegment.Length);
}
/// <inheritdoc/>
public ITextSource CreateSnapshot(int offset, int length)
{
return baseTextSource.CreateSnapshot(viewedSegment.Offset + offset, length);
}
/// <inheritdoc/>
public TextReader CreateReader()
{
return CreateSnapshot().CreateReader();
}
/// <inheritdoc/>
public int IndexOfAny(char[] anyOf, int startIndex, int count)
{
int offset = viewedSegment.Offset;
int result = baseTextSource.IndexOfAny(anyOf, startIndex + offset, count);
return result >= 0 ? result - offset : result;
}
}
/// <summary>
/// Implements the ITextSource interface using a string.
/// </summary>
public sealed class StringTextSource : ITextSource
{
readonly string text;
/// <summary>
/// Creates a new StringTextSource.
/// </summary>
public StringTextSource(string text)
{
if (text == null)
throw new ArgumentNullException("text");
this.text = text;
}
// Text can never change
event EventHandler ITextSource.TextChanged { add {} remove {} }
/// <inheritdoc/>
public string Text {
get { return text; }
}
/// <inheritdoc/>
public int TextLength {
get { return text.Length; }
}
/// <inheritdoc/>
public char GetCharAt(int offset)
{
// GetCharAt must throw ArgumentOutOfRangeException, not IndexOutOfRangeException
if (offset < 0 || offset >= text.Length)
throw new ArgumentOutOfRangeException("offset", offset, "offset must be between 0 and " + (text.Length - 1));
return text[offset];
}
/// <inheritdoc/>
public string GetText(int offset, int length)
{
return text.Substring(offset, length);
}
/// <inheritdoc/>
public TextReader CreateReader()
{
return new StringReader(text);
}
/// <inheritdoc/>
public ITextSource CreateSnapshot()
{
return this; // StringTextSource already is immutable
}
/// <inheritdoc/>
public ITextSource CreateSnapshot(int offset, int length)
{
return new StringTextSource(text.Substring(offset, length));
}
/// <inheritdoc/>
public int IndexOfAny(char[] anyOf, int startIndex, int count)
{
return text.IndexOfAny(anyOf, startIndex, count);
}
}
/// <summary>
/// Implements the ITextSource interface using a rope.
/// </summary>
public sealed class RopeTextSource : ITextSource
{
readonly Rope<char> rope;
/// <summary>
/// Creates a new RopeTextSource.
/// </summary>
public RopeTextSource(Rope<char> rope)
{
if (rope == null)
throw new ArgumentNullException("rope");
this.rope = rope;
}
/// <summary>
/// Returns a clone of the rope used for this text source.
/// </summary>
/// <remarks>
/// RopeTextSource only publishes a copy of the contained rope to ensure that the underlying rope cannot be modified.
/// Unless the creator of the RopeTextSource still has a reference on the rope, RopeTextSource is immutable.
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification="Not a property because it creates a clone")]
public Rope<char> GetRope()
{
return rope.Clone();
}
// Change event is not supported
event EventHandler ITextSource.TextChanged { add {} remove {} }
/// <inheritdoc/>
public string Text {
get { return rope.ToString(); }
}
/// <inheritdoc/>
public int TextLength {
get { return rope.Length; }
}
/// <inheritdoc/>
public char GetCharAt(int offset)
{
return rope[offset];
}
/// <inheritdoc/>
public string GetText(int offset, int length)
{
return rope.ToString(offset, length);
}
/// <inheritdoc/>
public TextReader CreateReader()
{
return new RopeTextReader(rope);
}
/// <inheritdoc/>
public ITextSource CreateSnapshot()
{
// we clone the underlying rope because the creator of the RopeTextSource might be modifying it
return new RopeTextSource(rope.Clone());
}
/// <inheritdoc/>
public ITextSource CreateSnapshot(int offset, int length)
{
return new RopeTextSource(rope.GetRange(offset, length));
}
/// <inheritdoc/>
public int IndexOfAny(char[] anyOf, int startIndex, int count)
{
return rope.IndexOfAny(anyOf, startIndex, count);
}
}
}

30
ICSharpCode.AvalonEdit/Document/IUndoableOperation.cs

@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
// 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;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// This Interface describes a the basic Undo/Redo operation
/// all Undo Operations must implement this interface.
/// </summary>
public interface IUndoableOperation
{
/// <summary>
/// Undo the last operation
/// </summary>
void Undo();
/// <summary>
/// Redo the last operation
/// </summary>
void Redo();
}
interface IUndoableOperationWithContext : IUndoableOperation
{
void Undo(UndoStack stack);
void Redo(UndoStack stack);
}
}

288
ICSharpCode.AvalonEdit/Document/LineManager.cs

@ -0,0 +1,288 @@ @@ -0,0 +1,288 @@
// 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 ICSharpCode.AvalonEdit.Utils;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// Creates/Deletes lines when text is inserted/removed.
/// </summary>
sealed class LineManager
{
#region Constructor
readonly TextDocument document;
readonly DocumentLineTree documentLineTree;
/// <summary>
/// A copy of the line trackers. We need a copy so that line trackers may remove themselves
/// while being notified (used e.g. by WeakLineTracker)
/// </summary>
ILineTracker[] lineTrackers;
internal void UpdateListOfLineTrackers()
{
this.lineTrackers = document.LineTrackers.ToArray();
}
public LineManager(DocumentLineTree documentLineTree, TextDocument document)
{
this.document = document;
this.documentLineTree = documentLineTree;
UpdateListOfLineTrackers();
Rebuild();
}
#endregion
#region Change events
/*
HashSet<DocumentLine> deletedLines = new HashSet<DocumentLine>();
readonly HashSet<DocumentLine> changedLines = new HashSet<DocumentLine>();
HashSet<DocumentLine> deletedOrChangedLines = new HashSet<DocumentLine>();
/// <summary>
/// Gets the list of lines deleted since the last RetrieveChangedLines() call.
/// The returned list is unsorted.
/// </summary>
public ICollection<DocumentLine> RetrieveDeletedLines()
{
var r = deletedLines;
deletedLines = new HashSet<DocumentLine>();
return r;
}
/// <summary>
/// Gets the list of lines changed since the last RetrieveChangedLines() call.
/// The returned list is sorted by line number and does not contain deleted lines.
/// </summary>
public List<DocumentLine> RetrieveChangedLines()
{
var list = (from line in changedLines
where !line.IsDeleted
let number = line.LineNumber
orderby number
select line).ToList();
changedLines.Clear();
return list;
}
/// <summary>
/// Gets the list of lines changed since the last RetrieveDeletedOrChangedLines() call.
/// The returned list is not sorted.
/// </summary>
public ICollection<DocumentLine> RetrieveDeletedOrChangedLines()
{
var r = deletedOrChangedLines;
deletedOrChangedLines = new HashSet<DocumentLine>();
return r;
}
*/
#endregion
#region Rebuild
public void Rebuild()
{
// keep the first document line
DocumentLine ls = documentLineTree.GetByNumber(1);
SimpleSegment ds = NewLineFinder.NextNewLine(document, 0);
List<DocumentLine> lines = new List<DocumentLine>();
int lastDelimiterEnd = 0;
while (ds != SimpleSegment.Invalid) {
ls.TotalLength = ds.Offset + ds.Length - lastDelimiterEnd;
ls.DelimiterLength = ds.Length;
lastDelimiterEnd = ds.Offset + ds.Length;
lines.Add(ls);
ls = new DocumentLine(document);
ds = NewLineFinder.NextNewLine(document, lastDelimiterEnd);
}
ls.ResetLine();
ls.TotalLength = document.TextLength - lastDelimiterEnd;
lines.Add(ls);
documentLineTree.RebuildTree(lines);
foreach (ILineTracker lineTracker in lineTrackers)
lineTracker.RebuildDocument();
}
#endregion
#region Remove
public void Remove(int offset, int length)
{
Debug.Assert(length >= 0);
if (length == 0) return;
DocumentLine startLine = documentLineTree.GetByOffset(offset);
int startLineOffset = startLine.Offset;
Debug.Assert(offset < startLineOffset + startLine.TotalLength);
if (offset > startLineOffset + startLine.Length) {
Debug.Assert(startLine.DelimiterLength == 2);
// we are deleting starting in the middle of a delimiter
// remove last delimiter part
SetLineLength(startLine, startLine.TotalLength - 1);
// remove remaining text
Remove(offset, length - 1);
return;
}
if (offset + length < startLineOffset + startLine.TotalLength) {
// just removing a part of this line
//startLine.RemovedLinePart(ref deferredEventList, offset - startLineOffset, length);
SetLineLength(startLine, startLine.TotalLength - length);
return;
}
// merge startLine with another line because startLine's delimiter was deleted
// possibly remove lines in between if multiple delimiters were deleted
int charactersRemovedInStartLine = startLineOffset + startLine.TotalLength - offset;
Debug.Assert(charactersRemovedInStartLine > 0);
//startLine.RemovedLinePart(ref deferredEventList, offset - startLineOffset, charactersRemovedInStartLine);
DocumentLine endLine = documentLineTree.GetByOffset(offset + length);
if (endLine == startLine) {
// special case: we are removing a part of the last line up to the
// end of the document
SetLineLength(startLine, startLine.TotalLength - length);
return;
}
int endLineOffset = endLine.Offset;
int charactersLeftInEndLine = endLineOffset + endLine.TotalLength - (offset + length);
//endLine.RemovedLinePart(ref deferredEventList, 0, endLine.TotalLength - charactersLeftInEndLine);
//startLine.MergedWith(endLine, offset - startLineOffset);
// remove all lines between startLine (excl.) and endLine (incl.)
DocumentLine tmp = startLine.NextLine;
DocumentLine lineToRemove;
do {
lineToRemove = tmp;
tmp = tmp.NextLine;
RemoveLine(lineToRemove);
} while (lineToRemove != endLine);
SetLineLength(startLine, startLine.TotalLength - charactersRemovedInStartLine + charactersLeftInEndLine);
}
void RemoveLine(DocumentLine lineToRemove)
{
foreach (ILineTracker lt in lineTrackers)
lt.BeforeRemoveLine(lineToRemove);
documentLineTree.RemoveLine(lineToRemove);
// foreach (ILineTracker lt in lineTracker)
// lt.AfterRemoveLine(lineToRemove);
// deletedLines.Add(lineToRemove);
// deletedOrChangedLines.Add(lineToRemove);
}
#endregion
#region Insert
public void Insert(int offset, string text)
{
DocumentLine line = documentLineTree.GetByOffset(offset);
int lineOffset = line.Offset;
Debug.Assert(offset <= lineOffset + line.TotalLength);
if (offset > lineOffset + line.Length) {
Debug.Assert(line.DelimiterLength == 2);
// we are inserting in the middle of a delimiter
// shorten line
SetLineLength(line, line.TotalLength - 1);
// add new line
line = InsertLineAfter(line, 1);
line = SetLineLength(line, 1);
}
SimpleSegment ds = NewLineFinder.NextNewLine(text, 0);
if (ds == SimpleSegment.Invalid) {
// no newline is being inserted, all text is inserted in a single line
//line.InsertedLinePart(offset - line.Offset, text.Length);
SetLineLength(line, line.TotalLength + text.Length);
return;
}
//DocumentLine firstLine = line;
//firstLine.InsertedLinePart(offset - firstLine.Offset, ds.Offset);
int lastDelimiterEnd = 0;
while (ds != SimpleSegment.Invalid) {
// split line segment at line delimiter
int lineBreakOffset = offset + ds.Offset + ds.Length;
lineOffset = line.Offset;
int lengthAfterInsertionPos = lineOffset + line.TotalLength - (offset + lastDelimiterEnd);
line = SetLineLength(line, lineBreakOffset - lineOffset);
DocumentLine newLine = InsertLineAfter(line, lengthAfterInsertionPos);
newLine = SetLineLength(newLine, lengthAfterInsertionPos);
line = newLine;
lastDelimiterEnd = ds.Offset + ds.Length;
ds = NewLineFinder.NextNewLine(text, lastDelimiterEnd);
}
//firstLine.SplitTo(line);
// insert rest after last delimiter
if (lastDelimiterEnd != text.Length) {
//line.InsertedLinePart(0, text.Length - lastDelimiterEnd);
SetLineLength(line, line.TotalLength + text.Length - lastDelimiterEnd);
}
}
DocumentLine InsertLineAfter(DocumentLine line, int length)
{
DocumentLine newLine = documentLineTree.InsertLineAfter(line, length);
foreach (ILineTracker lt in lineTrackers)
lt.LineInserted(line, newLine);
return newLine;
}
#endregion
#region SetLineLength
/// <summary>
/// Sets the total line length and checks the delimiter.
/// This method can cause line to be deleted when it contains a single '\n' character
/// and the previous line ends with '\r'.
/// </summary>
/// <returns>Usually returns <paramref name="line"/>, but if line was deleted due to
/// the "\r\n" merge, returns the previous line.</returns>
DocumentLine SetLineLength(DocumentLine line, int newTotalLength)
{
// changedLines.Add(line);
// deletedOrChangedLines.Add(line);
int delta = newTotalLength - line.TotalLength;
if (delta != 0) {
foreach (ILineTracker lt in lineTrackers)
lt.SetLineLength(line, newTotalLength);
line.TotalLength = newTotalLength;
DocumentLineTree.UpdateAfterChildrenChange(line);
}
// determine new DelimiterLength
if (newTotalLength == 0) {
line.DelimiterLength = 0;
} else {
int lineOffset = line.Offset;
char lastChar = document.GetCharAt(lineOffset + newTotalLength - 1);
if (lastChar == '\r') {
line.DelimiterLength = 1;
} else if (lastChar == '\n') {
if (newTotalLength >= 2 && document.GetCharAt(lineOffset + newTotalLength - 2) == '\r') {
line.DelimiterLength = 2;
} else if (newTotalLength == 1 && lineOffset > 0 && document.GetCharAt(lineOffset - 1) == '\r') {
// we need to join this line with the previous line
DocumentLine previousLine = line.PreviousLine;
RemoveLine(line);
return SetLineLength(previousLine, previousLine.TotalLength + 1);
} else {
line.DelimiterLength = 1;
}
} else {
line.DelimiterLength = 0;
}
}
return line;
}
#endregion
}
}

83
ICSharpCode.AvalonEdit/Document/LineNode.cs

@ -0,0 +1,83 @@ @@ -0,0 +1,83 @@
// 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;
namespace ICSharpCode.AvalonEdit.Document
{
using LineNode = DocumentLine;
// A tree node in the document line tree.
// For the purpose of the invariants, "children", "descendents", "siblings" etc. include the DocumentLine object,
// it is treated as a third child node between left and right.
// Originally, this was a separate class, with a reference to the documentLine. The documentLine had a reference
// back to the node. To save memory, the same object is used for both the documentLine and the line node.
// This saves 16 bytes per line (8 byte object overhead + two pointers).
// sealed class LineNode
// {
// internal readonly DocumentLine documentLine;
partial class DocumentLine
{
internal DocumentLine left, right, parent;
internal bool color;
// optimization note: I tried packing color and isDeleted into a single byte field, but that
// actually increased the memory requirements. The JIT packs two bools and a byte (delimiterSize)
// into a single DWORD, but two bytes get each their own DWORD. Three bytes end up in the same DWORD, so
// apparently the JIT only optimizes for memory when there are at least three small fields.
// Currently, DocumentLine takes 36 bytes on x86 (8 byte object overhead, 3 pointers, 3 ints, and another DWORD
// for the small fields).
// TODO: a possible optimization would be to combine 'totalLength' and the small fields into a single uint.
// delimiterSize takes only two bits, the two bools take another two bits; so there's still
// 28 bits left for totalLength. 268435455 characters per line should be enough for everyone :)
/// <summary>
/// Resets the line to enable its reuse after a document rebuild.
/// </summary>
internal void ResetLine()
{
totalLength = delimiterLength = 0;
isDeleted = color = false;
left = right = parent = null;
}
internal LineNode InitLineNode()
{
this.nodeTotalCount = 1;
this.nodeTotalLength = this.TotalLength;
return this;
}
internal LineNode LeftMost {
get {
LineNode node = this;
while (node.left != null)
node = node.left;
return node;
}
}
internal LineNode RightMost {
get {
LineNode node = this;
while (node.right != null)
node = node.right;
return node;
}
}
/// <summary>
/// The number of lines in this node and its child nodes.
/// Invariant:
/// nodeTotalCount = 1 + left.nodeTotalCount + right.nodeTotalCount
/// </summary>
internal int nodeTotalCount;
/// <summary>
/// The total text length of this node and its child nodes.
/// Invariant:
/// nodeTotalLength = left.nodeTotalLength + documentLine.TotalLength + right.nodeTotalLength
/// </summary>
internal int nodeTotalLength;
}
}

132
ICSharpCode.AvalonEdit/Document/NewLineFinder.cs

@ -0,0 +1,132 @@ @@ -0,0 +1,132 @@
// 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.Diagnostics;
using System.Text;
namespace ICSharpCode.AvalonEdit.Document
{
static class NewLineFinder
{
static readonly char[] newline = { '\r', '\n' };
/// <summary>
/// Gets the location of the next new line character, or SimpleSegment.Invalid
/// if none is found.
/// </summary>
internal static SimpleSegment NextNewLine(string text, int offset)
{
int pos = text.IndexOfAny(newline, offset);
if (pos >= 0) {
if (text[pos] == '\r') {
if (pos + 1 < text.Length && text[pos + 1] == '\n')
return new SimpleSegment(pos, 2);
}
return new SimpleSegment(pos, 1);
}
return SimpleSegment.Invalid;
}
/// <summary>
/// Gets the location of the next new line character, or SimpleSegment.Invalid
/// if none is found.
/// </summary>
internal static SimpleSegment NextNewLine(ITextSource text, int offset)
{
int textLength = text.TextLength;
int pos = text.IndexOfAny(newline, offset, textLength - offset);
if (pos >= 0) {
if (text.GetCharAt(pos) == '\r') {
if (pos + 1 < textLength && text.GetCharAt(pos + 1) == '\n')
return new SimpleSegment(pos, 2);
}
return new SimpleSegment(pos, 1);
}
return SimpleSegment.Invalid;
}
}
partial class TextUtilities
{
/// <summary>
/// Finds the next new line character starting at offset.
/// </summary>
/// <param name="text">The text source to search in.</param>
/// <param name="offset">The starting offset for the search.</param>
/// <param name="newLineType">The string representing the new line that was found, or null if no new line was found.</param>
/// <returns>The position of the first new line starting at or after <paramref name="offset"/>,
/// or -1 if no new line was found.</returns>
public static int FindNextNewLine(ITextSource text, int offset, out string newLineType)
{
if (text == null)
throw new ArgumentNullException("text");
if (offset < 0 || offset > text.TextLength)
throw new ArgumentOutOfRangeException("offset", offset, "offset is outside of text source");
SimpleSegment s = NewLineFinder.NextNewLine(text, offset);
if (s == SimpleSegment.Invalid) {
newLineType = null;
return -1;
} else {
if (s.Length == 2) {
newLineType = "\r\n";
} else if (text.GetCharAt(s.Offset) == '\n') {
newLineType = "\n";
} else {
newLineType = "\r";
}
return s.Offset;
}
}
/// <summary>
/// Gets whether the specified string is a newline sequence.
/// </summary>
public static bool IsNewLine(string newLine)
{
return newLine == "\r\n" || newLine == "\n" || newLine == "\r";
}
/// <summary>
/// Normalizes all new lines in <paramref name="input"/> to be <paramref name="newLine"/>.
/// </summary>
public static string NormalizeNewLines(string input, string newLine)
{
if (input == null)
return null;
if (!IsNewLine(newLine))
throw new ArgumentException("newLine must be one of the known newline sequences");
SimpleSegment ds = NewLineFinder.NextNewLine(input, 0);
if (ds == SimpleSegment.Invalid) // text does not contain any new lines
return input;
StringBuilder b = new StringBuilder(input.Length);
int lastEndOffset = 0;
do {
b.Append(input, lastEndOffset, ds.Offset - lastEndOffset);
b.Append(newLine);
lastEndOffset = ds.EndOffset;
ds = NewLineFinder.NextNewLine(input, lastEndOffset);
} while (ds != SimpleSegment.Invalid);
// remaining string (after last newline)
b.Append(input, lastEndOffset, input.Length - lastEndOffset);
return b.ToString();
}
/// <summary>
/// Gets the newline sequence used in the document at the specified line.
/// </summary>
public static string GetNewLineFromDocument(TextDocument document, int lineNumber)
{
DocumentLine line = document.GetLineByNumber(lineNumber);
if (line.DelimiterLength == 0) {
// at the end of the document, there's no line delimiter, so use the delimiter
// from the previous line
line = line.PreviousLine;
if (line == null)
return Environment.NewLine;
}
return document.GetText(line.Offset + line.Length, line.DelimiterLength);
}
}
}

347
ICSharpCode.AvalonEdit/Document/OffsetChangeMap.cs

@ -0,0 +1,347 @@ @@ -0,0 +1,347 @@
// 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.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// Contains predefined offset change mapping types.
/// </summary>
public enum OffsetChangeMappingType
{
/// <summary>
/// Normal replace.
/// Anchors in front of the replaced region will stay in front, anchors after the replaced region will stay after.
/// Anchors in the middle of the removed region will be deleted. Ifthey survive deletion,
/// they move depending on their AnchorMovementType.
/// </summary>
/// <remarks>
/// This is the default implementation of DocumentChangeEventArgs when OffsetChangeMap is null,
/// so using this option usually works without creating an OffsetChangeMap instance.
/// This is equivalent to an OffsetChangeMap with a single entry describing the replace operation.
/// </remarks>
Normal,
/// <summary>
/// First the old text is removed, then the new text is inserted.
/// Anchors immediately in front (or after) the replaced region may move to the other side of the insertion,
/// depending on the AnchorMovementType.
/// </summary>
/// <remarks>
/// This is implemented as an OffsetChangeMap with two entries: the removal, and the insertion.
/// </remarks>
RemoveAndInsert,
/// <summary>
/// The text is replaced character-by-character.
/// Anchors keep their position inside the replaced text.
/// Anchors after the replaced region will move accordingly if the replacement text has a different length than the replaced text.
/// If the new text is shorter than the old text, anchors inside the old text that would end up behind the replacement text
/// will be moved so that they point to the end of the replacement text.
/// </summary>
/// <remarks>
/// On the OffsetChangeMap level, growing text is implemented by replacing the last character in the replaced text
/// with itself and the additional text segment. A simple insertion of the additional text would have the undesired
/// effect of moving anchors immediately after the replaced text into the replacement text if they used
/// AnchorMovementStyle.BeforeInsertion.
/// Shrinking text is implemented by removing the text segment that's too long; but in a special mode that
/// causes anchors to always survive irrespective of their <see cref="TextAnchor.SurviveDeletion"/> setting.
/// If the text keeps its old size, this is implemented as OffsetChangeMap.Empty.
/// </remarks>
CharacterReplace,
/// <summary>
/// Like 'Normal', but anchors with <see cref="TextAnchor.MovementType"/> = Default will stay in front of the
/// insertion instead of being moved behind it.
/// </summary>
KeepAnchorBeforeInsertion
}
/// <summary>
/// Describes a series of offset changes.
/// </summary>
[Serializable]
[SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix",
Justification="It's a mapping old offsets -> new offsets")]
public sealed class OffsetChangeMap : Collection<OffsetChangeMapEntry>
{
/// <summary>
/// Immutable OffsetChangeMap that is empty.
/// </summary>
[SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes",
Justification="The Empty instance is immutable")]
public static readonly OffsetChangeMap Empty = new OffsetChangeMap(Empty<OffsetChangeMapEntry>.Array, true);
/// <summary>
/// Creates a new OffsetChangeMap with a single element.
/// </summary>
/// <param name="entry">The entry.</param>
/// <returns>Returns a frozen OffsetChangeMap with a single entry.</returns>
public static OffsetChangeMap FromSingleElement(OffsetChangeMapEntry entry)
{
return new OffsetChangeMap(new OffsetChangeMapEntry[] { entry }, true);
}
bool isFrozen;
/// <summary>
/// Creates a new OffsetChangeMap instance.
/// </summary>
public OffsetChangeMap()
{
}
internal OffsetChangeMap(int capacity)
: base(new List<OffsetChangeMapEntry>(capacity))
{
}
private OffsetChangeMap(IList<OffsetChangeMapEntry> entries, bool isFrozen)
: base(entries)
{
this.isFrozen = isFrozen;
}
/// <summary>
/// Gets the new offset where the specified offset moves after this document change.
/// </summary>
public int GetNewOffset(int offset, AnchorMovementType movementType)
{
IList<OffsetChangeMapEntry> items = this.Items;
int count = items.Count;
for (int i = 0; i < count; i++) {
offset = items[i].GetNewOffset(offset, movementType);
}
return offset;
}
/// <summary>
/// Gets whether this OffsetChangeMap is a valid explanation for the specified document change.
/// </summary>
public bool IsValidForDocumentChange(int offset, int removalLength, int insertionLength)
{
int endOffset = offset + removalLength;
foreach (OffsetChangeMapEntry entry in this) {
// check that ChangeMapEntry is in valid range for this document change
if (entry.Offset < offset || entry.Offset + entry.RemovalLength > endOffset)
return false;
endOffset += entry.InsertionLength - entry.RemovalLength;
}
// check that the total delta matches
return endOffset == offset + insertionLength;
}
/// <summary>
/// Calculates the inverted OffsetChangeMap (used for the undo operation).
/// </summary>
public OffsetChangeMap Invert()
{
if (this == Empty)
return this;
OffsetChangeMap newMap = new OffsetChangeMap(this.Count);
for (int i = this.Count - 1; i >= 0; i--) {
OffsetChangeMapEntry entry = this[i];
// swap InsertionLength and RemovalLength
newMap.Add(new OffsetChangeMapEntry(entry.Offset, entry.InsertionLength, entry.RemovalLength));
}
return newMap;
}
/// <inheritdoc/>
protected override void ClearItems()
{
CheckFrozen();
base.ClearItems();
}
/// <inheritdoc/>
protected override void InsertItem(int index, OffsetChangeMapEntry item)
{
CheckFrozen();
base.InsertItem(index, item);
}
/// <inheritdoc/>
protected override void RemoveItem(int index)
{
CheckFrozen();
base.RemoveItem(index);
}
/// <inheritdoc/>
protected override void SetItem(int index, OffsetChangeMapEntry item)
{
CheckFrozen();
base.SetItem(index, item);
}
void CheckFrozen()
{
if (isFrozen)
throw new InvalidOperationException("This instance is frozen and cannot be modified.");
}
/// <summary>
/// Gets if this instance is frozen. Frozen instances are immutable and thus thread-safe.
/// </summary>
public bool IsFrozen {
get { return isFrozen; }
}
/// <summary>
/// Freezes this instance.
/// </summary>
public void Freeze()
{
isFrozen = true;
}
}
/// <summary>
/// An entry in the OffsetChangeMap.
/// This represents the offset of a document change (either insertion or removal, not both at once).
/// </summary>
[Serializable]
public struct OffsetChangeMapEntry : IEquatable<OffsetChangeMapEntry>
{
readonly int offset;
// MSB: DefaultAnchorMovementIsBeforeInsertion
readonly uint insertionLengthWithMovementFlag;
// MSB: RemovalNeverCausesAnchorDeletion; other 31 bits: RemovalLength
readonly uint removalLengthWithDeletionFlag;
/// <summary>
/// The offset at which the change occurs.
/// </summary>
public int Offset {
get { return offset; }
}
/// <summary>
/// The number of characters inserted.
/// Returns 0 if this entry represents a removal.
/// </summary>
public int InsertionLength {
get { return (int)(insertionLengthWithMovementFlag & 0x7fffffff); }
}
/// <summary>
/// The number of characters removed.
/// Returns 0 if this entry represents an insertion.
/// </summary>
public int RemovalLength {
get { return (int)(removalLengthWithDeletionFlag & 0x7fffffff); }
}
/// <summary>
/// Gets whether the removal should not cause any anchor deletions.
/// </summary>
public bool RemovalNeverCausesAnchorDeletion {
get { return (removalLengthWithDeletionFlag & 0x80000000) != 0; }
}
/// <summary>
/// Gets whether default anchor movement causes the anchor to stay in front of the caret.
/// </summary>
public bool DefaultAnchorMovementIsBeforeInsertion {
get { return (insertionLengthWithMovementFlag & 0x80000000) != 0; }
}
/// <summary>
/// Gets the new offset where the specified offset moves after this document change.
/// </summary>
public int GetNewOffset(int oldOffset, AnchorMovementType movementType)
{
int insertionLength = this.InsertionLength;
int removalLength = this.RemovalLength;
if (!(removalLength == 0 && oldOffset == offset)) {
// we're getting trouble (both if statements in here would apply)
// if there's no removal and we insert at the offset
// -> we'd need to disambiguate by movementType, which is handled after the if
// offset is before start of change: no movement
if (oldOffset <= offset)
return oldOffset;
// offset is after end of change: movement by normal delta
if (oldOffset >= offset + removalLength)
return oldOffset + insertionLength - removalLength;
}
// we reach this point if
// a) the oldOffset is inside the deleted segment
// b) there was no removal and we insert at the caret position
if (movementType == AnchorMovementType.AfterInsertion)
return offset + insertionLength;
else if (movementType == AnchorMovementType.BeforeInsertion)
return offset;
else
return this.DefaultAnchorMovementIsBeforeInsertion ? offset : offset + insertionLength;
}
/// <summary>
/// Creates a new OffsetChangeMapEntry instance.
/// </summary>
public OffsetChangeMapEntry(int offset, int removalLength, int insertionLength)
{
ThrowUtil.CheckNotNegative(offset, "offset");
ThrowUtil.CheckNotNegative(removalLength, "removalLength");
ThrowUtil.CheckNotNegative(insertionLength, "insertionLength");
this.offset = offset;
this.removalLengthWithDeletionFlag = (uint)removalLength;
this.insertionLengthWithMovementFlag = (uint)insertionLength;
}
/// <summary>
/// Creates a new OffsetChangeMapEntry instance.
/// </summary>
public OffsetChangeMapEntry(int offset, int removalLength, int insertionLength, bool removalNeverCausesAnchorDeletion, bool defaultAnchorMovementIsBeforeInsertion)
: this(offset, removalLength, insertionLength)
{
if (removalNeverCausesAnchorDeletion)
this.removalLengthWithDeletionFlag |= 0x80000000;
if (defaultAnchorMovementIsBeforeInsertion)
this.insertionLengthWithMovementFlag |= 0x80000000;
}
/// <inheritdoc/>
public override int GetHashCode()
{
unchecked {
return offset + 3559 * (int)insertionLengthWithMovementFlag + 3571 * (int)removalLengthWithDeletionFlag;
}
}
/// <inheritdoc/>
public override bool Equals(object obj)
{
return obj is OffsetChangeMapEntry && this.Equals((OffsetChangeMapEntry)obj);
}
/// <inheritdoc/>
public bool Equals(OffsetChangeMapEntry other)
{
return offset == other.offset && insertionLengthWithMovementFlag == other.insertionLengthWithMovementFlag && removalLengthWithDeletionFlag == other.removalLengthWithDeletionFlag;
}
/// <summary>
/// Tests the two entries for equality.
/// </summary>
public static bool operator ==(OffsetChangeMapEntry left, OffsetChangeMapEntry right)
{
return left.Equals(right);
}
/// <summary>
/// Tests the two entries for inequality.
/// </summary>
public static bool operator !=(OffsetChangeMapEntry left, OffsetChangeMapEntry right)
{
return !left.Equals(right);
}
}
}

194
ICSharpCode.AvalonEdit/Document/TextAnchor.cs

@ -0,0 +1,194 @@ @@ -0,0 +1,194 @@
// 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 ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// The TextAnchor class references an offset (a position between two characters).
/// It automatically updates the offset when text is inserted/removed in front of the anchor.
/// </summary>
/// <remarks>
/// <para>Use the <see cref="Offset"/> property to get the offset from a text anchor.
/// Use the <see cref="TextDocument.CreateAnchor"/> method to create an anchor from an offset.
/// </para>
/// <para>
/// The document will automatically update all text anchors; and because it uses weak references to do so,
/// the garbage collector can simply collect the anchor object when you don't need it anymore.
/// </para>
/// <para>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 <see cref="Offset"/> property also runs in O(lg N).</para>
/// <inheritdoc cref="IsDeleted" />
/// <inheritdoc cref="MovementType" />
/// <para>If you want to track a segment, you can use the <see cref="AnchorSegment"/> class which
/// implements <see cref="ISegment"/> using two text anchors.</para>
/// </remarks>
/// <example>
/// Usage:
/// <code>TextAnchor anchor = document.CreateAnchor(offset);
/// ChangeMyDocument();
/// int newOffset = anchor.Offset;
/// </code>
/// </example>
public sealed class TextAnchor
{
readonly TextDocument document;
internal TextAnchorNode node;
internal TextAnchor(TextDocument document)
{
this.document = document;
}
/// <summary>
/// Gets the document owning the anchor.
/// </summary>
public TextDocument Document {
get { return document; }
}
/// <summary>
/// Controls how the anchor moves.
/// </summary>
/// <remarks>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 <see cref="MovementType"/> will be used to determine which of these two options the anchor will choose.
/// The default value is <see cref="AnchorMovementType.Default"/>.</remarks>
public AnchorMovementType MovementType { get; set; }
/// <summary>
/// <para>
/// Specifies whether the anchor survives deletion of the text containing it.
/// </para><para>
/// <c>false</c>: The anchor is deleted when the a selection that includes the anchor is deleted.
/// <c>true</c>: The anchor is not deleted.
/// </para>
/// </summary>
/// <remarks><inheritdoc cref="IsDeleted" /></remarks>
public bool SurviveDeletion { get; set; }
/// <summary>
/// Gets whether the anchor was deleted.
/// </summary>
/// <remarks>
/// <para>When a piece of text containing an anchor is removed, then that anchor will be deleted.
/// First, the <see cref="IsDeleted"/> property is set to true on all deleted anchors,
/// then the <see cref="Deleted"/> events are raised.
/// You cannot retrieve the offset from an anchor that has been deleted.</para>
/// <para>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 <c><see cref="SurviveDeletion"/> = true</c>.</para>
/// </remarks>
public bool IsDeleted {
get {
document.DebugVerifyAccess();
return node == null;
}
}
/// <summary>
/// Occurs after the anchor was deleted.
/// </summary>
/// <remarks>
/// <inheritdoc cref="IsDeleted" />
/// <para>Due to the 'weak reference' nature of TextAnchor, you will receive the Deleted event only
/// while your code holds a reference to the TextAnchor object.</para>
/// </remarks>
public event EventHandler Deleted;
internal void OnDeleted(DelayedEvents delayedEvents)
{
node = null;
delayedEvents.DelayedRaise(Deleted, this, EventArgs.Empty);
}
/// <summary>
/// Gets the offset of the text anchor.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when trying to get the Offset from a deleted anchor.</exception>
public int Offset {
get {
document.DebugVerifyAccess();
TextAnchorNode n = this.node;
if (n == null)
throw new InvalidOperationException();
int offset = n.length;
if (n.left != null)
offset += n.left.totalLength;
while (n.parent != null) {
if (n == n.parent.right) {
if (n.parent.left != null)
offset += n.parent.left.totalLength;
offset += n.parent.length;
}
n = n.parent;
}
return offset;
}
}
/// <summary>
/// Gets the line number of the anchor.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when trying to get the Offset from a deleted anchor.</exception>
public int Line {
get {
return document.GetLineByOffset(this.Offset).LineNumber;
}
}
/// <summary>
/// Gets the column number of this anchor.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when trying to get the Offset from a deleted anchor.</exception>
public int Column {
get {
int offset = this.Offset;
return offset - document.GetLineByOffset(offset).Offset + 1;
}
}
/// <summary>
/// Gets the text location of this anchor.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when trying to get the Offset from a deleted anchor.</exception>
public TextLocation Location {
get {
return document.GetLocation(this.Offset);
}
}
/// <inheritdoc/>
public override string ToString()
{
return "[TextAnchor Offset=" + Offset + "]";
}
}
/// <summary>
/// Defines how a text anchor moves.
/// </summary>
public enum AnchorMovementType
{
/// <summary>
/// When text is inserted at the anchor position, the type of the insertion
/// determines where the caret moves to. For normal insertions, the anchor will stay
/// behind the inserted text.
/// </summary>
Default,
/// <summary>
/// When text is inserted at the anchor position, the anchor will stay
/// before the inserted text.
/// </summary>
BeforeInsertion,
/// <summary>
/// When text is insered at the anchor position, the anchor will move
/// after the inserted text.
/// </summary>
AfterInsertion
}
}

87
ICSharpCode.AvalonEdit/Document/TextAnchorNode.cs

@ -0,0 +1,87 @@ @@ -0,0 +1,87 @@
// 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;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// A TextAnchorNode is placed in the TextAnchorTree.
/// It describes a section of text with a text anchor at the end of the section.
/// A weak reference is used to refer to the TextAnchor. (to save memory, we derive from WeakReference instead of referencing it)
/// </summary>
sealed class TextAnchorNode : WeakReference
{
internal TextAnchorNode left, right, parent;
internal bool color;
internal int length;
internal int totalLength; // totalLength = length + left.totalLength + right.totalLength
public TextAnchorNode(TextAnchor anchor) : base(anchor)
{
}
internal TextAnchorNode LeftMost {
get {
TextAnchorNode node = this;
while (node.left != null)
node = node.left;
return node;
}
}
internal TextAnchorNode RightMost {
get {
TextAnchorNode node = this;
while (node.right != null)
node = node.right;
return node;
}
}
/// <summary>
/// Gets the inorder successor of the node.
/// </summary>
internal TextAnchorNode Successor {
get {
if (right != null) {
return right.LeftMost;
} else {
TextAnchorNode node = this;
TextAnchorNode oldNode;
do {
oldNode = node;
node = node.parent;
// go up until we are coming out of a left subtree
} while (node != null && node.right == oldNode);
return node;
}
}
}
/// <summary>
/// Gets the inorder predecessor of the node.
/// </summary>
internal TextAnchorNode Predecessor {
get {
if (left != null) {
return left.RightMost;
} else {
TextAnchorNode node = this;
TextAnchorNode oldNode;
do {
oldNode = node;
node = node.parent;
// go up until we are coming out of a right subtree
} while (node != null && node.left == oldNode);
return node;
}
}
}
public override string ToString()
{
return "[TextAnchorNode Length=" + length + " TotalLength=" + totalLength + " Target=" + Target + "]";
}
}
}

753
ICSharpCode.AvalonEdit/Document/TextAnchorTree.cs

@ -0,0 +1,753 @@ @@ -0,0 +1,753 @@
// 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.Diagnostics;
using System.Text;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// A tree of TextAnchorNodes.
/// </summary>
sealed class TextAnchorTree
{
// The text anchor tree has difficult requirements:
// - it must QUICKLY update the offset in all anchors whenever there is a document change
// - it must not reference text anchors directly, using weak references instead
// Clearly, we cannot afford updating an Offset property on all anchors (that would be O(N)).
// So instead, the anchors need to be able to calculate their offset from a data structure
// that can be efficiently updated.
// This implementation is built using an augmented red-black-tree.
// There is a 'TextAnchorNode' for each text anchor.
// Such a node represents a section of text (just the length is stored) with a (weakly referenced) text anchor at the end.
// Basically, you can imagine the list of text anchors as a sorted list of text anchors, where each anchor
// just stores the distance to the previous anchor.
// (next node = TextAnchorNode.Successor, distance = TextAnchorNode.length)
// Distances are never negative, so this representation means anchors are always sorted by offset
// (the order of anchors at the same offset is undefined)
// Of course, a linked list of anchors would be way too slow (one would need to traverse the whole list
// every time the offset of an anchor is being looked up).
// Instead, we use a red-black-tree. We aren't actually using the tree for sorting - it's just a binary tree
// as storage format for what's conceptually a list, the red-black properties are used to keep the tree balanced.
// Other balanced binary trees would work, too.
// What makes the tree-form efficient is that is augments the data by a 'totalLength'. Where 'length'
// represents the distance to the previous node, 'totalLength' is the sum of all 'length' values in the subtree
// under that node.
// This allows computing the Offset from an anchor by walking up the list of parent nodes instead of going
// through all predecessor nodes. So computing the Offset runs in O(log N).
readonly TextDocument document;
readonly List<TextAnchorNode> nodesToDelete = new List<TextAnchorNode>();
TextAnchorNode root;
public TextAnchorTree(TextDocument document)
{
this.document = document;
}
[Conditional("DEBUG")]
static void Log(string text)
{
Debug.WriteLine("TextAnchorTree: " + text);
}
#region Insert Text
void InsertText(int offset, int length, bool defaultAnchorMovementIsBeforeInsertion)
{
if (length == 0 || root == null || offset > root.totalLength)
return;
// find the range of nodes that are placed exactly at offset
// beginNode is inclusive, endNode is exclusive
if (offset == root.totalLength) {
PerformInsertText(FindActualBeginNode(root.RightMost), null, length, defaultAnchorMovementIsBeforeInsertion);
} else {
TextAnchorNode endNode = FindNode(ref offset);
Debug.Assert(endNode.length > 0);
if (offset > 0) {
// there are no nodes exactly at offset
endNode.length += length;
UpdateAugmentedData(endNode);
} else {
PerformInsertText(FindActualBeginNode(endNode.Predecessor), endNode, length, defaultAnchorMovementIsBeforeInsertion);
}
}
DeleteMarkedNodes();
}
TextAnchorNode FindActualBeginNode(TextAnchorNode node)
{
// now find the actual beginNode
while (node != null && node.length == 0)
node = node.Predecessor;
if (node == null) {
// no predecessor = beginNode is first node in tree
node = root.LeftMost;
}
return node;
}
// Sorts the nodes in the range [beginNode, endNode) by MovementType
// and inserts the length between the BeforeInsertion and the AfterInsertion nodes.
void PerformInsertText(TextAnchorNode beginNode, TextAnchorNode endNode, int length, bool defaultAnchorMovementIsBeforeInsertion)
{
Debug.Assert(beginNode != null);
// endNode may be null at the end of the anchor tree
// now we need to sort the nodes in the range [beginNode, endNode); putting those with
// MovementType.BeforeInsertion in front of those with MovementType.AfterInsertion
List<TextAnchorNode> beforeInsert = new List<TextAnchorNode>();
//List<TextAnchorNode> afterInsert = new List<TextAnchorNode>();
TextAnchorNode temp = beginNode;
while (temp != endNode) {
TextAnchor anchor = (TextAnchor)temp.Target;
if (anchor == null) {
// afterInsert.Add(temp);
MarkNodeForDelete(temp);
} else if (defaultAnchorMovementIsBeforeInsertion
? anchor.MovementType != AnchorMovementType.AfterInsertion
: anchor.MovementType == AnchorMovementType.BeforeInsertion)
{
beforeInsert.Add(temp);
// } else {
// afterInsert.Add(temp);
}
temp = temp.Successor;
}
// now again go through the range and swap the nodes with those in the beforeInsert list
temp = beginNode;
foreach (TextAnchorNode node in beforeInsert) {
SwapAnchors(node, temp);
temp = temp.Successor;
}
// now temp is pointing to the first node that is afterInsert,
// or to endNode, if there is no afterInsert node at the offset
// So add the length to temp
if (temp == null) {
// temp might be null if endNode==null and no afterInserts
Debug.Assert(endNode == null);
} else {
temp.length += length;
UpdateAugmentedData(temp);
}
}
/// <summary>
/// Swaps the anchors stored in the two nodes.
/// </summary>
void SwapAnchors(TextAnchorNode n1, TextAnchorNode n2)
{
if (n1 != n2) {
TextAnchor anchor1 = (TextAnchor)n1.Target;
TextAnchor anchor2 = (TextAnchor)n2.Target;
if (anchor1 == null && anchor2 == null) {
// -> no swap required
return;
}
n1.Target = anchor2;
n2.Target = anchor1;
if (anchor1 == null) {
// unmark n1 from deletion, mark n2 for deletion
nodesToDelete.Remove(n1);
MarkNodeForDelete(n2);
anchor2.node = n1;
} else if (anchor2 == null) {
// unmark n2 from deletion, mark n1 for deletion
nodesToDelete.Remove(n2);
MarkNodeForDelete(n1);
anchor1.node = n2;
} else {
anchor1.node = n2;
anchor2.node = n1;
}
}
}
#endregion
#region Remove or Replace text
public void HandleTextChange(OffsetChangeMapEntry entry, DelayedEvents delayedEvents)
{
//Log("HandleTextChange(" + entry + ")");
if (entry.RemovalLength == 0) {
// This is a pure insertion.
// Unlike a replace with removal, a pure insertion can result in nodes at the same location
// to split depending on their MovementType.
// Thus, we handle this case on a separate code path
// (the code below looks like it does something similar, but it can only split
// the set of deletion survivors, not all nodes at an offset)
InsertText(entry.Offset, entry.InsertionLength, entry.DefaultAnchorMovementIsBeforeInsertion);
return;
}
// When handling a replacing text change, we need to:
// - find all anchors in the deleted segment and delete them / move them to the appropriate
// surviving side.
// - adjust the segment size between the left and right side
int offset = entry.Offset;
int remainingRemovalLength = entry.RemovalLength;
// if the text change is happening after the last anchor, we don't have to do anything
if (root == null || offset >= root.totalLength)
return;
TextAnchorNode node = FindNode(ref offset);
TextAnchorNode firstDeletionSurvivor = null;
// go forward through the tree and delete all nodes in the removal segment
while (node != null && offset + remainingRemovalLength > node.length) {
TextAnchor anchor = (TextAnchor)node.Target;
if (anchor != null && (anchor.SurviveDeletion || entry.RemovalNeverCausesAnchorDeletion)) {
if (firstDeletionSurvivor == null)
firstDeletionSurvivor = node;
// This node should be deleted, but it wants to survive.
// We'll just remove the deleted length segment, so the node will be positioned
// in front of the removed segment.
remainingRemovalLength -= node.length - offset;
node.length = offset;
offset = 0;
UpdateAugmentedData(node);
node = node.Successor;
} else {
// delete node
TextAnchorNode s = node.Successor;
remainingRemovalLength -= node.length;
RemoveNode(node);
// we already deleted the node, don't delete it twice
nodesToDelete.Remove(node);
if (anchor != null)
anchor.OnDeleted(delayedEvents);
node = s;
}
}
// 'node' now is the first anchor after the deleted segment.
// If there are no anchors after the deleted segment, 'node' is null.
// firstDeletionSurvivor was set to the first node surviving deletion.
// Because all non-surviving nodes up to 'node' were deleted, the node range
// [firstDeletionSurvivor, node) now refers to the set of all deletion survivors.
// do the remaining job of the removal
if (node != null) {
node.length -= remainingRemovalLength;
Debug.Assert(node.length >= 0);
}
if (entry.InsertionLength > 0) {
// we are performing a replacement
if (firstDeletionSurvivor != null) {
// We got deletion survivors which need to be split into BeforeInsertion
// and AfterInsertion groups.
// Take care that we don't regroup everything at offset, but only the deletion
// survivors - from firstDeletionSurvivor (inclusive) to node (exclusive).
// This ensures that nodes immediately before or after the replaced segment
// stay where they are (independent from their MovementType)
PerformInsertText(firstDeletionSurvivor, node, entry.InsertionLength, entry.DefaultAnchorMovementIsBeforeInsertion);
} else if (node != null) {
// No deletion survivors:
// just perform the insertion
node.length += entry.InsertionLength;
}
}
if (node != null) {
UpdateAugmentedData(node);
}
DeleteMarkedNodes();
}
#endregion
#region Node removal when TextAnchor was GC'ed
void MarkNodeForDelete(TextAnchorNode node)
{
if (!nodesToDelete.Contains(node))
nodesToDelete.Add(node);
}
void DeleteMarkedNodes()
{
CheckProperties();
while (nodesToDelete.Count > 0) {
int pos = nodesToDelete.Count - 1;
TextAnchorNode n = nodesToDelete[pos];
// combine section of n with the following section
TextAnchorNode s = n.Successor;
if (s != null) {
s.length += n.length;
}
RemoveNode(n);
if (s != null) {
UpdateAugmentedData(s);
}
nodesToDelete.RemoveAt(pos);
CheckProperties();
}
CheckProperties();
}
#endregion
#region FindNode
/// <summary>
/// Finds the node at the specified offset.
/// After the method has run, offset is relative to the beginning of the returned node.
/// </summary>
TextAnchorNode FindNode(ref int offset)
{
TextAnchorNode n = root;
while (true) {
if (n.left != null) {
if (offset < n.left.totalLength) {
n = n.left; // descend into left subtree
continue;
} else {
offset -= n.left.totalLength; // skip left subtree
}
}
if (!n.IsAlive)
MarkNodeForDelete(n);
if (offset < n.length) {
return n; // found correct node
} else {
offset -= n.length; // skip this node
}
if (n.right != null) {
n = n.right; // descend into right subtree
} else {
// didn't find any node containing the offset
return null;
}
}
}
#endregion
#region UpdateAugmentedData
void UpdateAugmentedData(TextAnchorNode n)
{
if (!n.IsAlive)
MarkNodeForDelete(n);
int totalLength = n.length;
if (n.left != null)
totalLength += n.left.totalLength;
if (n.right != null)
totalLength += n.right.totalLength;
if (n.totalLength != totalLength) {
n.totalLength = totalLength;
if (n.parent != null)
UpdateAugmentedData(n.parent);
}
}
#endregion
#region CreateAnchor
public TextAnchor CreateAnchor(int offset)
{
Log("CreateAnchor(" + offset + ")");
TextAnchor anchor = new TextAnchor(document);
anchor.node = new TextAnchorNode(anchor);
if (root == null) {
// creating the first text anchor
root = anchor.node;
root.totalLength = root.length = offset;
} else if (offset >= root.totalLength) {
// append anchor at end of tree
anchor.node.totalLength = anchor.node.length = offset - root.totalLength;
InsertAsRight(root.RightMost, anchor.node);
} else {
// insert anchor in middle of tree
TextAnchorNode n = FindNode(ref offset);
Debug.Assert(offset < n.length);
// split segment 'n' at offset
anchor.node.totalLength = anchor.node.length = offset;
n.length -= offset;
InsertBefore(n, anchor.node);
}
DeleteMarkedNodes();
return anchor;
}
void InsertBefore(TextAnchorNode node, TextAnchorNode newNode)
{
if (node.left == null) {
InsertAsLeft(node, newNode);
} else {
InsertAsRight(node.left.RightMost, newNode);
}
}
#endregion
#region Red/Black Tree
internal const bool RED = true;
internal const bool BLACK = false;
void InsertAsLeft(TextAnchorNode parentNode, TextAnchorNode newNode)
{
Debug.Assert(parentNode.left == null);
parentNode.left = newNode;
newNode.parent = parentNode;
newNode.color = RED;
UpdateAugmentedData(parentNode);
FixTreeOnInsert(newNode);
}
void InsertAsRight(TextAnchorNode parentNode, TextAnchorNode newNode)
{
Debug.Assert(parentNode.right == null);
parentNode.right = newNode;
newNode.parent = parentNode;
newNode.color = RED;
UpdateAugmentedData(parentNode);
FixTreeOnInsert(newNode);
}
void FixTreeOnInsert(TextAnchorNode node)
{
Debug.Assert(node != null);
Debug.Assert(node.color == RED);
Debug.Assert(node.left == null || node.left.color == BLACK);
Debug.Assert(node.right == null || node.right.color == BLACK);
TextAnchorNode parentNode = node.parent;
if (parentNode == null) {
// we inserted in the root -> the node must be black
// since this is a root node, making the node black increments the number of black nodes
// on all paths by one, so it is still the same for all paths.
node.color = BLACK;
return;
}
if (parentNode.color == BLACK) {
// if the parent node where we inserted was black, our red node is placed correctly.
// since we inserted a red node, the number of black nodes on each path is unchanged
// -> the tree is still balanced
return;
}
// parentNode is red, so there is a conflict here!
// because the root is black, parentNode is not the root -> there is a grandparent node
TextAnchorNode grandparentNode = parentNode.parent;
TextAnchorNode uncleNode = Sibling(parentNode);
if (uncleNode != null && uncleNode.color == RED) {
parentNode.color = BLACK;
uncleNode.color = BLACK;
grandparentNode.color = RED;
FixTreeOnInsert(grandparentNode);
return;
}
// now we know: parent is red but uncle is black
// First rotation:
if (node == parentNode.right && parentNode == grandparentNode.left) {
RotateLeft(parentNode);
node = node.left;
} else if (node == parentNode.left && parentNode == grandparentNode.right) {
RotateRight(parentNode);
node = node.right;
}
// because node might have changed, reassign variables:
parentNode = node.parent;
grandparentNode = parentNode.parent;
// Now recolor a bit:
parentNode.color = BLACK;
grandparentNode.color = RED;
// Second rotation:
if (node == parentNode.left && parentNode == grandparentNode.left) {
RotateRight(grandparentNode);
} else {
// because of the first rotation, this is guaranteed:
Debug.Assert(node == parentNode.right && parentNode == grandparentNode.right);
RotateLeft(grandparentNode);
}
}
void RemoveNode(TextAnchorNode removedNode)
{
if (removedNode.left != null && removedNode.right != null) {
// replace removedNode with it's in-order successor
TextAnchorNode leftMost = removedNode.right.LeftMost;
RemoveNode(leftMost); // remove leftMost from its current location
// and overwrite the removedNode with it
ReplaceNode(removedNode, leftMost);
leftMost.left = removedNode.left;
if (leftMost.left != null) leftMost.left.parent = leftMost;
leftMost.right = removedNode.right;
if (leftMost.right != null) leftMost.right.parent = leftMost;
leftMost.color = removedNode.color;
UpdateAugmentedData(leftMost);
if (leftMost.parent != null) UpdateAugmentedData(leftMost.parent);
return;
}
// now either removedNode.left or removedNode.right is null
// get the remaining child
TextAnchorNode parentNode = removedNode.parent;
TextAnchorNode childNode = removedNode.left ?? removedNode.right;
ReplaceNode(removedNode, childNode);
if (parentNode != null) UpdateAugmentedData(parentNode);
if (removedNode.color == BLACK) {
if (childNode != null && childNode.color == RED) {
childNode.color = BLACK;
} else {
FixTreeOnDelete(childNode, parentNode);
}
}
}
void FixTreeOnDelete(TextAnchorNode node, TextAnchorNode parentNode)
{
Debug.Assert(node == null || node.parent == parentNode);
if (parentNode == null)
return;
// warning: node may be null
TextAnchorNode sibling = Sibling(node, parentNode);
if (sibling.color == RED) {
parentNode.color = RED;
sibling.color = BLACK;
if (node == parentNode.left) {
RotateLeft(parentNode);
} else {
RotateRight(parentNode);
}
sibling = Sibling(node, parentNode); // update value of sibling after rotation
}
if (parentNode.color == BLACK
&& sibling.color == BLACK
&& GetColor(sibling.left) == BLACK
&& GetColor(sibling.right) == BLACK)
{
sibling.color = RED;
FixTreeOnDelete(parentNode, parentNode.parent);
return;
}
if (parentNode.color == RED
&& sibling.color == BLACK
&& GetColor(sibling.left) == BLACK
&& GetColor(sibling.right) == BLACK)
{
sibling.color = RED;
parentNode.color = BLACK;
return;
}
if (node == parentNode.left &&
sibling.color == BLACK &&
GetColor(sibling.left) == RED &&
GetColor(sibling.right) == BLACK)
{
sibling.color = RED;
sibling.left.color = BLACK;
RotateRight(sibling);
}
else if (node == parentNode.right &&
sibling.color == BLACK &&
GetColor(sibling.right) == RED &&
GetColor(sibling.left) == BLACK)
{
sibling.color = RED;
sibling.right.color = BLACK;
RotateLeft(sibling);
}
sibling = Sibling(node, parentNode); // update value of sibling after rotation
sibling.color = parentNode.color;
parentNode.color = BLACK;
if (node == parentNode.left) {
if (sibling.right != null) {
Debug.Assert(sibling.right.color == RED);
sibling.right.color = BLACK;
}
RotateLeft(parentNode);
} else {
if (sibling.left != null) {
Debug.Assert(sibling.left.color == RED);
sibling.left.color = BLACK;
}
RotateRight(parentNode);
}
}
void ReplaceNode(TextAnchorNode replacedNode, TextAnchorNode newNode)
{
if (replacedNode.parent == null) {
Debug.Assert(replacedNode == root);
root = newNode;
} else {
if (replacedNode.parent.left == replacedNode)
replacedNode.parent.left = newNode;
else
replacedNode.parent.right = newNode;
}
if (newNode != null) {
newNode.parent = replacedNode.parent;
}
replacedNode.parent = null;
}
void RotateLeft(TextAnchorNode p)
{
// let q be p's right child
TextAnchorNode q = p.right;
Debug.Assert(q != null);
Debug.Assert(q.parent == p);
// set q to be the new root
ReplaceNode(p, q);
// set p's right child to be q's left child
p.right = q.left;
if (p.right != null) p.right.parent = p;
// set q's left child to be p
q.left = p;
p.parent = q;
UpdateAugmentedData(p);
UpdateAugmentedData(q);
}
void RotateRight(TextAnchorNode p)
{
// let q be p's left child
TextAnchorNode q = p.left;
Debug.Assert(q != null);
Debug.Assert(q.parent == p);
// set q to be the new root
ReplaceNode(p, q);
// set p's left child to be q's right child
p.left = q.right;
if (p.left != null) p.left.parent = p;
// set q's right child to be p
q.right = p;
p.parent = q;
UpdateAugmentedData(p);
UpdateAugmentedData(q);
}
static TextAnchorNode Sibling(TextAnchorNode node)
{
if (node == node.parent.left)
return node.parent.right;
else
return node.parent.left;
}
static TextAnchorNode Sibling(TextAnchorNode node, TextAnchorNode parentNode)
{
Debug.Assert(node == null || node.parent == parentNode);
if (node == parentNode.left)
return parentNode.right;
else
return parentNode.left;
}
static bool GetColor(TextAnchorNode node)
{
return node != null ? node.color : BLACK;
}
#endregion
#region CheckProperties
[Conditional("DATACONSISTENCYTEST")]
internal void CheckProperties()
{
#if DEBUG
if (root != null) {
CheckProperties(root);
// check red-black property:
int blackCount = -1;
CheckNodeProperties(root, null, RED, 0, ref blackCount);
}
#endif
}
#if DEBUG
void CheckProperties(TextAnchorNode node)
{
int totalLength = node.length;
if (node.left != null) {
CheckProperties(node.left);
totalLength += node.left.totalLength;
}
if (node.right != null) {
CheckProperties(node.right);
totalLength += node.right.totalLength;
}
Debug.Assert(node.totalLength == totalLength);
}
/*
1. A node is either red or black.
2. The root is black.
3. All leaves are black. (The leaves are the NIL children.)
4. Both children of every red node are black. (So every red node must have a black parent.)
5. Every simple path from a node to a descendant leaf contains the same number of black nodes. (Not counting the leaf node.)
*/
void CheckNodeProperties(TextAnchorNode node, TextAnchorNode parentNode, bool parentColor, int blackCount, ref int expectedBlackCount)
{
if (node == null) return;
Debug.Assert(node.parent == parentNode);
if (parentColor == RED) {
Debug.Assert(node.color == BLACK);
}
if (node.color == BLACK) {
blackCount++;
}
if (node.left == null && node.right == null) {
// node is a leaf node:
if (expectedBlackCount == -1)
expectedBlackCount = blackCount;
else
Debug.Assert(expectedBlackCount == blackCount);
}
CheckNodeProperties(node.left, node, node.color, blackCount, ref expectedBlackCount);
CheckNodeProperties(node.right, node, node.color, blackCount, ref expectedBlackCount);
}
#endif
#endregion
#region GetTreeAsString
#if DEBUG
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
public string GetTreeAsString()
{
if (root == null)
return "<empty tree>";
StringBuilder b = new StringBuilder();
AppendTreeToString(root, b, 0);
return b.ToString();
}
static void AppendTreeToString(TextAnchorNode node, StringBuilder b, int indent)
{
if (node.color == RED)
b.Append("RED ");
else
b.Append("BLACK ");
b.AppendLine(node.ToString());
indent += 2;
if (node.left != null) {
b.Append(' ', indent);
b.Append("L: ");
AppendTreeToString(node.left, b, indent);
}
if (node.right != null) {
b.Append(' ', indent);
b.Append("R: ");
AppendTreeToString(node.right, b, indent);
}
}
#endif
#endregion
}
}

817
ICSharpCode.AvalonEdit/Document/TextDocument.cs

@ -0,0 +1,817 @@ @@ -0,0 +1,817 @@
// 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.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Globalization;
using System.Linq.Expressions;
using System.Threading;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// This class is the main class of the text model. Basically, it is a <see cref="System.Text.StringBuilder"/> with events.
/// </summary>
/// <remarks>
/// <b>Thread safety:</b>
/// <inheritdoc cref="VerifyAccess"/>
/// <para>However, there is a single method that is thread-safe: <see cref="CreateSnapshot()"/> (and its overloads).</para>
/// </remarks>
public sealed class TextDocument : ITextSource
{
#region Thread ownership
readonly object lockObject = new object();
Thread owner = Thread.CurrentThread;
/// <summary>
/// Verifies that the current thread is the documents owner thread.
/// Throws an <see cref="InvalidOperationException"/> if the wrong thread accesses the TextDocument.
/// </summary>
/// <remarks>
/// <para>The TextDocument class is not thread-safe. A document instance expects to have a single owner thread
/// and will throw an <see cref="InvalidOperationException"/> when accessed from another thread.
/// It is possible to change the owner thread using the <see cref="SetOwnerThread"/> method.</para>
/// </remarks>
public void VerifyAccess()
{
if (Thread.CurrentThread != owner)
throw new InvalidOperationException("TextDocument can be accessed only from the thread that owns it.");
}
/// <summary>
/// Transfers ownership of the document to another thread. This method can be used to load
/// a file into a TextDocument on a background thread and then transfer ownership to the UI thread
/// for displaying the document.
/// </summary>
/// <remarks>
/// <inheritdoc cref="VerifyAccess"/>
/// <para>
/// The owner can be set to null, which means that no thread can access the document. But, if the document
/// has no owner thread, any thread may take ownership by calling <see cref="SetOwnerThread"/>.
/// </para>
/// </remarks>
public void SetOwnerThread(Thread newOwner)
{
// We need to lock here to ensure that in the null owner case,
// only one thread succeeds in taking ownership.
lock (lockObject) {
if (owner != null) {
VerifyAccess();
}
owner = newOwner;
}
}
#endregion
#region Fields + Constructor
readonly Rope<char> rope;
readonly DocumentLineTree lineTree;
readonly LineManager lineManager;
readonly TextAnchorTree anchorTree;
ChangeTrackingCheckpoint currentCheckpoint;
/// <summary>
/// Create an empty text document.
/// </summary>
public TextDocument()
: this(string.Empty)
{
}
/// <summary>
/// Create a new text document with the specified initial text.
/// </summary>
public TextDocument(IEnumerable<char> initialText)
{
if (initialText == null)
throw new ArgumentNullException("initialText");
rope = new Rope<char>(initialText);
lineTree = new DocumentLineTree(this);
lineManager = new LineManager(lineTree, this);
lineTrackers.CollectionChanged += delegate {
lineManager.UpdateListOfLineTrackers();
};
anchorTree = new TextAnchorTree(this);
undoStack = new UndoStack();
FireChangeEvents();
}
/// <summary>
/// Create a new text document with the specified initial text.
/// </summary>
public TextDocument(ITextSource initialText)
: this(GetTextFromTextSource(initialText))
{
}
// gets the text from a text source, directly retrieving the underlying rope where possible
static IEnumerable<char> GetTextFromTextSource(ITextSource textSource)
{
if (textSource == null)
throw new ArgumentNullException("textSource");
RopeTextSource rts = textSource as RopeTextSource;
if (rts != null)
return rts.GetRope();
TextDocument doc = textSource as TextDocument;
if (doc != null)
return doc.rope;
return textSource.Text;
}
#endregion
#region Text
void ThrowIfRangeInvalid(int offset, int length)
{
if (offset < 0 || offset > rope.Length) {
throw new ArgumentOutOfRangeException("offset", offset, "0 <= offset <= " + rope.Length.ToString(CultureInfo.InvariantCulture));
}
if (length < 0 || offset + length > rope.Length) {
throw new ArgumentOutOfRangeException("length", length, "0 <= length, offset(" + offset + ")+length <= " + rope.Length.ToString(CultureInfo.InvariantCulture));
}
}
/// <inheritdoc/>
public string GetText(int offset, int length)
{
VerifyAccess();
return rope.ToString(offset, length);
}
/// <summary>
/// Retrieves the text for a portion of the document.
/// </summary>
public string GetText(ISegment segment)
{
if (segment == null)
throw new ArgumentNullException("segment");
return GetText(segment.Offset, segment.Length);
}
int ITextSource.IndexOfAny(char[] anyOf, int startIndex, int count)
{
DebugVerifyAccess(); // frequently called (NewLineFinder), so must be fast in release builds
return rope.IndexOfAny(anyOf, startIndex, count);
}
/// <inheritdoc/>
public char GetCharAt(int offset)
{
DebugVerifyAccess(); // frequently called, so must be fast in release builds
return rope[offset];
}
WeakReference cachedText;
/// <summary>
/// Gets/Sets the text of the whole document.
/// </summary>
public string Text {
get {
VerifyAccess();
string completeText = cachedText != null ? (cachedText.Target as string) : null;
if (completeText == null) {
completeText = rope.ToString();
cachedText = new WeakReference(completeText);
}
return completeText;
}
set {
VerifyAccess();
if (value == null)
throw new ArgumentNullException("value");
Replace(0, rope.Length, value);
}
}
/// <inheritdoc/>
/// <remarks><inheritdoc cref="Changing"/></remarks>
public event EventHandler TextChanged;
/// <inheritdoc/>
public int TextLength {
get {
VerifyAccess();
return rope.Length;
}
}
/// <summary>
/// Is raised when the TextLength property changes.
/// </summary>
/// <remarks><inheritdoc cref="Changing"/></remarks>
public event EventHandler TextLengthChanged;
/// <summary>
/// Is raised before the document changes.
/// </summary>
/// <remarks>
/// <para>Here is the order in which events are raised during a document update:</para>
/// <list type="bullet">
/// <item><description><b><see cref="BeginUpdate">BeginUpdate()</see></b></description>
/// <list type="bullet">
/// <item><description>Start of change group (on undo stack)</description></item>
/// <item><description><see cref="UpdateStarted"/> event is raised</description></item>
/// </list></item>
/// <item><description><b><see cref="Insert(int,string)">Insert()</see> / <see cref="Remove(int,int)">Remove()</see> / <see cref="Replace(int,int,string)">Replace()</see></b></description>
/// <list type="bullet">
/// <item><description><see cref="Changing"/> event is raised</description></item>
/// <item><description>The document is changed</description></item>
/// <item><description><see cref="TextAnchor.Deleted">TextAnchor.Deleted</see> event is raised if anchors were
/// in the deleted text portion</description></item>
/// <item><description><see cref="Changed"/> event is raised</description></item>
/// </list></item>
/// <item><description><b><see cref="EndUpdate">EndUpdate()</see></b></description>
/// <list type="bullet">
/// <item><description><see cref="TextChanged"/> event is raised</description></item>
/// <item><description><see cref="TextLengthChanged"/> event is raised</description></item>
/// <item><description><see cref="LineCountChanged"/> event is raised</description></item>
/// <item><description>End of change group (on undo stack)</description></item>
/// <item><description><see cref="UpdateFinished"/> event is raised</description></item>
/// </list></item>
/// </list>
/// <para>
/// If the insert/remove/replace methods are called without a call to <c>BeginUpdate()</c>,
/// they will call <c>BeginUpdate()</c> and <c>EndUpdate()</c> to ensure no change happens outside of <c>UpdateStarted</c>/<c>UpdateFinished</c>.
/// </para><para>
/// There can be multiple document changes between the <c>BeginUpdate()</c> and <c>EndUpdate()</c> calls.
/// In this case, the events associated with EndUpdate will be raised only once after the whole document update is done.
/// </para><para>
/// The <see cref="UndoStack"/> listens to the <c>UpdateStarted</c> and <c>UpdateFinished</c> events to group all changes into a single undo step.
/// </para>
/// </remarks>
public event EventHandler<DocumentChangeEventArgs> Changing;
/// <summary>
/// Is raised after the document has changed.
/// </summary>
/// <remarks><inheritdoc cref="Changing"/></remarks>
public event EventHandler<DocumentChangeEventArgs> Changed;
/// <summary>
/// Creates a snapshot of the current text.
/// </summary>
/// <remarks>
/// <para>This method returns an immutable snapshot of the document, and may be safely called even when
/// the document's owner thread is concurrently modifying the document.
/// </para><para>
/// This special thread-safety guarantee is valid only for TextDocument.CreateSnapshot(), not necessarily for other
/// classes implementing ITextSource.CreateSnapshot().
/// </para><para>
/// </para>
/// </remarks>
public ITextSource CreateSnapshot()
{
lock (lockObject) {
return new RopeTextSource(rope.Clone());
}
}
/// <summary>
/// Creates a snapshot of the current text.
/// Additionally, creates a checkpoint that allows tracking document changes.
/// </summary>
/// <remarks><inheritdoc cref="CreateSnapshot()"/><inheritdoc cref="ChangeTrackingCheckpoint"/></remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", Justification = "Need to return snapshot and checkpoint together to ensure thread-safety")]
public ITextSource CreateSnapshot(out ChangeTrackingCheckpoint checkpoint)
{
lock (lockObject) {
if (currentCheckpoint == null)
currentCheckpoint = new ChangeTrackingCheckpoint(lockObject);
checkpoint = currentCheckpoint;
return new RopeTextSource(rope.Clone());
}
}
internal ChangeTrackingCheckpoint CreateChangeTrackingCheckpoint()
{
lock (lockObject) {
if (currentCheckpoint == null)
currentCheckpoint = new ChangeTrackingCheckpoint(lockObject);
return currentCheckpoint;
}
}
/// <summary>
/// Creates a snapshot of a part of the current text.
/// </summary>
/// <remarks><inheritdoc cref="CreateSnapshot()"/></remarks>
public ITextSource CreateSnapshot(int offset, int length)
{
lock (lockObject) {
return new RopeTextSource(rope.GetRange(offset, length));
}
}
/// <inheritdoc/>
public System.IO.TextReader CreateReader()
{
lock (lockObject) {
return new RopeTextReader(rope);
}
}
#endregion
#region BeginUpdate / EndUpdate
int beginUpdateCount;
/// <summary>
/// Gets if an update is running.
/// </summary>
/// <remarks><inheritdoc cref="BeginUpdate"/></remarks>
public bool IsInUpdate {
get {
VerifyAccess();
return beginUpdateCount > 0;
}
}
/// <summary>
/// Immediately calls <see cref="BeginUpdate()"/>,
/// and returns an IDisposable that calls <see cref="EndUpdate()"/>.
/// </summary>
/// <remarks><inheritdoc cref="BeginUpdate"/></remarks>
public IDisposable RunUpdate()
{
BeginUpdate();
return new CallbackOnDispose(EndUpdate);
}
/// <summary>
/// <para>Begins a group of document changes.</para>
/// <para>Some events are suspended until EndUpdate is called, and the <see cref="UndoStack"/> will
/// group all changes into a single action.</para>
/// <para>Calling BeginUpdate several times increments a counter, only after the appropriate number
/// of EndUpdate calls the events resume their work.</para>
/// </summary>
/// <remarks><inheritdoc cref="Changing"/></remarks>
public void BeginUpdate()
{
VerifyAccess();
if (inDocumentChanging)
throw new InvalidOperationException("Cannot change document within another document change.");
beginUpdateCount++;
if (beginUpdateCount == 1) {
undoStack.StartUndoGroup();
if (UpdateStarted != null)
UpdateStarted(this, EventArgs.Empty);
}
}
/// <summary>
/// Ends a group of document changes.
/// </summary>
/// <remarks><inheritdoc cref="Changing"/></remarks>
public void EndUpdate()
{
VerifyAccess();
if (inDocumentChanging)
throw new InvalidOperationException("Cannot end update within document change.");
if (beginUpdateCount == 0)
throw new InvalidOperationException("No update is active.");
if (beginUpdateCount == 1) {
// fire change events inside the change group - event handlers might add additional
// document changes to the change group
FireChangeEvents();
undoStack.EndUndoGroup();
beginUpdateCount = 0;
if (UpdateFinished != null)
UpdateFinished(this, EventArgs.Empty);
} else {
beginUpdateCount -= 1;
}
}
/// <summary>
/// Occurs when a document change starts.
/// </summary>
/// <remarks><inheritdoc cref="Changing"/></remarks>
public event EventHandler UpdateStarted;
/// <summary>
/// Occurs when a document change is finished.
/// </summary>
/// <remarks><inheritdoc cref="Changing"/></remarks>
public event EventHandler UpdateFinished;
#endregion
#region Fire events after update
int oldTextLength;
int oldLineCount;
bool fireTextChanged;
/// <summary>
/// Fires TextChanged, TextLengthChanged, LineCountChanged if required.
/// </summary>
internal void FireChangeEvents()
{
// it may be necessary to fire the event multiple times if the document is changed
// from inside the event handlers
while (fireTextChanged) {
fireTextChanged = false;
if (TextChanged != null)
TextChanged(this, EventArgs.Empty);
int textLength = rope.Length;
if (textLength != oldTextLength) {
oldTextLength = textLength;
if (TextLengthChanged != null)
TextLengthChanged(this, EventArgs.Empty);
}
int lineCount = lineTree.LineCount;
if (lineCount != oldLineCount) {
oldLineCount = lineCount;
if (LineCountChanged != null)
LineCountChanged(this, EventArgs.Empty);
}
}
}
#endregion
#region Insert / Remove / Replace
/// <summary>
/// Inserts text.
/// </summary>
public void Insert(int offset, string text)
{
Replace(offset, 0, text);
}
/// <summary>
/// Removes text.
/// </summary>
public void Remove(ISegment segment)
{
Replace(segment, string.Empty);
}
/// <summary>
/// Removes text.
/// </summary>
public void Remove(int offset, int length)
{
Replace(offset, length, string.Empty);
}
internal bool inDocumentChanging;
/// <summary>
/// Replaces text.
/// </summary>
public void Replace(ISegment segment, string text)
{
if (segment == null)
throw new ArgumentNullException("segment");
Replace(segment.Offset, segment.Length, text, null);
}
/// <summary>
/// Replaces text.
/// </summary>
public void Replace(int offset, int length, string text)
{
Replace(offset, length, text, null);
}
/// <summary>
/// Replaces text.
/// </summary>
/// <param name="offset">The starting offset of the text to be replaced.</param>
/// <param name="length">The length of the text to be replaced.</param>
/// <param name="text">The new text.</param>
/// <param name="offsetChangeMappingType">The offsetChangeMappingType determines how offsets inside the old text are mapped to the new text.
/// This affects how the anchors and segments inside the replaced region behave.</param>
public void Replace(int offset, int length, string text, OffsetChangeMappingType offsetChangeMappingType)
{
if (text == null)
throw new ArgumentNullException("text");
// Please see OffsetChangeMappingType XML comments for details on how these modes work.
switch (offsetChangeMappingType) {
case OffsetChangeMappingType.Normal:
Replace(offset, length, text, null);
break;
case OffsetChangeMappingType.KeepAnchorBeforeInsertion:
Replace(offset, length, text, OffsetChangeMap.FromSingleElement(
new OffsetChangeMapEntry(offset, length, text.Length, false, true)));
break;
case OffsetChangeMappingType.RemoveAndInsert:
if (length == 0 || text.Length == 0) {
// only insertion or only removal?
// OffsetChangeMappingType doesn't matter, just use Normal.
Replace(offset, length, text, null);
} else {
OffsetChangeMap map = new OffsetChangeMap(2);
map.Add(new OffsetChangeMapEntry(offset, length, 0));
map.Add(new OffsetChangeMapEntry(offset, 0, text.Length));
map.Freeze();
Replace(offset, length, text, map);
}
break;
case OffsetChangeMappingType.CharacterReplace:
if (length == 0 || text.Length == 0) {
// only insertion or only removal?
// OffsetChangeMappingType doesn't matter, just use Normal.
Replace(offset, length, text, null);
} else if (text.Length > length) {
// look at OffsetChangeMappingType.CharacterReplace XML comments on why we need to replace
// the last character
OffsetChangeMapEntry entry = new OffsetChangeMapEntry(offset + length - 1, 1, 1 + text.Length - length);
Replace(offset, length, text, OffsetChangeMap.FromSingleElement(entry));
} else if (text.Length < length) {
OffsetChangeMapEntry entry = new OffsetChangeMapEntry(offset + text.Length, length - text.Length, 0, true, false);
Replace(offset, length, text, OffsetChangeMap.FromSingleElement(entry));
} else {
Replace(offset, length, text, OffsetChangeMap.Empty);
}
break;
default:
throw new ArgumentOutOfRangeException("offsetChangeMappingType", offsetChangeMappingType, "Invalid enum value");
}
}
/// <summary>
/// Replaces text.
/// </summary>
/// <param name="offset">The starting offset of the text to be replaced.</param>
/// <param name="length">The length of the text to be replaced.</param>
/// <param name="text">The new text.</param>
/// <param name="offsetChangeMap">The offsetChangeMap determines how offsets inside the old text are mapped to the new text.
/// This affects how the anchors and segments inside the replaced region behave.
/// If you pass null (the default when using one of the other overloads), the offsets are changed as
/// in OffsetChangeMappingType.Normal mode.
/// If you pass OffsetChangeMap.Empty, then everything will stay in its old place (OffsetChangeMappingType.CharacterReplace mode).
/// The offsetChangeMap must be a valid 'explanation' for the document change. See <see cref="OffsetChangeMap.IsValidForDocumentChange"/>.
/// Passing an OffsetChangeMap to the Replace method will automatically freeze it to ensure the thread safety of the resulting
/// DocumentChangeEventArgs instance.
/// </param>
public void Replace(int offset, int length, string text, OffsetChangeMap offsetChangeMap)
{
if (text == null)
throw new ArgumentNullException("text");
if (offsetChangeMap != null)
offsetChangeMap.Freeze();
// Ensure that all changes take place inside an update group.
// Will also take care of throwing an exception if inDocumentChanging is set.
BeginUpdate();
try {
// protect document change against corruption by other changes inside the event handlers
inDocumentChanging = true;
try {
// The range verification must wait until after the BeginUpdate() call because the document
// might be modified inside the UpdateStarted event.
ThrowIfRangeInvalid(offset, length);
DoReplace(offset, length, text, offsetChangeMap);
} finally {
inDocumentChanging = false;
}
} finally {
EndUpdate();
}
}
void DoReplace(int offset, int length, string newText, OffsetChangeMap offsetChangeMap)
{
if (length == 0 && newText.Length == 0)
return;
// trying to replace a single character in 'Normal' mode?
// for single characters, 'CharacterReplace' mode is equivalent, but more performant
// (we don't have to touch the anchorTree at all in 'CharacterReplace' mode)
if (length == 1 && newText.Length == 1 && offsetChangeMap == null)
offsetChangeMap = OffsetChangeMap.Empty;
string removedText = rope.ToString(offset, length);
DocumentChangeEventArgs args = new DocumentChangeEventArgs(offset, removedText, newText, offsetChangeMap);
// fire DocumentChanging event
if (Changing != null)
Changing(this, args);
undoStack.Push(this, args);
cachedText = null; // reset cache of complete document text
fireTextChanged = true;
DelayedEvents delayedEvents = new DelayedEvents();
lock (lockObject) {
// create linked list of checkpoints, if required
if (currentCheckpoint != null) {
currentCheckpoint = currentCheckpoint.Append(args);
}
// now update the textBuffer and lineTree
if (offset == 0 && length == rope.Length) {
// optimize replacing the whole document
rope.Clear();
rope.InsertText(0, newText);
lineManager.Rebuild();
} else {
rope.RemoveRange(offset, length);
lineManager.Remove(offset, length);
#if DEBUG
lineTree.CheckProperties();
#endif
rope.InsertText(offset, newText);
lineManager.Insert(offset, newText);
#if DEBUG
lineTree.CheckProperties();
#endif
}
}
// update text anchors
if (offsetChangeMap == null) {
anchorTree.HandleTextChange(args.CreateSingleChangeMapEntry(), delayedEvents);
} else {
foreach (OffsetChangeMapEntry entry in offsetChangeMap) {
anchorTree.HandleTextChange(entry, delayedEvents);
}
}
// raise delayed events after our data structures are consistent again
delayedEvents.RaiseEvents();
// fire DocumentChanged event
if (Changed != null)
Changed(this, args);
}
#endregion
#region GetLineBy...
/// <summary>
/// Gets a read-only list of lines.
/// </summary>
/// <remarks><inheritdoc cref="DocumentLine"/></remarks>
public IList<DocumentLine> Lines {
get { return lineTree; }
}
/// <summary>
/// Gets a line by the line number: O(log n)
/// </summary>
public DocumentLine GetLineByNumber(int number)
{
VerifyAccess();
if (number < 1 || number > lineTree.LineCount)
throw new ArgumentOutOfRangeException("number", number, "Value must be between 1 and " + lineTree.LineCount);
return lineTree.GetByNumber(number);
}
/// <summary>
/// Gets a document lines by offset.
/// Runtime: O(log n)
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.Int32.ToString")]
public DocumentLine GetLineByOffset(int offset)
{
VerifyAccess();
if (offset < 0 || offset > rope.Length) {
throw new ArgumentOutOfRangeException("offset", offset, "0 <= offset <= " + rope.Length.ToString());
}
return lineTree.GetByOffset(offset);
}
#endregion
/// <summary>
/// Gets the offset from a text location.
/// </summary>
/// <seealso cref="GetLocation"/>
public int GetOffset(TextLocation location)
{
return GetOffset(location.Line, location.Column);
}
/// <summary>
/// Gets the offset from a text location.
/// </summary>
/// <seealso cref="GetLocation"/>
public int GetOffset(int line, int column)
{
DocumentLine docLine = GetLineByNumber(line);
if (column <= 0)
return docLine.Offset;
if (column > docLine.Length)
return docLine.EndOffset;
return docLine.Offset + column - 1;
}
/// <summary>
/// Gets the location from an offset.
/// </summary>
/// <seealso cref="GetOffset(TextLocation)"/>
public TextLocation GetLocation(int offset)
{
DocumentLine line = GetLineByOffset(offset);
return new TextLocation(line.LineNumber, offset - line.Offset + 1);
}
readonly ObservableCollection<ILineTracker> lineTrackers = new ObservableCollection<ILineTracker>();
/// <summary>
/// Gets the list of <see cref="ILineTracker"/>s attached to this document.
/// You can add custom line trackers to this list.
/// </summary>
public IList<ILineTracker> LineTrackers {
get {
VerifyAccess();
return lineTrackers;
}
}
UndoStack undoStack;
/// <summary>
/// Gets the <see cref="UndoStack"/> of the document.
/// </summary>
/// <remarks>This property can also be used to set the undo stack, e.g. for sharing a common undo stack between multiple documents.</remarks>
public UndoStack UndoStack {
get { return undoStack; }
set {
if (value == null)
throw new ArgumentNullException();
if (value != undoStack) {
undoStack.ClearAll(); // first clear old undo stack, so that it can't be used to perform unexpected changes on this document
// ClearAll() will also throw an exception when it's not safe to replace the undo stack (e.g. update is currently in progress)
undoStack = value;
}
}
}
/// <summary>
/// Creates a new <see cref="TextAnchor"/> at the specified offset.
/// </summary>
/// <inheritdoc cref="TextAnchor" select="remarks|example"/>
public TextAnchor CreateAnchor(int offset)
{
VerifyAccess();
if (offset < 0 || offset > rope.Length) {
throw new ArgumentOutOfRangeException("offset", offset, "0 <= offset <= " + rope.Length.ToString(CultureInfo.InvariantCulture));
}
return anchorTree.CreateAnchor(offset);
}
#region LineCount
/// <summary>
/// Gets the total number of lines in the document.
/// Runtime: O(1).
/// </summary>
public int LineCount {
get {
VerifyAccess();
return lineTree.LineCount;
}
}
/// <summary>
/// Is raised when the LineCount property changes.
/// </summary>
public event EventHandler LineCountChanged;
#endregion
#region Debugging
[Conditional("DEBUG")]
internal void DebugVerifyAccess()
{
VerifyAccess();
}
/// <summary>
/// Gets the document lines tree in string form.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
internal string GetLineTreeAsString()
{
#if DEBUG
return lineTree.GetTreeAsString();
#else
return "Not available in release build.";
#endif
}
/// <summary>
/// Gets the text anchor tree in string form.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
internal string GetTextAnchorTreeAsString()
{
#if DEBUG
return anchorTree.GetTreeAsString();
#else
return "Not available in release build.";
#endif
}
#endregion
}
}

147
ICSharpCode.AvalonEdit/Document/TextDocumentWeakEventManager.cs

@ -0,0 +1,147 @@ @@ -0,0 +1,147 @@
// 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 ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// Contains weak event managers for the TextDocument events.
/// </summary>
public static class TextDocumentWeakEventManager
{
/// <summary>
/// Weak event manager for the <see cref="TextDocument.UpdateStarted"/> event.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
public sealed class UpdateStarted : WeakEventManagerBase<UpdateStarted, TextDocument>
{
/// <inheritdoc/>
protected override void StartListening(TextDocument source)
{
source.UpdateStarted += DeliverEvent;
}
/// <inheritdoc/>
protected override void StopListening(TextDocument source)
{
source.UpdateStarted -= DeliverEvent;
}
}
/// <summary>
/// Weak event manager for the <see cref="TextDocument.UpdateFinished"/> event.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
public sealed class UpdateFinished : WeakEventManagerBase<UpdateFinished, TextDocument>
{
/// <inheritdoc/>
protected override void StartListening(TextDocument source)
{
source.UpdateFinished += DeliverEvent;
}
/// <inheritdoc/>
protected override void StopListening(TextDocument source)
{
source.UpdateFinished -= DeliverEvent;
}
}
/// <summary>
/// Weak event manager for the <see cref="TextDocument.Changing"/> event.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
public sealed class Changing : WeakEventManagerBase<Changing, TextDocument>
{
/// <inheritdoc/>
protected override void StartListening(TextDocument source)
{
source.Changing += DeliverEvent;
}
/// <inheritdoc/>
protected override void StopListening(TextDocument source)
{
source.Changing -= DeliverEvent;
}
}
/// <summary>
/// Weak event manager for the <see cref="TextDocument.Changed"/> event.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
public sealed class Changed : WeakEventManagerBase<Changed, TextDocument>
{
/// <inheritdoc/>
protected override void StartListening(TextDocument source)
{
source.Changed += DeliverEvent;
}
/// <inheritdoc/>
protected override void StopListening(TextDocument source)
{
source.Changed -= DeliverEvent;
}
}
/// <summary>
/// Weak event manager for the <see cref="TextDocument.LineCountChanged"/> event.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
public sealed class LineCountChanged : WeakEventManagerBase<LineCountChanged, TextDocument>
{
/// <inheritdoc/>
protected override void StartListening(TextDocument source)
{
source.LineCountChanged += DeliverEvent;
}
/// <inheritdoc/>
protected override void StopListening(TextDocument source)
{
source.LineCountChanged -= DeliverEvent;
}
}
/// <summary>
/// Weak event manager for the <see cref="TextDocument.TextLengthChanged"/> event.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
public sealed class TextLengthChanged : WeakEventManagerBase<TextLengthChanged, TextDocument>
{
/// <inheritdoc/>
protected override void StartListening(TextDocument source)
{
source.TextLengthChanged += DeliverEvent;
}
/// <inheritdoc/>
protected override void StopListening(TextDocument source)
{
source.TextLengthChanged -= DeliverEvent;
}
}
/// <summary>
/// Weak event manager for the <see cref="TextDocument.TextChanged"/> event.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
public sealed class TextChanged : WeakEventManagerBase<TextChanged, TextDocument>
{
/// <inheritdoc/>
protected override void StartListening(TextDocument source)
{
source.TextChanged += DeliverEvent;
}
/// <inheritdoc/>
protected override void StopListening(TextDocument source)
{
source.TextChanged -= DeliverEvent;
}
}
}
}

166
ICSharpCode.AvalonEdit/Document/TextLocation.cs

@ -0,0 +1,166 @@ @@ -0,0 +1,166 @@
// 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.Globalization;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// A line/column position.
/// Text editor lines/columns are counted started from one.
/// </summary>
/// <remarks>
/// The document provides the methods <see cref="TextDocument.GetLocation"/> and
/// <see cref="TextDocument.GetOffset(TextLocation)"/> to convert between offsets and TextLocations.
/// </remarks>
public struct TextLocation : IComparable<TextLocation>, IEquatable<TextLocation>
{
/// <summary>
/// Represents no text location (0, 0).
/// </summary>
public static readonly TextLocation Empty = new TextLocation(0, 0);
/// <summary>
/// Creates a TextLocation instance.
/// <para>
/// Warning: the parameters are (line, column).
/// Not (column, line) as in ICSharpCode.TextEditor!
/// </para>
/// </summary>
public TextLocation(int line, int column)
{
y = line;
x = column;
}
int x, y;
/// <summary>
/// Gets the line number.
/// </summary>
public int Line {
get { return y; }
}
/// <summary>
/// Gets the column number.
/// </summary>
public int Column {
get { return x; }
}
/// <summary>
/// Gets whether the TextLocation instance is empty.
/// </summary>
public bool IsEmpty {
get {
return x <= 0 && y <= 0;
}
}
/// <summary>
/// Gets a string representation for debugging purposes.
/// </summary>
public override string ToString()
{
return string.Format(CultureInfo.InvariantCulture, "(Line {1}, Col {0})", this.x, this.y);
}
/// <summary>
/// Gets a hash code.
/// </summary>
public override int GetHashCode()
{
return unchecked (87 * x.GetHashCode() ^ y.GetHashCode());
}
/// <summary>
/// Equality test.
/// </summary>
public override bool Equals(object obj)
{
if (!(obj is TextLocation)) return false;
return (TextLocation)obj == this;
}
/// <summary>
/// Equality test.
/// </summary>
public bool Equals(TextLocation other)
{
return this == other;
}
/// <summary>
/// Equality test.
/// </summary>
public static bool operator ==(TextLocation left, TextLocation right)
{
return left.x == right.x && left.y == right.y;
}
/// <summary>
/// Inequality test.
/// </summary>
public static bool operator !=(TextLocation left, TextLocation right)
{
return left.x != right.x || left.y != right.y;
}
/// <summary>
/// Compares two text locations.
/// </summary>
public static bool operator <(TextLocation left, TextLocation right)
{
if (left.y < right.y)
return true;
else if (left.y == right.y)
return left.x < right.x;
else
return false;
}
/// <summary>
/// Compares two text locations.
/// </summary>
public static bool operator >(TextLocation left, TextLocation right)
{
if (left.y > right.y)
return true;
else if (left.y == right.y)
return left.x > right.x;
else
return false;
}
/// <summary>
/// Compares two text locations.
/// </summary>
public static bool operator <=(TextLocation left, TextLocation right)
{
return !(left > right);
}
/// <summary>
/// Compares two text locations.
/// </summary>
public static bool operator >=(TextLocation left, TextLocation right)
{
return !(left < right);
}
/// <summary>
/// Compares two text locations.
/// </summary>
public int CompareTo(TextLocation other)
{
if (this == other)
return 0;
if (this < other)
return -1;
else
return 1;
}
}
}

236
ICSharpCode.AvalonEdit/Document/TextSegment.cs

@ -0,0 +1,236 @@ @@ -0,0 +1,236 @@
// 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.Diagnostics;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// A segment that can be put into a <see cref="TextSegmentCollection{T}"/>.
/// </summary>
/// <remarks>
/// <para>
/// A <see cref="TextSegment"/> can be stand-alone or part of a <see cref="TextSegmentCollection{T}"/>.
/// If the segment is stored inside a TextSegmentCollection, its Offset and Length will be updated by that collection.
/// </para>
/// <para>
/// When the document changes, the offsets of all text segments in the TextSegmentCollection will be adjusted accordingly.
/// Start offsets move like <see cref="AnchorMovementType">AnchorMovementType.AfterInsertion</see>,
/// end offsets move like <see cref="AnchorMovementType">AnchorMovementType.BeforeInsertion</see>
/// (i.e. the segment will always stay as small as possible).</para>
/// <para>
/// If a document change causes a segment to be deleted completely, it will be reduced to length 0, but segments are
/// never automatically removed from the collection.
/// Segments with length 0 will never expand due to document changes, and they move as <c>AfterInsertion</c>.
/// </para>
/// <para>
/// Thread-safety: a TextSegmentCollection that is connected to a <see cref="TextDocument"/> may only be used on that document's owner thread.
/// A disconnected TextSegmentCollection is safe for concurrent reads, but concurrent access is not safe when there are writes.
/// Keep in mind that reading the Offset properties of a text segment inside the collection is a read access on the
/// collection; and setting an Offset property of a text segment is a write access on the collection.
/// </para>
/// </remarks>
/// <seealso cref="ISegment"/>
/// <seealso cref="AnchorSegment"/>
/// <seealso cref="TextSegmentCollection{T}"/>
public class TextSegment : ISegment
{
internal ISegmentTree ownerTree;
internal TextSegment left, right, parent;
/// <summary>
/// The color of the segment in the red/black tree.
/// </summary>
internal bool color;
/// <summary>
/// The "length" of the node (distance to previous node)
/// </summary>
internal int nodeLength;
/// <summary>
/// The total "length" of this subtree.
/// </summary>
internal int totalNodeLength; // totalNodeLength = nodeLength + left.totalNodeLength + right.totalNodeLength
/// <summary>
/// The length of the segment (do not confuse with nodeLength).
/// </summary>
internal int segmentLength;
/// <summary>
/// distanceToMaxEnd = Max(segmentLength,
/// left.distanceToMaxEnd + left.Offset - Offset,
/// left.distanceToMaxEnd + right.Offset - Offset)
/// </summary>
internal int distanceToMaxEnd;
int ISegment.Offset {
get { return StartOffset; }
}
/// <summary>
/// Gets whether this segment is connected to a TextSegmentCollection and will automatically
/// update its offsets.
/// </summary>
protected bool IsConnectedToCollection {
get {
return ownerTree != null;
}
}
/// <summary>
/// Gets/Sets the start offset of the segment.
/// </summary>
/// <remarks>
/// When setting the start offset, the end offset will change, too: the Length of the segment will stay constant.
/// </remarks>
public int StartOffset {
get {
// If the segment is not connected to a tree, we store the offset in "nodeLength".
// Otherwise, "nodeLength" contains the distance to the start offset of the previous node
Debug.Assert(!(ownerTree == null && parent != null));
Debug.Assert(!(ownerTree == null && left != null));
TextSegment n = this;
int offset = n.nodeLength;
if (n.left != null)
offset += n.left.totalNodeLength;
while (n.parent != null) {
if (n == n.parent.right) {
if (n.parent.left != null)
offset += n.parent.left.totalNodeLength;
offset += n.parent.nodeLength;
}
n = n.parent;
}
return offset;
}
set {
if (value < 0)
throw new ArgumentOutOfRangeException("value", "Offset must not be negative");
if (this.StartOffset != value) {
// need a copy of the variable because ownerTree.Remove() sets this.ownerTree to null
ISegmentTree ownerTree = this.ownerTree;
if (ownerTree != null) {
ownerTree.Remove(this);
nodeLength = value;
ownerTree.Add(this);
} else {
nodeLength = value;
}
}
}
}
/// <summary>
/// Gets/Sets the end offset of the segment.
/// </summary>
/// <remarks>
/// Setting the end offset will change the length, the start offset will stay constant.
/// </remarks>
public int EndOffset {
get {
return StartOffset + Length;
}
set {
int newLength = value - StartOffset;
if (newLength < 0)
throw new ArgumentOutOfRangeException("value", "EndOffset must be greater or equal to StartOffset");
Length = newLength;
}
}
/// <summary>
/// Gets/Sets the length of the segment.
/// </summary>
/// <remarks>
/// Setting the length will change the end offset, the start offset will stay constant.
/// </remarks>
public int Length {
get {
return segmentLength;
}
set {
if (value < 0)
throw new ArgumentOutOfRangeException("value", "Length must not be negative");
segmentLength = value;
if (ownerTree != null)
ownerTree.UpdateAugmentedData(this);
}
}
internal TextSegment LeftMost {
get {
TextSegment node = this;
while (node.left != null)
node = node.left;
return node;
}
}
internal TextSegment RightMost {
get {
TextSegment node = this;
while (node.right != null)
node = node.right;
return node;
}
}
/// <summary>
/// Gets the inorder successor of the node.
/// </summary>
internal TextSegment Successor {
get {
if (right != null) {
return right.LeftMost;
} else {
TextSegment node = this;
TextSegment oldNode;
do {
oldNode = node;
node = node.parent;
// go up until we are coming out of a left subtree
} while (node != null && node.right == oldNode);
return node;
}
}
}
/// <summary>
/// Gets the inorder predecessor of the node.
/// </summary>
internal TextSegment Predecessor {
get {
if (left != null) {
return left.RightMost;
} else {
TextSegment node = this;
TextSegment oldNode;
do {
oldNode = node;
node = node.parent;
// go up until we are coming out of a right subtree
} while (node != null && node.left == oldNode);
return node;
}
}
}
#if DEBUG
internal string ToDebugString()
{
return "[nodeLength=" + nodeLength + " totalNodeLength=" + totalNodeLength
+ " distanceToMaxEnd=" + distanceToMaxEnd + " MaxEndOffset=" + (StartOffset + distanceToMaxEnd) + "]";
}
#endif
/// <inheritdoc/>
public override string ToString()
{
return "[" + GetType().Name + " Offset=" + StartOffset + " Length=" + Length + " EndOffset=" + EndOffset + "]";
}
}
}

951
ICSharpCode.AvalonEdit/Document/TextSegmentCollection.cs

@ -0,0 +1,951 @@ @@ -0,0 +1,951 @@
// 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 ICSharpCode.AvalonEdit.Utils;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Windows;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// Interface to allow TextSegments to access the TextSegmentCollection - we cannot use a direct reference
/// because TextSegmentCollection is generic.
/// </summary>
interface ISegmentTree
{
void Add(TextSegment s);
void Remove(TextSegment s);
void UpdateAugmentedData(TextSegment s);
}
/// <summary>
/// <para>
/// A collection of text segments that supports efficient lookup of segments
/// intersecting with another segment.
/// </para>
/// </summary>
/// <remarks><inheritdoc cref="TextSegment"/></remarks>
/// <see cref="TextSegment"/>
public sealed class TextSegmentCollection<T> : ICollection<T>, ISegmentTree, IWeakEventListener where T : TextSegment
{
// Implementation: this is basically a mixture of an augmented interval tree
// and the TextAnchorTree.
// WARNING: you need to understand interval trees (the version with the augmented 'high'/'max' field)
// and how the TextAnchorTree works before you have any chance of understanding this code.
// This means that every node holds two "segments":
// one like the segments in the text anchor tree to support efficient offset changes
// and another that is the interval as seen by the user
// So basically, the tree contains a list of contiguous node segments of the first kind,
// with interval segments starting at the end of every node segment.
// Performance:
// Add is O(lg n)
// Remove is O(lg n)
// DocumentChanged is O(m * lg n), with m the number of segments that intersect with the changed document section
// FindFirstSegmentWithStartAfter is O(m + lg n) with m being the number of segments at the same offset as the result segment
// FindIntersectingSegments is O(m + lg n) with m being the number of intersecting segments.
int count;
TextSegment root;
bool isConnectedToDocument;
#region Constructor
/// <summary>
/// Creates a new TextSegmentCollection that needs manual calls to <see cref="UpdateOffsets(DocumentChangeEventArgs)"/>.
/// </summary>
public TextSegmentCollection()
{
}
/// <summary>
/// Creates a new TextSegmentCollection that updates the offsets automatically.
/// </summary>
/// <param name="textDocument">The document to which the text segments
/// that will be added to the tree belong. When the document changes, the
/// position of the text segments will be updated accordingly.</param>
public TextSegmentCollection(TextDocument textDocument)
{
if (textDocument == null)
throw new ArgumentNullException("textDocument");
textDocument.VerifyAccess();
isConnectedToDocument = true;
TextDocumentWeakEventManager.Changed.AddListener(textDocument, this);
}
#endregion
#region OnDocumentChanged / UpdateOffsets
bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
if (managerType == typeof(TextDocumentWeakEventManager.Changed)) {
OnDocumentChanged((DocumentChangeEventArgs)e);
return true;
}
return false;
}
/// <summary>
/// Updates the start and end offsets of all segments stored in this collection.
/// </summary>
/// <param name="e">DocumentChangeEventArgs instance describing the change to the document.</param>
public void UpdateOffsets(DocumentChangeEventArgs e)
{
if (e == null)
throw new ArgumentNullException("e");
if (isConnectedToDocument)
throw new InvalidOperationException("This TextSegmentCollection will automatically update offsets; do not call UpdateOffsets manually!");
OnDocumentChanged(e);
CheckProperties();
}
void OnDocumentChanged(DocumentChangeEventArgs e)
{
OffsetChangeMap map = e.OffsetChangeMapOrNull;
if (map != null) {
foreach (OffsetChangeMapEntry entry in map) {
UpdateOffsetsInternal(entry);
}
} else {
UpdateOffsetsInternal(e.CreateSingleChangeMapEntry());
}
}
/// <summary>
/// Updates the start and end offsets of all segments stored in this collection.
/// </summary>
/// <param name="change">OffsetChangeMapEntry instance describing the change to the document.</param>
public void UpdateOffsets(OffsetChangeMapEntry change)
{
if (isConnectedToDocument)
throw new InvalidOperationException("This TextSegmentCollection will automatically update offsets; do not call UpdateOffsets manually!");
UpdateOffsetsInternal(change);
CheckProperties();
}
#endregion
#region UpdateOffsets (implementation)
void UpdateOffsetsInternal(OffsetChangeMapEntry change)
{
// Special case pure insertions, because they don't always cause a text segment to increase in size when the replaced region
// is inside a segment (when offset is at start or end of a text semgent).
if (change.RemovalLength == 0) {
InsertText(change.Offset, change.InsertionLength);
} else {
ReplaceText(change);
}
}
void InsertText(int offset, int length)
{
if (length == 0)
return;
// enlarge segments that contain offset (excluding those that have offset as endpoint)
foreach (TextSegment segment in FindSegmentsContaining(offset)) {
if (segment.StartOffset < offset && offset < segment.EndOffset) {
segment.Length += length;
}
}
// move start offsets of all segments >= offset
TextSegment node = FindFirstSegmentWithStartAfter(offset);
if (node != null) {
node.nodeLength += length;
UpdateAugmentedData(node);
}
}
void ReplaceText(OffsetChangeMapEntry change)
{
Debug.Assert(change.RemovalLength > 0);
int offset = change.Offset;
foreach (TextSegment segment in FindOverlappingSegments(offset, change.RemovalLength)) {
if (segment.StartOffset <= offset) {
if (segment.EndOffset >= offset + change.RemovalLength) {
// Replacement inside segment: adjust segment length
segment.Length += change.InsertionLength - change.RemovalLength;
} else {
// Replacement starting inside segment and ending after segment end: set segment end to removal position
//segment.EndOffset = offset;
segment.Length = offset - segment.StartOffset;
}
} else {
// Replacement starting in front of text segment and running into segment.
// Keep segment.EndOffset constant and move segment.StartOffset to the end of the replacement
int remainingLength = segment.EndOffset - (offset + change.RemovalLength);
RemoveSegment(segment);
segment.StartOffset = offset + change.RemovalLength;
segment.Length = Math.Max(0, remainingLength);
AddSegment(segment);
}
}
// move start offsets of all segments > offset
TextSegment node = FindFirstSegmentWithStartAfter(offset + 1);
if (node != null) {
Debug.Assert(node.nodeLength >= change.RemovalLength);
node.nodeLength += change.InsertionLength - change.RemovalLength;
UpdateAugmentedData(node);
}
}
#endregion
#region Add
/// <summary>
/// Adds the specified segment to the tree. This will cause the segment to update when the
/// document changes.
/// </summary>
public void Add(T item)
{
if (item == null)
throw new ArgumentNullException("item");
if (item.ownerTree != null)
throw new ArgumentException("The segment is already added to a SegmentCollection.");
AddSegment(item);
}
void ISegmentTree.Add(TextSegment s)
{
AddSegment(s);
}
void AddSegment(TextSegment node)
{
int insertionOffset = node.StartOffset;
node.distanceToMaxEnd = node.segmentLength;
if (root == null) {
root = node;
node.totalNodeLength = node.nodeLength;
} else if (insertionOffset >= root.totalNodeLength) {
// append segment at end of tree
node.nodeLength = node.totalNodeLength = insertionOffset - root.totalNodeLength;
InsertAsRight(root.RightMost, node);
} else {
// insert in middle of tree
TextSegment n = FindNode(ref insertionOffset);
Debug.Assert(insertionOffset < n.nodeLength);
// split node segment 'n' at offset
node.totalNodeLength = node.nodeLength = insertionOffset;
n.nodeLength -= insertionOffset;
InsertBefore(n, node);
}
node.ownerTree = this;
count++;
CheckProperties();
}
void InsertBefore(TextSegment node, TextSegment newNode)
{
if (node.left == null) {
InsertAsLeft(node, newNode);
} else {
InsertAsRight(node.left.RightMost, newNode);
}
}
#endregion
#region GetNextSegment / GetPreviousSegment
/// <summary>
/// Gets the next segment after the specified segment.
/// Segments are sorted by their start offset.
/// Returns null if segment is the last segment.
/// </summary>
public T GetNextSegment(T segment)
{
if (!Contains(segment))
throw new ArgumentException("segment is not inside the segment tree");
return (T)segment.Successor;
}
/// <summary>
/// Gets the previous segment before the specified segment.
/// Segments are sorted by their start offset.
/// Returns null if segment is the last segment.
/// </summary>
public T GetPreviousSegment(T segment)
{
if (!Contains(segment))
throw new ArgumentException("segment is not inside the segment tree");
return (T)segment.Predecessor;
}
#endregion
#region FindFirstSegmentWithStartAfter
/// <summary>
/// Gets the first segment with a start offset greater or equal to <paramref name="startOffset"/>.
/// Returns null if no such segment is found.
/// </summary>
public T FindFirstSegmentWithStartAfter(int startOffset)
{
if (root == null)
return null;
if (startOffset <= 0)
return (T)root.LeftMost;
TextSegment s = FindNode(ref startOffset);
// startOffset means that the previous segment is starting at the offset we were looking for
while (startOffset == 0) {
TextSegment p = (s == null) ? root.RightMost : s.Predecessor;
// There must always be a predecessor: if we were looking for the first node, we would have already
// returned it as root.LeftMost above.
Debug.Assert(p != null);
startOffset += p.nodeLength;
s = p;
}
return (T)s;
}
/// <summary>
/// Finds the node at the specified offset.
/// After the method has run, offset is relative to the beginning of the returned node.
/// </summary>
TextSegment FindNode(ref int offset)
{
TextSegment n = root;
while (true) {
if (n.left != null) {
if (offset < n.left.totalNodeLength) {
n = n.left; // descend into left subtree
continue;
} else {
offset -= n.left.totalNodeLength; // skip left subtree
}
}
if (offset < n.nodeLength) {
return n; // found correct node
} else {
offset -= n.nodeLength; // skip this node
}
if (n.right != null) {
n = n.right; // descend into right subtree
} else {
// didn't find any node containing the offset
return null;
}
}
}
#endregion
#region FindOverlappingSegments
/// <summary>
/// Finds all segments that contain the given offset.
/// (StartOffset &lt;= offset &lt;= EndOffset)
/// Segments are returned in the order given by GetNextSegment/GetPreviousSegment.
/// </summary>
/// <returns>Returns a new collection containing the results of the query.
/// This means it is safe to modify the TextSegmentCollection while iterating through the result collection.</returns>
public ReadOnlyCollection<T> FindSegmentsContaining(int offset)
{
return FindOverlappingSegments(offset, 0);
}
/// <summary>
/// Finds all segments that overlap with the given segment (including touching segments).
/// </summary>
/// <returns>Returns a new collection containing the results of the query.
/// This means it is safe to modify the TextSegmentCollection while iterating through the result collection.</returns>
public ReadOnlyCollection<T> FindOverlappingSegments(ISegment segment)
{
if (segment == null)
throw new ArgumentNullException("segment");
return FindOverlappingSegments(segment.Offset, segment.Length);
}
/// <summary>
/// Finds all segments that overlap with the given segment (including touching segments).
/// Segments are returned in the order given by GetNextSegment/GetPreviousSegment.
/// </summary>
/// <returns>Returns a new collection containing the results of the query.
/// This means it is safe to modify the TextSegmentCollection while iterating through the result collection.</returns>
public ReadOnlyCollection<T> FindOverlappingSegments(int offset, int length)
{
ThrowUtil.CheckNotNegative(length, "length");
List<T> results = new List<T>();
if (root != null) {
FindOverlappingSegments(results, root, offset, offset + length);
}
return results.AsReadOnly();
}
void FindOverlappingSegments(List<T> results, TextSegment node, int low, int high)
{
// low and high are relative to node.LeftMost startpos (not node.LeftMost.Offset)
if (high < 0) {
// node is irrelevant for search because all intervals in node are after high
return;
}
// find values relative to node.Offset
int nodeLow = low - node.nodeLength;
int nodeHigh = high - node.nodeLength;
if (node.left != null) {
nodeLow -= node.left.totalNodeLength;
nodeHigh -= node.left.totalNodeLength;
}
if (node.distanceToMaxEnd < nodeLow) {
// node is irrelevant for search because all intervals in node are before low
return;
}
if (node.left != null)
FindOverlappingSegments(results, node.left, low, high);
if (nodeHigh < 0) {
// node and everything in node.right is before low
return;
}
if (nodeLow <= node.segmentLength) {
results.Add((T)node);
}
if (node.right != null)
FindOverlappingSegments(results, node.right, nodeLow, nodeHigh);
}
#endregion
#region UpdateAugmentedData
void UpdateAugmentedData(TextSegment node)
{
int totalLength = node.nodeLength;
int distanceToMaxEnd = node.segmentLength;
if (node.left != null) {
totalLength += node.left.totalNodeLength;
int leftDTME = node.left.distanceToMaxEnd;
// dtme is relative, so convert it to the coordinates of node:
if (node.left.right != null)
leftDTME -= node.left.right.totalNodeLength;
leftDTME -= node.nodeLength;
if (leftDTME > distanceToMaxEnd)
distanceToMaxEnd = leftDTME;
}
if (node.right != null) {
totalLength += node.right.totalNodeLength;
int rightDTME = node.right.distanceToMaxEnd;
// dtme is relative, so convert it to the coordinates of node:
rightDTME += node.right.nodeLength;
if (node.right.left != null)
rightDTME += node.right.left.totalNodeLength;
if (rightDTME > distanceToMaxEnd)
distanceToMaxEnd = rightDTME;
}
if (node.totalNodeLength != totalLength
|| node.distanceToMaxEnd != distanceToMaxEnd)
{
node.totalNodeLength = totalLength;
node.distanceToMaxEnd = distanceToMaxEnd;
if (node.parent != null)
UpdateAugmentedData(node.parent);
}
}
void ISegmentTree.UpdateAugmentedData(TextSegment node)
{
UpdateAugmentedData(node);
}
#endregion
#region Remove
/// <summary>
/// Removes the specified segment from the tree. This will cause the segment to not update
/// anymore when the document changes.
/// </summary>
public bool Remove(T item)
{
if (!Contains(item))
return false;
RemoveSegment(item);
return true;
}
void ISegmentTree.Remove(TextSegment s)
{
RemoveSegment(s);
}
void RemoveSegment(TextSegment s)
{
int oldOffset = s.StartOffset;
TextSegment successor = s.Successor;
if (successor != null)
successor.nodeLength += s.nodeLength;
RemoveNode(s);
if (successor != null)
UpdateAugmentedData(successor);
Disconnect(s, oldOffset);
CheckProperties();
}
void Disconnect(TextSegment s, int offset)
{
s.left = s.right = s.parent = null;
s.ownerTree = null;
s.nodeLength = offset;
count--;
}
/// <summary>
/// Removes all segments from the tree.
/// </summary>
public void Clear()
{
T[] segments = this.ToArray();
root = null;
int offset = 0;
foreach (TextSegment s in segments) {
offset += s.nodeLength;
Disconnect(s, offset);
}
CheckProperties();
}
#endregion
#region CheckProperties
[Conditional("DATACONSISTENCYTEST")]
internal void CheckProperties()
{
#if DEBUG
if (root != null) {
CheckProperties(root);
// check red-black property:
int blackCount = -1;
CheckNodeProperties(root, null, RED, 0, ref blackCount);
}
int expectedCount = 0;
// we cannot trust LINQ not to call ICollection.Count, so we need this loop
// to count the elements in the tree
using (IEnumerator<T> en = GetEnumerator()) {
while (en.MoveNext()) expectedCount++;
}
Debug.Assert(count == expectedCount);
#endif
}
#if DEBUG
void CheckProperties(TextSegment node)
{
int totalLength = node.nodeLength;
int distanceToMaxEnd = node.segmentLength;
if (node.left != null) {
CheckProperties(node.left);
totalLength += node.left.totalNodeLength;
distanceToMaxEnd = Math.Max(distanceToMaxEnd,
node.left.distanceToMaxEnd + node.left.StartOffset - node.StartOffset);
}
if (node.right != null) {
CheckProperties(node.right);
totalLength += node.right.totalNodeLength;
distanceToMaxEnd = Math.Max(distanceToMaxEnd,
node.right.distanceToMaxEnd + node.right.StartOffset - node.StartOffset);
}
Debug.Assert(node.totalNodeLength == totalLength);
Debug.Assert(node.distanceToMaxEnd == distanceToMaxEnd);
}
/*
1. A node is either red or black.
2. The root is black.
3. All leaves are black. (The leaves are the NIL children.)
4. Both children of every red node are black. (So every red node must have a black parent.)
5. Every simple path from a node to a descendant leaf contains the same number of black nodes. (Not counting the leaf node.)
*/
void CheckNodeProperties(TextSegment node, TextSegment parentNode, bool parentColor, int blackCount, ref int expectedBlackCount)
{
if (node == null) return;
Debug.Assert(node.parent == parentNode);
if (parentColor == RED) {
Debug.Assert(node.color == BLACK);
}
if (node.color == BLACK) {
blackCount++;
}
if (node.left == null && node.right == null) {
// node is a leaf node:
if (expectedBlackCount == -1)
expectedBlackCount = blackCount;
else
Debug.Assert(expectedBlackCount == blackCount);
}
CheckNodeProperties(node.left, node, node.color, blackCount, ref expectedBlackCount);
CheckNodeProperties(node.right, node, node.color, blackCount, ref expectedBlackCount);
}
static void AppendTreeToString(TextSegment node, StringBuilder b, int indent)
{
if (node.color == RED)
b.Append("RED ");
else
b.Append("BLACK ");
b.AppendLine(node.ToString() + node.ToDebugString());
indent += 2;
if (node.left != null) {
b.Append(' ', indent);
b.Append("L: ");
AppendTreeToString(node.left, b, indent);
}
if (node.right != null) {
b.Append(' ', indent);
b.Append("R: ");
AppendTreeToString(node.right, b, indent);
}
}
#endif
internal string GetTreeAsString()
{
#if DEBUG
StringBuilder b = new StringBuilder();
if (root != null)
AppendTreeToString(root, b, 0);
return b.ToString();
#else
return "Not available in release build.";
#endif
}
#endregion
#region Red/Black Tree
internal const bool RED = true;
internal const bool BLACK = false;
void InsertAsLeft(TextSegment parentNode, TextSegment newNode)
{
Debug.Assert(parentNode.left == null);
parentNode.left = newNode;
newNode.parent = parentNode;
newNode.color = RED;
UpdateAugmentedData(parentNode);
FixTreeOnInsert(newNode);
}
void InsertAsRight(TextSegment parentNode, TextSegment newNode)
{
Debug.Assert(parentNode.right == null);
parentNode.right = newNode;
newNode.parent = parentNode;
newNode.color = RED;
UpdateAugmentedData(parentNode);
FixTreeOnInsert(newNode);
}
void FixTreeOnInsert(TextSegment node)
{
Debug.Assert(node != null);
Debug.Assert(node.color == RED);
Debug.Assert(node.left == null || node.left.color == BLACK);
Debug.Assert(node.right == null || node.right.color == BLACK);
TextSegment parentNode = node.parent;
if (parentNode == null) {
// we inserted in the root -> the node must be black
// since this is a root node, making the node black increments the number of black nodes
// on all paths by one, so it is still the same for all paths.
node.color = BLACK;
return;
}
if (parentNode.color == BLACK) {
// if the parent node where we inserted was black, our red node is placed correctly.
// since we inserted a red node, the number of black nodes on each path is unchanged
// -> the tree is still balanced
return;
}
// parentNode is red, so there is a conflict here!
// because the root is black, parentNode is not the root -> there is a grandparent node
TextSegment grandparentNode = parentNode.parent;
TextSegment uncleNode = Sibling(parentNode);
if (uncleNode != null && uncleNode.color == RED) {
parentNode.color = BLACK;
uncleNode.color = BLACK;
grandparentNode.color = RED;
FixTreeOnInsert(grandparentNode);
return;
}
// now we know: parent is red but uncle is black
// First rotation:
if (node == parentNode.right && parentNode == grandparentNode.left) {
RotateLeft(parentNode);
node = node.left;
} else if (node == parentNode.left && parentNode == grandparentNode.right) {
RotateRight(parentNode);
node = node.right;
}
// because node might have changed, reassign variables:
parentNode = node.parent;
grandparentNode = parentNode.parent;
// Now recolor a bit:
parentNode.color = BLACK;
grandparentNode.color = RED;
// Second rotation:
if (node == parentNode.left && parentNode == grandparentNode.left) {
RotateRight(grandparentNode);
} else {
// because of the first rotation, this is guaranteed:
Debug.Assert(node == parentNode.right && parentNode == grandparentNode.right);
RotateLeft(grandparentNode);
}
}
void RemoveNode(TextSegment removedNode)
{
if (removedNode.left != null && removedNode.right != null) {
// replace removedNode with it's in-order successor
TextSegment leftMost = removedNode.right.LeftMost;
RemoveNode(leftMost); // remove leftMost from its current location
// and overwrite the removedNode with it
ReplaceNode(removedNode, leftMost);
leftMost.left = removedNode.left;
if (leftMost.left != null) leftMost.left.parent = leftMost;
leftMost.right = removedNode.right;
if (leftMost.right != null) leftMost.right.parent = leftMost;
leftMost.color = removedNode.color;
UpdateAugmentedData(leftMost);
if (leftMost.parent != null) UpdateAugmentedData(leftMost.parent);
return;
}
// now either removedNode.left or removedNode.right is null
// get the remaining child
TextSegment parentNode = removedNode.parent;
TextSegment childNode = removedNode.left ?? removedNode.right;
ReplaceNode(removedNode, childNode);
if (parentNode != null) UpdateAugmentedData(parentNode);
if (removedNode.color == BLACK) {
if (childNode != null && childNode.color == RED) {
childNode.color = BLACK;
} else {
FixTreeOnDelete(childNode, parentNode);
}
}
}
void FixTreeOnDelete(TextSegment node, TextSegment parentNode)
{
Debug.Assert(node == null || node.parent == parentNode);
if (parentNode == null)
return;
// warning: node may be null
TextSegment sibling = Sibling(node, parentNode);
if (sibling.color == RED) {
parentNode.color = RED;
sibling.color = BLACK;
if (node == parentNode.left) {
RotateLeft(parentNode);
} else {
RotateRight(parentNode);
}
sibling = Sibling(node, parentNode); // update value of sibling after rotation
}
if (parentNode.color == BLACK
&& sibling.color == BLACK
&& GetColor(sibling.left) == BLACK
&& GetColor(sibling.right) == BLACK)
{
sibling.color = RED;
FixTreeOnDelete(parentNode, parentNode.parent);
return;
}
if (parentNode.color == RED
&& sibling.color == BLACK
&& GetColor(sibling.left) == BLACK
&& GetColor(sibling.right) == BLACK)
{
sibling.color = RED;
parentNode.color = BLACK;
return;
}
if (node == parentNode.left &&
sibling.color == BLACK &&
GetColor(sibling.left) == RED &&
GetColor(sibling.right) == BLACK)
{
sibling.color = RED;
sibling.left.color = BLACK;
RotateRight(sibling);
}
else if (node == parentNode.right &&
sibling.color == BLACK &&
GetColor(sibling.right) == RED &&
GetColor(sibling.left) == BLACK)
{
sibling.color = RED;
sibling.right.color = BLACK;
RotateLeft(sibling);
}
sibling = Sibling(node, parentNode); // update value of sibling after rotation
sibling.color = parentNode.color;
parentNode.color = BLACK;
if (node == parentNode.left) {
if (sibling.right != null) {
Debug.Assert(sibling.right.color == RED);
sibling.right.color = BLACK;
}
RotateLeft(parentNode);
} else {
if (sibling.left != null) {
Debug.Assert(sibling.left.color == RED);
sibling.left.color = BLACK;
}
RotateRight(parentNode);
}
}
void ReplaceNode(TextSegment replacedNode, TextSegment newNode)
{
if (replacedNode.parent == null) {
Debug.Assert(replacedNode == root);
root = newNode;
} else {
if (replacedNode.parent.left == replacedNode)
replacedNode.parent.left = newNode;
else
replacedNode.parent.right = newNode;
}
if (newNode != null) {
newNode.parent = replacedNode.parent;
}
replacedNode.parent = null;
}
void RotateLeft(TextSegment p)
{
// let q be p's right child
TextSegment q = p.right;
Debug.Assert(q != null);
Debug.Assert(q.parent == p);
// set q to be the new root
ReplaceNode(p, q);
// set p's right child to be q's left child
p.right = q.left;
if (p.right != null) p.right.parent = p;
// set q's left child to be p
q.left = p;
p.parent = q;
UpdateAugmentedData(p);
UpdateAugmentedData(q);
}
void RotateRight(TextSegment p)
{
// let q be p's left child
TextSegment q = p.left;
Debug.Assert(q != null);
Debug.Assert(q.parent == p);
// set q to be the new root
ReplaceNode(p, q);
// set p's left child to be q's right child
p.left = q.right;
if (p.left != null) p.left.parent = p;
// set q's right child to be p
q.right = p;
p.parent = q;
UpdateAugmentedData(p);
UpdateAugmentedData(q);
}
static TextSegment Sibling(TextSegment node)
{
if (node == node.parent.left)
return node.parent.right;
else
return node.parent.left;
}
static TextSegment Sibling(TextSegment node, TextSegment parentNode)
{
Debug.Assert(node == null || node.parent == parentNode);
if (node == parentNode.left)
return parentNode.right;
else
return parentNode.left;
}
static bool GetColor(TextSegment node)
{
return node != null ? node.color : BLACK;
}
#endregion
#region ICollection<T> implementation
/// <summary>
/// Gets the number of segments in the tree.
/// </summary>
public int Count {
get { return count; }
}
bool ICollection<T>.IsReadOnly {
get { return false; }
}
/// <summary>
/// Gets whether this tree contains the specified item.
/// </summary>
public bool Contains(T item)
{
return item != null && item.ownerTree == this;
}
/// <summary>
/// Copies all segments in this SegmentTree to the specified array.
/// </summary>
public void CopyTo(T[] array, int arrayIndex)
{
if (array == null)
throw new ArgumentNullException("array");
if (array.Length < this.Count)
throw new ArgumentException("The array is too small", "array");
if (arrayIndex < 0 || arrayIndex + count > array.Length)
throw new ArgumentOutOfRangeException("arrayIndex", arrayIndex, "Value must be between 0 and " + (array.Length - count));
foreach (T s in this) {
array[arrayIndex++] = s;
}
}
/// <summary>
/// Gets an enumerator to enumerate the segments.
/// </summary>
public IEnumerator<T> GetEnumerator()
{
if (root != null) {
TextSegment current = root.LeftMost;
while (current != null) {
yield return (T)current;
// TODO: check if collection was modified during enumeration
current = current.Successor;
}
}
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
#endregion
}
}

332
ICSharpCode.AvalonEdit/Document/TextUtilities.cs

@ -0,0 +1,332 @@ @@ -0,0 +1,332 @@
// 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.Globalization;
using System.Windows.Documents;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// Specifies the mode for getting the next caret position.
/// </summary>
public enum CaretPositioningMode
{
/// <summary>
/// Normal positioning (stop at every caret position)
/// </summary>
Normal,
/// <summary>
/// Stop only on word borders.
/// </summary>
WordBorder,
/// <summary>
/// Stop only at the beginning of words. This is used for Ctrl+Left/Ctrl+Right.
/// </summary>
WordStart,
/// <summary>
/// Stop only at the beginning of words, and anywhere in the middle of symbols.
/// </summary>
WordStartOrSymbol,
/// <summary>
/// Stop only on word borders, and anywhere in the middle of symbols.
/// </summary>
WordBorderOrSymbol
}
/// <summary>
/// Static helper methods for working with text.
/// </summary>
public static partial class TextUtilities
{
#region GetControlCharacterName
// the names of the first 32 ASCII characters = Unicode C0 block
static readonly string[] c0Table = {
"NUL", "SOH", "STX", "ETX", "EOT", "ENQ", "ACK", "BEL", "BS", "HT",
"LF", "VT", "FF", "CR", "SO", "SI", "DLE", "DC1", "DC2", "DC3",
"DC4", "NAK", "SYN", "ETB", "CAN", "EM", "SUB", "ESC", "FS", "GS",
"RS", "US"
};
// DEL (ASCII 127) and
// the names of the control characters in the C1 block (Unicode 128 to 159)
static readonly string[] delAndC1Table = {
"DEL",
"PAD", "HOP", "BPH", "NBH", "IND", "NEL", "SSA", "ESA", "HTS", "HTJ",
"VTS", "PLD", "PLU", "RI", "SS2", "SS3", "DCS", "PU1", "PU2", "STS",
"CCH", "MW", "SPA", "EPA", "SOS", "SGCI", "SCI", "CSI", "ST", "OSC",
"PM", "APC"
};
/// <summary>
/// Gets the name of the control character.
/// For unknown characters, the unicode codepoint is returned as 4-digit hexadecimal value.
/// </summary>
public static string GetControlCharacterName(char controlCharacter)
{
int num = (int)controlCharacter;
if (num < c0Table.Length)
return c0Table[num];
else if (num >= 127 && num <= 159)
return delAndC1Table[num - 127];
else
return num.ToString("x4", CultureInfo.InvariantCulture);
}
#endregion
#region GetWhitespace
/// <summary>
/// Gets all whitespace (' ' and '\t', but no newlines) after offset.
/// </summary>
/// <param name="textSource">The text source.</param>
/// <param name="offset">The offset where the whitespace starts.</param>
/// <returns>The segment containing the whitespace.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace",
Justification = "WPF uses 'Whitespace'")]
public static ISegment GetWhitespaceAfter(ITextSource textSource, int offset)
{
if (textSource == null)
throw new ArgumentNullException("textSource");
int pos;
for (pos = offset; pos < textSource.TextLength; pos++) {
char c = textSource.GetCharAt(pos);
if (c != ' ' && c != '\t')
break;
}
return new SimpleSegment(offset, pos - offset);
}
/// <summary>
/// Gets all whitespace (' ' and '\t', but no newlines) before offset.
/// </summary>
/// <param name="textSource">The text source.</param>
/// <param name="offset">The offset where the whitespace ends.</param>
/// <returns>The segment containing the whitespace.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace",
Justification = "WPF uses 'Whitespace'")]
public static ISegment GetWhitespaceBefore(ITextSource textSource, int offset)
{
if (textSource == null)
throw new ArgumentNullException("textSource");
int pos;
for (pos = offset - 1; pos >= 0; pos--) {
char c = textSource.GetCharAt(pos);
if (c != ' ' && c != '\t')
break;
}
pos++; // go back the one character that isn't whitespace
return new SimpleSegment(pos, offset - pos);
}
/// <summary>
/// Gets the leading whitespace segment on the document line.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace",
Justification = "WPF uses 'Whitespace'")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters",
Justification = "Parameter cannot be ITextSource because it must belong to the DocumentLine")]
public static ISegment GetLeadingWhitespace(TextDocument document, DocumentLine documentLine)
{
if (documentLine == null)
throw new ArgumentNullException("documentLine");
return GetWhitespaceAfter(document, documentLine.Offset);
}
/// <summary>
/// Gets the trailing whitespace segment on the document line.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace",
Justification = "WPF uses 'Whitespace'")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters",
Justification = "Parameter cannot be ITextSource because it must belong to the DocumentLine")]
public static ISegment GetTrailingWhitespace(TextDocument document, DocumentLine documentLine)
{
if (documentLine == null)
throw new ArgumentNullException("documentLine");
ISegment segment = GetWhitespaceBefore(document, documentLine.EndOffset);
// If the whole line consists of whitespace, we consider all of it as leading whitespace,
// so return an empty segment as trailing whitespace.
if (segment.Offset == documentLine.Offset)
return new SimpleSegment(documentLine.EndOffset, 0);
else
return segment;
}
#endregion
#region GetSingleIndentationSegment
/// <summary>
/// Gets a single indentation segment starting at <paramref name="offset"/> - at most one tab
/// or <paramref name="indentationSize"/> spaces.
/// </summary>
/// <param name="textSource">The text source.</param>
/// <param name="offset">The offset where the indentation segment starts.</param>
/// <param name="indentationSize">The size of an indentation unit. See <see cref="TextEditorOptions.IndentationSize"/>.</param>
/// <returns>The indentation segment.
/// If there is no indentation character at the specified <paramref name="offset"/>,
/// an empty segment is returned.</returns>
public static ISegment GetSingleIndentationSegment(ITextSource textSource, int offset, int indentationSize)
{
if (textSource == null)
throw new ArgumentNullException("textSource");
int pos = offset;
while (pos < textSource.TextLength) {
char c = textSource.GetCharAt(pos);
if (c == '\t') {
if (pos == offset)
return new SimpleSegment(offset, 1);
else
break;
} else if (c == ' ') {
if (pos - offset >= indentationSize)
break;
} else {
break;
}
// continue only if c==' ' and (pos-offset)<tabSize
pos++;
}
return new SimpleSegment(offset, pos - offset);
}
#endregion
#region GetCharacterClass
/// <summary>
/// Gets whether the character is whitespace, part of an identifier, or line terminator.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "c")]
public static CharacterClass GetCharacterClass(char c)
{
if (c == '\r' || c == '\n')
return CharacterClass.LineTerminator;
else if (char.IsWhiteSpace(c))
return CharacterClass.Whitespace;
else if (char.IsLetterOrDigit(c) || c == '_')
return CharacterClass.IdentifierPart;
else
return CharacterClass.Other;
}
#endregion
#region GetNextCaretPosition
/// <summary>
/// Gets the next caret position.
/// </summary>
/// <param name="textSource">The text source.</param>
/// <param name="offset">The start offset inside the text source.</param>
/// <param name="direction">The search direction (forwards or backwards).</param>
/// <param name="mode">The mode for caret positioning.</param>
/// <returns>The offset of the next caret position, or -1 if there is no further caret position
/// in the text source.</returns>
/// <remarks>
/// This method is NOT equivalent to the actual caret movement when using VisualLine.GetNextCaretPosition.
/// In real caret movement, there are additional caret stops at line starts and ends. This method
/// treats linefeeds as simple whitespace.
/// </remarks>
public static int GetNextCaretPosition(ITextSource textSource, int offset, LogicalDirection direction, CaretPositioningMode mode)
{
if (textSource == null)
throw new ArgumentNullException("textSource");
if (mode != CaretPositioningMode.Normal
&& mode != CaretPositioningMode.WordBorder
&& mode != CaretPositioningMode.WordStart
&& mode != CaretPositioningMode.WordBorderOrSymbol
&& mode != CaretPositioningMode.WordStartOrSymbol)
{
throw new ArgumentException("Unsupported CaretPositioningMode: " + mode, "mode");
}
if (direction != LogicalDirection.Backward
&& direction != LogicalDirection.Forward)
{
throw new ArgumentException("Invalid LogicalDirection: " + direction, "direction");
}
int textLength = textSource.TextLength;
if (textLength <= 0) {
// empty document? has a normal caret position at 0, though no word borders
if (mode == CaretPositioningMode.Normal) {
if (offset > 0 && direction == LogicalDirection.Backward) return 0;
if (offset < 0 && direction == LogicalDirection.Forward) return 0;
}
return -1;
}
while (true) {
int nextPos = (direction == LogicalDirection.Backward) ? offset - 1 : offset + 1;
// return -1 if there is no further caret position in the text source
// we also need this to handle offset values outside the valid range
if (nextPos < 0 || nextPos > textLength)
return -1;
// stop at every caret position? we can stop immediately.
if (mode == CaretPositioningMode.Normal)
return nextPos;
// not normal mode? we're looking for word borders...
// check if we've run against the textSource borders.
// a 'textSource' usually isn't the whole document, but a single VisualLineElement.
if (nextPos == 0) {
// at the document start, there's only a word border
// if the first character is not whitespace
if (!char.IsWhiteSpace(textSource.GetCharAt(0)))
return nextPos;
} else if (nextPos == textLength) {
// at the document end, there's never a word start
if (mode != CaretPositioningMode.WordStart && mode != CaretPositioningMode.WordStartOrSymbol) {
// at the document end, there's only a word border
// if the last character is not whitespace
if (!char.IsWhiteSpace(textSource.GetCharAt(textLength - 1)))
return nextPos;
}
} else {
CharacterClass charBefore = GetCharacterClass(textSource.GetCharAt(nextPos - 1));
CharacterClass charAfter = GetCharacterClass(textSource.GetCharAt(nextPos));
if (charBefore == charAfter) {
if (charBefore == CharacterClass.Other &&
(mode == CaretPositioningMode.WordBorderOrSymbol || mode == CaretPositioningMode.WordStartOrSymbol))
{
// With the "OrSymbol" modes, there's a word border and start between any two unknown characters
return nextPos;
}
} else {
// this looks like a possible border
// if we're looking for word starts, check that this is a word start (and not a word end)
// if we're just checking for word borders, accept unconditionally
if (!((mode == CaretPositioningMode.WordStart || mode == CaretPositioningMode.WordStartOrSymbol)
&& (charAfter == CharacterClass.Whitespace || charAfter == CharacterClass.LineTerminator)))
{
return nextPos;
}
}
}
// we'll have to continue searching...
offset = nextPos;
}
}
#endregion
}
/// <summary>
/// Classifies a character as whitespace, line terminator, part of an identifier, or other.
/// </summary>
public enum CharacterClass
{
/// <summary>
/// The character is not whitespace, line terminator or part of an identifier.
/// </summary>
Other,
/// <summary>
/// The character is whitespace (but not line terminator).
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace",
Justification = "WPF uses 'Whitespace'")]
Whitespace,
/// <summary>
/// The character can be part of an identifier (Letter, digit or underscore).
/// </summary>
IdentifierPart,
/// <summary>
/// The character is line terminator (\r or \n).
/// </summary>
LineTerminator
}
}

61
ICSharpCode.AvalonEdit/Document/UndoOperationGroup.cs

@ -0,0 +1,61 @@ @@ -0,0 +1,61 @@
// 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.Diagnostics;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// This class stacks the last x operations from the undostack and makes
/// one undo/redo operation from it.
/// </summary>
sealed class UndoOperationGroup : IUndoableOperationWithContext
{
IUndoableOperation[] undolist;
public UndoOperationGroup(Deque<IUndoableOperation> stack, int numops)
{
if (stack == null) {
throw new ArgumentNullException("stack");
}
Debug.Assert(numops > 0 , "UndoOperationGroup : numops should be > 0");
Debug.Assert(numops <= stack.Count);
undolist = new IUndoableOperation[numops];
for (int i = 0; i < numops; ++i) {
undolist[i] = stack.PopBack();
}
}
public void Undo()
{
for (int i = 0; i < undolist.Length; ++i) {
undolist[i].Undo();
}
}
public void Undo(UndoStack stack)
{
for (int i = 0; i < undolist.Length; ++i) {
stack.RunUndo(undolist[i]);
}
}
public void Redo()
{
for (int i = undolist.Length - 1; i >= 0; --i) {
undolist[i].Redo();
}
}
public void Redo(UndoStack stack)
{
for (int i = undolist.Length - 1; i >= 0; --i) {
stack.RunRedo(undolist[i]);
}
}
}
}

442
ICSharpCode.AvalonEdit/Document/UndoStack.cs

@ -0,0 +1,442 @@ @@ -0,0 +1,442 @@
// 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.ComponentModel;
using System.Diagnostics;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// Undo stack implementation.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")]
public sealed class UndoStack : INotifyPropertyChanged
{
/// undo stack is listening for changes
internal const int StateListen = 0;
/// undo stack is reverting/repeating a set of changes
internal const int StatePlayback = 1;
// undo stack is reverting/repeating a set of changes and modifies the document to do this
internal const int StatePlaybackModifyDocument = 2;
/// state is used for checking that noone but the UndoStack performs changes
/// during Undo events
internal int state = StateListen;
Deque<IUndoableOperation> undostack = new Deque<IUndoableOperation>();
Deque<IUndoableOperation> redostack = new Deque<IUndoableOperation>();
int sizeLimit = int.MaxValue;
int undoGroupDepth;
int actionCountInUndoGroup;
int optionalActionCount;
object lastGroupDescriptor;
bool allowContinue;
#region IsOriginalFile implementation
// implements feature request SD2-784 - File still considered dirty after undoing all changes
/// <summary>
/// Number of times undo must be executed until the original state is reached.
/// Negative: number of times redo must be executed until the original state is reached.
/// Special case: int.MinValue == original state is unreachable
/// </summary>
int elementsOnUndoUntilOriginalFile;
bool isOriginalFile = true;
/// <summary>
/// Gets whether the document is currently in its original state (no modifications).
/// </summary>
public bool IsOriginalFile {
get { return isOriginalFile; }
}
void RecalcIsOriginalFile()
{
bool newIsOriginalFile = (elementsOnUndoUntilOriginalFile == 0);
if (newIsOriginalFile != isOriginalFile) {
isOriginalFile = newIsOriginalFile;
NotifyPropertyChanged("IsOriginalFile");
}
}
/// <summary>
/// Marks the current state as original. Discards any previous "original" markers.
/// </summary>
public void MarkAsOriginalFile()
{
elementsOnUndoUntilOriginalFile = 0;
RecalcIsOriginalFile();
}
/// <summary>
/// Discards the current "original" marker.
/// </summary>
public void DiscardOriginalFileMarker()
{
elementsOnUndoUntilOriginalFile = int.MinValue;
RecalcIsOriginalFile();
}
void FileModified(int newElementsOnUndoStack)
{
if (elementsOnUndoUntilOriginalFile == int.MinValue)
return;
elementsOnUndoUntilOriginalFile += newElementsOnUndoStack;
if (elementsOnUndoUntilOriginalFile > undostack.Count)
elementsOnUndoUntilOriginalFile = int.MinValue;
// don't call RecalcIsOriginalFile(): wait until end of undo group
}
#endregion
/// <summary>
/// Gets if the undo stack currently accepts changes.
/// Is false while an undo action is running.
/// </summary>
public bool AcceptChanges {
get { return state == StateListen; }
}
/// <summary>
/// Gets if there are actions on the undo stack.
/// Use the PropertyChanged event to listen to changes of this property.
/// </summary>
public bool CanUndo {
get { return undostack.Count > 0; }
}
/// <summary>
/// Gets if there are actions on the redo stack.
/// Use the PropertyChanged event to listen to changes of this property.
/// </summary>
public bool CanRedo {
get { return redostack.Count > 0; }
}
/// <summary>
/// Gets/Sets the limit on the number of items on the undo stack.
/// </summary>
/// <remarks>The size limit is enforced only on the number of stored top-level undo groups.
/// Elements within undo groups do not count towards the size limit.</remarks>
public int SizeLimit {
get { return sizeLimit; }
set {
if (value < 0)
ThrowUtil.CheckNotNegative(value, "value");
if (sizeLimit != value) {
sizeLimit = value;
NotifyPropertyChanged("SizeLimit");
if (undoGroupDepth == 0)
EnforceSizeLimit();
}
}
}
void EnforceSizeLimit()
{
Debug.Assert(undoGroupDepth == 0);
while (undostack.Count > sizeLimit)
undostack.PopFront();
while (redostack.Count > sizeLimit)
redostack.PopFront();
}
/// <summary>
/// If an undo group is open, gets the group descriptor of the current top-level
/// undo group.
/// If no undo group is open, gets the group descriptor from the previous undo group.
/// </summary>
/// <remarks>The group descriptor can be used to join adjacent undo groups:
/// use a group descriptor to mark your changes, and on the second action,
/// compare LastGroupDescriptor and use <see cref="StartContinuedUndoGroup"/> if you
/// want to join the undo groups.</remarks>
public object LastGroupDescriptor {
get { return lastGroupDescriptor; }
}
/// <summary>
/// Starts grouping changes.
/// Maintains a counter so that nested calls are possible.
/// </summary>
public void StartUndoGroup()
{
StartUndoGroup(null);
}
/// <summary>
/// Starts grouping changes.
/// Maintains a counter so that nested calls are possible.
/// </summary>
/// <param name="groupDescriptor">An object that is stored with the undo group.
/// If this is not a top-level undo group, the parameter is ignored.</param>
public void StartUndoGroup(object groupDescriptor)
{
if (undoGroupDepth == 0) {
actionCountInUndoGroup = 0;
optionalActionCount = 0;
lastGroupDescriptor = groupDescriptor;
}
undoGroupDepth++;
//Util.LoggingService.Debug("Open undo group (new depth=" + undoGroupDepth + ")");
}
/// <summary>
/// Starts grouping changes, continuing with the previously closed undo group if possible.
/// Maintains a counter so that nested calls are possible.
/// If the call to StartContinuedUndoGroup is a nested call, it behaves exactly
/// as <see cref="StartUndoGroup()"/>, only top-level calls can continue existing undo groups.
/// </summary>
/// <param name="groupDescriptor">An object that is stored with the undo group.
/// If this is not a top-level undo group, the parameter is ignored.</param>
public void StartContinuedUndoGroup(object groupDescriptor)
{
if (undoGroupDepth == 0) {
actionCountInUndoGroup = (allowContinue && undostack.Count > 0) ? 1 : 0;
optionalActionCount = 0;
lastGroupDescriptor = groupDescriptor;
}
undoGroupDepth++;
//Util.LoggingService.Debug("Continue undo group (new depth=" + undoGroupDepth + ")");
}
/// <summary>
/// Stops grouping changes.
/// </summary>
public void EndUndoGroup()
{
if (undoGroupDepth == 0) throw new InvalidOperationException("There are no open undo groups");
undoGroupDepth--;
//Util.LoggingService.Debug("Close undo group (new depth=" + undoGroupDepth + ")");
if (undoGroupDepth == 0) {
Debug.Assert(state == StateListen || actionCountInUndoGroup == 0);
if (actionCountInUndoGroup == optionalActionCount) {
// only optional actions: don't store them
for (int i = 0; i < optionalActionCount; i++) {
undostack.PopBack();
}
} else if (actionCountInUndoGroup > 1) {
// combine all actions within the group into a single grouped action
undostack.PushBack(new UndoOperationGroup(undostack, actionCountInUndoGroup));
FileModified(-actionCountInUndoGroup + 1 + optionalActionCount);
}
//if (state == StateListen) {
EnforceSizeLimit();
allowContinue = true;
RecalcIsOriginalFile(); // can raise event
//}
}
}
/// <summary>
/// Throws an InvalidOperationException if an undo group is current open.
/// </summary>
void ThrowIfUndoGroupOpen()
{
if (undoGroupDepth != 0) {
undoGroupDepth = 0;
throw new InvalidOperationException("No undo group should be open at this point");
}
if (state != StateListen) {
throw new InvalidOperationException("This method cannot be called while an undo operation is being performed");
}
}
List<TextDocument> affectedDocuments;
internal void RegisterAffectedDocument(TextDocument document)
{
if (affectedDocuments == null)
affectedDocuments = new List<TextDocument>();
if (!affectedDocuments.Contains(document)) {
affectedDocuments.Add(document);
document.BeginUpdate();
}
}
void CallEndUpdateOnAffectedDocuments()
{
if (affectedDocuments != null) {
foreach (TextDocument doc in affectedDocuments) {
doc.EndUpdate();
}
affectedDocuments = null;
}
}
/// <summary>
/// Call this method to undo the last operation on the stack
/// </summary>
public void Undo()
{
ThrowIfUndoGroupOpen();
if (undostack.Count > 0) {
// disallow continuing undo groups after undo operation
lastGroupDescriptor = null; allowContinue = false;
// fetch operation to undo and move it to redo stack
IUndoableOperation uedit = undostack.PopBack();
redostack.PushBack(uedit);
state = StatePlayback;
try {
RunUndo(uedit);
} finally {
state = StateListen;
FileModified(-1);
CallEndUpdateOnAffectedDocuments();
}
RecalcIsOriginalFile();
if (undostack.Count == 0)
NotifyPropertyChanged("CanUndo");
if (redostack.Count == 1)
NotifyPropertyChanged("CanRedo");
}
}
internal void RunUndo(IUndoableOperation op)
{
IUndoableOperationWithContext opWithCtx = op as IUndoableOperationWithContext;
if (opWithCtx != null)
opWithCtx.Undo(this);
else
op.Undo();
}
/// <summary>
/// Call this method to redo the last undone operation
/// </summary>
public void Redo()
{
ThrowIfUndoGroupOpen();
if (redostack.Count > 0) {
lastGroupDescriptor = null; allowContinue = false;
IUndoableOperation uedit = redostack.PopBack();
undostack.PushBack(uedit);
state = StatePlayback;
try {
RunRedo(uedit);
} finally {
state = StateListen;
FileModified(1);
CallEndUpdateOnAffectedDocuments();
}
RecalcIsOriginalFile();
if (redostack.Count == 0)
NotifyPropertyChanged("CanRedo");
if (undostack.Count == 1)
NotifyPropertyChanged("CanUndo");
}
}
internal void RunRedo(IUndoableOperation op)
{
IUndoableOperationWithContext opWithCtx = op as IUndoableOperationWithContext;
if (opWithCtx != null)
opWithCtx.Redo(this);
else
op.Redo();
}
/// <summary>
/// Call this method to push an UndoableOperation on the undostack.
/// The redostack will be cleared if you use this method.
/// </summary>
public void Push(IUndoableOperation operation)
{
Push(operation, false);
}
/// <summary>
/// Call this method to push an UndoableOperation on the undostack.
/// However, the operation will be only stored if the undo group contains a
/// non-optional operation.
/// Use this method to store the caret position/selection on the undo stack to
/// prevent having only actions that affect only the caret and not the document.
/// </summary>
public void PushOptional(IUndoableOperation operation)
{
if (undoGroupDepth == 0)
throw new InvalidOperationException("Cannot use PushOptional outside of undo group");
Push(operation, true);
}
void Push(IUndoableOperation operation, bool isOptional)
{
if (operation == null) {
throw new ArgumentNullException("operation");
}
if (state == StateListen && sizeLimit > 0) {
bool wasEmpty = undostack.Count == 0;
bool needsUndoGroup = undoGroupDepth == 0;
if (needsUndoGroup) StartUndoGroup();
undostack.PushBack(operation);
actionCountInUndoGroup++;
if (isOptional)
optionalActionCount++;
else
FileModified(1);
if (needsUndoGroup) EndUndoGroup();
if (wasEmpty)
NotifyPropertyChanged("CanUndo");
ClearRedoStack();
}
}
/// <summary>
/// Call this method, if you want to clear the redo stack
/// </summary>
public void ClearRedoStack()
{
if (redostack.Count != 0) {
redostack.Clear();
NotifyPropertyChanged("CanRedo");
// if the "original file" marker is on the redo stack: remove it
if (elementsOnUndoUntilOriginalFile < 0)
elementsOnUndoUntilOriginalFile = int.MinValue;
}
}
/// <summary>
/// Clears both the undo and redo stack.
/// </summary>
public void ClearAll()
{
ThrowIfUndoGroupOpen();
actionCountInUndoGroup = 0;
optionalActionCount = 0;
if (undostack.Count != 0) {
lastGroupDescriptor = null;
allowContinue = false;
undostack.Clear();
NotifyPropertyChanged("CanUndo");
}
ClearRedoStack();
}
internal void Push(TextDocument document, DocumentChangeEventArgs e)
{
if (state == StatePlayback)
throw new InvalidOperationException("Document changes during undo/redo operations are not allowed.");
if (state == StatePlaybackModifyDocument)
state = StatePlayback; // allow only 1 change per expected modification
else
Push(new DocumentChangeOperation(document, e));
}
/// <summary>
/// Is raised when a property (CanUndo, CanRedo) changed.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}

85
ICSharpCode.AvalonEdit/Document/WeakLineTracker.cs

@ -0,0 +1,85 @@ @@ -0,0 +1,85 @@
// 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;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// Allows registering a line tracker on a TextDocument using a weak reference from the document to the line tracker.
/// </summary>
public sealed class WeakLineTracker : ILineTracker
{
TextDocument textDocument;
WeakReference targetObject;
private WeakLineTracker(TextDocument textDocument, ILineTracker targetTracker)
{
this.textDocument = textDocument;
this.targetObject = new WeakReference(targetTracker);
}
/// <summary>
/// Registers the <paramref name="targetTracker"/> as line tracker for the <paramref name="textDocument"/>.
/// A weak reference to the target tracker will be used, and the WeakLineTracker will deregister itself
/// when the target tracker is garbage collected.
/// </summary>
public static WeakLineTracker Register(TextDocument textDocument, ILineTracker targetTracker)
{
if (textDocument == null)
throw new ArgumentNullException("textDocument");
if (targetTracker == null)
throw new ArgumentNullException("targetTracker");
WeakLineTracker wlt = new WeakLineTracker(textDocument, targetTracker);
textDocument.LineTrackers.Add(wlt);
return wlt;
}
/// <summary>
/// Deregisters the weak line tracker.
/// </summary>
public void Deregister()
{
if (textDocument != null) {
textDocument.LineTrackers.Remove(this);
textDocument = null;
}
}
void ILineTracker.BeforeRemoveLine(DocumentLine line)
{
ILineTracker targetTracker = targetObject.Target as ILineTracker;
if (targetTracker != null)
targetTracker.BeforeRemoveLine(line);
else
Deregister();
}
void ILineTracker.SetLineLength(DocumentLine line, int newTotalLength)
{
ILineTracker targetTracker = targetObject.Target as ILineTracker;
if (targetTracker != null)
targetTracker.SetLineLength(line, newTotalLength);
else
Deregister();
}
void ILineTracker.LineInserted(DocumentLine insertionPos, DocumentLine newLine)
{
ILineTracker targetTracker = targetObject.Target as ILineTracker;
if (targetTracker != null)
targetTracker.LineInserted(insertionPos, newLine);
else
Deregister();
}
void ILineTracker.RebuildDocument()
{
ILineTracker targetTracker = targetObject.Target as ILineTracker;
if (targetTracker != null)
targetTracker.RebuildDocument();
else
Deregister();
}
}
}

102
ICSharpCode.AvalonEdit/Editing/AbstractMargin.cs

@ -0,0 +1,102 @@ @@ -0,0 +1,102 @@
// 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.Diagnostics;
using System.Windows;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Rendering;
namespace ICSharpCode.AvalonEdit.Editing
{
/// <summary>
/// Base class for margins.
/// Margins don't have to derive from this class, it just helps maintaining a reference to the TextView
/// and the TextDocument.
/// AbstractMargin derives from FrameworkElement, so if you don't want to handle visual children and rendering
/// on your own, choose another base class for your margin!
/// </summary>
public abstract class AbstractMargin : FrameworkElement, ITextViewConnect
{
/// <summary>
/// TextView property.
/// </summary>
public static readonly DependencyProperty TextViewProperty =
DependencyProperty.Register("TextView", typeof(TextView), typeof(AbstractMargin),
new FrameworkPropertyMetadata(OnTextViewChanged));
/// <summary>
/// Gets/sets the text view for which line numbers are displayed.
/// </summary>
/// <remarks>Adding a margin to <see cref="TextArea.LeftMargins"/> will automatically set this property to the text area's TextView.</remarks>
public TextView TextView {
get { return (TextView)GetValue(TextViewProperty); }
set { SetValue(TextViewProperty, value); }
}
static void OnTextViewChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
{
AbstractMargin margin = (AbstractMargin)dp;
margin.wasAutoAddedToTextView = false;
margin.OnTextViewChanged((TextView)e.OldValue, (TextView)e.NewValue);
}
// automatically set/unset TextView property using ITextViewConnect
bool wasAutoAddedToTextView;
void ITextViewConnect.AddToTextView(TextView textView)
{
if (this.TextView == null) {
this.TextView = textView;
wasAutoAddedToTextView = true;
} else if (this.TextView != textView) {
throw new InvalidOperationException("This margin belongs to a different TextView.");
}
}
void ITextViewConnect.RemoveFromTextView(TextView textView)
{
if (wasAutoAddedToTextView && this.TextView == textView) {
this.TextView = null;
Debug.Assert(!wasAutoAddedToTextView); // setting this.TextView should have unset this flag
}
}
TextDocument document;
/// <summary>
/// Gets the document associated with the margin.
/// </summary>
public TextDocument Document {
get { return document; }
}
/// <summary>
/// Called when the <see cref="TextView"/> is changing.
/// </summary>
protected virtual void OnTextViewChanged(TextView oldTextView, TextView newTextView)
{
if (oldTextView != null) {
oldTextView.DocumentChanged -= TextViewDocumentChanged;
}
if (newTextView != null) {
newTextView.DocumentChanged += TextViewDocumentChanged;
}
TextViewDocumentChanged(null, null);
}
void TextViewDocumentChanged(object sender, EventArgs e)
{
OnDocumentChanged(document, TextView != null ? TextView.Document : null);
}
/// <summary>
/// Called when the <see cref="Document"/> is changing.
/// </summary>
protected virtual void OnDocumentChanged(TextDocument oldDocument, TextDocument newDocument)
{
document = newDocument;
}
}
}

469
ICSharpCode.AvalonEdit/Editing/Caret.cs

@ -0,0 +1,469 @@ @@ -0,0 +1,469 @@
// 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.Diagnostics;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
using System.Windows.Threading;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Rendering;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Editing
{
/// <summary>
/// Helper class with caret-related methods.
/// </summary>
public sealed class Caret
{
readonly TextArea textArea;
readonly TextView textView;
readonly CaretLayer caretAdorner;
bool visible;
internal Caret(TextArea textArea)
{
this.textArea = textArea;
this.textView = textArea.TextView;
position = new TextViewPosition(1, 1, 0);
caretAdorner = new CaretLayer(textView);
textView.InsertLayer(caretAdorner, KnownLayer.Caret, LayerInsertionPosition.Replace);
textView.VisualLinesChanged += TextView_VisualLinesChanged;
textView.ScrollOffsetChanged += TextView_ScrollOffsetChanged;
}
void TextView_VisualLinesChanged(object sender, EventArgs e)
{
if (visible) {
Show();
}
// required because the visual columns might have changed if the
// element generators did something differently than on the last run
// (e.g. a FoldingSection was collapsed)
InvalidateVisualColumn();
}
void TextView_ScrollOffsetChanged(object sender, EventArgs e)
{
if (caretAdorner != null) {
caretAdorner.InvalidateVisual();
}
}
double desiredXPos = double.NaN;
TextViewPosition position;
/// <summary>
/// Gets/Sets the position of the caret.
/// Retrieving this property will validate the visual column (which can be expensive).
/// Use the <see cref="Location"/> property instead if you don't need the visual column.
/// </summary>
public TextViewPosition Position {
get {
ValidateVisualColumn();
return position;
}
set {
if (position != value) {
position = value;
storedCaretOffset = -1;
//Debug.WriteLine("Caret position changing to " + value);
ValidatePosition();
InvalidateVisualColumn();
RaisePositionChanged();
Log("Caret position changed to " + value);
if (visible)
Show();
}
}
}
/// <summary>
/// Gets the caret position without validating it.
/// </summary>
internal TextViewPosition NonValidatedPosition {
get {
return position;
}
}
/// <summary>
/// Gets/Sets the location of the caret.
/// The getter of this property is faster than <see cref="Position"/> because it doesn't have
/// to validate the visual column.
/// </summary>
public TextLocation Location {
get {
return position;
}
set {
this.Position = new TextViewPosition(value);
}
}
/// <summary>
/// Gets/Sets the caret line.
/// </summary>
public int Line {
get { return position.Line; }
set {
this.Position = new TextViewPosition(value, position.Column);
}
}
/// <summary>
/// Gets/Sets the caret column.
/// </summary>
public int Column {
get { return position.Column; }
set {
this.Position = new TextViewPosition(position.Line, value);
}
}
/// <summary>
/// Gets/Sets the caret visual column.
/// </summary>
public int VisualColumn {
get {
ValidateVisualColumn();
return position.VisualColumn;
}
set {
this.Position = new TextViewPosition(position.Line, position.Column, value);
}
}
int storedCaretOffset;
internal void OnDocumentChanging()
{
storedCaretOffset = this.Offset;
InvalidateVisualColumn();
}
internal void OnDocumentChanged(DocumentChangeEventArgs e)
{
InvalidateVisualColumn();
if (storedCaretOffset >= 0) {
int newCaretOffset = e.GetNewOffset(storedCaretOffset, AnchorMovementType.Default);
TextDocument document = textArea.Document;
if (document != null) {
// keep visual column
this.Position = new TextViewPosition(document.GetLocation(newCaretOffset), position.VisualColumn);
}
}
storedCaretOffset = -1;
}
/// <summary>
/// Gets/Sets the caret offset.
/// Setting the caret offset has the side effect of setting the <see cref="DesiredXPos"/> to NaN.
/// </summary>
public int Offset {
get {
TextDocument document = textArea.Document;
if (document == null) {
return 0;
} else {
return document.GetOffset(position);
}
}
set {
TextDocument document = textArea.Document;
if (document != null) {
this.Position = new TextViewPosition(document.GetLocation(value));
this.DesiredXPos = double.NaN;
}
}
}
/// <summary>
/// Gets/Sets the desired x-position of the caret, in device-independent pixels.
/// This property is NaN if the caret has no desired position.
/// </summary>
public double DesiredXPos {
get { return desiredXPos; }
set { desiredXPos = value; }
}
void ValidatePosition()
{
if (position.Line < 1)
position.Line = 1;
if (position.Column < 1)
position.Column = 1;
if (position.VisualColumn < -1)
position.VisualColumn = -1;
TextDocument document = textArea.Document;
if (document != null) {
if (position.Line > document.LineCount) {
position.Line = document.LineCount;
position.Column = document.GetLineByNumber(position.Line).Length + 1;
position.VisualColumn = -1;
} else {
DocumentLine line = document.GetLineByNumber(position.Line);
if (position.Column > line.Length + 1) {
position.Column = line.Length + 1;
position.VisualColumn = -1;
}
}
}
}
/// <summary>
/// Event raised when the caret position has changed.
/// If the caret position is changed inside a document update (between BeginUpdate/EndUpdate calls),
/// the PositionChanged event is raised only once at the end of the document update.
/// </summary>
public event EventHandler PositionChanged;
bool raisePositionChangedOnUpdateFinished;
void RaisePositionChanged()
{
if (textArea.Document != null && textArea.Document.IsInUpdate) {
raisePositionChangedOnUpdateFinished = true;
} else {
if (PositionChanged != null) {
PositionChanged(this, EventArgs.Empty);
}
}
}
internal void OnDocumentUpdateFinished()
{
if (raisePositionChangedOnUpdateFinished) {
if (PositionChanged != null) {
PositionChanged(this, EventArgs.Empty);
}
}
}
bool visualColumnValid;
void ValidateVisualColumn()
{
if (!visualColumnValid) {
TextDocument document = textArea.Document;
if (document != null) {
Debug.WriteLine("Explicit validation of caret column");
var documentLine = document.GetLineByNumber(position.Line);
RevalidateVisualColumn(textView.GetOrConstructVisualLine(documentLine));
}
}
}
void InvalidateVisualColumn()
{
visualColumnValid = false;
}
/// <summary>
/// Validates the visual column of the caret using the specified visual line.
/// The visual line must contain the caret offset.
/// </summary>
void RevalidateVisualColumn(VisualLine visualLine)
{
if (visualLine == null)
throw new ArgumentNullException("visualLine");
// mark column as validated
visualColumnValid = true;
int caretOffset = textView.Document.GetOffset(position);
int firstDocumentLineOffset = visualLine.FirstDocumentLine.Offset;
if (position.VisualColumn < 0) {
position.VisualColumn = visualLine.GetVisualColumn(caretOffset - firstDocumentLineOffset);
} else {
int offsetFromVisualColumn = visualLine.GetRelativeOffset(position.VisualColumn);
offsetFromVisualColumn += firstDocumentLineOffset;
if (offsetFromVisualColumn != caretOffset) {
position.VisualColumn = visualLine.GetVisualColumn(caretOffset - firstDocumentLineOffset);
} else {
if (position.VisualColumn > visualLine.VisualLength) {
position.VisualColumn = visualLine.VisualLength;
}
}
}
// search possible caret positions
int newVisualColumnForwards = visualLine.GetNextCaretPosition(position.VisualColumn - 1, LogicalDirection.Forward, CaretPositioningMode.Normal);
// If position.VisualColumn was valid, we're done with validation.
if (newVisualColumnForwards != position.VisualColumn) {
// also search backwards so that we can pick the better match
int newVisualColumnBackwards = visualLine.GetNextCaretPosition(position.VisualColumn + 1, LogicalDirection.Backward, CaretPositioningMode.Normal);
if (newVisualColumnForwards < 0 && newVisualColumnBackwards < 0)
throw ThrowUtil.NoValidCaretPosition();
// determine offsets for new visual column positions
int newOffsetForwards;
if (newVisualColumnForwards >= 0)
newOffsetForwards = visualLine.GetRelativeOffset(newVisualColumnForwards) + firstDocumentLineOffset;
else
newOffsetForwards = -1;
int newOffsetBackwards;
if (newVisualColumnBackwards >= 0)
newOffsetBackwards = visualLine.GetRelativeOffset(newVisualColumnBackwards) + firstDocumentLineOffset;
else
newOffsetBackwards = -1;
int newVisualColumn, newOffset;
// if there's only one valid position, use it
if (newVisualColumnForwards < 0) {
newVisualColumn = newVisualColumnBackwards;
newOffset = newOffsetBackwards;
} else if (newVisualColumnBackwards < 0) {
newVisualColumn = newVisualColumnForwards;
newOffset = newOffsetForwards;
} else {
// two valid positions: find the better match
if (Math.Abs(newOffsetBackwards - caretOffset) < Math.Abs(newOffsetForwards - caretOffset)) {
// backwards is better
newVisualColumn = newVisualColumnBackwards;
newOffset = newOffsetBackwards;
} else {
// forwards is better
newVisualColumn = newVisualColumnForwards;
newOffset = newOffsetForwards;
}
}
this.Position = new TextViewPosition(textView.Document.GetLocation(newOffset), newVisualColumn);
}
}
Rect CalcCaretRectangle(VisualLine visualLine)
{
if (!visualColumnValid) {
RevalidateVisualColumn(visualLine);
}
TextLine textLine = visualLine.GetTextLine(position.VisualColumn);
double xPos = textLine.GetDistanceFromCharacterHit(new CharacterHit(position.VisualColumn, 0));
double lineTop = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextTop);
double lineBottom = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.LineBottom);
return new Rect(xPos,
lineTop,
SystemParameters.CaretWidth,
lineBottom - lineTop);
}
/// <summary>
/// Returns the caret rectangle. The coordinate system is in device-independent pixels from the top of the document.
/// </summary>
public Rect CalculateCaretRectangle()
{
if (textView != null && textView.Document != null) {
VisualLine visualLine = textView.GetOrConstructVisualLine(textView.Document.GetLineByNumber(position.Line));
return CalcCaretRectangle(visualLine);
} else {
return Rect.Empty;
}
}
/// <summary>
/// Minimum distance of the caret to the view border.
/// </summary>
internal const double MinimumDistanceToViewBorder = 30;
/// <summary>
/// Scrolls the text view so that the caret is visible.
/// </summary>
public void BringCaretToView()
{
BringCaretToView(MinimumDistanceToViewBorder);
}
internal void BringCaretToView(double border)
{
Rect caretRectangle = CalculateCaretRectangle();
if (!caretRectangle.IsEmpty) {
caretRectangle.Inflate(border, border);
textView.MakeVisible(caretRectangle);
}
}
/// <summary>
/// Makes the caret visible and updates its on-screen position.
/// </summary>
public void Show()
{
Log("Caret.Show()");
visible = true;
if (!showScheduled) {
showScheduled = true;
textArea.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(ShowInternal));
}
}
bool showScheduled;
bool hasWin32Caret;
void ShowInternal()
{
showScheduled = false;
// if show was scheduled but caret hidden in the meantime
if (!visible)
return;
if (caretAdorner != null && textView != null) {
VisualLine visualLine = textView.GetVisualLine(position.Line);
if (visualLine != null) {
Rect caretRect = CalcCaretRectangle(visualLine);
// Create Win32 caret so that Windows knows where our managed caret is. This is necessary for
// features like 'Follow text editing' in the Windows Magnifier.
if (!hasWin32Caret) {
hasWin32Caret = Win32.CreateCaret(textView, caretRect.Size);
}
if (hasWin32Caret) {
Win32.SetCaretPosition(textView, caretRect.Location - textView.ScrollOffset);
}
caretAdorner.Show(caretRect);
} else {
caretAdorner.Hide();
}
}
}
/// <summary>
/// Makes the caret invisible.
/// </summary>
public void Hide()
{
Log("Caret.Hide()");
visible = false;
if (hasWin32Caret) {
Win32.DestroyCaret();
hasWin32Caret = false;
}
if (caretAdorner != null) {
caretAdorner.Hide();
}
}
[Conditional("DEBUG")]
static void Log(string text)
{
// commented out to make debug output less noisy - add back if there are any problems with the caret
//Debug.WriteLine(text);
}
/// <summary>
/// Gets/Sets the color of the caret.
/// </summary>
public Brush CaretBrush {
get { return caretAdorner.CaretBrush; }
set { caretAdorner.CaretBrush = value; }
}
}
}

86
ICSharpCode.AvalonEdit/Editing/CaretLayer.cs

@ -0,0 +1,86 @@ @@ -0,0 +1,86 @@
// 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.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Threading;
using ICSharpCode.AvalonEdit.Rendering;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Editing
{
sealed class CaretLayer : Layer
{
bool isVisible;
Rect caretRectangle;
DispatcherTimer caretBlinkTimer = new DispatcherTimer();
bool blink;
public CaretLayer(TextView textView) : base(textView, KnownLayer.Caret)
{
this.IsHitTestVisible = false;
caretBlinkTimer.Tick += new EventHandler(caretBlinkTimer_Tick);
}
void caretBlinkTimer_Tick(object sender, EventArgs e)
{
blink = !blink;
InvalidateVisual();
}
public void Show(Rect caretRectangle)
{
this.caretRectangle = caretRectangle;
this.isVisible = true;
InvalidateVisual();
StartBlinkAnimation();
}
public void Hide()
{
if (isVisible) {
isVisible = false;
StopBlinkAnimation();
InvalidateVisual();
}
}
void StartBlinkAnimation()
{
TimeSpan blinkTime = Win32.CaretBlinkTime;
if (blinkTime.TotalMilliseconds >= 0) {
blink = false;
caretBlinkTimer_Tick(null, null);
caretBlinkTimer.Interval = blinkTime;
caretBlinkTimer.Start();
}
}
void StopBlinkAnimation()
{
caretBlinkTimer.Stop();
}
internal Brush CaretBrush;
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
if (isVisible && blink) {
Brush caretBrush = this.CaretBrush;
if (caretBrush == null)
caretBrush = (Brush)textView.GetValue(TextBlock.ForegroundProperty);
Rect r = new Rect(caretRectangle.X - textView.HorizontalOffset,
caretRectangle.Y - textView.VerticalOffset,
caretRectangle.Width,
caretRectangle.Height);
drawingContext.DrawRectangle(caretBrush, null, PixelSnapHelpers.Round(r, PixelSnapHelpers.GetPixelSize(this)));
}
}
}
}

335
ICSharpCode.AvalonEdit/Editing/CaretNavigationCommandHandler.cs

@ -0,0 +1,335 @@ @@ -0,0 +1,335 @@
// 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.Diagnostics;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media.TextFormatting;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Rendering;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Editing
{
static class CaretNavigationCommandHandler
{
/// <summary>
/// Creates a new <see cref="TextAreaInputHandler"/> for the text area.
/// </summary>
public static TextAreaInputHandler Create(TextArea textArea)
{
TextAreaInputHandler handler = new TextAreaInputHandler(textArea);
handler.CommandBindings.AddRange(CommandBindings);
handler.InputBindings.AddRange(InputBindings);
return handler;
}
static readonly List<CommandBinding> CommandBindings = new List<CommandBinding>();
static readonly List<InputBinding> InputBindings = new List<InputBinding>();
static void AddBinding(ICommand command, ModifierKeys modifiers, Key key, ExecutedRoutedEventHandler handler)
{
CommandBindings.Add(new CommandBinding(command, handler));
InputBindings.Add(TextAreaDefaultInputHandler.CreateFrozenKeyBinding(command, modifiers, key));
}
static CaretNavigationCommandHandler()
{
const ModifierKeys None = ModifierKeys.None;
const ModifierKeys Ctrl = ModifierKeys.Control;
const ModifierKeys Shift = ModifierKeys.Shift;
AddBinding(EditingCommands.MoveLeftByCharacter, None, Key.Left, OnMoveCaret(CaretMovementType.CharLeft));
AddBinding(EditingCommands.SelectLeftByCharacter, Shift, Key.Left, OnMoveCaretExtendSelection(CaretMovementType.CharLeft));
AddBinding(EditingCommands.MoveRightByCharacter, None, Key.Right, OnMoveCaret(CaretMovementType.CharRight));
AddBinding(EditingCommands.SelectRightByCharacter, Shift, Key.Right, OnMoveCaretExtendSelection(CaretMovementType.CharRight));
AddBinding(EditingCommands.MoveLeftByWord, Ctrl, Key.Left, OnMoveCaret(CaretMovementType.WordLeft));
AddBinding(EditingCommands.SelectLeftByWord, Ctrl | Shift, Key.Left, OnMoveCaretExtendSelection(CaretMovementType.WordLeft));
AddBinding(EditingCommands.MoveRightByWord, Ctrl, Key.Right, OnMoveCaret(CaretMovementType.WordRight));
AddBinding(EditingCommands.SelectRightByWord, Ctrl | Shift, Key.Right, OnMoveCaretExtendSelection(CaretMovementType.WordRight));
AddBinding(EditingCommands.MoveUpByLine, None, Key.Up, OnMoveCaret(CaretMovementType.LineUp));
AddBinding(EditingCommands.SelectUpByLine, Shift, Key.Up, OnMoveCaretExtendSelection(CaretMovementType.LineUp));
AddBinding(EditingCommands.MoveDownByLine, None, Key.Down, OnMoveCaret(CaretMovementType.LineDown));
AddBinding(EditingCommands.SelectDownByLine, Shift, Key.Down, OnMoveCaretExtendSelection(CaretMovementType.LineDown));
AddBinding(EditingCommands.MoveDownByPage, None, Key.PageDown, OnMoveCaret(CaretMovementType.PageDown));
AddBinding(EditingCommands.SelectDownByPage, Shift, Key.PageDown, OnMoveCaretExtendSelection(CaretMovementType.PageDown));
AddBinding(EditingCommands.MoveUpByPage, None, Key.PageUp, OnMoveCaret(CaretMovementType.PageUp));
AddBinding(EditingCommands.SelectUpByPage, Shift, Key.PageUp, OnMoveCaretExtendSelection(CaretMovementType.PageUp));
AddBinding(EditingCommands.MoveToLineStart, None, Key.Home, OnMoveCaret(CaretMovementType.LineStart));
AddBinding(EditingCommands.SelectToLineStart, Shift, Key.Home, OnMoveCaretExtendSelection(CaretMovementType.LineStart));
AddBinding(EditingCommands.MoveToLineEnd, None, Key.End, OnMoveCaret(CaretMovementType.LineEnd));
AddBinding(EditingCommands.SelectToLineEnd, Shift, Key.End, OnMoveCaretExtendSelection(CaretMovementType.LineEnd));
AddBinding(EditingCommands.MoveToDocumentStart, Ctrl, Key.Home, OnMoveCaret(CaretMovementType.DocumentStart));
AddBinding(EditingCommands.SelectToDocumentStart, Ctrl | Shift, Key.Home, OnMoveCaretExtendSelection(CaretMovementType.DocumentStart));
AddBinding(EditingCommands.MoveToDocumentEnd, Ctrl, Key.End, OnMoveCaret(CaretMovementType.DocumentEnd));
AddBinding(EditingCommands.SelectToDocumentEnd, Ctrl | Shift, Key.End, OnMoveCaretExtendSelection(CaretMovementType.DocumentEnd));
CommandBindings.Add(new CommandBinding(ApplicationCommands.SelectAll, OnSelectAll));
}
static void OnSelectAll(object target, ExecutedRoutedEventArgs args)
{
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.Document != null) {
args.Handled = true;
textArea.Caret.Offset = textArea.Document.TextLength;
textArea.Selection = new SimpleSelection(0, textArea.Document.TextLength);
}
}
static TextArea GetTextArea(object target)
{
return target as TextArea;
}
enum CaretMovementType
{
CharLeft,
CharRight,
WordLeft,
WordRight,
LineUp,
LineDown,
PageUp,
PageDown,
LineStart,
LineEnd,
DocumentStart,
DocumentEnd
}
static ExecutedRoutedEventHandler OnMoveCaret(CaretMovementType direction)
{
return (target, args) => {
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.Document != null) {
args.Handled = true;
textArea.Selection = Selection.Empty;
MoveCaret(textArea, direction);
textArea.Caret.BringCaretToView();
}
};
}
static ExecutedRoutedEventHandler OnMoveCaretExtendSelection(CaretMovementType direction)
{
return (target, args) => {
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.Document != null) {
args.Handled = true;
int oldOffset = textArea.Caret.Offset;
MoveCaret(textArea, direction);
textArea.Selection = textArea.Selection.StartSelectionOrSetEndpoint(oldOffset, textArea.Caret.Offset);
textArea.Caret.BringCaretToView();
}
};
}
#region Caret movement
static void MoveCaret(TextArea textArea, CaretMovementType direction)
{
DocumentLine caretLine = textArea.Document.GetLineByNumber(textArea.Caret.Line);
VisualLine visualLine = textArea.TextView.GetOrConstructVisualLine(caretLine);
TextViewPosition caretPosition = textArea.Caret.Position;
TextLine textLine = visualLine.GetTextLine(caretPosition.VisualColumn);
switch (direction) {
case CaretMovementType.CharLeft:
MoveCaretLeft(textArea, caretPosition, visualLine, CaretPositioningMode.Normal);
break;
case CaretMovementType.CharRight:
MoveCaretRight(textArea, caretPosition, visualLine, CaretPositioningMode.Normal);
break;
case CaretMovementType.WordLeft:
MoveCaretLeft(textArea, caretPosition, visualLine, CaretPositioningMode.WordStart);
break;
case CaretMovementType.WordRight:
MoveCaretRight(textArea, caretPosition, visualLine, CaretPositioningMode.WordStart);
break;
case CaretMovementType.LineUp:
case CaretMovementType.LineDown:
case CaretMovementType.PageUp:
case CaretMovementType.PageDown:
MoveCaretUpDown(textArea, direction, visualLine, textLine, caretPosition.VisualColumn);
break;
case CaretMovementType.DocumentStart:
SetCaretPosition(textArea, 0, 0);
break;
case CaretMovementType.DocumentEnd:
SetCaretPosition(textArea, -1, textArea.Document.TextLength);
break;
case CaretMovementType.LineStart:
MoveCaretToStartOfLine(textArea, visualLine);
break;
case CaretMovementType.LineEnd:
MoveCaretToEndOfLine(textArea, visualLine);
break;
default:
throw new NotSupportedException(direction.ToString());
}
}
#endregion
#region Home/End
static void MoveCaretToStartOfLine(TextArea textArea, VisualLine visualLine)
{
int newVC = visualLine.GetNextCaretPosition(-1, LogicalDirection.Forward, CaretPositioningMode.WordStart);
if (newVC < 0)
throw ThrowUtil.NoValidCaretPosition();
// when the caret is already at the start of the text, jump to start before whitespace
if (newVC == textArea.Caret.VisualColumn)
newVC = 0;
int offset = visualLine.FirstDocumentLine.Offset + visualLine.GetRelativeOffset(newVC);
SetCaretPosition(textArea, newVC, offset);
}
static void MoveCaretToEndOfLine(TextArea textArea, VisualLine visualLine)
{
int newVC = visualLine.VisualLength;
int offset = visualLine.FirstDocumentLine.Offset + visualLine.GetRelativeOffset(newVC);
SetCaretPosition(textArea, newVC, offset);
}
#endregion
#region By-character / By-word movement
static void MoveCaretRight(TextArea textArea, TextViewPosition caretPosition, VisualLine visualLine, CaretPositioningMode mode)
{
int pos = visualLine.GetNextCaretPosition(caretPosition.VisualColumn, LogicalDirection.Forward, mode);
if (pos >= 0) {
SetCaretPosition(textArea, pos, visualLine.GetRelativeOffset(pos) + visualLine.FirstDocumentLine.Offset);
} else {
// move to start of next line
DocumentLine nextDocumentLine = visualLine.LastDocumentLine.NextLine;
if (nextDocumentLine != null) {
VisualLine nextLine = textArea.TextView.GetOrConstructVisualLine(nextDocumentLine);
pos = nextLine.GetNextCaretPosition(-1, LogicalDirection.Forward, mode);
if (pos < 0)
throw ThrowUtil.NoValidCaretPosition();
SetCaretPosition(textArea, pos, nextLine.GetRelativeOffset(pos) + nextLine.FirstDocumentLine.Offset);
} else {
// at end of document
Debug.Assert(visualLine.LastDocumentLine.Offset + visualLine.LastDocumentLine.TotalLength == textArea.Document.TextLength);
SetCaretPosition(textArea, -1, textArea.Document.TextLength);
}
}
}
static void MoveCaretLeft(TextArea textArea, TextViewPosition caretPosition, VisualLine visualLine, CaretPositioningMode mode)
{
int pos = visualLine.GetNextCaretPosition(caretPosition.VisualColumn, LogicalDirection.Backward, mode);
if (pos >= 0) {
SetCaretPosition(textArea, pos, visualLine.GetRelativeOffset(pos) + visualLine.FirstDocumentLine.Offset);
} else {
// move to end of previous line
DocumentLine previousDocumentLine = visualLine.FirstDocumentLine.PreviousLine;
if (previousDocumentLine != null) {
VisualLine previousLine = textArea.TextView.GetOrConstructVisualLine(previousDocumentLine);
pos = previousLine.GetNextCaretPosition(previousLine.VisualLength + 1, LogicalDirection.Backward, mode);
if (pos < 0)
throw ThrowUtil.NoValidCaretPosition();
SetCaretPosition(textArea, pos, previousLine.GetRelativeOffset(pos) + previousLine.FirstDocumentLine.Offset);
} else {
// at start of document
Debug.Assert(visualLine.FirstDocumentLine.Offset == 0);
SetCaretPosition(textArea, 0, 0);
}
}
}
#endregion
#region Line+Page up/down
static void MoveCaretUpDown(TextArea textArea, CaretMovementType direction, VisualLine visualLine, TextLine textLine, int caretVisualColumn)
{
// moving up/down happens using the desired visual X position
double xPos = textArea.Caret.DesiredXPos;
if (double.IsNaN(xPos))
xPos = textLine.GetDistanceFromCharacterHit(new CharacterHit(caretVisualColumn, 0));
// now find the TextLine+VisualLine where the caret will end up in
VisualLine targetVisualLine = visualLine;
TextLine targetLine;
int textLineIndex = visualLine.TextLines.IndexOf(textLine);
switch (direction) {
case CaretMovementType.LineUp:
{
// Move up: move to the previous TextLine in the same visual line
// or move to the last TextLine of the previous visual line
int prevLineNumber = visualLine.FirstDocumentLine.LineNumber - 1;
if (textLineIndex > 0) {
targetLine = visualLine.TextLines[textLineIndex - 1];
} else if (prevLineNumber >= 1) {
DocumentLine prevLine = textArea.Document.GetLineByNumber(prevLineNumber);
targetVisualLine = textArea.TextView.GetOrConstructVisualLine(prevLine);
targetLine = targetVisualLine.TextLines[targetVisualLine.TextLines.Count - 1];
} else {
targetLine = null;
}
break;
}
case CaretMovementType.LineDown:
{
// Move down: move to the next TextLine in the same visual line
// or move to the first TextLine of the next visual line
int nextLineNumber = visualLine.LastDocumentLine.LineNumber + 1;
if (textLineIndex < visualLine.TextLines.Count - 1) {
targetLine = visualLine.TextLines[textLineIndex + 1];
} else if (nextLineNumber <= textArea.Document.LineCount) {
DocumentLine nextLine = textArea.Document.GetLineByNumber(nextLineNumber);
targetVisualLine = textArea.TextView.GetOrConstructVisualLine(nextLine);
targetLine = targetVisualLine.TextLines[0];
} else {
targetLine = null;
}
break;
}
case CaretMovementType.PageUp:
case CaretMovementType.PageDown:
{
// Page up/down: find the target line using its visual position
double yPos = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.LineMiddle);
if (direction == CaretMovementType.PageUp)
yPos -= textArea.TextView.RenderSize.Height;
else
yPos += textArea.TextView.RenderSize.Height;
DocumentLine newLine = textArea.TextView.GetDocumentLineByVisualTop(yPos);
targetVisualLine = textArea.TextView.GetOrConstructVisualLine(newLine);
targetLine = targetVisualLine.GetTextLineByVisualYPosition(yPos);
break;
}
default:
throw new NotSupportedException(direction.ToString());
}
if (targetLine != null) {
CharacterHit ch = targetLine.GetCharacterHitFromDistance(xPos);
SetCaretPosition(textArea, targetVisualLine, targetLine, ch, false);
textArea.Caret.DesiredXPos = xPos;
}
}
#endregion
#region SetCaretPosition
static void SetCaretPosition(TextArea textArea, VisualLine targetVisualLine, TextLine targetLine,
CharacterHit ch, bool allowWrapToNextLine)
{
int newVisualColumn = ch.FirstCharacterIndex + ch.TrailingLength;
int targetLineStartCol = targetVisualLine.GetTextLineVisualStartColumn(targetLine);
if (!allowWrapToNextLine && newVisualColumn >= targetLineStartCol + targetLine.Length)
newVisualColumn = targetLineStartCol + targetLine.Length - 1;
int newOffset = targetVisualLine.GetRelativeOffset(newVisualColumn) + targetVisualLine.FirstDocumentLine.Offset;
SetCaretPosition(textArea, newVisualColumn, newOffset);
}
static void SetCaretPosition(TextArea textArea, int newVisualColumn, int newOffset)
{
textArea.Caret.Position = new TextViewPosition(textArea.Document.GetLocation(newOffset), newVisualColumn);
textArea.Caret.DesiredXPos = double.NaN;
}
#endregion
}
}

33
ICSharpCode.AvalonEdit/Editing/CaretWeakEventHandler.cs

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
// 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 ICSharpCode.AvalonEdit.Utils;
using System;
namespace ICSharpCode.AvalonEdit.Editing
{
/// <summary>
/// Contains classes for handling weak events on the Caret class.
/// </summary>
public static class CaretWeakEventManager
{
/// <summary>
/// Handles the Caret.PositionChanged event.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
public sealed class PositionChanged : WeakEventManagerBase<PositionChanged, Caret>
{
/// <inheritdoc/>
protected override void StartListening(Caret source)
{
source.PositionChanged += DeliverEvent;
}
/// <inheritdoc/>
protected override void StopListening(Caret source)
{
source.PositionChanged -= DeliverEvent;
}
}
}
}

63
ICSharpCode.AvalonEdit/Editing/DottedLineMargin.cs

@ -0,0 +1,63 @@ @@ -0,0 +1,63 @@
// 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.Windows;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Shapes;
namespace ICSharpCode.AvalonEdit.Editing
{
/// <summary>
/// Margin for use with the text area.
/// A vertical dotted line to separate the line numbers from the text view.
/// </summary>
public static class DottedLineMargin
{
static readonly object tag = new object();
/// <summary>
/// Creates a vertical dotted line to separate the line numbers from the text view.
/// </summary>
public static UIElement Create()
{
Line line = new Line {
X1 = 0, Y1 = 0, X2 = 0, Y2 = 1,
StrokeDashArray = { 0, 2 },
Stretch = Stretch.Fill,
StrokeThickness = 1,
StrokeDashCap = PenLineCap.Round,
Margin = new Thickness(2, 0, 2, 0),
Tag = tag
};
return line;
}
/// <summary>
/// Creates a vertical dotted line to separate the line numbers from the text view.
/// </summary>
[Obsolete("This method got published accidentally; and will be removed again in a future version. Use the parameterless overload instead.")]
public static UIElement Create(TextEditor editor)
{
Line line = (Line)Create();
line.SetBinding(
Line.StrokeProperty,
new Binding("LineNumbersForeground") { Source = editor }
);
return line;
}
/// <summary>
/// Gets whether the specified UIElement is the result of a DottedLineMargin.Create call.
/// </summary>
public static bool IsDottedLineMargin(UIElement element)
{
Line l = element as Line;
return l != null && l.Tag == tag;
}
}
}

46
ICSharpCode.AvalonEdit/Editing/DragDropException.cs

@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
// 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.Runtime.Serialization;
namespace ICSharpCode.AvalonEdit.Editing
{
/// <summary>
/// Wraps exceptions that occur during drag'n'drop.
/// Exceptions during drag'n'drop might
/// get swallowed by WPF/COM, so AvalonEdit catches them and re-throws them later
/// wrapped in a DragDropException.
/// </summary>
[Serializable()]
public class DragDropException : Exception
{
/// <summary>
/// Creates a new DragDropException.
/// </summary>
public DragDropException() : base()
{
}
/// <summary>
/// Creates a new DragDropException.
/// </summary>
public DragDropException(string message) : base(message)
{
}
/// <summary>
/// Creates a new DragDropException.
/// </summary>
public DragDropException(string message, Exception innerException) : base(message, innerException)
{
}
/// <summary>
/// Deserializes a DragDropException.
/// </summary>
protected DragDropException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
}

554
ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.cs

@ -0,0 +1,554 @@ @@ -0,0 +1,554 @@
// 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.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Input;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Highlighting;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Editing
{
/// <summary>
/// We re-use the CommandBinding and InputBinding instances between multiple text areas,
/// so this class is static.
/// </summary>
static class EditingCommandHandler
{
/// <summary>
/// Creates a new <see cref="TextAreaInputHandler"/> for the text area.
/// </summary>
public static TextAreaInputHandler Create(TextArea textArea)
{
TextAreaInputHandler handler = new TextAreaInputHandler(textArea);
handler.CommandBindings.AddRange(CommandBindings);
handler.InputBindings.AddRange(InputBindings);
return handler;
}
static readonly List<CommandBinding> CommandBindings = new List<CommandBinding>();
static readonly List<InputBinding> InputBindings = new List<InputBinding>();
static void AddBinding(ICommand command, ModifierKeys modifiers, Key key, ExecutedRoutedEventHandler handler)
{
CommandBindings.Add(new CommandBinding(command, handler));
InputBindings.Add(TextAreaDefaultInputHandler.CreateFrozenKeyBinding(command, modifiers, key));
}
static EditingCommandHandler()
{
CommandBindings.Add(new CommandBinding(ApplicationCommands.Delete, OnDelete(ApplicationCommands.NotACommand), CanDelete));
AddBinding(EditingCommands.Delete, ModifierKeys.None, Key.Delete, OnDelete(EditingCommands.SelectRightByCharacter));
AddBinding(EditingCommands.DeleteNextWord, ModifierKeys.Control, Key.Delete, OnDelete(EditingCommands.SelectRightByWord));
AddBinding(EditingCommands.Backspace, ModifierKeys.None, Key.Back, OnDelete(EditingCommands.SelectLeftByCharacter));
InputBindings.Add(new KeyBinding(EditingCommands.Backspace, Key.Back, ModifierKeys.Shift)); // make Shift-Backspace do the same as plain backspace
AddBinding(EditingCommands.DeletePreviousWord, ModifierKeys.Control, Key.Back, OnDelete(EditingCommands.SelectLeftByWord));
AddBinding(EditingCommands.EnterParagraphBreak, ModifierKeys.None, Key.Enter, OnEnter);
AddBinding(EditingCommands.EnterLineBreak, ModifierKeys.Shift, Key.Enter, OnEnter);
AddBinding(EditingCommands.TabForward, ModifierKeys.None, Key.Tab, OnTab);
AddBinding(EditingCommands.TabBackward, ModifierKeys.Shift, Key.Tab, OnShiftTab);
CommandBindings.Add(new CommandBinding(ApplicationCommands.Copy, OnCopy, CanCutOrCopy));
CommandBindings.Add(new CommandBinding(ApplicationCommands.Cut, OnCut, CanCutOrCopy));
CommandBindings.Add(new CommandBinding(ApplicationCommands.Paste, OnPaste, CanPaste));
CommandBindings.Add(new CommandBinding(AvalonEditCommands.DeleteLine, OnDeleteLine));
CommandBindings.Add(new CommandBinding(AvalonEditCommands.RemoveLeadingWhitespace, OnRemoveLeadingWhitespace));
CommandBindings.Add(new CommandBinding(AvalonEditCommands.RemoveTrailingWhitespace, OnRemoveTrailingWhitespace));
CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertToUppercase, OnConvertToUpperCase));
CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertToLowercase, OnConvertToLowerCase));
CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertToTitleCase, OnConvertToTitleCase));
CommandBindings.Add(new CommandBinding(AvalonEditCommands.InvertCase, OnInvertCase));
CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertTabsToSpaces, OnConvertTabsToSpaces));
CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertSpacesToTabs, OnConvertSpacesToTabs));
CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertLeadingTabsToSpaces, OnConvertLeadingTabsToSpaces));
CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertLeadingSpacesToTabs, OnConvertLeadingSpacesToTabs));
CommandBindings.Add(new CommandBinding(AvalonEditCommands.IndentSelection, OnIndentSelection));
}
static TextArea GetTextArea(object target)
{
return target as TextArea;
}
#region Text Transformation Helpers
enum DefaultSegmentType
{
None,
WholeDocument,
CurrentLine
}
/// <summary>
/// Calls transformLine on all lines in the selected range.
/// transformLine needs to handle read-only segments!
/// </summary>
static void TransformSelectedLines(Action<TextArea, DocumentLine> transformLine, object target, ExecutedRoutedEventArgs args, DefaultSegmentType defaultSegmentType)
{
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.Document != null) {
using (textArea.Document.RunUpdate()) {
DocumentLine start, end;
if (textArea.Selection.IsEmpty) {
if (defaultSegmentType == DefaultSegmentType.CurrentLine) {
start = end = textArea.Document.GetLineByNumber(textArea.Caret.Line);
} else if (defaultSegmentType == DefaultSegmentType.WholeDocument) {
start = textArea.Document.Lines.First();
end = textArea.Document.Lines.Last();
} else {
start = end = null;
}
} else {
ISegment segment = textArea.Selection.SurroundingSegment;
start = textArea.Document.GetLineByOffset(segment.Offset);
end = textArea.Document.GetLineByOffset(segment.EndOffset);
// don't include the last line if no characters on it are selected
if (start != end && end.Offset == segment.EndOffset)
end = end.PreviousLine;
}
if (start != null) {
transformLine(textArea, start);
while (start != end) {
start = start.NextLine;
transformLine(textArea, start);
}
}
}
textArea.Caret.BringCaretToView();
args.Handled = true;
}
}
/// <summary>
/// Calls transformLine on all writable segment in the selected range.
/// </summary>
static void TransformSelectedSegments(Action<TextArea, ISegment> transformSegment, object target, ExecutedRoutedEventArgs args, DefaultSegmentType defaultSegmentType)
{
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.Document != null) {
using (textArea.Document.RunUpdate()) {
IEnumerable<ISegment> segments;
if (textArea.Selection.IsEmpty) {
if (defaultSegmentType == DefaultSegmentType.CurrentLine) {
segments = new ISegment[] { textArea.Document.GetLineByNumber(textArea.Caret.Line) };
} else if (defaultSegmentType == DefaultSegmentType.WholeDocument) {
segments = textArea.Document.Lines.Cast<ISegment>();
} else {
segments = null;
}
} else {
segments = textArea.Selection.Segments;
}
if (segments != null) {
foreach (ISegment segment in segments.Reverse()) {
foreach (ISegment writableSegment in textArea.GetDeletableSegments(segment).Reverse()) {
transformSegment(textArea, writableSegment);
}
}
}
}
textArea.Caret.BringCaretToView();
args.Handled = true;
}
}
#endregion
#region EnterLineBreak
static void OnEnter(object target, ExecutedRoutedEventArgs args)
{
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.IsKeyboardFocused) {
textArea.PerformTextInput("\n");
args.Handled = true;
}
}
#endregion
#region Tab
static void OnTab(object target, ExecutedRoutedEventArgs args)
{
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.Document != null) {
using (textArea.Document.RunUpdate()) {
if (textArea.Selection.IsMultiline(textArea.Document)) {
var segment = textArea.Selection.SurroundingSegment;
DocumentLine start = textArea.Document.GetLineByOffset(segment.Offset);
DocumentLine end = textArea.Document.GetLineByOffset(segment.EndOffset);
// don't include the last line if no characters on it are selected
if (start != end && end.Offset == segment.EndOffset)
end = end.PreviousLine;
DocumentLine current = start;
while (true) {
int offset = current.Offset;
if (textArea.ReadOnlySectionProvider.CanInsert(offset))
textArea.Document.Replace(offset, 0, textArea.Options.IndentationString, OffsetChangeMappingType.KeepAnchorBeforeInsertion);
if (current == end)
break;
current = current.NextLine;
}
} else {
string indentationString = textArea.Options.GetIndentationString(textArea.Caret.Column);
textArea.ReplaceSelectionWithText(indentationString);
}
}
textArea.Caret.BringCaretToView();
args.Handled = true;
}
}
static void OnShiftTab(object target, ExecutedRoutedEventArgs args)
{
TransformSelectedLines(
delegate (TextArea textArea, DocumentLine line) {
int offset = line.Offset;
ISegment s = TextUtilities.GetSingleIndentationSegment(textArea.Document, offset, textArea.Options.IndentationSize);
if (s.Length > 0) {
s = textArea.GetDeletableSegments(s).FirstOrDefault();
if (s != null && s.Length > 0) {
textArea.Document.Remove(s.Offset, s.Length);
}
}
}, target, args, DefaultSegmentType.CurrentLine);
}
#endregion
#region Delete
static ExecutedRoutedEventHandler OnDelete(RoutedUICommand selectingCommand)
{
return (target, args) => {
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.Document != null) {
// call BeginUpdate before running the 'selectingCommand'
// so that undoing the delete does not select the deleted character
using (textArea.Document.RunUpdate()) {
if (textArea.Selection.IsEmpty) {
TextViewPosition oldCaretPosition = textArea.Caret.Position;
selectingCommand.Execute(args.Parameter, textArea);
bool hasSomethingDeletable = false;
foreach (ISegment s in textArea.Selection.Segments) {
if (textArea.GetDeletableSegments(s).Length > 0) {
hasSomethingDeletable = true;
break;
}
}
if (!hasSomethingDeletable) {
// If nothing in the selection is deletable; then reset caret+selection
// to the previous value. This prevents the caret from moving through read-only sections.
textArea.Caret.Position = oldCaretPosition;
textArea.Selection = Selection.Empty;
}
}
textArea.RemoveSelectedText();
}
textArea.Caret.BringCaretToView();
args.Handled = true;
}
};
}
static void CanDelete(object target, CanExecuteRoutedEventArgs args)
{
// HasSomethingSelected for delete command
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.Document != null) {
args.CanExecute = !textArea.Selection.IsEmpty;
args.Handled = true;
}
}
#endregion
#region Clipboard commands
static void CanCutOrCopy(object target, CanExecuteRoutedEventArgs args)
{
// HasSomethingSelected for copy and cut commands
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.Document != null) {
args.CanExecute = textArea.Options.CutCopyWholeLine || !textArea.Selection.IsEmpty;
args.Handled = true;
}
}
static void OnCopy(object target, ExecutedRoutedEventArgs args)
{
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.Document != null) {
if (textArea.Selection.IsEmpty && textArea.Options.CutCopyWholeLine) {
DocumentLine currentLine = textArea.Document.GetLineByNumber(textArea.Caret.Line);
CopyWholeLine(textArea, currentLine);
} else {
CopySelectedText(textArea);
}
args.Handled = true;
}
}
static void OnCut(object target, ExecutedRoutedEventArgs args)
{
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.Document != null) {
if (textArea.Selection.IsEmpty && textArea.Options.CutCopyWholeLine) {
DocumentLine currentLine = textArea.Document.GetLineByNumber(textArea.Caret.Line);
CopyWholeLine(textArea, currentLine);
textArea.Document.Remove(currentLine.Offset, currentLine.TotalLength);
} else {
CopySelectedText(textArea);
textArea.RemoveSelectedText();
}
textArea.Caret.BringCaretToView();
args.Handled = true;
}
}
static void CopySelectedText(TextArea textArea)
{
var data = textArea.Selection.CreateDataObject(textArea);
try {
Clipboard.SetDataObject(data, true);
} catch (ExternalException) {
// Apparently this exception sometimes happens randomly.
// The MS controls just ignore it, so we'll do the same.
return;
}
string text = textArea.Selection.GetText(textArea.Document);
text = TextUtilities.NormalizeNewLines(text, Environment.NewLine);
textArea.OnTextCopied(new TextEventArgs(text));
}
const string LineSelectedType = "MSDEVLineSelect"; // This is the type VS 2003 and 2005 use for flagging a whole line copy
static void CopyWholeLine(TextArea textArea, DocumentLine line)
{
ISegment wholeLine = new SimpleSegment(line.Offset, line.TotalLength);
string text = textArea.Document.GetText(wholeLine);
// Ensure we use the appropriate newline sequence for the OS
text = TextUtilities.NormalizeNewLines(text, Environment.NewLine);
DataObject data = new DataObject(text);
// Also copy text in HTML format to clipboard - good for pasting text into Word
// or to the SharpDevelop forums.
IHighlighter highlighter = textArea.GetService(typeof(IHighlighter)) as IHighlighter;
HtmlClipboard.SetHtml(data, HtmlClipboard.CreateHtmlFragment(textArea.Document, highlighter, wholeLine, new HtmlOptions(textArea.Options)));
MemoryStream lineSelected = new MemoryStream(1);
lineSelected.WriteByte(1);
data.SetData(LineSelectedType, lineSelected, false);
try {
Clipboard.SetDataObject(data, true);
} catch (ExternalException) {
// Apparently this exception sometimes happens randomly.
// The MS controls just ignore it, so we'll do the same.
return;
}
textArea.OnTextCopied(new TextEventArgs(text));
}
static void CanPaste(object target, CanExecuteRoutedEventArgs args)
{
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.Document != null) {
args.CanExecute = textArea.ReadOnlySectionProvider.CanInsert(textArea.Caret.Offset)
&& Clipboard.ContainsText();
args.Handled = true;
}
}
static void OnPaste(object target, ExecutedRoutedEventArgs args)
{
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.Document != null) {
Debug.WriteLine( Clipboard.GetText(TextDataFormat.Html) );
// convert text back to correct newlines for this document
string newLine = TextUtilities.GetNewLineFromDocument(textArea.Document, textArea.Caret.Line);
string text = TextUtilities.NormalizeNewLines(Clipboard.GetText(), newLine);
if (!string.IsNullOrEmpty(text)) {
bool fullLine = textArea.Options.CutCopyWholeLine && Clipboard.ContainsData(LineSelectedType);
bool rectangular = Clipboard.ContainsData(RectangleSelection.RectangularSelectionDataType);
if (fullLine) {
DocumentLine currentLine = textArea.Document.GetLineByNumber(textArea.Caret.Line);
if (textArea.ReadOnlySectionProvider.CanInsert(currentLine.Offset)) {
textArea.Document.Insert(currentLine.Offset, text);
}
} else if (rectangular && textArea.Selection.IsEmpty) {
if (!RectangleSelection.PerformRectangularPaste(textArea, textArea.Caret.Offset, text, false))
textArea.ReplaceSelectionWithText(text);
} else {
textArea.ReplaceSelectionWithText(text);
}
}
textArea.Caret.BringCaretToView();
args.Handled = true;
}
}
#endregion
#region DeleteLine
static void OnDeleteLine(object target, ExecutedRoutedEventArgs args)
{
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.Document != null) {
DocumentLine currentLine = textArea.Document.GetLineByNumber(textArea.Caret.Line);
textArea.Selection = new SimpleSelection(currentLine.Offset, currentLine.Offset + currentLine.TotalLength);
textArea.RemoveSelectedText();
args.Handled = true;
}
}
#endregion
#region Remove..Whitespace / Convert Tabs-Spaces
static void OnRemoveLeadingWhitespace(object target, ExecutedRoutedEventArgs args)
{
TransformSelectedLines(
delegate (TextArea textArea, DocumentLine line) {
textArea.Document.Remove(TextUtilities.GetLeadingWhitespace(textArea.Document, line));
}, target, args, DefaultSegmentType.WholeDocument);
}
static void OnRemoveTrailingWhitespace(object target, ExecutedRoutedEventArgs args)
{
TransformSelectedLines(
delegate (TextArea textArea, DocumentLine line) {
textArea.Document.Remove(TextUtilities.GetTrailingWhitespace(textArea.Document, line));
}, target, args, DefaultSegmentType.WholeDocument);
}
static void OnConvertTabsToSpaces(object target, ExecutedRoutedEventArgs args)
{
TransformSelectedSegments(ConvertTabsToSpaces, target, args, DefaultSegmentType.WholeDocument);
}
static void OnConvertLeadingTabsToSpaces(object target, ExecutedRoutedEventArgs args)
{
TransformSelectedLines(
delegate (TextArea textArea, DocumentLine line) {
ConvertTabsToSpaces(textArea, TextUtilities.GetLeadingWhitespace(textArea.Document, line));
}, target, args, DefaultSegmentType.WholeDocument);
}
static void ConvertTabsToSpaces(TextArea textArea, ISegment segment)
{
TextDocument document = textArea.Document;
int endOffset = segment.EndOffset;
string indentationString = new string(' ', textArea.Options.IndentationSize);
for (int offset = segment.Offset; offset < endOffset; offset++) {
if (document.GetCharAt(offset) == '\t') {
document.Replace(offset, 1, indentationString, OffsetChangeMappingType.CharacterReplace);
endOffset += indentationString.Length - 1;
}
}
}
static void OnConvertSpacesToTabs(object target, ExecutedRoutedEventArgs args)
{
TransformSelectedSegments(ConvertSpacesToTabs, target, args, DefaultSegmentType.WholeDocument);
}
static void OnConvertLeadingSpacesToTabs(object target, ExecutedRoutedEventArgs args)
{
TransformSelectedLines(
delegate (TextArea textArea, DocumentLine line) {
ConvertSpacesToTabs(textArea, TextUtilities.GetLeadingWhitespace(textArea.Document, line));
}, target, args, DefaultSegmentType.WholeDocument);
}
static void ConvertSpacesToTabs(TextArea textArea, ISegment segment)
{
TextDocument document = textArea.Document;
int endOffset = segment.EndOffset;
int indentationSize = textArea.Options.IndentationSize;
int spacesCount = 0;
for (int offset = segment.Offset; offset < endOffset; offset++) {
if (document.GetCharAt(offset) == ' ') {
spacesCount++;
if (spacesCount == indentationSize) {
document.Replace(offset - (indentationSize - 1), indentationSize, "\t", OffsetChangeMappingType.CharacterReplace);
spacesCount = 0;
offset -= indentationSize - 1;
endOffset -= indentationSize - 1;
}
} else {
spacesCount = 0;
}
}
}
#endregion
#region Convert...Case
static void ConvertCase(Func<string, string> transformText, object target, ExecutedRoutedEventArgs args)
{
TransformSelectedSegments(
delegate (TextArea textArea, ISegment segment) {
string oldText = textArea.Document.GetText(segment);
string newText = transformText(oldText);
textArea.Document.Replace(segment.Offset, segment.Length, newText, OffsetChangeMappingType.CharacterReplace);
}, target, args, DefaultSegmentType.WholeDocument);
}
static void OnConvertToUpperCase(object target, ExecutedRoutedEventArgs args)
{
ConvertCase(CultureInfo.CurrentCulture.TextInfo.ToUpper, target, args);
}
static void OnConvertToLowerCase(object target, ExecutedRoutedEventArgs args)
{
ConvertCase(CultureInfo.CurrentCulture.TextInfo.ToLower, target, args);
}
static void OnConvertToTitleCase(object target, ExecutedRoutedEventArgs args)
{
ConvertCase(CultureInfo.CurrentCulture.TextInfo.ToTitleCase, target, args);
}
static void OnInvertCase(object target, ExecutedRoutedEventArgs args)
{
ConvertCase(InvertCase, target, args);
}
static string InvertCase(string text)
{
CultureInfo culture = CultureInfo.CurrentCulture;
char[] buffer = text.ToCharArray();
for (int i = 0; i < buffer.Length; ++i) {
char c = buffer[i];
buffer[i] = char.IsUpper(c) ? char.ToLower(c, culture) : char.ToUpper(c, culture);
}
return new string(buffer);
}
#endregion
static void OnIndentSelection(object target, ExecutedRoutedEventArgs args)
{
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.Document != null) {
using (textArea.Document.RunUpdate()) {
int start, end;
if (textArea.Selection.IsEmpty) {
start = 1;
end = textArea.Document.LineCount;
} else {
start = textArea.Document.GetLineByOffset(textArea.Selection.SurroundingSegment.Offset).LineNumber;
end = textArea.Document.GetLineByOffset(textArea.Selection.SurroundingSegment.EndOffset).LineNumber;
}
textArea.IndentationStrategy.IndentLines(textArea.Document, start, end);
}
textArea.Caret.BringCaretToView();
args.Handled = true;
}
}
}
}

32
ICSharpCode.AvalonEdit/Editing/IReadOnlySectionProvider.cs

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
// 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 ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Editing
{
/// <summary>
/// Determines whether the document can be modified.
/// </summary>
public interface IReadOnlySectionProvider
{
/// <summary>
/// Gets whether insertion is possible at the specified offset.
/// </summary>
bool CanInsert(int offset);
/// <summary>
/// Gets the deletable segments inside the given segment.
/// </summary>
/// <remarks>
/// All segments in the result must be within the given segment, and they must be returned in order
/// (e.g. if two segments are returned, EndOffset of first segment must be less than StartOffset of second segment).
///
/// For replacements, the last segment being returned will be replaced with the new text. If an empty list is returned,
/// no replacement will be done.
/// </remarks>
IEnumerable<ISegment> GetDeletableSegments(ISegment segment);
}
}

232
ICSharpCode.AvalonEdit/Editing/LineNumberMargin.cs

@ -0,0 +1,232 @@ @@ -0,0 +1,232 @@
// 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.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Rendering;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Editing
{
/// <summary>
/// Margin showing line numbers.
/// </summary>
public class LineNumberMargin : AbstractMargin, IWeakEventListener
{
static LineNumberMargin()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(LineNumberMargin),
new FrameworkPropertyMetadata(typeof(LineNumberMargin)));
}
TextArea textArea;
Typeface typeface;
double emSize;
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
{
typeface = this.CreateTypeface();
emSize = (double)GetValue(TextBlock.FontSizeProperty);
FormattedText text = TextFormatterFactory.CreateFormattedText(
this,
new string('9', maxLineNumberLength),
typeface,
emSize,
(Brush)GetValue(Control.ForegroundProperty)
);
return new Size(text.Width, 0);
}
/// <inheritdoc/>
protected override void OnRender(DrawingContext drawingContext)
{
TextView textView = this.TextView;
Size renderSize = this.RenderSize;
if (textView != null && textView.VisualLinesValid) {
var foreground = (Brush)GetValue(Control.ForegroundProperty);
foreach (VisualLine line in textView.VisualLines) {
int lineNumber = line.FirstDocumentLine.LineNumber;
FormattedText text = TextFormatterFactory.CreateFormattedText(
this,
lineNumber.ToString(CultureInfo.CurrentCulture),
typeface, emSize, foreground
);
drawingContext.DrawText(text, new Point(renderSize.Width - text.Width,
line.VisualTop - textView.VerticalOffset));
}
}
}
/// <inheritdoc/>
protected override void OnTextViewChanged(TextView oldTextView, TextView newTextView)
{
if (oldTextView != null) {
oldTextView.VisualLinesChanged -= TextViewVisualLinesChanged;
}
base.OnTextViewChanged(oldTextView, newTextView);
if (newTextView != null) {
newTextView.VisualLinesChanged += TextViewVisualLinesChanged;
// find the text area belonging to the new text view
textArea = newTextView.Services.GetService(typeof(TextArea)) as TextArea;
} else {
textArea = null;
}
InvalidateVisual();
}
/// <inheritdoc/>
protected override void OnDocumentChanged(TextDocument oldDocument, TextDocument newDocument)
{
if (oldDocument != null) {
TextDocumentWeakEventManager.LineCountChanged.RemoveListener(oldDocument, this);
}
base.OnDocumentChanged(oldDocument, newDocument);
if (newDocument != null) {
TextDocumentWeakEventManager.LineCountChanged.AddListener(newDocument, this);
}
OnDocumentLineCountChanged();
}
/// <inheritdoc cref="IWeakEventListener.ReceiveWeakEvent"/>
protected virtual bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
if (managerType == typeof(TextDocumentWeakEventManager.LineCountChanged)) {
OnDocumentLineCountChanged();
return true;
}
return false;
}
bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
return ReceiveWeakEvent(managerType, sender, e);
}
int maxLineNumberLength = 1;
void OnDocumentLineCountChanged()
{
int documentLineCount = Document != null ? Document.LineCount : 1;
int newLength = documentLineCount.ToString(CultureInfo.CurrentCulture).Length;
// The margin looks too small when there is only one digit, so always reserve space for
// at least two digits
if (newLength < 2)
newLength = 2;
if (newLength != maxLineNumberLength) {
maxLineNumberLength = newLength;
InvalidateMeasure();
}
}
void TextViewVisualLinesChanged(object sender, EventArgs e)
{
InvalidateVisual();
}
AnchorSegment selectionStart;
bool selecting;
/// <inheritdoc/>
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
if (!e.Handled && TextView != null && textArea != null) {
e.Handled = true;
textArea.Focus();
SimpleSegment currentSeg = GetTextLineSegment(e);
if (currentSeg == SimpleSegment.Invalid)
return;
textArea.Caret.Offset = currentSeg.Offset + currentSeg.Length;
if (CaptureMouse()) {
selecting = true;
selectionStart = new AnchorSegment(Document, currentSeg.Offset, currentSeg.Length);
if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) {
SimpleSelection simpleSelection = textArea.Selection as SimpleSelection;
if (simpleSelection != null)
selectionStart = new AnchorSegment(Document, simpleSelection);
}
textArea.Selection = new SimpleSelection(selectionStart);
if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) {
ExtendSelection(currentSeg);
}
}
}
}
SimpleSegment GetTextLineSegment(MouseEventArgs e)
{
Point pos = e.GetPosition(TextView);
pos.X = 0;
pos.Y += TextView.VerticalOffset;
VisualLine vl = TextView.GetVisualLineFromVisualTop(pos.Y);
if (vl == null)
return SimpleSegment.Invalid;
TextLine tl = vl.GetTextLineByVisualYPosition(pos.Y);
int visualStartColumn = vl.GetTextLineVisualStartColumn(tl);
int visualEndColumn = visualStartColumn + tl.Length;
int relStart = vl.FirstDocumentLine.Offset;
int startOffset = vl.GetRelativeOffset(visualStartColumn) + relStart;
int endOffset = vl.GetRelativeOffset(visualEndColumn) + relStart;
if (endOffset == vl.LastDocumentLine.Offset + vl.LastDocumentLine.Length)
endOffset += vl.LastDocumentLine.DelimiterLength;
return new SimpleSegment(startOffset, endOffset - startOffset);
}
void ExtendSelection(SimpleSegment currentSeg)
{
if (currentSeg.Offset < selectionStart.Offset) {
textArea.Caret.Offset = currentSeg.Offset;
textArea.Selection = new SimpleSelection(currentSeg.Offset, selectionStart.Offset + selectionStart.Length);
} else {
textArea.Caret.Offset = currentSeg.Offset + currentSeg.Length;
textArea.Selection = new SimpleSelection(selectionStart.Offset, currentSeg.Offset + currentSeg.Length);
}
}
/// <inheritdoc/>
protected override void OnMouseMove(MouseEventArgs e)
{
if (selecting && textArea != null && TextView != null) {
e.Handled = true;
SimpleSegment currentSeg = GetTextLineSegment(e);
if (currentSeg == SimpleSegment.Invalid)
return;
ExtendSelection(currentSeg);
}
base.OnMouseMove(e);
}
/// <inheritdoc/>
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
if (selecting) {
selecting = false;
selectionStart = null;
ReleaseMouseCapture();
e.Handled = true;
}
base.OnMouseLeftButtonUp(e);
}
/// <inheritdoc/>
protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
{
// accept clicks even when clicking on the background
return new PointHitTestResult(this, hitTestParameters.HitPoint);
}
}
}

50
ICSharpCode.AvalonEdit/Editing/NoReadOnlySections.cs

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
// 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.Linq;
using System.Collections.Generic;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Editing
{
/// <summary>
/// <see cref="IReadOnlySectionProvider"/> that has no read-only sections; all text is editable.
/// </summary>
sealed class NoReadOnlySections : IReadOnlySectionProvider
{
public static readonly NoReadOnlySections Instance = new NoReadOnlySections();
public bool CanInsert(int offset)
{
return true;
}
public IEnumerable<ISegment> GetDeletableSegments(ISegment segment)
{
if (segment == null)
throw new ArgumentNullException("segment");
// the segment is always deletable
return ExtensionMethods.Sequence(segment);
}
}
/// <summary>
/// <see cref="IReadOnlySectionProvider"/> that completely disables editing.
/// </summary>
sealed class ReadOnlyDocument : IReadOnlySectionProvider
{
public static readonly ReadOnlyDocument Instance = new ReadOnlyDocument();
public bool CanInsert(int offset)
{
return false;
}
public IEnumerable<ISegment> GetDeletableSegments(ISegment segment)
{
return Enumerable.Empty<ISegment>();
}
}
}

279
ICSharpCode.AvalonEdit/Editing/RectangleSelection.cs

@ -0,0 +1,279 @@ @@ -0,0 +1,279 @@
// 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.IO;
using System.Linq;
using System.Text;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Editing
{
/// <summary>
/// Rectangular selection.
/// </summary>
public sealed class RectangleSelection : Selection
{
TextDocument document;
/// <summary>
/// Gets the start position of the selection.
/// </summary>
public int StartOffset { get; private set; }
/// <summary>
/// Gets the end position of the selection.
/// </summary>
public int EndOffset { get; private set; }
/// <summary>
/// Creates a new rectangular selection.
/// </summary>
public RectangleSelection(TextDocument document, int start, int end)
{
if (document == null)
throw new ArgumentNullException("document");
this.document = document;
this.StartOffset = start;
this.EndOffset = end;
}
/// <inheritdoc/>
public override bool IsEmpty {
get {
TextLocation start = document.GetLocation(StartOffset);
TextLocation end = document.GetLocation(EndOffset);
return start.Column == end.Column;
}
}
/// <inheritdoc/>
public override bool Contains(int offset)
{
if (Math.Min(StartOffset, EndOffset) <= offset && offset <= Math.Max(StartOffset, EndOffset)) {
foreach (ISegment s in this.Segments) {
if (s.Contains(offset))
return true;
}
}
return false;
}
/// <inheritdoc/>
public override string GetText(TextDocument document)
{
StringBuilder b = new StringBuilder();
foreach (ISegment s in this.Segments) {
if (b.Length > 0)
b.AppendLine();
b.Append(document.GetText(s));
}
return b.ToString();
}
/// <inheritdoc/>
public override Selection StartSelectionOrSetEndpoint(int startOffset, int newEndOffset)
{
return SetEndpoint(newEndOffset);
}
/// <inheritdoc/>
public override int Length {
get {
return this.Segments.Sum(s => s.Length);
}
}
/// <inheritdoc/>
public override ISegment SurroundingSegment {
get {
return new SimpleSegment(Math.Min(StartOffset, EndOffset), Math.Abs(EndOffset - StartOffset));
}
}
/// <inheritdoc/>
public override IEnumerable<ISegment> Segments {
get {
TextLocation start = document.GetLocation(StartOffset);
TextLocation end = document.GetLocation(EndOffset);
DocumentLine line = document.GetLineByNumber(Math.Min(start.Line, end.Line));
int numberOfLines = Math.Abs(start.Line - end.Line);
int startCol = Math.Min(start.Column, end.Column);
int endCol = Math.Max(start.Column, end.Column);
for (int i = 0; i <= numberOfLines; i++) {
if (line.Length + 1 >= startCol) {
int thisLineEndCol = Math.Min(endCol, line.Length + 1);
yield return new SimpleSegment(line.Offset + startCol - 1, thisLineEndCol - startCol);
}
line = line.NextLine;
}
}
}
/// <inheritdoc/>
public override bool Equals(object obj)
{
RectangleSelection r = obj as RectangleSelection;
return r != null && r.document == this.document && r.StartOffset == this.StartOffset && r.EndOffset == this.EndOffset;
}
/// <inheritdoc/>
public override int GetHashCode()
{
return StartOffset ^ EndOffset;
}
/// <inheritdoc/>
public override Selection SetEndpoint(int newEndOffset)
{
return new RectangleSelection(this.document, this.StartOffset, newEndOffset);
}
/// <inheritdoc/>
public override Selection UpdateOnDocumentChange(DocumentChangeEventArgs e)
{
return new RectangleSelection(document,
e.GetNewOffset(StartOffset, AnchorMovementType.AfterInsertion),
e.GetNewOffset(EndOffset, AnchorMovementType.BeforeInsertion));
}
/// <inheritdoc/>
public override void ReplaceSelectionWithText(TextArea textArea, string newText)
{
if (textArea == null)
throw new ArgumentNullException("textArea");
if (newText == null)
throw new ArgumentNullException("newText");
using (textArea.Document.RunUpdate()) {
TextLocation start = document.GetLocation(StartOffset);
TextLocation end = document.GetLocation(EndOffset);
int editColumn = Math.Min(start.Column, end.Column);
if (NewLineFinder.NextNewLine(newText, 0) == SimpleSegment.Invalid) {
// insert same text into every line
foreach (ISegment lineSegment in this.Segments.Reverse()) {
ReplaceSingleLineText(textArea, lineSegment, newText);
}
TextLocation newStart = new TextLocation(start.Line, editColumn + newText.Length);
TextLocation newEnd = new TextLocation(end.Line, editColumn + newText.Length);
textArea.Caret.Location = newEnd;
textArea.Selection = new RectangleSelection(document, document.GetOffset(newStart), document.GetOffset(newEnd));
} else {
// convert all segment start/ends to anchors
var segments = this.Segments.Select(s => new AnchorSegment(this.document, s)).ToList();
SimpleSegment ds = NewLineFinder.NextNewLine(newText, 0);
// we'll check whether all lines have the same length. If so, we can continue using a rectangular selection.
int commonLength = -1;
// now insert lines into rectangular selection
int lastDelimiterEnd = 0;
bool isAtEnd = false;
int i;
for (i = 0; i < segments.Count; i++) {
string lineText;
if (ds == SimpleSegment.Invalid || (i == segments.Count - 1)) {
lineText = newText.Substring(lastDelimiterEnd);
isAtEnd = true;
// if we have more lines to insert than this selection is long, we cannot continue using a rectangular selection
if (ds != SimpleSegment.Invalid)
commonLength = -1;
} else {
lineText = newText.Substring(lastDelimiterEnd, ds.Offset - lastDelimiterEnd);
}
if (i == 0) {
commonLength = lineText.Length;
} else if (commonLength != lineText.Length) {
commonLength = -1;
}
ReplaceSingleLineText(textArea, segments[i], lineText);
if (isAtEnd)
break;
lastDelimiterEnd = ds.EndOffset;
ds = NewLineFinder.NextNewLine(newText, lastDelimiterEnd);
}
if (commonLength >= 0) {
TextLocation newStart = new TextLocation(start.Line, editColumn + commonLength);
TextLocation newEnd = new TextLocation(start.Line + i, editColumn + commonLength);
textArea.Selection = new RectangleSelection(document, document.GetOffset(newStart), document.GetOffset(newEnd));
} else {
textArea.Selection = Selection.Empty;
}
}
}
}
static void ReplaceSingleLineText(TextArea textArea, ISegment lineSegment, string newText)
{
if (lineSegment.Length == 0) {
if (newText.Length > 0 && textArea.ReadOnlySectionProvider.CanInsert(lineSegment.Offset)) {
textArea.Document.Insert(lineSegment.Offset, newText);
}
} else {
ISegment[] segmentsToDelete = textArea.GetDeletableSegments(lineSegment);
for (int i = segmentsToDelete.Length - 1; i >= 0; i--) {
if (i == segmentsToDelete.Length - 1) {
textArea.Document.Replace(segmentsToDelete[i], newText);
} else {
textArea.Document.Remove(segmentsToDelete[i]);
}
}
}
}
/// <summary>
/// Performs a rectangular paste operation.
/// </summary>
public static bool PerformRectangularPaste(TextArea textArea, int startOffset, string text, bool selectInsertedText)
{
if (textArea == null)
throw new ArgumentNullException("textArea");
if (text == null)
throw new ArgumentNullException("text");
int newLineCount = text.Count(c => c == '\n');
TextLocation startLocation = textArea.Document.GetLocation(startOffset);
TextLocation endLocation = new TextLocation(startLocation.Line + newLineCount, startLocation.Column);
if (endLocation.Line <= textArea.Document.LineCount) {
int endOffset = textArea.Document.GetOffset(endLocation);
if (textArea.Document.GetLocation(endOffset) == endLocation) {
RectangleSelection rsel = new RectangleSelection(textArea.Document, startOffset, endOffset);
rsel.ReplaceSelectionWithText(textArea, text);
if (selectInsertedText && textArea.Selection is RectangleSelection) {
RectangleSelection sel = (RectangleSelection)textArea.Selection;
textArea.Selection = new RectangleSelection(textArea.Document, startOffset, sel.EndOffset);
}
return true;
}
}
return false;
}
/// <summary>
/// Gets the name of the entry in the DataObject that signals rectangle selections.
/// </summary>
public const string RectangularSelectionDataType = "AvalonEditRectangularSelection";
/// <inheritdoc/>
public override System.Windows.DataObject CreateDataObject(TextArea textArea)
{
var data = base.CreateDataObject(textArea);
MemoryStream isRectangle = new MemoryStream(1);
isRectangle.WriteByte(1);
data.SetData(RectangularSelectionDataType, isRectangle, false);
return data;
}
/// <inheritdoc/>
public override string ToString()
{
// It's possible that ToString() gets called on old (invalid) selections, e.g. for "change from... to..." debug message
// make sure we don't crash even when the desired locations don't exist anymore.
if (StartOffset < document.TextLength && EndOffset < document.TextLength)
return "[RectangleSelection " + document.GetLocation(StartOffset) + " to " + document.GetLocation(EndOffset) + "]";
else
return "[RectangleSelection " + StartOffset + " to " + EndOffset + "]";
}
}
}

181
ICSharpCode.AvalonEdit/Editing/Selection.cs

@ -0,0 +1,181 @@ @@ -0,0 +1,181 @@
// 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.Text;
using System.Windows;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Highlighting;
namespace ICSharpCode.AvalonEdit.Editing
{
/// <summary>
/// Base class for selections.
/// </summary>
public abstract class Selection
{
/// <summary>
/// Gets the empty selection.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification="Empty selection is immutable")]
public static readonly Selection Empty = new SimpleSelection(-1, -1);
/// <summary>
/// Gets the selected text segments.
/// </summary>
public abstract IEnumerable<ISegment> Segments { get; }
/// <summary>
/// Gets the smallest segment that contains all segments in this selection.
/// May return null if the selection is empty.
/// </summary>
public abstract ISegment SurroundingSegment { get; }
/// <summary>
/// Replaces the selection with the specified text.
/// </summary>
public abstract void ReplaceSelectionWithText(TextArea textArea, string newText);
/// <summary>
/// Updates the selection when the document changes.
/// </summary>
public abstract Selection UpdateOnDocumentChange(DocumentChangeEventArgs e);
/// <summary>
/// Gets whether the selection is empty.
/// </summary>
public virtual bool IsEmpty {
get { return Length == 0; }
}
/// <summary>
/// Gets the selection length.
/// </summary>
public abstract int Length { get; }
/// <summary>
/// Returns a new selection with the changed end point.
/// </summary>
/// <exception cref="NotSupportedException">Cannot set endpoint for empty selection</exception>
public abstract Selection SetEndpoint(int newEndOffset);
/// <summary>
/// If this selection is empty, starts a new selection from <paramref name="startOffset"/> to
/// <paramref name="newEndOffset"/>, otherwise, changes the endpoint of this selection.
/// </summary>
public virtual Selection StartSelectionOrSetEndpoint(int startOffset, int newEndOffset)
{
if (IsEmpty)
return new SimpleSelection(startOffset, newEndOffset);
else
return SetEndpoint(newEndOffset);
}
/// <summary>
/// Gets whether the selection is multi-line.
/// </summary>
public virtual bool IsMultiline(TextDocument document)
{
if (document == null)
throw new ArgumentNullException("document");
ISegment surroundingSegment = this.SurroundingSegment;
if (surroundingSegment == null)
return false;
int start = surroundingSegment.Offset;
int end = start + surroundingSegment.Length;
return document.GetLineByOffset(start) != document.GetLineByOffset(end);
}
/// <summary>
/// Gets the selected text.
/// </summary>
public virtual string GetText(TextDocument document)
{
if (document == null)
throw new ArgumentNullException("document");
StringBuilder b = null;
string text = null;
foreach (ISegment s in Segments) {
if (text != null) {
if (b == null)
b = new StringBuilder(text);
else
b.Append(text);
}
text = document.GetText(s);
}
if (b != null) {
if (text != null) b.Append(text);
return b.ToString();
} else {
return text ?? string.Empty;
}
}
/// <summary>
/// Creates a HTML fragment for the selected text.
/// </summary>
public string CreateHtmlFragment(TextArea textArea, HtmlOptions options)
{
if (textArea == null)
throw new ArgumentNullException("textArea");
if (options == null)
throw new ArgumentNullException("options");
IHighlighter highlighter = textArea.GetService(typeof(IHighlighter)) as IHighlighter;
StringBuilder html = new StringBuilder();
bool first = true;
foreach (ISegment selectedSegment in this.Segments) {
if (first)
first = false;
else
html.AppendLine("<br>");
html.Append(HtmlClipboard.CreateHtmlFragment(textArea.Document, highlighter, selectedSegment, options));
}
return html.ToString();
}
/// <inheritdoc/>
public abstract override bool Equals(object obj);
/// <inheritdoc/>
public abstract override int GetHashCode();
/// <summary>
/// Gets whether the specified offset is included in the selection.
/// </summary>
/// <returns>True, if the selection contains the offset (selection borders inclusive);
/// otherwise, false.</returns>
public virtual bool Contains(int offset)
{
if (this.IsEmpty)
return false;
if (this.SurroundingSegment.Contains(offset)) {
foreach (ISegment s in this.Segments) {
if (s.Contains(offset)) {
return true;
}
}
}
return false;
}
/// <summary>
/// Creates a data object containing the selection's text.
/// </summary>
public virtual DataObject CreateDataObject(TextArea textArea)
{
string text = GetText(textArea.Document);
// Ensure we use the appropriate newline sequence for the OS
DataObject data = new DataObject(TextUtilities.NormalizeNewLines(text, Environment.NewLine));
// we cannot use DataObject.SetText - then we cannot drag to SciTe
// (but dragging to Word works in both cases)
// Also copy text in HTML format to clipboard - good for pasting text into Word
// or to the SharpDevelop forums.
HtmlClipboard.SetHtml(data, CreateHtmlFragment(textArea, new HtmlOptions(textArea.Options)));
return data;
}
}
}

48
ICSharpCode.AvalonEdit/Editing/SelectionColorizer.cs

@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
// 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.Windows;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Rendering;
namespace ICSharpCode.AvalonEdit.Editing
{
sealed class SelectionColorizer : ColorizingTransformer
{
TextArea textArea;
public SelectionColorizer(TextArea textArea)
{
if (textArea == null)
throw new ArgumentNullException("textArea");
this.textArea = textArea;
}
protected override void Colorize(ITextRunConstructionContext context)
{
// if SelectionForeground is null, keep the existing foreground color
if (textArea.SelectionForeground == null)
return;
int lineStartOffset = context.VisualLine.FirstDocumentLine.Offset;
int lineEndOffset = context.VisualLine.LastDocumentLine.Offset + context.VisualLine.LastDocumentLine.TotalLength;
foreach (ISegment segment in textArea.Selection.Segments) {
int segmentStart = segment.Offset;
int segmentEnd = segment.Offset + segment.Length;
if (segmentEnd <= lineStartOffset)
continue;
if (segmentStart >= lineEndOffset)
continue;
int startColumn = context.VisualLine.GetVisualColumn(Math.Max(0, segmentStart - lineStartOffset));
int endColumn = context.VisualLine.GetVisualColumn(segmentEnd - lineStartOffset);
ChangeVisualElements(
startColumn, endColumn,
element => {
element.TextRunProperties.SetForegroundBrush(textArea.SelectionForeground);
});
}
}
}
}

52
ICSharpCode.AvalonEdit/Editing/SelectionLayer.cs

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
// 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.Windows;
using System.Windows.Media;
using ICSharpCode.AvalonEdit.Rendering;
namespace ICSharpCode.AvalonEdit.Editing
{
sealed class SelectionLayer : Layer, IWeakEventListener
{
readonly TextArea textArea;
public SelectionLayer(TextArea textArea) : base(textArea.TextView, KnownLayer.Selection)
{
this.IsHitTestVisible = false;
this.textArea = textArea;
TextViewWeakEventManager.VisualLinesChanged.AddListener(textView, this);
TextViewWeakEventManager.ScrollOffsetChanged.AddListener(textView, this);
}
bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
if (managerType == typeof(TextViewWeakEventManager.VisualLinesChanged)
|| managerType == typeof(TextViewWeakEventManager.ScrollOffsetChanged))
{
InvalidateVisual();
return true;
}
return false;
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
BackgroundGeometryBuilder geoBuilder = new BackgroundGeometryBuilder();
geoBuilder.AlignToMiddleOfPixels = true;
geoBuilder.CornerRadius = textArea.SelectionCornerRadius;
foreach (var segment in textArea.Selection.Segments) {
geoBuilder.AddSegment(textView, segment);
}
Geometry geometry = geoBuilder.CreateGeometry();
if (geometry != null) {
drawingContext.DrawGeometry(textArea.SelectionBrush, textArea.SelectionBorder, geometry);
}
}
}
}

580
ICSharpCode.AvalonEdit/Editing/SelectionMouseHandler.cs

@ -0,0 +1,580 @@ @@ -0,0 +1,580 @@
// 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.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Threading;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Rendering;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Editing
{
/// <summary>
/// Handles selection of text using the mouse.
/// </summary>
sealed class SelectionMouseHandler : ITextAreaInputHandler
{
#region enum SelectionMode
enum SelectionMode
{
/// <summary>
/// no selection (no mouse button down)
/// </summary>
None,
/// <summary>
/// left mouse button down on selection, might be normal click
/// or might be drag'n'drop
/// </summary>
PossibleDragStart,
/// <summary>
/// dragging text
/// </summary>
Drag,
/// <summary>
/// normal selection (click+drag)
/// </summary>
Normal,
/// <summary>
/// whole-word selection (double click+drag or ctrl+click+drag)
/// </summary>
WholeWord,
/// <summary>
/// whole-line selection (triple click+drag)
/// </summary>
WholeLine,
/// <summary>
/// rectangular selection (alt+click+drag)
/// </summary>
Rectangular
}
#endregion
// TODO: allow disabling text drag'n'drop
const bool AllowTextDragDrop = true;
readonly TextArea textArea;
SelectionMode mode;
AnchorSegment startWord;
Point possibleDragStartMousePos;
#region Constructor + Attach + Detach
public SelectionMouseHandler(TextArea textArea)
{
if (textArea == null)
throw new ArgumentNullException("textArea");
this.textArea = textArea;
}
public TextArea TextArea {
get { return textArea; }
}
public void Attach()
{
textArea.MouseLeftButtonDown += textArea_MouseLeftButtonDown;
textArea.MouseMove += textArea_MouseMove;
textArea.MouseLeftButtonUp += textArea_MouseLeftButtonUp;
textArea.QueryCursor += textArea_QueryCursor;
if (AllowTextDragDrop) {
textArea.AllowDrop = true;
textArea.GiveFeedback += textArea_GiveFeedback;
textArea.QueryContinueDrag += textArea_QueryContinueDrag;
textArea.DragEnter += textArea_DragEnter;
textArea.DragOver += textArea_DragOver;
textArea.DragLeave += textArea_DragLeave;
textArea.Drop += textArea_Drop;
}
}
public void Detach()
{
mode = SelectionMode.None;
textArea.MouseLeftButtonDown -= textArea_MouseLeftButtonDown;
textArea.MouseMove -= textArea_MouseMove;
textArea.MouseLeftButtonUp -= textArea_MouseLeftButtonUp;
textArea.QueryCursor -= textArea_QueryCursor;
if (AllowTextDragDrop) {
textArea.AllowDrop = false;
textArea.GiveFeedback -= textArea_GiveFeedback;
textArea.QueryContinueDrag -= textArea_QueryContinueDrag;
textArea.DragEnter -= textArea_DragEnter;
textArea.DragOver -= textArea_DragOver;
textArea.DragLeave -= textArea_DragLeave;
textArea.Drop -= textArea_Drop;
}
}
#endregion
#region Dropping text
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
void textArea_DragEnter(object sender, DragEventArgs e)
{
try {
e.Effects = GetEffect(e);
textArea.Caret.Show();
} catch (Exception ex) {
OnDragException(ex);
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
void textArea_DragOver(object sender, DragEventArgs e)
{
try {
e.Effects = GetEffect(e);
} catch (Exception ex) {
OnDragException(ex);
}
}
DragDropEffects GetEffect(DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.UnicodeText, true)) {
e.Handled = true;
int visualColumn;
int offset = GetOffsetFromMousePosition(e.GetPosition(textArea.TextView), out visualColumn);
if (offset >= 0) {
textArea.Caret.Position = new TextViewPosition(textArea.Document.GetLocation(offset), visualColumn);
textArea.Caret.DesiredXPos = double.NaN;
if (textArea.ReadOnlySectionProvider.CanInsert(offset)) {
if ((e.AllowedEffects & DragDropEffects.Move) == DragDropEffects.Move
&& (e.KeyStates & DragDropKeyStates.ControlKey) != DragDropKeyStates.ControlKey)
{
return DragDropEffects.Move;
} else {
return e.AllowedEffects & DragDropEffects.Copy;
}
}
}
}
return DragDropEffects.None;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
void textArea_DragLeave(object sender, DragEventArgs e)
{
try {
e.Handled = true;
if (!textArea.IsKeyboardFocusWithin)
textArea.Caret.Hide();
} catch (Exception ex) {
OnDragException(ex);
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
void textArea_Drop(object sender, DragEventArgs e)
{
try {
DragDropEffects effect = GetEffect(e);
e.Effects = effect;
if (effect != DragDropEffects.None) {
string text = e.Data.GetData(DataFormats.UnicodeText, true) as string;
if (text != null) {
int start = textArea.Caret.Offset;
if (mode == SelectionMode.Drag && textArea.Selection.Contains(start)) {
Debug.WriteLine("Drop: did not drop: drop target is inside selection");
e.Effects = DragDropEffects.None;
} else {
Debug.WriteLine("Drop: insert at " + start);
bool rectangular = e.Data.GetDataPresent(RectangleSelection.RectangularSelectionDataType);
string newLine = TextUtilities.GetNewLineFromDocument(textArea.Document, textArea.Caret.Line);
text = TextUtilities.NormalizeNewLines(text, newLine);
// Mark the undo group with the currentDragDescriptor, if the drag
// is originating from the same control. This allows combining
// the undo groups when text is moved.
textArea.Document.UndoStack.StartUndoGroup(this.currentDragDescriptor);
try {
if (rectangular && RectangleSelection.PerformRectangularPaste(textArea, start, text, true)) {
} else {
textArea.Document.Insert(start, text);
textArea.Selection = new SimpleSelection(start, start + text.Length);
}
} finally {
textArea.Document.UndoStack.EndUndoGroup();
}
}
e.Handled = true;
}
}
} catch (Exception ex) {
OnDragException(ex);
}
}
void OnDragException(Exception ex)
{
// WPF swallows exceptions during drag'n'drop or reports them incorrectly, so
// we re-throw them later to allow the application's unhandled exception handler
// to catch them
textArea.Dispatcher.BeginInvoke(
DispatcherPriority.Normal,
new Action(delegate {
throw new DragDropException("Exception during drag'n'drop", ex);
}));
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
void textArea_GiveFeedback(object sender, GiveFeedbackEventArgs e)
{
try {
e.UseDefaultCursors = true;
e.Handled = true;
} catch (Exception ex) {
OnDragException(ex);
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
void textArea_QueryContinueDrag(object sender, QueryContinueDragEventArgs e)
{
try {
if (e.EscapePressed) {
e.Action = DragAction.Cancel;
} else if ((e.KeyStates & DragDropKeyStates.LeftMouseButton) != DragDropKeyStates.LeftMouseButton) {
e.Action = DragAction.Drop;
} else {
e.Action = DragAction.Continue;
}
e.Handled = true;
} catch (Exception ex) {
OnDragException(ex);
}
}
#endregion
#region Start Drag
object currentDragDescriptor;
void StartDrag()
{
// prevent nested StartDrag calls
mode = SelectionMode.Drag;
// mouse capture and Drag'n'Drop doesn't mix
textArea.ReleaseMouseCapture();
DataObject dataObject = textArea.Selection.CreateDataObject(textArea);
DragDropEffects allowedEffects = DragDropEffects.All;
var deleteOnMove = textArea.Selection.Segments.Select(s => new AnchorSegment(textArea.Document, s)).ToList();
foreach (ISegment s in deleteOnMove) {
ISegment[] result = textArea.GetDeletableSegments(s);
if (result.Length != 1 || result[0].Offset != s.Offset || result[0].EndOffset != s.EndOffset) {
allowedEffects &= ~DragDropEffects.Move;
}
}
object dragDescriptor = new object();
this.currentDragDescriptor = dragDescriptor;
DragDropEffects resultEffect;
using (textArea.AllowCaretOutsideSelection()) {
var oldCaretPosition = textArea.Caret.Position;
try {
Debug.WriteLine("DoDragDrop with allowedEffects=" + allowedEffects);
resultEffect = DragDrop.DoDragDrop(textArea, dataObject, allowedEffects);
Debug.WriteLine("DoDragDrop done, resultEffect=" + resultEffect);
} catch (COMException ex) {
// ignore COM errors - don't crash on badly implemented drop targets
Debug.WriteLine("DoDragDrop failed: " + ex.ToString());
return;
}
if (resultEffect == DragDropEffects.None) {
// reset caret if drag was aborted
textArea.Caret.Position = oldCaretPosition;
}
}
this.currentDragDescriptor = null;
if (deleteOnMove != null && resultEffect == DragDropEffects.Move && (allowedEffects & DragDropEffects.Move) == DragDropEffects.Move) {
bool draggedInsideSingleDocument = (dragDescriptor == textArea.Document.UndoStack.LastGroupDescriptor);
if (draggedInsideSingleDocument)
textArea.Document.UndoStack.StartContinuedUndoGroup(null);
textArea.Document.BeginUpdate();
try {
foreach (ISegment s in deleteOnMove) {
textArea.Document.Remove(s.Offset, s.Length);
}
} finally {
textArea.Document.EndUpdate();
if (draggedInsideSingleDocument)
textArea.Document.UndoStack.EndUndoGroup();
}
}
}
#endregion
#region QueryCursor
// provide the IBeam Cursor for the text area
void textArea_QueryCursor(object sender, QueryCursorEventArgs e)
{
if (!e.Handled) {
if (mode != SelectionMode.None || !AllowTextDragDrop) {
e.Cursor = Cursors.IBeam;
e.Handled = true;
} else if (textArea.TextView.VisualLinesValid) {
// Only query the cursor if the visual lines are valid.
// If they are invalid, the cursor will get re-queried when the visual lines
// get refreshed.
Point p = e.GetPosition(textArea.TextView);
if (p.X >= 0 && p.Y >= 0 && p.X <= textArea.TextView.ActualWidth && p.Y <= textArea.TextView.ActualHeight) {
int visualColumn;
int offset = GetOffsetFromMousePosition(e, out visualColumn);
if (textArea.Selection.Contains(offset))
e.Cursor = Cursors.Arrow;
else
e.Cursor = Cursors.IBeam;
e.Handled = true;
}
}
}
}
#endregion
#region LeftButtonDown
void textArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
mode = SelectionMode.None;
if (!e.Handled && e.ChangedButton == MouseButton.Left) {
ModifierKeys modifiers = Keyboard.Modifiers;
bool shift = (modifiers & ModifierKeys.Shift) == ModifierKeys.Shift;
if (AllowTextDragDrop && e.ClickCount == 1 && !shift) {
int visualColumn;
int offset = GetOffsetFromMousePosition(e, out visualColumn);
if (textArea.Selection.Contains(offset)) {
if (textArea.CaptureMouse()) {
mode = SelectionMode.PossibleDragStart;
possibleDragStartMousePos = e.GetPosition(textArea);
}
e.Handled = true;
return;
}
}
int oldOffset = textArea.Caret.Offset;
SetCaretOffsetToMousePosition(e);
if (!shift) {
textArea.Selection = Selection.Empty;
}
if (textArea.CaptureMouse()) {
if ((modifiers & ModifierKeys.Alt) == ModifierKeys.Alt) {
mode = SelectionMode.Rectangular;
if (shift && textArea.Selection is RectangleSelection) {
textArea.Selection = textArea.Selection.StartSelectionOrSetEndpoint(oldOffset, textArea.Caret.Offset);
}
} else if (e.ClickCount == 1 && ((modifiers & ModifierKeys.Control) == 0)) {
mode = SelectionMode.Normal;
if (shift && !(textArea.Selection is RectangleSelection)) {
textArea.Selection = textArea.Selection.StartSelectionOrSetEndpoint(oldOffset, textArea.Caret.Offset);
}
} else {
SimpleSegment startWord;
if (e.ClickCount == 3) {
mode = SelectionMode.WholeLine;
startWord = GetLineAtMousePosition(e);
} else {
mode = SelectionMode.WholeWord;
startWord = GetWordAtMousePosition(e);
}
if (startWord == SimpleSegment.Invalid) {
mode = SelectionMode.None;
textArea.ReleaseMouseCapture();
return;
}
if (shift && !textArea.Selection.IsEmpty) {
if (startWord.Offset < textArea.Selection.SurroundingSegment.Offset) {
textArea.Selection = textArea.Selection.SetEndpoint(startWord.Offset);
} else if (startWord.EndOffset > textArea.Selection.SurroundingSegment.EndOffset) {
textArea.Selection = textArea.Selection.SetEndpoint(startWord.EndOffset);
}
this.startWord = new AnchorSegment(textArea.Document, textArea.Selection.SurroundingSegment);
} else {
textArea.Selection = new SimpleSelection(startWord.Offset, startWord.EndOffset);
this.startWord = new AnchorSegment(textArea.Document, startWord.Offset, startWord.Length);
}
}
}
}
e.Handled = true;
}
#endregion
#region Mouse Position <-> Text coordinates
SimpleSegment GetWordAtMousePosition(MouseEventArgs e)
{
TextView textView = textArea.TextView;
if (textView == null) return SimpleSegment.Invalid;
Point pos = e.GetPosition(textView);
if (pos.Y < 0)
pos.Y = 0;
if (pos.Y > textView.ActualHeight)
pos.Y = textView.ActualHeight;
pos += textView.ScrollOffset;
VisualLine line = textView.GetVisualLineFromVisualTop(pos.Y);
if (line != null) {
int visualColumn = line.GetVisualColumn(pos);
int wordStartVC = line.GetNextCaretPosition(visualColumn + 1, LogicalDirection.Backward, CaretPositioningMode.WordStartOrSymbol);
if (wordStartVC == -1)
wordStartVC = 0;
int wordEndVC = line.GetNextCaretPosition(wordStartVC, LogicalDirection.Forward, CaretPositioningMode.WordBorderOrSymbol);
if (wordEndVC == -1)
wordEndVC = line.VisualLength;
int relOffset = line.FirstDocumentLine.Offset;
int wordStartOffset = line.GetRelativeOffset(wordStartVC) + relOffset;
int wordEndOffset = line.GetRelativeOffset(wordEndVC) + relOffset;
return new SimpleSegment(wordStartOffset, wordEndOffset - wordStartOffset);
} else {
return SimpleSegment.Invalid;
}
}
SimpleSegment GetLineAtMousePosition(MouseEventArgs e)
{
TextView textView = textArea.TextView;
if (textView == null) return SimpleSegment.Invalid;
Point pos = e.GetPosition(textView);
if (pos.Y < 0)
pos.Y = 0;
if (pos.Y > textView.ActualHeight)
pos.Y = textView.ActualHeight;
pos += textView.ScrollOffset;
VisualLine line = textView.GetVisualLineFromVisualTop(pos.Y);
if (line != null) {
return new SimpleSegment(line.StartOffset, line.LastDocumentLine.EndOffset - line.StartOffset);
} else {
return SimpleSegment.Invalid;
}
}
int GetOffsetFromMousePosition(MouseEventArgs e, out int visualColumn)
{
return GetOffsetFromMousePosition(e.GetPosition(textArea.TextView), out visualColumn);
}
int GetOffsetFromMousePosition(Point positionRelativeToTextView, out int visualColumn)
{
visualColumn = 0;
TextView textView = textArea.TextView;
Point pos = positionRelativeToTextView;
if (pos.Y < 0)
pos.Y = 0;
if (pos.Y > textView.ActualHeight)
pos.Y = textView.ActualHeight;
pos += textView.ScrollOffset;
if (pos.Y > textView.DocumentHeight)
pos.Y = textView.DocumentHeight - ExtensionMethods.Epsilon;
VisualLine line = textView.GetVisualLineFromVisualTop(pos.Y);
if (line != null) {
visualColumn = line.GetVisualColumn(pos);
return line.GetRelativeOffset(visualColumn) + line.FirstDocumentLine.Offset;
}
return -1;
}
#endregion
#region MouseMove
void textArea_MouseMove(object sender, MouseEventArgs e)
{
if (e.Handled)
return;
if (mode == SelectionMode.Normal || mode == SelectionMode.WholeWord || mode == SelectionMode.WholeLine || mode == SelectionMode.Rectangular) {
e.Handled = true;
if (textArea.TextView.VisualLinesValid) {
// If the visual lines are not valid, don't extend the selection.
// Extending the selection forces a VisualLine refresh, and it is sufficient
// to do that on MouseUp, we don't have to do it every MouseMove.
ExtendSelectionToMouse(e);
}
} else if (mode == SelectionMode.PossibleDragStart) {
e.Handled = true;
Vector mouseMovement = e.GetPosition(textArea) - possibleDragStartMousePos;
if (Math.Abs(mouseMovement.X) > SystemParameters.MinimumHorizontalDragDistance
|| Math.Abs(mouseMovement.Y) > SystemParameters.MinimumVerticalDragDistance)
{
StartDrag();
}
}
}
#endregion
#region ExtendSelection
void SetCaretOffsetToMousePosition(MouseEventArgs e)
{
SetCaretOffsetToMousePosition(e, null);
}
void SetCaretOffsetToMousePosition(MouseEventArgs e, ISegment allowedSegment)
{
int visualColumn;
int offset = GetOffsetFromMousePosition(e, out visualColumn);
if (allowedSegment != null) {
offset = offset.CoerceValue(allowedSegment.Offset, allowedSegment.EndOffset);
}
if (offset >= 0) {
textArea.Caret.Position = new TextViewPosition(textArea.Document.GetLocation(offset), visualColumn);
textArea.Caret.DesiredXPos = double.NaN;
}
}
void ExtendSelectionToMouse(MouseEventArgs e)
{
int oldOffset = textArea.Caret.Offset;
if (mode == SelectionMode.Normal || mode == SelectionMode.Rectangular) {
SetCaretOffsetToMousePosition(e);
if (mode == SelectionMode.Normal && textArea.Selection is RectangleSelection)
textArea.Selection = new SimpleSelection(oldOffset, textArea.Caret.Offset);
else if (mode == SelectionMode.Rectangular && !(textArea.Selection is RectangleSelection))
textArea.Selection = new RectangleSelection(textArea.Document, oldOffset, textArea.Caret.Offset);
else
textArea.Selection = textArea.Selection.StartSelectionOrSetEndpoint(oldOffset, textArea.Caret.Offset);
} else if (mode == SelectionMode.WholeWord || mode == SelectionMode.WholeLine) {
var newWord = (mode == SelectionMode.WholeLine) ? GetLineAtMousePosition(e) : GetWordAtMousePosition(e);
if (newWord != SimpleSegment.Invalid) {
textArea.Selection = new SimpleSelection(Math.Min(newWord.Offset, startWord.Offset),
Math.Max(newWord.EndOffset, startWord.EndOffset));
// Set caret offset, but limit the caret to stay inside the selection.
// in whole-word selection, it's otherwise possible that we get the caret outside the
// selection - but the TextArea doesn't like that and will reset the selection, causing
// flickering.
SetCaretOffsetToMousePosition(e, textArea.Selection.SurroundingSegment);
}
}
textArea.Caret.BringCaretToView(5.0);
}
#endregion
#region MouseLeftButtonUp
void textArea_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (mode == SelectionMode.None || e.Handled)
return;
e.Handled = true;
if (mode == SelectionMode.PossibleDragStart) {
// -> this was not a drag start (mouse didn't move after mousedown)
SetCaretOffsetToMousePosition(e);
textArea.Selection = Selection.Empty;
} else if (mode == SelectionMode.Normal || mode == SelectionMode.WholeWord || mode == SelectionMode.WholeLine || mode == SelectionMode.Rectangular) {
ExtendSelectionToMouse(e);
}
mode = SelectionMode.None;
textArea.ReleaseMouseCapture();
}
#endregion
}
}

170
ICSharpCode.AvalonEdit/Editing/SimpleSelection.cs

@ -0,0 +1,170 @@ @@ -0,0 +1,170 @@
// 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.Linq;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Editing
{
/// <summary>
/// A simple selection.
/// </summary>
public sealed class SimpleSelection : Selection, ISegment
{
readonly int startOffset, endOffset;
/// <summary>
/// Creates a new SimpleSelection instance.
/// </summary>
public SimpleSelection(int startOffset, int endOffset)
{
this.startOffset = startOffset;
this.endOffset = endOffset;
}
/// <summary>
/// Creates a new SimpleSelection instance.
/// </summary>
public SimpleSelection(ISegment segment)
{
if (segment == null)
throw new ArgumentNullException("segment");
this.startOffset = segment.Offset;
this.endOffset = startOffset + segment.Length;
}
/// <inheritdoc/>
public override IEnumerable<ISegment> Segments {
get {
if (!IsEmpty) {
return ExtensionMethods.Sequence<ISegment>(this);
} else {
return Empty<ISegment>.Array;
}
}
}
/// <inheritdoc/>
public override ISegment SurroundingSegment {
get {
if (IsEmpty)
return null;
else
return this;
}
}
/// <inheritdoc/>
public override void ReplaceSelectionWithText(TextArea textArea, string newText)
{
if (textArea == null)
throw new ArgumentNullException("textArea");
if (newText == null)
throw new ArgumentNullException("newText");
using (textArea.Document.RunUpdate()) {
if (IsEmpty) {
if (newText.Length > 0) {
if (textArea.ReadOnlySectionProvider.CanInsert(textArea.Caret.Offset)) {
textArea.Document.Insert(textArea.Caret.Offset, newText);
}
}
} else {
ISegment[] segmentsToDelete = textArea.GetDeletableSegments(this);
for (int i = segmentsToDelete.Length - 1; i >= 0; i--) {
if (i == segmentsToDelete.Length - 1) {
textArea.Caret.Offset = segmentsToDelete[i].EndOffset;
textArea.Document.Replace(segmentsToDelete[i], newText);
} else {
textArea.Document.Remove(segmentsToDelete[i]);
}
}
if (segmentsToDelete.Length != 0) {
textArea.Selection = Selection.Empty;
}
}
}
}
/// <summary>
/// Gets the start offset.
/// </summary>
public int StartOffset {
get { return startOffset; }
}
/// <summary>
/// Gets the end offset.
/// </summary>
public int EndOffset {
get { return endOffset; }
}
/// <inheritdoc/>
public override Selection UpdateOnDocumentChange(DocumentChangeEventArgs e)
{
if (e == null)
throw new ArgumentNullException("e");
return new SimpleSelection(
e.GetNewOffset(startOffset, AnchorMovementType.Default),
e.GetNewOffset(endOffset, AnchorMovementType.Default)
);
}
/// <inheritdoc/>
public override bool IsEmpty {
get { return startOffset == endOffset; }
}
// For segments, Offset must be less than or equal to EndOffset;
// so we must use Min/Max.
int ISegment.Offset {
get { return Math.Min(startOffset, endOffset); }
}
int ISegment.EndOffset {
get { return Math.Max(startOffset, endOffset); }
}
/// <inheritdoc/>
public override int Length {
get {
return Math.Abs(endOffset - startOffset);
}
}
/// <inheritdoc/>
public override Selection SetEndpoint(int newEndOffset)
{
if (IsEmpty)
throw new NotSupportedException();
else
return new SimpleSelection(startOffset, newEndOffset);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return startOffset ^ endOffset;
}
/// <inheritdoc/>
public override bool Equals(object obj)
{
SimpleSelection other = obj as SimpleSelection;
if (other == null) return false;
if (IsEmpty && other.IsEmpty)
return true;
return this.startOffset == other.startOffset && this.endOffset == other.endOffset;
}
/// <inheritdoc/>
public override string ToString()
{
return "[SimpleSelection Start=" + startOffset + " End=" + endOffset + "]";
}
}
}

1025
ICSharpCode.AvalonEdit/Editing/TextArea.cs

File diff suppressed because it is too large Load Diff

109
ICSharpCode.AvalonEdit/Editing/TextAreaDefaultInputHandlers.cs

@ -0,0 +1,109 @@ @@ -0,0 +1,109 @@
// 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.Windows;
using System.Windows.Input;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Editing
{
/// <summary>
/// Contains the predefined input handlers.
/// </summary>
public class TextAreaDefaultInputHandler : TextAreaInputHandler
{
/// <summary>
/// Gets the caret navigation input handler.
/// </summary>
public TextAreaInputHandler CaretNavigation { get; private set; }
/// <summary>
/// Gets the editing input handler.
/// </summary>
public TextAreaInputHandler Editing { get; private set; }
/// <summary>
/// Gets the mouse selection input handler.
/// </summary>
public ITextAreaInputHandler MouseSelection { get; private set; }
/// <summary>
/// Creates a new TextAreaDefaultInputHandler instance.
/// </summary>
public TextAreaDefaultInputHandler(TextArea textArea) : base(textArea)
{
this.NestedInputHandlers.Add(CaretNavigation = CaretNavigationCommandHandler.Create(textArea));
this.NestedInputHandlers.Add(Editing = EditingCommandHandler.Create(textArea));
this.NestedInputHandlers.Add(MouseSelection = new SelectionMouseHandler(textArea));
this.CommandBindings.Add(new CommandBinding(ApplicationCommands.Undo, ExecuteUndo, CanExecuteUndo));
this.CommandBindings.Add(new CommandBinding(ApplicationCommands.Redo, ExecuteRedo, CanExecuteRedo));
}
internal static KeyBinding CreateFrozenKeyBinding(ICommand command, ModifierKeys modifiers, Key key)
{
KeyBinding kb = new KeyBinding(command, key, modifiers);
// Mark KeyBindings as frozen because they're shared between multiple editor instances.
// KeyBinding derives from Freezable only in .NET 4, so we have to use this little trick:
Freezable f = ((object)kb) as Freezable;
if (f != null)
f.Freeze();
return kb;
}
#region Undo / Redo
UndoStack GetUndoStack()
{
TextDocument document = this.TextArea.Document;
if (document != null)
return document.UndoStack;
else
return null;
}
void ExecuteUndo(object sender, ExecutedRoutedEventArgs e)
{
var undoStack = GetUndoStack();
if (undoStack != null) {
if (undoStack.CanUndo) {
undoStack.Undo();
this.TextArea.Caret.BringCaretToView();
}
e.Handled = true;
}
}
void CanExecuteUndo(object sender, CanExecuteRoutedEventArgs e)
{
var undoStack = GetUndoStack();
if (undoStack != null) {
e.Handled = true;
e.CanExecute = undoStack.CanUndo;
}
}
void ExecuteRedo(object sender, ExecutedRoutedEventArgs e)
{
var undoStack = GetUndoStack();
if (undoStack != null) {
if (undoStack.CanRedo) {
undoStack.Redo();
this.TextArea.Caret.BringCaretToView();
}
e.Handled = true;
}
}
void CanExecuteRedo(object sender, CanExecuteRoutedEventArgs e)
{
var undoStack = GetUndoStack();
if (undoStack != null) {
e.Handled = true;
e.CanExecute = undoStack.CanRedo;
}
}
#endregion
}
}

242
ICSharpCode.AvalonEdit/Editing/TextAreaInputHandler.cs

@ -0,0 +1,242 @@ @@ -0,0 +1,242 @@
// 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 ICSharpCode.AvalonEdit.Utils;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows.Input;
namespace ICSharpCode.AvalonEdit.Editing
{
/// <summary>
/// A set of input bindings and event handlers for the text area.
/// </summary>
/// <remarks>
/// <para>
/// There is one active input handler per text area (<see cref="Editing.TextArea.ActiveInputHandler"/>), plus
/// a number of active stacked input handlers.
/// </para>
/// <para>
/// The text area also stores a reference to a default input handler, but that is not necessarily active.
/// </para>
/// <para>
/// Stacked input handlers work in addition to the set of currently active handlers (without detaching them).
/// They are detached in the reverse order of being attached.
/// </para>
/// </remarks>
public interface ITextAreaInputHandler
{
/// <summary>
/// Gets the text area that the input handler belongs to.
/// </summary>
TextArea TextArea {
get;
}
/// <summary>
/// Attaches an input handler to the text area.
/// </summary>
void Attach();
/// <summary>
/// Detaches the input handler from the text area.
/// </summary>
void Detach();
}
/// <summary>
/// Stacked input handler.
/// Uses OnEvent-methods instead of registering event handlers to ensure that the events are handled in the correct order.
/// </summary>
public abstract class TextAreaStackedInputHandler : ITextAreaInputHandler
{
readonly TextArea textArea;
/// <inheritdoc/>
public TextArea TextArea {
get { return textArea; }
}
/// <summary>
/// Creates a new TextAreaInputHandler.
/// </summary>
protected TextAreaStackedInputHandler(TextArea textArea)
{
if (textArea == null)
throw new ArgumentNullException("textArea");
this.textArea = textArea;
}
/// <inheritdoc/>
public virtual void Attach()
{
}
/// <inheritdoc/>
public virtual void Detach()
{
}
/// <summary>
/// Called for the PreviewKeyDown event.
/// </summary>
public virtual void OnPreviewKeyDown(KeyEventArgs e)
{
}
/// <summary>
/// Called for the PreviewKeyUp event.
/// </summary>
public virtual void OnPreviewKeyUp(KeyEventArgs e)
{
}
}
/// <summary>
/// Default-implementation of <see cref="ITextAreaInputHandler"/>.
/// </summary>
/// <remarks><inheritdoc cref="ITextAreaInputHandler"/></remarks>
public class TextAreaInputHandler : ITextAreaInputHandler
{
readonly ObserveAddRemoveCollection<CommandBinding> commandBindings;
readonly ObserveAddRemoveCollection<InputBinding> inputBindings;
readonly ObserveAddRemoveCollection<ITextAreaInputHandler> nestedInputHandlers;
readonly TextArea textArea;
bool isAttached;
/// <summary>
/// Creates a new TextAreaInputHandler.
/// </summary>
public TextAreaInputHandler(TextArea textArea)
{
if (textArea == null)
throw new ArgumentNullException("textArea");
this.textArea = textArea;
commandBindings = new ObserveAddRemoveCollection<CommandBinding>(CommandBinding_Added, CommandBinding_Removed);
inputBindings = new ObserveAddRemoveCollection<InputBinding>(InputBinding_Added, InputBinding_Removed);
nestedInputHandlers = new ObserveAddRemoveCollection<ITextAreaInputHandler>(NestedInputHandler_Added, NestedInputHandler_Removed);
}
/// <inheritdoc/>
public TextArea TextArea {
get { return textArea; }
}
/// <summary>
/// Gets whether the input handler is currently attached to the text area.
/// </summary>
public bool IsAttached {
get { return isAttached; }
}
#region CommandBindings / InputBindings
/// <summary>
/// Gets the command bindings of this input handler.
/// </summary>
public ICollection<CommandBinding> CommandBindings {
get { return commandBindings; }
}
void CommandBinding_Added(CommandBinding commandBinding)
{
if (isAttached)
textArea.CommandBindings.Add(commandBinding);
}
void CommandBinding_Removed(CommandBinding commandBinding)
{
if (isAttached)
textArea.CommandBindings.Remove(commandBinding);
}
/// <summary>
/// Gets the input bindings of this input handler.
/// </summary>
public ICollection<InputBinding> InputBindings {
get { return inputBindings; }
}
void InputBinding_Added(InputBinding inputBinding)
{
if (isAttached)
textArea.InputBindings.Add(inputBinding);
}
void InputBinding_Removed(InputBinding inputBinding)
{
if (isAttached)
textArea.InputBindings.Remove(inputBinding);
}
/// <summary>
/// Adds a command and input binding.
/// </summary>
/// <param name="command">The command ID.</param>
/// <param name="modifiers">The modifiers of the keyboard shortcut.</param>
/// <param name="key">The key of the keyboard shortcut.</param>
/// <param name="handler">The event handler to run when the command is executed.</param>
public void AddBinding(ICommand command, ModifierKeys modifiers, Key key, ExecutedRoutedEventHandler handler)
{
this.CommandBindings.Add(new CommandBinding(command, handler));
this.InputBindings.Add(new KeyBinding(command, key, modifiers));
}
#endregion
#region NestedInputHandlers
/// <summary>
/// Gets the collection of nested input handlers. NestedInputHandlers are activated and deactivated
/// together with this input handler.
/// </summary>
public ICollection<ITextAreaInputHandler> NestedInputHandlers {
get { return nestedInputHandlers; }
}
void NestedInputHandler_Added(ITextAreaInputHandler handler)
{
if (handler == null)
throw new ArgumentNullException("handler");
if (handler.TextArea != textArea)
throw new ArgumentException("The nested handler must be working for the same text area!");
if (isAttached)
handler.Attach();
}
void NestedInputHandler_Removed(ITextAreaInputHandler handler)
{
if (isAttached)
handler.Detach();
}
#endregion
#region Attach/Detach
/// <inheritdoc/>
public virtual void Attach()
{
if (isAttached)
throw new InvalidOperationException("Input handler is already attached");
isAttached = true;
textArea.CommandBindings.AddRange(commandBindings);
textArea.InputBindings.AddRange(inputBindings);
foreach (ITextAreaInputHandler handler in nestedInputHandlers)
handler.Attach();
}
/// <inheritdoc/>
public virtual void Detach()
{
if (!isAttached)
throw new InvalidOperationException("Input handler is not attached");
isAttached = false;
foreach (CommandBinding b in commandBindings)
textArea.CommandBindings.Remove(b);
foreach (InputBinding b in inputBindings)
textArea.InputBindings.Remove(b);
foreach (ITextAreaInputHandler handler in nestedInputHandlers)
handler.Detach();
}
#endregion
}
}

80
ICSharpCode.AvalonEdit/Editing/TextSegmentReadOnlySectionProvider.cs

@ -0,0 +1,80 @@ @@ -0,0 +1,80 @@
// 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 ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Editing
{
/// <summary>
/// Implementation for <see cref="IReadOnlySectionProvider"/> that stores the segments
/// in a <see cref="TextSegmentCollection{T}"/>.
/// </summary>
public class TextSegmentReadOnlySectionProvider<T> : IReadOnlySectionProvider where T : TextSegment
{
readonly TextSegmentCollection<T> segments;
/// <summary>
/// Gets the collection storing the read-only segments.
/// </summary>
public TextSegmentCollection<T> Segments {
get { return segments; }
}
/// <summary>
/// Creates a new TextSegmentReadOnlySectionProvider instance for the specified document.
/// </summary>
public TextSegmentReadOnlySectionProvider(TextDocument textDocument)
{
segments = new TextSegmentCollection<T>(textDocument);
}
/// <summary>
/// Creates a new TextSegmentReadOnlySectionProvider instance using the specified TextSegmentCollection.
/// </summary>
public TextSegmentReadOnlySectionProvider(TextSegmentCollection<T> segments)
{
if (segments == null)
throw new ArgumentNullException("segments");
this.segments = segments;
}
/// <summary>
/// Gets whether insertion is possible at the specified offset.
/// </summary>
public virtual bool CanInsert(int offset)
{
foreach (TextSegment segment in segments.FindSegmentsContaining(offset)) {
if (segment.StartOffset < offset && offset < segment.EndOffset)
return false;
}
return true;
}
/// <summary>
/// Gets the deletable segments inside the given segment.
/// </summary>
public virtual IEnumerable<ISegment> GetDeletableSegments(ISegment segment)
{
if (segment == null)
throw new ArgumentNullException("segment");
int readonlyUntil = segment.Offset;
foreach (TextSegment ts in segments.FindOverlappingSegments(segment)) {
int start = ts.StartOffset;
int end = start + ts.Length;
if (start > readonlyUntil) {
yield return new SimpleSegment(readonlyUntil, start - readonlyUntil);
}
if (end > readonlyUntil) {
readonlyUntil = end;
}
}
int endOffset = segment.EndOffset;
if (readonlyUntil < endOffset) {
yield return new SimpleSegment(readonlyUntil, endOffset - readonlyUntil);
}
}
}
}

30
ICSharpCode.AvalonEdit/Folding/AbstractFoldingStrategy.cs

@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
// 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 ICSharpCode.AvalonEdit.Document;
using System.Collections.Generic;
namespace ICSharpCode.AvalonEdit.Folding
{
/// <summary>
/// Base class for folding strategies.
/// </summary>
public abstract class AbstractFoldingStrategy
{
/// <summary>
/// Create <see cref="NewFolding"/>s for the specified document and updates the folding manager with them.
/// </summary>
public void UpdateFoldings(FoldingManager manager, TextDocument document)
{
int firstErrorOffset;
IEnumerable<NewFolding> foldings = CreateNewFoldings(document, out firstErrorOffset);
manager.UpdateFoldings(foldings, firstErrorOffset);
}
/// <summary>
/// Create <see cref="NewFolding"/>s for the specified document.
/// </summary>
public abstract IEnumerable<NewFolding> CreateNewFoldings(TextDocument document, out int firstErrorOffset);
}
}

179
ICSharpCode.AvalonEdit/Folding/FoldingElementGenerator.cs

@ -0,0 +1,179 @@ @@ -0,0 +1,179 @@
// 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.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
using ICSharpCode.AvalonEdit.Rendering;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Folding
{
/// <summary>
/// A <see cref="VisualLineElementGenerator"/> that produces line elements for folded <see cref="FoldingSection"/>s.
/// </summary>
public sealed class FoldingElementGenerator : VisualLineElementGenerator, ITextViewConnect
{
readonly List<TextView> textViews = new List<TextView>();
FoldingManager foldingManager;
#region FoldingManager property / connecting with TextView
/// <summary>
/// Gets/Sets the folding manager from which the foldings should be shown.
/// </summary>
public FoldingManager FoldingManager {
get {
return foldingManager;
}
set {
if (foldingManager != value) {
if (foldingManager != null) {
foreach (TextView v in textViews)
foldingManager.RemoveFromTextView(v);
}
foldingManager = value;
if (foldingManager != null) {
foreach (TextView v in textViews)
foldingManager.AddToTextView(v);
}
}
}
}
void ITextViewConnect.AddToTextView(TextView textView)
{
textViews.Add(textView);
if (foldingManager != null)
foldingManager.AddToTextView(textView);
}
void ITextViewConnect.RemoveFromTextView(TextView textView)
{
textViews.Remove(textView);
if (foldingManager != null)
foldingManager.RemoveFromTextView(textView);
}
#endregion
/// <inheritdoc/>
public override void StartGeneration(ITextRunConstructionContext context)
{
base.StartGeneration(context);
if (foldingManager != null) {
if (!foldingManager.textViews.Contains(context.TextView))
throw new ArgumentException("Invalid TextView");
if (context.Document != foldingManager.document)
throw new ArgumentException("Invalid document");
}
}
/// <inheritdoc/>
public override int GetFirstInterestedOffset(int startOffset)
{
if (foldingManager != null)
return foldingManager.GetNextFoldedFoldingStart(startOffset);
else
return -1;
}
/// <inheritdoc/>
public override VisualLineElement ConstructElement(int offset)
{
if (foldingManager == null)
return null;
int foldedUntil = -1;
FoldingSection foldingSection = null;
foreach (FoldingSection fs in foldingManager.GetFoldingsAt(offset)) {
if (fs.IsFolded) {
if (fs.EndOffset > foldedUntil) {
foldedUntil = fs.EndOffset;
foldingSection = fs;
}
}
}
if (foldedUntil > offset && foldingSection != null) {
// Handle overlapping foldings: if there's another folded folding
// (starting within the foldingSection) that continues after the end of the folded section,
// then we'll extend our fold element to cover that overlapping folding.
bool foundOverlappingFolding;
do {
foundOverlappingFolding = false;
foreach (FoldingSection fs in FoldingManager.GetFoldingsContaining(foldedUntil)) {
if (fs.IsFolded && fs.EndOffset > foldedUntil) {
foldedUntil = fs.EndOffset;
foundOverlappingFolding = true;
}
}
} while (foundOverlappingFolding);
string title = foldingSection.Title;
if (string.IsNullOrEmpty(title))
title = "...";
var p = new VisualLineElementTextRunProperties(CurrentContext.GlobalTextRunProperties);
p.SetForegroundBrush(textBrush);
var textFormatter = TextFormatterFactory.Create(CurrentContext.TextView);
var text = FormattedTextElement.PrepareText(textFormatter, title, p);
return new FoldingLineElement(foldingSection, text, foldedUntil - offset) { textBrush = textBrush };
} else {
return null;
}
}
sealed class FoldingLineElement : FormattedTextElement
{
readonly FoldingSection fs;
internal Brush textBrush;
public FoldingLineElement(FoldingSection fs, TextLine text, int documentLength) : base(text, documentLength)
{
this.fs = fs;
}
public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context)
{
return new FoldingLineTextRun(this, this.TextRunProperties) { textBrush = textBrush };
}
protected internal override void OnMouseDown(MouseButtonEventArgs e)
{
if (e.ClickCount == 2 && e.ChangedButton == MouseButton.Left) {
fs.IsFolded = false;
e.Handled = true;
} else {
base.OnMouseDown(e);
}
}
}
sealed class FoldingLineTextRun : FormattedTextRun
{
internal Brush textBrush;
public FoldingLineTextRun(FormattedTextElement element, TextRunProperties properties)
: base(element, properties)
{
}
public override void Draw(DrawingContext drawingContext, Point origin, bool rightToLeft, bool sideways)
{
var metrics = Format(double.PositiveInfinity);
Rect r = new Rect(origin.X, origin.Y - metrics.Baseline, metrics.Width, metrics.Height);
drawingContext.DrawRectangle(null, new Pen(textBrush, 1), r);
base.Draw(drawingContext, origin, rightToLeft, sideways);
}
}
public static readonly Brush DefaultTextBrush = Brushes.Gray;
static Brush textBrush = DefaultTextBrush;
public static Brush TextBrush {
get { return textBrush; }
set { textBrush = value; }
}
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save