Browse Source
git-svn-id: svn://svn.sharpdevelop.net/sharpdevelop/trunk@3635 1ccf3a8d-04fe-1044-b7c0-cef0b8235c61shortcuts
159 changed files with 24296 additions and 236 deletions
@ -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> |
@ -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> |
@ -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.*")] |
@ -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); |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
@ -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"); |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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
|
||||
} |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
<PartCoverSettings> |
||||
<Rule>+[ICSharpCode.AvalonEdit]*</Rule> |
||||
</PartCoverSettings> |
@ -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> |
@ -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.*")] |
@ -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()); |
||||
} |
||||
} |
||||
} |
@ -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)); |
||||
} |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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> |
@ -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. |
@ -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; |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
@ -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
|
||||
} |
||||
} |
@ -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
|
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
@ -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
|
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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 |
||||
} |
||||
} |
@ -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 + "]"; |
||||
} |
||||
} |
||||
} |
@ -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
|
||||
} |
||||
} |
@ -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
|
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
@ -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 + "]"; |
||||
} |
||||
} |
||||
} |
@ -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 <= offset <= 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
|
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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)); |
||||
} |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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)); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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
|
||||
} |
||||
} |
@ -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 + "]"; |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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
|
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
@ -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; } } |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -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; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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
|
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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; } |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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) |
||||
{ |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
@ -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 + "]"; |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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
|
||||
} |
||||
} |
@ -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> |
@ -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); |
||||
} |
||||
} |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
@ -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) |
||||
{ |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
@ -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; } } |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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
|
||||
} |
||||
} |
@ -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 <span class="colorName"> 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() + "]"; |
||||
} |
||||
} |
||||
} |
@ -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; } |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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) |
||||
{ |
||||
} |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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; } |
||||
} |
||||
} |
@ -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; } |
||||
} |
||||
} |
@ -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; } |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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"><%</Begin> |
||||
<End color="Black" bgcolor="Yellow">%></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…
Reference in new issue