Создание теггера с более чем одним типом тега для расширения VS

Я работаю над расширением Visual Studio, которое предоставляет некоторые функции для пользовательского языка. Я сделал простую подсветку синтаксиса, и я хочу перейти к таким вещам, как подсветка синтаксической ошибки, согласование фигурных скобок, определение и т.д. Основная проблема, которую я сейчас рассматриваю, заключается в том, что для этого требуются разные типы тегов, которые (насколько мне видно) потребуют разных тегов. Тем не менее, я не вижу никакого интуитивного способа обмена информацией между метками, так как все три из этих вещей могут быть сделаны при одном анализе содержимого. Я имею в виду, я мог бы разобрать его три раза, но это не похоже на хорошее решение.

Как я могу вернуть несколько тегов из тегов (возможно, использовать ITag?) или обмениваться информацией между несколькими тегами?

Моя текущая структура такова:

    internal class HighlightWordTagger : ITagger<ClassificationTag>
    {
        ITextBuffer TextBuffer;
        IClassificationType Keyword;
        IClassificationType Comment;
        IClassificationType Literal;

        // Probably a giant memory leak
        Dictionary<ITextSnapshot, List<TagSpan<ClassificationTag>>> SnapshotResults = new Dictionary<ITextSnapshot, List<TagSpan<ClassificationTag>>>();

        public HighlightWordTagger(ITextBuffer sourceBuffer, IClassificationTypeRegistryService typeService)
        {
            TextBuffer = sourceBuffer;

            TextBuffer.Changed += (sender, args) =>
            {
                LexSnapshot(args.After);

                TagsChanged(this, new SnapshotSpanEventArgs(new SnapshotSpan(args.After, new Span(0, args.After.Length))));
            };
            Keyword = typeService.GetClassificationType("WideKeyword");
            Comment = typeService.GetClassificationType("WideComment");
            Literal = typeService.GetClassificationType("WideLiteral");
        }

        public IEnumerable<ITagSpan<ClassificationTag>> GetTags(NormalizedSnapshotSpanCollection spans)
        {
            LexSnapshot(spans[0].Snapshot);
            foreach (var snapshotspan in SnapshotResults[spans[0].Snapshot])
            {
                foreach (var span in spans)
                {
                    if (snapshotspan.Span.IntersectsWith(span))
                    {
                        yield return snapshotspan;
                    }
                }
            }
        }

        Span SpanFromLexer(Lexer.Range range)
        {
            return new Span((int)range.begin.offset, (int)(range.end.offset - range.begin.offset));
        }

        void LexSnapshot(ITextSnapshot shot)
        {
            if (SnapshotResults.ContainsKey(shot))
                return;

            var lexer = new Lexer();
            var list = new List<TagSpan<ClassificationTag>>();
            SnapshotResults[shot] = list;
            lexer.Read(
                shot.GetText(),
                (where, what) =>
                {
                    if (what == Lexer.Failure.UnlexableCharacter)
                        return false;
                    var loc = new Span(
                        (int)where.offset,
                        (int)shot.Length - (int)where.offset
                    );
                    if (what == Lexer.Failure.UnterminatedComment)
                        list.Add(new TagSpan<ClassificationTag>(new SnapshotSpan(shot, loc), new ClassificationTag(Comment)));
                    if (what == Lexer.Failure.UnterminatedStringLiteral)
                        list.Add(new TagSpan<ClassificationTag>(new SnapshotSpan(shot, loc), new ClassificationTag(Literal)));
                    return false;
                }, 
                where =>
                {
                    // Clamp this so it doesn't go over the end when we add \n in the lexer.
                    where.end.offset = where.end.offset > shot.Length ? (uint)(shot.Length) : where.end.offset;
                    var loc = SpanFromLexer(where);
                    list.Add(new TagSpan<ClassificationTag>(new SnapshotSpan(shot, loc), new ClassificationTag(Comment)));
                },
                token => {
                    var location = SpanFromLexer(token.location);
                    if (token.type == Lexer.TokenType.String || token.type == Lexer.TokenType.Integer)
                    {
                        list.Add(new TagSpan<ClassificationTag>(new SnapshotSpan(shot, location), new ClassificationTag(Literal)));
                    }
                    if (lexer.IsKeyword(token.type))
                    {
                        list.Add(new TagSpan<ClassificationTag>(new SnapshotSpan(shot, location), new ClassificationTag(Keyword)));
                    }
                    return false;
                }
            );
        }

        public event EventHandler<SnapshotSpanEventArgs> TagsChanged = delegate { };
    }

Я мог бы, наверное, лучше справиться с повторным лексированием, но это для другого вопроса.

Ответ 1

В итоге мне пришлось отделить эти проблемы. Вы можете использовать ITextBuffer.Properties.GetOrCreateSingletonProperty для связывания произвольных объектов по вашему выбору с текстовым буфером. Я закончил создание отдельного класса lexer, связав его с текстовым буфером, а затем просто выполнил почти всю логику, кроме тегов там. Затем в реализации каждого теггера я просто опроса lexer для результатов, а затем пометить их. Это позволяет нескольким атрибуторам зависеть от одного и того же экземпляра lexer.

Учитывая, что большинство лексеров и парсеров создавали более одного типа тега, я удивляюсь, что VS заставляет вас так сильно взломать, чтобы произвести этот результат.

Ответ 2

Как сказал щенок, вы можете использовать несколько отдельных тегов, и они могут достигать друг друга через GetOrCreateSingletonProperty, но я полагаю, что нет никакой гарантии относительно порядка, в котором будут создаваться тегеры, поэтому процесс инициализации может быть неудобным.

Между тем, мне удалось объединить несколько тегов в одном классе, и я даже сделал полный "образец языка", чтобы продемонстрировать эту технику. Вот он:

/// <summary>Boilerplate factory class that associates <see cref="SampleLanguageForVS"/>,
/// and file extension .samplelang, with content type "Sample Language".</summary>
[Export(typeof(IClassifierProvider))]
[Export(typeof(ITaggerProvider))]
[TagType(typeof(ClassificationTag))]
[TagType(typeof(ErrorTag))]
[ContentType("Sample Language")]
internal class SampleLanguageForVSProvider : IClassifierProvider, ITaggerProvider
{
    [Export]
    [Name("Sample Language")] // Must match the [ContentType] attributes
    [BaseDefinition("code")]
    internal static ContentTypeDefinition _ = null;
    [Export]
    [FileExtension(".samplelang")]
    [ContentType("Sample Language")]
    internal static FileExtensionToContentTypeDefinition _1 = null;

    [Import] IClassificationTypeRegistryService _registry = null; // Set via MEF

    public static SampleLanguageForVS Get(IClassificationTypeRegistryService registry, ITextBuffer buffer)
    {
        return buffer.Properties.GetOrCreateSingletonProperty<SampleLanguageForVS>(
            delegate { return new SampleLanguageForVS(registry, buffer); });
    }
    public IClassifier GetClassifier(ITextBuffer buffer)
    {
        return Get(_registry, buffer);
    }
    public ITagger<T> CreateTagger<T>(ITextBuffer buffer) where T : ITag
    {
        return Get(_registry, buffer) as ITagger<T>;
    }
}

internal class SampleLanguageForVS : IClassifier,
    ITagger<ClassificationTag>,
    ITagger<ErrorTag>,
    IBackgroundAnalyzerImpl<object, IList<ITagSpan<ITag>>>
{
    protected IClassificationTypeRegistryService _registry;
    protected ITextBuffer _buffer;
    protected IClassificationType _commentType;
    protected ClassificationTag _outerParenTag;
    protected IList<ITagSpan<ITag>> _resultTags;
    protected BackgroundAnalyzerForVS<object, IList<ITagSpan<ITag>>> _parseHelper;

    public SampleLanguageForVS(IClassificationTypeRegistryService registry,ITextBuffer buffer)
    {
        _registry = registry;
        _buffer = buffer;
        _commentType = registry.GetClassificationType(PredefinedClassificationTypeNames.Comment);
        _outerParenTag = MakeTag(PredefinedClassificationTypeNames.Keyword);
        _parseHelper = new BackgroundAnalyzerForVS<object, IList<ITagSpan<ITag>>>(buffer, this, true);
    }
    ClassificationTag MakeTag(string name)
    {
        return new ClassificationTag(_registry.GetClassificationType(name));
    }

    #region Classifier (lexical analysis)

    public event EventHandler<ClassificationChangedEventArgs> ClassificationChanged;

    public IList<ClassificationSpan> GetClassificationSpans(SnapshotSpan span)
    {
        List<ClassificationSpan> spans = new List<ClassificationSpan>();
        var line = span.Snapshot.GetLineFromPosition(span.Start);
        do {
            var cspan = GetLineClassification(line);
            if (cspan != null)
                spans.Add(cspan);

            if (line.EndIncludingLineBreak.Position >= span.Snapshot.Length) break;
            line = span.Snapshot.GetLineFromPosition(line.EndIncludingLineBreak.Position);
        } while (line.EndIncludingLineBreak < span.End.Position);
        return spans;
    }

    public ClassificationSpan GetLineClassification(ITextSnapshotLine line)
    {
        var span = new Span(line.Start.Position, line.Length);
        var sspan = new SnapshotSpan(line.Snapshot, span);
        int i;
        for (i = span.Start; i < line.Snapshot.Length && char.IsWhiteSpace(line.Snapshot[i]); i++) { }
        if (i < line.Snapshot.Length && 
            (line.Snapshot[i] == '#' ||
             line.Snapshot[i] == '/' && i + 1 < line.Snapshot.Length && line.Snapshot[i+1] == '/'))
            return new ClassificationSpan(sspan, _commentType);
        return null;
    }

    #endregion

    #region Background analysis (the two taggers)

    public object GetInputSnapshot()
    {
        return null; // this example has no state to pass to the analysis thread.
    }
    public IList<ITagSpan<ITag>> RunAnalysis(ITextSnapshot snapshot, object input, System.Threading.CancellationToken cancelToken)
    {
        List<ITagSpan<ITag>> results = new List<ITagSpan<ITag>>();
        // On analysis thread: produce classification tags for nested [(parens)]
        // and warning tags for backslashes.
        int parenLevel = 0;
        for (int i = 0; i < snapshot.Length; i++)
        {
            char c = snapshot[i];
            if (c == '\\')
                results.Add(new TagSpan<ErrorTag>(
                    new SnapshotSpan(snapshot, new Span(i, 1)),
                    new ErrorTag("compiler warning", "Caution: that not really a slash, it a backslash!!")));
            bool open = (c == '[' || c == '(');
            bool close = (c == ']' || c == ')');
            if (close) {
                if (parenLevel > 0)
                    parenLevel--;
                else {
                    results.Add(new TagSpan<ErrorTag>(
                        new SnapshotSpan(snapshot, new Span(i, Math.Min(2, snapshot.Length-i))),
                        new ErrorTag("syntax error", "Caution: closing parenthesis without matching opener")));
                }
            }
            if ((open || close) && parenLevel == 0)
                results.Add(new TagSpan<ClassificationTag>(
                    new SnapshotSpan(snapshot, new Span(i, 1)), 
                    _outerParenTag));
            if (open)
                parenLevel++;
        }
        return results;
    }
    public void OnRunSucceeded(IList<ITagSpan<ITag>> results)
    {
        _resultTags = results;
        // We don't know which tags changed unless we do some fancy diff, so
        // act as if everything changed.
        if (TagsChanged != null) // should always be true
            TagsChanged(this, new SnapshotSpanEventArgs(new SnapshotSpan(_buffer.CurrentSnapshot, new Span(0, _buffer.CurrentSnapshot.Length))));
    }

    #endregion

    #region ITagger<ClassificationTag> and ITagger<ErrorTag> Members

    IEnumerable<ITagSpan<ErrorTag>> ITagger<ErrorTag>.GetTags(NormalizedSnapshotSpanCollection spans)
    {
        return GetTags<ErrorTag>(spans);
    }
    IEnumerable<ITagSpan<ClassificationTag>> ITagger<ClassificationTag>.GetTags(NormalizedSnapshotSpanCollection spans)
    {
        return GetTags<ClassificationTag>(spans);
    }
    public IEnumerable<ITagSpan<TTag>> GetTags<TTag>(NormalizedSnapshotSpanCollection spans) where TTag : ITag
    {
        if (_resultTags == null)
            return null;

        // TODO: make more efficient for large files with e.g. binary search
        int start = spans[0].Start.Position, end = spans[spans.Count-1].End.Position;
        return _resultTags.Where(ts => ts.Span.End >= start && ts.Span.Start <= end).OfType<ITagSpan<TTag>>();
    }

    public event EventHandler<SnapshotSpanEventArgs> TagsChanged;

    #endregion
}

Все, что отсутствует выше, это операторы using (см. полный исходный файл) и BackgroundAnalyzerForVS. Если вы подключите этот код к проекту vsix, вы получите "lexing", задержанный "синтаксический разбор", предупреждающие и теги ошибок. Демо файл:

Open this in Visual Studio to see "sample" syntax highlighting.
  // Backslashes are underlined.
  \\ <-- Such as those ones.
When you start a parenthetical (like this) the parens are highlighted, 
but ([nested parens (like this)]) are not highlighted.
# Do not write a closing ")" without an opening "(".