diff --git a/TODOnewNR.txt b/TODOnewNR.txt index 826d2f845d..4cbc423e6f 100644 --- a/TODOnewNR.txt +++ b/TODOnewNR.txt @@ -142,6 +142,12 @@ Functionality changes: The IUnresolvedFile is stored permanently (both in ParserService and in the IProjectContents). + Solution model: + The class 'Solution' has been replaced with the interface 'ISolution'. + The static events that report changes to the solution (e.g. project added) no longer exist on IProjectService; + instead the ISolution.Projects collection itself has a changed event. + + Text editor and document services: In SharpDevelop 4.x it was possible to use IDocument.GetService(typeof(ITextEditor)) to find the editor that presents the document. diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/DocumentHighlighter.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/DocumentHighlighter.cs index 3f867a0288..4826e9fdab 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/DocumentHighlighter.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/DocumentHighlighter.cs @@ -29,6 +29,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting readonly CompressingTreeList isValid = new CompressingTreeList((a, b) => a == b); readonly IDocument document; readonly IHighlightingDefinition definition; + readonly HighlightingEngine engine; readonly WeakLineTracker weakLineTracker; bool isHighlighting; bool isInHighlightingGroup; @@ -52,6 +53,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting throw new ArgumentNullException("definition"); this.document = document; this.definition = definition; + this.engine = new HighlightingEngine(definition.MainRuleSet); document.VerifyAccess(); weakLineTracker = WeakLineTracker.Register(document, this); InvalidateHighlighting(); @@ -68,6 +70,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting throw new ArgumentNullException("definition"); this.document = document; this.definition = definition; + this.engine = new HighlightingEngine(definition.MainRuleSet); InvalidateHighlighting(); } @@ -131,10 +134,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting public ImmutableStack InitialSpanStack { get { return initialSpanStack; } set { - if (value == null) - initialSpanStack = SpanStack.Empty; - else - initialSpanStack = value; + initialSpanStack = value ?? SpanStack.Empty; InvalidateHighlighting(); } } @@ -167,11 +167,10 @@ namespace ICSharpCode.AvalonEdit.Highlighting try { HighlightUpTo(lineNumber - 1); IDocumentLine line = document.GetLineByNumber(lineNumber); - highlightedLine = new HighlightedLine(document, line); - HighlightLineAndUpdateTreeList(line, lineNumber); - return highlightedLine; + HighlightedLine result = engine.HighlightLine(document, line); + UpdateTreeList(lineNumber); + return result; } finally { - highlightedLine = null; isHighlighting = false; } } @@ -221,23 +220,39 @@ namespace ICSharpCode.AvalonEdit.Highlighting } } + /// + /// Sets the engine's CurrentSpanStack to the end of the target line. + /// Updates the span stack for all lines up to (and including) the target line, if necessary. + /// void HighlightUpTo(int targetLineNumber) { - Debug.Assert(highlightedLine == null); // ensure this method is only outside the actual highlighting logic - while (firstInvalidLine <= targetLineNumber) { - HighlightLineAndUpdateTreeList(document.GetLineByNumber(firstInvalidLine), firstInvalidLine); + for (int currentLine = 0; currentLine <= targetLineNumber; currentLine++) { + if (firstInvalidLine > currentLine) { + // (this branch is always taken on the first loop iteration, as firstInvalidLine > 0) + + if (firstInvalidLine <= targetLineNumber) { + // Skip valid lines to next invalid line: + engine.CurrentSpanStack = storedSpanStacks[firstInvalidLine - 1]; + currentLine = firstInvalidLine; + } else { + // Skip valid lines to target line: + engine.CurrentSpanStack = storedSpanStacks[targetLineNumber]; + break; + } + } + Debug.Assert(EqualSpanStacks(engine.CurrentSpanStack, storedSpanStacks[currentLine - 1])); + engine.ScanLine(document, document.GetLineByNumber(currentLine)); + UpdateTreeList(currentLine); } + Debug.Assert(EqualSpanStacks(engine.CurrentSpanStack, storedSpanStacks[targetLineNumber])); } - void HighlightLineAndUpdateTreeList(IDocumentLine line, int lineNumber) + void UpdateTreeList(int lineNumber) { - //Debug.WriteLine("Highlight line " + lineNumber + (highlightedLine != null ? "" : " (span stack only)")); - spanStack = storedSpanStacks[lineNumber - 1]; - HighlightLineInternal(line); - if (!EqualSpanStacks(spanStack, storedSpanStacks[lineNumber])) { + if (!EqualSpanStacks(engine.CurrentSpanStack, storedSpanStacks[lineNumber])) { isValid[lineNumber] = true; //Debug.WriteLine("Span stack in line " + lineNumber + " changed from " + storedSpanStacks[lineNumber] + " to " + spanStack); - storedSpanStacks[lineNumber] = spanStack; + storedSpanStacks[lineNumber] = engine.CurrentSpanStack; if (lineNumber + 1 < isValid.Count) { isValid[lineNumber + 1] = false; firstInvalidLine = lineNumber + 1; @@ -288,231 +303,6 @@ namespace ICSharpCode.AvalonEdit.Highlighting HighlightingStateChanged(fromLineNumber, toLineNumber); } - #region Highlighting Engine - SpanStack spanStack; - - // local variables from HighlightLineInternal (are member because they are accessed by HighlighLine helper methods) - string lineText; - int lineStartOffset; - int position; - - /// - /// the HighlightedLine where highlighting output is being written to. - /// if this variable is null, nothing is highlighted and only the span state is updated - /// - HighlightedLine highlightedLine; - - void HighlightLineInternal(IDocumentLine line) - { - lineStartOffset = line.Offset; - lineText = document.GetText(lineStartOffset, line.Length); - position = 0; - ResetColorStack(); - HighlightingRuleSet currentRuleSet = this.CurrentRuleSet; - Stack storedMatchArrays = new Stack(); - Match[] matches = AllocateMatchArray(currentRuleSet.Spans.Count); - Match endSpanMatch = null; - - while (true) { - for (int i = 0; i < matches.Length; i++) { - if (matches[i] == null || (matches[i].Success && matches[i].Index < position)) - matches[i] = currentRuleSet.Spans[i].StartExpression.Match(lineText, position); - } - if (endSpanMatch == null && !spanStack.IsEmpty) - endSpanMatch = spanStack.Peek().EndExpression.Match(lineText, position); - - Match firstMatch = Minimum(matches, endSpanMatch); - if (firstMatch == null) - break; - - HighlightNonSpans(firstMatch.Index); - - Debug.Assert(position == firstMatch.Index); - - if (firstMatch == endSpanMatch) { - HighlightingSpan poppedSpan = spanStack.Peek(); - if (!poppedSpan.SpanColorIncludesEnd) - PopColor(); // pop SpanColor - PushColor(poppedSpan.EndColor); - position = firstMatch.Index + firstMatch.Length; - PopColor(); // pop EndColor - if (poppedSpan.SpanColorIncludesEnd) - PopColor(); // pop SpanColor - spanStack = spanStack.Pop(); - currentRuleSet = this.CurrentRuleSet; - //FreeMatchArray(matches); - if (storedMatchArrays.Count > 0) { - matches = storedMatchArrays.Pop(); - int index = currentRuleSet.Spans.IndexOf(poppedSpan); - Debug.Assert(index >= 0 && index < matches.Length); - if (matches[index].Index == position) { - throw new InvalidOperationException( - "A highlighting span matched 0 characters, which would cause an endless loop.\n" + - "Change the highlighting definition so that either the start or the end regex matches at least one character.\n" + - "Start regex: " + poppedSpan.StartExpression + "\n" + - "End regex: " + poppedSpan.EndExpression); - } - } else { - matches = AllocateMatchArray(currentRuleSet.Spans.Count); - } - } else { - int index = Array.IndexOf(matches, firstMatch); - Debug.Assert(index >= 0); - HighlightingSpan newSpan = currentRuleSet.Spans[index]; - spanStack = spanStack.Push(newSpan); - currentRuleSet = this.CurrentRuleSet; - storedMatchArrays.Push(matches); - matches = AllocateMatchArray(currentRuleSet.Spans.Count); - if (newSpan.SpanColorIncludesStart) - PushColor(newSpan.SpanColor); - PushColor(newSpan.StartColor); - position = firstMatch.Index + firstMatch.Length; - PopColor(); - if (!newSpan.SpanColorIncludesStart) - PushColor(newSpan.SpanColor); - } - endSpanMatch = null; - } - HighlightNonSpans(line.Length); - - PopAllColors(); - } - - void HighlightNonSpans(int until) - { - Debug.Assert(position <= until); - if (position == until) - return; - if (highlightedLine != null) { - IList rules = CurrentRuleSet.Rules; - Match[] matches = AllocateMatchArray(rules.Count); - while (true) { - for (int i = 0; i < matches.Length; i++) { - if (matches[i] == null || (matches[i].Success && matches[i].Index < position)) - matches[i] = rules[i].Regex.Match(lineText, position, until - position); - } - Match firstMatch = Minimum(matches, null); - if (firstMatch == null) - break; - - position = firstMatch.Index; - int ruleIndex = Array.IndexOf(matches, firstMatch); - if (firstMatch.Length == 0) { - throw new InvalidOperationException( - "A highlighting rule matched 0 characters, which would cause an endless loop.\n" + - "Change the highlighting definition so that the rule matches at least one character.\n" + - "Regex: " + rules[ruleIndex].Regex); - } - PushColor(rules[ruleIndex].Color); - position = firstMatch.Index + firstMatch.Length; - PopColor(); - } - //FreeMatchArray(matches); - } - position = until; - } - - static readonly HighlightingRuleSet emptyRuleSet = new HighlightingRuleSet() { Name = "EmptyRuleSet" }; - - HighlightingRuleSet CurrentRuleSet { - get { - if (spanStack.IsEmpty) - return definition.MainRuleSet; - else - return spanStack.Peek().RuleSet ?? emptyRuleSet; - } - } - #endregion - - #region Color Stack Management - Stack highlightedSectionStack; - HighlightedSection lastPoppedSection; - - void ResetColorStack() - { - Debug.Assert(position == 0); - lastPoppedSection = null; - if (highlightedLine == null) { - highlightedSectionStack = null; - } else { - highlightedSectionStack = new Stack(); - foreach (HighlightingSpan span in spanStack.Reverse()) { - PushColor(span.SpanColor); - } - } - } - - void PushColor(HighlightingColor color) - { - if (highlightedLine == null) - return; - if (color == null) { - highlightedSectionStack.Push(null); - } else if (lastPoppedSection != null && lastPoppedSection.Color == color - && lastPoppedSection.Offset + lastPoppedSection.Length == position + lineStartOffset) - { - highlightedSectionStack.Push(lastPoppedSection); - lastPoppedSection = null; - } else { - HighlightedSection hs = new HighlightedSection { - Offset = position + lineStartOffset, - Color = color - }; - highlightedLine.Sections.Add(hs); - highlightedSectionStack.Push(hs); - lastPoppedSection = null; - } - } - - void PopColor() - { - if (highlightedLine == null) - return; - HighlightedSection s = highlightedSectionStack.Pop(); - if (s != null) { - s.Length = (position + lineStartOffset) - s.Offset; - if (s.Length == 0) - highlightedLine.Sections.Remove(s); - else - lastPoppedSection = s; - } - } - - void PopAllColors() - { - if (highlightedSectionStack != null) { - while (highlightedSectionStack.Count > 0) - PopColor(); - } - } - #endregion - - #region Match helpers - /// - /// Returns the first match from the array or endSpanMatch. - /// - static Match Minimum(Match[] arr, Match endSpanMatch) - { - Match min = null; - foreach (Match v in arr) { - if (v.Success && (min == null || v.Index < min.Index)) - min = v; - } - if (endSpanMatch != null && endSpanMatch.Success && (min == null || endSpanMatch.Index < min.Index)) - return endSpanMatch; - else - return min; - } - - static Match[] AllocateMatchArray(int count) - { - if (count == 0) - return Empty.Array; - else - return new Match[count]; - } - #endregion - /// public HighlightingColor DefaultTextColor { get { return null; } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingEngine.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingEngine.cs new file mode 100644 index 0000000000..bd279a21d4 --- /dev/null +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Highlighting/HighlightingEngine.cs @@ -0,0 +1,303 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text.RegularExpressions; +using ICSharpCode.NRefactory.Editor; +using ICSharpCode.AvalonEdit.Utils; +using SpanStack = ICSharpCode.NRefactory.Utils.ImmutableStack; + +namespace ICSharpCode.AvalonEdit.Highlighting +{ + /// + /// Regex-based highlighting engine. + /// + public class HighlightingEngine + { + readonly HighlightingRuleSet mainRuleSet; + SpanStack spanStack = SpanStack.Empty; + + public HighlightingEngine(HighlightingRuleSet mainRuleSet) + { + if (mainRuleSet == null) + throw new ArgumentNullException("mainRuleSet"); + this.mainRuleSet = mainRuleSet; + } + + /// + /// Gets/sets the current span stack. + /// + public SpanStack CurrentSpanStack { + get { return spanStack; } + set { + spanStack = value ?? SpanStack.Empty; + } + } + + #region Highlighting Engine + + // local variables from HighlightLineInternal (are member because they are accessed by HighlighLine helper methods) + string lineText; + int lineStartOffset; + int position; + + /// + /// the HighlightedLine where highlighting output is being written to. + /// if this variable is null, nothing is highlighted and only the span state is updated + /// + HighlightedLine highlightedLine; + + /// + /// Highlights the specified line in the specified document. + /// + /// Before calling this method, must be set to the proper + /// state for the beginning of this line. After highlighting has completed, + /// will be updated to represent the state after the line. + /// + public HighlightedLine HighlightLine(IDocument document, IDocumentLine line) + { + this.lineStartOffset = line.Offset; + this.lineText = document.GetText(line); + try { + this.highlightedLine = new HighlightedLine(document, line); + HighlightLineInternal(); + return this.highlightedLine; + } finally { + this.highlightedLine = null; + this.lineText = null; + this.lineStartOffset = 0; + } + } + + /// + /// Updates for the specified line in the specified document. + /// + /// Before calling this method, must be set to the proper + /// state for the beginning of this line. After highlighting has completed, + /// will be updated to represent the state after the line. + /// + public void ScanLine(IDocument document, IDocumentLine line) + { + //this.lineStartOffset = line.Offset; not necessary for scanning + this.lineText = document.GetText(line); + try { + Debug.Assert(highlightedLine == null); + HighlightLineInternal(); + } finally { + this.lineText = null; + } + } + + void HighlightLineInternal() + { + position = 0; + ResetColorStack(); + HighlightingRuleSet currentRuleSet = this.CurrentRuleSet; + Stack storedMatchArrays = new Stack(); + Match[] matches = AllocateMatchArray(currentRuleSet.Spans.Count); + Match endSpanMatch = null; + + while (true) { + for (int i = 0; i < matches.Length; i++) { + if (matches[i] == null || (matches[i].Success && matches[i].Index < position)) + matches[i] = currentRuleSet.Spans[i].StartExpression.Match(lineText, position); + } + if (endSpanMatch == null && !spanStack.IsEmpty) + endSpanMatch = spanStack.Peek().EndExpression.Match(lineText, position); + + Match firstMatch = Minimum(matches, endSpanMatch); + if (firstMatch == null) + break; + + HighlightNonSpans(firstMatch.Index); + + Debug.Assert(position == firstMatch.Index); + + if (firstMatch == endSpanMatch) { + HighlightingSpan poppedSpan = spanStack.Peek(); + if (!poppedSpan.SpanColorIncludesEnd) + PopColor(); // pop SpanColor + PushColor(poppedSpan.EndColor); + position = firstMatch.Index + firstMatch.Length; + PopColor(); // pop EndColor + if (poppedSpan.SpanColorIncludesEnd) + PopColor(); // pop SpanColor + spanStack = spanStack.Pop(); + currentRuleSet = this.CurrentRuleSet; + //FreeMatchArray(matches); + if (storedMatchArrays.Count > 0) { + matches = storedMatchArrays.Pop(); + int index = currentRuleSet.Spans.IndexOf(poppedSpan); + Debug.Assert(index >= 0 && index < matches.Length); + if (matches[index].Index == position) { + throw new InvalidOperationException( + "A highlighting span matched 0 characters, which would cause an endless loop.\n" + + "Change the highlighting definition so that either the start or the end regex matches at least one character.\n" + + "Start regex: " + poppedSpan.StartExpression + "\n" + + "End regex: " + poppedSpan.EndExpression); + } + } else { + matches = AllocateMatchArray(currentRuleSet.Spans.Count); + } + } else { + int index = Array.IndexOf(matches, firstMatch); + Debug.Assert(index >= 0); + HighlightingSpan newSpan = currentRuleSet.Spans[index]; + spanStack = spanStack.Push(newSpan); + currentRuleSet = this.CurrentRuleSet; + storedMatchArrays.Push(matches); + matches = AllocateMatchArray(currentRuleSet.Spans.Count); + if (newSpan.SpanColorIncludesStart) + PushColor(newSpan.SpanColor); + PushColor(newSpan.StartColor); + position = firstMatch.Index + firstMatch.Length; + PopColor(); + if (!newSpan.SpanColorIncludesStart) + PushColor(newSpan.SpanColor); + } + endSpanMatch = null; + } + HighlightNonSpans(lineText.Length); + + PopAllColors(); + } + + void HighlightNonSpans(int until) + { + Debug.Assert(position <= until); + if (position == until) + return; + if (highlightedLine != null) { + IList rules = CurrentRuleSet.Rules; + Match[] matches = AllocateMatchArray(rules.Count); + while (true) { + for (int i = 0; i < matches.Length; i++) { + if (matches[i] == null || (matches[i].Success && matches[i].Index < position)) + matches[i] = rules[i].Regex.Match(lineText, position, until - position); + } + Match firstMatch = Minimum(matches, null); + if (firstMatch == null) + break; + + position = firstMatch.Index; + int ruleIndex = Array.IndexOf(matches, firstMatch); + if (firstMatch.Length == 0) { + throw new InvalidOperationException( + "A highlighting rule matched 0 characters, which would cause an endless loop.\n" + + "Change the highlighting definition so that the rule matches at least one character.\n" + + "Regex: " + rules[ruleIndex].Regex); + } + PushColor(rules[ruleIndex].Color); + position = firstMatch.Index + firstMatch.Length; + PopColor(); + } + //FreeMatchArray(matches); + } + position = until; + } + + static readonly HighlightingRuleSet emptyRuleSet = new HighlightingRuleSet() { Name = "EmptyRuleSet" }; + + HighlightingRuleSet CurrentRuleSet { + get { + if (spanStack.IsEmpty) + return mainRuleSet; + else + return spanStack.Peek().RuleSet ?? emptyRuleSet; + } + } + #endregion + + #region Color Stack Management + Stack highlightedSectionStack; + HighlightedSection lastPoppedSection; + + void ResetColorStack() + { + Debug.Assert(position == 0); + lastPoppedSection = null; + if (highlightedLine == null) { + highlightedSectionStack = null; + } else { + highlightedSectionStack = new Stack(); + foreach (HighlightingSpan span in spanStack.Reverse()) { + PushColor(span.SpanColor); + } + } + } + + void PushColor(HighlightingColor color) + { + if (highlightedLine == null) + return; + if (color == null) { + highlightedSectionStack.Push(null); + } else if (lastPoppedSection != null && lastPoppedSection.Color == color + && lastPoppedSection.Offset + lastPoppedSection.Length == position + lineStartOffset) + { + highlightedSectionStack.Push(lastPoppedSection); + lastPoppedSection = null; + } else { + HighlightedSection hs = new HighlightedSection { + Offset = position + lineStartOffset, + Color = color + }; + highlightedLine.Sections.Add(hs); + highlightedSectionStack.Push(hs); + lastPoppedSection = null; + } + } + + void PopColor() + { + if (highlightedLine == null) + return; + HighlightedSection s = highlightedSectionStack.Pop(); + if (s != null) { + s.Length = (position + lineStartOffset) - s.Offset; + if (s.Length == 0) + highlightedLine.Sections.Remove(s); + else + lastPoppedSection = s; + } + } + + void PopAllColors() + { + if (highlightedSectionStack != null) { + while (highlightedSectionStack.Count > 0) + PopColor(); + } + } + #endregion + + #region Match helpers + /// + /// Returns the first match from the array or endSpanMatch. + /// + static Match Minimum(Match[] arr, Match endSpanMatch) + { + Match min = null; + foreach (Match v in arr) { + if (v.Success && (min == null || v.Index < min.Index)) + min = v; + } + if (endSpanMatch != null && endSpanMatch.Success && (min == null || endSpanMatch.Index < min.Index)) + return endSpanMatch; + else + return min; + } + + static Match[] AllocateMatchArray(int count) + { + if (count == 0) + return Empty.Array; + else + return new Match[count]; + } + #endregion + } +} diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj index 7d336ecb1b..6f7e4d9b5c 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj @@ -193,6 +193,7 @@ + diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Utils/CompressingTreeList.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Utils/CompressingTreeList.cs index fb984c13ad..24a66b60b0 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Utils/CompressingTreeList.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Utils/CompressingTreeList.cs @@ -110,6 +110,18 @@ namespace ICSharpCode.AvalonEdit.Utils readonly Func comparisonFunc; Node root; + /// + /// Creates a new CompressingTreeList instance. + /// + /// A function that checks two values for equality. If this + /// function returns true, a single node may be used to store the two values. + public CompressingTreeList(IEqualityComparer equalityComparer) + { + if (equalityComparer == null) + throw new ArgumentNullException("equalityComparer"); + this.comparisonFunc = equalityComparer.Equals; + } + /// /// Creates a new CompressingTreeList instance. ///