/**************************************************************************** ** ** 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.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 { readonly 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 { readonly 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); private ITextView TextView { get; } private ITextBuffer Buffer { get; } readonly object criticalSection = new object(); readonly string classificationType; ParserKey currentParserKey; TagListKey currentTagListKey; SharedTagList currentTagList; readonly Dispatcher dispatcher; readonly 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; Parse(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(); Parse(e.After); } private void Timer_Tick(object sender, EventArgs e) { timer.Stop(); Parse(Buffer.CurrentSnapshot); } private void Parse(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; _ = 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; } _ = 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 ClientObjects { get; set; } } readonly Dictionary data = new Dictionary(); static readonly object staticCriticalSection = new object(); readonly object criticalSection = new object(); protected SharedDataStore() { data = new Dictionary(); } public TValue Get(object client, TKey key) { lock (criticalSection) { if (!data.TryGetValue(key, out ValueRef valueRef)) { valueRef = new ValueRef { Value = GetDefaultValue(key), ClientObjects = new HashSet { client } }; data.Add(key, valueRef); } else { valueRef.ClientObjects.Add(client); } return valueRef.Value; } } public void Release(object client, TKey key) { IDisposable disposable = null; lock (criticalSection) { if (data.TryGetValue(key, out ValueRef valueRef)) { valueRef.ClientObjects.Remove(client); if (valueRef.ClientObjects.Count == 0) { data.Remove(key); disposable = valueRef.Value as IDisposable; } } } if (disposable != null) disposable.Dispose(); } private static TInstance instance = null; public static TInstance Instance { get { lock (staticCriticalSection) { if (instance == null) { instance = new TInstance(); } return instance; } } } } class TagListKey { private string Classification { get; } private ITextSnapshot Snapshot { get; } public TagListKey(string classification, ITextSnapshot snapshot) { Classification = classification; Snapshot = snapshot; } public override bool Equals(object obj) { var that = obj as TagListKey; if (that == null) return false; if (Classification != that.Classification) return false; if (Snapshot.TextBuffer != that.Snapshot.TextBuffer) return false; if (Snapshot.Version.VersionNumber != that.Snapshot.Version.VersionNumber) return false; return true; } public override int GetHashCode() { var hashBase = new Tuple( Classification, Snapshot.TextBuffer, Snapshot.Version.VersionNumber); return hashBase.GetHashCode(); } } class TagListStore : SharedDataStore { protected override SharedTagList GetDefaultValue(TagListKey key) { return new SharedTagList(); } } class ParserKey { public ITextSnapshot Snapshot { get; } public ParserKey(ITextSnapshot snapshot) { Snapshot = snapshot; } public override bool Equals(object obj) { var that = obj as ParserKey; if (that == null) return false; if (Snapshot.TextBuffer != that.Snapshot.TextBuffer) return false; if (Snapshot.Version.VersionNumber != that.Snapshot.Version.VersionNumber) return false; return true; } public override int GetHashCode() { var hashBase = new Tuple( Snapshot.TextBuffer, Snapshot.Version.VersionNumber); return hashBase.GetHashCode(); } } class ParserStore : SharedDataStore { protected override Parser GetDefaultValue(ParserKey key) { return Parser.Parse(key.Snapshot.GetText()); } } } }