//
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
//
// This file contain implementations details that are subject to change without notice.
// Use at your own risk.
//
namespace Microsoft.VisualStudio.Text.Classification.Implementation
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Tagging;
///
/// Defines the aggregator for all the classifiers - and is available to the external world via a VisualStudio
/// Service
///
internal sealed class ClassifierAggregator : IAccurateClassifier, IDisposable
{
#region Private Members
private ITextBuffer _textBuffer;
private IClassificationTypeRegistryService _classificationTypeRegistry;
private IAccurateTagAggregator _tagAggregator;
#endregion // Private Members
#region Constructors
internal ClassifierAggregator(ITextBuffer textBuffer,
IBufferTagAggregatorFactoryService bufferTagAggregatorFactory,
IClassificationTypeRegistryService classificationTypeRegistry)
{
// Validate.
if (textBuffer == null)
{
throw new ArgumentNullException(nameof(textBuffer));
}
if (bufferTagAggregatorFactory == null)
{
throw new ArgumentNullException(nameof(bufferTagAggregatorFactory));
}
if (classificationTypeRegistry == null)
{
throw new ArgumentNullException(nameof(classificationTypeRegistry));
}
_textBuffer = textBuffer;
_classificationTypeRegistry = classificationTypeRegistry;
// Create a tag aggregator that maps by content type, so we don't map through projection buffers that aren't "projection" content type.
_tagAggregator = bufferTagAggregatorFactory.CreateTagAggregator(textBuffer, TagAggregatorOptions.MapByContentType) as IAccurateTagAggregator;
_tagAggregator.BatchedTagsChanged += OnBatchedTagsChanged;
}
internal ClassifierAggregator(ITextView textView,
IViewTagAggregatorFactoryService viewTagAggregatorFactory,
IClassificationTypeRegistryService classificationTypeRegistry)
{
// Validate.
if (textView == null)
{
throw new ArgumentNullException(nameof(textView));
}
if (viewTagAggregatorFactory == null)
{
throw new ArgumentNullException(nameof(viewTagAggregatorFactory));
}
if (classificationTypeRegistry == null)
{
throw new ArgumentNullException(nameof(classificationTypeRegistry));
}
_textBuffer = textView.TextBuffer;
_classificationTypeRegistry = classificationTypeRegistry;
// Create a tag aggregator that maps by content type, so we don't map through projection buffers that aren't "projection" content type.
_tagAggregator = viewTagAggregatorFactory.CreateTagAggregator(textView, TagAggregatorOptions.MapByContentType) as IAccurateTagAggregator;
_tagAggregator.BatchedTagsChanged += OnBatchedTagsChanged;
}
#endregion // Constructors
#region Exposed Methods
///
/// Gets all classification spans that overlap the given range of text
///
///
/// The span of text of interest
///
///
/// A list of lists of ClassificationSpans that intersect with the given range
///
public IList GetClassificationSpans(SnapshotSpan span)
{
if (_tagAggregator == null)
return new List(0);
return this.InternalGetClassificationSpans(span, _tagAggregator.GetTags(span));
}
public IList GetAllClassificationSpans(SnapshotSpan span, CancellationToken cancel)
{
if (_tagAggregator == null)
return new List(0);
return this.InternalGetClassificationSpans(span, _tagAggregator.GetAllTags(span, cancel));
}
#endregion // Exposed Methods
#region IDisposable
public void Dispose()
{
if (_tagAggregator != null)
{
_tagAggregator.BatchedTagsChanged -= OnBatchedTagsChanged;
_tagAggregator.Dispose();
_tagAggregator = null;
}
}
#endregion
#region Public Events
///
/// Notifies a classification change. The aggregator also bubbles ClassificationChanged
/// events up from aggregated classifiers
///
public event EventHandler ClassificationChanged;
#endregion // Public Events
#region Event Handlers
private void OnBatchedTagsChanged(object sender, BatchedTagsChangedEventArgs e)
{
var tempEvent = ClassificationChanged;
if (tempEvent != null)
{
foreach (var mappingSpan in e.Spans)
{
foreach (var span in mappingSpan.GetSpans(_textBuffer))
{
tempEvent(this, new ClassificationChangedEventArgs(span));
}
}
}
}
#endregion // Event Handlers
#region Private Helpers
private IList InternalGetClassificationSpans(SnapshotSpan span, IEnumerable> tags)
{
List allSpans = new List();
foreach (var tagSpan in tags)
{
NormalizedSnapshotSpanCollection tagSpans = tagSpan.Span.GetSpans(span.Snapshot.TextBuffer);
for (int s = 0; s < tagSpans.Count; ++s)
{
allSpans.Add(new ClassificationSpan(
tagSpans[s].TranslateTo(span.Snapshot, SpanTrackingMode.EdgeExclusive),
tagSpan.Tag.ClassificationType));
}
}
return this.NormalizeClassificationSpans(span, allSpans);
}
#region Private classes
struct PointData
{
public readonly bool IsStart;
public readonly int Position;
public readonly IClassificationType ClassificationType;
public PointData(bool isStart, int position, IClassificationType classificationType)
{
this.IsStart = isStart;
this.Position = position;
this.ClassificationType = classificationType;
}
}
class OpenSpanData
{
public readonly IClassificationType ClassificationType;
public int Count = 1; // How many open classification spans with this classification type are there?
public OpenSpanData(IClassificationType classificationType)
{
this.ClassificationType = classificationType;
}
};
#endregion
///
/// Normalizes a list of classifications. Given an arbitrary set of ClassificationSpan lists, creates
/// a new list with each ClassificationSpan, sorted by position and with the appropriate transient
/// classification types for spans that had multiple classifications.
///
/// A list of lists that contain ClassificationSpans.
///
/// The requested range of this call so results can be trimmed if there are any
/// classifiers that returned spans past the requested range they can be trimmed.
///
/// A sorted list of normalized spans.
///
/// Basic algorithm:
/// Create a list of the starting and ending points for each classification (clipped against requestedRange).
/// Sort the points list by position. If the positions are the same, starting points go before ending points (this
/// prevents a seam between two adjoining classifications of the same type).
///
/// Make a list of the currently open spans.
/// Walk the points in order.
/// When encountering a starting point:
/// If there is already an open span of starting point's type, increment the count associated with the open span.
/// Otherwise, add a new classification span based on the currently open spans and then add an open span (based on the startpoint) to the list of open spans.
///
/// When encountering an ending point:
/// Check the count for the open span of the ending point's type (there must be one):
/// If it is one, add a new classiciation based on the open spans and remove that open span from the list of open spans.
/// Otherwise, decrement the count associated with the open span.
///
private List NormalizeClassificationSpans(SnapshotSpan requestedRange, IList spans)
{
// Add the start and end points of each classification to one sorted list
List points = new List(spans.Count * 2);
int lastEndPoint = -1;
IClassificationType lastClassificationType = null;
bool requiresNormalization = false;
foreach (ClassificationSpan classification in spans)
{
// Only consider classifications that overlap the requested span
Span? span = requestedRange.Overlap(classification.Span.TranslateTo(requestedRange.Snapshot, SpanTrackingMode.EdgeExclusive));
if (span.HasValue)
{
// Use a <= test so that adjoining classifications will go through the normalizing process
// (so that adjoining classifications of the same type will be merged into a single
// classification span).
Span overlap = span.Value;
if ((overlap.Start < lastEndPoint) ||
((overlap.Start == lastEndPoint) && (classification.ClassificationType == lastClassificationType)))
{
requiresNormalization = true;
}
lastEndPoint = overlap.End;
lastClassificationType = classification.ClassificationType;
points.Add(new PointData(true, overlap.Start, lastClassificationType));
points.Add(new PointData(false, overlap.End, lastClassificationType));
}
}
List results = new List();
if (!requiresNormalization)
{
// The generated points list is already sorted, so simple generate a list of classifications from it without normalizing
// (we can't use spans since its classifications have not been clipped to requestedRange).
for (int i = 1; (i < points.Count); i += 2)
{
PointData startPoint = points[i - 1];
PointData endPoint = points[i];
results.Add(new ClassificationSpan(new SnapshotSpan(requestedRange.Snapshot, startPoint.Position, endPoint.Position - startPoint.Position),
startPoint.ClassificationType));
}
}
else
{
points.Sort(Compare);
int nextSpanStart = 0;
List openSpans = new List();
for (int p = 0; p < points.Count; ++p)
{
PointData point = points[p];
OpenSpanData openSpanData = null;
// write this little search explicitly instead of using the Find method, because allocating
// a delegate here in a high-frequency loop allocates noticeable amounts of memory
for (int os = 0; os < openSpans.Count; ++os)
{
if (openSpans[os].ClassificationType == point.ClassificationType)
{
openSpanData = openSpans[os];
break;
}
}
if (point.IsStart)
{
if (openSpanData != null)
{
++(openSpanData.Count);
}
else
{
if ((openSpans.Count > 0) && (point.Position > nextSpanStart))
{
this.AddClassificationSpan(openSpans, requestedRange.Snapshot, nextSpanStart, point.Position, results);
}
nextSpanStart = point.Position;
openSpans.Add(new OpenSpanData(point.ClassificationType));
}
}
else
{
if (openSpanData.Count > 1)
{
--(openSpanData.Count);
}
else
{
if (point.Position > nextSpanStart)
{
this.AddClassificationSpan(openSpans, requestedRange.Snapshot, nextSpanStart, point.Position, results);
}
nextSpanStart = point.Position;
openSpans.Remove(openSpanData);
}
}
}
}
// Return our list of aggregated spans.
return results;
}
private static int Compare(PointData a, PointData b)
{
if (a.Position == b.Position)
return (b.IsStart.CompareTo(a.IsStart)); // startpoints go before end points when positions are tied
else
return (a.Position - b.Position);
}
///
/// Add a classification span to that corresponds to the open classification spans in
/// between and .
///
private void AddClassificationSpan(List openSpans, ITextSnapshot snapshot, int start, int end, IList results)
{
IClassificationType classificationType;
if (openSpans.Count == 1)
{
// If there is only one classification type, then don't create a transient type.
classificationType = openSpans[0].ClassificationType;
}
else
{
// There is more than one classification type, create a transient type that corresponds
// to all the open classificaiton types.
List classifications = new List(openSpans.Count);
foreach (OpenSpanData osd in openSpans)
classifications.Add(osd.ClassificationType);
classificationType = _classificationTypeRegistry.CreateTransientClassificationType(classifications);
}
results.Add(new ClassificationSpan(new SnapshotSpan(snapshot, start, end - start),
classificationType));
}
#endregion
}
}