// // // // // $Revision$ // using System; using System.Collections.Generic; using System.Diagnostics; namespace ICSharpCode.TextEditor.Document { internal sealed class LineManager { LineSegmentTree lineCollection = new LineSegmentTree(); IDocument document; IHighlightingStrategy highlightingStrategy; public IList LineSegmentCollection { get { return lineCollection; } } public int TotalNumberOfLines { get { return lineCollection.Count; } } public IHighlightingStrategy HighlightingStrategy { get { return highlightingStrategy; } set { if (highlightingStrategy != value) { highlightingStrategy = value; if (highlightingStrategy != null) { highlightingStrategy.MarkTokens(document); } } } } public LineManager(IDocument document, IHighlightingStrategy highlightingStrategy) { this.document = document; this.highlightingStrategy = highlightingStrategy; } public int GetLineNumberForOffset(int offset) { return GetLineSegmentForOffset(offset).LineNumber; } public LineSegment GetLineSegmentForOffset(int offset) { return lineCollection.GetByOffset(offset); } public LineSegment GetLineSegment(int lineNr) { return lineCollection[lineNr]; } public void Insert(int offset, string text) { Replace(offset, 0, text); } public void Remove(int offset, int length) { Replace(offset, length, String.Empty); } public void Replace(int offset, int length, string text) { Debug.WriteLine("Replace offset="+offset+" length="+length+" text.Length="+text.Length); int lineStart = GetLineNumberForOffset(offset); int oldNumberOfLines = this.TotalNumberOfLines; DeferredEventList deferredEventList = new DeferredEventList(); RemoveInternal(ref deferredEventList, offset, length); int numberOfLinesAfterRemoving = this.TotalNumberOfLines; if (!string.IsNullOrEmpty(text)) { InsertInternal(offset, text); } // #if DEBUG // Console.WriteLine("New line collection:"); // Console.WriteLine(lineCollection.GetTreeAsString()); // Console.WriteLine("New text:"); // Console.WriteLine("'" + document.TextContent + "'"); // #endif // Only fire events after RemoveInternal+InsertInternal finished completely: // Otherwise we would expose inconsistent state to the event handlers. RunHighlighter(lineStart, 1 + Math.Max(0, this.TotalNumberOfLines - numberOfLinesAfterRemoving)); if (deferredEventList.removedLines != null) { foreach (LineSegment ls in deferredEventList.removedLines) OnLineDeleted(new LineEventArgs(document, ls)); } deferredEventList.RaiseEvents(); if (this.TotalNumberOfLines != oldNumberOfLines) { OnLineCountChanged(new LineCountChangeEventArgs(document, lineStart, this.TotalNumberOfLines - oldNumberOfLines)); } } void RemoveInternal(ref DeferredEventList deferredEventList, int offset, int length) { Debug.Assert(length >= 0); if (length == 0) return; LineSegmentTree.Enumerator it = lineCollection.GetEnumeratorForOffset(offset); LineSegment startSegment = it.Current; int startSegmentOffset = startSegment.Offset; if (offset + length < startSegmentOffset + startSegment.TotalLength) { // just removing a part of this line segment startSegment.RemovedLinePart(ref deferredEventList, offset - startSegmentOffset, length); SetSegmentLength(startSegment, startSegment.TotalLength - length); return; } // merge startSegment with another line segment because startSegment's delimiter was deleted // possibly remove lines in between if multiple delimiters were deleted int charactersRemovedInStartLine = startSegmentOffset + startSegment.TotalLength - offset; Debug.Assert(charactersRemovedInStartLine > 0); startSegment.RemovedLinePart(ref deferredEventList, offset - startSegmentOffset, charactersRemovedInStartLine); LineSegment endSegment = lineCollection.GetByOffset(offset + length); if (endSegment == startSegment) { // special case: we are removing a part of the last line up to the // end of the document SetSegmentLength(startSegment, startSegment.TotalLength - length); return; } int endSegmentOffset = endSegment.Offset; int charactersLeftInEndLine = endSegmentOffset + endSegment.TotalLength - (offset + length); endSegment.RemovedLinePart(ref deferredEventList, 0, endSegment.TotalLength - charactersLeftInEndLine); startSegment.MergedWith(endSegment, offset - startSegmentOffset); SetSegmentLength(startSegment, startSegment.TotalLength - charactersRemovedInStartLine + charactersLeftInEndLine); startSegment.DelimiterLength = endSegment.DelimiterLength; // remove all segments between startSegment (excl.) and endSegment (incl.) it.MoveNext(); LineSegment segmentToRemove; do { segmentToRemove = it.Current; it.MoveNext(); lineCollection.RemoveSegment(segmentToRemove); segmentToRemove.Deleted(ref deferredEventList); } while (segmentToRemove != endSegment); } void InsertInternal(int offset, string text) { LineSegment segment = lineCollection.GetByOffset(offset); DelimiterSegment ds = NextDelimiter(text, 0); if (ds == null) { // no newline is being inserted, all text is inserted in a single line segment.InsertedLinePart(offset - segment.Offset, text.Length); SetSegmentLength(segment, segment.TotalLength + text.Length); return; } LineSegment firstLine = segment; firstLine.InsertedLinePart(offset - firstLine.Offset, ds.Offset); int lastDelimiterEnd = 0; while (ds != null) { // split line segment at line delimiter int lineBreakOffset = offset + ds.Offset + ds.Length; int segmentOffset = segment.Offset; int lengthAfterInsertionPos = segmentOffset + segment.TotalLength - (offset + lastDelimiterEnd); lineCollection.SetSegmentLength(segment, lineBreakOffset - segmentOffset); LineSegment newSegment = lineCollection.InsertSegmentAfter(segment, lengthAfterInsertionPos); segment.DelimiterLength = ds.Length; segment = newSegment; lastDelimiterEnd = ds.Offset + ds.Length; ds = NextDelimiter(text, lastDelimiterEnd); } firstLine.SplitTo(segment); // insert rest after last delimiter if (lastDelimiterEnd != text.Length) { segment.InsertedLinePart(0, text.Length - lastDelimiterEnd); SetSegmentLength(segment, segment.TotalLength + text.Length - lastDelimiterEnd); } } void SetSegmentLength(LineSegment segment, int newTotalLength) { int delta = newTotalLength - segment.TotalLength; if (delta != 0) { lineCollection.SetSegmentLength(segment, newTotalLength); OnLineLengthChanged(new LineLengthChangeEventArgs(document, segment, delta)); } } void RunHighlighter(int firstLine, int lineCount) { if (highlightingStrategy != null) { List markLines = new List(); LineSegmentTree.Enumerator it = lineCollection.GetEnumeratorForIndex(firstLine); for (int i = 0; i < lineCount && it.IsValid; i++) { markLines.Add(it.Current); it.MoveNext(); } highlightingStrategy.MarkTokens(document, markLines); } } public void SetContent(string text) { lineCollection.Clear(); if (text != null) { Replace(0, 0, text); } } public int GetVisibleLine(int logicalLineNumber) { if (!document.TextEditorProperties.EnableFolding) { return logicalLineNumber; } int visibleLine = 0; int foldEnd = 0; List foldings = document.FoldingManager.GetTopLevelFoldedFoldings(); foreach (FoldMarker fm in foldings) { if (fm.StartLine >= logicalLineNumber) { break; } if (fm.StartLine >= foldEnd) { visibleLine += fm.StartLine - foldEnd; if (fm.EndLine > logicalLineNumber) { return visibleLine; } foldEnd = fm.EndLine; } } // Debug.Assert(logicalLineNumber >= foldEnd); visibleLine += logicalLineNumber - foldEnd; return visibleLine; } public int GetFirstLogicalLine(int visibleLineNumber) { if (!document.TextEditorProperties.EnableFolding) { return visibleLineNumber; } int v = 0; int foldEnd = 0; List foldings = document.FoldingManager.GetTopLevelFoldedFoldings(); foreach (FoldMarker fm in foldings) { if (fm.StartLine >= foldEnd) { if (v + fm.StartLine - foldEnd >= visibleLineNumber) { break; } v += fm.StartLine - foldEnd; foldEnd = fm.EndLine; } } // help GC foldings.Clear(); foldings = null; return foldEnd + visibleLineNumber - v; } public int GetLastLogicalLine(int visibleLineNumber) { if (!document.TextEditorProperties.EnableFolding) { return visibleLineNumber; } return GetFirstLogicalLine(visibleLineNumber + 1) - 1; } // TODO : speedup the next/prev visible line search // HOW? : save the foldings in a sorted list and lookup the // line numbers in this list public int GetNextVisibleLineAbove(int lineNumber, int lineCount) { int curLineNumber = lineNumber; if (document.TextEditorProperties.EnableFolding) { for (int i = 0; i < lineCount && curLineNumber < TotalNumberOfLines; ++i) { ++curLineNumber; while (curLineNumber < TotalNumberOfLines && (curLineNumber >= lineCollection.Count || !document.FoldingManager.IsLineVisible(curLineNumber))) { ++curLineNumber; } } } else { curLineNumber += lineCount; } return Math.Min(TotalNumberOfLines - 1, curLineNumber); } public int GetNextVisibleLineBelow(int lineNumber, int lineCount) { int curLineNumber = lineNumber; if (document.TextEditorProperties.EnableFolding) { for (int i = 0; i < lineCount; ++i) { --curLineNumber; while (curLineNumber >= 0 && !document.FoldingManager.IsLineVisible(curLineNumber)) { --curLineNumber; } } } else { curLineNumber -= lineCount; } return Math.Max(0, curLineNumber); } // use always the same DelimiterSegment object for the NextDelimiter DelimiterSegment delimiterSegment = new DelimiterSegment(); DelimiterSegment NextDelimiter(string text, int offset) { for (int i = offset; i < text.Length; i++) { switch (text[i]) { case '\r': if (i + 1 < text.Length) { if (text[i + 1] == '\n') { delimiterSegment.Offset = i; delimiterSegment.Length = 2; return delimiterSegment; } } #if DATACONSISTENCYTEST //Debug.Assert(false, "Found lone \\r, data consistency problems?"); #endif goto case '\n'; case '\n': delimiterSegment.Offset = i; delimiterSegment.Length = 1; return delimiterSegment; } } return null; } void OnLineCountChanged(LineCountChangeEventArgs e) { if (LineCountChanged != null) { LineCountChanged(this, e); } } void OnLineLengthChanged(LineLengthChangeEventArgs e) { if (LineLengthChanged != null) { LineLengthChanged(this, e); } } void OnLineDeleted(LineEventArgs e) { if (LineDeleted != null) { LineDeleted(this, e); } } public event EventHandler LineLengthChanged; public event EventHandler LineCountChanged; public event EventHandler LineDeleted; sealed class DelimiterSegment { internal int Offset; internal int Length; } } }