/****************************************************************************
**
** Copyright (C) 2018 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt VS Tools.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
/// This file contains the definition of the abstract class QmlAsyncClassifier which is the base
/// class for asynchronous implementations of text classifiers, e.g. for syntax highlighting and
/// syntax error annotations
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Tagging;
namespace QtVsTools.Qml.Classification
{
using HelperTypes;
///
/// A SharedTagList is a list of tracking tags (instances of TrackingTag), sorted by starting
/// location. It works as write-once-read-many data storage. Write access must be requested and
/// will only be granted to the first object/thread, which will be responsible for filling in
/// the data. Once writing is complete, concurrent read-only access will then be allowed.
///
class SharedTagList : Concurrent
{
SortedList data = new SortedList();
object owner;
public bool Ready { get; private set; }
public enum AccessType { ReadOnly, ReadWrite }
public AccessType RequestWriteAccess(object client)
{
EnterCriticalSection();
if (owner == null) {
owner = client;
return AccessType.ReadWrite;
} else {
LeaveCriticalSection();
return AccessType.ReadOnly;
}
}
public void WriteComplete(object client)
{
if (owner != client)
return;
Ready = true;
try {
LeaveCriticalSection();
} catch { }
}
public void AddRange(object client, IEnumerable tags)
{
if (owner != client || Ready)
return;
foreach (var tag in tags)
Add(client, tag);
}
public void Add(object client, TrackingTag tag)
{
if (owner != client || Ready)
return;
data[tag.Start] = tag;
}
class TrackingTagComparer : Comparer
{
ITextSnapshot snapshot;
public TrackingTagComparer(ITextSnapshot snapshot)
{
this.snapshot = snapshot;
}
public override int Compare(TrackingTag t1, TrackingTag t2)
{
int t1Version = t1.Snapshot.Version.VersionNumber;
int t2Version = t2.Snapshot.Version.VersionNumber;
if (t1Version == t2Version && t2Version == snapshot.Version.VersionNumber)
return Comparer.Default.Compare(t1.Start, t2.Start);
var t1Mapped = t1.MapToSnapshot(snapshot);
var t2Mapped = t2.MapToSnapshot(snapshot);
return Comparer.Default.Compare(t1Mapped.Span.Start, t2Mapped.Span.Start);
}
}
///
/// Perform a binary search to find the tag whose start precedes a given location relative
/// to a text snapshot. If the tags in the list are relative to another version of the
/// text, their location will be mapped to the given snapshot.
///
/// Text snapshot
/// Location in the given snapshot
///
/// Index of the tag in the list; -1 indicates error
///
public int FindTagAtLocation(ITextSnapshot snapshot, int location)
{
if (!Ready)
return -1;
var firstTag = data.Values.FirstOrDefault();
if (firstTag == null)
return -1;
bool sameVersion =
(firstTag.Snapshot.Version.VersionNumber == snapshot.Version.VersionNumber);
int? idx = null;
if (sameVersion) {
idx = data.Keys.BinarySearch(location);
} else {
if (location >= snapshot.Length)
return -1;
var locationTag = new TrackingTag(snapshot, location, 1);
var comparer = new TrackingTagComparer(snapshot);
idx = data.Values.BinarySearch(locationTag, comparer);
}
if (idx == null)
return -1;
if (idx < 0) {
// location was not found; idx has the bitwise complement of the smallest element
// that is after location, or the bitwise complement of the list count in case all
// elements are before location.
if (~idx == 0) // first tag starts after location
return -1;
// Because we are looking for the nearest tag that starts before location, we will
// return the element that precedes the one found
idx = ~idx - 1;
}
return idx.Value;
}
public IList Values
{
get
{
if (!Ready)
return new List();
return data.Values;
}
}
}
///
/// Base class for QML classifier classes implementing the ITagger interface. This interface
/// is used in the Visual Studio text editor extensibility for e.g. syntax highlighting. The
/// processing of the QML source code is done asynchronously in a background thread in order
/// to prevent the UI thread from blocking.
///
/// The result of the processing is a list of tracking tags that is stored in a SharedTagList
/// and is made available to any instances of QmlAsyncClassifier working on the same source
/// code. This prevents the processing being invoked more than once for any given version of
/// that source code.
///
/// Derived classes are required to implement the processing of the source code as well as the
/// conversion from TrackingTag to the type expected by the Visual Studio text editor.
///
///
/// Type of classification tag expected by the Visual Studio text editor extensibility
///
abstract class QmlAsyncClassifier : ITagger where T : ITag
{
protected enum ClassificationRefresh
{
FullText,
TagsOnly
}
///
/// Process QML source code. Implementations will override this method with the specific
/// processing required to convert the parser results into a list of tracking tags
///
/// The current version of the source code
/// The result of parsing the source code
/// Shared list of tracking tags
///
/// If true, the instance is required to populate the list of tags;
/// otherwise, the instance has read-only access and cannot modify the list.
///
///
/// Hint on how to notify Visual Studio concerning the tags in the list
/// FullText: refresh the entire contents of the text editor
/// TagsOnly: refresh only the spans pointed to by the tags
///
protected abstract ClassificationRefresh ProcessText(
ITextSnapshot snapshot,
Parser parseResult,
SharedTagList tagList,
bool writeAccess);
///
/// Conversion from TrackingTag to the type T of classification tag expected by the
/// Visual Studio text editor extensibility.
///
/// TrackingTag to convert
/// Instance of T corresponding to the given TrackingTag
protected abstract T GetClassification(TrackingTag tag);
protected ITextView TextView { get; private set; }
protected ITextBuffer Buffer { get; private set; }
readonly object criticalSection = new object();
string classificationType;
ParserKey currentParserKey;
TagListKey currentTagListKey;
SharedTagList currentTagList;
Dispatcher dispatcher;
DispatcherTimer timer;
bool flag = false;
protected QmlAsyncClassifier(
string classificationType,
ITextView textView,
ITextBuffer buffer)
{
TextView = textView;
textView.Closed += TextView_Closed;
Buffer = buffer;
buffer.Changed += Buffer_Changed;
dispatcher = Dispatcher.CurrentDispatcher;
timer = new DispatcherTimer(DispatcherPriority.ApplicationIdle, dispatcher)
{
Interval = TimeSpan.FromMilliseconds(250)
};
timer.Tick += Timer_Tick;
currentParserKey = null;
currentTagListKey = null;
currentTagList = null;
this.classificationType = classificationType;
AsyncParse(buffer.CurrentSnapshot);
}
private void TextView_Closed(object sender, EventArgs e)
{
if (currentParserKey != null) {
ParserStore.Instance.Release(this, currentParserKey);
currentParserKey = null;
}
if (currentTagListKey != null) {
TagListStore.Instance.Release(this, currentTagListKey);
currentTagListKey = null;
}
currentTagList = null;
}
private void Buffer_Changed(object sender, TextContentChangedEventArgs e)
{
timer.Stop();
AsyncParse(e.After);
}
private void Timer_Tick(object sender, EventArgs e)
{
timer.Stop();
AsyncParse(Buffer.CurrentSnapshot);
}
private async void AsyncParse(ITextSnapshot snapshot)
{
lock (criticalSection) {
if (flag)
return;
flag = true;
}
var newParserKey = new ParserKey(snapshot);
var newTagListKey = new TagListKey(classificationType, snapshot);
if (newParserKey == currentParserKey || newTagListKey == currentTagListKey)
return;
ParserKey oldParserKey = null;
TagListKey oldTagListKey = null;
await Task.Run(() =>
{
var parser = ParserStore.Instance.Get(this, newParserKey);
var tagList = TagListStore.Instance.Get(this, newTagListKey);
var refresh = ClassificationRefresh.FullText;
try {
var accessType = tagList.RequestWriteAccess(this);
refresh = ProcessText(snapshot, parser, tagList,
accessType == SharedTagList.AccessType.ReadWrite);
} finally {
tagList.WriteComplete(this);
}
oldParserKey = currentParserKey;
currentParserKey = newParserKey;
oldTagListKey = currentTagListKey;
currentTagListKey = newTagListKey;
currentTagList = tagList;
RefreshClassification(snapshot, refresh, tagList);
var currentVersion = Buffer.CurrentSnapshot.Version;
if (snapshot.Version.VersionNumber == currentVersion.VersionNumber)
timer.Stop();
else
timer.Start();
});
lock (criticalSection) {
flag = false;
}
await Task.Run(() =>
{
if (oldParserKey != null)
ParserStore.Instance.Release(this, oldParserKey);
if (oldTagListKey != null)
TagListStore.Instance.Release(this, oldTagListKey);
});
}
private void RefreshClassification(
ITextSnapshot snapshot,
ClassificationRefresh refresh,
SharedTagList tagList)
{
var tagsChangedHandler = TagsChanged;
if (refresh == ClassificationRefresh.FullText) {
var span = new SnapshotSpan(Buffer.CurrentSnapshot,
0, Buffer.CurrentSnapshot.Length);
if (tagsChangedHandler != null)
tagsChangedHandler.Invoke(this, new SnapshotSpanEventArgs(span));
} else {
foreach (var tag in tagList.Values) {
var tagMapped = tag.MapToSnapshot(snapshot);
if (tagsChangedHandler != null)
tagsChangedHandler.Invoke(this, new SnapshotSpanEventArgs(tagMapped.Span));
}
}
}
public IEnumerable> GetTags(NormalizedSnapshotSpanCollection spans)
{
if (currentTagList == null || !currentTagList.Ready)
yield break;
var firstTag = currentTagList.Values.FirstOrDefault();
if (firstTag == null)
yield break;
var snapshot = spans[0].Snapshot;
bool sameVersion =
(firstTag.Snapshot.Version.VersionNumber == snapshot.Version.VersionNumber);
foreach (var span in spans) {
int idx = currentTagList.FindTagAtLocation(snapshot, span.Start);
if (idx == -1)
continue;
for (; idx < currentTagList.Values.Count; idx++) {
var tag = currentTagList.Values[idx];
if (sameVersion && tag.Start > span.End)
break;
var tagMapped = tag.MapToSnapshot(snapshot);
if (tagMapped.Span.Length == 0)
continue;
if (!sameVersion && tagMapped.Span.Start > span.End)
break;
if (!span.IntersectsWith(tagMapped.Span))
continue;
var classification = GetClassification(tag);
if (classification == null)
continue;
var tracking = tagMapped.Tag.Span;
yield return
new TagSpan(tracking.GetSpan(snapshot), classification);
}
}
}
public event EventHandler TagsChanged;
}
namespace HelperTypes
{
public static class BinarySearchExtensions
{
///
/// Generic BinarySearch method that will work on any IList(T),
/// based on Microsoft’s ownArray.BinarySearch(T) implementation
/// Adapted from http://philosopherdeveloper.com/posts/whats-annoying-about-sorted-list-index-of-key.html
///
///
/// The index of the specified value in the specified list, if value is found;
/// otherwise, a negative number. If value is not found and value is less than one or
/// more elements in list, the negative number returned is the bitwise complement of
/// the index of the first element that is larger than value. If value is not found and
/// value is greater than all elements in list, the negative number returned is the
/// bitwise complement of (the index of the last element plus 1). If this method is
/// called with a non-sorted list, the return value can be incorrect and a negative
/// number could be returned, even if value is present in list.
/// (cf. https://docs.microsoft.com/en-us/dotnet/api/system.array.binarysearch)
///
/// In case of error, returns null.
///
public static int? BinarySearch(
this IList list,
int index,
int length,
T value,
IComparer comparer)
{
if (list == null)
return null;
if (index < 0 || length < 0)
return null;
if (list.Count - index < length)
return null;
int lower = index;
int upper = (index + length) - 1;
while (lower <= upper) {
int adjustedIndex = lower + ((upper - lower) >> 1);
int comparison = comparer.Compare(list[adjustedIndex], value);
if (comparison == 0)
return adjustedIndex;
else if (comparison < 0)
lower = adjustedIndex + 1;
else
upper = adjustedIndex - 1;
}
return ~lower;
}
public static int? BinarySearch(this IList list, T value, IComparer comparer)
{
return list.BinarySearch(0, list.Count, value, comparer);
}
public static int? BinarySearch(this IList list, T value)
where T : IComparable
{
return list.BinarySearch(value, Comparer.Default);
}
}
///
/// Base class for thread-safe, indexed data storage. References to stored values are
/// explicitly tracked to allow for timely disposal as soon as a value becomes
/// unreferenced. Shared data stores are intended to be used as singletons. For this
/// purpose, classes that inherit from SharedDataStore will include a static instance
/// member.
///
/// Value key type
/// Value type
///
/// Type of singleton instance, i.e. the same class that is derived from SharedDataStore
///
abstract class SharedDataStore
where TInstance : SharedDataStore, new()
{
protected abstract TValue GetDefaultValue(TKey key);
class ValueRef
{
public TValue Value { get; set; }
public HashSet