From 00d9ad5898198a52b889b3f5d0c30c6dc13402b8 Mon Sep 17 00:00:00 2001 From: Joshua Grzybowski Date: Tue, 9 May 2023 15:16:59 -0400 Subject: [PATCH] Make HighlightRule extensible so rules aren't required to use Regex --- .../Highlighting/HighlightingEngine.cs | 34 +++++----- .../Highlighting/HighlightingRule.cs | 11 +++- .../Highlighting/HighlightingRuleSet.cs | 4 +- .../Highlighting/HighlightingSpan.cs | 22 +++++++ .../Highlighting/IHighlightingRule.cs | 47 ++++++++++++++ .../Highlighting/RuleMatch.cs | 62 +++++++++++++++++++ .../Xshd/XmlHighlightingDefinition.cs | 2 +- 7 files changed, 161 insertions(+), 21 deletions(-) create mode 100644 ICSharpCode.AvalonEdit/Highlighting/IHighlightingRule.cs create mode 100644 ICSharpCode.AvalonEdit/Highlighting/RuleMatch.cs diff --git a/ICSharpCode.AvalonEdit/Highlighting/HighlightingEngine.cs b/ICSharpCode.AvalonEdit/Highlighting/HighlightingEngine.cs index 949ad1da..579c7522 100644 --- a/ICSharpCode.AvalonEdit/Highlighting/HighlightingEngine.cs +++ b/ICSharpCode.AvalonEdit/Highlighting/HighlightingEngine.cs @@ -116,19 +116,19 @@ void HighlightLineInternal() position = 0; ResetColorStack(); HighlightingRuleSet currentRuleSet = this.CurrentRuleSet; - Stack storedMatchArrays = new Stack(); - Match[] matches = AllocateMatchArray(currentRuleSet.Spans.Count); - Match endSpanMatch = null; + Stack storedMatchArrays = new Stack(); + RuleMatch[] matches = AllocateMatchArray(currentRuleSet.Spans.Count); + RuleMatch 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); + matches[i] = currentRuleSet.Spans[i].GetStartMatch(lineText, position); } if (endSpanMatch == null && !spanStack.IsEmpty) - endSpanMatch = spanStack.Peek().EndExpression.Match(lineText, position); + endSpanMatch = spanStack.Peek().GetEndMatch(lineText, position); - Match firstMatch = Minimum(matches, endSpanMatch); + RuleMatch firstMatch = Minimum(matches, endSpanMatch); if (firstMatch == null) break; @@ -191,14 +191,14 @@ void HighlightNonSpans(int until) if (position == until) return; if (highlightedLine != null) { - IList rules = CurrentRuleSet.Rules; - Match[] matches = AllocateMatchArray(rules.Count); + IList rules = CurrentRuleSet.Rules; + RuleMatch[] 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); + matches[i] = rules[i].GetMatch(lineText, position, until - position, highlightedLine.DocumentLine.LineNumber); } - Match firstMatch = Minimum(matches, null); + RuleMatch firstMatch = Minimum(matches, null); if (firstMatch == null) break; @@ -208,7 +208,7 @@ void HighlightNonSpans(int until) 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); + rules[ruleIndex].RuleInfo); } PushColor(rules[ruleIndex].Color); position = firstMatch.Index + firstMatch.Length; @@ -297,10 +297,10 @@ void PopAllColors() /// /// Returns the first match from the array or endSpanMatch. /// - static Match Minimum(Match[] arr, Match endSpanMatch) + static RuleMatch Minimum(RuleMatch[] arr, RuleMatch endSpanMatch) { - Match min = null; - foreach (Match v in arr) { + RuleMatch min = null; + foreach (RuleMatch v in arr) { if (v.Success && (min == null || v.Index < min.Index)) min = v; } @@ -310,12 +310,12 @@ static Match Minimum(Match[] arr, Match endSpanMatch) return min; } - static Match[] AllocateMatchArray(int count) + static RuleMatch[] AllocateMatchArray(int count) { if (count == 0) - return Empty.Array; + return Empty.Array; else - return new Match[count]; + return new RuleMatch[count]; } #endregion } diff --git a/ICSharpCode.AvalonEdit/Highlighting/HighlightingRule.cs b/ICSharpCode.AvalonEdit/Highlighting/HighlightingRule.cs index 405e9aff..6c0e3393 100644 --- a/ICSharpCode.AvalonEdit/Highlighting/HighlightingRule.cs +++ b/ICSharpCode.AvalonEdit/Highlighting/HighlightingRule.cs @@ -25,7 +25,7 @@ namespace ICSharpCode.AvalonEdit.Highlighting /// A highlighting rule. /// [Serializable] - public class HighlightingRule + public class HighlightingRule : IHighlightingRule { /// /// Gets/Sets the regular expression for the rule. @@ -37,6 +37,15 @@ public class HighlightingRule /// public HighlightingColor Color { get; set; } + /// + public string RuleInfo => $"Regex: {Regex}"; + + /// + public RuleMatch GetMatch(string input, int beginning, int length, int lineNumber) + { + return RuleMatch.FromRegexMatch(Regex.Match(input, beginning, length)); + } + /// public override string ToString() { diff --git a/ICSharpCode.AvalonEdit/Highlighting/HighlightingRuleSet.cs b/ICSharpCode.AvalonEdit/Highlighting/HighlightingRuleSet.cs index 5df40562..6ffa6835 100644 --- a/ICSharpCode.AvalonEdit/Highlighting/HighlightingRuleSet.cs +++ b/ICSharpCode.AvalonEdit/Highlighting/HighlightingRuleSet.cs @@ -35,7 +35,7 @@ public class HighlightingRuleSet public HighlightingRuleSet() { this.Spans = new NullSafeCollection(); - this.Rules = new NullSafeCollection(); + this.Rules = new NullSafeCollection(); } /// @@ -51,7 +51,7 @@ public HighlightingRuleSet() /// /// Gets the list of rules. /// - public IList Rules { get; private set; } + public IList Rules { get; private set; } /// public override string ToString() diff --git a/ICSharpCode.AvalonEdit/Highlighting/HighlightingSpan.cs b/ICSharpCode.AvalonEdit/Highlighting/HighlightingSpan.cs index 05788d8d..2095dd9d 100644 --- a/ICSharpCode.AvalonEdit/Highlighting/HighlightingSpan.cs +++ b/ICSharpCode.AvalonEdit/Highlighting/HighlightingSpan.cs @@ -70,6 +70,28 @@ public class HighlightingSpan /// public bool SpanColorIncludesEnd { get; set; } + /// + /// Gets the match from the specified position using the regex. + /// + /// The string to search for a match + /// The zero-based character position at which to start the search + /// And object that contains information about the match + public RuleMatch GetStartMatch(string text, int position) + { + return RuleMatch.FromRegexMatch(StartExpression.Match(text, position)); + } + + /// + /// Gets the match from the specified position using the regex. + /// + /// The string to search for a match + /// The zero-based character position at which to start the search + /// And object that contains information about the match + public RuleMatch GetEndMatch(string text, int position) + { + return RuleMatch.FromRegexMatch(EndExpression.Match(text, position)); + } + /// public override string ToString() { diff --git a/ICSharpCode.AvalonEdit/Highlighting/IHighlightingRule.cs b/ICSharpCode.AvalonEdit/Highlighting/IHighlightingRule.cs new file mode 100644 index 00000000..8d9aa05f --- /dev/null +++ b/ICSharpCode.AvalonEdit/Highlighting/IHighlightingRule.cs @@ -0,0 +1,47 @@ +// 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 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. + +namespace ICSharpCode.AvalonEdit.Highlighting +{ + /// + /// Interface of a highlighting rule + /// + public interface IHighlightingRule + { + /// + /// Gets the first match for the rule + /// + /// The string to search for a match. + /// The zero-based character position in the input string that defines the leftmost + /// position to be searched. + /// The number of characters in the substring to include in the search. + /// The line number of the string. + /// An object that contains information about the match. + RuleMatch GetMatch(string input, int beginning, int length, int lineNumber); + + /// + /// Gets the highlighting color. + /// + HighlightingColor Color { get; } + + /// + /// Info about rule. Used to help figure out why rule failed + /// + string RuleInfo { get; } + } +} diff --git a/ICSharpCode.AvalonEdit/Highlighting/RuleMatch.cs b/ICSharpCode.AvalonEdit/Highlighting/RuleMatch.cs new file mode 100644 index 00000000..27a6643f --- /dev/null +++ b/ICSharpCode.AvalonEdit/Highlighting/RuleMatch.cs @@ -0,0 +1,62 @@ +// 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 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 System.Text.RegularExpressions; + +namespace ICSharpCode.AvalonEdit.Highlighting +{ + /// + /// And object that contains information about a rule's match + /// + public class RuleMatch + { + /// + /// Creates a new RuleMatch instance. + /// + public RuleMatch() { } + + /// + /// Gets a value indicating whether the match was successful. + /// + public bool Success { get; set; } + + /// + /// The position in the original string where the first character of captured substring was found. + /// + public int Index { get; set; } + + /// + /// The length of the captured substring. + /// + public int Length { get; set; } + + /// + /// Creates a new RuleMatch instance from a instance. + /// + /// Match to use + /// RuleMatch instance built from match parameter + public static RuleMatch FromRegexMatch(Match match) + { + return new RuleMatch() { + Success = match.Success, + Index = match.Index, + Length = match.Length, + }; + } + } +} diff --git a/ICSharpCode.AvalonEdit/Highlighting/Xshd/XmlHighlightingDefinition.cs b/ICSharpCode.AvalonEdit/Highlighting/Xshd/XmlHighlightingDefinition.cs index 5a493456..22dac6df 100644 --- a/ICSharpCode.AvalonEdit/Highlighting/Xshd/XmlHighlightingDefinition.cs +++ b/ICSharpCode.AvalonEdit/Highlighting/Xshd/XmlHighlightingDefinition.cs @@ -172,7 +172,7 @@ public object VisitRuleSet(XshdRuleSet ruleSet) if (span != null) { rs.Spans.Add(span); } else { - HighlightingRule elementRule = o as HighlightingRule; + IHighlightingRule elementRule = o as IHighlightingRule; if (elementRule != null) { rs.Rules.Add(elementRule); }