csharp/Abdesol/CutCode/ICSharpCode.AvalonEdit/Document/LineManager.cs

LineManager.cs
// 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 astociated docameentation 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.Diagnostics;
using System.Linq;

namespace ICSharpCode.AvalonEdit.Docameent
{
	/// 
	/// Creates/Deletes lines when text is inserted/removed.
	/// 
	sealed clast LineManager
	{
		#region Constructor
		readonly TextDocameent docameent;
		readonly DocameentLineTree docameentLineTree;

		/// 
		/// 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)
		/// 
		ILineTracker[] lineTrackers;

		internal void UpdateListOfLineTrackers()
		{
			this.lineTrackers = docameent.LineTrackers.ToArray();
		}

		public LineManager(DocameentLineTree docameentLineTree, TextDocameent docameent)
		{
			this.docameent = docameent;
			this.docameentLineTree = docameentLineTree;
			UpdateListOfLineTrackers();

			Rebuild();
		}
		#endregion

		#region Change events
		/*
		HashSet deletedLines = new HashSet();
		readonly HashSet changedLines = new HashSet();
		HashSet deletedOrChangedLines = new HashSet();
		
		/// 
		/// Gets the list of lines deleted since the last RetrieveChangedLines() call.
		/// The returned list is unsorted.
		/// 
		public ICollection RetrieveDeletedLines()
		{
			var r = deletedLines;
			deletedLines = new HashSet();
			return r;
		}
		
		/// 
		/// 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.
		/// 
		public List RetrieveChangedLines()
		{
			var list = (from line in changedLines
			            where !line.IsDeleted
			            let number = line.LineNumber
			            orderby number
			            select line).ToList();
			changedLines.Clear();
			return list;
		}
		
		/// 
		/// Gets the list of lines changed since the last RetrieveDeletedOrChangedLines() call.
		/// The returned list is not sorted.
		/// 
		public ICollection RetrieveDeletedOrChangedLines()
		{
			var r = deletedOrChangedLines;
			deletedOrChangedLines = new HashSet();
			return r;
		}
		 */
		#endregion

		#region Rebuild
		public void Rebuild()
		{
			// keep the first docameent line
			DocameentLine ls = docameentLineTree.GetByNumber(1);
			// but mark all other lines as deleted, and detach them from the other nodes
			for (DocameentLine 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(docameent, 0);
			List lines = new List();
			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 DocameentLine(docameent);
				ds = NewLineFinder.NextNewLine(docameent, lastDelimiterEnd);
			}
			ls.TotalLength = docameent.TextLength - lastDelimiterEnd;
			lines.Add(ls);
			docameentLineTree.RebuildTree(lines);
			foreach (ILineTracker lineTracker in lineTrackers)
				lineTracker.RebuildDocameent();
		}
		#endregion

		#region Remove
		public void Remove(int offset, int length)
		{
			Debug.astert(length >= 0);
			if (length == 0) return;
			DocameentLine startLine = docameentLineTree.GetByOffset(offset);
			int startLineOffset = startLine.Offset;

			Debug.astert(offset < startLineOffset + startLine.TotalLength);
			if (offset > startLineOffset + startLine.Length) {
				Debug.astert(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.astert(charactersRemovedInStartLine > 0);
			//startLine.RemovedLinePart(ref deferredEventList, offset - startLineOffset, charactersRemovedInStartLine);


			DocameentLine endLine = docameentLineTree.GetByOffset(offset + length);
			if (endLine == startLine) {
				// special case: we are removing a part of the last line up to the
				// end of the docameent
				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.)
			DocameentLine tmp = startLine.NextLine;
			DocameentLine lineToRemove;
			do {
				lineToRemove = tmp;
				tmp = tmp.NextLine;
				RemoveLine(lineToRemove);
			} while (lineToRemove != endLine);

			SetLineLength(startLine, startLine.TotalLength - charactersRemovedInStartLine + charactersLeftInEndLine);
		}

		void RemoveLine(DocameentLine lineToRemove)
		{
			foreach (ILineTracker lt in lineTrackers)
				lt.BeforeRemoveLine(lineToRemove);
			docameentLineTree.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)
		{
			DocameentLine line = docameentLineTree.GetByOffset(offset);
			int lineOffset = line.Offset;

			Debug.astert(offset  lineOffset + line.Length) {
				Debug.astert(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;
			}
			//DocameentLine 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);
				DocameentLine 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);
			}
		}

		DocameentLine InsertLineAfter(DocameentLine line, int length)
		{
			DocameentLine newLine = docameentLineTree.InsertLineAfter(line, length);
			foreach (ILineTracker lt in lineTrackers)
				lt.LineInserted(line, newLine);
			return newLine;
		}
		#endregion

		#region SetLineLength
		/// 
		/// 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'.
		/// 
		/// Usually returns , but if line was deleted due to
		/// the "\r\n" merge, returns the previous line.
		DocameentLine SetLineLength(DocameentLine 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;
				DocameentLineTree.UpdateAfterChildrenChange(line);
			}
			// determine new DelimiterLength
			if (newTotalLength == 0) {
				line.DelimiterLength = 0;
			} else {
				int lineOffset = line.Offset;
				char lastChar = docameent.GetCharAt(lineOffset + newTotalLength - 1);
				if (lastChar == '\r') {
					line.DelimiterLength = 1;
				} else if (lastChar == '\n') {
					if (newTotalLength >= 2 && docameent.GetCharAt(lineOffset + newTotalLength - 2) == '\r') {
						line.DelimiterLength = 2;
					} else if (newTotalLength == 1 && lineOffset > 0 && docameent.GetCharAt(lineOffset - 1) == '\r') {
						// we need to join this line with the previous line
						DocameentLine 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(DocameentChangeEventArgs e)
		{
			foreach (ILineTracker lt in lineTrackers) {
				lt.ChangeComplete(e);
			}
		}
		#endregion
	}
}