aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMiguel Costa <miguel.costa@qt.io>2018-07-18 17:58:46 +0200
committerMiguel Costa <miguel.costa@qt.io>2018-08-13 08:56:34 +0000
commitdc0bd5767f1868f47cfca61ba44a3be130ff917a (patch)
tree9e54bd17eac5ebeb9c5e30807091e17221459f01
parent726eae6e836f221e3148d3f5403151ea35449c5c (diff)
Optimize QML syntax highlighting for larger files
The performance of the QML syntax highlighting was noticeably poor when handling larger files (> 3000 lines); it took some time for the highlighting to be displayed after the file had been opened, and also, when scrolling down, the text editor would freeze for a moment. The optimization is mostly encapsulated in the newly created QmlAsyncClassifier class and auxiliary types. The approach was twofold: 1. Classifiers that need to parse the same version of a QML file will now use a shared QML parser instance; only the first access will trigger the parsing and will store the result of classification in a shared list of tags. Subsequent accesses by other classifier objects will just read the shared data. Previously, each classifier had its own instance of the QML parser. Because VS will create several classifier objects for the same file, this meant that the QML parser would be needlessly invoked several times for the exact same content. This is what caused the delay in displaying the syntax highlighting. 2. Access to the classification results is now achieved through a binary search on the list of tags. Previously, this list was accessed linearly, which caused a delay when the classifiers were queried by VS (e.g. when scrolling down in the text editor). Change-Id: I83544a9b03f964ff11e9ce3cbbde54304bb7ca03 Reviewed-by: Joerg Bornemann <joerg.bornemann@qt.io> Reviewed-by: Oliver Wolff <oliver.wolff@qt.io>
-rw-r--r--src/qtvstools/QML/Classification/QmlAsyncClassifier.cs652
-rw-r--r--src/qtvstools/QML/Classification/QmlClassificationFormat.cs24
-rw-r--r--src/qtvstools/QML/Classification/QmlErrorClassifier.cs107
-rw-r--r--src/qtvstools/QML/Classification/QmlSyntaxClassifier.cs140
-rw-r--r--src/qtvstools/QML/Classification/QmlTag.cs101
-rw-r--r--src/qtvstools/QtVsTools.csproj1
6 files changed, 768 insertions, 257 deletions
diff --git a/src/qtvstools/QML/Classification/QmlAsyncClassifier.cs b/src/qtvstools/QML/Classification/QmlAsyncClassifier.cs
new file mode 100644
index 00000000..2f7ed473
--- /dev/null
+++ b/src/qtvstools/QML/Classification/QmlAsyncClassifier.cs
@@ -0,0 +1,652 @@
+/****************************************************************************
+**
+** 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;
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ class SharedTagList
+ {
+ SortedList<int, TrackingTag> data = new SortedList<int, TrackingTag>();
+ Mutex exclusiveWriteAccess = new Mutex();
+ object owner;
+
+ public bool Ready { get; private set; }
+
+ public enum AccessType { ReadOnly, ReadWrite }
+ public AccessType RequestWriteAccess(object client)
+ {
+ exclusiveWriteAccess.WaitOne();
+ if (owner == null) {
+ owner = client;
+ return AccessType.ReadWrite;
+ } else {
+ exclusiveWriteAccess.ReleaseMutex();
+ return AccessType.ReadOnly;
+ }
+ }
+
+ public void WriteComplete(object client)
+ {
+ if (owner != client)
+ return;
+ Ready = true;
+ try {
+ exclusiveWriteAccess.ReleaseMutex();
+ } catch { }
+ }
+
+ public void AddRange(object client, IEnumerable<TrackingTag> 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<TrackingTag>
+ {
+ 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<int>.Default.Compare(t1.Start, t2.Start);
+
+ var t1Mapped = t1.MapToSnapshot(snapshot);
+ var t2Mapped = t2.MapToSnapshot(snapshot);
+ return Comparer<int>.Default.Compare(t1Mapped.Span.Start, t2Mapped.Span.Start);
+ }
+ }
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ /// <param name="snapshot">Text snapshot</param>
+ /// <param name="location">Location in the given snapshot</param>
+ /// <returns>
+ /// Index of the tag in the list; -1 indicates error
+ /// </returns>
+ 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<TrackingTag> Values
+ {
+ get
+ {
+ if (!Ready)
+ return new List<TrackingTag>();
+ return data.Values;
+ }
+ }
+ }
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ /// <typeparam name="T">
+ /// Type of classification tag expected by the Visual Studio text editor extensibility
+ /// </typeparam>
+ abstract class QmlAsyncClassifier<T> : ITagger<T> where T : ITag
+ {
+ protected enum ClassificationRefresh
+ {
+ FullText,
+ TagsOnly
+ }
+
+ /// <summary>
+ /// 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
+ /// </summary>
+ /// <param name="snapshot">The current version of the source code</param>
+ /// <param name="parseResult">The result of parsing the source code</param>
+ /// <param name="tagList">Shared list of tracking tags</param>
+ /// <param name="writeAccess">
+ /// If true, the instance is required to populate the list of tags;
+ /// otherwise, the instance has read-only access and cannot modify the list.
+ /// </param>
+ /// <returns>
+ /// 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
+ /// </returns>
+ protected abstract ClassificationRefresh ProcessText(
+ ITextSnapshot snapshot,
+ Parser parseResult,
+ SharedTagList tagList,
+ bool writeAccess);
+
+ /// <summary>
+ /// Conversion from TrackingTag to the type T of classification tag expected by the
+ /// Visual Studio text editor extensibility.
+ /// </summary>
+ /// <param name="tag">TrackingTag to convert</param>
+ /// <returns>Instance of T corresponding to the given TrackingTag</returns>
+ 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<ITagSpan<T>> 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<T>(tracking.GetSpan(snapshot), classification);
+ }
+ }
+ }
+
+ public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
+ }
+
+ namespace HelperTypes
+ {
+ public static class BinarySearchExtensions
+ {
+ /// <summary>
+ /// 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
+ /// </summary>
+ /// <returns>
+ /// 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.
+ /// </returns>
+ public static int? BinarySearch<T>(
+ this IList<T> list,
+ int index,
+ int length,
+ T value,
+ IComparer<T> 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<T>(this IList<T> list, T value, IComparer<T> comparer)
+ {
+ return list.BinarySearch(0, list.Count, value, comparer);
+ }
+
+ public static int? BinarySearch<T>(this IList<T> list, T value)
+ where T : IComparable<T>
+ {
+ return list.BinarySearch(value, Comparer<T>.Default);
+ }
+ }
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ /// <typeparam name="TKey">Value key type</typeparam>
+ /// <typeparam name="TValue">Value type</typeparam>
+ /// <typeparam name="TInstance">
+ /// Type of singleton instance, i.e. the same class that is derived from SharedDataStore
+ /// </typeparam>
+ abstract class SharedDataStore<TKey, TValue, TInstance>
+ where TInstance : SharedDataStore<TKey, TValue, TInstance>, new()
+ {
+ protected abstract TValue GetDefaultValue(TKey key);
+
+ class ValueRef
+ {
+ public TValue Value { get; set; }
+ public HashSet<object> ClientObjects { get; set; }
+ }
+ Dictionary<TKey, ValueRef> data = new Dictionary<TKey, ValueRef>();
+
+ static readonly object staticCriticalSection = new object();
+ readonly object criticalSection = new object();
+
+ protected SharedDataStore()
+ {
+ data = new Dictionary<TKey, ValueRef>();
+ }
+
+ public TValue Get(object client, TKey key)
+ {
+ lock (criticalSection) {
+ ValueRef valueRef;
+ if (!data.TryGetValue(key, out valueRef)) {
+ valueRef = new ValueRef
+ {
+ Value = GetDefaultValue(key),
+ ClientObjects = new HashSet<object> { 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) {
+ ValueRef valueRef;
+ if (data.TryGetValue(key, out 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
+ {
+ public string Classification { get; private set; }
+ public ITextSnapshot Snapshot { get; private set; }
+ 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<string, ITextBuffer, int>(
+ Classification, Snapshot.TextBuffer,
+ Snapshot.Version.VersionNumber);
+ return hashBase.GetHashCode();
+ }
+ }
+
+ class TagListStore : SharedDataStore<TagListKey, SharedTagList, TagListStore>
+ {
+ protected override SharedTagList GetDefaultValue(TagListKey key)
+ {
+ return new SharedTagList();
+ }
+ }
+
+ class ParserKey
+ {
+ public ITextSnapshot Snapshot { get; private set; }
+ 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<ITextBuffer, int>(
+ Snapshot.TextBuffer, Snapshot.Version.VersionNumber);
+ return hashBase.GetHashCode();
+ }
+ }
+
+ class ParserStore : SharedDataStore<ParserKey, Parser, ParserStore>
+ {
+ protected override Parser GetDefaultValue(ParserKey key)
+ {
+ return Parser.Parse(key.Snapshot.GetText());
+ }
+ }
+ }
+}
diff --git a/src/qtvstools/QML/Classification/QmlClassificationFormat.cs b/src/qtvstools/QML/Classification/QmlClassificationFormat.cs
index 3f33e16c..80dc9ab4 100644
--- a/src/qtvstools/QML/Classification/QmlClassificationFormat.cs
+++ b/src/qtvstools/QML/Classification/QmlClassificationFormat.cs
@@ -37,8 +37,8 @@ using Microsoft.VisualStudio.Utilities;
namespace QtVsTools.Qml.Classification
{
[Export(typeof(EditorFormatDefinition))]
- [ClassificationType(ClassificationTypeNames = QmlTag.Keyword)]
- [Name(QmlTag.Keyword)]
+ [ClassificationType(ClassificationTypeNames = QmlSyntaxTag.Keyword)]
+ [Name(QmlSyntaxTag.Keyword)]
[UserVisible(true)]
[Order(Before = Priority.Default)]
internal sealed class QmlKeywordFormat : ClassificationFormatDefinition
@@ -51,8 +51,8 @@ namespace QtVsTools.Qml.Classification
}
[Export(typeof(EditorFormatDefinition))]
- [ClassificationType(ClassificationTypeNames = QmlTag.Numeric)]
- [Name(QmlTag.Numeric)]
+ [ClassificationType(ClassificationTypeNames = QmlSyntaxTag.Numeric)]
+ [Name(QmlSyntaxTag.Numeric)]
[UserVisible(true)]
[Order(Before = Priority.Default)]
internal sealed class QmlNumberFormat : ClassificationFormatDefinition
@@ -65,8 +65,8 @@ namespace QtVsTools.Qml.Classification
}
[Export(typeof(EditorFormatDefinition))]
- [ClassificationType(ClassificationTypeNames = QmlTag.String)]
- [Name(QmlTag.String)]
+ [ClassificationType(ClassificationTypeNames = QmlSyntaxTag.String)]
+ [Name(QmlSyntaxTag.String)]
[UserVisible(true)]
[Order(Before = Priority.Default)]
internal sealed class QmlStringFormat : ClassificationFormatDefinition
@@ -79,10 +79,10 @@ namespace QtVsTools.Qml.Classification
}
[Export(typeof(EditorFormatDefinition))]
- [ClassificationType(ClassificationTypeNames = QmlTag.TypeName)]
- [Name(QmlTag.TypeName)]
+ [ClassificationType(ClassificationTypeNames = QmlSyntaxTag.TypeName)]
+ [Name(QmlSyntaxTag.TypeName)]
[UserVisible(true)]
- [Order(Before = Priority.Default, After = QmlTag.Keyword)]
+ [Order(Before = Priority.Default, After = QmlSyntaxTag.Keyword)]
internal sealed class QmlTypeNameFormat : ClassificationFormatDefinition
{
public QmlTypeNameFormat()
@@ -93,10 +93,10 @@ namespace QtVsTools.Qml.Classification
}
[Export(typeof(EditorFormatDefinition))]
- [ClassificationType(ClassificationTypeNames = QmlTag.Binding)]
- [Name(QmlTag.Binding)]
+ [ClassificationType(ClassificationTypeNames = QmlSyntaxTag.Binding)]
+ [Name(QmlSyntaxTag.Binding)]
[UserVisible(true)]
- [Order(Before = Priority.Default, After = QmlTag.Keyword)]
+ [Order(Before = Priority.Default, After = QmlSyntaxTag.Keyword)]
internal sealed class QmlBindingFormat : ClassificationFormatDefinition
{
public QmlBindingFormat()
diff --git a/src/qtvstools/QML/Classification/QmlErrorClassifier.cs b/src/qtvstools/QML/Classification/QmlErrorClassifier.cs
index 95e57ccb..3e5f62e0 100644
--- a/src/qtvstools/QML/Classification/QmlErrorClassifier.cs
+++ b/src/qtvstools/QML/Classification/QmlErrorClassifier.cs
@@ -51,108 +51,37 @@ namespace QtVsTools.Qml.Classification
public ITagger<T> CreateTagger<T>(ITextView textView, ITextBuffer buffer) where T : ITag
{
- return new QmlErrorClassifier(buffer, classificationTypeRegistry) as ITagger<T>;
+ QmlClassificationType.InitClassificationTypes(classificationTypeRegistry);
+ return new QmlErrorClassifier(textView, buffer) as ITagger<T>;
}
}
- internal sealed class QmlErrorClassifier : ITagger<ErrorTag>
+ internal sealed class QmlErrorClassifier : QmlAsyncClassifier<ErrorTag>
{
- ITextBuffer buffer;
- Dispatcher dispatcher;
- DispatcherTimer timer;
-
- internal QmlErrorClassifier(ITextBuffer buffer,
- IClassificationTypeRegistryService typeService)
- {
- this.buffer = buffer;
- QmlClassificationType.InitClassificationTypes(typeService);
- ParseQML(buffer.CurrentSnapshot);
- buffer.Changed += Buffer_Changed;
-
- dispatcher = Dispatcher.CurrentDispatcher;
- timer = new DispatcherTimer(DispatcherPriority.ApplicationIdle, dispatcher)
- {
- Interval = TimeSpan.FromMilliseconds(500)
- };
- timer.Tick += Timer_Tick;
- }
-
- private void Timer_Tick(object sender, EventArgs e)
- {
- timer.Stop();
- var snapshot = buffer.CurrentSnapshot;
- AsyncParseQML(snapshot);
- }
-
- private void Buffer_Changed(object sender, TextContentChangedEventArgs e)
- {
- AsyncParseQML(e.After);
- timer.Stop();
- timer.Start();
- }
-
- public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
-
- bool flag = false;
- List<QmlDiagnosticsTag> tags = new List<QmlDiagnosticsTag>();
- object syncChanged = new object();
-
- async void AsyncParseQML(ITextSnapshot snapshot)
+ internal QmlErrorClassifier(
+ ITextView textView,
+ ITextBuffer buffer)
+ : base("Error", textView, buffer)
{
- if (flag)
- return;
- flag = true;
- await Task.Run(() =>
- {
- ParseQML(snapshot);
- flag = false;
- var currentVersion = buffer.CurrentSnapshot.Version;
- if (snapshot.Version.VersionNumber == currentVersion.VersionNumber) {
- timer.Stop();
- } else {
- timer.Start();
- }
- });
}
- void ParseQML(ITextSnapshot snapshot)
+ protected override ClassificationRefresh ProcessText(
+ ITextSnapshot snapshot,
+ Parser parseResult,
+ SharedTagList tagList,
+ bool writeAccess)
{
- lock (syncChanged) {
- tags.Clear();
- var text = snapshot.GetText();
- using (var parser = Parser.Parse(text)) {
- if (!parser.ParsedCorrectly) {
- foreach (var diag in parser.DiagnosticMessages) {
- tags.Add(new QmlDiagnosticsTag(snapshot, diag));
- }
- }
+ if (writeAccess) {
+ foreach (var diag in parseResult.DiagnosticMessages) {
+ tagList.Add(this, new QmlDiagnosticsTag(snapshot, diag));
}
}
- var span = new SnapshotSpan(buffer.CurrentSnapshot, 0, buffer.CurrentSnapshot.Length);
- var tagsChangedHandler = TagsChanged;
- if (tagsChangedHandler != null)
- tagsChangedHandler.Invoke(this, new SnapshotSpanEventArgs(span));
+ return ClassificationRefresh.FullText;
}
- public IEnumerable<ITagSpan<ErrorTag>> GetTags(NormalizedSnapshotSpanCollection spans)
+ protected override ErrorTag GetClassification(TrackingTag tag)
{
- List<QmlDiagnosticsTag> tagsCopy;
- var snapshot = spans[0].Snapshot;
- lock (syncChanged) {
- tagsCopy = new List<QmlDiagnosticsTag>(tags);
- }
- foreach (var tag in tagsCopy) {
- var tagSpan = tag.ToTagSpan(snapshot);
- if (tagSpan.Span.Length == 0)
- continue;
-
- if (!spans.IntersectsWith(new NormalizedSnapshotSpanCollection(tagSpan.Span)))
- continue;
-
- yield return
- new TagSpan<ErrorTag>(tagSpan.Tag.Span.GetSpan(snapshot),
- new ErrorTag("ERROR"));
- }
+ return new ErrorTag("ERROR");
}
}
}
diff --git a/src/qtvstools/QML/Classification/QmlSyntaxClassifier.cs b/src/qtvstools/QML/Classification/QmlSyntaxClassifier.cs
index 5277a12e..c6d8305b 100644
--- a/src/qtvstools/QML/Classification/QmlSyntaxClassifier.cs
+++ b/src/qtvstools/QML/Classification/QmlSyntaxClassifier.cs
@@ -67,134 +67,50 @@ namespace QtVsTools.Qml.Classification
public ITagger<T> CreateTagger<T>(ITextView textView, ITextBuffer buffer) where T : ITag
{
- return new QmlSyntaxClassifier(buffer, classificationTypeRegistry) as ITagger<T>;
+ QmlClassificationType.InitClassificationTypes(classificationTypeRegistry);
+ return new QmlSyntaxClassifier(textView, buffer) as ITagger<T>;
}
}
- internal sealed class QmlSyntaxClassifier : ITagger<ClassificationTag>
+ internal sealed class QmlSyntaxClassifier : QmlAsyncClassifier<ClassificationTag>
{
- ITextBuffer buffer;
- Dispatcher dispatcher;
- DispatcherTimer timer;
-
- internal QmlSyntaxClassifier(ITextBuffer buffer,
- IClassificationTypeRegistryService typeService)
- {
- this.buffer = buffer;
- QmlClassificationType.InitClassificationTypes(typeService);
- ParseQML(buffer.CurrentSnapshot);
- buffer.Changed += Buffer_Changed;
-
- dispatcher = Dispatcher.CurrentDispatcher;
- timer = new DispatcherTimer(DispatcherPriority.ApplicationIdle, dispatcher)
- {
- Interval = TimeSpan.FromMilliseconds(500)
- };
- timer.Tick += Timer_Tick;
- }
-
- private void Timer_Tick(object sender, EventArgs e)
+ internal QmlSyntaxClassifier(
+ ITextView textView,
+ ITextBuffer buffer)
+ : base("Syntax", textView, buffer)
{
- timer.Stop();
- var snapshot = buffer.CurrentSnapshot;
- AsyncParseQML(snapshot);
}
- private void Buffer_Changed(object sender, TextContentChangedEventArgs e)
- {
- AsyncParseQML(e.After);
- timer.Stop();
- timer.Start();
- }
-
- public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
-
- bool flag = false;
- List<QmlTag> tags = new List<QmlTag>();
- object syncChanged = new object();
-
- async void AsyncParseQML(ITextSnapshot snapshot)
+ protected override ClassificationRefresh ProcessText(
+ ITextSnapshot snapshot,
+ Parser parseResult,
+ SharedTagList tagList,
+ bool writeAccess)
{
- if (flag)
- return;
- flag = true;
- await Task.Run(() =>
- {
- ParseQML(snapshot);
- flag = false;
- var currentVersion = buffer.CurrentSnapshot.Version;
- if (snapshot.Version.VersionNumber == currentVersion.VersionNumber) {
- timer.Stop();
- } else {
- timer.Start();
- }
- });
- }
+ bool parsedCorrectly = parseResult.ParsedCorrectly;
- void ParseQML(ITextSnapshot snapshot)
- {
- bool parsedCorrectly = true;
- lock (syncChanged) {
- tags.Clear();
- var text = snapshot.GetText();
- using (var parser = Parser.Parse(text)) {
- parsedCorrectly = parser.ParsedCorrectly;
- foreach (var token in parser.Tokens) {
- if (parsedCorrectly
- || token.Location.Offset < parser.FirstErrorOffset) {
- tags.AddRange(QmlTag.GetClassification(snapshot, token));
- }
- }
- foreach (var node in parser.AstNodes) {
- if (parsedCorrectly
- || node.FirstSourceLocation.Offset < parser.FirstErrorOffset) {
- tags.AddRange(QmlTag.GetClassification(snapshot, node));
- }
- }
- }
- }
- var tagsChangedHandler = TagsChanged;
- if (parsedCorrectly) {
- var span = new SnapshotSpan(buffer.CurrentSnapshot,
- 0, buffer.CurrentSnapshot.Length);
- if (tagsChangedHandler != null)
- tagsChangedHandler.Invoke(this, new SnapshotSpanEventArgs(span));
- } else {
- List<QmlTag> tagsCopy;
- lock (syncChanged) {
- tagsCopy = new List<QmlTag>(tags);
+ if (writeAccess) {
+ foreach (var token in parseResult.Tokens) {
+ tagList.AddRange(this, QmlSyntaxTag.GetClassification(snapshot, token));
}
- foreach (var tag in tagsCopy) {
- var tagSpan = tag.ToTagSpan(snapshot);
- if (tagsChangedHandler != null)
- tagsChangedHandler.Invoke(this, new SnapshotSpanEventArgs(tagSpan.Span));
+ foreach (var node in parseResult.AstNodes) {
+ tagList.AddRange(this, QmlSyntaxTag.GetClassification(snapshot, node));
}
}
+
+ if (parsedCorrectly)
+ return ClassificationRefresh.FullText;
+ else
+ return ClassificationRefresh.TagsOnly;
}
- public IEnumerable<ITagSpan<ClassificationTag>> GetTags(
- NormalizedSnapshotSpanCollection spans)
+ protected override ClassificationTag GetClassification(TrackingTag tag)
{
- List<QmlTag> tagsCopy;
- var snapshot = spans[0].Snapshot;
- lock (syncChanged) {
- tagsCopy = new List<QmlTag>(tags);
- }
- foreach (var tag in tagsCopy) {
- var tagSpan = tag.ToTagSpan(snapshot);
- if (tagSpan.Span.Length == 0)
- continue;
-
- if (!spans.IntersectsWith(new NormalizedSnapshotSpanCollection(tagSpan.Span)))
- continue;
+ var syntaxTag = tag as QmlSyntaxTag;
+ if (syntaxTag == null || syntaxTag.ClassificationType == null)
+ return null;
- if (tag.ClassificationType == null)
- continue;
-
- yield return
- new TagSpan<ClassificationTag>(tagSpan.Tag.Span.GetSpan(snapshot),
- new ClassificationTag(tag.ClassificationType));
- }
+ return new ClassificationTag(syntaxTag.ClassificationType);
}
}
}
diff --git a/src/qtvstools/QML/Classification/QmlTag.cs b/src/qtvstools/QML/Classification/QmlTag.cs
index 1654722d..a3783fb9 100644
--- a/src/qtvstools/QML/Classification/QmlTag.cs
+++ b/src/qtvstools/QML/Classification/QmlTag.cs
@@ -43,9 +43,31 @@ namespace QtVsTools.Qml.Classification
using VisualStudio.Text.Extensions;
/// <summary>
+ /// Represents a classification tag that can be mapped onto future versions of the source code
+ /// </summary>
+ public class TrackingTag : ITag
+ {
+ public ITextSnapshot Snapshot { get; private set; }
+ public int Start { get; private set; }
+ public int Length { get; private set; }
+ public ITrackingSpan Span { get; private set; }
+ public TrackingTag(ITextSnapshot snapshot, int start, int length)
+ {
+ Snapshot = snapshot;
+ Start = start;
+ Length = length;
+ Span = snapshot.CreateTrackingSpan(start, length, SpanTrackingMode.EdgeExclusive);
+ }
+ public ITagSpan<TrackingTag> MapToSnapshot(ITextSnapshot snapshot)
+ {
+ return new TagSpan<TrackingTag>(Span.GetSpan(snapshot), this);
+ }
+ }
+
+ /// <summary>
/// Represents the classification of a QML syntax element
/// </summary>
- public class QmlTag : ITag
+ public class QmlSyntaxTag : TrackingTag
{
public const string Keyword = "keyword.qml";
public const string Numeric = "numeric.qml";
@@ -56,17 +78,15 @@ namespace QtVsTools.Qml.Classification
public SyntaxElement SyntaxElement { get; private set; }
public SourceLocation SourceLocation { get; private set; }
- public ITrackingSpan Span { get; private set; }
public IClassificationType ClassificationType { get; private set; }
- private QmlTag(ITextSnapshot snapshot, SourceLocation location)
+ private QmlSyntaxTag(ITextSnapshot snapshot, SourceLocation location)
+ : base(snapshot, location.Offset, location.Length)
{
SourceLocation = location;
- Span = snapshot.CreateTrackingSpan(
- location.Offset, location.Length, SpanTrackingMode.EdgeExclusive);
}
- public QmlTag(
+ public QmlSyntaxTag(
ITextSnapshot snapshot,
SyntaxElement element,
string classificationType,
@@ -77,12 +97,7 @@ namespace QtVsTools.Qml.Classification
ClassificationType = QmlClassificationType.Get(classificationType);
}
- public ITagSpan<QmlTag> ToTagSpan(ITextSnapshot snapshot)
- {
- return new TagSpan<QmlTag>(Span.GetSpan(snapshot), this);
- }
-
- static QmlTag GetClassificationTag(
+ static QmlSyntaxTag GetClassificationTag(
ITextSnapshot snapshot,
AstNode parentNode,
string classificationType,
@@ -100,7 +115,7 @@ namespace QtVsTools.Qml.Classification
Length = lastName.Offset + lastName.Length - firstName.Offset
};
- return new QmlTag(snapshot, parentNode, classificationType, fullNameLocation);
+ return new QmlSyntaxTag(snapshot, parentNode, classificationType, fullNameLocation);
}
public static readonly HashSet<string> QmlBasicTypes = new HashSet<string> {
@@ -109,23 +124,23 @@ namespace QtVsTools.Qml.Classification
"date", "point", "rect", "size", "alias"
};
- public static IEnumerable<QmlTag> GetClassification(
+ public static IEnumerable<QmlSyntaxTag> GetClassification(
ITextSnapshot snapshot,
SyntaxElement element)
{
- var tags = new List<QmlTag>();
+ var tags = new List<QmlSyntaxTag>();
if (element is KeywordToken) {
var token = element as KeywordToken;
- tags.Add(new QmlTag(snapshot, token, Keyword, token.Location));
+ tags.Add(new QmlSyntaxTag(snapshot, token, Keyword, token.Location));
} else if (element is NumberToken) {
var token = element as NumberToken;
- tags.Add(new QmlTag(snapshot, token, Numeric, token.Location));
+ tags.Add(new QmlSyntaxTag(snapshot, token, Numeric, token.Location));
} else if (element is StringToken) {
var token = element as StringToken;
- tags.Add(new QmlTag(snapshot, token, String, token.Location));
+ tags.Add(new QmlSyntaxTag(snapshot, token, String, token.Location));
} else if (element is CommentToken) {
var token = element as CommentToken;
@@ -139,12 +154,12 @@ namespace QtVsTools.Qml.Classification
commentLocation.Offset -= 2;
commentLocation.Length += 4;
}
- tags.Add(new QmlTag(snapshot, token, Comment, commentLocation));
+ tags.Add(new QmlSyntaxTag(snapshot, token, Comment, commentLocation));
} else if (element is UiImport) {
var node = element as UiImport;
if (node.ImportIdToken.Length > 0)
- tags.Add(new QmlTag(snapshot, node, TypeName, node.ImportIdToken));
+ tags.Add(new QmlSyntaxTag(snapshot, node, TypeName, node.ImportIdToken));
} else if (element is UiObjectDefinition) {
var node = element as UiObjectDefinition;
@@ -194,59 +209,52 @@ namespace QtVsTools.Qml.Classification
if (node.Type == UiPublicMemberType.Property && node.TypeToken.Length > 0) {
var typeName = snapshot.GetText(node.TypeToken);
if (QmlBasicTypes.Contains(typeName))
- tags.Add(new QmlTag(snapshot, node, Keyword, node.TypeToken));
+ tags.Add(new QmlSyntaxTag(snapshot, node, Keyword, node.TypeToken));
else
- tags.Add(new QmlTag(snapshot, node, TypeName, node.TypeToken));
+ tags.Add(new QmlSyntaxTag(snapshot, node, TypeName, node.TypeToken));
}
if (node.IdentifierToken.Length > 0)
- tags.Add(new QmlTag(snapshot, node, Binding, node.IdentifierToken));
+ tags.Add(new QmlSyntaxTag(snapshot, node, Binding, node.IdentifierToken));
}
return tags;
}
}
- public class QmlDiagnosticsTag : ITag
+ public class QmlDiagnosticsTag : TrackingTag
{
public DiagnosticMessage DiagnosticMessage { get; private set; }
- public ITrackingSpan Span { get; private set; }
public QmlDiagnosticsTag(ITextSnapshot snapshot, DiagnosticMessage diagnosticMessage)
+ : base(snapshot, diagnosticMessage.Location.Offset, diagnosticMessage.Location.Length)
{
DiagnosticMessage = diagnosticMessage;
- Span = snapshot.CreateTrackingSpan(
- diagnosticMessage.Location.Offset, diagnosticMessage.Location.Length,
- SpanTrackingMode.EdgeExclusive);
- }
- public ITagSpan<QmlDiagnosticsTag> ToTagSpan(ITextSnapshot snapshot)
- {
- return new TagSpan<QmlDiagnosticsTag>(Span.GetSpan(snapshot), this);
}
}
internal static class QmlClassificationType
{
[Export(typeof(ClassificationTypeDefinition))]
- [Name(QmlTag.Keyword)]
+ [Name(QmlSyntaxTag.Keyword)]
internal static ClassificationTypeDefinition qmlKeyword = null;
[Export(typeof(ClassificationTypeDefinition))]
- [Name(QmlTag.Numeric)]
+ [Name(QmlSyntaxTag.Numeric)]
internal static ClassificationTypeDefinition qmlNumber = null;
[Export(typeof(ClassificationTypeDefinition))]
- [Name(QmlTag.String)]
+ [Name(QmlSyntaxTag.String)]
internal static ClassificationTypeDefinition qmlString = null;
[Export(typeof(ClassificationTypeDefinition))]
- [Name(QmlTag.Comment)]
+ [Name(QmlSyntaxTag.Comment)]
internal static ClassificationTypeDefinition qmlComment = null;
[Export(typeof(ClassificationTypeDefinition))]
- [Name(QmlTag.TypeName)]
+ [Name(QmlSyntaxTag.TypeName)]
internal static ClassificationTypeDefinition qmlTypeName = null;
[Export(typeof(ClassificationTypeDefinition))]
- [Name(QmlTag.Binding)]
+ [Name(QmlSyntaxTag.Binding)]
internal static ClassificationTypeDefinition qmlBinding = null;
public static IDictionary<string, IClassificationType> ClassificationTypes
@@ -260,14 +268,19 @@ namespace QtVsTools.Qml.Classification
return;
ClassificationTypes = new Dictionary<string, IClassificationType>
{
- { QmlTag.Keyword, typeService.GetClassificationType(QmlTag.Keyword) },
- { QmlTag.Numeric, typeService.GetClassificationType(QmlTag.Numeric) },
- { QmlTag.String, typeService.GetClassificationType(QmlTag.String) },
- { QmlTag.TypeName, typeService.GetClassificationType(QmlTag.TypeName) },
- { QmlTag.Binding, typeService.GetClassificationType(QmlTag.Binding) },
+ { QmlSyntaxTag.Keyword,
+ typeService.GetClassificationType(QmlSyntaxTag.Keyword) },
+ { QmlSyntaxTag.Numeric,
+ typeService.GetClassificationType(QmlSyntaxTag.Numeric) },
+ { QmlSyntaxTag.String,
+ typeService.GetClassificationType(QmlSyntaxTag.String) },
+ { QmlSyntaxTag.TypeName,
+ typeService.GetClassificationType(QmlSyntaxTag.TypeName) },
+ { QmlSyntaxTag.Binding,
+ typeService.GetClassificationType(QmlSyntaxTag.Binding) },
// QML comments are mapped to the Visual Studio pre-defined comment classification
- { QmlTag.Comment,
+ { QmlSyntaxTag.Comment,
typeService.GetClassificationType(PredefinedClassificationTypeNames.Comment) }
};
}
diff --git a/src/qtvstools/QtVsTools.csproj b/src/qtvstools/QtVsTools.csproj
index d96e4b9f..0cd37c44 100644
--- a/src/qtvstools/QtVsTools.csproj
+++ b/src/qtvstools/QtVsTools.csproj
@@ -78,6 +78,7 @@
<Compile Include="ProjectQtSettings.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="QMakeWrapper.cs" />
+ <Compile Include="QML\Classification\QmlAsyncClassifier.cs" />
<Compile Include="QML\Classification\QmlClassificationFormat.cs" />
<Compile Include="QML\Classification\QmlSyntaxClassifier.cs" />
<Compile Include="QML\Classification\QmlErrorClassifier.cs" />