// // TextPasteIndentEngine.cs // // Author: // Matej Miklečić // // Copyright (c) 2013 Matej Miklečić (matej.miklecic@gmail.com) // // 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.NRefactory.Editor; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; namespace ICSharpCode.NRefactory.CSharp { /// /// Represents a decorator of an IStateMachineIndentEngine instance /// that provides logic for text paste events. /// public class TextPasteIndentEngine : IDocumentIndentEngine, ITextPasteHandler { #region Properties /// /// An instance of IStateMachineIndentEngine which handles /// the indentation logic. /// IStateMachineIndentEngine engine; /// /// Text editor options. /// internal readonly TextEditorOptions textEditorOptions; internal readonly CSharpFormattingOptions formattingOptions; #endregion #region Constructors /// /// Creates a new TextPasteIndentEngine instance. /// /// /// An instance of to which the /// logic for indentation will be delegated. /// /// /// Text editor options for indentation. /// /// /// C# formatting options. /// public TextPasteIndentEngine(IStateMachineIndentEngine decoratedEngine, TextEditorOptions textEditorOptions, CSharpFormattingOptions formattingOptions) { this.engine = decoratedEngine; this.textEditorOptions = textEditorOptions; this.formattingOptions = formattingOptions; this.engine.EnableCustomIndentLevels = false; } #endregion #region ITextPasteHandler /// string ITextPasteHandler.FormatPlainText(int offset, string text, byte[] copyData) { if (copyData != null && copyData.Length == 1) { var strategy = TextPasteUtils.Strategies [(PasteStrategy)copyData [0]]; text = strategy.Decode(text); } engine.Update(offset); if (engine.IsInsideStringLiteral) { int idx = text.IndexOf('"'); if (idx > 0) { var o = offset; while (o < engine.Document.TextLength) { char ch = engine.Document.GetCharAt(o); engine.Push(ch); if (NewLine.IsNewLine(ch)) break; o++; if (!engine.IsInsideStringLiteral) return TextPasteUtils.StringLiteralStrategy.Encode(text); } return TextPasteUtils.StringLiteralStrategy.Encode(text.Substring(0, idx)) + text.Substring(idx); } return TextPasteUtils.StringLiteralStrategy.Encode(text); } else if (engine.IsInsideVerbatimString) { int idx = text.IndexOf('"'); if (idx > 0) { var o = offset; while (o < engine.Document.TextLength) { char ch = engine.Document.GetCharAt(o); engine.Push(ch); o++; if (!engine.IsInsideVerbatimString) return TextPasteUtils.VerbatimStringStrategy.Encode(text); } return TextPasteUtils.VerbatimStringStrategy.Encode(text.Substring(0, idx)) + text.Substring(idx); } return TextPasteUtils.VerbatimStringStrategy.Encode(text); } var line = engine.Document.GetLineByOffset(offset); var pasteAtLineStart = line.Offset == offset; var indentedText = new StringBuilder(); var curLine = new StringBuilder(); var clonedEngine = engine.Clone(); bool isNewLine = false, gotNewLine = false; for (int i = 0; i < text.Length; i++) { var ch = text [i]; if (clonedEngine.IsInsideVerbatimString || clonedEngine.IsInsideMultiLineComment) { clonedEngine.Push(ch); curLine.Append(ch); continue; } var delimiterLength = NewLine.GetDelimiterLength(ch, i + 1 < text.Length ? text[i + 1] : ' '); if (delimiterLength > 0) { isNewLine = true; if (gotNewLine || pasteAtLineStart) { if (curLine.Length > 0 || formattingOptions.EmptyLineFormatting == EmptyLineFormatting.Indent) indentedText.Append(clonedEngine.ThisLineIndent); } indentedText.Append(curLine); indentedText.Append(textEditorOptions.EolMarker); curLine.Length = 0; gotNewLine = true; i += delimiterLength - 1; // textEditorOptions.EolMarker[0] is the newLineChar used by the indentation engine. clonedEngine.Push(textEditorOptions.EolMarker[0]); } else { if (isNewLine) { if (ch == '\t' || ch == ' ') { clonedEngine.Push(ch); continue; } isNewLine = false; } curLine.Append(ch); clonedEngine.Push(ch); } if (clonedEngine.IsInsideVerbatimString || clonedEngine.IsInsideMultiLineComment && !(clonedEngine.LineBeganInsideVerbatimString || clonedEngine.LineBeganInsideMultiLineComment)) { if (gotNewLine) { if (curLine.Length > 0 || formattingOptions.EmptyLineFormatting == EmptyLineFormatting.Indent) indentedText.Append(clonedEngine.ThisLineIndent); } pasteAtLineStart = false; indentedText.Append(curLine); curLine.Length = 0; gotNewLine = false; continue; } } if (gotNewLine && (!pasteAtLineStart || curLine.Length > 0)) { indentedText.Append(clonedEngine.ThisLineIndent); } if (curLine.Length > 0) { indentedText.Append(curLine); } return indentedText.ToString(); } /// byte[] ITextPasteHandler.GetCopyData(ISegment segment) { engine.Update(segment.Offset); if (engine.IsInsideStringLiteral) { return new[] { (byte)PasteStrategy.StringLiteral }; } else if (engine.IsInsideVerbatimString) { return new[] { (byte)PasteStrategy.VerbatimString }; } return null; } #endregion #region IDocumentIndentEngine /// public IDocument Document { get { return engine.Document; } } /// public string ThisLineIndent { get { return engine.ThisLineIndent; } } /// public string NextLineIndent { get { return engine.NextLineIndent; } } /// public string CurrentIndent { get { return engine.CurrentIndent; } } /// public bool NeedsReindent { get { return engine.NeedsReindent; } } /// public int Offset { get { return engine.Offset; } } /// public TextLocation Location { get { return engine.Location; } } /// public bool EnableCustomIndentLevels { get { return engine.EnableCustomIndentLevels; } set { engine.EnableCustomIndentLevels = value; } } /// public void Push(char ch) { engine.Push(ch); } /// public void Reset() { engine.Reset(); } /// public void Update(int offset) { engine.Update(offset); } #endregion #region IClonable public IDocumentIndentEngine Clone() { return new TextPasteIndentEngine(engine, textEditorOptions, formattingOptions); } object ICloneable.Clone() { return Clone(); } #endregion } /// /// Types of text-paste strategies. /// public enum PasteStrategy : byte { PlainText = 0, StringLiteral = 1, VerbatimString = 2 } /// /// Defines some helper methods for dealing with text-paste events. /// public static class TextPasteUtils { /// /// Collection of text-paste strategies. /// public static TextPasteStrategies Strategies = new TextPasteStrategies(); /// /// The interface for a text-paste strategy. /// public interface IPasteStrategy { /// /// Formats the given text according with this strategy rules. /// /// /// The text to format. /// /// /// Formatted text. /// string Encode(string text); /// /// Converts text formatted according with this strategy rules /// to its original form. /// /// /// Formatted text to convert. /// /// /// Original form of the given formatted text. /// string Decode(string text); /// /// Type of this strategy. /// PasteStrategy Type { get; } } /// /// Wrapper that discovers all defined text-paste strategies and defines a way /// to easily access them through their type. /// public sealed class TextPasteStrategies { /// /// Collection of discovered text-paste strategies. /// IDictionary strategies; /// /// Uses reflection to find all types derived from /// and adds an instance of each strategy to . /// public TextPasteStrategies() { strategies = Assembly.GetExecutingAssembly() .GetTypes() .Where(t => typeof(IPasteStrategy).IsAssignableFrom(t) && t.IsClass) .Select(t => (IPasteStrategy)t.GetProperty("Instance").GetValue(null, null)) .ToDictionary(s => s.Type); } /// /// Checks if there is a strategy of the given type and returns it. /// /// /// Type of the strategy instance. /// /// /// A strategy instance of the requested type, /// or if it wasn't found. /// public IPasteStrategy this [PasteStrategy strategy] { get { if (strategies.ContainsKey(strategy)) { return strategies [strategy]; } return DefaultStrategy; } } } /// /// Doesn't do any formatting. Serves as the default strategy. /// public class PlainTextPasteStrategy : IPasteStrategy { #region Singleton public static IPasteStrategy Instance { get { return instance ?? (instance = new PlainTextPasteStrategy()); } } static PlainTextPasteStrategy instance; protected PlainTextPasteStrategy() { } #endregion /// public string Encode(string text) { return text; } /// public string Decode(string text) { return text; } /// public PasteStrategy Type { get { return PasteStrategy.PlainText; } } } /// /// Escapes chars in the given text so that they don't /// break a valid string literal. /// public class StringLiteralPasteStrategy : IPasteStrategy { #region Singleton public static IPasteStrategy Instance { get { return instance ?? (instance = new StringLiteralPasteStrategy()); } } static StringLiteralPasteStrategy instance; protected StringLiteralPasteStrategy() { } #endregion /// public string Encode(string text) { return CSharpOutputVisitor.ConvertString(text); } /// public string Decode(string text) { var result = new StringBuilder(); bool isEscaped = false; for (int i = 0; i < text.Length; i++) { var ch = text[i]; if (isEscaped) { switch (ch) { case 'a': result.Append('\a'); break; case 'b': result.Append('\b'); break; case 'n': result.Append('\n'); break; case 't': result.Append('\t'); break; case 'v': result.Append('\v'); break; case 'r': result.Append('\r'); break; case '\\': result.Append('\\'); break; case 'f': result.Append('\f'); break; case '0': result.Append(0); break; case '"': result.Append('"'); break; case '\'': result.Append('\''); break; case 'x': char r; if (TryGetHex(text, -1, ref i, out r)) { result.Append(r); break; } goto default; case 'u': if (TryGetHex(text, 4, ref i, out r)) { result.Append(r); break; } goto default; case 'U': if (TryGetHex(text, 8, ref i, out r)) { result.Append(r); break; } goto default; default: result.Append('\\'); result.Append(ch); break; } isEscaped = false; continue; } if (ch != '\\') { result.Append(ch); } else { isEscaped = true; } } return result.ToString(); } static bool TryGetHex(string text, int count, ref int idx, out char r) { int i; int total = 0; int top = count != -1 ? count : 4; for (i = 0; i < top; i++) { int c = text[idx + 1 + i]; if (c >= '0' && c <= '9') c = (int) c - (int) '0'; else if (c >= 'A' && c <= 'F') c = (int) c - (int) 'A' + 10; else if (c >= 'a' && c <= 'f') c = (int) c - (int) 'a' + 10; else { r = '\0'; return false; } total = (total * 16) + c; } if (top == 8) { if (total > 0x0010FFFF) { r = '\0'; return false; } if (total >= 0x00010000) total = ((total - 0x00010000) / 0x0400 + 0xD800); } r = (char)total; idx += top; return true; } /// public PasteStrategy Type { get { return PasteStrategy.StringLiteral; } } } /// /// Escapes chars in the given text so that they don't /// break a valid verbatim string. /// public class VerbatimStringPasteStrategy : IPasteStrategy { #region Singleton public static IPasteStrategy Instance { get { return instance ?? (instance = new VerbatimStringPasteStrategy()); } } static VerbatimStringPasteStrategy instance; protected VerbatimStringPasteStrategy() { } #endregion static readonly Dictionary> encodeReplace = new Dictionary> { { '\"', "\"\"" }, }; /// public string Encode(string text) { return string.Concat(text.SelectMany(c => encodeReplace.ContainsKey(c) ? encodeReplace [c] : new[] { c })); } /// public string Decode(string text) { bool isEscaped = false; return string.Concat(text.Where(c => !(isEscaped = !isEscaped && c == '"'))); } /// public PasteStrategy Type { get { return PasteStrategy.VerbatimString; } } } /// /// The default text-paste strategy. /// public static IPasteStrategy DefaultStrategy = PlainTextPasteStrategy.Instance; /// /// String literal text-paste strategy. /// public static IPasteStrategy StringLiteralStrategy = StringLiteralPasteStrategy.Instance; /// /// Verbatim string text-paste strategy. /// public static IPasteStrategy VerbatimStringStrategy = VerbatimStringPasteStrategy.Instance; } }