aboutsummaryrefslogtreecommitdiffstats
path: root/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/AvalonEditCommands.cs
blob: 5aaed19d8f5c0d8f0c075fa7f10d4a7c728b2374 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// 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.Windows.Input;

namespace Tango.Scripting.Editors
{
	/// <summary>
	/// Custom commands for AvalonEdit.
	/// </summary>
	public static class AvalonEditCommands
	{
		/// <summary>
		/// Deletes the current line.
		/// The default shortcut is Ctrl+D.
		/// </summary>
		public static readonly RoutedCommand DeleteLine = new RoutedCommand(
			"DeleteLine", typeof(TextEditor),
			new InputGestureCollection {
				new KeyGesture(Key.D, ModifierKeys.Control)
			});
		
		/// <summary>
		/// Removes leading whitespace from the selected lines (or the whole document if the selection is empty).
		/// </summary>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace",
		                                                 Justification = "WPF uses 'Whitespace'")]
		public static readonly RoutedCommand RemoveLeadingWhitespace = new RoutedCommand("RemoveLeadingWhitespace", typeof(TextEditor));
				
		/// <summary>
		/// Removes trailing whitespace from the selected lines (or the whole document if the selection is empty).
		/// </summary>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace",
		                                                 Justification = "WPF uses 'Whitespace'")]
		public static readonly RoutedCommand RemoveTrailingWhitespace = new RoutedCommand("RemoveTrailingWhitespace", typeof(TextEditor));
		
		/// <summary>
		/// Converts the selected text to upper case.
		/// </summary>
		public static readonly RoutedCommand ConvertToUppercase = new RoutedCommand("ConvertToUppercase", typeof(TextEditor));
		
		/// <summary>
		/// Converts the selected text to lower case.
		/// </summary>
		public static readonly RoutedCommand ConvertToLowercase = new RoutedCommand("ConvertToLowercase", typeof(TextEditor));
		
		/// <summary>
		/// Converts the selected text to title case.
		/// </summary>
		public static readonly RoutedCommand ConvertToTitleCase = new RoutedCommand("ConvertToTitleCase", typeof(TextEditor));
		
		/// <summary>
		/// Inverts the case of the selected text.
		/// </summary>
		public static readonly RoutedCommand InvertCase = new RoutedCommand("InvertCase", typeof(TextEditor));
		
		/// <summary>
		/// Converts tabs to spaces in the selected text.
		/// </summary>
		public static readonly RoutedCommand ConvertTabsToSpaces = new RoutedCommand("ConvertTabsToSpaces", typeof(TextEditor));
		
		/// <summary>
		/// Converts spaces to tabs in the selected text.
		/// </summary>
		public static readonly RoutedCommand ConvertSpacesToTabs = new RoutedCommand("ConvertSpacesToTabs", typeof(TextEditor));
		
		/// <summary>
		/// Converts leading tabs to spaces in the selected lines (or the whole document if the selection is empty).
		/// </summary>
		public static readonly RoutedCommand ConvertLeadingTabsToSpaces = new RoutedCommand("ConvertLeadingTabsToSpaces", typeof(TextEditor));
		
		/// <summary>
		/// Converts leading spaces to tabs in the selected lines (or the whole document if the selection is empty).
		/// </summary>
		public static readonly RoutedCommand ConvertLeadingSpacesToTabs = new RoutedCommand("ConvertLeadingSpacesToTabs", typeof(TextEditor));
		
		/// <summary>
		/// Runs the IIndentationStrategy on the selected lines (or the whole document if the selection is empty).
		/// </summary>
		public static readonly RoutedCommand IndentSelection = new RoutedCommand(
			"IndentSelection", typeof(TextEditor),
			new InputGestureCollection {
				new KeyGesture(Key.I, ModifierKeys.Control)
			});
	}
}
s="k">namespace Tango.Scripting.Editors.Highlighting { /// <summary> /// This class can syntax-highlight a document. /// It automatically manages invalidating the highlighting when the document changes. /// </summary> public class DocumentHighlighter : ILineTracker, IHighlighter { /// <summary> /// Stores the span state at the end of each line. /// storedSpanStacks[0] = state at beginning of document /// storedSpanStacks[i] = state after line i /// </summary> readonly CompressingTreeList<SpanStack> storedSpanStacks = new CompressingTreeList<SpanStack>(object.ReferenceEquals); readonly CompressingTreeList<bool> isValid = new CompressingTreeList<bool>((a, b) => a == b); readonly TextDocument document; readonly HighlightingRuleSet baseRuleSet; bool isHighlighting; /// <summary> /// Gets the document that this DocumentHighlighter is highlighting. /// </summary> public TextDocument Document { get { return document; } } /// <summary> /// Creates a new DocumentHighlighter instance. /// </summary> public DocumentHighlighter(TextDocument document, HighlightingRuleSet baseRuleSet) { if (document == null) throw new ArgumentNullException("document"); if (baseRuleSet == null) throw new ArgumentNullException("baseRuleSet"); this.document = document; this.baseRuleSet = baseRuleSet; WeakLineTracker.Register(document, this); InvalidateHighlighting(); } void ILineTracker.BeforeRemoveLine(DocumentLine line) { CheckIsHighlighting(); int number = line.LineNumber; storedSpanStacks.RemoveAt(number); isValid.RemoveAt(number); if (number < isValid.Count) { isValid[number] = false; if (number < firstInvalidLine) firstInvalidLine = number; } } void ILineTracker.SetLineLength(DocumentLine line, int newTotalLength) { CheckIsHighlighting(); int number = line.LineNumber; isValid[number] = false; if (number < firstInvalidLine) firstInvalidLine = number; } void ILineTracker.LineInserted(DocumentLine insertionPos, DocumentLine newLine) { CheckIsHighlighting(); Debug.Assert(insertionPos.LineNumber + 1 == newLine.LineNumber); int lineNumber = newLine.LineNumber; storedSpanStacks.Insert(lineNumber, null); isValid.Insert(lineNumber, false); if (lineNumber < firstInvalidLine) firstInvalidLine = lineNumber; } void ILineTracker.RebuildDocument() { InvalidateHighlighting(); } ImmutableStack<HighlightingSpan> initialSpanStack = SpanStack.Empty; /// <summary> /// Gets/sets the the initial span stack of the document. Default value is <see cref="SpanStack.Empty" />. /// </summary> public ImmutableStack<HighlightingSpan> InitialSpanStack { get { return initialSpanStack; } set { if (value == null) initialSpanStack = SpanStack.Empty; else initialSpanStack = value; InvalidateHighlighting(); } } /// <summary> /// Invalidates all stored highlighting info. /// When the document changes, the highlighting is invalidated automatically, this method /// needs to be called only when there are changes to the highlighting rule set. /// </summary> public void InvalidateHighlighting() { CheckIsHighlighting(); storedSpanStacks.Clear(); storedSpanStacks.Add(initialSpanStack); storedSpanStacks.InsertRange(1, document.LineCount, null); isValid.Clear(); isValid.Add(true); isValid.InsertRange(1, document.LineCount, false); firstInvalidLine = 1; } int firstInvalidLine; /// <summary> /// Highlights the specified document line. /// </summary> /// <param name="line">The line to highlight.</param> /// <returns>A <see cref="HighlightedLine"/> line object that represents the highlighted sections.</returns> [ObsoleteAttribute("Use the (int lineNumber) overload instead")] public HighlightedLine HighlightLine(DocumentLine line) { if (!document.Lines.Contains(line)) throw new ArgumentException("The specified line does not belong to the document."); return HighlightLine(line.LineNumber); } /// <inheritdoc/> public HighlightedLine HighlightLine(int lineNumber) { ThrowUtil.CheckInRangeInclusive(lineNumber, "lineNumber", 1, document.LineCount); CheckIsHighlighting(); isHighlighting = true; try { HighlightUpTo(lineNumber); DocumentLine line = document.GetLineByNumber(lineNumber); highlightedLine = new HighlightedLine(document, line); HighlightLineAndUpdateTreeList(line, lineNumber); return highlightedLine; } finally { highlightedLine = null; isHighlighting = false; } } /// <inheritdoc/> public SpanStack GetSpanStack(int lineNumber) { ThrowUtil.CheckInRangeInclusive(lineNumber, "lineNumber", 0, document.LineCount); if (firstInvalidLine <= lineNumber) { CheckIsHighlighting(); isHighlighting = true; try { HighlightUpTo(lineNumber + 1); } finally { isHighlighting = false; } } return storedSpanStacks[lineNumber]; } void CheckIsHighlighting() { if (isHighlighting) { throw new InvalidOperationException("Invalid call - a highlighting operation is currently running."); } } void HighlightUpTo(int targetLineNumber) { Debug.Assert(highlightedLine == null); // ensure this method is only used for while (firstInvalidLine < targetLineNumber) { HighlightLineAndUpdateTreeList(document.GetLineByNumber(firstInvalidLine), firstInvalidLine); } } void HighlightLineAndUpdateTreeList(DocumentLine line, int lineNumber) { //Debug.WriteLine("Highlight line " + lineNumber + (highlightedLine != null ? "" : " (span stack only)")); spanStack = storedSpanStacks[lineNumber - 1]; HighlightLineInternal(line); if (!EqualSpanStacks(spanStack, storedSpanStacks[lineNumber])) { isValid[lineNumber] = true; //Debug.WriteLine("Span stack in line " + lineNumber + " changed from " + storedSpanStacks[lineNumber] + " to " + spanStack); storedSpanStacks[lineNumber] = spanStack; if (lineNumber + 1 < isValid.Count) { isValid[lineNumber + 1] = false; firstInvalidLine = lineNumber + 1; } else { firstInvalidLine = int.MaxValue; } OnHighlightStateChanged(line, lineNumber); } else if (firstInvalidLine == lineNumber) { isValid[lineNumber] = true; firstInvalidLine = isValid.IndexOf(false); if (firstInvalidLine < 0) firstInvalidLine = int.MaxValue; } } static bool EqualSpanStacks(SpanStack a, SpanStack b) { // We must use value equality between the stacks because TextViewDocumentHighlighter.OnHighlightStateChanged // depends on the fact that equal input state + unchanged line contents produce equal output state. if (a == b) return true; if (a == null || b == null) return false; while (!a.IsEmpty && !b.IsEmpty) { if (a.Peek() != b.Peek()) return false; a = a.Pop(); b = b.Pop(); if (a == b) return true; } return a.IsEmpty && b.IsEmpty; } /// <summary> /// Is called when the highlighting state at the end of the specified line has changed. /// </summary> /// <remarks>This callback must not call HighlightLine or InvalidateHighlighting. /// It may call GetSpanStack, but only for the changed line and lines above. /// This method must not modify the document.</remarks> protected virtual void OnHighlightStateChanged(DocumentLine line, int lineNumber) { } #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; /// <summary> /// the HighlightedLine where highlighting output is being written to. /// if this variable is null, nothing is highlighted and only the span state is updated /// </summary> HighlightedLine highlightedLine; void HighlightLineInternal(DocumentLine line) { lineStartOffset = line.Offset; lineText = document.GetText(line.Offset, line.Length); position = 0; ResetColorStack(); HighlightingRuleSet currentRuleSet = this.CurrentRuleSet; Stack<Match[]> storedMatchArrays = new Stack<Match[]>(); 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<HighlightingRule> 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 baseRuleSet; else return spanStack.Peek().RuleSet ?? emptyRuleSet; } } #endregion #region Color Stack Management Stack<HighlightedSection> highlightedSectionStack; HighlightedSection lastPoppedSection; void ResetColorStack() { Debug.Assert(position == 0); lastPoppedSection = null; if (highlightedLine == null) { highlightedSectionStack = null; } else { highlightedSectionStack = new Stack<HighlightedSection>(); 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 /// <summary> /// Returns the first match from the array or endSpanMatch. /// </summary> 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<Match>.Array; else return new Match[count]; } #endregion } }