1915 changed files with 0 additions and 496163 deletions
@ -1,7 +0,0 @@
@@ -1,7 +0,0 @@
|
||||
# ignore all obj and bin folders (even in subdirectories) |
||||
obj/ |
||||
bin/ |
||||
/Documentation/Help |
||||
/packages/AvalonEdit |
||||
/packages/AvalonEdit.Sample |
||||
/packages/NUnit.2.6.3 |
@ -1,33 +0,0 @@
@@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<topic id="5d1af8a2-fc1b-4a1b-b6c1-f33fb14bec1f" revisionNumber="1"> |
||||
<developerConceptualDocument xmlns="http://ddue.schemas.microsoft.com/authoring/2003/5" xmlns:xlink="http://www.w3.org/1999/xlink"> |
||||
<!-- |
||||
<summary> |
||||
<para>Optional summary abstract</para> |
||||
</summary> |
||||
--> |
||||
<introduction> |
||||
<!-- Uncomment this to generate an outline of the section and sub-section |
||||
titles. Specify a numeric value as the inner text to limit it to |
||||
a specific number of sub-topics when creating the outline. Specify |
||||
zero (0) to limit it to top-level sections only. --> |
||||
<!-- <autoOutline /> --> |
||||
<mediaLink><image xlink:href="NamespaceDependencies" placement="center"/></mediaLink> |
||||
<para>As you can see in this dependency graph, AvalonEdit consists of a few |
||||
sub-namespaces that have cleanly separated jobs. |
||||
Most of the namespaces have a kind of 'main' class.</para> |
||||
<para>Here is the visual tree of the TextEditor control:</para> |
||||
<mediaLink><image xlink:href="VisualTree" placement="center"/></mediaLink> |
||||
<para>It's important to understand that AvalonEdit is a composite control |
||||
with the three layers: |
||||
<codeEntityReference>T:ICSharpCode.AvalonEdit.TextEditor</codeEntityReference> (main control), |
||||
<codeEntityReference>T:ICSharpCode.AvalonEdit.Editing.TextArea</codeEntityReference> (editing), |
||||
<codeEntityReference>T:ICSharpCode.AvalonEdit.Rendering.TextView</codeEntityReference> (rendering). |
||||
</para><para> |
||||
While the main control provides some convenience methods for common tasks, |
||||
for most advanced features you have to work directly with the inner controls. |
||||
You can access them using <codeInline>textEditor.TextArea</codeInline> or |
||||
<codeInline>textEditor.TextArea.TextView</codeInline>.</para> |
||||
</introduction> |
||||
</developerConceptualDocument> |
||||
</topic> |
@ -1,133 +0,0 @@
@@ -1,133 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<topic id="47c58b63-f30c-4290-a2f2-881d21227446" revisionNumber="1"> |
||||
<developerConceptualDocument xmlns="http://ddue.schemas.microsoft.com/authoring/2003/5" xmlns:xlink="http://www.w3.org/1999/xlink"> |
||||
<introduction> |
||||
<para> |
||||
AvalonEdit comes with a code completion drop down window. |
||||
You only have to handle the text entering events to determine |
||||
when you want to show the window; all the UI is already done for you. |
||||
</para> |
||||
</introduction> |
||||
<section> |
||||
<title>Usage of the Code Completion Window</title> |
||||
<content> |
||||
<code language="cs"> |
||||
// in the constructor: |
||||
textEditor.TextArea.TextEntering += textEditor_TextArea_TextEntering; |
||||
textEditor.TextArea.TextEntered += textEditor_TextArea_TextEntered; |
||||
} |
||||
|
||||
CompletionWindow completionWindow; |
||||
|
||||
void textEditor_TextArea_TextEntered(object sender, TextCompositionEventArgs e) |
||||
{ |
||||
if (e.Text == ".") { |
||||
// Open code completion after the user has pressed dot: |
||||
completionWindow = new CompletionWindow(textEditor.TextArea); |
||||
IList<ICompletionData> data = completionWindow.CompletionList.CompletionData; |
||||
data.Add(new MyCompletionData("Item1")); |
||||
data.Add(new MyCompletionData("Item2")); |
||||
data.Add(new MyCompletionData("Item3")); |
||||
completionWindow.Show(); |
||||
completionWindow.Closed += delegate { |
||||
completionWindow = null; |
||||
}; |
||||
} |
||||
} |
||||
|
||||
void textEditor_TextArea_TextEntering(object sender, TextCompositionEventArgs e) |
||||
{ |
||||
if (e.Text.Length > 0 && completionWindow != null) { |
||||
if (!char.IsLetterOrDigit(e.Text[0])) { |
||||
// Whenever a non-letter is typed while the completion window is open, |
||||
// insert the currently selected element. |
||||
completionWindow.CompletionList.RequestInsertion(e); |
||||
} |
||||
} |
||||
// Do not set e.Handled=true. |
||||
// We still want to insert the character that was typed. |
||||
} |
||||
</code> |
||||
<para> |
||||
This code will open the code completion window whenever '.' is pressed. |
||||
By default, the |
||||
<codeEntityReference>T:ICSharpCode.AvalonEdit.CodeCompletion.CompletionWindow</codeEntityReference> |
||||
only handles key presses like Tab and Enter to insert the currently |
||||
selected item. To also make it complete when keys like '.' or ';' are pressed, |
||||
we attach another handler to the <codeInline>TextEntering</codeInline> event |
||||
and tell the completion window to insert the selected item. |
||||
</para> |
||||
<para> |
||||
The <codeInline>CompletionWindow</codeInline> will actually never have |
||||
focus - instead, it hijacks |
||||
the WPF keyboard input events on the text area and passes them through its |
||||
<codeInline>ListBox</codeInline>. |
||||
This allows selecting entries in the completion list using the |
||||
keyboard and normal typing in the editor at the same time. |
||||
</para> |
||||
<para> |
||||
Here is the implementation of the MyCompletionData class used in the code above: |
||||
<code language="cs"> |
||||
/// Implements AvalonEdit ICompletionData interface to provide the entries in the |
||||
/// completion drop down. |
||||
public class MyCompletionData : ICompletionData |
||||
{ |
||||
public MyCompletionData(string text) |
||||
{ |
||||
this.Text = text; |
||||
} |
||||
|
||||
public System.Windows.Media.ImageSource Image { |
||||
get { return null; } |
||||
} |
||||
|
||||
public string Text { get; private set; } |
||||
|
||||
// Use this property if you want to show a fancy UIElement in the list. |
||||
public object Content { |
||||
get { return this.Text; } |
||||
} |
||||
|
||||
public object Description { |
||||
get { return "Description for " + this.Text; } |
||||
} |
||||
|
||||
public void Complete(TextArea textArea, ISegment completionSegment, |
||||
EventArgs insertionRequestEventArgs) |
||||
{ |
||||
textArea.Document.Replace(completionSegment, this.Text); |
||||
} |
||||
} |
||||
</code> |
||||
Both the content and the description shown may be any content acceptable in WPF, |
||||
including custom UIElements. |
||||
You may also implement custom logic in the <codeInline>Complete</codeInline> |
||||
method if you want to do more than simply inserting the text. |
||||
The <codeInline>insertionRequestEventArgs</codeInline> can help decide which |
||||
kind of insertion the user wants - depending on how the insertion was triggered, |
||||
it is an instance of <codeInline>TextCompositionEventArgs</codeInline>, |
||||
<codeInline>KeyEventArgs</codeInline> or <codeInline>MouseEventArgs</codeInline>. |
||||
</para> |
||||
</content> |
||||
</section> |
||||
<section> |
||||
<title>Code Completion for C#</title> |
||||
<content> |
||||
<para> |
||||
Full C# code completion is not in the scope of AvalonEdit. |
||||
You will need a C# parser, a C# type system, and the ability |
||||
to resolve C# expressions in your type system. |
||||
</para> |
||||
<para> |
||||
If you want to learn how this is handled in SharpDevelop, please |
||||
see: |
||||
<externalLink> |
||||
<linkText>Code Completion in SharpDevelop</linkText> |
||||
<linkUri>https://github.com/icsharpcode/SharpDevelop/wiki/Code-Completion</linkUri> |
||||
<linkTarget>_blank</linkTarget> |
||||
</externalLink> |
||||
</para> |
||||
</content> |
||||
</section> |
||||
</developerConceptualDocument> |
||||
</topic> |
@ -1,175 +0,0 @@
@@ -1,175 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<topic id="5b1854b4-884c-4713-b921-b28e96a1b43e" revisionNumber="1"> |
||||
<developerConceptualDocument xmlns="http://ddue.schemas.microsoft.com/authoring/2003/5" xmlns:xlink="http://www.w3.org/1999/xlink"> |
||||
<!-- |
||||
<summary> |
||||
<para>Optional summary abstract</para> |
||||
</summary> |
||||
--> |
||||
<introduction> |
||||
<!-- Uncomment this to generate an outline of the section and sub-section |
||||
titles. Specify a numeric value as the inner text to limit it to |
||||
a specific number of sub-topics when creating the outline. Specify |
||||
zero (0) to limit it to top-level sections only. --> |
||||
<!-- <autoOutline /> --> |
||||
<para>The text editor makes use of several different coordinate systems. |
||||
Here's an explanation of them.</para> |
||||
</introduction> |
||||
<!-- Add one or more top-level section elements. These are collapsible. |
||||
If using <autoOutline /> tag, add an address attribute to identify |
||||
it so that it can be jumped to with a hyperlink. --> |
||||
<section> |
||||
<title>Offset</title> |
||||
<content> |
||||
<para>In AvalonEdit, an index into the document is called an <newTerm>offset</newTerm>.</para> |
||||
<para> |
||||
Offsets usually represent the position between two characters. |
||||
The first offset at the start of the document is 0; |
||||
the offset after the first char in the document is 1. |
||||
The last valid offset is <codeInline>document.TextLength</codeInline>, |
||||
representing the end of the document. |
||||
This is exactly the same as the <codeInline>index</codeInline> parameter |
||||
used by methods in the .NET String or StringBuilder classes. |
||||
</para> |
||||
</content> |
||||
</section> |
||||
<section> |
||||
<title>TextLocation</title> |
||||
<content> |
||||
<para>The |
||||
<codeEntityReference qualifyHint="true">T:ICSharpCode.AvalonEdit.Document.TextLocation</codeEntityReference> |
||||
struct represents a Line/Column pair. Line and column are counted from 1.</para> |
||||
<para>The document provides the methods |
||||
<codeEntityReference qualifyHint="true">M:ICSharpCode.AvalonEdit.Document.TextDocument.GetLocation(System.Int32)</codeEntityReference> |
||||
and |
||||
<codeEntityReference qualifyHint="true">M:ICSharpCode.AvalonEdit.Document.TextDocument.GetOffset(ICSharpCode.AvalonEdit.Document.TextLocation)</codeEntityReference> |
||||
to convert between offsets and <codeInline>TextLocation</codeInline>s.</para> |
||||
</content> |
||||
</section> |
||||
<section> |
||||
<title>TextAnchor</title> |
||||
<content> |
||||
<para>If you are working with the text editor, you will likely run into the problem |
||||
that you need to store an offset, but want it to adjust automatically whenever |
||||
text is inserted prior to that offset. </para> |
||||
<para>Sure, you could listen to the TextDocument.Changed event and call |
||||
GetNewOffset on the DocumentChangeEventArgs to translate the offset, |
||||
but that gets tedious; especially when your object is short-lived and you |
||||
have to deal with deregistering the event handler at the correct point of time.</para> |
||||
<para>A text anchor object stores an Offset, but automatically |
||||
updates the offset when text is inserted/removed before the offset. |
||||
</para> |
||||
<para> |
||||
A much simpler solution is to use the |
||||
<codeEntityReference qualifyHint="true">T:ICSharpCode.AvalonEdit.Document.TextAnchor</codeEntityReference> |
||||
class. |
||||
Please take a look at the documentation for that class for more details. |
||||
</para> |
||||
</content> |
||||
</section> |
||||
<section> |
||||
<title>RelativeTextOffset</title> |
||||
<content> |
||||
<para>An offset in the document, but relative to the start offset of a <codeEntityReference>T:ICSharpCode.AvalonEdit.Rendering.VisualLine</codeEntityReference>.</para> |
||||
<para>Relative text offsets are used to store document offsets in visual lines.</para> |
||||
<para>You can convert between relative text offsets and document offsets |
||||
by adding/subtracting |
||||
<codeEntityReference qualifyHint="true">P:ICSharpCode.AvalonEdit.Rendering.VisualLine.FirstDocumentLine</codeEntityReference>.<codeEntityReference>P:ICSharpCode.AvalonEdit.Document.DocumentLine.Offset</codeEntityReference>. |
||||
</para> |
||||
</content> |
||||
</section> |
||||
<section> |
||||
<title>VisualColumn</title> |
||||
<content> |
||||
<para>An integer value that specifies a position inside a VisualLine.</para> |
||||
<para> |
||||
Not only text has a length in the visual line, but also other VisualLineElements. |
||||
VisualColumn is counting from 0 for each visual line. |
||||
</para> |
||||
<para>For example, tab markers take 2 visual columns (the marker and the tab space), |
||||
newline markers take 1 visual column; folding markers take just 1 visual column |
||||
even though they are longer in the document text.</para> |
||||
<para>Use the |
||||
<codeEntityReference qualifyHint="true">M:ICSharpCode.AvalonEdit.Rendering.VisualLine.GetVisualColumn(System.Int32)</codeEntityReference> |
||||
and |
||||
<codeEntityReference qualifyHint="true">M:ICSharpCode.AvalonEdit.Rendering.VisualLine.GetRelativeOffset(System.Int32)</codeEntityReference> |
||||
methods to convert between |
||||
visual columns and relative text offsets.</para> |
||||
<alert class="note"> |
||||
<para>Do not confuse VisualColumn with text columns. |
||||
VisualColumn starts at 0, text column at 1. Text may have different length |
||||
in the two coordinate systems (e.g. tab markers, foldings).</para> |
||||
</alert> |
||||
</content> |
||||
</section> |
||||
<section> |
||||
<title>TextViewPosition</title> |
||||
<content> |
||||
<para>A Line,Column,VisualColumn triple.</para> |
||||
<para>The <codeEntityReference qualifyHint="true">T:ICSharpCode.AvalonEdit.TextViewPosition</codeEntityReference> |
||||
struct can be implicitly converted |
||||
to <codeEntityReference qualifyHint="false">T:ICSharpCode.AvalonEdit.Document.TextLocation</codeEntityReference>, |
||||
but has the additional VisualColumn information |
||||
that is necessary to accurately hold the caret position when |
||||
VisualLineElements with DocumentLength 0 are in use.</para> |
||||
</content> |
||||
</section> |
||||
<section> |
||||
<title>VisualTop</title> |
||||
<content> |
||||
<para>A double value that specifies the distance from the top of |
||||
the document to the top of a line measured in device-independent pixels.</para> |
||||
<para>VisualTop is equivalent to the Y component of a VisualPosition.</para> |
||||
</content> |
||||
</section> |
||||
<section> |
||||
<title>VisualPosition</title> |
||||
<content> |
||||
<para>A Point value (double X,Y) that specifies the position of an |
||||
element from the top left document corner measured in device-independent pixels.</para> |
||||
<para>To convert a VisualPosition to or from a (mouse) position inside |
||||
the TextView, simply subtract or add |
||||
<codeEntityReference qualifyHint="true">P:ICSharpCode.AvalonEdit.Rendering.TextView.ScrollOffset</codeEntityReference> |
||||
to it. |
||||
</para> |
||||
</content> |
||||
</section> |
||||
<relatedTopics> |
||||
<!-- One or more of the following: |
||||
- A local link |
||||
- An external link |
||||
- A code entity reference |
||||
|
||||
<link xlink:href="Other Topic's ID"/> |
||||
<link xlink:href="Other Topic's ID">Link inner text</link> |
||||
|
||||
<externalLink> |
||||
<linkText>Link text</linkText> |
||||
<linkAlternateText>Optional alternate link text</linkAlternateText> |
||||
<linkUri>URI</linkUri> |
||||
</externalLink> |
||||
|
||||
<codeEntityReference>API member ID</codeEntityReference> |
||||
|
||||
Examples: |
||||
|
||||
<link xlink:href="00e97994-e9e6-46e0-b420-5be86b2f8270" /> |
||||
<link xlink:href="00e97994-e9e6-46e0-b420-5be86b2f8278">Some other topic</link> |
||||
|
||||
<externalLink> |
||||
<linkText>SHFB on CodePlex</linkText> |
||||
<linkAlternateText>Go to CodePlex</linkAlternateText> |
||||
<linkUri>http://www.codeplex.com/SHFB</linkUri> |
||||
</externalLink> |
||||
|
||||
<codeEntityReference>T:TestDoc.TestClass</codeEntityReference> |
||||
<codeEntityReference>P:TestDoc.TestClass.SomeProperty</codeEntityReference> |
||||
<codeEntityReference>M:TestDoc.TestClass.#ctor</codeEntityReference> |
||||
<codeEntityReference>M:TestDoc.TestClass.#ctor(System.String,System.Int32)</codeEntityReference> |
||||
<codeEntityReference>M:TestDoc.TestClass.ToString</codeEntityReference> |
||||
<codeEntityReference>M:TestDoc.TestClass.FirstMethod</codeEntityReference> |
||||
<codeEntityReference>M:TestDoc.TestClass.SecondMethod(System.Int32,System.String)</codeEntityReference> |
||||
--> |
||||
</relatedTopics> |
||||
</developerConceptualDocument> |
||||
</topic> |
@ -1,58 +0,0 @@
@@ -1,58 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<topic id="440df648-413e-4f42-a28b-6b2b0e9b1084" revisionNumber="1"> |
||||
<developerConceptualDocument xmlns="http://ddue.schemas.microsoft.com/authoring/2003/5" xmlns:xlink="http://www.w3.org/1999/xlink"> |
||||
<!--<introduction> |
||||
<para> |
||||
Introduction for 'Folding'. |
||||
</para> |
||||
</introduction>--> |
||||
<section> |
||||
<title>How to use Folding in your application</title> |
||||
<content> |
||||
<para> |
||||
Folding (code collapsing) is implemented as an extension to the editor. |
||||
It could have been implemented in a separate assembly without having to |
||||
modify the AvalonEdit code. |
||||
A <codeEntityReference>T:ICSharpCode.AvalonEdit.Rendering.VisualLineElementGenerator</codeEntityReference> |
||||
takes care of the collapsed sections in the text document; and a custom |
||||
margin draws the plus and minus buttons. |
||||
</para> |
||||
<para>You could use the relevant classes separately; |
||||
but, to make it a bit easier to use, the static |
||||
<codeEntityReference qualifyHint="true">M:ICSharpCode.AvalonEdit.Folding.FoldingManager.Install(ICSharpCode.AvalonEdit.Editing.TextArea)</codeEntityReference> |
||||
method will create and register the necessary parts automatically.</para> |
||||
<para>All that's left for you is to regularly call |
||||
<codeEntityReference qualifyHint="true">M:ICSharpCode.AvalonEdit.Folding.FoldingManager.UpdateFoldings(System.Collections.Generic.IEnumerable{ICSharpCode.AvalonEdit.Folding.NewFolding},System.Int32)</codeEntityReference> |
||||
with the list of foldings you want to provide. |
||||
You could calculate that list yourself, or you could use a built-in |
||||
folding strategy to do it for you.</para> |
||||
<para>Here is the full code required to enable folding: |
||||
<code language="cs">foldingManager = FoldingManager.Install(textEditor.TextArea); |
||||
foldingStrategy = new XmlFoldingStrategy(); |
||||
foldingStrategy.UpdateFoldings(foldingManager, textEditor.Document);</code> |
||||
If you want the folding markers to update when the text is changed, |
||||
you have to repeat the <codeInline>foldingStrategy.UpdateFoldings</codeInline> call regularly. |
||||
</para> |
||||
</content> |
||||
</section> |
||||
<section> |
||||
<title>How the FoldingManager works</title> |
||||
<content> |
||||
<para> |
||||
The FoldingManager maintains a list of foldings. The FoldMargin displays those foldings and provides |
||||
the UI for collapsing/expanding.</para><para> |
||||
Folded foldings cause the FoldingElementGenerator to produce a line element that spans the whole folded |
||||
text section, causing the text generation for the visual line that contains the folding start to |
||||
continue after the folding end in another line.</para><para> |
||||
To ensure scrolling works correctly in the presence of foldings, lines inside folded regions must not |
||||
be used as start lines for the visual line generation. This is done by setting the line height of all |
||||
such lines to 0. To efficiently set the height on a large number of lines and support reverting to the |
||||
old height when the folding is uncollapsed, a CollapsedLineSection is used. |
||||
</para> |
||||
</content> |
||||
</section> |
||||
<relatedTopics> |
||||
<codeEntityReference>T:ICSharpCode.AvalonEdit.Folding.FoldingManager</codeEntityReference> |
||||
</relatedTopics> |
||||
</developerConceptualDocument> |
||||
</topic> |
@ -1,11 +0,0 @@
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<Topics defaultTopic="c52241ea-3eba-4ddf-b463-6349cbff38fd"> |
||||
<Topic id="c52241ea-3eba-4ddf-b463-6349cbff38fd" visible="True" /> |
||||
<Topic id="70c4df51-5ecb-4e24-a574-8c5a84306bd1" visible="True" /> |
||||
<Topic id="5d1af8a2-fc1b-4a1b-b6c1-f33fb14bec1f" visible="True" /> |
||||
<Topic id="5b1854b4-884c-4713-b921-b28e96a1b43e" visible="True" /> |
||||
<Topic id="c06e9832-9ef0-4d65-ac2e-11f7ce9c7774" visible="True" /> |
||||
<Topic id="4d4ceb51-154d-43f0-b876-ad9640c5d2d8" visible="True" /> |
||||
<Topic id="440df648-413e-4f42-a28b-6b2b0e9b1084" visible="True" /> |
||||
<Topic id="47c58b63-f30c-4290-a2f2-881d21227446" visible="True" /> |
||||
</Topics> |
@ -1,209 +0,0 @@
@@ -1,209 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0"> |
||||
<PropertyGroup> |
||||
<!-- The configuration and platform will be used to determine which |
||||
assemblies to include from solution and project documentation |
||||
sources --> |
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> |
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> |
||||
<SchemaVersion>2.0</SchemaVersion> |
||||
<ProjectGuid>{850b6602-0a7f-413a-864a-e816b98d7407}</ProjectGuid> |
||||
<SHFBSchemaVersion>1.9.5.0</SHFBSchemaVersion> |
||||
<!-- AssemblyName, Name, and RootNamespace are not used by SHFB but Visual |
||||
Studio adds them anyway --> |
||||
<AssemblyName>Documentation</AssemblyName> |
||||
<RootNamespace>Documentation</RootNamespace> |
||||
<Name>Documentation</Name> |
||||
<!-- SHFB properties --> |
||||
<OutputPath>.\Help\</OutputPath> |
||||
<HtmlHelpName>AvalonEdit Documentation</HtmlHelpName> |
||||
<ProjectSummary> |
||||
</ProjectSummary> |
||||
<MissingTags>Summary, AutoDocumentCtors, Namespace</MissingTags> |
||||
<VisibleItems>InheritedMembers, InheritedFrameworkMembers, Protected, ProtectedInternalAsProtected</VisibleItems> |
||||
<HtmlHelp1xCompilerPath> |
||||
</HtmlHelp1xCompilerPath> |
||||
<HtmlHelp2xCompilerPath> |
||||
</HtmlHelp2xCompilerPath> |
||||
<SandcastlePath> |
||||
</SandcastlePath> |
||||
<WorkingPath> |
||||
</WorkingPath> |
||||
<BuildLogFile> |
||||
</BuildLogFile> |
||||
<FrameworkVersion>.NET Framework 4.0</FrameworkVersion> |
||||
<HelpTitle>AvalonEdit</HelpTitle> |
||||
<CopyrightText>Copyright 2008-2014, Daniel Grunwald</CopyrightText> |
||||
<PresentationStyle>Prototype</PresentationStyle> |
||||
<HelpFileVersion>4.4.0.0</HelpFileVersion> |
||||
<ComponentConfigurations> |
||||
<ComponentConfig id="Code Block Component" enabled="True"> |
||||
<component id="Code Block Component" type="SandcastleBuilder.Components.CodeBlockComponent" assembly="{@SHFBFolder}SandcastleBuilder.Components.dll"> |
||||
<!-- Base path for relative filenames in source attributes (optional) --> |
||||
<basePath value="{@HtmlEncProjectFolder}" /> |
||||
<!-- Base output paths for the files (required). These should match the parent folder of the output path |
||||
of the HTML files (see each of the SaveComponent instances in the configuration files). --> |
||||
<outputPaths> |
||||
{@HelpFormatOutputPaths} |
||||
</outputPaths> |
||||
<!-- Allow missing source files (Optional). If omitted, it will generate errors if referenced source files |
||||
are missing. --> |
||||
<allowMissingSource value="false" /> |
||||
<!-- Remove region markers from imported code blocks. If omitted, region markers in imported code blocks |
||||
are left alone. --> |
||||
<removeRegionMarkers value="false" /> |
||||
<!-- Code colorizer options (required). |
||||
Attributes: |
||||
Language syntax configuration file (required) |
||||
XSLT stylesheet file (required) |
||||
CSS stylesheet file (required) |
||||
Script file (required) |
||||
Disabled (optional, leading whitespace normalization only) |
||||
Default language (optional) |
||||
Enable line numbering (optional) |
||||
Enable outlining (optional) |
||||
Keep XML comment "see" tags within the code (optional) |
||||
Tab size override (optional, 0 = Use syntax file setting) |
||||
Use language name as default title (optional) --> |
||||
<colorizer syntaxFile="{@SHFBFolder}Colorizer\highlight.xml" styleFile="{@SHFBFolder}Colorizer\highlight.xsl" stylesheet="{@SHFBFolder}Colorizer\highlight.css" scriptFile="{@SHFBFolder}Colorizer\highlight.js" disabled="{@DisableCodeBlockComponent}" language="cs" numberLines="false" outlining="false" keepSeeTags="false" tabSize="0" defaultTitle="true" /> |
||||
</component> |
||||
</ComponentConfig> |
||||
<ComponentConfig id="IntelliSense Component" enabled="True"> |
||||
<component id="IntelliSense Component" type="SandcastleBuilder.Components.IntelliSenseComponent" assembly="{@SHFBFolder}SandcastleBuilder.Components.dll"> |
||||
<!-- Output options (optional) |
||||
Attributes: |
||||
Include Namespaces (false by default) |
||||
Namespaces filename ("Namespaces" if not specified or empty) |
||||
Directory (current folder if not specified or empty) --> |
||||
<output includeNamespaces="false" namespacesFile="Namespaces" folder="{@OutputFolder}" /> |
||||
</component> |
||||
</ComponentConfig> |
||||
<ComponentConfig id="Cached Framework Comments Index Data" enabled="True"> |
||||
<component id="Cached Framework Comments Index Data" type="SandcastleBuilder.Components.CachedCopyFromIndexComponent" assembly="{@SHFBFolder}SandcastleBuilder.Components.dll"> |
||||
<index name="comments" value="/doc/members/member" key="@name" cache="100"> |
||||
{@CachedFrameworkCommentList} |
||||
{@CommentFileList} |
||||
</index> |
||||
<copy name="comments" source="*" target="/document/comments" /> |
||||
</component> |
||||
</ComponentConfig> |
||||
<ComponentConfig id="Cached Reflection Index Data" enabled="True"> |
||||
<component id="Cached Reflection Index Data" type="SandcastleBuilder.Components.CachedCopyFromIndexComponent" assembly="{@SHFBFolder}SandcastleBuilder.Components.dll"> |
||||
<index name="reflection" value="/reflection/apis/api" key="@id" cache="10"> |
||||
<cache base="{@SandcastlePath}Data\Reflection" recurse="true" files="*.xml" cacheFile="{@LocalDataFolder}Cache\Reflection.cache" /> |
||||
<data files="reflection.xml" /> |
||||
</index> |
||||
<copy name="reflection" source="*" target="/document/reference" /> |
||||
</component> |
||||
</ComponentConfig> |
||||
<ComponentConfig id="API Token Resolution" enabled="True"> |
||||
<component id="API Token Resolution" type="Microsoft.Ddue.Tools.SharedContentComponent" assembly="{@SandcastlePath}ProductionTools\BuildComponents.dll"> |
||||
|
||||
{@TokenFiles} |
||||
<replace elements="/*//token" item="string(.)" /></component> |
||||
</ComponentConfig> |
||||
<ComponentConfig id="Cached MSDN URL References" enabled="True"> |
||||
<component id="Cached MSDN URL References" type="SandcastleBuilder.Components.CachedResolveReferenceLinksComponent" assembly="{@SHFBFolder}SandcastleBuilder.Components.dll" locale="{@Locale}" linkTarget="{@SdkLinkTarget}"> |
||||
<helpOutput format="HtmlHelp1"> |
||||
<cache filename="{@LocalDataFolder}Cache\MsdnUrl.cache" /> |
||||
<targets base="{@SandcastlePath}Data\Reflection" recurse="true" files="*.xml" type="{@HtmlSdkLinkType}" /> |
||||
<targets files="reflection.xml" type="Local" /> |
||||
</helpOutput> |
||||
<helpOutput format="MSHelp2"> |
||||
<cache filename="{@LocalDataFolder}Cache\MsdnUrl.cache" /> |
||||
<targets base="{@SandcastlePath}Data\Reflection" recurse="true" files="*.xml" type="{@MSHelp2SdkLinkType}" /> |
||||
<targets files="reflection.xml" type="Index" /> |
||||
</helpOutput> |
||||
<helpOutput format="MSHelpViewer"> |
||||
<cache filename="{@LocalDataFolder}Cache\MsdnUrl.cache" /> |
||||
<targets base="{@SandcastlePath}Data\Reflection" recurse="true" files="*.xml" type="{@MSHelpViewerSdkLinkType}" /> |
||||
<targets files="reflection.xml" type="Id" /> |
||||
</helpOutput> |
||||
<helpOutput format="Website"> |
||||
<cache filename="{@LocalDataFolder}Cache\MsdnUrl.cache" /> |
||||
<targets base="{@SandcastlePath}Data\Reflection" recurse="true" files="*.xml" type="{@WebsiteSdkLinkType}" /> |
||||
<targets files="reflection.xml" type="Local" /> |
||||
</helpOutput> |
||||
</component> |
||||
</ComponentConfig> |
||||
</ComponentConfigurations> |
||||
<DocumentationSources> |
||||
<DocumentationSource sourceFile="..\ICSharpCode.AvalonEdit\ICSharpCode.AvalonEdit.csproj" /> |
||||
</DocumentationSources> |
||||
<NamespaceSummaries> |
||||
<NamespaceSummaryItem name="(global)" isDocumented="False" /> |
||||
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit" isDocumented="True">This is the main AvalonEdit namespace.</NamespaceSummaryItem> |
||||
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit.CodeCompletion" isDocumented="True">This namespace contains classes to show the code completion window.</NamespaceSummaryItem> |
||||
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit.Document" isDocumented="True">This namespace contains the document model. |
||||
The most important class here is TextDocument, which represents document that can be displayed and edited in the text editor.</NamespaceSummaryItem> |
||||
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit.Editing" isDocumented="True">This namespace is the home of the TextArea class. It manages user input and handles the caret and the selection.</NamespaceSummaryItem> |
||||
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit.Folding" isDocumented="True">This namespace contains the folding (code collapsing) implementation.</NamespaceSummaryItem> |
||||
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit.Highlighting" isDocumented="True">This namespace contains the engine for highlighting text documents (DocumentHighlighter). |
||||
Additionally, the class HighlightingColorizer provides integration of the highlighting engine into the text editor GUI.</NamespaceSummaryItem> |
||||
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit.Highlighting.Xshd" isDocumented="True">This namespace contains a document model for syntax highlighting definitions (.xshd files).</NamespaceSummaryItem> |
||||
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit.Indentation" isDocumented="True">This namespace contains the logic for automatic indentation.</NamespaceSummaryItem> |
||||
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit.Rendering" isDocumented="True">This namespace contains the text rendering infrastructure.</NamespaceSummaryItem> |
||||
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit.Utils" isDocumented="True">This namespace contains various utility classes.</NamespaceSummaryItem> |
||||
<NamespaceSummaryItem name="XamlGeneratedNamespace" isDocumented="False" /> |
||||
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit.Xml" isDocumented="True">This namespace contains an error-tolerant XML parser with support for incremental parsing, only reparsing the changed regions of a TextDocument.</NamespaceSummaryItem> |
||||
<NamespaceSummaryItem name="ICSharpCode.AvalonEdit.Snippets" isDocumented="True">Snippets perform automatic text insertion. Snippets can be interactive after they were expanded, for example to allow the user to easily replace fields in the expanded snippet.</NamespaceSummaryItem> |
||||
</NamespaceSummaries> |
||||
<CleanIntermediates>True</CleanIntermediates> |
||||
<SyntaxFilters>Standard</SyntaxFilters> |
||||
<SdkLinkTarget>Blank</SdkLinkTarget> |
||||
<RootNamespaceContainer>False</RootNamespaceContainer> |
||||
<Preliminary>False</Preliminary> |
||||
<NamingMethod>Guid</NamingMethod> |
||||
<Language>en-US</Language> |
||||
<ContentPlacement>AboveNamespaces</ContentPlacement> |
||||
<BuildAssemblerVerbosity>OnlyWarningsAndErrors</BuildAssemblerVerbosity> |
||||
<HelpFileFormat>HtmlHelp1, Website</HelpFileFormat> |
||||
<IndentHtml>False</IndentHtml> |
||||
<KeepLogFile>True</KeepLogFile> |
||||
<DisableCodeBlockComponent>False</DisableCodeBlockComponent> |
||||
<CppCommentsFixup>False</CppCommentsFixup> |
||||
</PropertyGroup> |
||||
<!-- There are no properties for these two groups but they need to appear in |
||||
order for Visual Studio to perform the build. --> |
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> |
||||
</PropertyGroup> |
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> |
||||
</PropertyGroup> |
||||
<ItemGroup> |
||||
<None Include="Coordinate Systems.aml" /> |
||||
<None Include="Architecture.aml" /> |
||||
<None Include="Code Completion.aml" /> |
||||
<None Include="Sample Application.aml" /> |
||||
<None Include="Folding.aml" /> |
||||
<None Include="Syntax Highlighting.aml" /> |
||||
<None Include="Text Rendering.aml" /> |
||||
<None Include="Welcome.aml" /> |
||||
</ItemGroup> |
||||
<ItemGroup> |
||||
<ContentLayout Include="ICSharpCode.AvalonEdit.content" /> |
||||
</ItemGroup> |
||||
<ItemGroup> |
||||
<Image Include="Media\WelcomeScreenshot.png"> |
||||
<ImageId>WelcomeScreenshot</ImageId> |
||||
</Image> |
||||
<Image Include="Media\VisualTree.png"> |
||||
<ImageId>VisualTree</ImageId> |
||||
<AlternateText>Visual Tree</AlternateText> |
||||
</Image> |
||||
<Image Include="Media\RenderingPipeline.png"> |
||||
<ImageId>RenderingPipeline</ImageId> |
||||
</Image> |
||||
<Image Include="Media\NamespaceDependencies.png"> |
||||
<ImageId>NamespaceDependencies</ImageId> |
||||
<AlternateText>Namespace Dependency Graph</AlternateText> |
||||
</Image> |
||||
<Content Include="License.html"> |
||||
<ExcludeFromToc>True</ExcludeFromToc> |
||||
</Content> |
||||
</ItemGroup> |
||||
<ItemGroup> |
||||
<Folder Include="Media\" /> |
||||
</ItemGroup> |
||||
<!-- Import the SHFB build targets --> |
||||
<Import Project="$(SHFBROOT)\SandcastleHelpFileBuilder.targets" /> |
||||
</Project> |
@ -1,35 +0,0 @@
@@ -1,35 +0,0 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> |
||||
<html xmlns="http://www.w3.org/1999/xhtml"> |
||||
<head> |
||||
<title>License</title> |
||||
<link rel="stylesheet" type="text/css" href="../styles/presentation.css" /> |
||||
</head> |
||||
|
||||
<body> |
||||
|
||||
<h3>The MIT License (MIT)</h3> |
||||
|
||||
<p> |
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
of this software and associated documentation files (the "Software"), to deal |
||||
in the Software without restriction, including without limitation the rights |
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
copies of the Software, and to permit persons to whom the Software is |
||||
furnished to do so, subject to the following conditions: |
||||
</p> |
||||
<p> |
||||
The above copyright notice and this permission notice shall be included in |
||||
all copies or substantial portions of the Software. |
||||
</p> |
||||
<p> |
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
||||
THE SOFTWARE. |
||||
</p> |
||||
</body> |
||||
|
||||
</html> |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 84 KiB |
@ -1,78 +0,0 @@
@@ -1,78 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<topic id="70c4df51-5ecb-4e24-a574-8c5a84306bd1" revisionNumber="1"> |
||||
<developerSampleDocument xmlns="http://ddue.schemas.microsoft.com/authoring/2003/5" xmlns:xlink="http://www.w3.org/1999/xlink"> |
||||
<!-- |
||||
<summary> |
||||
<para>Optional summary abstract</para> |
||||
</summary> |
||||
--> |
||||
<introduction> |
||||
<!-- Uncomment this to generate an outline of the section and sub-section |
||||
titles. Specify a numeric value as the inner text to limit it to |
||||
a specific number of sub-topics when creating the outline. Specify |
||||
zero (0) to limit it to top-level sections only. --> |
||||
<!-- <autoOutline /> --> |
||||
<para>In the SharpDevelop source code download, you will find a small sample |
||||
application in SharpDevelop\samples\AvalonEdit.Sample.</para> |
||||
</introduction> |
||||
|
||||
<mediaLink><image xlink:href="WelcomeScreenshot" placement="center"/></mediaLink> |
||||
<!-- <procedure>Optional procedures. See How To document for procedure layout example.</procedure> --> |
||||
<!-- <requirements>Optional requirements section</requirements> --> |
||||
<!-- <demonstrates>Optional info about what is demonstrated</demonstrates> --> |
||||
<!-- <codeExample>Optional code example</codeExample> --> |
||||
<!-- Add one or more top-level section elements. These are collapsible. |
||||
If using <autoOutline />, add an address attribute to identify it |
||||
and specify a title so that it can be jumped to with a hyperlink. --> |
||||
<section> |
||||
<title>The Code Project article</title> |
||||
<content> |
||||
<para> |
||||
There is a Code Project article based on the sample application: |
||||
<externalLink> |
||||
<linkText>http://www.codeproject.com/KB/edit/AvalonEdit.aspx</linkText> |
||||
<linkUri>http://www.codeproject.com/KB/edit/AvalonEdit.aspx</linkUri> |
||||
<linkTarget>_blank</linkTarget> |
||||
</externalLink> |
||||
</para> |
||||
<para> |
||||
However, most of the material from that article has been included in this help file. |
||||
</para> |
||||
</content> |
||||
</section> |
||||
|
||||
<relatedTopics> |
||||
<!-- One or more of the following: |
||||
- A local link |
||||
- An external link |
||||
- A code entity reference |
||||
|
||||
<link xlink:href="Other Topic's ID">Link text</link> |
||||
<externalLink> |
||||
<linkText>Link text</linkText> |
||||
<linkAlternateText>Optional alternate link text</linkAlternateText> |
||||
<linkUri>URI</linkUri> |
||||
</externalLink> |
||||
<codeEntityReference>API member ID</codeEntityReference> |
||||
|
||||
Examples: |
||||
|
||||
<link xlink:href="00e97994-e9e6-46e0-b420-5be86b2f8278">Some other topic</link> |
||||
|
||||
<externalLink> |
||||
<linkText>SHFB on CodePlex</linkText> |
||||
<linkAlternateText>Go to CodePlex</linkAlternateText> |
||||
<linkUri>http://shfb.codeplex.com</linkUri> |
||||
</externalLink> |
||||
|
||||
<codeEntityReference>T:TestDoc.TestClass</codeEntityReference> |
||||
<codeEntityReference>P:TestDoc.TestClass.SomeProperty</codeEntityReference> |
||||
<codeEntityReference>M:TestDoc.TestClass.#ctor</codeEntityReference> |
||||
<codeEntityReference>M:TestDoc.TestClass.#ctor(System.String,System.Int32)</codeEntityReference> |
||||
<codeEntityReference>M:TestDoc.TestClass.ToString</codeEntityReference> |
||||
<codeEntityReference>M:TestDoc.TestClass.FirstMethod</codeEntityReference> |
||||
<codeEntityReference>M:TestDoc.TestClass.SecondMethod(System.Int32,System.String)</codeEntityReference> |
||||
--> |
||||
</relatedTopics> |
||||
</developerSampleDocument> |
||||
</topic> |
@ -1,234 +0,0 @@
@@ -1,234 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<topic id="4d4ceb51-154d-43f0-b876-ad9640c5d2d8" revisionNumber="1"> |
||||
<developerConceptualDocument xmlns="http://ddue.schemas.microsoft.com/authoring/2003/5" xmlns:xlink="http://www.w3.org/1999/xlink"> |
||||
<introduction> |
||||
<para>Probably the most important feature for any text editor is syntax highlighting.</para> |
||||
<para>AvalonEdit has a flexible text rendering model, see |
||||
<link xlink:href="c06e9832-9ef0-4d65-ac2e-11f7ce9c7774" />. Among the |
||||
text rendering extension points is the support for "visual line transformers" that |
||||
can change the display of a visual line after it has been constructed by the "visual element generators". |
||||
A useful base class implementing IVisualLineTransformer for the purpose of syntax highlighting |
||||
is <codeEntityReference>T:ICSharpCode.AvalonEdit.Rendering.DocumentColorizingTransformer</codeEntityReference>. |
||||
Take a look at that class' documentation to see |
||||
how to write fully custom syntax highlighters. This article only discusses the XML-driven built-in |
||||
highlighting engine. |
||||
</para> |
||||
</introduction> |
||||
<section> |
||||
<title>The highlighting engine</title> |
||||
<content> |
||||
<para> |
||||
The highlighting engine in AvalonEdit is implemented in the class |
||||
<codeEntityReference>T:ICSharpCode.AvalonEdit.Highlighting.DocumentHighlighter</codeEntityReference>. |
||||
Highlighting is the process of taking a DocumentLine and constructing |
||||
a <codeEntityReference>T:ICSharpCode.AvalonEdit.Highlighting.HighlightedLine</codeEntityReference> |
||||
instance for it by assigning colors to different sections of the line. |
||||
A <codeInline>HighlightedLine</codeInline> is simply a list of |
||||
(possibly nested) highlighted text sections. |
||||
</para><para> |
||||
The <codeInline>HighlightingColorizer</codeInline> class is the only |
||||
link between highlighting and rendering. |
||||
It uses a <codeInline>DocumentHighlighter</codeInline> to implement |
||||
a line transformer that applies the |
||||
highlighting to the visual lines in the rendering process. |
||||
</para><para> |
||||
Except for this single call, syntax highlighting is independent from the |
||||
rendering namespace. To help with other potential uses of the highlighting |
||||
engine, the <codeInline>HighlightedLine</codeInline> class has the |
||||
method <codeInline>ToHtml()</codeInline> |
||||
to produce syntax highlighted HTML source code. |
||||
</para> |
||||
<para>The highlighting rules used by the highlighting engine to highlight |
||||
the document are described by the following classes: |
||||
</para> |
||||
|
||||
<definitionTable> |
||||
<definedTerm>HighlightingRuleSet</definedTerm> |
||||
<definition>Describes a set of highlighting spans and rules.</definition> |
||||
<definedTerm>HighlightingSpan</definedTerm> |
||||
<definition>A span consists of two regular expressions (Start and End), a color, |
||||
and a child ruleset. |
||||
The region between Start and End expressions will be assigned the |
||||
given color, and inside that span, the rules of the child |
||||
ruleset apply. |
||||
If the child ruleset also has <codeInline>HighlightingSpan</codeInline>s, |
||||
they can be nested, allowing highlighting constructs like nested comments or one language |
||||
embedded in another.</definition> |
||||
<definedTerm>HighlightingRule</definedTerm> |
||||
<definition>A highlighting rule is a regular expression with a color. |
||||
It will highlight matches of the regular expression using that color.</definition> |
||||
<definedTerm>HighlightingColor</definedTerm> |
||||
<definition>A highlighting color isn't just a color: it consists of a foreground |
||||
color, font weight and font style.</definition> |
||||
</definitionTable> |
||||
<para> |
||||
The highlighting engine works by first analyzing the spans: whenever a |
||||
begin RegEx matches some text, that span is pushed onto a stack. |
||||
Whenever the end RegEx of the current span matches some text, |
||||
the span is popped from the stack. |
||||
</para><para> |
||||
Each span has a nested rule set associated with it, which is empty |
||||
by default. This is why keywords won't be highlighted inside comments: |
||||
the span's empty ruleset is active there, so the keyword rule is not applied. |
||||
</para><para> |
||||
This feature is also used in the string span: the nested span will match |
||||
when a backslash is encountered, and the character following the backslash |
||||
will be consumed by the end RegEx of the nested span |
||||
(<codeInline>.</codeInline> matches any character). |
||||
This ensures that <codeInline>\"</codeInline> does not denote the end of the string span; |
||||
but <codeInline>\\"</codeInline> still does. |
||||
</para><para> |
||||
What's great about the highlighting engine is that it highlights only |
||||
on-demand, works incrementally, and yet usually requires only a |
||||
few KB of memory even for large code files. |
||||
</para><para> |
||||
On-demand means that when a document is opened, only the lines initially |
||||
visible will be highlighted. When the user scrolls down, highlighting will continue |
||||
from the point where it stopped the last time. If the user scrolls quickly, |
||||
so that the first visible line is far below the last highlighted line, |
||||
then the highlighting engine still has to process all the lines in between |
||||
– there might be comment starts in them. However, it will only scan that region |
||||
for changes in the span stack; highlighting rules will not be tested. |
||||
</para><para> |
||||
The stack of active spans is stored at the beginning of every line. |
||||
If the user scrolls back up, the lines getting into view can be highlighted |
||||
immediately because the necessary context (the span stack) is still available. |
||||
</para><para> |
||||
Incrementally means that even if the document is changed, the stored span stacks |
||||
will be reused as far as possible. If the user types <codeInline>/*</codeInline>, |
||||
that would theoretically cause the whole remainder of the file to become |
||||
highlighted in the comment color. |
||||
However, because the engine works on-demand, it will only update the span |
||||
stacks within the currently visible region and keep a notice |
||||
'the highlighting state is not consistent between line X and line X+1', |
||||
where X is the last line in the visible region. |
||||
Now, if the user would scroll down, |
||||
the highlighting state would be updated and the 'not consistent' notice |
||||
would be moved down. But usually, the user will continue typing |
||||
and type <codeInline>*/</codeInline> only a few lines later. |
||||
Now the highlighting state in the visible region will revert to the normal |
||||
'only the main ruleset is on the stack of active spans'. |
||||
When the user now scrolls down below the line with the 'not consistent' marker; |
||||
the engine will notice that the old stack and the new stack are identical; |
||||
and will remove the 'not consistent' marker. |
||||
This allows reusing the stored span stacks cached from before the user typed |
||||
<codeInline>/*</codeInline>. |
||||
</para><para> |
||||
While the stack of active spans might change frequently inside the lines, |
||||
it rarely changes from the beginning of one line to the beginning of the next line. |
||||
With most languages, such changes happen only at the start and end of multiline comments. |
||||
The highlighting engine exploits this property by storing the list of |
||||
span stacks in a special data structure |
||||
(<codeEntityReference>T:ICSharpCode.AvalonEdit.Utils.CompressingTreeList`1</codeEntityReference>). |
||||
The memory usage of the highlighting engine is linear to the number of span stack changes; |
||||
not to the total number of lines. |
||||
This allows the highlighting engine to store the span stacks for big code |
||||
files using only a tiny amount of memory, especially in languages like |
||||
C# where sequences of <codeInline>//</codeInline> or <codeInline>///</codeInline> |
||||
are more popular than <codeInline>/* */</codeInline> comments. |
||||
</para> |
||||
</content> |
||||
</section> |
||||
<section> |
||||
<title>XML highlighting definitions</title> |
||||
<content> |
||||
<para>AvalonEdit supports XML syntax highlighting definitions (.xshd files).</para> |
||||
<para>In the AvalonEdit source code, you can find the file |
||||
<codeInline>ICSharpCode.AvalonEdit\Highlighting\Resources\ModeV2.xsd</codeInline>. |
||||
This is an XML schema for the .xshd file format; you can use it to |
||||
code completion for .xshd files in XML editors. |
||||
</para> |
||||
<para>Here is an example highlighting definition for a sub-set of C#: |
||||
<code language="xml"><![CDATA[ |
||||
<SyntaxDefinition name="C#" |
||||
xmlns="http://icsharpcode.net/sharpdevelop/syntaxdefinition/2008"> |
||||
<Color name="Comment" foreground="Green" /> |
||||
<Color name="String" foreground="Blue" /> |
||||
|
||||
<!-- This is the main ruleset. --> |
||||
<RuleSet> |
||||
<Span color="Comment" begin="//" /> |
||||
<Span color="Comment" multiline="true" begin="/\*" end="\*/" /> |
||||
|
||||
<Span color="String"> |
||||
<Begin>"</Begin> |
||||
<End>"</End> |
||||
<RuleSet> |
||||
<!-- nested span for escape sequences --> |
||||
<Span begin="\\" end="." /> |
||||
</RuleSet> |
||||
</Span> |
||||
|
||||
<Keywords fontWeight="bold" foreground="Blue"> |
||||
<Word>if</Word> |
||||
<Word>else</Word> |
||||
<!-- ... --> |
||||
</Keywords> |
||||
|
||||
<!-- Digits --> |
||||
<Rule foreground="DarkBlue"> |
||||
\b0[xX][0-9a-fA-F]+ # hex number |
||||
| \b |
||||
( \d+(\.[0-9]+)? #number with optional floating point |
||||
| \.[0-9]+ #or just starting with floating point |
||||
) |
||||
([eE][+-]?[0-9]+)? # optional exponent |
||||
</Rule> |
||||
</RuleSet> |
||||
</SyntaxDefinition> |
||||
]]></code> |
||||
</para> |
||||
</content> |
||||
</section> |
||||
<section> |
||||
<title>ICSharpCode.TextEditor XML highlighting definitions</title> |
||||
<content> |
||||
<para>ICSharpCode.TextEditor (the predecessor of AvalonEdit) used |
||||
a different version of the XSHD file format. |
||||
AvalonEdit detects the difference between the formats using the XML namespace: |
||||
The new format uses <codeInline>xmlns="http://icsharpcode.net/sharpdevelop/syntaxdefinition/2008"</codeInline>, |
||||
the old format does not use any XML namespace. |
||||
</para><para> |
||||
AvalonEdit can load .xshd files written in that old format, and even |
||||
automatically convert them to the new format. However, not all |
||||
constructs of the old file format are supported by AvalonEdit. |
||||
</para> |
||||
<code language="cs"><![CDATA[// convert from old .xshd format to new format |
||||
XshdSyntaxDefinition xshd; |
||||
using (XmlTextReader reader = new XmlTextReader("input.xshd")) { |
||||
xshd = HighlightingLoader.LoadXshd(reader); |
||||
} |
||||
using (XmlTextWriter writer = new XmlTextWriter("output.xshd", System.Text.Encoding.UTF8)) { |
||||
writer.Formatting = Formatting.Indented; |
||||
new SaveXshdVisitor(writer).WriteDefinition(xshd); |
||||
} |
||||
]]></code> |
||||
</content> |
||||
</section> |
||||
<section> |
||||
<title>Programmatically accessing highlighting information</title> |
||||
<content> |
||||
<para>As described above, the highlighting engine only stores the "span stack" |
||||
at the start of each line. This information can be retrieved using the |
||||
<codeEntityReference>M:ICSharpCode.AvalonEdit.Highlighting.DocumentHighlighter.GetSpanStack(System.Int32)</codeEntityReference> |
||||
method: |
||||
<code language="cs"><![CDATA[bool isInComment = documentHighlighter.GetSpanStack(1).Any( |
||||
s => s.SpanColor != null && s.SpanColor.Name == "Comment"); |
||||
// returns true if the end of line 1 (=start of line 2) is inside a multiline comment]]></code> |
||||
Spans can be identified using their color. For this purpose, named colors should be used in the syntax definition. |
||||
</para> |
||||
<para>For more detailed results inside lines, the highlighting algorithm |
||||
must be executed for that line: |
||||
<code language="cs"><![CDATA[int off = document.GetOffset(7, 22); |
||||
HighlightedLine result = documentHighlighter.HighlightLine(document.GetLineByNumber(7)); |
||||
bool isInComment = result.Sections.Any( |
||||
s => s.Offset <= off && s.Offset+s.Length >= off |
||||
&& s.Color.Name == "Comment");]]></code> |
||||
</para> |
||||
</content> |
||||
</section> |
||||
<relatedTopics> |
||||
<codeEntityReference>N:ICSharpCode.AvalonEdit.Highlighting</codeEntityReference> |
||||
</relatedTopics> |
||||
</developerConceptualDocument> |
||||
</topic> |
@ -1,178 +0,0 @@
@@ -1,178 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<topic id="c06e9832-9ef0-4d65-ac2e-11f7ce9c7774" revisionNumber="1"> |
||||
<developerConceptualDocument xmlns="http://ddue.schemas.microsoft.com/authoring/2003/5" xmlns:xlink="http://www.w3.org/1999/xlink"> |
||||
<summary> |
||||
<para>This document describes how the TextView class renders the text, and |
||||
how you can extend the text rendering process to add new features to the text editor. |
||||
</para> |
||||
</summary> |
||||
<introduction> |
||||
<para>The <codeEntityReference qualifyHint="true">T:ICSharpCode.AvalonEdit.Rendering.TextView</codeEntityReference> |
||||
class is the heart of AvalonEdit. |
||||
It takes care of getting the document onto the screen.</para> |
||||
<para>To do this in an extensible way, the TextView uses its own kind of model: |
||||
the <codeEntityReference>T:ICSharpCode.AvalonEdit.Rendering.VisualLine</codeEntityReference>. |
||||
Visual lines are created only for the visible part of the document.</para> |
||||
<para> |
||||
The rendering process looks like this: |
||||
<mediaLink><image xlink:href="RenderingPipeline" placement="center"/></mediaLink> |
||||
The "element generators", "line transformers" and "background renderers" are |
||||
the extension points; it is possible to add custom implementations of them to |
||||
the TextView to implement additional features in the editor. |
||||
</para> |
||||
</introduction> |
||||
<!-- Add one or more top-level section elements. These are collapsible. |
||||
If using <autoOutline /> tag, add an address attribute to identify |
||||
it so that it can be jumped to with a hyperlink. --> |
||||
<section> |
||||
<title>Lifetime of VisualLines</title> |
||||
<content> |
||||
<para> |
||||
VisualLines are only created for the visible part of the document. |
||||
Lots of actions can trigger their creation, but most commonly the creation will be |
||||
caused by the MeasureOverride method of TextView. |
||||
When the TextView is measured, it uses the height tree to determine the first |
||||
document line in the visible region. Then, it constructs and measures a VisualLine |
||||
for that first line, and repeats that with the following lines |
||||
until the visible region is filled. |
||||
</para> |
||||
<para> |
||||
The TextView caches VisualLines - when the user scrolls down, only the VisualLines |
||||
coming into view are created, the rest is reused. |
||||
The VisualLine cache can be manually invalidated using the Redraw method family; |
||||
moreover, lots of actions cause automatic invalidation: |
||||
<list class="bullet"><listItem>any change in the document will invalidate the affected VisualLines</listItem><listItem>changing the width of the TextView invalidates all VisualLines if word-wrap is enabled</listItem><listItem>changing any text editor settings (word-wrap, font size, etc.) will invalidate all VisualLines</listItem><listItem>VisualLines leaving the visible area after scrolling will be disposed</listItem></list> |
||||
In general, manual invalidation is required only if you have written a text editor extension |
||||
(BackgroundRenderer, VisualLineElementGenerator or VisualLineTransformer) that also consumes |
||||
external data - in that case, you'll have to notify the text editor that VisualLines need |
||||
to be recreated when your external data changes. |
||||
</para> |
||||
<alert class="note"> |
||||
<para>If external data used by your text editor extension changes, call |
||||
<codeEntityReference qualifyHint="true">M:ICSharpCode.AvalonEdit.Rendering.TextView.Redraw</codeEntityReference> |
||||
to invalidate the VisualLine. |
||||
</para> |
||||
</alert> |
||||
<para> |
||||
Invalidating VisualLines does not cause immediate recreation of the lines! |
||||
Rather, the VisualLines are recreated when the text view is next re-measured. |
||||
While measurement in WPF normally happens with DispatcherPriority.Render, |
||||
the TextView also supports redrawing with a lower priority than that. |
||||
For example, normal text insertion causes a redraw at background priority, so that |
||||
in case the user types faster than the text view can redraw, there will be only |
||||
one redraw for multiple input actions. |
||||
</para> |
||||
<alert class="note"> |
||||
<para> |
||||
The TextView will never return invalid lines to you, but you |
||||
may run into the case that the valid visual lines are not available. |
||||
</para> |
||||
<para> |
||||
What happens in this case depends on the method you are calling - |
||||
the new visual line might get created automatically, |
||||
null could be returned, or you may get a |
||||
<codeEntityReference>T:ICSharpCode.AvalonEdit.Rendering.VisualLinesInvalidException</codeEntityReference>. |
||||
</para> |
||||
<para> |
||||
You can call |
||||
<codeEntityReference qualifyHint="true">M:ICSharpCode.AvalonEdit.Rendering.TextView.EnsureVisualLines</codeEntityReference> |
||||
to make the text view create all VisualLines in the visible region. |
||||
</para> |
||||
</alert> |
||||
</content> |
||||
</section> |
||||
<section> |
||||
<title>Building visual line elements</title> |
||||
<content> |
||||
<para> |
||||
As a first step, the VisualLineElementGenerators are used to produce elements. The |
||||
room in between the elements returned from the generators is filled with text elements. |
||||
Then, the VisualLine assigns the VisualColumn and RelativeTextOffset properties of the line elements. |
||||
</para> |
||||
<para> |
||||
For example, a line contains the text "Hello, World". |
||||
The user has enabled "ShowSpaces", so the text editor should show a little dot instead of the space. |
||||
In this case, the SingleCharacterElementGenerator, which is responsible for ShowSpaces, will produce |
||||
a "SpaceTextElement" for the space character. Because no other generators are interested in the line, |
||||
the remaining strings "Hello," and "World" will be represented by VisualLineText elements. |
||||
</para> |
||||
</content> |
||||
</section> |
||||
<section> |
||||
<title>Transforming visual line elements</title> |
||||
<content> |
||||
<para> |
||||
After that, the IVisualLineTransformers are used to modify the produced line elements. Transformers |
||||
must not add elements, but they may split existing elements, e.g. to colorize only parts of an |
||||
element. When splitting elements (or somehow modifying the elements collection), care must be taken |
||||
that the VisualColumn,VisualLine,RelativeTextOffset and DocumentLength properties stay correct. |
||||
</para> |
||||
<para> |
||||
The ColorizingTransformer base class provides helper methods for splitting, so the derived class |
||||
can simply say "color this section in that color". |
||||
</para> |
||||
<para> |
||||
The DocumentColorizingTransformer extends the ColorizingTransformer and additionally |
||||
allows highlighting on per DocumentLine, coloring text segments (instead of directly |
||||
working with visual line elements). |
||||
</para> |
||||
</content> |
||||
</section> |
||||
<section> |
||||
<title>Constructing TextLines</title> |
||||
<content> |
||||
<para> |
||||
After building the visual line elements, the TextLines for the visual line are constructed. |
||||
A visual line may result in multiple text lines when word wrapping is active or when special visual |
||||
line elements force a line break. |
||||
Building text lines: |
||||
The text line construction is done by a WPF TextFormatter. |
||||
The VisualLineTextSource will take the visual line elements and build WPF TextRuns from it, |
||||
while the WPF TextFormatter takes care of word wrapping etc. |
||||
VisualLineElements are requested to produce TextRuns for their full or a partial length. |
||||
The TextView will take care to measure any inline UI elements in the visual lines. |
||||
</para> |
||||
</content> |
||||
</section> |
||||
<section> |
||||
<title>Rest of the Rendering</title> |
||||
<content> |
||||
<para> |
||||
After the visible region is filled, the TextView updates the heights stored in the document lines to |
||||
the measured heights. This way, scrolling takes account for word-wrapping. |
||||
The constructed text lines are stored for the arrange and render steps. |
||||
Now, finally, the measure step is complete. |
||||
</para> |
||||
<para> |
||||
The WPF arrange step doesn't have much work to do: |
||||
It just arranges inline UI elements at their position inside the text. |
||||
</para> |
||||
<para> |
||||
The actual rendering does not happen directly in the TextView, but in its |
||||
various layers. |
||||
</para> |
||||
<para> |
||||
These are the predefined layers: |
||||
<list class="bullet"> |
||||
<listItem>Background layer: renders the background colors associated with the visual elements</listItem> |
||||
<listItem>Selection layer: renders the background of the selection</listItem> |
||||
<listItem>Text layer: renders the TextLines that were constructed during the Measure step. |
||||
Starting with AvalonEdit 4.1, the TextLayer uses child elements to draw the text: one DrawingVisual for each VisualLine. |
||||
</listItem> |
||||
<listItem> |
||||
Immediately after the text layer, any inline UI elements are placed as if they were separate layers. |
||||
</listItem> |
||||
<listItem>Caret layer: renders a blinking caret</listItem></list> |
||||
It's also possible to insert new layers into the TextView using the |
||||
<codeEntityReference qualifyHint="true">M:ICSharpCode.AvalonEdit.Rendering.TextView.InsertLayer(System.Windows.UIElement,ICSharpCode.AvalonEdit.Rendering.KnownLayer,ICSharpCode.AvalonEdit.Rendering.LayerInsertionPosition)</codeEntityReference> |
||||
method. |
||||
This allows adding custom interactive components to the editor |
||||
while being in full control of the Z-Order. |
||||
</para> |
||||
</content> |
||||
</section> |
||||
<relatedTopics> |
||||
<codeEntityReference>T:ICSharpCode.AvalonEdit.Rendering.TextView</codeEntityReference> |
||||
</relatedTopics> |
||||
</developerConceptualDocument> |
||||
</topic> |
@ -1,70 +0,0 @@
@@ -1,70 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<topic id="c52241ea-3eba-4ddf-b463-6349cbff38fd" revisionNumber="1"> |
||||
<developerConceptualDocument xmlns="http://ddue.schemas.microsoft.com/authoring/2003/5" xmlns:xlink="http://www.w3.org/1999/xlink"> |
||||
<summary> |
||||
<para>AvalonEdit is a WPF-based extensible text editor.</para> |
||||
</summary> |
||||
<introduction> |
||||
<para>While the WPF RichTextBox is quite powerful, you quickly run into its limits |
||||
when trying to use it as a code editor: it's hard to write efficient syntax highlighting for it, |
||||
and you cannot really implement features like code folding with the standard RichTextBox.</para> |
||||
<para>The problem is: the RichTextBox edits a rich document. |
||||
In contrast, AvalonEdit simply edits text.</para> |
||||
<para>However, AvalonEdit offers lots of possibilities on how the text document is |
||||
displayed - so it is much more suitable for a code editor where things like the text color |
||||
are not controlled by the user, but instead depend on the text (syntax highlighting). |
||||
</para> |
||||
<para> |
||||
AvalonEdit was written for the SharpDevelop IDE. It replaces our old |
||||
Windows Forms-based text editor (ICSharpCode.TextEditor). |
||||
</para> |
||||
</introduction> |
||||
|
||||
<mediaLink><image xlink:href="WelcomeScreenshot" placement="center"/></mediaLink> |
||||
|
||||
<section> |
||||
<title>Usage</title> |
||||
<content> |
||||
<para>The main class of the editor is <codeEntityReference qualifyHint="true">T:ICSharpCode.AvalonEdit.TextEditor</codeEntityReference>. |
||||
You can use it similar to a normal WPF TextBox:</para> |
||||
<code language="xml"><![CDATA[ |
||||
<avalonEdit:TextEditor |
||||
xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit" |
||||
Name="textEditor" |
||||
SyntaxHighlighting="C#" |
||||
FontFamily="Consolas" |
||||
FontSize="10pt"/> |
||||
]]></code> |
||||
</content> |
||||
</section> |
||||
|
||||
<section> |
||||
<title>System requirements</title> |
||||
<content> |
||||
<para>There are two versions of AvalonEdit - the normal one requires |
||||
<externalLink> |
||||
<linkText>.NET Framework 4.0</linkText> |
||||
<linkUri>http://msdn.microsoft.com/en-us/library/w0x726c2.aspx</linkUri> |
||||
<linkTarget>_blank</linkTarget> |
||||
</externalLink> or higher; but we also offer a modified version for .NET 3.5 SP1. |
||||
For compiling AvalonEdit, you will need a C# 4.0 compiler (SharpDevelop 4.x or Visual Studio 2010). |
||||
</para> |
||||
<para>AvalonEdit requires FullTrust and will not run as XBAP.</para> |
||||
</content> |
||||
</section> |
||||
|
||||
<relatedTopics> |
||||
<codeEntityReference qualifyHint="true">T:ICSharpCode.AvalonEdit.TextEditor</codeEntityReference> |
||||
<externalLink> |
||||
<linkText>www.avalonedit.net</linkText> |
||||
<linkUri>http://www.avalonedit.net</linkUri> |
||||
<linkTarget>_blank</linkTarget> |
||||
</externalLink> |
||||
<externalLink> |
||||
<linkText>www.icsharpcode.net</linkText> |
||||
<linkUri>http://www.icsharpcode.net</linkUri> |
||||
<linkTarget>_blank</linkTarget> |
||||
</externalLink> |
||||
</relatedTopics> |
||||
</developerConceptualDocument> |
||||
</topic> |
@ -1,79 +0,0 @@
@@ -1,79 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Linq; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory.Editor; |
||||
#endif
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
[TestFixture] |
||||
public class ChangeTrackingTest |
||||
{ |
||||
[Test] |
||||
public void NoChanges() |
||||
{ |
||||
TextDocument document = new TextDocument("initial text"); |
||||
ITextSource snapshot1 = document.CreateSnapshot(); |
||||
ITextSource snapshot2 = document.CreateSnapshot(); |
||||
Assert.AreEqual(0, snapshot1.Version.CompareAge(snapshot2.Version)); |
||||
Assert.AreEqual(0, snapshot1.Version.GetChangesTo(snapshot2.Version).Count()); |
||||
Assert.AreEqual(document.Text, snapshot1.Text); |
||||
Assert.AreEqual(document.Text, snapshot2.Text); |
||||
} |
||||
|
||||
[Test] |
||||
public void ForwardChanges() |
||||
{ |
||||
TextDocument document = new TextDocument("initial text"); |
||||
ITextSource snapshot1 = document.CreateSnapshot(); |
||||
document.Replace(0, 7, "nw"); |
||||
document.Insert(1, "e"); |
||||
ITextSource snapshot2 = document.CreateSnapshot(); |
||||
Assert.AreEqual(-1, snapshot1.Version.CompareAge(snapshot2.Version)); |
||||
TextChangeEventArgs[] arr = snapshot1.Version.GetChangesTo(snapshot2.Version).ToArray(); |
||||
Assert.AreEqual(2, arr.Length); |
||||
Assert.AreEqual("nw", arr[0].InsertedText.Text); |
||||
Assert.AreEqual("e", arr[1].InsertedText.Text); |
||||
|
||||
Assert.AreEqual("initial text", snapshot1.Text); |
||||
Assert.AreEqual("new text", snapshot2.Text); |
||||
} |
||||
|
||||
[Test] |
||||
public void BackwardChanges() |
||||
{ |
||||
TextDocument document = new TextDocument("initial text"); |
||||
ITextSource snapshot1 = document.CreateSnapshot(); |
||||
document.Replace(0, 7, "nw"); |
||||
document.Insert(1, "e"); |
||||
ITextSource snapshot2 = document.CreateSnapshot(); |
||||
Assert.AreEqual(1, snapshot2.Version.CompareAge(snapshot1.Version)); |
||||
TextChangeEventArgs[] arr = snapshot2.Version.GetChangesTo(snapshot1.Version).ToArray(); |
||||
Assert.AreEqual(2, arr.Length); |
||||
Assert.AreEqual("", arr[0].InsertedText.Text); |
||||
Assert.AreEqual("initial", arr[1].InsertedText.Text); |
||||
|
||||
Assert.AreEqual("initial text", snapshot1.Text); |
||||
Assert.AreEqual("new text", snapshot2.Text); |
||||
} |
||||
} |
||||
} |
@ -1,170 +0,0 @@
@@ -1,170 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using ICSharpCode.AvalonEdit.Rendering; |
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
[TestFixture] |
||||
public class CollapsingTests |
||||
{ |
||||
TextDocument document; |
||||
HeightTree heightTree; |
||||
|
||||
[SetUp] |
||||
public void Setup() |
||||
{ |
||||
document = new TextDocument(); |
||||
document.Text = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10"; |
||||
heightTree = new HeightTree(document, 10); |
||||
foreach (DocumentLine line in document.Lines) { |
||||
heightTree.SetHeight(line, line.LineNumber); |
||||
} |
||||
} |
||||
|
||||
CollapsedLineSection SimpleCheck(int from, int to) |
||||
{ |
||||
CollapsedLineSection sec1 = heightTree.CollapseText(document.GetLineByNumber(from), document.GetLineByNumber(to)); |
||||
for (int i = 1; i < from; i++) { |
||||
Assert.IsFalse(heightTree.GetIsCollapsed(i)); |
||||
} |
||||
for (int i = from; i <= to; i++) { |
||||
Assert.IsTrue(heightTree.GetIsCollapsed(i)); |
||||
} |
||||
for (int i = to + 1; i <= 10; i++) { |
||||
Assert.IsFalse(heightTree.GetIsCollapsed(i)); |
||||
} |
||||
CheckHeights(); |
||||
return sec1; |
||||
} |
||||
|
||||
[Test] |
||||
public void SimpleCheck() |
||||
{ |
||||
SimpleCheck(4, 6); |
||||
} |
||||
|
||||
[Test] |
||||
public void SimpleUncollapse() |
||||
{ |
||||
CollapsedLineSection sec1 = heightTree.CollapseText(document.GetLineByNumber(4), document.GetLineByNumber(6)); |
||||
sec1.Uncollapse(); |
||||
for (int i = 1; i <= 10; i++) { |
||||
Assert.IsFalse(heightTree.GetIsCollapsed(i)); |
||||
} |
||||
CheckHeights(); |
||||
} |
||||
|
||||
[Test] |
||||
public void FullCheck() |
||||
{ |
||||
for (int from = 1; from <= 10; from++) { |
||||
for (int to = from; to <= 10; to++) { |
||||
try { |
||||
SimpleCheck(from, to).Uncollapse(); |
||||
for (int i = 1; i <= 10; i++) { |
||||
Assert.IsFalse(heightTree.GetIsCollapsed(i)); |
||||
} |
||||
CheckHeights(); |
||||
} catch { |
||||
Console.WriteLine("from = " + from + ", to = " + to); |
||||
throw; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
[Test] |
||||
public void InsertInCollapsedSection() |
||||
{ |
||||
CollapsedLineSection sec1 = heightTree.CollapseText(document.GetLineByNumber(4), document.GetLineByNumber(6)); |
||||
document.Insert(document.GetLineByNumber(5).Offset, "a\nb\nc"); |
||||
for (int i = 1; i < 4; i++) { |
||||
Assert.IsFalse(heightTree.GetIsCollapsed(i)); |
||||
} |
||||
for (int i = 4; i <= 8; i++) { |
||||
Assert.IsTrue(heightTree.GetIsCollapsed(i)); |
||||
} |
||||
for (int i = 9; i <= 12; i++) { |
||||
Assert.IsFalse(heightTree.GetIsCollapsed(i)); |
||||
} |
||||
CheckHeights(); |
||||
} |
||||
|
||||
[Test] |
||||
public void RemoveInCollapsedSection() |
||||
{ |
||||
CollapsedLineSection sec1 = heightTree.CollapseText(document.GetLineByNumber(3), document.GetLineByNumber(7)); |
||||
int line4Offset = document.GetLineByNumber(4).Offset; |
||||
int line6Offset = document.GetLineByNumber(6).Offset; |
||||
document.Remove(line4Offset, line6Offset - line4Offset); |
||||
for (int i = 1; i < 3; i++) { |
||||
Assert.IsFalse(heightTree.GetIsCollapsed(i)); |
||||
} |
||||
for (int i = 3; i <= 5; i++) { |
||||
Assert.IsTrue(heightTree.GetIsCollapsed(i)); |
||||
} |
||||
for (int i = 6; i <= 8; i++) { |
||||
Assert.IsFalse(heightTree.GetIsCollapsed(i)); |
||||
} |
||||
CheckHeights(); |
||||
} |
||||
|
||||
[Test] |
||||
public void RemoveEndOfCollapsedSection() |
||||
{ |
||||
CollapsedLineSection sec1 = heightTree.CollapseText(document.GetLineByNumber(3), document.GetLineByNumber(6)); |
||||
int line5Offset = document.GetLineByNumber(5).Offset; |
||||
int line8Offset = document.GetLineByNumber(8).Offset; |
||||
document.Remove(line5Offset, line8Offset - line5Offset); |
||||
for (int i = 1; i < 3; i++) { |
||||
Assert.IsFalse(heightTree.GetIsCollapsed(i)); |
||||
} |
||||
for (int i = 3; i <= 5; i++) { |
||||
Assert.IsTrue(heightTree.GetIsCollapsed(i)); |
||||
} |
||||
for (int i = 6; i <= 7; i++) { |
||||
Assert.IsFalse(heightTree.GetIsCollapsed(i)); |
||||
} |
||||
CheckHeights(); |
||||
} |
||||
|
||||
[Test] |
||||
public void RemoveCollapsedSection() |
||||
{ |
||||
CollapsedLineSection sec1 = heightTree.CollapseText(document.GetLineByNumber(3), document.GetLineByNumber(3)); |
||||
int line3Offset = document.GetLineByNumber(3).Offset; |
||||
document.Remove(line3Offset - 1, 1); |
||||
for (int i = 1; i <= 9; i++) { |
||||
Assert.IsFalse(heightTree.GetIsCollapsed(i)); |
||||
} |
||||
CheckHeights(); |
||||
Assert.AreSame(null, sec1.Start); |
||||
Assert.AreSame(null, sec1.End); |
||||
// section gets uncollapsed when it is removed
|
||||
Assert.IsFalse(sec1.IsCollapsed); |
||||
} |
||||
|
||||
void CheckHeights() |
||||
{ |
||||
HeightTests.CheckHeights(document, heightTree); |
||||
} |
||||
} |
||||
} |
@ -1,91 +0,0 @@
@@ -1,91 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Linq; |
||||
using ICSharpCode.AvalonEdit.Rendering; |
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
[TestFixture] |
||||
public class HeightTests |
||||
{ |
||||
TextDocument document; |
||||
HeightTree heightTree; |
||||
|
||||
[SetUp] |
||||
public void Setup() |
||||
{ |
||||
document = new TextDocument(); |
||||
document.Text = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10"; |
||||
heightTree = new HeightTree(document, 10); |
||||
foreach (DocumentLine line in document.Lines) { |
||||
heightTree.SetHeight(line, line.LineNumber); |
||||
} |
||||
} |
||||
|
||||
[Test] |
||||
public void SimpleCheck() |
||||
{ |
||||
CheckHeights(); |
||||
} |
||||
|
||||
[Test] |
||||
public void TestLinesRemoved() |
||||
{ |
||||
document.Remove(5, 4); |
||||
CheckHeights(); |
||||
} |
||||
|
||||
[Test] |
||||
public void TestHeightChanged() |
||||
{ |
||||
heightTree.SetHeight(document.GetLineByNumber(4), 100); |
||||
CheckHeights(); |
||||
} |
||||
|
||||
[Test] |
||||
public void TestLinesInserted() |
||||
{ |
||||
document.Insert(0, "x\ny\n"); |
||||
heightTree.SetHeight(document.Lines[0], 100); |
||||
heightTree.SetHeight(document.Lines[1], 1000); |
||||
heightTree.SetHeight(document.Lines[2], 10000); |
||||
CheckHeights(); |
||||
} |
||||
|
||||
void CheckHeights() |
||||
{ |
||||
CheckHeights(document, heightTree); |
||||
} |
||||
|
||||
internal static void CheckHeights(TextDocument document, HeightTree heightTree) |
||||
{ |
||||
double[] heights = document.Lines.Select(l => heightTree.GetIsCollapsed(l.LineNumber) ? 0 : heightTree.GetHeight(l)).ToArray(); |
||||
double[] visualPositions = new double[document.LineCount+1]; |
||||
for (int i = 0; i < heights.Length; i++) { |
||||
visualPositions[i+1]=visualPositions[i]+heights[i]; |
||||
} |
||||
foreach (DocumentLine ls in document.Lines) { |
||||
Assert.AreEqual(visualPositions[ls.LineNumber-1], heightTree.GetVisualPosition(ls)); |
||||
} |
||||
Assert.AreEqual(visualPositions[document.LineCount], heightTree.TotalHeight); |
||||
} |
||||
} |
||||
} |
@ -1,553 +0,0 @@
@@ -1,553 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
[TestFixture] |
||||
public class LineManagerTests |
||||
{ |
||||
TextDocument document; |
||||
|
||||
[SetUp] |
||||
public void SetUp() |
||||
{ |
||||
document = new TextDocument(); |
||||
} |
||||
|
||||
[Test] |
||||
public void CheckEmptyDocument() |
||||
{ |
||||
Assert.AreEqual("", document.Text); |
||||
Assert.AreEqual(0, document.TextLength); |
||||
Assert.AreEqual(1, document.LineCount); |
||||
} |
||||
|
||||
[Test] |
||||
public void CheckClearingDocument() |
||||
{ |
||||
document.Text = "Hello,\nWorld!"; |
||||
Assert.AreEqual(2, document.LineCount); |
||||
var oldLines = document.Lines.ToArray(); |
||||
document.Text = ""; |
||||
Assert.AreEqual("", document.Text); |
||||
Assert.AreEqual(0, document.TextLength); |
||||
Assert.AreEqual(1, document.LineCount); |
||||
Assert.AreSame(oldLines[0], document.Lines.Single()); |
||||
Assert.IsFalse(oldLines[0].IsDeleted); |
||||
Assert.IsTrue(oldLines[1].IsDeleted); |
||||
Assert.IsNull(oldLines[0].NextLine); |
||||
Assert.IsNull(oldLines[1].PreviousLine); |
||||
} |
||||
|
||||
[Test] |
||||
public void CheckGetLineInEmptyDocument() |
||||
{ |
||||
Assert.AreEqual(1, document.Lines.Count); |
||||
List<DocumentLine> lines = new List<DocumentLine>(document.Lines); |
||||
Assert.AreEqual(1, lines.Count); |
||||
DocumentLine line = document.Lines[0]; |
||||
Assert.AreSame(line, lines[0]); |
||||
Assert.AreSame(line, document.GetLineByNumber(1)); |
||||
Assert.AreSame(line, document.GetLineByOffset(0)); |
||||
} |
||||
|
||||
[Test] |
||||
public void CheckLineSegmentInEmptyDocument() |
||||
{ |
||||
DocumentLine line = document.GetLineByNumber(1); |
||||
Assert.AreEqual(1, line.LineNumber); |
||||
Assert.AreEqual(0, line.Offset); |
||||
Assert.IsFalse(line.IsDeleted); |
||||
Assert.AreEqual(0, line.Length); |
||||
Assert.AreEqual(0, line.TotalLength); |
||||
Assert.AreEqual(0, line.DelimiterLength); |
||||
} |
||||
|
||||
[Test] |
||||
public void LineIndexOfTest() |
||||
{ |
||||
DocumentLine line = document.GetLineByNumber(1); |
||||
Assert.AreEqual(0, document.Lines.IndexOf(line)); |
||||
DocumentLine lineFromOtherDocument = new TextDocument().GetLineByNumber(1); |
||||
Assert.AreEqual(-1, document.Lines.IndexOf(lineFromOtherDocument)); |
||||
document.Text = "a\nb\nc"; |
||||
DocumentLine middleLine = document.GetLineByNumber(2); |
||||
Assert.AreEqual(1, document.Lines.IndexOf(middleLine)); |
||||
document.Remove(1, 3); |
||||
Assert.IsTrue(middleLine.IsDeleted); |
||||
Assert.AreEqual(-1, document.Lines.IndexOf(middleLine)); |
||||
} |
||||
|
||||
[Test] |
||||
public void InsertInEmptyDocument() |
||||
{ |
||||
document.Insert(0, "a"); |
||||
Assert.AreEqual(document.LineCount, 1); |
||||
DocumentLine line = document.GetLineByNumber(1); |
||||
Assert.AreEqual("a", document.GetText(line)); |
||||
} |
||||
|
||||
[Test] |
||||
public void SetText() |
||||
{ |
||||
document.Text = "a"; |
||||
Assert.AreEqual(document.LineCount, 1); |
||||
DocumentLine line = document.GetLineByNumber(1); |
||||
Assert.AreEqual("a", document.GetText(line)); |
||||
} |
||||
|
||||
[Test] |
||||
public void InsertNothing() |
||||
{ |
||||
document.Insert(0, ""); |
||||
Assert.AreEqual(document.LineCount, 1); |
||||
Assert.AreEqual(document.TextLength, 0); |
||||
} |
||||
|
||||
[Test, ExpectedException(typeof(ArgumentNullException))] |
||||
public void InsertNull() |
||||
{ |
||||
document.Insert(0, (string)null); |
||||
} |
||||
|
||||
[Test, ExpectedException(typeof(ArgumentNullException))] |
||||
public void SetTextNull() |
||||
{ |
||||
document.Text = null; |
||||
} |
||||
|
||||
[Test] |
||||
public void RemoveNothing() |
||||
{ |
||||
document.Remove(0, 0); |
||||
Assert.AreEqual(document.LineCount, 1); |
||||
Assert.AreEqual(document.TextLength, 0); |
||||
} |
||||
|
||||
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))] |
||||
public void GetCharAt0EmptyDocument() |
||||
{ |
||||
document.GetCharAt(0); |
||||
} |
||||
|
||||
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))] |
||||
public void GetCharAtNegativeOffset() |
||||
{ |
||||
document.Text = "a\nb"; |
||||
document.GetCharAt(-1); |
||||
} |
||||
|
||||
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))] |
||||
public void GetCharAtEndOffset() |
||||
{ |
||||
document.Text = "a\nb"; |
||||
document.GetCharAt(document.TextLength); |
||||
} |
||||
|
||||
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))] |
||||
public void InsertAtNegativeOffset() |
||||
{ |
||||
document.Text = "a\nb"; |
||||
document.Insert(-1, "text"); |
||||
} |
||||
|
||||
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))] |
||||
public void InsertAfterEndOffset() |
||||
{ |
||||
document.Text = "a\nb"; |
||||
document.Insert(4, "text"); |
||||
} |
||||
|
||||
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))] |
||||
public void RemoveNegativeAmount() |
||||
{ |
||||
document.Text = "abcd"; |
||||
document.Remove(2, -1); |
||||
} |
||||
|
||||
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))] |
||||
public void RemoveTooMuch() |
||||
{ |
||||
document.Text = "abcd"; |
||||
document.Remove(2, 10); |
||||
} |
||||
|
||||
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))] |
||||
public void GetLineByNumberNegative() |
||||
{ |
||||
document.Text = "a\nb"; |
||||
document.GetLineByNumber(-1); |
||||
} |
||||
|
||||
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))] |
||||
public void GetLineByNumberTooHigh() |
||||
{ |
||||
document.Text = "a\nb"; |
||||
document.GetLineByNumber(3); |
||||
} |
||||
|
||||
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))] |
||||
public void GetLineByOffsetNegative() |
||||
{ |
||||
document.Text = "a\nb"; |
||||
document.GetLineByOffset(-1); |
||||
} |
||||
|
||||
|
||||
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))] |
||||
public void GetLineByOffsetToHigh() |
||||
{ |
||||
document.Text = "a\nb"; |
||||
document.GetLineByOffset(10); |
||||
} |
||||
|
||||
[Test] |
||||
public void InsertAtEndOffset() |
||||
{ |
||||
document.Text = "a\nb"; |
||||
CheckDocumentLines("a", |
||||
"b"); |
||||
document.Insert(3, "text"); |
||||
CheckDocumentLines("a", |
||||
"btext"); |
||||
} |
||||
|
||||
[Test] |
||||
public void GetCharAt() |
||||
{ |
||||
document.Text = "a\r\nb"; |
||||
Assert.AreEqual('a', document.GetCharAt(0)); |
||||
Assert.AreEqual('\r', document.GetCharAt(1)); |
||||
Assert.AreEqual('\n', document.GetCharAt(2)); |
||||
Assert.AreEqual('b', document.GetCharAt(3)); |
||||
} |
||||
|
||||
[Test] |
||||
public void CheckMixedNewLineTest() |
||||
{ |
||||
const string mixedNewlineText = "line 1\nline 2\r\nline 3\rline 4"; |
||||
document.Text = mixedNewlineText; |
||||
Assert.AreEqual(mixedNewlineText, document.Text); |
||||
Assert.AreEqual(4, document.LineCount); |
||||
for (int i = 1; i < 4; i++) { |
||||
DocumentLine line = document.GetLineByNumber(i); |
||||
Assert.AreEqual(i, line.LineNumber); |
||||
Assert.AreEqual("line " + i, document.GetText(line)); |
||||
} |
||||
Assert.AreEqual(1, document.GetLineByNumber(1).DelimiterLength); |
||||
Assert.AreEqual(2, document.GetLineByNumber(2).DelimiterLength); |
||||
Assert.AreEqual(1, document.GetLineByNumber(3).DelimiterLength); |
||||
Assert.AreEqual(0, document.GetLineByNumber(4).DelimiterLength); |
||||
} |
||||
|
||||
[Test] |
||||
public void LfCrIsTwoNewLinesTest() |
||||
{ |
||||
document.Text = "a\n\rb"; |
||||
Assert.AreEqual("a\n\rb", document.Text); |
||||
CheckDocumentLines("a", |
||||
"", |
||||
"b"); |
||||
} |
||||
|
||||
[Test] |
||||
public void RemoveFirstPartOfDelimiter() |
||||
{ |
||||
document.Text = "a\r\nb"; |
||||
document.Remove(1, 1); |
||||
Assert.AreEqual("a\nb", document.Text); |
||||
CheckDocumentLines("a", |
||||
"b"); |
||||
} |
||||
|
||||
[Test] |
||||
public void RemoveLineContentAndJoinDelimiters() |
||||
{ |
||||
document.Text = "a\rb\nc"; |
||||
document.Remove(2, 1); |
||||
Assert.AreEqual("a\r\nc", document.Text); |
||||
CheckDocumentLines("a", |
||||
"c"); |
||||
} |
||||
|
||||
[Test] |
||||
public void RemoveLineContentAndJoinDelimiters2() |
||||
{ |
||||
document.Text = "a\rb\nc\nd"; |
||||
document.Remove(2, 3); |
||||
Assert.AreEqual("a\r\nd", document.Text); |
||||
CheckDocumentLines("a", |
||||
"d"); |
||||
} |
||||
|
||||
[Test] |
||||
public void RemoveLineContentAndJoinDelimiters3() |
||||
{ |
||||
document.Text = "a\rb\r\nc"; |
||||
document.Remove(2, 2); |
||||
Assert.AreEqual("a\r\nc", document.Text); |
||||
CheckDocumentLines("a", |
||||
"c"); |
||||
} |
||||
|
||||
[Test] |
||||
public void RemoveLineContentAndJoinNonMatchingDelimiters() |
||||
{ |
||||
document.Text = "a\nb\nc"; |
||||
document.Remove(2, 1); |
||||
Assert.AreEqual("a\n\nc", document.Text); |
||||
CheckDocumentLines("a", |
||||
"", |
||||
"c"); |
||||
} |
||||
|
||||
[Test] |
||||
public void RemoveLineContentAndJoinNonMatchingDelimiters2() |
||||
{ |
||||
document.Text = "a\nb\rc"; |
||||
document.Remove(2, 1); |
||||
Assert.AreEqual("a\n\rc", document.Text); |
||||
CheckDocumentLines("a", |
||||
"", |
||||
"c"); |
||||
} |
||||
|
||||
[Test] |
||||
public void RemoveMultilineUpToFirstPartOfDelimiter() |
||||
{ |
||||
document.Text = "0\n1\r\n2"; |
||||
document.Remove(1, 3); |
||||
Assert.AreEqual("0\n2", document.Text); |
||||
CheckDocumentLines("0", |
||||
"2"); |
||||
} |
||||
|
||||
[Test] |
||||
public void RemoveSecondPartOfDelimiter() |
||||
{ |
||||
document.Text = "a\r\nb"; |
||||
document.Remove(2, 1); |
||||
Assert.AreEqual("a\rb", document.Text); |
||||
CheckDocumentLines("a", |
||||
"b"); |
||||
} |
||||
|
||||
[Test] |
||||
public void RemoveFromSecondPartOfDelimiter() |
||||
{ |
||||
document.Text = "a\r\nb\nc"; |
||||
document.Remove(2, 3); |
||||
Assert.AreEqual("a\rc", document.Text); |
||||
CheckDocumentLines("a", |
||||
"c"); |
||||
} |
||||
|
||||
[Test] |
||||
public void RemoveFromSecondPartOfDelimiterToDocumentEnd() |
||||
{ |
||||
document.Text = "a\r\nb"; |
||||
document.Remove(2, 2); |
||||
Assert.AreEqual("a\r", document.Text); |
||||
CheckDocumentLines("a", |
||||
""); |
||||
} |
||||
|
||||
[Test] |
||||
public void RemoveUpToMatchingDelimiter1() |
||||
{ |
||||
document.Text = "a\r\nb\nc"; |
||||
document.Remove(2, 2); |
||||
Assert.AreEqual("a\r\nc", document.Text); |
||||
CheckDocumentLines("a", |
||||
"c"); |
||||
} |
||||
|
||||
[Test] |
||||
public void RemoveUpToMatchingDelimiter2() |
||||
{ |
||||
document.Text = "a\r\nb\r\nc"; |
||||
document.Remove(2, 3); |
||||
Assert.AreEqual("a\r\nc", document.Text); |
||||
CheckDocumentLines("a", |
||||
"c"); |
||||
} |
||||
|
||||
[Test] |
||||
public void RemoveUpToNonMatchingDelimiter() |
||||
{ |
||||
document.Text = "a\r\nb\rc"; |
||||
document.Remove(2, 2); |
||||
Assert.AreEqual("a\r\rc", document.Text); |
||||
CheckDocumentLines("a", |
||||
"", |
||||
"c"); |
||||
} |
||||
|
||||
[Test] |
||||
public void RemoveTwoCharDelimiter() |
||||
{ |
||||
document.Text = "a\r\nb"; |
||||
document.Remove(1, 2); |
||||
Assert.AreEqual("ab", document.Text); |
||||
CheckDocumentLines("ab"); |
||||
} |
||||
|
||||
[Test] |
||||
public void RemoveOneCharDelimiter() |
||||
{ |
||||
document.Text = "a\nb"; |
||||
document.Remove(1, 1); |
||||
Assert.AreEqual("ab", document.Text); |
||||
CheckDocumentLines("ab"); |
||||
} |
||||
|
||||
void CheckDocumentLines(params string[] lines) |
||||
{ |
||||
Assert.AreEqual(lines.Length, document.LineCount, "LineCount"); |
||||
for (int i = 0; i < lines.Length; i++) { |
||||
Assert.AreEqual(lines[i], document.GetText(document.Lines[i]), "Text of line " + (i + 1)); |
||||
} |
||||
} |
||||
|
||||
[Test] |
||||
public void FixUpFirstPartOfDelimiter() |
||||
{ |
||||
document.Text = "a\n\nb"; |
||||
document.Replace(1, 1, "\r"); |
||||
Assert.AreEqual("a\r\nb", document.Text); |
||||
CheckDocumentLines("a", |
||||
"b"); |
||||
} |
||||
|
||||
[Test] |
||||
public void FixUpSecondPartOfDelimiter() |
||||
{ |
||||
document.Text = "a\r\rb"; |
||||
document.Replace(2, 1, "\n"); |
||||
Assert.AreEqual("a\r\nb", document.Text); |
||||
CheckDocumentLines("a", |
||||
"b"); |
||||
} |
||||
|
||||
[Test] |
||||
public void InsertInsideDelimiter() |
||||
{ |
||||
document.Text = "a\r\nc"; |
||||
document.Insert(2, "b"); |
||||
Assert.AreEqual("a\rb\nc", document.Text); |
||||
CheckDocumentLines("a", |
||||
"b", |
||||
"c"); |
||||
} |
||||
|
||||
[Test] |
||||
public void InsertInsideDelimiter2() |
||||
{ |
||||
document.Text = "a\r\nd"; |
||||
document.Insert(2, "b\nc"); |
||||
Assert.AreEqual("a\rb\nc\nd", document.Text); |
||||
CheckDocumentLines("a", |
||||
"b", |
||||
"c", |
||||
"d"); |
||||
} |
||||
|
||||
[Test] |
||||
public void InsertInsideDelimiter3() |
||||
{ |
||||
document.Text = "a\r\nc"; |
||||
document.Insert(2, "b\r"); |
||||
Assert.AreEqual("a\rb\r\nc", document.Text); |
||||
CheckDocumentLines("a", |
||||
"b", |
||||
"c"); |
||||
} |
||||
|
||||
[Test] |
||||
public void ExtendDelimiter1() |
||||
{ |
||||
document.Text = "a\nc"; |
||||
document.Insert(1, "b\r"); |
||||
Assert.AreEqual("ab\r\nc", document.Text); |
||||
CheckDocumentLines("ab", |
||||
"c"); |
||||
} |
||||
|
||||
[Test] |
||||
public void ExtendDelimiter2() |
||||
{ |
||||
document.Text = "a\rc"; |
||||
document.Insert(2, "\nb"); |
||||
Assert.AreEqual("a\r\nbc", document.Text); |
||||
CheckDocumentLines("a", |
||||
"bc"); |
||||
} |
||||
|
||||
[Test] |
||||
public void ReplaceLineContentBetweenMatchingDelimiters() |
||||
{ |
||||
document.Text = "a\rb\nc"; |
||||
document.Replace(2, 1, "x"); |
||||
Assert.AreEqual("a\rx\nc", document.Text); |
||||
CheckDocumentLines("a", |
||||
"x", |
||||
"c"); |
||||
} |
||||
|
||||
[Test] |
||||
public void GetOffset() |
||||
{ |
||||
document.Text = "Hello,\nWorld!"; |
||||
Assert.AreEqual(0, document.GetOffset(1, 1)); |
||||
Assert.AreEqual(1, document.GetOffset(1, 2)); |
||||
Assert.AreEqual(5, document.GetOffset(1, 6)); |
||||
Assert.AreEqual(6, document.GetOffset(1, 7)); |
||||
Assert.AreEqual(7, document.GetOffset(2, 1)); |
||||
Assert.AreEqual(8, document.GetOffset(2, 2)); |
||||
Assert.AreEqual(12, document.GetOffset(2, 6)); |
||||
Assert.AreEqual(13, document.GetOffset(2, 7)); |
||||
} |
||||
|
||||
[Test] |
||||
public void GetOffsetIgnoreNegativeColumns() |
||||
{ |
||||
document.Text = "Hello,\nWorld!"; |
||||
Assert.AreEqual(0, document.GetOffset(1, -1)); |
||||
Assert.AreEqual(0, document.GetOffset(1, -100)); |
||||
Assert.AreEqual(0, document.GetOffset(1, 0)); |
||||
Assert.AreEqual(7, document.GetOffset(2, -1)); |
||||
Assert.AreEqual(7, document.GetOffset(2, -100)); |
||||
Assert.AreEqual(7, document.GetOffset(2, 0)); |
||||
} |
||||
|
||||
[Test] |
||||
public void GetOffsetIgnoreTooHighColumns() |
||||
{ |
||||
document.Text = "Hello,\nWorld!"; |
||||
Assert.AreEqual(6, document.GetOffset(1, 8)); |
||||
Assert.AreEqual(6, document.GetOffset(1, 100)); |
||||
Assert.AreEqual(13, document.GetOffset(2, 8)); |
||||
Assert.AreEqual(13, document.GetOffset(2, 100)); |
||||
} |
||||
} |
||||
} |
@ -1,183 +0,0 @@
@@ -1,183 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using ICSharpCode.AvalonEdit.Rendering; |
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
/// <summary>
|
||||
/// A randomized test for the line manager.
|
||||
/// </summary>
|
||||
[TestFixture] |
||||
public class RandomizedLineManagerTest |
||||
{ |
||||
TextDocument document; |
||||
Random rnd; |
||||
|
||||
[TestFixtureSetUp] |
||||
public void FixtureSetup() |
||||
{ |
||||
int seed = Environment.TickCount; |
||||
Console.WriteLine("RandomizedLineManagerTest Seed: " + seed); |
||||
rnd = new Random(seed); |
||||
} |
||||
|
||||
[SetUp] |
||||
public void Setup() |
||||
{ |
||||
document = new TextDocument(); |
||||
} |
||||
|
||||
[Test] |
||||
public void ShortReplacements() |
||||
{ |
||||
char[] chars = { 'a', 'b', '\r', '\n' }; |
||||
char[] buffer = new char[20]; |
||||
for (int i = 0; i < 2500; i++) { |
||||
int offset = rnd.Next(0, document.TextLength); |
||||
int length = rnd.Next(0, document.TextLength - offset); |
||||
int newTextLength = rnd.Next(0, 20); |
||||
for (int j = 0; j < newTextLength; j++) { |
||||
buffer[j] = chars[rnd.Next(0, chars.Length)]; |
||||
} |
||||
|
||||
document.Replace(offset, length, new string(buffer, 0, newTextLength)); |
||||
CheckLines(); |
||||
} |
||||
} |
||||
|
||||
[Test] |
||||
public void LargeReplacements() |
||||
{ |
||||
char[] chars = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', '\r', '\n' }; |
||||
char[] buffer = new char[1000]; |
||||
for (int i = 0; i < 20; i++) { |
||||
int offset = rnd.Next(0, document.TextLength); |
||||
int length = rnd.Next(0, (document.TextLength - offset) / 4); |
||||
int newTextLength = rnd.Next(0, 1000); |
||||
for (int j = 0; j < newTextLength; j++) { |
||||
buffer[j] = chars[rnd.Next(0, chars.Length)]; |
||||
} |
||||
|
||||
string newText = new string(buffer, 0, newTextLength); |
||||
string expectedText = document.Text.Remove(offset, length).Insert(offset, newText); |
||||
document.Replace(offset, length, newText); |
||||
Assert.AreEqual(expectedText, document.Text); |
||||
CheckLines(); |
||||
} |
||||
} |
||||
|
||||
void CheckLines() |
||||
{ |
||||
string text = document.Text; |
||||
int lineNumber = 1; |
||||
int lineStart = 0; |
||||
for (int i = 0; i < text.Length; i++) { |
||||
char c = text[i]; |
||||
if (c == '\r' && i + 1 < text.Length && text[i + 1] == '\n') { |
||||
DocumentLine line = document.GetLineByNumber(lineNumber); |
||||
Assert.AreEqual(lineNumber, line.LineNumber); |
||||
Assert.AreEqual(2, line.DelimiterLength); |
||||
Assert.AreEqual(lineStart, line.Offset); |
||||
Assert.AreEqual(i - lineStart, line.Length); |
||||
i++; // consume \n
|
||||
lineNumber++; |
||||
lineStart = i+1; |
||||
} else if (c == '\r' || c == '\n') { |
||||
DocumentLine line = document.GetLineByNumber(lineNumber); |
||||
Assert.AreEqual(lineNumber, line.LineNumber); |
||||
Assert.AreEqual(1, line.DelimiterLength); |
||||
Assert.AreEqual(lineStart, line.Offset); |
||||
Assert.AreEqual(i - lineStart, line.Length); |
||||
lineNumber++; |
||||
lineStart = i+1; |
||||
} |
||||
} |
||||
Assert.AreEqual(lineNumber, document.LineCount); |
||||
} |
||||
|
||||
[Test] |
||||
public void CollapsingTest() |
||||
{ |
||||
char[] chars = { 'a', 'b', '\r', '\n' }; |
||||
char[] buffer = new char[20]; |
||||
HeightTree heightTree = new HeightTree(document, 10); |
||||
List<CollapsedLineSection> collapsedSections = new List<CollapsedLineSection>(); |
||||
for (int i = 0; i < 2500; i++) { |
||||
// Console.WriteLine("Iteration " + i);
|
||||
// Console.WriteLine(heightTree.GetTreeAsString());
|
||||
// foreach (CollapsedLineSection cs in collapsedSections) {
|
||||
// Console.WriteLine(cs);
|
||||
// }
|
||||
|
||||
switch (rnd.Next(0, 10)) { |
||||
case 0: |
||||
case 1: |
||||
case 2: |
||||
case 3: |
||||
case 4: |
||||
case 5: |
||||
int offset = rnd.Next(0, document.TextLength); |
||||
int length = rnd.Next(0, document.TextLength - offset); |
||||
int newTextLength = rnd.Next(0, 20); |
||||
for (int j = 0; j < newTextLength; j++) { |
||||
buffer[j] = chars[rnd.Next(0, chars.Length)]; |
||||
} |
||||
|
||||
document.Replace(offset, length, new string(buffer, 0, newTextLength)); |
||||
break; |
||||
case 6: |
||||
case 7: |
||||
int startLine = rnd.Next(1, document.LineCount + 1); |
||||
int endLine = rnd.Next(startLine, document.LineCount + 1); |
||||
collapsedSections.Add(heightTree.CollapseText(document.GetLineByNumber(startLine), document.GetLineByNumber(endLine))); |
||||
break; |
||||
case 8: |
||||
if (collapsedSections.Count > 0) { |
||||
CollapsedLineSection cs = collapsedSections[rnd.Next(0, collapsedSections.Count)]; |
||||
// unless the text section containing the CollapsedSection was deleted:
|
||||
if (cs.Start != null) { |
||||
cs.Uncollapse(); |
||||
} |
||||
collapsedSections.Remove(cs); |
||||
} |
||||
break; |
||||
case 9: |
||||
foreach (DocumentLine ls in document.Lines) { |
||||
heightTree.SetHeight(ls, ls.LineNumber); |
||||
} |
||||
break; |
||||
} |
||||
var treeSections = new HashSet<CollapsedLineSection>(heightTree.GetAllCollapsedSections()); |
||||
int expectedCount = 0; |
||||
foreach (CollapsedLineSection cs in collapsedSections) { |
||||
if (cs.Start != null) { |
||||
expectedCount++; |
||||
Assert.IsTrue(treeSections.Contains(cs)); |
||||
} |
||||
} |
||||
Assert.AreEqual(expectedCount, treeSections.Count); |
||||
CheckLines(); |
||||
HeightTests.CheckHeights(document, heightTree); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,331 +0,0 @@
@@ -1,331 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory.Editor; |
||||
#endif
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
[TestFixture] |
||||
public class TextAnchorTest |
||||
{ |
||||
TextDocument document; |
||||
|
||||
[SetUp] |
||||
public void SetUp() |
||||
{ |
||||
document = new TextDocument(); |
||||
} |
||||
|
||||
[Test] |
||||
public void AnchorInEmptyDocument() |
||||
{ |
||||
TextAnchor a1 = document.CreateAnchor(0); |
||||
TextAnchor a2 = document.CreateAnchor(0); |
||||
a1.MovementType = AnchorMovementType.BeforeInsertion; |
||||
a2.MovementType = AnchorMovementType.AfterInsertion; |
||||
Assert.AreEqual(0, a1.Offset); |
||||
Assert.AreEqual(0, a2.Offset); |
||||
document.Insert(0, "x"); |
||||
Assert.AreEqual(0, a1.Offset); |
||||
Assert.AreEqual(1, a2.Offset); |
||||
} |
||||
|
||||
[Test] |
||||
public void AnchorsSurviveDeletion() |
||||
{ |
||||
document.Text = new string(' ', 10); |
||||
TextAnchor[] a1 = new TextAnchor[11]; |
||||
TextAnchor[] a2 = new TextAnchor[11]; |
||||
for (int i = 0; i < 11; i++) { |
||||
//Console.WriteLine("Insert first at i = " + i);
|
||||
a1[i] = document.CreateAnchor(i); |
||||
a1[i].SurviveDeletion = true; |
||||
//Console.WriteLine(document.GetTextAnchorTreeAsString());
|
||||
//Console.WriteLine("Insert second at i = " + i);
|
||||
a2[i] = document.CreateAnchor(i); |
||||
a2[i].SurviveDeletion = false; |
||||
//Console.WriteLine(document.GetTextAnchorTreeAsString());
|
||||
} |
||||
for (int i = 0; i < 11; i++) { |
||||
Assert.AreEqual(i, a1[i].Offset); |
||||
Assert.AreEqual(i, a2[i].Offset); |
||||
} |
||||
document.Remove(1, 8); |
||||
for (int i = 0; i < 11; i++) { |
||||
if (i <= 1) { |
||||
Assert.IsFalse(a1[i].IsDeleted); |
||||
Assert.IsFalse(a2[i].IsDeleted); |
||||
Assert.AreEqual(i, a1[i].Offset); |
||||
Assert.AreEqual(i, a2[i].Offset); |
||||
} else if (i <= 8) { |
||||
Assert.IsFalse(a1[i].IsDeleted); |
||||
Assert.IsTrue(a2[i].IsDeleted); |
||||
Assert.AreEqual(1, a1[i].Offset); |
||||
} else { |
||||
Assert.IsFalse(a1[i].IsDeleted); |
||||
Assert.IsFalse(a2[i].IsDeleted); |
||||
Assert.AreEqual(i - 8, a1[i].Offset); |
||||
Assert.AreEqual(i - 8, a2[i].Offset); |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
Random rnd; |
||||
|
||||
[TestFixtureSetUp] |
||||
public void FixtureSetup() |
||||
{ |
||||
int seed = Environment.TickCount; |
||||
Console.WriteLine("TextAnchorTest Seed: " + seed); |
||||
rnd = new Random(seed); |
||||
} |
||||
|
||||
[Test] |
||||
public void CreateAnchors() |
||||
{ |
||||
List<TextAnchor> anchors = new List<TextAnchor>(); |
||||
List<int> expectedOffsets = new List<int>(); |
||||
document.Text = new string(' ', 1000); |
||||
for (int i = 0; i < 1000; i++) { |
||||
int offset = rnd.Next(1000); |
||||
anchors.Add(document.CreateAnchor(offset)); |
||||
expectedOffsets.Add(offset); |
||||
} |
||||
for (int i = 0; i < anchors.Count; i++) { |
||||
Assert.AreEqual(expectedOffsets[i], anchors[i].Offset); |
||||
} |
||||
GC.KeepAlive(anchors); |
||||
} |
||||
|
||||
[Test] |
||||
public void CreateAndGCAnchors() |
||||
{ |
||||
List<TextAnchor> anchors = new List<TextAnchor>(); |
||||
List<int> expectedOffsets = new List<int>(); |
||||
document.Text = new string(' ', 1000); |
||||
for (int t = 0; t < 250; t++) { |
||||
int c = rnd.Next(50); |
||||
if (rnd.Next(2) == 0) { |
||||
for (int i = 0; i < c; i++) { |
||||
int offset = rnd.Next(1000); |
||||
anchors.Add(document.CreateAnchor(offset)); |
||||
expectedOffsets.Add(offset); |
||||
} |
||||
} else if (c <= anchors.Count) { |
||||
anchors.RemoveRange(0, c); |
||||
expectedOffsets.RemoveRange(0, c); |
||||
GC.Collect(); |
||||
} |
||||
for (int j = 0; j < anchors.Count; j++) { |
||||
Assert.AreEqual(expectedOffsets[j], anchors[j].Offset); |
||||
} |
||||
} |
||||
GC.KeepAlive(anchors); |
||||
} |
||||
|
||||
[Test] |
||||
public void MoveAnchorsDuringReplace() |
||||
{ |
||||
document.Text = "abcd"; |
||||
TextAnchor start = document.CreateAnchor(1); |
||||
TextAnchor middleDeletable = document.CreateAnchor(2); |
||||
TextAnchor middleSurvivorLeft = document.CreateAnchor(2); |
||||
middleSurvivorLeft.SurviveDeletion = true; |
||||
middleSurvivorLeft.MovementType = AnchorMovementType.BeforeInsertion; |
||||
TextAnchor middleSurvivorRight = document.CreateAnchor(2); |
||||
middleSurvivorRight.SurviveDeletion = true; |
||||
middleSurvivorRight.MovementType = AnchorMovementType.AfterInsertion; |
||||
TextAnchor end = document.CreateAnchor(3); |
||||
document.Replace(1, 2, "BxC"); |
||||
|
||||
Assert.AreEqual(1, start.Offset); |
||||
Assert.IsTrue(middleDeletable.IsDeleted); |
||||
Assert.AreEqual(1, middleSurvivorLeft.Offset); |
||||
Assert.AreEqual(4, middleSurvivorRight.Offset); |
||||
Assert.AreEqual(4, end.Offset); |
||||
} |
||||
|
||||
[Test] |
||||
public void CreateAndMoveAnchors() |
||||
{ |
||||
List<TextAnchor> anchors = new List<TextAnchor>(); |
||||
List<int> expectedOffsets = new List<int>(); |
||||
document.Text = new string(' ', 1000); |
||||
for (int t = 0; t < 250; t++) { |
||||
//Console.Write("t = " + t + " ");
|
||||
int c = rnd.Next(50); |
||||
switch (rnd.Next(5)) { |
||||
case 0: |
||||
//Console.WriteLine("Add c=" + c + " anchors");
|
||||
for (int i = 0; i < c; i++) { |
||||
int offset = rnd.Next(document.TextLength); |
||||
TextAnchor anchor = document.CreateAnchor(offset); |
||||
if (rnd.Next(2) == 0) |
||||
anchor.MovementType = AnchorMovementType.BeforeInsertion; |
||||
else |
||||
anchor.MovementType = AnchorMovementType.AfterInsertion; |
||||
anchor.SurviveDeletion = rnd.Next(2) == 0; |
||||
anchors.Add(anchor); |
||||
expectedOffsets.Add(offset); |
||||
} |
||||
break; |
||||
case 1: |
||||
if (c <= anchors.Count) { |
||||
//Console.WriteLine("Remove c=" + c + " anchors");
|
||||
anchors.RemoveRange(0, c); |
||||
expectedOffsets.RemoveRange(0, c); |
||||
GC.Collect(); |
||||
} |
||||
break; |
||||
case 2: |
||||
int insertOffset = rnd.Next(document.TextLength); |
||||
int insertLength = rnd.Next(1000); |
||||
//Console.WriteLine("insertOffset=" + insertOffset + " insertLength="+insertLength);
|
||||
document.Insert(insertOffset, new string(' ', insertLength)); |
||||
for (int i = 0; i < anchors.Count; i++) { |
||||
if (anchors[i].MovementType == AnchorMovementType.BeforeInsertion) { |
||||
if (expectedOffsets[i] > insertOffset) |
||||
expectedOffsets[i] += insertLength; |
||||
} else { |
||||
if (expectedOffsets[i] >= insertOffset) |
||||
expectedOffsets[i] += insertLength; |
||||
} |
||||
} |
||||
break; |
||||
case 3: |
||||
int removalOffset = rnd.Next(document.TextLength); |
||||
int removalLength = rnd.Next(document.TextLength - removalOffset); |
||||
//Console.WriteLine("RemovalOffset=" + removalOffset + " RemovalLength="+removalLength);
|
||||
document.Remove(removalOffset, removalLength); |
||||
for (int i = anchors.Count - 1; i >= 0; i--) { |
||||
if (expectedOffsets[i] > removalOffset && expectedOffsets[i] < removalOffset + removalLength) { |
||||
if (anchors[i].SurviveDeletion) { |
||||
expectedOffsets[i] = removalOffset; |
||||
} else { |
||||
Assert.IsTrue(anchors[i].IsDeleted); |
||||
anchors.RemoveAt(i); |
||||
expectedOffsets.RemoveAt(i); |
||||
} |
||||
} else if (expectedOffsets[i] > removalOffset) { |
||||
expectedOffsets[i] -= removalLength; |
||||
} |
||||
} |
||||
break; |
||||
case 4: |
||||
int replaceOffset = rnd.Next(document.TextLength); |
||||
int replaceRemovalLength = rnd.Next(document.TextLength - replaceOffset); |
||||
int replaceInsertLength = rnd.Next(1000); |
||||
//Console.WriteLine("ReplaceOffset=" + replaceOffset + " RemovalLength="+replaceRemovalLength + " InsertLength=" + replaceInsertLength);
|
||||
document.Replace(replaceOffset, replaceRemovalLength, new string(' ', replaceInsertLength)); |
||||
for (int i = anchors.Count - 1; i >= 0; i--) { |
||||
if (expectedOffsets[i] > replaceOffset && expectedOffsets[i] < replaceOffset + replaceRemovalLength) { |
||||
if (anchors[i].SurviveDeletion) { |
||||
if (anchors[i].MovementType == AnchorMovementType.AfterInsertion) |
||||
expectedOffsets[i] = replaceOffset + replaceInsertLength; |
||||
else |
||||
expectedOffsets[i] = replaceOffset; |
||||
} else { |
||||
Assert.IsTrue(anchors[i].IsDeleted); |
||||
anchors.RemoveAt(i); |
||||
expectedOffsets.RemoveAt(i); |
||||
} |
||||
} else if (expectedOffsets[i] > replaceOffset) { |
||||
expectedOffsets[i] += replaceInsertLength - replaceRemovalLength; |
||||
} else if (expectedOffsets[i] == replaceOffset && replaceRemovalLength == 0 && anchors[i].MovementType == AnchorMovementType.AfterInsertion) { |
||||
expectedOffsets[i] += replaceInsertLength - replaceRemovalLength; |
||||
} |
||||
} |
||||
break; |
||||
} |
||||
Assert.AreEqual(anchors.Count, expectedOffsets.Count); |
||||
for (int j = 0; j < anchors.Count; j++) { |
||||
Assert.AreEqual(expectedOffsets[j], anchors[j].Offset); |
||||
} |
||||
} |
||||
GC.KeepAlive(anchors); |
||||
} |
||||
|
||||
[Test] |
||||
public void RepeatedTextDragDrop() |
||||
{ |
||||
document.Text = new string(' ', 1000); |
||||
for (int i = 0; i < 20; i++) { |
||||
TextAnchor a = document.CreateAnchor(144); |
||||
TextAnchor b = document.CreateAnchor(157); |
||||
document.Insert(128, new string('a', 13)); |
||||
document.Remove(157, 13); |
||||
a = document.CreateAnchor(128); |
||||
b = document.CreateAnchor(141); |
||||
|
||||
document.Insert(157, new string('b', 13)); |
||||
document.Remove(128, 13); |
||||
|
||||
a = null; |
||||
b = null; |
||||
if ((i % 5) == 0) |
||||
GC.Collect(); |
||||
} |
||||
} |
||||
|
||||
[Test] |
||||
public void ReplaceSpacesWithTab() |
||||
{ |
||||
document.Text = "a b"; |
||||
TextAnchor before = document.CreateAnchor(1); |
||||
before.MovementType = AnchorMovementType.AfterInsertion; |
||||
TextAnchor after = document.CreateAnchor(5); |
||||
TextAnchor survivingMiddle = document.CreateAnchor(2); |
||||
TextAnchor deletedMiddle = document.CreateAnchor(3); |
||||
|
||||
document.Replace(1, 4, "\t", OffsetChangeMappingType.CharacterReplace); |
||||
Assert.AreEqual("a\tb", document.Text); |
||||
// yes, the movement is a bit strange; but that's how CharacterReplace works when the text gets shorter
|
||||
Assert.AreEqual(1, before.Offset); |
||||
Assert.AreEqual(2, after.Offset); |
||||
Assert.AreEqual(2, survivingMiddle.Offset); |
||||
Assert.AreEqual(2, deletedMiddle.Offset); |
||||
} |
||||
|
||||
[Test] |
||||
public void ReplaceTwoCharactersWithThree() |
||||
{ |
||||
document.Text = "a12b"; |
||||
TextAnchor before = document.CreateAnchor(1); |
||||
before.MovementType = AnchorMovementType.AfterInsertion; |
||||
TextAnchor after = document.CreateAnchor(3); |
||||
before.MovementType = AnchorMovementType.BeforeInsertion; |
||||
TextAnchor middleB = document.CreateAnchor(2); |
||||
before.MovementType = AnchorMovementType.BeforeInsertion; |
||||
TextAnchor middleA = document.CreateAnchor(2); |
||||
before.MovementType = AnchorMovementType.AfterInsertion; |
||||
|
||||
document.Replace(1, 2, "123", OffsetChangeMappingType.CharacterReplace); |
||||
Assert.AreEqual("a123b", document.Text); |
||||
Assert.AreEqual(1, before.Offset); |
||||
Assert.AreEqual(4, after.Offset); |
||||
Assert.AreEqual(2, middleA.Offset); |
||||
Assert.AreEqual(2, middleB.Offset); |
||||
} |
||||
} |
||||
} |
@ -1,368 +0,0 @@
@@ -1,368 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory.Editor; |
||||
#endif
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
[TestFixture] |
||||
public class TextSegmentTreeTest |
||||
{ |
||||
Random rnd; |
||||
|
||||
[TestFixtureSetUp] |
||||
public void FixtureSetup() |
||||
{ |
||||
int seed = Environment.TickCount; |
||||
Console.WriteLine("TextSegmentTreeTest Seed: " + seed); |
||||
rnd = new Random(seed); |
||||
} |
||||
|
||||
class TestTextSegment : TextSegment |
||||
{ |
||||
internal int ExpectedOffset, ExpectedLength; |
||||
|
||||
public TestTextSegment(int expectedOffset, int expectedLength) |
||||
{ |
||||
this.ExpectedOffset = expectedOffset; |
||||
this.ExpectedLength = expectedLength; |
||||
this.StartOffset = expectedOffset; |
||||
this.Length = expectedLength; |
||||
} |
||||
} |
||||
|
||||
TextSegmentCollection<TestTextSegment> tree; |
||||
List<TestTextSegment> expectedSegments; |
||||
|
||||
[SetUp] |
||||
public void SetUp() |
||||
{ |
||||
tree = new TextSegmentCollection<TestTextSegment>(); |
||||
expectedSegments = new List<TestTextSegment>(); |
||||
} |
||||
|
||||
[Test] |
||||
public void FindInEmptyTree() |
||||
{ |
||||
Assert.AreSame(null, tree.FindFirstSegmentWithStartAfter(0)); |
||||
Assert.AreEqual(0, tree.FindSegmentsContaining(0).Count); |
||||
Assert.AreEqual(0, tree.FindOverlappingSegments(10, 20).Count); |
||||
} |
||||
|
||||
[Test] |
||||
public void FindFirstSegmentWithStartAfter() |
||||
{ |
||||
var s1 = new TestTextSegment(5, 10); |
||||
var s2 = new TestTextSegment(10, 10); |
||||
tree.Add(s1); |
||||
tree.Add(s2); |
||||
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(-100)); |
||||
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(0)); |
||||
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(4)); |
||||
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(5)); |
||||
Assert.AreSame(s2, tree.FindFirstSegmentWithStartAfter(6)); |
||||
Assert.AreSame(s2, tree.FindFirstSegmentWithStartAfter(9)); |
||||
Assert.AreSame(s2, tree.FindFirstSegmentWithStartAfter(10)); |
||||
Assert.AreSame(null, tree.FindFirstSegmentWithStartAfter(11)); |
||||
Assert.AreSame(null, tree.FindFirstSegmentWithStartAfter(100)); |
||||
} |
||||
|
||||
[Test] |
||||
public void FindFirstSegmentWithStartAfterWithDuplicates() |
||||
{ |
||||
var s1 = new TestTextSegment(5, 10); |
||||
var s1b = new TestTextSegment(5, 7); |
||||
var s2 = new TestTextSegment(10, 10); |
||||
var s2b = new TestTextSegment(10, 7); |
||||
tree.Add(s1); |
||||
tree.Add(s1b); |
||||
tree.Add(s2); |
||||
tree.Add(s2b); |
||||
Assert.AreSame(s1b, tree.GetNextSegment(s1)); |
||||
Assert.AreSame(s2b, tree.GetNextSegment(s2)); |
||||
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(-100)); |
||||
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(0)); |
||||
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(4)); |
||||
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(5)); |
||||
Assert.AreSame(s2, tree.FindFirstSegmentWithStartAfter(6)); |
||||
Assert.AreSame(s2, tree.FindFirstSegmentWithStartAfter(9)); |
||||
Assert.AreSame(s2, tree.FindFirstSegmentWithStartAfter(10)); |
||||
Assert.AreSame(null, tree.FindFirstSegmentWithStartAfter(11)); |
||||
Assert.AreSame(null, tree.FindFirstSegmentWithStartAfter(100)); |
||||
} |
||||
|
||||
[Test] |
||||
public void FindFirstSegmentWithStartAfterWithDuplicates2() |
||||
{ |
||||
var s1 = new TestTextSegment(5, 1); |
||||
var s2 = new TestTextSegment(5, 2); |
||||
var s3 = new TestTextSegment(5, 3); |
||||
var s4 = new TestTextSegment(5, 4); |
||||
tree.Add(s1); |
||||
tree.Add(s2); |
||||
tree.Add(s3); |
||||
tree.Add(s4); |
||||
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(0)); |
||||
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(1)); |
||||
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(4)); |
||||
Assert.AreSame(s1, tree.FindFirstSegmentWithStartAfter(5)); |
||||
Assert.AreSame(null, tree.FindFirstSegmentWithStartAfter(6)); |
||||
} |
||||
|
||||
TestTextSegment AddSegment(int offset, int length) |
||||
{ |
||||
// Console.WriteLine("Add " + offset + ", " + length);
|
||||
TestTextSegment s = new TestTextSegment(offset, length); |
||||
tree.Add(s); |
||||
expectedSegments.Add(s); |
||||
return s; |
||||
} |
||||
|
||||
void RemoveSegment(TestTextSegment s) |
||||
{ |
||||
// Console.WriteLine("Remove " + s);
|
||||
expectedSegments.Remove(s); |
||||
tree.Remove(s); |
||||
} |
||||
|
||||
void TestRetrieval(int offset, int length) |
||||
{ |
||||
HashSet<TestTextSegment> actual = new HashSet<TestTextSegment>(tree.FindOverlappingSegments(offset, length)); |
||||
HashSet<TestTextSegment> expected = new HashSet<TestTextSegment>(); |
||||
foreach (TestTextSegment e in expectedSegments) { |
||||
if (e.ExpectedOffset + e.ExpectedLength < offset) |
||||
continue; |
||||
if (e.ExpectedOffset > offset + length) |
||||
continue; |
||||
expected.Add(e); |
||||
} |
||||
Assert.IsTrue(actual.IsSubsetOf(expected)); |
||||
Assert.IsTrue(expected.IsSubsetOf(actual)); |
||||
} |
||||
|
||||
void CheckSegments() |
||||
{ |
||||
Assert.AreEqual(expectedSegments.Count, tree.Count); |
||||
foreach (TestTextSegment s in expectedSegments) { |
||||
Assert.AreEqual(s.ExpectedOffset, s.StartOffset /*, "startoffset for " + s*/); |
||||
Assert.AreEqual(s.ExpectedLength, s.Length /*, "length for " + s*/); |
||||
} |
||||
} |
||||
|
||||
[Test] |
||||
public void AddSegments() |
||||
{ |
||||
TestTextSegment s1 = AddSegment(10, 20); |
||||
TestTextSegment s2 = AddSegment(15, 10); |
||||
CheckSegments(); |
||||
} |
||||
|
||||
void ChangeDocument(OffsetChangeMapEntry change) |
||||
{ |
||||
tree.UpdateOffsets(change); |
||||
foreach (TestTextSegment s in expectedSegments) { |
||||
int endOffset = s.ExpectedOffset + s.ExpectedLength; |
||||
s.ExpectedOffset = change.GetNewOffset(s.ExpectedOffset, AnchorMovementType.AfterInsertion); |
||||
s.ExpectedLength = Math.Max(0, change.GetNewOffset(endOffset, AnchorMovementType.BeforeInsertion) - s.ExpectedOffset); |
||||
} |
||||
} |
||||
|
||||
[Test] |
||||
public void InsertionBeforeAllSegments() |
||||
{ |
||||
TestTextSegment s1 = AddSegment(10, 20); |
||||
TestTextSegment s2 = AddSegment(15, 10); |
||||
ChangeDocument(new OffsetChangeMapEntry(5, 0, 2)); |
||||
CheckSegments(); |
||||
} |
||||
|
||||
[Test] |
||||
public void ReplacementBeforeAllSegmentsTouchingFirstSegment() |
||||
{ |
||||
TestTextSegment s1 = AddSegment(10, 20); |
||||
TestTextSegment s2 = AddSegment(15, 10); |
||||
ChangeDocument(new OffsetChangeMapEntry(5, 5, 2)); |
||||
CheckSegments(); |
||||
} |
||||
|
||||
[Test] |
||||
public void InsertionAfterAllSegments() |
||||
{ |
||||
TestTextSegment s1 = AddSegment(10, 20); |
||||
TestTextSegment s2 = AddSegment(15, 10); |
||||
ChangeDocument(new OffsetChangeMapEntry(45, 0, 2)); |
||||
CheckSegments(); |
||||
} |
||||
|
||||
[Test] |
||||
public void ReplacementOverlappingWithStartOfSegment() |
||||
{ |
||||
TestTextSegment s1 = AddSegment(10, 20); |
||||
TestTextSegment s2 = AddSegment(15, 10); |
||||
ChangeDocument(new OffsetChangeMapEntry(9, 7, 2)); |
||||
CheckSegments(); |
||||
} |
||||
|
||||
[Test] |
||||
public void ReplacementOfWholeSegment() |
||||
{ |
||||
TestTextSegment s1 = AddSegment(10, 20); |
||||
TestTextSegment s2 = AddSegment(15, 10); |
||||
ChangeDocument(new OffsetChangeMapEntry(10, 20, 30)); |
||||
CheckSegments(); |
||||
} |
||||
|
||||
[Test] |
||||
public void ReplacementAtEndOfSegment() |
||||
{ |
||||
TestTextSegment s1 = AddSegment(10, 20); |
||||
TestTextSegment s2 = AddSegment(15, 10); |
||||
ChangeDocument(new OffsetChangeMapEntry(24, 6, 10)); |
||||
CheckSegments(); |
||||
} |
||||
|
||||
[Test] |
||||
public void RandomizedNoDocumentChanges() |
||||
{ |
||||
for (int i = 0; i < 1000; i++) { |
||||
// Console.WriteLine(tree.GetTreeAsString());
|
||||
// Console.WriteLine("Iteration " + i);
|
||||
|
||||
switch (rnd.Next(3)) { |
||||
case 0: |
||||
AddSegment(rnd.Next(500), rnd.Next(30)); |
||||
break; |
||||
case 1: |
||||
AddSegment(rnd.Next(500), rnd.Next(300)); |
||||
break; |
||||
case 2: |
||||
if (tree.Count > 0) { |
||||
RemoveSegment(expectedSegments[rnd.Next(tree.Count)]); |
||||
} |
||||
break; |
||||
} |
||||
CheckSegments(); |
||||
} |
||||
} |
||||
|
||||
[Test] |
||||
public void RandomizedCloseNoDocumentChanges() |
||||
{ |
||||
// Lots of segments in a short document. Tests how the tree copes with multiple identical segments.
|
||||
for (int i = 0; i < 1000; i++) { |
||||
switch (rnd.Next(3)) { |
||||
case 0: |
||||
AddSegment(rnd.Next(20), rnd.Next(10)); |
||||
break; |
||||
case 1: |
||||
AddSegment(rnd.Next(20), rnd.Next(20)); |
||||
break; |
||||
case 2: |
||||
if (tree.Count > 0) { |
||||
RemoveSegment(expectedSegments[rnd.Next(tree.Count)]); |
||||
} |
||||
break; |
||||
} |
||||
CheckSegments(); |
||||
} |
||||
} |
||||
|
||||
[Test] |
||||
public void RandomizedRetrievalTest() |
||||
{ |
||||
for (int i = 0; i < 1000; i++) { |
||||
AddSegment(rnd.Next(500), rnd.Next(300)); |
||||
} |
||||
CheckSegments(); |
||||
for (int i = 0; i < 1000; i++) { |
||||
TestRetrieval(rnd.Next(1000) - 100, rnd.Next(500)); |
||||
} |
||||
} |
||||
|
||||
[Test] |
||||
public void RandomizedWithDocumentChanges() |
||||
{ |
||||
for (int i = 0; i < 500; i++) { |
||||
// Console.WriteLine(tree.GetTreeAsString());
|
||||
// Console.WriteLine("Iteration " + i);
|
||||
|
||||
switch (rnd.Next(6)) { |
||||
case 0: |
||||
AddSegment(rnd.Next(500), rnd.Next(30)); |
||||
break; |
||||
case 1: |
||||
AddSegment(rnd.Next(500), rnd.Next(300)); |
||||
break; |
||||
case 2: |
||||
if (tree.Count > 0) { |
||||
RemoveSegment(expectedSegments[rnd.Next(tree.Count)]); |
||||
} |
||||
break; |
||||
case 3: |
||||
ChangeDocument(new OffsetChangeMapEntry(rnd.Next(800), rnd.Next(50), rnd.Next(50))); |
||||
break; |
||||
case 4: |
||||
ChangeDocument(new OffsetChangeMapEntry(rnd.Next(800), 0, rnd.Next(50))); |
||||
break; |
||||
case 5: |
||||
ChangeDocument(new OffsetChangeMapEntry(rnd.Next(800), rnd.Next(50), 0)); |
||||
break; |
||||
} |
||||
CheckSegments(); |
||||
} |
||||
} |
||||
|
||||
[Test] |
||||
public void RandomizedWithDocumentChangesClose() |
||||
{ |
||||
for (int i = 0; i < 500; i++) { |
||||
// Console.WriteLine(tree.GetTreeAsString());
|
||||
// Console.WriteLine("Iteration " + i);
|
||||
|
||||
switch (rnd.Next(6)) { |
||||
case 0: |
||||
AddSegment(rnd.Next(50), rnd.Next(30)); |
||||
break; |
||||
case 1: |
||||
AddSegment(rnd.Next(50), rnd.Next(3)); |
||||
break; |
||||
case 2: |
||||
if (tree.Count > 0) { |
||||
RemoveSegment(expectedSegments[rnd.Next(tree.Count)]); |
||||
} |
||||
break; |
||||
case 3: |
||||
ChangeDocument(new OffsetChangeMapEntry(rnd.Next(80), rnd.Next(10), rnd.Next(10))); |
||||
break; |
||||
case 4: |
||||
ChangeDocument(new OffsetChangeMapEntry(rnd.Next(80), 0, rnd.Next(10))); |
||||
break; |
||||
case 5: |
||||
ChangeDocument(new OffsetChangeMapEntry(rnd.Next(80), rnd.Next(10), 0)); |
||||
break; |
||||
} |
||||
CheckSegments(); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,94 +0,0 @@
@@ -1,94 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory.Editor; |
||||
#endif
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
[TestFixture] |
||||
public class TextUtilitiesTests |
||||
{ |
||||
#region GetWhitespaceAfter
|
||||
[Test] |
||||
public void TestGetWhitespaceAfter() |
||||
{ |
||||
Assert.AreEqual(new SimpleSegment(2, 3), TextUtilities.GetWhitespaceAfter(new StringTextSource("a \t \tb"), 2)); |
||||
} |
||||
|
||||
[Test] |
||||
public void TestGetWhitespaceAfterDoesNotSkipNewLine() |
||||
{ |
||||
Assert.AreEqual(new SimpleSegment(2, 3), TextUtilities.GetWhitespaceAfter(new StringTextSource("a \t \tb"), 2)); |
||||
} |
||||
|
||||
[Test] |
||||
public void TestGetWhitespaceAfterEmptyResult() |
||||
{ |
||||
Assert.AreEqual(new SimpleSegment(2, 0), TextUtilities.GetWhitespaceAfter(new StringTextSource("a b"), 2)); |
||||
} |
||||
|
||||
[Test] |
||||
public void TestGetWhitespaceAfterEndOfString() |
||||
{ |
||||
Assert.AreEqual(new SimpleSegment(2, 0), TextUtilities.GetWhitespaceAfter(new StringTextSource("a "), 2)); |
||||
} |
||||
|
||||
[Test] |
||||
public void TestGetWhitespaceAfterUntilEndOfString() |
||||
{ |
||||
Assert.AreEqual(new SimpleSegment(2, 3), TextUtilities.GetWhitespaceAfter(new StringTextSource("a \t \t"), 2)); |
||||
} |
||||
#endregion
|
||||
|
||||
#region GetWhitespaceBefore
|
||||
[Test] |
||||
public void TestGetWhitespaceBefore() |
||||
{ |
||||
Assert.AreEqual(new SimpleSegment(1, 3), TextUtilities.GetWhitespaceBefore(new StringTextSource("a\t \t b"), 4)); |
||||
} |
||||
|
||||
[Test] |
||||
public void TestGetWhitespaceBeforeDoesNotSkipNewLine() |
||||
{ |
||||
Assert.AreEqual(new SimpleSegment(2, 1), TextUtilities.GetWhitespaceBefore(new StringTextSource("a\n b"), 3)); |
||||
} |
||||
|
||||
[Test] |
||||
public void TestGetWhitespaceBeforeEmptyResult() |
||||
{ |
||||
Assert.AreEqual(new SimpleSegment(2, 0), TextUtilities.GetWhitespaceBefore(new StringTextSource(" a b"), 2)); |
||||
} |
||||
|
||||
[Test] |
||||
public void TestGetWhitespaceBeforeStartOfString() |
||||
{ |
||||
Assert.AreEqual(new SimpleSegment(0, 0), TextUtilities.GetWhitespaceBefore(new StringTextSource(" a"), 0)); |
||||
} |
||||
|
||||
[Test] |
||||
public void TestGetWhitespaceBeforeUntilStartOfString() |
||||
{ |
||||
Assert.AreEqual(new SimpleSegment(0, 2), TextUtilities.GetWhitespaceBefore(new StringTextSource(" \t a"), 2)); |
||||
} |
||||
#endregion
|
||||
} |
||||
} |
@ -1,92 +0,0 @@
@@ -1,92 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
public class UndoStackTests |
||||
{ |
||||
[Test] |
||||
public void ContinueUndoGroup() |
||||
{ |
||||
var doc = new TextDocument(); |
||||
doc.Insert(0, "a"); |
||||
doc.UndoStack.StartContinuedUndoGroup(); |
||||
doc.Insert(1, "b"); |
||||
doc.UndoStack.EndUndoGroup(); |
||||
doc.UndoStack.Undo(); |
||||
Assert.AreEqual("", doc.Text); |
||||
} |
||||
|
||||
[Test] |
||||
public void ContinueEmptyUndoGroup() |
||||
{ |
||||
var doc = new TextDocument(); |
||||
doc.Insert(0, "a"); |
||||
doc.UndoStack.StartUndoGroup(); |
||||
doc.UndoStack.EndUndoGroup(); |
||||
doc.UndoStack.StartContinuedUndoGroup(); |
||||
doc.Insert(1, "b"); |
||||
doc.UndoStack.EndUndoGroup(); |
||||
doc.UndoStack.Undo(); |
||||
Assert.AreEqual("a", doc.Text); |
||||
} |
||||
|
||||
[Test] |
||||
public void ContinueEmptyUndoGroup_WithOptionalEntries() |
||||
{ |
||||
var doc = new TextDocument(); |
||||
doc.Insert(0, "a"); |
||||
doc.UndoStack.StartUndoGroup(); |
||||
doc.UndoStack.PushOptional(new StubUndoableAction()); |
||||
doc.UndoStack.EndUndoGroup(); |
||||
doc.UndoStack.StartContinuedUndoGroup(); |
||||
doc.Insert(1, "b"); |
||||
doc.UndoStack.EndUndoGroup(); |
||||
doc.UndoStack.Undo(); |
||||
Assert.AreEqual("a", doc.Text); |
||||
} |
||||
|
||||
[Test] |
||||
public void EmptyContinuationGroup() |
||||
{ |
||||
var doc = new TextDocument(); |
||||
doc.Insert(0, "a"); |
||||
doc.UndoStack.StartContinuedUndoGroup(); |
||||
doc.UndoStack.EndUndoGroup(); |
||||
doc.UndoStack.StartContinuedUndoGroup(); |
||||
doc.Insert(1, "b"); |
||||
doc.UndoStack.EndUndoGroup(); |
||||
doc.UndoStack.Undo(); |
||||
Assert.AreEqual("", doc.Text); |
||||
} |
||||
|
||||
class StubUndoableAction : IUndoableOperation |
||||
{ |
||||
public void Undo() |
||||
{ |
||||
} |
||||
|
||||
public void Redo() |
||||
{ |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,78 +0,0 @@
@@ -1,78 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Text; |
||||
using ICSharpCode.AvalonEdit.Document; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory; |
||||
#endif
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Editing |
||||
{ |
||||
[TestFixture] |
||||
public class ChangeDocumentTests |
||||
{ |
||||
[Test] |
||||
public void ClearCaretAndSelectionOnDocumentChange() |
||||
{ |
||||
TextArea textArea = new TextArea(); |
||||
textArea.Document = new TextDocument("1\n2\n3\n4th line"); |
||||
textArea.Caret.Offset = 6; |
||||
textArea.Selection = Selection.Create(textArea, 3, 6); |
||||
textArea.Document = new TextDocument("1\n2nd"); |
||||
Assert.AreEqual(0, textArea.Caret.Offset); |
||||
Assert.AreEqual(new TextLocation(1, 1), textArea.Caret.Location); |
||||
Assert.IsTrue(textArea.Selection.IsEmpty); |
||||
} |
||||
|
||||
[Test] |
||||
public void SetDocumentToNull() |
||||
{ |
||||
TextArea textArea = new TextArea(); |
||||
textArea.Document = new TextDocument("1\n2\n3\n4th line"); |
||||
textArea.Caret.Offset = 6; |
||||
textArea.Selection = Selection.Create(textArea, 3, 6); |
||||
textArea.Document = null; |
||||
Assert.AreEqual(0, textArea.Caret.Offset); |
||||
Assert.AreEqual(new TextLocation(1, 1), textArea.Caret.Location); |
||||
Assert.IsTrue(textArea.Selection.IsEmpty); |
||||
} |
||||
|
||||
[Test] |
||||
public void CheckEventOrderOnDocumentChange() |
||||
{ |
||||
TextArea textArea = new TextArea(); |
||||
TextDocument newDocument = new TextDocument(); |
||||
StringBuilder b = new StringBuilder(); |
||||
textArea.TextView.DocumentChanged += delegate { |
||||
b.Append("TextView.DocumentChanged;"); |
||||
Assert.AreSame(newDocument, textArea.TextView.Document); |
||||
Assert.AreSame(newDocument, textArea.Document); |
||||
}; |
||||
textArea.DocumentChanged += delegate { |
||||
b.Append("TextArea.DocumentChanged;"); |
||||
Assert.AreSame(newDocument, textArea.TextView.Document); |
||||
Assert.AreSame(newDocument, textArea.Document); |
||||
}; |
||||
textArea.Document = newDocument; |
||||
Assert.AreEqual("TextView.DocumentChanged;TextArea.DocumentChanged;", b.ToString()); |
||||
} |
||||
} |
||||
} |
@ -1,183 +0,0 @@
@@ -1,183 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using ICSharpCode.AvalonEdit.Document; |
||||
using System; |
||||
using System.Linq; |
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Editing |
||||
{ |
||||
[TestFixture] |
||||
public class TextSegmentReadOnlySectionTests |
||||
{ |
||||
TextSegmentCollection<TextSegment> segments; |
||||
TextSegmentReadOnlySectionProvider<TextSegment> provider; |
||||
|
||||
[SetUp] |
||||
public void SetUp() |
||||
{ |
||||
segments = new TextSegmentCollection<TextSegment>(); |
||||
provider = new TextSegmentReadOnlySectionProvider<TextSegment>(segments); |
||||
} |
||||
|
||||
[Test] |
||||
public void InsertionPossibleWhenNothingIsReadOnly() |
||||
{ |
||||
Assert.IsTrue(provider.CanInsert(0)); |
||||
Assert.IsTrue(provider.CanInsert(100)); |
||||
} |
||||
|
||||
[Test] |
||||
public void DeletionPossibleWhenNothingIsReadOnly() |
||||
{ |
||||
var result = provider.GetDeletableSegments(new SimpleSegment(10, 20)).ToList(); |
||||
Assert.AreEqual(1, result.Count); |
||||
Assert.AreEqual(10, result[0].Offset); |
||||
Assert.AreEqual(20, result[0].Length); |
||||
} |
||||
|
||||
[Test] |
||||
public void EmptyDeletionPossibleWhenNothingIsReadOnly() |
||||
{ |
||||
var result = provider.GetDeletableSegments(new SimpleSegment(10, 0)).ToList(); |
||||
Assert.AreEqual(1, result.Count); |
||||
Assert.AreEqual(10, result[0].Offset); |
||||
Assert.AreEqual(0, result[0].Length); |
||||
} |
||||
|
||||
[Test] |
||||
public void InsertionPossibleBeforeReadOnlySegment() |
||||
{ |
||||
segments.Add(new TextSegment { StartOffset = 10, EndOffset = 15 }); |
||||
Assert.IsTrue(provider.CanInsert(5)); |
||||
} |
||||
|
||||
[Test] |
||||
public void InsertionPossibleAtStartOfReadOnlySegment() |
||||
{ |
||||
segments.Add(new TextSegment { StartOffset = 10, EndOffset = 15 }); |
||||
Assert.IsTrue(provider.CanInsert(10)); |
||||
} |
||||
|
||||
[Test] |
||||
public void InsertionImpossibleInsideReadOnlySegment() |
||||
{ |
||||
segments.Add(new TextSegment { StartOffset = 10, EndOffset = 15 }); |
||||
Assert.IsFalse(provider.CanInsert(11)); |
||||
Assert.IsFalse(provider.CanInsert(12)); |
||||
Assert.IsFalse(provider.CanInsert(13)); |
||||
Assert.IsFalse(provider.CanInsert(14)); |
||||
} |
||||
|
||||
[Test] |
||||
public void InsertionPossibleAtEndOfReadOnlySegment() |
||||
{ |
||||
segments.Add(new TextSegment { StartOffset = 10, EndOffset = 15 }); |
||||
Assert.IsTrue(provider.CanInsert(15)); |
||||
} |
||||
|
||||
[Test] |
||||
public void InsertionPossibleBetweenReadOnlySegments() |
||||
{ |
||||
segments.Add(new TextSegment { StartOffset = 10, EndOffset = 15 }); |
||||
segments.Add(new TextSegment { StartOffset = 15, EndOffset = 20 }); |
||||
Assert.IsTrue(provider.CanInsert(15)); |
||||
} |
||||
|
||||
[Test] |
||||
public void DeletionImpossibleInReadOnlySegment() |
||||
{ |
||||
segments.Add(new TextSegment { StartOffset = 10, Length = 5 }); |
||||
var result = provider.GetDeletableSegments(new SimpleSegment(11, 2)).ToList(); |
||||
Assert.AreEqual(0, result.Count); |
||||
} |
||||
|
||||
[Test] |
||||
public void EmptyDeletionImpossibleInReadOnlySegment() |
||||
{ |
||||
segments.Add(new TextSegment { StartOffset = 10, Length = 5 }); |
||||
var result = provider.GetDeletableSegments(new SimpleSegment(11, 0)).ToList(); |
||||
Assert.AreEqual(0, result.Count); |
||||
} |
||||
|
||||
[Test] |
||||
public void EmptyDeletionPossibleAtStartOfReadOnlySegment() |
||||
{ |
||||
segments.Add(new TextSegment { StartOffset = 10, Length = 5 }); |
||||
var result = provider.GetDeletableSegments(new SimpleSegment(10, 0)).ToList(); |
||||
Assert.AreEqual(1, result.Count); |
||||
Assert.AreEqual(10, result[0].Offset); |
||||
Assert.AreEqual(0, result[0].Length); |
||||
} |
||||
|
||||
[Test] |
||||
public void EmptyDeletionPossibleAtEndOfReadOnlySegment() |
||||
{ |
||||
segments.Add(new TextSegment { StartOffset = 10, Length = 5 }); |
||||
var result = provider.GetDeletableSegments(new SimpleSegment(15, 0)).ToList(); |
||||
Assert.AreEqual(1, result.Count); |
||||
Assert.AreEqual(15, result[0].Offset); |
||||
Assert.AreEqual(0, result[0].Length); |
||||
} |
||||
|
||||
[Test] |
||||
public void DeletionAroundReadOnlySegment() |
||||
{ |
||||
segments.Add(new TextSegment { StartOffset = 20, Length = 5 }); |
||||
var result = provider.GetDeletableSegments(new SimpleSegment(15, 16)).ToList(); |
||||
Assert.AreEqual(2, result.Count); |
||||
Assert.AreEqual(15, result[0].Offset); |
||||
Assert.AreEqual(5, result[0].Length); |
||||
Assert.AreEqual(25, result[1].Offset); |
||||
Assert.AreEqual(6, result[1].Length); |
||||
} |
||||
|
||||
[Test] |
||||
public void DeleteLastCharacterInReadOnlySegment() |
||||
{ |
||||
segments.Add(new TextSegment { StartOffset = 20, Length = 5 }); |
||||
var result = provider.GetDeletableSegments(new SimpleSegment(24, 1)).ToList(); |
||||
Assert.AreEqual(0, result.Count); |
||||
/* // we would need this result for the old Backspace code so that the last character doesn't get selected:
|
||||
Assert.AreEqual(1, result.Count); |
||||
Assert.AreEqual(25, result[0].Offset); |
||||
Assert.AreEqual(0, result[0].Length);*/ |
||||
} |
||||
|
||||
[Test] |
||||
public void DeleteFirstCharacterInReadOnlySegment() |
||||
{ |
||||
segments.Add(new TextSegment { StartOffset = 20, Length = 5 }); |
||||
var result = provider.GetDeletableSegments(new SimpleSegment(20, 1)).ToList(); |
||||
Assert.AreEqual(0, result.Count); |
||||
/* // we would need this result for the old Delete code so that the first character doesn't get selected:
|
||||
Assert.AreEqual(1, result.Count); |
||||
Assert.AreEqual(2, result[0].Offset); |
||||
Assert.AreEqual(0, result[0].Length);*/ |
||||
} |
||||
|
||||
[Test] |
||||
public void DeleteWholeReadOnlySegment() |
||||
{ |
||||
segments.Add(new TextSegment { StartOffset = 20, Length = 5 }); |
||||
var result = provider.GetDeletableSegments(new SimpleSegment(20, 5)).ToList(); |
||||
Assert.AreEqual(0, result.Count); |
||||
} |
||||
} |
||||
} |
@ -1,182 +0,0 @@
@@ -1,182 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using ICSharpCode.AvalonEdit.Document; |
||||
using ICSharpCode.NRefactory.Editor; |
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Highlighting |
||||
{ |
||||
[TestFixture] |
||||
public class HighlightedLineMergeTests |
||||
{ |
||||
IDocument document = new TextDocument(new string(' ', 20)); |
||||
|
||||
[Test] |
||||
public void SimpleMerge1() |
||||
{ |
||||
HighlightedLine baseLine = new HighlightedLine(document, document.GetLineByNumber(1)); |
||||
baseLine.Sections.Add(MakeSection(0, 1, "B")); |
||||
|
||||
HighlightedLine additionalLine = new HighlightedLine(document, document.GetLineByNumber(1)); |
||||
additionalLine.Sections.Add(MakeSection(0, 2, "A")); |
||||
|
||||
baseLine.MergeWith(additionalLine); |
||||
// The additional section gets split up so that it fits into the tree structure
|
||||
Assert.That(baseLine.Sections, Is.EqualTo( |
||||
new[] { |
||||
MakeSection(0, 1, "B"), |
||||
MakeSection(0, 1, "A"), |
||||
MakeSection(1, 2, "A") |
||||
}).Using(new SectionComparer())); |
||||
} |
||||
|
||||
[Test] |
||||
public void SimpleMerge2() |
||||
{ |
||||
HighlightedLine baseLine = new HighlightedLine(document, document.GetLineByNumber(1)); |
||||
baseLine.Sections.Add(MakeSection(0, 1, "B")); |
||||
baseLine.Sections.Add(MakeSection(0, 1, "BN")); |
||||
|
||||
HighlightedLine additionalLine = new HighlightedLine(document, document.GetLineByNumber(1)); |
||||
additionalLine.Sections.Add(MakeSection(0, 2, "A")); |
||||
|
||||
baseLine.MergeWith(additionalLine); |
||||
// The additional section gets split up so that it fits into the tree structure
|
||||
Assert.That(baseLine.Sections, Is.EqualTo( |
||||
new[] { |
||||
MakeSection(0, 1, "B"), |
||||
MakeSection(0, 1, "BN"), |
||||
MakeSection(0, 1, "A"), |
||||
MakeSection(1, 2, "A") |
||||
}).Using(new SectionComparer())); |
||||
} |
||||
|
||||
HighlightedSection MakeSection(int start, int end, string name) |
||||
{ |
||||
return new HighlightedSection { Offset = start, Length = end - start, Color = new HighlightingColor { Name = name }}; |
||||
} |
||||
|
||||
class SectionComparer : IEqualityComparer<HighlightedSection> |
||||
{ |
||||
public bool Equals(HighlightedSection a, HighlightedSection b) |
||||
{ |
||||
return a.Offset == b.Offset && a.Length == b.Length && a.Color.Name == b.Color.Name; |
||||
} |
||||
|
||||
public int GetHashCode(HighlightedSection obj) |
||||
{ |
||||
return obj.Offset; |
||||
} |
||||
} |
||||
|
||||
#region Automatic Test
|
||||
/* |
||||
const int combinations = 6 * 3 * 4 * 3 * 3 * 4; |
||||
HighlightingColor[] baseLineColors = { |
||||
new HighlightingColor { Name = "Base-A" }, |
||||
new HighlightingColor { Name = "Base-B" }, |
||||
new HighlightingColor { Name = "Base-N" }, |
||||
new HighlightingColor { Name = "Base-C" } |
||||
}; |
||||
HighlightingColor[] additionalLineColors = { |
||||
new HighlightingColor { Name = "Add-A" }, |
||||
new HighlightingColor { Name = "Add-B" }, |
||||
new HighlightingColor { Name = "Add-N" }, |
||||
new HighlightingColor { Name = "Add-C" } |
||||
}; |
||||
|
||||
HighlightedLine BuildHighlightedLine(int num, HighlightingColor[] colors) |
||||
{ |
||||
// We are build a HighlightedLine with 4 segments:
|
||||
// A B C (top-level) and N nested within B.
|
||||
// These are the integers controlling the generating process:
|
||||
|
||||
int aStart = GetNum(ref num, 5); // start offset of A
|
||||
int aLength = GetNum(ref num, 2); // length of A
|
||||
|
||||
int bDistance = GetNum(ref num, 3); // distance from start of B to end of A
|
||||
int bStart = aStart + aLength + bDistance; |
||||
int nDistance = GetNum(ref num, 2); // distance from start of B to start of N, range 0-2
|
||||
int nLength = GetNum(ref num, 2); // length of N
|
||||
int bEndDistance = GetNum(ref num, 2); // distance from end of N to end of B
|
||||
int bLength = nDistance + nLength + bEndDistance; |
||||
|
||||
int cDistance = GetNum(ref num, 3); // distance from end of B to start of C
|
||||
int cStart = bStart + bLength + cDistance; |
||||
int cLength = 1; |
||||
Assert.AreEqual(0, num); |
||||
|
||||
var documentLine = document.GetLineByNumber(1); |
||||
HighlightedLine line = new HighlightedLine(document, documentLine); |
||||
line.Sections.Add(new HighlightedSection { Offset = aStart, Length = aLength, Color = colors[0] }); |
||||
line.Sections.Add(new HighlightedSection { Offset = bStart, Length = bLength, Color = colors[1] }); |
||||
line.Sections.Add(new HighlightedSection { Offset = bStart + nDistance, Length = nLength, Color = colors[2] }); |
||||
line.Sections.Add(new HighlightedSection { Offset = cStart, Length = cLength, Color = colors[3] }); |
||||
|
||||
return line; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets a number between 0 and max (inclusive)
|
||||
/// </summary>
|
||||
int GetNum(ref int num, int max) |
||||
{ |
||||
int result = num % (max+1); |
||||
num = num / (max + 1); |
||||
return result; |
||||
} |
||||
|
||||
[Test] |
||||
public void TestAll() |
||||
{ |
||||
for (int c1 = 0; c1 < combinations; c1++) { |
||||
HighlightedLine line1 = BuildHighlightedLine(c1, additionalLineColors); |
||||
for (int c2 = 0; c2 < combinations; c2++) { |
||||
HighlightedLine line2 = BuildHighlightedLine(c2, baseLineColors); |
||||
HighlightingColor[] expectedPerCharColors = new HighlightingColor[document.TextLength]; |
||||
ApplyColors(expectedPerCharColors, line2); |
||||
ApplyColors(expectedPerCharColors, line1); |
||||
try { |
||||
line2.MergeWith(line1); |
||||
} catch (InvalidOperationException ex) { |
||||
throw new InvalidOperationException(string.Format("Error for c1 = {0}, c2 = {1}", c1, c2), ex); |
||||
} |
||||
|
||||
HighlightingColor[] actualPerCharColors = new HighlightingColor[document.TextLength]; |
||||
ApplyColors(actualPerCharColors, line2); |
||||
Assert.AreEqual(expectedPerCharColors, actualPerCharColors, string.Format("c1 = {0}, c2 = {1}", c1, c2)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
void ApplyColors(HighlightingColor[] perCharColors, HighlightedLine line) |
||||
{ |
||||
foreach (var section in line.Sections) { |
||||
for (int i = 0; i < section.Length; i++) { |
||||
perCharColors[section.Offset + i] = section.Color; |
||||
} |
||||
} |
||||
} |
||||
*/ |
||||
#endregion
|
||||
} |
||||
} |
@ -1,56 +0,0 @@
@@ -1,56 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Windows; |
||||
using ICSharpCode.AvalonEdit.Document; |
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Highlighting |
||||
{ |
||||
[TestFixture] |
||||
public class HtmlClipboardTests |
||||
{ |
||||
TextDocument document; |
||||
DocumentHighlighter highlighter; |
||||
|
||||
public HtmlClipboardTests() |
||||
{ |
||||
document = new TextDocument("using System.Text;\n\tstring text = SomeMethod();"); |
||||
highlighter = new DocumentHighlighter(document, HighlightingManager.Instance.GetDefinition("C#")); |
||||
} |
||||
|
||||
[Test] |
||||
public void FullDocumentTest() |
||||
{ |
||||
var segment = new TextSegment { StartOffset = 0, Length = document.TextLength }; |
||||
string html = HtmlClipboard.CreateHtmlFragment(document, highlighter, segment, new HtmlOptions()); |
||||
Assert.AreEqual("<span style=\"color: #008000; font-weight: bold; \">using</span> System.Text;<br>" + Environment.NewLine + |
||||
" <span style=\"color: #ff0000; \">string</span> " + |
||||
"text = <span style=\"color: #191970; font-weight: bold; \">SomeMethod</span>();", html); |
||||
} |
||||
|
||||
[Test] |
||||
public void PartOfHighlightedWordTest() |
||||
{ |
||||
var segment = new TextSegment { StartOffset = 1, Length = 3 }; |
||||
string html = HtmlClipboard.CreateHtmlFragment(document, highlighter, segment, new HtmlOptions()); |
||||
Assert.AreEqual("<span style=\"color: #008000; font-weight: bold; \">sin</span>", html); |
||||
} |
||||
} |
||||
} |
@ -1,3 +0,0 @@
@@ -1,3 +0,0 @@
|
||||
<PartCoverSettings> |
||||
<Rule>+[ICSharpCode.AvalonEdit]*</Rule> |
||||
</PartCoverSettings> |
@ -1,126 +0,0 @@
@@ -1,126 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
||||
<PropertyGroup> |
||||
<ProjectGuid>{6222A3A1-83CE-47A3-A4E4-A018F82D44D8}</ProjectGuid> |
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> |
||||
<Platform Condition=" '$(Platform)' == '' ">Net40</Platform> |
||||
<OutputType>Library</OutputType> |
||||
<RootNamespace>ICSharpCode.AvalonEdit</RootNamespace> |
||||
<AssemblyName>ICSharpCode.AvalonEdit.Tests</AssemblyName> |
||||
<TargetFrameworkVersion Condition=" '$(Configuration)' == '' ">v4.0</TargetFrameworkVersion> |
||||
<AppDesignerFolder>Properties</AppDesignerFolder> |
||||
<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\$(Configuration)\</OutputPath> |
||||
<RunPostBuildEvent>OnBuildSuccess</RunPostBuildEvent> |
||||
<TargetFrameworkProfile> |
||||
</TargetFrameworkProfile> |
||||
<NoWin32Manifest>False</NoWin32Manifest> |
||||
<IntermediateOutputPath>obj\$(Configuration)\</IntermediateOutputPath> |
||||
<RegisterForComInterop>False</RegisterForComInterop> |
||||
<GenerateSerializationAssemblies>Auto</GenerateSerializationAssemblies> |
||||
<BaseAddress>4194304</BaseAddress> |
||||
<PlatformTarget>AnyCPU</PlatformTarget> |
||||
<FileAlignment>4096</FileAlignment> |
||||
</PropertyGroup> |
||||
<PropertyGroup Condition=" '$(Platform)' == 'Net35' "> |
||||
<TargetFrameworkVersion>v3.5</TargetFrameworkVersion> |
||||
</PropertyGroup> |
||||
<PropertyGroup Condition=" '$(Platform)' == 'Net40' "> |
||||
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion> |
||||
<DefineConstants>DOTNET4</DefineConstants> |
||||
</PropertyGroup> |
||||
<PropertyGroup Condition=" '$(Platform)' == 'WithNRefactory' "> |
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion> |
||||
<DefineConstants>NREFACTORY</DefineConstants> |
||||
</PropertyGroup> |
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> |
||||
<DebugSymbols>true</DebugSymbols> |
||||
<DebugType>Full</DebugType> |
||||
<Optimize>False</Optimize> |
||||
<CheckForOverflowUnderflow>True</CheckForOverflowUnderflow> |
||||
<BaseIntermediateOutputPath>obj\</BaseIntermediateOutputPath> |
||||
<DefineDebug>True</DefineDebug> |
||||
<DefineTrace>True</DefineTrace> |
||||
</PropertyGroup> |
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' "> |
||||
<DebugSymbols>false</DebugSymbols> |
||||
<DebugType>None</DebugType> |
||||
<Optimize>True</Optimize> |
||||
<CheckForOverflowUnderflow>False</CheckForOverflowUnderflow> |
||||
<BaseIntermediateOutputPath>obj\</BaseIntermediateOutputPath> |
||||
<DefineTrace>True</DefineTrace> |
||||
</PropertyGroup> |
||||
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.Targets" /> |
||||
<ItemGroup> |
||||
<ProjectReference Include="..\..\NRefactory\ICSharpCode.NRefactory\ICSharpCode.NRefactory.csproj" Condition="$(DefineConstants.Contains('NREFACTORY'))"> |
||||
<Project>{3B2A5653-EC97-4001-BB9B-D90F1AF2C371}</Project> |
||||
<Name>ICSharpCode.NRefactory</Name> |
||||
</ProjectReference> |
||||
<Reference Include="nunit.framework"> |
||||
<HintPath>..\..\..\Tools\NUnit\nunit.framework.dll</HintPath> |
||||
</Reference> |
||||
<Reference Include="PresentationCore"> |
||||
<RequiredTargetFramework>3.0</RequiredTargetFramework> |
||||
</Reference> |
||||
<Reference Include="PresentationFramework"> |
||||
<RequiredTargetFramework>3.0</RequiredTargetFramework> |
||||
</Reference> |
||||
<Reference Include="System" /> |
||||
<Reference Include="System.Core"> |
||||
<RequiredTargetFramework>3.5</RequiredTargetFramework> |
||||
</Reference> |
||||
<Reference Include="System.Xaml"> |
||||
<RequiredTargetFramework>4.0</RequiredTargetFramework> |
||||
</Reference> |
||||
<Reference Include="System.Xml" /> |
||||
<Reference Include="System.Xml.Linq"> |
||||
<RequiredTargetFramework>3.5</RequiredTargetFramework> |
||||
</Reference> |
||||
<Reference Include="WindowsBase"> |
||||
<RequiredTargetFramework>3.0</RequiredTargetFramework> |
||||
</Reference> |
||||
</ItemGroup> |
||||
<ItemGroup> |
||||
<Compile Include="Document\ChangeTrackingTest.cs" /> |
||||
<Compile Include="Document\TextAnchorTest.cs" /> |
||||
<Compile Include="Document\TextSegmentTreeTest.cs" /> |
||||
<Compile Include="Document\TextUtilitiesTests.cs" /> |
||||
<Compile Include="Document\UndoStackTests.cs" /> |
||||
<Compile Include="Editing\ChangeDocumentTests.cs" /> |
||||
<Compile Include="Editing\TextSegmentReadOnlySectionTests.cs" /> |
||||
<Compile Include="Highlighting\HighlightedLineMergeTests.cs" /> |
||||
<Compile Include="Highlighting\HtmlClipboardTests.cs" /> |
||||
<Compile Include="MultipleUIThreads.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="Search\FindTests.cs" /> |
||||
<Compile Include="Utils\CaretNavigationTests.cs" /> |
||||
<Compile Include="Utils\CompressingTreeListTests.cs" /> |
||||
<Compile Include="Utils\ExtensionMethodsTests.cs" /> |
||||
<Compile Include="Utils\IndentationStringTests.cs" /> |
||||
<Compile Include="Utils\RopeTests.cs" /> |
||||
<Compile Include="WeakReferenceTests.cs" /> |
||||
</ItemGroup> |
||||
<ItemGroup> |
||||
<ProjectReference Include="..\ICSharpCode.AvalonEdit\ICSharpCode.AvalonEdit.csproj"> |
||||
<Project>{6C55B776-26D4-4DB3-A6AB-87E783B2F3D1}</Project> |
||||
<Name>ICSharpCode.AvalonEdit</Name> |
||||
</ProjectReference> |
||||
</ItemGroup> |
||||
<ItemGroup> |
||||
<Folder Include="Search" /> |
||||
</ItemGroup> |
||||
<ItemGroup> |
||||
<None Include="packages.config" /> |
||||
</ItemGroup> |
||||
</Project> |
@ -1,44 +0,0 @@
@@ -1,44 +0,0 @@
|
||||
// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
|
||||
// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
|
||||
|
||||
using System; |
||||
using System.Threading; |
||||
using System.Windows; |
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.AvalonEdit |
||||
{ |
||||
[TestFixture] |
||||
public class MultipleUIThreads |
||||
{ |
||||
Exception error; |
||||
|
||||
[Test] |
||||
public void CreateEditorInstancesOnMultipleUIThreads() |
||||
{ |
||||
Thread t1 = new Thread(new ThreadStart(Run)); |
||||
Thread t2 = new Thread(new ThreadStart(Run)); |
||||
t1.SetApartmentState(ApartmentState.STA); |
||||
t2.SetApartmentState(ApartmentState.STA); |
||||
t1.Start(); |
||||
t2.Start(); |
||||
t1.Join(); |
||||
t2.Join(); |
||||
if (error != null) |
||||
throw new InvalidOperationException(error.Message, error); |
||||
} |
||||
|
||||
[STAThread] |
||||
void Run() |
||||
{ |
||||
try { |
||||
var window = new Window(); |
||||
window.Content = new TextEditor(); |
||||
window.ShowActivated = false; |
||||
window.Show(); |
||||
} catch (Exception ex) { |
||||
error = ex; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,55 +0,0 @@
@@ -1,55 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
#region Using directives
|
||||
|
||||
using System; |
||||
using System.Reflection; |
||||
using System.Runtime.InteropServices; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
#endregion
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("ICSharpCode.AvalonEdit.Tests")] |
||||
[assembly: AssemblyDescription("")] |
||||
[assembly: AssemblyConfiguration("")] |
||||
[assembly: AssemblyCompany("")] |
||||
[assembly: AssemblyProduct("ICSharpCode.AvalonEdit.Tests")] |
||||
[assembly: AssemblyTrademark("")] |
||||
[assembly: AssemblyCulture("")] |
||||
|
||||
// This sets the default COM visibility of types in the assembly to invisible.
|
||||
// If you need to expose a type to COM, use [ComVisible(true)] on that type.
|
||||
[assembly: ComVisible(false)] |
||||
|
||||
// The assembly version has following format :
|
||||
//
|
||||
// Major.Minor.Build.Revision
|
||||
//
|
||||
// You can specify all the values or you can use the default the Revision and
|
||||
// Build Numbers by using the '*' as shown below:
|
||||
[assembly: AssemblyVersion("1.0.*")] |
||||
|
||||
// Run unit tests on STA thread.
|
||||
[assembly: RequiresSTA] |
||||
|
||||
namespace ICSharpCode.NRefactory.Editor {} |
@ -1,132 +0,0 @@
@@ -1,132 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Linq; |
||||
using ICSharpCode.AvalonEdit.Document; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory.Editor; |
||||
#endif
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Search |
||||
{ |
||||
[TestFixture] |
||||
public class FindTests |
||||
{ |
||||
[Test] |
||||
public void SkipWordBorderSimple() |
||||
{ |
||||
var strategy = SearchStrategyFactory.Create("All", false, true, SearchMode.Normal); |
||||
var text = new StringTextSource(" FindAllTests "); |
||||
var results = strategy.FindAll(text, 0, text.TextLength).ToArray(); |
||||
|
||||
Assert.IsEmpty(results, "No results should be found!"); |
||||
} |
||||
|
||||
[Test] |
||||
public void SkipWordBorder() |
||||
{ |
||||
var strategy = SearchStrategyFactory.Create("AllTests", false, true, SearchMode.Normal); |
||||
var text = new StringTextSource("name=\"{FindAllTests}\""); |
||||
var results = strategy.FindAll(text, 0, text.TextLength).ToArray(); |
||||
|
||||
Assert.IsEmpty(results, "No results should be found!"); |
||||
} |
||||
|
||||
[Test] |
||||
public void SkipWordBorder2() |
||||
{ |
||||
var strategy = SearchStrategyFactory.Create("AllTests", false, true, SearchMode.Normal); |
||||
var text = new StringTextSource("name=\"FindAllTests "); |
||||
var results = strategy.FindAll(text, 0, text.TextLength).ToArray(); |
||||
|
||||
Assert.IsEmpty(results, "No results should be found!"); |
||||
} |
||||
|
||||
[Test] |
||||
public void SkipWordBorder3() |
||||
{ |
||||
var strategy = SearchStrategyFactory.Create("// find", false, true, SearchMode.Normal); |
||||
var text = new StringTextSource(" // findtest"); |
||||
var results = strategy.FindAll(text, 0, text.TextLength).ToArray(); |
||||
|
||||
Assert.IsEmpty(results, "No results should be found!"); |
||||
} |
||||
|
||||
[Test] |
||||
public void WordBorderTest() |
||||
{ |
||||
var strategy = SearchStrategyFactory.Create("// find", false, true, SearchMode.Normal); |
||||
var text = new StringTextSource(" // find me"); |
||||
var results = strategy.FindAll(text, 0, text.TextLength).ToArray(); |
||||
|
||||
Assert.AreEqual(1, results.Length, "One result should be found!"); |
||||
Assert.AreEqual(" ".Length, results[0].Offset); |
||||
Assert.AreEqual("// find".Length, results[0].Length); |
||||
} |
||||
|
||||
[Test] |
||||
public void ResultAtStart() |
||||
{ |
||||
var strategy = SearchStrategyFactory.Create("result", false, true, SearchMode.Normal); |
||||
var text = new StringTextSource("result // find me"); |
||||
var results = strategy.FindAll(text, 0, text.TextLength).ToArray(); |
||||
|
||||
Assert.AreEqual(1, results.Length, "One result should be found!"); |
||||
Assert.AreEqual(0, results[0].Offset); |
||||
Assert.AreEqual("result".Length, results[0].Length); |
||||
} |
||||
|
||||
[Test] |
||||
public void ResultAtEnd() |
||||
{ |
||||
var strategy = SearchStrategyFactory.Create("me", false, true, SearchMode.Normal); |
||||
var text = new StringTextSource("result // find me"); |
||||
var results = strategy.FindAll(text, 0, text.TextLength).ToArray(); |
||||
|
||||
Assert.AreEqual(1, results.Length, "One result should be found!"); |
||||
Assert.AreEqual("result // find ".Length, results[0].Offset); |
||||
Assert.AreEqual("me".Length, results[0].Length); |
||||
} |
||||
|
||||
[Test] |
||||
public void TextWithDots() |
||||
{ |
||||
var strategy = SearchStrategyFactory.Create("Text", false, true, SearchMode.Normal); |
||||
var text = new StringTextSource(".Text."); |
||||
var results = strategy.FindAll(text, 0, text.TextLength).ToArray(); |
||||
|
||||
Assert.AreEqual(1, results.Length, "One result should be found!"); |
||||
Assert.AreEqual(".".Length, results[0].Offset); |
||||
Assert.AreEqual("Text".Length, results[0].Length); |
||||
} |
||||
|
||||
[Test] |
||||
public void SimpleTest() |
||||
{ |
||||
var strategy = SearchStrategyFactory.Create("AllTests", false, false, SearchMode.Normal); |
||||
var text = new StringTextSource("name=\"FindAllTests "); |
||||
var results = strategy.FindAll(text, 0, text.TextLength).ToArray(); |
||||
|
||||
Assert.AreEqual(1, results.Length, "One result should be found!"); |
||||
Assert.AreEqual("name=\"Find".Length, results[0].Offset); |
||||
Assert.AreEqual("AllTests".Length, results[0].Length); |
||||
} |
||||
} |
||||
} |
@ -1,146 +0,0 @@
@@ -1,146 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Linq; |
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Utils |
||||
{ |
||||
[TestFixture] |
||||
public class CompressingTreeListTests |
||||
{ |
||||
[Test] |
||||
public void EmptyTreeList() |
||||
{ |
||||
CompressingTreeList<string> list = new CompressingTreeList<string>(string.Equals); |
||||
Assert.AreEqual(0, list.Count); |
||||
foreach (string v in list) { |
||||
Assert.Fail(); |
||||
} |
||||
string[] arr = new string[0]; |
||||
list.CopyTo(arr, 0); |
||||
} |
||||
|
||||
[Test] |
||||
public void CheckAdd10BillionElements() |
||||
{ |
||||
const int billion = 1000000000; |
||||
CompressingTreeList<string> list = new CompressingTreeList<string>(string.Equals); |
||||
list.InsertRange(0, billion, "A"); |
||||
list.InsertRange(1, billion, "B"); |
||||
Assert.AreEqual(2 * billion, list.Count); |
||||
Assert.Throws<OverflowException>(delegate { list.InsertRange(2, billion, "C"); }); |
||||
} |
||||
|
||||
[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()); |
||||
} |
||||
|
||||
[Test] |
||||
public void Transform() |
||||
{ |
||||
CompressingTreeList<int> list = new CompressingTreeList<int>((a, b) => a == b); |
||||
list.AddRange(new[] { 0, 1, 1, 0 }); |
||||
int calls = 0; |
||||
list.Transform(i => { calls++; return i + 1; }); |
||||
Assert.AreEqual(3, calls); |
||||
Assert.AreEqual(new[] { 1, 2, 2, 1 }, list.ToArray()); |
||||
} |
||||
|
||||
[Test] |
||||
public void TransformToZero() |
||||
{ |
||||
CompressingTreeList<int> list = new CompressingTreeList<int>((a, b) => a == b); |
||||
list.AddRange(new[] { 0, 1, 1, 0 }); |
||||
list.Transform(i => 0); |
||||
Assert.AreEqual(new[] { 0, 0, 0, 0 }, list.ToArray()); |
||||
} |
||||
|
||||
[Test] |
||||
public void TransformRange() |
||||
{ |
||||
CompressingTreeList<int> list = new CompressingTreeList<int>((a, b) => a == b); |
||||
list.AddRange(new[] { 0, 1, 1, 1, 0, 0 }); |
||||
list.TransformRange(2, 3, i => 0); |
||||
Assert.AreEqual(new[] { 0, 1, 0, 0, 0, 0 }, list.ToArray()); |
||||
} |
||||
} |
||||
} |
@ -1,51 +0,0 @@
@@ -1,51 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Utils |
||||
{ |
||||
[TestFixture] |
||||
public class ExtensionMethodsTests |
||||
{ |
||||
[Test] |
||||
public void ZeroIsNotCloseToOne() |
||||
{ |
||||
Assert.IsFalse(0.0.IsClose(1)); |
||||
} |
||||
|
||||
[Test] |
||||
public void ZeroIsCloseToZero() |
||||
{ |
||||
Assert.IsTrue(0.0.IsClose(0)); |
||||
} |
||||
|
||||
[Test] |
||||
public void InfinityIsCloseToInfinity() |
||||
{ |
||||
Assert.IsTrue(double.PositiveInfinity.IsClose(double.PositiveInfinity)); |
||||
} |
||||
|
||||
[Test] |
||||
public void NaNIsNotCloseToNaN() |
||||
{ |
||||
Assert.IsFalse(double.NaN.IsClose(double.NaN)); |
||||
} |
||||
} |
||||
} |
@ -1,51 +0,0 @@
@@ -1,51 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Utils |
||||
{ |
||||
[TestFixture] |
||||
public class IndentationStringTests |
||||
{ |
||||
[Test] |
||||
public void IndentWithSingleTab() |
||||
{ |
||||
var options = new TextEditorOptions { IndentationSize = 4, ConvertTabsToSpaces = false }; |
||||
Assert.AreEqual("\t", options.IndentationString); |
||||
Assert.AreEqual("\t", options.GetIndentationString(2)); |
||||
Assert.AreEqual("\t", options.GetIndentationString(3)); |
||||
Assert.AreEqual("\t", options.GetIndentationString(4)); |
||||
Assert.AreEqual("\t", options.GetIndentationString(5)); |
||||
Assert.AreEqual("\t", options.GetIndentationString(6)); |
||||
} |
||||
|
||||
[Test] |
||||
public void IndentWith4Spaces() |
||||
{ |
||||
var options = new TextEditorOptions { IndentationSize = 4, ConvertTabsToSpaces = true }; |
||||
Assert.AreEqual(" ", options.IndentationString); |
||||
Assert.AreEqual(" ", options.GetIndentationString(2)); |
||||
Assert.AreEqual(" ", options.GetIndentationString(3)); |
||||
Assert.AreEqual(" ", options.GetIndentationString(4)); |
||||
Assert.AreEqual(" ", options.GetIndentationString(5)); |
||||
Assert.AreEqual(" ", options.GetIndentationString(6)); |
||||
} |
||||
} |
||||
} |
@ -1,195 +0,0 @@
@@ -1,195 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.IO; |
||||
using NUnit.Framework; |
||||
using System.Text; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Utils |
||||
{ |
||||
[TestFixture] |
||||
public class RopeTests |
||||
{ |
||||
[Test] |
||||
public void EmptyRope() |
||||
{ |
||||
Rope<char> empty = new Rope<char>(); |
||||
Assert.AreEqual(0, empty.Length); |
||||
Assert.AreEqual("", empty.ToString()); |
||||
} |
||||
|
||||
[Test] |
||||
public void EmptyRopeFromString() |
||||
{ |
||||
Rope<char> empty = new Rope<char>(string.Empty); |
||||
Assert.AreEqual(0, empty.Length); |
||||
Assert.AreEqual("", empty.ToString()); |
||||
} |
||||
|
||||
[Test] |
||||
public void InitializeRopeFromShortString() |
||||
{ |
||||
Rope<char> rope = new Rope<char>("Hello, World"); |
||||
Assert.AreEqual(12, rope.Length); |
||||
Assert.AreEqual("Hello, World", rope.ToString()); |
||||
} |
||||
|
||||
string BuildLongString(int lines) |
||||
{ |
||||
StringWriter w = new StringWriter(); |
||||
w.NewLine = "\n"; |
||||
for (int i = 1; i <= lines; i++) { |
||||
w.WriteLine(i.ToString()); |
||||
} |
||||
return w.ToString(); |
||||
} |
||||
|
||||
[Test] |
||||
public void InitializeRopeFromLongString() |
||||
{ |
||||
string text = BuildLongString(1000); |
||||
Rope<char> rope = new Rope<char>(text); |
||||
Assert.AreEqual(text.Length, rope.Length); |
||||
Assert.AreEqual(text, rope.ToString()); |
||||
Assert.AreEqual(text.ToCharArray(), rope.ToArray()); |
||||
} |
||||
|
||||
[Test] |
||||
public void TestToArrayAndToStringWithParts() |
||||
{ |
||||
string text = BuildLongString(1000); |
||||
Rope<char> rope = new Rope<char>(text); |
||||
|
||||
string textPart = text.Substring(1200, 600); |
||||
char[] arrayPart = textPart.ToCharArray(); |
||||
Assert.AreEqual(textPart, rope.ToString(1200, 600)); |
||||
Assert.AreEqual(arrayPart, rope.ToArray(1200, 600)); |
||||
|
||||
Rope<char> partialRope = rope.GetRange(1200, 600); |
||||
Assert.AreEqual(textPart, partialRope.ToString()); |
||||
Assert.AreEqual(arrayPart, partialRope.ToArray()); |
||||
} |
||||
|
||||
[Test] |
||||
public void ConcatenateStringToRope() |
||||
{ |
||||
StringBuilder b = new StringBuilder(); |
||||
Rope<char> rope = new Rope<char>(); |
||||
for (int i = 1; i <= 1000; i++) { |
||||
b.Append(i.ToString()); |
||||
rope.AddText(i.ToString()); |
||||
b.Append(' '); |
||||
rope.Add(' '); |
||||
} |
||||
Assert.AreEqual(b.ToString(), rope.ToString()); |
||||
} |
||||
|
||||
[Test] |
||||
public void ConcatenateSmallRopesToRope() |
||||
{ |
||||
StringBuilder b = new StringBuilder(); |
||||
Rope<char> rope = new Rope<char>(); |
||||
for (int i = 1; i <= 1000; i++) { |
||||
b.Append(i.ToString()); |
||||
b.Append(' '); |
||||
rope.AddRange(CharRope.Create(i.ToString() + " ")); |
||||
} |
||||
Assert.AreEqual(b.ToString(), rope.ToString()); |
||||
} |
||||
|
||||
[Test] |
||||
public void AppendLongTextToEmptyRope() |
||||
{ |
||||
string text = BuildLongString(1000); |
||||
Rope<char> rope = new Rope<char>(); |
||||
rope.AddText(text); |
||||
Assert.AreEqual(text, rope.ToString()); |
||||
} |
||||
|
||||
[Test] |
||||
public void ConcatenateStringToRopeBackwards() |
||||
{ |
||||
StringBuilder b = new StringBuilder(); |
||||
Rope<char> rope = new Rope<char>(); |
||||
for (int i = 1; i <= 1000; i++) { |
||||
b.Append(i.ToString()); |
||||
b.Append(' '); |
||||
} |
||||
for (int i = 1000; i >= 1; i--) { |
||||
rope.Insert(0, ' '); |
||||
rope.InsertText(0, i.ToString()); |
||||
} |
||||
Assert.AreEqual(b.ToString(), rope.ToString()); |
||||
} |
||||
|
||||
[Test] |
||||
public void ConcatenateSmallRopesToRopeBackwards() |
||||
{ |
||||
StringBuilder b = new StringBuilder(); |
||||
Rope<char> rope = new Rope<char>(); |
||||
for (int i = 1; i <= 1000; i++) { |
||||
b.Append(i.ToString()); |
||||
b.Append(' '); |
||||
} |
||||
for (int i = 1000; i >= 1; i--) { |
||||
rope.InsertRange(0, CharRope.Create(i.ToString() + " ")); |
||||
} |
||||
Assert.AreEqual(b.ToString(), rope.ToString()); |
||||
} |
||||
|
||||
[Test] |
||||
public void ConcatenateStringToRopeByInsertionInMiddle() |
||||
{ |
||||
StringBuilder b = new StringBuilder(); |
||||
Rope<char> rope = new Rope<char>(); |
||||
for (int i = 1; i <= 998; i++) { |
||||
b.Append(i.ToString("d3")); |
||||
b.Append(' '); |
||||
} |
||||
int middle = 0; |
||||
for (int i = 1; i <= 499; i++) { |
||||
rope.InsertText(middle, i.ToString("d3")); |
||||
middle += 3; |
||||
rope.Insert(middle, ' '); |
||||
middle++; |
||||
rope.InsertText(middle, (999-i).ToString("d3")); |
||||
rope.Insert(middle + 3, ' '); |
||||
} |
||||
Assert.AreEqual(b.ToString(), rope.ToString()); |
||||
} |
||||
|
||||
[Test] |
||||
public void ConcatenateSmallRopesByInsertionInMiddle() |
||||
{ |
||||
StringBuilder b = new StringBuilder(); |
||||
Rope<char> rope = new Rope<char>(); |
||||
for (int i = 1; i <= 1000; i++) { |
||||
b.Append(i.ToString("d3")); |
||||
b.Append(' '); |
||||
} |
||||
int middle = 0; |
||||
for (int i = 1; i <= 500; i++) { |
||||
rope.InsertRange(middle, CharRope.Create(i.ToString("d3") + " ")); |
||||
middle += 4; |
||||
rope.InsertRange(middle, CharRope.Create((1001-i).ToString("d3") + " ")); |
||||
} |
||||
Assert.AreEqual(b.ToString(), rope.ToString()); |
||||
} |
||||
} |
||||
} |
@ -1,127 +0,0 @@
@@ -1,127 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Windows.Threading; |
||||
using ICSharpCode.AvalonEdit.Document; |
||||
using ICSharpCode.AvalonEdit.Editing; |
||||
using ICSharpCode.AvalonEdit.Rendering; |
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.AvalonEdit |
||||
{ |
||||
[TestFixture] |
||||
public class WeakReferenceTests |
||||
{ |
||||
[Test] |
||||
public void TextViewCanBeCollectedTest() |
||||
{ |
||||
TextView textView = new TextView(); |
||||
WeakReference wr = new WeakReference(textView); |
||||
textView = null; |
||||
GarbageCollect(); |
||||
Assert.IsFalse(wr.IsAlive); |
||||
} |
||||
|
||||
[Test] |
||||
public void DocumentDoesNotHoldReferenceToTextView() |
||||
{ |
||||
TextDocument textDocument = new TextDocument(); |
||||
Assert.AreEqual(0, textDocument.LineTrackers.Count); |
||||
|
||||
TextView textView = new TextView(); |
||||
WeakReference wr = new WeakReference(textView); |
||||
textView.Document = textDocument; |
||||
Assert.AreEqual(1, textDocument.LineTrackers.Count); |
||||
textView = null; |
||||
|
||||
GarbageCollect(); |
||||
Assert.IsFalse(wr.IsAlive); |
||||
// document cannot immediately clear the line tracker
|
||||
Assert.AreEqual(1, textDocument.LineTrackers.Count); |
||||
|
||||
// but it should clear it on the next change
|
||||
textDocument.Insert(0, "a"); |
||||
Assert.AreEqual(0, textDocument.LineTrackers.Count); |
||||
} |
||||
|
||||
[Test] |
||||
public void DocumentDoesNotHoldReferenceToTextArea() |
||||
{ |
||||
TextDocument textDocument = new TextDocument(); |
||||
|
||||
TextArea textArea = new TextArea(); |
||||
WeakReference wr = new WeakReference(textArea); |
||||
textArea.Document = textDocument; |
||||
textArea = null; |
||||
|
||||
GarbageCollect(); |
||||
Assert.IsFalse(wr.IsAlive); |
||||
GC.KeepAlive(textDocument); |
||||
} |
||||
|
||||
[Test] |
||||
public void DocumentDoesNotHoldReferenceToTextEditor() |
||||
{ |
||||
TextDocument textDocument = new TextDocument(); |
||||
|
||||
TextEditor textEditor = new TextEditor(); |
||||
WeakReference wr = new WeakReference(textEditor); |
||||
textEditor.Document = textDocument; |
||||
textEditor = null; |
||||
|
||||
GarbageCollect(); |
||||
Assert.IsFalse(wr.IsAlive); |
||||
GC.KeepAlive(textDocument); |
||||
} |
||||
|
||||
[Test] |
||||
public void DocumentDoesNotHoldReferenceToLineMargin() |
||||
{ |
||||
TextDocument textDocument = new TextDocument(); |
||||
|
||||
WeakReference wr = DocumentDoesNotHoldReferenceToLineMargin_CreateMargin(textDocument); |
||||
|
||||
GarbageCollect(); |
||||
Assert.IsFalse(wr.IsAlive); |
||||
GC.KeepAlive(textDocument); |
||||
} |
||||
|
||||
// using a method to ensure the local variables can be garbage collected after the method returns
|
||||
WeakReference DocumentDoesNotHoldReferenceToLineMargin_CreateMargin(TextDocument textDocument) |
||||
{ |
||||
TextView textView = new TextView() { |
||||
Document = textDocument |
||||
}; |
||||
LineNumberMargin margin = new LineNumberMargin() { |
||||
TextView = textView |
||||
}; |
||||
return new WeakReference(textView); |
||||
} |
||||
|
||||
static void GarbageCollect() |
||||
{ |
||||
for (int i = 0; i < 3; i++) { |
||||
GC.WaitForPendingFinalizers(); |
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); |
||||
// pump WPF messages so that WeakEventManager can unregister
|
||||
Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Background, new Action(delegate {})); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,102 +0,0 @@
@@ -1,102 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Windows.Input; |
||||
|
||||
namespace ICSharpCode.AvalonEdit |
||||
{ |
||||
/// <summary>
|
||||
/// Custom commands for AvalonEdit.
|
||||
/// </summary>
|
||||
public static class AvalonEditCommands |
||||
{ |
||||
/// <summary>
|
||||
/// Deletes the current line.
|
||||
/// The default shortcut is Ctrl+D.
|
||||
/// </summary>
|
||||
public static readonly RoutedCommand DeleteLine = new RoutedCommand( |
||||
"DeleteLine", typeof(TextEditor), |
||||
new InputGestureCollection { |
||||
new KeyGesture(Key.D, ModifierKeys.Control) |
||||
}); |
||||
|
||||
/// <summary>
|
||||
/// Removes leading whitespace from the selected lines (or the whole document if the selection is empty).
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace", |
||||
Justification = "WPF uses 'Whitespace'")] |
||||
public static readonly RoutedCommand RemoveLeadingWhitespace = new RoutedCommand("RemoveLeadingWhitespace", typeof(TextEditor)); |
||||
|
||||
/// <summary>
|
||||
/// Removes trailing whitespace from the selected lines (or the whole document if the selection is empty).
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace", |
||||
Justification = "WPF uses 'Whitespace'")] |
||||
public static readonly RoutedCommand RemoveTrailingWhitespace = new RoutedCommand("RemoveTrailingWhitespace", typeof(TextEditor)); |
||||
|
||||
/// <summary>
|
||||
/// Converts the selected text to upper case.
|
||||
/// </summary>
|
||||
public static readonly RoutedCommand ConvertToUppercase = new RoutedCommand("ConvertToUppercase", typeof(TextEditor)); |
||||
|
||||
/// <summary>
|
||||
/// Converts the selected text to lower case.
|
||||
/// </summary>
|
||||
public static readonly RoutedCommand ConvertToLowercase = new RoutedCommand("ConvertToLowercase", typeof(TextEditor)); |
||||
|
||||
/// <summary>
|
||||
/// Converts the selected text to title case.
|
||||
/// </summary>
|
||||
public static readonly RoutedCommand ConvertToTitleCase = new RoutedCommand("ConvertToTitleCase", typeof(TextEditor)); |
||||
|
||||
/// <summary>
|
||||
/// Inverts the case of the selected text.
|
||||
/// </summary>
|
||||
public static readonly RoutedCommand InvertCase = new RoutedCommand("InvertCase", typeof(TextEditor)); |
||||
|
||||
/// <summary>
|
||||
/// Converts tabs to spaces in the selected text.
|
||||
/// </summary>
|
||||
public static readonly RoutedCommand ConvertTabsToSpaces = new RoutedCommand("ConvertTabsToSpaces", typeof(TextEditor)); |
||||
|
||||
/// <summary>
|
||||
/// Converts spaces to tabs in the selected text.
|
||||
/// </summary>
|
||||
public static readonly RoutedCommand ConvertSpacesToTabs = new RoutedCommand("ConvertSpacesToTabs", typeof(TextEditor)); |
||||
|
||||
/// <summary>
|
||||
/// Converts leading tabs to spaces in the selected lines (or the whole document if the selection is empty).
|
||||
/// </summary>
|
||||
public static readonly RoutedCommand ConvertLeadingTabsToSpaces = new RoutedCommand("ConvertLeadingTabsToSpaces", typeof(TextEditor)); |
||||
|
||||
/// <summary>
|
||||
/// Converts leading spaces to tabs in the selected lines (or the whole document if the selection is empty).
|
||||
/// </summary>
|
||||
public static readonly RoutedCommand ConvertLeadingSpacesToTabs = new RoutedCommand("ConvertLeadingSpacesToTabs", typeof(TextEditor)); |
||||
|
||||
/// <summary>
|
||||
/// Runs the IIndentationStrategy on the selected lines (or the whole document if the selection is empty).
|
||||
/// </summary>
|
||||
public static readonly RoutedCommand IndentSelection = new RoutedCommand( |
||||
"IndentSelection", typeof(TextEditor), |
||||
new InputGestureCollection { |
||||
new KeyGesture(Key.I, ModifierKeys.Control) |
||||
}); |
||||
} |
||||
} |
@ -1,416 +0,0 @@
@@ -1,416 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Collections.ObjectModel; |
||||
using System.Globalization; |
||||
using System.Windows; |
||||
using System.Windows.Controls; |
||||
using System.Windows.Controls.Primitives; |
||||
using System.Windows.Documents; |
||||
using System.Windows.Input; |
||||
using System.Linq; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.CodeCompletion |
||||
{ |
||||
/// <summary>
|
||||
/// The listbox used inside the CompletionWindow, contains CompletionListBox.
|
||||
/// </summary>
|
||||
public class CompletionList : Control |
||||
{ |
||||
static CompletionList() |
||||
{ |
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(CompletionList), |
||||
new FrameworkPropertyMetadata(typeof(CompletionList))); |
||||
} |
||||
|
||||
bool isFiltering = true; |
||||
/// <summary>
|
||||
/// If true, the CompletionList is filtered to show only matching items. Also enables search by substring.
|
||||
/// If false, enables the old behavior: no filtering, search by string.StartsWith.
|
||||
/// </summary>
|
||||
public bool IsFiltering { |
||||
get { return isFiltering; } |
||||
set { isFiltering = value; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Dependency property for <see cref="EmptyTemplate" />.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty EmptyTemplateProperty = |
||||
DependencyProperty.Register("EmptyTemplate", typeof(ControlTemplate), typeof(CompletionList), |
||||
new FrameworkPropertyMetadata()); |
||||
|
||||
/// <summary>
|
||||
/// Content of EmptyTemplate will be shown when CompletionList contains no items.
|
||||
/// If EmptyTemplate is null, nothing will be shown.
|
||||
/// </summary>
|
||||
public ControlTemplate EmptyTemplate { |
||||
get { return (ControlTemplate)GetValue(EmptyTemplateProperty); } |
||||
set { SetValue(EmptyTemplateProperty, value); } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Is raised when the completion list indicates that the user has chosen
|
||||
/// an entry to be completed.
|
||||
/// </summary>
|
||||
public event EventHandler InsertionRequested; |
||||
|
||||
/// <summary>
|
||||
/// Raises the InsertionRequested event.
|
||||
/// </summary>
|
||||
public void RequestInsertion(EventArgs e) |
||||
{ |
||||
if (InsertionRequested != null) |
||||
InsertionRequested(this, e); |
||||
} |
||||
|
||||
CompletionListBox listBox; |
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnApplyTemplate() |
||||
{ |
||||
base.OnApplyTemplate(); |
||||
|
||||
listBox = GetTemplateChild("PART_ListBox") as CompletionListBox; |
||||
if (listBox != null) { |
||||
listBox.ItemsSource = completionData; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the list box.
|
||||
/// </summary>
|
||||
public CompletionListBox ListBox { |
||||
get { |
||||
if (listBox == null) |
||||
ApplyTemplate(); |
||||
return listBox; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the scroll viewer used in this list box.
|
||||
/// </summary>
|
||||
public ScrollViewer ScrollViewer { |
||||
get { return listBox != null ? listBox.scrollViewer : null; } |
||||
} |
||||
|
||||
ObservableCollection<ICompletionData> completionData = new ObservableCollection<ICompletionData>(); |
||||
|
||||
/// <summary>
|
||||
/// Gets the list to which completion data can be added.
|
||||
/// </summary>
|
||||
public IList<ICompletionData> CompletionData { |
||||
get { return completionData; } |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnKeyDown(KeyEventArgs e) |
||||
{ |
||||
base.OnKeyDown(e); |
||||
if (!e.Handled) { |
||||
HandleKey(e); |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Handles a key press. Used to let the completion list handle key presses while the
|
||||
/// focus is still on the text editor.
|
||||
/// </summary>
|
||||
public void HandleKey(KeyEventArgs e) |
||||
{ |
||||
if (listBox == null) |
||||
return; |
||||
|
||||
// We have to do some key handling manually, because the default doesn't work with
|
||||
// our simulated events.
|
||||
// Also, the default PageUp/PageDown implementation changes the focus, so we avoid it.
|
||||
switch (e.Key) { |
||||
case Key.Down: |
||||
e.Handled = true; |
||||
listBox.SelectIndex(listBox.SelectedIndex + 1); |
||||
break; |
||||
case Key.Up: |
||||
e.Handled = true; |
||||
listBox.SelectIndex(listBox.SelectedIndex - 1); |
||||
break; |
||||
case Key.PageDown: |
||||
e.Handled = true; |
||||
listBox.SelectIndex(listBox.SelectedIndex + listBox.VisibleItemCount); |
||||
break; |
||||
case Key.PageUp: |
||||
e.Handled = true; |
||||
listBox.SelectIndex(listBox.SelectedIndex - listBox.VisibleItemCount); |
||||
break; |
||||
case Key.Home: |
||||
e.Handled = true; |
||||
listBox.SelectIndex(0); |
||||
break; |
||||
case Key.End: |
||||
e.Handled = true; |
||||
listBox.SelectIndex(listBox.Items.Count - 1); |
||||
break; |
||||
case Key.Tab: |
||||
case Key.Enter: |
||||
e.Handled = true; |
||||
RequestInsertion(e); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnMouseDoubleClick(MouseButtonEventArgs e) |
||||
{ |
||||
base.OnMouseDoubleClick(e); |
||||
if (e.ChangedButton == MouseButton.Left) { |
||||
// only process double clicks on the ListBoxItems, not on the scroll bar
|
||||
if (ExtensionMethods.VisualAncestorsAndSelf(e.OriginalSource as DependencyObject).TakeWhile(obj => obj != this).Any(obj => obj is ListBoxItem)) { |
||||
e.Handled = true; |
||||
RequestInsertion(e); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets/Sets the selected item.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The setter of this property does not scroll to the selected item.
|
||||
/// You might want to also call <see cref="ScrollIntoView"/>.
|
||||
/// </remarks>
|
||||
public ICompletionData SelectedItem { |
||||
get { |
||||
return (listBox != null ? listBox.SelectedItem : null) as ICompletionData; |
||||
} |
||||
set { |
||||
if (listBox == null && value != null) |
||||
ApplyTemplate(); |
||||
if (listBox != null) // may still be null if ApplyTemplate fails, or if listBox and value both are null
|
||||
listBox.SelectedItem = value; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Scrolls the specified item into view.
|
||||
/// </summary>
|
||||
public void ScrollIntoView(ICompletionData item) |
||||
{ |
||||
if (listBox == null) |
||||
ApplyTemplate(); |
||||
if (listBox != null) |
||||
listBox.ScrollIntoView(item); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Occurs when the SelectedItem property changes.
|
||||
/// </summary>
|
||||
public event SelectionChangedEventHandler SelectionChanged { |
||||
add { AddHandler(Selector.SelectionChangedEvent, value); } |
||||
remove { RemoveHandler(Selector.SelectionChangedEvent, value); } |
||||
} |
||||
|
||||
// SelectItem gets called twice for every typed character (once from FormatLine), this helps execute SelectItem only once
|
||||
string currentText; |
||||
ObservableCollection<ICompletionData> currentList; |
||||
|
||||
/// <summary>
|
||||
/// Selects the best match, and filter the items if turned on using <see cref="IsFiltering" />.
|
||||
/// </summary>
|
||||
public void SelectItem(string text) |
||||
{ |
||||
if (text == currentText) |
||||
return; |
||||
if (listBox == null) |
||||
ApplyTemplate(); |
||||
|
||||
if (this.IsFiltering) { |
||||
SelectItemFiltering(text); |
||||
} |
||||
else { |
||||
SelectItemWithStart(text); |
||||
} |
||||
currentText = text; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Filters CompletionList items to show only those matching given query, and selects the best match.
|
||||
/// </summary>
|
||||
void SelectItemFiltering(string query) |
||||
{ |
||||
// if the user just typed one more character, don't filter all data but just filter what we are already displaying
|
||||
var listToFilter = (this.currentList != null && (!string.IsNullOrEmpty(this.currentText)) && (!string.IsNullOrEmpty(query)) && |
||||
query.StartsWith(this.currentText, StringComparison.Ordinal)) ? |
||||
this.currentList : this.completionData; |
||||
|
||||
var matchingItems = |
||||
from item in listToFilter |
||||
let quality = GetMatchQuality(item.Text, query) |
||||
where quality > 0 |
||||
select new { Item = item, Quality = quality }; |
||||
|
||||
// e.g. "DateTimeKind k = (*cc here suggests DateTimeKind*)"
|
||||
ICompletionData suggestedItem = listBox.SelectedIndex != -1 ? (ICompletionData)(listBox.Items[listBox.SelectedIndex]) : null; |
||||
|
||||
var listBoxItems = new ObservableCollection<ICompletionData>(); |
||||
int bestIndex = -1; |
||||
int bestQuality = -1; |
||||
double bestPriority = 0; |
||||
int i = 0; |
||||
foreach (var matchingItem in matchingItems) { |
||||
double priority = matchingItem.Item == suggestedItem ? double.PositiveInfinity : matchingItem.Item.Priority; |
||||
int quality = matchingItem.Quality; |
||||
if (quality > bestQuality || (quality == bestQuality && (priority > bestPriority))) { |
||||
bestIndex = i; |
||||
bestPriority = priority; |
||||
bestQuality = quality; |
||||
} |
||||
listBoxItems.Add(matchingItem.Item); |
||||
i++; |
||||
} |
||||
this.currentList = listBoxItems; |
||||
listBox.ItemsSource = listBoxItems; |
||||
SelectIndexCentered(bestIndex); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Selects the item that starts with the specified query.
|
||||
/// </summary>
|
||||
void SelectItemWithStart(string query) |
||||
{ |
||||
if (string.IsNullOrEmpty(query)) |
||||
return; |
||||
|
||||
int suggestedIndex = listBox.SelectedIndex; |
||||
|
||||
int bestIndex = -1; |
||||
int bestQuality = -1; |
||||
double bestPriority = 0; |
||||
for (int i = 0; i < completionData.Count; ++i) { |
||||
int quality = GetMatchQuality(completionData[i].Text, query); |
||||
if (quality < 0) |
||||
continue; |
||||
|
||||
double priority = completionData[i].Priority; |
||||
bool useThisItem; |
||||
if (bestQuality < quality) { |
||||
useThisItem = true; |
||||
} else { |
||||
if (bestIndex == suggestedIndex) { |
||||
useThisItem = false; |
||||
} else if (i == suggestedIndex) { |
||||
// prefer recommendedItem, regardless of its priority
|
||||
useThisItem = bestQuality == quality; |
||||
} else { |
||||
useThisItem = bestQuality == quality && bestPriority < priority; |
||||
} |
||||
} |
||||
if (useThisItem) { |
||||
bestIndex = i; |
||||
bestPriority = priority; |
||||
bestQuality = quality; |
||||
} |
||||
} |
||||
SelectIndexCentered(bestIndex); |
||||
} |
||||
|
||||
void SelectIndexCentered(int bestIndex) |
||||
{ |
||||
if (bestIndex < 0) { |
||||
listBox.ClearSelection(); |
||||
} else { |
||||
int firstItem = listBox.FirstVisibleItem; |
||||
if (bestIndex < firstItem || firstItem + listBox.VisibleItemCount <= bestIndex) { |
||||
// CenterViewOn does nothing as CompletionListBox.ScrollViewer is null
|
||||
listBox.CenterViewOn(bestIndex); |
||||
listBox.SelectIndex(bestIndex); |
||||
} else { |
||||
listBox.SelectIndex(bestIndex); |
||||
} |
||||
} |
||||
} |
||||
|
||||
int GetMatchQuality(string itemText, string query) |
||||
{ |
||||
if (itemText == null) |
||||
throw new ArgumentNullException("itemText", "ICompletionData.Text returned null"); |
||||
|
||||
// Qualities:
|
||||
// 8 = full match case sensitive
|
||||
// 7 = full match
|
||||
// 6 = match start case sensitive
|
||||
// 5 = match start
|
||||
// 4 = match CamelCase when length of query is 1 or 2 characters
|
||||
// 3 = match substring case sensitive
|
||||
// 2 = match substring
|
||||
// 1 = match CamelCase
|
||||
// -1 = no match
|
||||
if (query == itemText) |
||||
return 8; |
||||
if (string.Equals(itemText, query, StringComparison.InvariantCultureIgnoreCase)) |
||||
return 7; |
||||
|
||||
if (itemText.StartsWith(query, StringComparison.InvariantCulture)) |
||||
return 6; |
||||
if (itemText.StartsWith(query, StringComparison.InvariantCultureIgnoreCase)) |
||||
return 5; |
||||
|
||||
bool? camelCaseMatch = null; |
||||
if (query.Length <= 2) { |
||||
camelCaseMatch = CamelCaseMatch(itemText, query); |
||||
if (camelCaseMatch == true) return 4; |
||||
} |
||||
|
||||
// search by substring, if filtering (i.e. new behavior) turned on
|
||||
if (IsFiltering) { |
||||
if (itemText.IndexOf(query, StringComparison.InvariantCulture) >= 0) |
||||
return 3; |
||||
if (itemText.IndexOf(query, StringComparison.InvariantCultureIgnoreCase) >= 0) |
||||
return 2; |
||||
} |
||||
|
||||
if (!camelCaseMatch.HasValue) |
||||
camelCaseMatch = CamelCaseMatch(itemText, query); |
||||
if (camelCaseMatch == true) |
||||
return 1; |
||||
|
||||
return -1; |
||||
} |
||||
|
||||
static bool CamelCaseMatch(string text, string query) |
||||
{ |
||||
// We take the first letter of the text regardless of whether or not it's upper case so we match
|
||||
// against camelCase text as well as PascalCase text ("cct" matches "camelCaseText")
|
||||
var theFirstLetterOfEachWord = text.Take(1).Concat(text.Skip(1).Where(char.IsUpper)); |
||||
|
||||
int i = 0; |
||||
foreach (var letter in theFirstLetterOfEachWord) { |
||||
if (i > query.Length - 1) |
||||
return true; // return true here for CamelCase partial match ("CQ" matches "CodeQualityAnalysis")
|
||||
if (char.ToUpperInvariant(query[i]) != char.ToUpperInvariant(letter)) |
||||
return false; |
||||
i++; |
||||
} |
||||
if (i >= query.Length) |
||||
return true; |
||||
return false; |
||||
} |
||||
} |
||||
} |
@ -1,56 +0,0 @@
@@ -1,56 +0,0 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:AvalonEdit="clr-namespace:ICSharpCode.AvalonEdit" |
||||
xmlns:cc="clr-namespace:ICSharpCode.AvalonEdit.CodeCompletion" |
||||
> |
||||
<Style TargetType="{x:Type ListBoxItem}" x:Key="CompletionListBoxItem"> |
||||
<Setter Property="Template"> |
||||
<Setter.Value> |
||||
<ControlTemplate TargetType="{x:Type ListBoxItem}"> |
||||
<Border Name="Bd" |
||||
Background="{TemplateBinding Background}" |
||||
BorderBrush="{TemplateBinding BorderBrush}" |
||||
BorderThickness="{TemplateBinding BorderThickness}" |
||||
Padding="{TemplateBinding Padding}" |
||||
SnapsToDevicePixels="true"> |
||||
<ContentPresenter |
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" |
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" |
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/> |
||||
</Border> |
||||
<!-- Simplified triggers: |
||||
we don't want a gray selection background when the ListBox doesn't have focus |
||||
--> |
||||
<ControlTemplate.Triggers> |
||||
<Trigger Property="IsSelected" Value="true"> |
||||
<Setter Property="Background" |
||||
Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/> |
||||
<Setter Property="Foreground" |
||||
Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/> |
||||
</Trigger> |
||||
</ControlTemplate.Triggers> |
||||
</ControlTemplate> |
||||
</Setter.Value> |
||||
</Setter> |
||||
</Style> |
||||
|
||||
<Style TargetType="{x:Type cc:CompletionList}"> |
||||
<Setter Property="Template"> |
||||
<Setter.Value> |
||||
<ControlTemplate TargetType="{x:Type cc:CompletionList}"> |
||||
<cc:CompletionListBox x:Name="PART_ListBox" |
||||
ItemContainerStyle="{StaticResource CompletionListBoxItem}"> |
||||
<ItemsControl.ItemTemplate> |
||||
<DataTemplate> |
||||
<StackPanel Orientation="Horizontal"> |
||||
<Image Source="{Binding Image}" Width="16" Height="16" Margin="0,0,2,0"/> |
||||
<ContentPresenter Content="{Binding Content}"/> |
||||
</StackPanel> |
||||
</DataTemplate> |
||||
</ItemsControl.ItemTemplate> |
||||
</cc:CompletionListBox> |
||||
</ControlTemplate> |
||||
</Setter.Value> |
||||
</Setter> |
||||
</Style> |
||||
</ResourceDictionary> |
@ -1,111 +0,0 @@
@@ -1,111 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Windows; |
||||
using System.Windows.Controls; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.CodeCompletion |
||||
{ |
||||
/// <summary>
|
||||
/// The list box used inside the CompletionList.
|
||||
/// </summary>
|
||||
public class CompletionListBox : ListBox |
||||
{ |
||||
internal ScrollViewer scrollViewer; |
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnApplyTemplate() |
||||
{ |
||||
base.OnApplyTemplate(); |
||||
|
||||
// Find the scroll viewer:
|
||||
scrollViewer = null; |
||||
if (this.VisualChildrenCount > 0) { |
||||
Border border = this.GetVisualChild(0) as Border; |
||||
if (border != null) |
||||
scrollViewer = border.Child as ScrollViewer; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the number of the first visible item.
|
||||
/// </summary>
|
||||
public int FirstVisibleItem { |
||||
get { |
||||
if (scrollViewer == null || scrollViewer.ExtentHeight == 0) { |
||||
return 0; |
||||
} else { |
||||
return (int)(this.Items.Count * scrollViewer.VerticalOffset / scrollViewer.ExtentHeight); |
||||
} |
||||
} |
||||
set { |
||||
value = value.CoerceValue(0, this.Items.Count - this.VisibleItemCount); |
||||
if (scrollViewer != null) { |
||||
scrollViewer.ScrollToVerticalOffset((double)value / this.Items.Count * scrollViewer.ExtentHeight); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the number of visible items.
|
||||
/// </summary>
|
||||
public int VisibleItemCount { |
||||
get { |
||||
if (scrollViewer == null || scrollViewer.ExtentHeight == 0) { |
||||
return 10; |
||||
} else { |
||||
return Math.Max( |
||||
3, |
||||
(int)Math.Ceiling(this.Items.Count * scrollViewer.ViewportHeight |
||||
/ scrollViewer.ExtentHeight)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Removes the selection.
|
||||
/// </summary>
|
||||
public void ClearSelection() |
||||
{ |
||||
this.SelectedIndex = -1; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Selects the item with the specified index and scrolls it into view.
|
||||
/// </summary>
|
||||
public void SelectIndex(int index) |
||||
{ |
||||
if (index >= this.Items.Count) |
||||
index = this.Items.Count - 1; |
||||
if (index < 0) |
||||
index = 0; |
||||
this.SelectedIndex = index; |
||||
this.ScrollIntoView(this.SelectedItem); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Centers the view on the item with the specified index.
|
||||
/// </summary>
|
||||
public void CenterViewOn(int index) |
||||
{ |
||||
this.FirstVisibleItem = index - VisibleItemCount / 2; |
||||
} |
||||
} |
||||
} |
@ -1,210 +0,0 @@
@@ -1,210 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Windows; |
||||
using System.Windows.Controls; |
||||
using System.Windows.Controls.Primitives; |
||||
using System.Windows.Input; |
||||
using ICSharpCode.AvalonEdit.Document; |
||||
using ICSharpCode.AvalonEdit.Editing; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.CodeCompletion |
||||
{ |
||||
/// <summary>
|
||||
/// The code completion window.
|
||||
/// </summary>
|
||||
public class CompletionWindow : CompletionWindowBase |
||||
{ |
||||
readonly CompletionList completionList = new CompletionList(); |
||||
ToolTip toolTip = new ToolTip(); |
||||
|
||||
/// <summary>
|
||||
/// Gets the completion list used in this completion window.
|
||||
/// </summary>
|
||||
public CompletionList CompletionList { |
||||
get { return completionList; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Creates a new code completion window.
|
||||
/// </summary>
|
||||
public CompletionWindow(TextArea textArea) : base(textArea) |
||||
{ |
||||
// keep height automatic
|
||||
this.CloseAutomatically = true; |
||||
this.SizeToContent = SizeToContent.Height; |
||||
this.MaxHeight = 300; |
||||
this.Width = 175; |
||||
this.Content = completionList; |
||||
// prevent user from resizing window to 0x0
|
||||
this.MinHeight = 15; |
||||
this.MinWidth = 30; |
||||
|
||||
toolTip.PlacementTarget = this; |
||||
toolTip.Placement = PlacementMode.Right; |
||||
toolTip.Closed += toolTip_Closed; |
||||
|
||||
AttachEvents(); |
||||
} |
||||
|
||||
#region ToolTip handling
|
||||
void toolTip_Closed(object sender, RoutedEventArgs e) |
||||
{ |
||||
// Clear content after tooltip is closed.
|
||||
// We cannot clear is immediately when setting IsOpen=false
|
||||
// because the tooltip uses an animation for closing.
|
||||
if (toolTip != null) |
||||
toolTip.Content = null; |
||||
} |
||||
|
||||
void completionList_SelectionChanged(object sender, SelectionChangedEventArgs e) |
||||
{ |
||||
var item = completionList.SelectedItem; |
||||
if (item == null) |
||||
return; |
||||
object description = item.Description; |
||||
if (description != null) { |
||||
string descriptionText = description as string; |
||||
if (descriptionText != null) { |
||||
toolTip.Content = new TextBlock { |
||||
Text = descriptionText, |
||||
TextWrapping = TextWrapping.Wrap |
||||
}; |
||||
} else { |
||||
toolTip.Content = description; |
||||
} |
||||
toolTip.IsOpen = true; |
||||
} else { |
||||
toolTip.IsOpen = false; |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
void completionList_InsertionRequested(object sender, EventArgs e) |
||||
{ |
||||
Close(); |
||||
// The window must close before Complete() is called.
|
||||
// If the Complete callback pushes stacked input handlers, we don't want to pop those when the CC window closes.
|
||||
var item = completionList.SelectedItem; |
||||
if (item != null) |
||||
item.Complete(this.TextArea, new AnchorSegment(this.TextArea.Document, this.StartOffset, this.EndOffset - this.StartOffset), e); |
||||
} |
||||
|
||||
void AttachEvents() |
||||
{ |
||||
this.completionList.InsertionRequested += completionList_InsertionRequested; |
||||
this.completionList.SelectionChanged += completionList_SelectionChanged; |
||||
this.TextArea.Caret.PositionChanged += CaretPositionChanged; |
||||
this.TextArea.MouseWheel += textArea_MouseWheel; |
||||
this.TextArea.PreviewTextInput += textArea_PreviewTextInput; |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void DetachEvents() |
||||
{ |
||||
this.completionList.InsertionRequested -= completionList_InsertionRequested; |
||||
this.completionList.SelectionChanged -= completionList_SelectionChanged; |
||||
this.TextArea.Caret.PositionChanged -= CaretPositionChanged; |
||||
this.TextArea.MouseWheel -= textArea_MouseWheel; |
||||
this.TextArea.PreviewTextInput -= textArea_PreviewTextInput; |
||||
base.DetachEvents(); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnClosed(EventArgs e) |
||||
{ |
||||
base.OnClosed(e); |
||||
if (toolTip != null) { |
||||
toolTip.IsOpen = false; |
||||
toolTip = null; |
||||
} |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnKeyDown(KeyEventArgs e) |
||||
{ |
||||
base.OnKeyDown(e); |
||||
if (!e.Handled) { |
||||
completionList.HandleKey(e); |
||||
} |
||||
} |
||||
|
||||
void textArea_PreviewTextInput(object sender, TextCompositionEventArgs e) |
||||
{ |
||||
e.Handled = RaiseEventPair(this, PreviewTextInputEvent, TextInputEvent, |
||||
new TextCompositionEventArgs(e.Device, e.TextComposition)); |
||||
} |
||||
|
||||
void textArea_MouseWheel(object sender, MouseWheelEventArgs e) |
||||
{ |
||||
e.Handled = RaiseEventPair(GetScrollEventTarget(), |
||||
PreviewMouseWheelEvent, MouseWheelEvent, |
||||
new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)); |
||||
} |
||||
|
||||
UIElement GetScrollEventTarget() |
||||
{ |
||||
if (completionList == null) |
||||
return this; |
||||
return completionList.ScrollViewer ?? completionList.ListBox ?? (UIElement)completionList; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets/Sets whether the completion window should close automatically.
|
||||
/// The default value is true.
|
||||
/// </summary>
|
||||
public bool CloseAutomatically { get; set; } |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool CloseOnFocusLost { |
||||
get { return this.CloseAutomatically; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// When this flag is set, code completion closes if the caret moves to the
|
||||
/// beginning of the allowed range. This is useful in Ctrl+Space and "complete when typing",
|
||||
/// but not in dot-completion.
|
||||
/// Has no effect if CloseAutomatically is false.
|
||||
/// </summary>
|
||||
public bool CloseWhenCaretAtBeginning { get; set; } |
||||
|
||||
void CaretPositionChanged(object sender, EventArgs e) |
||||
{ |
||||
int offset = this.TextArea.Caret.Offset; |
||||
if (offset == this.StartOffset) { |
||||
if (CloseAutomatically && CloseWhenCaretAtBeginning) { |
||||
Close(); |
||||
} else { |
||||
completionList.SelectItem(string.Empty); |
||||
} |
||||
return; |
||||
} |
||||
if (offset < this.StartOffset || offset > this.EndOffset) { |
||||
if (CloseAutomatically) { |
||||
Close(); |
||||
} |
||||
} else { |
||||
TextDocument document = this.TextArea.Document; |
||||
if (document != null) { |
||||
completionList.SelectItem(document.GetText(this.StartOffset, offset - this.StartOffset)); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,397 +0,0 @@
@@ -1,397 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Diagnostics; |
||||
using System.Linq; |
||||
using System.Windows; |
||||
using System.Windows.Controls.Primitives; |
||||
using System.Windows.Input; |
||||
using System.Windows.Threading; |
||||
|
||||
using ICSharpCode.AvalonEdit.Document; |
||||
using ICSharpCode.AvalonEdit.Editing; |
||||
using ICSharpCode.AvalonEdit.Rendering; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
using ICSharpCode.NRefactory.Editor; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.CodeCompletion |
||||
{ |
||||
/// <summary>
|
||||
/// Base class for completion windows. Handles positioning the window at the caret.
|
||||
/// </summary>
|
||||
public class CompletionWindowBase : Window |
||||
{ |
||||
static CompletionWindowBase() |
||||
{ |
||||
WindowStyleProperty.OverrideMetadata(typeof(CompletionWindowBase), new FrameworkPropertyMetadata(WindowStyle.None)); |
||||
ShowActivatedProperty.OverrideMetadata(typeof(CompletionWindowBase), new FrameworkPropertyMetadata(Boxes.False)); |
||||
ShowInTaskbarProperty.OverrideMetadata(typeof(CompletionWindowBase), new FrameworkPropertyMetadata(Boxes.False)); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the parent TextArea.
|
||||
/// </summary>
|
||||
public TextArea TextArea { get; private set; } |
||||
|
||||
Window parentWindow; |
||||
TextDocument document; |
||||
|
||||
/// <summary>
|
||||
/// Gets/Sets the start of the text range in which the completion window stays open.
|
||||
/// This text portion is used to determine the text used to select an entry in the completion list by typing.
|
||||
/// </summary>
|
||||
public int StartOffset { get; set; } |
||||
|
||||
/// <summary>
|
||||
/// Gets/Sets the end of the text range in which the completion window stays open.
|
||||
/// This text portion is used to determine the text used to select an entry in the completion list by typing.
|
||||
/// </summary>
|
||||
public int EndOffset { get; set; } |
||||
|
||||
/// <summary>
|
||||
/// Gets whether the window was opened above the current line.
|
||||
/// </summary>
|
||||
protected bool IsUp { get; private set; } |
||||
|
||||
/// <summary>
|
||||
/// Creates a new CompletionWindowBase.
|
||||
/// </summary>
|
||||
public CompletionWindowBase(TextArea textArea) |
||||
{ |
||||
if (textArea == null) |
||||
throw new ArgumentNullException("textArea"); |
||||
this.TextArea = textArea; |
||||
parentWindow = Window.GetWindow(textArea); |
||||
this.Owner = parentWindow; |
||||
this.AddHandler(MouseUpEvent, new MouseButtonEventHandler(OnMouseUp), true); |
||||
|
||||
StartOffset = EndOffset = this.TextArea.Caret.Offset; |
||||
|
||||
AttachEvents(); |
||||
} |
||||
|
||||
#region Event Handlers
|
||||
void AttachEvents() |
||||
{ |
||||
document = this.TextArea.Document; |
||||
if (document != null) { |
||||
document.Changing += textArea_Document_Changing; |
||||
} |
||||
// LostKeyboardFocus seems to be more reliable than PreviewLostKeyboardFocus - see SD-1729
|
||||
this.TextArea.LostKeyboardFocus += TextAreaLostFocus; |
||||
this.TextArea.TextView.ScrollOffsetChanged += TextViewScrollOffsetChanged; |
||||
this.TextArea.DocumentChanged += TextAreaDocumentChanged; |
||||
if (parentWindow != null) { |
||||
parentWindow.LocationChanged += parentWindow_LocationChanged; |
||||
} |
||||
|
||||
// close previous completion windows of same type
|
||||
foreach (InputHandler x in this.TextArea.StackedInputHandlers.OfType<InputHandler>()) { |
||||
if (x.window.GetType() == this.GetType()) |
||||
this.TextArea.PopStackedInputHandler(x); |
||||
} |
||||
|
||||
myInputHandler = new InputHandler(this); |
||||
this.TextArea.PushStackedInputHandler(myInputHandler); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Detaches events from the text area.
|
||||
/// </summary>
|
||||
protected virtual void DetachEvents() |
||||
{ |
||||
if (document != null) { |
||||
document.Changing -= textArea_Document_Changing; |
||||
} |
||||
this.TextArea.LostKeyboardFocus -= TextAreaLostFocus; |
||||
this.TextArea.TextView.ScrollOffsetChanged -= TextViewScrollOffsetChanged; |
||||
this.TextArea.DocumentChanged -= TextAreaDocumentChanged; |
||||
if (parentWindow != null) { |
||||
parentWindow.LocationChanged -= parentWindow_LocationChanged; |
||||
} |
||||
this.TextArea.PopStackedInputHandler(myInputHandler); |
||||
} |
||||
|
||||
#region InputHandler
|
||||
InputHandler myInputHandler; |
||||
|
||||
/// <summary>
|
||||
/// A dummy input handler (that justs invokes the default input handler).
|
||||
/// This is used to ensure the completion window closes when any other input handler
|
||||
/// becomes active.
|
||||
/// </summary>
|
||||
sealed class InputHandler : TextAreaStackedInputHandler |
||||
{ |
||||
internal readonly CompletionWindowBase window; |
||||
|
||||
public InputHandler(CompletionWindowBase window) |
||||
: base(window.TextArea) |
||||
{ |
||||
Debug.Assert(window != null); |
||||
this.window = window; |
||||
} |
||||
|
||||
public override void Detach() |
||||
{ |
||||
base.Detach(); |
||||
window.Close(); |
||||
} |
||||
|
||||
const Key KeyDeadCharProcessed = (Key)0xac; // Key.DeadCharProcessed; // new in .NET 4
|
||||
|
||||
public override void OnPreviewKeyDown(KeyEventArgs e) |
||||
{ |
||||
// prevents crash when typing deadchar while CC window is open
|
||||
if (e.Key == KeyDeadCharProcessed) |
||||
return; |
||||
e.Handled = RaiseEventPair(window, PreviewKeyDownEvent, KeyDownEvent, |
||||
new KeyEventArgs(e.KeyboardDevice, e.InputSource, e.Timestamp, e.Key)); |
||||
} |
||||
|
||||
public override void OnPreviewKeyUp(KeyEventArgs e) |
||||
{ |
||||
if (e.Key == KeyDeadCharProcessed) |
||||
return; |
||||
e.Handled = RaiseEventPair(window, PreviewKeyUpEvent, KeyUpEvent, |
||||
new KeyEventArgs(e.KeyboardDevice, e.InputSource, e.Timestamp, e.Key)); |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
void TextViewScrollOffsetChanged(object sender, EventArgs e) |
||||
{ |
||||
// Workaround for crash #1580 (reproduction steps unknown):
|
||||
// NullReferenceException in System.Windows.Window.CreateSourceWindow()
|
||||
if (!sourceIsInitialized) |
||||
return; |
||||
|
||||
IScrollInfo scrollInfo = this.TextArea.TextView; |
||||
Rect visibleRect = new Rect(scrollInfo.HorizontalOffset, scrollInfo.VerticalOffset, scrollInfo.ViewportWidth, scrollInfo.ViewportHeight); |
||||
// close completion window when the user scrolls so far that the anchor position is leaving the visible area
|
||||
if (visibleRect.Contains(visualLocation) || visibleRect.Contains(visualLocationTop)) |
||||
UpdatePosition(); |
||||
else |
||||
Close(); |
||||
} |
||||
|
||||
void TextAreaDocumentChanged(object sender, EventArgs e) |
||||
{ |
||||
Close(); |
||||
} |
||||
|
||||
void TextAreaLostFocus(object sender, RoutedEventArgs e) |
||||
{ |
||||
Dispatcher.BeginInvoke(new Action(CloseIfFocusLost), DispatcherPriority.Background); |
||||
} |
||||
|
||||
void parentWindow_LocationChanged(object sender, EventArgs e) |
||||
{ |
||||
UpdatePosition(); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnDeactivated(EventArgs e) |
||||
{ |
||||
base.OnDeactivated(e); |
||||
Dispatcher.BeginInvoke(new Action(CloseIfFocusLost), DispatcherPriority.Background); |
||||
} |
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Raises a tunnel/bubble event pair for a WPF control.
|
||||
/// </summary>
|
||||
/// <param name="target">The WPF control for which the event should be raised.</param>
|
||||
/// <param name="previewEvent">The tunneling event.</param>
|
||||
/// <param name="event">The bubbling event.</param>
|
||||
/// <param name="args">The event args to use.</param>
|
||||
/// <returns>The <see cref="RoutedEventArgs.Handled"/> value of the event args.</returns>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate")] |
||||
protected static bool RaiseEventPair(UIElement target, RoutedEvent previewEvent, RoutedEvent @event, RoutedEventArgs args) |
||||
{ |
||||
if (target == null) |
||||
throw new ArgumentNullException("target"); |
||||
if (previewEvent == null) |
||||
throw new ArgumentNullException("previewEvent"); |
||||
if (@event == null) |
||||
throw new ArgumentNullException("event"); |
||||
if (args == null) |
||||
throw new ArgumentNullException("args"); |
||||
args.RoutedEvent = previewEvent; |
||||
target.RaiseEvent(args); |
||||
args.RoutedEvent = @event; |
||||
target.RaiseEvent(args); |
||||
return args.Handled; |
||||
} |
||||
|
||||
// Special handler: handledEventsToo
|
||||
void OnMouseUp(object sender, MouseButtonEventArgs e) |
||||
{ |
||||
ActivateParentWindow(); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Activates the parent window.
|
||||
/// </summary>
|
||||
protected virtual void ActivateParentWindow() |
||||
{ |
||||
if (parentWindow != null) |
||||
parentWindow.Activate(); |
||||
} |
||||
|
||||
void CloseIfFocusLost() |
||||
{ |
||||
if (CloseOnFocusLost) { |
||||
Debug.WriteLine("CloseIfFocusLost: this.IsActive=" + this.IsActive + " IsTextAreaFocused=" + IsTextAreaFocused); |
||||
if (!this.IsActive && !IsTextAreaFocused) { |
||||
Close(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets whether the completion window should automatically close when the text editor looses focus.
|
||||
/// </summary>
|
||||
protected virtual bool CloseOnFocusLost { |
||||
get { return true; } |
||||
} |
||||
|
||||
bool IsTextAreaFocused { |
||||
get { |
||||
if (parentWindow != null && !parentWindow.IsActive) |
||||
return false; |
||||
return this.TextArea.IsKeyboardFocused; |
||||
} |
||||
} |
||||
|
||||
bool sourceIsInitialized; |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnSourceInitialized(EventArgs e) |
||||
{ |
||||
base.OnSourceInitialized(e); |
||||
|
||||
if (document != null && this.StartOffset != this.TextArea.Caret.Offset) { |
||||
SetPosition(new TextViewPosition(document.GetLocation(this.StartOffset))); |
||||
} else { |
||||
SetPosition(this.TextArea.Caret.Position); |
||||
} |
||||
sourceIsInitialized = true; |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnClosed(EventArgs e) |
||||
{ |
||||
base.OnClosed(e); |
||||
DetachEvents(); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnKeyDown(KeyEventArgs e) |
||||
{ |
||||
base.OnKeyDown(e); |
||||
if (!e.Handled && e.Key == Key.Escape) { |
||||
e.Handled = true; |
||||
Close(); |
||||
} |
||||
} |
||||
|
||||
Point visualLocation, visualLocationTop; |
||||
|
||||
/// <summary>
|
||||
/// Positions the completion window at the specified position.
|
||||
/// </summary>
|
||||
protected void SetPosition(TextViewPosition position) |
||||
{ |
||||
TextView textView = this.TextArea.TextView; |
||||
|
||||
visualLocation = textView.GetVisualPosition(position, VisualYPosition.LineBottom); |
||||
visualLocationTop = textView.GetVisualPosition(position, VisualYPosition.LineTop); |
||||
UpdatePosition(); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Updates the position of the CompletionWindow based on the parent TextView position and the screen working area.
|
||||
/// It ensures that the CompletionWindow is completely visible on the screen.
|
||||
/// </summary>
|
||||
protected void UpdatePosition() |
||||
{ |
||||
TextView textView = this.TextArea.TextView; |
||||
// PointToScreen returns device dependent units (physical pixels)
|
||||
Point location = textView.PointToScreen(visualLocation - textView.ScrollOffset); |
||||
Point locationTop = textView.PointToScreen(visualLocationTop - textView.ScrollOffset); |
||||
|
||||
// Let's use device dependent units for everything
|
||||
Size completionWindowSize = new Size(this.ActualWidth, this.ActualHeight).TransformToDevice(textView); |
||||
Rect bounds = new Rect(location, completionWindowSize); |
||||
Rect workingScreen = System.Windows.Forms.Screen.GetWorkingArea(location.ToSystemDrawing()).ToWpf(); |
||||
if (!workingScreen.Contains(bounds)) { |
||||
if (bounds.Left < workingScreen.Left) { |
||||
bounds.X = workingScreen.Left; |
||||
} else if (bounds.Right > workingScreen.Right) { |
||||
bounds.X = workingScreen.Right - bounds.Width; |
||||
} |
||||
if (bounds.Bottom > workingScreen.Bottom) { |
||||
bounds.Y = locationTop.Y - bounds.Height; |
||||
IsUp = true; |
||||
} else { |
||||
IsUp = false; |
||||
} |
||||
if (bounds.Y < workingScreen.Top) { |
||||
bounds.Y = workingScreen.Top; |
||||
} |
||||
} |
||||
// Convert the window bounds to device independent units
|
||||
bounds = bounds.TransformFromDevice(textView); |
||||
this.Left = bounds.X; |
||||
this.Top = bounds.Y; |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) |
||||
{ |
||||
base.OnRenderSizeChanged(sizeInfo); |
||||
if (sizeInfo.HeightChanged && IsUp) { |
||||
this.Top += sizeInfo.PreviousSize.Height - sizeInfo.NewSize.Height; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets/sets whether the completion window should expect text insertion at the start offset,
|
||||
/// which not go into the completion region, but before it.
|
||||
/// </summary>
|
||||
/// <remarks>This property allows only a single insertion, it is reset to false
|
||||
/// when that insertion has occurred.</remarks>
|
||||
public bool ExpectInsertionBeforeStart { get; set; } |
||||
|
||||
void textArea_Document_Changing(object sender, DocumentChangeEventArgs e) |
||||
{ |
||||
if (e.Offset + e.RemovalLength == this.StartOffset && e.RemovalLength > 0) { |
||||
Close(); // removal immediately in front of completion segment: close the window
|
||||
// this is necessary when pressing backspace after dot-completion
|
||||
} |
||||
if (e.Offset == StartOffset && e.RemovalLength == 0 && ExpectInsertionBeforeStart) { |
||||
StartOffset = e.GetNewOffset(StartOffset, AnchorMovementType.AfterInsertion); |
||||
this.ExpectInsertionBeforeStart = false; |
||||
} else { |
||||
StartOffset = e.GetNewOffset(StartOffset, AnchorMovementType.BeforeInsertion); |
||||
} |
||||
EndOffset = e.GetNewOffset(EndOffset, AnchorMovementType.AfterInsertion); |
||||
} |
||||
} |
||||
} |
@ -1,73 +0,0 @@
@@ -1,73 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Windows.Media; |
||||
using ICSharpCode.AvalonEdit.Editing; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory.Editor; |
||||
#else
|
||||
using ICSharpCode.AvalonEdit.Document; |
||||
#endif
|
||||
|
||||
namespace ICSharpCode.AvalonEdit.CodeCompletion |
||||
{ |
||||
/// <summary>
|
||||
/// Describes an entry in the <see cref="CompletionList"/>.
|
||||
/// </summary>
|
||||
public interface ICompletionData |
||||
{ |
||||
/// <summary>
|
||||
/// Gets the image.
|
||||
/// </summary>
|
||||
ImageSource Image { get; } |
||||
|
||||
/// <summary>
|
||||
/// Gets the text. This property is used to filter the list of visible elements.
|
||||
/// </summary>
|
||||
string Text { get; } |
||||
|
||||
/// <summary>
|
||||
/// The displayed content. This can be the same as 'Text', or a WPF UIElement if
|
||||
/// you want to display rich content.
|
||||
/// </summary>
|
||||
object Content { get; } |
||||
|
||||
/// <summary>
|
||||
/// Gets the description.
|
||||
/// </summary>
|
||||
object Description { get; } |
||||
|
||||
/// <summary>
|
||||
/// Gets the priority. This property is used in the selection logic. You can use it to prefer selecting those items
|
||||
/// which the user is accessing most frequently.
|
||||
/// </summary>
|
||||
double Priority { get; } |
||||
|
||||
/// <summary>
|
||||
/// Perform the completion.
|
||||
/// </summary>
|
||||
/// <param name="textArea">The text area on which completion is performed.</param>
|
||||
/// <param name="completionSegment">The text segment that was used by the completion window if
|
||||
/// the user types (segment between CompletionWindow.StartOffset and CompletionWindow.EndOffset).</param>
|
||||
/// <param name="insertionRequestEventArgs">The EventArgs used for the insertion request.
|
||||
/// These can be TextCompositionEventArgs, KeyEventArgs, MouseEventArgs, depending on how
|
||||
/// the insertion was triggered.</param>
|
||||
void Complete(TextArea textArea, ISegment completionSegment, EventArgs insertionRequestEventArgs); |
||||
} |
||||
} |
@ -1,57 +0,0 @@
@@ -1,57 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.ObjectModel; |
||||
using System.ComponentModel; |
||||
using System.Windows; |
||||
using System.Windows.Controls; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.CodeCompletion |
||||
{ |
||||
/// <summary>
|
||||
/// Provides the items for the OverloadViewer.
|
||||
/// </summary>
|
||||
public interface IOverloadProvider : INotifyPropertyChanged |
||||
{ |
||||
/// <summary>
|
||||
/// Gets/Sets the selected index.
|
||||
/// </summary>
|
||||
int SelectedIndex { get; set; } |
||||
|
||||
/// <summary>
|
||||
/// Gets the number of overloads.
|
||||
/// </summary>
|
||||
int Count { get; } |
||||
|
||||
/// <summary>
|
||||
/// Gets the text 'SelectedIndex of Count'.
|
||||
/// </summary>
|
||||
string CurrentIndexText { get; } |
||||
|
||||
/// <summary>
|
||||
/// Gets the current header.
|
||||
/// </summary>
|
||||
object CurrentHeader { get; } |
||||
|
||||
/// <summary>
|
||||
/// Gets the current content.
|
||||
/// </summary>
|
||||
object CurrentContent { get; } |
||||
} |
||||
} |
@ -1,109 +0,0 @@
@@ -1,109 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Windows; |
||||
using System.Windows.Controls; |
||||
using ICSharpCode.AvalonEdit.Editing; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.CodeCompletion |
||||
{ |
||||
/// <summary>
|
||||
/// A popup-like window that is attached to a text segment.
|
||||
/// </summary>
|
||||
public class InsightWindow : CompletionWindowBase |
||||
{ |
||||
static InsightWindow() |
||||
{ |
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(InsightWindow), |
||||
new FrameworkPropertyMetadata(typeof(InsightWindow))); |
||||
AllowsTransparencyProperty.OverrideMetadata(typeof(InsightWindow), |
||||
new FrameworkPropertyMetadata(Boxes.True)); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Creates a new InsightWindow.
|
||||
/// </summary>
|
||||
public InsightWindow(TextArea textArea) : base(textArea) |
||||
{ |
||||
this.CloseAutomatically = true; |
||||
AttachEvents(); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnSourceInitialized(EventArgs e) |
||||
{ |
||||
Rect caret = this.TextArea.Caret.CalculateCaretRectangle(); |
||||
Point pointOnScreen = this.TextArea.TextView.PointToScreen(caret.Location - this.TextArea.TextView.ScrollOffset); |
||||
Rect workingArea = System.Windows.Forms.Screen.FromPoint(pointOnScreen.ToSystemDrawing()).WorkingArea.ToWpf().TransformFromDevice(this); |
||||
|
||||
MaxHeight = workingArea.Height; |
||||
MaxWidth = Math.Min(workingArea.Width, Math.Max(1000, workingArea.Width * 0.6)); |
||||
|
||||
base.OnSourceInitialized(e); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets/Sets whether the insight window should close automatically.
|
||||
/// The default value is true.
|
||||
/// </summary>
|
||||
public bool CloseAutomatically { get; set; } |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool CloseOnFocusLost { |
||||
get { return this.CloseAutomatically; } |
||||
} |
||||
|
||||
void AttachEvents() |
||||
{ |
||||
this.TextArea.Caret.PositionChanged += CaretPositionChanged; |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void DetachEvents() |
||||
{ |
||||
this.TextArea.Caret.PositionChanged -= CaretPositionChanged; |
||||
base.DetachEvents(); |
||||
} |
||||
|
||||
void CaretPositionChanged(object sender, EventArgs e) |
||||
{ |
||||
if (this.CloseAutomatically) { |
||||
int offset = this.TextArea.Caret.Offset; |
||||
if (offset < this.StartOffset || offset > this.EndOffset) { |
||||
Close(); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// TemplateSelector for InsightWindow to replace plain string content by a TextBlock with TextWrapping.
|
||||
/// </summary>
|
||||
internal sealed class InsightWindowTemplateSelector : DataTemplateSelector |
||||
{ |
||||
public override DataTemplate SelectTemplate(object item, DependencyObject container) |
||||
{ |
||||
if (item is string) |
||||
return (DataTemplate)((FrameworkElement)container).FindResource("TextBlockTemplate"); |
||||
|
||||
return null; |
||||
} |
||||
} |
||||
} |
@ -1,118 +0,0 @@
@@ -1,118 +0,0 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:cc="clr-namespace:ICSharpCode.AvalonEdit.CodeCompletion" |
||||
> |
||||
<cc:InsightWindowTemplateSelector x:Key="templateSelector" /> |
||||
|
||||
<!-- Template for InsightWindow. Based on the template for ToolTip. --> |
||||
<Style TargetType="{x:Type cc:InsightWindow}"> |
||||
<Setter Property="SizeToContent" Value="WidthAndHeight" /> |
||||
|
||||
<Setter Property="BorderThickness" Value="1" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource {x:Static SystemColors.WindowFrameBrushKey}}" /> |
||||
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.InfoBrushKey}}" /> |
||||
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.InfoTextBrushKey}}" /> |
||||
<Setter Property="FontFamily" Value="{DynamicResource {x:Static SystemFonts.StatusFontFamilyKey}}" /> |
||||
<Setter Property="FontSize" Value="{DynamicResource {x:Static SystemFonts.StatusFontSizeKey}}" /> |
||||
<Setter Property="FontStyle" Value="{DynamicResource {x:Static SystemFonts.StatusFontStyleKey}}" /> |
||||
<Setter Property="FontWeight" Value="{DynamicResource {x:Static SystemFonts.StatusFontWeightKey}}" /> |
||||
<Setter Property="Padding" Value="1,1,3,1" /> |
||||
<Setter Property="HorizontalContentAlignment" Value="Left" /> |
||||
<Setter Property="VerticalContentAlignment" Value="Center" /> |
||||
<Setter Property="Template"> |
||||
<Setter.Value> |
||||
<ControlTemplate TargetType="{x:Type cc:InsightWindow}"> |
||||
<Border BorderThickness="{TemplateBinding Border.BorderThickness}" |
||||
Padding="{TemplateBinding Control.Padding}" |
||||
CornerRadius="2,2,2,2" |
||||
BorderBrush="{TemplateBinding Border.BorderBrush}" |
||||
Background="{TemplateBinding Panel.Background}"> |
||||
<AdornerDecorator> |
||||
<ContentPresenter |
||||
Content="{TemplateBinding ContentControl.Content}" |
||||
ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}" |
||||
ContentStringFormat="{TemplateBinding ContentControl.ContentStringFormat}" |
||||
HorizontalAlignment="{TemplateBinding Control.HorizontalContentAlignment}" |
||||
VerticalAlignment="{TemplateBinding Control.VerticalContentAlignment}" |
||||
SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" /> |
||||
</AdornerDecorator> |
||||
</Border> |
||||
</ControlTemplate> |
||||
</Setter.Value> |
||||
</Setter> |
||||
</Style> |
||||
|
||||
<!-- Template for OverloadViewer. --> |
||||
<Style TargetType="{x:Type cc:OverloadViewer}"> |
||||
<Setter Property="Template"> |
||||
<Setter.Value> |
||||
<ControlTemplate TargetType="{x:Type cc:OverloadViewer}"> |
||||
<Grid> |
||||
<Grid.Resources> |
||||
<cc:CollapseIfSingleOverloadConverter x:Key="collapseIfSingleOverloadConverter"/> |
||||
|
||||
<!-- Style of the UpDownButton --> |
||||
<Style TargetType="{x:Type Button}" x:Key="upDownButtonStyle"> |
||||
<Style.Setters> |
||||
<Setter Property="Background" Value="LightGray"/> |
||||
<Setter Property="Padding" Value="2,2,2,2"/> |
||||
<Setter Property="Width" Value="9"/> |
||||
<Setter Property="Height" Value="9"/> |
||||
<Setter Property="SnapsToDevicePixels" Value="True"/> |
||||
<Setter Property="OverridesDefaultStyle" Value="True"/> |
||||
<Setter Property="Template"> |
||||
<Setter.Value> |
||||
<ControlTemplate TargetType="{x:Type Button}"> |
||||
<Border Name="bd" |
||||
Background="{TemplateBinding Background}" CornerRadius="2"> |
||||
<ContentPresenter Margin="{TemplateBinding Padding}" |
||||
Content="{TemplateBinding Content}"/> |
||||
</Border> |
||||
<ControlTemplate.Triggers> |
||||
<Trigger Property="IsMouseOver" Value="true"> |
||||
<Setter TargetName="bd" Property="Background" Value="LightBlue"/> |
||||
</Trigger> |
||||
</ControlTemplate.Triggers> |
||||
</ControlTemplate> |
||||
</Setter.Value> |
||||
</Setter> |
||||
</Style.Setters> |
||||
</Style> |
||||
|
||||
<DataTemplate x:Key="TextBlockTemplate"> |
||||
<TextBlock TextWrapping="Wrap" Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Content}" /> |
||||
</DataTemplate> |
||||
</Grid.Resources> |
||||
|
||||
<Grid.ColumnDefinitions> |
||||
<ColumnDefinition Width="Auto"/> |
||||
<ColumnDefinition Width="1*"/> |
||||
</Grid.ColumnDefinitions> |
||||
<Grid.RowDefinitions> |
||||
<RowDefinition Height="Auto"/> |
||||
<RowDefinition Height="1*"/> |
||||
</Grid.RowDefinitions> |
||||
|
||||
<StackPanel Grid.Row="0" Grid.Column="0" |
||||
Margin="0,0,4,0" |
||||
Orientation="Horizontal" |
||||
Visibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Provider.Count, Converter={StaticResource collapseIfSingleOverloadConverter}}"> |
||||
<Button Name="PART_UP" Style="{StaticResource upDownButtonStyle}"> |
||||
<Path Stroke="Black" Fill="Black" Data="M 0,0.866 L 1,0.866 L 0.5,0 Z" Stretch="UniformToFill" /> |
||||
</Button> |
||||
<TextBlock Margin="2,0,2,0" |
||||
Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Provider.CurrentIndexText}"/> |
||||
<Button Name="PART_DOWN" Style="{StaticResource upDownButtonStyle}"> |
||||
<Path Stroke="Black" Fill="Black" Data="M 0,0 L 1,0 L 0.5,0.866 Z" Stretch="UniformToFill" /> |
||||
</Button> |
||||
</StackPanel> |
||||
<ContentPresenter Grid.Row="0" Grid.Column="1" ContentTemplateSelector="{StaticResource templateSelector}" |
||||
Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Provider.CurrentHeader}" /> |
||||
<ContentPresenter Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" ContentTemplateSelector="{StaticResource templateSelector}" |
||||
Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Provider.CurrentContent}" /> |
||||
</Grid> |
||||
</ControlTemplate> |
||||
</Setter.Value> |
||||
</Setter> |
||||
</Style> |
||||
</ResourceDictionary> |
@ -1,73 +0,0 @@
@@ -1,73 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Windows; |
||||
using System.Windows.Input; |
||||
|
||||
using ICSharpCode.AvalonEdit.Editing; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.CodeCompletion |
||||
{ |
||||
/// <summary>
|
||||
/// Insight window that shows an OverloadViewer.
|
||||
/// </summary>
|
||||
public class OverloadInsightWindow : InsightWindow |
||||
{ |
||||
OverloadViewer overloadViewer = new OverloadViewer(); |
||||
|
||||
/// <summary>
|
||||
/// Creates a new OverloadInsightWindow.
|
||||
/// </summary>
|
||||
public OverloadInsightWindow(TextArea textArea) : base(textArea) |
||||
{ |
||||
overloadViewer.Margin = new Thickness(2,0,0,0); |
||||
this.Content = overloadViewer; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets/Sets the item provider.
|
||||
/// </summary>
|
||||
public IOverloadProvider Provider { |
||||
get { return overloadViewer.Provider; } |
||||
set { overloadViewer.Provider = value; } |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnKeyDown(KeyEventArgs e) |
||||
{ |
||||
base.OnKeyDown(e); |
||||
if (!e.Handled && this.Provider != null && this.Provider.Count > 1) { |
||||
switch (e.Key) { |
||||
case Key.Up: |
||||
e.Handled = true; |
||||
overloadViewer.ChangeIndex(-1); |
||||
break; |
||||
case Key.Down: |
||||
e.Handled = true; |
||||
overloadViewer.ChangeIndex(+1); |
||||
break; |
||||
} |
||||
if (e.Handled) { |
||||
UpdateLayout(); |
||||
UpdatePosition(); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,116 +0,0 @@
@@ -1,116 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.ObjectModel; |
||||
using System.ComponentModel; |
||||
using System.Globalization; |
||||
using System.Windows; |
||||
using System.Windows.Controls; |
||||
using System.Windows.Data; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.CodeCompletion |
||||
{ |
||||
/// <summary>
|
||||
/// Represents a text between "Up" and "Down" buttons.
|
||||
/// </summary>
|
||||
public class OverloadViewer : Control |
||||
{ |
||||
static OverloadViewer() |
||||
{ |
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(OverloadViewer), |
||||
new FrameworkPropertyMetadata(typeof(OverloadViewer))); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// The text property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty TextProperty = |
||||
DependencyProperty.Register("Text", typeof(string), typeof(OverloadViewer)); |
||||
|
||||
/// <summary>
|
||||
/// Gets/Sets the text between the Up and Down buttons.
|
||||
/// </summary>
|
||||
public string Text { |
||||
get { return (string)GetValue(TextProperty); } |
||||
set { SetValue(TextProperty, value); } |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnApplyTemplate() |
||||
{ |
||||
base.OnApplyTemplate(); |
||||
|
||||
Button upButton = (Button)this.Template.FindName("PART_UP", this); |
||||
upButton.Click += (sender, e) => { |
||||
e.Handled = true; |
||||
ChangeIndex(-1); |
||||
}; |
||||
|
||||
Button downButton = (Button)this.Template.FindName("PART_DOWN", this); |
||||
downButton.Click += (sender, e) => { |
||||
e.Handled = true; |
||||
ChangeIndex(+1); |
||||
}; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// The ItemProvider property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty ProviderProperty = |
||||
DependencyProperty.Register("Provider", typeof(IOverloadProvider), typeof(OverloadViewer)); |
||||
|
||||
/// <summary>
|
||||
/// Gets/Sets the item provider.
|
||||
/// </summary>
|
||||
public IOverloadProvider Provider { |
||||
get { return (IOverloadProvider)GetValue(ProviderProperty); } |
||||
set { SetValue(ProviderProperty, value); } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Changes the selected index.
|
||||
/// </summary>
|
||||
/// <param name="relativeIndexChange">The relative index change - usual values are +1 or -1.</param>
|
||||
public void ChangeIndex(int relativeIndexChange) |
||||
{ |
||||
IOverloadProvider p = this.Provider; |
||||
if (p != null) { |
||||
int newIndex = p.SelectedIndex + relativeIndexChange; |
||||
if (newIndex < 0) |
||||
newIndex = p.Count - 1; |
||||
if (newIndex >= p.Count) |
||||
newIndex = 0; |
||||
p.SelectedIndex = newIndex; |
||||
} |
||||
} |
||||
} |
||||
|
||||
sealed class CollapseIfSingleOverloadConverter : IValueConverter |
||||
{ |
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) |
||||
{ |
||||
return ((int)value < 2) ? Visibility.Collapsed : Visibility.Visible; |
||||
} |
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) |
||||
{ |
||||
throw new NotImplementedException(); |
||||
} |
||||
} |
||||
} |
@ -1,123 +0,0 @@
@@ -1,123 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using ICSharpCode.AvalonEdit.Document; |
||||
using ICSharpCode.NRefactory.Editor; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
/// <summary>
|
||||
/// Describes a change of the document text.
|
||||
/// This class is thread-safe.
|
||||
/// </summary>
|
||||
[Serializable] |
||||
public class DocumentChangeEventArgs : TextChangeEventArgs |
||||
{ |
||||
volatile OffsetChangeMap offsetChangeMap; |
||||
|
||||
/// <summary>
|
||||
/// Gets the OffsetChangeMap associated with this document change.
|
||||
/// </summary>
|
||||
/// <remarks>The OffsetChangeMap instance is guaranteed to be frozen and thus thread-safe.</remarks>
|
||||
public OffsetChangeMap OffsetChangeMap { |
||||
get { |
||||
OffsetChangeMap map = offsetChangeMap; |
||||
if (map == null) { |
||||
// create OffsetChangeMap on demand
|
||||
map = OffsetChangeMap.FromSingleElement(CreateSingleChangeMapEntry()); |
||||
offsetChangeMap = map; |
||||
} |
||||
return map; |
||||
} |
||||
} |
||||
|
||||
internal OffsetChangeMapEntry CreateSingleChangeMapEntry() |
||||
{ |
||||
return new OffsetChangeMapEntry(this.Offset, this.RemovalLength, this.InsertionLength); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the OffsetChangeMap, or null if the default offset map (=single replacement) is being used.
|
||||
/// </summary>
|
||||
internal OffsetChangeMap OffsetChangeMapOrNull { |
||||
get { |
||||
return offsetChangeMap; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the new offset where the specified offset moves after this document change.
|
||||
/// </summary>
|
||||
public override int GetNewOffset(int offset, AnchorMovementType movementType = AnchorMovementType.Default) |
||||
{ |
||||
if (offsetChangeMap != null) |
||||
return offsetChangeMap.GetNewOffset(offset, movementType); |
||||
else |
||||
return CreateSingleChangeMapEntry().GetNewOffset(offset, movementType); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Creates a new DocumentChangeEventArgs object.
|
||||
/// </summary>
|
||||
public DocumentChangeEventArgs(int offset, string removedText, string insertedText) |
||||
: this(offset, removedText, insertedText, null) |
||||
{ |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Creates a new DocumentChangeEventArgs object.
|
||||
/// </summary>
|
||||
public DocumentChangeEventArgs(int offset, string removedText, string insertedText, OffsetChangeMap offsetChangeMap) |
||||
: base(offset, removedText, insertedText) |
||||
{ |
||||
SetOffsetChangeMap(offsetChangeMap); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Creates a new DocumentChangeEventArgs object.
|
||||
/// </summary>
|
||||
public DocumentChangeEventArgs(int offset, ITextSource removedText, ITextSource insertedText, OffsetChangeMap offsetChangeMap) |
||||
: base(offset, removedText, insertedText) |
||||
{ |
||||
SetOffsetChangeMap(offsetChangeMap); |
||||
} |
||||
|
||||
void SetOffsetChangeMap(OffsetChangeMap offsetChangeMap) |
||||
{ |
||||
if (offsetChangeMap != null) { |
||||
if (!offsetChangeMap.IsFrozen) |
||||
throw new ArgumentException("The OffsetChangeMap must be frozen before it can be used in DocumentChangeEventArgs"); |
||||
if (!offsetChangeMap.IsValidForDocumentChange(this.Offset, this.RemovalLength, this.InsertionLength)) |
||||
throw new ArgumentException("OffsetChangeMap is not valid for this document change", "offsetChangeMap"); |
||||
this.offsetChangeMap = offsetChangeMap; |
||||
} |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override TextChangeEventArgs Invert() |
||||
{ |
||||
OffsetChangeMap map = this.OffsetChangeMapOrNull; |
||||
if (map != null) { |
||||
map = map.Invert(); |
||||
map.Freeze(); |
||||
} |
||||
return new DocumentChangeEventArgs(this.Offset, this.InsertedText, this.RemovedText, map); |
||||
} |
||||
} |
||||
} |
@ -1,67 +0,0 @@
@@ -1,67 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Diagnostics; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
/// <summary>
|
||||
/// Describes a change to a TextDocument.
|
||||
/// </summary>
|
||||
sealed class DocumentChangeOperation : IUndoableOperationWithContext |
||||
{ |
||||
TextDocument document; |
||||
DocumentChangeEventArgs change; |
||||
|
||||
public DocumentChangeOperation(TextDocument document, DocumentChangeEventArgs change) |
||||
{ |
||||
this.document = document; |
||||
this.change = change; |
||||
} |
||||
|
||||
public void Undo(UndoStack stack) |
||||
{ |
||||
Debug.Assert(stack.state == UndoStack.StatePlayback); |
||||
stack.RegisterAffectedDocument(document); |
||||
stack.state = UndoStack.StatePlaybackModifyDocument; |
||||
this.Undo(); |
||||
stack.state = UndoStack.StatePlayback; |
||||
} |
||||
|
||||
public void Redo(UndoStack stack) |
||||
{ |
||||
Debug.Assert(stack.state == UndoStack.StatePlayback); |
||||
stack.RegisterAffectedDocument(document); |
||||
stack.state = UndoStack.StatePlaybackModifyDocument; |
||||
this.Redo(); |
||||
stack.state = UndoStack.StatePlayback; |
||||
} |
||||
|
||||
public void Undo() |
||||
{ |
||||
OffsetChangeMap map = change.OffsetChangeMapOrNull; |
||||
document.Replace(change.Offset, change.InsertionLength, change.RemovedText, map != null ? map.Invert() : null); |
||||
} |
||||
|
||||
public void Redo() |
||||
{ |
||||
document.Replace(change.Offset, change.RemovalLength, change.InsertedText, change.OffsetChangeMapOrNull); |
||||
} |
||||
} |
||||
} |
@ -1,268 +0,0 @@
@@ -1,268 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Diagnostics; |
||||
using System.Globalization; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory.Editor; |
||||
#endif
|
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
/// <summary>
|
||||
/// Represents a line inside a <see cref="TextDocument"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The <see cref="TextDocument.Lines"/> collection contains one DocumentLine instance
|
||||
/// for every line in the document. This collection is read-only to user code and is automatically
|
||||
/// updated to reflect the current document content.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Internally, the DocumentLine instances are arranged in a binary tree that allows for both efficient updates and lookup.
|
||||
/// Converting between offset and line number is possible in O(lg N) time,
|
||||
/// and the data structure also updates all offsets in O(lg N) whenever a line is inserted or removed.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed partial class DocumentLine : IDocumentLine |
||||
{ |
||||
#region Constructor
|
||||
#if DEBUG
|
||||
// Required for thread safety check which is done only in debug builds.
|
||||
// To save space, we don't store the document reference in release builds as we don't need it there.
|
||||
readonly TextDocument document; |
||||
#endif
|
||||
|
||||
internal bool isDeleted; |
||||
|
||||
internal DocumentLine(TextDocument document) |
||||
{ |
||||
#if DEBUG
|
||||
Debug.Assert(document != null); |
||||
this.document = document; |
||||
#endif
|
||||
} |
||||
|
||||
[Conditional("DEBUG")] |
||||
void DebugVerifyAccess() |
||||
{ |
||||
#if DEBUG
|
||||
document.DebugVerifyAccess(); |
||||
#endif
|
||||
} |
||||
#endregion
|
||||
|
||||
#region Events
|
||||
// /// <summary>
|
||||
// /// Is raised when the line is deleted.
|
||||
// /// </summary>
|
||||
// public event EventHandler Deleted;
|
||||
//
|
||||
// /// <summary>
|
||||
// /// Is raised when the line's text changes.
|
||||
// /// </summary>
|
||||
// public event EventHandler TextChanged;
|
||||
//
|
||||
// /// <summary>
|
||||
// /// Raises the Deleted or TextChanged event.
|
||||
// /// </summary>
|
||||
// internal void RaiseChanged()
|
||||
// {
|
||||
// if (IsDeleted) {
|
||||
// if (Deleted != null)
|
||||
// Deleted(this, EventArgs.Empty);
|
||||
// } else {
|
||||
// if (TextChanged != null)
|
||||
// TextChanged(this, EventArgs.Empty);
|
||||
// }
|
||||
// }
|
||||
#endregion
|
||||
|
||||
#region Properties stored in tree
|
||||
/// <summary>
|
||||
/// Gets if this line was deleted from the document.
|
||||
/// </summary>
|
||||
public bool IsDeleted { |
||||
get { |
||||
DebugVerifyAccess(); |
||||
return isDeleted; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the number of this line.
|
||||
/// Runtime: O(log n)
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">The line was deleted.</exception>
|
||||
public int LineNumber { |
||||
get { |
||||
if (IsDeleted) |
||||
throw new InvalidOperationException(); |
||||
return DocumentLineTree.GetIndexFromNode(this) + 1; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the starting offset of the line in the document's text.
|
||||
/// Runtime: O(log n)
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">The line was deleted.</exception>
|
||||
public int Offset { |
||||
get { |
||||
if (IsDeleted) |
||||
throw new InvalidOperationException(); |
||||
return DocumentLineTree.GetOffsetFromNode(this); |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the end offset of the line in the document's text (the offset before the line delimiter).
|
||||
/// Runtime: O(log n)
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">The line was deleted.</exception>
|
||||
/// <remarks>EndOffset = <see cref="Offset"/> + <see cref="Length"/>.</remarks>
|
||||
public int EndOffset { |
||||
get { return this.Offset + this.Length; } |
||||
} |
||||
#endregion
|
||||
|
||||
#region Length
|
||||
int totalLength; |
||||
byte delimiterLength; |
||||
|
||||
/// <summary>
|
||||
/// Gets the length of this line. The length does not include the line delimiter. O(1)
|
||||
/// </summary>
|
||||
/// <remarks>This property is still available even if the line was deleted;
|
||||
/// in that case, it contains the line's length before the deletion.</remarks>
|
||||
public int Length { |
||||
get { |
||||
DebugVerifyAccess(); |
||||
return totalLength - delimiterLength; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the length of this line, including the line delimiter. O(1)
|
||||
/// </summary>
|
||||
/// <remarks>This property is still available even if the line was deleted;
|
||||
/// in that case, it contains the line's length before the deletion.</remarks>
|
||||
public int TotalLength { |
||||
get { |
||||
DebugVerifyAccess(); |
||||
return totalLength; |
||||
} |
||||
internal set { |
||||
// this is set by DocumentLineTree
|
||||
totalLength = value; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// <para>Gets the length of the line delimiter.</para>
|
||||
/// <para>The value is 1 for single <c>"\r"</c> or <c>"\n"</c>, 2 for the <c>"\r\n"</c> sequence;
|
||||
/// and 0 for the last line in the document.</para>
|
||||
/// </summary>
|
||||
/// <remarks>This property is still available even if the line was deleted;
|
||||
/// in that case, it contains the line delimiter's length before the deletion.</remarks>
|
||||
public int DelimiterLength { |
||||
get { |
||||
DebugVerifyAccess(); |
||||
return delimiterLength; |
||||
} |
||||
internal set { |
||||
Debug.Assert(value >= 0 && value <= 2); |
||||
delimiterLength = (byte)value; |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
#region Previous / Next Line
|
||||
/// <summary>
|
||||
/// Gets the next line in the document.
|
||||
/// </summary>
|
||||
/// <returns>The line following this line, or null if this is the last line.</returns>
|
||||
public DocumentLine NextLine { |
||||
get { |
||||
DebugVerifyAccess(); |
||||
|
||||
if (right != null) { |
||||
return right.LeftMost; |
||||
} else { |
||||
DocumentLine node = this; |
||||
DocumentLine oldNode; |
||||
do { |
||||
oldNode = node; |
||||
node = node.parent; |
||||
// we are on the way up from the right part, don't output node again
|
||||
} while (node != null && node.right == oldNode); |
||||
return node; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the previous line in the document.
|
||||
/// </summary>
|
||||
/// <returns>The line before this line, or null if this is the first line.</returns>
|
||||
public DocumentLine PreviousLine { |
||||
get { |
||||
DebugVerifyAccess(); |
||||
|
||||
if (left != null) { |
||||
return left.RightMost; |
||||
} else { |
||||
DocumentLine node = this; |
||||
DocumentLine oldNode; |
||||
do { |
||||
oldNode = node; |
||||
node = node.parent; |
||||
// we are on the way up from the left part, don't output node again
|
||||
} while (node != null && node.left == oldNode); |
||||
return node; |
||||
} |
||||
} |
||||
} |
||||
|
||||
IDocumentLine IDocumentLine.NextLine { |
||||
get { return this.NextLine; } |
||||
} |
||||
|
||||
IDocumentLine IDocumentLine.PreviousLine { |
||||
get { return this.PreviousLine; } |
||||
} |
||||
#endregion
|
||||
|
||||
#region ToString
|
||||
/// <summary>
|
||||
/// Gets a string with debug output showing the line number and offset.
|
||||
/// Does not include the line's text.
|
||||
/// </summary>
|
||||
public override string ToString() |
||||
{ |
||||
if (IsDeleted) |
||||
return "[DocumentLine deleted]"; |
||||
else |
||||
return string.Format( |
||||
CultureInfo.InvariantCulture, |
||||
"[DocumentLine Number={0} Offset={1} Length={2}]", LineNumber, Offset, Length); |
||||
} |
||||
#endregion
|
||||
} |
||||
} |
@ -1,727 +0,0 @@
@@ -1,727 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Diagnostics; |
||||
using System.Text; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
using LineNode = DocumentLine; |
||||
|
||||
/// <summary>
|
||||
/// Data structure for efficient management of the document lines (most operations are O(lg n)).
|
||||
/// This implements an augmented red-black tree.
|
||||
/// See <see cref="LineNode"/> for the augmented data.
|
||||
///
|
||||
/// NOTE: The tree is never empty, initially it contains an empty line.
|
||||
/// </summary>
|
||||
sealed class DocumentLineTree : IList<DocumentLine> |
||||
{ |
||||
#region Constructor
|
||||
readonly TextDocument document; |
||||
LineNode root; |
||||
|
||||
public DocumentLineTree(TextDocument document) |
||||
{ |
||||
this.document = document; |
||||
|
||||
DocumentLine emptyLine = new DocumentLine(document); |
||||
root = emptyLine.InitLineNode(); |
||||
} |
||||
#endregion
|
||||
|
||||
#region Rotation callbacks
|
||||
internal static void UpdateAfterChildrenChange(LineNode node) |
||||
{ |
||||
int totalCount = 1; |
||||
int totalLength = node.TotalLength; |
||||
if (node.left != null) { |
||||
totalCount += node.left.nodeTotalCount; |
||||
totalLength += node.left.nodeTotalLength; |
||||
} |
||||
if (node.right != null) { |
||||
totalCount += node.right.nodeTotalCount; |
||||
totalLength += node.right.nodeTotalLength; |
||||
} |
||||
if (totalCount != node.nodeTotalCount |
||||
|| totalLength != node.nodeTotalLength) |
||||
{ |
||||
node.nodeTotalCount = totalCount; |
||||
node.nodeTotalLength = totalLength; |
||||
if (node.parent != null) UpdateAfterChildrenChange(node.parent); |
||||
} |
||||
} |
||||
|
||||
static void UpdateAfterRotateLeft(LineNode node) |
||||
{ |
||||
UpdateAfterChildrenChange(node); |
||||
|
||||
// not required: rotations only happen on insertions/deletions
|
||||
// -> totalCount changes -> the parent is always updated
|
||||
//UpdateAfterChildrenChange(node.parent);
|
||||
} |
||||
|
||||
static void UpdateAfterRotateRight(LineNode node) |
||||
{ |
||||
UpdateAfterChildrenChange(node); |
||||
|
||||
// not required: rotations only happen on insertions/deletions
|
||||
// -> totalCount changes -> the parent is always updated
|
||||
//UpdateAfterChildrenChange(node.parent);
|
||||
} |
||||
#endregion
|
||||
|
||||
#region RebuildDocument
|
||||
/// <summary>
|
||||
/// Rebuild the tree, in O(n).
|
||||
/// </summary>
|
||||
public void RebuildTree(List<DocumentLine> documentLines) |
||||
{ |
||||
LineNode[] nodes = new LineNode[documentLines.Count]; |
||||
for (int i = 0; i < documentLines.Count; i++) { |
||||
DocumentLine ls = documentLines[i]; |
||||
LineNode node = ls.InitLineNode(); |
||||
nodes[i] = node; |
||||
} |
||||
Debug.Assert(nodes.Length > 0); |
||||
// now build the corresponding balanced tree
|
||||
int height = GetTreeHeight(nodes.Length); |
||||
Debug.WriteLine("DocumentLineTree will have height: " + height); |
||||
root = BuildTree(nodes, 0, nodes.Length, height); |
||||
root.color = BLACK; |
||||
#if DEBUG
|
||||
CheckProperties(); |
||||
#endif
|
||||
} |
||||
|
||||
internal static int GetTreeHeight(int size) |
||||
{ |
||||
if (size == 0) |
||||
return 0; |
||||
else |
||||
return GetTreeHeight(size / 2) + 1; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// build a tree from a list of nodes
|
||||
/// </summary>
|
||||
LineNode BuildTree(LineNode[] nodes, int start, int end, int subtreeHeight) |
||||
{ |
||||
Debug.Assert(start <= end); |
||||
if (start == end) { |
||||
return null; |
||||
} |
||||
int middle = (start + end) / 2; |
||||
LineNode node = nodes[middle]; |
||||
node.left = BuildTree(nodes, start, middle, subtreeHeight - 1); |
||||
node.right = BuildTree(nodes, middle + 1, end, subtreeHeight - 1); |
||||
if (node.left != null) node.left.parent = node; |
||||
if (node.right != null) node.right.parent = node; |
||||
if (subtreeHeight == 1) |
||||
node.color = RED; |
||||
UpdateAfterChildrenChange(node); |
||||
return node; |
||||
} |
||||
#endregion
|
||||
|
||||
#region GetNodeBy... / Get...FromNode
|
||||
LineNode GetNodeByIndex(int index) |
||||
{ |
||||
Debug.Assert(index >= 0); |
||||
Debug.Assert(index < root.nodeTotalCount); |
||||
LineNode node = root; |
||||
while (true) { |
||||
if (node.left != null && index < node.left.nodeTotalCount) { |
||||
node = node.left; |
||||
} else { |
||||
if (node.left != null) { |
||||
index -= node.left.nodeTotalCount; |
||||
} |
||||
if (index == 0) |
||||
return node; |
||||
index--; |
||||
node = node.right; |
||||
} |
||||
} |
||||
} |
||||
|
||||
internal static int GetIndexFromNode(LineNode node) |
||||
{ |
||||
int index = (node.left != null) ? node.left.nodeTotalCount : 0; |
||||
while (node.parent != null) { |
||||
if (node == node.parent.right) { |
||||
if (node.parent.left != null) |
||||
index += node.parent.left.nodeTotalCount; |
||||
index++; |
||||
} |
||||
node = node.parent; |
||||
} |
||||
return index; |
||||
} |
||||
|
||||
LineNode GetNodeByOffset(int offset) |
||||
{ |
||||
Debug.Assert(offset >= 0); |
||||
Debug.Assert(offset <= root.nodeTotalLength); |
||||
if (offset == root.nodeTotalLength) { |
||||
return root.RightMost; |
||||
} |
||||
LineNode node = root; |
||||
while (true) { |
||||
if (node.left != null && offset < node.left.nodeTotalLength) { |
||||
node = node.left; |
||||
} else { |
||||
if (node.left != null) { |
||||
offset -= node.left.nodeTotalLength; |
||||
} |
||||
offset -= node.TotalLength; |
||||
if (offset < 0) |
||||
return node; |
||||
node = node.right; |
||||
} |
||||
} |
||||
} |
||||
|
||||
internal static int GetOffsetFromNode(LineNode node) |
||||
{ |
||||
int offset = (node.left != null) ? node.left.nodeTotalLength : 0; |
||||
while (node.parent != null) { |
||||
if (node == node.parent.right) { |
||||
if (node.parent.left != null) |
||||
offset += node.parent.left.nodeTotalLength; |
||||
offset += node.parent.TotalLength; |
||||
} |
||||
node = node.parent; |
||||
} |
||||
return offset; |
||||
} |
||||
#endregion
|
||||
|
||||
#region GetLineBy
|
||||
public DocumentLine GetByNumber(int number) |
||||
{ |
||||
return GetNodeByIndex(number - 1); |
||||
} |
||||
|
||||
public DocumentLine GetByOffset(int offset) |
||||
{ |
||||
return GetNodeByOffset(offset); |
||||
} |
||||
#endregion
|
||||
|
||||
#region LineCount
|
||||
public int LineCount { |
||||
get { |
||||
return root.nodeTotalCount; |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
#region CheckProperties
|
||||
#if DEBUG
|
||||
[Conditional("DATACONSISTENCYTEST")] |
||||
internal void CheckProperties() |
||||
{ |
||||
Debug.Assert(root.nodeTotalLength == document.TextLength); |
||||
CheckProperties(root); |
||||
|
||||
// check red-black property:
|
||||
int blackCount = -1; |
||||
CheckNodeProperties(root, null, RED, 0, ref blackCount); |
||||
} |
||||
|
||||
void CheckProperties(LineNode node) |
||||
{ |
||||
int totalCount = 1; |
||||
int totalLength = node.TotalLength; |
||||
if (node.left != null) { |
||||
CheckProperties(node.left); |
||||
totalCount += node.left.nodeTotalCount; |
||||
totalLength += node.left.nodeTotalLength; |
||||
} |
||||
if (node.right != null) { |
||||
CheckProperties(node.right); |
||||
totalCount += node.right.nodeTotalCount; |
||||
totalLength += node.right.nodeTotalLength; |
||||
} |
||||
Debug.Assert(node.nodeTotalCount == totalCount); |
||||
Debug.Assert(node.nodeTotalLength == totalLength); |
||||
} |
||||
|
||||
/* |
||||
1. A node is either red or black. |
||||
2. The root is black. |
||||
3. All leaves are black. (The leaves are the NIL children.) |
||||
4. Both children of every red node are black. (So every red node must have a black parent.) |
||||
5. Every simple path from a node to a descendant leaf contains the same number of black nodes. (Not counting the leaf node.) |
||||
*/ |
||||
void CheckNodeProperties(LineNode node, LineNode parentNode, bool parentColor, int blackCount, ref int expectedBlackCount) |
||||
{ |
||||
if (node == null) return; |
||||
|
||||
Debug.Assert(node.parent == parentNode); |
||||
|
||||
if (parentColor == RED) { |
||||
Debug.Assert(node.color == BLACK); |
||||
} |
||||
if (node.color == BLACK) { |
||||
blackCount++; |
||||
} |
||||
if (node.left == null && node.right == null) { |
||||
// node is a leaf node:
|
||||
if (expectedBlackCount == -1) |
||||
expectedBlackCount = blackCount; |
||||
else |
||||
Debug.Assert(expectedBlackCount == blackCount); |
||||
} |
||||
CheckNodeProperties(node.left, node, node.color, blackCount, ref expectedBlackCount); |
||||
CheckNodeProperties(node.right, node, node.color, blackCount, ref expectedBlackCount); |
||||
} |
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] |
||||
public string GetTreeAsString() |
||||
{ |
||||
StringBuilder b = new StringBuilder(); |
||||
AppendTreeToString(root, b, 0); |
||||
return b.ToString(); |
||||
} |
||||
|
||||
static void AppendTreeToString(LineNode node, StringBuilder b, int indent) |
||||
{ |
||||
if (node.color == RED) |
||||
b.Append("RED "); |
||||
else |
||||
b.Append("BLACK "); |
||||
b.AppendLine(node.ToString()); |
||||
indent += 2; |
||||
if (node.left != null) { |
||||
b.Append(' ', indent); |
||||
b.Append("L: "); |
||||
AppendTreeToString(node.left, b, indent); |
||||
} |
||||
if (node.right != null) { |
||||
b.Append(' ', indent); |
||||
b.Append("R: "); |
||||
AppendTreeToString(node.right, b, indent); |
||||
} |
||||
} |
||||
#endif
|
||||
#endregion
|
||||
|
||||
#region Insert/Remove lines
|
||||
public void RemoveLine(DocumentLine line) |
||||
{ |
||||
RemoveNode(line); |
||||
line.isDeleted = true; |
||||
} |
||||
|
||||
public DocumentLine InsertLineAfter(DocumentLine line, int totalLength) |
||||
{ |
||||
DocumentLine newLine = new DocumentLine(document); |
||||
newLine.TotalLength = totalLength; |
||||
|
||||
InsertAfter(line, newLine); |
||||
return newLine; |
||||
} |
||||
|
||||
void InsertAfter(LineNode node, DocumentLine newLine) |
||||
{ |
||||
LineNode newNode = newLine.InitLineNode(); |
||||
if (node.right == null) { |
||||
InsertAsRight(node, newNode); |
||||
} else { |
||||
InsertAsLeft(node.right.LeftMost, newNode); |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
#region Red/Black Tree
|
||||
internal const bool RED = true; |
||||
internal const bool BLACK = false; |
||||
|
||||
void InsertAsLeft(LineNode parentNode, LineNode newNode) |
||||
{ |
||||
Debug.Assert(parentNode.left == null); |
||||
parentNode.left = newNode; |
||||
newNode.parent = parentNode; |
||||
newNode.color = RED; |
||||
UpdateAfterChildrenChange(parentNode); |
||||
FixTreeOnInsert(newNode); |
||||
} |
||||
|
||||
void InsertAsRight(LineNode parentNode, LineNode newNode) |
||||
{ |
||||
Debug.Assert(parentNode.right == null); |
||||
parentNode.right = newNode; |
||||
newNode.parent = parentNode; |
||||
newNode.color = RED; |
||||
UpdateAfterChildrenChange(parentNode); |
||||
FixTreeOnInsert(newNode); |
||||
} |
||||
|
||||
void FixTreeOnInsert(LineNode node) |
||||
{ |
||||
Debug.Assert(node != null); |
||||
Debug.Assert(node.color == RED); |
||||
Debug.Assert(node.left == null || node.left.color == BLACK); |
||||
Debug.Assert(node.right == null || node.right.color == BLACK); |
||||
|
||||
LineNode parentNode = node.parent; |
||||
if (parentNode == null) { |
||||
// we inserted in the root -> the node must be black
|
||||
// since this is a root node, making the node black increments the number of black nodes
|
||||
// on all paths by one, so it is still the same for all paths.
|
||||
node.color = BLACK; |
||||
return; |
||||
} |
||||
if (parentNode.color == BLACK) { |
||||
// if the parent node where we inserted was black, our red node is placed correctly.
|
||||
// since we inserted a red node, the number of black nodes on each path is unchanged
|
||||
// -> the tree is still balanced
|
||||
return; |
||||
} |
||||
// parentNode is red, so there is a conflict here!
|
||||
|
||||
// because the root is black, parentNode is not the root -> there is a grandparent node
|
||||
LineNode grandparentNode = parentNode.parent; |
||||
LineNode uncleNode = Sibling(parentNode); |
||||
if (uncleNode != null && uncleNode.color == RED) { |
||||
parentNode.color = BLACK; |
||||
uncleNode.color = BLACK; |
||||
grandparentNode.color = RED; |
||||
FixTreeOnInsert(grandparentNode); |
||||
return; |
||||
} |
||||
// now we know: parent is red but uncle is black
|
||||
// First rotation:
|
||||
if (node == parentNode.right && parentNode == grandparentNode.left) { |
||||
RotateLeft(parentNode); |
||||
node = node.left; |
||||
} else if (node == parentNode.left && parentNode == grandparentNode.right) { |
||||
RotateRight(parentNode); |
||||
node = node.right; |
||||
} |
||||
// because node might have changed, reassign variables:
|
||||
parentNode = node.parent; |
||||
grandparentNode = parentNode.parent; |
||||
|
||||
// Now recolor a bit:
|
||||
parentNode.color = BLACK; |
||||
grandparentNode.color = RED; |
||||
// Second rotation:
|
||||
if (node == parentNode.left && parentNode == grandparentNode.left) { |
||||
RotateRight(grandparentNode); |
||||
} else { |
||||
// because of the first rotation, this is guaranteed:
|
||||
Debug.Assert(node == parentNode.right && parentNode == grandparentNode.right); |
||||
RotateLeft(grandparentNode); |
||||
} |
||||
} |
||||
|
||||
void RemoveNode(LineNode removedNode) |
||||
{ |
||||
if (removedNode.left != null && removedNode.right != null) { |
||||
// replace removedNode with it's in-order successor
|
||||
|
||||
LineNode leftMost = removedNode.right.LeftMost; |
||||
RemoveNode(leftMost); // remove leftMost from its current location
|
||||
|
||||
// and overwrite the removedNode with it
|
||||
ReplaceNode(removedNode, leftMost); |
||||
leftMost.left = removedNode.left; |
||||
if (leftMost.left != null) leftMost.left.parent = leftMost; |
||||
leftMost.right = removedNode.right; |
||||
if (leftMost.right != null) leftMost.right.parent = leftMost; |
||||
leftMost.color = removedNode.color; |
||||
|
||||
UpdateAfterChildrenChange(leftMost); |
||||
if (leftMost.parent != null) UpdateAfterChildrenChange(leftMost.parent); |
||||
return; |
||||
} |
||||
|
||||
// now either removedNode.left or removedNode.right is null
|
||||
// get the remaining child
|
||||
LineNode parentNode = removedNode.parent; |
||||
LineNode childNode = removedNode.left ?? removedNode.right; |
||||
ReplaceNode(removedNode, childNode); |
||||
if (parentNode != null) UpdateAfterChildrenChange(parentNode); |
||||
if (removedNode.color == BLACK) { |
||||
if (childNode != null && childNode.color == RED) { |
||||
childNode.color = BLACK; |
||||
} else { |
||||
FixTreeOnDelete(childNode, parentNode); |
||||
} |
||||
} |
||||
} |
||||
|
||||
void FixTreeOnDelete(LineNode node, LineNode parentNode) |
||||
{ |
||||
Debug.Assert(node == null || node.parent == parentNode); |
||||
if (parentNode == null) |
||||
return; |
||||
|
||||
// warning: node may be null
|
||||
LineNode sibling = Sibling(node, parentNode); |
||||
if (sibling.color == RED) { |
||||
parentNode.color = RED; |
||||
sibling.color = BLACK; |
||||
if (node == parentNode.left) { |
||||
RotateLeft(parentNode); |
||||
} else { |
||||
RotateRight(parentNode); |
||||
} |
||||
|
||||
sibling = Sibling(node, parentNode); // update value of sibling after rotation
|
||||
} |
||||
|
||||
if (parentNode.color == BLACK |
||||
&& sibling.color == BLACK |
||||
&& GetColor(sibling.left) == BLACK |
||||
&& GetColor(sibling.right) == BLACK) |
||||
{ |
||||
sibling.color = RED; |
||||
FixTreeOnDelete(parentNode, parentNode.parent); |
||||
return; |
||||
} |
||||
|
||||
if (parentNode.color == RED |
||||
&& sibling.color == BLACK |
||||
&& GetColor(sibling.left) == BLACK |
||||
&& GetColor(sibling.right) == BLACK) |
||||
{ |
||||
sibling.color = RED; |
||||
parentNode.color = BLACK; |
||||
return; |
||||
} |
||||
|
||||
if (node == parentNode.left && |
||||
sibling.color == BLACK && |
||||
GetColor(sibling.left) == RED && |
||||
GetColor(sibling.right) == BLACK) |
||||
{ |
||||
sibling.color = RED; |
||||
sibling.left.color = BLACK; |
||||
RotateRight(sibling); |
||||
} |
||||
else if (node == parentNode.right && |
||||
sibling.color == BLACK && |
||||
GetColor(sibling.right) == RED && |
||||
GetColor(sibling.left) == BLACK) |
||||
{ |
||||
sibling.color = RED; |
||||
sibling.right.color = BLACK; |
||||
RotateLeft(sibling); |
||||
} |
||||
sibling = Sibling(node, parentNode); // update value of sibling after rotation
|
||||
|
||||
sibling.color = parentNode.color; |
||||
parentNode.color = BLACK; |
||||
if (node == parentNode.left) { |
||||
if (sibling.right != null) { |
||||
Debug.Assert(sibling.right.color == RED); |
||||
sibling.right.color = BLACK; |
||||
} |
||||
RotateLeft(parentNode); |
||||
} else { |
||||
if (sibling.left != null) { |
||||
Debug.Assert(sibling.left.color == RED); |
||||
sibling.left.color = BLACK; |
||||
} |
||||
RotateRight(parentNode); |
||||
} |
||||
} |
||||
|
||||
void ReplaceNode(LineNode replacedNode, LineNode newNode) |
||||
{ |
||||
if (replacedNode.parent == null) { |
||||
Debug.Assert(replacedNode == root); |
||||
root = newNode; |
||||
} else { |
||||
if (replacedNode.parent.left == replacedNode) |
||||
replacedNode.parent.left = newNode; |
||||
else |
||||
replacedNode.parent.right = newNode; |
||||
} |
||||
if (newNode != null) { |
||||
newNode.parent = replacedNode.parent; |
||||
} |
||||
replacedNode.parent = null; |
||||
} |
||||
|
||||
void RotateLeft(LineNode p) |
||||
{ |
||||
// let q be p's right child
|
||||
LineNode q = p.right; |
||||
Debug.Assert(q != null); |
||||
Debug.Assert(q.parent == p); |
||||
// set q to be the new root
|
||||
ReplaceNode(p, q); |
||||
|
||||
// set p's right child to be q's left child
|
||||
p.right = q.left; |
||||
if (p.right != null) p.right.parent = p; |
||||
// set q's left child to be p
|
||||
q.left = p; |
||||
p.parent = q; |
||||
UpdateAfterRotateLeft(p); |
||||
} |
||||
|
||||
void RotateRight(LineNode p) |
||||
{ |
||||
// let q be p's left child
|
||||
LineNode q = p.left; |
||||
Debug.Assert(q != null); |
||||
Debug.Assert(q.parent == p); |
||||
// set q to be the new root
|
||||
ReplaceNode(p, q); |
||||
|
||||
// set p's left child to be q's right child
|
||||
p.left = q.right; |
||||
if (p.left != null) p.left.parent = p; |
||||
// set q's right child to be p
|
||||
q.right = p; |
||||
p.parent = q; |
||||
UpdateAfterRotateRight(p); |
||||
} |
||||
|
||||
static LineNode Sibling(LineNode node) |
||||
{ |
||||
if (node == node.parent.left) |
||||
return node.parent.right; |
||||
else |
||||
return node.parent.left; |
||||
} |
||||
|
||||
static LineNode Sibling(LineNode node, LineNode parentNode) |
||||
{ |
||||
Debug.Assert(node == null || node.parent == parentNode); |
||||
if (node == parentNode.left) |
||||
return parentNode.right; |
||||
else |
||||
return parentNode.left; |
||||
} |
||||
|
||||
static bool GetColor(LineNode node) |
||||
{ |
||||
return node != null ? node.color : BLACK; |
||||
} |
||||
#endregion
|
||||
|
||||
#region IList implementation
|
||||
DocumentLine IList<DocumentLine>.this[int index] { |
||||
get { |
||||
document.VerifyAccess(); |
||||
return GetByNumber(1 + index); |
||||
} |
||||
set { |
||||
throw new NotSupportedException(); |
||||
} |
||||
} |
||||
|
||||
int ICollection<DocumentLine>.Count { |
||||
get { |
||||
document.VerifyAccess(); |
||||
return LineCount; |
||||
} |
||||
} |
||||
|
||||
bool ICollection<DocumentLine>.IsReadOnly { |
||||
get { return true; } |
||||
} |
||||
|
||||
int IList<DocumentLine>.IndexOf(DocumentLine item) |
||||
{ |
||||
document.VerifyAccess(); |
||||
if (item == null || item.IsDeleted) |
||||
return -1; |
||||
int index = item.LineNumber - 1; |
||||
if (index < LineCount && GetNodeByIndex(index) == item) |
||||
return index; |
||||
else |
||||
return -1; |
||||
} |
||||
|
||||
void IList<DocumentLine>.Insert(int index, DocumentLine item) |
||||
{ |
||||
throw new NotSupportedException(); |
||||
} |
||||
|
||||
void IList<DocumentLine>.RemoveAt(int index) |
||||
{ |
||||
throw new NotSupportedException(); |
||||
} |
||||
|
||||
void ICollection<DocumentLine>.Add(DocumentLine item) |
||||
{ |
||||
throw new NotSupportedException(); |
||||
} |
||||
|
||||
void ICollection<DocumentLine>.Clear() |
||||
{ |
||||
throw new NotSupportedException(); |
||||
} |
||||
|
||||
bool ICollection<DocumentLine>.Contains(DocumentLine item) |
||||
{ |
||||
IList<DocumentLine> self = this; |
||||
return self.IndexOf(item) >= 0; |
||||
} |
||||
|
||||
void ICollection<DocumentLine>.CopyTo(DocumentLine[] array, int arrayIndex) |
||||
{ |
||||
if (array == null) |
||||
throw new ArgumentNullException("array"); |
||||
if (array.Length < LineCount) |
||||
throw new ArgumentException("The array is too small", "array"); |
||||
if (arrayIndex < 0 || arrayIndex + LineCount > array.Length) |
||||
throw new ArgumentOutOfRangeException("arrayIndex", arrayIndex, "Value must be between 0 and " + (array.Length - LineCount)); |
||||
foreach (DocumentLine ls in this) { |
||||
array[arrayIndex++] = ls; |
||||
} |
||||
} |
||||
|
||||
bool ICollection<DocumentLine>.Remove(DocumentLine item) |
||||
{ |
||||
throw new NotSupportedException(); |
||||
} |
||||
|
||||
public IEnumerator<DocumentLine> GetEnumerator() |
||||
{ |
||||
document.VerifyAccess(); |
||||
return Enumerate(); |
||||
} |
||||
|
||||
IEnumerator<DocumentLine> Enumerate() |
||||
{ |
||||
document.VerifyAccess(); |
||||
DocumentLine line = root.LeftMost; |
||||
while (line != null) { |
||||
yield return line; |
||||
line = line.NextLine; |
||||
} |
||||
} |
||||
|
||||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() |
||||
{ |
||||
return this.GetEnumerator(); |
||||
} |
||||
#endregion
|
||||
} |
||||
} |
@ -1,86 +0,0 @@
@@ -1,86 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.IO; |
||||
using System.Text; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory.Editor; |
||||
#endif
|
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
/// <summary>
|
||||
/// A TextWriter implementation that directly inserts into a document.
|
||||
/// </summary>
|
||||
public class DocumentTextWriter : TextWriter |
||||
{ |
||||
readonly IDocument document; |
||||
int insertionOffset; |
||||
|
||||
/// <summary>
|
||||
/// Creates a new DocumentTextWriter that inserts into document, starting at insertionOffset.
|
||||
/// </summary>
|
||||
public DocumentTextWriter(IDocument document, int insertionOffset) |
||||
{ |
||||
this.insertionOffset = insertionOffset; |
||||
if (document == null) |
||||
throw new ArgumentNullException("document"); |
||||
this.document = document; |
||||
var line = document.GetLineByOffset(insertionOffset); |
||||
if (line.DelimiterLength == 0) |
||||
line = line.PreviousLine; |
||||
if (line != null) |
||||
this.NewLine = document.GetText(line.EndOffset, line.DelimiterLength); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets/Sets the current insertion offset.
|
||||
/// </summary>
|
||||
public int InsertionOffset { |
||||
get { return insertionOffset; } |
||||
set { insertionOffset = value; } |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(char value) |
||||
{ |
||||
document.Insert(insertionOffset, value.ToString()); |
||||
insertionOffset++; |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(char[] buffer, int index, int count) |
||||
{ |
||||
document.Insert(insertionOffset, new string(buffer, index, count)); |
||||
insertionOffset += count; |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(string value) |
||||
{ |
||||
document.Insert(insertionOffset, value); |
||||
insertionOffset += value.Length; |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override Encoding Encoding { |
||||
get { return Encoding.UTF8; } |
||||
} |
||||
} |
||||
} |
@ -1,207 +0,0 @@
@@ -1,207 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
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; |
||||
} |
||||
} |
||||
*/ |
||||
} |
@ -1,343 +0,0 @@
@@ -1,343 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
#if !NREFACTORY
|
||||
/// <summary>
|
||||
/// A document representing a source code file for refactoring.
|
||||
/// Line and column counting starts at 1.
|
||||
/// Offset counting starts at 0.
|
||||
/// </summary>
|
||||
public interface IDocument : ITextSource, IServiceProvider |
||||
{ |
||||
#if NREFACTORY
|
||||
/// <summary>
|
||||
/// Creates an immutable snapshot of this document.
|
||||
/// </summary>
|
||||
IDocument CreateDocumentSnapshot(); |
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Gets/Sets the text of the whole document..
|
||||
/// </summary>
|
||||
new string Text { get; set; } // hides ITextSource.Text to add the setter
|
||||
|
||||
/// <summary>
|
||||
/// This event is called directly before a change is applied to the document.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It is invalid to modify the document within this event handler.
|
||||
/// Aborting the change (by throwing an exception) is likely to cause corruption of data structures
|
||||
/// that listen to the Changing and Changed events.
|
||||
/// </remarks>
|
||||
event EventHandler<TextChangeEventArgs> TextChanging; |
||||
|
||||
/// <summary>
|
||||
/// This event is called directly after a change is applied to the document.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It is invalid to modify the document within this event handler.
|
||||
/// Aborting the event handler (by throwing an exception) is likely to cause corruption of data structures
|
||||
/// that listen to the Changing and Changed events.
|
||||
/// </remarks>
|
||||
event EventHandler<TextChangeEventArgs> TextChanged; |
||||
|
||||
/// <summary>
|
||||
/// This event is called after a group of changes is completed.
|
||||
/// </summary>
|
||||
/// <seealso cref="EndUndoableAction"/>
|
||||
event EventHandler ChangeCompleted; |
||||
|
||||
/// <summary>
|
||||
/// Gets the number of lines in the document.
|
||||
/// </summary>
|
||||
int LineCount { get; } |
||||
|
||||
/// <summary>
|
||||
/// Gets the document line with the specified number.
|
||||
/// </summary>
|
||||
/// <param name="lineNumber">The number of the line to retrieve. The first line has number 1.</param>
|
||||
IDocumentLine GetLineByNumber(int lineNumber); |
||||
|
||||
/// <summary>
|
||||
/// Gets the document line that contains the specified offset.
|
||||
/// </summary>
|
||||
IDocumentLine GetLineByOffset(int offset); |
||||
|
||||
/// <summary>
|
||||
/// Gets the offset from a text location.
|
||||
/// </summary>
|
||||
/// <seealso cref="GetLocation"/>
|
||||
int GetOffset(int line, int column); |
||||
|
||||
/// <summary>
|
||||
/// Gets the offset from a text location.
|
||||
/// </summary>
|
||||
/// <seealso cref="GetLocation"/>
|
||||
int GetOffset(TextLocation location); |
||||
|
||||
/// <summary>
|
||||
/// Gets the location from an offset.
|
||||
/// </summary>
|
||||
/// <seealso cref="GetOffset(TextLocation)"/>
|
||||
TextLocation GetLocation(int offset); |
||||
|
||||
/// <summary>
|
||||
/// Inserts text.
|
||||
/// </summary>
|
||||
/// <param name="offset">The offset at which the text is inserted.</param>
|
||||
/// <param name="text">The new text.</param>
|
||||
/// <remarks>
|
||||
/// Anchors positioned exactly at the insertion offset will move according to their movement type.
|
||||
/// For AnchorMovementType.Default, they will move behind the inserted text.
|
||||
/// The caret will also move behind the inserted text.
|
||||
/// </remarks>
|
||||
void Insert(int offset, string text); |
||||
|
||||
/// <summary>
|
||||
/// Inserts text.
|
||||
/// </summary>
|
||||
/// <param name="offset">The offset at which the text is inserted.</param>
|
||||
/// <param name="text">The new text.</param>
|
||||
/// <remarks>
|
||||
/// Anchors positioned exactly at the insertion offset will move according to their movement type.
|
||||
/// For AnchorMovementType.Default, they will move behind the inserted text.
|
||||
/// The caret will also move behind the inserted text.
|
||||
/// </remarks>
|
||||
void Insert(int offset, ITextSource text); |
||||
|
||||
/// <summary>
|
||||
/// Inserts text.
|
||||
/// </summary>
|
||||
/// <param name="offset">The offset at which the text is inserted.</param>
|
||||
/// <param name="text">The new text.</param>
|
||||
/// <param name="defaultAnchorMovementType">
|
||||
/// Anchors positioned exactly at the insertion offset will move according to the anchor's movement type.
|
||||
/// For AnchorMovementType.Default, they will move according to the movement type specified by this parameter.
|
||||
/// The caret will also move according to the <paramref name="defaultAnchorMovementType"/> parameter.
|
||||
/// </param>
|
||||
void Insert(int offset, string text, AnchorMovementType defaultAnchorMovementType); |
||||
|
||||
/// <summary>
|
||||
/// Inserts text.
|
||||
/// </summary>
|
||||
/// <param name="offset">The offset at which the text is inserted.</param>
|
||||
/// <param name="text">The new text.</param>
|
||||
/// <param name="defaultAnchorMovementType">
|
||||
/// Anchors positioned exactly at the insertion offset will move according to the anchor's movement type.
|
||||
/// For AnchorMovementType.Default, they will move according to the movement type specified by this parameter.
|
||||
/// The caret will also move according to the <paramref name="defaultAnchorMovementType"/> parameter.
|
||||
/// </param>
|
||||
void Insert(int offset, ITextSource text, AnchorMovementType defaultAnchorMovementType); |
||||
|
||||
/// <summary>
|
||||
/// Removes text.
|
||||
/// </summary>
|
||||
/// <param name="offset">Starting offset of the text to be removed.</param>
|
||||
/// <param name="length">Length of the text to be removed.</param>
|
||||
void Remove(int offset, int length); |
||||
|
||||
/// <summary>
|
||||
/// Replaces text.
|
||||
/// </summary>
|
||||
/// <param name="offset">The starting offset of the text to be replaced.</param>
|
||||
/// <param name="length">The length of the text to be replaced.</param>
|
||||
/// <param name="newText">The new text.</param>
|
||||
void Replace(int offset, int length, string newText); |
||||
|
||||
/// <summary>
|
||||
/// Replaces text.
|
||||
/// </summary>
|
||||
/// <param name="offset">The starting offset of the text to be replaced.</param>
|
||||
/// <param name="length">The length of the text to be replaced.</param>
|
||||
/// <param name="newText">The new text.</param>
|
||||
void Replace(int offset, int length, ITextSource newText); |
||||
|
||||
/// <summary>
|
||||
/// Make the document combine the following actions into a single
|
||||
/// action for undo purposes.
|
||||
/// </summary>
|
||||
void StartUndoableAction(); |
||||
|
||||
/// <summary>
|
||||
/// Ends the undoable action started with <see cref="StartUndoableAction"/>.
|
||||
/// </summary>
|
||||
void EndUndoableAction(); |
||||
|
||||
/// <summary>
|
||||
/// Creates an undo group. Dispose the returned value to close the undo group.
|
||||
/// </summary>
|
||||
/// <returns>An object that closes the undo group when Dispose() is called.</returns>
|
||||
IDisposable OpenUndoGroup(); |
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ITextAnchor"/> at the specified offset.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="ITextAnchor" select="remarks|example"/>
|
||||
ITextAnchor CreateAnchor(int offset); |
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the file the document is stored in.
|
||||
/// Could also be a non-existent dummy file name or null if no name has been set.
|
||||
/// </summary>
|
||||
string FileName { get; } |
||||
|
||||
/// <summary>
|
||||
/// Fired when the file name of the document changes.
|
||||
/// </summary>
|
||||
event EventHandler FileNameChanged; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// A line inside a <see cref="IDocument"/>.
|
||||
/// </summary>
|
||||
public interface IDocumentLine : ISegment |
||||
{ |
||||
/// <summary>
|
||||
/// Gets the length of this line, including the line delimiter.
|
||||
/// </summary>
|
||||
int TotalLength { get; } |
||||
|
||||
/// <summary>
|
||||
/// Gets the length of the line terminator.
|
||||
/// Returns 1 or 2; or 0 at the end of the document.
|
||||
/// </summary>
|
||||
int DelimiterLength { get; } |
||||
|
||||
/// <summary>
|
||||
/// Gets the number of this line.
|
||||
/// The first line has the number 1.
|
||||
/// </summary>
|
||||
int LineNumber { get; } |
||||
|
||||
/// <summary>
|
||||
/// Gets the previous line. Returns null if this is the first line in the document.
|
||||
/// </summary>
|
||||
IDocumentLine PreviousLine { get; } |
||||
|
||||
/// <summary>
|
||||
/// Gets the next line. Returns null if this is the last line in the document.
|
||||
/// </summary>
|
||||
IDocumentLine NextLine { get; } |
||||
|
||||
/// <summary>
|
||||
/// Gets whether the line was deleted.
|
||||
/// </summary>
|
||||
bool IsDeleted { get; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Describes a change of the document text.
|
||||
/// This class is thread-safe.
|
||||
/// </summary>
|
||||
[Serializable] |
||||
public class TextChangeEventArgs : EventArgs |
||||
{ |
||||
readonly int offset; |
||||
readonly ITextSource removedText; |
||||
readonly ITextSource insertedText; |
||||
|
||||
/// <summary>
|
||||
/// The offset at which the change occurs.
|
||||
/// </summary>
|
||||
public int Offset { |
||||
get { return offset; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// The text that was removed.
|
||||
/// </summary>
|
||||
public ITextSource RemovedText { |
||||
get { return removedText; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// The number of characters removed.
|
||||
/// </summary>
|
||||
public int RemovalLength { |
||||
get { return removedText.TextLength; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// The text that was inserted.
|
||||
/// </summary>
|
||||
public ITextSource InsertedText { |
||||
get { return insertedText; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// The number of characters inserted.
|
||||
/// </summary>
|
||||
public int InsertionLength { |
||||
get { return insertedText.TextLength; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Creates a new TextChangeEventArgs object.
|
||||
/// </summary>
|
||||
public TextChangeEventArgs(int offset, string removedText, string insertedText) |
||||
{ |
||||
if (offset < 0) |
||||
throw new ArgumentOutOfRangeException("offset", offset, "offset must not be negative"); |
||||
this.offset = offset; |
||||
this.removedText = removedText != null ? new StringTextSource(removedText) : StringTextSource.Empty; |
||||
this.insertedText = insertedText != null ? new StringTextSource(insertedText) : StringTextSource.Empty; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Creates a new TextChangeEventArgs object.
|
||||
/// </summary>
|
||||
public TextChangeEventArgs(int offset, ITextSource removedText, ITextSource insertedText) |
||||
{ |
||||
if (offset < 0) |
||||
throw new ArgumentOutOfRangeException("offset", offset, "offset must not be negative"); |
||||
this.offset = offset; |
||||
this.removedText = removedText ?? StringTextSource.Empty; |
||||
this.insertedText = insertedText ?? StringTextSource.Empty; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the new offset where the specified offset moves after this document change.
|
||||
/// </summary>
|
||||
public virtual int GetNewOffset(int offset, AnchorMovementType movementType = AnchorMovementType.Default) |
||||
{ |
||||
if (offset >= this.Offset && offset <= this.Offset + this.RemovalLength) { |
||||
if (movementType == AnchorMovementType.BeforeInsertion) |
||||
return this.Offset; |
||||
else |
||||
return this.Offset + this.InsertionLength; |
||||
} else if (offset > this.Offset) { |
||||
return offset + this.InsertionLength - this.RemovalLength; |
||||
} else { |
||||
return offset; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Creates TextChangeEventArgs for the reverse change.
|
||||
/// </summary>
|
||||
public virtual TextChangeEventArgs Invert() |
||||
{ |
||||
return new TextChangeEventArgs(offset, insertedText, removedText); |
||||
} |
||||
} |
||||
#endif
|
||||
} |
@ -1,77 +0,0 @@
@@ -1,77 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
/// <summary>
|
||||
/// Allows for low-level line tracking.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The methods on this interface are called by the TextDocument's LineManager immediately after the document
|
||||
/// has changed, *while* the DocumentLineTree is updating.
|
||||
/// Thus, the DocumentLineTree may be in an invalid state when these methods are called.
|
||||
/// This interface should only be used to update per-line data structures like the HeightTree.
|
||||
/// Line trackers must not cause any events to be raised during an update to prevent other code from seeing
|
||||
/// the invalid state.
|
||||
/// Line trackers may be called while the TextDocument has taken a lock.
|
||||
/// You must be careful not to dead-lock inside ILineTracker callbacks.
|
||||
/// </remarks>
|
||||
public interface ILineTracker |
||||
{ |
||||
/// <summary>
|
||||
/// Is called immediately before a document line is removed.
|
||||
/// </summary>
|
||||
void BeforeRemoveLine(DocumentLine line); |
||||
|
||||
// /// <summary>
|
||||
// /// Is called immediately after a document line is removed.
|
||||
// /// </summary>
|
||||
// void AfterRemoveLine(DocumentLine line);
|
||||
|
||||
/// <summary>
|
||||
/// Is called immediately before a document line changes length.
|
||||
/// This method will be called whenever the line is changed, even when the length stays as it is.
|
||||
/// The method might be called multiple times for a single line because
|
||||
/// a replacement is internally handled as removal followed by insertion.
|
||||
/// </summary>
|
||||
void SetLineLength(DocumentLine line, int newTotalLength); |
||||
|
||||
/// <summary>
|
||||
/// Is called immediately after a line was inserted.
|
||||
/// </summary>
|
||||
/// <param name="newLine">The new line</param>
|
||||
/// <param name="insertionPos">The existing line before the new line</param>
|
||||
void LineInserted(DocumentLine insertionPos, DocumentLine newLine); |
||||
|
||||
/// <summary>
|
||||
/// Indicates that there were changes to the document that the line tracker was not notified of.
|
||||
/// The document is in a consistent state (but the line trackers aren't), and line trackers should
|
||||
/// throw away their data and rebuild the document.
|
||||
/// </summary>
|
||||
void RebuildDocument(); |
||||
|
||||
/// <summary>
|
||||
/// Notifies the line tracker that a document change (a single change, not a change group) has completed.
|
||||
/// This method gets called after the change has been performed, but before the <see cref="TextDocument.Changed"/> event
|
||||
/// is raised.
|
||||
/// </summary>
|
||||
void ChangeComplete(DocumentChangeEventArgs e); |
||||
} |
||||
} |
@ -1,142 +0,0 @@
@@ -1,142 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using ICSharpCode.NRefactory; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
#if !NREFACTORY
|
||||
/// <summary>
|
||||
/// The TextAnchor class references an offset (a position between two characters).
|
||||
/// It automatically updates the offset when text is inserted/removed in front of the anchor.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Use the <see cref="ITextAnchor.Offset"/> property to get the offset from a text anchor.
|
||||
/// Use the <see cref="IDocument.CreateAnchor"/> method to create an anchor from an offset.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The document will automatically update all text anchors; and because it uses weak references to do so,
|
||||
/// the garbage collector can simply collect the anchor object when you don't need it anymore.
|
||||
/// </para>
|
||||
/// <para>Moreover, the document is able to efficiently update a large number of anchors without having to look
|
||||
/// at each anchor object individually. Updating the offsets of all anchors usually only takes time logarithmic
|
||||
/// to the number of anchors. Retrieving the <see cref="ITextAnchor.Offset"/> property also runs in O(lg N).</para>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// Usage:
|
||||
/// <code>TextAnchor anchor = document.CreateAnchor(offset);
|
||||
/// ChangeMyDocument();
|
||||
/// int newOffset = anchor.Offset;
|
||||
/// </code>
|
||||
/// </example>
|
||||
public interface ITextAnchor |
||||
{ |
||||
/// <summary>
|
||||
/// Gets the text location of this anchor.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown when trying to get the Offset from a deleted anchor.</exception>
|
||||
TextLocation Location { get; } |
||||
|
||||
/// <summary>
|
||||
/// Gets the offset of the text anchor.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown when trying to get the Offset from a deleted anchor.</exception>
|
||||
int Offset { get; } |
||||
|
||||
/// <summary>
|
||||
/// Controls how the anchor moves.
|
||||
/// </summary>
|
||||
/// <remarks>Anchor movement is ambiguous if text is inserted exactly at the anchor's location.
|
||||
/// Does the anchor stay before the inserted text, or does it move after it?
|
||||
/// The property <see cref="MovementType"/> will be used to determine which of these two options the anchor will choose.
|
||||
/// The default value is <see cref="AnchorMovementType.Default"/>.</remarks>
|
||||
AnchorMovementType MovementType { get; set; } |
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Specifies whether the anchor survives deletion of the text containing it.
|
||||
/// </para><para>
|
||||
/// <c>false</c>: The anchor is deleted when the a selection that includes the anchor is deleted.
|
||||
/// <c>true</c>: The anchor is not deleted.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <remarks><inheritdoc cref="IsDeleted" /></remarks>
|
||||
bool SurviveDeletion { get; set; } |
||||
|
||||
/// <summary>
|
||||
/// Gets whether the anchor was deleted.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>When a piece of text containing an anchor is removed, then that anchor will be deleted.
|
||||
/// First, the <see cref="IsDeleted"/> property is set to true on all deleted anchors,
|
||||
/// then the <see cref="Deleted"/> events are raised.
|
||||
/// You cannot retrieve the offset from an anchor that has been deleted.</para>
|
||||
/// <para>This deletion behavior might be useful when using anchors for building a bookmark feature,
|
||||
/// but in other cases you want to still be able to use the anchor. For those cases, set <c><see cref="SurviveDeletion"/> = true</c>.</para>
|
||||
/// </remarks>
|
||||
bool IsDeleted { get; } |
||||
|
||||
/// <summary>
|
||||
/// Occurs after the anchor was deleted.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <inheritdoc cref="IsDeleted" />
|
||||
/// <para>Due to the 'weak reference' nature of text anchors, you will receive
|
||||
/// the Deleted event only while your code holds a reference to the TextAnchor object.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
event EventHandler Deleted; |
||||
|
||||
/// <summary>
|
||||
/// Gets the line number of the anchor.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown when trying to get the Offset from a deleted anchor.</exception>
|
||||
int Line { get; } |
||||
|
||||
/// <summary>
|
||||
/// Gets the column number of this anchor.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown when trying to get the Offset from a deleted anchor.</exception>
|
||||
int Column { get; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Defines how a text anchor moves.
|
||||
/// </summary>
|
||||
public enum AnchorMovementType |
||||
{ |
||||
/// <summary>
|
||||
/// When text is inserted at the anchor position, the type of the insertion
|
||||
/// determines where the caret moves to. For normal insertions, the anchor will move
|
||||
/// after the inserted text.
|
||||
/// </summary>
|
||||
Default, |
||||
/// <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 |
||||
} |
||||
#endif
|
||||
} |
@ -1,357 +0,0 @@
@@ -1,357 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.Collections.Generic; |
||||
using System; |
||||
using System.IO; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
#if !NREFACTORY
|
||||
/// <summary>
|
||||
/// A read-only view on a (potentially mutable) text source.
|
||||
/// The IDocument interface derives from this interface.
|
||||
/// </summary>
|
||||
public interface ITextSource |
||||
{ |
||||
/// <summary>
|
||||
/// Gets a version identifier for this text source.
|
||||
/// Returns null for unversioned text sources.
|
||||
/// </summary>
|
||||
ITextSourceVersion Version { get; } |
||||
|
||||
/// <summary>
|
||||
/// Creates an immutable snapshot of this text source.
|
||||
/// Unlike all other methods in this interface, this method is thread-safe.
|
||||
/// </summary>
|
||||
ITextSource CreateSnapshot(); |
||||
|
||||
/// <summary>
|
||||
/// Creates an immutable snapshot of a part of this text source.
|
||||
/// Unlike all other methods in this interface, this method is thread-safe.
|
||||
/// </summary>
|
||||
ITextSource CreateSnapshot(int offset, int length); |
||||
|
||||
/// <summary>
|
||||
/// Creates a new TextReader to read from this text source.
|
||||
/// </summary>
|
||||
TextReader CreateReader(); |
||||
|
||||
/// <summary>
|
||||
/// Creates a new TextReader to read from this text source.
|
||||
/// </summary>
|
||||
TextReader CreateReader(int offset, int length); |
||||
|
||||
/// <summary>
|
||||
/// Gets the total text length.
|
||||
/// </summary>
|
||||
/// <returns>The length of the text, in characters.</returns>
|
||||
/// <remarks>This is the same as Text.Length, but is more efficient because
|
||||
/// it doesn't require creating a String object.</remarks>
|
||||
int TextLength { get; } |
||||
|
||||
/// <summary>
|
||||
/// Gets the whole text as string.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods")] |
||||
string Text { get; } |
||||
|
||||
/// <summary>
|
||||
/// Gets a character at the specified position in the document.
|
||||
/// </summary>
|
||||
/// <paramref name="offset">The index of the character to get.</paramref>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Offset is outside the valid range (0 to TextLength-1).</exception>
|
||||
/// <returns>The character at the specified position.</returns>
|
||||
/// <remarks>This is the same as Text[offset], but is more efficient because
|
||||
/// it doesn't require creating a String object.</remarks>
|
||||
char GetCharAt(int offset); |
||||
|
||||
/// <summary>
|
||||
/// Retrieves the text for a portion of the document.
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentOutOfRangeException">offset or length is outside the valid range.</exception>
|
||||
/// <remarks>This is the same as Text.Substring, but is more efficient because
|
||||
/// it doesn't require creating a String object for the whole document.</remarks>
|
||||
string GetText(int offset, int length); |
||||
|
||||
/// <summary>
|
||||
/// Retrieves the text for a portion of the document.
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentOutOfRangeException">offset or length is outside the valid range.</exception>
|
||||
string GetText(ISegment segment); |
||||
|
||||
/// <summary>
|
||||
/// Writes the text from this document into the TextWriter.
|
||||
/// </summary>
|
||||
void WriteTextTo(TextWriter writer); |
||||
|
||||
/// <summary>
|
||||
/// Writes the text from this document into the TextWriter.
|
||||
/// </summary>
|
||||
void WriteTextTo(TextWriter writer, int offset, int length); |
||||
|
||||
/// <summary>
|
||||
/// Gets the index of the first occurrence of the character in the specified array.
|
||||
/// </summary>
|
||||
/// <param name="c">Character to search for</param>
|
||||
/// <param name="startIndex">Start index of the area to search.</param>
|
||||
/// <param name="count">Length of the area to search.</param>
|
||||
/// <returns>The first index where the character was found; or -1 if no occurrence was found.</returns>
|
||||
int IndexOf(char c, int startIndex, int count); |
||||
|
||||
/// <summary>
|
||||
/// Gets the index of the first occurrence of any character in the specified array.
|
||||
/// </summary>
|
||||
/// <param name="anyOf">Characters to search for</param>
|
||||
/// <param name="startIndex">Start index of the area to search.</param>
|
||||
/// <param name="count">Length of the area to search.</param>
|
||||
/// <returns>The first index where any character was found; or -1 if no occurrence was found.</returns>
|
||||
int IndexOfAny(char[] anyOf, int startIndex, int count); |
||||
|
||||
/// <summary>
|
||||
/// Gets the index of the first occurrence of the specified search text in this text source.
|
||||
/// </summary>
|
||||
/// <param name="searchText">The search text</param>
|
||||
/// <param name="startIndex">Start index of the area to search.</param>
|
||||
/// <param name="count">Length of the area to search.</param>
|
||||
/// <param name="comparisonType">String comparison to use.</param>
|
||||
/// <returns>The first index where the search term was found; or -1 if no occurrence was found.</returns>
|
||||
int IndexOf(string searchText, int startIndex, int count, StringComparison comparisonType); |
||||
|
||||
/// <summary>
|
||||
/// Gets the index of the last occurrence of the specified character in this text source.
|
||||
/// </summary>
|
||||
/// <param name="c">The search character</param>
|
||||
/// <param name="startIndex">Start index of the area to search.</param>
|
||||
/// <param name="count">Length of the area to search.</param>
|
||||
/// <returns>The last index where the search term was found; or -1 if no occurrence was found.</returns>
|
||||
/// <remarks>The search proceeds backwards from (startIndex+count) to startIndex.
|
||||
/// This is different than the meaning of the parameters on string.LastIndexOf!</remarks>
|
||||
int LastIndexOf(char c, int startIndex, int count); |
||||
|
||||
/// <summary>
|
||||
/// Gets the index of the last occurrence of the specified search text in this text source.
|
||||
/// </summary>
|
||||
/// <param name="searchText">The search text</param>
|
||||
/// <param name="startIndex">Start index of the area to search.</param>
|
||||
/// <param name="count">Length of the area to search.</param>
|
||||
/// <param name="comparisonType">String comparison to use.</param>
|
||||
/// <returns>The last index where the search term was found; or -1 if no occurrence was found.</returns>
|
||||
/// <remarks>The search proceeds backwards from (startIndex+count) to startIndex.
|
||||
/// This is different than the meaning of the parameters on string.LastIndexOf!</remarks>
|
||||
int LastIndexOf(string searchText, int startIndex, int count, StringComparison comparisonType); |
||||
|
||||
/* What about: |
||||
void Insert (int offset, string value); |
||||
void Remove (int offset, int count); |
||||
void Remove (ISegment segment); |
||||
|
||||
void Replace (int offset, int count, string value); |
||||
|
||||
Or more search operations: |
||||
|
||||
IEnumerable<int> SearchForward (string pattern, int startIndex); |
||||
IEnumerable<int> SearchForwardIgnoreCase (string pattern, int startIndex); |
||||
|
||||
IEnumerable<int> SearchBackward (string pattern, int startIndex); |
||||
IEnumerable<int> SearchBackwardIgnoreCase (string pattern, int startIndex); |
||||
*/ |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Represents a version identifier for a text source.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Verions can be used to efficiently detect whether a document has changed and needs reparsing;
|
||||
/// or even to implement incremental parsers.
|
||||
/// It is a separate class from ITextSource to allow the GC to collect the text source while
|
||||
/// the version checkpoint is still in use.
|
||||
/// </remarks>
|
||||
public interface ITextSourceVersion |
||||
{ |
||||
/// <summary>
|
||||
/// Gets whether this checkpoint belongs to the same document as the other checkpoint.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Returns false when given <c>null</c>.
|
||||
/// </remarks>
|
||||
bool BelongsToSameDocumentAs(ITextSourceVersion other); |
||||
|
||||
/// <summary>
|
||||
/// Compares the age of this checkpoint to the other checkpoint.
|
||||
/// </summary>
|
||||
/// <remarks>This method is thread-safe.</remarks>
|
||||
/// <exception cref="ArgumentException">Raised if 'other' belongs to a different document than this version.</exception>
|
||||
/// <returns>-1 if this version is older than <paramref name="other"/>.
|
||||
/// 0 if <c>this</c> version instance represents the same version as <paramref name="other"/>.
|
||||
/// 1 if this version is newer than <paramref name="other"/>.</returns>
|
||||
int CompareAge(ITextSourceVersion other); |
||||
|
||||
/// <summary>
|
||||
/// Gets the changes from this checkpoint to the other checkpoint.
|
||||
/// If 'other' is older than this checkpoint, reverse changes are calculated.
|
||||
/// </summary>
|
||||
/// <remarks>This method is thread-safe.</remarks>
|
||||
/// <exception cref="ArgumentException">Raised if 'other' belongs to a different document than this checkpoint.</exception>
|
||||
IEnumerable<TextChangeEventArgs> GetChangesTo(ITextSourceVersion other); |
||||
|
||||
/// <summary>
|
||||
/// Calculates where the offset has moved in the other buffer version.
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentException">Raised if 'other' belongs to a different document than this checkpoint.</exception>
|
||||
int MoveOffsetTo(ITextSourceVersion other, int oldOffset, AnchorMovementType movement = AnchorMovementType.Default); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Implements the ITextSource interface using a string.
|
||||
/// </summary>
|
||||
[Serializable] |
||||
public class StringTextSource : ITextSource |
||||
{ |
||||
/// <summary>
|
||||
/// Gets a text source containing the empty string.
|
||||
/// </summary>
|
||||
public static readonly StringTextSource Empty = new StringTextSource(string.Empty); |
||||
|
||||
readonly string text; |
||||
readonly ITextSourceVersion version; |
||||
|
||||
/// <summary>
|
||||
/// Creates a new StringTextSource with the given text.
|
||||
/// </summary>
|
||||
public StringTextSource(string text) |
||||
{ |
||||
if (text == null) |
||||
throw new ArgumentNullException("text"); |
||||
this.text = text; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Creates a new StringTextSource with the given text.
|
||||
/// </summary>
|
||||
public StringTextSource(string text, ITextSourceVersion version) |
||||
{ |
||||
if (text == null) |
||||
throw new ArgumentNullException("text"); |
||||
this.text = text; |
||||
this.version = version; |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public ITextSourceVersion Version { |
||||
get { return version; } |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public int TextLength { |
||||
get { return text.Length; } |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public string Text { |
||||
get { return text; } |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public ITextSource CreateSnapshot() |
||||
{ |
||||
return this; // StringTextSource is immutable
|
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public ITextSource CreateSnapshot(int offset, int length) |
||||
{ |
||||
return new StringTextSource(text.Substring(offset, length)); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public TextReader CreateReader() |
||||
{ |
||||
return new StringReader(text); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public TextReader CreateReader(int offset, int length) |
||||
{ |
||||
return new StringReader(text.Substring(offset, length)); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public void WriteTextTo(TextWriter writer) |
||||
{ |
||||
writer.Write(text); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public void WriteTextTo(TextWriter writer, int offset, int length) |
||||
{ |
||||
writer.Write(text.Substring(offset, length)); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public char GetCharAt(int offset) |
||||
{ |
||||
return text[offset]; |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetText(int offset, int length) |
||||
{ |
||||
return text.Substring(offset, length); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetText(ISegment segment) |
||||
{ |
||||
if (segment == null) |
||||
throw new ArgumentNullException("segment"); |
||||
return text.Substring(segment.Offset, segment.Length); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public int IndexOf(char c, int startIndex, int count) |
||||
{ |
||||
return text.IndexOf(c, startIndex, count); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public int IndexOfAny(char[] anyOf, int startIndex, int count) |
||||
{ |
||||
return text.IndexOfAny(anyOf, startIndex, count); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public int IndexOf(string searchText, int startIndex, int count, StringComparison comparisonType) |
||||
{ |
||||
return text.IndexOf(searchText, startIndex, count, comparisonType); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public int LastIndexOf(char c, int startIndex, int count) |
||||
{ |
||||
return text.LastIndexOf(c, startIndex + count - 1, count); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public int LastIndexOf(string searchText, int startIndex, int count, StringComparison comparisonType) |
||||
{ |
||||
return text.LastIndexOf(searchText, startIndex + count - 1, count, comparisonType); |
||||
} |
||||
} |
||||
#endif
|
||||
} |
@ -1,45 +0,0 @@
@@ -1,45 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
/// <summary>
|
||||
/// This Interface describes a the basic Undo/Redo operation
|
||||
/// all Undo Operations must implement this interface.
|
||||
/// </summary>
|
||||
public interface IUndoableOperation |
||||
{ |
||||
/// <summary>
|
||||
/// Undo the last operation
|
||||
/// </summary>
|
||||
void Undo(); |
||||
|
||||
/// <summary>
|
||||
/// Redo the last operation
|
||||
/// </summary>
|
||||
void Redo(); |
||||
} |
||||
|
||||
interface IUndoableOperationWithContext : IUndoableOperation |
||||
{ |
||||
void Undo(UndoStack stack); |
||||
void Redo(UndoStack stack); |
||||
} |
||||
} |
@ -1,318 +0,0 @@
@@ -1,318 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Diagnostics; |
||||
using System.Linq; |
||||
using ICSharpCode.NRefactory.Editor; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
/// <summary>
|
||||
/// Creates/Deletes lines when text is inserted/removed.
|
||||
/// </summary>
|
||||
sealed class LineManager |
||||
{ |
||||
#region Constructor
|
||||
readonly TextDocument document; |
||||
readonly DocumentLineTree documentLineTree; |
||||
|
||||
/// <summary>
|
||||
/// A copy of the line trackers. We need a copy so that line trackers may remove themselves
|
||||
/// while being notified (used e.g. by WeakLineTracker)
|
||||
/// </summary>
|
||||
ILineTracker[] lineTrackers; |
||||
|
||||
internal void UpdateListOfLineTrackers() |
||||
{ |
||||
this.lineTrackers = document.LineTrackers.ToArray(); |
||||
} |
||||
|
||||
public LineManager(DocumentLineTree documentLineTree, TextDocument document) |
||||
{ |
||||
this.document = document; |
||||
this.documentLineTree = documentLineTree; |
||||
UpdateListOfLineTrackers(); |
||||
|
||||
Rebuild(); |
||||
} |
||||
#endregion
|
||||
|
||||
#region Change events
|
||||
/* |
||||
HashSet<DocumentLine> deletedLines = new HashSet<DocumentLine>(); |
||||
readonly HashSet<DocumentLine> changedLines = new HashSet<DocumentLine>(); |
||||
HashSet<DocumentLine> deletedOrChangedLines = new HashSet<DocumentLine>(); |
||||
|
||||
/// <summary>
|
||||
/// Gets the list of lines deleted since the last RetrieveChangedLines() call.
|
||||
/// The returned list is unsorted.
|
||||
/// </summary>
|
||||
public ICollection<DocumentLine> RetrieveDeletedLines() |
||||
{ |
||||
var r = deletedLines; |
||||
deletedLines = new HashSet<DocumentLine>(); |
||||
return r; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the list of lines changed since the last RetrieveChangedLines() call.
|
||||
/// The returned list is sorted by line number and does not contain deleted lines.
|
||||
/// </summary>
|
||||
public List<DocumentLine> RetrieveChangedLines() |
||||
{ |
||||
var list = (from line in changedLines |
||||
where !line.IsDeleted |
||||
let number = line.LineNumber |
||||
orderby number |
||||
select line).ToList(); |
||||
changedLines.Clear(); |
||||
return list; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the list of lines changed since the last RetrieveDeletedOrChangedLines() call.
|
||||
/// The returned list is not sorted.
|
||||
/// </summary>
|
||||
public ICollection<DocumentLine> RetrieveDeletedOrChangedLines() |
||||
{ |
||||
var r = deletedOrChangedLines; |
||||
deletedOrChangedLines = new HashSet<DocumentLine>(); |
||||
return r; |
||||
} |
||||
*/ |
||||
#endregion
|
||||
|
||||
#region Rebuild
|
||||
public void Rebuild() |
||||
{ |
||||
// keep the first document line
|
||||
DocumentLine ls = documentLineTree.GetByNumber(1); |
||||
// but mark all other lines as deleted, and detach them from the other nodes
|
||||
for (DocumentLine line = ls.NextLine; line != null; line = line.NextLine) { |
||||
line.isDeleted = true; |
||||
line.parent = line.left = line.right = null; |
||||
} |
||||
// Reset the first line to detach it from the deleted lines
|
||||
ls.ResetLine(); |
||||
SimpleSegment ds = NewLineFinder.NextNewLine(document, 0); |
||||
List<DocumentLine> lines = new List<DocumentLine>(); |
||||
int lastDelimiterEnd = 0; |
||||
while (ds != SimpleSegment.Invalid) { |
||||
ls.TotalLength = ds.Offset + ds.Length - lastDelimiterEnd; |
||||
ls.DelimiterLength = ds.Length; |
||||
lastDelimiterEnd = ds.Offset + ds.Length; |
||||
lines.Add(ls); |
||||
|
||||
ls = new DocumentLine(document); |
||||
ds = NewLineFinder.NextNewLine(document, lastDelimiterEnd); |
||||
} |
||||
ls.TotalLength = document.TextLength - lastDelimiterEnd; |
||||
lines.Add(ls); |
||||
documentLineTree.RebuildTree(lines); |
||||
foreach (ILineTracker lineTracker in lineTrackers) |
||||
lineTracker.RebuildDocument(); |
||||
} |
||||
#endregion
|
||||
|
||||
#region Remove
|
||||
public void Remove(int offset, int length) |
||||
{ |
||||
Debug.Assert(length >= 0); |
||||
if (length == 0) return; |
||||
DocumentLine startLine = documentLineTree.GetByOffset(offset); |
||||
int startLineOffset = startLine.Offset; |
||||
|
||||
Debug.Assert(offset < startLineOffset + startLine.TotalLength); |
||||
if (offset > startLineOffset + startLine.Length) { |
||||
Debug.Assert(startLine.DelimiterLength == 2); |
||||
// we are deleting starting in the middle of a delimiter
|
||||
|
||||
// remove last delimiter part
|
||||
SetLineLength(startLine, startLine.TotalLength - 1); |
||||
// remove remaining text
|
||||
Remove(offset, length - 1); |
||||
return; |
||||
} |
||||
|
||||
if (offset + length < startLineOffset + startLine.TotalLength) { |
||||
// just removing a part of this line
|
||||
//startLine.RemovedLinePart(ref deferredEventList, offset - startLineOffset, length);
|
||||
SetLineLength(startLine, startLine.TotalLength - length); |
||||
return; |
||||
} |
||||
// merge startLine with another line because startLine's delimiter was deleted
|
||||
// possibly remove lines in between if multiple delimiters were deleted
|
||||
int charactersRemovedInStartLine = startLineOffset + startLine.TotalLength - offset; |
||||
Debug.Assert(charactersRemovedInStartLine > 0); |
||||
//startLine.RemovedLinePart(ref deferredEventList, offset - startLineOffset, charactersRemovedInStartLine);
|
||||
|
||||
|
||||
DocumentLine endLine = documentLineTree.GetByOffset(offset + length); |
||||
if (endLine == startLine) { |
||||
// special case: we are removing a part of the last line up to the
|
||||
// end of the document
|
||||
SetLineLength(startLine, startLine.TotalLength - length); |
||||
return; |
||||
} |
||||
int endLineOffset = endLine.Offset; |
||||
int charactersLeftInEndLine = endLineOffset + endLine.TotalLength - (offset + length); |
||||
//endLine.RemovedLinePart(ref deferredEventList, 0, endLine.TotalLength - charactersLeftInEndLine);
|
||||
//startLine.MergedWith(endLine, offset - startLineOffset);
|
||||
|
||||
// remove all lines between startLine (excl.) and endLine (incl.)
|
||||
DocumentLine tmp = startLine.NextLine; |
||||
DocumentLine lineToRemove; |
||||
do { |
||||
lineToRemove = tmp; |
||||
tmp = tmp.NextLine; |
||||
RemoveLine(lineToRemove); |
||||
} while (lineToRemove != endLine); |
||||
|
||||
SetLineLength(startLine, startLine.TotalLength - charactersRemovedInStartLine + charactersLeftInEndLine); |
||||
} |
||||
|
||||
void RemoveLine(DocumentLine lineToRemove) |
||||
{ |
||||
foreach (ILineTracker lt in lineTrackers) |
||||
lt.BeforeRemoveLine(lineToRemove); |
||||
documentLineTree.RemoveLine(lineToRemove); |
||||
// foreach (ILineTracker lt in lineTracker)
|
||||
// lt.AfterRemoveLine(lineToRemove);
|
||||
// deletedLines.Add(lineToRemove);
|
||||
// deletedOrChangedLines.Add(lineToRemove);
|
||||
} |
||||
|
||||
#endregion
|
||||
|
||||
#region Insert
|
||||
public void Insert(int offset, ITextSource text) |
||||
{ |
||||
DocumentLine line = documentLineTree.GetByOffset(offset); |
||||
int lineOffset = line.Offset; |
||||
|
||||
Debug.Assert(offset <= lineOffset + line.TotalLength); |
||||
if (offset > lineOffset + line.Length) { |
||||
Debug.Assert(line.DelimiterLength == 2); |
||||
// we are inserting in the middle of a delimiter
|
||||
|
||||
// shorten line
|
||||
SetLineLength(line, line.TotalLength - 1); |
||||
// add new line
|
||||
line = InsertLineAfter(line, 1); |
||||
line = SetLineLength(line, 1); |
||||
} |
||||
|
||||
SimpleSegment ds = NewLineFinder.NextNewLine(text, 0); |
||||
if (ds == SimpleSegment.Invalid) { |
||||
// no newline is being inserted, all text is inserted in a single line
|
||||
//line.InsertedLinePart(offset - line.Offset, text.Length);
|
||||
SetLineLength(line, line.TotalLength + text.TextLength); |
||||
return; |
||||
} |
||||
//DocumentLine firstLine = line;
|
||||
//firstLine.InsertedLinePart(offset - firstLine.Offset, ds.Offset);
|
||||
int lastDelimiterEnd = 0; |
||||
while (ds != SimpleSegment.Invalid) { |
||||
// split line segment at line delimiter
|
||||
int lineBreakOffset = offset + ds.Offset + ds.Length; |
||||
lineOffset = line.Offset; |
||||
int lengthAfterInsertionPos = lineOffset + line.TotalLength - (offset + lastDelimiterEnd); |
||||
line = SetLineLength(line, lineBreakOffset - lineOffset); |
||||
DocumentLine newLine = InsertLineAfter(line, lengthAfterInsertionPos); |
||||
newLine = SetLineLength(newLine, lengthAfterInsertionPos); |
||||
|
||||
line = newLine; |
||||
lastDelimiterEnd = ds.Offset + ds.Length; |
||||
|
||||
ds = NewLineFinder.NextNewLine(text, lastDelimiterEnd); |
||||
} |
||||
//firstLine.SplitTo(line);
|
||||
// insert rest after last delimiter
|
||||
if (lastDelimiterEnd != text.TextLength) { |
||||
//line.InsertedLinePart(0, text.Length - lastDelimiterEnd);
|
||||
SetLineLength(line, line.TotalLength + text.TextLength - lastDelimiterEnd); |
||||
} |
||||
} |
||||
|
||||
DocumentLine InsertLineAfter(DocumentLine line, int length) |
||||
{ |
||||
DocumentLine newLine = documentLineTree.InsertLineAfter(line, length); |
||||
foreach (ILineTracker lt in lineTrackers) |
||||
lt.LineInserted(line, newLine); |
||||
return newLine; |
||||
} |
||||
#endregion
|
||||
|
||||
#region SetLineLength
|
||||
/// <summary>
|
||||
/// Sets the total line length and checks the delimiter.
|
||||
/// This method can cause line to be deleted when it contains a single '\n' character
|
||||
/// and the previous line ends with '\r'.
|
||||
/// </summary>
|
||||
/// <returns>Usually returns <paramref name="line"/>, but if line was deleted due to
|
||||
/// the "\r\n" merge, returns the previous line.</returns>
|
||||
DocumentLine SetLineLength(DocumentLine line, int newTotalLength) |
||||
{ |
||||
// changedLines.Add(line);
|
||||
// deletedOrChangedLines.Add(line);
|
||||
int delta = newTotalLength - line.TotalLength; |
||||
if (delta != 0) { |
||||
foreach (ILineTracker lt in lineTrackers) |
||||
lt.SetLineLength(line, newTotalLength); |
||||
line.TotalLength = newTotalLength; |
||||
DocumentLineTree.UpdateAfterChildrenChange(line); |
||||
} |
||||
// determine new DelimiterLength
|
||||
if (newTotalLength == 0) { |
||||
line.DelimiterLength = 0; |
||||
} else { |
||||
int lineOffset = line.Offset; |
||||
char lastChar = document.GetCharAt(lineOffset + newTotalLength - 1); |
||||
if (lastChar == '\r') { |
||||
line.DelimiterLength = 1; |
||||
} else if (lastChar == '\n') { |
||||
if (newTotalLength >= 2 && document.GetCharAt(lineOffset + newTotalLength - 2) == '\r') { |
||||
line.DelimiterLength = 2; |
||||
} else if (newTotalLength == 1 && lineOffset > 0 && document.GetCharAt(lineOffset - 1) == '\r') { |
||||
// we need to join this line with the previous line
|
||||
DocumentLine previousLine = line.PreviousLine; |
||||
RemoveLine(line); |
||||
return SetLineLength(previousLine, previousLine.TotalLength + 1); |
||||
} else { |
||||
line.DelimiterLength = 1; |
||||
} |
||||
} else { |
||||
line.DelimiterLength = 0; |
||||
} |
||||
} |
||||
return line; |
||||
} |
||||
#endregion
|
||||
|
||||
#region ChangeComplete
|
||||
public void ChangeComplete(DocumentChangeEventArgs e) |
||||
{ |
||||
foreach (ILineTracker lt in lineTrackers) { |
||||
lt.ChangeComplete(e); |
||||
} |
||||
} |
||||
#endregion
|
||||
} |
||||
} |
@ -1,98 +0,0 @@
@@ -1,98 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
using LineNode = DocumentLine; |
||||
|
||||
// A tree node in the document line tree.
|
||||
// For the purpose of the invariants, "children", "descendents", "siblings" etc. include the DocumentLine object,
|
||||
// it is treated as a third child node between left and right.
|
||||
|
||||
// Originally, this was a separate class, with a reference to the documentLine. The documentLine had a reference
|
||||
// back to the node. To save memory, the same object is used for both the documentLine and the line node.
|
||||
// This saves 16 bytes per line (8 byte object overhead + two pointers).
|
||||
// sealed class LineNode
|
||||
// {
|
||||
// internal readonly DocumentLine documentLine;
|
||||
partial class DocumentLine |
||||
{ |
||||
internal DocumentLine left, right, parent; |
||||
internal bool color; |
||||
// optimization note: I tried packing color and isDeleted into a single byte field, but that
|
||||
// actually increased the memory requirements. The JIT packs two bools and a byte (delimiterSize)
|
||||
// into a single DWORD, but two bytes get each their own DWORD. Three bytes end up in the same DWORD, so
|
||||
// apparently the JIT only optimizes for memory when there are at least three small fields.
|
||||
// Currently, DocumentLine takes 36 bytes on x86 (8 byte object overhead, 3 pointers, 3 ints, and another DWORD
|
||||
// for the small fields).
|
||||
// TODO: a possible optimization would be to combine 'totalLength' and the small fields into a single uint.
|
||||
// delimiterSize takes only two bits, the two bools take another two bits; so there's still
|
||||
// 28 bits left for totalLength. 268435455 characters per line should be enough for everyone :)
|
||||
|
||||
/// <summary>
|
||||
/// Resets the line to enable its reuse after a document rebuild.
|
||||
/// </summary>
|
||||
internal void ResetLine() |
||||
{ |
||||
totalLength = delimiterLength = 0; |
||||
isDeleted = color = false; |
||||
left = right = parent = null; |
||||
} |
||||
|
||||
internal LineNode InitLineNode() |
||||
{ |
||||
this.nodeTotalCount = 1; |
||||
this.nodeTotalLength = this.TotalLength; |
||||
return this; |
||||
} |
||||
|
||||
internal LineNode LeftMost { |
||||
get { |
||||
LineNode node = this; |
||||
while (node.left != null) |
||||
node = node.left; |
||||
return node; |
||||
} |
||||
} |
||||
|
||||
internal LineNode RightMost { |
||||
get { |
||||
LineNode node = this; |
||||
while (node.right != null) |
||||
node = node.right; |
||||
return node; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// The number of lines in this node and its child nodes.
|
||||
/// Invariant:
|
||||
/// nodeTotalCount = 1 + left.nodeTotalCount + right.nodeTotalCount
|
||||
/// </summary>
|
||||
internal int nodeTotalCount; |
||||
|
||||
/// <summary>
|
||||
/// The total text length of this node and its child nodes.
|
||||
/// Invariant:
|
||||
/// nodeTotalLength = left.nodeTotalLength + documentLine.TotalLength + right.nodeTotalLength
|
||||
/// </summary>
|
||||
internal int nodeTotalLength; |
||||
} |
||||
} |
@ -1,150 +0,0 @@
@@ -1,150 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Text; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory.Editor; |
||||
#endif
|
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
static class NewLineFinder |
||||
{ |
||||
static readonly char[] newline = { '\r', '\n' }; |
||||
|
||||
internal static readonly string[] NewlineStrings = { "\r\n", "\r", "\n" }; |
||||
|
||||
/// <summary>
|
||||
/// Gets the location of the next new line character, or SimpleSegment.Invalid
|
||||
/// if none is found.
|
||||
/// </summary>
|
||||
internal static SimpleSegment NextNewLine(string text, int offset) |
||||
{ |
||||
int pos = text.IndexOfAny(newline, offset); |
||||
if (pos >= 0) { |
||||
if (text[pos] == '\r') { |
||||
if (pos + 1 < text.Length && text[pos + 1] == '\n') |
||||
return new SimpleSegment(pos, 2); |
||||
} |
||||
return new SimpleSegment(pos, 1); |
||||
} |
||||
return SimpleSegment.Invalid; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the location of the next new line character, or SimpleSegment.Invalid
|
||||
/// if none is found.
|
||||
/// </summary>
|
||||
internal static SimpleSegment NextNewLine(ITextSource text, int offset) |
||||
{ |
||||
int textLength = text.TextLength; |
||||
int pos = text.IndexOfAny(newline, offset, textLength - offset); |
||||
if (pos >= 0) { |
||||
if (text.GetCharAt(pos) == '\r') { |
||||
if (pos + 1 < textLength && text.GetCharAt(pos + 1) == '\n') |
||||
return new SimpleSegment(pos, 2); |
||||
} |
||||
return new SimpleSegment(pos, 1); |
||||
} |
||||
return SimpleSegment.Invalid; |
||||
} |
||||
} |
||||
|
||||
partial class TextUtilities |
||||
{ |
||||
/// <summary>
|
||||
/// Finds the next new line character starting at offset.
|
||||
/// </summary>
|
||||
/// <param name="text">The text source to search in.</param>
|
||||
/// <param name="offset">The starting offset for the search.</param>
|
||||
/// <param name="newLineType">The string representing the new line that was found, or null if no new line was found.</param>
|
||||
/// <returns>The position of the first new line starting at or after <paramref name="offset"/>,
|
||||
/// or -1 if no new line was found.</returns>
|
||||
public static int FindNextNewLine(ITextSource text, int offset, out string newLineType) |
||||
{ |
||||
if (text == null) |
||||
throw new ArgumentNullException("text"); |
||||
if (offset < 0 || offset > text.TextLength) |
||||
throw new ArgumentOutOfRangeException("offset", offset, "offset is outside of text source"); |
||||
SimpleSegment s = NewLineFinder.NextNewLine(text, offset); |
||||
if (s == SimpleSegment.Invalid) { |
||||
newLineType = null; |
||||
return -1; |
||||
} else { |
||||
if (s.Length == 2) { |
||||
newLineType = "\r\n"; |
||||
} else if (text.GetCharAt(s.Offset) == '\n') { |
||||
newLineType = "\n"; |
||||
} else { |
||||
newLineType = "\r"; |
||||
} |
||||
return s.Offset; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets whether the specified string is a newline sequence.
|
||||
/// </summary>
|
||||
public static bool IsNewLine(string newLine) |
||||
{ |
||||
return newLine == "\r\n" || newLine == "\n" || newLine == "\r"; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Normalizes all new lines in <paramref name="input"/> to be <paramref name="newLine"/>.
|
||||
/// </summary>
|
||||
public static string NormalizeNewLines(string input, string newLine) |
||||
{ |
||||
if (input == null) |
||||
return null; |
||||
if (!IsNewLine(newLine)) |
||||
throw new ArgumentException("newLine must be one of the known newline sequences"); |
||||
SimpleSegment ds = NewLineFinder.NextNewLine(input, 0); |
||||
if (ds == SimpleSegment.Invalid) // text does not contain any new lines
|
||||
return input; |
||||
StringBuilder b = new StringBuilder(input.Length); |
||||
int lastEndOffset = 0; |
||||
do { |
||||
b.Append(input, lastEndOffset, ds.Offset - lastEndOffset); |
||||
b.Append(newLine); |
||||
lastEndOffset = ds.EndOffset; |
||||
ds = NewLineFinder.NextNewLine(input, lastEndOffset); |
||||
} while (ds != SimpleSegment.Invalid); |
||||
// remaining string (after last newline)
|
||||
b.Append(input, lastEndOffset, input.Length - lastEndOffset); |
||||
return b.ToString(); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the newline sequence used in the document at the specified line.
|
||||
/// </summary>
|
||||
public static string GetNewLineFromDocument(IDocument document, int lineNumber) |
||||
{ |
||||
IDocumentLine line = document.GetLineByNumber(lineNumber); |
||||
if (line.DelimiterLength == 0) { |
||||
// at the end of the document, there's no line delimiter, so use the delimiter
|
||||
// from the previous line
|
||||
line = line.PreviousLine; |
||||
if (line == null) |
||||
return Environment.NewLine; |
||||
} |
||||
return document.GetText(line.Offset + line.Length, line.DelimiterLength); |
||||
} |
||||
} |
||||
} |
@ -1,364 +0,0 @@
@@ -1,364 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Collections.ObjectModel; |
||||
using System.Diagnostics.CodeAnalysis; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory.Editor; |
||||
#endif
|
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
/// <summary>
|
||||
/// Contains predefined offset change mapping types.
|
||||
/// </summary>
|
||||
public enum OffsetChangeMappingType |
||||
{ |
||||
/// <summary>
|
||||
/// Normal replace.
|
||||
/// Anchors in front of the replaced region will stay in front, anchors after the replaced region will stay after.
|
||||
/// Anchors in the middle of the removed region will be deleted. If they survive deletion,
|
||||
/// they move depending on their AnchorMovementType.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the default implementation of DocumentChangeEventArgs when OffsetChangeMap is null,
|
||||
/// so using this option usually works without creating an OffsetChangeMap instance.
|
||||
/// This is equivalent to an OffsetChangeMap with a single entry describing the replace operation.
|
||||
/// </remarks>
|
||||
Normal, |
||||
/// <summary>
|
||||
/// First the old text is removed, then the new text is inserted.
|
||||
/// Anchors immediately in front (or after) the replaced region may move to the other side of the insertion,
|
||||
/// depending on the AnchorMovementType.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is implemented as an OffsetChangeMap with two entries: the removal, and the insertion.
|
||||
/// </remarks>
|
||||
RemoveAndInsert, |
||||
/// <summary>
|
||||
/// The text is replaced character-by-character.
|
||||
/// Anchors keep their position inside the replaced text.
|
||||
/// Anchors after the replaced region will move accordingly if the replacement text has a different length than the replaced text.
|
||||
/// If the new text is shorter than the old text, anchors inside the old text that would end up behind the replacement text
|
||||
/// will be moved so that they point to the end of the replacement text.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// On the OffsetChangeMap level, growing text is implemented by replacing the last character in the replaced text
|
||||
/// with itself and the additional text segment. A simple insertion of the additional text would have the undesired
|
||||
/// effect of moving anchors immediately after the replaced text into the replacement text if they used
|
||||
/// AnchorMovementStyle.BeforeInsertion.
|
||||
/// Shrinking text is implemented by removing the text segment that's too long; but in a special mode that
|
||||
/// causes anchors to always survive irrespective of their <see cref="TextAnchor.SurviveDeletion"/> setting.
|
||||
/// If the text keeps its old size, this is implemented as OffsetChangeMap.Empty.
|
||||
/// </remarks>
|
||||
CharacterReplace, |
||||
/// <summary>
|
||||
/// Like 'Normal', but anchors with <see cref="TextAnchor.MovementType"/> = Default will stay in front of the
|
||||
/// insertion instead of being moved behind it.
|
||||
/// </summary>
|
||||
KeepAnchorBeforeInsertion |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Describes a series of offset changes.
|
||||
/// </summary>
|
||||
[Serializable] |
||||
[SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", |
||||
Justification="It's a mapping old offsets -> new offsets")] |
||||
public sealed class OffsetChangeMap : Collection<OffsetChangeMapEntry> |
||||
{ |
||||
/// <summary>
|
||||
/// Immutable OffsetChangeMap that is empty.
|
||||
/// </summary>
|
||||
[SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", |
||||
Justification="The Empty instance is immutable")] |
||||
public static readonly OffsetChangeMap Empty = new OffsetChangeMap(Empty<OffsetChangeMapEntry>.Array, true); |
||||
|
||||
/// <summary>
|
||||
/// Creates a new OffsetChangeMap with a single element.
|
||||
/// </summary>
|
||||
/// <param name="entry">The entry.</param>
|
||||
/// <returns>Returns a frozen OffsetChangeMap with a single entry.</returns>
|
||||
public static OffsetChangeMap FromSingleElement(OffsetChangeMapEntry entry) |
||||
{ |
||||
return new OffsetChangeMap(new OffsetChangeMapEntry[] { entry }, true); |
||||
} |
||||
|
||||
bool isFrozen; |
||||
|
||||
/// <summary>
|
||||
/// Creates a new OffsetChangeMap instance.
|
||||
/// </summary>
|
||||
public OffsetChangeMap() |
||||
{ |
||||
} |
||||
|
||||
internal OffsetChangeMap(int capacity) |
||||
: base(new List<OffsetChangeMapEntry>(capacity)) |
||||
{ |
||||
} |
||||
|
||||
private OffsetChangeMap(IList<OffsetChangeMapEntry> entries, bool isFrozen) |
||||
: base(entries) |
||||
{ |
||||
this.isFrozen = isFrozen; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the new offset where the specified offset moves after this document change.
|
||||
/// </summary>
|
||||
public int GetNewOffset(int offset, AnchorMovementType movementType = AnchorMovementType.Default) |
||||
{ |
||||
IList<OffsetChangeMapEntry> items = this.Items; |
||||
int count = items.Count; |
||||
for (int i = 0; i < count; i++) { |
||||
offset = items[i].GetNewOffset(offset, movementType); |
||||
} |
||||
return offset; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets whether this OffsetChangeMap is a valid explanation for the specified document change.
|
||||
/// </summary>
|
||||
public bool IsValidForDocumentChange(int offset, int removalLength, int insertionLength) |
||||
{ |
||||
int endOffset = offset + removalLength; |
||||
foreach (OffsetChangeMapEntry entry in this) { |
||||
// check that ChangeMapEntry is in valid range for this document change
|
||||
if (entry.Offset < offset || entry.Offset + entry.RemovalLength > endOffset) |
||||
return false; |
||||
endOffset += entry.InsertionLength - entry.RemovalLength; |
||||
} |
||||
// check that the total delta matches
|
||||
return endOffset == offset + insertionLength; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Calculates the inverted OffsetChangeMap (used for the undo operation).
|
||||
/// </summary>
|
||||
public OffsetChangeMap Invert() |
||||
{ |
||||
if (this == Empty) |
||||
return this; |
||||
OffsetChangeMap newMap = new OffsetChangeMap(this.Count); |
||||
for (int i = this.Count - 1; i >= 0; i--) { |
||||
OffsetChangeMapEntry entry = this[i]; |
||||
// swap InsertionLength and RemovalLength
|
||||
newMap.Add(new OffsetChangeMapEntry(entry.Offset, entry.InsertionLength, entry.RemovalLength)); |
||||
} |
||||
return newMap; |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void ClearItems() |
||||
{ |
||||
CheckFrozen(); |
||||
base.ClearItems(); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void InsertItem(int index, OffsetChangeMapEntry item) |
||||
{ |
||||
CheckFrozen(); |
||||
base.InsertItem(index, item); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void RemoveItem(int index) |
||||
{ |
||||
CheckFrozen(); |
||||
base.RemoveItem(index); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void SetItem(int index, OffsetChangeMapEntry item) |
||||
{ |
||||
CheckFrozen(); |
||||
base.SetItem(index, item); |
||||
} |
||||
|
||||
void CheckFrozen() |
||||
{ |
||||
if (isFrozen) |
||||
throw new InvalidOperationException("This instance is frozen and cannot be modified."); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets if this instance is frozen. Frozen instances are immutable and thus thread-safe.
|
||||
/// </summary>
|
||||
public bool IsFrozen { |
||||
get { return isFrozen; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Freezes this instance.
|
||||
/// </summary>
|
||||
public void Freeze() |
||||
{ |
||||
isFrozen = true; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// An entry in the OffsetChangeMap.
|
||||
/// This represents the offset of a document change (either insertion or removal, not both at once).
|
||||
/// </summary>
|
||||
[Serializable] |
||||
public struct OffsetChangeMapEntry : IEquatable<OffsetChangeMapEntry> |
||||
{ |
||||
readonly int offset; |
||||
|
||||
// MSB: DefaultAnchorMovementIsBeforeInsertion
|
||||
readonly uint insertionLengthWithMovementFlag; |
||||
|
||||
// MSB: RemovalNeverCausesAnchorDeletion; other 31 bits: RemovalLength
|
||||
readonly uint removalLengthWithDeletionFlag; |
||||
|
||||
/// <summary>
|
||||
/// The offset at which the change occurs.
|
||||
/// </summary>
|
||||
public int Offset { |
||||
get { return offset; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// The number of characters inserted.
|
||||
/// Returns 0 if this entry represents a removal.
|
||||
/// </summary>
|
||||
public int InsertionLength { |
||||
get { return (int)(insertionLengthWithMovementFlag & 0x7fffffff); } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// The number of characters removed.
|
||||
/// Returns 0 if this entry represents an insertion.
|
||||
/// </summary>
|
||||
public int RemovalLength { |
||||
get { return (int)(removalLengthWithDeletionFlag & 0x7fffffff); } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets whether the removal should not cause any anchor deletions.
|
||||
/// </summary>
|
||||
public bool RemovalNeverCausesAnchorDeletion { |
||||
get { return (removalLengthWithDeletionFlag & 0x80000000) != 0; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets whether default anchor movement causes the anchor to stay in front of the caret.
|
||||
/// </summary>
|
||||
public bool DefaultAnchorMovementIsBeforeInsertion { |
||||
get { return (insertionLengthWithMovementFlag & 0x80000000) != 0; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the new offset where the specified offset moves after this document change.
|
||||
/// </summary>
|
||||
public int GetNewOffset(int oldOffset, AnchorMovementType movementType = AnchorMovementType.Default) |
||||
{ |
||||
int insertionLength = this.InsertionLength; |
||||
int removalLength = this.RemovalLength; |
||||
if (!(removalLength == 0 && oldOffset == offset)) { |
||||
// we're getting trouble (both if statements in here would apply)
|
||||
// if there's no removal and we insert at the offset
|
||||
// -> we'd need to disambiguate by movementType, which is handled after the if
|
||||
|
||||
// offset is before start of change: no movement
|
||||
if (oldOffset <= offset) |
||||
return oldOffset; |
||||
// offset is after end of change: movement by normal delta
|
||||
if (oldOffset >= offset + removalLength) |
||||
return oldOffset + insertionLength - removalLength; |
||||
} |
||||
// we reach this point if
|
||||
// a) the oldOffset is inside the deleted segment
|
||||
// b) there was no removal and we insert at the caret position
|
||||
if (movementType == AnchorMovementType.AfterInsertion) |
||||
return offset + insertionLength; |
||||
else if (movementType == AnchorMovementType.BeforeInsertion) |
||||
return offset; |
||||
else |
||||
return this.DefaultAnchorMovementIsBeforeInsertion ? offset : offset + insertionLength; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Creates a new OffsetChangeMapEntry instance.
|
||||
/// </summary>
|
||||
public OffsetChangeMapEntry(int offset, int removalLength, int insertionLength) |
||||
{ |
||||
ThrowUtil.CheckNotNegative(offset, "offset"); |
||||
ThrowUtil.CheckNotNegative(removalLength, "removalLength"); |
||||
ThrowUtil.CheckNotNegative(insertionLength, "insertionLength"); |
||||
|
||||
this.offset = offset; |
||||
this.removalLengthWithDeletionFlag = (uint)removalLength; |
||||
this.insertionLengthWithMovementFlag = (uint)insertionLength; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Creates a new OffsetChangeMapEntry instance.
|
||||
/// </summary>
|
||||
public OffsetChangeMapEntry(int offset, int removalLength, int insertionLength, bool removalNeverCausesAnchorDeletion, bool defaultAnchorMovementIsBeforeInsertion) |
||||
: this(offset, removalLength, insertionLength) |
||||
{ |
||||
if (removalNeverCausesAnchorDeletion) |
||||
this.removalLengthWithDeletionFlag |= 0x80000000; |
||||
if (defaultAnchorMovementIsBeforeInsertion) |
||||
this.insertionLengthWithMovementFlag |= 0x80000000; |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() |
||||
{ |
||||
unchecked { |
||||
return offset + 3559 * (int)insertionLengthWithMovementFlag + 3571 * (int)removalLengthWithDeletionFlag; |
||||
} |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object obj) |
||||
{ |
||||
return obj is OffsetChangeMapEntry && this.Equals((OffsetChangeMapEntry)obj); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(OffsetChangeMapEntry other) |
||||
{ |
||||
return offset == other.offset && insertionLengthWithMovementFlag == other.insertionLengthWithMovementFlag && removalLengthWithDeletionFlag == other.removalLengthWithDeletionFlag; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Tests the two entries for equality.
|
||||
/// </summary>
|
||||
public static bool operator ==(OffsetChangeMapEntry left, OffsetChangeMapEntry right) |
||||
{ |
||||
return left.Equals(right); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Tests the two entries for inequality.
|
||||
/// </summary>
|
||||
public static bool operator !=(OffsetChangeMapEntry left, OffsetChangeMapEntry right) |
||||
{ |
||||
return !left.Equals(right); |
||||
} |
||||
} |
||||
} |
@ -1,169 +0,0 @@
@@ -1,169 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.IO; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory.Editor; |
||||
#endif
|
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
/// <summary>
|
||||
/// Implements the ITextSource interface using a rope.
|
||||
/// </summary>
|
||||
[Serializable] |
||||
public sealed class RopeTextSource : ITextSource |
||||
{ |
||||
readonly Rope<char> rope; |
||||
readonly ITextSourceVersion version; |
||||
|
||||
/// <summary>
|
||||
/// Creates a new RopeTextSource.
|
||||
/// </summary>
|
||||
public RopeTextSource(Rope<char> rope) |
||||
{ |
||||
if (rope == null) |
||||
throw new ArgumentNullException("rope"); |
||||
this.rope = rope.Clone(); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Creates a new RopeTextSource.
|
||||
/// </summary>
|
||||
public RopeTextSource(Rope<char> rope, ITextSourceVersion version) |
||||
{ |
||||
if (rope == null) |
||||
throw new ArgumentNullException("rope"); |
||||
this.rope = rope.Clone(); |
||||
this.version = version; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Returns a clone of the rope used for this text source.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// RopeTextSource only publishes a copy of the contained rope to ensure that the underlying rope cannot be modified.
|
||||
/// </remarks>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification="Not a property because it creates a clone")] |
||||
public Rope<char> GetRope() |
||||
{ |
||||
return rope.Clone(); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public string Text { |
||||
get { return rope.ToString(); } |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public int TextLength { |
||||
get { return rope.Length; } |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public char GetCharAt(int offset) |
||||
{ |
||||
return rope[offset]; |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetText(int offset, int length) |
||||
{ |
||||
return rope.ToString(offset, length); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetText(ISegment segment) |
||||
{ |
||||
return rope.ToString(segment.Offset, segment.Length); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public TextReader CreateReader() |
||||
{ |
||||
return new RopeTextReader(rope); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public TextReader CreateReader(int offset, int length) |
||||
{ |
||||
return new RopeTextReader(rope.GetRange(offset, length)); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public ITextSource CreateSnapshot() |
||||
{ |
||||
return this; |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public ITextSource CreateSnapshot(int offset, int length) |
||||
{ |
||||
return new RopeTextSource(rope.GetRange(offset, length)); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public int IndexOf(char c, int startIndex, int count) |
||||
{ |
||||
return rope.IndexOf(c, startIndex, count); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public int IndexOfAny(char[] anyOf, int startIndex, int count) |
||||
{ |
||||
return rope.IndexOfAny(anyOf, startIndex, count); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public int LastIndexOf(char c, int startIndex, int count) |
||||
{ |
||||
return rope.LastIndexOf(c, startIndex, count); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public ITextSourceVersion Version { |
||||
get { return version; } |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public int IndexOf(string searchText, int startIndex, int count, StringComparison comparisonType) |
||||
{ |
||||
return rope.IndexOf(searchText, startIndex, count, comparisonType); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public int LastIndexOf(string searchText, int startIndex, int count, StringComparison comparisonType) |
||||
{ |
||||
return rope.LastIndexOf(searchText, startIndex, count, comparisonType); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public void WriteTextTo(TextWriter writer) |
||||
{ |
||||
rope.WriteTo(writer, 0, rope.Length); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public void WriteTextTo(TextWriter writer, int offset, int length) |
||||
{ |
||||
rope.WriteTo(writer, offset, length); |
||||
} |
||||
} |
||||
} |
@ -1,197 +0,0 @@
@@ -1,197 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Diagnostics; |
||||
using System.Globalization; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory.Editor; |
||||
#endif
|
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
/// <summary>
|
||||
/// Represents a simple segment (Offset,Length pair) that is not automatically updated
|
||||
/// on document changes.
|
||||
/// </summary>
|
||||
struct SimpleSegment : IEquatable<SimpleSegment>, ISegment |
||||
{ |
||||
public static readonly SimpleSegment Invalid = new SimpleSegment(-1, -1); |
||||
|
||||
/// <summary>
|
||||
/// Gets the overlapping portion of the segments.
|
||||
/// Returns SimpleSegment.Invalid if the segments don't overlap.
|
||||
/// </summary>
|
||||
public static SimpleSegment GetOverlap(ISegment segment1, ISegment segment2) |
||||
{ |
||||
int start = Math.Max(segment1.Offset, segment2.Offset); |
||||
int end = Math.Min(segment1.EndOffset, segment2.EndOffset); |
||||
if (end < start) |
||||
return SimpleSegment.Invalid; |
||||
else |
||||
return new SimpleSegment(start, end - start); |
||||
} |
||||
|
||||
public readonly int Offset, Length; |
||||
|
||||
int ISegment.Offset { |
||||
get { return Offset; } |
||||
} |
||||
|
||||
int ISegment.Length { |
||||
get { return Length; } |
||||
} |
||||
|
||||
public int EndOffset { |
||||
get { |
||||
return Offset + Length; |
||||
} |
||||
} |
||||
|
||||
public SimpleSegment(int offset, int length) |
||||
{ |
||||
this.Offset = offset; |
||||
this.Length = length; |
||||
} |
||||
|
||||
public SimpleSegment(ISegment segment) |
||||
{ |
||||
Debug.Assert(segment != null); |
||||
this.Offset = segment.Offset; |
||||
this.Length = segment.Length; |
||||
} |
||||
|
||||
public override int GetHashCode() |
||||
{ |
||||
unchecked { |
||||
return Offset + 10301 * Length; |
||||
} |
||||
} |
||||
|
||||
public override bool Equals(object obj) |
||||
{ |
||||
return (obj is SimpleSegment) && Equals((SimpleSegment)obj); |
||||
} |
||||
|
||||
public bool Equals(SimpleSegment other) |
||||
{ |
||||
return this.Offset == other.Offset && this.Length == other.Length; |
||||
} |
||||
|
||||
public static bool operator ==(SimpleSegment left, SimpleSegment right) |
||||
{ |
||||
return left.Equals(right); |
||||
} |
||||
|
||||
public static bool operator !=(SimpleSegment left, SimpleSegment right) |
||||
{ |
||||
return !left.Equals(right); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() |
||||
{ |
||||
return "[Offset=" + Offset.ToString(CultureInfo.InvariantCulture) + ", Length=" + Length.ToString(CultureInfo.InvariantCulture) + "]"; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// A segment using <see cref="TextAnchor"/>s as start and end positions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// For the constructors creating new anchors, the start position will be AfterInsertion and the end position will be BeforeInsertion.
|
||||
/// Should the end position move before the start position, the segment will have length 0.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="ISegment"/>
|
||||
/// <seealso cref="TextSegment"/>
|
||||
public sealed class AnchorSegment : ISegment |
||||
{ |
||||
readonly TextAnchor start, end; |
||||
|
||||
/// <inheritdoc/>
|
||||
public int Offset { |
||||
get { return start.Offset; } |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public int Length { |
||||
get { |
||||
// Math.Max takes care of the fact that end.Offset might move before start.Offset.
|
||||
return Math.Max(0, end.Offset - start.Offset); |
||||
} |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public int EndOffset { |
||||
get { |
||||
// Math.Max takes care of the fact that end.Offset might move before start.Offset.
|
||||
return Math.Max(start.Offset, end.Offset); |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Creates a new AnchorSegment using the specified anchors.
|
||||
/// The anchors must have <see cref="TextAnchor.SurviveDeletion"/> set to true.
|
||||
/// </summary>
|
||||
public AnchorSegment(TextAnchor start, TextAnchor end) |
||||
{ |
||||
if (start == null) |
||||
throw new ArgumentNullException("start"); |
||||
if (end == null) |
||||
throw new ArgumentNullException("end"); |
||||
if (!start.SurviveDeletion) |
||||
throw new ArgumentException("Anchors for AnchorSegment must use SurviveDeletion", "start"); |
||||
if (!end.SurviveDeletion) |
||||
throw new ArgumentException("Anchors for AnchorSegment must use SurviveDeletion", "end"); |
||||
this.start = start; |
||||
this.end = end; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Creates a new AnchorSegment that creates new anchors.
|
||||
/// </summary>
|
||||
public AnchorSegment(TextDocument document, ISegment segment) |
||||
: this(document, ThrowUtil.CheckNotNull(segment, "segment").Offset, segment.Length) |
||||
{ |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Creates a new AnchorSegment that creates new anchors.
|
||||
/// </summary>
|
||||
public AnchorSegment(TextDocument document, int offset, int length) |
||||
{ |
||||
if (document == null) |
||||
throw new ArgumentNullException("document"); |
||||
this.start = document.CreateAnchor(offset); |
||||
this.start.SurviveDeletion = true; |
||||
this.start.MovementType = AnchorMovementType.AfterInsertion; |
||||
this.end = document.CreateAnchor(offset + length); |
||||
this.end.SurviveDeletion = true; |
||||
this.end.MovementType = AnchorMovementType.BeforeInsertion; |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() |
||||
{ |
||||
return "[Offset=" + Offset.ToString(CultureInfo.InvariantCulture) + ", EndOffset=" + EndOffset.ToString(CultureInfo.InvariantCulture) + "]"; |
||||
} |
||||
} |
||||
} |
@ -1,158 +0,0 @@
@@ -1,158 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
using ICSharpCode.NRefactory; |
||||
using ICSharpCode.NRefactory.Editor; |
||||
using ICSharpCode.AvalonEdit.Document; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
/// <summary>
|
||||
/// The TextAnchor class references an offset (a position between two characters).
|
||||
/// It automatically updates the offset when text is inserted/removed in front of the anchor.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Use the <see cref="Offset"/> property to get the offset from a text anchor.
|
||||
/// Use the <see cref="TextDocument.CreateAnchor"/> method to create an anchor from an offset.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The document will automatically update all text anchors; and because it uses weak references to do so,
|
||||
/// the garbage collector can simply collect the anchor object when you don't need it anymore.
|
||||
/// </para>
|
||||
/// <para>Moreover, the document is able to efficiently update a large number of anchors without having to look
|
||||
/// at each anchor object individually. Updating the offsets of all anchors usually only takes time logarithmic
|
||||
/// to the number of anchors. Retrieving the <see cref="Offset"/> property also runs in O(lg N).</para>
|
||||
/// <inheritdoc cref="IsDeleted" />
|
||||
/// <inheritdoc cref="MovementType" />
|
||||
/// <para>If you want to track a segment, you can use the <see cref="AnchorSegment"/> class which
|
||||
/// implements <see cref="ISegment"/> using two text anchors.</para>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// Usage:
|
||||
/// <code>TextAnchor anchor = document.CreateAnchor(offset);
|
||||
/// ChangeMyDocument();
|
||||
/// int newOffset = anchor.Offset;
|
||||
/// </code>
|
||||
/// </example>
|
||||
public sealed class TextAnchor : ITextAnchor |
||||
{ |
||||
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; } |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public AnchorMovementType MovementType { get; set; } |
||||
|
||||
/// <inheritdoc/>
|
||||
public bool SurviveDeletion { get; set; } |
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsDeleted { |
||||
get { |
||||
document.DebugVerifyAccess(); |
||||
return node == null; |
||||
} |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler Deleted; |
||||
|
||||
internal void OnDeleted(DelayedEvents delayedEvents) |
||||
{ |
||||
node = null; |
||||
delayedEvents.DelayedRaise(Deleted, this, EventArgs.Empty); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the offset of the text anchor.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown when trying to get the Offset from a deleted anchor.</exception>
|
||||
public int Offset { |
||||
get { |
||||
document.DebugVerifyAccess(); |
||||
|
||||
TextAnchorNode n = this.node; |
||||
if (n == null) |
||||
throw new InvalidOperationException(); |
||||
|
||||
int offset = n.length; |
||||
if (n.left != null) |
||||
offset += n.left.totalLength; |
||||
while (n.parent != null) { |
||||
if (n == n.parent.right) { |
||||
if (n.parent.left != null) |
||||
offset += n.parent.left.totalLength; |
||||
offset += n.parent.length; |
||||
} |
||||
n = n.parent; |
||||
} |
||||
return offset; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the line number of the anchor.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown when trying to get the Offset from a deleted anchor.</exception>
|
||||
public int Line { |
||||
get { |
||||
return document.GetLineByOffset(this.Offset).LineNumber; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the column number of this anchor.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown when trying to get the Offset from a deleted anchor.</exception>
|
||||
public int Column { |
||||
get { |
||||
int offset = this.Offset; |
||||
return offset - document.GetLineByOffset(offset).Offset + 1; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the text location of this anchor.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown when trying to get the Offset from a deleted anchor.</exception>
|
||||
public TextLocation Location { |
||||
get { |
||||
return document.GetLocation(this.Offset); |
||||
} |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() |
||||
{ |
||||
return "[TextAnchor Offset=" + Offset + "]"; |
||||
} |
||||
} |
||||
} |
@ -1,102 +0,0 @@
@@ -1,102 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
/// <summary>
|
||||
/// A TextAnchorNode is placed in the TextAnchorTree.
|
||||
/// It describes a section of text with a text anchor at the end of the section.
|
||||
/// A weak reference is used to refer to the TextAnchor. (to save memory, we derive from WeakReference instead of referencing it)
|
||||
/// </summary>
|
||||
sealed class TextAnchorNode : WeakReference |
||||
{ |
||||
internal TextAnchorNode left, right, parent; |
||||
internal bool color; |
||||
internal int length; |
||||
internal int totalLength; // totalLength = length + left.totalLength + right.totalLength
|
||||
|
||||
public TextAnchorNode(TextAnchor anchor) : base(anchor) |
||||
{ |
||||
} |
||||
|
||||
internal TextAnchorNode LeftMost { |
||||
get { |
||||
TextAnchorNode node = this; |
||||
while (node.left != null) |
||||
node = node.left; |
||||
return node; |
||||
} |
||||
} |
||||
|
||||
internal TextAnchorNode RightMost { |
||||
get { |
||||
TextAnchorNode node = this; |
||||
while (node.right != null) |
||||
node = node.right; |
||||
return node; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the inorder successor of the node.
|
||||
/// </summary>
|
||||
internal TextAnchorNode Successor { |
||||
get { |
||||
if (right != null) { |
||||
return right.LeftMost; |
||||
} else { |
||||
TextAnchorNode node = this; |
||||
TextAnchorNode oldNode; |
||||
do { |
||||
oldNode = node; |
||||
node = node.parent; |
||||
// go up until we are coming out of a left subtree
|
||||
} while (node != null && node.right == oldNode); |
||||
return node; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the inorder predecessor of the node.
|
||||
/// </summary>
|
||||
internal TextAnchorNode Predecessor { |
||||
get { |
||||
if (left != null) { |
||||
return left.RightMost; |
||||
} else { |
||||
TextAnchorNode node = this; |
||||
TextAnchorNode oldNode; |
||||
do { |
||||
oldNode = node; |
||||
node = node.parent; |
||||
// go up until we are coming out of a right subtree
|
||||
} while (node != null && node.left == oldNode); |
||||
return node; |
||||
} |
||||
} |
||||
} |
||||
|
||||
public override string ToString() |
||||
{ |
||||
return "[TextAnchorNode Length=" + length + " TotalLength=" + totalLength + " Target=" + Target + "]"; |
||||
} |
||||
} |
||||
} |
@ -1,770 +0,0 @@
@@ -1,770 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Diagnostics; |
||||
using System.Text; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory.Editor; |
||||
#endif
|
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
/// <summary>
|
||||
/// A tree of TextAnchorNodes.
|
||||
/// </summary>
|
||||
sealed class TextAnchorTree |
||||
{ |
||||
// The text anchor tree has difficult requirements:
|
||||
// - it must QUICKLY update the offset in all anchors whenever there is a document change
|
||||
// - it must not reference text anchors directly, using weak references instead
|
||||
|
||||
// Clearly, we cannot afford updating an Offset property on all anchors (that would be O(N)).
|
||||
// So instead, the anchors need to be able to calculate their offset from a data structure
|
||||
// that can be efficiently updated.
|
||||
|
||||
// This implementation is built using an augmented red-black-tree.
|
||||
// There is a 'TextAnchorNode' for each text anchor.
|
||||
// Such a node represents a section of text (just the length is stored) with a (weakly referenced) text anchor at the end.
|
||||
|
||||
// Basically, you can imagine the list of text anchors as a sorted list of text anchors, where each anchor
|
||||
// just stores the distance to the previous anchor.
|
||||
// (next node = TextAnchorNode.Successor, distance = TextAnchorNode.length)
|
||||
// Distances are never negative, so this representation means anchors are always sorted by offset
|
||||
// (the order of anchors at the same offset is undefined)
|
||||
|
||||
// Of course, a linked list of anchors would be way too slow (one would need to traverse the whole list
|
||||
// every time the offset of an anchor is being looked up).
|
||||
// Instead, we use a red-black-tree. We aren't actually using the tree for sorting - it's just a binary tree
|
||||
// as storage format for what's conceptually a list, the red-black properties are used to keep the tree balanced.
|
||||
// Other balanced binary trees would work, too.
|
||||
|
||||
// What makes the tree-form efficient is that is augments the data by a 'totalLength'. Where 'length'
|
||||
// represents the distance to the previous node, 'totalLength' is the sum of all 'length' values in the subtree
|
||||
// under that node.
|
||||
// This allows computing the Offset from an anchor by walking up the list of parent nodes instead of going
|
||||
// through all predecessor nodes. So computing the Offset runs in O(log N).
|
||||
|
||||
readonly TextDocument document; |
||||
readonly List<TextAnchorNode> nodesToDelete = new List<TextAnchorNode>(); |
||||
TextAnchorNode root; |
||||
|
||||
public TextAnchorTree(TextDocument document) |
||||
{ |
||||
this.document = document; |
||||
} |
||||
|
||||
[Conditional("DEBUG")] |
||||
static void Log(string text) |
||||
{ |
||||
Debug.WriteLine("TextAnchorTree: " + text); |
||||
} |
||||
|
||||
#region Insert Text
|
||||
void InsertText(int offset, int length, bool defaultAnchorMovementIsBeforeInsertion) |
||||
{ |
||||
if (length == 0 || root == null || offset > root.totalLength) |
||||
return; |
||||
|
||||
// find the range of nodes that are placed exactly at offset
|
||||
// beginNode is inclusive, endNode is exclusive
|
||||
if (offset == root.totalLength) { |
||||
PerformInsertText(FindActualBeginNode(root.RightMost), null, length, defaultAnchorMovementIsBeforeInsertion); |
||||
} else { |
||||
TextAnchorNode endNode = FindNode(ref offset); |
||||
Debug.Assert(endNode.length > 0); |
||||
|
||||
if (offset > 0) { |
||||
// there are no nodes exactly at offset
|
||||
endNode.length += length; |
||||
UpdateAugmentedData(endNode); |
||||
} else { |
||||
PerformInsertText(FindActualBeginNode(endNode.Predecessor), endNode, length, defaultAnchorMovementIsBeforeInsertion); |
||||
} |
||||
} |
||||
DeleteMarkedNodes(); |
||||
} |
||||
|
||||
TextAnchorNode FindActualBeginNode(TextAnchorNode node) |
||||
{ |
||||
// now find the actual beginNode
|
||||
while (node != null && node.length == 0) |
||||
node = node.Predecessor; |
||||
if (node == null) { |
||||
// no predecessor = beginNode is first node in tree
|
||||
node = root.LeftMost; |
||||
} |
||||
return node; |
||||
} |
||||
|
||||
// Sorts the nodes in the range [beginNode, endNode) by MovementType
|
||||
// and inserts the length between the BeforeInsertion and the AfterInsertion nodes.
|
||||
void PerformInsertText(TextAnchorNode beginNode, TextAnchorNode endNode, int length, bool defaultAnchorMovementIsBeforeInsertion) |
||||
{ |
||||
Debug.Assert(beginNode != null); |
||||
// endNode may be null at the end of the anchor tree
|
||||
|
||||
// now we need to sort the nodes in the range [beginNode, endNode); putting those with
|
||||
// MovementType.BeforeInsertion in front of those with MovementType.AfterInsertion
|
||||
List<TextAnchorNode> beforeInsert = new List<TextAnchorNode>(); |
||||
//List<TextAnchorNode> afterInsert = new List<TextAnchorNode>();
|
||||
TextAnchorNode temp = beginNode; |
||||
while (temp != endNode) { |
||||
TextAnchor anchor = (TextAnchor)temp.Target; |
||||
if (anchor == null) { |
||||
// afterInsert.Add(temp);
|
||||
MarkNodeForDelete(temp); |
||||
} else if (defaultAnchorMovementIsBeforeInsertion |
||||
? anchor.MovementType != AnchorMovementType.AfterInsertion |
||||
: anchor.MovementType == AnchorMovementType.BeforeInsertion) |
||||
{ |
||||
beforeInsert.Add(temp); |
||||
// } else {
|
||||
// afterInsert.Add(temp);
|
||||
} |
||||
temp = temp.Successor; |
||||
} |
||||
// now again go through the range and swap the nodes with those in the beforeInsert list
|
||||
temp = beginNode; |
||||
foreach (TextAnchorNode node in beforeInsert) { |
||||
SwapAnchors(node, temp); |
||||
temp = temp.Successor; |
||||
} |
||||
// now temp is pointing to the first node that is afterInsert,
|
||||
// or to endNode, if there is no afterInsert node at the offset
|
||||
// So add the length to temp
|
||||
if (temp == null) { |
||||
// temp might be null if endNode==null and no afterInserts
|
||||
Debug.Assert(endNode == null); |
||||
} else { |
||||
temp.length += length; |
||||
UpdateAugmentedData(temp); |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Swaps the anchors stored in the two nodes.
|
||||
/// </summary>
|
||||
void SwapAnchors(TextAnchorNode n1, TextAnchorNode n2) |
||||
{ |
||||
if (n1 != n2) { |
||||
TextAnchor anchor1 = (TextAnchor)n1.Target; |
||||
TextAnchor anchor2 = (TextAnchor)n2.Target; |
||||
if (anchor1 == null && anchor2 == null) { |
||||
// -> no swap required
|
||||
return; |
||||
} |
||||
n1.Target = anchor2; |
||||
n2.Target = anchor1; |
||||
if (anchor1 == null) { |
||||
// unmark n1 from deletion, mark n2 for deletion
|
||||
nodesToDelete.Remove(n1); |
||||
MarkNodeForDelete(n2); |
||||
anchor2.node = n1; |
||||
} else if (anchor2 == null) { |
||||
// unmark n2 from deletion, mark n1 for deletion
|
||||
nodesToDelete.Remove(n2); |
||||
MarkNodeForDelete(n1); |
||||
anchor1.node = n2; |
||||
} else { |
||||
anchor1.node = n2; |
||||
anchor2.node = n1; |
||||
} |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
#region Remove or Replace text
|
||||
public void HandleTextChange(OffsetChangeMapEntry entry, DelayedEvents delayedEvents) |
||||
{ |
||||
//Log("HandleTextChange(" + entry + ")");
|
||||
if (entry.RemovalLength == 0) { |
||||
// This is a pure insertion.
|
||||
// Unlike a replace with removal, a pure insertion can result in nodes at the same location
|
||||
// to split depending on their MovementType.
|
||||
// Thus, we handle this case on a separate code path
|
||||
// (the code below looks like it does something similar, but it can only split
|
||||
// the set of deletion survivors, not all nodes at an offset)
|
||||
InsertText(entry.Offset, entry.InsertionLength, entry.DefaultAnchorMovementIsBeforeInsertion); |
||||
return; |
||||
} |
||||
// When handling a replacing text change, we need to:
|
||||
// - find all anchors in the deleted segment and delete them / move them to the appropriate
|
||||
// surviving side.
|
||||
// - adjust the segment size between the left and right side
|
||||
|
||||
int offset = entry.Offset; |
||||
int remainingRemovalLength = entry.RemovalLength; |
||||
// if the text change is happening after the last anchor, we don't have to do anything
|
||||
if (root == null || offset >= root.totalLength) |
||||
return; |
||||
TextAnchorNode node = FindNode(ref offset); |
||||
TextAnchorNode firstDeletionSurvivor = null; |
||||
// go forward through the tree and delete all nodes in the removal segment
|
||||
while (node != null && offset + remainingRemovalLength > node.length) { |
||||
TextAnchor anchor = (TextAnchor)node.Target; |
||||
if (anchor != null && (anchor.SurviveDeletion || entry.RemovalNeverCausesAnchorDeletion)) { |
||||
if (firstDeletionSurvivor == null) |
||||
firstDeletionSurvivor = node; |
||||
// This node should be deleted, but it wants to survive.
|
||||
// We'll just remove the deleted length segment, so the node will be positioned
|
||||
// in front of the removed segment.
|
||||
remainingRemovalLength -= node.length - offset; |
||||
node.length = offset; |
||||
offset = 0; |
||||
UpdateAugmentedData(node); |
||||
node = node.Successor; |
||||
} else { |
||||
// delete node
|
||||
TextAnchorNode s = node.Successor; |
||||
remainingRemovalLength -= node.length; |
||||
RemoveNode(node); |
||||
// we already deleted the node, don't delete it twice
|
||||
nodesToDelete.Remove(node); |
||||
if (anchor != null) |
||||
anchor.OnDeleted(delayedEvents); |
||||
node = s; |
||||
} |
||||
} |
||||
// 'node' now is the first anchor after the deleted segment.
|
||||
// If there are no anchors after the deleted segment, 'node' is null.
|
||||
|
||||
// firstDeletionSurvivor was set to the first node surviving deletion.
|
||||
// Because all non-surviving nodes up to 'node' were deleted, the node range
|
||||
// [firstDeletionSurvivor, node) now refers to the set of all deletion survivors.
|
||||
|
||||
// do the remaining job of the removal
|
||||
if (node != null) { |
||||
node.length -= remainingRemovalLength; |
||||
Debug.Assert(node.length >= 0); |
||||
} |
||||
if (entry.InsertionLength > 0) { |
||||
// we are performing a replacement
|
||||
if (firstDeletionSurvivor != null) { |
||||
// We got deletion survivors which need to be split into BeforeInsertion
|
||||
// and AfterInsertion groups.
|
||||
// Take care that we don't regroup everything at offset, but only the deletion
|
||||
// survivors - from firstDeletionSurvivor (inclusive) to node (exclusive).
|
||||
// This ensures that nodes immediately before or after the replaced segment
|
||||
// stay where they are (independent from their MovementType)
|
||||
PerformInsertText(firstDeletionSurvivor, node, entry.InsertionLength, entry.DefaultAnchorMovementIsBeforeInsertion); |
||||
} else if (node != null) { |
||||
// No deletion survivors:
|
||||
// just perform the insertion
|
||||
node.length += entry.InsertionLength; |
||||
} |
||||
} |
||||
if (node != null) { |
||||
UpdateAugmentedData(node); |
||||
} |
||||
DeleteMarkedNodes(); |
||||
} |
||||
#endregion
|
||||
|
||||
#region Node removal when TextAnchor was GC'ed
|
||||
void MarkNodeForDelete(TextAnchorNode node) |
||||
{ |
||||
if (!nodesToDelete.Contains(node)) |
||||
nodesToDelete.Add(node); |
||||
} |
||||
|
||||
void DeleteMarkedNodes() |
||||
{ |
||||
CheckProperties(); |
||||
while (nodesToDelete.Count > 0) { |
||||
int pos = nodesToDelete.Count - 1; |
||||
TextAnchorNode n = nodesToDelete[pos]; |
||||
// combine section of n with the following section
|
||||
TextAnchorNode s = n.Successor; |
||||
if (s != null) { |
||||
s.length += n.length; |
||||
} |
||||
RemoveNode(n); |
||||
if (s != null) { |
||||
UpdateAugmentedData(s); |
||||
} |
||||
nodesToDelete.RemoveAt(pos); |
||||
CheckProperties(); |
||||
} |
||||
CheckProperties(); |
||||
} |
||||
#endregion
|
||||
|
||||
#region FindNode
|
||||
/// <summary>
|
||||
/// Finds the node at the specified offset.
|
||||
/// After the method has run, offset is relative to the beginning of the returned node.
|
||||
/// </summary>
|
||||
TextAnchorNode FindNode(ref int offset) |
||||
{ |
||||
TextAnchorNode n = root; |
||||
while (true) { |
||||
if (n.left != null) { |
||||
if (offset < n.left.totalLength) { |
||||
n = n.left; // descend into left subtree
|
||||
continue; |
||||
} else { |
||||
offset -= n.left.totalLength; // skip left subtree
|
||||
} |
||||
} |
||||
if (!n.IsAlive) |
||||
MarkNodeForDelete(n); |
||||
if (offset < n.length) { |
||||
return n; // found correct node
|
||||
} else { |
||||
offset -= n.length; // skip this node
|
||||
} |
||||
if (n.right != null) { |
||||
n = n.right; // descend into right subtree
|
||||
} else { |
||||
// didn't find any node containing the offset
|
||||
return null; |
||||
} |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
#region UpdateAugmentedData
|
||||
void UpdateAugmentedData(TextAnchorNode n) |
||||
{ |
||||
if (!n.IsAlive) |
||||
MarkNodeForDelete(n); |
||||
|
||||
int totalLength = n.length; |
||||
if (n.left != null) |
||||
totalLength += n.left.totalLength; |
||||
if (n.right != null) |
||||
totalLength += n.right.totalLength; |
||||
if (n.totalLength != totalLength) { |
||||
n.totalLength = totalLength; |
||||
if (n.parent != null) |
||||
UpdateAugmentedData(n.parent); |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
#region CreateAnchor
|
||||
public TextAnchor CreateAnchor(int offset) |
||||
{ |
||||
Log("CreateAnchor(" + offset + ")"); |
||||
TextAnchor anchor = new TextAnchor(document); |
||||
anchor.node = new TextAnchorNode(anchor); |
||||
if (root == null) { |
||||
// creating the first text anchor
|
||||
root = anchor.node; |
||||
root.totalLength = root.length = offset; |
||||
} else if (offset >= root.totalLength) { |
||||
// append anchor at end of tree
|
||||
anchor.node.totalLength = anchor.node.length = offset - root.totalLength; |
||||
InsertAsRight(root.RightMost, anchor.node); |
||||
} else { |
||||
// insert anchor in middle of tree
|
||||
TextAnchorNode n = FindNode(ref offset); |
||||
Debug.Assert(offset < n.length); |
||||
// split segment 'n' at offset
|
||||
anchor.node.totalLength = anchor.node.length = offset; |
||||
n.length -= offset; |
||||
InsertBefore(n, anchor.node); |
||||
} |
||||
DeleteMarkedNodes(); |
||||
return anchor; |
||||
} |
||||
|
||||
void InsertBefore(TextAnchorNode node, TextAnchorNode newNode) |
||||
{ |
||||
if (node.left == null) { |
||||
InsertAsLeft(node, newNode); |
||||
} else { |
||||
InsertAsRight(node.left.RightMost, newNode); |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
#region Red/Black Tree
|
||||
internal const bool RED = true; |
||||
internal const bool BLACK = false; |
||||
|
||||
void InsertAsLeft(TextAnchorNode parentNode, TextAnchorNode newNode) |
||||
{ |
||||
Debug.Assert(parentNode.left == null); |
||||
parentNode.left = newNode; |
||||
newNode.parent = parentNode; |
||||
newNode.color = RED; |
||||
UpdateAugmentedData(parentNode); |
||||
FixTreeOnInsert(newNode); |
||||
} |
||||
|
||||
void InsertAsRight(TextAnchorNode parentNode, TextAnchorNode newNode) |
||||
{ |
||||
Debug.Assert(parentNode.right == null); |
||||
parentNode.right = newNode; |
||||
newNode.parent = parentNode; |
||||
newNode.color = RED; |
||||
UpdateAugmentedData(parentNode); |
||||
FixTreeOnInsert(newNode); |
||||
} |
||||
|
||||
void FixTreeOnInsert(TextAnchorNode node) |
||||
{ |
||||
Debug.Assert(node != null); |
||||
Debug.Assert(node.color == RED); |
||||
Debug.Assert(node.left == null || node.left.color == BLACK); |
||||
Debug.Assert(node.right == null || node.right.color == BLACK); |
||||
|
||||
TextAnchorNode parentNode = node.parent; |
||||
if (parentNode == null) { |
||||
// we inserted in the root -> the node must be black
|
||||
// since this is a root node, making the node black increments the number of black nodes
|
||||
// on all paths by one, so it is still the same for all paths.
|
||||
node.color = BLACK; |
||||
return; |
||||
} |
||||
if (parentNode.color == BLACK) { |
||||
// if the parent node where we inserted was black, our red node is placed correctly.
|
||||
// since we inserted a red node, the number of black nodes on each path is unchanged
|
||||
// -> the tree is still balanced
|
||||
return; |
||||
} |
||||
// parentNode is red, so there is a conflict here!
|
||||
|
||||
// because the root is black, parentNode is not the root -> there is a grandparent node
|
||||
TextAnchorNode grandparentNode = parentNode.parent; |
||||
TextAnchorNode uncleNode = Sibling(parentNode); |
||||
if (uncleNode != null && uncleNode.color == RED) { |
||||
parentNode.color = BLACK; |
||||
uncleNode.color = BLACK; |
||||
grandparentNode.color = RED; |
||||
FixTreeOnInsert(grandparentNode); |
||||
return; |
||||
} |
||||
// now we know: parent is red but uncle is black
|
||||
// First rotation:
|
||||
if (node == parentNode.right && parentNode == grandparentNode.left) { |
||||
RotateLeft(parentNode); |
||||
node = node.left; |
||||
} else if (node == parentNode.left && parentNode == grandparentNode.right) { |
||||
RotateRight(parentNode); |
||||
node = node.right; |
||||
} |
||||
// because node might have changed, reassign variables:
|
||||
parentNode = node.parent; |
||||
grandparentNode = parentNode.parent; |
||||
|
||||
// Now recolor a bit:
|
||||
parentNode.color = BLACK; |
||||
grandparentNode.color = RED; |
||||
// Second rotation:
|
||||
if (node == parentNode.left && parentNode == grandparentNode.left) { |
||||
RotateRight(grandparentNode); |
||||
} else { |
||||
// because of the first rotation, this is guaranteed:
|
||||
Debug.Assert(node == parentNode.right && parentNode == grandparentNode.right); |
||||
RotateLeft(grandparentNode); |
||||
} |
||||
} |
||||
|
||||
void RemoveNode(TextAnchorNode removedNode) |
||||
{ |
||||
if (removedNode.left != null && removedNode.right != null) { |
||||
// replace removedNode with it's in-order successor
|
||||
|
||||
TextAnchorNode leftMost = removedNode.right.LeftMost; |
||||
RemoveNode(leftMost); // remove leftMost from its current location
|
||||
|
||||
// and overwrite the removedNode with it
|
||||
ReplaceNode(removedNode, leftMost); |
||||
leftMost.left = removedNode.left; |
||||
if (leftMost.left != null) leftMost.left.parent = leftMost; |
||||
leftMost.right = removedNode.right; |
||||
if (leftMost.right != null) leftMost.right.parent = leftMost; |
||||
leftMost.color = removedNode.color; |
||||
|
||||
UpdateAugmentedData(leftMost); |
||||
if (leftMost.parent != null) UpdateAugmentedData(leftMost.parent); |
||||
return; |
||||
} |
||||
|
||||
// now either removedNode.left or removedNode.right is null
|
||||
// get the remaining child
|
||||
TextAnchorNode parentNode = removedNode.parent; |
||||
TextAnchorNode childNode = removedNode.left ?? removedNode.right; |
||||
ReplaceNode(removedNode, childNode); |
||||
if (parentNode != null) UpdateAugmentedData(parentNode); |
||||
if (removedNode.color == BLACK) { |
||||
if (childNode != null && childNode.color == RED) { |
||||
childNode.color = BLACK; |
||||
} else { |
||||
FixTreeOnDelete(childNode, parentNode); |
||||
} |
||||
} |
||||
} |
||||
|
||||
void FixTreeOnDelete(TextAnchorNode node, TextAnchorNode parentNode) |
||||
{ |
||||
Debug.Assert(node == null || node.parent == parentNode); |
||||
if (parentNode == null) |
||||
return; |
||||
|
||||
// warning: node may be null
|
||||
TextAnchorNode sibling = Sibling(node, parentNode); |
||||
if (sibling.color == RED) { |
||||
parentNode.color = RED; |
||||
sibling.color = BLACK; |
||||
if (node == parentNode.left) { |
||||
RotateLeft(parentNode); |
||||
} else { |
||||
RotateRight(parentNode); |
||||
} |
||||
|
||||
sibling = Sibling(node, parentNode); // update value of sibling after rotation
|
||||
} |
||||
|
||||
if (parentNode.color == BLACK |
||||
&& sibling.color == BLACK |
||||
&& GetColor(sibling.left) == BLACK |
||||
&& GetColor(sibling.right) == BLACK) |
||||
{ |
||||
sibling.color = RED; |
||||
FixTreeOnDelete(parentNode, parentNode.parent); |
||||
return; |
||||
} |
||||
|
||||
if (parentNode.color == RED |
||||
&& sibling.color == BLACK |
||||
&& GetColor(sibling.left) == BLACK |
||||
&& GetColor(sibling.right) == BLACK) |
||||
{ |
||||
sibling.color = RED; |
||||
parentNode.color = BLACK; |
||||
return; |
||||
} |
||||
|
||||
if (node == parentNode.left && |
||||
sibling.color == BLACK && |
||||
GetColor(sibling.left) == RED && |
||||
GetColor(sibling.right) == BLACK) |
||||
{ |
||||
sibling.color = RED; |
||||
sibling.left.color = BLACK; |
||||
RotateRight(sibling); |
||||
} |
||||
else if (node == parentNode.right && |
||||
sibling.color == BLACK && |
||||
GetColor(sibling.right) == RED && |
||||
GetColor(sibling.left) == BLACK) |
||||
{ |
||||
sibling.color = RED; |
||||
sibling.right.color = BLACK; |
||||
RotateLeft(sibling); |
||||
} |
||||
sibling = Sibling(node, parentNode); // update value of sibling after rotation
|
||||
|
||||
sibling.color = parentNode.color; |
||||
parentNode.color = BLACK; |
||||
if (node == parentNode.left) { |
||||
if (sibling.right != null) { |
||||
Debug.Assert(sibling.right.color == RED); |
||||
sibling.right.color = BLACK; |
||||
} |
||||
RotateLeft(parentNode); |
||||
} else { |
||||
if (sibling.left != null) { |
||||
Debug.Assert(sibling.left.color == RED); |
||||
sibling.left.color = BLACK; |
||||
} |
||||
RotateRight(parentNode); |
||||
} |
||||
} |
||||
|
||||
void ReplaceNode(TextAnchorNode replacedNode, TextAnchorNode newNode) |
||||
{ |
||||
if (replacedNode.parent == null) { |
||||
Debug.Assert(replacedNode == root); |
||||
root = newNode; |
||||
} else { |
||||
if (replacedNode.parent.left == replacedNode) |
||||
replacedNode.parent.left = newNode; |
||||
else |
||||
replacedNode.parent.right = newNode; |
||||
} |
||||
if (newNode != null) { |
||||
newNode.parent = replacedNode.parent; |
||||
} |
||||
replacedNode.parent = null; |
||||
} |
||||
|
||||
void RotateLeft(TextAnchorNode p) |
||||
{ |
||||
// let q be p's right child
|
||||
TextAnchorNode q = p.right; |
||||
Debug.Assert(q != null); |
||||
Debug.Assert(q.parent == p); |
||||
// set q to be the new root
|
||||
ReplaceNode(p, q); |
||||
|
||||
// set p's right child to be q's left child
|
||||
p.right = q.left; |
||||
if (p.right != null) p.right.parent = p; |
||||
// set q's left child to be p
|
||||
q.left = p; |
||||
p.parent = q; |
||||
UpdateAugmentedData(p); |
||||
UpdateAugmentedData(q); |
||||
} |
||||
|
||||
void RotateRight(TextAnchorNode p) |
||||
{ |
||||
// let q be p's left child
|
||||
TextAnchorNode q = p.left; |
||||
Debug.Assert(q != null); |
||||
Debug.Assert(q.parent == p); |
||||
// set q to be the new root
|
||||
ReplaceNode(p, q); |
||||
|
||||
// set p's left child to be q's right child
|
||||
p.left = q.right; |
||||
if (p.left != null) p.left.parent = p; |
||||
// set q's right child to be p
|
||||
q.right = p; |
||||
p.parent = q; |
||||
UpdateAugmentedData(p); |
||||
UpdateAugmentedData(q); |
||||
} |
||||
|
||||
static TextAnchorNode Sibling(TextAnchorNode node) |
||||
{ |
||||
if (node == node.parent.left) |
||||
return node.parent.right; |
||||
else |
||||
return node.parent.left; |
||||
} |
||||
|
||||
static TextAnchorNode Sibling(TextAnchorNode node, TextAnchorNode parentNode) |
||||
{ |
||||
Debug.Assert(node == null || node.parent == parentNode); |
||||
if (node == parentNode.left) |
||||
return parentNode.right; |
||||
else |
||||
return parentNode.left; |
||||
} |
||||
|
||||
static bool GetColor(TextAnchorNode node) |
||||
{ |
||||
return node != null ? node.color : BLACK; |
||||
} |
||||
#endregion
|
||||
|
||||
#region CheckProperties
|
||||
[Conditional("DATACONSISTENCYTEST")] |
||||
internal void CheckProperties() |
||||
{ |
||||
#if DEBUG
|
||||
if (root != null) { |
||||
CheckProperties(root); |
||||
|
||||
// check red-black property:
|
||||
int blackCount = -1; |
||||
CheckNodeProperties(root, null, RED, 0, ref blackCount); |
||||
} |
||||
#endif
|
||||
} |
||||
|
||||
#if DEBUG
|
||||
void CheckProperties(TextAnchorNode node) |
||||
{ |
||||
int totalLength = node.length; |
||||
if (node.left != null) { |
||||
CheckProperties(node.left); |
||||
totalLength += node.left.totalLength; |
||||
} |
||||
if (node.right != null) { |
||||
CheckProperties(node.right); |
||||
totalLength += node.right.totalLength; |
||||
} |
||||
Debug.Assert(node.totalLength == totalLength); |
||||
} |
||||
|
||||
/* |
||||
1. A node is either red or black. |
||||
2. The root is black. |
||||
3. All leaves are black. (The leaves are the NIL children.) |
||||
4. Both children of every red node are black. (So every red node must have a black parent.) |
||||
5. Every simple path from a node to a descendant leaf contains the same number of black nodes. (Not counting the leaf node.) |
||||
*/ |
||||
void CheckNodeProperties(TextAnchorNode node, TextAnchorNode parentNode, bool parentColor, int blackCount, ref int expectedBlackCount) |
||||
{ |
||||
if (node == null) return; |
||||
|
||||
Debug.Assert(node.parent == parentNode); |
||||
|
||||
if (parentColor == RED) { |
||||
Debug.Assert(node.color == BLACK); |
||||
} |
||||
if (node.color == BLACK) { |
||||
blackCount++; |
||||
} |
||||
if (node.left == null && node.right == null) { |
||||
// node is a leaf node:
|
||||
if (expectedBlackCount == -1) |
||||
expectedBlackCount = blackCount; |
||||
else |
||||
Debug.Assert(expectedBlackCount == blackCount); |
||||
} |
||||
CheckNodeProperties(node.left, node, node.color, blackCount, ref expectedBlackCount); |
||||
CheckNodeProperties(node.right, node, node.color, blackCount, ref expectedBlackCount); |
||||
} |
||||
#endif
|
||||
#endregion
|
||||
|
||||
#region GetTreeAsString
|
||||
#if DEBUG
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] |
||||
public string GetTreeAsString() |
||||
{ |
||||
if (root == null) |
||||
return "<empty tree>"; |
||||
StringBuilder b = new StringBuilder(); |
||||
AppendTreeToString(root, b, 0); |
||||
return b.ToString(); |
||||
} |
||||
|
||||
static void AppendTreeToString(TextAnchorNode node, StringBuilder b, int indent) |
||||
{ |
||||
if (node.color == RED) |
||||
b.Append("RED "); |
||||
else |
||||
b.Append("BLACK "); |
||||
b.AppendLine(node.ToString()); |
||||
indent += 2; |
||||
if (node.left != null) { |
||||
b.Append(' ', indent); |
||||
b.Append("L: "); |
||||
AppendTreeToString(node.left, b, indent); |
||||
} |
||||
if (node.right != null) { |
||||
b.Append(' ', indent); |
||||
b.Append("R: "); |
||||
AppendTreeToString(node.right, b, indent); |
||||
} |
||||
} |
||||
#endif
|
||||
#endregion
|
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -1,167 +0,0 @@
@@ -1,167 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory.Editor; |
||||
#endif
|
||||
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")] |
||||
[Obsolete("The TextDocument.LineCountChanged event will be removed in a future version. Use PropertyChangedEventManager instead.")] |
||||
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")] |
||||
[Obsolete("The TextDocument.TextLengthChanged event will be removed in a future version. Use PropertyChangedEventManager instead.")] |
||||
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; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,271 +0,0 @@
@@ -1,271 +0,0 @@
|
||||
// Copyright (c) 2010-2013 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.ComponentModel; |
||||
using System.Globalization; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
#if !NREFACTORY
|
||||
/// <summary>
|
||||
/// A line/column position.
|
||||
/// Text editor lines/columns are counted started from one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The document provides the methods <see cref="IDocument.GetLocation"/> and
|
||||
/// <see cref="IDocument.GetOffset(TextLocation)"/> to convert between offsets and TextLocations.
|
||||
/// </remarks>
|
||||
[Serializable] |
||||
[TypeConverter(typeof(TextLocationConverter))] |
||||
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.
|
||||
/// </summary>
|
||||
public TextLocation(int line, int column) |
||||
{ |
||||
this.line = line; |
||||
this.column = column; |
||||
} |
||||
|
||||
readonly int column, line; |
||||
|
||||
/// <summary>
|
||||
/// Gets the line number.
|
||||
/// </summary>
|
||||
public int Line { |
||||
get { return line; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the column number.
|
||||
/// </summary>
|
||||
public int Column { |
||||
get { return column; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets whether the TextLocation instance is empty.
|
||||
/// </summary>
|
||||
public bool IsEmpty { |
||||
get { |
||||
return column <= 0 && line <= 0; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets a string representation for debugging purposes.
|
||||
/// </summary>
|
||||
public override string ToString() |
||||
{ |
||||
return string.Format(CultureInfo.InvariantCulture, "(Line {1}, Col {0})", this.column, this.line); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets a hash code.
|
||||
/// </summary>
|
||||
public override int GetHashCode() |
||||
{ |
||||
return unchecked (191 * column.GetHashCode() ^ line.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.column == right.column && left.line == right.line; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Inequality test.
|
||||
/// </summary>
|
||||
public static bool operator !=(TextLocation left, TextLocation right) |
||||
{ |
||||
return left.column != right.column || left.line != right.line; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Compares two text locations.
|
||||
/// </summary>
|
||||
public static bool operator <(TextLocation left, TextLocation right) |
||||
{ |
||||
if (left.line < right.line) |
||||
return true; |
||||
else if (left.line == right.line) |
||||
return left.column < right.column; |
||||
else |
||||
return false; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Compares two text locations.
|
||||
/// </summary>
|
||||
public static bool operator >(TextLocation left, TextLocation right) |
||||
{ |
||||
if (left.line > right.line) |
||||
return true; |
||||
else if (left.line == right.line) |
||||
return left.column > right.column; |
||||
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; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Converts strings of the form '0+[;,]0+' to a <see cref="TextLocation"/>.
|
||||
/// </summary>
|
||||
public class TextLocationConverter : TypeConverter |
||||
{ |
||||
/// <inheritdoc/>
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
||||
{ |
||||
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) |
||||
{ |
||||
return destinationType == typeof(TextLocation) || base.CanConvertTo(context, destinationType); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
||||
{ |
||||
if (value is string) { |
||||
string[] parts = ((string)value).Split(';', ','); |
||||
if (parts.Length == 2) { |
||||
return new TextLocation(int.Parse(parts[0], culture), int.Parse(parts[1], culture)); |
||||
} |
||||
} |
||||
return base.ConvertFrom(context, culture, value); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) |
||||
{ |
||||
if (value is TextLocation && destinationType == typeof(string)) { |
||||
var loc = (TextLocation)value; |
||||
return loc.Line.ToString(culture) + ";" + loc.Column.ToString(culture); |
||||
} |
||||
return base.ConvertTo(context, culture, value, destinationType); |
||||
} |
||||
} |
||||
|
||||
/// <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>
|
||||
/// <remarks>For line segments (IDocumentLine), the length does not include the line delimeter.</remarks>
|
||||
int Length { get; } |
||||
|
||||
/// <summary>
|
||||
/// Gets the end offset of the segment.
|
||||
/// </summary>
|
||||
/// <remarks>EndOffset = Offset + Length;</remarks>
|
||||
int EndOffset { get; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="ISegment"/>.
|
||||
/// </summary>
|
||||
public static class ISegmentExtensions |
||||
{ |
||||
/// <summary>
|
||||
/// Gets whether <paramref name="segment"/> fully contains the specified segment.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use <c>segment.Contains(offset, 0)</c> to detect whether a segment (end inclusive) contains offset;
|
||||
/// use <c>segment.Contains(offset, 1)</c> to detect whether a segment (end exclusive) contains offset.
|
||||
/// </remarks>
|
||||
public static bool Contains (this ISegment segment, int offset, int length) |
||||
{ |
||||
return segment.Offset <= offset && offset + length <= segment.EndOffset; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets whether <paramref name="thisSegment"/> fully contains the specified segment.
|
||||
/// </summary>
|
||||
public static bool Contains (this ISegment thisSegment, ISegment segment) |
||||
{ |
||||
return segment != null && thisSegment.Offset <= segment.Offset && segment.EndOffset <= thisSegment.EndOffset; |
||||
} |
||||
} |
||||
#endif
|
||||
} |
@ -1,266 +0,0 @@
@@ -1,266 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Diagnostics; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory.Editor; |
||||
#endif
|
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
/// <summary>
|
||||
/// A segment that can be put into a <see cref="TextSegmentCollection{T}"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// A <see cref="TextSegment"/> can be stand-alone or part of a <see cref="TextSegmentCollection{T}"/>.
|
||||
/// If the segment is stored inside a TextSegmentCollection, its Offset and Length will be updated by that collection.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When the document changes, the offsets of all text segments in the TextSegmentCollection will be adjusted accordingly.
|
||||
/// Start offsets move like <see cref="AnchorMovementType">AnchorMovementType.AfterInsertion</see>,
|
||||
/// end offsets move like <see cref="AnchorMovementType">AnchorMovementType.BeforeInsertion</see>
|
||||
/// (i.e. the segment will always stay as small as possible).</para>
|
||||
/// <para>
|
||||
/// If a document change causes a segment to be deleted completely, it will be reduced to length 0, but segments are
|
||||
/// never automatically removed from the collection.
|
||||
/// Segments with length 0 will never expand due to document changes, and they move as <c>AfterInsertion</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Thread-safety: a TextSegmentCollection that is connected to a <see cref="TextDocument"/> may only be used on that document's owner thread.
|
||||
/// A disconnected TextSegmentCollection is safe for concurrent reads, but concurrent access is not safe when there are writes.
|
||||
/// Keep in mind that reading the Offset properties of a text segment inside the collection is a read access on the
|
||||
/// collection; and setting an Offset property of a text segment is a write access on the collection.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="ISegment"/>
|
||||
/// <seealso cref="AnchorSegment"/>
|
||||
/// <seealso cref="TextSegmentCollection{T}"/>
|
||||
public class TextSegment : ISegment |
||||
{ |
||||
internal ISegmentTree ownerTree; |
||||
internal TextSegment left, right, parent; |
||||
|
||||
/// <summary>
|
||||
/// The color of the segment in the red/black tree.
|
||||
/// </summary>
|
||||
internal bool color; |
||||
|
||||
/// <summary>
|
||||
/// The "length" of the node (distance to previous node)
|
||||
/// </summary>
|
||||
internal int nodeLength; |
||||
|
||||
/// <summary>
|
||||
/// The total "length" of this subtree.
|
||||
/// </summary>
|
||||
internal int totalNodeLength; // totalNodeLength = nodeLength + left.totalNodeLength + right.totalNodeLength
|
||||
|
||||
/// <summary>
|
||||
/// The length of the segment (do not confuse with nodeLength).
|
||||
/// </summary>
|
||||
internal int segmentLength; |
||||
|
||||
/// <summary>
|
||||
/// distanceToMaxEnd = Max(segmentLength,
|
||||
/// left.distanceToMaxEnd + left.Offset - Offset,
|
||||
/// left.distanceToMaxEnd + right.Offset - Offset)
|
||||
/// </summary>
|
||||
internal int distanceToMaxEnd; |
||||
|
||||
int ISegment.Offset { |
||||
get { return StartOffset; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets whether this segment is connected to a TextSegmentCollection and will automatically
|
||||
/// update its offsets.
|
||||
/// </summary>
|
||||
protected bool IsConnectedToCollection { |
||||
get { |
||||
return ownerTree != null; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets/Sets the start offset of the segment.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When setting the start offset, the end offset will change, too: the Length of the segment will stay constant.
|
||||
/// </remarks>
|
||||
public int StartOffset { |
||||
get { |
||||
// If the segment is not connected to a tree, we store the offset in "nodeLength".
|
||||
// Otherwise, "nodeLength" contains the distance to the start offset of the previous node
|
||||
Debug.Assert(!(ownerTree == null && parent != null)); |
||||
Debug.Assert(!(ownerTree == null && left != null)); |
||||
|
||||
TextSegment n = this; |
||||
int offset = n.nodeLength; |
||||
if (n.left != null) |
||||
offset += n.left.totalNodeLength; |
||||
while (n.parent != null) { |
||||
if (n == n.parent.right) { |
||||
if (n.parent.left != null) |
||||
offset += n.parent.left.totalNodeLength; |
||||
offset += n.parent.nodeLength; |
||||
} |
||||
n = n.parent; |
||||
} |
||||
return offset; |
||||
} |
||||
set { |
||||
if (value < 0) |
||||
throw new ArgumentOutOfRangeException("value", "Offset must not be negative"); |
||||
if (this.StartOffset != value) { |
||||
// need a copy of the variable because ownerTree.Remove() sets this.ownerTree to null
|
||||
ISegmentTree ownerTree = this.ownerTree; |
||||
if (ownerTree != null) { |
||||
ownerTree.Remove(this); |
||||
nodeLength = value; |
||||
ownerTree.Add(this); |
||||
} else { |
||||
nodeLength = value; |
||||
} |
||||
OnSegmentChanged(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets/Sets the end offset of the segment.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Setting the end offset will change the length, the start offset will stay constant.
|
||||
/// </remarks>
|
||||
public int EndOffset { |
||||
get { |
||||
return StartOffset + Length; |
||||
} |
||||
set { |
||||
int newLength = value - StartOffset; |
||||
if (newLength < 0) |
||||
throw new ArgumentOutOfRangeException("value", "EndOffset must be greater or equal to StartOffset"); |
||||
Length = newLength; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets/Sets the length of the segment.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Setting the length will change the end offset, the start offset will stay constant.
|
||||
/// </remarks>
|
||||
public int Length { |
||||
get { |
||||
return segmentLength; |
||||
} |
||||
set { |
||||
if (value < 0) |
||||
throw new ArgumentOutOfRangeException("value", "Length must not be negative"); |
||||
if (segmentLength != value) { |
||||
segmentLength = value; |
||||
if (ownerTree != null) |
||||
ownerTree.UpdateAugmentedData(this); |
||||
OnSegmentChanged(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// This method gets called when the StartOffset/Length/EndOffset properties are set.
|
||||
/// It is not called when StartOffset/Length/EndOffset change due to document changes
|
||||
/// </summary>
|
||||
protected virtual void OnSegmentChanged() |
||||
{ |
||||
} |
||||
|
||||
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 + "]"; |
||||
} |
||||
} |
||||
} |
@ -1,989 +0,0 @@
@@ -1,989 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Collections.ObjectModel; |
||||
using System.Diagnostics; |
||||
using System.Linq; |
||||
using System.Text; |
||||
using System.Windows; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory.Editor; |
||||
#endif
|
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
/// <summary>
|
||||
/// Interface to allow TextSegments to access the TextSegmentCollection - we cannot use a direct reference
|
||||
/// because TextSegmentCollection is generic.
|
||||
/// </summary>
|
||||
interface ISegmentTree |
||||
{ |
||||
void Add(TextSegment s); |
||||
void Remove(TextSegment s); |
||||
void UpdateAugmentedData(TextSegment s); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// A collection of text segments that supports efficient lookup of segments
|
||||
/// intersecting with another segment.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <remarks><inheritdoc cref="TextSegment"/></remarks>
|
||||
/// <see cref="TextSegment"/>
|
||||
public sealed class TextSegmentCollection<T> : ICollection<T>, ISegmentTree, IWeakEventListener where T : TextSegment |
||||
{ |
||||
// Implementation: this is basically a mixture of an augmented interval tree
|
||||
// and the TextAnchorTree.
|
||||
|
||||
// WARNING: you need to understand interval trees (the version with the augmented 'high'/'max' field)
|
||||
// and how the TextAnchorTree works before you have any chance of understanding this code.
|
||||
|
||||
// This means that every node holds two "segments":
|
||||
// one like the segments in the text anchor tree to support efficient offset changes
|
||||
// and another that is the interval as seen by the user
|
||||
|
||||
// So basically, the tree contains a list of contiguous node segments of the first kind,
|
||||
// with interval segments starting at the end of every node segment.
|
||||
|
||||
// Performance:
|
||||
// Add is O(lg n)
|
||||
// Remove is O(lg n)
|
||||
// DocumentChanged is O(m * lg n), with m the number of segments that intersect with the changed document section
|
||||
// FindFirstSegmentWithStartAfter is O(m + lg n) with m being the number of segments at the same offset as the result segment
|
||||
// FindIntersectingSegments is O(m + lg n) with m being the number of intersecting segments.
|
||||
|
||||
int count; |
||||
TextSegment root; |
||||
bool isConnectedToDocument; |
||||
|
||||
#region Constructor
|
||||
/// <summary>
|
||||
/// Creates a new TextSegmentCollection that needs manual calls to <see cref="UpdateOffsets(DocumentChangeEventArgs)"/>.
|
||||
/// </summary>
|
||||
public TextSegmentCollection() |
||||
{ |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Creates a new TextSegmentCollection that updates the offsets automatically.
|
||||
/// </summary>
|
||||
/// <param name="textDocument">The document to which the text segments
|
||||
/// that will be added to the tree belong. When the document changes, the
|
||||
/// position of the text segments will be updated accordingly.</param>
|
||||
public TextSegmentCollection(TextDocument textDocument) |
||||
{ |
||||
if (textDocument == null) |
||||
throw new ArgumentNullException("textDocument"); |
||||
|
||||
textDocument.VerifyAccess(); |
||||
isConnectedToDocument = true; |
||||
TextDocumentWeakEventManager.Changed.AddListener(textDocument, this); |
||||
} |
||||
#endregion
|
||||
|
||||
#region OnDocumentChanged / UpdateOffsets
|
||||
bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) |
||||
{ |
||||
if (managerType == typeof(TextDocumentWeakEventManager.Changed)) { |
||||
OnDocumentChanged((DocumentChangeEventArgs)e); |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Updates the start and end offsets of all segments stored in this collection.
|
||||
/// </summary>
|
||||
/// <param name="e">DocumentChangeEventArgs instance describing the change to the document.</param>
|
||||
public void UpdateOffsets(DocumentChangeEventArgs e) |
||||
{ |
||||
if (e == null) |
||||
throw new ArgumentNullException("e"); |
||||
if (isConnectedToDocument) |
||||
throw new InvalidOperationException("This TextSegmentCollection will automatically update offsets; do not call UpdateOffsets manually!"); |
||||
OnDocumentChanged(e); |
||||
CheckProperties(); |
||||
} |
||||
|
||||
void OnDocumentChanged(DocumentChangeEventArgs e) |
||||
{ |
||||
OffsetChangeMap map = e.OffsetChangeMapOrNull; |
||||
if (map != null) { |
||||
foreach (OffsetChangeMapEntry entry in map) { |
||||
UpdateOffsetsInternal(entry); |
||||
} |
||||
} else { |
||||
UpdateOffsetsInternal(e.CreateSingleChangeMapEntry()); |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Updates the start and end offsets of all segments stored in this collection.
|
||||
/// </summary>
|
||||
/// <param name="change">OffsetChangeMapEntry instance describing the change to the document.</param>
|
||||
public void UpdateOffsets(OffsetChangeMapEntry change) |
||||
{ |
||||
if (isConnectedToDocument) |
||||
throw new InvalidOperationException("This TextSegmentCollection will automatically update offsets; do not call UpdateOffsets manually!"); |
||||
UpdateOffsetsInternal(change); |
||||
CheckProperties(); |
||||
} |
||||
#endregion
|
||||
|
||||
#region UpdateOffsets (implementation)
|
||||
void UpdateOffsetsInternal(OffsetChangeMapEntry change) |
||||
{ |
||||
// Special case pure insertions, because they don't always cause a text segment to increase in size when the replaced region
|
||||
// is inside a segment (when offset is at start or end of a text semgent).
|
||||
if (change.RemovalLength == 0) { |
||||
InsertText(change.Offset, change.InsertionLength); |
||||
} else { |
||||
ReplaceText(change); |
||||
} |
||||
} |
||||
|
||||
void InsertText(int offset, int length) |
||||
{ |
||||
if (length == 0) |
||||
return; |
||||
|
||||
// enlarge segments that contain offset (excluding those that have offset as endpoint)
|
||||
foreach (TextSegment segment in FindSegmentsContaining(offset)) { |
||||
if (segment.StartOffset < offset && offset < segment.EndOffset) { |
||||
segment.Length += length; |
||||
} |
||||
} |
||||
|
||||
// move start offsets of all segments >= offset
|
||||
TextSegment node = FindFirstSegmentWithStartAfter(offset); |
||||
if (node != null) { |
||||
node.nodeLength += length; |
||||
UpdateAugmentedData(node); |
||||
} |
||||
} |
||||
|
||||
void ReplaceText(OffsetChangeMapEntry change) |
||||
{ |
||||
Debug.Assert(change.RemovalLength > 0); |
||||
int offset = change.Offset; |
||||
foreach (TextSegment segment in FindOverlappingSegments(offset, change.RemovalLength)) { |
||||
if (segment.StartOffset <= offset) { |
||||
if (segment.EndOffset >= offset + change.RemovalLength) { |
||||
// Replacement inside segment: adjust segment length
|
||||
segment.Length += change.InsertionLength - change.RemovalLength; |
||||
} else { |
||||
// Replacement starting inside segment and ending after segment end: set segment end to removal position
|
||||
//segment.EndOffset = offset;
|
||||
segment.Length = offset - segment.StartOffset; |
||||
} |
||||
} else { |
||||
// Replacement starting in front of text segment and running into segment.
|
||||
// Keep segment.EndOffset constant and move segment.StartOffset to the end of the replacement
|
||||
int remainingLength = segment.EndOffset - (offset + change.RemovalLength); |
||||
RemoveSegment(segment); |
||||
segment.StartOffset = offset + change.RemovalLength; |
||||
segment.Length = Math.Max(0, remainingLength); |
||||
AddSegment(segment); |
||||
} |
||||
} |
||||
// move start offsets of all segments > offset
|
||||
TextSegment node = FindFirstSegmentWithStartAfter(offset + 1); |
||||
if (node != null) { |
||||
Debug.Assert(node.nodeLength >= change.RemovalLength); |
||||
node.nodeLength += change.InsertionLength - change.RemovalLength; |
||||
UpdateAugmentedData(node); |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
#region Add
|
||||
/// <summary>
|
||||
/// Adds the specified segment to the tree. This will cause the segment to update when the
|
||||
/// document changes.
|
||||
/// </summary>
|
||||
public void Add(T item) |
||||
{ |
||||
if (item == null) |
||||
throw new ArgumentNullException("item"); |
||||
if (item.ownerTree != null) |
||||
throw new ArgumentException("The segment is already added to a SegmentCollection."); |
||||
AddSegment(item); |
||||
} |
||||
|
||||
void ISegmentTree.Add(TextSegment s) |
||||
{ |
||||
AddSegment(s); |
||||
} |
||||
|
||||
void AddSegment(TextSegment node) |
||||
{ |
||||
int insertionOffset = node.StartOffset; |
||||
node.distanceToMaxEnd = node.segmentLength; |
||||
if (root == null) { |
||||
root = node; |
||||
node.totalNodeLength = node.nodeLength; |
||||
} else if (insertionOffset >= root.totalNodeLength) { |
||||
// append segment at end of tree
|
||||
node.nodeLength = node.totalNodeLength = insertionOffset - root.totalNodeLength; |
||||
InsertAsRight(root.RightMost, node); |
||||
} else { |
||||
// insert in middle of tree
|
||||
TextSegment n = FindNode(ref insertionOffset); |
||||
Debug.Assert(insertionOffset < n.nodeLength); |
||||
// split node segment 'n' at offset
|
||||
node.totalNodeLength = node.nodeLength = insertionOffset; |
||||
n.nodeLength -= insertionOffset; |
||||
InsertBefore(n, node); |
||||
} |
||||
node.ownerTree = this; |
||||
count++; |
||||
CheckProperties(); |
||||
} |
||||
|
||||
void InsertBefore(TextSegment node, TextSegment newNode) |
||||
{ |
||||
if (node.left == null) { |
||||
InsertAsLeft(node, newNode); |
||||
} else { |
||||
InsertAsRight(node.left.RightMost, newNode); |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
#region GetNextSegment / GetPreviousSegment
|
||||
/// <summary>
|
||||
/// Gets the next segment after the specified segment.
|
||||
/// Segments are sorted by their start offset.
|
||||
/// Returns null if segment is the last segment.
|
||||
/// </summary>
|
||||
public T GetNextSegment(T segment) |
||||
{ |
||||
if (!Contains(segment)) |
||||
throw new ArgumentException("segment is not inside the segment tree"); |
||||
return (T)segment.Successor; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the previous segment before the specified segment.
|
||||
/// Segments are sorted by their start offset.
|
||||
/// Returns null if segment is the first 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 FirstSegment/LastSegment
|
||||
/// <summary>
|
||||
/// Returns the first segment in the collection or null, if the collection is empty.
|
||||
/// </summary>
|
||||
public T FirstSegment { |
||||
get { |
||||
return root == null ? null : (T)root.LeftMost; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Returns the last segment in the collection or null, if the collection is empty.
|
||||
/// </summary>
|
||||
public T LastSegment { |
||||
get { |
||||
return root == null ? null : (T)root.RightMost; |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
#region FindFirstSegmentWithStartAfter
|
||||
/// <summary>
|
||||
/// Gets the first segment with a start offset greater or equal to <paramref name="startOffset"/>.
|
||||
/// Returns null if no such segment is found.
|
||||
/// </summary>
|
||||
public T FindFirstSegmentWithStartAfter(int startOffset) |
||||
{ |
||||
if (root == null) |
||||
return null; |
||||
if (startOffset <= 0) |
||||
return (T)root.LeftMost; |
||||
TextSegment s = FindNode(ref startOffset); |
||||
// startOffset means that the previous segment is starting at the offset we were looking for
|
||||
while (startOffset == 0) { |
||||
TextSegment p = (s == null) ? root.RightMost : s.Predecessor; |
||||
// There must always be a predecessor: if we were looking for the first node, we would have already
|
||||
// returned it as root.LeftMost above.
|
||||
Debug.Assert(p != null); |
||||
startOffset += p.nodeLength; |
||||
s = p; |
||||
} |
||||
return (T)s; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Finds the node at the specified offset.
|
||||
/// After the method has run, offset is relative to the beginning of the returned node.
|
||||
/// </summary>
|
||||
TextSegment FindNode(ref int offset) |
||||
{ |
||||
TextSegment n = root; |
||||
while (true) { |
||||
if (n.left != null) { |
||||
if (offset < n.left.totalNodeLength) { |
||||
n = n.left; // descend into left subtree
|
||||
continue; |
||||
} else { |
||||
offset -= n.left.totalNodeLength; // skip left subtree
|
||||
} |
||||
} |
||||
if (offset < n.nodeLength) { |
||||
return n; // found correct node
|
||||
} else { |
||||
offset -= n.nodeLength; // skip this node
|
||||
} |
||||
if (n.right != null) { |
||||
n = n.right; // descend into right subtree
|
||||
} else { |
||||
// didn't find any node containing the offset
|
||||
return null; |
||||
} |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
#region FindOverlappingSegments
|
||||
/// <summary>
|
||||
/// Finds all segments that contain the given offset.
|
||||
/// (StartOffset <= offset <= EndOffset)
|
||||
/// Segments are returned in the order given by GetNextSegment/GetPreviousSegment.
|
||||
/// </summary>
|
||||
/// <returns>Returns a new collection containing the results of the query.
|
||||
/// This means it is safe to modify the TextSegmentCollection while iterating through the result collection.</returns>
|
||||
public ReadOnlyCollection<T> FindSegmentsContaining(int offset) |
||||
{ |
||||
return FindOverlappingSegments(offset, 0); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Finds all segments that overlap with the given segment (including touching segments).
|
||||
/// </summary>
|
||||
/// <returns>Returns a new collection containing the results of the query.
|
||||
/// This means it is safe to modify the TextSegmentCollection while iterating through the result collection.</returns>
|
||||
public ReadOnlyCollection<T> FindOverlappingSegments(ISegment segment) |
||||
{ |
||||
if (segment == null) |
||||
throw new ArgumentNullException("segment"); |
||||
return FindOverlappingSegments(segment.Offset, segment.Length); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Finds all segments that overlap with the given segment (including touching segments).
|
||||
/// Segments are returned in the order given by GetNextSegment/GetPreviousSegment.
|
||||
/// </summary>
|
||||
/// <returns>Returns a new collection containing the results of the query.
|
||||
/// This means it is safe to modify the TextSegmentCollection while iterating through the result collection.</returns>
|
||||
public ReadOnlyCollection<T> FindOverlappingSegments(int offset, int length) |
||||
{ |
||||
ThrowUtil.CheckNotNegative(length, "length"); |
||||
List<T> results = new List<T>(); |
||||
if (root != null) { |
||||
FindOverlappingSegments(results, root, offset, offset + length); |
||||
} |
||||
return results.AsReadOnly(); |
||||
} |
||||
|
||||
void FindOverlappingSegments(List<T> results, TextSegment node, int low, int high) |
||||
{ |
||||
// low and high are relative to node.LeftMost startpos (not node.LeftMost.Offset)
|
||||
if (high < 0) { |
||||
// node is irrelevant for search because all intervals in node are after high
|
||||
return; |
||||
} |
||||
|
||||
// find values relative to node.Offset
|
||||
int nodeLow = low - node.nodeLength; |
||||
int nodeHigh = high - node.nodeLength; |
||||
if (node.left != null) { |
||||
nodeLow -= node.left.totalNodeLength; |
||||
nodeHigh -= node.left.totalNodeLength; |
||||
} |
||||
|
||||
if (node.distanceToMaxEnd < nodeLow) { |
||||
// node is irrelevant for search because all intervals in node are before low
|
||||
return; |
||||
} |
||||
|
||||
if (node.left != null) |
||||
FindOverlappingSegments(results, node.left, low, high); |
||||
|
||||
if (nodeHigh < 0) { |
||||
// node and everything in node.right is before low
|
||||
return; |
||||
} |
||||
|
||||
if (nodeLow <= node.segmentLength) { |
||||
results.Add((T)node); |
||||
} |
||||
|
||||
if (node.right != null) |
||||
FindOverlappingSegments(results, node.right, nodeLow, nodeHigh); |
||||
} |
||||
#endregion
|
||||
|
||||
#region UpdateAugmentedData
|
||||
void UpdateAugmentedData(TextSegment node) |
||||
{ |
||||
int totalLength = node.nodeLength; |
||||
int distanceToMaxEnd = node.segmentLength; |
||||
if (node.left != null) { |
||||
totalLength += node.left.totalNodeLength; |
||||
|
||||
int leftDTME = node.left.distanceToMaxEnd; |
||||
// dtme is relative, so convert it to the coordinates of node:
|
||||
if (node.left.right != null) |
||||
leftDTME -= node.left.right.totalNodeLength; |
||||
leftDTME -= node.nodeLength; |
||||
if (leftDTME > distanceToMaxEnd) |
||||
distanceToMaxEnd = leftDTME; |
||||
} |
||||
if (node.right != null) { |
||||
totalLength += node.right.totalNodeLength; |
||||
|
||||
int rightDTME = node.right.distanceToMaxEnd; |
||||
// dtme is relative, so convert it to the coordinates of node:
|
||||
rightDTME += node.right.nodeLength; |
||||
if (node.right.left != null) |
||||
rightDTME += node.right.left.totalNodeLength; |
||||
if (rightDTME > distanceToMaxEnd) |
||||
distanceToMaxEnd = rightDTME; |
||||
} |
||||
if (node.totalNodeLength != totalLength |
||||
|| node.distanceToMaxEnd != distanceToMaxEnd) |
||||
{ |
||||
node.totalNodeLength = totalLength; |
||||
node.distanceToMaxEnd = distanceToMaxEnd; |
||||
if (node.parent != null) |
||||
UpdateAugmentedData(node.parent); |
||||
} |
||||
} |
||||
|
||||
void ISegmentTree.UpdateAugmentedData(TextSegment node) |
||||
{ |
||||
UpdateAugmentedData(node); |
||||
} |
||||
#endregion
|
||||
|
||||
#region Remove
|
||||
/// <summary>
|
||||
/// Removes the specified segment from the tree. This will cause the segment to not update
|
||||
/// anymore when the document changes.
|
||||
/// </summary>
|
||||
public bool Remove(T item) |
||||
{ |
||||
if (!Contains(item)) |
||||
return false; |
||||
RemoveSegment(item); |
||||
return true; |
||||
} |
||||
|
||||
void ISegmentTree.Remove(TextSegment s) |
||||
{ |
||||
RemoveSegment(s); |
||||
} |
||||
|
||||
void RemoveSegment(TextSegment s) |
||||
{ |
||||
int oldOffset = s.StartOffset; |
||||
TextSegment successor = s.Successor; |
||||
if (successor != null) |
||||
successor.nodeLength += s.nodeLength; |
||||
RemoveNode(s); |
||||
if (successor != null) |
||||
UpdateAugmentedData(successor); |
||||
Disconnect(s, oldOffset); |
||||
CheckProperties(); |
||||
} |
||||
|
||||
void Disconnect(TextSegment s, int offset) |
||||
{ |
||||
s.left = s.right = s.parent = null; |
||||
s.ownerTree = null; |
||||
s.nodeLength = offset; |
||||
count--; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Removes all segments from the tree.
|
||||
/// </summary>
|
||||
public void Clear() |
||||
{ |
||||
T[] segments = this.ToArray(); |
||||
root = null; |
||||
int offset = 0; |
||||
foreach (TextSegment s in segments) { |
||||
offset += s.nodeLength; |
||||
Disconnect(s, offset); |
||||
} |
||||
CheckProperties(); |
||||
} |
||||
#endregion
|
||||
|
||||
#region CheckProperties
|
||||
[Conditional("DATACONSISTENCYTEST")] |
||||
internal void CheckProperties() |
||||
{ |
||||
#if DEBUG
|
||||
if (root != null) { |
||||
CheckProperties(root); |
||||
|
||||
// check red-black property:
|
||||
int blackCount = -1; |
||||
CheckNodeProperties(root, null, RED, 0, ref blackCount); |
||||
} |
||||
|
||||
int expectedCount = 0; |
||||
// we cannot trust LINQ not to call ICollection.Count, so we need this loop
|
||||
// to count the elements in the tree
|
||||
using (IEnumerator<T> en = GetEnumerator()) { |
||||
while (en.MoveNext()) expectedCount++; |
||||
} |
||||
Debug.Assert(count == expectedCount); |
||||
#endif
|
||||
} |
||||
|
||||
#if DEBUG
|
||||
void CheckProperties(TextSegment node) |
||||
{ |
||||
int totalLength = node.nodeLength; |
||||
int distanceToMaxEnd = node.segmentLength; |
||||
if (node.left != null) { |
||||
CheckProperties(node.left); |
||||
totalLength += node.left.totalNodeLength; |
||||
distanceToMaxEnd = Math.Max(distanceToMaxEnd, |
||||
node.left.distanceToMaxEnd + node.left.StartOffset - node.StartOffset); |
||||
} |
||||
if (node.right != null) { |
||||
CheckProperties(node.right); |
||||
totalLength += node.right.totalNodeLength; |
||||
distanceToMaxEnd = Math.Max(distanceToMaxEnd, |
||||
node.right.distanceToMaxEnd + node.right.StartOffset - node.StartOffset); |
||||
} |
||||
Debug.Assert(node.totalNodeLength == totalLength); |
||||
Debug.Assert(node.distanceToMaxEnd == distanceToMaxEnd); |
||||
} |
||||
|
||||
/* |
||||
1. A node is either red or black. |
||||
2. The root is black. |
||||
3. All leaves are black. (The leaves are the NIL children.) |
||||
4. Both children of every red node are black. (So every red node must have a black parent.) |
||||
5. Every simple path from a node to a descendant leaf contains the same number of black nodes. (Not counting the leaf node.) |
||||
*/ |
||||
void CheckNodeProperties(TextSegment node, TextSegment parentNode, bool parentColor, int blackCount, ref int expectedBlackCount) |
||||
{ |
||||
if (node == null) return; |
||||
|
||||
Debug.Assert(node.parent == parentNode); |
||||
|
||||
if (parentColor == RED) { |
||||
Debug.Assert(node.color == BLACK); |
||||
} |
||||
if (node.color == BLACK) { |
||||
blackCount++; |
||||
} |
||||
if (node.left == null && node.right == null) { |
||||
// node is a leaf node:
|
||||
if (expectedBlackCount == -1) |
||||
expectedBlackCount = blackCount; |
||||
else |
||||
Debug.Assert(expectedBlackCount == blackCount); |
||||
} |
||||
CheckNodeProperties(node.left, node, node.color, blackCount, ref expectedBlackCount); |
||||
CheckNodeProperties(node.right, node, node.color, blackCount, ref expectedBlackCount); |
||||
} |
||||
|
||||
static void AppendTreeToString(TextSegment node, StringBuilder b, int indent) |
||||
{ |
||||
if (node.color == RED) |
||||
b.Append("RED "); |
||||
else |
||||
b.Append("BLACK "); |
||||
b.AppendLine(node.ToString() + node.ToDebugString()); |
||||
indent += 2; |
||||
if (node.left != null) { |
||||
b.Append(' ', indent); |
||||
b.Append("L: "); |
||||
AppendTreeToString(node.left, b, indent); |
||||
} |
||||
if (node.right != null) { |
||||
b.Append(' ', indent); |
||||
b.Append("R: "); |
||||
AppendTreeToString(node.right, b, indent); |
||||
} |
||||
} |
||||
#endif
|
||||
|
||||
internal string GetTreeAsString() |
||||
{ |
||||
#if DEBUG
|
||||
StringBuilder b = new StringBuilder(); |
||||
if (root != null) |
||||
AppendTreeToString(root, b, 0); |
||||
return b.ToString(); |
||||
#else
|
||||
return "Not available in release build."; |
||||
#endif
|
||||
} |
||||
#endregion
|
||||
|
||||
#region Red/Black Tree
|
||||
internal const bool RED = true; |
||||
internal const bool BLACK = false; |
||||
|
||||
void InsertAsLeft(TextSegment parentNode, TextSegment newNode) |
||||
{ |
||||
Debug.Assert(parentNode.left == null); |
||||
parentNode.left = newNode; |
||||
newNode.parent = parentNode; |
||||
newNode.color = RED; |
||||
UpdateAugmentedData(parentNode); |
||||
FixTreeOnInsert(newNode); |
||||
} |
||||
|
||||
void InsertAsRight(TextSegment parentNode, TextSegment newNode) |
||||
{ |
||||
Debug.Assert(parentNode.right == null); |
||||
parentNode.right = newNode; |
||||
newNode.parent = parentNode; |
||||
newNode.color = RED; |
||||
UpdateAugmentedData(parentNode); |
||||
FixTreeOnInsert(newNode); |
||||
} |
||||
|
||||
void FixTreeOnInsert(TextSegment node) |
||||
{ |
||||
Debug.Assert(node != null); |
||||
Debug.Assert(node.color == RED); |
||||
Debug.Assert(node.left == null || node.left.color == BLACK); |
||||
Debug.Assert(node.right == null || node.right.color == BLACK); |
||||
|
||||
TextSegment parentNode = node.parent; |
||||
if (parentNode == null) { |
||||
// we inserted in the root -> the node must be black
|
||||
// since this is a root node, making the node black increments the number of black nodes
|
||||
// on all paths by one, so it is still the same for all paths.
|
||||
node.color = BLACK; |
||||
return; |
||||
} |
||||
if (parentNode.color == BLACK) { |
||||
// if the parent node where we inserted was black, our red node is placed correctly.
|
||||
// since we inserted a red node, the number of black nodes on each path is unchanged
|
||||
// -> the tree is still balanced
|
||||
return; |
||||
} |
||||
// parentNode is red, so there is a conflict here!
|
||||
|
||||
// because the root is black, parentNode is not the root -> there is a grandparent node
|
||||
TextSegment grandparentNode = parentNode.parent; |
||||
TextSegment uncleNode = Sibling(parentNode); |
||||
if (uncleNode != null && uncleNode.color == RED) { |
||||
parentNode.color = BLACK; |
||||
uncleNode.color = BLACK; |
||||
grandparentNode.color = RED; |
||||
FixTreeOnInsert(grandparentNode); |
||||
return; |
||||
} |
||||
// now we know: parent is red but uncle is black
|
||||
// First rotation:
|
||||
if (node == parentNode.right && parentNode == grandparentNode.left) { |
||||
RotateLeft(parentNode); |
||||
node = node.left; |
||||
} else if (node == parentNode.left && parentNode == grandparentNode.right) { |
||||
RotateRight(parentNode); |
||||
node = node.right; |
||||
} |
||||
// because node might have changed, reassign variables:
|
||||
parentNode = node.parent; |
||||
grandparentNode = parentNode.parent; |
||||
|
||||
// Now recolor a bit:
|
||||
parentNode.color = BLACK; |
||||
grandparentNode.color = RED; |
||||
// Second rotation:
|
||||
if (node == parentNode.left && parentNode == grandparentNode.left) { |
||||
RotateRight(grandparentNode); |
||||
} else { |
||||
// because of the first rotation, this is guaranteed:
|
||||
Debug.Assert(node == parentNode.right && parentNode == grandparentNode.right); |
||||
RotateLeft(grandparentNode); |
||||
} |
||||
} |
||||
|
||||
void RemoveNode(TextSegment removedNode) |
||||
{ |
||||
if (removedNode.left != null && removedNode.right != null) { |
||||
// replace removedNode with it's in-order successor
|
||||
|
||||
TextSegment leftMost = removedNode.right.LeftMost; |
||||
RemoveNode(leftMost); // remove leftMost from its current location
|
||||
|
||||
// and overwrite the removedNode with it
|
||||
ReplaceNode(removedNode, leftMost); |
||||
leftMost.left = removedNode.left; |
||||
if (leftMost.left != null) leftMost.left.parent = leftMost; |
||||
leftMost.right = removedNode.right; |
||||
if (leftMost.right != null) leftMost.right.parent = leftMost; |
||||
leftMost.color = removedNode.color; |
||||
|
||||
UpdateAugmentedData(leftMost); |
||||
if (leftMost.parent != null) UpdateAugmentedData(leftMost.parent); |
||||
return; |
||||
} |
||||
|
||||
// now either removedNode.left or removedNode.right is null
|
||||
// get the remaining child
|
||||
TextSegment parentNode = removedNode.parent; |
||||
TextSegment childNode = removedNode.left ?? removedNode.right; |
||||
ReplaceNode(removedNode, childNode); |
||||
if (parentNode != null) UpdateAugmentedData(parentNode); |
||||
if (removedNode.color == BLACK) { |
||||
if (childNode != null && childNode.color == RED) { |
||||
childNode.color = BLACK; |
||||
} else { |
||||
FixTreeOnDelete(childNode, parentNode); |
||||
} |
||||
} |
||||
} |
||||
|
||||
void FixTreeOnDelete(TextSegment node, TextSegment parentNode) |
||||
{ |
||||
Debug.Assert(node == null || node.parent == parentNode); |
||||
if (parentNode == null) |
||||
return; |
||||
|
||||
// warning: node may be null
|
||||
TextSegment sibling = Sibling(node, parentNode); |
||||
if (sibling.color == RED) { |
||||
parentNode.color = RED; |
||||
sibling.color = BLACK; |
||||
if (node == parentNode.left) { |
||||
RotateLeft(parentNode); |
||||
} else { |
||||
RotateRight(parentNode); |
||||
} |
||||
|
||||
sibling = Sibling(node, parentNode); // update value of sibling after rotation
|
||||
} |
||||
|
||||
if (parentNode.color == BLACK |
||||
&& sibling.color == BLACK |
||||
&& GetColor(sibling.left) == BLACK |
||||
&& GetColor(sibling.right) == BLACK) |
||||
{ |
||||
sibling.color = RED; |
||||
FixTreeOnDelete(parentNode, parentNode.parent); |
||||
return; |
||||
} |
||||
|
||||
if (parentNode.color == RED |
||||
&& sibling.color == BLACK |
||||
&& GetColor(sibling.left) == BLACK |
||||
&& GetColor(sibling.right) == BLACK) |
||||
{ |
||||
sibling.color = RED; |
||||
parentNode.color = BLACK; |
||||
return; |
||||
} |
||||
|
||||
if (node == parentNode.left && |
||||
sibling.color == BLACK && |
||||
GetColor(sibling.left) == RED && |
||||
GetColor(sibling.right) == BLACK) |
||||
{ |
||||
sibling.color = RED; |
||||
sibling.left.color = BLACK; |
||||
RotateRight(sibling); |
||||
} |
||||
else if (node == parentNode.right && |
||||
sibling.color == BLACK && |
||||
GetColor(sibling.right) == RED && |
||||
GetColor(sibling.left) == BLACK) |
||||
{ |
||||
sibling.color = RED; |
||||
sibling.right.color = BLACK; |
||||
RotateLeft(sibling); |
||||
} |
||||
sibling = Sibling(node, parentNode); // update value of sibling after rotation
|
||||
|
||||
sibling.color = parentNode.color; |
||||
parentNode.color = BLACK; |
||||
if (node == parentNode.left) { |
||||
if (sibling.right != null) { |
||||
Debug.Assert(sibling.right.color == RED); |
||||
sibling.right.color = BLACK; |
||||
} |
||||
RotateLeft(parentNode); |
||||
} else { |
||||
if (sibling.left != null) { |
||||
Debug.Assert(sibling.left.color == RED); |
||||
sibling.left.color = BLACK; |
||||
} |
||||
RotateRight(parentNode); |
||||
} |
||||
} |
||||
|
||||
void ReplaceNode(TextSegment replacedNode, TextSegment newNode) |
||||
{ |
||||
if (replacedNode.parent == null) { |
||||
Debug.Assert(replacedNode == root); |
||||
root = newNode; |
||||
} else { |
||||
if (replacedNode.parent.left == replacedNode) |
||||
replacedNode.parent.left = newNode; |
||||
else |
||||
replacedNode.parent.right = newNode; |
||||
} |
||||
if (newNode != null) { |
||||
newNode.parent = replacedNode.parent; |
||||
} |
||||
replacedNode.parent = null; |
||||
} |
||||
|
||||
void RotateLeft(TextSegment p) |
||||
{ |
||||
// let q be p's right child
|
||||
TextSegment q = p.right; |
||||
Debug.Assert(q != null); |
||||
Debug.Assert(q.parent == p); |
||||
// set q to be the new root
|
||||
ReplaceNode(p, q); |
||||
|
||||
// set p's right child to be q's left child
|
||||
p.right = q.left; |
||||
if (p.right != null) p.right.parent = p; |
||||
// set q's left child to be p
|
||||
q.left = p; |
||||
p.parent = q; |
||||
UpdateAugmentedData(p); |
||||
UpdateAugmentedData(q); |
||||
} |
||||
|
||||
void RotateRight(TextSegment p) |
||||
{ |
||||
// let q be p's left child
|
||||
TextSegment q = p.left; |
||||
Debug.Assert(q != null); |
||||
Debug.Assert(q.parent == p); |
||||
// set q to be the new root
|
||||
ReplaceNode(p, q); |
||||
|
||||
// set p's left child to be q's right child
|
||||
p.left = q.right; |
||||
if (p.left != null) p.left.parent = p; |
||||
// set q's right child to be p
|
||||
q.right = p; |
||||
p.parent = q; |
||||
UpdateAugmentedData(p); |
||||
UpdateAugmentedData(q); |
||||
} |
||||
|
||||
static TextSegment Sibling(TextSegment node) |
||||
{ |
||||
if (node == node.parent.left) |
||||
return node.parent.right; |
||||
else |
||||
return node.parent.left; |
||||
} |
||||
|
||||
static TextSegment Sibling(TextSegment node, TextSegment parentNode) |
||||
{ |
||||
Debug.Assert(node == null || node.parent == parentNode); |
||||
if (node == parentNode.left) |
||||
return parentNode.right; |
||||
else |
||||
return parentNode.left; |
||||
} |
||||
|
||||
static bool GetColor(TextSegment node) |
||||
{ |
||||
return node != null ? node.color : BLACK; |
||||
} |
||||
#endregion
|
||||
|
||||
#region ICollection<T> implementation
|
||||
/// <summary>
|
||||
/// Gets the number of segments in the tree.
|
||||
/// </summary>
|
||||
public int Count { |
||||
get { return count; } |
||||
} |
||||
|
||||
bool ICollection<T>.IsReadOnly { |
||||
get { return false; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets whether this tree contains the specified item.
|
||||
/// </summary>
|
||||
public bool Contains(T item) |
||||
{ |
||||
return item != null && item.ownerTree == this; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Copies all segments in this SegmentTree to the specified array.
|
||||
/// </summary>
|
||||
public void CopyTo(T[] array, int arrayIndex) |
||||
{ |
||||
if (array == null) |
||||
throw new ArgumentNullException("array"); |
||||
if (array.Length < this.Count) |
||||
throw new ArgumentException("The array is too small", "array"); |
||||
if (arrayIndex < 0 || arrayIndex + count > array.Length) |
||||
throw new ArgumentOutOfRangeException("arrayIndex", arrayIndex, "Value must be between 0 and " + (array.Length - count)); |
||||
foreach (T s in this) { |
||||
array[arrayIndex++] = s; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets an enumerator to enumerate the segments.
|
||||
/// </summary>
|
||||
public IEnumerator<T> GetEnumerator() |
||||
{ |
||||
if (root != null) { |
||||
TextSegment current = root.LeftMost; |
||||
while (current != null) { |
||||
yield return (T)current; |
||||
// TODO: check if collection was modified during enumeration
|
||||
current = current.Successor; |
||||
} |
||||
} |
||||
} |
||||
|
||||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() |
||||
{ |
||||
return this.GetEnumerator(); |
||||
} |
||||
#endregion
|
||||
} |
||||
} |
@ -1,136 +0,0 @@
@@ -1,136 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Diagnostics; |
||||
using System.Linq; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
#if !NREFACTORY
|
||||
/// <summary>
|
||||
/// Provides ITextSourceVersion instances.
|
||||
/// </summary>
|
||||
public class TextSourceVersionProvider |
||||
{ |
||||
Version currentVersion; |
||||
|
||||
/// <summary>
|
||||
/// Creates a new TextSourceVersionProvider instance.
|
||||
/// </summary>
|
||||
public TextSourceVersionProvider() |
||||
{ |
||||
this.currentVersion = new Version(this); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the current version.
|
||||
/// </summary>
|
||||
public ITextSourceVersion CurrentVersion { |
||||
get { return currentVersion; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Replaces the current version with a new version.
|
||||
/// </summary>
|
||||
/// <param name="change">Change from current version to new version</param>
|
||||
public void AppendChange(TextChangeEventArgs change) |
||||
{ |
||||
if (change == null) |
||||
throw new ArgumentNullException("change"); |
||||
currentVersion.change = change; |
||||
currentVersion.next = new Version(currentVersion); |
||||
currentVersion = currentVersion.next; |
||||
} |
||||
|
||||
[DebuggerDisplay("Version #{id}")] |
||||
sealed class Version : ITextSourceVersion |
||||
{ |
||||
// Reference back to the provider.
|
||||
// Used to determine if two checkpoints belong to the same document.
|
||||
readonly TextSourceVersionProvider provider; |
||||
// ID used for CompareAge()
|
||||
readonly int id; |
||||
|
||||
// the change from this version to the next version
|
||||
internal TextChangeEventArgs change; |
||||
internal Version next; |
||||
|
||||
internal Version(TextSourceVersionProvider provider) |
||||
{ |
||||
this.provider = provider; |
||||
} |
||||
|
||||
internal Version(Version prev) |
||||
{ |
||||
this.provider = prev.provider; |
||||
this.id = unchecked( prev.id + 1 ); |
||||
} |
||||
|
||||
public bool BelongsToSameDocumentAs(ITextSourceVersion other) |
||||
{ |
||||
Version o = other as Version; |
||||
return o != null && provider == o.provider; |
||||
} |
||||
|
||||
public int CompareAge(ITextSourceVersion other) |
||||
{ |
||||
if (other == null) |
||||
throw new ArgumentNullException("other"); |
||||
Version o = other as Version; |
||||
if (o == null || provider != o.provider) |
||||
throw new ArgumentException("Versions do not belong to the same document."); |
||||
// We will allow overflows, but assume that the maximum distance between checkpoints is 2^31-1.
|
||||
// This is guaranteed on x86 because so many checkpoints don't fit into memory.
|
||||
return Math.Sign(unchecked( this.id - o.id )); |
||||
} |
||||
|
||||
public IEnumerable<TextChangeEventArgs> GetChangesTo(ITextSourceVersion other) |
||||
{ |
||||
int result = CompareAge(other); |
||||
Version o = (Version)other; |
||||
if (result < 0) |
||||
return GetForwardChanges(o); |
||||
else if (result > 0) |
||||
return o.GetForwardChanges(this).Reverse().Select(change => change.Invert()); |
||||
else |
||||
return Empty<TextChangeEventArgs>.Array; |
||||
} |
||||
|
||||
IEnumerable<TextChangeEventArgs> GetForwardChanges(Version other) |
||||
{ |
||||
// Return changes from this(inclusive) to other(exclusive).
|
||||
for (Version node = this; node != other; node = node.next) { |
||||
yield return node.change; |
||||
} |
||||
} |
||||
|
||||
public int MoveOffsetTo(ITextSourceVersion other, int oldOffset, AnchorMovementType movement) |
||||
{ |
||||
int offset = oldOffset; |
||||
foreach (var e in GetChangesTo(other)) { |
||||
offset = e.GetNewOffset(offset, movement); |
||||
} |
||||
return offset; |
||||
} |
||||
} |
||||
} |
||||
#endif
|
||||
} |
@ -1,422 +0,0 @@
@@ -1,422 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Globalization; |
||||
using System.Windows.Documents; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory.Editor; |
||||
#endif
|
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
/// <summary>
|
||||
/// Specifies the mode for getting the next caret position.
|
||||
/// </summary>
|
||||
public enum CaretPositioningMode |
||||
{ |
||||
/// <summary>
|
||||
/// Normal positioning (stop after every grapheme)
|
||||
/// </summary>
|
||||
Normal, |
||||
/// <summary>
|
||||
/// Stop only on word borders.
|
||||
/// </summary>
|
||||
WordBorder, |
||||
/// <summary>
|
||||
/// Stop only at the beginning of words. This is used for Ctrl+Left/Ctrl+Right.
|
||||
/// </summary>
|
||||
WordStart, |
||||
/// <summary>
|
||||
/// Stop only at the beginning of words, and anywhere in the middle of symbols.
|
||||
/// </summary>
|
||||
WordStartOrSymbol, |
||||
/// <summary>
|
||||
/// Stop only on word borders, and anywhere in the middle of symbols.
|
||||
/// </summary>
|
||||
WordBorderOrSymbol, |
||||
/// <summary>
|
||||
/// Stop between every Unicode codepoint, even within the same grapheme.
|
||||
/// This is used to implement deleting the previous grapheme when Backspace is pressed.
|
||||
/// </summary>
|
||||
EveryCodepoint |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Static helper methods for working with text.
|
||||
/// </summary>
|
||||
public static partial class TextUtilities |
||||
{ |
||||
#region GetControlCharacterName
|
||||
// the names of the first 32 ASCII characters = Unicode C0 block
|
||||
static readonly string[] c0Table = { |
||||
"NUL", "SOH", "STX", "ETX", "EOT", "ENQ", "ACK", "BEL", "BS", "HT", |
||||
"LF", "VT", "FF", "CR", "SO", "SI", "DLE", "DC1", "DC2", "DC3", |
||||
"DC4", "NAK", "SYN", "ETB", "CAN", "EM", "SUB", "ESC", "FS", "GS", |
||||
"RS", "US" |
||||
}; |
||||
|
||||
// DEL (ASCII 127) and
|
||||
// the names of the control characters in the C1 block (Unicode 128 to 159)
|
||||
static readonly string[] delAndC1Table = { |
||||
"DEL", |
||||
"PAD", "HOP", "BPH", "NBH", "IND", "NEL", "SSA", "ESA", "HTS", "HTJ", |
||||
"VTS", "PLD", "PLU", "RI", "SS2", "SS3", "DCS", "PU1", "PU2", "STS", |
||||
"CCH", "MW", "SPA", "EPA", "SOS", "SGCI", "SCI", "CSI", "ST", "OSC", |
||||
"PM", "APC" |
||||
}; |
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the control character.
|
||||
/// For unknown characters, the unicode codepoint is returned as 4-digit hexadecimal value.
|
||||
/// </summary>
|
||||
public static string GetControlCharacterName(char controlCharacter) |
||||
{ |
||||
int num = (int)controlCharacter; |
||||
if (num < c0Table.Length) |
||||
return c0Table[num]; |
||||
else if (num >= 127 && num <= 159) |
||||
return delAndC1Table[num - 127]; |
||||
else |
||||
return num.ToString("x4", CultureInfo.InvariantCulture); |
||||
} |
||||
#endregion
|
||||
|
||||
#region GetWhitespace
|
||||
/// <summary>
|
||||
/// Gets all whitespace (' ' and '\t', but no newlines) after offset.
|
||||
/// </summary>
|
||||
/// <param name="textSource">The text source.</param>
|
||||
/// <param name="offset">The offset where the whitespace starts.</param>
|
||||
/// <returns>The segment containing the whitespace.</returns>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace", |
||||
Justification = "WPF uses 'Whitespace'")] |
||||
public static ISegment GetWhitespaceAfter(ITextSource textSource, int offset) |
||||
{ |
||||
if (textSource == null) |
||||
throw new ArgumentNullException("textSource"); |
||||
int pos; |
||||
for (pos = offset; pos < textSource.TextLength; pos++) { |
||||
char c = textSource.GetCharAt(pos); |
||||
if (c != ' ' && c != '\t') |
||||
break; |
||||
} |
||||
return new SimpleSegment(offset, pos - offset); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets all whitespace (' ' and '\t', but no newlines) before offset.
|
||||
/// </summary>
|
||||
/// <param name="textSource">The text source.</param>
|
||||
/// <param name="offset">The offset where the whitespace ends.</param>
|
||||
/// <returns>The segment containing the whitespace.</returns>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace", |
||||
Justification = "WPF uses 'Whitespace'")] |
||||
public static ISegment GetWhitespaceBefore(ITextSource textSource, int offset) |
||||
{ |
||||
if (textSource == null) |
||||
throw new ArgumentNullException("textSource"); |
||||
int pos; |
||||
for (pos = offset - 1; pos >= 0; pos--) { |
||||
char c = textSource.GetCharAt(pos); |
||||
if (c != ' ' && c != '\t') |
||||
break; |
||||
} |
||||
pos++; // go back the one character that isn't whitespace
|
||||
return new SimpleSegment(pos, offset - pos); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the leading whitespace segment on the document line.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace", |
||||
Justification = "WPF uses 'Whitespace'")] |
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", |
||||
Justification = "Parameter cannot be ITextSource because it must belong to the DocumentLine")] |
||||
public static ISegment GetLeadingWhitespace(TextDocument document, DocumentLine documentLine) |
||||
{ |
||||
if (documentLine == null) |
||||
throw new ArgumentNullException("documentLine"); |
||||
return GetWhitespaceAfter(document, documentLine.Offset); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the trailing whitespace segment on the document line.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace", |
||||
Justification = "WPF uses 'Whitespace'")] |
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", |
||||
Justification = "Parameter cannot be ITextSource because it must belong to the DocumentLine")] |
||||
public static ISegment GetTrailingWhitespace(TextDocument document, DocumentLine documentLine) |
||||
{ |
||||
if (documentLine == null) |
||||
throw new ArgumentNullException("documentLine"); |
||||
ISegment segment = GetWhitespaceBefore(document, documentLine.EndOffset); |
||||
// If the whole line consists of whitespace, we consider all of it as leading whitespace,
|
||||
// so return an empty segment as trailing whitespace.
|
||||
if (segment.Offset == documentLine.Offset) |
||||
return new SimpleSegment(documentLine.EndOffset, 0); |
||||
else |
||||
return segment; |
||||
} |
||||
#endregion
|
||||
|
||||
#region GetSingleIndentationSegment
|
||||
/// <summary>
|
||||
/// Gets a single indentation segment starting at <paramref name="offset"/> - at most one tab
|
||||
/// or <paramref name="indentationSize"/> spaces.
|
||||
/// </summary>
|
||||
/// <param name="textSource">The text source.</param>
|
||||
/// <param name="offset">The offset where the indentation segment starts.</param>
|
||||
/// <param name="indentationSize">The size of an indentation unit. See <see cref="TextEditorOptions.IndentationSize"/>.</param>
|
||||
/// <returns>The indentation segment.
|
||||
/// If there is no indentation character at the specified <paramref name="offset"/>,
|
||||
/// an empty segment is returned.</returns>
|
||||
public static ISegment GetSingleIndentationSegment(ITextSource textSource, int offset, int indentationSize) |
||||
{ |
||||
if (textSource == null) |
||||
throw new ArgumentNullException("textSource"); |
||||
int pos = offset; |
||||
while (pos < textSource.TextLength) { |
||||
char c = textSource.GetCharAt(pos); |
||||
if (c == '\t') { |
||||
if (pos == offset) |
||||
return new SimpleSegment(offset, 1); |
||||
else |
||||
break; |
||||
} else if (c == ' ') { |
||||
if (pos - offset >= indentationSize) |
||||
break; |
||||
} else { |
||||
break; |
||||
} |
||||
// continue only if c==' ' and (pos-offset)<tabSize
|
||||
pos++; |
||||
} |
||||
return new SimpleSegment(offset, pos - offset); |
||||
} |
||||
#endregion
|
||||
|
||||
#region GetCharacterClass
|
||||
/// <summary>
|
||||
/// Gets whether the character is whitespace, part of an identifier, or line terminator.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "c")] |
||||
public static CharacterClass GetCharacterClass(char c) |
||||
{ |
||||
if (c == '\r' || c == '\n') |
||||
return CharacterClass.LineTerminator; |
||||
if (c == '_') |
||||
return CharacterClass.IdentifierPart; |
||||
return GetCharacterClass(char.GetUnicodeCategory(c)); |
||||
} |
||||
|
||||
static CharacterClass GetCharacterClass(char highSurrogate, char lowSurrogate) |
||||
{ |
||||
if (char.IsSurrogatePair(highSurrogate, lowSurrogate)) { |
||||
return GetCharacterClass(char.GetUnicodeCategory(highSurrogate.ToString() + lowSurrogate.ToString(), 0)); |
||||
} else { |
||||
// malformed surrogate pair
|
||||
return CharacterClass.Other; |
||||
} |
||||
} |
||||
|
||||
static CharacterClass GetCharacterClass(UnicodeCategory c) |
||||
{ |
||||
switch (c) { |
||||
case UnicodeCategory.SpaceSeparator: |
||||
case UnicodeCategory.LineSeparator: |
||||
case UnicodeCategory.ParagraphSeparator: |
||||
case UnicodeCategory.Control: |
||||
return CharacterClass.Whitespace; |
||||
case UnicodeCategory.UppercaseLetter: |
||||
case UnicodeCategory.LowercaseLetter: |
||||
case UnicodeCategory.TitlecaseLetter: |
||||
case UnicodeCategory.ModifierLetter: |
||||
case UnicodeCategory.OtherLetter: |
||||
case UnicodeCategory.DecimalDigitNumber: |
||||
return CharacterClass.IdentifierPart; |
||||
case UnicodeCategory.NonSpacingMark: |
||||
case UnicodeCategory.SpacingCombiningMark: |
||||
case UnicodeCategory.EnclosingMark: |
||||
return CharacterClass.CombiningMark; |
||||
default: |
||||
return CharacterClass.Other; |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
#region GetNextCaretPosition
|
||||
/// <summary>
|
||||
/// Gets the next caret position.
|
||||
/// </summary>
|
||||
/// <param name="textSource">The text source.</param>
|
||||
/// <param name="offset">The start offset inside the text source.</param>
|
||||
/// <param name="direction">The search direction (forwards or backwards).</param>
|
||||
/// <param name="mode">The mode for caret positioning.</param>
|
||||
/// <returns>The offset of the next caret position, or -1 if there is no further caret position
|
||||
/// in the text source.</returns>
|
||||
/// <remarks>
|
||||
/// This method is NOT equivalent to the actual caret movement when using VisualLine.GetNextCaretPosition.
|
||||
/// In real caret movement, there are additional caret stops at line starts and ends. This method
|
||||
/// treats linefeeds as simple whitespace.
|
||||
/// </remarks>
|
||||
public static int GetNextCaretPosition(ITextSource textSource, int offset, LogicalDirection direction, CaretPositioningMode mode) |
||||
{ |
||||
if (textSource == null) |
||||
throw new ArgumentNullException("textSource"); |
||||
switch (mode) { |
||||
case CaretPositioningMode.Normal: |
||||
case CaretPositioningMode.EveryCodepoint: |
||||
case CaretPositioningMode.WordBorder: |
||||
case CaretPositioningMode.WordBorderOrSymbol: |
||||
case CaretPositioningMode.WordStart: |
||||
case CaretPositioningMode.WordStartOrSymbol: |
||||
break; // OK
|
||||
default: |
||||
throw new ArgumentException("Unsupported CaretPositioningMode: " + mode, "mode"); |
||||
} |
||||
if (direction != LogicalDirection.Backward |
||||
&& direction != LogicalDirection.Forward) |
||||
{ |
||||
throw new ArgumentException("Invalid LogicalDirection: " + direction, "direction"); |
||||
} |
||||
int textLength = textSource.TextLength; |
||||
if (textLength <= 0) { |
||||
// empty document? has a normal caret position at 0, though no word borders
|
||||
if (IsNormal(mode)) { |
||||
if (offset > 0 && direction == LogicalDirection.Backward) return 0; |
||||
if (offset < 0 && direction == LogicalDirection.Forward) return 0; |
||||
} |
||||
return -1; |
||||
} |
||||
while (true) { |
||||
int nextPos = (direction == LogicalDirection.Backward) ? offset - 1 : offset + 1; |
||||
|
||||
// return -1 if there is no further caret position in the text source
|
||||
// we also need this to handle offset values outside the valid range
|
||||
if (nextPos < 0 || nextPos > textLength) |
||||
return -1; |
||||
|
||||
// check if we've run against the textSource borders.
|
||||
// a 'textSource' usually isn't the whole document, but a single VisualLineElement.
|
||||
if (nextPos == 0) { |
||||
// at the document start, there's only a word border
|
||||
// if the first character is not whitespace
|
||||
if (IsNormal(mode) || !char.IsWhiteSpace(textSource.GetCharAt(0))) |
||||
return nextPos; |
||||
} else if (nextPos == textLength) { |
||||
// at the document end, there's never a word start
|
||||
if (mode != CaretPositioningMode.WordStart && mode != CaretPositioningMode.WordStartOrSymbol) { |
||||
// at the document end, there's only a word border
|
||||
// if the last character is not whitespace
|
||||
if (IsNormal(mode) || !char.IsWhiteSpace(textSource.GetCharAt(textLength - 1))) |
||||
return nextPos; |
||||
} |
||||
} else { |
||||
char charBefore = textSource.GetCharAt(nextPos - 1); |
||||
char charAfter = textSource.GetCharAt(nextPos); |
||||
// Don't stop in the middle of a surrogate pair
|
||||
if (!char.IsSurrogatePair(charBefore, charAfter)) { |
||||
CharacterClass classBefore = GetCharacterClass(charBefore); |
||||
CharacterClass classAfter = GetCharacterClass(charAfter); |
||||
// get correct class for characters outside BMP:
|
||||
if (char.IsLowSurrogate(charBefore) && nextPos >= 2) { |
||||
classBefore = GetCharacterClass(textSource.GetCharAt(nextPos - 2), charBefore); |
||||
} |
||||
if (char.IsHighSurrogate(charAfter) && nextPos + 1 < textLength) { |
||||
classAfter = GetCharacterClass(charAfter, textSource.GetCharAt(nextPos + 1)); |
||||
} |
||||
if (StopBetweenCharacters(mode, classBefore, classAfter)) { |
||||
return nextPos; |
||||
} |
||||
} |
||||
} |
||||
// we'll have to continue searching...
|
||||
offset = nextPos; |
||||
} |
||||
} |
||||
|
||||
static bool IsNormal(CaretPositioningMode mode) |
||||
{ |
||||
return mode == CaretPositioningMode.Normal || mode == CaretPositioningMode.EveryCodepoint; |
||||
} |
||||
|
||||
static bool StopBetweenCharacters(CaretPositioningMode mode, CharacterClass charBefore, CharacterClass charAfter) |
||||
{ |
||||
if (mode == CaretPositioningMode.EveryCodepoint) |
||||
return true; |
||||
// Don't stop in the middle of a grapheme
|
||||
if (charAfter == CharacterClass.CombiningMark) |
||||
return false; |
||||
// Stop after every grapheme in normal mode
|
||||
if (mode == CaretPositioningMode.Normal) |
||||
return true; |
||||
if (charBefore == charAfter) { |
||||
if (charBefore == CharacterClass.Other && |
||||
(mode == CaretPositioningMode.WordBorderOrSymbol || mode == CaretPositioningMode.WordStartOrSymbol)) |
||||
{ |
||||
// With the "OrSymbol" modes, there's a word border and start between any two unknown characters
|
||||
return true; |
||||
} |
||||
} else { |
||||
// this looks like a possible border
|
||||
|
||||
// if we're looking for word starts, check that this is a word start (and not a word end)
|
||||
// if we're just checking for word borders, accept unconditionally
|
||||
if (!((mode == CaretPositioningMode.WordStart || mode == CaretPositioningMode.WordStartOrSymbol) |
||||
&& (charAfter == CharacterClass.Whitespace || charAfter == CharacterClass.LineTerminator))) |
||||
{ |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
#endregion
|
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Classifies a character as whitespace, line terminator, part of an identifier, or other.
|
||||
/// </summary>
|
||||
public enum CharacterClass |
||||
{ |
||||
/// <summary>
|
||||
/// The character is not whitespace, line terminator or part of an identifier.
|
||||
/// </summary>
|
||||
Other, |
||||
/// <summary>
|
||||
/// The character is whitespace (but not line terminator).
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace", |
||||
Justification = "WPF uses 'Whitespace'")] |
||||
Whitespace, |
||||
/// <summary>
|
||||
/// The character can be part of an identifier (Letter, digit or underscore).
|
||||
/// </summary>
|
||||
IdentifierPart, |
||||
/// <summary>
|
||||
/// The character is line terminator (\r or \n).
|
||||
/// </summary>
|
||||
LineTerminator, |
||||
/// <summary>
|
||||
/// The character is a unicode combining mark that modifies the previous character.
|
||||
/// Corresponds to the Unicode designations "Mn", "Mc" and "Me".
|
||||
/// </summary>
|
||||
CombiningMark |
||||
} |
||||
} |
@ -1,76 +0,0 @@
@@ -1,76 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Diagnostics; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
/// <summary>
|
||||
/// This class stacks the last x operations from the undostack and makes
|
||||
/// one undo/redo operation from it.
|
||||
/// </summary>
|
||||
sealed class UndoOperationGroup : IUndoableOperationWithContext |
||||
{ |
||||
IUndoableOperation[] undolist; |
||||
|
||||
public UndoOperationGroup(Deque<IUndoableOperation> stack, int numops) |
||||
{ |
||||
if (stack == null) { |
||||
throw new ArgumentNullException("stack"); |
||||
} |
||||
|
||||
Debug.Assert(numops > 0 , "UndoOperationGroup : numops should be > 0"); |
||||
Debug.Assert(numops <= stack.Count); |
||||
|
||||
undolist = new IUndoableOperation[numops]; |
||||
for (int i = 0; i < numops; ++i) { |
||||
undolist[i] = stack.PopBack(); |
||||
} |
||||
} |
||||
|
||||
public void Undo() |
||||
{ |
||||
for (int i = 0; i < undolist.Length; ++i) { |
||||
undolist[i].Undo(); |
||||
} |
||||
} |
||||
|
||||
public void Undo(UndoStack stack) |
||||
{ |
||||
for (int i = 0; i < undolist.Length; ++i) { |
||||
stack.RunUndo(undolist[i]); |
||||
} |
||||
} |
||||
|
||||
public void Redo() |
||||
{ |
||||
for (int i = undolist.Length - 1; i >= 0; --i) { |
||||
undolist[i].Redo(); |
||||
} |
||||
} |
||||
|
||||
public void Redo(UndoStack stack) |
||||
{ |
||||
for (int i = undolist.Length - 1; i >= 0; --i) { |
||||
stack.RunRedo(undolist[i]); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,458 +0,0 @@
@@ -1,458 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.ComponentModel; |
||||
using System.Diagnostics; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
/// <summary>
|
||||
/// Undo stack implementation.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")] |
||||
public sealed class UndoStack : INotifyPropertyChanged |
||||
{ |
||||
/// undo stack is listening for changes
|
||||
internal const int StateListen = 0; |
||||
/// undo stack is reverting/repeating a set of changes
|
||||
internal const int StatePlayback = 1; |
||||
// undo stack is reverting/repeating a set of changes and modifies the document to do this
|
||||
internal const int StatePlaybackModifyDocument = 2; |
||||
/// state is used for checking that noone but the UndoStack performs changes
|
||||
/// during Undo events
|
||||
internal int state = StateListen; |
||||
|
||||
Deque<IUndoableOperation> undostack = new Deque<IUndoableOperation>(); |
||||
Deque<IUndoableOperation> redostack = new Deque<IUndoableOperation>(); |
||||
int sizeLimit = int.MaxValue; |
||||
|
||||
int undoGroupDepth; |
||||
int actionCountInUndoGroup; |
||||
int optionalActionCount; |
||||
object lastGroupDescriptor; |
||||
bool allowContinue; |
||||
|
||||
#region IsOriginalFile implementation
|
||||
// implements feature request SD2-784 - File still considered dirty after undoing all changes
|
||||
|
||||
/// <summary>
|
||||
/// Number of times undo must be executed until the original state is reached.
|
||||
/// Negative: number of times redo must be executed until the original state is reached.
|
||||
/// Special case: int.MinValue == original state is unreachable
|
||||
/// </summary>
|
||||
int elementsOnUndoUntilOriginalFile; |
||||
|
||||
bool isOriginalFile = true; |
||||
|
||||
/// <summary>
|
||||
/// Gets whether the document is currently in its original state (no modifications).
|
||||
/// </summary>
|
||||
public bool IsOriginalFile { |
||||
get { return isOriginalFile; } |
||||
} |
||||
|
||||
void RecalcIsOriginalFile() |
||||
{ |
||||
bool newIsOriginalFile = (elementsOnUndoUntilOriginalFile == 0); |
||||
if (newIsOriginalFile != isOriginalFile) { |
||||
isOriginalFile = newIsOriginalFile; |
||||
NotifyPropertyChanged("IsOriginalFile"); |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Marks the current state as original. Discards any previous "original" markers.
|
||||
/// </summary>
|
||||
public void MarkAsOriginalFile() |
||||
{ |
||||
elementsOnUndoUntilOriginalFile = 0; |
||||
RecalcIsOriginalFile(); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Discards the current "original" marker.
|
||||
/// </summary>
|
||||
public void DiscardOriginalFileMarker() |
||||
{ |
||||
elementsOnUndoUntilOriginalFile = int.MinValue; |
||||
RecalcIsOriginalFile(); |
||||
} |
||||
|
||||
void FileModified(int newElementsOnUndoStack) |
||||
{ |
||||
if (elementsOnUndoUntilOriginalFile == int.MinValue) |
||||
return; |
||||
|
||||
elementsOnUndoUntilOriginalFile += newElementsOnUndoStack; |
||||
if (elementsOnUndoUntilOriginalFile > undostack.Count) |
||||
elementsOnUndoUntilOriginalFile = int.MinValue; |
||||
|
||||
// don't call RecalcIsOriginalFile(): wait until end of undo group
|
||||
} |
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Gets if the undo stack currently accepts changes.
|
||||
/// Is false while an undo action is running.
|
||||
/// </summary>
|
||||
public bool AcceptChanges { |
||||
get { return state == StateListen; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets if there are actions on the undo stack.
|
||||
/// Use the PropertyChanged event to listen to changes of this property.
|
||||
/// </summary>
|
||||
public bool CanUndo { |
||||
get { return undostack.Count > 0; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets if there are actions on the redo stack.
|
||||
/// Use the PropertyChanged event to listen to changes of this property.
|
||||
/// </summary>
|
||||
public bool CanRedo { |
||||
get { return redostack.Count > 0; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets/Sets the limit on the number of items on the undo stack.
|
||||
/// </summary>
|
||||
/// <remarks>The size limit is enforced only on the number of stored top-level undo groups.
|
||||
/// Elements within undo groups do not count towards the size limit.</remarks>
|
||||
public int SizeLimit { |
||||
get { return sizeLimit; } |
||||
set { |
||||
if (value < 0) |
||||
ThrowUtil.CheckNotNegative(value, "value"); |
||||
if (sizeLimit != value) { |
||||
sizeLimit = value; |
||||
NotifyPropertyChanged("SizeLimit"); |
||||
if (undoGroupDepth == 0) |
||||
EnforceSizeLimit(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
void EnforceSizeLimit() |
||||
{ |
||||
Debug.Assert(undoGroupDepth == 0); |
||||
while (undostack.Count > sizeLimit) |
||||
undostack.PopFront(); |
||||
while (redostack.Count > sizeLimit) |
||||
redostack.PopFront(); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// If an undo group is open, gets the group descriptor of the current top-level
|
||||
/// undo group.
|
||||
/// If no undo group is open, gets the group descriptor from the previous undo group.
|
||||
/// </summary>
|
||||
/// <remarks>The group descriptor can be used to join adjacent undo groups:
|
||||
/// use a group descriptor to mark your changes, and on the second action,
|
||||
/// compare LastGroupDescriptor and use <see cref="StartContinuedUndoGroup"/> if you
|
||||
/// want to join the undo groups.</remarks>
|
||||
public object LastGroupDescriptor { |
||||
get { return lastGroupDescriptor; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Starts grouping changes.
|
||||
/// Maintains a counter so that nested calls are possible.
|
||||
/// </summary>
|
||||
public void StartUndoGroup() |
||||
{ |
||||
StartUndoGroup(null); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Starts grouping changes.
|
||||
/// Maintains a counter so that nested calls are possible.
|
||||
/// </summary>
|
||||
/// <param name="groupDescriptor">An object that is stored with the undo group.
|
||||
/// If this is not a top-level undo group, the parameter is ignored.</param>
|
||||
public void StartUndoGroup(object groupDescriptor) |
||||
{ |
||||
if (undoGroupDepth == 0) { |
||||
actionCountInUndoGroup = 0; |
||||
optionalActionCount = 0; |
||||
lastGroupDescriptor = groupDescriptor; |
||||
} |
||||
undoGroupDepth++; |
||||
//Util.LoggingService.Debug("Open undo group (new depth=" + undoGroupDepth + ")");
|
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Starts grouping changes, continuing with the previously closed undo group if possible.
|
||||
/// Maintains a counter so that nested calls are possible.
|
||||
/// If the call to StartContinuedUndoGroup is a nested call, it behaves exactly
|
||||
/// as <see cref="StartUndoGroup()"/>, only top-level calls can continue existing undo groups.
|
||||
/// </summary>
|
||||
/// <param name="groupDescriptor">An object that is stored with the undo group.
|
||||
/// If this is not a top-level undo group, the parameter is ignored.</param>
|
||||
public void StartContinuedUndoGroup(object groupDescriptor = null) |
||||
{ |
||||
if (undoGroupDepth == 0) { |
||||
actionCountInUndoGroup = (allowContinue && undostack.Count > 0) ? 1 : 0; |
||||
optionalActionCount = 0; |
||||
lastGroupDescriptor = groupDescriptor; |
||||
} |
||||
undoGroupDepth++; |
||||
//Util.LoggingService.Debug("Continue undo group (new depth=" + undoGroupDepth + ")");
|
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Stops grouping changes.
|
||||
/// </summary>
|
||||
public void EndUndoGroup() |
||||
{ |
||||
if (undoGroupDepth == 0) throw new InvalidOperationException("There are no open undo groups"); |
||||
undoGroupDepth--; |
||||
//Util.LoggingService.Debug("Close undo group (new depth=" + undoGroupDepth + ")");
|
||||
if (undoGroupDepth == 0) { |
||||
Debug.Assert(state == StateListen || actionCountInUndoGroup == 0); |
||||
allowContinue = true; |
||||
if (actionCountInUndoGroup == optionalActionCount) { |
||||
// only optional actions: don't store them
|
||||
for (int i = 0; i < optionalActionCount; i++) { |
||||
undostack.PopBack(); |
||||
} |
||||
allowContinue = false; |
||||
} else if (actionCountInUndoGroup > 1) { |
||||
// combine all actions within the group into a single grouped action
|
||||
undostack.PushBack(new UndoOperationGroup(undostack, actionCountInUndoGroup)); |
||||
FileModified(-actionCountInUndoGroup + 1 + optionalActionCount); |
||||
} |
||||
//if (state == StateListen) {
|
||||
EnforceSizeLimit(); |
||||
RecalcIsOriginalFile(); // can raise event
|
||||
//}
|
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Throws an InvalidOperationException if an undo group is current open.
|
||||
/// </summary>
|
||||
void ThrowIfUndoGroupOpen() |
||||
{ |
||||
if (undoGroupDepth != 0) { |
||||
undoGroupDepth = 0; |
||||
throw new InvalidOperationException("No undo group should be open at this point"); |
||||
} |
||||
if (state != StateListen) { |
||||
throw new InvalidOperationException("This method cannot be called while an undo operation is being performed"); |
||||
} |
||||
} |
||||
|
||||
List<TextDocument> affectedDocuments; |
||||
|
||||
internal void RegisterAffectedDocument(TextDocument document) |
||||
{ |
||||
if (affectedDocuments == null) |
||||
affectedDocuments = new List<TextDocument>(); |
||||
if (!affectedDocuments.Contains(document)) { |
||||
affectedDocuments.Add(document); |
||||
document.BeginUpdate(); |
||||
} |
||||
} |
||||
|
||||
void CallEndUpdateOnAffectedDocuments() |
||||
{ |
||||
if (affectedDocuments != null) { |
||||
foreach (TextDocument doc in affectedDocuments) { |
||||
doc.EndUpdate(); |
||||
} |
||||
affectedDocuments = null; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Call this method to undo the last operation on the stack
|
||||
/// </summary>
|
||||
public void Undo() |
||||
{ |
||||
ThrowIfUndoGroupOpen(); |
||||
if (undostack.Count > 0) { |
||||
// disallow continuing undo groups after undo operation
|
||||
lastGroupDescriptor = null; allowContinue = false; |
||||
// fetch operation to undo and move it to redo stack
|
||||
IUndoableOperation uedit = undostack.PopBack(); |
||||
redostack.PushBack(uedit); |
||||
state = StatePlayback; |
||||
try { |
||||
RunUndo(uedit); |
||||
} finally { |
||||
state = StateListen; |
||||
FileModified(-1); |
||||
CallEndUpdateOnAffectedDocuments(); |
||||
} |
||||
RecalcIsOriginalFile(); |
||||
if (undostack.Count == 0) |
||||
NotifyPropertyChanged("CanUndo"); |
||||
if (redostack.Count == 1) |
||||
NotifyPropertyChanged("CanRedo"); |
||||
} |
||||
} |
||||
|
||||
internal void RunUndo(IUndoableOperation op) |
||||
{ |
||||
IUndoableOperationWithContext opWithCtx = op as IUndoableOperationWithContext; |
||||
if (opWithCtx != null) |
||||
opWithCtx.Undo(this); |
||||
else |
||||
op.Undo(); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Call this method to redo the last undone operation
|
||||
/// </summary>
|
||||
public void Redo() |
||||
{ |
||||
ThrowIfUndoGroupOpen(); |
||||
if (redostack.Count > 0) { |
||||
lastGroupDescriptor = null; allowContinue = false; |
||||
IUndoableOperation uedit = redostack.PopBack(); |
||||
undostack.PushBack(uedit); |
||||
state = StatePlayback; |
||||
try { |
||||
RunRedo(uedit); |
||||
} finally { |
||||
state = StateListen; |
||||
FileModified(1); |
||||
CallEndUpdateOnAffectedDocuments(); |
||||
} |
||||
RecalcIsOriginalFile(); |
||||
if (redostack.Count == 0) |
||||
NotifyPropertyChanged("CanRedo"); |
||||
if (undostack.Count == 1) |
||||
NotifyPropertyChanged("CanUndo"); |
||||
} |
||||
} |
||||
|
||||
internal void RunRedo(IUndoableOperation op) |
||||
{ |
||||
IUndoableOperationWithContext opWithCtx = op as IUndoableOperationWithContext; |
||||
if (opWithCtx != null) |
||||
opWithCtx.Redo(this); |
||||
else |
||||
op.Redo(); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Call this method to push an UndoableOperation on the undostack.
|
||||
/// The redostack will be cleared if you use this method.
|
||||
/// </summary>
|
||||
public void Push(IUndoableOperation operation) |
||||
{ |
||||
Push(operation, false); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Call this method to push an UndoableOperation on the undostack.
|
||||
/// However, the operation will be only stored if the undo group contains a
|
||||
/// non-optional operation.
|
||||
/// Use this method to store the caret position/selection on the undo stack to
|
||||
/// prevent having only actions that affect only the caret and not the document.
|
||||
/// </summary>
|
||||
public void PushOptional(IUndoableOperation operation) |
||||
{ |
||||
if (undoGroupDepth == 0) |
||||
throw new InvalidOperationException("Cannot use PushOptional outside of undo group"); |
||||
Push(operation, true); |
||||
} |
||||
|
||||
void Push(IUndoableOperation operation, bool isOptional) |
||||
{ |
||||
if (operation == null) { |
||||
throw new ArgumentNullException("operation"); |
||||
} |
||||
|
||||
if (state == StateListen && sizeLimit > 0) { |
||||
bool wasEmpty = undostack.Count == 0; |
||||
|
||||
bool needsUndoGroup = undoGroupDepth == 0; |
||||
if (needsUndoGroup) StartUndoGroup(); |
||||
undostack.PushBack(operation); |
||||
actionCountInUndoGroup++; |
||||
if (isOptional) |
||||
optionalActionCount++; |
||||
else |
||||
FileModified(1); |
||||
if (needsUndoGroup) EndUndoGroup(); |
||||
if (wasEmpty) |
||||
NotifyPropertyChanged("CanUndo"); |
||||
ClearRedoStack(); |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Call this method, if you want to clear the redo stack
|
||||
/// </summary>
|
||||
public void ClearRedoStack() |
||||
{ |
||||
if (redostack.Count != 0) { |
||||
redostack.Clear(); |
||||
NotifyPropertyChanged("CanRedo"); |
||||
// if the "original file" marker is on the redo stack: remove it
|
||||
if (elementsOnUndoUntilOriginalFile < 0) |
||||
elementsOnUndoUntilOriginalFile = int.MinValue; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Clears both the undo and redo stack.
|
||||
/// </summary>
|
||||
public void ClearAll() |
||||
{ |
||||
ThrowIfUndoGroupOpen(); |
||||
actionCountInUndoGroup = 0; |
||||
optionalActionCount = 0; |
||||
if (undostack.Count != 0) { |
||||
lastGroupDescriptor = null; |
||||
allowContinue = false; |
||||
undostack.Clear(); |
||||
NotifyPropertyChanged("CanUndo"); |
||||
} |
||||
ClearRedoStack(); |
||||
} |
||||
|
||||
internal void Push(TextDocument document, DocumentChangeEventArgs e) |
||||
{ |
||||
if (state == StatePlayback) |
||||
throw new InvalidOperationException("Document changes during undo/redo operations are not allowed."); |
||||
if (state == StatePlaybackModifyDocument) |
||||
state = StatePlayback; // allow only 1 change per expected modification
|
||||
else |
||||
Push(new DocumentChangeOperation(document, e)); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Is raised when a property (CanUndo, CanRedo) changed.
|
||||
/// </summary>
|
||||
public event PropertyChangedEventHandler PropertyChanged; |
||||
|
||||
void NotifyPropertyChanged(string propertyName) |
||||
{ |
||||
if (PropertyChanged != null) |
||||
PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); |
||||
} |
||||
} |
||||
} |
@ -1,109 +0,0 @@
@@ -1,109 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Document |
||||
{ |
||||
/// <summary>
|
||||
/// Allows registering a line tracker on a TextDocument using a weak reference from the document to the line tracker.
|
||||
/// </summary>
|
||||
public sealed class WeakLineTracker : ILineTracker |
||||
{ |
||||
TextDocument textDocument; |
||||
WeakReference targetObject; |
||||
|
||||
private WeakLineTracker(TextDocument textDocument, ILineTracker targetTracker) |
||||
{ |
||||
this.textDocument = textDocument; |
||||
this.targetObject = new WeakReference(targetTracker); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Registers the <paramref name="targetTracker"/> as line tracker for the <paramref name="textDocument"/>.
|
||||
/// A weak reference to the target tracker will be used, and the WeakLineTracker will deregister itself
|
||||
/// when the target tracker is garbage collected.
|
||||
/// </summary>
|
||||
public static WeakLineTracker Register(TextDocument textDocument, ILineTracker targetTracker) |
||||
{ |
||||
if (textDocument == null) |
||||
throw new ArgumentNullException("textDocument"); |
||||
if (targetTracker == null) |
||||
throw new ArgumentNullException("targetTracker"); |
||||
WeakLineTracker wlt = new WeakLineTracker(textDocument, targetTracker); |
||||
textDocument.LineTrackers.Add(wlt); |
||||
return wlt; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Deregisters the weak line tracker.
|
||||
/// </summary>
|
||||
public void Deregister() |
||||
{ |
||||
if (textDocument != null) { |
||||
textDocument.LineTrackers.Remove(this); |
||||
textDocument = null; |
||||
} |
||||
} |
||||
|
||||
void ILineTracker.BeforeRemoveLine(DocumentLine line) |
||||
{ |
||||
ILineTracker targetTracker = targetObject.Target as ILineTracker; |
||||
if (targetTracker != null) |
||||
targetTracker.BeforeRemoveLine(line); |
||||
else |
||||
Deregister(); |
||||
} |
||||
|
||||
void ILineTracker.SetLineLength(DocumentLine line, int newTotalLength) |
||||
{ |
||||
ILineTracker targetTracker = targetObject.Target as ILineTracker; |
||||
if (targetTracker != null) |
||||
targetTracker.SetLineLength(line, newTotalLength); |
||||
else |
||||
Deregister(); |
||||
} |
||||
|
||||
void ILineTracker.LineInserted(DocumentLine insertionPos, DocumentLine newLine) |
||||
{ |
||||
ILineTracker targetTracker = targetObject.Target as ILineTracker; |
||||
if (targetTracker != null) |
||||
targetTracker.LineInserted(insertionPos, newLine); |
||||
else |
||||
Deregister(); |
||||
} |
||||
|
||||
void ILineTracker.RebuildDocument() |
||||
{ |
||||
ILineTracker targetTracker = targetObject.Target as ILineTracker; |
||||
if (targetTracker != null) |
||||
targetTracker.RebuildDocument(); |
||||
else |
||||
Deregister(); |
||||
} |
||||
|
||||
void ILineTracker.ChangeComplete(DocumentChangeEventArgs e) |
||||
{ |
||||
ILineTracker targetTracker = targetObject.Target as ILineTracker; |
||||
if (targetTracker != null) |
||||
targetTracker.ChangeComplete(e); |
||||
else |
||||
Deregister(); |
||||
} |
||||
} |
||||
} |
@ -1,117 +0,0 @@
@@ -1,117 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Diagnostics; |
||||
using System.Windows; |
||||
|
||||
using ICSharpCode.AvalonEdit.Document; |
||||
using ICSharpCode.AvalonEdit.Rendering; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Editing |
||||
{ |
||||
/// <summary>
|
||||
/// Base class for margins.
|
||||
/// Margins don't have to derive from this class, it just helps maintaining a reference to the TextView
|
||||
/// and the TextDocument.
|
||||
/// AbstractMargin derives from FrameworkElement, so if you don't want to handle visual children and rendering
|
||||
/// on your own, choose another base class for your margin!
|
||||
/// </summary>
|
||||
public abstract class AbstractMargin : FrameworkElement, ITextViewConnect |
||||
{ |
||||
/// <summary>
|
||||
/// TextView property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty TextViewProperty = |
||||
DependencyProperty.Register("TextView", typeof(TextView), typeof(AbstractMargin), |
||||
new FrameworkPropertyMetadata(OnTextViewChanged)); |
||||
|
||||
/// <summary>
|
||||
/// Gets/sets the text view for which line numbers are displayed.
|
||||
/// </summary>
|
||||
/// <remarks>Adding a margin to <see cref="TextArea.LeftMargins"/> will automatically set this property to the text area's TextView.</remarks>
|
||||
public TextView TextView { |
||||
get { return (TextView)GetValue(TextViewProperty); } |
||||
set { SetValue(TextViewProperty, value); } |
||||
} |
||||
|
||||
static void OnTextViewChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e) |
||||
{ |
||||
AbstractMargin margin = (AbstractMargin)dp; |
||||
margin.wasAutoAddedToTextView = false; |
||||
margin.OnTextViewChanged((TextView)e.OldValue, (TextView)e.NewValue); |
||||
} |
||||
|
||||
// automatically set/unset TextView property using ITextViewConnect
|
||||
bool wasAutoAddedToTextView; |
||||
|
||||
void ITextViewConnect.AddToTextView(TextView textView) |
||||
{ |
||||
if (this.TextView == null) { |
||||
this.TextView = textView; |
||||
wasAutoAddedToTextView = true; |
||||
} else if (this.TextView != textView) { |
||||
throw new InvalidOperationException("This margin belongs to a different TextView."); |
||||
} |
||||
} |
||||
|
||||
void ITextViewConnect.RemoveFromTextView(TextView textView) |
||||
{ |
||||
if (wasAutoAddedToTextView && this.TextView == textView) { |
||||
this.TextView = null; |
||||
Debug.Assert(!wasAutoAddedToTextView); // setting this.TextView should have unset this flag
|
||||
} |
||||
} |
||||
|
||||
TextDocument document; |
||||
|
||||
/// <summary>
|
||||
/// Gets the document associated with the margin.
|
||||
/// </summary>
|
||||
public TextDocument Document { |
||||
get { return document; } |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Called when the <see cref="TextView"/> is changing.
|
||||
/// </summary>
|
||||
protected virtual void OnTextViewChanged(TextView oldTextView, TextView newTextView) |
||||
{ |
||||
if (oldTextView != null) { |
||||
oldTextView.DocumentChanged -= TextViewDocumentChanged; |
||||
} |
||||
if (newTextView != null) { |
||||
newTextView.DocumentChanged += TextViewDocumentChanged; |
||||
} |
||||
TextViewDocumentChanged(null, null); |
||||
} |
||||
|
||||
void TextViewDocumentChanged(object sender, EventArgs e) |
||||
{ |
||||
OnDocumentChanged(document, TextView != null ? TextView.Document : null); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Called when the <see cref="Document"/> is changing.
|
||||
/// </summary>
|
||||
protected virtual void OnDocumentChanged(TextDocument oldDocument, TextDocument newDocument) |
||||
{ |
||||
document = newDocument; |
||||
} |
||||
} |
||||
} |
@ -1,538 +0,0 @@
@@ -1,538 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Diagnostics; |
||||
using System.Linq; |
||||
using System.Windows; |
||||
using System.Windows.Documents; |
||||
using System.Windows.Media; |
||||
using System.Windows.Media.TextFormatting; |
||||
using System.Windows.Threading; |
||||
using ICSharpCode.AvalonEdit.Document; |
||||
using ICSharpCode.AvalonEdit.Rendering; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory; |
||||
using ICSharpCode.NRefactory.Editor; |
||||
#endif
|
||||
|
||||
namespace ICSharpCode.AvalonEdit.Editing |
||||
{ |
||||
/// <summary>
|
||||
/// Helper class with caret-related methods.
|
||||
/// </summary>
|
||||
public sealed class Caret |
||||
{ |
||||
readonly TextArea textArea; |
||||
readonly TextView textView; |
||||
readonly CaretLayer caretAdorner; |
||||
bool visible; |
||||
|
||||
internal Caret(TextArea textArea) |
||||
{ |
||||
this.textArea = textArea; |
||||
this.textView = textArea.TextView; |
||||
position = new TextViewPosition(1, 1, 0); |
||||
|
||||
caretAdorner = new CaretLayer(textArea); |
||||
textView.InsertLayer(caretAdorner, KnownLayer.Caret, LayerInsertionPosition.Replace); |
||||
textView.VisualLinesChanged += TextView_VisualLinesChanged; |
||||
textView.ScrollOffsetChanged += TextView_ScrollOffsetChanged; |
||||
} |
||||
|
||||
internal void UpdateIfVisible() |
||||
{ |
||||
if (visible) { |
||||
Show(); |
||||
} |
||||
} |
||||
|
||||
void TextView_VisualLinesChanged(object sender, EventArgs e) |
||||
{ |
||||
if (visible) { |
||||
Show(); |
||||
} |
||||
// required because the visual columns might have changed if the
|
||||
// element generators did something differently than on the last run
|
||||
// (e.g. a FoldingSection was collapsed)
|
||||
InvalidateVisualColumn(); |
||||
} |
||||
|
||||
void TextView_ScrollOffsetChanged(object sender, EventArgs e) |
||||
{ |
||||
if (caretAdorner != null) { |
||||
caretAdorner.InvalidateVisual(); |
||||
} |
||||
} |
||||
|
||||
double desiredXPos = double.NaN; |
||||
TextViewPosition position; |
||||
|
||||
/// <summary>
|
||||
/// Gets/Sets the position of the caret.
|
||||
/// Retrieving this property will validate the visual column (which can be expensive).
|
||||
/// Use the <see cref="Location"/> property instead if you don't need the visual column.
|
||||
/// </summary>
|
||||
public TextViewPosition Position { |
||||
get { |
||||
ValidateVisualColumn(); |
||||
return position; |
||||
} |
||||
set { |
||||
if (position != value) { |
||||
position = value; |
||||
|
||||
storedCaretOffset = -1; |
||||
|
||||
//Debug.WriteLine("Caret position changing to " + value);
|
||||
|
||||
ValidatePosition(); |
||||
InvalidateVisualColumn(); |
||||
RaisePositionChanged(); |
||||
Log("Caret position changed to " + value); |
||||
if (visible) |
||||
Show(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the caret position without validating it.
|
||||
/// </summary>
|
||||
internal TextViewPosition NonValidatedPosition { |
||||
get { |
||||
return position; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets/Sets the location of the caret.
|
||||
/// The getter of this property is faster than <see cref="Position"/> because it doesn't have
|
||||
/// to validate the visual column.
|
||||
/// </summary>
|
||||
public TextLocation Location { |
||||
get { |
||||
return position.Location; |
||||
} |
||||
set { |
||||
this.Position = new TextViewPosition(value); |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets/Sets the caret line.
|
||||
/// </summary>
|
||||
public int Line { |
||||
get { return position.Line; } |
||||
set { |
||||
this.Position = new TextViewPosition(value, position.Column); |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets/Sets the caret column.
|
||||
/// </summary>
|
||||
public int Column { |
||||
get { return position.Column; } |
||||
set { |
||||
this.Position = new TextViewPosition(position.Line, value); |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets/Sets the caret visual column.
|
||||
/// </summary>
|
||||
public int VisualColumn { |
||||
get { |
||||
ValidateVisualColumn(); |
||||
return position.VisualColumn; |
||||
} |
||||
set { |
||||
this.Position = new TextViewPosition(position.Line, position.Column, value); |
||||
} |
||||
} |
||||
|
||||
bool isInVirtualSpace; |
||||
|
||||
/// <summary>
|
||||
/// Gets whether the caret is in virtual space.
|
||||
/// </summary>
|
||||
public bool IsInVirtualSpace { |
||||
get { |
||||
ValidateVisualColumn(); |
||||
return isInVirtualSpace; |
||||
} |
||||
} |
||||
|
||||
int storedCaretOffset; |
||||
|
||||
internal void OnDocumentChanging() |
||||
{ |
||||
storedCaretOffset = this.Offset; |
||||
InvalidateVisualColumn(); |
||||
} |
||||
|
||||
internal void OnDocumentChanged(DocumentChangeEventArgs e) |
||||
{ |
||||
InvalidateVisualColumn(); |
||||
if (storedCaretOffset >= 0) { |
||||
// If the caret is at the end of a selection, we don't expand the selection if something
|
||||
// is inserted at the end. Thus we also need to keep the caret in front of the insertion.
|
||||
AnchorMovementType caretMovementType; |
||||
if (!textArea.Selection.IsEmpty && storedCaretOffset == textArea.Selection.SurroundingSegment.EndOffset) |
||||
caretMovementType = AnchorMovementType.BeforeInsertion; |
||||
else |
||||
caretMovementType = AnchorMovementType.Default; |
||||
int newCaretOffset = e.GetNewOffset(storedCaretOffset, caretMovementType); |
||||
TextDocument document = textArea.Document; |
||||
if (document != null) { |
||||
// keep visual column
|
||||
this.Position = new TextViewPosition(document.GetLocation(newCaretOffset), position.VisualColumn); |
||||
} |
||||
} |
||||
storedCaretOffset = -1; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets/Sets the caret offset.
|
||||
/// Setting the caret offset has the side effect of setting the <see cref="DesiredXPos"/> to NaN.
|
||||
/// </summary>
|
||||
public int Offset { |
||||
get { |
||||
TextDocument document = textArea.Document; |
||||
if (document == null) { |
||||
return 0; |
||||
} else { |
||||
return document.GetOffset(position.Location); |
||||
} |
||||
} |
||||
set { |
||||
TextDocument document = textArea.Document; |
||||
if (document != null) { |
||||
this.Position = new TextViewPosition(document.GetLocation(value)); |
||||
this.DesiredXPos = double.NaN; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets/Sets the desired x-position of the caret, in device-independent pixels.
|
||||
/// This property is NaN if the caret has no desired position.
|
||||
/// </summary>
|
||||
public double DesiredXPos { |
||||
get { return desiredXPos; } |
||||
set { desiredXPos = value; } |
||||
} |
||||
|
||||
void ValidatePosition() |
||||
{ |
||||
if (position.Line < 1) |
||||
position.Line = 1; |
||||
if (position.Column < 1) |
||||
position.Column = 1; |
||||
if (position.VisualColumn < -1) |
||||
position.VisualColumn = -1; |
||||
TextDocument document = textArea.Document; |
||||
if (document != null) { |
||||
if (position.Line > document.LineCount) { |
||||
position.Line = document.LineCount; |
||||
position.Column = document.GetLineByNumber(position.Line).Length + 1; |
||||
position.VisualColumn = -1; |
||||
} else { |
||||
DocumentLine line = document.GetLineByNumber(position.Line); |
||||
if (position.Column > line.Length + 1) { |
||||
position.Column = line.Length + 1; |
||||
position.VisualColumn = -1; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Event raised when the caret position has changed.
|
||||
/// If the caret position is changed inside a document update (between BeginUpdate/EndUpdate calls),
|
||||
/// the PositionChanged event is raised only once at the end of the document update.
|
||||
/// </summary>
|
||||
public event EventHandler PositionChanged; |
||||
|
||||
bool raisePositionChangedOnUpdateFinished; |
||||
|
||||
void RaisePositionChanged() |
||||
{ |
||||
if (textArea.Document != null && textArea.Document.IsInUpdate) { |
||||
raisePositionChangedOnUpdateFinished = true; |
||||
} else { |
||||
if (PositionChanged != null) { |
||||
PositionChanged(this, EventArgs.Empty); |
||||
} |
||||
} |
||||
} |
||||
|
||||
internal void OnDocumentUpdateFinished() |
||||
{ |
||||
if (raisePositionChangedOnUpdateFinished) { |
||||
if (PositionChanged != null) { |
||||
PositionChanged(this, EventArgs.Empty); |
||||
} |
||||
} |
||||
} |
||||
|
||||
bool visualColumnValid; |
||||
|
||||
void ValidateVisualColumn() |
||||
{ |
||||
if (!visualColumnValid) { |
||||
TextDocument document = textArea.Document; |
||||
if (document != null) { |
||||
Debug.WriteLine("Explicit validation of caret column"); |
||||
var documentLine = document.GetLineByNumber(position.Line); |
||||
RevalidateVisualColumn(textView.GetOrConstructVisualLine(documentLine)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
void InvalidateVisualColumn() |
||||
{ |
||||
visualColumnValid = false; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Validates the visual column of the caret using the specified visual line.
|
||||
/// The visual line must contain the caret offset.
|
||||
/// </summary>
|
||||
void RevalidateVisualColumn(VisualLine visualLine) |
||||
{ |
||||
if (visualLine == null) |
||||
throw new ArgumentNullException("visualLine"); |
||||
|
||||
// mark column as validated
|
||||
visualColumnValid = true; |
||||
|
||||
int caretOffset = textView.Document.GetOffset(position.Location); |
||||
int firstDocumentLineOffset = visualLine.FirstDocumentLine.Offset; |
||||
position.VisualColumn = visualLine.ValidateVisualColumn(position, textArea.Selection.EnableVirtualSpace); |
||||
|
||||
// search possible caret positions
|
||||
int newVisualColumnForwards = visualLine.GetNextCaretPosition(position.VisualColumn - 1, LogicalDirection.Forward, CaretPositioningMode.Normal, textArea.Selection.EnableVirtualSpace); |
||||
// If position.VisualColumn was valid, we're done with validation.
|
||||
if (newVisualColumnForwards != position.VisualColumn) { |
||||
// also search backwards so that we can pick the better match
|
||||
int newVisualColumnBackwards = visualLine.GetNextCaretPosition(position.VisualColumn + 1, LogicalDirection.Backward, CaretPositioningMode.Normal, textArea.Selection.EnableVirtualSpace); |
||||
|
||||
if (newVisualColumnForwards < 0 && newVisualColumnBackwards < 0) |
||||
throw ThrowUtil.NoValidCaretPosition(); |
||||
|
||||
// determine offsets for new visual column positions
|
||||
int newOffsetForwards; |
||||
if (newVisualColumnForwards >= 0) |
||||
newOffsetForwards = visualLine.GetRelativeOffset(newVisualColumnForwards) + firstDocumentLineOffset; |
||||
else |
||||
newOffsetForwards = -1; |
||||
int newOffsetBackwards; |
||||
if (newVisualColumnBackwards >= 0) |
||||
newOffsetBackwards = visualLine.GetRelativeOffset(newVisualColumnBackwards) + firstDocumentLineOffset; |
||||
else |
||||
newOffsetBackwards = -1; |
||||
|
||||
int newVisualColumn, newOffset; |
||||
// if there's only one valid position, use it
|
||||
if (newVisualColumnForwards < 0) { |
||||
newVisualColumn = newVisualColumnBackwards; |
||||
newOffset = newOffsetBackwards; |
||||
} else if (newVisualColumnBackwards < 0) { |
||||
newVisualColumn = newVisualColumnForwards; |
||||
newOffset = newOffsetForwards; |
||||
} else { |
||||
// two valid positions: find the better match
|
||||
if (Math.Abs(newOffsetBackwards - caretOffset) < Math.Abs(newOffsetForwards - caretOffset)) { |
||||
// backwards is better
|
||||
newVisualColumn = newVisualColumnBackwards; |
||||
newOffset = newOffsetBackwards; |
||||
} else { |
||||
// forwards is better
|
||||
newVisualColumn = newVisualColumnForwards; |
||||
newOffset = newOffsetForwards; |
||||
} |
||||
} |
||||
this.Position = new TextViewPosition(textView.Document.GetLocation(newOffset), newVisualColumn); |
||||
} |
||||
isInVirtualSpace = (position.VisualColumn > visualLine.VisualLength); |
||||
} |
||||
|
||||
Rect CalcCaretRectangle(VisualLine visualLine) |
||||
{ |
||||
if (!visualColumnValid) { |
||||
RevalidateVisualColumn(visualLine); |
||||
} |
||||
|
||||
TextLine textLine = visualLine.GetTextLine(position.VisualColumn, position.IsAtEndOfLine); |
||||
double xPos = visualLine.GetTextLineVisualXPosition(textLine, position.VisualColumn); |
||||
double lineTop = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextTop); |
||||
double lineBottom = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextBottom); |
||||
|
||||
return new Rect(xPos, |
||||
lineTop, |
||||
SystemParameters.CaretWidth, |
||||
lineBottom - lineTop); |
||||
} |
||||
|
||||
Rect CalcCaretOverstrikeRectangle(VisualLine visualLine) |
||||
{ |
||||
if (!visualColumnValid) { |
||||
RevalidateVisualColumn(visualLine); |
||||
} |
||||
|
||||
int currentPos = position.VisualColumn; |
||||
// The text being overwritten in overstrike mode is everything up to the next normal caret stop
|
||||
int nextPos = visualLine.GetNextCaretPosition(currentPos, LogicalDirection.Forward, CaretPositioningMode.Normal, true); |
||||
TextLine textLine = visualLine.GetTextLine(currentPos); |
||||
|
||||
Rect r; |
||||
if (currentPos < visualLine.VisualLength) { |
||||
// If the caret is within the text, use GetTextBounds() for the text being overwritten.
|
||||
// This is necessary to ensure the rectangle is calculated correctly in bidirectional text.
|
||||
var textBounds = textLine.GetTextBounds(currentPos, nextPos - currentPos)[0]; |
||||
r = textBounds.Rectangle; |
||||
r.Y += visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.LineTop); |
||||
} else { |
||||
// If the caret is at the end of the line (or in virtual space),
|
||||
// use the visual X position of currentPos and nextPos (one or more of which will be in virtual space)
|
||||
double xPos = visualLine.GetTextLineVisualXPosition(textLine, currentPos); |
||||
double xPos2 = visualLine.GetTextLineVisualXPosition(textLine, nextPos); |
||||
double lineTop = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextTop); |
||||
double lineBottom = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextBottom); |
||||
r = new Rect(xPos, lineTop, xPos2 - xPos, lineBottom - lineTop); |
||||
} |
||||
// If the caret is too small (e.g. in front of zero-width character), ensure it's still visible
|
||||
if (r.Width < SystemParameters.CaretWidth) |
||||
r.Width = SystemParameters.CaretWidth; |
||||
return r; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Returns the caret rectangle. The coordinate system is in device-independent pixels from the top of the document.
|
||||
/// </summary>
|
||||
public Rect CalculateCaretRectangle() |
||||
{ |
||||
if (textView != null && textView.Document != null) { |
||||
VisualLine visualLine = textView.GetOrConstructVisualLine(textView.Document.GetLineByNumber(position.Line)); |
||||
return textArea.OverstrikeMode ? CalcCaretOverstrikeRectangle(visualLine) : CalcCaretRectangle(visualLine); |
||||
} else { |
||||
return Rect.Empty; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Minimum distance of the caret to the view border.
|
||||
/// </summary>
|
||||
internal const double MinimumDistanceToViewBorder = 30; |
||||
|
||||
/// <summary>
|
||||
/// Scrolls the text view so that the caret is visible.
|
||||
/// </summary>
|
||||
public void BringCaretToView() |
||||
{ |
||||
BringCaretToView(MinimumDistanceToViewBorder); |
||||
} |
||||
|
||||
internal void BringCaretToView(double border) |
||||
{ |
||||
Rect caretRectangle = CalculateCaretRectangle(); |
||||
if (!caretRectangle.IsEmpty) { |
||||
caretRectangle.Inflate(border, border); |
||||
textView.MakeVisible(caretRectangle); |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Makes the caret visible and updates its on-screen position.
|
||||
/// </summary>
|
||||
public void Show() |
||||
{ |
||||
Log("Caret.Show()"); |
||||
visible = true; |
||||
if (!showScheduled) { |
||||
showScheduled = true; |
||||
textArea.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(ShowInternal)); |
||||
} |
||||
} |
||||
|
||||
bool showScheduled; |
||||
bool hasWin32Caret; |
||||
|
||||
void ShowInternal() |
||||
{ |
||||
showScheduled = false; |
||||
|
||||
// if show was scheduled but caret hidden in the meantime
|
||||
if (!visible) |
||||
return; |
||||
|
||||
if (caretAdorner != null && textView != null) { |
||||
VisualLine visualLine = textView.GetVisualLine(position.Line); |
||||
if (visualLine != null) { |
||||
Rect caretRect = this.textArea.OverstrikeMode ? CalcCaretOverstrikeRectangle(visualLine) : CalcCaretRectangle(visualLine); |
||||
// Create Win32 caret so that Windows knows where our managed caret is. This is necessary for
|
||||
// features like 'Follow text editing' in the Windows Magnifier.
|
||||
if (!hasWin32Caret) { |
||||
hasWin32Caret = Win32.CreateCaret(textView, caretRect.Size); |
||||
} |
||||
if (hasWin32Caret) { |
||||
Win32.SetCaretPosition(textView, caretRect.Location - textView.ScrollOffset); |
||||
} |
||||
caretAdorner.Show(caretRect); |
||||
textArea.ime.UpdateCompositionWindow(); |
||||
} else { |
||||
caretAdorner.Hide(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Makes the caret invisible.
|
||||
/// </summary>
|
||||
public void Hide() |
||||
{ |
||||
Log("Caret.Hide()"); |
||||
visible = false; |
||||
if (hasWin32Caret) { |
||||
Win32.DestroyCaret(); |
||||
hasWin32Caret = false; |
||||
} |
||||
if (caretAdorner != null) { |
||||
caretAdorner.Hide(); |
||||
} |
||||
} |
||||
|
||||
[Conditional("DEBUG")] |
||||
static void Log(string text) |
||||
{ |
||||
// commented out to make debug output less noisy - add back if there are any problems with the caret
|
||||
//Debug.WriteLine(text);
|
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets/Sets the color of the caret.
|
||||
/// </summary>
|
||||
public Brush CaretBrush { |
||||
get { return caretAdorner.CaretBrush; } |
||||
set { caretAdorner.CaretBrush = value; } |
||||
} |
||||
} |
||||
} |
@ -1,115 +0,0 @@
@@ -1,115 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Windows; |
||||
using System.Windows.Controls; |
||||
using System.Windows.Media; |
||||
using System.Windows.Media.Animation; |
||||
using System.Windows.Threading; |
||||
|
||||
using ICSharpCode.AvalonEdit.Rendering; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Editing |
||||
{ |
||||
sealed class CaretLayer : Layer |
||||
{ |
||||
TextArea textArea; |
||||
|
||||
bool isVisible; |
||||
Rect caretRectangle; |
||||
|
||||
DispatcherTimer caretBlinkTimer = new DispatcherTimer(); |
||||
bool blink; |
||||
|
||||
public CaretLayer(TextArea textArea) : base(textArea.TextView, KnownLayer.Caret) |
||||
{ |
||||
this.textArea = textArea; |
||||
this.IsHitTestVisible = false; |
||||
caretBlinkTimer.Tick += new EventHandler(caretBlinkTimer_Tick); |
||||
} |
||||
|
||||
void caretBlinkTimer_Tick(object sender, EventArgs e) |
||||
{ |
||||
blink = !blink; |
||||
InvalidateVisual(); |
||||
} |
||||
|
||||
public void Show(Rect caretRectangle) |
||||
{ |
||||
this.caretRectangle = caretRectangle; |
||||
this.isVisible = true; |
||||
StartBlinkAnimation(); |
||||
InvalidateVisual(); |
||||
} |
||||
|
||||
public void Hide() |
||||
{ |
||||
if (isVisible) { |
||||
isVisible = false; |
||||
StopBlinkAnimation(); |
||||
InvalidateVisual(); |
||||
} |
||||
} |
||||
|
||||
void StartBlinkAnimation() |
||||
{ |
||||
TimeSpan blinkTime = Win32.CaretBlinkTime; |
||||
blink = true; // the caret should visible initially
|
||||
// This is important if blinking is disabled (system reports a negative blinkTime)
|
||||
if (blinkTime.TotalMilliseconds > 0) { |
||||
caretBlinkTimer.Interval = blinkTime; |
||||
caretBlinkTimer.Start(); |
||||
} |
||||
} |
||||
|
||||
void StopBlinkAnimation() |
||||
{ |
||||
caretBlinkTimer.Stop(); |
||||
} |
||||
|
||||
internal Brush CaretBrush; |
||||
|
||||
protected override void OnRender(DrawingContext drawingContext) |
||||
{ |
||||
base.OnRender(drawingContext); |
||||
if (isVisible && blink) { |
||||
Brush caretBrush = this.CaretBrush; |
||||
if (caretBrush == null) |
||||
caretBrush = (Brush)textView.GetValue(TextBlock.ForegroundProperty); |
||||
|
||||
if (this.textArea.OverstrikeMode) { |
||||
SolidColorBrush scBrush = caretBrush as SolidColorBrush; |
||||
if (scBrush != null) { |
||||
Color brushColor = scBrush.Color; |
||||
Color newColor = Color.FromArgb(100, brushColor.R, brushColor.G, brushColor.B); |
||||
caretBrush = new SolidColorBrush(newColor); |
||||
caretBrush.Freeze(); |
||||
} |
||||
} |
||||
|
||||
Rect r = new Rect(caretRectangle.X - textView.HorizontalOffset, |
||||
caretRectangle.Y - textView.VerticalOffset, |
||||
caretRectangle.Width, |
||||
caretRectangle.Height); |
||||
drawingContext.DrawRectangle(caretBrush, null, PixelSnapHelpers.Round(r, PixelSnapHelpers.GetPixelSize(this))); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,392 +0,0 @@
@@ -1,392 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Diagnostics; |
||||
using System.Linq; |
||||
using System.Windows; |
||||
using System.Windows.Documents; |
||||
using System.Windows.Input; |
||||
using System.Windows.Media.TextFormatting; |
||||
using ICSharpCode.AvalonEdit.Document; |
||||
using ICSharpCode.AvalonEdit.Rendering; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Editing |
||||
{ |
||||
enum CaretMovementType |
||||
{ |
||||
None, |
||||
CharLeft, |
||||
CharRight, |
||||
Backspace, |
||||
WordLeft, |
||||
WordRight, |
||||
LineUp, |
||||
LineDown, |
||||
PageUp, |
||||
PageDown, |
||||
LineStart, |
||||
LineEnd, |
||||
DocumentStart, |
||||
DocumentEnd |
||||
} |
||||
|
||||
static class CaretNavigationCommandHandler |
||||
{ |
||||
/// <summary>
|
||||
/// Creates a new <see cref="TextAreaInputHandler"/> for the text area.
|
||||
/// </summary>
|
||||
public static TextAreaInputHandler Create(TextArea textArea) |
||||
{ |
||||
TextAreaInputHandler handler = new TextAreaInputHandler(textArea); |
||||
handler.CommandBindings.AddRange(CommandBindings); |
||||
handler.InputBindings.AddRange(InputBindings); |
||||
return handler; |
||||
} |
||||
|
||||
static readonly List<CommandBinding> CommandBindings = new List<CommandBinding>(); |
||||
static readonly List<InputBinding> InputBindings = new List<InputBinding>(); |
||||
|
||||
static void AddBinding(ICommand command, ModifierKeys modifiers, Key key, ExecutedRoutedEventHandler handler) |
||||
{ |
||||
CommandBindings.Add(new CommandBinding(command, handler)); |
||||
InputBindings.Add(TextAreaDefaultInputHandler.CreateFrozenKeyBinding(command, modifiers, key)); |
||||
} |
||||
|
||||
static CaretNavigationCommandHandler() |
||||
{ |
||||
const ModifierKeys None = ModifierKeys.None; |
||||
const ModifierKeys Ctrl = ModifierKeys.Control; |
||||
const ModifierKeys Shift = ModifierKeys.Shift; |
||||
const ModifierKeys Alt = ModifierKeys.Alt; |
||||
|
||||
AddBinding(EditingCommands.MoveLeftByCharacter, None, Key.Left, OnMoveCaret(CaretMovementType.CharLeft)); |
||||
AddBinding(EditingCommands.SelectLeftByCharacter, Shift, Key.Left, OnMoveCaretExtendSelection(CaretMovementType.CharLeft)); |
||||
AddBinding(RectangleSelection.BoxSelectLeftByCharacter, Alt | Shift, Key.Left, OnMoveCaretBoxSelection(CaretMovementType.CharLeft)); |
||||
AddBinding(EditingCommands.MoveRightByCharacter, None, Key.Right, OnMoveCaret(CaretMovementType.CharRight)); |
||||
AddBinding(EditingCommands.SelectRightByCharacter, Shift, Key.Right, OnMoveCaretExtendSelection(CaretMovementType.CharRight)); |
||||
AddBinding(RectangleSelection.BoxSelectRightByCharacter, Alt | Shift, Key.Right, OnMoveCaretBoxSelection(CaretMovementType.CharRight)); |
||||
|
||||
AddBinding(EditingCommands.MoveLeftByWord, Ctrl, Key.Left, OnMoveCaret(CaretMovementType.WordLeft)); |
||||
AddBinding(EditingCommands.SelectLeftByWord, Ctrl | Shift, Key.Left, OnMoveCaretExtendSelection(CaretMovementType.WordLeft)); |
||||
AddBinding(RectangleSelection.BoxSelectLeftByWord, Ctrl | Alt | Shift, Key.Left, OnMoveCaretBoxSelection(CaretMovementType.WordLeft)); |
||||
AddBinding(EditingCommands.MoveRightByWord, Ctrl, Key.Right, OnMoveCaret(CaretMovementType.WordRight)); |
||||
AddBinding(EditingCommands.SelectRightByWord, Ctrl | Shift, Key.Right, OnMoveCaretExtendSelection(CaretMovementType.WordRight)); |
||||
AddBinding(RectangleSelection.BoxSelectRightByWord, Ctrl | Alt | Shift, Key.Right, OnMoveCaretBoxSelection(CaretMovementType.WordRight)); |
||||
|
||||
AddBinding(EditingCommands.MoveUpByLine, None, Key.Up, OnMoveCaret(CaretMovementType.LineUp)); |
||||
AddBinding(EditingCommands.SelectUpByLine, Shift, Key.Up, OnMoveCaretExtendSelection(CaretMovementType.LineUp)); |
||||
AddBinding(RectangleSelection.BoxSelectUpByLine, Alt | Shift, Key.Up, OnMoveCaretBoxSelection(CaretMovementType.LineUp)); |
||||
AddBinding(EditingCommands.MoveDownByLine, None, Key.Down, OnMoveCaret(CaretMovementType.LineDown)); |
||||
AddBinding(EditingCommands.SelectDownByLine, Shift, Key.Down, OnMoveCaretExtendSelection(CaretMovementType.LineDown)); |
||||
AddBinding(RectangleSelection.BoxSelectDownByLine, Alt | Shift, Key.Down, OnMoveCaretBoxSelection(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(RectangleSelection.BoxSelectToLineStart, Alt | Shift, Key.Home, OnMoveCaretBoxSelection(CaretMovementType.LineStart)); |
||||
AddBinding(EditingCommands.MoveToLineEnd, None, Key.End, OnMoveCaret(CaretMovementType.LineEnd)); |
||||
AddBinding(EditingCommands.SelectToLineEnd, Shift, Key.End, OnMoveCaretExtendSelection(CaretMovementType.LineEnd)); |
||||
AddBinding(RectangleSelection.BoxSelectToLineEnd, Alt | Shift, Key.End, OnMoveCaretBoxSelection(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)); |
||||
|
||||
TextAreaDefaultInputHandler.WorkaroundWPFMemoryLeak(InputBindings); |
||||
} |
||||
|
||||
static void OnSelectAll(object target, ExecutedRoutedEventArgs args) |
||||
{ |
||||
TextArea textArea = GetTextArea(target); |
||||
if (textArea != null && textArea.Document != null) { |
||||
args.Handled = true; |
||||
textArea.Caret.Offset = textArea.Document.TextLength; |
||||
textArea.Selection = SimpleSelection.Create(textArea, 0, textArea.Document.TextLength); |
||||
} |
||||
} |
||||
|
||||
static TextArea GetTextArea(object target) |
||||
{ |
||||
return target as TextArea; |
||||
} |
||||
|
||||
static ExecutedRoutedEventHandler OnMoveCaret(CaretMovementType direction) |
||||
{ |
||||
return (target, args) => { |
||||
TextArea textArea = GetTextArea(target); |
||||
if (textArea != null && textArea.Document != null) { |
||||
args.Handled = true; |
||||
textArea.ClearSelection(); |
||||
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; |
||||
TextViewPosition oldPosition = textArea.Caret.Position; |
||||
MoveCaret(textArea, direction); |
||||
textArea.Selection = textArea.Selection.StartSelectionOrSetEndpoint(oldPosition, textArea.Caret.Position); |
||||
textArea.Caret.BringCaretToView(); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
static ExecutedRoutedEventHandler OnMoveCaretBoxSelection(CaretMovementType direction) |
||||
{ |
||||
return (target, args) => { |
||||
TextArea textArea = GetTextArea(target); |
||||
if (textArea != null && textArea.Document != null) { |
||||
args.Handled = true; |
||||
// First, convert the selection into a rectangle selection
|
||||
// (this is required so that virtual space gets enabled for the caret movement)
|
||||
if (textArea.Options.EnableRectangularSelection && !(textArea.Selection is RectangleSelection)) { |
||||
if (textArea.Selection.IsEmpty) { |
||||
textArea.Selection = new RectangleSelection(textArea, textArea.Caret.Position, textArea.Caret.Position); |
||||
} else { |
||||
// Convert normal selection to rectangle selection
|
||||
textArea.Selection = new RectangleSelection(textArea, textArea.Selection.StartPosition, textArea.Caret.Position); |
||||
} |
||||
} |
||||
// Now move the caret and extend the selection
|
||||
TextViewPosition oldPosition = textArea.Caret.Position; |
||||
MoveCaret(textArea, direction); |
||||
textArea.Selection = textArea.Selection.StartSelectionOrSetEndpoint(oldPosition, textArea.Caret.Position); |
||||
textArea.Caret.BringCaretToView(); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
#region Caret movement
|
||||
internal static void MoveCaret(TextArea textArea, CaretMovementType direction) |
||||
{ |
||||
double desiredXPos = textArea.Caret.DesiredXPos; |
||||
textArea.Caret.Position = GetNewCaretPosition(textArea.TextView, textArea.Caret.Position, direction, textArea.Selection.EnableVirtualSpace, ref desiredXPos); |
||||
textArea.Caret.DesiredXPos = desiredXPos; |
||||
} |
||||
|
||||
internal static TextViewPosition GetNewCaretPosition(TextView textView, TextViewPosition caretPosition, CaretMovementType direction, bool enableVirtualSpace, ref double desiredXPos) |
||||
{ |
||||
switch (direction) { |
||||
case CaretMovementType.None: |
||||
return caretPosition; |
||||
case CaretMovementType.DocumentStart: |
||||
desiredXPos = double.NaN; |
||||
return new TextViewPosition(0, 0); |
||||
case CaretMovementType.DocumentEnd: |
||||
desiredXPos = double.NaN; |
||||
return new TextViewPosition(textView.Document.GetLocation(textView.Document.TextLength)); |
||||
} |
||||
DocumentLine caretLine = textView.Document.GetLineByNumber(caretPosition.Line); |
||||
VisualLine visualLine = textView.GetOrConstructVisualLine(caretLine); |
||||
TextLine textLine = visualLine.GetTextLine(caretPosition.VisualColumn, caretPosition.IsAtEndOfLine); |
||||
switch (direction) { |
||||
case CaretMovementType.CharLeft: |
||||
desiredXPos = double.NaN; |
||||
return GetPrevCaretPosition(textView, caretPosition, visualLine, CaretPositioningMode.Normal, enableVirtualSpace); |
||||
case CaretMovementType.Backspace: |
||||
desiredXPos = double.NaN; |
||||
return GetPrevCaretPosition(textView, caretPosition, visualLine, CaretPositioningMode.EveryCodepoint, enableVirtualSpace); |
||||
case CaretMovementType.CharRight: |
||||
desiredXPos = double.NaN; |
||||
return GetNextCaretPosition(textView, caretPosition, visualLine, CaretPositioningMode.Normal, enableVirtualSpace); |
||||
case CaretMovementType.WordLeft: |
||||
desiredXPos = double.NaN; |
||||
return GetPrevCaretPosition(textView, caretPosition, visualLine, CaretPositioningMode.WordStart, enableVirtualSpace); |
||||
case CaretMovementType.WordRight: |
||||
desiredXPos = double.NaN; |
||||
return GetNextCaretPosition(textView, caretPosition, visualLine, CaretPositioningMode.WordStart, enableVirtualSpace); |
||||
case CaretMovementType.LineUp: |
||||
case CaretMovementType.LineDown: |
||||
case CaretMovementType.PageUp: |
||||
case CaretMovementType.PageDown: |
||||
return GetUpDownCaretPosition(textView, caretPosition, direction, visualLine, textLine, enableVirtualSpace, ref desiredXPos); |
||||
case CaretMovementType.LineStart: |
||||
desiredXPos = double.NaN; |
||||
return GetStartOfLineCaretPosition(caretPosition.VisualColumn, visualLine, textLine, enableVirtualSpace); |
||||
case CaretMovementType.LineEnd: |
||||
desiredXPos = double.NaN; |
||||
return GetEndOfLineCaretPosition(visualLine, textLine); |
||||
default: |
||||
throw new NotSupportedException(direction.ToString()); |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
#region Home/End
|
||||
static TextViewPosition GetStartOfLineCaretPosition(int oldVC, VisualLine visualLine, TextLine textLine, bool enableVirtualSpace) |
||||
{ |
||||
int newVC = visualLine.GetTextLineVisualStartColumn(textLine); |
||||
if (newVC == 0) |
||||
newVC = visualLine.GetNextCaretPosition(newVC - 1, LogicalDirection.Forward, CaretPositioningMode.WordStart, enableVirtualSpace); |
||||
if (newVC < 0) |
||||
throw ThrowUtil.NoValidCaretPosition(); |
||||
// when the caret is already at the start of the text, jump to start before whitespace
|
||||
if (newVC == oldVC) |
||||
newVC = 0; |
||||
return visualLine.GetTextViewPosition(newVC); |
||||
} |
||||
|
||||
static TextViewPosition GetEndOfLineCaretPosition(VisualLine visualLine, TextLine textLine) |
||||
{ |
||||
int newVC = visualLine.GetTextLineVisualStartColumn(textLine) + textLine.Length - textLine.TrailingWhitespaceLength; |
||||
TextViewPosition pos = visualLine.GetTextViewPosition(newVC); |
||||
pos.IsAtEndOfLine = true; |
||||
return pos; |
||||
} |
||||
#endregion
|
||||
|
||||
#region By-character / By-word movement
|
||||
static TextViewPosition GetNextCaretPosition(TextView textView, TextViewPosition caretPosition, VisualLine visualLine, CaretPositioningMode mode, bool enableVirtualSpace) |
||||
{ |
||||
int pos = visualLine.GetNextCaretPosition(caretPosition.VisualColumn, LogicalDirection.Forward, mode, enableVirtualSpace); |
||||
if (pos >= 0) { |
||||
return visualLine.GetTextViewPosition(pos); |
||||
} else { |
||||
// move to start of next line
|
||||
DocumentLine nextDocumentLine = visualLine.LastDocumentLine.NextLine; |
||||
if (nextDocumentLine != null) { |
||||
VisualLine nextLine = textView.GetOrConstructVisualLine(nextDocumentLine); |
||||
pos = nextLine.GetNextCaretPosition(-1, LogicalDirection.Forward, mode, enableVirtualSpace); |
||||
if (pos < 0) |
||||
throw ThrowUtil.NoValidCaretPosition(); |
||||
return nextLine.GetTextViewPosition(pos); |
||||
} else { |
||||
// at end of document
|
||||
Debug.Assert(visualLine.LastDocumentLine.Offset + visualLine.LastDocumentLine.TotalLength == textView.Document.TextLength); |
||||
return new TextViewPosition(textView.Document.GetLocation(textView.Document.TextLength)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
static TextViewPosition GetPrevCaretPosition(TextView textView, TextViewPosition caretPosition, VisualLine visualLine, CaretPositioningMode mode, bool enableVirtualSpace) |
||||
{ |
||||
int pos = visualLine.GetNextCaretPosition(caretPosition.VisualColumn, LogicalDirection.Backward, mode, enableVirtualSpace); |
||||
if (pos >= 0) { |
||||
return visualLine.GetTextViewPosition(pos); |
||||
} else { |
||||
// move to end of previous line
|
||||
DocumentLine previousDocumentLine = visualLine.FirstDocumentLine.PreviousLine; |
||||
if (previousDocumentLine != null) { |
||||
VisualLine previousLine = textView.GetOrConstructVisualLine(previousDocumentLine); |
||||
pos = previousLine.GetNextCaretPosition(previousLine.VisualLength + 1, LogicalDirection.Backward, mode, enableVirtualSpace); |
||||
if (pos < 0) |
||||
throw ThrowUtil.NoValidCaretPosition(); |
||||
return previousLine.GetTextViewPosition(pos); |
||||
} else { |
||||
// at start of document
|
||||
Debug.Assert(visualLine.FirstDocumentLine.Offset == 0); |
||||
return new TextViewPosition(0, 0); |
||||
} |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
#region Line+Page up/down
|
||||
static TextViewPosition GetUpDownCaretPosition(TextView textView, TextViewPosition caretPosition, CaretMovementType direction, VisualLine visualLine, TextLine textLine, bool enableVirtualSpace, ref double xPos) |
||||
{ |
||||
// moving up/down happens using the desired visual X position
|
||||
if (double.IsNaN(xPos)) |
||||
xPos = visualLine.GetTextLineVisualXPosition(textLine, caretPosition.VisualColumn); |
||||
// 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 = textView.Document.GetLineByNumber(prevLineNumber); |
||||
targetVisualLine = 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 <= textView.Document.LineCount) { |
||||
DocumentLine nextLine = textView.Document.GetLineByNumber(nextLineNumber); |
||||
targetVisualLine = textView.GetOrConstructVisualLine(nextLine); |
||||
targetLine = targetVisualLine.TextLines[0]; |
||||
} else { |
||||
targetLine = null; |
||||
} |
||||
break; |
||||
} |
||||
case CaretMovementType.PageUp: |
||||
case CaretMovementType.PageDown: |
||||
{ |
||||
// Page up/down: find the target line using its visual position
|
||||
double yPos = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.LineMiddle); |
||||
if (direction == CaretMovementType.PageUp) |
||||
yPos -= textView.RenderSize.Height; |
||||
else |
||||
yPos += textView.RenderSize.Height; |
||||
DocumentLine newLine = textView.GetDocumentLineByVisualTop(yPos); |
||||
targetVisualLine = textView.GetOrConstructVisualLine(newLine); |
||||
targetLine = targetVisualLine.GetTextLineByVisualYPosition(yPos); |
||||
break; |
||||
} |
||||
default: |
||||
throw new NotSupportedException(direction.ToString()); |
||||
} |
||||
if (targetLine != null) { |
||||
double yPos = targetVisualLine.GetTextLineVisualYPosition(targetLine, VisualYPosition.LineMiddle); |
||||
int newVisualColumn = targetVisualLine.GetVisualColumn(new Point(xPos, yPos), enableVirtualSpace); |
||||
|
||||
// prevent wrapping to the next line; TODO: could 'IsAtEnd' help here?
|
||||
int targetLineStartCol = targetVisualLine.GetTextLineVisualStartColumn(targetLine); |
||||
if (newVisualColumn >= targetLineStartCol + targetLine.Length) { |
||||
if (newVisualColumn <= targetVisualLine.VisualLength) |
||||
newVisualColumn = targetLineStartCol + targetLine.Length - 1; |
||||
} |
||||
return targetVisualLine.GetTextViewPosition(newVisualColumn); |
||||
} else { |
||||
return caretPosition; |
||||
} |
||||
} |
||||
#endregion
|
||||
} |
||||
} |
@ -1,48 +0,0 @@
@@ -1,48 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
using System; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Editing |
||||
{ |
||||
/// <summary>
|
||||
/// Contains classes for handling weak events on the Caret class.
|
||||
/// </summary>
|
||||
public static class CaretWeakEventManager |
||||
{ |
||||
/// <summary>
|
||||
/// Handles the Caret.PositionChanged event.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")] |
||||
public sealed class PositionChanged : WeakEventManagerBase<PositionChanged, Caret> |
||||
{ |
||||
/// <inheritdoc/>
|
||||
protected override void StartListening(Caret source) |
||||
{ |
||||
source.PositionChanged += DeliverEvent; |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void StopListening(Caret source) |
||||
{ |
||||
source.PositionChanged -= DeliverEvent; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,78 +0,0 @@
@@ -1,78 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Windows; |
||||
using System.Windows.Data; |
||||
using System.Windows.Media; |
||||
using System.Windows.Shapes; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Editing |
||||
{ |
||||
/// <summary>
|
||||
/// Margin for use with the text area.
|
||||
/// A vertical dotted line to separate the line numbers from the text view.
|
||||
/// </summary>
|
||||
public static class DottedLineMargin |
||||
{ |
||||
static readonly object tag = new object(); |
||||
|
||||
/// <summary>
|
||||
/// Creates a vertical dotted line to separate the line numbers from the text view.
|
||||
/// </summary>
|
||||
public static UIElement Create() |
||||
{ |
||||
Line line = new Line { |
||||
X1 = 0, Y1 = 0, X2 = 0, Y2 = 1, |
||||
StrokeDashArray = { 0, 2 }, |
||||
Stretch = Stretch.Fill, |
||||
StrokeThickness = 1, |
||||
StrokeDashCap = PenLineCap.Round, |
||||
Margin = new Thickness(2, 0, 2, 0), |
||||
Tag = tag |
||||
}; |
||||
|
||||
return line; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Creates a vertical dotted line to separate the line numbers from the text view.
|
||||
/// </summary>
|
||||
[Obsolete("This method got published accidentally; and will be removed again in a future version. Use the parameterless overload instead.")] |
||||
public static UIElement Create(TextEditor editor) |
||||
{ |
||||
Line line = (Line)Create(); |
||||
|
||||
line.SetBinding( |
||||
Line.StrokeProperty, |
||||
new Binding("LineNumbersForeground") { Source = editor } |
||||
); |
||||
|
||||
return line; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets whether the specified UIElement is the result of a DottedLineMargin.Create call.
|
||||
/// </summary>
|
||||
public static bool IsDottedLineMargin(UIElement element) |
||||
{ |
||||
Line l = element as Line; |
||||
return l != null && l.Tag == tag; |
||||
} |
||||
} |
||||
} |
@ -1,61 +0,0 @@
@@ -1,61 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Runtime.Serialization; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Editing |
||||
{ |
||||
/// <summary>
|
||||
/// Wraps exceptions that occur during drag'n'drop.
|
||||
/// Exceptions during drag'n'drop might
|
||||
/// get swallowed by WPF/COM, so AvalonEdit catches them and re-throws them later
|
||||
/// wrapped in a DragDropException.
|
||||
/// </summary>
|
||||
[Serializable()] |
||||
public class DragDropException : Exception |
||||
{ |
||||
/// <summary>
|
||||
/// Creates a new DragDropException.
|
||||
/// </summary>
|
||||
public DragDropException() : base() |
||||
{ |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Creates a new DragDropException.
|
||||
/// </summary>
|
||||
public DragDropException(string message) : base(message) |
||||
{ |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Creates a new DragDropException.
|
||||
/// </summary>
|
||||
public DragDropException(string message, Exception innerException) : base(message, innerException) |
||||
{ |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Deserializes a DragDropException.
|
||||
/// </summary>
|
||||
protected DragDropException(SerializationInfo info, StreamingContext context) : base(info, context) |
||||
{ |
||||
} |
||||
} |
||||
} |
@ -1,643 +0,0 @@
@@ -1,643 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Diagnostics; |
||||
using System.Globalization; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Runtime.InteropServices; |
||||
using System.Windows; |
||||
using System.Windows.Documents; |
||||
using System.Windows.Input; |
||||
using ICSharpCode.AvalonEdit.Document; |
||||
using ICSharpCode.AvalonEdit.Highlighting; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory.Editor; |
||||
#endif
|
||||
|
||||
namespace ICSharpCode.AvalonEdit.Editing |
||||
{ |
||||
/// <summary>
|
||||
/// We re-use the CommandBinding and InputBinding instances between multiple text areas,
|
||||
/// so this class is static.
|
||||
/// </summary>
|
||||
static class EditingCommandHandler |
||||
{ |
||||
/// <summary>
|
||||
/// Creates a new <see cref="TextAreaInputHandler"/> for the text area.
|
||||
/// </summary>
|
||||
public static TextAreaInputHandler Create(TextArea textArea) |
||||
{ |
||||
TextAreaInputHandler handler = new TextAreaInputHandler(textArea); |
||||
handler.CommandBindings.AddRange(CommandBindings); |
||||
handler.InputBindings.AddRange(InputBindings); |
||||
return handler; |
||||
} |
||||
|
||||
static readonly List<CommandBinding> CommandBindings = new List<CommandBinding>(); |
||||
static readonly List<InputBinding> InputBindings = new List<InputBinding>(); |
||||
|
||||
static void AddBinding(ICommand command, ModifierKeys modifiers, Key key, ExecutedRoutedEventHandler handler) |
||||
{ |
||||
CommandBindings.Add(new CommandBinding(command, handler)); |
||||
InputBindings.Add(TextAreaDefaultInputHandler.CreateFrozenKeyBinding(command, modifiers, key)); |
||||
} |
||||
|
||||
static EditingCommandHandler() |
||||
{ |
||||
CommandBindings.Add(new CommandBinding(ApplicationCommands.Delete, OnDelete(CaretMovementType.None), CanDelete)); |
||||
AddBinding(EditingCommands.Delete, ModifierKeys.None, Key.Delete, OnDelete(CaretMovementType.CharRight)); |
||||
AddBinding(EditingCommands.DeleteNextWord, ModifierKeys.Control, Key.Delete, OnDelete(CaretMovementType.WordRight)); |
||||
AddBinding(EditingCommands.Backspace, ModifierKeys.None, Key.Back, OnDelete(CaretMovementType.Backspace)); |
||||
InputBindings.Add(TextAreaDefaultInputHandler.CreateFrozenKeyBinding(EditingCommands.Backspace, ModifierKeys.Shift, Key.Back)); // make Shift-Backspace do the same as plain backspace
|
||||
AddBinding(EditingCommands.DeletePreviousWord, ModifierKeys.Control, Key.Back, OnDelete(CaretMovementType.WordLeft)); |
||||
AddBinding(EditingCommands.EnterParagraphBreak, ModifierKeys.None, Key.Enter, OnEnter); |
||||
AddBinding(EditingCommands.EnterLineBreak, ModifierKeys.Shift, Key.Enter, OnEnter); |
||||
AddBinding(EditingCommands.TabForward, ModifierKeys.None, Key.Tab, OnTab); |
||||
AddBinding(EditingCommands.TabBackward, ModifierKeys.Shift, Key.Tab, OnShiftTab); |
||||
|
||||
CommandBindings.Add(new CommandBinding(ApplicationCommands.Copy, OnCopy, CanCutOrCopy)); |
||||
CommandBindings.Add(new CommandBinding(ApplicationCommands.Cut, OnCut, CanCutOrCopy)); |
||||
CommandBindings.Add(new CommandBinding(ApplicationCommands.Paste, OnPaste, CanPaste)); |
||||
|
||||
CommandBindings.Add(new CommandBinding(AvalonEditCommands.DeleteLine, OnDeleteLine)); |
||||
|
||||
CommandBindings.Add(new CommandBinding(AvalonEditCommands.RemoveLeadingWhitespace, OnRemoveLeadingWhitespace)); |
||||
CommandBindings.Add(new CommandBinding(AvalonEditCommands.RemoveTrailingWhitespace, OnRemoveTrailingWhitespace)); |
||||
CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertToUppercase, OnConvertToUpperCase)); |
||||
CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertToLowercase, OnConvertToLowerCase)); |
||||
CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertToTitleCase, OnConvertToTitleCase)); |
||||
CommandBindings.Add(new CommandBinding(AvalonEditCommands.InvertCase, OnInvertCase)); |
||||
CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertTabsToSpaces, OnConvertTabsToSpaces)); |
||||
CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertSpacesToTabs, OnConvertSpacesToTabs)); |
||||
CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertLeadingTabsToSpaces, OnConvertLeadingTabsToSpaces)); |
||||
CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertLeadingSpacesToTabs, OnConvertLeadingSpacesToTabs)); |
||||
CommandBindings.Add(new CommandBinding(AvalonEditCommands.IndentSelection, OnIndentSelection)); |
||||
|
||||
TextAreaDefaultInputHandler.WorkaroundWPFMemoryLeak(InputBindings); |
||||
} |
||||
|
||||
static TextArea GetTextArea(object target) |
||||
{ |
||||
return target as TextArea; |
||||
} |
||||
|
||||
#region Text Transformation Helpers
|
||||
enum DefaultSegmentType |
||||
{ |
||||
None, |
||||
WholeDocument, |
||||
CurrentLine |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Calls transformLine on all lines in the selected range.
|
||||
/// transformLine needs to handle read-only segments!
|
||||
/// </summary>
|
||||
static void TransformSelectedLines(Action<TextArea, DocumentLine> transformLine, object target, ExecutedRoutedEventArgs args, DefaultSegmentType defaultSegmentType) |
||||
{ |
||||
TextArea textArea = GetTextArea(target); |
||||
if (textArea != null && textArea.Document != null) { |
||||
using (textArea.Document.RunUpdate()) { |
||||
DocumentLine start, end; |
||||
if (textArea.Selection.IsEmpty) { |
||||
if (defaultSegmentType == DefaultSegmentType.CurrentLine) { |
||||
start = end = textArea.Document.GetLineByNumber(textArea.Caret.Line); |
||||
} else if (defaultSegmentType == DefaultSegmentType.WholeDocument) { |
||||
start = textArea.Document.Lines.First(); |
||||
end = textArea.Document.Lines.Last(); |
||||
} else { |
||||
start = end = null; |
||||
} |
||||
} else { |
||||
ISegment segment = textArea.Selection.SurroundingSegment; |
||||
start = textArea.Document.GetLineByOffset(segment.Offset); |
||||
end = textArea.Document.GetLineByOffset(segment.EndOffset); |
||||
// don't include the last line if no characters on it are selected
|
||||
if (start != end && end.Offset == segment.EndOffset) |
||||
end = end.PreviousLine; |
||||
} |
||||
if (start != null) { |
||||
transformLine(textArea, start); |
||||
while (start != end) { |
||||
start = start.NextLine; |
||||
transformLine(textArea, start); |
||||
} |
||||
} |
||||
} |
||||
textArea.Caret.BringCaretToView(); |
||||
args.Handled = true; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Calls transformLine on all writable segment in the selected range.
|
||||
/// </summary>
|
||||
static void TransformSelectedSegments(Action<TextArea, ISegment> transformSegment, object target, ExecutedRoutedEventArgs args, DefaultSegmentType defaultSegmentType) |
||||
{ |
||||
TextArea textArea = GetTextArea(target); |
||||
if (textArea != null && textArea.Document != null) { |
||||
using (textArea.Document.RunUpdate()) { |
||||
IEnumerable<ISegment> segments; |
||||
if (textArea.Selection.IsEmpty) { |
||||
if (defaultSegmentType == DefaultSegmentType.CurrentLine) { |
||||
segments = new ISegment[] { textArea.Document.GetLineByNumber(textArea.Caret.Line) }; |
||||
} else if (defaultSegmentType == DefaultSegmentType.WholeDocument) { |
||||
segments = textArea.Document.Lines.Cast<ISegment>(); |
||||
} else { |
||||
segments = null; |
||||
} |
||||
} else { |
||||
segments = textArea.Selection.Segments.Cast<ISegment>(); |
||||
} |
||||
if (segments != null) { |
||||
foreach (ISegment segment in segments.Reverse()) { |
||||
foreach (ISegment writableSegment in textArea.GetDeletableSegments(segment).Reverse()) { |
||||
transformSegment(textArea, writableSegment); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
textArea.Caret.BringCaretToView(); |
||||
args.Handled = true; |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
#region EnterLineBreak
|
||||
static void OnEnter(object target, ExecutedRoutedEventArgs args) |
||||
{ |
||||
TextArea textArea = GetTextArea(target); |
||||
if (textArea != null && textArea.IsKeyboardFocused) { |
||||
textArea.PerformTextInput("\n"); |
||||
args.Handled = true; |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
#region Tab
|
||||
static void OnTab(object target, ExecutedRoutedEventArgs args) |
||||
{ |
||||
TextArea textArea = GetTextArea(target); |
||||
if (textArea != null && textArea.Document != null) { |
||||
using (textArea.Document.RunUpdate()) { |
||||
if (textArea.Selection.IsMultiline) { |
||||
var segment = textArea.Selection.SurroundingSegment; |
||||
DocumentLine start = textArea.Document.GetLineByOffset(segment.Offset); |
||||
DocumentLine end = textArea.Document.GetLineByOffset(segment.EndOffset); |
||||
// don't include the last line if no characters on it are selected
|
||||
if (start != end && end.Offset == segment.EndOffset) |
||||
end = end.PreviousLine; |
||||
DocumentLine current = start; |
||||
while (true) { |
||||
int offset = current.Offset; |
||||
if (textArea.ReadOnlySectionProvider.CanInsert(offset)) |
||||
textArea.Document.Replace(offset, 0, textArea.Options.IndentationString, OffsetChangeMappingType.KeepAnchorBeforeInsertion); |
||||
if (current == end) |
||||
break; |
||||
current = current.NextLine; |
||||
} |
||||
} else { |
||||
string indentationString = textArea.Options.GetIndentationString(textArea.Caret.Column); |
||||
textArea.ReplaceSelectionWithText(indentationString); |
||||
} |
||||
} |
||||
textArea.Caret.BringCaretToView(); |
||||
args.Handled = true; |
||||
} |
||||
} |
||||
|
||||
static void OnShiftTab(object target, ExecutedRoutedEventArgs args) |
||||
{ |
||||
TransformSelectedLines( |
||||
delegate (TextArea textArea, DocumentLine line) { |
||||
int offset = line.Offset; |
||||
ISegment s = TextUtilities.GetSingleIndentationSegment(textArea.Document, offset, textArea.Options.IndentationSize); |
||||
if (s.Length > 0) { |
||||
s = textArea.GetDeletableSegments(s).FirstOrDefault(); |
||||
if (s != null && s.Length > 0) { |
||||
textArea.Document.Remove(s.Offset, s.Length); |
||||
} |
||||
} |
||||
}, target, args, DefaultSegmentType.CurrentLine); |
||||
} |
||||
#endregion
|
||||
|
||||
#region Delete
|
||||
static ExecutedRoutedEventHandler OnDelete(CaretMovementType caretMovement) |
||||
{ |
||||
return (target, args) => { |
||||
TextArea textArea = GetTextArea(target); |
||||
if (textArea != null && textArea.Document != null) { |
||||
if (textArea.Selection.IsEmpty) { |
||||
TextViewPosition startPos = textArea.Caret.Position; |
||||
bool enableVirtualSpace = textArea.Options.EnableVirtualSpace; |
||||
// When pressing delete; don't move the caret further into virtual space - instead delete the newline
|
||||
if (caretMovement == CaretMovementType.CharRight) |
||||
enableVirtualSpace = false; |
||||
double desiredXPos = textArea.Caret.DesiredXPos; |
||||
TextViewPosition endPos = CaretNavigationCommandHandler.GetNewCaretPosition( |
||||
textArea.TextView, startPos, caretMovement, enableVirtualSpace, ref desiredXPos); |
||||
// GetNewCaretPosition may return (0,0) as new position,
|
||||
// thus we need to validate endPos before using it in the selection.
|
||||
if (endPos.Line < 1 || endPos.Column < 1) |
||||
endPos = new TextViewPosition(Math.Max(endPos.Line, 1), Math.Max(endPos.Column, 1)); |
||||
// Don't select the text to be deleted; just reuse the ReplaceSelectionWithText logic
|
||||
var sel = new SimpleSelection(textArea, startPos, endPos); |
||||
sel.ReplaceSelectionWithText(string.Empty); |
||||
} else { |
||||
textArea.RemoveSelectedText(); |
||||
} |
||||
textArea.Caret.BringCaretToView(); |
||||
args.Handled = true; |
||||
} |
||||
}; |
||||
} |
||||
|
||||
static void CanDelete(object target, CanExecuteRoutedEventArgs args) |
||||
{ |
||||
// HasSomethingSelected for delete command
|
||||
TextArea textArea = GetTextArea(target); |
||||
if (textArea != null && textArea.Document != null) { |
||||
args.CanExecute = !textArea.Selection.IsEmpty; |
||||
args.Handled = true; |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
#region Clipboard commands
|
||||
static void CanCutOrCopy(object target, CanExecuteRoutedEventArgs args) |
||||
{ |
||||
// HasSomethingSelected for copy and cut commands
|
||||
TextArea textArea = GetTextArea(target); |
||||
if (textArea != null && textArea.Document != null) { |
||||
args.CanExecute = textArea.Options.CutCopyWholeLine || !textArea.Selection.IsEmpty; |
||||
args.Handled = true; |
||||
} |
||||
} |
||||
|
||||
static void OnCopy(object target, ExecutedRoutedEventArgs args) |
||||
{ |
||||
TextArea textArea = GetTextArea(target); |
||||
if (textArea != null && textArea.Document != null) { |
||||
if (textArea.Selection.IsEmpty && textArea.Options.CutCopyWholeLine) { |
||||
DocumentLine currentLine = textArea.Document.GetLineByNumber(textArea.Caret.Line); |
||||
CopyWholeLine(textArea, currentLine); |
||||
} else { |
||||
CopySelectedText(textArea); |
||||
} |
||||
args.Handled = true; |
||||
} |
||||
} |
||||
|
||||
static void OnCut(object target, ExecutedRoutedEventArgs args) |
||||
{ |
||||
TextArea textArea = GetTextArea(target); |
||||
if (textArea != null && textArea.Document != null) { |
||||
if (textArea.Selection.IsEmpty && textArea.Options.CutCopyWholeLine) { |
||||
DocumentLine currentLine = textArea.Document.GetLineByNumber(textArea.Caret.Line); |
||||
if (CopyWholeLine(textArea, currentLine)) { |
||||
ISegment[] segmentsToDelete = textArea.GetDeletableSegments(new SimpleSegment(currentLine.Offset, currentLine.TotalLength)); |
||||
for (int i = segmentsToDelete.Length - 1; i >= 0; i--) { |
||||
textArea.Document.Remove(segmentsToDelete[i]); |
||||
} |
||||
} |
||||
} else { |
||||
if (CopySelectedText(textArea)) |
||||
textArea.RemoveSelectedText(); |
||||
} |
||||
textArea.Caret.BringCaretToView(); |
||||
args.Handled = true; |
||||
} |
||||
} |
||||
|
||||
static bool CopySelectedText(TextArea textArea) |
||||
{ |
||||
var data = textArea.Selection.CreateDataObject(textArea); |
||||
var copyingEventArgs = new DataObjectCopyingEventArgs(data, false); |
||||
textArea.RaiseEvent(copyingEventArgs); |
||||
if (copyingEventArgs.CommandCancelled) |
||||
return false; |
||||
|
||||
try { |
||||
Clipboard.SetDataObject(data, true); |
||||
} catch (ExternalException) { |
||||
// Apparently this exception sometimes happens randomly.
|
||||
// The MS controls just ignore it, so we'll do the same.
|
||||
} |
||||
|
||||
string text = textArea.Selection.GetText(); |
||||
text = TextUtilities.NormalizeNewLines(text, Environment.NewLine); |
||||
textArea.OnTextCopied(new TextEventArgs(text)); |
||||
return true; |
||||
} |
||||
|
||||
const string LineSelectedType = "MSDEVLineSelect"; // This is the type VS 2003 and 2005 use for flagging a whole line copy
|
||||
|
||||
public static bool ConfirmDataFormat(TextArea textArea, DataObject dataObject, string format) |
||||
{ |
||||
var e = new DataObjectSettingDataEventArgs(dataObject, format); |
||||
textArea.RaiseEvent(e); |
||||
return !e.CommandCancelled; |
||||
} |
||||
|
||||
static bool CopyWholeLine(TextArea textArea, DocumentLine line) |
||||
{ |
||||
ISegment wholeLine = new SimpleSegment(line.Offset, line.TotalLength); |
||||
string text = textArea.Document.GetText(wholeLine); |
||||
// Ensure we use the appropriate newline sequence for the OS
|
||||
text = TextUtilities.NormalizeNewLines(text, Environment.NewLine); |
||||
DataObject data = new DataObject(); |
||||
if (ConfirmDataFormat(textArea, data, DataFormats.UnicodeText)) |
||||
data.SetText(text); |
||||
|
||||
// Also copy text in HTML format to clipboard - good for pasting text into Word
|
||||
// or to the SharpDevelop forums.
|
||||
if (ConfirmDataFormat(textArea, data, DataFormats.Html)) { |
||||
IHighlighter highlighter = textArea.GetService(typeof(IHighlighter)) as IHighlighter; |
||||
HtmlClipboard.SetHtml(data, HtmlClipboard.CreateHtmlFragment(textArea.Document, highlighter, wholeLine, new HtmlOptions(textArea.Options))); |
||||
} |
||||
|
||||
if (ConfirmDataFormat(textArea, data, LineSelectedType)) { |
||||
MemoryStream lineSelected = new MemoryStream(1); |
||||
lineSelected.WriteByte(1); |
||||
data.SetData(LineSelectedType, lineSelected, false); |
||||
} |
||||
|
||||
var copyingEventArgs = new DataObjectCopyingEventArgs(data, false); |
||||
textArea.RaiseEvent(copyingEventArgs); |
||||
if (copyingEventArgs.CommandCancelled) |
||||
return false; |
||||
|
||||
try { |
||||
Clipboard.SetDataObject(data, true); |
||||
} catch (ExternalException) { |
||||
// Apparently this exception sometimes happens randomly.
|
||||
// The MS controls just ignore it, so we'll do the same.
|
||||
return false; |
||||
} |
||||
textArea.OnTextCopied(new TextEventArgs(text)); |
||||
return 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(); |
||||
// WPF Clipboard.ContainsText() is safe to call without catching ExternalExceptions
|
||||
// because it doesn't try to lock the clipboard - it just peeks inside with IsClipboardFormatAvailable().
|
||||
args.Handled = true; |
||||
} |
||||
} |
||||
|
||||
static void OnPaste(object target, ExecutedRoutedEventArgs args) |
||||
{ |
||||
TextArea textArea = GetTextArea(target); |
||||
if (textArea != null && textArea.Document != null) { |
||||
IDataObject dataObject; |
||||
try { |
||||
dataObject = Clipboard.GetDataObject(); |
||||
} catch (ExternalException) { |
||||
return; |
||||
} |
||||
if (dataObject == null) |
||||
return; |
||||
Debug.WriteLine(dataObject.GetData(DataFormats.Html) as string); |
||||
|
||||
// convert text back to correct newlines for this document
|
||||
string newLine = TextUtilities.GetNewLineFromDocument(textArea.Document, textArea.Caret.Line); |
||||
string text; |
||||
try { |
||||
text = (string)dataObject.GetData(DataFormats.UnicodeText); |
||||
text = TextUtilities.NormalizeNewLines(text, newLine); |
||||
} catch (OutOfMemoryException) { |
||||
return; |
||||
} |
||||
|
||||
if (!string.IsNullOrEmpty(text)) { |
||||
bool fullLine = textArea.Options.CutCopyWholeLine && dataObject.GetDataPresent(LineSelectedType); |
||||
bool rectangular = dataObject.GetDataPresent(RectangleSelection.RectangularSelectionDataType); |
||||
|
||||
string pasteFormat; |
||||
// fill the suggested DataFormat used for the paste action:
|
||||
if (fullLine) |
||||
pasteFormat = LineSelectedType; |
||||
else if (rectangular && textArea.Selection.IsEmpty && !(textArea.Selection is RectangleSelection)) |
||||
pasteFormat = RectangleSelection.RectangularSelectionDataType; |
||||
else |
||||
pasteFormat = DataFormats.UnicodeText; |
||||
|
||||
var pastingEventArgs = new DataObjectPastingEventArgs(dataObject, false, pasteFormat); |
||||
textArea.RaiseEvent(pastingEventArgs); |
||||
if (pastingEventArgs.CommandCancelled) |
||||
return; |
||||
|
||||
// DataObject.PastingEvent handlers might have changed the format to apply.
|
||||
pasteFormat = pastingEventArgs.FormatToApply; |
||||
|
||||
fullLine = pasteFormat == LineSelectedType; |
||||
rectangular = pasteFormat == RectangleSelection.RectangularSelectionDataType; |
||||
|
||||
if (fullLine) { |
||||
DocumentLine currentLine = textArea.Document.GetLineByNumber(textArea.Caret.Line); |
||||
if (textArea.ReadOnlySectionProvider.CanInsert(currentLine.Offset)) { |
||||
textArea.Document.Insert(currentLine.Offset, text); |
||||
} |
||||
} else if (rectangular && textArea.Selection.IsEmpty && !(textArea.Selection is RectangleSelection)) { |
||||
if (!RectangleSelection.PerformRectangularPaste(textArea, textArea.Caret.Position, text, false)) |
||||
textArea.ReplaceSelectionWithText(text); |
||||
} else { |
||||
textArea.ReplaceSelectionWithText(text); |
||||
} |
||||
} |
||||
textArea.Caret.BringCaretToView(); |
||||
args.Handled = true; |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
#region DeleteLine
|
||||
static void OnDeleteLine(object target, ExecutedRoutedEventArgs args) |
||||
{ |
||||
TextArea textArea = GetTextArea(target); |
||||
if (textArea != null && textArea.Document != null) { |
||||
int firstLineIndex, lastLineIndex; |
||||
if (textArea.Selection.Length == 0) { |
||||
// There is no selection, simply delete current line
|
||||
firstLineIndex = lastLineIndex = textArea.Caret.Line; |
||||
} else { |
||||
// There is a selection, remove all lines affected by it (use Min/Max to be independent from selection direction)
|
||||
firstLineIndex = Math.Min(textArea.Selection.StartPosition.Line, textArea.Selection.EndPosition.Line); |
||||
lastLineIndex = Math.Max(textArea.Selection.StartPosition.Line, textArea.Selection.EndPosition.Line); |
||||
} |
||||
DocumentLine startLine = textArea.Document.GetLineByNumber(firstLineIndex); |
||||
DocumentLine endLine = textArea.Document.GetLineByNumber(lastLineIndex); |
||||
textArea.Selection = Selection.Create(textArea, startLine.Offset, endLine.Offset + endLine.TotalLength); |
||||
textArea.RemoveSelectedText(); |
||||
args.Handled = true; |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
#region Remove..Whitespace / Convert Tabs-Spaces
|
||||
static void OnRemoveLeadingWhitespace(object target, ExecutedRoutedEventArgs args) |
||||
{ |
||||
TransformSelectedLines( |
||||
delegate (TextArea textArea, DocumentLine line) { |
||||
textArea.Document.Remove(TextUtilities.GetLeadingWhitespace(textArea.Document, line)); |
||||
}, target, args, DefaultSegmentType.WholeDocument); |
||||
} |
||||
|
||||
static void OnRemoveTrailingWhitespace(object target, ExecutedRoutedEventArgs args) |
||||
{ |
||||
TransformSelectedLines( |
||||
delegate (TextArea textArea, DocumentLine line) { |
||||
textArea.Document.Remove(TextUtilities.GetTrailingWhitespace(textArea.Document, line)); |
||||
}, target, args, DefaultSegmentType.WholeDocument); |
||||
} |
||||
|
||||
static void OnConvertTabsToSpaces(object target, ExecutedRoutedEventArgs args) |
||||
{ |
||||
TransformSelectedSegments(ConvertTabsToSpaces, target, args, DefaultSegmentType.WholeDocument); |
||||
} |
||||
|
||||
static void OnConvertLeadingTabsToSpaces(object target, ExecutedRoutedEventArgs args) |
||||
{ |
||||
TransformSelectedLines( |
||||
delegate (TextArea textArea, DocumentLine line) { |
||||
ConvertTabsToSpaces(textArea, TextUtilities.GetLeadingWhitespace(textArea.Document, line)); |
||||
}, target, args, DefaultSegmentType.WholeDocument); |
||||
} |
||||
|
||||
static void ConvertTabsToSpaces(TextArea textArea, ISegment segment) |
||||
{ |
||||
TextDocument document = textArea.Document; |
||||
int endOffset = segment.EndOffset; |
||||
string indentationString = new string(' ', textArea.Options.IndentationSize); |
||||
for (int offset = segment.Offset; offset < endOffset; offset++) { |
||||
if (document.GetCharAt(offset) == '\t') { |
||||
document.Replace(offset, 1, indentationString, OffsetChangeMappingType.CharacterReplace); |
||||
endOffset += indentationString.Length - 1; |
||||
} |
||||
} |
||||
} |
||||
|
||||
static void OnConvertSpacesToTabs(object target, ExecutedRoutedEventArgs args) |
||||
{ |
||||
TransformSelectedSegments(ConvertSpacesToTabs, target, args, DefaultSegmentType.WholeDocument); |
||||
} |
||||
|
||||
static void OnConvertLeadingSpacesToTabs(object target, ExecutedRoutedEventArgs args) |
||||
{ |
||||
TransformSelectedLines( |
||||
delegate (TextArea textArea, DocumentLine line) { |
||||
ConvertSpacesToTabs(textArea, TextUtilities.GetLeadingWhitespace(textArea.Document, line)); |
||||
}, target, args, DefaultSegmentType.WholeDocument); |
||||
} |
||||
|
||||
static void ConvertSpacesToTabs(TextArea textArea, ISegment segment) |
||||
{ |
||||
TextDocument document = textArea.Document; |
||||
int endOffset = segment.EndOffset; |
||||
int indentationSize = textArea.Options.IndentationSize; |
||||
int spacesCount = 0; |
||||
for (int offset = segment.Offset; offset < endOffset; offset++) { |
||||
if (document.GetCharAt(offset) == ' ') { |
||||
spacesCount++; |
||||
if (spacesCount == indentationSize) { |
||||
document.Replace(offset - (indentationSize - 1), indentationSize, "\t", OffsetChangeMappingType.CharacterReplace); |
||||
spacesCount = 0; |
||||
offset -= indentationSize - 1; |
||||
endOffset -= indentationSize - 1; |
||||
} |
||||
} else { |
||||
spacesCount = 0; |
||||
} |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
#region Convert...Case
|
||||
static void ConvertCase(Func<string, string> transformText, object target, ExecutedRoutedEventArgs args) |
||||
{ |
||||
TransformSelectedSegments( |
||||
delegate (TextArea textArea, ISegment segment) { |
||||
string oldText = textArea.Document.GetText(segment); |
||||
string newText = transformText(oldText); |
||||
textArea.Document.Replace(segment.Offset, segment.Length, newText, OffsetChangeMappingType.CharacterReplace); |
||||
}, target, args, DefaultSegmentType.WholeDocument); |
||||
} |
||||
|
||||
static void OnConvertToUpperCase(object target, ExecutedRoutedEventArgs args) |
||||
{ |
||||
ConvertCase(CultureInfo.CurrentCulture.TextInfo.ToUpper, target, args); |
||||
} |
||||
|
||||
static void OnConvertToLowerCase(object target, ExecutedRoutedEventArgs args) |
||||
{ |
||||
ConvertCase(CultureInfo.CurrentCulture.TextInfo.ToLower, target, args); |
||||
} |
||||
|
||||
static void OnConvertToTitleCase(object target, ExecutedRoutedEventArgs args) |
||||
{ |
||||
ConvertCase(CultureInfo.CurrentCulture.TextInfo.ToTitleCase, target, args); |
||||
} |
||||
|
||||
static void OnInvertCase(object target, ExecutedRoutedEventArgs args) |
||||
{ |
||||
ConvertCase(InvertCase, target, args); |
||||
} |
||||
|
||||
static string InvertCase(string text) |
||||
{ |
||||
CultureInfo culture = CultureInfo.CurrentCulture; |
||||
char[] buffer = text.ToCharArray(); |
||||
for (int i = 0; i < buffer.Length; ++i) { |
||||
char c = buffer[i]; |
||||
buffer[i] = char.IsUpper(c) ? char.ToLower(c, culture) : char.ToUpper(c, culture); |
||||
} |
||||
return new string(buffer); |
||||
} |
||||
#endregion
|
||||
|
||||
static void OnIndentSelection(object target, ExecutedRoutedEventArgs args) |
||||
{ |
||||
TextArea textArea = GetTextArea(target); |
||||
if (textArea != null && textArea.Document != null) { |
||||
using (textArea.Document.RunUpdate()) { |
||||
int start, end; |
||||
if (textArea.Selection.IsEmpty) { |
||||
start = 1; |
||||
end = textArea.Document.LineCount; |
||||
} else { |
||||
start = textArea.Document.GetLineByOffset(textArea.Selection.SurroundingSegment.Offset).LineNumber; |
||||
end = textArea.Document.GetLineByOffset(textArea.Selection.SurroundingSegment.EndOffset).LineNumber; |
||||
} |
||||
textArea.IndentationStrategy.IndentLines(textArea.Document, start, end); |
||||
} |
||||
textArea.Caret.BringCaretToView(); |
||||
args.Handled = true; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,105 +0,0 @@
@@ -1,105 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Runtime.CompilerServices; |
||||
|
||||
using ICSharpCode.AvalonEdit.Document; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory; |
||||
using ICSharpCode.NRefactory.Editor; |
||||
#endif
|
||||
|
||||
namespace ICSharpCode.AvalonEdit.Editing |
||||
{ |
||||
sealed class EmptySelection : Selection |
||||
{ |
||||
public EmptySelection(TextArea textArea) : base(textArea) |
||||
{ |
||||
} |
||||
|
||||
public override Selection UpdateOnDocumentChange(DocumentChangeEventArgs e) |
||||
{ |
||||
return this; |
||||
} |
||||
|
||||
public override TextViewPosition StartPosition { |
||||
get { return new TextViewPosition(TextLocation.Empty); } |
||||
} |
||||
|
||||
public override TextViewPosition EndPosition { |
||||
get { return new TextViewPosition(TextLocation.Empty); } |
||||
} |
||||
|
||||
public override ISegment SurroundingSegment { |
||||
get { return null; } |
||||
} |
||||
|
||||
public override Selection SetEndpoint(TextViewPosition endPosition) |
||||
{ |
||||
throw new NotSupportedException(); |
||||
} |
||||
|
||||
public override Selection StartSelectionOrSetEndpoint(TextViewPosition startPosition, TextViewPosition endPosition) |
||||
{ |
||||
var document = textArea.Document; |
||||
if (document == null) |
||||
throw ThrowUtil.NoDocumentAssigned(); |
||||
return Create(textArea, startPosition, endPosition); |
||||
} |
||||
|
||||
public override IEnumerable<SelectionSegment> Segments { |
||||
get { return Empty<SelectionSegment>.Array; } |
||||
} |
||||
|
||||
public override string GetText() |
||||
{ |
||||
return string.Empty; |
||||
} |
||||
|
||||
public override void ReplaceSelectionWithText(string newText) |
||||
{ |
||||
if (newText == null) |
||||
throw new ArgumentNullException("newText"); |
||||
newText = AddSpacesIfRequired(newText, textArea.Caret.Position, textArea.Caret.Position); |
||||
if (newText.Length > 0) { |
||||
if (textArea.ReadOnlySectionProvider.CanInsert(textArea.Caret.Offset)) { |
||||
textArea.Document.Insert(textArea.Caret.Offset, newText); |
||||
} |
||||
} |
||||
textArea.Caret.VisualColumn = -1; |
||||
} |
||||
|
||||
public override int Length { |
||||
get { return 0; } |
||||
} |
||||
|
||||
// Use reference equality because there's only one EmptySelection per text area.
|
||||
public override int GetHashCode() |
||||
{ |
||||
return RuntimeHelpers.GetHashCode(this); |
||||
} |
||||
|
||||
public override bool Equals(object obj) |
||||
{ |
||||
return this == obj; |
||||
} |
||||
} |
||||
} |
@ -1,51 +0,0 @@
@@ -1,51 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory.Editor; |
||||
#else
|
||||
using ICSharpCode.AvalonEdit.Document; |
||||
#endif
|
||||
|
||||
namespace ICSharpCode.AvalonEdit.Editing |
||||
{ |
||||
/// <summary>
|
||||
/// Determines whether the document can be modified.
|
||||
/// </summary>
|
||||
public interface IReadOnlySectionProvider |
||||
{ |
||||
/// <summary>
|
||||
/// Gets whether insertion is possible at the specified offset.
|
||||
/// </summary>
|
||||
bool CanInsert(int offset); |
||||
|
||||
/// <summary>
|
||||
/// Gets the deletable segments inside the given segment.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// All segments in the result must be within the given segment, and they must be returned in order
|
||||
/// (e.g. if two segments are returned, EndOffset of first segment must be less than StartOffset of second segment).
|
||||
///
|
||||
/// For replacements, the last segment being returned will be replaced with the new text. If an empty list is returned,
|
||||
/// no replacement will be done.
|
||||
/// </remarks>
|
||||
IEnumerable<ISegment> GetDeletableSegments(ISegment segment); |
||||
} |
||||
} |
@ -1,221 +0,0 @@
@@ -1,221 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Linq; |
||||
using System.Runtime.CompilerServices; |
||||
using System.Runtime.InteropServices; |
||||
using System.Security; |
||||
using System.Windows; |
||||
using System.Windows.Input; |
||||
using System.Windows.Interop; |
||||
using System.Windows.Media; |
||||
using System.Windows.Media.TextFormatting; |
||||
|
||||
using ICSharpCode.AvalonEdit; |
||||
using ICSharpCode.AvalonEdit.Document; |
||||
using ICSharpCode.AvalonEdit.Rendering; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
using Draw = System.Drawing; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Editing |
||||
{ |
||||
/// <summary>
|
||||
/// Native API required for IME support.
|
||||
/// </summary>
|
||||
static class ImeNativeWrapper |
||||
{ |
||||
[StructLayout(LayoutKind.Sequential)] |
||||
struct CompositionForm |
||||
{ |
||||
public int dwStyle; |
||||
public POINT ptCurrentPos; |
||||
public RECT rcArea; |
||||
} |
||||
|
||||
[StructLayout(LayoutKind.Sequential)] |
||||
struct POINT |
||||
{ |
||||
public int x; |
||||
public int y; |
||||
} |
||||
|
||||
[StructLayout(LayoutKind.Sequential)] |
||||
struct RECT |
||||
{ |
||||
public int left; |
||||
public int top; |
||||
public int right; |
||||
public int bottom; |
||||
} |
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] |
||||
struct LOGFONT |
||||
{ |
||||
public int lfHeight; |
||||
public int lfWidth; |
||||
public int lfEscapement; |
||||
public int lfOrientation; |
||||
public int lfWeight; |
||||
public byte lfItalic; |
||||
public byte lfUnderline; |
||||
public byte lfStrikeOut; |
||||
public byte lfCharSet; |
||||
public byte lfOutPrecision; |
||||
public byte lfClipPrecision; |
||||
public byte lfQuality; |
||||
public byte lfPitchAndFamily; |
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)] public string lfFaceName; |
||||
} |
||||
|
||||
const int CPS_CANCEL = 0x4; |
||||
const int NI_COMPOSITIONSTR = 0x15; |
||||
const int GCS_COMPSTR = 0x0008; |
||||
|
||||
public const int WM_IME_COMPOSITION = 0x10F; |
||||
public const int WM_IME_SETCONTEXT = 0x281; |
||||
public const int WM_INPUTLANGCHANGE = 0x51; |
||||
|
||||
[DllImport("imm32.dll")] |
||||
public static extern IntPtr ImmAssociateContext(IntPtr hWnd, IntPtr hIMC); |
||||
[DllImport("imm32.dll")] |
||||
internal static extern IntPtr ImmGetContext(IntPtr hWnd); |
||||
[DllImport("imm32.dll")] |
||||
internal static extern IntPtr ImmGetDefaultIMEWnd(IntPtr hWnd); |
||||
[DllImport("imm32.dll")] |
||||
[return: MarshalAs(UnmanagedType.Bool)] |
||||
internal static extern bool ImmReleaseContext(IntPtr hWnd, IntPtr hIMC); |
||||
[DllImport("imm32.dll")] |
||||
[return: MarshalAs(UnmanagedType.Bool)] |
||||
static extern bool ImmNotifyIME(IntPtr hIMC, int dwAction, int dwIndex, int dwValue = 0); |
||||
[DllImport("imm32.dll")] |
||||
[return: MarshalAs(UnmanagedType.Bool)] |
||||
static extern bool ImmSetCompositionWindow(IntPtr hIMC, ref CompositionForm form); |
||||
[DllImport("imm32.dll", CharSet = CharSet.Unicode)] |
||||
[return: MarshalAs(UnmanagedType.Bool)] |
||||
static extern bool ImmSetCompositionFont(IntPtr hIMC, ref LOGFONT font); |
||||
[DllImport("imm32.dll")] |
||||
[return: MarshalAs(UnmanagedType.Bool)] |
||||
static extern bool ImmGetCompositionFont(IntPtr hIMC, out LOGFONT font); |
||||
|
||||
[DllImport("msctf.dll")] |
||||
static extern int TF_CreateThreadMgr(out ITfThreadMgr threadMgr); |
||||
|
||||
[ThreadStatic] static bool textFrameworkThreadMgrInitialized; |
||||
[ThreadStatic] static ITfThreadMgr textFrameworkThreadMgr; |
||||
|
||||
public static ITfThreadMgr GetTextFrameworkThreadManager() |
||||
{ |
||||
if (!textFrameworkThreadMgrInitialized) { |
||||
textFrameworkThreadMgrInitialized = true; |
||||
TF_CreateThreadMgr(out textFrameworkThreadMgr); |
||||
} |
||||
return textFrameworkThreadMgr; |
||||
} |
||||
|
||||
public static bool NotifyIme(IntPtr hIMC) |
||||
{ |
||||
return ImmNotifyIME(hIMC, NI_COMPOSITIONSTR, CPS_CANCEL); |
||||
} |
||||
|
||||
public static bool SetCompositionWindow(HwndSource source, IntPtr hIMC, TextArea textArea) |
||||
{ |
||||
if (textArea == null) |
||||
throw new ArgumentNullException("textArea"); |
||||
Rect textViewBounds = textArea.TextView.GetBounds(source); |
||||
Rect characterBounds = textArea.TextView.GetCharacterBounds(textArea.Caret.Position, source); |
||||
CompositionForm form = new CompositionForm(); |
||||
form.dwStyle = 0x0020; |
||||
form.ptCurrentPos.x = (int)Math.Max(characterBounds.Left, textViewBounds.Left); |
||||
form.ptCurrentPos.y = (int)Math.Max(characterBounds.Top, textViewBounds.Top); |
||||
form.rcArea.left = (int)textViewBounds.Left; |
||||
form.rcArea.top = (int)textViewBounds.Top; |
||||
form.rcArea.right = (int)textViewBounds.Right; |
||||
form.rcArea.bottom = (int)textViewBounds.Bottom; |
||||
return ImmSetCompositionWindow(hIMC, ref form); |
||||
} |
||||
|
||||
public static bool SetCompositionFont(HwndSource source, IntPtr hIMC, TextArea textArea) |
||||
{ |
||||
if (textArea == null) |
||||
throw new ArgumentNullException("textArea"); |
||||
LOGFONT lf = new LOGFONT(); |
||||
Rect characterBounds = textArea.TextView.GetCharacterBounds(textArea.Caret.Position, source); |
||||
lf.lfFaceName = textArea.FontFamily.Source; |
||||
lf.lfHeight = (int)characterBounds.Height; |
||||
return ImmSetCompositionFont(hIMC, ref lf); |
||||
} |
||||
|
||||
static Rect GetBounds(this TextView textView, HwndSource source) |
||||
{ |
||||
// this may happen during layout changes in AvalonDock, so we just return an empty rectangle
|
||||
// in those cases. It should be refreshed immediately.
|
||||
if (source.RootVisual == null || !source.RootVisual.IsAncestorOf(textView)) |
||||
return EMPTY_RECT; |
||||
Rect displayRect = new Rect(0, 0, textView.ActualWidth, textView.ActualHeight); |
||||
return textView |
||||
.TransformToAncestor(source.RootVisual).TransformBounds(displayRect) // rect on root visual
|
||||
.TransformToDevice(source.RootVisual); // rect on HWND
|
||||
} |
||||
|
||||
static readonly Rect EMPTY_RECT = new Rect(0, 0, 0, 0); |
||||
|
||||
static Rect GetCharacterBounds(this TextView textView, TextViewPosition pos, HwndSource source) |
||||
{ |
||||
VisualLine vl = textView.GetVisualLine(pos.Line); |
||||
if (vl == null) |
||||
return EMPTY_RECT; |
||||
// this may happen during layout changes in AvalonDock, so we just return an empty rectangle
|
||||
// in those cases. It should be refreshed immediately.
|
||||
if (source.RootVisual == null || !source.RootVisual.IsAncestorOf(textView)) |
||||
return EMPTY_RECT; |
||||
TextLine line = vl.GetTextLine(pos.VisualColumn, pos.IsAtEndOfLine); |
||||
Rect displayRect; |
||||
// calculate the display rect for the current character
|
||||
if (pos.VisualColumn < vl.VisualLengthWithEndOfLineMarker) { |
||||
displayRect = line.GetTextBounds(pos.VisualColumn, 1).First().Rectangle; |
||||
displayRect.Offset(0, vl.GetTextLineVisualYPosition(line, VisualYPosition.LineTop)); |
||||
} else { |
||||
// if we are in virtual space, we just use one wide-space as character width
|
||||
displayRect = new Rect(vl.GetVisualPosition(pos.VisualColumn, VisualYPosition.TextTop), |
||||
new Size(textView.WideSpaceWidth, textView.DefaultLineHeight)); |
||||
} |
||||
// adjust to current scrolling
|
||||
displayRect.Offset(-textView.ScrollOffset); |
||||
return textView |
||||
.TransformToAncestor(source.RootVisual).TransformBounds(displayRect) // rect on root visual
|
||||
.TransformToDevice(source.RootVisual); // rect on HWND
|
||||
} |
||||
} |
||||
|
||||
[ComImport, Guid("aa80e801-2021-11d2-93e0-0060b067b86e"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] |
||||
interface ITfThreadMgr |
||||
{ |
||||
void Activate(out int clientId); |
||||
void Deactivate(); |
||||
void CreateDocumentMgr(out IntPtr docMgr); |
||||
void EnumDocumentMgrs(out IntPtr enumDocMgrs); |
||||
void GetFocus(out IntPtr docMgr); |
||||
void SetFocus(IntPtr docMgr); |
||||
void AssociateFocus(IntPtr hwnd, IntPtr newDocMgr, out IntPtr prevDocMgr); |
||||
void IsThreadFocus([MarshalAs(UnmanagedType.Bool)] out bool isFocus); |
||||
void GetFunctionProvider(ref Guid classId, out IntPtr funcProvider); |
||||
void EnumFunctionProviders(out IntPtr enumProviders); |
||||
void GetGlobalCompartment(out IntPtr compartmentMgr); |
||||
} |
||||
} |
@ -1,165 +0,0 @@
@@ -1,165 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.ComponentModel; |
||||
using System.Diagnostics; |
||||
using System.Linq; |
||||
using System.Runtime.CompilerServices; |
||||
using System.Runtime.InteropServices; |
||||
using System.Security; |
||||
using System.Windows; |
||||
using System.Windows.Controls; |
||||
using System.Windows.Input; |
||||
using System.Windows.Interop; |
||||
using System.Windows.Media; |
||||
using System.Windows.Media.TextFormatting; |
||||
using ICSharpCode.AvalonEdit; |
||||
using ICSharpCode.AvalonEdit.Document; |
||||
using ICSharpCode.AvalonEdit.Rendering; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Editing |
||||
{ |
||||
class ImeSupport |
||||
{ |
||||
readonly TextArea textArea; |
||||
IntPtr currentContext; |
||||
IntPtr previousContext; |
||||
IntPtr defaultImeWnd; |
||||
HwndSource hwndSource; |
||||
EventHandler requerySuggestedHandler; // we need to keep the event handler instance alive because CommandManager.RequerySuggested uses weak references
|
||||
bool isReadOnly; |
||||
|
||||
public ImeSupport(TextArea textArea) |
||||
{ |
||||
if (textArea == null) |
||||
throw new ArgumentNullException("textArea"); |
||||
this.textArea = textArea; |
||||
InputMethod.SetIsInputMethodSuspended(this.textArea, textArea.Options.EnableImeSupport); |
||||
// We listen to CommandManager.RequerySuggested for both caret offset changes and changes to the set of read-only sections.
|
||||
// This is because there's no dedicated event for read-only section changes; but RequerySuggested needs to be raised anyways
|
||||
// to invalidate the Paste command.
|
||||
requerySuggestedHandler = OnRequerySuggested; |
||||
CommandManager.RequerySuggested += requerySuggestedHandler; |
||||
textArea.OptionChanged += TextAreaOptionChanged; |
||||
} |
||||
|
||||
void OnRequerySuggested(object sender, EventArgs e) |
||||
{ |
||||
UpdateImeEnabled(); |
||||
} |
||||
|
||||
void TextAreaOptionChanged(object sender, PropertyChangedEventArgs e) |
||||
{ |
||||
if (e.PropertyName == "EnableImeSupport") { |
||||
InputMethod.SetIsInputMethodSuspended(this.textArea, textArea.Options.EnableImeSupport); |
||||
UpdateImeEnabled(); |
||||
} |
||||
} |
||||
|
||||
public void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e) |
||||
{ |
||||
UpdateImeEnabled(); |
||||
} |
||||
|
||||
public void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e) |
||||
{ |
||||
if (e.OldFocus == textArea && currentContext != IntPtr.Zero) |
||||
ImeNativeWrapper.NotifyIme(currentContext); |
||||
ClearContext(); |
||||
} |
||||
|
||||
void UpdateImeEnabled() |
||||
{ |
||||
if (textArea.Options.EnableImeSupport && textArea.IsKeyboardFocused) { |
||||
bool newReadOnly = !textArea.ReadOnlySectionProvider.CanInsert(textArea.Caret.Offset); |
||||
if (hwndSource == null || isReadOnly != newReadOnly) { |
||||
ClearContext(); // clear existing context (on read-only change)
|
||||
isReadOnly = newReadOnly; |
||||
CreateContext(); |
||||
} |
||||
} else { |
||||
ClearContext(); |
||||
} |
||||
} |
||||
|
||||
void ClearContext() |
||||
{ |
||||
if (hwndSource != null) { |
||||
ImeNativeWrapper.ImmAssociateContext(hwndSource.Handle, previousContext); |
||||
ImeNativeWrapper.ImmReleaseContext(defaultImeWnd, currentContext); |
||||
currentContext = IntPtr.Zero; |
||||
defaultImeWnd = IntPtr.Zero; |
||||
hwndSource.RemoveHook(WndProc); |
||||
hwndSource = null; |
||||
} |
||||
} |
||||
|
||||
void CreateContext() |
||||
{ |
||||
hwndSource = (HwndSource)PresentationSource.FromVisual(this.textArea); |
||||
if (hwndSource != null) { |
||||
if (isReadOnly) { |
||||
defaultImeWnd = IntPtr.Zero; |
||||
currentContext = IntPtr.Zero; |
||||
} else { |
||||
defaultImeWnd = ImeNativeWrapper.ImmGetDefaultIMEWnd(IntPtr.Zero); |
||||
currentContext = ImeNativeWrapper.ImmGetContext(defaultImeWnd); |
||||
} |
||||
previousContext = ImeNativeWrapper.ImmAssociateContext(hwndSource.Handle, currentContext); |
||||
hwndSource.AddHook(WndProc); |
||||
// UpdateCompositionWindow() will be called by the caret becoming visible
|
||||
|
||||
var threadMgr = ImeNativeWrapper.GetTextFrameworkThreadManager(); |
||||
if (threadMgr != null) { |
||||
// Even though the docu says passing null is invalid, this seems to help
|
||||
// activating the IME on the default input context that is shared with WPF
|
||||
threadMgr.SetFocus(IntPtr.Zero); |
||||
} |
||||
} |
||||
} |
||||
|
||||
IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) |
||||
{ |
||||
switch (msg) { |
||||
case ImeNativeWrapper.WM_INPUTLANGCHANGE: |
||||
// Don't mark the message as handled; other windows
|
||||
// might want to handle it as well.
|
||||
|
||||
// If we have a context, recreate it
|
||||
if (hwndSource != null) { |
||||
ClearContext(); |
||||
CreateContext(); |
||||
} |
||||
break; |
||||
case ImeNativeWrapper.WM_IME_COMPOSITION: |
||||
UpdateCompositionWindow(); |
||||
break; |
||||
} |
||||
return IntPtr.Zero; |
||||
} |
||||
|
||||
public void UpdateCompositionWindow() |
||||
{ |
||||
if (currentContext != IntPtr.Zero) { |
||||
ImeNativeWrapper.SetCompositionFont(hwndSource, currentContext, textArea); |
||||
ImeNativeWrapper.SetCompositionWindow(hwndSource, currentContext, textArea); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,260 +0,0 @@
@@ -1,260 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.ComponentModel; |
||||
using System.Globalization; |
||||
using System.Windows; |
||||
using System.Windows.Controls; |
||||
using System.Windows.Input; |
||||
using System.Windows.Media; |
||||
using System.Windows.Media.TextFormatting; |
||||
|
||||
using ICSharpCode.AvalonEdit.Document; |
||||
using ICSharpCode.AvalonEdit.Rendering; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Editing |
||||
{ |
||||
/// <summary>
|
||||
/// Margin showing line numbers.
|
||||
/// </summary>
|
||||
public class LineNumberMargin : AbstractMargin, IWeakEventListener |
||||
{ |
||||
static LineNumberMargin() |
||||
{ |
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(LineNumberMargin), |
||||
new FrameworkPropertyMetadata(typeof(LineNumberMargin))); |
||||
} |
||||
|
||||
TextArea textArea; |
||||
|
||||
/// <summary>
|
||||
/// The typeface used for rendering the line number margin.
|
||||
/// This field is calculated in MeasureOverride() based on the FontFamily etc. properties.
|
||||
/// </summary>
|
||||
protected Typeface typeface; |
||||
|
||||
/// <summary>
|
||||
/// The font size used for rendering the line number margin.
|
||||
/// This field is calculated in MeasureOverride() based on the FontFamily etc. properties.
|
||||
/// </summary>
|
||||
protected double emSize; |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Size MeasureOverride(Size availableSize) |
||||
{ |
||||
typeface = this.CreateTypeface(); |
||||
emSize = (double)GetValue(TextBlock.FontSizeProperty); |
||||
|
||||
FormattedText text = TextFormatterFactory.CreateFormattedText( |
||||
this, |
||||
new string('9', maxLineNumberLength), |
||||
typeface, |
||||
emSize, |
||||
(Brush)GetValue(Control.ForegroundProperty) |
||||
); |
||||
return new Size(text.Width, 0); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnRender(DrawingContext drawingContext) |
||||
{ |
||||
TextView textView = this.TextView; |
||||
Size renderSize = this.RenderSize; |
||||
if (textView != null && textView.VisualLinesValid) { |
||||
var foreground = (Brush)GetValue(Control.ForegroundProperty); |
||||
foreach (VisualLine line in textView.VisualLines) { |
||||
int lineNumber = line.FirstDocumentLine.LineNumber; |
||||
FormattedText text = TextFormatterFactory.CreateFormattedText( |
||||
this, |
||||
lineNumber.ToString(CultureInfo.CurrentCulture), |
||||
typeface, emSize, foreground |
||||
); |
||||
double y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.TextTop); |
||||
drawingContext.DrawText(text, new Point(renderSize.Width - text.Width, y - textView.VerticalOffset)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnTextViewChanged(TextView oldTextView, TextView newTextView) |
||||
{ |
||||
if (oldTextView != null) { |
||||
oldTextView.VisualLinesChanged -= TextViewVisualLinesChanged; |
||||
} |
||||
base.OnTextViewChanged(oldTextView, newTextView); |
||||
if (newTextView != null) { |
||||
newTextView.VisualLinesChanged += TextViewVisualLinesChanged; |
||||
|
||||
// find the text area belonging to the new text view
|
||||
textArea = newTextView.GetService(typeof(TextArea)) as TextArea; |
||||
} else { |
||||
textArea = null; |
||||
} |
||||
InvalidateVisual(); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnDocumentChanged(TextDocument oldDocument, TextDocument newDocument) |
||||
{ |
||||
if (oldDocument != null) { |
||||
PropertyChangedEventManager.RemoveListener(oldDocument, this, "LineCount"); |
||||
} |
||||
base.OnDocumentChanged(oldDocument, newDocument); |
||||
if (newDocument != null) { |
||||
PropertyChangedEventManager.AddListener(newDocument, this, "LineCount"); |
||||
} |
||||
OnDocumentLineCountChanged(); |
||||
} |
||||
|
||||
/// <inheritdoc cref="IWeakEventListener.ReceiveWeakEvent"/>
|
||||
protected virtual bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) |
||||
{ |
||||
if (managerType == typeof(PropertyChangedEventManager)) { |
||||
OnDocumentLineCountChanged(); |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) |
||||
{ |
||||
return ReceiveWeakEvent(managerType, sender, e); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Maximum length of a line number, in characters
|
||||
/// </summary>
|
||||
protected int maxLineNumberLength = 1; |
||||
|
||||
void OnDocumentLineCountChanged() |
||||
{ |
||||
int documentLineCount = Document != null ? Document.LineCount : 1; |
||||
int newLength = documentLineCount.ToString(CultureInfo.CurrentCulture).Length; |
||||
|
||||
// The margin looks too small when there is only one digit, so always reserve space for
|
||||
// at least two digits
|
||||
if (newLength < 2) |
||||
newLength = 2; |
||||
|
||||
if (newLength != maxLineNumberLength) { |
||||
maxLineNumberLength = newLength; |
||||
InvalidateMeasure(); |
||||
} |
||||
} |
||||
|
||||
void TextViewVisualLinesChanged(object sender, EventArgs e) |
||||
{ |
||||
InvalidateVisual(); |
||||
} |
||||
|
||||
AnchorSegment selectionStart; |
||||
bool selecting; |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) |
||||
{ |
||||
base.OnMouseLeftButtonDown(e); |
||||
if (!e.Handled && TextView != null && textArea != null) { |
||||
e.Handled = true; |
||||
textArea.Focus(); |
||||
|
||||
SimpleSegment currentSeg = GetTextLineSegment(e); |
||||
if (currentSeg == SimpleSegment.Invalid) |
||||
return; |
||||
textArea.Caret.Offset = currentSeg.Offset + currentSeg.Length; |
||||
if (CaptureMouse()) { |
||||
selecting = true; |
||||
selectionStart = new AnchorSegment(Document, currentSeg.Offset, currentSeg.Length); |
||||
if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) { |
||||
SimpleSelection simpleSelection = textArea.Selection as SimpleSelection; |
||||
if (simpleSelection != null) |
||||
selectionStart = new AnchorSegment(Document, simpleSelection.SurroundingSegment); |
||||
} |
||||
textArea.Selection = Selection.Create(textArea, selectionStart); |
||||
if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) { |
||||
ExtendSelection(currentSeg); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
SimpleSegment GetTextLineSegment(MouseEventArgs e) |
||||
{ |
||||
Point pos = e.GetPosition(TextView); |
||||
pos.X = 0; |
||||
pos.Y += TextView.VerticalOffset; |
||||
VisualLine vl = TextView.GetVisualLineFromVisualTop(pos.Y); |
||||
if (vl == null) |
||||
return SimpleSegment.Invalid; |
||||
TextLine tl = vl.GetTextLineByVisualYPosition(pos.Y); |
||||
int visualStartColumn = vl.GetTextLineVisualStartColumn(tl); |
||||
int visualEndColumn = visualStartColumn + tl.Length; |
||||
int relStart = vl.FirstDocumentLine.Offset; |
||||
int startOffset = vl.GetRelativeOffset(visualStartColumn) + relStart; |
||||
int endOffset = vl.GetRelativeOffset(visualEndColumn) + relStart; |
||||
if (endOffset == vl.LastDocumentLine.Offset + vl.LastDocumentLine.Length) |
||||
endOffset += vl.LastDocumentLine.DelimiterLength; |
||||
return new SimpleSegment(startOffset, endOffset - startOffset); |
||||
} |
||||
|
||||
void ExtendSelection(SimpleSegment currentSeg) |
||||
{ |
||||
if (currentSeg.Offset < selectionStart.Offset) { |
||||
textArea.Caret.Offset = currentSeg.Offset; |
||||
textArea.Selection = Selection.Create(textArea, currentSeg.Offset, selectionStart.Offset + selectionStart.Length); |
||||
} else { |
||||
textArea.Caret.Offset = currentSeg.Offset + currentSeg.Length; |
||||
textArea.Selection = Selection.Create(textArea, selectionStart.Offset, currentSeg.Offset + currentSeg.Length); |
||||
} |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnMouseMove(MouseEventArgs e) |
||||
{ |
||||
if (selecting && textArea != null && TextView != null) { |
||||
e.Handled = true; |
||||
SimpleSegment currentSeg = GetTextLineSegment(e); |
||||
if (currentSeg == SimpleSegment.Invalid) |
||||
return; |
||||
ExtendSelection(currentSeg); |
||||
} |
||||
base.OnMouseMove(e); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) |
||||
{ |
||||
if (selecting) { |
||||
selecting = false; |
||||
selectionStart = null; |
||||
ReleaseMouseCapture(); |
||||
e.Handled = true; |
||||
} |
||||
base.OnMouseLeftButtonUp(e); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters) |
||||
{ |
||||
// accept clicks even when clicking on the background
|
||||
return new PointHitTestResult(this, hitTestParameters.HitPoint); |
||||
} |
||||
} |
||||
} |
@ -1,66 +0,0 @@
@@ -1,66 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using ICSharpCode.AvalonEdit.Document; |
||||
using ICSharpCode.NRefactory.Editor; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Editing |
||||
{ |
||||
/// <summary>
|
||||
/// <see cref="IReadOnlySectionProvider"/> that has no read-only sections; all text is editable.
|
||||
/// </summary>
|
||||
sealed class NoReadOnlySections : IReadOnlySectionProvider |
||||
{ |
||||
public static readonly NoReadOnlySections Instance = new NoReadOnlySections(); |
||||
|
||||
public bool CanInsert(int offset) |
||||
{ |
||||
return true; |
||||
} |
||||
|
||||
public IEnumerable<ISegment> GetDeletableSegments(ISegment segment) |
||||
{ |
||||
if (segment == null) |
||||
throw new ArgumentNullException("segment"); |
||||
// the segment is always deletable
|
||||
return ExtensionMethods.Sequence(segment); |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// <see cref="IReadOnlySectionProvider"/> that completely disables editing.
|
||||
/// </summary>
|
||||
sealed class ReadOnlySectionDocument : IReadOnlySectionProvider |
||||
{ |
||||
public static readonly ReadOnlySectionDocument Instance = new ReadOnlySectionDocument(); |
||||
|
||||
public bool CanInsert(int offset) |
||||
{ |
||||
return false; |
||||
} |
||||
|
||||
public IEnumerable<ISegment> GetDeletableSegments(ISegment segment) |
||||
{ |
||||
return Enumerable.Empty<ISegment>(); |
||||
} |
||||
} |
||||
} |
@ -1,417 +0,0 @@
@@ -1,417 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Text; |
||||
using System.Windows; |
||||
using System.Windows.Documents; |
||||
using System.Windows.Input; |
||||
using System.Windows.Media.TextFormatting; |
||||
using ICSharpCode.AvalonEdit.Document; |
||||
using ICSharpCode.AvalonEdit.Rendering; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory; |
||||
using ICSharpCode.NRefactory.Editor; |
||||
#endif
|
||||
|
||||
namespace ICSharpCode.AvalonEdit.Editing |
||||
{ |
||||
/// <summary>
|
||||
/// Rectangular selection ("box selection").
|
||||
/// </summary>
|
||||
public sealed class RectangleSelection : Selection |
||||
{ |
||||
#region Commands
|
||||
/// <summary>
|
||||
/// Expands the selection left by one character, creating a rectangular selection.
|
||||
/// Key gesture: Alt+Shift+Left
|
||||
/// </summary>
|
||||
public static readonly RoutedUICommand BoxSelectLeftByCharacter = Command("BoxSelectLeftByCharacter"); |
||||
|
||||
/// <summary>
|
||||
/// Expands the selection right by one character, creating a rectangular selection.
|
||||
/// Key gesture: Alt+Shift+Right
|
||||
/// </summary>
|
||||
public static readonly RoutedUICommand BoxSelectRightByCharacter = Command("BoxSelectRightByCharacter"); |
||||
|
||||
/// <summary>
|
||||
/// Expands the selection left by one word, creating a rectangular selection.
|
||||
/// Key gesture: Ctrl+Alt+Shift+Left
|
||||
/// </summary>
|
||||
public static readonly RoutedUICommand BoxSelectLeftByWord = Command("BoxSelectLeftByWord"); |
||||
|
||||
/// <summary>
|
||||
/// Expands the selection left by one word, creating a rectangular selection.
|
||||
/// Key gesture: Ctrl+Alt+Shift+Right
|
||||
/// </summary>
|
||||
public static readonly RoutedUICommand BoxSelectRightByWord = Command("BoxSelectRightByWord"); |
||||
|
||||
/// <summary>
|
||||
/// Expands the selection up by one line, creating a rectangular selection.
|
||||
/// Key gesture: Alt+Shift+Up
|
||||
/// </summary>
|
||||
public static readonly RoutedUICommand BoxSelectUpByLine = Command("BoxSelectUpByLine"); |
||||
|
||||
/// <summary>
|
||||
/// Expands the selection up by one line, creating a rectangular selection.
|
||||
/// Key gesture: Alt+Shift+Down
|
||||
/// </summary>
|
||||
public static readonly RoutedUICommand BoxSelectDownByLine = Command("BoxSelectDownByLine"); |
||||
|
||||
/// <summary>
|
||||
/// Expands the selection to the start of the line, creating a rectangular selection.
|
||||
/// Key gesture: Alt+Shift+Home
|
||||
/// </summary>
|
||||
public static readonly RoutedUICommand BoxSelectToLineStart = Command("BoxSelectToLineStart"); |
||||
|
||||
/// <summary>
|
||||
/// Expands the selection to the end of the line, creating a rectangular selection.
|
||||
/// Key gesture: Alt+Shift+End
|
||||
/// </summary>
|
||||
public static readonly RoutedUICommand BoxSelectToLineEnd = Command("BoxSelectToLineEnd"); |
||||
|
||||
static RoutedUICommand Command(string name) |
||||
{ |
||||
return new RoutedUICommand(name, name, typeof(RectangleSelection)); |
||||
} |
||||
#endregion
|
||||
|
||||
TextDocument document; |
||||
readonly int startLine, endLine; |
||||
readonly double startXPos, endXPos; |
||||
readonly int topLeftOffset, bottomRightOffset; |
||||
readonly TextViewPosition start, end; |
||||
|
||||
readonly List<SelectionSegment> segments = new List<SelectionSegment>(); |
||||
|
||||
#region Constructors
|
||||
/// <summary>
|
||||
/// Creates a new rectangular selection.
|
||||
/// </summary>
|
||||
public RectangleSelection(TextArea textArea, TextViewPosition start, TextViewPosition end) |
||||
: base(textArea) |
||||
{ |
||||
InitDocument(); |
||||
this.startLine = start.Line; |
||||
this.endLine = end.Line; |
||||
this.startXPos = GetXPos(textArea, start); |
||||
this.endXPos = GetXPos(textArea, end); |
||||
CalculateSegments(); |
||||
this.topLeftOffset = this.segments.First().StartOffset; |
||||
this.bottomRightOffset = this.segments.Last().EndOffset; |
||||
|
||||
this.start = start; |
||||
this.end = end; |
||||
} |
||||
|
||||
private RectangleSelection(TextArea textArea, int startLine, double startXPos, TextViewPosition end) |
||||
: base(textArea) |
||||
{ |
||||
InitDocument(); |
||||
this.startLine = startLine; |
||||
this.endLine = end.Line; |
||||
this.startXPos = startXPos; |
||||
this.endXPos = GetXPos(textArea, end); |
||||
CalculateSegments(); |
||||
this.topLeftOffset = this.segments.First().StartOffset; |
||||
this.bottomRightOffset = this.segments.Last().EndOffset; |
||||
|
||||
this.start = GetStart(); |
||||
this.end = end; |
||||
} |
||||
|
||||
private RectangleSelection(TextArea textArea, TextViewPosition start, int endLine, double endXPos) |
||||
: base(textArea) |
||||
{ |
||||
InitDocument(); |
||||
this.startLine = start.Line; |
||||
this.endLine = endLine; |
||||
this.startXPos = GetXPos(textArea, start); |
||||
this.endXPos = endXPos; |
||||
CalculateSegments(); |
||||
this.topLeftOffset = this.segments.First().StartOffset; |
||||
this.bottomRightOffset = this.segments.Last().EndOffset; |
||||
|
||||
this.start = start; |
||||
this.end = GetEnd(); |
||||
} |
||||
|
||||
void InitDocument() |
||||
{ |
||||
document = textArea.Document; |
||||
if (document == null) |
||||
throw ThrowUtil.NoDocumentAssigned(); |
||||
} |
||||
|
||||
static double GetXPos(TextArea textArea, TextViewPosition pos) |
||||
{ |
||||
DocumentLine documentLine = textArea.Document.GetLineByNumber(pos.Line); |
||||
VisualLine visualLine = textArea.TextView.GetOrConstructVisualLine(documentLine); |
||||
int vc = visualLine.ValidateVisualColumn(pos, true); |
||||
TextLine textLine = visualLine.GetTextLine(vc, pos.IsAtEndOfLine); |
||||
return visualLine.GetTextLineVisualXPosition(textLine, vc); |
||||
} |
||||
|
||||
void CalculateSegments() |
||||
{ |
||||
DocumentLine nextLine = document.GetLineByNumber(Math.Min(startLine, endLine)); |
||||
do { |
||||
VisualLine vl = textArea.TextView.GetOrConstructVisualLine(nextLine); |
||||
int startVC = vl.GetVisualColumn(new Point(startXPos, 0), true); |
||||
int endVC = vl.GetVisualColumn(new Point(endXPos, 0), true); |
||||
|
||||
int baseOffset = vl.FirstDocumentLine.Offset; |
||||
int startOffset = baseOffset + vl.GetRelativeOffset(startVC); |
||||
int endOffset = baseOffset + vl.GetRelativeOffset(endVC); |
||||
segments.Add(new SelectionSegment(startOffset, startVC, endOffset, endVC)); |
||||
|
||||
nextLine = vl.LastDocumentLine.NextLine; |
||||
} while (nextLine != null && nextLine.LineNumber <= Math.Max(startLine, endLine)); |
||||
} |
||||
|
||||
TextViewPosition GetStart() |
||||
{ |
||||
SelectionSegment segment = (startLine < endLine ? segments.First() : segments.Last()); |
||||
if (startXPos < endXPos) { |
||||
return new TextViewPosition(document.GetLocation(segment.StartOffset), segment.StartVisualColumn); |
||||
} else { |
||||
return new TextViewPosition(document.GetLocation(segment.EndOffset), segment.EndVisualColumn); |
||||
} |
||||
} |
||||
|
||||
TextViewPosition GetEnd() |
||||
{ |
||||
SelectionSegment segment = (startLine < endLine ? segments.Last() : segments.First()); |
||||
if (startXPos < endXPos) { |
||||
return new TextViewPosition(document.GetLocation(segment.EndOffset), segment.EndVisualColumn); |
||||
} else { |
||||
return new TextViewPosition(document.GetLocation(segment.StartOffset), segment.StartVisualColumn); |
||||
} |
||||
} |
||||
#endregion
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string GetText() |
||||
{ |
||||
StringBuilder b = new StringBuilder(); |
||||
foreach (ISegment s in this.Segments) { |
||||
if (b.Length > 0) |
||||
b.AppendLine(); |
||||
b.Append(document.GetText(s)); |
||||
} |
||||
return b.ToString(); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override Selection StartSelectionOrSetEndpoint(TextViewPosition startPosition, TextViewPosition endPosition) |
||||
{ |
||||
return SetEndpoint(endPosition); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override int Length { |
||||
get { |
||||
return this.Segments.Sum(s => s.Length); |
||||
} |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool EnableVirtualSpace { |
||||
get { return true; } |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override ISegment SurroundingSegment { |
||||
get { |
||||
return new SimpleSegment(topLeftOffset, bottomRightOffset - topLeftOffset); |
||||
} |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override IEnumerable<SelectionSegment> Segments { |
||||
get { return segments; } |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override TextViewPosition StartPosition { |
||||
get { return start; } |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override TextViewPosition EndPosition { |
||||
get { return end; } |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object obj) |
||||
{ |
||||
RectangleSelection r = obj as RectangleSelection; |
||||
return r != null && r.textArea == this.textArea |
||||
&& r.topLeftOffset == this.topLeftOffset && r.bottomRightOffset == this.bottomRightOffset |
||||
&& r.startLine == this.startLine && r.endLine == this.endLine |
||||
&& r.startXPos == this.startXPos && r.endXPos == this.endXPos; |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() |
||||
{ |
||||
return topLeftOffset ^ bottomRightOffset; |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override Selection SetEndpoint(TextViewPosition endPosition) |
||||
{ |
||||
return new RectangleSelection(textArea, startLine, startXPos, endPosition); |
||||
} |
||||
|
||||
int GetVisualColumnFromXPos(int line, double xPos) |
||||
{ |
||||
var vl = textArea.TextView.GetOrConstructVisualLine(textArea.Document.GetLineByNumber(line)); |
||||
return vl.GetVisualColumn(new Point(xPos, 0), true); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override Selection UpdateOnDocumentChange(DocumentChangeEventArgs e) |
||||
{ |
||||
TextLocation newStartLocation = textArea.Document.GetLocation(e.GetNewOffset(topLeftOffset, AnchorMovementType.AfterInsertion)); |
||||
TextLocation newEndLocation = textArea.Document.GetLocation(e.GetNewOffset(bottomRightOffset, AnchorMovementType.BeforeInsertion)); |
||||
|
||||
return new RectangleSelection(textArea, |
||||
new TextViewPosition(newStartLocation, GetVisualColumnFromXPos(newStartLocation.Line, startXPos)), |
||||
new TextViewPosition(newEndLocation, GetVisualColumnFromXPos(newEndLocation.Line, endXPos))); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ReplaceSelectionWithText(string newText) |
||||
{ |
||||
if (newText == null) |
||||
throw new ArgumentNullException("newText"); |
||||
using (textArea.Document.RunUpdate()) { |
||||
TextViewPosition start = new TextViewPosition(document.GetLocation(topLeftOffset), GetVisualColumnFromXPos(startLine, startXPos)); |
||||
TextViewPosition end = new TextViewPosition(document.GetLocation(bottomRightOffset), GetVisualColumnFromXPos(endLine, endXPos)); |
||||
int insertionLength; |
||||
int totalInsertionLength = 0; |
||||
int firstInsertionLength = 0; |
||||
int editOffset = Math.Min(topLeftOffset, bottomRightOffset); |
||||
TextViewPosition pos; |
||||
if (NewLineFinder.NextNewLine(newText, 0) == SimpleSegment.Invalid) { |
||||
// insert same text into every line
|
||||
foreach (SelectionSegment lineSegment in this.Segments.Reverse()) { |
||||
ReplaceSingleLineText(textArea, lineSegment, newText, out insertionLength); |
||||
totalInsertionLength += insertionLength; |
||||
firstInsertionLength = insertionLength; |
||||
} |
||||
|
||||
int newEndOffset = editOffset + totalInsertionLength; |
||||
pos = new TextViewPosition(document.GetLocation(editOffset + firstInsertionLength)); |
||||
|
||||
textArea.Selection = new RectangleSelection(textArea, pos, Math.Max(startLine, endLine), GetXPos(textArea, pos)); |
||||
} else { |
||||
string[] lines = newText.Split(NewLineFinder.NewlineStrings, segments.Count, StringSplitOptions.None); |
||||
int line = Math.Min(startLine, endLine); |
||||
for (int i = lines.Length - 1; i >= 0; i--) { |
||||
ReplaceSingleLineText(textArea, segments[i], lines[i], out insertionLength); |
||||
firstInsertionLength = insertionLength; |
||||
} |
||||
pos = new TextViewPosition(document.GetLocation(editOffset + firstInsertionLength)); |
||||
textArea.ClearSelection(); |
||||
} |
||||
textArea.Caret.Position = textArea.TextView.GetPosition(new Point(GetXPos(textArea, pos), textArea.TextView.GetVisualTopByDocumentLine(Math.Max(startLine, endLine)))).GetValueOrDefault(); |
||||
} |
||||
} |
||||
|
||||
void ReplaceSingleLineText(TextArea textArea, SelectionSegment lineSegment, string newText, out int insertionLength) |
||||
{ |
||||
if (lineSegment.Length == 0) { |
||||
if (newText.Length > 0 && textArea.ReadOnlySectionProvider.CanInsert(lineSegment.StartOffset)) { |
||||
newText = AddSpacesIfRequired(newText, new TextViewPosition(document.GetLocation(lineSegment.StartOffset), lineSegment.StartVisualColumn), new TextViewPosition(document.GetLocation(lineSegment.EndOffset), lineSegment.EndVisualColumn)); |
||||
textArea.Document.Insert(lineSegment.StartOffset, newText); |
||||
} |
||||
} else { |
||||
ISegment[] segmentsToDelete = textArea.GetDeletableSegments(lineSegment); |
||||
for (int i = segmentsToDelete.Length - 1; i >= 0; i--) { |
||||
if (i == segmentsToDelete.Length - 1) { |
||||
if (segmentsToDelete[i].Offset == SurroundingSegment.Offset && segmentsToDelete[i].Length == SurroundingSegment.Length) { |
||||
newText = AddSpacesIfRequired(newText, new TextViewPosition(document.GetLocation(lineSegment.StartOffset), lineSegment.StartVisualColumn), new TextViewPosition(document.GetLocation(lineSegment.EndOffset), lineSegment.EndVisualColumn)); |
||||
} |
||||
textArea.Document.Replace(segmentsToDelete[i], newText); |
||||
} else { |
||||
textArea.Document.Remove(segmentsToDelete[i]); |
||||
} |
||||
} |
||||
} |
||||
insertionLength = newText.Length; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Performs a rectangular paste operation.
|
||||
/// </summary>
|
||||
public static bool PerformRectangularPaste(TextArea textArea, TextViewPosition startPosition, string text, bool selectInsertedText) |
||||
{ |
||||
if (textArea == null) |
||||
throw new ArgumentNullException("textArea"); |
||||
if (text == null) |
||||
throw new ArgumentNullException("text"); |
||||
int newLineCount = text.Count(c => c == '\n'); // TODO might not work in all cases, but single \r line endings are really rare today.
|
||||
TextLocation endLocation = new TextLocation(startPosition.Line + newLineCount, startPosition.Column); |
||||
if (endLocation.Line <= textArea.Document.LineCount) { |
||||
int endOffset = textArea.Document.GetOffset(endLocation); |
||||
if (textArea.Selection.EnableVirtualSpace || textArea.Document.GetLocation(endOffset) == endLocation) { |
||||
RectangleSelection rsel = new RectangleSelection(textArea, startPosition, endLocation.Line, GetXPos(textArea, startPosition)); |
||||
rsel.ReplaceSelectionWithText(text); |
||||
if (selectInsertedText && textArea.Selection is RectangleSelection) { |
||||
RectangleSelection sel = (RectangleSelection)textArea.Selection; |
||||
textArea.Selection = new RectangleSelection(textArea, startPosition, sel.endLine, sel.endXPos); |
||||
} |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the entry in the DataObject that signals rectangle selections.
|
||||
/// </summary>
|
||||
public const string RectangularSelectionDataType = "AvalonEditRectangularSelection"; |
||||
|
||||
/// <inheritdoc/>
|
||||
public override System.Windows.DataObject CreateDataObject(TextArea textArea) |
||||
{ |
||||
var data = base.CreateDataObject(textArea); |
||||
|
||||
if (EditingCommandHandler.ConfirmDataFormat(textArea, data, RectangularSelectionDataType)) { |
||||
MemoryStream isRectangle = new MemoryStream(1); |
||||
isRectangle.WriteByte(1); |
||||
data.SetData(RectangularSelectionDataType, isRectangle, false); |
||||
} |
||||
return data; |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() |
||||
{ |
||||
// It's possible that ToString() gets called on old (invalid) selections, e.g. for "change from... to..." debug message
|
||||
// make sure we don't crash even when the desired locations don't exist anymore.
|
||||
return string.Format("[RectangleSelection {0} {1} {2} to {3} {4} {5}]", startLine, topLeftOffset, startXPos, endLine, bottomRightOffset, endXPos); |
||||
} |
||||
} |
||||
} |
@ -1,302 +0,0 @@
@@ -1,302 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Text; |
||||
using System.Windows; |
||||
|
||||
using ICSharpCode.AvalonEdit.Document; |
||||
using ICSharpCode.AvalonEdit.Highlighting; |
||||
using ICSharpCode.AvalonEdit.Utils; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory.Editor; |
||||
#endif
|
||||
|
||||
namespace ICSharpCode.AvalonEdit.Editing |
||||
{ |
||||
/// <summary>
|
||||
/// Base class for selections.
|
||||
/// </summary>
|
||||
public abstract class Selection |
||||
{ |
||||
/// <summary>
|
||||
/// Creates a new simple selection that selects the text from startOffset to endOffset.
|
||||
/// </summary>
|
||||
public static Selection Create(TextArea textArea, int startOffset, int endOffset) |
||||
{ |
||||
if (textArea == null) |
||||
throw new ArgumentNullException("textArea"); |
||||
if (startOffset == endOffset) |
||||
return textArea.emptySelection; |
||||
else |
||||
return new SimpleSelection(textArea, |
||||
new TextViewPosition(textArea.Document.GetLocation(startOffset)), |
||||
new TextViewPosition(textArea.Document.GetLocation(endOffset))); |
||||
} |
||||
|
||||
internal static Selection Create(TextArea textArea, TextViewPosition start, TextViewPosition end) |
||||
{ |
||||
if (textArea == null) |
||||
throw new ArgumentNullException("textArea"); |
||||
if (textArea.Document.GetOffset(start.Location) == textArea.Document.GetOffset(end.Location) && start.VisualColumn == end.VisualColumn) |
||||
return textArea.emptySelection; |
||||
else |
||||
return new SimpleSelection(textArea, start, end); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Creates a new simple selection that selects the text in the specified segment.
|
||||
/// </summary>
|
||||
public static Selection Create(TextArea textArea, ISegment segment) |
||||
{ |
||||
if (segment == null) |
||||
throw new ArgumentNullException("segment"); |
||||
return Create(textArea, segment.Offset, segment.EndOffset); |
||||
} |
||||
|
||||
internal readonly TextArea textArea; |
||||
|
||||
/// <summary>
|
||||
/// Constructor for Selection.
|
||||
/// </summary>
|
||||
protected Selection(TextArea textArea) |
||||
{ |
||||
if (textArea == null) |
||||
throw new ArgumentNullException("textArea"); |
||||
this.textArea = textArea; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the start position of the selection.
|
||||
/// </summary>
|
||||
public abstract TextViewPosition StartPosition { get; } |
||||
|
||||
/// <summary>
|
||||
/// Gets the end position of the selection.
|
||||
/// </summary>
|
||||
public abstract TextViewPosition EndPosition { get; } |
||||
|
||||
/// <summary>
|
||||
/// Gets the selected text segments.
|
||||
/// </summary>
|
||||
public abstract IEnumerable<SelectionSegment> Segments { get; } |
||||
|
||||
/// <summary>
|
||||
/// Gets the smallest segment that contains all segments in this selection.
|
||||
/// May return null if the selection is empty.
|
||||
/// </summary>
|
||||
public abstract ISegment SurroundingSegment { get; } |
||||
|
||||
/// <summary>
|
||||
/// Replaces the selection with the specified text.
|
||||
/// </summary>
|
||||
public abstract void ReplaceSelectionWithText(string newText); |
||||
|
||||
internal string AddSpacesIfRequired(string newText, TextViewPosition start, TextViewPosition end) |
||||
{ |
||||
if (EnableVirtualSpace && InsertVirtualSpaces(newText, start, end)) { |
||||
var line = textArea.Document.GetLineByNumber(start.Line); |
||||
string lineText = textArea.Document.GetText(line); |
||||
var vLine = textArea.TextView.GetOrConstructVisualLine(line); |
||||
int colDiff = start.VisualColumn - vLine.VisualLengthWithEndOfLineMarker; |
||||
if (colDiff > 0) { |
||||
string additionalSpaces = ""; |
||||
if (!textArea.Options.ConvertTabsToSpaces && lineText.Trim('\t').Length == 0) { |
||||
int tabCount = (int)(colDiff / textArea.Options.IndentationSize); |
||||
additionalSpaces = new string('\t', tabCount); |
||||
colDiff -= tabCount * textArea.Options.IndentationSize; |
||||
} |
||||
additionalSpaces += new string(' ', colDiff); |
||||
return additionalSpaces + newText; |
||||
} |
||||
} |
||||
return newText; |
||||
} |
||||
|
||||
bool InsertVirtualSpaces(string newText, TextViewPosition start, TextViewPosition end) |
||||
{ |
||||
return (!string.IsNullOrEmpty(newText) || !(IsInVirtualSpace(start) && IsInVirtualSpace(end))) |
||||
&& newText != "\r\n" |
||||
&& newText != "\n" |
||||
&& newText != "\r"; |
||||
} |
||||
|
||||
bool IsInVirtualSpace(TextViewPosition pos) |
||||
{ |
||||
return pos.VisualColumn > textArea.TextView.GetOrConstructVisualLine(textArea.Document.GetLineByNumber(pos.Line)).VisualLength; |
||||
} |
||||
|
||||
/// <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 whether virtual space is enabled for this selection.
|
||||
/// </summary>
|
||||
public virtual bool EnableVirtualSpace { |
||||
get { return textArea.Options.EnableVirtualSpace; } |
||||
} |
||||
|
||||
/// <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(TextViewPosition endPosition); |
||||
|
||||
/// <summary>
|
||||
/// If this selection is empty, starts a new selection from <paramref name="startPosition"/> to
|
||||
/// <paramref name="endPosition"/>, otherwise, changes the endpoint of this selection.
|
||||
/// </summary>
|
||||
public abstract Selection StartSelectionOrSetEndpoint(TextViewPosition startPosition, TextViewPosition endPosition); |
||||
|
||||
/// <summary>
|
||||
/// Gets whether the selection is multi-line.
|
||||
/// </summary>
|
||||
public virtual bool IsMultiline { |
||||
get { |
||||
ISegment surroundingSegment = this.SurroundingSegment; |
||||
if (surroundingSegment == null) |
||||
return false; |
||||
int start = surroundingSegment.Offset; |
||||
int end = start + surroundingSegment.Length; |
||||
var document = textArea.Document; |
||||
if (document == null) |
||||
throw ThrowUtil.NoDocumentAssigned(); |
||||
return document.GetLineByOffset(start) != document.GetLineByOffset(end); |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Gets the selected text.
|
||||
/// </summary>
|
||||
public virtual string GetText() |
||||
{ |
||||
var document = textArea.Document; |
||||
if (document == null) |
||||
throw ThrowUtil.NoDocumentAssigned(); |
||||
StringBuilder b = null; |
||||
string text = null; |
||||
foreach (ISegment s in Segments) { |
||||
if (text != null) { |
||||
if (b == null) |
||||
b = new StringBuilder(text); |
||||
else |
||||
b.Append(text); |
||||
} |
||||
text = document.GetText(s); |
||||
} |
||||
if (b != null) { |
||||
if (text != null) b.Append(text); |
||||
return b.ToString(); |
||||
} else { |
||||
return text ?? string.Empty; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Creates a HTML fragment for the selected text.
|
||||
/// </summary>
|
||||
public string CreateHtmlFragment(HtmlOptions options) |
||||
{ |
||||
if (options == null) |
||||
throw new ArgumentNullException("options"); |
||||
IHighlighter highlighter = textArea.GetService(typeof(IHighlighter)) as IHighlighter; |
||||
StringBuilder html = new StringBuilder(); |
||||
bool first = true; |
||||
foreach (ISegment selectedSegment in this.Segments) { |
||||
if (first) |
||||
first = false; |
||||
else |
||||
html.AppendLine("<br>"); |
||||
html.Append(HtmlClipboard.CreateHtmlFragment(textArea.Document, highlighter, selectedSegment, options)); |
||||
} |
||||
return html.ToString(); |
||||
} |
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract override bool Equals(object obj); |
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract override int GetHashCode(); |
||||
|
||||
/// <summary>
|
||||
/// Gets whether the specified offset is included in the selection.
|
||||
/// </summary>
|
||||
/// <returns>True, if the selection contains the offset (selection borders inclusive);
|
||||
/// otherwise, false.</returns>
|
||||
public virtual bool Contains(int offset) |
||||
{ |
||||
if (this.IsEmpty) |
||||
return false; |
||||
if (this.SurroundingSegment.Contains(offset, 0)) { |
||||
foreach (ISegment s in this.Segments) { |
||||
if (s.Contains(offset, 0)) { |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Creates a data object containing the selection's text.
|
||||
/// </summary>
|
||||
public virtual DataObject CreateDataObject(TextArea textArea) |
||||
{ |
||||
DataObject data = new DataObject(); |
||||
|
||||
// Ensure we use the appropriate newline sequence for the OS
|
||||
string text = TextUtilities.NormalizeNewLines(GetText(), Environment.NewLine); |
||||
|
||||
// Enable drag/drop to Word, Notepad++ and others
|
||||
if (EditingCommandHandler.ConfirmDataFormat(textArea, data, DataFormats.UnicodeText)) { |
||||
data.SetText(text); |
||||
} |
||||
|
||||
// Enable drag/drop to SciTe:
|
||||
// We cannot use SetText, thus we need to use typeof(string).FullName as data format.
|
||||
// new DataObject(object) calls SetData(object), which in turn calls SetData(Type, data),
|
||||
// which then uses Type.FullName as format.
|
||||
// We immitate that behavior here as well:
|
||||
if (EditingCommandHandler.ConfirmDataFormat(textArea, data, typeof(string).FullName)) { |
||||
data.SetData(typeof(string).FullName, text); |
||||
} |
||||
|
||||
// Also copy text in HTML format to clipboard - good for pasting text into Word
|
||||
// or to the SharpDevelop forums.
|
||||
if (EditingCommandHandler.ConfirmDataFormat(textArea, data, DataFormats.Html)) { |
||||
HtmlClipboard.SetHtml(data, CreateHtmlFragment(new HtmlOptions(textArea.Options))); |
||||
} |
||||
return data; |
||||
} |
||||
} |
||||
} |
@ -1,74 +0,0 @@
@@ -1,74 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using ICSharpCode.AvalonEdit.Rendering; |
||||
#if NREFACTORY
|
||||
using ICSharpCode.NRefactory.Editor; |
||||
#endif
|
||||
|
||||
namespace ICSharpCode.AvalonEdit.Editing |
||||
{ |
||||
sealed class SelectionColorizer : ColorizingTransformer |
||||
{ |
||||
TextArea textArea; |
||||
|
||||
public SelectionColorizer(TextArea textArea) |
||||
{ |
||||
if (textArea == null) |
||||
throw new ArgumentNullException("textArea"); |
||||
this.textArea = textArea; |
||||
} |
||||
|
||||
protected override void Colorize(ITextRunConstructionContext context) |
||||
{ |
||||
// if SelectionForeground is null, keep the existing foreground color
|
||||
if (textArea.SelectionForeground == null) |
||||
return; |
||||
|
||||
int lineStartOffset = context.VisualLine.FirstDocumentLine.Offset; |
||||
int lineEndOffset = context.VisualLine.LastDocumentLine.Offset + context.VisualLine.LastDocumentLine.TotalLength; |
||||
|
||||
foreach (SelectionSegment segment in textArea.Selection.Segments) { |
||||
int segmentStart = segment.StartOffset; |
||||
int segmentEnd = segment.EndOffset; |
||||
if (segmentEnd <= lineStartOffset) |
||||
continue; |
||||
if (segmentStart >= lineEndOffset) |
||||
continue; |
||||
int startColumn; |
||||
if (segmentStart < lineStartOffset) |
||||
startColumn = 0; |
||||
else |
||||
startColumn = context.VisualLine.ValidateVisualColumn(segment.StartOffset, segment.StartVisualColumn, textArea.Selection.EnableVirtualSpace); |
||||
|
||||
int endColumn; |
||||
if (segmentEnd > lineEndOffset) |
||||
endColumn = textArea.Selection.EnableVirtualSpace ? int.MaxValue : context.VisualLine.VisualLengthWithEndOfLineMarker; |
||||
else |
||||
endColumn = context.VisualLine.ValidateVisualColumn(segment.EndOffset, segment.EndVisualColumn, textArea.Selection.EnableVirtualSpace); |
||||
|
||||
ChangeVisualElements( |
||||
startColumn, endColumn, |
||||
element => { |
||||
element.TextRunProperties.SetForegroundBrush(textArea.SelectionForeground); |
||||
}); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,68 +0,0 @@
@@ -1,68 +0,0 @@
|
||||
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Windows; |
||||
using System.Windows.Media; |
||||
|
||||
using ICSharpCode.AvalonEdit.Rendering; |
||||
|
||||
namespace ICSharpCode.AvalonEdit.Editing |
||||
{ |
||||
sealed class SelectionLayer : Layer, IWeakEventListener |
||||
{ |
||||
readonly TextArea textArea; |
||||
|
||||
public SelectionLayer(TextArea textArea) : base(textArea.TextView, KnownLayer.Selection) |
||||
{ |
||||
this.IsHitTestVisible = false; |
||||
|
||||
this.textArea = textArea; |
||||
TextViewWeakEventManager.VisualLinesChanged.AddListener(textView, this); |
||||
TextViewWeakEventManager.ScrollOffsetChanged.AddListener(textView, this); |
||||
} |
||||
|
||||
bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) |
||||
{ |
||||
if (managerType == typeof(TextViewWeakEventManager.VisualLinesChanged) |
||||
|| managerType == typeof(TextViewWeakEventManager.ScrollOffsetChanged)) |
||||
{ |
||||
InvalidateVisual(); |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
protected override void OnRender(DrawingContext drawingContext) |
||||
{ |
||||
base.OnRender(drawingContext); |
||||
|
||||
BackgroundGeometryBuilder geoBuilder = new BackgroundGeometryBuilder(); |
||||
geoBuilder.AlignToMiddleOfPixels = true; |
||||
geoBuilder.ExtendToFullWidthAtLineEnd = textArea.Selection.EnableVirtualSpace; |
||||
geoBuilder.CornerRadius = textArea.SelectionCornerRadius; |
||||
foreach (var segment in textArea.Selection.Segments) { |
||||
geoBuilder.AddSegment(textView, segment); |
||||
} |
||||
Geometry geometry = geoBuilder.CreateGeometry(); |
||||
if (geometry != null) { |
||||
drawingContext.DrawGeometry(textArea.SelectionBrush, textArea.SelectionBorder, geometry); |
||||
} |
||||
} |
||||
} |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue