// 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.Windows; using ICSharpCode.NRefactory.Editor; using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Editing; namespace ICSharpCode.AvalonEdit.Snippets { /// /// Represents the context of a snippet insertion. /// public class InsertionContext : IWeakEventListener { enum Status { Insertion, RaisingInsertionCompleted, Interactive, RaisingDeactivated, Deactivated } Status currentStatus = Status.Insertion; /// /// Creates a new InsertionContext instance. /// public InsertionContext(TextArea textArea, int insertionPosition) { if (textArea == null) throw new ArgumentNullException("textArea"); this.TextArea = textArea; this.Document = textArea.Document; this.SelectedText = textArea.Selection.GetText(); this.InsertionPosition = insertionPosition; this.startPosition = 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.Tab = textArea.Options.IndentationString; this.LineTerminator = TextUtilities.GetNewLineFromDocument(this.Document, startLine.LineNumber); } /// /// Gets the text area. /// public TextArea TextArea { get; private set; } /// /// Gets the text document. /// public ICSharpCode.AvalonEdit.Document.TextDocument Document { get; private set; } /// /// Gets the text that was selected before the insertion of the snippet. /// public string SelectedText { get; private set; } /// /// Gets the indentation at the insertion position. /// public string Indentation { get; private set; } /// /// Gets the indentation string for a single indentation level. /// public string Tab { get; private set; } /// /// Gets the line terminator at the insertion position. /// public string LineTerminator { get; private set; } /// /// Gets/Sets the insertion position. /// public int InsertionPosition { get; set; } readonly int startPosition; ICSharpCode.AvalonEdit.Document.AnchorSegment wholeSnippetAnchor; bool deactivateIfSnippetEmpty; /// /// Gets the start position of the snippet insertion. /// public int StartPosition { get { if (wholeSnippetAnchor != null) return wholeSnippetAnchor.Offset; else return startPosition; } } /// /// Inserts text at the insertion position and advances the insertion position. /// This method will add the current indentation to every line in and will /// replace newlines with the expected newline for the document. /// public void InsertText(string text) { if (text == null) throw new ArgumentNullException("text"); if (currentStatus != Status.Insertion) throw new InvalidOperationException(); text = text.Replace("\t", this.Tab); using (this.Document.RunUpdate()) { 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 elementMap = new Dictionary(); List registeredElements = new List(); /// /// Registers an active element. Elements should be registered during insertion and will be called back /// when insertion has completed. /// /// The snippet element that created the active element. /// The active element. public void RegisterActiveElement(SnippetElement owner, IActiveElement element) { if (owner == null) throw new ArgumentNullException("owner"); if (element == null) throw new ArgumentNullException("element"); if (currentStatus != Status.Insertion) throw new InvalidOperationException(); elementMap.Add(owner, element); registeredElements.Add(element); } /// /// Returns the active element belonging to the specified snippet element, or null if no such active element is found. /// 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; } /// /// Gets the list of active elements. /// public IEnumerable ActiveElements { get { return registeredElements; } } /// /// Calls the method on all registered active elements /// and raises the event. /// /// The EventArgs to use [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", Justification="There is an event and this method is raising it.")] public void RaiseInsertionCompleted(EventArgs e) { if (currentStatus != Status.Insertion) throw new InvalidOperationException(); if (e == null) e = EventArgs.Empty; currentStatus = Status.RaisingInsertionCompleted; int endPosition = this.InsertionPosition; this.wholeSnippetAnchor = new AnchorSegment(Document, startPosition, endPosition - startPosition); TextDocumentWeakEventManager.UpdateFinished.AddListener(Document, this); deactivateIfSnippetEmpty = (endPosition != startPosition); foreach (IActiveElement element in registeredElements) { element.OnInsertionCompleted(); } if (InsertionCompleted != null) InsertionCompleted(this, e); currentStatus = Status.Interactive; if (registeredElements.Count == 0) { // deactivate immediately if there are no interactive elements Deactivate(new SnippetEventArgs(DeactivateReason.NoActiveElements)); } else { myInputHandler = new SnippetInputHandler(this); // disable existing snippet input handlers - there can be only 1 active snippet foreach (TextAreaStackedInputHandler h in TextArea.StackedInputHandlers) { if (h is SnippetInputHandler) TextArea.PopStackedInputHandler(h); } TextArea.PushStackedInputHandler(myInputHandler); } } SnippetInputHandler myInputHandler; /// /// Occurs when the all snippet elements have been inserted. /// public event EventHandler InsertionCompleted; /// /// Calls the method on all registered active elements. /// /// The EventArgs to use public void Deactivate(SnippetEventArgs e) { if (currentStatus == Status.Deactivated || currentStatus == Status.RaisingDeactivated) return; if (currentStatus != Status.Interactive) throw new InvalidOperationException("Cannot call Deactivate() until RaiseInsertionCompleted() has finished."); if (e == null) e = new SnippetEventArgs(DeactivateReason.Unknown); TextDocumentWeakEventManager.UpdateFinished.RemoveListener(Document, this); currentStatus = Status.RaisingDeactivated; TextArea.PopStackedInputHandler(myInputHandler); foreach (IActiveElement element in registeredElements) { element.Deactivate(e); } if (Deactivated != null) Deactivated(this, e); currentStatus = Status.Deactivated; } /// /// Occurs when the interactive mode is deactivated. /// public event EventHandler Deactivated; bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) { return ReceiveWeakEvent(managerType, sender, e); } /// protected virtual bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) { if (managerType == typeof(TextDocumentWeakEventManager.UpdateFinished)) { // Deactivate if snippet is deleted. This is necessary for correctly leaving interactive // mode if Undo is pressed after a snippet insertion. if (wholeSnippetAnchor.Length == 0 && deactivateIfSnippetEmpty) Deactivate(new SnippetEventArgs(DeactivateReason.Deleted)); return true; } return false; } /// /// Adds existing segments as snippet elements. /// public void Link(ISegment mainElement, ISegment[] boundElements) { var main = new SnippetReplaceableTextElement { Text = Document.GetText(mainElement) }; RegisterActiveElement(main, new ReplaceableActiveElement(this, mainElement.Offset, mainElement.EndOffset)); foreach (var boundElement in boundElements) { var bound = new SnippetBoundElement { TargetElement = main }; var start = Document.CreateAnchor(boundElement.Offset); start.MovementType = AnchorMovementType.BeforeInsertion; start.SurviveDeletion = true; var end = Document.CreateAnchor(boundElement.EndOffset); end.MovementType = AnchorMovementType.BeforeInsertion; end.SurviveDeletion = true; RegisterActiveElement(bound, new BoundActiveElement(this, main, bound, new AnchorSegment(start, end))); } } } }