Search code examples
c#visual-studio-extensions

Why yield return does not work when modifying local data?


I am writing a Visual Studio extension and currently adding margin glyphs to the editor. I have started with the walk-through example from MS documentation: https://learn.microsoft.com/en-us/visualstudio/extensibility/walkthrough-creating-a-margin-glyph?view=vs-2017.

My logic is a bit different from the example since I don't analyze the file content given by the spans parameter: I already get a set of results with defects locations from an analysis process before. And the rationale is simply to generate a tag if and only if it has not been rendered yet.

Indeed, I want to avoid a new glyph is falsely be rendered if the user adds a new line the line before an existing flag. Example: if there is a glyph on line 42, and cursor is on line 41, and user types a new line, then a new glyph is generated on line 42 (because the GetTags method is called again) while previous glyph is moved to line 43.

My code is:

internal class MyDefectTagger : ITagger<MyDefectTag>
{
    private IClassifier m_classifier;
    private ITextBuffer m_buffer;

    internal MyDefectTagger(IClassifier classifier, ITextBuffer buffer)
    {
        m_classifier = classifier;
        m_buffer = buffer;
    }

    IEnumerable<ITagSpan<MyDefectTag>>
        ITagger<MyDefectTag>.GetTags(NormalizedSnapshotSpanCollection spans)
    {
        var filename = GetFileName(m_buffer);

        if (MyModel.Instance == null || 
            MyModel.Instance.defectsLocation == null || 
            !MyModel.Instance.defectsLocation.ContainsKey(filename))
        {
            yield break;
        }

        foreach (SnapshotSpan span in spans)
        {
            ITextSnapshot textSnapshot = span.Snapshot;

            foreach (ITextSnapshotLine textSnapshotLine in textSnapshot.Lines)
            {
                var line = textSnapshotLine.LineNumber + 1; // Lines start at 1 in VS Editor

                if (MyModel.Instance.defectsLocation[filename].ContainsKey(line) &&
                    !MyModel.Instance.defectsLocation[filename][line].rendered)
                {
                    MyModel.Instance.defectsLocation[filename][line].rendered = true; // YIELD WORKS IF THIS LINE IS COMMENTED OUT
                    yield return new TagSpan<MyDefectTag>(new SnapshotSpan(textSnapshotLine.Start, 0), new MyDefectTag());
                }
            }
        }
    }
}

        public event EventHandler<SnapshotSpanEventArgs> TagsChanged;

        private string GetFileName(ITextBuffer buffer)
        {
            buffer.Properties.TryGetProperty(typeof(ITextDocument), out ITextDocument document);

            return document?.FilePath;
        }
    }

What is strange is that yield returns normally when the "rendered = true" line is commented out (although I get false new glyphs). While yields seems to be blocked when line rendered = true is run (while debugger still steps on the yield instruction).

Is there anything I am missing? Is it forbidden to modify local data when using yield return? Is there some hidden concurrency issue? Thanks for your help!


Solution

  • I finally found out that I was not correctly scanning lines given by the NormalizedSnapshotSpanCollection spans. I was wrongly re-iterating through all the lines for each span with foreach (ITextSnapshotLine textSnapshotLine in textSnapshot.Lines). In fact I needed to get the line number associated with the given span and now it works:

    • I get margin glyphs appropriately on each line my analysis give me a defect.
    • I don't get any more duplicates when adding new lines just before a line having a margin glyph.

    Hence my solution is:

            foreach (SnapshotSpan span in spans)
            {
                var lineNumber= span.Snapshot.GetLineNumberFromPosition(span.Start.Position) + 1;
    
                if (MyModel.Instance.defectsLocation[filename].ContainsKey(lineNumber) &&
                    !MyModel.Instance.defectsLocation[filename][lineNumber].rendered)
                {
                    //MyModel.Instance.defectsLocation[filename][lineNumber].rendered = true;//Strangely no getting any MyDefectTag displayed when I uncomment this line...
                    yield return new TagSpan<MyDefectTag>(new SnapshotSpan(span.Start, 0), new MyDefectTag());
                }
            }