// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNet.Razor.Parser.SyntaxTree; using Microsoft.AspNet.Razor.Text; using Microsoft.AspNet.Razor.Tokenizer.Symbols; namespace Microsoft.AspNet.Razor.Editor { // Manages edits to a span public class SpanEditHandler { private static readonly int TypeHashCode = typeof(SpanEditHandler).GetHashCode(); public SpanEditHandler(Func> tokenizer) : this(tokenizer, AcceptedCharacters.Any) { } public SpanEditHandler(Func> tokenizer, AcceptedCharacters accepted) { AcceptedCharacters = accepted; Tokenizer = tokenizer; } public AcceptedCharacters AcceptedCharacters { get; set; } /// /// Provides a set of hints to editors which may be manipulating the document in which this span is located. /// public EditorHints EditorHints { get; set; } public Func> Tokenizer { get; set; } public static SpanEditHandler CreateDefault() { return CreateDefault(s => Enumerable.Empty()); } public static SpanEditHandler CreateDefault(Func> tokenizer) { return new SpanEditHandler(tokenizer); } public virtual EditResult ApplyChange(Span target, TextChange change) { return ApplyChange(target, change, force: false); } public virtual EditResult ApplyChange(Span target, TextChange change, bool force) { var result = PartialParseResult.Accepted; var normalized = change.Normalize(); if (!force) { result = CanAcceptChange(target, normalized); } // If the change is accepted then apply the change if ((result & PartialParseResult.Accepted) == PartialParseResult.Accepted) { return new EditResult(result, UpdateSpan(target, normalized)); } return new EditResult(result, new SpanBuilder(target)); } public virtual bool OwnsChange(Span target, TextChange change) { var end = target.Start.AbsoluteIndex + target.Length; var changeOldEnd = change.OldPosition + change.OldLength; return change.OldPosition >= target.Start.AbsoluteIndex && (changeOldEnd < end || (changeOldEnd == end && AcceptedCharacters != AcceptedCharacters.None)); } protected virtual PartialParseResult CanAcceptChange(Span target, TextChange normalizedChange) { return PartialParseResult.Rejected; } protected virtual SpanBuilder UpdateSpan(Span target, TextChange normalizedChange) { var newContent = normalizedChange.ApplyChange(target); var newSpan = new SpanBuilder(target); newSpan.ClearSymbols(); foreach (ISymbol sym in Tokenizer(newContent)) { sym.OffsetStart(target.Start); newSpan.Accept(sym); } if (target.Next != null) { var newEnd = SourceLocationTracker.CalculateNewLocation(target.Start, newContent); target.Next.ChangeStart(newEnd); } return newSpan; } protected internal static bool IsAtEndOfFirstLine(Span target, TextChange change) { var endOfFirstLine = target.Content.IndexOfAny(new char[] { (char)0x000d, (char)0x000a, (char)0x2028, (char)0x2029 }); return (endOfFirstLine == -1 || (change.OldPosition - target.Start.AbsoluteIndex) <= endOfFirstLine); } /// /// Returns true if the specified change is an insertion of text at the end of this span. /// protected internal static bool IsEndInsertion(Span target, TextChange change) { return change.IsInsert && IsAtEndOfSpan(target, change); } /// /// Returns true if the specified change is an insertion of text at the end of this span. /// protected internal static bool IsEndDeletion(Span target, TextChange change) { return change.IsDelete && IsAtEndOfSpan(target, change); } /// /// Returns true if the specified change is a replacement of text at the end of this span. /// protected internal static bool IsEndReplace(Span target, TextChange change) { return change.IsReplace && IsAtEndOfSpan(target, change); } protected internal static bool IsAtEndOfSpan(Span target, TextChange change) { return (change.OldPosition + change.OldLength) == (target.Start.AbsoluteIndex + target.Length); } /// /// Returns the old text referenced by the change. /// /// /// If the content has already been updated by applying the change, this data will be _invalid_ /// protected internal static string GetOldText(Span target, TextChange change) { return target.Content.Substring(change.OldPosition - target.Start.AbsoluteIndex, change.OldLength); } // Is the specified span to the right of this span and immediately adjacent? internal static bool IsAdjacentOnRight(Span target, Span other) { return target.Start.AbsoluteIndex < other.Start.AbsoluteIndex && target.Start.AbsoluteIndex + target.Length == other.Start.AbsoluteIndex; } // Is the specified span to the left of this span and immediately adjacent? internal static bool IsAdjacentOnLeft(Span target, Span other) { return other.Start.AbsoluteIndex < target.Start.AbsoluteIndex && other.Start.AbsoluteIndex + other.Length == target.Start.AbsoluteIndex; } public override string ToString() { return GetType().Name + ";Accepts:" + AcceptedCharacters + ((EditorHints == EditorHints.None) ? string.Empty : (";Hints: " + EditorHints.ToString())); } public override bool Equals(object obj) { var other = obj as SpanEditHandler; return other != null && GetType() == other.GetType() && AcceptedCharacters == other.AcceptedCharacters && EditorHints == other.EditorHints; } public override int GetHashCode() { // Hash code should include only immutable properties but Equals also checks the type. return TypeHashCode; } } }