Browse Source

Add infrastructure for code snippets.

git-svn-id: svn://svn.sharpdevelop.net/sharpdevelop/trunk@5064 1ccf3a8d-04fe-1044-b7c0-cef0b8235c61
shortcuts
Daniel Grunwald 16 years ago
parent
commit
7d53a43afd
  1. 2
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/ILineTracker.cs
  2. 11
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj
  3. 27
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Snippets/IActiveElement.cs
  4. 178
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Snippets/InsertionContext.cs
  5. 36
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Snippets/Snippet.cs
  6. 96
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Snippets/SnippetBoundElement.cs
  7. 28
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Snippets/SnippetCaretElement.cs
  8. 45
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Snippets/SnippetContainerElement.cs
  9. 66
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Snippets/SnippetElement.cs
  10. 117
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Snippets/SnippetReplaceableTextElement.cs
  11. 39
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Snippets/SnippetTextElement.cs
  12. 72
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Utils/IFreezable.cs

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

@ -19,6 +19,8 @@ namespace ICSharpCode.AvalonEdit.Document @@ -19,6 +19,8 @@ namespace ICSharpCode.AvalonEdit.Document
/// 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
{

11
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj

@ -277,6 +277,15 @@ @@ -277,6 +277,15 @@
<Compile Include="Rendering\VisualYPosition.cs">
<DependentUpon>VisualLine.cs</DependentUpon>
</Compile>
<Compile Include="Snippets\IActiveElement.cs" />
<Compile Include="Snippets\Snippet.cs" />
<Compile Include="Snippets\SnippetBoundElement.cs" />
<Compile Include="Snippets\SnippetCaretElement.cs" />
<Compile Include="Snippets\SnippetContainerElement.cs" />
<Compile Include="Snippets\SnippetElement.cs" />
<Compile Include="Snippets\InsertionContext.cs" />
<Compile Include="Snippets\SnippetReplaceableTextElement.cs" />
<Compile Include="Snippets\SnippetTextElement.cs" />
<Compile Include="TextEditor.cs" />
<Compile Include="TextEditorAutomationPeer.cs" />
<Compile Include="TextEditorComponent.cs">
@ -309,6 +318,7 @@ @@ -309,6 +318,7 @@
<Compile Include="Utils\Empty.cs" />
<Compile Include="Utils\ExtensionMethods.cs" />
<Compile Include="Utils\FileReader.cs" />
<Compile Include="Utils\IFreezable.cs" />
<Compile Include="Utils\ImmutableStack.cs" />
<Compile Include="Utils\NullSafeCollection.cs" />
<Compile Include="Utils\ObserveAddRemoveCollection.cs" />
@ -376,6 +386,7 @@ @@ -376,6 +386,7 @@
<EmbeddedResource Include="Highlighting\Resources\XmlDoc.xshd" />
</ItemGroup>
<ItemGroup>
<Folder Include="Snippets" />
<Folder Include="Document" />
<Folder Include="Folding" />
<Folder Include="Highlighting" />

27
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Snippets/IActiveElement.cs

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
namespace ICSharpCode.AvalonEdit.Snippets
{
/// <summary>
/// Represents an active element that allows the snippet to stay interactive after insertion.
/// </summary>
public interface IActiveElement
{
/// <summary>
/// Called when the all snippet elements have been inserted.
/// </summary>
void OnInsertionCompleted();
/// <summary>
/// Called when the interactive mode is deactivated.
/// </summary>
void Deactivate();
}
}

178
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Snippets/InsertionContext.cs

@ -0,0 +1,178 @@ @@ -0,0 +1,178 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using System.Windows.Input;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Editing;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Snippets
{
/// <summary>
/// Represents the context of a snippet insertion.
/// </summary>
public class InsertionContext
{
/// <summary>
/// Creates a new InsertionContext instance.
/// </summary>
public InsertionContext(TextArea textArea, int insertionPosition)
{
if (textArea == null)
throw new ArgumentNullException("textArea");
this.TextArea = textArea;
this.Document = textArea.Document;
this.InsertionPosition = insertionPosition;
DocumentLine startLine = this.Document.GetLineByOffset(insertionPosition);
ISegment indentation = TextUtilities.GetWhitespaceAfter(this.Document, startLine.Offset);
this.Indentation = Document.GetText(indentation.Offset, Math.Min(indentation.EndOffset, insertionPosition) - indentation.Offset);
this.LineTerminator = NewLineFinder.GetNewLineFromDocument(this.Document, startLine.LineNumber);
}
/// <summary>
/// Gets the text area.
/// </summary>
public TextArea TextArea { get; private set; }
/// <summary>
/// Gets the text document.
/// </summary>
public TextDocument Document { get; private set; }
/// <summary>
/// Gets the indentation at the insertion position.
/// </summary>
public string Indentation { get; private set; }
/// <summary>
/// Gets the line terminator at the insertion position.
/// </summary>
public string LineTerminator { get; private set; }
/// <summary>
/// Gets/Sets the insertion position.
/// </summary>
public int InsertionPosition { get; set; }
/// <summary>
/// Inserts text at the insertion position and advances the insertion position.
/// </summary>
public void InsertText(string text)
{
if (text == null)
throw new ArgumentNullException("text");
int textOffset = 0;
SimpleSegment segment;
while ((segment = NewLineFinder.NextNewLine(text, textOffset)) != SimpleSegment.Invalid) {
string insertString = text.Substring(textOffset, segment.Offset - textOffset)
+ this.LineTerminator + this.Indentation;
this.Document.Insert(InsertionPosition, insertString);
this.InsertionPosition += insertString.Length;
textOffset = segment.EndOffset;
}
string remainingInsertString = text.Substring(textOffset);
this.Document.Insert(InsertionPosition, remainingInsertString);
this.InsertionPosition += remainingInsertString.Length;
}
Dictionary<SnippetElement, IActiveElement> elementMap = new Dictionary<SnippetElement, IActiveElement>();
List<IActiveElement> registeredElements = new List<IActiveElement>();
/// <summary>
/// Registers an active element. Elements should be registered during insertion and will be called back
/// when insertion has completed.
/// </summary>
/// <param name="owner">The snippet element that created the active element.</param>
/// <param name="element">The active element.</param>
public void RegisterActiveElement(SnippetElement owner, IActiveElement element)
{
if (owner == null)
throw new ArgumentNullException("owner");
if (element == null)
throw new ArgumentNullException("element");
elementMap.Add(owner, element);
registeredElements.Add(element);
}
/// <summary>
/// Returns the active element belonging to the specified snippet element, or null if no such active element is found.
/// </summary>
public IActiveElement GetActiveElement(SnippetElement owner)
{
if (owner == null)
throw new ArgumentNullException("owner");
IActiveElement element;
if (elementMap.TryGetValue(owner, out element))
return element;
else
return null;
}
bool insertionCompleted;
/// <summary>
/// Calls the <see cref="IActiveElement.OnInsertionCompleted"/> method on all registered active elements
/// and raises the <see cref="InsertionCompleted"/> event.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate",
Justification="There is an event and this method is raising it.")]
public void RaiseInsertionCompleted()
{
foreach (IActiveElement element in registeredElements) {
element.OnInsertionCompleted();
}
if (InsertionCompleted != null)
InsertionCompleted(this, EventArgs.Empty);
insertionCompleted = true;
if (registeredElements.Count == 0) {
// deactivate immediately if there are no interactive elements
Deactivate();
} else {
// register Escape key handler
TextArea.KeyDown += TextArea_KeyDown;
}
}
void TextArea_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Escape) {
Deactivate();
e.Handled = true;
}
}
/// <summary>
/// Occurs when the all snippet elements have been inserted.
/// </summary>
public event EventHandler InsertionCompleted;
/// <summary>
/// Calls the <see cref="IActiveElement.Deactivate"/> method on all registered active elements.
/// </summary>
public void Deactivate()
{
if (!insertionCompleted)
throw new InvalidOperationException("Cannot call Deactivate() until RaiseInsertionCompleted() has finished.");
TextArea.KeyDown -= TextArea_KeyDown;
foreach (IActiveElement element in registeredElements) {
element.Deactivate();
}
if (Deactivated != null)
Deactivated(this, EventArgs.Empty);
}
/// <summary>
/// Occurs when the interactive mode is deactivated.
/// </summary>
public event EventHandler Deactivated;
}
}

36
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Snippets/Snippet.cs

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using ICSharpCode.AvalonEdit.Editing;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Snippets
{
/// <summary>
/// A code snippet that can be inserted into the text editor.
/// </summary>
[Serializable]
public class Snippet : SnippetContainerElement
{
/// <summary>
/// Inserts the snippet into the text area.
/// </summary>
public void Insert(TextArea textArea)
{
if (textArea == null)
throw new ArgumentNullException("textArea");
Freeze();
InsertionContext context = new InsertionContext(textArea, textArea.Caret.Offset);
using (context.Document.RunUpdate()) {
Insert(context);
context.RaiseInsertionCompleted();
}
}
}
}

96
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Snippets/SnippetBoundElement.cs

@ -0,0 +1,96 @@ @@ -0,0 +1,96 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Snippets
{
/// <summary>
/// An element that binds to a <see cref="SnippetReplaceableTextElement"/> and displays the same text.
/// </summary>
[Serializable]
public class SnippetBoundElement : SnippetElement
{
SnippetReplaceableTextElement targetElement;
/// <summary>
/// Gets/Sets the target element.
/// </summary>
public SnippetReplaceableTextElement TargetElement {
get { return targetElement; }
set {
CheckBeforeMutation();
targetElement = value;
}
}
/// <summary>
/// Converts the text before copying it.
/// </summary>
public virtual string ConvertText(string input)
{
return input;
}
/// <inheritdoc/>
public override void Insert(InsertionContext context)
{
if (targetElement != null) {
int start = context.InsertionPosition;
string inputText = targetElement.Text;
if (inputText != null) {
context.InsertText(ConvertText(inputText));
}
int end = context.InsertionPosition;
AnchorSegment segment = new AnchorSegment(context.Document, start, end - start);
context.RegisterActiveElement(this, new BoundActiveElement(context, targetElement, this, segment));
}
}
}
sealed class BoundActiveElement : IActiveElement
{
InsertionContext context;
SnippetReplaceableTextElement targetSnippetElement;
SnippetBoundElement boundElement;
IReplaceableActiveElement targetElement;
AnchorSegment segment;
public BoundActiveElement(InsertionContext context, SnippetReplaceableTextElement targetSnippetElement, SnippetBoundElement boundElement, AnchorSegment segment)
{
this.context = context;
this.targetSnippetElement = targetSnippetElement;
this.boundElement = boundElement;
this.segment = segment;
}
public void OnInsertionCompleted()
{
targetElement = context.GetActiveElement(targetSnippetElement) as IReplaceableActiveElement;
if (targetElement != null) {
targetElement.TextChanged += targetElement_TextChanged;
}
}
void targetElement_TextChanged(object sender, EventArgs e)
{
int offset = segment.Offset;
int length = segment.Length;
string text = boundElement.ConvertText(targetElement.Text);
context.Document.Replace(offset, length, text);
if (length == 0) {
// replacing an empty anchor segment with text won't enlarge it, so we have to recreate it
segment = new AnchorSegment(context.Document, offset, text.Length);
}
}
public void Deactivate()
{
}
}
}

28
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Snippets/SnippetCaretElement.cs

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Snippets
{
/// <summary>
/// Sets the caret position after interactive mode has finished.
/// </summary>
public class SnippetCaretElement : SnippetElement
{
/// <inheritdoc/>
public override void Insert(InsertionContext context)
{
TextAnchor pos = context.Document.CreateAnchor(context.InsertionPosition);
pos.SurviveDeletion = true;
context.Deactivated += delegate {
context.TextArea.Caret.Offset = pos.Offset;
};
}
}
}

45
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Snippets/SnippetContainerElement.cs

@ -0,0 +1,45 @@ @@ -0,0 +1,45 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using ICSharpCode.AvalonEdit.Editing;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Snippets
{
/// <summary>
/// A snippet element that has sub-elements.
/// </summary>
[Serializable]
public class SnippetContainerElement : SnippetElement
{
FreezableNullSafeCollection<SnippetElement> elements = new FreezableNullSafeCollection<SnippetElement>();
/// <summary>
/// Gets the list of child elements.
/// </summary>
public IList<SnippetElement> Elements {
get { return elements; }
}
/// <inheritdoc/>
protected override void FreezeInternal()
{
elements.Freeze();
base.FreezeInternal();
}
/// <inheritdoc/>
public override void Insert(InsertionContext context)
{
foreach (SnippetElement e in this.Elements) {
e.Insert(context);
}
}
}
}

66
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Snippets/SnippetElement.cs

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using ICSharpCode.AvalonEdit.Editing;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Snippets
{
/// <summary>
/// An element inside a snippet.
/// </summary>
[Serializable]
public abstract class SnippetElement : IFreezable
{
// TODO: think about removing IFreezable, it may not be required after all
#region IFreezable infrastructure
bool isFrozen;
/// <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()
{
if (!isFrozen) {
FreezeInternal();
isFrozen = true;
}
}
/// <summary>
/// Override this method to freeze child elements.
/// </summary>
protected virtual void FreezeInternal()
{
}
/// <summary>
/// Throws an exception if this instance is frozen.
/// </summary>
protected void CheckBeforeMutation()
{
if (isFrozen)
throw new InvalidOperationException("Cannot mutate frozen " + GetType().Name);
}
#endregion
/// <summary>
/// Performs insertion of the snippet.
/// </summary>
public abstract void Insert(InsertionContext context);
}
}

117
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Snippets/SnippetReplaceableTextElement.cs

@ -0,0 +1,117 @@ @@ -0,0 +1,117 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Windows;
using System.Windows.Threading;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Snippets
{
/// <summary>
/// Text element that is supposed to be replaced by the user.
/// Will register an <see cref="IReplaceableActiveElement"/>.
/// </summary>
[Serializable]
public class SnippetReplaceableTextElement : SnippetTextElement
{
/// <inheritdoc/>
public override void Insert(InsertionContext context)
{
int start = context.InsertionPosition;
base.Insert(context);
int end = context.InsertionPosition;
context.RegisterActiveElement(this, new ReplaceableActiveElement(context, start, end));
}
}
/// <summary>
/// Interface for active element registered by <see cref="SnippetReplaceableTextElement"/>.
/// </summary>
public interface IReplaceableActiveElement : IActiveElement
{
/// <summary>
/// Gets the current text inside the element.
/// </summary>
string Text { get; }
/// <summary>
/// Occurs when the text inside the element changes.
/// </summary>
event EventHandler TextChanged;
}
sealed class ReplaceableActiveElement : IReplaceableActiveElement, IWeakEventListener
{
readonly InsertionContext context;
readonly int startOffset, endOffset;
TextAnchor start, end;
public ReplaceableActiveElement(InsertionContext context, int startOffset, int endOffset)
{
this.context = context;
this.startOffset = startOffset;
this.endOffset = endOffset;
}
void AnchorDeleted(object sender, EventArgs e)
{
context.Deactivate();
}
public void OnInsertionCompleted()
{
// anchors must be created in OnInsertionCompleted because they should move only
// due to user insertions, not due to insertions of other snippet parts
start = context.Document.CreateAnchor(startOffset);
start.MovementType = AnchorMovementType.BeforeInsertion;
end = context.Document.CreateAnchor(endOffset);
end.MovementType = AnchorMovementType.AfterInsertion;
start.Deleted += AnchorDeleted;
end.Deleted += AnchorDeleted;
// Be careful with references from the document to the editing/snippet layer - use weak events
// to prevent memory leaks when text areas get dropped from the UI while the snippet is active.
// The InsertionContext will keep us alive as long as the snippet is in interactive mode.
TextDocumentWeakEventManager.TextChanged.AddListener(context.Document, this);
this.Text = GetText();
}
public void Deactivate()
{
TextDocumentWeakEventManager.TextChanged.RemoveListener(context.Document, this);
}
public string Text { get; private set; }
string GetText()
{
if (start.IsDeleted || end.IsDeleted)
return string.Empty;
else
return context.Document.GetText(start.Offset, Math.Max(0, end.Offset - start.Offset));
}
public event EventHandler TextChanged;
bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
if (managerType == typeof(TextDocumentWeakEventManager.TextChanged)) {
string newText = GetText();
if (this.Text != newText) {
this.Text = newText;
if (TextChanged != null)
TextChanged(this, e);
}
return true;
}
return false;
}
}
}

39
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Snippets/SnippetTextElement.cs

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Snippets
{
/// <summary>
/// Represents a text element in a snippet.
/// </summary>
[Serializable]
public class SnippetTextElement : SnippetElement
{
string text;
/// <summary>
/// The text to be inserted.
/// </summary>
public string Text {
get { return text; }
set {
CheckBeforeMutation();
text = value;
}
}
/// <inheritdoc/>
public override void Insert(InsertionContext context)
{
if (text != null)
context.InsertText(text);
}
}
}

72
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Utils/IFreezable.cs

@ -0,0 +1,72 @@ @@ -0,0 +1,72 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
namespace ICSharpCode.AvalonEdit.Utils
{
interface IFreezable
{
/// <summary>
/// Gets if this instance is frozen. Frozen instances are immutable and thus thread-safe.
/// </summary>
bool IsFrozen { get; }
/// <summary>
/// Freezes this instance.
/// </summary>
void Freeze();
}
[Serializable]
sealed class FreezableNullSafeCollection<T> : NullSafeCollection<T>, IFreezable where T : class, IFreezable
{
bool isFrozen;
public bool IsFrozen { get { return isFrozen; } }
public void Freeze()
{
if (!isFrozen) {
foreach (T item in this) {
item.Freeze();
}
isFrozen = true;
}
}
void CheckBeforeMutation()
{
if (isFrozen)
throw new InvalidOperationException("Cannot mutate frozen " + GetType().Name);
}
protected override void ClearItems()
{
this.CheckBeforeMutation();
base.ClearItems();
}
protected override void InsertItem(int index, T item)
{
this.CheckBeforeMutation();
base.InsertItem(index, item);
}
protected override void RemoveItem(int index)
{
this.CheckBeforeMutation();
base.RemoveItem(index);
}
protected override void SetItem(int index, T item)
{
this.CheckBeforeMutation();
base.SetItem(index, item);
}
}
}
Loading…
Cancel
Save