Browse Source

Add AvalonEdit.

git-svn-id: svn://svn.sharpdevelop.net/sharpdevelop/trunk@3635 1ccf3a8d-04fe-1044-b7c0-cef0b8235c61
shortcuts
Daniel Grunwald 17 years ago
parent
commit
fd48c4b958
  1. 6
      AddIns/ICSharpCode.SharpDevelop.addin
  2. 24
      src/AddIns/DisplayBindings/AvalonEdit.AddIn/AvalonEdit.AddIn.addin
  3. 86
      src/AddIns/DisplayBindings/AvalonEdit.AddIn/AvalonEdit.AddIn.csproj
  4. 31
      src/AddIns/DisplayBindings/AvalonEdit.AddIn/Configuration/AssemblyInfo.cs
  5. 26
      src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/AvalonEditDisplayBinding.cs
  6. 50
      src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/AvalonEditViewContent.cs
  7. 158
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Document/CollapsingTests.cs
  8. 80
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Document/HeightTests.cs
  9. 490
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Document/LineManagerTests.cs
  10. 174
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Document/RandomizedLineManagerTest.cs
  11. 230
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Document/TextAnchorTest.cs
  12. 189
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Document/TextSegmentTreeTest.cs
  13. 3
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/ICSharpCode.AvalonEdit.Tests.PartCover.Settings
  14. 88
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/ICSharpCode.AvalonEdit.Tests.csproj
  15. 31
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Properties/AssemblyInfo.cs
  16. 112
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Utils/CompressingTreeListTests.cs
  17. 40
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Utils/ExtensionMethodsTests.cs
  18. 126
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/WeakReferenceTests.cs
  19. 15
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/app.config
  20. 11
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Architecture.txt
  21. 69
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs
  22. 40
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeOperation.cs
  23. 189
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentLine.cs
  24. 807
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentLineTree.cs
  25. 194
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/GapTextBuffer.cs
  26. 52
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/ILineTracker.cs
  27. 137
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/ISegment.cs
  28. 28
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/IUndoableOperation.cs
  29. 321
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/LineManager.cs
  30. 84
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/LineNode.cs
  31. 145
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextAnchor.cs
  32. 91
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextAnchorNode.cs
  33. 664
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextAnchorTree.cs
  34. 535
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextDocument.cs
  35. 151
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextDocumentWeakEventManager.cs
  36. 164
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextLocation.cs
  37. 187
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextSegment.cs
  38. 888
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextSegmentCollection.cs
  39. 52
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/UndoOperationGroup.cs
  40. 244
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/UndoStack.cs
  41. 64
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/WeakLineTracker.cs
  42. 79
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/AbstractMargin.cs
  43. 200
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/BackgroundGeometryBuilder.cs
  44. 276
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/Caret.cs
  45. 86
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/CaretAdorner.cs
  46. 324
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/CaretNavigationCommandHandler.cs
  47. 121
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/CollapsedLineSection.cs
  48. 81
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/ColorizingTransformer.cs
  49. 83
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/DocumentColorizingTransformer.cs
  50. 238
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/EditingCommandHandler.cs
  51. 101
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/FoldingElementGenerator.cs
  52. 105
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/FoldingManager.cs
  53. 230
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/FoldingMargin.cs
  54. 83
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/FoldingMarginMarker.cs
  55. 63
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/FoldingSection.cs
  56. 95
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/FormattedTextElement.cs
  57. 32
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/GlobalTextRunProperties.cs
  58. 1078
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/HeightTree.cs
  59. 53
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/HeightTreeLineNode.cs
  60. 159
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/HeightTreeNode.cs
  61. 23
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/IBackgroundRenderer.cs
  62. 30
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/IReadOnlySectionProvider.cs
  63. 39
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/ITextRunConstructionContext.cs
  64. 23
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/IVisualLineTransformer.cs
  65. 152
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/InlineObjectRun.cs
  66. 234
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/LineNumberMargin.cs
  67. 81
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/NewLineElementGenerator.cs
  68. 32
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/NoReadOnlySections.cs
  69. 263
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/Selection.cs
  70. 69
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/SelectionColorizer.cs
  71. 433
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/SelectionMouseHandler.cs
  72. 510
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/TextArea.cs
  73. 673
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/TextEditor.cs
  74. 53
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/TextEditor.xaml
  75. 85
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/TextSegmentReadOnlySectionProvider.cs
  76. 1046
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/TextView.cs
  77. 142
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/TextViewPosition.cs
  78. 56
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/TextViewWeakEventManager.cs
  79. 334
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLine.cs
  80. 215
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLineElement.cs
  81. 62
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLineElementGenerator.cs
  82. 193
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLineElementTextRunProperties.cs
  83. 134
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLineText.cs
  84. 33
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLineTextParagraphProperties.cs
  85. 71
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLineTextSource.cs
  86. 197
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/WhitespaceElementGenerator.cs
  87. 364
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/DocumentHighlighter.cs
  88. 114
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightedLine.cs
  89. 33
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightedSection.cs
  90. 82
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingBrush.cs
  91. 58
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingColor.cs
  92. 124
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingColorizer.cs
  93. 47
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingDefinitionInvalidException.cs
  94. 186
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingManager.cs
  95. 28
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingRule.cs
  96. 39
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingRuleSet.cs
  97. 49
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingSpan.cs
  98. 34
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/IHighlightingDefinition.cs
  99. 22
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/IHighlightingDefinitionReferenceResolver.cs
  100. 13
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/Resources/ASPX.xshd
  101. Some files were not shown because too many files have changed in this diff Show More

6
AddIns/ICSharpCode.SharpDevelop.addin

@ -1678,9 +1678,9 @@ @@ -1678,9 +1678,9 @@
</Path>
<Path name = "/SharpDevelop/Workbench/DisplayBindings">
<DisplayBinding id = "Text"
insertafter = "Browser"
title = "${res:Gui.ProjectBrowser.OpenWith.Bindings.TextEditor}"
<DisplayBinding id = "OldText"
insertafter = "Text"
title = "${res:Gui.ProjectBrowser.OpenWith.Bindings.TextEditor} (Old)"
class = "ICSharpCode.SharpDevelop.DefaultEditor.Gui.Editor.TextEditorDisplayBinding" />
<DisplayBinding id = "ShellExecute"
insertafter = "Text"

24
src/AddIns/DisplayBindings/AvalonEdit.AddIn/AvalonEdit.AddIn.addin

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
<AddIn name = "AvalonEdit.AddIn"
author = "Daniel Grunwald"
description = "The main text editor for SharpDevelop">
<Manifest>
<Identity name="ICSharpCode.AvalonEdit" />
</Manifest>
<Runtime>
<Import assembly = "ICSharpCode.AvalonEdit.AddIn.dll"/>
</Runtime>
<!-- Extend the SharpDevelop AddIn-Tree like this:
<Path name = ...>
<.../>
</Path>
-->
<Path name = "/SharpDevelop/Workbench/DisplayBindings">
<DisplayBinding id = "Text"
title = "${res:Gui.ProjectBrowser.OpenWith.Bindings.TextEditor} (AvalonEdit)"
class = "ICSharpCode.AvalonEdit.AddIn.AvalonEditDisplayBinding"/>
</Path>
</AddIn>

86
src/AddIns/DisplayBindings/AvalonEdit.AddIn/AvalonEdit.AddIn.csproj

@ -0,0 +1,86 @@ @@ -0,0 +1,86 @@
<Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{0162E499-42D0-409B-AA25-EED21F75336B}</ProjectGuid>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<OutputType>Library</OutputType>
<RootNamespace>ICSharpCode.AvalonEdit.AddIn</RootNamespace>
<AssemblyName>ICSharpCode.AvalonEdit.AddIn</AssemblyName>
<TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
<SourceAnalysisOverrideSettingsFile>C:\Users\Daniel\AppData\Roaming\ICSharpCode/SharpDevelop3.0\Settings.SourceAnalysis</SourceAnalysisOverrideSettingsFile>
<OutputPath>..\..\..\..\AddIns\AddIns\DisplayBindings\AvalonEdit\</OutputPath>
<AllowUnsafeBlocks>False</AllowUnsafeBlocks>
<NoStdLib>False</NoStdLib>
<WarningLevel>4</WarningLevel>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</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>AnyCPU</PlatformTarget>
<FileAlignment>4096</FileAlignment>
</PropertyGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.Targets" />
<ItemGroup>
<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.Xml" />
<Reference Include="WindowsBase">
<RequiredTargetFramework>3.0</RequiredTargetFramework>
</Reference>
</ItemGroup>
<ItemGroup>
<None Include="AvalonEdit.AddIn.addin">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<Compile Include="Configuration\AssemblyInfo.cs" />
<Compile Include="Src\AvalonEditDisplayBinding.cs" />
<Compile Include="Src\AvalonEditViewContent.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="Src" />
<ProjectReference Include="..\..\..\Libraries\AvalonEdit\ICSharpCode.AvalonEdit\ICSharpCode.AvalonEdit.csproj">
<Project>{6C55B776-26D4-4DB3-A6AB-87E783B2F3D1}</Project>
<Name>ICSharpCode.AvalonEdit</Name>
<Private>False</Private>
</ProjectReference>
<ProjectReference Include="..\..\..\Main\Base\Project\ICSharpCode.SharpDevelop.csproj">
<Project>{2748AD25-9C63-4E12-877B-4DCE96FBED54}</Project>
<Name>ICSharpCode.SharpDevelop</Name>
<Private>False</Private>
</ProjectReference>
<ProjectReference Include="..\..\..\Main\Core\Project\ICSharpCode.Core.csproj">
<Project>{35CEF10F-2D4C-45F2-9DD1-161E0FEC583C}</Project>
<Name>ICSharpCode.Core</Name>
<Private>False</Private>
</ProjectReference>
<ProjectReference Include="..\..\..\Main\ICSharpCode.Core.Presentation\ICSharpCode.Core.Presentation.csproj">
<Project>{7E4A7172-7FF5-48D0-B719-7CD959DD1AC9}</Project>
<Name>ICSharpCode.Core.Presentation</Name>
<Private>False</Private>
</ProjectReference>
</ItemGroup>
</Project>

31
src/AddIns/DisplayBindings/AvalonEdit.AddIn/Configuration/AssemblyInfo.cs

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
#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("AvalonEdit.AddIn")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("AvalonEdit.AddIn")]
[assembly: AssemblyCopyright("Copyright 2008")]
[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.*")]

26
src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/AvalonEditDisplayBinding.cs

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using ICSharpCode.SharpDevelop.Gui;
using System;
using ICSharpCode.SharpDevelop;
namespace ICSharpCode.AvalonEdit.AddIn
{
public class AvalonEditDisplayBinding : IDisplayBinding
{
public bool CanCreateContentForFile(string fileName)
{
return true;
}
public IViewContent CreateContentForFile(OpenedFile file)
{
return new AvalonEditViewContent(file);
}
}
}

50
src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/AvalonEditViewContent.cs

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.IO;
using ICSharpCode.AvalonEdit.Highlighting;
using ICSharpCode.SharpDevelop;
using ICSharpCode.SharpDevelop.Gui;
using System.Windows.Media;
namespace ICSharpCode.AvalonEdit.AddIn
{
public class AvalonEditViewContent : AbstractViewContent
{
TextEditor textEditor = new TextEditor {
Background = Brushes.White,
FontFamily = new FontFamily("Consolas")
};
public AvalonEditViewContent(OpenedFile file)
{
this.Files.Add(file);
file.ForceInitializeView(this);
}
public override object Content {
get { return textEditor; }
}
public override void Save(OpenedFile file, Stream stream)
{
if (file != PrimaryFile)
return;
textEditor.Save(stream);
}
public override void Load(OpenedFile file, Stream stream)
{
if (file != PrimaryFile)
return;
textEditor.SyntaxHighlighting =
HighlightingManager.Instance.GetDefinitionByExtension(Path.GetExtension(file.FileName));
textEditor.Load(stream);
}
}
}

158
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Document/CollapsingTests.cs

@ -0,0 +1,158 @@ @@ -0,0 +1,158 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using NUnit.Framework;
using ICSharpCode.AvalonEdit.Gui;
namespace ICSharpCode.AvalonEdit.Document.Tests
{
[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(document.GetLineByNumber(i)));
}
for (int i = from; i <= to; i++) {
Assert.IsTrue(heightTree.GetIsCollapsed(document.GetLineByNumber(i)));
}
for (int i = to + 1; i <= 10; i++) {
Assert.IsFalse(heightTree.GetIsCollapsed(document.GetLineByNumber(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(document.GetLineByNumber(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(document.GetLineByNumber(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(document.GetLineByNumber(i)));
}
for (int i = 4; i <= 8; i++) {
Assert.IsTrue(heightTree.GetIsCollapsed(document.GetLineByNumber(i)));
}
for (int i = 9; i <= 12; i++) {
Assert.IsFalse(heightTree.GetIsCollapsed(document.GetLineByNumber(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(document.GetLineByNumber(i)));
}
for (int i = 3; i <= 5; i++) {
Assert.IsTrue(heightTree.GetIsCollapsed(document.GetLineByNumber(i)));
}
for (int i = 6; i <= 8; i++) {
Assert.IsFalse(heightTree.GetIsCollapsed(document.GetLineByNumber(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(document.GetLineByNumber(i)));
}
for (int i = 3; i <= 5; i++) {
Assert.IsTrue(heightTree.GetIsCollapsed(document.GetLineByNumber(i)));
}
for (int i = 6; i <= 7; i++) {
Assert.IsFalse(heightTree.GetIsCollapsed(document.GetLineByNumber(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(document.GetLineByNumber(i)));
}
CheckHeights();
Assert.AreSame(null, sec1.Start);
Assert.AreSame(null, sec1.End);
Assert.IsTrue(sec1.IsCollapsed);
}
void CheckHeights()
{
HeightTests.CheckHeights(document, heightTree);
}
}
}

80
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Document/HeightTests.cs

@ -0,0 +1,80 @@ @@ -0,0 +1,80 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Linq;
using ICSharpCode.AvalonEdit.Gui;
using NUnit.Framework;
namespace ICSharpCode.AvalonEdit.Document.Tests
{
[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) ? 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);
}
}
}

490
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Document/LineManagerTests.cs

@ -0,0 +1,490 @@ @@ -0,0 +1,490 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;
namespace ICSharpCode.AvalonEdit.Document.Tests
{
[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);
Assert.AreEqual("", line.Text);
}
[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", line.Text);
}
[Test]
public void SetText()
{
document.Text = "a";
Assert.AreEqual(document.LineCount, 1);
DocumentLine line = document.GetLineByNumber(1);
Assert.AreEqual("a", line.Text);
}
[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, line.Text);
}
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 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.Lines[i].Text, "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");
}
}
}

174
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Document/RandomizedLineManagerTest.cs

@ -0,0 +1,174 @@ @@ -0,0 +1,174 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using ICSharpCode.AvalonEdit.Gui;
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;
namespace ICSharpCode.AvalonEdit.Document.Tests
{
/// <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);
}
}
}
}

230
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Document/TextAnchorTest.cs

@ -0,0 +1,230 @@ @@ -0,0 +1,230 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using NUnit.Framework;
using System.Collections.Generic;
namespace ICSharpCode.AvalonEdit.Document.Tests
{
[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 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(4)) {
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;
}
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();
}
}
}
}

189
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Document/TextSegmentTreeTest.cs

@ -0,0 +1,189 @@ @@ -0,0 +1,189 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using NUnit.Framework;
using System.Collections.Generic;
namespace ICSharpCode.AvalonEdit.Document.Tests
{
[TestFixture]
public class TextSegmentTreeTest
{
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;
}
}
static readonly string documentText = new string(' ', 1000);
TextDocument document;
TextSegmentCollection<TestTextSegment> tree;
List<TestTextSegment> expectedSegments;
[SetUp]
public void SetUp()
{
document = new TextDocument();
document.Text = documentText;
tree = new TextSegmentCollection<TestTextSegment>(document);
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));
}
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);
Assert.AreEqual(s.ExpectedLength, s.Length);
}
}
[Test]
public void AddSegments()
{
TestTextSegment s1 = AddSegment(10, 20);
TestTextSegment s2 = AddSegment(15, 10);
CheckSegments();
}
Random rnd;
[TestFixtureSetUp]
public void FixtureSetup()
{
int seed = Environment.TickCount;
Console.WriteLine("TextSegmentTreeTest Seed: " + seed);
rnd = new Random(seed);
}
[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 RandomizedClose()
{
// 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));
}
}
// TODO: insertion/removal tests
}
}

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

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

88
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/ICSharpCode.AvalonEdit.Tests.csproj

@ -0,0 +1,88 @@ @@ -0,0 +1,88 @@
<Project ToolsVersion="3.5" 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.Tests</RootNamespace>
<AssemblyName>ICSharpCode.AvalonEdit.Tests</AssemblyName>
<TargetFrameworkVersion>v3.5</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>
</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>AnyCPU</PlatformTarget>
<FileAlignment>4096</FileAlignment>
</PropertyGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.Targets" />
<ItemGroup>
<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.Xml" />
<Reference Include="System.Xml.Linq">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
<Reference Include="WindowsBase">
<RequiredTargetFramework>3.0</RequiredTargetFramework>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Document\TextAnchorTest.cs" />
<Compile Include="Document\TextSegmentTreeTest.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\CompressingTreeListTests.cs" />
<Compile Include="Utils\ExtensionMethodsTests.cs" />
<Compile Include="WeakReferenceTests.cs" />
<None Include="app.config" />
</ItemGroup>
<ItemGroup>
<Folder Include="Document" />
<Folder Include="Utils" />
<ProjectReference Include="..\ICSharpCode.AvalonEdit\ICSharpCode.AvalonEdit.csproj">
<Project>{6C55B776-26D4-4DB3-A6AB-87E783B2F3D1}</Project>
<Name>ICSharpCode.AvalonEdit</Name>
</ProjectReference>
</ItemGroup>
</Project>

31
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Properties/AssemblyInfo.cs

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
#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.CodeEditor.Tests")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("ICSharpCode.CodeEditor.Tests")]
[assembly: AssemblyCopyright("Copyright 2008")]
[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.*")]

112
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Utils/CompressingTreeListTests.cs

@ -0,0 +1,112 @@ @@ -0,0 +1,112 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Linq;
using NUnit.Framework;
namespace ICSharpCode.AvalonEdit.Utils.Tests
{
[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());
}
}
}

40
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Utils/ExtensionMethodsTests.cs

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using NUnit.Framework;
namespace ICSharpCode.AvalonEdit.Utils.Tests
{
[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));
}
}
}

126
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/WeakReferenceTests.cs

@ -0,0 +1,126 @@ @@ -0,0 +1,126 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Gui;
using NUnit.Framework;
using System.Windows.Threading;
namespace ICSharpCode.AvalonEdit.Tests
{
[TestFixture]
public class WeakReferenceTests
{
[Test]
public void GCCallbackTest()
{
bool collectedTextView = false;
TextView textView = new TextViewWithGCCallback(delegate { collectedTextView = true; });
textView = null;
GarbageCollect();
Assert.IsTrue(collectedTextView);
}
[Test]
public void DocumentDoesNotHoldReferenceToTextView()
{
bool collectedTextView = false;
TextDocument textDocument = new TextDocument();
Assert.AreEqual(0, textDocument.LineTracker.Count);
TextView textView = new TextViewWithGCCallback(delegate { collectedTextView = true; });
textView.Document = textDocument;
Assert.AreEqual(1, textDocument.LineTracker.Count);
textView = null;
GarbageCollect();
Assert.IsTrue(collectedTextView);
// document cannot immediately clear the line tracker
Assert.AreEqual(1, textDocument.LineTracker.Count);
// but it should clear it on the next change
textDocument.Insert(0, "a");
Assert.AreEqual(0, textDocument.LineTracker.Count);
}
[Test]
public void DocumentDoesNotHoldReferenceToTextArea()
{
bool collectedTextArea = false;
TextDocument textDocument = new TextDocument();
TextArea textArea = new TextAreaWithGCCallback(delegate { collectedTextArea = true; });
textArea.Document = textDocument;
textArea = null;
GarbageCollect();
Assert.IsTrue(collectedTextArea);
GC.KeepAlive(textDocument);
}
[Test]
public void DocumentDoesNotHoldReferenceToLineMargin()
{
bool collectedTextView = false;
TextDocument textDocument = new TextDocument();
DocumentDoesNotHoldReferenceToLineMargin_CreateMargin(textDocument, delegate { collectedTextView = true; });
GarbageCollect();
Assert.IsTrue(collectedTextView);
GC.KeepAlive(textDocument);
}
void DocumentDoesNotHoldReferenceToLineMargin_CreateMargin(TextDocument textDocument, Action finalizeAction)
{
TextView textView = new TextViewWithGCCallback(finalizeAction) {
Document = textDocument
};
LineNumberMargin margin = new LineNumberMargin() {
TextView = textView
};
}
static void GarbageCollect()
{
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
}
sealed class TextViewWithGCCallback : TextView
{
Action onFinalize;
public TextViewWithGCCallback(Action onFinalize)
{
this.onFinalize = onFinalize;
}
~TextViewWithGCCallback()
{
onFinalize();
}
}
sealed class TextAreaWithGCCallback : TextArea
{
Action onFinalize;
public TextAreaWithGCCallback(Action onFinalize)
{
this.onFinalize = onFinalize;
}
~TextAreaWithGCCallback()
{
onFinalize();
}
}
}
}

15
src/Libraries/AvalonEdit/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>

11
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Architecture.txt

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@

How foldings work:
The FoldingManager maintains a list of foldings. The FoldMargin displays those foldings and provides
the UI for collapsing/expanding.
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.
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.

69
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeEventArgs.cs

@ -0,0 +1,69 @@ @@ -0,0 +1,69 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// Describes a change of the document text.
/// </summary>
[Serializable]
public class DocumentChangeEventArgs : EventArgs
{
/// <summary>
/// The offset at which the change occurs.
/// </summary>
public int Offset { get; private set; }
/// <summary>
/// The number of characters removed.
/// </summary>
public int RemovalLength { get; private set; }
/// <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; }
}
/// <summary>
/// Gets the new offset where the specified offset moves after this document change.
/// </summary>
public int GetNewOffset(int offset, AnchorMovementType movementType)
{
if (offset >= this.Offset) {
if (offset <= this.Offset + this.RemovalLength) {
offset = this.Offset;
if (movementType == AnchorMovementType.AfterInsertion)
offset += this.InsertionLength;
} else {
offset += this.InsertionLength - this.RemovalLength;
}
}
return offset;
}
/// <summary>
/// Creates a new DocumentChangeEventArgs object.
/// </summary>
public DocumentChangeEventArgs(int offset, int removalLength, string insertedText)
{
if (insertedText == null)
throw new ArgumentNullException("insertedText");
this.Offset = offset;
this.RemovalLength = removalLength;
this.InsertedText = insertedText;
}
}
}

40
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentChangeOperation.cs

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// Describes a change to a TextDocument.
/// </summary>
sealed class DocumentChangeOperation : IUndoableOperation
{
TextDocument document;
int offset;
string removedText;
string insertedText;
public DocumentChangeOperation(TextDocument document, int offset, string removedText, string insertedText)
{
this.document = document;
this.offset = offset;
this.removedText = removedText;
this.insertedText = insertedText;
}
public void Undo()
{
document.Replace(offset, insertedText.Length, removedText);
}
public void Redo()
{
document.Replace(offset, removedText.Length, insertedText);
}
}
}

189
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentLine.cs

@ -0,0 +1,189 @@ @@ -0,0 +1,189 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Diagnostics;
using System.Globalization;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// Represents a line inside a <see cref="TextDocument"/>.
/// </summary>
public sealed partial class DocumentLine : ISegment
{
#region Constructor
readonly TextDocument document;
internal bool isDeleted;
internal DocumentLine(TextDocument document)
{
Debug.Assert(document != null);
this.document = document;
}
#endregion
#region Document / Text
/// <summary>
/// Gets the text document that owns this DocumentLine. O(1).
/// </summary>
/// <remarks>This property is still available even if the line was deleted.</remarks>
public TextDocument Document {
get {
document.DebugVerifyAccess();
return document;
}
}
/// <summary>
/// Gets the text on this line.
/// </summary>
/// <exception cref="InvalidOperationException">The line was deleted.</exception>
public string Text {
get {
return document.GetText(this.Offset, this.Length);
}
}
#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 {
document.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);
}
}
#endregion
#region Length
int totalLength;
byte delimiterLength;
/// <summary>
/// Gets the length of this line. 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 {
document.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 {
document.DebugVerifyAccess();
return totalLength;
}
internal set {
// this is set by DocumentLineTree
totalLength = value;
}
}
/// <summary>
/// Gets the length of the newline.
/// </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 DelimiterLength {
get {
document.DebugVerifyAccess();
return delimiterLength;
}
internal set {
Debug.Assert(value >= 0 && value <= 2);
delimiterLength = (byte)value;
}
}
#endregion
#region ParserState
// /// <summary>
// /// Gets the parser state array associated with this line.
// /// </summary>
// public object[] ParserState { get; internal set; }
#endregion
#region ToString
/// <summary>
/// Gets a string representation of the line.
/// </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
}
}

807
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/DocumentLineTree.cs

@ -0,0 +1,807 @@ @@ -0,0 +1,807 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
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;
Clear();
}
public void Clear()
{
DocumentLine emptyLine = new DocumentLine(document);
root = emptyLine.InitLineNode();
#if DEBUG
CheckProperties();
#endif
}
#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 / GetEnumeratorFor
public DocumentLine GetByNumber(int number)
{
return GetNodeByIndex(number - 1);
}
public DocumentLine GetByOffset(int offset)
{
return GetNodeByOffset(offset);
}
public Enumerator GetEnumeratorForOffset(int offset)
{
return new Enumerator(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 Enumerator
internal struct Enumerator : IEnumerator<DocumentLine>
{
LineNode node;
internal Enumerator(LineNode node)
{
this.node = node;
}
/// <summary>
/// Gets the current value. Runs in O(1).
/// </summary>
public DocumentLine Current {
get {
if (node == null)
throw new InvalidOperationException();
return node;
}
}
object System.Collections.IEnumerator.Current {
get {
return this.Current;
}
}
void IDisposable.Dispose()
{
}
/// <summary>
/// Moves to the next index. Runs in O(lg n), but for k calls, the combined time is only O(k+lg n).
/// </summary>
public bool MoveNext()
{
if (node == null)
return false;
if (node.right != null) {
node = node.right.LeftMost;
} else {
LineNode 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 != null;
}
/// <summary>
/// Moves to the previous index. Runs in O(lg n), but for k calls, the combined time is only O(k+lg n).
/// </summary>
public bool MoveBack()
{
if (node == null)
return false;
if (node.left != null) {
node = node.left.RightMost;
} else {
LineNode 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 != null;
}
void System.Collections.IEnumerator.Reset()
{
throw new NotSupportedException();
}
}
#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.Document != document || item.IsDeleted)
return -1;
else
return item.LineNumber - 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)
{
document.VerifyAccess();
return item != null && item.Document == document && !item.IsDeleted;
}
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();
LineNode dummyNode = new LineNode(document);
dummyNode.right = root;
return new Enumerator(dummyNode);
}
// enumerator that verifies thread on each call
// - this is overkill, checking on the GetEnumerator call should be enough.
// IEnumerator<DocumentLine> Enumerate()
// {
// document.VerifyAccess();
// Enumerator e = new Enumerator(tree.GetEnumerator());
// while (e.MoveNext()) {
// yield return e.Current;
// document.DebugVerifyAccess();
// }
// }
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
#endregion
}
}

194
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/GapTextBuffer.cs

@ -0,0 +1,194 @@ @@ -0,0 +1,194 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
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;
}
}
}

52
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/ILineTracker.cs

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// Allows for low-level line tracking.
/// 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.
/// </summary>
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.
/// </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();
}
}

137
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/ISegment.cs

@ -0,0 +1,137 @@ @@ -0,0 +1,137 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Diagnostics;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// An (Offset,Length)-pair.
/// </summary>
public interface ISegment
{
/// <summary>
/// Gets the start offset of the segment.
/// </summary>
int Offset { get; }
/// <summary>
/// Gets the length of the segment.
/// </summary>
int Length { get; }
}
/// <summary>
/// Represents a simple segment (Offset,Length pair) that is not automatically updated
/// on document changed.
/// </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; }
}
internal int GetEndOffset()
{
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);
}
}
/// <summary>
/// A segment using text anchors as start and end positions.
/// </summary>
sealed class AnchorSegment : ISegment
{
readonly TextAnchor start, end;
public int Offset {
get { return start.Offset; }
}
public int Length {
get { return end.Offset - start.Offset; }
}
internal int GetEndOffset()
{
return end.Offset;
}
public AnchorSegment(TextAnchor start, TextAnchor end)
{
Debug.Assert(start != null);
Debug.Assert(end != null);
Debug.Assert(start.SurviveDeletion);
Debug.Assert(end.SurviveDeletion);
this.start = start;
this.end = end;
}
public AnchorSegment(TextDocument document, ISegment segment)
: this(document, segment.Offset, segment.Length)
{
}
public AnchorSegment(TextDocument document, int offset, int length)
{
Debug.Assert(document != null);
this.start = document.CreateAnchor(offset);
this.start.SurviveDeletion = true;
this.end = document.CreateAnchor(offset + length);
this.end.SurviveDeletion = true;
}
}
}

28
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/IUndoableOperation.cs

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
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();
}
}

321
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/LineManager.cs

@ -0,0 +1,321 @@ @@ -0,0 +1,321 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
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 GapTextBuffer textBuffer;
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>
internal ILineTracker[] lineTracker;
public LineManager(GapTextBuffer textBuffer, DocumentLineTree documentLineTree, TextDocument document)
{
this.document = document;
this.textBuffer = textBuffer;
this.documentLineTree = documentLineTree;
this.lineTracker = document.LineTracker.ToArray();
}
#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(string text)
{
// keep the first document line
DocumentLine ls = documentLineTree.GetByNumber(1);
DelimiterSegment ds = NextDelimiter(text, 0);
List<DocumentLine> lines = new List<DocumentLine>();
int lastDelimiterEnd = 0;
while (ds != null) {
ls.TotalLength = ds.Offset + ds.Length - lastDelimiterEnd;
ls.DelimiterLength = ds.Length;
lastDelimiterEnd = ds.Offset + ds.Length;
lines.Add(ls);
ls = new DocumentLine(document);
ds = NextDelimiter(text, lastDelimiterEnd);
}
ls.ResetLine();
ls.TotalLength = text.Length - lastDelimiterEnd;
lines.Add(ls);
documentLineTree.RebuildTree(lines);
foreach (ILineTracker lt in lineTracker)
lt.RebuildDocument();
}
#endregion
#region Remove
public void Remove(int offset, int length)
{
Debug.Assert(length >= 0);
if (length == 0) return;
DocumentLineTree.Enumerator it = documentLineTree.GetEnumeratorForOffset(offset);
DocumentLine startLine = it.Current;
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.)
it.MoveNext();
DocumentLine lineToRemove;
do {
lineToRemove = it.Current;
it.MoveNext();
RemoveLine(lineToRemove);
} while (lineToRemove != endLine);
SetLineLength(startLine, startLine.TotalLength - charactersRemovedInStartLine + charactersLeftInEndLine);
}
void RemoveLine(DocumentLine lineToRemove)
{
foreach (ILineTracker lt in lineTracker)
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);
}
DelimiterSegment ds = NextDelimiter(text, 0);
if (ds == null) {
// 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 != null) {
// 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 = NextDelimiter(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 lineTracker)
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 lineTracker)
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 = textBuffer.GetCharAt(lineOffset + newTotalLength - 1);
if (lastChar == '\r') {
line.DelimiterLength = 1;
} else if (lastChar == '\n') {
if (newTotalLength >= 2 && textBuffer.GetCharAt(lineOffset + newTotalLength - 2) == '\r') {
line.DelimiterLength = 2;
} else if (newTotalLength == 1 && lineOffset > 0 && textBuffer.GetCharAt(lineOffset - 1) == '\r') {
// we need to join this line with the previous line
DocumentLineTree.Enumerator it = new DocumentLineTree.Enumerator(line);
it.MoveBack();
RemoveLine(line);
return SetLineLength(it.Current, it.Current.TotalLength + 1);
} else {
line.DelimiterLength = 1;
}
} else {
line.DelimiterLength = 0;
}
}
return line;
}
#endregion
#region Delimiter
sealed class DelimiterSegment
{
internal int Offset;
internal int Length;
}
// always use the same DelimiterSegment object for the NextDelimiter
DelimiterSegment delimiterSegment = new DelimiterSegment();
DelimiterSegment NextDelimiter(string text, int offset)
{
for (int i = offset; i < text.Length; i++) {
switch (text[i]) {
case '\r':
if (i + 1 < text.Length) {
if (text[i + 1] == '\n') {
delimiterSegment.Offset = i;
delimiterSegment.Length = 2;
return delimiterSegment;
}
}
goto case '\n';
case '\n':
delimiterSegment.Offset = i;
delimiterSegment.Length = 1;
return delimiterSegment;
}
}
return null;
}
#endregion
}
}

84
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/LineNode.cs

@ -0,0 +1,84 @@ @@ -0,0 +1,84 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
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 40 bytes on x86 (8 byte object overhead, 4 pointers, 3 ints, and another DWORD
// for the small fields).
/// <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;
}
}

145
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextAnchor.cs

@ -0,0 +1,145 @@ @@ -0,0 +1,145 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// The TextAnchor class references a text location - a position between two characters.
/// It automatically updates its offset when text is inserted/removed in front of the anchor.
/// </summary>
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>
public AnchorMovementType MovementType { get; set; }
/// <summary>
/// Specifies whether the anchor survives deletion of the text containing it.
/// <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.
/// </summary>
public bool SurviveDeletion { get; set; }
/// <summary>
/// Gets whether the anchor was deleted.
/// </summary>
public bool IsDeleted {
get {
document.DebugVerifyAccess();
return node == null;
}
}
/// <summary>
/// Occurs after the anchor was deleted.
/// </summary>
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>
public int Line {
get {
return document.GetLineByOffset(this.Offset).LineNumber;
}
}
/// <summary>
/// Gets the column number of this anchor.
/// </summary>
public int Column {
get {
int offset = this.Offset;
return offset - document.GetLineByOffset(offset).Offset + 1;
}
}
/// <summary>
/// Gets the text location of this anchor.
/// </summary>
public TextLocation Location {
get {
return new TextLocation(Line, Column);
}
}
/// <inheritdoc/>
public override string ToString()
{
return "[TextAnchor Offset=" + Offset + "]";
}
}
/// <summary>
/// Defines how a text anchor moves.
/// </summary>
public enum AnchorMovementType
{
/// <summary>
/// Behaves like a start marker - when text is inserted at the anchor position, the anchor will stay
/// before the inserted text.
/// </summary>
BeforeInsertion,
/// <summary>
/// Behave like an end marker - when text is insered at the anchor position, the anchor will move
/// after the inserted text.
/// </summary>
AfterInsertion
}
}

91
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextAnchorNode.cs

@ -0,0 +1,91 @@ @@ -0,0 +1,91 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
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.
/// </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 + "]";
}
}
}

664
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextAnchorTree.cs

@ -0,0 +1,664 @@ @@ -0,0 +1,664 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
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
{
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
public void InsertText(int offset, int length)
{
Log("InsertText(" + offset + ", " + length + ")");
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(root.RightMost, null, length);
} 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(endNode.Predecessor, endNode, length);
}
}
DeleteMarkedNodes();
}
void PerformInsertText(TextAnchorNode beginNode, TextAnchorNode endNode, int length)
{
// now find the actual beginNode
while (beginNode != null && beginNode.length == 0)
beginNode = beginNode.Predecessor;
if (beginNode == null) {
// no predecessor = beginNode is first node in tree
beginNode = root.LeftMost;
}
// 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 (anchor.MovementType == AnchorMovementType.AfterInsertion) {
// afterInsert.Add(temp);
} else {
beforeInsert.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 Text
public void RemoveText(int offset, int length, DelayedEvents delayedEvents)
{
Log("RemoveText(" + offset + ", " + length + ")");
if (length == 0 || root == null || offset >= root.totalLength)
return;
TextAnchorNode node = FindNode(ref offset);
while (node != null && offset + length > node.length) {
TextAnchor anchor = (TextAnchor)node.Target;
if (anchor != null && anchor.SurviveDeletion) {
// shorten node
length -= node.length - offset;
node.length = offset;
offset = 0;
UpdateAugmentedData(node);
node = node.Successor;
} else {
// delete node
TextAnchorNode s = node.Successor;
length -= 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;
}
}
if (node != null) {
node.length -= length;
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
}
}

535
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextDocument.cs

@ -0,0 +1,535 @@ @@ -0,0 +1,535 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Globalization;
using System.Threading;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// Runtimes:
/// n = number of lines in the document
/// </summary>
public sealed class TextDocument
{
#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>
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>
/// 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 SetOwnerThread.
/// </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 GapTextBuffer textBuffer = new GapTextBuffer();
readonly DocumentLineTree lineTree;
readonly LineManager lineManager;
readonly TextAnchorTree anchorTree;
//readonly ParserManager parserManager;
/// <summary>
/// Create an empty text document.
/// </summary>
public TextDocument()
{
//parserManager = new ParserManager(this);
lineTree = new DocumentLineTree(this);
lineManager = new LineManager(textBuffer, lineTree, this);
lineTracker.CollectionChanged += delegate {
lineManager.lineTracker = lineTracker.ToArray();
};
anchorTree = new TextAnchorTree(this);
undoStack = new UndoStack();
undoStack.AttachToDocument(this);
FireChangeEvents();
}
#endregion
#region DocumentParsers
/*
/// <summary>
/// Gets/Sets the document parser associated with this document.
/// </summary>
public IList<IDocumentParser> DocumentParsers {
get {
VerifyAccess();
return parserManager;
}
}
*/
#endregion
#region Text
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.Int32.ToString")]
void VerifyRange(int offset, int length)
{
if (offset < 0 || offset > textBuffer.Length) {
throw new ArgumentOutOfRangeException("offset", offset, "0 <= offset <= " + textBuffer.Length.ToString());
}
if (length < 0 || offset + length > textBuffer.Length) {
throw new ArgumentOutOfRangeException("length", length, "0 <= length, offset(" + offset + ")+length <= " + textBuffer.Length.ToString());
}
}
/// <summary>
/// Retrieves the text for a portion of the document.
/// </summary>
public string GetText(int offset, int length)
{
VerifyAccess();
VerifyRange(offset, length);
return textBuffer.GetText(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);
}
/// <summary>
/// Gets a character at the specified position in the document.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.Int32.ToString")]
public char GetCharAt(int offset)
{
VerifyAccess();
if (offset < 0 || offset >= textBuffer.Length) {
throw new ArgumentOutOfRangeException("offset", offset, "0 <= offset < " + textBuffer.Length.ToString());
}
return textBuffer.GetCharAt(offset);
}
// /// <summary>
// /// Like GetCharAt, but without any safety checks.
// /// </summary>
// internal char FastGetCharAt(int offset)
// {
// return textBuffer.GetCharAt(offset);
// }
/// <summary>
/// Gets/Sets the text of the whole document.
/// Get: O(n)
/// Set: O(n * log n)
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods")]
public string Text {
get {
VerifyAccess();
return textBuffer.Text;
}
set {
VerifyAccess();
if (value == null)
throw new ArgumentNullException("value");
Replace(0, textBuffer.Length, value);
}
}
/// <summary>
/// Is raised when the Text property changes.
/// </summary>
public event EventHandler TextChanged;
/// <summary>
/// Gets the total text length.
/// Runtime: O(1).
/// </summary>
public int TextLength {
get {
VerifyAccess();
return textBuffer.Length;
}
}
/// <summary>
/// Is raised when the TextLength property changes.
/// </summary>
public event EventHandler TextLengthChanged;
/// <summary>
/// Is raised before the document changes.
/// </summary>
public event EventHandler<DocumentChangeEventArgs> Changing;
/// <summary>
/// Is raised after the document has changed.
/// </summary>
public event EventHandler<DocumentChangeEventArgs> Changed;
#endregion
#region BeginUpdate / EndUpdate
int beginUpdateCount;
/// <summary>
/// Gets if an update is running.
/// </summary>
public bool IsInUpdate {
get {
VerifyAccess();
return beginUpdateCount > 0;
}
}
/// <summary>
/// Begins a group of document changes.
/// DocumentParsers and some events are suspended until EndUpdate is called.
/// Calling BeginUpdate several times increments a counter, only after the appropriate number
/// of EndUpdate calls the DocumentParsers and events resume their work.
/// </summary>
public void BeginUpdate()
{
VerifyAccess();
beginUpdateCount++;
if (beginUpdateCount == 1) {
if (UpdateStarted != null)
UpdateStarted(this, EventArgs.Empty);
}
}
/// <summary>
/// Ends a group of document changes.
/// </summary>
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.");
beginUpdateCount -= 1;
if (beginUpdateCount == 0) {
FireChangeEvents();
if (UpdateFinished != null)
UpdateFinished(this, EventArgs.Empty);
}
}
/// <summary>
/// Occurs when a document change starts.
/// </summary>
public event EventHandler UpdateStarted;
/// <summary>
/// Occurs when a document change is finished.
/// </summary>
public event EventHandler UpdateFinished;
#endregion
#region Fire events after update
int oldTextLength;
int oldLineCount;
bool fireTextChanged;
/// <summary>
/// Fires TextChanged, TextLengthChanged, TotalHeightChanged, LineCountChanged if required.
/// </summary>
internal void FireChangeEvents()
{
if (beginUpdateCount > 0)
return;
if (fireTextChanged) {
fireTextChanged = false;
if (TextChanged != null)
TextChanged(this, EventArgs.Empty);
}
int textLength = textBuffer.Length;
if (oldTextLength != textBuffer.Length) {
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.
/// Runtime:
/// for updating the text buffer: m=size of new text, d=distance to last change
/// usual: O(m+d)
/// rare: O(m+n)
/// for updating the document lines: O(m*log n), m=number of changed lines
/// </summary>
public void Insert(int offset, string text)
{
Replace(offset, 0, text);
}
/// <summary>
/// Removes text.
/// Runtime:
/// for updating the text buffer: d=distance to last change
/// usual: O(d)
/// rare: O(n)
/// for updating the document lines: O(m*log n), m=number of changed lines
/// </summary>
public void Remove(int offset, int length)
{
Replace(offset, length, string.Empty);
}
internal bool inDocumentChanging;
/// <summary>
/// Replaces text.
/// Runtime:
/// for updating the text buffer: m=size of new text, d=distance to last change
/// usual: O(m+d)
/// rare: O(m+n)
/// for updating the document lines: O(m*log n), m=number of changed lines
/// </summary>
public void Replace(int offset, int length, string text)
{
if (inDocumentChanging)
throw new InvalidOperationException("Cannot change document within another document change.");
BeginUpdate();
// protect document change against corruption by other changes inside the event handlers/IDocumentParser
inDocumentChanging = true;
try {
VerifyRange(offset, length);
if (text == null)
throw new ArgumentNullException("text");
if (length == 0 && text.Length == 0)
return;
fireTextChanged = true;
DocumentChangeEventArgs args = new DocumentChangeEventArgs(offset, length, text);
// fire DocumentChanging event
if (Changing != null)
Changing(this, args);
DelayedEvents delayedEvents = new DelayedEvents();
// now do the real work
anchorTree.RemoveText(offset, length, delayedEvents);
ReplaceInternal(offset, length, text);
anchorTree.InsertText(offset, text.Length);
delayedEvents.RaiseEvents();
//parserManager.ClearParserState(lineManager.RetrieveDeletedOrChangedLines());
// fire DocumentChanged event
if (Changed != null)
Changed(this, args);
} finally {
inDocumentChanging = false;
EndUpdate();
}
}
void ReplaceInternal(int offset, int length, string text)
{
if (offset == 0 && length == textBuffer.Length) {
textBuffer.Text = text;
lineManager.Rebuild(text);
} else {
textBuffer.Remove(offset, length, text.Length);
lineManager.Remove(offset, length);
#if DEBUG
lineTree.CheckProperties();
#endif
textBuffer.Insert(offset, text);
lineManager.Insert(offset, text);
#if DEBUG
lineTree.CheckProperties();
#endif
}
}
#endregion
#region GetLineBy...
/// <summary>
/// Gets a read-only list of lines.
/// </summary>
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 > textBuffer.Length) {
throw new ArgumentOutOfRangeException("offset", offset, "0 <= offset <= " + textBuffer.Length.ToString());
}
return lineTree.GetByOffset(offset);
}
#endregion
/// <summary>
/// Gets the offset from a text location.
/// </summary>
public int GetOffset(TextLocation location)
{
DocumentLine line = GetLineByNumber(location.Line);
return line.Offset + location.Column - 1;
}
/// <summary>
/// Gets the location from an offset.
/// </summary>
public TextLocation GetLocation(int offset)
{
DocumentLine line = GetLineByOffset(offset);
return new TextLocation(line.LineNumber, offset - line.Offset + 1);
}
readonly ObservableCollection<ILineTracker> lineTracker = new ObservableCollection<ILineTracker>();
/// <summary>
/// Gets the list of <see cref="ILineTracker"/> attached to this document.
/// </summary>
public IList<ILineTracker> LineTracker {
get {
VerifyAccess();
return lineTracker;
}
}
readonly UndoStack undoStack;
/// <summary>
/// Gets the <see cref="UndoStack"/> of the document.
/// </summary>
public UndoStack UndoStack {
get { return undoStack; }
}
/// <summary>
/// Creates a new text anchor at the specified offset.
/// </summary>
public TextAnchor CreateAnchor(int offset)
{
VerifyAccess();
if (offset < 0 || offset > textBuffer.Length) {
throw new ArgumentOutOfRangeException("offset", offset, "0 <= offset <= " + textBuffer.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
}
}

151
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextDocumentWeakEventManager.cs

@ -0,0 +1,151 @@ @@ -0,0 +1,151 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
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;
}
}
}
}

164
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextLocation.cs

@ -0,0 +1,164 @@ @@ -0,0 +1,164 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Globalization;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// A line/column position.
/// Text editor lines/columns are counting from one.
/// </summary>
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.
/// Warning: the parameters are (line, column).
/// Not (column, line) as in ICSharpCode.TextEditor!
/// </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;
}
}
}

187
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextSegment.cs

@ -0,0 +1,187 @@ @@ -0,0 +1,187 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// A segment that can be put into a SegmentTree.
/// </summary>
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; // totalLength = length + left.totalLength + right.totalLength
/// <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/Sets the start offset of the segment.
/// </summary>
public int StartOffset {
get {
if (ownerTree == null)
return nodeLength;
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 be non-negative");
if (this.StartOffset != value) {
// need a copy of the variable because ownerTree.Remove() sets ownerTree to null
ISegmentTree ownerTree = this.ownerTree;
if (ownerTree != null) {
ownerTree.Remove(this);
nodeLength = value;
ownerTree.Add(this);
} else {
nodeLength = value;
}
}
}
}
/// <summary>
/// Gets the end offset of the segment.
/// </summary>
public int EndOffset {
get {
return StartOffset + Length;
}
}
/// <summary>
/// Gets/Sets the length of the segment.
/// </summary>
public int Length {
get {
return segmentLength;
}
set {
if (value < 0)
throw new ArgumentOutOfRangeException("value", "value 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 + "]";
}
}
}

888
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextSegmentCollection.cs

@ -0,0 +1,888 @@ @@ -0,0 +1,888 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
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 TextSegmentTree - we cannot use a direct reference
/// because TextSegmentTree is generic.
/// </summary>
interface ISegmentTree
{
void Add(TextSegment s);
void Remove(TextSegment s);
void UpdateAugmentedData(TextSegment s);
}
/// <summary>
/// A collection of text segments that supports efficient lookup of segments
/// intersecting with another segment.
/// </summary>
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.
// 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(m + lg n) with m being the number of segments at the same start offset
// 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;
#region Constructor
/// <summary>
/// Creates a new SegmentTree.
/// </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");
TextDocumentWeakEventManager.Changed.AddListener(textDocument, this);
}
#endregion
#region OnDocumentChanged
bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
if (managerType == typeof(TextDocumentWeakEventManager.Changed)) {
OnDocumentChanged((DocumentChangeEventArgs)e);
return true;
}
return false;
}
void OnDocumentChanged(DocumentChangeEventArgs e)
{
RemoveText(e.Offset, e.RemovalLength);
InsertText(e.Offset, e.InsertionLength);
}
#endregion
#region Insert Text
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);
}
}
#endregion
#region Remove Text
void RemoveText(int offset, int length)
{
if (length == 0)
return;
foreach (TextSegment segment in FindOverlappingSegments(offset, length)) {
if (segment.StartOffset < offset) {
if (segment.EndOffset > offset + length) {
segment.Length -= length;
} else {
//segment.EndOffset = offset;
segment.Length = offset - segment.StartOffset;
}
} else {
int lengthLeft = segment.EndOffset - (offset + length);
RemoveSegment(segment);
segment.StartOffset = offset + length;
segment.Length = Math.Max(0, lengthLeft);
AddSegment(segment);
}
}
// move start offsets of all segments >= offset
TextSegment node = FindFirstSegmentWithStartAfter(offset);
if (node != null) {
Debug.Assert(node.nodeLength >= length);
node.nodeLength -= length;
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 SegmentTree.");
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);
if (s == null && startOffset == 0) {
s = root.RightMost;
startOffset += s.nodeLength;
}
while (s != null && startOffset == 0) {
startOffset += s.nodeLength;
TextSegment p = s.Predecessor;
if (p == null)
break;
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)
/// </summary>
public ReadOnlyCollection<T> FindSegmentsContaining(int offset)
{
return FindOverlappingSegments(offset, 0);
}
/// <summary>
/// Finds all segments that overlap with the given segment.
/// </summary>
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.
/// Segments are returned in the order given by GetNextSegment/GetPreviousSegment.
/// </summary>
public ReadOnlyCollection<T> FindOverlappingSegments(int offset, int length)
{
if (length < 0)
throw new ArgumentOutOfRangeException("length", "length must be non-negative");
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;
current = current.Successor;
}
}
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
#endregion
}
}

52
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/UndoOperationGroup.cs

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using System.Diagnostics;
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 : IUndoableOperation
{
IUndoableOperation[] undolist;
public UndoOperationGroup(Stack<IUndoableOperation> stack, int numops)
{
if (stack == null) {
throw new ArgumentNullException("stack");
}
Debug.Assert(numops > 0 , "UndoOperationGroup : numops should be > 0");
if (numops > stack.Count) {
numops = stack.Count;
}
undolist = new IUndoableOperation[numops];
for (int i = 0; i < numops; ++i) {
undolist[i] = stack.Pop();
}
}
public void Undo()
{
for (int i = 0; i < undolist.Length; ++i) {
undolist[i].Undo();
}
}
public void Redo()
{
for (int i = undolist.Length - 1; i >= 0; --i) {
undolist[i].Redo();
}
}
}
}

244
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/UndoStack.cs

@ -0,0 +1,244 @@ @@ -0,0 +1,244 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using System.ComponentModel;
namespace ICSharpCode.AvalonEdit.Document
{
/// <summary>
/// Undo stack implementation.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")]
public sealed class UndoStack : INotifyPropertyChanged
{
Stack<IUndoableOperation> undostack = new Stack<IUndoableOperation>();
Stack<IUndoableOperation> redostack = new Stack<IUndoableOperation>();
bool acceptChanges = true;
/// <summary>
/// Gets if the undo stack currently accepts changes.
/// Is false while an undo action is running.
/// </summary>
public bool AcceptChanges {
get { return acceptChanges; }
}
/// <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; }
}
int undoGroupDepth;
int actionCountInUndoGroup;
int optionalActionCount;
/// <summary>
/// Starts grouping changes.
/// Maintains a counter so that nested calls are possible.
/// </summary>
public void StartUndoGroup()
{
if (undoGroupDepth == 0) {
actionCountInUndoGroup = 0;
optionalActionCount = 0;
}
undoGroupDepth++;
//Util.LoggingService.Debug("Open 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) {
if (actionCountInUndoGroup == optionalActionCount) {
// only optional actions: don't store them
for (int i = 0; i < optionalActionCount; i++) {
undostack.Pop();
}
} else if (actionCountInUndoGroup > 1) {
undostack.Push(new UndoOperationGroup(undostack, actionCountInUndoGroup));
}
}
}
/// <summary>
/// Throws an InvalidOperationException if an undo group is current open.
/// </summary>
void VerifyNoUndoGroupOpen()
{
if (undoGroupDepth != 0) {
undoGroupDepth = 0;
throw new InvalidOperationException("No undo group should be open at this point");
}
}
/// <summary>
/// Call this method to undo the last operation on the stack
/// </summary>
public void Undo()
{
VerifyNoUndoGroupOpen();
if (undostack.Count > 0) {
acceptChanges = false;
IUndoableOperation uedit = undostack.Pop();
redostack.Push(uedit);
uedit.Undo();
acceptChanges = true;
if (undostack.Count == 0)
NotifyPropertyChanged("CanUndo");
if (redostack.Count == 1)
NotifyPropertyChanged("CanRedo");
}
}
/// <summary>
/// Call this method to redo the last undone operation
/// </summary>
public void Redo()
{
VerifyNoUndoGroupOpen();
if (redostack.Count > 0) {
acceptChanges = false;
IUndoableOperation uedit = redostack.Pop();
undostack.Push(uedit);
uedit.Redo();
acceptChanges = true;
if (redostack.Count == 0)
NotifyPropertyChanged("CanRedo");
if (undostack.Count == 1)
NotifyPropertyChanged("CanUndo");
}
}
/// <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 (acceptChanges) {
bool wasEmpty = undostack.Count == 0;
StartUndoGroup();
undostack.Push(operation);
actionCountInUndoGroup++;
if (isOptional)
optionalActionCount++;
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");
}
}
/// <summary>
/// Clears both the undo and redo stack.
/// </summary>
public void ClearAll()
{
VerifyNoUndoGroupOpen();
if (undostack.Count != 0) {
undostack.Clear();
NotifyPropertyChanged("CanUndo");
}
ClearRedoStack();
actionCountInUndoGroup = 0;
optionalActionCount = 0;
}
internal void AttachToDocument(TextDocument document)
{
document.UpdateStarted += document_UpdateStarted;
document.UpdateFinished += document_UpdateFinished;
document.Changing += document_Changing;
}
void document_UpdateStarted(object sender, EventArgs e)
{
StartUndoGroup();
}
void document_UpdateFinished(object sender, EventArgs e)
{
EndUndoGroup();
}
void document_Changing(object sender, DocumentChangeEventArgs e)
{
TextDocument document = (TextDocument)sender;
Push(new DocumentChangeOperation(
document,
e.Offset,
document.GetText(e.Offset, e.RemovalLength),
e.InsertedText));
}
/// <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));
}
}
}

64
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/WeakLineTracker.cs

@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
namespace ICSharpCode.AvalonEdit.Document
{
sealed class WeakLineTracker : ILineTracker
{
TextDocument textDocument;
WeakReference targetObject;
public WeakLineTracker(TextDocument textDocument, ILineTracker targetTracker)
{
this.textDocument = textDocument;
this.targetObject = new WeakReference(targetTracker);
}
void Deregister()
{
textDocument.LineTracker.Remove(this);
}
public void BeforeRemoveLine(DocumentLine line)
{
ILineTracker targetTracker = targetObject.Target as ILineTracker;
if (targetTracker != null)
targetTracker.BeforeRemoveLine(line);
else
Deregister();
}
public void SetLineLength(DocumentLine line, int newTotalLength)
{
ILineTracker targetTracker = targetObject.Target as ILineTracker;
if (targetTracker != null)
targetTracker.SetLineLength(line, newTotalLength);
else
Deregister();
}
public void LineInserted(DocumentLine insertionPos, DocumentLine newLine)
{
ILineTracker targetTracker = targetObject.Target as ILineTracker;
if (targetTracker != null)
targetTracker.LineInserted(insertionPos, newLine);
else
Deregister();
}
public void RebuildDocument()
{
ILineTracker targetTracker = targetObject.Target as ILineTracker;
if (targetTracker != null)
targetTracker.RebuildDocument();
else
Deregister();
}
}
}

79
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/AbstractMargin.cs

@ -0,0 +1,79 @@ @@ -0,0 +1,79 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Windows;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <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
{
/// <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>
public TextView TextView {
get { return (TextView)GetValue(TextViewProperty); }
set { SetValue(TextViewProperty, value); }
}
static void OnTextViewChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
{
((AbstractMargin)dp).OnTextViewChanged((TextView)e.OldValue, (TextView)e.NewValue);
}
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;
}
}
}

200
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/BackgroundGeometryBuilder.cs

@ -0,0 +1,200 @@ @@ -0,0 +1,200 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// Hel create a PathGeometry.
/// </summary>
public sealed class BackgroundGeometryBuilder
{
double cornerRadius = 3;
/// <summary>
/// Gets/sets the radius of the rounded corners.
/// </summary>
public double CornerRadius {
get { return cornerRadius; }
set { cornerRadius = value; }
}
/// <summary>
/// Creates a new BackgroundGeometryBuilder instance.
/// </summary>
public BackgroundGeometryBuilder()
{
}
/// <summary>
/// Adds the specified segments to the geometry.
/// </summary>
public void AddSegments(TextView textView, IEnumerable<ISegment> segments)
{
if (textView == null)
throw new ArgumentNullException("textView");
if (segments == null)
throw new ArgumentNullException("segments");
var scrollOffset = textView.ScrollOffset;
foreach (ISegment segment in segments) {
int segmentStart = segment.Offset;
int segmentEnd = segment.Offset + segment.Length;
foreach (VisualLine vl in textView.VisualLines) {
int vlStartOffset = vl.FirstDocumentLine.Offset;
if (vlStartOffset > segmentEnd)
break;
int vlEndOffset = vl.LastDocumentLine.Offset + vl.LastDocumentLine.Length;
if (vlEndOffset < segmentStart)
continue;
int segmentStartVC;
if (segmentStart < vlStartOffset)
segmentStartVC = 0;
else
segmentStartVC = vl.GetVisualColumn(segmentStart - vlStartOffset);
int segmentEndVC;
if (segmentEnd > vlEndOffset)
segmentEndVC = vl.VisualLength;
else
segmentEndVC = vl.GetVisualColumn(segmentEnd - vlStartOffset);
TextLine lastTextLine = vl.TextLines.Last();
foreach (TextLine line in vl.TextLines) {
double y = vl.GetTextLineVisualTop(line);
int visualStartCol = vl.GetTextLineVisualStartColumn(line);
int visualEndCol = visualStartCol + line.Length;
if (line != lastTextLine)
visualEndCol -= line.TrailingWhitespaceLength;
if (segmentEndVC < visualStartCol)
break;
if (segmentStartVC > visualEndCol)
continue;
double left = line.GetDistanceFromCharacterHit(new CharacterHit(Math.Max(segmentStartVC, visualStartCol), 0));
double right = line.GetDistanceFromCharacterHit(new CharacterHit(Math.Min(segmentEndVC, visualEndCol), 0));
y -= scrollOffset.Y;
left -= scrollOffset.X;
right -= scrollOffset.X;
AddRectangle(left, y, right, y + line.Height);
}
}
}
}
PathFigureCollection figures = new PathFigureCollection();
PathFigure figure;
int insertionIndex;
double lastTop, lastBottom;
double lastLeft, lastRight;
/// <summary>
/// Adds a rectangle to the geometry.
/// </summary>
public void AddRectangle(double left, double top, double right, double bottom)
{
if (!top.IsClose(lastBottom)) {
CloseFigure();
}
if (figure == null) {
figure = new PathFigure();
figure.StartPoint = new Point(left, top + cornerRadius);
if (Math.Abs(left - right) > cornerRadius) {
figure.Segments.Add(MakeArc(left + cornerRadius, top, SweepDirection.Clockwise));
figure.Segments.Add(MakeLineSegment(right - cornerRadius, top));
figure.Segments.Add(MakeArc(right, top + cornerRadius, SweepDirection.Clockwise));
}
figure.Segments.Add(MakeLineSegment(right, bottom - cornerRadius));
insertionIndex = figure.Segments.Count;
//figure.Segments.Add(MakeArc(left, bottom - cornerRadius, SweepDirection.Clockwise));
} else {
if (!lastRight.IsClose(right)) {
double cr = right < lastRight ? -cornerRadius : cornerRadius;
SweepDirection dir1 = right < lastRight ? SweepDirection.Clockwise : SweepDirection.Counterclockwise;
SweepDirection dir2 = right < lastRight ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;
figure.Segments.Insert(insertionIndex++, MakeArc(lastRight + cr, lastBottom, dir1));
figure.Segments.Insert(insertionIndex++, MakeLineSegment(right - cr, top));
figure.Segments.Insert(insertionIndex++, MakeArc(right, top + cornerRadius, dir2));
}
figure.Segments.Insert(insertionIndex++, MakeLineSegment(right, bottom - cornerRadius));
figure.Segments.Insert(insertionIndex, MakeLineSegment(lastLeft, lastTop + cornerRadius));
if (!lastLeft.IsClose(left)) {
double cr = left < lastLeft ? cornerRadius : -cornerRadius;
SweepDirection dir1 = left < lastLeft ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;
SweepDirection dir2 = left < lastLeft ? SweepDirection.Clockwise : SweepDirection.Counterclockwise;
figure.Segments.Insert(insertionIndex, MakeArc(lastLeft, lastBottom - cornerRadius, dir1));
figure.Segments.Insert(insertionIndex, MakeLineSegment(lastLeft - cr, lastBottom));
figure.Segments.Insert(insertionIndex, MakeArc(left + cr, lastBottom, dir2));
}
}
this.lastTop = top;
this.lastBottom = bottom;
this.lastLeft = left;
this.lastRight = right;
}
ArcSegment MakeArc(double x, double y, SweepDirection dir)
{
ArcSegment arc = new ArcSegment(
new Point(x, y),
new Size(cornerRadius, cornerRadius),
0, false, dir, true);
arc.Freeze();
return arc;
}
static LineSegment MakeLineSegment(double x, double y)
{
LineSegment ls = new LineSegment(new Point(x, y), true);
ls.Freeze();
return ls;
}
void CloseFigure()
{
if (figure != null) {
figure.Segments.Insert(insertionIndex, MakeLineSegment(lastLeft, lastTop + cornerRadius));
if (Math.Abs(lastLeft - lastRight) > cornerRadius) {
figure.Segments.Insert(insertionIndex, MakeArc(lastLeft, lastBottom - cornerRadius, SweepDirection.Clockwise));
figure.Segments.Insert(insertionIndex, MakeLineSegment(lastLeft + cornerRadius, lastBottom));
figure.Segments.Insert(insertionIndex, MakeArc(lastRight - cornerRadius, lastBottom, SweepDirection.Clockwise));
}
figure.IsClosed = true;
figures.Add(figure);
figure = null;
}
}
/// <summary>
/// Creates the geometry.
/// Returns null when the geometry is empty!
/// </summary>
public PathGeometry CreateGeometry()
{
CloseFigure();
if (figures.Count != 0) {
PathGeometry g = new PathGeometry(figures);
g.Freeze();
return g;
} else {
return null;
}
}
}
}

276
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/Caret.cs

@ -0,0 +1,276 @@ @@ -0,0 +1,276 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.TextFormatting;
using System.Windows.Threading;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// Helper class with caret-related methods.
/// </summary>
public sealed class Caret
{
readonly TextArea textArea;
readonly TextView textView;
CaretAdorner caretAdorner;
bool visible;
internal Caret(TextArea textArea)
{
this.textArea = textArea;
this.textView = textArea.TextView;
position = new TextViewPosition(1, 1, 0);
caretAdorner = new CaretAdorner(textView);
textView.Adorners.Add(caretAdorner);
textView.VisualLinesChanged += TextView_VisualLinesChanged;
textView.ScrollOffsetChanged += TextView_ScrollOffsetChanged;
}
void TextView_VisualLinesChanged(object sender, EventArgs e)
{
if (visible) {
Show();
}
}
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.
/// </summary>
public TextViewPosition Position {
get { return position; }
set {
if (position != value) {
position = value;
storedCaretOffset = -1;
//Debug.WriteLine("Caret position changing to " + value);
ValidatePosition();
if (PositionChanged != null) {
PositionChanged(this, EventArgs.Empty);
}
Debug.WriteLine("Caret position changed to " + value);
if (visible)
Show();
}
}
}
/// <summary>
/// Gets the caret line.
/// </summary>
public int Line {
get { return position.Line; }
}
/// <summary>
/// Gets the caret column.
/// </summary>
public int Column {
get { return position.Column; }
}
int storedCaretOffset;
internal void OnDocumentChanging()
{
storedCaretOffset = this.Offset;
}
internal void OnDocumentChanged(DocumentChangeEventArgs e)
{
if (storedCaretOffset >= 0) {
int newCaretOffset = e.GetNewOffset(storedCaretOffset, AnchorMovementType.AfterInsertion);
TextDocument document = textArea.Document;
if (document != null) {
// keep visual column
this.Position = new TextViewPosition(document.GetLocation(newCaretOffset), position.VisualColumn);
}
}
storedCaretOffset = -1;
}
/// <summary>
/// Gets the caret offset.
/// </summary>
public int Offset {
get {
TextDocument document = textArea.Document;
if (document == null) {
return 0;
} else {
ValidatePosition();
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.
/// </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.
/// </summary>
public event EventHandler PositionChanged;
/// <summary>
/// Validates the visual column of the caret using the specified visual line.
/// The visual line must contain the caret offset.
/// </summary>
public void ValidateVisualColumn(VisualLine visualLine)
{
if (visualLine == null)
throw new ArgumentNullException("visualLine");
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 position (first try forwards)
int newVisualColumn = visualLine.GetNextCaretPosition(position.VisualColumn - 1, false, CaretPositioningMode.Normal);
if (newVisualColumn < 0) {
// then try backwards
newVisualColumn = visualLine.GetNextCaretPosition(position.VisualColumn + 1, true, CaretPositioningMode.Normal);
}
if (newVisualColumn >= 0 && newVisualColumn != position.VisualColumn) {
int newOffset = visualLine.GetRelativeOffset(newVisualColumn) + firstDocumentLineOffset;
this.Position = new TextViewPosition(textView.Document.GetLocation(newOffset), newVisualColumn);
}
}
Rect CalcCaretRectangle(VisualLine visualLine)
{
ValidateVisualColumn(visualLine);
TextLine textLine = visualLine.GetTextLine(position.VisualColumn);
double xPos = textLine.GetDistanceFromCharacterHit(new CharacterHit(position.VisualColumn, 0));
double lineTop = visualLine.GetTextLineVisualTop(textLine);
double lineBottom = lineTop + textLine.Height;
double fontSize = (double)textArea.GetValue(TextBlock.FontSizeProperty);
return new Rect(xPos,
lineBottom - fontSize,
SystemParameters.CaretWidth,
fontSize);
}
/// <summary>
/// Scrolls the text view so that the caret is visible.
/// </summary>
public void BringCaretToView()
{
if (textView != null) {
ValidatePosition();
VisualLine visualLine = textView.GetOrConstructVisualLine(textView.Document.GetLineByNumber(position.Line));
textView.MakeVisible(CalcCaretRectangle(visualLine));
}
}
/// <summary>
/// Makes the caret visible and updates its on-screen position.
/// </summary>
public void Show()
{
visible = true;
if (!showScheduled) {
showScheduled = true;
textArea.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(ShowInternal));
}
}
bool showScheduled;
void ShowInternal()
{
showScheduled = false;
if (caretAdorner != null && textView != null) {
VisualLine visualLine = textView.GetVisualLine(position.Line);
if (visualLine != null) {
caretAdorner.Show(CalcCaretRectangle(visualLine));
} else {
caretAdorner.Hide();
}
}
}
/// <summary>
/// Makes the caret invisible.
/// </summary>
public void Hide()
{
visible = false;
if (caretAdorner != null) {
caretAdorner.Hide();
}
}
}
}

86
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/CaretAdorner.cs

@ -0,0 +1,86 @@ @@ -0,0 +1,86 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Gui
{
sealed class CaretAdorner : FrameworkElement
{
TextView textView;
bool isVisible;
Rect caretRectangle;
DoubleAnimationUsingKeyFrames blinkAnimation;
public CaretAdorner(TextView textView)
{
this.textView = textView;
this.IsHitTestVisible = false;
blinkAnimation = new DoubleAnimationUsingKeyFrames();
blinkAnimation.KeyFrames.Add(new DiscreteDoubleKeyFrame(1, KeyTime.FromPercent(0)));
blinkAnimation.KeyFrames.Add(new DiscreteDoubleKeyFrame(0, KeyTime.FromPercent(0.5)));
blinkAnimation.RepeatBehavior = RepeatBehavior.Forever;
}
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) {
BeginAnimation(OpacityProperty, null);
// duration = 2*blink time
blinkAnimation.Duration = new Duration(blinkTime + blinkTime);
BeginAnimation(OpacityProperty, blinkAnimation);
}
}
void StopBlinkAnimation()
{
BeginAnimation(OpacityProperty, null);
}
protected override Size MeasureOverride(Size constraint)
{
return caretRectangle.Size;
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
if (isVisible) {
drawingContext.DrawRectangle(Brushes.Black, null,
new Rect(caretRectangle.X - textView.HorizontalOffset,
caretRectangle.Y - textView.VerticalOffset,
caretRectangle.Width,
caretRectangle.Height));
}
}
}
}

324
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/CaretNavigationCommandHandler.cs

@ -0,0 +1,324 @@ @@ -0,0 +1,324 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media.TextFormatting;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <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. This is used for word-selection using the mouse.
/// </summary>
WordBorder,
/// <summary>
/// Stop only at the beginning of words. This is used for Ctrl+Left/Ctrl+Right.
/// </summary>
WordStart
}
static class CaretNavigationCommandHandler
{
public static readonly CommandBindingCollection CommandBindings = new CommandBindingCollection();
public static readonly InputBindingCollection InputBindings = new InputBindingCollection();
static void AddBinding(ICommand command, ModifierKeys modifiers, Key key, ExecutedRoutedEventHandler handler)
{
CommandBindings.Add(new CommandBinding(command, handler));
InputBindings.Add(new KeyBinding(command, key, modifiers));
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")]
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) {
textArea.Caret.Offset = textArea.Document.TextLength;
textArea.Selection = new SimpleSelection(0, textArea.Document.TextLength);
textArea.Caret.BringCaretToView();
}
}
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.Position.Line);
VisualLine visualLine = textArea.TextView.GetOrConstructVisualLine(caretLine);
textArea.Caret.ValidateVisualColumn(visualLine);
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, false, CaretPositioningMode.WordStart);
// in empty lines (whitespace only), jump to the end
if (newVC < 0)
newVC = visualLine.VisualLength;
// when the caret is already at the start of the text, jump to start before whitespace
if (newVC == textArea.Caret.Position.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, false, mode);
if (pos >= 0) {
SetCaretPosition(textArea, pos, visualLine.GetRelativeOffset(pos) + visualLine.FirstDocumentLine.Offset);
} else {
// move to start of next line
SetCaretPosition(textArea, 0, visualLine.LastDocumentLine.Offset + visualLine.LastDocumentLine.TotalLength);
}
}
static void MoveCaretLeft(TextArea textArea, TextViewPosition caretPosition, VisualLine visualLine, CaretPositioningMode mode)
{
int pos = visualLine.GetNextCaretPosition(caretPosition.VisualColumn, true, mode);
if (pos >= 0) {
SetCaretPosition(textArea, pos, visualLine.GetRelativeOffset(pos) + visualLine.FirstDocumentLine.Offset);
} else if (caretPosition.Line > 1) {
DocumentLine prevLine = textArea.Document.GetLineByNumber(caretPosition.Line - 1);
SetCaretPosition(textArea, -1, prevLine.Offset + prevLine.Length);
}
}
#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.GetTextLineVisualTop(textLine) + textLine.Height / 2;
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.GetTextLineByVisualTop(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
}
}

121
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/CollapsedLineSection.cs

@ -0,0 +1,121 @@ @@ -0,0 +1,121 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.ComponentModel;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// Represents a collapsed line section.
/// Use the Uncollapse() method to uncollapse the section.
/// </summary>
public sealed class CollapsedLineSection : INotifyPropertyChanged
{
bool isCollapsed = true;
DocumentLine start, end;
HeightTree heightTree;
#if DEBUG
internal string ID;
static int nextId;
#else
const string ID = "";
#endif
internal CollapsedLineSection(HeightTree heightTree, DocumentLine start, DocumentLine end)
{
this.heightTree = heightTree;
this.start = start;
this.end = end;
#if DEBUG
this.ID = "#" + (nextId++);
#endif
}
/// <summary>
/// Gets if the document line is collapsed.
/// This property initially is true and turns to false when uncollapsing the section.
/// </summary>
public bool IsCollapsed {
get { return isCollapsed; }
}
/// <summary>
/// Gets the start line of the section.
/// When the section is uncollapsed or the text containing it is deleted,
/// this property returns null.
/// </summary>
public DocumentLine Start {
get { return start; }
internal set {
start = value;
// TODO: raised property changed event (but only after the operation is complete)
}
}
/// <summary>
/// Gets the end line of the section.
/// When the section is uncollapsed or the text containing it is deleted,
/// this property returns null.
/// </summary>
public DocumentLine End {
get { return end; }
internal set {
end = value;
// TODO: raised property changed event (but only after the operation is complete)
}
}
/// <summary>
/// Uncollapses the section.
/// This causes the Start and End properties to be set to null!
/// Runtime: O(log(n))
/// </summary>
/// <exception cref="InvalidOperationException">
/// The section is already uncollapsed, or the text containing the section was deleted.
/// </exception>
public void Uncollapse()
{
if (start == null)
throw new InvalidOperationException();
heightTree.Uncollapse(this);
#if DEBUG
heightTree.CheckProperties();
#endif
start = end = null;
isCollapsed = false;
NotifyPropertyChanged("Start");
NotifyPropertyChanged("End");
NotifyPropertyChanged("IsCollapsed");
}
/// <summary>
/// Is raised when of the properties Start,End,IsCollapsed changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
/// <summary>
/// Gets a string representation of the collapsed section.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.Int32.ToString")]
public override string ToString()
{
return "[CollapsedSection " + ID + " Start=" + (start != null ? start.LineNumber.ToString() : "null")
+ " End=" + (end != null ? end.LineNumber.ToString() : "null") + " IsCollapsed=" + isCollapsed + "]";
}
}
}

81
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/ColorizingTransformer.cs

@ -0,0 +1,81 @@ @@ -0,0 +1,81 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// Base class for <see cref="IVisualLineTransformer"/> that helps
/// splitting visual elements so that colors (and other text properties) can be easily assigned
/// to individual words/characters.
/// </summary>
public abstract class ColorizingTransformer : IVisualLineTransformer
{
/// <summary>
/// Gets the list of elements currently being transformed.
/// </summary>
protected IList<VisualLineElement> CurrentElements { get; private set; }
/// <summary>
/// <see cref="IVisualLineTransformer.Transform"/> implementation.
/// Sets <see cref="CurrentElements"/> and calls <see cref="Colorize"/>.
/// </summary>
public void Transform(ITextRunConstructionContext context, IList<VisualLineElement> elements)
{
this.CurrentElements = elements;
Colorize(context);
this.CurrentElements = null;
}
/// <summary>
/// Performs the colorization.
/// </summary>
protected abstract void Colorize(ITextRunConstructionContext context);
/// <summary>
/// Changes visual element properties.
/// This method accesses <see cref="CurrentElements"/>, so it must be called only during
/// a <see cref="Transform"/> call.
/// This method splits <see cref="VisualLineElement"/>s as necessary to ensure that the region
/// can be colored by setting the <see cref="VisualLineElement.TextRunProperties"/> of whole elements,
/// and then calls the <paramref name="action"/> on all elements in the region.
/// </summary>
/// <param name="visualStartColumn">Start visual column of the region to change</param>
/// <param name="visualEndColumn">End visual column of the region to change</param>
/// <param name="action">Action that changes an individual <see cref="VisualLineElement"/>.</param>
protected void ChangeVisualElements(int visualStartColumn, int visualEndColumn, Action<VisualLineElement> action)
{
if (action == null)
throw new ArgumentNullException("action");
for (int i = 0; i < CurrentElements.Count; i++) {
VisualLineElement e = CurrentElements[i];
if (e.VisualColumn > visualEndColumn)
break;
if (e.VisualColumn < visualStartColumn &&
e.VisualColumn + e.VisualLength > visualStartColumn)
{
if (e.CanSplit) {
e.Split(visualStartColumn, CurrentElements, i--);
continue;
}
}
if (e.VisualColumn >= visualStartColumn && e.VisualColumn < visualEndColumn) {
if (e.VisualColumn + e.VisualLength > visualEndColumn) {
if (e.CanSplit) {
e.Split(visualEndColumn, CurrentElements, i--);
continue;
}
} else {
action(e);
}
}
}
}
}
}

83
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/DocumentColorizingTransformer.cs

@ -0,0 +1,83 @@ @@ -0,0 +1,83 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Linq;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// Base class for <see cref="IVisualLineTransformer"/> that helps
/// colorizing the document. Derived classes can work with document lines
/// and text offsets and this class takes care of the visual lines and visual columns.
/// </summary>
public abstract class DocumentColorizingTransformer : ColorizingTransformer
{
DocumentLine currentDocumentLine;
int firstLineStart;
int currentDocumentLineStartOffset, currentDocumentLineEndOffset;
/// <summary>
/// Gets the current ITextRunConstructionContext.
/// </summary>
protected ITextRunConstructionContext CurrentContext { get; private set; }
/// <inheritdoc/>
protected override void Colorize(ITextRunConstructionContext context)
{
this.CurrentContext = context;
currentDocumentLine = context.VisualLine.FirstDocumentLine;
firstLineStart = currentDocumentLineStartOffset = currentDocumentLine.Offset;
currentDocumentLineEndOffset = currentDocumentLineStartOffset + currentDocumentLine.Length;
if (context.VisualLine.FirstDocumentLine == context.VisualLine.LastDocumentLine) {
ColorizeLine(currentDocumentLine);
} else {
ColorizeLine(currentDocumentLine);
// ColorizeLine modifies the visual line elements, loop through a copy of the line elements
foreach (VisualLineElement e in context.VisualLine.Elements.ToArray()) {
int elementOffset = firstLineStart + e.RelativeTextOffset;
if (elementOffset >= currentDocumentLineEndOffset) {
currentDocumentLine = context.Document.GetLineByOffset(elementOffset);
currentDocumentLineStartOffset = currentDocumentLine.Offset;
currentDocumentLineEndOffset = currentDocumentLineStartOffset + currentDocumentLine.Length;
ColorizeLine(currentDocumentLine);
}
}
}
currentDocumentLine = null;
this.CurrentContext = null;
}
/// <summary>
/// Override this method to colorize an individual document line.
/// </summary>
protected abstract void ColorizeLine(DocumentLine line);
/// <summary>
/// Changes a part of the current document line.
/// </summary>
/// <param name="startOffset">Start offset of the region to change</param>
/// <param name="endOffset">End offset of the region to change</param>
/// <param name="action">Action that changes an individual <see cref="VisualLineElement"/>.</param>
protected void ChangeLinePart(int startOffset, int endOffset, Action<VisualLineElement> action)
{
if (startOffset < currentDocumentLineStartOffset || startOffset > currentDocumentLineEndOffset)
throw new ArgumentOutOfRangeException("startOffset", startOffset, "Value must be between " + currentDocumentLineStartOffset + " and " + currentDocumentLineEndOffset);
if (endOffset < startOffset || endOffset > currentDocumentLineEndOffset)
throw new ArgumentOutOfRangeException("endOffset", endOffset, "Value must be between " + startOffset + " and " + currentDocumentLineEndOffset);
VisualLine vl = this.CurrentContext.VisualLine;
int visualStart = vl.GetVisualColumn(startOffset - firstLineStart);
int visualEnd = vl.GetVisualColumn(endOffset - firstLineStart);
if (visualStart < visualEnd) {
ChangeVisualElements(visualStart, visualEnd, action);
}
}
}
}

238
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/EditingCommandHandler.cs

@ -0,0 +1,238 @@ @@ -0,0 +1,238 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Linq;
using System.Diagnostics;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Input;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Gui
{
static class EditingCommandHandler
{
public static readonly CommandBindingCollection CommandBindings = new CommandBindingCollection();
public static readonly InputBindingCollection InputBindings = new InputBindingCollection();
static void AddBinding(ICommand command, ModifierKeys modifiers, Key key, ExecutedRoutedEventHandler handler)
{
CommandBindings.Add(new CommandBinding(command, handler));
InputBindings.Add(new KeyBinding(command, key, modifiers));
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")]
static EditingCommandHandler()
{
CommandBindings.Add(new CommandBinding(ApplicationCommands.Delete, OnDelete(ApplicationCommands.NotACommand), HasSomethingSelected));
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));
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, HasSomethingSelected));
CommandBindings.Add(new CommandBinding(ApplicationCommands.Cut, OnCut, HasSomethingSelected));
CommandBindings.Add(new CommandBinding(ApplicationCommands.Paste, OnPaste, CanPaste));
}
static TextArea GetTextArea(object target)
{
return target as TextArea;
}
#region EnterLineBreak
static void OnEnter(object target, ExecutedRoutedEventArgs args)
{
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.Document != null) {
string newLine = GetLineDelimiter(textArea.Document, textArea.Caret.Line);
textArea.ReplaceSelectionWithText(newLine);
textArea.Caret.BringCaretToView();
args.Handled = true;
}
}
static string GetLineDelimiter(TextDocument document, int lineNumber)
{
DocumentLine line = document.GetLineByNumber(lineNumber);
if (line.DelimiterLength == 0) {
// TODO: add line delimiter setting
if (lineNumber > 1)
line = document.GetLineByNumber(lineNumber - 1);
else
return Environment.NewLine;
}
return document.GetText(line.Offset + line.Length, line.DelimiterLength);
}
#endregion
#region Tab
// TODO: make these per-textarea options
const string indentationString = "\t";
const int tabSize = 4;
static void OnTab(object target, ExecutedRoutedEventArgs args)
{
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.Document != null) {
textArea.Document.BeginUpdate();
try {
if (textArea.Selection.GetIsMultiline(textArea.Document)) {
DocumentLine start = textArea.Document.GetLineByOffset(textArea.Selection.SurroundingSegment.Offset);
DocumentLine end = textArea.Document.GetLineByOffset(textArea.Selection.SurroundingSegment.GetEndOffset());
while (true) {
int offset = start.Offset;
if (textArea.ReadOnlySectionProvider.CanInsert(offset))
textArea.Document.Insert(offset, indentationString);
if (start == end)
break;
start = textArea.Document.GetLineByNumber(start.LineNumber + 1);
}
} else {
textArea.ReplaceSelectionWithText(indentationString);
}
} finally {
textArea.Document.EndUpdate();
}
textArea.Caret.BringCaretToView();
args.Handled = true;
}
}
static void OnShiftTab(object target, ExecutedRoutedEventArgs args)
{
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.Document != null) {
textArea.Document.BeginUpdate();
try {
DocumentLine start, end;
if (textArea.Selection.IsEmpty) {
start = end = textArea.Document.GetLineByNumber(textArea.Caret.Line);
} else {
start = textArea.Document.GetLineByOffset(textArea.Selection.SurroundingSegment.Offset);
end = textArea.Document.GetLineByOffset(textArea.Selection.SurroundingSegment.GetEndOffset());
}
while (true) {
int offset = start.Offset;
ISegment s = GetFirstIndentationSegment(textArea.Document, offset);
if (s.Length > 0) {
s = textArea.ReadOnlySectionProvider.GetDeletableSegments(s).FirstOrDefault();
if (s != null && s.Length > 0) {
textArea.Document.Remove(s.Offset, s.Length);
}
}
if (start == end)
break;
start = textArea.Document.GetLineByNumber(start.LineNumber + 1);
}
} finally {
textArea.Document.EndUpdate();
}
textArea.Caret.BringCaretToView();
args.Handled = true;
}
}
static ISegment GetFirstIndentationSegment(TextDocument document, int offset)
{
int pos = offset;
while (pos < document.TextLength) {
char c = document.GetCharAt(pos);
if (c == '\t') {
if (pos == offset)
return new SimpleSegment(offset, 1);
else
break;
} else if (c == ' ') {
if (pos - offset >= tabSize)
break;
} else {
break;
}
// continue only if c==' ' and (pos-offset)<tabSize
pos++;
}
return new SimpleSegment(offset, pos - offset);
}
#endregion
#region Delete
static ExecutedRoutedEventHandler OnDelete(RoutedUICommand selectingCommand)
{
return (target, args) => {
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.Document != null) {
if (textArea.Selection.IsEmpty)
selectingCommand.Execute(args.Parameter, textArea);
textArea.RemoveSelectedText();
textArea.Caret.BringCaretToView();
args.Handled = true;
}
};
}
#endregion
#region Clipboard commands
static void HasSomethingSelected(object target, CanExecuteRoutedEventArgs args)
{
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.Document != null) {
args.CanExecute = !textArea.Selection.IsEmpty;
args.Handled = true;
}
}
static void OnCopy(object target, ExecutedRoutedEventArgs args)
{
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.Document != null) {
Clipboard.SetText(textArea.Selection.GetText(textArea.Document));
args.Handled = true;
}
}
static void OnCut(object target, ExecutedRoutedEventArgs args)
{
TextArea textArea = GetTextArea(target);
if (textArea != null && textArea.Document != null) {
Clipboard.SetText(textArea.Selection.GetText(textArea.Document));
textArea.RemoveSelectedText();
textArea.Caret.BringCaretToView();
args.Handled = true;
}
}
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) {
// TODO: normalize newlines on paste
textArea.ReplaceSelectionWithText(Clipboard.GetText());
textArea.Caret.BringCaretToView();
args.Handled = true;
}
}
#endregion
}
}

101
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/FoldingElementGenerator.cs

@ -0,0 +1,101 @@ @@ -0,0 +1,101 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// A <see cref="VisualLineElementGenerator"/> that produces line elements for folded <see cref="FoldingSection"/>s.
/// </summary>
public class FoldingElementGenerator : VisualLineElementGenerator
{
/// <summary>
/// Gets/Sets the folding manager from which the foldings should be shown.
/// </summary>
public FoldingManager FoldingManager { get; set; }
/// <inheritdoc/>
public override void StartGeneration(ITextRunConstructionContext context)
{
base.StartGeneration(context);
if (FoldingManager != null) {
if (context.TextView != FoldingManager.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;
foreach (FoldingSection fs in FoldingManager.GetFoldingsAt(offset)) {
if (fs.IsFolded) {
if (fs.EndOffset > foldedUntil)
foldedUntil = fs.EndOffset;
}
}
if (foldedUntil > offset) {
FormattedText text = new FormattedText(
"...",
CurrentContext.GlobalTextRunProperties.CultureInfo,
FlowDirection.LeftToRight,
CurrentContext.GlobalTextRunProperties.Typeface,
CurrentContext.GlobalTextRunProperties.FontRenderingEmSize,
Brushes.Gray
);
return new FoldingLineElement(text, foldedUntil - offset);
} else {
return null;
}
}
sealed class FoldingLineElement : FormattedTextElement
{
public FoldingLineElement(FormattedText text, int documentLength) : base(text, documentLength)
{
}
public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context)
{
return new FoldingLineTextRun(this, this.TextRunProperties);
}
}
sealed class FoldingLineTextRun : FormattedTextRun
{
public FoldingLineTextRun(FormattedTextElement element, TextRunProperties properties)
: base(element, properties)
{
}
public override void Draw(DrawingContext drawingContext, Point origin, bool rightToLeft, bool sideways)
{
Rect r = ComputeBoundingBox(rightToLeft, sideways);
r.Offset(origin.X, origin.Y - element.text.Baseline);
drawingContext.DrawRectangle(null, new Pen(Brushes.Gray, 1), r);
base.Draw(drawingContext, origin, rightToLeft, sideways);
}
}
}
}

105
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/FoldingManager.cs

@ -0,0 +1,105 @@ @@ -0,0 +1,105 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// Stores a list of foldings for a specific TextView and TextDocument.
/// </summary>
public class FoldingManager
{
internal readonly TextView textView;
internal readonly TextDocument document;
readonly TextSegmentCollection<FoldingSection> foldings;
/// <summary>
/// Creates a new FoldingManager instance.
/// </summary>
public FoldingManager(TextView textView, TextDocument document)
{
if (textView == null)
throw new ArgumentNullException("textView");
if (document == null)
throw new ArgumentNullException("document");
this.textView = textView;
this.document = document;
this.foldings = new TextSegmentCollection<FoldingSection>(document);
}
/// <summary>
/// Creates a folding for the specified text section.
/// </summary>
public FoldingSection CreateFolding(int startOffset, int endOffset)
{
if (startOffset >= endOffset)
throw new ArgumentException("startOffset must be less than endOffset");
FoldingSection fs = new FoldingSection(this, startOffset, endOffset);
foldings.Add(fs);
textView.Redraw();
return fs;
}
internal void RemoveFolding(FoldingSection fs)
{
document.VerifyAccess();
foldings.Remove(fs);
textView.Redraw();
}
/// <summary>
/// Gets the first offset greater or equal to <paramref name="startOffset"/> where a folded folding starts.
/// Returns -1 if there are no foldings after <paramref name="startOffset"/>.
/// </summary>
public int GetNextFoldedFoldingStart(int startOffset)
{
FoldingSection fs = foldings.FindFirstSegmentWithStartAfter(startOffset);
while (fs != null && !fs.IsFolded)
fs = foldings.GetNextSegment(fs);
return fs != null ? fs.StartOffset : -1;
}
/// <summary>
/// Gets the first folding with a <see cref="TextSegment.StartOffset"/> greater or equal to
/// <paramref name="startOffset"/>.
/// Returns null if there are no foldings after <paramref name="startOffset"/>.
/// </summary>
public FoldingSection GetNextFolding(int startOffset)
{
// TODO: returns the longest folding instead of any folding at the first position after startOffset
return foldings.FindFirstSegmentWithStartAfter(startOffset);
}
/// <summary>
/// Gets all foldings that start exactly at <paramref name="startOffset"/>.
/// </summary>
public ReadOnlyCollection<FoldingSection> GetFoldingsAt(int startOffset)
{
List<FoldingSection> result = new List<FoldingSection>();
FoldingSection fs = foldings.FindFirstSegmentWithStartAfter(startOffset);
while (fs != null && fs.StartOffset == startOffset) {
result.Add(fs);
fs = foldings.GetNextSegment(fs);
}
return result.AsReadOnly();
}
/// <summary>
/// Gets all foldings that contain <param name="offset" />.
/// </summary>
public ReadOnlyCollection<FoldingSection> GetFoldingsContaining(int offset)
{
return foldings.FindSegmentsContaining(offset);
}
}
}

230
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/FoldingMargin.cs

@ -0,0 +1,230 @@ @@ -0,0 +1,230 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// A margin that shows markers for foldings and allows to expand/collapse the foldings.
/// </summary>
public class FoldingMargin : AbstractMargin
{
/// <summary>
/// Gets/Sets the folding manager from which the foldings should be shown.
/// </summary>
public FoldingManager FoldingManager { get; set; }
internal const double SizeFactor = Constants.PixelPerPoint;
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
{
foreach (FoldingMarginMarker m in markers) {
m.Measure(availableSize);
}
return new Size(SizeFactor * (double)GetValue(TextBlock.FontSizeProperty), 0);
}
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
{
foreach (FoldingMarginMarker m in markers) {
int visualColumn = m.VisualLine.GetVisualColumn(m.FoldingSection.StartOffset - m.VisualLine.FirstDocumentLine.Offset);
TextLine textLine = m.VisualLine.GetTextLine(visualColumn);
double yPos = m.VisualLine.GetTextLineVisualTop(textLine) - TextView.VerticalOffset;
yPos += (textLine.Height - m.DesiredSize.Height) / 2;
double xPos = (finalSize.Width - m.DesiredSize.Width) / 2;
m.Arrange(new Rect(new Point(xPos, yPos), m.DesiredSize));
}
return base.ArrangeOverride(finalSize);
}
/// <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;
}
TextViewVisualLinesChanged(null, null);
}
List<FoldingMarginMarker> markers = new List<FoldingMarginMarker>();
void TextViewVisualLinesChanged(object sender, EventArgs e)
{
foreach (FoldingMarginMarker m in markers) {
RemoveVisualChild(m);
}
markers.Clear();
InvalidateVisual();
if (TextView != null && FoldingManager != null) {
foreach (VisualLine line in TextView.VisualLines) {
FoldingSection fs = FoldingManager.GetNextFolding(line.FirstDocumentLine.Offset);
if (fs == null)
continue;
if (fs.StartOffset <= line.LastDocumentLine.Offset + line.LastDocumentLine.Length) {
FoldingMarginMarker m = new FoldingMarginMarker {
IsExpanded = !fs.IsFolded,
SnapsToDevicePixels = true,
VisualLine = line,
FoldingSection = fs
};
markers.Add(m);
AddVisualChild(m);
m.IsMouseDirectlyOverChanged += delegate { InvalidateVisual(); };
InvalidateMeasure();
continue;
}
}
}
}
/// <inheritdoc/>
protected override int VisualChildrenCount {
get { return markers.Count; }
}
/// <inheritdoc/>
protected override Visual GetVisualChild(int index)
{
return markers[index];
}
/// <inheritdoc/>
protected override void OnRender(DrawingContext drawingContext)
{
if (TextView.VisualLines.Count == 0 || FoldingManager == null)
return;
Pen grayPen = new Pen(Brushes.Gray, 1);
grayPen.Freeze();
Pen blackPen = new Pen(Brushes.Black, 1);
blackPen.Freeze();
var allTextLines = TextView.VisualLines.SelectMany(vl => vl.TextLines).ToList();
Pen[] colors = new Pen[allTextLines.Count + 1];
Pen[] endMarker = new Pen[allTextLines.Count];
int viewStartOffset = TextView.VisualLines[0].FirstDocumentLine.Offset;
DocumentLine lastVisibleLine = TextView.VisualLines.Last().LastDocumentLine;
int viewEndOffset = lastVisibleLine.Offset + lastVisibleLine.Length;
var foldings = FoldingManager.GetFoldingsContaining(viewStartOffset);
int maxEndOffset = 0;
foreach (FoldingSection fs in foldings) {
int end = fs.EndOffset;
if (end < viewEndOffset && !fs.IsFolded) {
int textLineNr = GetTextLineIndexFromOffset(allTextLines, end);
if (textLineNr >= 0) {
endMarker[textLineNr] = grayPen;
}
}
if (end > maxEndOffset && fs.StartOffset < viewStartOffset) {
maxEndOffset = end;
}
}
if (maxEndOffset > 0) {
if (maxEndOffset > viewEndOffset) {
for (int i = 0; i < colors.Length; i++) {
colors[i] = grayPen;
}
} else {
int maxTextLine = GetTextLineIndexFromOffset(allTextLines, maxEndOffset);
for (int i = 0; i <= maxTextLine; i++) {
colors[i] = grayPen;
}
}
}
foreach (FoldingMarginMarker marker in markers) {
int end = marker.FoldingSection.EndOffset;
int endTextLineNr = GetTextLineIndexFromOffset(allTextLines, end);
if (!marker.FoldingSection.IsFolded && endTextLineNr >= 0) {
if (marker.IsMouseDirectlyOver)
endMarker[endTextLineNr] = blackPen;
else if (endMarker[endTextLineNr] == null)
endMarker[endTextLineNr] = grayPen;
}
int startTextLineNr = GetTextLineIndexFromOffset(allTextLines, marker.FoldingSection.StartOffset);
if (startTextLineNr >= 0) {
for (int i = startTextLineNr + 1; i < colors.Length && i - 1 != endTextLineNr; i++) {
if (marker.IsMouseDirectlyOver)
colors[i] = blackPen;
else if (colors[i] == null)
colors[i] = grayPen;
}
}
}
double markerXPos = Math.Round(RenderSize.Width / 2);
double startY = 0;
Pen currentPen = colors[0];
int tlNumber = 0;
foreach (VisualLine vl in TextView.VisualLines) {
foreach (TextLine tl in vl.TextLines) {
if (endMarker[tlNumber] != null) {
double visualPos = GetVisualPos(vl, tl);
drawingContext.DrawLine(endMarker[tlNumber],
new Point(markerXPos, visualPos),
new Point(RenderSize.Width, visualPos));
}
if (colors[tlNumber + 1] != currentPen) {
double visualPos = GetVisualPos(vl, tl);
if (currentPen != null) {
drawingContext.DrawLine(currentPen,
new Point(markerXPos, startY),
new Point(markerXPos, visualPos));
}
currentPen = colors[tlNumber + 1];
startY = visualPos;
}
tlNumber++;
}
}
if (currentPen != null) {
drawingContext.DrawLine(currentPen,
new Point(markerXPos, startY),
new Point(markerXPos, RenderSize.Height));
}
base.OnRender(drawingContext);
}
double GetVisualPos(VisualLine vl, TextLine tl)
{
double pos = vl.GetTextLineVisualTop(tl) + tl.Height / 2 - TextView.VerticalOffset;
return Math.Round(pos) + 0.5;
}
int GetTextLineIndexFromOffset(List<TextLine> textLines, int offset)
{
int lineNumber = TextView.Document.GetLineByOffset(offset).LineNumber;
VisualLine vl = TextView.GetVisualLine(lineNumber);
if (vl != null) {
int relOffset = offset - vl.FirstDocumentLine.Offset;
TextLine line = vl.GetTextLine(vl.GetVisualColumn(relOffset));
return textLines.IndexOf(line);
}
return -1;
}
}
}

83
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/FoldingMarginMarker.cs

@ -0,0 +1,83 @@ @@ -0,0 +1,83 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace ICSharpCode.AvalonEdit.Gui
{
sealed class FoldingMarginMarker : UIElement
{
internal VisualLine VisualLine;
internal FoldingSection FoldingSection;
bool isExpanded;
public bool IsExpanded {
get { return isExpanded; }
set {
if (isExpanded != value) {
isExpanded = value;
InvalidateVisual();
}
if (FoldingSection != null)
FoldingSection.IsFolded = !value;
}
}
protected override void OnMouseDown(MouseButtonEventArgs e)
{
base.OnMouseDown(e);
if (!e.Handled) {
if (e.ChangedButton == MouseButton.Left) {
IsExpanded = !IsExpanded;
}
}
}
const double MarginSizeFactor = 0.7;
protected override Size MeasureCore(Size availableSize)
{
double size = MarginSizeFactor * FoldingMargin.SizeFactor * (double)GetValue(TextBlock.FontSizeProperty);
size = Math.Round(size);
if (Math.Abs((size % 2) - 1) < 0.001) {
size -= 1;
}
return new Size(size, size);
}
protected override void OnRender(DrawingContext drawingContext)
{
Pen blackPen = new Pen(Brushes.Black, 1);
Rect rect = new Rect(new Point(0.5, 0.5), this.RenderSize);
drawingContext.DrawRectangle(Brushes.White,
IsMouseDirectlyOver ? blackPen : new Pen(Brushes.Gray, 1),
rect);
double middleX = rect.Left + rect.Width / 2;
double middleY = rect.Top + rect.Height / 2;
double space = Math.Round(rect.Width / 8) + 1;
drawingContext.DrawLine(blackPen,
new Point(rect.Left + space, middleY),
new Point(rect.Right - space, middleY));
if (!isExpanded) {
drawingContext.DrawLine(blackPen,
new Point(middleX, rect.Top + space),
new Point(middleX, rect.Bottom - space));
}
}
protected override void OnIsMouseDirectlyOverChanged(DependencyPropertyChangedEventArgs e)
{
base.OnIsMouseDirectlyOverChanged(e);
InvalidateVisual();
}
}
}

63
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/FoldingSection.cs

@ -0,0 +1,63 @@ @@ -0,0 +1,63 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// A section that can be folded.
/// </summary>
public sealed class FoldingSection : TextSegment
{
FoldingManager manager;
bool isFolded;
CollapsedLineSection collapsedSection;
/// <summary>
/// Gets/sets if the section is folded.
/// </summary>
public bool IsFolded {
get { return isFolded; }
set {
if (isFolded != value) {
isFolded = value;
if (value) {
DocumentLine startLine = manager.document.GetLineByOffset(StartOffset);
DocumentLine endLine = manager.document.GetLineByOffset(EndOffset);
if (startLine != endLine) {
DocumentLine startLinePlusOne = manager.document.GetLineByNumber(startLine.LineNumber + 1);
collapsedSection = manager.textView.CollapseLines(startLinePlusOne, endLine);
}
} else {
if (collapsedSection != null) {
collapsedSection.Uncollapse();
collapsedSection = null;
}
}
manager.textView.Redraw();
}
}
}
internal FoldingSection(FoldingManager manager, int startOffset, int endOffset)
{
this.manager = manager;
this.StartOffset = startOffset;
this.Length = endOffset - startOffset;
}
/// <summary>
/// Deletes the folding section.
/// </summary>
public void Remove()
{
manager.RemoveFolding(this);
}
}
}

95
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/FormattedTextElement.cs

@ -0,0 +1,95 @@ @@ -0,0 +1,95 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// Formatted text (not normal document text)
/// </summary>
class FormattedTextElement : VisualLineElement
{
internal readonly FormattedText text;
public FormattedTextElement(FormattedText text, int documentLength) : base(1, documentLength)
{
if (text == null)
throw new ArgumentNullException("text");
this.text = text;
this.BreakBefore = LineBreakCondition.BreakPossible;
this.BreakAfter = LineBreakCondition.BreakPossible;
}
public LineBreakCondition BreakBefore { get; set; }
public LineBreakCondition BreakAfter { get; set; }
public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context)
{
return new FormattedTextRun(this, this.TextRunProperties);
}
}
class FormattedTextRun : TextEmbeddedObject
{
protected readonly FormattedTextElement element;
TextRunProperties properties;
public FormattedTextRun(FormattedTextElement element, TextRunProperties properties)
{
if (properties == null)
throw new ArgumentNullException("properties");
this.properties = properties;
this.element = element;
}
public override LineBreakCondition BreakBefore {
get { return element.BreakBefore; }
}
public override LineBreakCondition BreakAfter {
get { return element.BreakAfter; }
}
public override bool HasFixedSize {
get { return true; }
}
public override CharacterBufferReference CharacterBufferReference {
get { return new CharacterBufferReference(); }
}
public override int Length {
get { return element.VisualLength; }
}
public override TextRunProperties Properties {
get { return properties; }
}
public override TextEmbeddedObjectMetrics Format(double remainingParagraphWidth)
{
return new TextEmbeddedObjectMetrics(element.text.WidthIncludingTrailingWhitespace,
element.text.Height,
element.text.Baseline);
}
public override Rect ComputeBoundingBox(bool rightToLeft, bool sideways)
{
return new Rect(0, 0, element.text.WidthIncludingTrailingWhitespace, element.text.Height);
}
public override void Draw(DrawingContext drawingContext, Point origin, bool rightToLeft, bool sideways)
{
origin.Y -= element.text.Baseline;
drawingContext.DrawText(element.text, origin);
}
}
}

32
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/GlobalTextRunProperties.cs

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
namespace ICSharpCode.AvalonEdit.Gui
{
sealed class GlobalTextRunProperties : TextRunProperties
{
internal Typeface typeface;
internal double fontRenderingEmSize;
internal Brush foregroundBrush;
internal Brush backgroundBrush;
internal System.Globalization.CultureInfo cultureInfo;
public override Typeface Typeface { get { return typeface; } }
public override double FontRenderingEmSize { get { return fontRenderingEmSize; } }
public override double FontHintingEmSize { get { return fontRenderingEmSize; } }
public override TextDecorationCollection TextDecorations { get { return null; } }
public override Brush ForegroundBrush { get { return foregroundBrush; } }
public override Brush BackgroundBrush { get { return backgroundBrush; } }
public override System.Globalization.CultureInfo CultureInfo { get { return cultureInfo; } }
public override TextEffectCollection TextEffects { get { return null; } }
}
}

1078
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/HeightTree.cs

File diff suppressed because it is too large Load Diff

53
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/HeightTreeLineNode.cs

@ -0,0 +1,53 @@ @@ -0,0 +1,53 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace ICSharpCode.AvalonEdit.Gui
{
struct HeightTreeLineNode
{
internal HeightTreeLineNode(double height)
{
this.collapsedSections = null;
this.height = height;
}
internal double height;
internal List<CollapsedLineSection> collapsedSections;
internal bool IsDirectlyCollapsed {
get { return collapsedSections != null; }
}
internal void AddDirectlyCollapsed(CollapsedLineSection section)
{
if (collapsedSections == null)
collapsedSections = new List<CollapsedLineSection>();
collapsedSections.Add(section);
}
internal void RemoveDirectlyCollapsed(CollapsedLineSection section)
{
Debug.Assert(collapsedSections.Contains(section));
collapsedSections.Remove(section);
if (collapsedSections.Count == 0)
collapsedSections = null;
}
/// <summary>
/// Returns 0 if the line is directly collapsed, otherwise, returns <see cref="height"/>.
/// </summary>
internal double TotalHeight {
get {
return IsDirectlyCollapsed ? 0 : height;
}
}
}
}

159
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/HeightTreeNode.cs

@ -0,0 +1,159 @@ @@ -0,0 +1,159 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using System.Diagnostics;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// A node in the text view's height tree.
/// </summary>
sealed class HeightTreeNode
{
internal readonly DocumentLine documentLine;
internal HeightTreeLineNode lineNode;
internal HeightTreeNode left, right, parent;
internal bool color;
internal HeightTreeNode()
{
}
internal HeightTreeNode(DocumentLine documentLine, double height)
{
this.documentLine = documentLine;
this.totalCount = 1;
this.lineNode = new HeightTreeLineNode(height);
this.totalHeight = height;
}
internal HeightTreeNode LeftMost {
get {
HeightTreeNode node = this;
while (node.left != null)
node = node.left;
return node;
}
}
internal HeightTreeNode RightMost {
get {
HeightTreeNode node = this;
while (node.right != null)
node = node.right;
return node;
}
}
/// <summary>
/// Gets the inorder successor of the node.
/// </summary>
internal HeightTreeNode Successor {
get {
if (right != null) {
return right.LeftMost;
} else {
HeightTreeNode node = this;
HeightTreeNode 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>
/// The number of lines in this node and its child nodes.
/// Invariant:
/// totalCount = 1 + left.totalCount + right.totalCount
/// </summary>
internal int totalCount;
/// <summary>
/// The total height of this node and its child nodes, excluding directly collapsed nodes.
/// Invariant:
/// totalHeight = left.IsDirectlyCollapsed ? 0 : left.totalHeight
/// + lineNode.IsDirectlyCollapsed ? 0 : lineNode.Height
/// + right.IsDirectlyCollapsed ? 0 : right.totalHeight
/// </summary>
internal double totalHeight;
/// <summary>
/// List of the sections that hold this node collapsed.
/// Invariant 1:
/// For each document line in the range described by a CollapsedSection, exactly one ancestor
/// contains that CollapsedSection.
/// Invariant 2:
/// A CollapsedSection is contained either in left+middle or middle+right or just middle.
/// Invariant 3:
/// Start and end of a CollapsedSection always contain the collapsedSection in their
/// documentLine (middle node).
/// </summary>
internal List<CollapsedLineSection> collapsedSections;
internal bool IsDirectlyCollapsed {
get {
return collapsedSections != null;
}
}
internal void AddDirectlyCollapsed(CollapsedLineSection section)
{
if (collapsedSections == null) {
collapsedSections = new List<CollapsedLineSection>();
totalHeight = 0;
}
Debug.Assert(!collapsedSections.Contains(section));
collapsedSections.Add(section);
}
internal void RemoveDirectlyCollapsed(CollapsedLineSection section)
{
Debug.Assert(collapsedSections.Contains(section));
collapsedSections.Remove(section);
if (collapsedSections.Count == 0) {
collapsedSections = null;
totalHeight = lineNode.TotalHeight;
if (left != null)
totalHeight += left.totalHeight;
if (right != null)
totalHeight += right.totalHeight;
}
}
#if DEBUG
public override string ToString()
{
return "[HeightTreeNode "
+ documentLine.LineNumber + " CS=" + GetCollapsedSections(collapsedSections)
+ " Line.CS=" + GetCollapsedSections(lineNode.collapsedSections)
+ " Line.Height=" + lineNode.height
+ " TotalHeight=" + totalHeight
+ "]";
}
static string GetCollapsedSections(List<CollapsedLineSection> list)
{
if (list == null)
return "{}";
return "{" +
string.Join(",",
list.ConvertAll(cs=>cs.ID).ToArray())
+ "}";
}
#endif
}
}

23
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/IBackgroundRenderer.cs

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Windows.Media;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// Background renderers allow drawing behind the text layer.
/// </summary>
public interface IBackgroundRenderer
{
/// <summary>
/// Causes the background renderer to draw.
/// </summary>
void Draw(DrawingContext dc);
}
}

30
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/IReadOnlySectionProvider.cs

@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Utils;
using System.Collections.Generic;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <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>
IEnumerable<ISegment> GetDeletableSegments(ISegment segment);
}
}

39
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/ITextRunConstructionContext.cs

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Windows.Media.TextFormatting;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// Contains information relevant for text run creation.
/// </summary>
public interface ITextRunConstructionContext
{
/// <summary>
/// Gets the text document.
/// </summary>
TextDocument Document { get; }
/// <summary>
/// Gets the text view for which the construction runs.
/// </summary>
TextView TextView { get; }
/// <summary>
/// Gets the visual line that is currently being constructed.
/// </summary>
VisualLine VisualLine { get; }
/// <summary>
/// Gets the global text run properties.
/// </summary>
TextRunProperties GlobalTextRunProperties { get; }
}
}

23
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/IVisualLineTransformer.cs

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// Allows transforming visual line elements.
/// </summary>
public interface IVisualLineTransformer
{
/// <summary>
/// Applies the transformation to the specified list of visual line elements.
/// </summary>
void Transform(ITextRunConstructionContext context, IList<VisualLineElement> elements);
}
}

152
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/InlineObjectRun.cs

@ -0,0 +1,152 @@ @@ -0,0 +1,152 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// A inline UIElement in the document.
/// </summary>
public class InlineObjectElement : VisualLineElement
{
/// <summary>
/// Gets the inline element that is displayed.
/// </summary>
public UIElement Element { get; private set; }
/// <summary>
/// Creates a new InlineObjectElement.
/// </summary>
/// <param name="documentLength">The length of the element in the document. Must be non-negative.</param>
/// <param name="element">The element to display.</param>
public InlineObjectElement(int documentLength, UIElement element)
: base(1, documentLength)
{
if (element == null)
throw new ArgumentNullException("element");
this.Element = element;
}
/// <inheritdoc/>
public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context)
{
if (context == null)
throw new ArgumentNullException("context");
// remove inline object if its already added, can happen e.g. when recreating textrun for word-wrapping
context.TextView.RemoveInlineObject(this.Element);
return new InlineObjectRun(1, this.TextRunProperties, this.Element);
}
}
/// <summary>
/// A text run with an embedded UIElement.
/// </summary>
public class InlineObjectRun : TextEmbeddedObject
{
UIElement element;
int length;
TextRunProperties properties;
/// <summary>
/// Creates a new InlineObjectRun instance.
/// </summary>
/// <param name="length">The length of the TextRun.</param>
/// <param name="properties">The <see cref="TextRunProperties"/> to use.</param>
/// <param name="element">The <see cref="UIElement"/> to display.</param>
public InlineObjectRun(int length, TextRunProperties properties, UIElement element)
{
if (length < 0)
throw new ArgumentOutOfRangeException("length", length, "Value must be positive");
if (properties == null)
throw new ArgumentNullException("properties");
if (element == null)
throw new ArgumentNullException("element");
this.length = length;
this.properties = properties;
this.element = element;
}
/// <summary>
/// Gets the element displayed by the InlineObjectRun.
/// </summary>
public UIElement Element {
get { return element; }
}
/// <summary>
/// Gets the VisualLine that contains this object. This property is only available after the object
/// was added to the text view.
/// </summary>
public VisualLine VisualLine { get; internal set; }
/// <inheritdoc/>
public override LineBreakCondition BreakBefore {
get { return LineBreakCondition.BreakDesired; }
}
/// <inheritdoc/>
public override LineBreakCondition BreakAfter {
get { return LineBreakCondition.BreakDesired; }
}
/// <inheritdoc/>
public override bool HasFixedSize {
get { return true; }
}
/// <inheritdoc/>
public override CharacterBufferReference CharacterBufferReference {
get { return new CharacterBufferReference(); }
}
/// <inheritdoc/>
public override int Length {
get { return length; }
}
/// <inheritdoc/>
public override TextRunProperties Properties {
get { return properties; }
}
/// <inheritdoc/>
public override TextEmbeddedObjectMetrics Format(double remainingParagraphWidth)
{
Size size = element.DesiredSize;
double baseline = TextBlock.GetBaselineOffset(element);
if (double.IsNaN(baseline))
baseline = size.Height;
return new TextEmbeddedObjectMetrics(size.Width, size.Height, baseline);
}
/// <inheritdoc/>
public override Rect ComputeBoundingBox(bool rightToLeft, bool sideways)
{
if (this.element.IsArrangeValid) {
double baseline = TextBlock.GetBaselineOffset(element);
if (double.IsNaN(baseline))
baseline = element.DesiredSize.Height;
return new Rect(new Point(0, -baseline), element.DesiredSize);
} else {
return Rect.Empty;
}
}
/// <inheritdoc/>
public override void Draw(DrawingContext drawingContext, Point origin, bool rightToLeft, bool sideways)
{
}
}
}

234
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/LineNumberMargin.cs

@ -0,0 +1,234 @@ @@ -0,0 +1,234 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
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.Utils;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// Margin showing line numbers.
/// </summary>
public class LineNumberMargin : AbstractMargin, IWeakEventListener
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")]
static LineNumberMargin()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(LineNumberMargin),
new FrameworkPropertyMetadata(typeof(LineNumberMargin)));
}
/// <summary>
/// TextArea property.
/// </summary>
public static readonly DependencyProperty TextAreaProperty =
DependencyProperty.Register("TextArea", typeof(TextArea), typeof(LineNumberMargin));
/// <summary>
/// Gets/sets the text area in which text should be selected.
/// </summary>
public TextArea TextArea {
get { return (TextArea)GetValue(TextAreaProperty); }
set { SetValue(TextAreaProperty, value); }
}
Typeface typeface;
double emSize;
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
{
typeface = this.CreateTypeface();
emSize = (double)GetValue(TextBlock.FontSizeProperty);
FormattedText text = new FormattedText(
new string('9', maxLineNumberLength),
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
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) {
var foreground = (Brush)GetValue(Control.ForegroundProperty);
foreach (VisualLine line in textView.VisualLines) {
int lineNumber = line.FirstDocumentLine.LineNumber;
FormattedText text = new FormattedText(
lineNumber.ToString(CultureInfo.CurrentCulture),
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
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;
}
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();
}
bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
if (managerType == typeof(TextDocumentWeakEventManager.LineCountChanged)) {
OnDocumentLineCountChanged();
return true;
}
return false;
}
int maxLineNumberLength = 1;
void OnDocumentLineCountChanged()
{
int documentLineCount = Document != null ? Document.LineCount : 1;
int newLength = documentLineCount.ToString(CultureInfo.CurrentCulture).Length;
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)
{
TextView.EnsureVisualLines();
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.GetTextLineByVisualTop(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 backgroudn
return new PointHitTestResult(this, hitTestParameters.HitPoint);
}
}
}

81
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/NewLineElementGenerator.cs

@ -0,0 +1,81 @@ @@ -0,0 +1,81 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Windows;
using System.Windows.Media;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// Elements generator that displays "¶" at the end of lines.
/// </summary>
public class NewLineElementGenerator : VisualLineElementGenerator
{
/// <inheritdoc/>
public override int GetFirstInterestedOffset(int startOffset)
{
DocumentLine lastDocumentLine = CurrentContext.VisualLine.LastDocumentLine;
if (lastDocumentLine.DelimiterLength > 0)
return lastDocumentLine.Offset + lastDocumentLine.Length;
else
return -1;
}
/// <inheritdoc/>
public override VisualLineElement ConstructElement(int offset)
{
string newlineText;
DocumentLine lastDocumentLine = CurrentContext.VisualLine.LastDocumentLine;
if (lastDocumentLine.DelimiterLength == 2) {
newlineText = "\u00B6";
} else if (lastDocumentLine.DelimiterLength == 1) {
char newlineChar = CurrentContext.Document.GetCharAt(lastDocumentLine.Offset + lastDocumentLine.Length);
if (newlineChar == '\r')
newlineText = "\\r";
else if (newlineChar == '\n')
newlineText = "\\n";
else
newlineText = "?";
} else {
return null;
}
FormattedText text = new FormattedText(
newlineText,
CurrentContext.GlobalTextRunProperties.CultureInfo,
FlowDirection.LeftToRight,
CurrentContext.GlobalTextRunProperties.Typeface,
CurrentContext.GlobalTextRunProperties.FontRenderingEmSize,
Brushes.LightGray
);
return new NewLineTextElement(text);
}
class NewLineTextElement : FormattedTextElement
{
public NewLineTextElement(FormattedText text) : base(text, 0)
{
BreakBefore = LineBreakCondition.BreakPossible;
BreakAfter = LineBreakCondition.BreakRestrained;
}
public override int GetNextCaretPosition(int visualColumn, bool backwards, CaretPositioningMode mode)
{
// only place a caret stop before the newline, no caret stop after it
if (visualColumn > this.VisualColumn && backwards ||
visualColumn < this.VisualColumn && !backwards)
{
return this.VisualColumn;
} else {
return -1;
}
}
}
}
}

32
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/NoReadOnlySections.cs

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Utils;
using System.Collections.Generic;
namespace ICSharpCode.AvalonEdit.Gui
{
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);
}
}
}

263
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/Selection.cs

@ -0,0 +1,263 @@ @@ -0,0 +1,263 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using ICSharpCode.AvalonEdit.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <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.
/// Returns null if the selection is empty.
/// </summary>
public abstract ISegment SurroundingSegment { get; }
/// <summary>
/// Removes the selected text from the document.
/// </summary>
public abstract void RemoveSelectedText(TextArea textArea);
/// <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 GetIsMultiline(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;
}
}
/// <inheritdoc/>
public abstract override bool Equals(object obj);
/// <inheritdoc/>
public abstract override int GetHashCode();
}
/// <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 RemoveSelectedText(TextArea textArea)
{
if (!IsEmpty) {
var segmentsToDelete = textArea.ReadOnlySectionProvider.GetDeletableSegments(this).ToList();
textArea.Document.BeginUpdate();
try {
for (int i = segmentsToDelete.Count - 1; i >= 0; i--) {
textArea.Document.Remove(segmentsToDelete[i].Offset, segmentsToDelete[i].Length);
}
} finally {
textArea.Document.EndUpdate();
}
}
}
/// <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.AfterInsertion),
e.GetNewOffset(endOffset, AnchorMovementType.AfterInsertion)
);
}
/// <inheritdoc/>
public override bool IsEmpty {
get { return startOffset == endOffset; }
}
int ISegment.Offset {
get { return Math.Min(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 + "]";
}
}
}

69
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/SelectionColorizer.cs

@ -0,0 +1,69 @@ @@ -0,0 +1,69 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Gui
{
sealed class SelectionColorizer : ColorizingTransformer, IBackgroundRenderer
{
TextArea textArea;
public SelectionColorizer(TextArea textArea)
{
if (textArea == null)
throw new ArgumentNullException("textArea");
this.textArea = textArea;
}
protected override void Colorize(ITextRunConstructionContext context)
{
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)
return;
if (segmentStart >= lineEndOffset)
return;
int startColumn = context.VisualLine.GetVisualColumn(Math.Max(0, segmentStart - lineStartOffset));
int endColumn = context.VisualLine.GetVisualColumn(segmentEnd - lineStartOffset);
ChangeVisualElements(
startColumn, endColumn,
element => {
element.TextRunProperties.SetForegroundBrush(SystemColors.HighlightTextBrush);
//element.TextRunProperties.SetBackgroundBrush(SystemColors.HighlightBrush);
});
}
}
public void Draw(DrawingContext dc)
{
BackgroundGeometryBuilder geoBuilder = new BackgroundGeometryBuilder();
geoBuilder.AddSegments(textArea.TextView, textArea.Selection.Segments);
PathGeometry geometry = geoBuilder.CreateGeometry();
if (geometry != null) {
SolidColorBrush lightHighlightBrush = new SolidColorBrush(SystemColors.HighlightColor);
lightHighlightBrush.Opacity = 0.7;
lightHighlightBrush.Freeze();
Pen pen = new Pen(SystemColors.HighlightBrush, 1);
//pen.LineJoin = PenLineJoin.Round;
pen.Freeze();
dc.DrawGeometry(lightHighlightBrush, pen, geometry);
}
}
}
}

433
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/SelectionMouseHandler.cs

@ -0,0 +1,433 @@ @@ -0,0 +1,433 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Utils;
using System.Windows.Threading;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// Handles selection of text using the mouse.
/// </summary>
sealed class SelectionMouseHandler
{
readonly TextArea textArea;
public SelectionMouseHandler(TextArea textArea)
{
this.textArea = 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;
}
}
void textArea_DragEnter(object sender, DragEventArgs e)
{
try {
e.Effects = GetEffect(e);
} catch (Exception ex) {
OnDragException(ex);
}
}
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 ((e.AllowedEffects & DragDropEffects.Move) == DragDropEffects.Move
&& (e.KeyStates & DragDropKeyStates.ControlKey) != DragDropKeyStates.ControlKey
&& textArea.ReadOnlySectionProvider.CanInsert(offset))
{
return DragDropEffects.Move;
} else {
return e.AllowedEffects & DragDropEffects.Copy;
}
}
}
return DragDropEffects.None;
}
void textArea_DragLeave(object sender, DragEventArgs e)
{
e.Handled = true;
}
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 && Contains(textArea.Selection.SurroundingSegment, start)) {
Debug.WriteLine("Drop: did not drop: drop target is inside selection");
e.Effects = DragDropEffects.None;
} else {
Debug.WriteLine("Drop: insert at " + start);
textArea.Document.Insert(start, text);
textArea.Selection = new SimpleSelection(start, start + text.Length);
}
}
}
} 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 Exception("Exception during drag'n'drop", ex);
}));
}
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.GiveFeedback -= textArea_GiveFeedback;
textArea.QueryContinueDrag -= textArea_QueryContinueDrag;
}
}
// TODO: allow disabling text drag'n'drop
const bool AllowTextDragDrop = true;
// 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 {
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 (SelectionContains(textArea.Selection, offset))
e.Cursor = Cursors.Arrow;
else
e.Cursor = Cursors.IBeam;
e.Handled = true;
}
}
}
}
static bool SelectionContains(Selection selection, int offset)
{
if (selection.IsEmpty)
return false;
if (offset >= 0 && Contains(selection.SurroundingSegment, offset)) {
foreach (ISegment s in selection.Segments) {
if (Contains(s, offset)) {
return true;
}
}
}
return false;
}
static bool Contains(ISegment segment, int offset)
{
if (segment == null)
return false;
int start = segment.Offset;
int end = start + segment.Length;
return offset >= start && offset <= end;
}
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)
/// </summary>
WholeWord
}
SelectionMode mode;
AnchorSegment startWord;
Point possibleDragStartMousePos;
void textArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
mode = SelectionMode.None;
if (!e.Handled && e.ChangedButton == MouseButton.Left) {
bool shift = (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift;
if (AllowTextDragDrop && e.ClickCount == 1 && !shift) {
int visualColumn;
int offset = GetOffsetFromMousePosition(e, out visualColumn);
if (SelectionContains(textArea.Selection, 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 (e.ClickCount == 1) {
mode = SelectionMode.Normal;
if (shift) {
textArea.Selection = textArea.Selection.StartSelectionOrSetEndpoint(oldOffset, textArea.Caret.Offset);
}
} else {
mode = SelectionMode.WholeWord;
var 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.GetEndOffset() > textArea.Selection.SurroundingSegment.GetEndOffset()) {
textArea.Selection = textArea.Selection.SetEndpoint(startWord.GetEndOffset());
}
this.startWord = new AnchorSegment(textArea.Document, textArea.Selection.SurroundingSegment);
} else {
textArea.Selection = new SimpleSelection(startWord.Offset, startWord.GetEndOffset());
this.startWord = new AnchorSegment(textArea.Document, startWord.Offset, startWord.Length);
}
}
}
}
e.Handled = true;
}
SimpleSegment GetWordAtMousePosition(MouseEventArgs e)
{
TextView textView = textArea.TextView;
if (textView == null) return SimpleSegment.Invalid;
textView.EnsureVisualLines();
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, true, CaretPositioningMode.WordStart);
if (wordStartVC == -1)
wordStartVC = 0;
int wordEndVC = line.GetNextCaretPosition(wordStartVC, false, CaretPositioningMode.WordBorder);
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;
}
}
void SetCaretOffsetToMousePosition(MouseEventArgs e)
{
int visualColumn;
int offset = GetOffsetFromMousePosition(e, out visualColumn);
if (offset >= 0) {
textArea.Caret.Position = new TextViewPosition(textArea.Document.GetLocation(offset), visualColumn);
textArea.Caret.DesiredXPos = double.NaN;
}
}
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;
textView.EnsureVisualLines();
Point pos = positionRelativeToTextView;
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) {
visualColumn = line.GetVisualColumn(pos);
return line.GetRelativeOffset(visualColumn) + line.FirstDocumentLine.Offset;
}
return -1;
}
void textArea_MouseMove(object sender, MouseEventArgs e)
{
if (e.Handled)
return;
if (mode == SelectionMode.Normal || mode == SelectionMode.WholeWord) {
e.Handled = true;
int oldOffset = textArea.Caret.Offset;
SetCaretOffsetToMousePosition(e);
if (mode == SelectionMode.Normal) {
textArea.Selection = textArea.Selection.StartSelectionOrSetEndpoint(oldOffset, textArea.Caret.Offset);
} else if (mode == SelectionMode.WholeWord) {
var newWord = GetWordAtMousePosition(e);
if (newWord != SimpleSegment.Invalid) {
textArea.Selection = new SimpleSelection(
Math.Min(newWord.Offset, startWord.Offset),
Math.Max(newWord.GetEndOffset(), startWord.GetEndOffset()));
}
}
} 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();
}
}
}
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;
}
mode = SelectionMode.None;
textArea.ReleaseMouseCapture();
}
void StartDrag()
{
// prevent nested StartDrag calls
mode = SelectionMode.Drag;
// mouse capture and Drag'n'Drop doesn't mix
textArea.ReleaseMouseCapture();
string text = textArea.Selection.GetText(textArea.Document);
DataObject dataObject = new DataObject();
dataObject.SetText(text);
DragDropEffects allowedEffects = DragDropEffects.All;
List<AnchorSegment> deleteOnMove;
deleteOnMove = textArea.Selection.Segments.Select(s => new AnchorSegment(textArea.Document, s)).ToList();
Debug.WriteLine("DoDragDrop with allowedEffects=" + allowedEffects);
DragDropEffects resultEffect = DragDrop.DoDragDrop(textArea, dataObject, allowedEffects);
Debug.WriteLine("DoDragDrop done, resultEffect=" + resultEffect);
if (deleteOnMove != null && resultEffect == DragDropEffects.Move) {
textArea.Document.BeginUpdate();
try {
foreach (ISegment s in deleteOnMove) {
textArea.Document.Remove(s.Offset, s.Length);
}
} finally {
textArea.Document.EndUpdate();
}
}
}
void textArea_GiveFeedback(object sender, GiveFeedbackEventArgs e)
{
e.UseDefaultCursors = true;
e.Handled = true;
}
void textArea_QueryContinueDrag(object sender, QueryContinueDragEventArgs e)
{
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;
}
}
}

510
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/TextArea.cs

@ -0,0 +1,510 @@ @@ -0,0 +1,510 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.ObjectModel;
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.Media;
using System.Windows.Shapes;
using System.Windows.Threading;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Gui;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit
{
/// <summary>
/// Control that wraps a TextView and adds support for user input and the caret.
/// </summary>
public class TextArea : Control, IScrollInfo, IWeakEventListener
{
#region Constructor
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")]
static TextArea()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(TextArea),
new FrameworkPropertyMetadata(typeof(TextArea)));
KeyboardNavigation.IsTabStopProperty.OverrideMetadata(
typeof(TextArea), new FrameworkPropertyMetadata(Boxes.True));
KeyboardNavigation.TabNavigationProperty.OverrideMetadata(
typeof(TextArea), new FrameworkPropertyMetadata(KeyboardNavigationMode.None));
}
/// <summary>
/// Creates a new TextArea instance.
/// </summary>
public TextArea() : this(new TextView())
{
}
/// <summary>
/// Creates a new TextArea instance.
/// </summary>
protected TextArea(TextView textView)
{
if (textView == null)
throw new ArgumentNullException("textView");
this.textView = textView;
textView.SetBinding(TextView.DocumentProperty, new Binding("Document") { Source = this });
leftMargins.Add(new LineNumberMargin { TextView = textView, TextArea = this } );
leftMargins.Add(new Line {
X1 = 0, Y1 = 0, X2 = 0, Y2 = 1,
StrokeDashArray = { 0, 2 },
Stretch = Stretch.Fill,
Stroke = Brushes.Gray,
StrokeThickness = 1,
StrokeDashCap = PenLineCap.Round,
Margin = new Thickness(2, 0, 2, 0)
});
SelectionColorizer sc = new SelectionColorizer(this);
textView.LineTransformers.Add(sc);
textView.BackgroundRenderer.Add(sc);
caret = new Caret(this);
this.CommandBindings.Add(new CommandBinding(ApplicationCommands.Undo, ExecuteUndo, CanExecuteUndo));
this.CommandBindings.Add(new CommandBinding(ApplicationCommands.Redo, ExecuteRedo, CanExecuteRedo));
this.CommandBindings.AddRange(CaretNavigationCommandHandler.CommandBindings);
this.InputBindings.AddRange(CaretNavigationCommandHandler.InputBindings);
this.CommandBindings.AddRange(EditingCommandHandler.CommandBindings);
this.InputBindings.AddRange(EditingCommandHandler.InputBindings);
new SelectionMouseHandler(this).Attach();
}
#endregion
#region Document property
/// <summary>
/// Document property.
/// </summary>
public static readonly DependencyProperty DocumentProperty
= TextEditor.DocumentProperty.AddOwner(typeof(TextArea), new FrameworkPropertyMetadata(OnDocumentChanged));
/// <summary>
/// Gets/Sets the document displayed by the text editor.
/// </summary>
public TextDocument Document {
get { return (TextDocument)GetValue(DocumentProperty); }
set { SetValue(DocumentProperty, value); }
}
static void OnDocumentChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
{
((TextArea)dp).OnDocumentChanged((TextDocument)e.OldValue, (TextDocument)e.NewValue);
}
void OnDocumentChanged(TextDocument oldValue, TextDocument newValue)
{
if (oldValue != null) {
TextDocumentWeakEventManager.Changing.RemoveListener(oldValue, this);
TextDocumentWeakEventManager.Changed.RemoveListener(oldValue, this);
TextDocumentWeakEventManager.UpdateStarted.RemoveListener(oldValue, this);
}
if (newValue != null) {
TextDocumentWeakEventManager.Changing.AddListener(newValue, this);
TextDocumentWeakEventManager.Changed.AddListener(newValue, this);
TextDocumentWeakEventManager.UpdateStarted.AddListener(newValue, this);
}
CommandManager.InvalidateRequerySuggested();
}
#endregion
bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
if (managerType == typeof(TextDocumentWeakEventManager.Changing)) {
caret.OnDocumentChanging();
return true;
} else if (managerType == typeof(TextDocumentWeakEventManager.Changed)) {
OnDocumentChanged((DocumentChangeEventArgs)e);
return true;
} else if (managerType == typeof(TextDocumentWeakEventManager.UpdateStarted)) {
OnUpdateStarted();
return true;
}
return false;
}
void OnDocumentChanged(DocumentChangeEventArgs e)
{
caret.OnDocumentChanged(e);
this.Selection = selection.UpdateOnDocumentChange(e);
}
void OnUpdateStarted()
{
Document.UndoStack.PushOptional(new RestoreCaretAndSelectionUndoAction(this));
}
sealed class RestoreCaretAndSelectionUndoAction : IUndoableOperation
{
// keep textarea in weak reference because the IUndoableOperation is stored with the document
WeakReference textAreaReference;
TextViewPosition caretPosition;
Selection selection;
public RestoreCaretAndSelectionUndoAction(TextArea textArea)
{
this.textAreaReference = new WeakReference(textArea);
this.caretPosition = textArea.Caret.Position;
this.selection = textArea.Selection;
}
public void Undo()
{
TextArea textArea = (TextArea)textAreaReference.Target;
if (textArea != null) {
textArea.Caret.Position = caretPosition;
textArea.Selection = selection;
}
}
public void Redo()
{
// redo=undo: we just restore the caret/selection state
Undo();
}
}
readonly Caret caret;
/// <summary>
/// Gets the Caret used for this text area.
/// </summary>
public Caret Caret {
get { return caret; }
}
Selection selection = Selection.Empty;
/// <summary>
/// Gets/Sets the selection in this text area.
/// </summary>
public Selection Selection {
get { return selection; }
set {
if (value == null)
throw new ArgumentNullException("value");
if (!object.Equals(selection, value)) {
Debug.WriteLine("Selection change from " + selection + " to " + value);
if (textView != null) {
textView.Redraw(selection.SurroundingSegment, DispatcherPriority.Background);
textView.Redraw(value.SurroundingSegment, DispatcherPriority.Background);
}
selection = value;
if (SelectionChanged != null)
SelectionChanged(this, EventArgs.Empty);
}
}
}
/// <summary>
/// Occurs when the selection has changed.
/// </summary>
public event EventHandler SelectionChanged;
readonly TextView textView;
IScrollInfo scrollInfo;
/// <inheritdoc/>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
scrollInfo = textView;
ApplyScrollInfo();
}
/// <summary>
/// Gets the text view used to display text in this text area.
/// </summary>
public TextView TextView {
get {
return textView;
}
}
ObservableCollection<UIElement> leftMargins = new ObservableCollection<UIElement>();
/// <summary>
/// Gets the collection of margins displayed to the left of the text view.
/// </summary>
public ObservableCollection<UIElement> LeftMargins {
get {
return leftMargins;
}
}
#region Undo / Redo
UndoStack GetUndoStack()
{
TextDocument document = this.Document;
if (document != null)
return document.UndoStack;
else
return null;
}
void ExecuteUndo(object sender, ExecutedRoutedEventArgs e)
{
var undoStack = GetUndoStack();
if (undoStack != null && undoStack.CanUndo)
undoStack.Undo();
}
void CanExecuteUndo(object sender, CanExecuteRoutedEventArgs e)
{
var undoStack = GetUndoStack();
e.CanExecute = undoStack != null && undoStack.CanUndo;
}
void ExecuteRedo(object sender, ExecutedRoutedEventArgs e)
{
var undoStack = GetUndoStack();
if (undoStack != null && undoStack.CanRedo)
undoStack.Redo();
}
void CanExecuteRedo(object sender, CanExecuteRoutedEventArgs e)
{
var undoStack = GetUndoStack();
e.CanExecute = undoStack != null && undoStack.CanRedo;
}
#endregion
#region IScrollInfo implementation
ScrollViewer scrollOwner;
bool canVerticallyScroll, canHorizontallyScroll;
void ApplyScrollInfo()
{
if (scrollInfo != null) {
scrollInfo.ScrollOwner = scrollOwner;
scrollInfo.CanVerticallyScroll = canVerticallyScroll;
scrollInfo.CanHorizontallyScroll = canHorizontallyScroll;
scrollOwner = null;
}
}
bool IScrollInfo.CanVerticallyScroll {
get { return scrollInfo != null ? scrollInfo.CanVerticallyScroll : false; }
set {
canVerticallyScroll = value;
if (scrollInfo != null)
scrollInfo.CanVerticallyScroll = value;
}
}
bool IScrollInfo.CanHorizontallyScroll {
get { return scrollInfo != null ? scrollInfo.CanHorizontallyScroll : false; }
set {
canHorizontallyScroll = value;
if (scrollInfo != null)
scrollInfo.CanHorizontallyScroll = value;
}
}
double IScrollInfo.ExtentWidth {
get { return scrollInfo != null ? scrollInfo.ExtentWidth : 0; }
}
double IScrollInfo.ExtentHeight {
get { return scrollInfo != null ? scrollInfo.ExtentHeight : 0; }
}
double IScrollInfo.ViewportWidth {
get { return scrollInfo != null ? scrollInfo.ViewportWidth : 0; }
}
double IScrollInfo.ViewportHeight {
get { return scrollInfo != null ? scrollInfo.ViewportHeight : 0; }
}
double IScrollInfo.HorizontalOffset {
get { return scrollInfo != null ? scrollInfo.HorizontalOffset : 0; }
}
double IScrollInfo.VerticalOffset {
get { return scrollInfo != null ? scrollInfo.VerticalOffset : 0; }
}
ScrollViewer IScrollInfo.ScrollOwner {
get { return scrollInfo != null ? scrollInfo.ScrollOwner : null; }
set {
if (scrollInfo != null)
scrollInfo.ScrollOwner = value;
else
scrollOwner = value;
}
}
void IScrollInfo.LineUp()
{
if (scrollInfo != null) scrollInfo.LineUp();
}
void IScrollInfo.LineDown()
{
if (scrollInfo != null) scrollInfo.LineDown();
}
void IScrollInfo.LineLeft()
{
if (scrollInfo != null) scrollInfo.LineLeft();
}
void IScrollInfo.LineRight()
{
if (scrollInfo != null) scrollInfo.LineRight();
}
void IScrollInfo.PageUp()
{
if (scrollInfo != null) scrollInfo.PageUp();
}
void IScrollInfo.PageDown()
{
if (scrollInfo != null) scrollInfo.PageDown();
}
void IScrollInfo.PageLeft()
{
if (scrollInfo != null) scrollInfo.PageLeft();
}
void IScrollInfo.PageRight()
{
if (scrollInfo != null) scrollInfo.PageRight();
}
void IScrollInfo.MouseWheelUp()
{
if (scrollInfo != null) scrollInfo.MouseWheelUp();
}
void IScrollInfo.MouseWheelDown()
{
if (scrollInfo != null) scrollInfo.MouseWheelDown();
}
void IScrollInfo.MouseWheelLeft()
{
if (scrollInfo != null) scrollInfo.MouseWheelLeft();
}
void IScrollInfo.MouseWheelRight()
{
if (scrollInfo != null) scrollInfo.MouseWheelRight();
}
void IScrollInfo.SetHorizontalOffset(double offset)
{
if (scrollInfo != null) scrollInfo.SetHorizontalOffset(offset);
}
void IScrollInfo.SetVerticalOffset(double offset)
{
if (scrollInfo != null) scrollInfo.SetVerticalOffset(offset);
}
Rect IScrollInfo.MakeVisible(System.Windows.Media.Visual visual, Rect rectangle)
{
if (scrollInfo != null)
return scrollInfo.MakeVisible(visual, rectangle);
else
return Rect.Empty;
}
#endregion
/// <inheritdoc/>
protected override void OnMouseDown(MouseButtonEventArgs e)
{
base.OnMouseDown(e);
Focus();
}
/// <inheritdoc/>
protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
base.OnGotKeyboardFocus(e);
caret.Show();
}
/// <inheritdoc/>
protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
base.OnLostKeyboardFocus(e);
caret.Hide();
}
IReadOnlySectionProvider readOnlySectionProvider = NoReadOnlySections.Instance;
/// <summary>
/// Gets/Sets an object that provides read-only sections for the text area.
/// </summary>
public IReadOnlySectionProvider ReadOnlySectionProvider {
get { return readOnlySectionProvider; }
set {
if (value == null)
throw new ArgumentNullException("value");
readOnlySectionProvider = value;
}
}
/// <inheritdoc/>
protected override void OnTextInput(TextCompositionEventArgs e)
{
base.OnTextInput(e);
if (!e.Handled) {
TextDocument document = this.Document;
if (document != null) {
document.BeginUpdate();
try {
RemoveSelectedText();
if (readOnlySectionProvider.CanInsert(caret.Offset))
document.Insert(caret.Offset, e.Text);
} finally {
document.EndUpdate();
}
caret.BringCaretToView();
e.Handled = true;
}
}
}
internal void RemoveSelectedText()
{
selection.RemoveSelectedText(this);
#if DEBUG
if (!selection.IsEmpty) {
// TODO: assert that the remaining selection is read-only
}
#endif
}
internal void ReplaceSelectionWithText(string newText)
{
Document.BeginUpdate();
try {
RemoveSelectedText();
if (ReadOnlySectionProvider.CanInsert(Caret.Offset)) {
Document.Insert(Caret.Offset, newText);
}
} finally {
Document.EndUpdate();
}
}
}
}

673
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/TextEditor.cs

@ -0,0 +1,673 @@ @@ -0,0 +1,673 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.ComponentModel;
using System.IO;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Markup;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Gui;
using ICSharpCode.AvalonEdit.Highlighting;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit
{
/// <summary>
/// The text editor control.
/// Contains a scrollable TextArea.
/// </summary>
[Localizability(LocalizationCategory.Text), ContentProperty("Text")]
public class TextEditor : Control
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")]
static TextEditor()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(TextEditor),
new FrameworkPropertyMetadata(typeof(TextEditor)));
KeyboardNavigation.IsTabStopProperty.OverrideMetadata(
typeof(TextEditor), new FrameworkPropertyMetadata(Boxes.False));
}
/// <summary>
/// Creates a new TextEditor instance.
/// </summary>
public TextEditor() : this(new TextArea()) {}
/// <summary>
/// Creates a new TextEditor instance.
/// </summary>
protected TextEditor(TextArea textArea)
{
if (textArea == null)
throw new ArgumentNullException("textArea");
this.textArea = textArea;
textArea.SetBinding(TextArea.DocumentProperty, new Binding("Document") { Source = this });
}
/// <summary>
/// Document property.
/// </summary>
public static readonly DependencyProperty DocumentProperty =
DependencyProperty.Register("Document", typeof(TextDocument), typeof(TextEditor),
new FrameworkPropertyMetadata(OnDocumentChanged));
/// <summary>
/// Gets/Sets the document displayed by the text editor.
/// This is a dependency property.
/// </summary>
public TextDocument Document {
get { return (TextDocument)GetValue(DocumentProperty); }
set { SetValue(DocumentProperty, value); }
}
static void OnDocumentChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
{
((TextEditor)dp).OnDocumentChanged((TextDocument)e.OldValue, (TextDocument)e.NewValue);
}
void OnDocumentChanged(TextDocument oldValue, TextDocument newValue)
{
if (oldValue != null) {
oldValue.TextChanged -= DocumentTextChanged;
}
if (newValue != null) {
newValue.TextChanged += DocumentTextChanged;
}
}
/// <summary>
/// Gets/Sets the text of the current document.
/// </summary>
[Localizability(LocalizationCategory.Text), DefaultValue("")]
public string Text {
get {
TextDocument document = this.Document;
return document != null ? document.Text : string.Empty;
}
set {
if (value == null)
value = string.Empty;
TextDocument document = GetOrCreateDocument();
document.Text = value;
document.UndoStack.ClearAll();
}
}
TextDocument GetOrCreateDocument()
{
TextDocument document = this.Document;
if (document == null) {
document = new TextDocument();
this.Document = document;
}
return document;
}
/// <summary>
/// Occurs when the Text property changes.
/// </summary>
public event EventHandler TextChanged;
void DocumentTextChanged(object sender, EventArgs e)
{
if (TextChanged != null)
TextChanged(this, e);
}
readonly TextArea textArea;
ScrollViewer scrollViewer;
/// <summary>
/// Is called after the template was applied.
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
scrollViewer = (ScrollViewer)Template.FindName("PART_ScrollViewer", this);
}
/// <summary>
/// Gets the text area.
/// </summary>
public TextArea TextArea {
get {
return textArea;
}
}
bool CanExecute(RoutedUICommand command)
{
TextArea textArea = this.TextArea;
if (textArea == null)
return false;
else
return command.CanExecute(null, textArea);
}
void Execute(RoutedUICommand command)
{
TextArea textArea = this.TextArea;
if (textArea != null)
command.Execute(null, textArea);
}
#region Syntax highlighting
IHighlightingDefinition syntaxHighlighting;
HighlightingColorizer colorizer;
/// <summary>
/// Gets/sets the syntax highlighting definition used to colorize the text.
/// </summary>
public IHighlightingDefinition SyntaxHighlighting {
get { return syntaxHighlighting; }
set {
if (syntaxHighlighting != value) {
if (colorizer != null) {
this.TextArea.TextView.LineTransformers.Remove(colorizer);
colorizer = null;
}
syntaxHighlighting = value;
if (value != null) {
TextView textView = this.TextArea.TextView;
colorizer = new HighlightingColorizer(textView, value.MainRuleSet);
textView.LineTransformers.Insert(0, colorizer);
}
}
}
}
#endregion
#region WordWrap
/// <summary>
/// Word wrap dependency property.
/// </summary>
public static readonly DependencyProperty WordWrapProperty =
DependencyProperty.Register("WordWrap", typeof(bool), typeof(TextEditor),
new FrameworkPropertyMetadata(Boxes.False));
/// <summary>
/// Specifies whether the text editor uses word wrapping.
/// </summary>
public bool WordWrap {
get { return (bool)GetValue(WordWrapProperty); }
set { SetValue(WordWrapProperty, Boxes.Box(value)); }
}
#endregion
#region TextBoxBase-like methods
/// <summary>
/// Appends text to the end of the document.
/// </summary>
public void AppendText(string textData)
{
TextDocument document = GetOrCreateDocument();
document.Insert(document.TextLength, textData);
}
/// <summary>
/// Begins a group of document changes.
/// </summary>
public void BeginChange()
{
GetOrCreateDocument().BeginUpdate();
}
/// <summary>
/// Copies the current selection to the clipboard.
/// </summary>
public void Copy()
{
Execute(ApplicationCommands.Copy);
}
/// <summary>
/// Removes the current selection and copies it to the clipboard.
/// </summary>
public void Cut()
{
Execute(ApplicationCommands.Cut);
}
/// <summary>
/// Begins a group of document changes and returns an object that ends the group of document
/// changes when it is disposed.
/// </summary>
public IDisposable DeclareChangeBlock()
{
return new ChangeBlock(GetOrCreateDocument());
}
sealed class ChangeBlock : IDisposable
{
TextDocument document;
public ChangeBlock(TextDocument document)
{
this.document = document;
document.BeginUpdate();
}
public void Dispose()
{
document.EndUpdate();
}
}
/// <summary>
/// Ends the current group of document changes.
/// </summary>
public void EndChange()
{
GetOrCreateDocument().EndUpdate();
}
/// <summary>
/// Scrolls one line down.
/// </summary>
public void LineDown()
{
if (scrollViewer != null)
scrollViewer.LineDown();
}
/// <summary>
/// Scrolls to the left.
/// </summary>
public void LineLeft()
{
if (scrollViewer != null)
scrollViewer.LineLeft();
}
/// <summary>
/// Scrolls to the right.
/// </summary>
public void LineRight()
{
if (scrollViewer != null)
scrollViewer.LineRight();
}
/// <summary>
/// Scrolls one line up.
/// </summary>
public void LineUp()
{
if (scrollViewer != null)
scrollViewer.LineUp();
}
/// <summary>
/// Scrolls one page down.
/// </summary>
public void PageDown()
{
if (scrollViewer != null)
scrollViewer.PageDown();
}
/// <summary>
/// Scrolls one page up.
/// </summary>
public void PageUp()
{
if (scrollViewer != null)
scrollViewer.PageUp();
}
/// <summary>
/// Scrolls one page left.
/// </summary>
public void PageLeft()
{
if (scrollViewer != null)
scrollViewer.PageLeft();
}
/// <summary>
/// Scrolls one page right.
/// </summary>
public void PageRight()
{
if (scrollViewer != null)
scrollViewer.PageRight();
}
/// <summary>
/// Pastes the clipboard content.
/// </summary>
public void Paste()
{
Execute(ApplicationCommands.Paste);
}
/// <summary>
/// Redoes the most recent undone command.
/// </summary>
/// <returns>True is the redo operation was successful, false is the redo stack is empty.</returns>
public bool Redo()
{
if (CanExecute(ApplicationCommands.Redo)) {
Execute(ApplicationCommands.Redo);
return true;
}
return false;
}
/// <summary>
/// Scrolls to the end of the document.
/// </summary>
public void ScrollToEnd()
{
if (scrollViewer != null)
scrollViewer.ScrollToEnd();
}
/// <summary>
/// Scrolls to the start of the document.
/// </summary>
public void ScrollToHome()
{
if (scrollViewer != null)
scrollViewer.ScrollToHome();
}
/// <summary>
/// Scrolls to the specified position in the document.
/// </summary>
public void ScrollToHorizontalOffset(double offset)
{
if (scrollViewer != null)
scrollViewer.ScrollToHorizontalOffset(offset);
}
/// <summary>
/// Scrolls to the specified position in the document.
/// </summary>
public void ScrollToVerticalOffset(double offset)
{
if (scrollViewer != null)
scrollViewer.ScrollToVerticalOffset(offset);
}
/// <summary>
/// Selects the entire text.
/// </summary>
public void SelectAll()
{
ApplicationCommands.SelectAll.Execute(null, TextArea);
}
/// <summary>
/// Undoes the most recent command.
/// </summary>
/// <returns>True is the undo operation was successful, false is the undo stack is empty.</returns>
public bool Undo()
{
if (CanExecute(ApplicationCommands.Undo)) {
Execute(ApplicationCommands.Undo);
return true;
}
return false;
}
/// <summary>
/// Gets if the most recent undone command can be redone.
/// </summary>
public bool CanRedo {
get { return CanExecute(ApplicationCommands.Redo); }
}
/// <summary>
/// Gets if the most recent command can be undone.
/// </summary>
public bool CanUndo {
get { return CanExecute(ApplicationCommands.Undo); }
}
/// <summary>
/// Gets the vertical size of the document.
/// </summary>
public double ExtentHeight {
get {
return scrollViewer != null ? scrollViewer.ExtentHeight : 0;
}
}
/// <summary>
/// Gets the horizontal size of the current document region.
/// </summary>
public double ExtentWidth {
get {
return scrollViewer != null ? scrollViewer.ExtentWidth : 0;
}
}
/// <summary>
/// Gets the horizontal size of the viewport.
/// </summary>
public double ViewportHeight {
get {
return scrollViewer != null ? scrollViewer.ViewportHeight : 0;
}
}
/// <summary>
/// Gets the horizontal size of the viewport.
/// </summary>
public double ViewportWidth {
get {
return scrollViewer != null ? scrollViewer.ViewportWidth : 0;
}
}
/// <summary>
/// Gets the vertical scroll position.
/// </summary>
public double VerticalOffset {
get {
return scrollViewer != null ? scrollViewer.VerticalOffset : 0;
}
}
/// <summary>
/// Gets the horizontal scroll position.
/// </summary>
public double HorizontalOffset {
get {
return scrollViewer != null ? scrollViewer.HorizontalOffset : 0;
}
}
#endregion
#region TextBox methods
/// <summary>
/// Gets/Sets the selected text.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public string SelectedText {
get {
TextArea textArea = this.TextArea;
if (textArea != null && textArea.Document != null)
return textArea.Selection.GetText(textArea.Document);
else
return string.Empty;
}
set {
if (value == null)
throw new ArgumentNullException("value");
TextArea textArea = this.TextArea;
if (textArea != null && textArea.Document != null) {
textArea.Document.BeginUpdate();
try {
textArea.RemoveSelectedText();
if (textArea.ReadOnlySectionProvider.CanInsert(textArea.Caret.Offset))
textArea.Document.Insert(textArea.Caret.Offset, value);
} finally {
textArea.Document.EndUpdate();
}
}
}
}
/// <summary>
/// Gets/sets the caret position.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public int CaretOffset {
get {
TextArea textArea = this.TextArea;
if (textArea != null)
return textArea.Caret.Offset;
else
return 0;
}
set {
TextArea textArea = this.TextArea;
if (textArea != null)
textArea.Caret.Offset = value;
}
}
/// <summary>
/// Gets/sets the start position of the selection.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public int SelectionStart {
get {
TextArea textArea = this.TextArea;
if (textArea != null) {
if (textArea.Selection.IsEmpty)
return textArea.Caret.Offset;
else
return textArea.Selection.SurroundingSegment.Offset;
} else {
return 0;
}
}
set {
Select(value, SelectionLength);
}
}
/// <summary>
/// Gets/sets the length of the selection.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public int SelectionLength {
get {
TextArea textArea = this.TextArea;
if (textArea != null)
return textArea.Selection.Length;
else
return 0;
}
set {
Select(SelectionStart, value);
}
}
/// <summary>
/// Selects the specified text section.
/// </summary>
public void Select(int start, int length)
{
int documentLength = Document != null ? Document.TextLength : 0;
if (start < 0 || start > documentLength)
throw new ArgumentOutOfRangeException("start", start, "Value must be between 0 and " + documentLength);
if (length < 0 || start + length > documentLength)
throw new ArgumentOutOfRangeException("length", length, "Value must be between 0 and " + (documentLength - length));
textArea.Selection = new SimpleSelection(start, start + length);
textArea.Caret.Offset = start + length;
}
/// <summary>
/// Gets the number of lines in the document.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public int LineCount {
get {
TextDocument document = this.Document;
if (document != null)
return document.LineCount;
else
return 1;
}
}
/// <summary>
/// Clears the text.
/// </summary>
public void Clear()
{
this.Text = string.Empty;
}
#endregion
#region Loading from stream
/// <summary>
/// Loads the text from the stream, auto-detecting the encoding.
/// </summary>
public void Load(Stream stream)
{
using (StreamReader reader = FileReader.OpenStream(stream, Encoding ?? Encoding.UTF8)) {
reader.Peek(); // peek so that the StreamReader can autodetect the encoding
Text = reader.ReadToEnd();
Encoding = reader.CurrentEncoding;
}
}
/// <summary>
/// Loads the text from the stream, auto-detecting the encoding.
/// </summary>
public void Load(string fileName)
{
if (fileName == null)
throw new ArgumentNullException("fileName");
using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)) {
Load(fs);
}
}
/// <summary>
/// Gets/sets the encoding used when the file is saved.
/// </summary>
public Encoding Encoding { get; set; }
/// <summary>
/// Saves the text to the stream.
/// </summary>
public void Save(Stream stream)
{
if (stream == null)
throw new ArgumentNullException("stream");
StreamWriter writer = new StreamWriter(stream, Encoding ?? Encoding.UTF8);
writer.Write(Text);
writer.Flush();
// do not close the stream
}
/// <summary>
/// Loads the text from the stream, auto-detecting the encoding.
/// </summary>
public void Save(string fileName)
{
if (fileName == null)
throw new ArgumentNullException("fileName");
using (FileStream fs = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None)) {
Save(fs);
}
}
#endregion
}
}

53
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/TextEditor.xaml

@ -0,0 +1,53 @@ @@ -0,0 +1,53 @@
<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:gui="clr-namespace:ICSharpCode.AvalonEdit.Gui"
>
<Style TargetType="{x:Type AvalonEdit:TextEditor}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type AvalonEdit:TextEditor}">
<ScrollViewer
Name="PART_ScrollViewer"
CanContentScroll="True"
VerticalScrollBarVisibility="Visible"
HorizontalScrollBarVisibility="Visible"
Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TextArea}"
VerticalContentAlignment="Top"
HorizontalContentAlignment="Left"
Background="{TemplateBinding Background}"
/>
<ControlTemplate.Triggers>
<Trigger Property="WordWrap"
Value="True">
<Setter TargetName="PART_ScrollViewer"
Property="HorizontalScrollBarVisibility"
Value="Disabled" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type AvalonEdit:TextArea}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type AvalonEdit:TextArea}">
<DockPanel>
<ItemsControl DockPanel.Dock="Left"
ItemsSource="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=LeftMargins}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<ContentPresenter Panel.ZIndex="-1" Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TextView}"/>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
</Style>
</ResourceDictionary>

85
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/TextSegmentReadOnlySectionProvider.cs

@ -0,0 +1,85 @@ @@ -0,0 +1,85 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Utils;
using System.Collections.Generic;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <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 for the specified document.
/// </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.GetEndOffset();
if (readonlyUntil < endOffset) {
yield return new SimpleSegment(readonlyUntil, endOffset - readonlyUntil);
}
}
}
}

1046
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/TextView.cs

File diff suppressed because it is too large Load Diff

142
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/TextViewPosition.cs

@ -0,0 +1,142 @@ @@ -0,0 +1,142 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Globalization;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// Represents a text location with a visual column.
/// </summary>
public struct TextViewPosition : IEquatable<TextViewPosition>
{
int line, column, visualColumn;
/// <summary>
/// Gets/Sets the line number.
/// </summary>
public int Line {
get { return line; }
set { line = value; }
}
/// <summary>
/// Gets/Sets the (text) column number.
/// </summary>
public int Column {
get { return column; }
set { column = value; }
}
/// <summary>
/// Gets/Sets the visual column number.
/// Can be -1 (meaning unknown visual column).
/// </summary>
public int VisualColumn {
get { return visualColumn; }
set { visualColumn = value; }
}
/// <summary>
/// Creates a new TextViewPosition instance.
/// </summary>
public TextViewPosition(int line, int column, int visualColumn)
{
this.line = line;
this.column = column;
this.visualColumn = visualColumn;
}
/// <summary>
/// Creates a new TextViewPosition instance.
/// </summary>
public TextViewPosition(TextLocation location, int visualColumn)
{
this.line = location.Line;
this.column = location.Column;
this.visualColumn = visualColumn;
}
/// <summary>
/// Creates a new TextViewPosition instance.
/// </summary>
public TextViewPosition(TextLocation location)
{
this.line = location.Line;
this.column = location.Column;
this.visualColumn = -1;
}
/// <inheritdoc/>
public override string ToString()
{
return string.Format(CultureInfo.InvariantCulture,
"[TextViewPosition Line={0} Column={1} VisualColumn={2}]",
this.line, this.column, this.visualColumn);
}
/// <summary>
/// Implicit conversion to <see cref="TextLocation"/>.
/// </summary>
public static implicit operator TextLocation(TextViewPosition position)
{
return new TextLocation(position.Line, position.Column);
}
#region Equals and GetHashCode implementation
// The code in this region is useful if you want to use this structure in collections.
// If you don't need it, you can just remove the region and the ": IEquatable<Struct1>" declaration.
/// <inheritdoc/>
public override bool Equals(object obj)
{
if (obj is TextViewPosition)
return Equals((TextViewPosition)obj); // use Equals method below
else
return false;
}
/// <inheritdoc/>
public override int GetHashCode()
{
int hashCode = 0;
unchecked {
hashCode += 1000000007 * Line.GetHashCode();
hashCode += 1000000009 * Column.GetHashCode();
hashCode += 1000000021 * VisualColumn.GetHashCode();
}
return hashCode;
}
/// <summary>
/// Equality test.
/// </summary>
public bool Equals(TextViewPosition other)
{
return this.Line == other.Line && this.Column == other.Column && this.VisualColumn == other.VisualColumn;
}
/// <summary>
/// Equality test.
/// </summary>
public static bool operator ==(TextViewPosition left, TextViewPosition right)
{
return left.Equals(right);
}
/// <summary>
/// Inequality test.
/// </summary>
public static bool operator !=(TextViewPosition left, TextViewPosition right)
{
return !(left.Equals(right)); // use operator == and negate result
}
#endregion
}
}

56
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/TextViewWeakEventManager.cs

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// Contains weak event managers for the TextView events.
/// </summary>
public static class TextViewWeakEventManager
{
/// <summary>
/// Weak event manager for the <see cref="TextView.DocumentChanged"/> event.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
public sealed class DocumentChanged : WeakEventManagerBase<DocumentChanged, TextView>
{
/// <inheritdoc/>
protected override void StartListening(TextView source)
{
source.DocumentChanged += DeliverEvent;
}
/// <inheritdoc/>
protected override void StopListening(TextView source)
{
source.DocumentChanged -= DeliverEvent;
}
}
/// <summary>
/// Weak event manager for the <see cref="TextView.VisualLinesChanged"/> event.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
public sealed class VisualLinesChanged : WeakEventManagerBase<VisualLinesChanged, TextView>
{
/// <inheritdoc/>
protected override void StartListening(TextView source)
{
source.VisualLinesChanged += DeliverEvent;
}
/// <inheritdoc/>
protected override void StopListening(TextView source)
{
source.VisualLinesChanged -= DeliverEvent;
}
}
}
}

334
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLine.cs

@ -0,0 +1,334 @@ @@ -0,0 +1,334 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Windows;
using System.Windows.Media.TextFormatting;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// Represents a visual line in the document.
/// A visual line usually corresponds to one DocumentLine, but it can span multiple lines if
/// all but the first are collapsed.
/// </summary>
public sealed class VisualLine
{
List<VisualLineElement> elements;
/// <summary>
/// Gets the first document line displayed by this visual line.
/// </summary>
public DocumentLine FirstDocumentLine { get; private set; }
/// <summary>
/// Gets the last document line displayed by this visual line.
/// </summary>
public DocumentLine LastDocumentLine { get; private set; }
/// <summary>
/// Gets a read-only collection of line elements.
/// </summary>
public ReadOnlyCollection<VisualLineElement> Elements { get; private set; }
/// <summary>
/// Gets a read-only collection of text lines.
/// </summary>
public ReadOnlyCollection<TextLine> TextLines { get; private set; }
/// <summary>
/// Length in visual line coordinates.
/// </summary>
public int VisualLength { get; private set; }
/// <summary>
/// Gets the height of the visual line in device-independent pixels.
/// </summary>
public double Height { get; private set; }
/// <summary>
/// Gets the position at which the line is visible.
/// </summary>
public double VisualTop { get; internal set; }
internal VisualLine(DocumentLine firstDocumentLine)
{
Debug.Assert(firstDocumentLine != null);
this.FirstDocumentLine = firstDocumentLine;
}
internal void ConstructVisualElements(ITextRunConstructionContext context, VisualLineElementGenerator[] generators)
{
foreach (VisualLineElementGenerator g in generators) {
g.StartGeneration(context);
}
elements = new List<VisualLineElement>();
PerformVisualElementConstruction(generators);
foreach (VisualLineElementGenerator g in generators) {
g.FinishGeneration();
}
// if (FirstDocumentLine.Length != 0)
// elements.Add(new VisualLineText(FirstDocumentLine.Text, FirstDocumentLine.Length));
// //elements.Add(new VisualNewLine(VisualNewLine.NewLineType.Lf));
this.Elements = elements.AsReadOnly();
CalculateOffsets(context.GlobalTextRunProperties);
}
void PerformVisualElementConstruction(VisualLineElementGenerator[] generators)
{
TextDocument document = FirstDocumentLine.Document;
int offset = FirstDocumentLine.Offset;
int currentLineEnd = offset + FirstDocumentLine.Length;
LastDocumentLine = FirstDocumentLine;
int askInterestOffset = 0; // 0 or 1
while (offset + askInterestOffset <= currentLineEnd) {
int textPieceEndOffset = currentLineEnd;
foreach (VisualLineElementGenerator g in generators) {
g.cachedInterest = g.GetFirstInterestedOffset(offset + askInterestOffset);
if (g.cachedInterest != -1) {
if (g.cachedInterest < offset)
throw new ArgumentOutOfRangeException(g.GetType().Name + ".GetFirstInterestedOffset",
g.cachedInterest,
"GetFirstInterestedOffset must not return an offset less than startOffset. Return -1 to signal no interest.");
if (g.cachedInterest < textPieceEndOffset)
textPieceEndOffset = g.cachedInterest;
}
}
Debug.Assert(textPieceEndOffset >= offset);
if (textPieceEndOffset > offset) {
int textPieceLength = textPieceEndOffset - offset;
elements.Add(new VisualLineText(this, textPieceLength));
offset = textPieceEndOffset;
}
// if no elements constructed / only zero-length elements constructed:
// prevent endless loop by asking the generators again for the same location
askInterestOffset = 1;
foreach (VisualLineElementGenerator g in generators) {
if (g.cachedInterest == offset) {
VisualLineElement element = g.ConstructElement(offset);
if (element != null) {
elements.Add(element);
if (element.DocumentLength > 0) {
// a non-zero-length element was constructed
askInterestOffset = 0;
offset += element.DocumentLength;
if (offset > currentLineEnd) {
LastDocumentLine = document.GetLineByOffset(offset);
currentLineEnd = LastDocumentLine.Offset + LastDocumentLine.Length;
}
break;
}
}
}
}
}
}
void CalculateOffsets(TextRunProperties globalTextRunProperties)
{
int visualOffset = 0;
int textOffset = 0;
foreach (VisualLineElement element in Elements) {
element.VisualColumn = visualOffset;
element.RelativeTextOffset = textOffset;
element.SetTextRunProperties(new VisualLineElementTextRunProperties(globalTextRunProperties));
visualOffset += element.VisualLength;
textOffset += element.DocumentLength;
}
VisualLength = visualOffset;
Debug.Assert(textOffset == LastDocumentLine.Offset + LastDocumentLine.Length - FirstDocumentLine.Offset);
}
internal void RunTransformers(ITextRunConstructionContext context, IVisualLineTransformer[] transformers)
{
foreach (IVisualLineTransformer transformer in transformers) {
transformer.Transform(context, elements);
}
}
internal void SetTextLines(List<TextLine> textLines)
{
this.TextLines = textLines.AsReadOnly();
Height = 0;
foreach (TextLine line in textLines)
Height += line.Height;
}
/// <summary>
/// Gets the visual column from a document offset relative to the first line start.
/// </summary>
public int GetVisualColumn(int relativeTextOffset)
{
if (relativeTextOffset < 0)
throw new ArgumentOutOfRangeException("relativeTextOffset", relativeTextOffset, "Value must be non-negative");
foreach (VisualLineElement element in elements) {
if (element.RelativeTextOffset <= relativeTextOffset
&& element.RelativeTextOffset + element.DocumentLength >= relativeTextOffset)
{
return element.GetVisualColumn(relativeTextOffset);
}
}
return VisualLength;
}
/// <summary>
/// Gets the document offset (relative to the first line start) from a visual column.
/// </summary>
public int GetRelativeOffset(int visualColumn)
{
if (visualColumn < 0)
throw new ArgumentOutOfRangeException("visualColumn", visualColumn, "Value must be non-negative");
int documentLength = 0;
foreach (VisualLineElement element in elements) {
if (element.VisualColumn <= visualColumn
&& element.VisualColumn + element.VisualLength > visualColumn)
{
return element.GetRelativeOffset(visualColumn);
}
documentLength += element.DocumentLength;
}
return documentLength;
}
/// <summary>
/// Gets the text line containing the specified visual column.
/// </summary>
public TextLine GetTextLine(int visualColumn)
{
if (visualColumn < 0 || visualColumn > VisualLength)
throw new ArgumentOutOfRangeException("visualColumn", visualColumn, "Value must be between 0 and " + VisualLength);
if (visualColumn == VisualLength)
return TextLines[TextLines.Count - 1];
foreach (TextLine line in TextLines) {
if (visualColumn < line.Length)
return line;
else
visualColumn -= line.Length;
}
throw new InvalidOperationException("Shouldn't happen (VisualLength incorrect?)");
}
/// <summary>
/// Gets the visual top from the specified text line.
/// </summary>
/// <returns>Distance in device-independent pixels
/// from the top of the document to the top of the specified text line.</returns>
public double GetTextLineVisualTop(TextLine textLine)
{
if (!TextLines.Contains(textLine))
throw new ArgumentException("textLine is not a line in this VisualLine");
double pos = VisualTop;
foreach (TextLine tl in TextLines) {
if (tl == textLine)
break;
else
pos += tl.Height;
}
return pos;
}
/// <summary>
/// Gets the start visual column from the specified text line.
/// </summary>
public int GetTextLineVisualStartColumn(TextLine textLine)
{
if (!TextLines.Contains(textLine))
throw new ArgumentException("textLine is not a line in this VisualLine");
int col = 0;
foreach (TextLine tl in TextLines) {
if (tl == textLine)
break;
else
col += tl.Length;
}
return col;
}
/// <summary>
/// Gets a TextLine by the visual position.
/// </summary>
public TextLine GetTextLineByVisualTop(double visualTop)
{
const double epsilon = 0.0001;
double pos = this.VisualTop;
foreach (TextLine tl in TextLines) {
pos += tl.Height;
if (visualTop + epsilon < pos)
return tl;
}
return TextLines[TextLines.Count - 1];
}
/// <summary>
/// Gets the visual position from the specified visualColumn.
/// </summary>
/// <returns>Position in device-independent pixels
/// relative to the top left of the document.</returns>
public Point GetVisualPosition(int visualColumn)
{
TextLine textLine = GetTextLine(visualColumn);
double xPos = textLine.GetDistanceFromCharacterHit(new CharacterHit(visualColumn, 0));
double yPos = GetTextLineVisualTop(textLine);
return new Point(xPos, yPos);
}
/// <summary>
/// Gets the visual column from a document position (relative to top left of the document).
/// </summary>
public int GetVisualColumn(Point point)
{
TextLine textLine = GetTextLineByVisualTop(point.Y);
CharacterHit ch = textLine.GetCharacterHitFromDistance(point.X);
return ch.FirstCharacterIndex + ch.TrailingLength;
}
/// <summary>
/// Gets whether the visual line was disposed.
/// </summary>
public bool IsDisposed { get; internal set; }
/// <summary>
/// Gets the next possible caret position after visualColumn, or -1 if there is no caret position.
/// </summary>
public int GetNextCaretPosition(int visualColumn, bool backwards, CaretPositioningMode mode)
{
int i;
if (backwards) {
for (i = elements.Count - 1; i >= 0; i--) {
if (elements[i].VisualColumn < visualColumn)
break;
}
for (; i >= 0; i--) {
int pos = elements[i].GetNextCaretPosition(
Math.Min(visualColumn, elements[i].VisualColumn + elements[i].VisualLength + 1),
backwards, mode);
if (pos >= 0)
return pos;
}
} else {
for (i = 0; i < elements.Count; i++) {
if (elements[i].VisualColumn + elements[i].VisualLength > visualColumn)
break;
}
for (; i < elements.Count; i++) {
int pos = elements[i].GetNextCaretPosition(
Math.Max(visualColumn, elements[i].VisualColumn - 1),
backwards, mode);
if (pos >= 0)
return pos;
}
}
return -1;
}
}
}

215
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLineElement.cs

@ -0,0 +1,215 @@ @@ -0,0 +1,215 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using System.Windows.Input;
using System.Windows.Media.TextFormatting;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// Represents a visual element in the document.
/// </summary>
public abstract class VisualLineElement
{
/// <summary>
/// Creates a new VisualLineElement.
/// </summary>
/// <param name="visualLength">The length of the element in VisualLine coordinates. Must be positive.</param>
/// <param name="documentLength">The length of the element in the document. Must be non-negative.</param>
protected VisualLineElement(int visualLength, int documentLength)
{
if (visualLength < 1)
throw new ArgumentOutOfRangeException("visualLength", visualLength, "Value must be at least 1");
if (documentLength < 0)
throw new ArgumentOutOfRangeException("documentLength", documentLength, "Value must be at least 0");
this.VisualLength = visualLength;
this.DocumentLength = documentLength;
}
/// <summary>
/// Gets the length of this element in visual columns.
/// </summary>
public int VisualLength { get; private set; }
/// <summary>
/// Gets the length of this element in the text document.
/// </summary>
public int DocumentLength { get; private set; }
/// <summary>
/// Gets the visual column where this element starts.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods",
Justification = "This property holds the start visual column, use GetVisualColumn to get inner visual columns.")]
public int VisualColumn { get; internal set; }
/// <summary>
/// Gets the text offset where this element starts, relative to the start text offset of the visual line.
/// </summary>
public int RelativeTextOffset { get; internal set; }
/// <summary>
/// Gets the text run properties.
/// A unique <see cref="VisualLineElementTextRunProperties"/> instance is used for each
/// <see cref="VisualLineElement"/>; colorizing code may assume that modifying the
/// <see cref="VisualLineElementTextRunProperties"/> will affect only this
/// <see cref="VisualLineElement"/>.
/// </summary>
public VisualLineElementTextRunProperties TextRunProperties { get; set; }
internal void SetTextRunProperties(VisualLineElementTextRunProperties p)
{
this.TextRunProperties = p;
}
/// <summary>
/// Creates the TextRun for this line element.
/// </summary>
/// <param name="startVisualColumn">
/// The visual column from which the run should be constructed.
/// Normally the same value as the <see cref="VisualColumn"/> property is used to construct the full run;
/// but when word-wrapping is active, partial runs might be created.
/// </param>
/// <param name="context">
/// Context object that contains information relevant for text run creation.
/// </param>
public abstract TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context);
/// <summary>
/// Gets if this VisualLineElement can be split.
/// </summary>
public virtual bool CanSplit {
get { return false; }
}
/// <summary>
/// Splits the element.
/// </summary>
/// <param name="splitVisualColumn">Position inside this element at which it should be broken</param>
/// <param name="elements">The collection of line elements</param>
/// <param name="elementIndex">The index at which this element is in the elements list.</param>
public virtual void Split(int splitVisualColumn, IList<VisualLineElement> elements, int elementIndex)
{
throw new NotSupportedException();
}
/// <summary>
/// Helper method for splitting this line element into two, correctly updating the
/// <see cref="VisualLength"/>, <see cref="DocumentLength"/>, <see cref="VisualColumn"/>
/// and <see cref="RelativeTextOffset"/> properties.
/// </summary>
/// <param name="firstPart">The element before the split position.</param>
/// <param name="secondPart">The element after the split position.</param>
/// <param name="splitVisualColumn">The split position as visual column.</param>
/// <param name="splitRelativeTextOffset">The split position as text offset.</param>
protected void SplitHelper(VisualLineElement firstPart, VisualLineElement secondPart, int splitVisualColumn, int splitRelativeTextOffset)
{
if (firstPart == null)
throw new ArgumentNullException("firstPart");
if (secondPart == null)
throw new ArgumentNullException("secondPart");
int relativeSplitVisualColumn = splitVisualColumn - VisualColumn;
int relativeSplitRelativeTextOffset = splitRelativeTextOffset - RelativeTextOffset;
if (relativeSplitVisualColumn <= 0 || relativeSplitVisualColumn >= VisualLength)
throw new ArgumentOutOfRangeException("splitVisualColumn", splitVisualColumn, "Value must be between " + (VisualColumn + 1) + " and " + (VisualColumn + VisualLength - 1));
if (relativeSplitRelativeTextOffset < 0 || relativeSplitRelativeTextOffset > DocumentLength)
throw new ArgumentOutOfRangeException("splitRelativeTextOffset", splitRelativeTextOffset, "Value must be between " + (RelativeTextOffset) + " and " + (RelativeTextOffset + DocumentLength));
int oldVisualLength = VisualLength;
int oldDocumentLength = DocumentLength;
int oldVisualColumn = VisualColumn;
int oldRelativeTextOffset = RelativeTextOffset;
firstPart.VisualColumn = oldVisualColumn;
secondPart.VisualColumn = oldVisualColumn + relativeSplitVisualColumn;
firstPart.RelativeTextOffset = oldRelativeTextOffset;
secondPart.RelativeTextOffset = oldRelativeTextOffset + relativeSplitRelativeTextOffset;
firstPart.VisualLength = relativeSplitVisualColumn;
secondPart.VisualLength = oldVisualLength - relativeSplitVisualColumn;
firstPart.DocumentLength = relativeSplitRelativeTextOffset;
secondPart.DocumentLength = oldDocumentLength - relativeSplitRelativeTextOffset;
if (firstPart.TextRunProperties == null)
firstPart.TextRunProperties = new VisualLineElementTextRunProperties(TextRunProperties);
if (secondPart.TextRunProperties == null)
secondPart.TextRunProperties = new VisualLineElementTextRunProperties(TextRunProperties);
}
/// <summary>
/// Gets the visual column of a text location inside this element.
/// The text offset is given relative to the visual line start.
/// </summary>
public virtual int GetVisualColumn(int relativeTextOffset)
{
if (relativeTextOffset >= this.RelativeTextOffset + DocumentLength)
return VisualColumn + VisualLength;
else
return VisualColumn;
}
/// <summary>
/// Gets the text offset of a visual column inside this element.
/// </summary>
/// <returns>A text offset relative to the visual line start.</returns>
public virtual int GetRelativeOffset(int visualColumn)
{
if (visualColumn >= this.VisualColumn + VisualLength)
return RelativeTextOffset + DocumentLength;
else
return RelativeTextOffset;
}
/// <summary>
/// Gets the next caret position inside this element.
/// </summary>
/// <param name="visualColumn">The visual column from which the search should be started.</param>
/// <param name="backwards">Whether to search backwards (false=forwards,true=backwards).</param>
/// <param name="mode">Whether to stop only at word borders.</param>
/// <returns>The visual column of the next caret position, or -1 if there is no next caret position.</returns>
/// <remarks>
/// In the space between two line elements, usually both of them contain a caret position.
/// </remarks>
public virtual int GetNextCaretPosition(int visualColumn, bool backwards, CaretPositioningMode mode)
{
int stop1 = this.VisualColumn;
int stop2 = this.VisualColumn + this.VisualLength;
if (backwards) {
if (mode != CaretPositioningMode.WordStart && visualColumn > stop2)
return stop2;
else if (visualColumn > stop1)
return stop1;
} else {
if (visualColumn < stop1)
return stop1;
else if (mode != CaretPositioningMode.WordStart && visualColumn < stop2)
return stop2;
}
return -1;
}
/// <summary>
/// Queries the cursor over the visual line element.
/// </summary>
protected internal virtual void OnQueryCursor(QueryCursorEventArgs e)
{
}
/// <summary>
/// Allows the visual line element to handle a mouse event.
/// </summary>
protected internal virtual void OnMouseDown(MouseButtonEventArgs e)
{
}
/// <summary>
/// Allows the visual line element to handle a mouse event.
/// </summary>
protected internal virtual void OnMouseUp(MouseButtonEventArgs e)
{
}
}
}

62
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLineElementGenerator.cs

@ -0,0 +1,62 @@ @@ -0,0 +1,62 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// Abstract base class for generators that produce new visual line elements.
/// </summary>
public abstract class VisualLineElementGenerator
{
/// <summary>
/// Gets the text run construction context.
/// </summary>
protected ITextRunConstructionContext CurrentContext { get; private set; }
/// <summary>
/// Initializes the generator for the <see cref="ITextRunConstructionContext"/>
/// </summary>
public virtual void StartGeneration(ITextRunConstructionContext context)
{
if (context == null)
throw new ArgumentNullException("context");
this.CurrentContext = context;
}
/// <summary>
/// De-initializes the generator.
/// </summary>
public virtual void FinishGeneration()
{
this.CurrentContext = null;
}
/// <summary>
/// Should only be used by VisualLine.ConstructVisualElements.
/// </summary>
internal int cachedInterest;
/// <summary>
/// Gets the first offset >= startOffset where the generator wants to construct an element.
/// Return -1 to signal no interest.
/// </summary>
public abstract int GetFirstInterestedOffset(int startOffset);
/// <summary>
/// Constructs an element at the specified offset.
/// May return null if no element should be constructed.
/// </summary>
/// <remarks>
/// Avoid signalling interest and then building no element by returning null - doing so
/// causes the generated <see cref="VisualLineText"/> elements to be unnecessarily split
/// at the position where you signalled interest.
/// </remarks>
public abstract VisualLineElement ConstructElement(int offset);
}
}

193
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLineElementTextRunProperties.cs

@ -0,0 +1,193 @@ @@ -0,0 +1,193 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// <see cref="TextRunProperties"/> implementation that allows changing the properties.
/// A <see cref="VisualLineElementTextRunProperties"/> instance usually is assigned to a single
/// <see cref="VisualLineElement"/>.
/// </summary>
public class VisualLineElementTextRunProperties : TextRunProperties
{
Brush backgroundBrush;
BaselineAlignment baselineAlignment;
CultureInfo cultureInfo;
double fontHintingEmSize;
double fontRenderingEmSize;
Brush foregroundBrush;
Typeface typeface;
TextDecorationCollection textDecorations;
TextEffectCollection textEffects;
/// <summary>
/// Creates a new VisualLineElementTextRunProperties instance that copies its values
/// from the specified <paramref name="textRunProperties"/>.
/// For the <see cref="TextDecorations"/> and <see cref="TextEffects"/> collections, deep copies
/// are created if those collections are not frozen.
/// </summary>
public VisualLineElementTextRunProperties(TextRunProperties textRunProperties)
{
if (textRunProperties == null)
throw new ArgumentNullException("textRunProperties");
backgroundBrush = textRunProperties.BackgroundBrush;
baselineAlignment = textRunProperties.BaselineAlignment;
cultureInfo = textRunProperties.CultureInfo;
fontHintingEmSize = textRunProperties.FontHintingEmSize;
fontRenderingEmSize = textRunProperties.FontRenderingEmSize;
foregroundBrush = textRunProperties.ForegroundBrush;
typeface = textRunProperties.Typeface;
textDecorations = textRunProperties.TextDecorations;
if (textDecorations != null && !textDecorations.IsFrozen) {
textDecorations = textDecorations.Clone();
}
textEffects = textRunProperties.TextEffects;
if (textEffects != null && !textEffects.IsFrozen) {
textEffects = textEffects.Clone();
}
}
/// <inheritdoc/>
public override Brush BackgroundBrush {
get { return backgroundBrush; }
}
/// <summary>
/// Sets the <see cref="BackgroundBrush"/>.
/// </summary>
public void SetBackgroundBrush(Brush value)
{
backgroundBrush = value;
}
/// <inheritdoc/>
public override BaselineAlignment BaselineAlignment {
get { return baselineAlignment; }
}
/// <summary>
/// Sets the <see cref="BaselineAlignment"/>.
/// </summary>
public void SetBaselineAlignment(BaselineAlignment value)
{
baselineAlignment = value;
}
/// <inheritdoc/>
public override CultureInfo CultureInfo {
get { return cultureInfo; }
}
/// <summary>
/// Sets the <see cref="CultureInfo"/>.
/// </summary>
public void SetCultureInfo(CultureInfo value)
{
if (value == null)
throw new ArgumentNullException("value");
cultureInfo = value;
}
/// <inheritdoc/>
public override double FontHintingEmSize {
get { return fontHintingEmSize; }
}
/// <summary>
/// Sets the <see cref="FontHintingEmSize"/>.
/// </summary>
public void SetFontHintingEmSize(double value)
{
fontHintingEmSize = value;
}
/// <inheritdoc/>
public override double FontRenderingEmSize {
get { return fontRenderingEmSize; }
}
/// <summary>
/// Sets the <see cref="FontRenderingEmSize"/>.
/// </summary>
public void SetFontRenderingEmSize(double value)
{
fontRenderingEmSize = value;
}
/// <inheritdoc/>
public override Brush ForegroundBrush {
get { return foregroundBrush; }
}
/// <summary>
/// Sets the <see cref="ForegroundBrush"/>.
/// </summary>
public void SetForegroundBrush(Brush value)
{
foregroundBrush = value;
}
/// <inheritdoc/>
public override Typeface Typeface {
get { return typeface; }
}
/// <summary>
/// Sets the <see cref="Typeface"/>.
/// </summary>
public void SetTypeface(Typeface value)
{
if (value == null)
throw new ArgumentNullException("value");
typeface = value;
}
/// <summary>
/// Gets the text decorations. The value may be null, a frozen <see cref="TextDecorationCollection"/>
/// or an unfrozen <see cref="TextDecorationCollection"/>.
/// If the value is an unfrozen <see cref="TextDecorationCollection"/>, you may assume that the
/// collection instance is only used for this <see cref="TextRunProperties"/> instance and it is safe
/// to add <see cref="TextDecoration"/>s.
/// </summary>
public override TextDecorationCollection TextDecorations {
get { return textDecorations; }
}
/// <summary>
/// Sets the <see cref="TextDecorations"/>.
/// </summary>
public void SetTextDecorations(TextDecorationCollection value)
{
textDecorations = value;
}
/// <summary>
/// Gets the text effects. The value may be null, a frozen <see cref="TextEffectCollection"/>
/// or an unfrozen <see cref="TextEffectCollection"/>.
/// If the value is an unfrozen <see cref="TextEffectCollection"/>, you may assume that the
/// collection instance is only used for this <see cref="TextRunProperties"/> instance and it is safe
/// to add <see cref="TextEffect"/>s.
/// </summary>
public override TextEffectCollection TextEffects {
get { return textEffects; }
}
/// <summary>
/// Sets the <see cref="TextEffects"/>.
/// </summary>
public void SetTextEffects(TextEffectCollection value)
{
textEffects = value;
}
}
}

134
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLineText.cs

@ -0,0 +1,134 @@ @@ -0,0 +1,134 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using System.Windows.Media.TextFormatting;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// VisualLineElement that represents a piece of text.
/// </summary>
public class VisualLineText : VisualLineElement
{
VisualLine parentVisualLine;
/// <summary>
/// Gets the parent visual line.
/// </summary>
public VisualLine ParentVisualLine {
get { return parentVisualLine; }
}
/// <summary>
/// Creates a visual line text element with the specified length.
/// It uses the <see cref="ITextRunConstructionContext.VisualLine"/> and its
/// <see cref="VisualLineElement.RelativeTextOffset"/> to find the actual text string.
/// </summary>
public VisualLineText(VisualLine parentVisualLine, int length) : base(length, length)
{
if (parentVisualLine == null)
throw new ArgumentNullException("parentVisualLine");
this.parentVisualLine = parentVisualLine;
}
/// <summary>
/// Override this method to control the type of new VisualLineText instances when
/// the visual line is split due to syntax highlighting.
/// </summary>
protected virtual VisualLineText CreateInstance(int length)
{
return new VisualLineText(parentVisualLine, length);
}
/// <inheritdoc/>
public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context)
{
if (context == null)
throw new ArgumentNullException("context");
int relativeOffset = startVisualColumn - VisualColumn;
string text = context.Document.GetText(context.VisualLine.FirstDocumentLine.Offset + RelativeTextOffset + relativeOffset, DocumentLength - relativeOffset);
return new TextCharacters(text, 0, text.Length, this.TextRunProperties);
}
/// <inheritdoc/>
public override bool CanSplit {
get { return true; }
}
/// <inheritdoc/>
public override void Split(int splitVisualColumn, IList<VisualLineElement> elements, int elementIndex)
{
if (splitVisualColumn <= VisualColumn || splitVisualColumn >= VisualColumn + VisualLength)
throw new ArgumentOutOfRangeException("splitVisualColumn", splitVisualColumn, "Value must be between " + (VisualColumn + 1) + " and " + (VisualColumn + VisualLength - 1));
if (elements == null)
throw new ArgumentNullException("elements");
if (elements[elementIndex] != this)
throw new ArgumentException("Invalid elementIndex - couldn't find this element at the index");
int relativeSplitPos = splitVisualColumn - VisualColumn;
VisualLineText splitPart = CreateInstance(DocumentLength - relativeSplitPos);
SplitHelper(this, splitPart, splitVisualColumn, relativeSplitPos + RelativeTextOffset);
elements.Insert(elementIndex + 1, splitPart);
}
/// <inheritdoc/>
public override int GetRelativeOffset(int visualColumn)
{
return this.RelativeTextOffset + visualColumn - this.VisualColumn;
}
/// <inheritdoc/>
public override int GetVisualColumn(int relativeTextOffset)
{
return VisualColumn + relativeTextOffset - this.RelativeTextOffset;
}
/// <inheritdoc/>
public override int GetNextCaretPosition(int visualColumn, bool backwards, CaretPositioningMode mode)
{
int nextPos = backwards ? visualColumn - 1 : visualColumn + 1;
if (nextPos >= this.VisualColumn && nextPos <= this.VisualColumn + this.VisualLength) {
if (mode == CaretPositioningMode.WordBorder || mode == CaretPositioningMode.WordStart) {
TextDocument document = parentVisualLine.FirstDocumentLine.Document;
int textOffset = parentVisualLine.FirstDocumentLine.Offset + GetRelativeOffset(nextPos);
if (textOffset > 0 && textOffset < document.TextLength) {
CharClass charBefore = GetCharClass(document.GetCharAt(textOffset - 1));
CharClass charAfter = GetCharClass(document.GetCharAt(textOffset));
if (charBefore == charAfter || (charAfter == CharClass.Whitespace && mode == CaretPositioningMode.WordStart))
return GetNextCaretPosition(nextPos, backwards, mode);
}
}
return nextPos;
}
return -1;
}
enum CharClass
{
Whitespace,
IdentifierPart,
LineTerminator,
Other
}
static CharClass GetCharClass(char c)
{
if (c == '\r' || c == '\n')
return CharClass.LineTerminator;
else if (char.IsWhiteSpace(c))
return CharClass.Whitespace;
else if (char.IsLetterOrDigit(c) || c == '_')
return CharClass.IdentifierPart;
else
return CharClass.Other;
}
}
}

33
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLineTextParagraphProperties.cs

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Windows;
using System.Windows.Media.TextFormatting;
namespace ICSharpCode.AvalonEdit.Gui
{
class VisualLineTextParagraphProperties : TextParagraphProperties
{
internal TextRunProperties defaultTextRunProperties;
internal TextWrapping textWrapping;
internal double tabSize;
public override double DefaultIncrementalTab {
get { return tabSize; }
}
public override FlowDirection FlowDirection { get { return FlowDirection.LeftToRight; } }
public override TextAlignment TextAlignment { get { return TextAlignment.Left; } }
public override double LineHeight { get { return double.NaN; } }
public override bool FirstLineInParagraph { get { return false; } }
public override TextRunProperties DefaultTextRunProperties { get { return defaultTextRunProperties; } }
public override TextWrapping TextWrapping { get { return textWrapping; } }
public override TextMarkerProperties TextMarkerProperties { get { return null; } }
public override double Indent { get { return 0; } }
}
}

71
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLineTextSource.cs

@ -0,0 +1,71 @@ @@ -0,0 +1,71 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Diagnostics;
using System.Windows.Media.TextFormatting;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// WPF TextSource implementation that creates TextRuns for a VisualLine.
/// </summary>
class VisualLineTextSource : TextSource, ITextRunConstructionContext
{
public VisualLineTextSource(VisualLine visualLine)
{
this.VisualLine = visualLine;
}
public VisualLine VisualLine { get; private set; }
public TextView TextView { get; set; }
public TextDocument Document { get; set; }
public TextRunProperties GlobalTextRunProperties { get; set; }
public override TextRun GetTextRun(int textSourceCharacterIndex)
{
try {
foreach (VisualLineElement element in VisualLine.Elements) {
if (textSourceCharacterIndex >= element.VisualColumn
&& textSourceCharacterIndex < element.VisualColumn + element.VisualLength)
{
int relativeOffset = textSourceCharacterIndex - element.VisualColumn;
TextRun run = element.CreateTextRun(textSourceCharacterIndex, this);
if (run == null)
throw new ArgumentNullException(element.GetType().Name + ".CreateTextRun");
if (run.Length == 0)
throw new ArgumentException("The returned TextRun must not have length 0.", element.GetType().Name + ".Length");
if (relativeOffset + run.Length > element.VisualLength)
throw new ArgumentException("The returned TextRun is too long.", element.GetType().Name + ".CreateTextRun");
InlineObjectRun inlineRun = run as InlineObjectRun;
if (inlineRun != null) {
inlineRun.VisualLine = VisualLine;
TextView.AddInlineObject(inlineRun);
}
return run;
}
}
return new TextEndOfParagraph(1);
} catch (Exception ex) {
Debug.WriteLine(ex.ToString());
throw;
}
}
public override TextSpan<CultureSpecificCharacterBufferRange> GetPrecedingText(int textSourceCharacterIndexLimit)
{
throw new NotImplementedException();
}
public override int GetTextEffectCharacterIndexFromTextSourceCharacterIndex(int textSourceCharacterIndex)
{
throw new NotImplementedException();
}
}
}

197
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/WhitespaceElementGenerator.cs

@ -0,0 +1,197 @@ @@ -0,0 +1,197 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Windows;
using System.Windows.Media;
using ICSharpCode.AvalonEdit.Document;
using System.Windows.Media.TextFormatting;
namespace ICSharpCode.AvalonEdit.Gui
{
/// <summary>
/// Element generator that displays · for spaces and » for tabs.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace")]
public class WhitespaceElementGenerator : VisualLineElementGenerator
{
readonly static char[] tabSpace = { ' ', '\t' };
/// <summary>
/// Gets/Sets whether to show · for spaces.
/// </summary>
public bool ShowSpaces { get; set; }
/// <summary>
/// Gets/Sets whether to show » for tabs.
/// </summary>
public bool ShowTabs { get; set; }
/// <summary>
/// Creates a new WhitespaceElementGenerator instance.
/// </summary>
public WhitespaceElementGenerator()
{
this.ShowSpaces = true;
this.ShowTabs = true;
}
/// <inheritdoc/>
public override int GetFirstInterestedOffset(int startOffset)
{
DocumentLine endLine = CurrentContext.VisualLine.LastDocumentLine;
int endOffset = endLine.Offset + endLine.Length;
string relevantText = CurrentContext.Document.GetText(startOffset, endOffset - startOffset);
int pos;
if (ShowTabs && ShowSpaces)
pos = relevantText.IndexOfAny(tabSpace);
else if (ShowTabs)
pos = relevantText.IndexOf('\t');
else if (ShowSpaces)
pos = relevantText.IndexOf(' ');
else
pos = -1;
if (pos >= 0)
return startOffset + pos;
else
return -1;
}
/// <inheritdoc/>
public override VisualLineElement ConstructElement(int offset)
{
char c = CurrentContext.Document.GetCharAt(offset);
if (c == ' ') {
FormattedText text = new FormattedText(
"\u00B7",
CurrentContext.GlobalTextRunProperties.CultureInfo,
FlowDirection.LeftToRight,
CurrentContext.GlobalTextRunProperties.Typeface,
CurrentContext.GlobalTextRunProperties.FontRenderingEmSize,
Brushes.LightGray
);
return new SpaceTextElement(text);
} else if (c == '\t') {
FormattedText text = new FormattedText(
"\u00BB",
CurrentContext.GlobalTextRunProperties.CultureInfo,
FlowDirection.LeftToRight,
CurrentContext.GlobalTextRunProperties.Typeface,
CurrentContext.GlobalTextRunProperties.FontRenderingEmSize,
Brushes.LightGray
);
return new TabTextElement(text);
} else {
return null;
}
}
class SpaceTextElement : FormattedTextElement
{
public SpaceTextElement(FormattedText text) : base(text, 1)
{
BreakBefore = LineBreakCondition.BreakPossible;
BreakAfter = LineBreakCondition.BreakDesired;
}
public override int GetNextCaretPosition(int visualColumn, bool backwards, CaretPositioningMode mode)
{
if (mode == CaretPositioningMode.Normal)
return base.GetNextCaretPosition(visualColumn, backwards, mode);
else
return -1;
}
}
class TabTextElement : VisualLineElement
{
internal readonly FormattedText text;
public TabTextElement(FormattedText text) : base(2, 1)
{
this.text = text;
}
public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context)
{
if (startVisualColumn == this.VisualColumn)
return new TabGlyphRun(this, this.TextRunProperties);
else if (startVisualColumn == this.VisualColumn + 1)
return new TextCharacters("\t", 0, 1, this.TextRunProperties);
else
throw new ArgumentOutOfRangeException("startVisualColumn");
}
public override int GetNextCaretPosition(int visualColumn, bool backwards, CaretPositioningMode mode)
{
if (mode == CaretPositioningMode.Normal)
return base.GetNextCaretPosition(visualColumn, backwards, mode);
else
return -1;
}
}
class TabGlyphRun : TextEmbeddedObject
{
protected readonly TabTextElement element;
TextRunProperties properties;
public TabGlyphRun(TabTextElement element, TextRunProperties properties)
{
if (properties == null)
throw new ArgumentNullException("properties");
this.properties = properties;
this.element = element;
}
public override LineBreakCondition BreakBefore {
get { return LineBreakCondition.BreakPossible; }
}
public override LineBreakCondition BreakAfter {
get { return LineBreakCondition.BreakRestrained; }
}
public override bool HasFixedSize {
get { return true; }
}
public override CharacterBufferReference CharacterBufferReference {
get { return new CharacterBufferReference(); }
}
public override int Length {
get { return 1; }
}
public override TextRunProperties Properties {
get { return properties; }
}
public override TextEmbeddedObjectMetrics Format(double remainingParagraphWidth)
{
return new TextEmbeddedObjectMetrics(element.text.WidthIncludingTrailingWhitespace,
element.text.Height,
element.text.Baseline);
}
public override Rect ComputeBoundingBox(bool rightToLeft, bool sideways)
{
return new Rect(0, 0, element.text.WidthIncludingTrailingWhitespace, element.text.Height);
}
public override void Draw(DrawingContext drawingContext, Point origin, bool rightToLeft, bool sideways)
{
origin.Y -= element.text.Baseline;
drawingContext.DrawText(element.text, origin);
}
}
}
}

364
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/DocumentHighlighter.cs

@ -0,0 +1,364 @@ @@ -0,0 +1,364 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Utils;
using SpanStack = ICSharpCode.AvalonEdit.Utils.ImmutableStack<ICSharpCode.AvalonEdit.Highlighting.HighlightingSpan>;
namespace ICSharpCode.AvalonEdit.Highlighting
{
/// <summary>
/// This class can syntax-highlight a document.
/// It automatically manages invalidating the highlighting when the document changes.
/// </summary>
public class DocumentHighlighter : ILineTracker
{
readonly CompressingTreeList<SpanStack> storedSpanStacks = new CompressingTreeList<SpanStack>(object.ReferenceEquals);
readonly CompressingTreeList<bool> isValid = new CompressingTreeList<bool>((a, b) => a == b);
readonly TextDocument document;
readonly HighlightingRuleSet baseRuleSet;
/// <summary>
/// Creates a new DocumentHighlighter instance.
/// </summary>
public DocumentHighlighter(TextDocument document, HighlightingRuleSet baseRuleSet)
{
if (document == null)
throw new ArgumentNullException("document");
if (baseRuleSet == null)
throw new ArgumentNullException("baseRuleSet");
this.document = document;
this.baseRuleSet = baseRuleSet;
document.LineTracker.Add(new WeakLineTracker(document, this));
InvalidateHighlighting();
}
void ILineTracker.BeforeRemoveLine(DocumentLine line)
{
int number = line.LineNumber;
InvalidateHighlighting();
storedSpanStacks.RemoveAt(number);
isValid.RemoveAt(number);
if (number < isValid.Count) {
isValid[number] = false;
if (number < firstInvalidLine)
firstInvalidLine = number;
}
}
void ILineTracker.SetLineLength(DocumentLine line, int newTotalLength)
{
int number = line.LineNumber;
isValid[number] = false;
if (number < firstInvalidLine)
firstInvalidLine = number;
}
void ILineTracker.LineInserted(DocumentLine insertionPos, DocumentLine newLine)
{
Debug.Assert(insertionPos.LineNumber + 1 == newLine.LineNumber);
int lineNumber = newLine.LineNumber;
storedSpanStacks.Insert(lineNumber, null);
isValid.Insert(lineNumber, false);
if (lineNumber < firstInvalidLine)
firstInvalidLine = lineNumber;
}
void ILineTracker.RebuildDocument()
{
InvalidateHighlighting();
}
/// <summary>
/// Invalidates all stored highlighting info.
/// When the document changes, the highlighting is invalidated automatically, this method
/// needs to be called only when there are changes to the highlighting rule set.
/// </summary>
public void InvalidateHighlighting()
{
storedSpanStacks.Clear();
storedSpanStacks.Add(SpanStack.Empty);
storedSpanStacks.InsertRange(1, document.LineCount, null);
isValid.Clear();
isValid.Add(true);
isValid.InsertRange(1, document.LineCount, false);
firstInvalidLine = 1;
}
int firstInvalidLine;
/// <summary>
/// Highlights the specified document line.
/// </summary>
/// <param name="line">The line to highlight.</param>
/// <returns>A <see cref="HighlightedLine"/> line object that represents the highlighted sections.</returns>
public HighlightedLine HighlightLine(DocumentLine line)
{
if (!document.Lines.Contains(line))
throw new ArgumentException("The specified line does not belong to the document.");
highlightedLine = null;
int targetLineNumber = line.LineNumber;
while (firstInvalidLine < targetLineNumber) {
HighlightLineAndUpdateTreeList(document.GetLineByNumber(firstInvalidLine), firstInvalidLine);
}
highlightedLine = new HighlightedLine { DocumentLine = line };
HighlightLineAndUpdateTreeList(line, targetLineNumber);
return highlightedLine;
}
void HighlightLineAndUpdateTreeList(DocumentLine line, int lineNumber)
{
//Debug.WriteLine("Highlight line " + lineNumber + (highlightedLine != null ? "" : " (span stack only)"));
spanStack = storedSpanStacks[lineNumber - 1];
HighlightLineInternal(line);
if (storedSpanStacks[lineNumber] != spanStack) {
isValid[lineNumber] = true;
//Debug.WriteLine("Span stack in line " + lineNumber + " changed from " + storedSpanStacks[lineNumber] + " to " + spanStack);
storedSpanStacks[lineNumber] = spanStack;
if (lineNumber + 1 < isValid.Count) {
isValid[lineNumber + 1] = false;
firstInvalidLine = lineNumber + 1;
} else {
firstInvalidLine = int.MaxValue;
}
OnHighlightStateChanged(line, lineNumber);
} else if (firstInvalidLine == lineNumber) {
isValid[lineNumber] = true;
firstInvalidLine = isValid.IndexOf(false);
if (firstInvalidLine < 0)
firstInvalidLine = int.MaxValue;
}
}
/// <summary>
/// Is called from the highlighting state at the end of the specified line has changed.
/// </summary>
protected virtual void OnHighlightStateChanged(DocumentLine line, int lineNumber)
{
}
#region Highlighting Engine
SpanStack spanStack;
// local variables from HighlightLineInternal (are member because they are accessed by HighlighLine helper methods)
string lineText;
int lineStartOffset;
int position;
HighlightedLine highlightedLine;
void HighlightLineInternal(DocumentLine line)
{
lineStartOffset = line.Offset;
lineText = line.Text;
position = 0;
ResetColorStack();
HighlightingRuleSet currentRuleSet = this.CurrentRuleSet;
Stack<Match[]> storedMatchArrays = new Stack<Match[]>();
Match[] matches = AllocateMatchArray(currentRuleSet.Spans.Count);
Match endSpanMatch = null;
while (true) {
for (int i = 0; i < matches.Length; i++) {
if (matches[i] == null || (matches[i].Success && matches[i].Index < position))
matches[i] = currentRuleSet.Spans[i].StartExpression.Match(lineText, position);
}
if (endSpanMatch == null && !spanStack.IsEmpty)
endSpanMatch = spanStack.Peek().EndExpression.Match(lineText, position);
Match firstMatch = Minimum(matches, endSpanMatch);
if (firstMatch == null)
break;
HighlightNonSpans(firstMatch.Index);
Debug.Assert(position == firstMatch.Index);
if (firstMatch == endSpanMatch) {
PopColor(); // pop SpanColor
HighlightingSpan poppedSpan = spanStack.Peek();
PushColor(poppedSpan.EndColor);
position = firstMatch.Index + firstMatch.Length;
PopColor(); // pop EndColor
spanStack = spanStack.Pop();
currentRuleSet = this.CurrentRuleSet;
//FreeMatchArray(matches);
if (storedMatchArrays.Count > 0) {
matches = storedMatchArrays.Pop();
int index = currentRuleSet.Spans.IndexOf(poppedSpan);
Debug.Assert(index >= 0 && index < matches.Length);
if (matches[index].Index == position) {
throw new InvalidOperationException(
"A highlighting span matched 0 characters, which would cause an endlees loop.\n" +
"Change the highlighting definition so that either the start or the end regex matches at least one character.\n" +
"Start regex: " + poppedSpan.StartExpression + "\n" +
"End regex: " + poppedSpan.EndExpression);
}
} else {
matches = AllocateMatchArray(currentRuleSet.Spans.Count);
}
} else {
int index = Array.IndexOf(matches, firstMatch);
Debug.Assert(index >= 0);
HighlightingSpan newSpan = currentRuleSet.Spans[index];
spanStack = spanStack.Push(newSpan);
currentRuleSet = this.CurrentRuleSet;
storedMatchArrays.Push(matches);
matches = AllocateMatchArray(currentRuleSet.Spans.Count);
PushColor(newSpan.StartColor);
position = firstMatch.Index + firstMatch.Length;
PopColor();
PushColor(newSpan.SpanColor);
}
endSpanMatch = null;
}
HighlightNonSpans(line.Length);
PopAllColors();
}
void HighlightNonSpans(int until)
{
Debug.Assert(position <= until);
if (position == until)
return;
if (highlightedLine != null) {
IList<HighlightingRule> rules = CurrentRuleSet.Rules;
Match[] matches = AllocateMatchArray(rules.Count);
while (true) {
for (int i = 0; i < matches.Length; i++) {
if (matches[i] == null || (matches[i].Success && matches[i].Index < position))
matches[i] = rules[i].Regex.Match(lineText, position, until - position);
}
Match firstMatch = Minimum(matches, null);
if (firstMatch == null)
break;
position = firstMatch.Index;
int ruleIndex = Array.IndexOf(matches, firstMatch);
if (firstMatch.Length == 0) {
throw new InvalidOperationException(
"A highlighting rule matched 0 characters, which would cause an endlees loop.\n" +
"Change the highlighting definition so that the rule matches at least one character.\n" +
"Regex: " + rules[ruleIndex].Regex);
}
PushColor(rules[ruleIndex].Color);
position = firstMatch.Index + firstMatch.Length;
PopColor();
}
//FreeMatchArray(matches);
}
position = until;
}
static readonly HighlightingRuleSet emptyRuleSet = new HighlightingRuleSet();
HighlightingRuleSet CurrentRuleSet {
get {
if (spanStack.IsEmpty)
return baseRuleSet;
else
return spanStack.Peek().RuleSet ?? emptyRuleSet;
}
}
#endregion
#region Color Stack Management
Stack<HighlightedSection> highlightedSectionStack;
HighlightedSection lastPoppedSection;
void ResetColorStack()
{
Debug.Assert(position == 0);
lastPoppedSection = null;
if (highlightedLine == null) {
highlightedSectionStack = null;
} else {
highlightedSectionStack = new Stack<HighlightedSection>();
foreach (HighlightingSpan span in spanStack.Reverse()) {
PushColor(span.SpanColor);
}
}
}
void PushColor(HighlightingColor color)
{
if (highlightedLine == null)
return;
if (color == null) {
highlightedSectionStack.Push(null);
} else if (lastPoppedSection != null && lastPoppedSection.Color == color
&& lastPoppedSection.Offset + lastPoppedSection.Length == position + lineStartOffset)
{
highlightedSectionStack.Push(lastPoppedSection);
lastPoppedSection = null;
} else {
HighlightedSection hs = new HighlightedSection {
Offset = position + lineStartOffset,
Color = color
};
highlightedLine.Sections.Add(hs);
highlightedSectionStack.Push(hs);
lastPoppedSection = null;
}
}
void PopColor()
{
if (highlightedLine == null)
return;
HighlightedSection s = highlightedSectionStack.Pop();
if (s != null) {
s.Length = (position + lineStartOffset) - s.Offset;
if (s.Length == 0)
highlightedLine.Sections.Remove(s);
else
lastPoppedSection = s;
}
}
void PopAllColors()
{
if (highlightedSectionStack != null) {
while (highlightedSectionStack.Count > 0)
PopColor();
}
}
#endregion
#region Match helpers
/// <summary>
/// Returns the first match from the array or endSpanMatch.
/// </summary>
static Match Minimum(Match[] arr, Match endSpanMatch)
{
Match min = null;
foreach (Match v in arr) {
if (v.Success && (min == null || v.Index < min.Index))
min = v;
}
if (endSpanMatch != null && endSpanMatch.Success && (min == null || endSpanMatch.Index < min.Index))
return endSpanMatch;
else
return min;
}
static Match[] AllocateMatchArray(int count)
{
if (count == 0)
return Empty<Match>.Array;
else
return new Match[count];
}
#endregion
}
}

114
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightedLine.cs

@ -0,0 +1,114 @@ @@ -0,0 +1,114 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using System.Text;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Highlighting
{
/// <summary>
/// Represents a highlighted document line.
/// </summary>
public class HighlightedLine
{
/// <summary>
/// Creates a new HighlightedLine instance.
/// </summary>
public HighlightedLine()
{
this.Sections = new List<HighlightedSection>();
}
/// <summary>
/// Gets/Sets the document line associated with this HighlightedLine.
/// </summary>
public DocumentLine DocumentLine { get; set; }
/// <summary>
/// Gets the highlighted sections.
/// The sections are not overlapping, but they may be nested.
/// In that case, outer sections come in the list before inner sections.
/// The sections are sorted by start offset.
/// </summary>
public IList<HighlightedSection> Sections { get; private set; }
sealed class HtmlElement : IComparable<HtmlElement>
{
internal readonly int Offset;
internal readonly int Nesting;
internal readonly bool IsEnd;
internal readonly HighlightingColor Color;
public HtmlElement(int offset, int nesting, bool isEnd, HighlightingColor color)
{
this.Offset = offset;
this.Nesting = nesting;
this.IsEnd = isEnd;
this.Color = color;
}
public int CompareTo(HtmlElement other)
{
int r = Offset.CompareTo(other.Offset);
if (r != 0)
return r;
if (IsEnd != other.IsEnd) {
if (IsEnd)
return -1;
else
return 1;
} else {
if (IsEnd)
return -Nesting.CompareTo(other.Nesting);
else
return Nesting.CompareTo(other.Nesting);
}
}
}
/// <summary>
/// Produces HTML code for the line, with &lt;span class="colorName"&gt; tags.
/// </summary>
public string ToHtml()
{
List<HtmlElement> elements = new List<HtmlElement>();
for (int i = 0; i < this.Sections.Count; i++) {
HighlightedSection s = this.Sections[i];
elements.Add(new HtmlElement(s.Offset, i, false, s.Color));
elements.Add(new HtmlElement(s.Offset + s.Length, i, true, s.Color));
}
elements.Sort();
TextDocument document = DocumentLine.Document;
StringBuilder b = new StringBuilder();
int textOffset = DocumentLine.Offset;
foreach (HtmlElement e in elements) {
b.Append(document.GetText(textOffset, e.Offset - textOffset));
textOffset = e.Offset;
if (e.IsEnd) {
b.Append("</span>");
} else {
b.Append("<span style=\"");
b.Append(e.Color.ToCss());
b.Append("\">");
}
}
b.Append(document.GetText(textOffset, DocumentLine.Offset + DocumentLine.Length - textOffset));
return b.ToString();
}
/// <inheritdoc/>
public override string ToString()
{
return "[" + GetType().Name + " " + ToHtml() + "]";
}
}
}

33
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightedSection.cs

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Highlighting
{
/// <summary>
/// A text section with syntax highlighting information.
/// </summary>
public class HighlightedSection : ISegment
{
/// <summary>
/// Gets/sets the document offset of the section.
/// </summary>
public int Offset { get; set; }
/// <summary>
/// Gets/sets the length of the section.
/// </summary>
public int Length { get; set; }
/// <summary>
/// Gets the highlighting color associated with the highlighted section.
/// </summary>
public HighlightingColor Color { get; set; }
}
}

82
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingBrush.cs

@ -0,0 +1,82 @@ @@ -0,0 +1,82 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Windows;
using System.Windows.Media;
using ICSharpCode.AvalonEdit.Gui;
namespace ICSharpCode.AvalonEdit.Highlighting
{
/// <summary>
/// A brush used for syntax highlighting. Can retrieve a real brush on-demand.
/// </summary>
public abstract class HighlightingBrush
{
/// <summary>
/// Gets the real brush.
/// </summary>
public abstract Brush GetBrush(ITextRunConstructionContext context);
}
/// <summary>
/// Highlighting brush implementation that takes a frozen brush.
/// </summary>
sealed class SimpleHighlightingBrush : HighlightingBrush
{
readonly Brush brush;
public SimpleHighlightingBrush(Brush brush)
{
brush.Freeze();
this.brush = brush;
}
public SimpleHighlightingBrush(Color color) : this(new SolidColorBrush(color)) {}
public override Brush GetBrush(ITextRunConstructionContext context)
{
return brush;
}
public override string ToString()
{
SolidColorBrush scb = brush as SolidColorBrush;
if (scb != null) {
return scb.Color.ToString();
} else {
return brush.ToString();
}
}
}
/// <summary>
/// HighlightingBrush implementation that finds a brush using a resource.
/// </summary>
sealed class ResourceKeyHighlightingBrush : HighlightingBrush
{
readonly ResourceKey resourceKey;
readonly string name;
public ResourceKeyHighlightingBrush(ResourceKey resourceKey, string name)
{
this.resourceKey = resourceKey;
this.name = name;
}
public override Brush GetBrush(ITextRunConstructionContext context)
{
return (Brush)context.TextView.FindResource(resourceKey);
}
public override string ToString()
{
return name;
}
}
}

58
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingColor.cs

@ -0,0 +1,58 @@ @@ -0,0 +1,58 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Text;
using System.Windows;
namespace ICSharpCode.AvalonEdit.Highlighting
{
/// <summary>
/// A highlighting color is a set of font properties and foreground and background color.
/// </summary>
public class HighlightingColor
{
/// <summary>
/// Gets/sets the font weight. Null if the highlighting color does not change the font weight.
/// </summary>
public FontWeight? FontWeight { get; set; }
/// <summary>
/// Gets/sets the font style. Null if the highlighting color does not change the font style.
/// </summary>
public FontStyle? FontStyle { get; set; }
/// <summary>
/// Gets/sets the foreground color applied by the highlighting.
/// </summary>
public HighlightingBrush Foreground { get; set; }
/// <summary>
/// Gets CSS code for the color.
/// </summary>
public virtual string ToCss()
{
StringBuilder b = new StringBuilder();
if (Foreground != null) {
b.Append("color: ");
b.Append(Foreground.ToString());
b.Append("; ");
}
if (FontWeight != null) {
b.Append("font-weight: ");
b.Append(FontWeight.Value.ToString().ToLowerInvariant());
b.Append("; ");
}
if (FontStyle != null) {
b.Append("font-style: ");
b.Append(FontStyle.Value.ToString().ToLowerInvariant());
b.Append("; ");
}
return b.ToString();
}
}
}

124
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingColorizer.cs

@ -0,0 +1,124 @@ @@ -0,0 +1,124 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Media;
using System.Windows.Threading;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Gui;
namespace ICSharpCode.AvalonEdit.Highlighting
{
/// <summary>
/// A colorizes that interprets a highlighting rule set and colors the document accordingly.
/// </summary>
public class HighlightingColorizer : DocumentColorizingTransformer, IWeakEventListener
{
readonly TextView textView;
readonly HighlightingRuleSet ruleSet;
DocumentHighlighter highlighter;
/// <summary>
/// Creates a new HighlightingColorizer instance.
/// </summary>
/// <param name="textView">The text view for which the highlighting should be provided.</param>
/// <param name="ruleSet">The root highlighting rule set.</param>
public HighlightingColorizer(TextView textView, HighlightingRuleSet ruleSet)
{
if (textView == null)
throw new ArgumentNullException("textView");
if (ruleSet == null)
throw new ArgumentNullException("ruleSet");
this.textView = textView;
this.ruleSet = ruleSet;
TextViewWeakEventManager.DocumentChanged.AddListener(textView, this);
OnDocumentChanged();
}
bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
if (managerType == typeof(TextViewWeakEventManager.DocumentChanged)) {
OnDocumentChanged();
return true;
}
return false;
}
void OnDocumentChanged()
{
TextDocument document = textView.Document;
if (document != null)
highlighter = new TextViewDocumentHighlighter(this, document, ruleSet);
else
highlighter = null;
}
int currentLineEndOffset;
/// <inheritdoc/>
protected override void ColorizeLine(DocumentLine line)
{
if (CurrentContext.TextView != textView)
throw new InvalidOperationException("Wrong TextView");
if (highlighter != null) {
currentLineEndOffset = line.Offset + line.TotalLength;
HighlightedLine hl = highlighter.HighlightLine(line);
foreach (HighlightedSection section in hl.Sections) {
ChangeLinePart(section.Offset, section.Offset + section.Length,
visualLineElement => ApplyColorToElement(visualLineElement, section.Color));
}
}
}
/// <summary>
/// Applies a highlighting color to a visual line element.
/// </summary>
protected virtual void ApplyColorToElement(VisualLineElement element, HighlightingColor color)
{
if (color.Foreground != null) {
Brush b = color.Foreground.GetBrush(CurrentContext);
if (b != null)
element.TextRunProperties.SetForegroundBrush(b);
}
if (color.FontWeight != null) {
Typeface tf = element.TextRunProperties.Typeface;
element.TextRunProperties.SetTypeface(new Typeface(
tf.FontFamily,
color.FontStyle ?? tf.Style,
color.FontWeight ?? tf.Weight,
tf.Stretch
));
}
}
sealed class TextViewDocumentHighlighter : DocumentHighlighter
{
HighlightingColorizer colorizer;
public TextViewDocumentHighlighter(HighlightingColorizer colorizer, TextDocument document, HighlightingRuleSet baseRuleSet)
: base(document, baseRuleSet)
{
Debug.Assert(colorizer != null);
this.colorizer = colorizer;
}
protected override void OnHighlightStateChanged(DocumentLine line, int lineNumber)
{
base.OnHighlightStateChanged(line, lineNumber);
if (colorizer.currentLineEndOffset >= 0) {
colorizer.textView.Redraw(colorizer.currentLineEndOffset,
colorizer.CurrentContext.Document.TextLength - colorizer.currentLineEndOffset,
DispatcherPriority.Normal);
colorizer.currentLineEndOffset = -1;
}
}
}
}
}

47
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingDefinitionInvalidException.cs

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Runtime.Serialization;
namespace ICSharpCode.AvalonEdit.Highlighting
{
/// <summary>
/// Indicates that the highlighting definition that was tried to load was invalid.
/// </summary>
[Serializable()]
public class HighlightingDefinitionInvalidException : Exception
{
/// <summary>
/// Creates a new HighlightingDefinitionInvalidException instance.
/// </summary>
public HighlightingDefinitionInvalidException() : base()
{
}
/// <summary>
/// Creates a new HighlightingDefinitionInvalidException instance.
/// </summary>
public HighlightingDefinitionInvalidException(string message) : base(message)
{
}
/// <summary>
/// Creates a new HighlightingDefinitionInvalidException instance.
/// </summary>
public HighlightingDefinitionInvalidException(string message, Exception innerException) : base(message, innerException)
{
}
/// <summary>
/// Creates a new HighlightingDefinitionInvalidException instance.
/// </summary>
protected HighlightingDefinitionInvalidException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
}

186
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingManager.cs

@ -0,0 +1,186 @@ @@ -0,0 +1,186 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Xml;
namespace ICSharpCode.AvalonEdit.Highlighting
{
/// <summary>
/// Manages a list of syntax highlighting definitions.
/// </summary>
public class HighlightingManager : IHighlightingDefinitionReferenceResolver
{
sealed class RegisteredHighlighting
{
public IHighlightingDefinition Definition;
public Func<IHighlightingDefinition> LazyLoadingFunction;
}
Dictionary<string, RegisteredHighlighting> highlightingsByName = new Dictionary<string, RegisteredHighlighting>();
Dictionary<string, RegisteredHighlighting> highlightingsByExtension = new Dictionary<string, RegisteredHighlighting>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Gets a highlighting definition by name.
/// Returns null if the definition is not found.
/// </summary>
public IHighlightingDefinition GetDefinition(string name)
{
RegisteredHighlighting rh;
if (highlightingsByName.TryGetValue(name, out rh))
return GetDefinition(rh);
else
return null;
}
/// <summary>
/// Gets the names of the registered highlightings.
/// </summary>
public IEnumerable<string> HighlightingNames {
get { return highlightingsByName.Keys; }
}
/// <summary>
/// Gets a highlighting definition by extension.
/// Returns null if the definition is not found.
/// </summary>
public IHighlightingDefinition GetDefinitionByExtension(string extension)
{
RegisteredHighlighting rh;
if (highlightingsByExtension.TryGetValue(extension, out rh))
return GetDefinition(rh);
else
return null;
}
static IHighlightingDefinition GetDefinition(RegisteredHighlighting rh)
{
if (rh != null) {
var func = rh.LazyLoadingFunction;
if (func != null) {
// prevent endless recursion when there are cyclic references between syntax definitions
rh.LazyLoadingFunction = null;
rh.Definition = func();
}
return rh.Definition;
} else {
return null;
}
}
/// <summary>
/// Registers a highlighting definition.
/// </summary>
/// <param name="name">The name to register the definition with.</param>
/// <param name="extensions">The file extensions to register the definition for.</param>
/// <param name="highlighting">The highlighting definition.</param>
public void RegisterHighlighting(string name, string[] extensions, IHighlightingDefinition highlighting)
{
if (highlighting == null)
throw new ArgumentNullException("highlighting");
RegisterHighlighting(name, extensions, new RegisteredHighlighting { Definition = highlighting });
}
/// <summary>
/// Registers a highlighting definition.
/// </summary>
/// <param name="name">The name to register the definition with.</param>
/// <param name="extensions">The file extensions to register the definition for.</param>
/// <param name="lazyLoadedHighlighting">A function that loads the highlighting definition.</param>
public void RegisterHighlighting(string name, string[] extensions, Func<IHighlightingDefinition> lazyLoadedHighlighting)
{
if (lazyLoadedHighlighting == null)
throw new ArgumentNullException("lazyLoadedHighlighting");
RegisterHighlighting(name, extensions, new RegisteredHighlighting { LazyLoadingFunction = lazyLoadedHighlighting });
}
void RegisterHighlighting(string name, string[] extensions, RegisteredHighlighting rh)
{
if (name != null) {
highlightingsByName[name] = rh;
}
if (extensions != null) {
foreach (string ext in extensions) {
highlightingsByExtension[ext] = rh;
}
}
}
/// <summary>
/// Gets the default HighlightingManager instance.
/// The default HighlightingManager comes with built-in highlightings.
/// </summary>
public static HighlightingManager Instance {
get {
return DefaultHighlightingManager.Instance;
}
}
internal sealed class DefaultHighlightingManager : HighlightingManager
{
public new static readonly DefaultHighlightingManager Instance = new DefaultHighlightingManager();
public DefaultHighlightingManager()
{
Resources.RegisterBuiltInHighlightings(this);
}
// Registering a built-in highlighting
internal void RegisterHighlighting(string name, string[] extensions, string resourceName)
{
try {
#if DEBUG
// don't use lazy-loading in debug builds, show errors immediately
Xshd.XshdSyntaxDefinition xshd;
using (Stream s = Resources.OpenStream(resourceName)) {
using (XmlTextReader reader = new XmlTextReader(s)) {
xshd = Xshd.HighlightingLoader.LoadXshd(reader, false);
}
}
Debug.Assert(name == xshd.Name);
if (extensions != null)
Debug.Assert(System.Linq.Enumerable.SequenceEqual(extensions, xshd.Extensions));
else
Debug.Assert(xshd.Extensions.Count == 0);
// // round-trip xshd:
// using (XmlTextWriter writer = new XmlTextWriter("c:\\temp\\" + resourceName, System.Text.Encoding.UTF8)) {
// writer.Formatting = Formatting.Indented;
// new Xshd.SaveXshdVisitor(writer).WriteDefinition(xshd);
// }
RegisterHighlighting(name, extensions, Xshd.HighlightingLoader.Load(xshd, this));
#else
RegisterHighlighting(name, extensions, LoadHighlighting(resourceName));
#endif
} catch (HighlightingDefinitionInvalidException ex) {
throw new InvalidOperationException("The built-in highlighting '" + name + "' is invalid.", ex);
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode",
Justification = "LoadHighlighting is used only in release builds")]
Func<IHighlightingDefinition> LoadHighlighting(string resourceName)
{
Func<IHighlightingDefinition> func = delegate {
Xshd.XshdSyntaxDefinition xshd;
using (Stream s = Resources.OpenStream(resourceName)) {
using (XmlTextReader reader = new XmlTextReader(s)) {
// in release builds, skip validating the built-in highlightings
xshd = Xshd.HighlightingLoader.LoadXshd(reader, true);
}
}
return Xshd.HighlightingLoader.Load(xshd, this);
};
return func;
}
}
}
}

28
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingRule.cs

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Text.RegularExpressions;
namespace ICSharpCode.AvalonEdit.Highlighting
{
/// <summary>
/// A highlighting rule.
/// </summary>
public class HighlightingRule
{
/// <summary>
/// Gets/Sets the regular expression for the rule.
/// </summary>
public Regex Regex { get; set; }
/// <summary>
/// Gets/Sets the highlighting color.
/// </summary>
public HighlightingColor Color { get; set; }
}
}

39
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingRuleSet.cs

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Highlighting
{
/// <summary>
/// A highlighting rule set describes a set of spans that are valid at a given code location.
/// </summary>
public class HighlightingRuleSet
{
/// <summary>
/// Creates a new RuleSet instance.
/// </summary>
public HighlightingRuleSet()
{
this.Spans = new NullSafeCollection<HighlightingSpan>();
this.Rules = new NullSafeCollection<HighlightingRule>();
}
/// <summary>
/// Gets the list of spans.
/// </summary>
public IList<HighlightingSpan> Spans { get; private set; }
/// <summary>
/// Gets the list of rules.
/// </summary>
public IList<HighlightingRule> Rules { get; private set; }
}
}

49
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingSpan.cs

@ -0,0 +1,49 @@ @@ -0,0 +1,49 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Text.RegularExpressions;
namespace ICSharpCode.AvalonEdit.Highlighting
{
/// <summary>
/// A highlighting span is a region with start+end expression that has a different RuleSet inside
/// and colors the region.
/// </summary>
public class HighlightingSpan
{
/// <summary>
/// Gets/Sets the start expression.
/// </summary>
public Regex StartExpression { get; set; }
/// <summary>
/// Gets/Sets the end expression.
/// </summary>
public Regex EndExpression { get; set; }
/// <summary>
/// Gets/Sets the rule set that applies inside this span.
/// </summary>
public HighlightingRuleSet RuleSet { get; set; }
/// <summary>
/// Gets the color used for the text matching the start expression.
/// </summary>
public HighlightingColor StartColor { get; set; }
/// <summary>
/// Gets the color used for the text between start and end.
/// </summary>
public HighlightingColor SpanColor { get; set; }
/// <summary>
/// Gets the color used for the text matching the end expression.
/// </summary>
public HighlightingColor EndColor { get; set; }
}
}

34
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/IHighlightingDefinition.cs

@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
namespace ICSharpCode.AvalonEdit.Highlighting
{
/// <summary>
/// A highlighting definition.
/// </summary>
public interface IHighlightingDefinition
{
/// <summary>
/// Gets the main rule set.
/// </summary>
HighlightingRuleSet MainRuleSet { get; }
/// <summary>
/// Gets a rule set by name.
/// </summary>
/// <returns>The rule set, or null if it is not found.</returns>
HighlightingRuleSet GetNamedRuleSet(string name);
/// <summary>
/// Gets a named highlighting color.
/// </summary>
/// <returns>The highlighting color, or null if it is not found.</returns>
HighlightingColor GetNamedColor(string name);
}
}

22
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/IHighlightingDefinitionReferenceResolver.cs

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
namespace ICSharpCode.AvalonEdit.Highlighting
{
/// <summary>
/// Interface for resolvers that can solve cross-definition references.
/// </summary>
public interface IHighlightingDefinitionReferenceResolver
{
/// <summary>
/// Gets the highlighting definition by name, or null if it is not found.
/// </summary>
IHighlightingDefinition GetDefinition(string name);
}
}

13
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/Resources/ASPX.xshd

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
<?xml version="1.0"?>
<SyntaxDefinition name = "ASP/XHTML" extensions = ".asp;.aspx;.asax;.asmx" extends = "HTML">
<RuleSets>
<RuleSet ignorecase = "true">
<Span name = "ASPCode" rule = "ASP" bold = "false" italic = "false" color = "Black" bgcolor = "#F7F2E3" stopateol = "false">
<Begin color="Black" bgcolor="Yellow">&lt;%</Begin>
<End color="Black" bgcolor="Yellow">%&gt;</End>
</Span>
</RuleSet>
<RuleSet name="ASP" reference="C#" />
</RuleSets>
</SyntaxDefinition>

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

Loading…
Cancel
Save